@@ -19,15 +19,17 @@ import {
1919 ApprovalPendingError ,
2020 ApprovalTimeoutError ,
2121 InvalidInputError ,
22+ ModuleError ,
2223 ModuleNotFoundError ,
2324 ModuleTimeoutError ,
2425 SchemaValidationError ,
2526} from './errors.js' ;
2627import { AfterMiddleware , BeforeMiddleware , Middleware } from './middleware/index.js' ;
2728import { MiddlewareChainError , MiddlewareManager } from './middleware/manager.js' ;
2829import { guardCallChain } from './utils/call-chain.js' ;
29- import type { ModuleAnnotations , ValidationResult } from './module.js' ;
30- import { DEFAULT_ANNOTATIONS } from './module.js' ;
30+ import type { ModuleAnnotations , PreflightCheckResult , PreflightResult } from './module.js' ;
31+ import { DEFAULT_ANNOTATIONS , createPreflightResult } from './module.js' ;
32+ import { MODULE_ID_PATTERN } from './registry/registry.js' ;
3133import type { Registry } from './registry/registry.js' ;
3234
3335export const REDACTED_VALUE : string = '***REDACTED***' ;
@@ -178,7 +180,7 @@ export class Executor {
178180 this . _acl = acl ;
179181 }
180182
181- /** Set the approval handler for Step 4. 5 gate. */
183+ /** Set the approval handler for Step 5 gate. */
182184 setApprovalHandler ( handler : ApprovalHandler ) : void {
183185 this . _approvalHandler = handler ;
184186 }
@@ -336,7 +338,7 @@ export class Executor {
336338 const mod = this . _lookupModule ( moduleId ) ;
337339 this . _checkAcl ( moduleId , ctx ) ;
338340
339- // Step 4. 5 -- Approval Gate (strips internal keys like _approval_token)
341+ // Step 5 -- Approval Gate (strips internal keys like _approval_token)
340342 effectiveInputs = await this . _checkApproval ( mod , moduleId , effectiveInputs , ctx ) ;
341343
342344 effectiveInputs = this . _validateInputs ( mod , effectiveInputs , ctx ) ;
@@ -457,32 +459,98 @@ export class Executor {
457459 }
458460 }
459461
460- validate ( moduleId : string , inputs : Record < string , unknown > ) : ValidationResult {
462+ /**
463+ * Non-destructive preflight check through Steps 1-6 of the pipeline.
464+ * Returns a PreflightResult that is duck-type compatible with ValidationResult.
465+ */
466+ validate (
467+ moduleId : string ,
468+ inputs ?: Record < string , unknown > | null ,
469+ context ?: Context | null ,
470+ ) : PreflightResult {
471+ const effectiveInputs = inputs ?? { } ;
472+ const checks : PreflightCheckResult [ ] = [ ] ;
473+ let requiresApproval = false ;
474+
475+ // Check 1: module_id format
476+ if ( ! MODULE_ID_PATTERN . test ( moduleId ) ) {
477+ checks . push ( {
478+ check : 'module_id' , passed : false ,
479+ error : { code : 'INVALID_INPUT' , message : `Invalid module ID: "${ moduleId } "` } ,
480+ } ) ;
481+ return createPreflightResult ( checks ) ;
482+ }
483+ checks . push ( { check : 'module_id' , passed : true } ) ;
484+
485+ // Check 2: module lookup
461486 const module = this . _registry . get ( moduleId ) ;
462487 if ( module === null ) {
463- throw new ModuleNotFoundError ( moduleId ) ;
488+ checks . push ( {
489+ check : 'module_lookup' , passed : false ,
490+ error : { code : 'MODULE_NOT_FOUND' , message : `Module not found: ${ moduleId } ` } ,
491+ } ) ;
492+ return createPreflightResult ( checks ) ;
464493 }
465-
494+ checks . push ( { check : 'module_lookup' , passed : true } ) ;
466495 const mod = module as Record < string , unknown > ;
467- const inputSchema = mod [ 'inputSchema' ] as TSchema | undefined ;
468496
469- if ( inputSchema == null ) {
470- return { valid : true , errors : [ ] } ;
497+ // Check 3: call chain safety
498+ const ctx = this . _createContext ( moduleId , context ) ;
499+ try {
500+ this . _checkSafety ( moduleId , ctx ) ;
501+ checks . push ( { check : 'call_chain' , passed : true } ) ;
502+ } catch ( e ) {
503+ const err = e instanceof ModuleError
504+ ? { code : e . code , message : e . message }
505+ : { code : 'CALL_CHAIN_ERROR' , message : String ( e ) } ;
506+ checks . push ( { check : 'call_chain' , passed : false , error : err } ) ;
471507 }
472508
473- if ( Value . Check ( inputSchema , inputs ) ) {
474- return { valid : true , errors : [ ] } ;
509+ // Check 4: ACL
510+ if ( this . _acl !== null ) {
511+ const allowed = this . _acl . check ( ctx . callerId , moduleId , ctx ) ;
512+ if ( ! allowed ) {
513+ checks . push ( {
514+ check : 'acl' , passed : false ,
515+ error : { code : 'ACL_DENIED' , message : `Access denied: ${ ctx . callerId } -> ${ moduleId } ` } ,
516+ } ) ;
517+ } else {
518+ checks . push ( { check : 'acl' , passed : true } ) ;
519+ }
520+ } else {
521+ checks . push ( { check : 'acl' , passed : true } ) ;
475522 }
476523
477- const errors : Array < Record < string , string > > = [ ] ;
478- for ( const error of Value . Errors ( inputSchema , inputs ) ) {
479- errors . push ( {
480- field : error . path || '/' ,
481- code : String ( error . type ) ,
482- message : error . message ,
483- } ) ;
524+ // Check 5: approval detection (report only, no handler invocation)
525+ if ( this . _needsApproval ( mod ) ) {
526+ requiresApproval = true ;
484527 }
485- return { valid : false , errors } ;
528+ checks . push ( { check : 'approval' , passed : true } ) ;
529+
530+ // Check 6: input schema validation
531+ const inputSchema = mod [ 'inputSchema' ] as TSchema | undefined ;
532+ if ( inputSchema != null ) {
533+ if ( Value . Check ( inputSchema , effectiveInputs ) ) {
534+ checks . push ( { check : 'schema' , passed : true } ) ;
535+ } else {
536+ const errors : Array < Record < string , unknown > > = [ ] ;
537+ for ( const error of Value . Errors ( inputSchema , effectiveInputs ) ) {
538+ errors . push ( {
539+ field : error . path || '/' ,
540+ code : String ( error . type ) ,
541+ message : error . message ,
542+ } ) ;
543+ }
544+ checks . push ( {
545+ check : 'schema' , passed : false ,
546+ error : { code : 'SCHEMA_VALIDATION_ERROR' , errors } ,
547+ } ) ;
548+ }
549+ } else {
550+ checks . push ( { check : 'schema' , passed : true } ) ;
551+ }
552+
553+ return createPreflightResult ( checks , requiresApproval ) ;
486554 }
487555
488556 private _checkSafety ( moduleId : string , ctx : Context ) : void {
@@ -568,7 +636,7 @@ export class Executor {
568636 }
569637 }
570638
571- /** Step 4. 5: Approval gate. Returns inputs with internal keys stripped. */
639+ /** Step 5: Approval gate. Returns inputs with internal keys stripped. */
572640 private async _checkApproval (
573641 mod : Record < string , unknown > ,
574642 moduleId : string ,
0 commit comments