apcore-js 0.16.0 → 0.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/executor.js CHANGED
@@ -4,20 +4,16 @@
4
4
  * Async-only execution pipeline. Python's call() + call_async() merge into one async call().
5
5
  * Timeout uses Promise.race instead of threading.
6
6
  */
7
- import { Kind } from '@sinclair/typebox';
8
- import { Value } from '@sinclair/typebox/value';
9
- import { jsonSchemaToTypeBox } from './schema/loader.js';
10
- import { createApprovalRequest } from './approval.js';
11
7
  import { Context } from './context.js';
12
8
  import { ExecutionCancelledError } from './cancel.js';
13
- import { ACLDeniedError, ApprovalDeniedError, ApprovalPendingError, ApprovalTimeoutError, InvalidInputError, ModuleError, ModuleNotFoundError, ModuleTimeoutError, SchemaValidationError, } from './errors.js';
9
+ import { InvalidInputError, ModuleError, } from './errors.js';
14
10
  import { AfterMiddleware, BeforeMiddleware } from './middleware/index.js';
15
11
  import { MiddlewareChainError, MiddlewareManager } from './middleware/manager.js';
16
- import { guardCallChain } from './utils/call-chain.js';
17
- import { DEFAULT_ANNOTATIONS, createPreflightResult } from './module.js';
12
+ import { createPreflightResult } from './module.js';
18
13
  import { MODULE_ID_PATTERN } from './registry/registry.js';
19
- import { PipelineEngine, StrategyNotFoundError } from './pipeline.js';
20
- import { buildStandardStrategy, buildInternalStrategy, buildTestingStrategy, buildPerformanceStrategy, } from './builtin-steps.js';
14
+ import { ExecutionStrategy, PipelineEngine, PipelineAbortError, StrategyNotFoundError } from './pipeline.js';
15
+ import { BuiltinACLCheck, BuiltinApprovalGate, buildStandardStrategy, buildInternalStrategy, buildTestingStrategy, buildPerformanceStrategy, } from './builtin-steps.js';
16
+ import { propagateError } from './utils/error-propagation.js';
21
17
  export const REDACTED_VALUE = '***REDACTED***';
22
18
  /** Well-known context.data keys used internally by the runtime. */
23
19
  export const CTX_GLOBAL_DEADLINE = '_apcore.executor.global_deadline';
@@ -76,27 +72,6 @@ function redactSecretPrefix(data) {
76
72
  }
77
73
  }
78
74
  }
79
- /**
80
- * Normalize a dict-form annotations object into a ModuleAnnotations interface.
81
- * Handles both camelCase and snake_case keys (parallel to Python's
82
- * ``ModuleAnnotations(**{k: v for k, v in annotations.items() if k in valid_fields})``).
83
- */
84
- function dictToAnnotations(dict) {
85
- return {
86
- readonly: Boolean(dict['readonly'] ?? false),
87
- destructive: Boolean(dict['destructive'] ?? false),
88
- idempotent: Boolean(dict['idempotent'] ?? false),
89
- requiresApproval: Boolean(dict['requiresApproval'] ?? dict['requires_approval'] ?? false),
90
- openWorld: Boolean(dict['openWorld'] ?? dict['open_world'] ?? true),
91
- streaming: Boolean(dict['streaming'] ?? false),
92
- cacheable: Boolean(dict['cacheable'] ?? false),
93
- cacheTtl: Number(dict['cacheTtl'] ?? dict['cache_ttl'] ?? 0),
94
- cacheKeyFields: (dict['cacheKeyFields'] ?? dict['cache_key_fields'] ?? null),
95
- paginated: Boolean(dict['paginated'] ?? false),
96
- paginationStyle: (dict['paginationStyle'] ?? dict['pagination_style'] ?? 'cursor'),
97
- extra: Object.freeze(dict['extra'] ?? {}),
98
- };
99
- }
100
75
  export class Executor {
101
76
  _registry;
102
77
  _middlewareManager;
@@ -108,6 +83,7 @@ export class Executor {
108
83
  _maxCallDepth;
109
84
  _maxModuleRepeat;
110
85
  _strategy;
86
+ _pipelineEngine;
111
87
  /** Global strategy registry for name-based resolution. */
112
88
  static _strategyRegistry = new Map();
113
89
  constructor(options) {
@@ -133,11 +109,11 @@ export class Executor {
133
109
  this._maxCallDepth = 32;
134
110
  this._maxModuleRepeat = 3;
135
111
  }
136
- // Resolve strategy option
112
+ // Resolve strategy option (default to standard)
137
113
  const strategyOpt = options.strategy;
138
114
  if (strategyOpt === undefined || strategyOpt === null) {
139
- // null/undefined: no pipeline strategy, use legacy execution path
140
- this._strategy = null;
115
+ // Default to standard strategy (pipeline-first)
116
+ this._strategy = buildStandardStrategy(this._buildStrategyDeps());
141
117
  }
142
118
  else if (typeof strategyOpt === 'string') {
143
119
  // Resolve by name from the global registry
@@ -167,6 +143,7 @@ export class Executor {
167
143
  // ExecutionStrategy instance
168
144
  this._strategy = strategyOpt;
169
145
  }
146
+ this._pipelineEngine = new PipelineEngine();
170
147
  }
171
148
  /** Build the dependency bag for strategy factories. */
172
149
  _buildStrategyDeps() {
@@ -196,7 +173,7 @@ export class Executor {
196
173
  return null;
197
174
  return target.info();
198
175
  }
199
- /** Get the current execution strategy (may be null for legacy mode). */
176
+ /** Get the current execution strategy. */
200
177
  get currentStrategy() {
201
178
  return this._strategy;
202
179
  }
@@ -209,13 +186,25 @@ export class Executor {
209
186
  get middlewares() {
210
187
  return this._middlewareManager.snapshot();
211
188
  }
212
- /** Set the access control provider. */
189
+ /** Set the access control provider. Updates both the executor field and the strategy's ACL step. */
213
190
  setAcl(acl) {
214
191
  this._acl = acl;
192
+ for (const step of this._strategy.steps) {
193
+ if (step.name === 'acl_check' && step instanceof BuiltinACLCheck) {
194
+ step.setAcl(acl);
195
+ break;
196
+ }
197
+ }
215
198
  }
216
- /** Set the approval handler for Step 5 gate. */
199
+ /** Set the approval handler for Step 5 gate. Updates both the executor field and the strategy's approval step. */
217
200
  setApprovalHandler(handler) {
218
201
  this._approvalHandler = handler;
202
+ for (const step of this._strategy.steps) {
203
+ if (step.name === 'approval_gate' && step instanceof BuiltinApprovalGate) {
204
+ step.setApprovalHandler(handler);
205
+ break;
206
+ }
207
+ }
219
208
  }
220
209
  use(middleware) {
221
210
  this._middlewareManager.add(middleware);
@@ -232,9 +221,45 @@ export class Executor {
232
221
  remove(middleware) {
233
222
  return this._middlewareManager.remove(middleware);
234
223
  }
235
- async call(moduleId, inputs, context, _versionHint) {
236
- const { mod, effectiveInputs, ctx } = await this._prepareExecution(moduleId, inputs, context);
237
- return this._executeWithMiddleware(mod, moduleId, effectiveInputs, ctx);
224
+ async call(moduleId, inputs, context, versionHint) {
225
+ this._validateModuleId(moduleId);
226
+ const ctx = context != null ? context.child(moduleId) : Context.create(this).child(moduleId);
227
+ const pipeCtx = {
228
+ moduleId,
229
+ inputs: inputs ?? {},
230
+ context: ctx,
231
+ module: null,
232
+ validatedInputs: null,
233
+ output: null,
234
+ validatedOutput: null,
235
+ stream: false,
236
+ outputStream: null,
237
+ strategy: this._strategy,
238
+ trace: null,
239
+ versionHint: versionHint ?? null,
240
+ };
241
+ try {
242
+ const [output, _trace] = await this._pipelineEngine.run(this._strategy, pipeCtx);
243
+ return (output ?? {});
244
+ }
245
+ catch (exc) {
246
+ if (exc instanceof ExecutionCancelledError)
247
+ throw exc;
248
+ // Pipeline errors propagate with their original types (ModuleNotFoundError, etc.)
249
+ // because builtin steps now throw directly.
250
+ const ctxObj = pipeCtx.context;
251
+ const wrapped = propagateError(exc, moduleId, ctxObj);
252
+ const executedMw = pipeCtx.executedMiddlewares;
253
+ if (executedMw && executedMw.length > 0) {
254
+ const recovery = this._middlewareManager.executeOnError(moduleId, pipeCtx.inputs, wrapped, ctxObj, executedMw);
255
+ if (recovery !== null)
256
+ return recovery;
257
+ }
258
+ if (exc instanceof MiddlewareChainError) {
259
+ throw new ModuleError('MODULE_EXECUTE_ERROR', String(exc), { moduleId });
260
+ }
261
+ throw wrapped;
262
+ }
238
263
  }
239
264
  /**
240
265
  * Alias for call(). Provided for compatibility with MCP bridge packages
@@ -250,9 +275,6 @@ export class Executor {
250
275
  */
251
276
  async callWithTrace(moduleId, inputs, context, options) {
252
277
  const strategy = options?.strategy ?? this._strategy;
253
- if (strategy === null) {
254
- throw new InvalidInputError('callWithTrace requires a pipeline strategy. Set one on the Executor or pass via options.');
255
- }
256
278
  const ctx = context ?? Context.create(this).child(moduleId);
257
279
  const pipelineCtx = {
258
280
  moduleId,
@@ -267,8 +289,7 @@ export class Executor {
267
289
  strategy,
268
290
  trace: null,
269
291
  };
270
- const engine = new PipelineEngine();
271
- const [output, trace] = await engine.run(strategy, pipelineCtx);
292
+ const [output, trace] = await this._pipelineEngine.run(strategy, pipelineCtx);
272
293
  return [(output ?? {}), trace];
273
294
  }
274
295
  /**
@@ -282,191 +303,103 @@ export class Executor {
282
303
  * validation/side-effects but its return value is not yielded since chunks were already
283
304
  * emitted. In the non-streaming fallback, after-middleware can transform the output.
284
305
  */
285
- async *stream(moduleId, inputs, context, _versionHint) {
286
- const { mod, effectiveInputs, ctx } = await this._prepareExecution(moduleId, inputs, context);
287
- yield* this._streamWithMiddleware(mod, moduleId, effectiveInputs, ctx);
288
- }
289
- async *_streamWithMiddleware(mod, moduleId, inputs, ctx) {
290
- let effectiveInputs = inputs;
291
- let executedMiddlewares = [];
306
+ /**
307
+ * Streaming execution pipeline using split-pipeline design.
308
+ *
309
+ * Phase 1: Pipeline runs all steps with ctx.stream=true. BuiltinExecute detects
310
+ * stream mode and sets ctx.outputStream if module has stream().
311
+ * Phase 2: Iterate stream, accumulate chunks and yield each.
312
+ * Phase 3: Run output_validation + middleware_after on accumulated output.
313
+ *
314
+ * If the module has no stream(), the pipeline executes normally and yields ctx.output.
315
+ */
316
+ async *stream(moduleId, inputs, context, versionHint) {
317
+ this._validateModuleId(moduleId);
318
+ const ctx = context != null ? context.child(moduleId) : Context.create(this).child(moduleId);
319
+ const pipeCtx = {
320
+ moduleId,
321
+ inputs: inputs ?? {},
322
+ context: ctx,
323
+ module: null,
324
+ validatedInputs: null,
325
+ output: null,
326
+ validatedOutput: null,
327
+ stream: true,
328
+ outputStream: null,
329
+ strategy: this._strategy,
330
+ trace: null,
331
+ versionHint: versionHint ?? null,
332
+ };
333
+ // Phase 1: Run the full pipeline. BuiltinExecute detects ctx.stream=true.
292
334
  try {
293
- try {
294
- [effectiveInputs, executedMiddlewares] = this._middlewareManager.executeBefore(moduleId, effectiveInputs, ctx);
295
- }
296
- catch (e) {
297
- if (e instanceof MiddlewareChainError) {
298
- executedMiddlewares = e.executedMiddlewares;
299
- const recovery = this._middlewareManager.executeOnError(moduleId, effectiveInputs, e.original, ctx, executedMiddlewares);
300
- if (recovery !== null) {
301
- yield recovery;
302
- return;
303
- }
304
- executedMiddlewares = [];
305
- throw e.original;
306
- }
307
- throw e;
308
- }
309
- // Cancel check before execution
310
- if (ctx.cancelToken !== null) {
311
- ctx.cancelToken.check();
312
- }
313
- const streamFn = mod['stream'];
314
- if (typeof streamFn === 'function') {
315
- // Module has a stream() method: iterate and yield each chunk
316
- let accumulated = {};
317
- for await (const chunk of streamFn.call(mod, effectiveInputs, ctx)) {
318
- accumulated = { ...accumulated, ...chunk };
319
- yield chunk;
320
- }
321
- // Validate accumulated output against output schema
322
- this._validateOutput(mod, accumulated);
323
- // Run after-middleware on the accumulated result
324
- this._middlewareManager.executeAfter(moduleId, effectiveInputs, accumulated, ctx);
325
- }
326
- else {
327
- // Fallback: execute normally and yield single chunk
328
- let output = await this._executeWithTimeout(mod, moduleId, effectiveInputs, ctx);
329
- this._validateOutput(mod, output);
330
- output = this._middlewareManager.executeAfter(moduleId, effectiveInputs, output, ctx);
331
- yield output;
332
- }
335
+ await this._pipelineEngine.run(this._strategy, pipeCtx);
333
336
  }
334
337
  catch (exc) {
335
338
  if (exc instanceof ExecutionCancelledError)
336
339
  throw exc;
337
- if (executedMiddlewares.length > 0) {
338
- const recovery = this._middlewareManager.executeOnError(moduleId, effectiveInputs, exc, ctx, executedMiddlewares);
340
+ const ctxObj = pipeCtx.context;
341
+ const wrapped = propagateError(exc, moduleId, ctxObj);
342
+ if (pipeCtx.executedMiddlewares && pipeCtx.executedMiddlewares.length > 0) {
343
+ const recovery = this._middlewareManager.executeOnError(moduleId, pipeCtx.inputs, wrapped, ctxObj, pipeCtx.executedMiddlewares);
339
344
  if (recovery !== null) {
340
345
  yield recovery;
341
346
  return;
342
347
  }
343
348
  }
344
- throw exc;
349
+ throw wrapped;
345
350
  }
346
- }
347
- /**
348
- * Shared pipeline: context -> global deadline -> safety -> lookup -> ACL -> approval -> validate.
349
- */
350
- async _prepareExecution(moduleId, inputs, context) {
351
- let effectiveInputs = inputs ?? {};
352
- const ctx = this._createContext(moduleId, context);
353
- // Set global deadline on root call only
354
- if (!(CTX_GLOBAL_DEADLINE in ctx.data) && this._globalTimeout > 0) {
355
- ctx.data[CTX_GLOBAL_DEADLINE] = Date.now() + this._globalTimeout;
356
- }
357
- this._checkSafety(moduleId, ctx);
358
- const mod = this._lookupModule(moduleId);
359
- this._checkAcl(moduleId, ctx);
360
- // Step 5 -- Approval Gate (strips internal keys like _approval_token)
361
- effectiveInputs = await this._checkApproval(mod, moduleId, effectiveInputs, ctx);
362
- effectiveInputs = this._validateInputs(mod, effectiveInputs, ctx);
363
- return { mod, effectiveInputs, ctx };
364
- }
365
- _createContext(moduleId, context) {
366
- if (context == null) {
367
- return Context.create(this).child(moduleId);
368
- }
369
- return context.child(moduleId);
370
- }
371
- _lookupModule(moduleId) {
372
- const module = this._registry.get(moduleId);
373
- if (module === null) {
374
- throw new ModuleNotFoundError(moduleId);
375
- }
376
- return module;
377
- }
378
- _checkAcl(moduleId, ctx) {
379
- if (this._acl !== null) {
380
- const allowed = this._acl.check(ctx.callerId, moduleId, ctx);
381
- if (!allowed) {
382
- throw new ACLDeniedError(ctx.callerId, moduleId);
383
- }
384
- }
385
- }
386
- _validateInputs(mod, inputs, ctx) {
387
- const inputSchema = this._resolveSchema(mod, 'inputSchema');
388
- if (inputSchema == null)
389
- return inputs;
390
- this._validateSchema(inputSchema, inputs, 'Input');
391
- ctx.redactedInputs = redactSensitive(inputs, inputSchema);
392
- return inputs;
393
- }
394
- _resolveSchema(mod, key) {
395
- const schema = mod[key];
396
- if (schema == null)
397
- return null;
398
- if (Kind in schema)
399
- return schema;
400
- const converted = jsonSchemaToTypeBox(schema);
401
- mod[key] = converted;
402
- return converted;
403
- }
404
- _validateSchema(schema, data, direction) {
405
- if (Value.Check(schema, data))
351
+ // If no outputStream, pipeline already executed normally — yield single result
352
+ if (pipeCtx.outputStream == null) {
353
+ yield (pipeCtx.output ?? {});
406
354
  return;
407
- const errors = [];
408
- for (const error of Value.Errors(schema, data)) {
409
- errors.push({
410
- field: error.path || '/',
411
- code: String(error.type),
412
- message: error.message,
413
- });
414
355
  }
415
- throw new SchemaValidationError(`${direction} validation failed`, errors);
416
- }
417
- async _executeWithMiddleware(mod, moduleId, inputs, ctx) {
418
- let effectiveInputs = inputs;
419
- let executedMiddlewares = [];
356
+ // Phase 2: Iterate stream, accumulate chunks
357
+ const outputStream = pipeCtx.outputStream;
358
+ let accumulated = {};
420
359
  try {
421
- try {
422
- [effectiveInputs, executedMiddlewares] = this._middlewareManager.executeBefore(moduleId, effectiveInputs, ctx);
423
- }
424
- catch (e) {
425
- if (e instanceof MiddlewareChainError) {
426
- executedMiddlewares = e.executedMiddlewares;
427
- const recovery = this._middlewareManager.executeOnError(moduleId, effectiveInputs, e.original, ctx, executedMiddlewares);
428
- if (recovery !== null)
429
- return recovery;
430
- executedMiddlewares = [];
431
- throw e.original;
432
- }
433
- throw e;
360
+ for await (const chunk of outputStream) {
361
+ accumulated = { ...accumulated, ...chunk };
362
+ yield chunk;
434
363
  }
435
- // Cancel check before execution
436
- if (ctx.cancelToken !== null) {
437
- ctx.cancelToken.check();
438
- }
439
- let output = await this._executeWithTimeout(mod, moduleId, effectiveInputs, ctx);
440
- this._validateOutput(mod, output);
441
- output = this._middlewareManager.executeAfter(moduleId, effectiveInputs, output, ctx);
442
- return output;
443
364
  }
444
365
  catch (exc) {
445
366
  if (exc instanceof ExecutionCancelledError)
446
367
  throw exc;
447
- if (executedMiddlewares.length > 0) {
448
- const recovery = this._middlewareManager.executeOnError(moduleId, effectiveInputs, exc, ctx, executedMiddlewares);
449
- if (recovery !== null)
450
- return recovery;
368
+ const ctxObj = pipeCtx.context;
369
+ const wrapped = propagateError(exc, moduleId, ctxObj);
370
+ if (pipeCtx.executedMiddlewares && pipeCtx.executedMiddlewares.length > 0) {
371
+ const recovery = this._middlewareManager.executeOnError(moduleId, pipeCtx.inputs, wrapped, ctxObj, pipeCtx.executedMiddlewares);
372
+ if (recovery !== null) {
373
+ yield recovery;
374
+ return;
375
+ }
451
376
  }
452
- throw exc;
377
+ throw wrapped;
453
378
  }
454
- }
455
- _validateOutput(mod, output) {
456
- const outputSchema = this._resolveSchema(mod, 'outputSchema');
457
- if (outputSchema != null) {
458
- this._validateSchema(outputSchema, output, 'Output');
379
+ // Phase 3: Output validation + middleware_after on accumulated result
380
+ pipeCtx.output = accumulated;
381
+ const postSteps = this._strategy.steps.filter((s) => s.name === 'output_validation' || s.name === 'middleware_after' || s.name === 'return_result');
382
+ if (postSteps.length > 0) {
383
+ const postStrategy = new ExecutionStrategy('post_stream', postSteps);
384
+ try {
385
+ await this._pipelineEngine.run(postStrategy, pipeCtx);
386
+ }
387
+ catch {
388
+ // Post-stream validation errors are non-fatal for already-yielded chunks
389
+ }
459
390
  }
460
391
  }
461
392
  /**
462
- * Non-destructive preflight check through Steps 1-6 of the pipeline.
463
- * Returns a PreflightResult that is duck-type compatible with ValidationResult.
393
+ * Non-destructive preflight check using pipeline dry_run mode.
394
+ *
395
+ * Runs all pure steps (context creation, call chain guard, module lookup,
396
+ * ACL, input validation). Steps with pure=false (approval, middleware,
397
+ * execute) are automatically skipped. Returns a PreflightResult.
464
398
  */
465
- validate(moduleId, inputs, context) {
399
+ async validate(moduleId, inputs, context) {
466
400
  const effectiveInputs = inputs ?? {};
467
401
  const checks = [];
468
- let requiresApproval = false;
469
- // Check 1: module_id format
402
+ // Check 0: module_id format (before pipeline)
470
403
  if (!MODULE_ID_PATTERN.test(moduleId)) {
471
404
  checks.push({
472
405
  check: 'module_id', passed: false,
@@ -475,100 +408,117 @@ export class Executor {
475
408
  return createPreflightResult(checks);
476
409
  }
477
410
  checks.push({ check: 'module_id', passed: true });
478
- // Check 2: module lookup
479
- const module = this._registry.get(moduleId);
480
- if (module === null) {
481
- checks.push({
482
- check: 'module_lookup', passed: false,
483
- error: { code: 'MODULE_NOT_FOUND', message: `Module not found: ${moduleId}` },
484
- });
485
- return createPreflightResult(checks);
486
- }
487
- checks.push({ check: 'module_lookup', passed: true });
488
- const mod = module;
489
- // Check 3: call chain safety
490
- const ctx = this._createContext(moduleId, context);
411
+ // Run pipeline in dry_run mode — pure=false steps are skipped
412
+ const pipeCtx = {
413
+ moduleId,
414
+ inputs: effectiveInputs,
415
+ context: context ?? Context.create(this).child(moduleId),
416
+ module: null,
417
+ validatedInputs: null,
418
+ output: null,
419
+ validatedOutput: null,
420
+ stream: false,
421
+ outputStream: null,
422
+ strategy: this._strategy,
423
+ trace: null,
424
+ dryRun: true,
425
+ };
426
+ let trace = null;
491
427
  try {
492
- this._checkSafety(moduleId, ctx);
493
- checks.push({ check: 'call_chain', passed: true });
428
+ const [, t] = await this._pipelineEngine.run(this._strategy, pipeCtx);
429
+ trace = t;
494
430
  }
495
431
  catch (e) {
496
- const err = e instanceof ModuleError
497
- ? { code: e.code, message: e.message }
498
- : { code: 'CALL_CHAIN_ERROR', message: String(e) };
499
- checks.push({ check: 'call_chain', passed: false, error: err });
500
- }
501
- // Check 4: ACL
502
- if (this._acl !== null) {
503
- const allowed = this._acl.check(ctx.callerId, moduleId, ctx);
504
- if (!allowed) {
505
- checks.push({
506
- check: 'acl', passed: false,
507
- error: { code: 'ACL_DENIED', message: `Access denied: ${ctx.callerId} -> ${moduleId}` },
508
- });
432
+ // Step raised a domain error (ModuleNotFoundError, ACLDeniedError, etc.)
433
+ if (e instanceof PipelineAbortError) {
434
+ trace = e.pipelineTrace;
509
435
  }
510
436
  else {
511
- checks.push({ check: 'acl', passed: true });
437
+ const errorDict = (e instanceof ModuleError)
438
+ ? { code: e.code, message: e.message }
439
+ : { code: e.constructor?.name ?? 'Error', message: String(e) };
440
+ const code = (e instanceof ModuleError) ? e.code : e.constructor?.name ?? 'Error';
441
+ let checkName;
442
+ if (code === 'MODULE_NOT_FOUND')
443
+ checkName = 'module_lookup';
444
+ else if (code === 'ACL_DENIED')
445
+ checkName = 'acl';
446
+ else if (code === 'SCHEMA_VALIDATION_ERROR' || code === 'INVALID_INPUT')
447
+ checkName = 'schema';
448
+ else if (code === 'CALL_DEPTH_EXCEEDED' || code === 'CIRCULAR_CALL' || code === 'CALL_FREQUENCY_EXCEEDED')
449
+ checkName = 'call_chain';
450
+ else
451
+ checkName = 'unknown';
452
+ checks.push({ check: checkName, passed: false, error: errorDict });
512
453
  }
513
454
  }
514
- else {
515
- checks.push({ check: 'acl', passed: true });
516
- }
517
- // Check 5: approval detection (report only, no handler invocation)
518
- if (this._needsApproval(mod)) {
519
- requiresApproval = true;
455
+ // Convert pipeline trace to PreflightResult checks
456
+ if (trace !== null) {
457
+ checks.push(...this._traceToChecks(trace));
520
458
  }
521
- checks.push({ check: 'approval', passed: true });
522
- // Check 6: input schema validation
523
- const inputSchema = mod['inputSchema'];
524
- if (inputSchema != null) {
525
- if (Value.Check(inputSchema, effectiveInputs)) {
526
- checks.push({ check: 'schema', passed: true });
527
- }
528
- else {
529
- const errors = [];
530
- for (const error of Value.Errors(inputSchema, effectiveInputs)) {
531
- errors.push({
532
- field: error.path || '/',
533
- code: String(error.type),
534
- message: error.message,
459
+ // Detect requires_approval
460
+ let requiresApproval = false;
461
+ if (pipeCtx.module != null) {
462
+ requiresApproval = this._needsApproval(pipeCtx.module);
463
+ }
464
+ // Module-level preflight (optional)
465
+ if (pipeCtx.module != null) {
466
+ const mod = pipeCtx.module;
467
+ const modWithPreflight = mod;
468
+ if (typeof modWithPreflight.preflight === 'function') {
469
+ try {
470
+ const preflightWarnings = modWithPreflight.preflight(effectiveInputs, pipeCtx.context);
471
+ if (Array.isArray(preflightWarnings) && preflightWarnings.length > 0) {
472
+ checks.push({ check: 'module_preflight', passed: true, warnings: preflightWarnings });
473
+ }
474
+ else {
475
+ checks.push({ check: 'module_preflight', passed: true });
476
+ }
477
+ }
478
+ catch (exc) {
479
+ const excName = exc instanceof Error ? exc.constructor.name : 'Error';
480
+ const excMsg = exc instanceof Error ? exc.message : String(exc);
481
+ checks.push({
482
+ check: 'module_preflight',
483
+ passed: true,
484
+ warnings: [`preflight() raised ${excName}: ${excMsg}`],
535
485
  });
536
486
  }
537
- checks.push({
538
- check: 'schema', passed: false,
539
- error: { code: 'SCHEMA_VALIDATION_ERROR', errors },
540
- });
541
487
  }
542
488
  }
543
- else {
544
- checks.push({ check: 'schema', passed: true });
545
- }
546
- // Check 7: module-level preflight (optional)
547
- const modWithPreflight = mod;
548
- if (typeof modWithPreflight.preflight === 'function') {
549
- try {
550
- const preflightWarnings = modWithPreflight.preflight(effectiveInputs, ctx);
551
- if (Array.isArray(preflightWarnings) && preflightWarnings.length > 0) {
552
- checks.push({ check: 'module_preflight', passed: true, warnings: preflightWarnings });
553
- }
554
- else {
555
- checks.push({ check: 'module_preflight', passed: true });
556
- }
557
- }
558
- catch (exc) {
559
- const excName = exc instanceof Error ? exc.constructor.name : 'Error';
560
- const excMsg = exc instanceof Error ? exc.message : String(exc);
561
- checks.push({
562
- check: 'module_preflight',
563
- passed: true,
564
- warnings: [`preflight() raised ${excName}: ${excMsg}`],
565
- });
489
+ return createPreflightResult(checks, requiresApproval);
490
+ }
491
+ /** Map pipeline step names to PreflightResult check names. */
492
+ static _STEP_TO_CHECK = {
493
+ context_creation: 'context',
494
+ call_chain_guard: 'call_chain',
495
+ module_lookup: 'module_lookup',
496
+ acl_check: 'acl',
497
+ approval_gate: 'approval',
498
+ middleware_before: 'middleware',
499
+ input_validation: 'schema',
500
+ };
501
+ /** Convert PipelineTrace steps to PreflightCheckResult list. */
502
+ _traceToChecks(trace) {
503
+ const checks = [];
504
+ for (const st of trace.steps) {
505
+ if (st.skipped)
506
+ continue;
507
+ const checkName = Executor._STEP_TO_CHECK[st.name] ?? st.name;
508
+ const passed = st.result.action !== 'abort';
509
+ let error;
510
+ if (!passed && st.result.explanation) {
511
+ error = { code: `STEP_${st.name.toUpperCase()}_FAILED`, message: st.result.explanation };
566
512
  }
513
+ checks.push({ check: checkName, passed, error });
567
514
  }
568
- return createPreflightResult(checks, requiresApproval);
515
+ return checks;
569
516
  }
570
- _checkSafety(moduleId, ctx) {
571
- guardCallChain(moduleId, ctx.callChain, this._maxCallDepth, this._maxModuleRepeat);
517
+ /** Validate module_id format at public entry points. */
518
+ _validateModuleId(moduleId) {
519
+ if (!moduleId || !MODULE_ID_PATTERN.test(moduleId)) {
520
+ throw new InvalidInputError(`Invalid module ID: '${moduleId}'. Must match pattern: ${MODULE_ID_PATTERN.source}`);
521
+ }
572
522
  }
573
523
  /** Check if a module requires approval, handling both interface and dict annotations. */
574
524
  _needsApproval(mod) {
@@ -577,127 +527,13 @@ export class Executor {
577
527
  return false;
578
528
  if (typeof annotations !== 'object')
579
529
  return false;
580
- // ModuleAnnotations interface (camelCase)
581
530
  if ('requiresApproval' in annotations) {
582
531
  return Boolean(annotations.requiresApproval);
583
532
  }
584
- // Dict-form annotations (snake_case)
585
533
  if ('requires_approval' in annotations) {
586
534
  return Boolean(annotations['requires_approval']);
587
535
  }
588
536
  return false;
589
537
  }
590
- /** Build an ApprovalRequest from module metadata. */
591
- _buildApprovalRequest(mod, moduleId, inputs, ctx) {
592
- const annotations = mod['annotations'];
593
- let ann;
594
- if (annotations != null && typeof annotations === 'object' && 'requiresApproval' in annotations) {
595
- ann = annotations;
596
- }
597
- else if (annotations != null && typeof annotations === 'object') {
598
- ann = dictToAnnotations(annotations);
599
- }
600
- else {
601
- ann = DEFAULT_ANNOTATIONS;
602
- }
603
- return createApprovalRequest({
604
- moduleId,
605
- arguments: inputs,
606
- context: ctx,
607
- annotations: ann,
608
- description: mod['description'] ?? null,
609
- tags: mod['tags'] ?? [],
610
- });
611
- }
612
- /** Map an ApprovalResult status to the appropriate action or error. */
613
- _handleApprovalResult(result, moduleId) {
614
- if (result.status === 'approved')
615
- return;
616
- if (result.status === 'rejected') {
617
- throw new ApprovalDeniedError(result, moduleId);
618
- }
619
- if (result.status === 'timeout') {
620
- throw new ApprovalTimeoutError(result, moduleId);
621
- }
622
- if (result.status === 'pending') {
623
- throw new ApprovalPendingError(result, moduleId);
624
- }
625
- // Unknown status treated as denied
626
- console.warn(`[apcore:executor] Unknown approval status '${result.status}' for module ${moduleId}, treating as denied`);
627
- throw new ApprovalDeniedError(result, moduleId);
628
- }
629
- /** Emit an audit event for the approval decision (logging + span event). */
630
- _emitApprovalEvent(result, moduleId, ctx) {
631
- console.warn(`[apcore:executor] Approval decision: module=${moduleId} status=${result.status} approved_by=${result.approvedBy} reason=${result.reason}`);
632
- const spansStack = ctx.data[CTX_TRACING_SPANS];
633
- if (spansStack && spansStack.length > 0) {
634
- spansStack[spansStack.length - 1].events.push({
635
- name: 'approval_decision',
636
- module_id: moduleId,
637
- status: result.status,
638
- approved_by: result.approvedBy ?? '',
639
- reason: result.reason ?? '',
640
- approval_id: result.approvalId ?? '',
641
- });
642
- }
643
- }
644
- /** Step 5: Approval gate. Returns inputs with internal keys stripped. */
645
- async _checkApproval(mod, moduleId, inputs, ctx) {
646
- if (this._approvalHandler === null)
647
- return inputs;
648
- if (!this._needsApproval(mod))
649
- return inputs;
650
- let result;
651
- let cleanInputs = inputs;
652
- if ('_approval_token' in inputs) {
653
- const token = inputs['_approval_token'];
654
- const { _approval_token: _, ...rest } = inputs;
655
- cleanInputs = rest;
656
- result = await this._approvalHandler.checkApproval(token);
657
- }
658
- else {
659
- const request = this._buildApprovalRequest(mod, moduleId, inputs, ctx);
660
- result = await this._approvalHandler.requestApproval(request);
661
- }
662
- this._emitApprovalEvent(result, moduleId, ctx);
663
- this._handleApprovalResult(result, moduleId);
664
- return cleanInputs;
665
- }
666
- async _executeWithTimeout(mod, moduleId, inputs, ctx) {
667
- let timeoutMs = this._defaultTimeout;
668
- // Respect global deadline: use whichever is shorter
669
- const globalDeadline = ctx.data[CTX_GLOBAL_DEADLINE];
670
- if (globalDeadline !== undefined) {
671
- const remaining = globalDeadline - Date.now();
672
- if (remaining <= 0) {
673
- throw new ModuleTimeoutError(moduleId, 0);
674
- }
675
- if (timeoutMs === 0 || remaining < timeoutMs) {
676
- timeoutMs = remaining;
677
- }
678
- }
679
- if (timeoutMs < 0) {
680
- throw new InvalidInputError(`Negative timeout: ${timeoutMs}ms`);
681
- }
682
- const executeFn = mod['execute'];
683
- if (typeof executeFn !== 'function') {
684
- throw new InvalidInputError(`Module '${moduleId}' has no execute method`);
685
- }
686
- const executionPromise = Promise.resolve(executeFn
687
- .call(mod, inputs, ctx));
688
- if (timeoutMs === 0) {
689
- console.warn('[apcore:executor] Timeout is 0, timeout limit disabled');
690
- return executionPromise;
691
- }
692
- let timer;
693
- const timeoutPromise = new Promise((_, reject) => {
694
- timer = setTimeout(() => {
695
- reject(new ModuleTimeoutError(moduleId, timeoutMs));
696
- }, timeoutMs);
697
- });
698
- return Promise.race([executionPromise, timeoutPromise]).finally(() => {
699
- clearTimeout(timer);
700
- });
701
- }
702
538
  }
703
539
  //# sourceMappingURL=executor.js.map