@aiwg/cli 2026.10.9 → 2026.10.11

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.
Files changed (116) hide show
  1. package/agentic/code/providers/model-capabilities.v1.json +275 -17
  2. package/bin/aiwg.mjs +8 -0
  3. package/dist/src/cli/handlers/mc-admission.js +39 -0
  4. package/dist/src/cli/handlers/mc.js +2 -31
  5. package/dist/src/cli/handlers/models-help.js +52 -0
  6. package/dist/src/cli/handlers/models.js +11 -81
  7. package/dist/src/cli/handlers/sessions.js +198 -15
  8. package/dist/src/cli/handlers/use-addon-profile.js +72 -0
  9. package/dist/src/cli/handlers/use.js +203 -234
  10. package/dist/src/cli/ui.js +1 -0
  11. package/dist/src/config/aiwg-config.js +38 -184
  12. package/dist/src/config/cli-help.js +54 -0
  13. package/dist/src/config/cli.js +5 -69
  14. package/dist/src/config/config-normalization.js +10 -0
  15. package/dist/src/config/worker-activation-gate.js +26 -0
  16. package/dist/src/config/worker-adoption-cli.js +102 -0
  17. package/dist/src/config/worker-adoption.js +368 -0
  18. package/dist/src/config/worker-config-digest.js +14 -0
  19. package/dist/src/config/worker-context.js +337 -0
  20. package/dist/src/config/worker-group-templates.js +150 -0
  21. package/dist/src/config/worker-guidance-warning.js +28 -0
  22. package/dist/src/config/worker-owner-lock.js +152 -0
  23. package/dist/src/config/worker-parallelism.js +154 -0
  24. package/dist/src/config/worker-write.js +75 -0
  25. package/dist/src/decision/telemetry/exporter.js +2 -1
  26. package/dist/src/decision/telemetry/index.js +1 -0
  27. package/dist/src/decision/telemetry/mapping.js +43 -0
  28. package/dist/src/decision/telemetry/otlp-http.js +5 -2
  29. package/dist/src/decision/telemetry/redaction.js +6 -1
  30. package/dist/src/decision/telemetry/types.js +3 -0
  31. package/dist/src/decision/telemetry/worker.js +69 -0
  32. package/dist/src/lint/rule-activation.js +15 -7
  33. package/dist/src/lint/runner.js +2 -2
  34. package/dist/src/mcp/server.mjs +2 -11
  35. package/dist/src/mcp/stdio-lifecycle.mjs +27 -0
  36. package/dist/src/mcp/tools/worker-failure-diagnostics.mjs +9 -0
  37. package/dist/src/mcp/tools/worker-invoke.mjs +114 -16
  38. package/dist/src/mission-protocol/codecs.js +8 -38
  39. package/dist/src/mission-protocol/worker-contract-diagnostics.js +62 -0
  40. package/dist/src/mission-protocol/worker-invocation-types.js +2 -0
  41. package/dist/src/models/conductor-data-policy.d.mts +8 -0
  42. package/dist/src/models/conductor-data-policy.mjs +190 -0
  43. package/dist/src/models/config-loader.js +19 -3
  44. package/dist/src/models/config-writer.js +27 -0
  45. package/dist/src/models/model-capabilities.v1.json +275 -17
  46. package/dist/src/models/provider-policy.js +14 -155
  47. package/dist/src/models/resolver.js +2 -293
  48. package/dist/src/models/worker-artifact-binding.mjs +12 -0
  49. package/dist/src/models/worker-direct-readiness-policy.js +155 -0
  50. package/dist/src/models/worker-direct-readiness.js +9 -218
  51. package/dist/src/models/worker-health-readiness.js +105 -26
  52. package/dist/src/models/worker-installation.mjs +12 -14
  53. package/dist/src/models/worker-member-references.js +64 -0
  54. package/dist/src/models/worker-plan.js +317 -0
  55. package/dist/src/models/worker-selection-policy.js +217 -0
  56. package/dist/src/models/wrapper-deployment.js +1 -1
  57. package/dist/src/policy/authorization.js +29 -1
  58. package/dist/src/process/aiwg-probe-command.d.mts +15 -0
  59. package/dist/src/process/aiwg-probe-command.mjs +37 -0
  60. package/dist/src/serve/shared-host-admission-checks.js +116 -0
  61. package/dist/src/serve/shared-host-scheduler.js +25 -107
  62. package/dist/src/serve/shared-host-starvation.js +73 -0
  63. package/dist/src/serve/worker-artifacts.d.mts +4 -0
  64. package/dist/src/serve/worker-artifacts.mjs +56 -0
  65. package/dist/src/serve/worker-command-request.js +68 -0
  66. package/dist/src/serve/worker-invocation-paths.js +144 -0
  67. package/dist/src/serve/worker-invocation-preparation.js +169 -0
  68. package/dist/src/serve/worker-invocation.js +24 -291
  69. package/dist/src/serve/worker-stdin-frame.d.mts +2 -2
  70. package/dist/src/serve/worker-stdin-frame.mjs +6 -6
  71. package/dist/src/sessions/adapters/grokbot.js +7 -0
  72. package/dist/src/sessions/batch-import.js +34 -6
  73. package/dist/src/sessions/contracts.js +19 -0
  74. package/dist/src/sessions/coverage-preflight.js +7 -1
  75. package/dist/src/sessions/fortemi-export-mapping.js +33 -3
  76. package/dist/src/sessions/host-catalog.js +157 -0
  77. package/dist/src/sessions/image-blobs.js +141 -0
  78. package/dist/src/sessions/importer.js +23 -3
  79. package/dist/src/sessions/index.js +2 -0
  80. package/dist/src/sessions/refresh.js +25 -2
  81. package/dist/src/sessions/repository.js +164 -22
  82. package/dist/src/sessions/workspace-discovery.js +5 -2
  83. package/dist/src/smiths/context-pipeline/parallelism-section.js +19 -7
  84. package/package.json +15 -1
  85. package/schemas/mission-v1.schema.json +20 -19
  86. package/schemas/models/model-config-compatibility.v1.schema.json +16 -0
  87. package/tools/agents/providers/claude-post-deploy.mjs +35 -0
  88. package/tools/agents/providers/claude.mjs +3 -30
  89. package/tools/agents/providers/codex-post-deploy.mjs +13 -0
  90. package/tools/agents/providers/codex.mjs +2 -8
  91. package/tools/agents/providers/conductor-data-guard.mjs +61 -0
  92. package/tools/agents/providers/rule-delivery.mjs +3 -16
  93. package/tools/agents/providers/rule-presentation.mjs +38 -0
  94. package/tools/plugin/package-plugins.mjs +1 -1
  95. package/tools/ralph-external/lib/claude-adapter.mjs +2 -2
  96. package/tools/ralph-external/lib/codex-adapter.mjs +3 -3
  97. package/tools/ralph-external/lib/managed-worker-attempt.mjs +86 -0
  98. package/tools/ralph-external/lib/muse-adapter.mjs +10 -0
  99. package/tools/ralph-external/lib/provider-adapter.mjs +11 -260
  100. package/tools/ralph-external/lib/session-attempt.mjs +85 -0
  101. package/tools/ralph-external/lib/strict-worker-json.mjs +190 -0
  102. package/tools/ralph-external/lib/worker-artifacts.mjs +209 -0
  103. package/tools/ralph-external/lib/worker-context.mjs +237 -0
  104. package/tools/ralph-external/lib/worker-final.mjs +143 -0
  105. package/tools/ralph-external/lib/worker-inputs.mjs +55 -0
  106. package/tools/ralph-external/lib/worker-invocation-ancestry.mjs +132 -0
  107. package/tools/ralph-external/lib/worker-invocation-processes.mjs +123 -0
  108. package/tools/ralph-external/lib/worker-isolation-status.mjs +48 -0
  109. package/tools/ralph-external/lib/worker-isolation.mjs +19 -73
  110. package/tools/ralph-external/lib/worker-launch-cli.mjs +49 -0
  111. package/tools/ralph-external/lib/worker-native-sandbox.mjs +83 -0
  112. package/tools/ralph-external/lib/worker-overflow.mjs +48 -13
  113. package/tools/ralph-external/lib/worker-retry-evidence.mjs +103 -0
  114. package/tools/ralph-external/lib/worker-termination.mjs +82 -0
  115. package/tools/ralph-external/session-launcher.mjs +124 -313
  116. package/vscode-extension/schemas/aiwg.config.v1.json +161 -2
@@ -12,8 +12,7 @@
12
12
  * - #2890 ProviderAdapter.parseWorkerFinal + assembleWorkerResult
13
13
  * (tools/ralph-external/lib/provider-adapter.mjs, mission-protocol/codecs.ts)
14
14
  *
15
- * This module is the FIRST MCP-side consumer of that pipeline end-to-end,
16
- * without a shell/CLI round trip and without Mission/MC state. It is
15
+ * This bridge runs without a shell/CLI round trip or Mission/MC state. It is
17
16
  * intentionally conservative where the pipeline cannot yet produce a fully
18
17
  * attested identity: verification is always left `pending` here (never
19
18
  * self-accepted) — a separate verifier is required before a direct result is
@@ -157,6 +156,7 @@
157
156
  * @issues #2892, #2896 (review reference, not reused as-is)
158
157
  */
159
158
 
159
+ import { workerFailureDiagnostics } from './worker-failure-diagnostics.mjs';
160
160
  import { signalChild } from '../../process/signal-child.mjs';
161
161
  import { z } from 'zod';
162
162
 
@@ -178,6 +178,13 @@ import { fileURLToPath, pathToFileURL } from 'node:url';
178
178
  import { mcpError, mcpJson } from '../helpers.mjs';
179
179
  import { buildShellStopControl, waitForUnitTermination } from './worker-shell-control.mjs';
180
180
  import { launchPreparedShellWorker, sweepWorkerStdin } from './worker-stdin-runtime.mjs';
181
+ // NOTE: no static import of ../../config/worker-context.js here. That
182
+ // module is TypeScript: a literal relative import resolves under vitest
183
+ // but throws ERR_MODULE_NOT_FOUND under raw node (e.g. `node
184
+ // src/mcp/cli.mjs`, which test/unit/mcp exercises directly). The
185
+ // request-time loader below resolves it through importImpl (dist when
186
+ // built, source otherwise), so merely loading this bridge never touches
187
+ // it. See tools/_resolve-impl.mjs (#3062).
181
188
 
182
189
  const execFileAsync = promisify(execFile);
183
190
 
@@ -642,6 +649,64 @@ function describeGateFailures(failures) {
642
649
  return `worker dispatch gate blocked: ${failures.map(f => `${f.signal} (${f.reason})`).join('; ')}`;
643
650
  }
644
651
 
652
+ /** Request-time loader for the TypeScript worker-context module (see the
653
+ * import note above): resolved through importImpl so production uses the
654
+ * compiled `dist` tree while a checkout falls back to source. Cached for
655
+ * the process lifetime; only request paths that actually ensure worker
656
+ * context ever trigger the load. */
657
+ let cachedWorkerContextModule = null;
658
+ async function loadWorkerContextModule() {
659
+ if (!cachedWorkerContextModule) cachedWorkerContextModule = await importImpl('config/worker-context.js');
660
+ return cachedWorkerContextModule;
661
+ }
662
+
663
+ /** Map the worker CLI to its provider and ensure its bootstrap is present
664
+ * in the worker workdir (#3685 round 2). Deploys the resolved union set
665
+ * (config providers plus orchestration CLIs) in one pass when the worker
666
+ * provider's bootstrap is missing; an unmappable CLI refuses with the
667
+ * typed diagnostic, since no deploy could target it. */
668
+ async function ensureWorkerContextForInvocation({ invocation, config }) {
669
+ const { ensureWorkerContextBootstrap, mapCliToProviderId, resolveWorkerContextProvidersFromConfig, WorkerContextBootstrapError } =
670
+ await loadWorkerContextModule();
671
+ const cli = invocation?.selected?.cli;
672
+ const mapped = mapCliToProviderId(cli, config?.parallelism);
673
+ if (!('provider' in mapped) || !mapped.provider) {
674
+ throw new WorkerContextBootstrapError(
675
+ `cannot ensure worker context: CLI '${cli}' maps to no AIWG provider (${mapped.skipped ?? 'unmapped'})`,
676
+ `configure parallelism.clis.${cli} with a providers:<id> adapter, or dispatch a CLI whose adapter maps to a known provider`,
677
+ { workdir: invocation?.cwd },
678
+ );
679
+ }
680
+ let providers = [mapped.provider];
681
+ try {
682
+ providers = [...new Set([...resolveWorkerContextProvidersFromConfig(config).providers, mapped.provider])];
683
+ } catch {
684
+ providers = [mapped.provider];
685
+ }
686
+ return ensureWorkerContextBootstrap({ workdir: invocation?.cwd, provider: mapped.provider, providers });
687
+ }
688
+
689
+ /** Refuse a failed bootstrap ensure as a typed blocked diagnostic that
690
+ * names the remedy — never a bare string the operator cannot act on.
691
+ * `code`/`remedy`/`provider`/`workdir` survive on any error object that
692
+ * carries them (the tools-side ensure throws a duck-typed refusal, not
693
+ * necessarily this module's class instance); only genuinely bare failures
694
+ * fall back to the generic code and remedy. */
695
+ function toWorkerContextDiagnostic(error) {
696
+ const fields = (error !== null && typeof error === 'object') ? error : {};
697
+ // Duck-typed on purpose: the worker-context module loads lazily (see
698
+ // above), so no class identity is available at module scope. Every
699
+ // refusal path sets `code`/`remedy` on the thrown object instead.
700
+ const code = (typeof fields.code === 'string' && fields.code ? fields.code : 'OG_WORKER_CONTEXT_UNAVAILABLE');
701
+ const remedy = (typeof fields.remedy === 'string' && fields.remedy ? fields.remedy
702
+ : 'ensure the worker CLI maps to a known AIWG provider and the aiwg CLI is installed (AIWG_BIN), then retry the run');
703
+ return { code,
704
+ message: error instanceof Error ? error.message : String(error?.message ?? error),
705
+ remedy,
706
+ ...(fields.provider !== undefined ? { provider: fields.provider } : {}),
707
+ ...(fields.workdir !== undefined ? { workdir: fields.workdir } : {}) };
708
+ }
709
+
645
710
  // Audited observer; refresh the historical baseline graph after rebase.
646
711
  const waitForUnitTerminationBaseline = waitForUnitTermination;
647
712
  export { waitForUnitTerminationBaseline as __testOnlyWaitForUnitTerminationBaseline };
@@ -758,6 +823,7 @@ export class ChildProcessTransport {
758
823
  // cap since a long-lived connection could otherwise grow it unbounded.
759
824
  this._rawResponses = new Map();
760
825
  this._buffer = undefined;
826
+ this._bufferBytes = 0;
761
827
  }
762
828
 
763
829
  async start() {
@@ -769,7 +835,7 @@ export class ChildProcessTransport {
769
835
  try {
770
836
  // Check each frame BEFORE concatenation or UTF-8 decoding.
771
837
  let offset = 0;
772
- let frameBytes = this._buffer?.length ?? 0;
838
+ let frameBytes = this._bufferBytes;
773
839
  while (offset < chunk.length) {
774
840
  const newline = chunk.indexOf(10, offset);
775
841
  const end = newline === -1 ? chunk.length : newline;
@@ -782,8 +848,7 @@ export class ChildProcessTransport {
782
848
  frameBytes = 0;
783
849
  offset = newline + 1;
784
850
  }
785
- this._buffer = this._buffer ? Buffer.concat([this._buffer, chunk]) : chunk;
786
- this._drain();
851
+ this._drain(chunk);
787
852
  } catch (error) { this.onerror?.(error); }
788
853
  });
789
854
  child.stdout?.on('error', error => this.onerror?.(error));
@@ -799,6 +864,7 @@ export class ChildProcessTransport {
799
864
  if (this._closed) return;
800
865
  this._closed = true;
801
866
  this._buffer = undefined;
867
+ this._bufferBytes = 0;
802
868
  this._rawResponses.clear();
803
869
  this.onclose?.();
804
870
  }
@@ -807,6 +873,7 @@ export class ChildProcessTransport {
807
873
  if (this._failure) return;
808
874
  this._failure = Object.assign(new Error(code), { code });
809
875
  this._buffer = undefined;
876
+ this._bufferBytes = 0;
810
877
  this._rawResponses.clear();
811
878
  // Start stopping before pipe closure can exit the leader; onclose rejects
812
879
  // pending SDK requests immediately while termination proof is awaited.
@@ -825,14 +892,26 @@ export class ChildProcessTransport {
825
892
  // (own-property-preserving, retained in `_rawResponses` — see
826
893
  // constructor doc) and once through the SDK's own `JSONRPCMessageSchema`
827
894
  // (routed to `onmessage`, unchanged behavior for the Client/Protocol).
828
- _drain() {
829
- while (this._buffer) {
830
- const index = this._buffer.indexOf('\n');
831
- if (index === -1) return;
832
- const line = this._buffer.toString('utf8', 0, index).replace(/\r$/, '');
833
- this._buffer = this._buffer.subarray(index + 1);
895
+ _drain(chunk) {
896
+ // Retain fragments without copying or rescanning the growing partial
897
+ // frame on every pipe read. Join once, only when a newline arrives.
898
+ let offset = 0;
899
+ while (offset < chunk.length) {
900
+ const newline = chunk.indexOf(10, offset);
901
+ const end = newline === -1 ? chunk.length : newline;
902
+ if (end > offset) {
903
+ (this._buffer ??= []).push(chunk.subarray(offset, end));
904
+ this._bufferBytes += end - offset;
905
+ }
906
+ if (newline === -1) return;
907
+ const frame = this._buffer?.length === 1
908
+ ? this._buffer[0] : Buffer.concat(this._buffer ?? [], this._bufferBytes);
909
+ const line = frame.toString('utf8').replace(/\r$/, '');
910
+ this._buffer = undefined;
911
+ this._bufferBytes = 0;
834
912
  this._handleLine(line);
835
- if (this._buffer?.length === 0) this._buffer = undefined;
913
+ if (this._closed || this._failure) return;
914
+ offset = newline + 1;
836
915
  }
837
916
  }
838
917
 
@@ -905,6 +984,7 @@ export class ChildProcessTransport {
905
984
  try { signalChild(child, 'SIGKILL'); } catch { /* already gone */ }
906
985
  }
907
986
  this._buffer = undefined;
987
+ this._bufferBytes = 0;
908
988
  }
909
989
  }
910
990
 
@@ -1745,6 +1825,24 @@ export async function registerWorkerInvokeToolset(server, options = {}) {
1745
1825
  }
1746
1826
  const { root, snapshot, drivers, invocation } = prefix;
1747
1827
 
1828
+ // #3685 round 2: the worker CLI's AIWG bootstrap must be present in
1829
+ // the worker workdir before dispatch — a context-free worker would run
1830
+ // with no project rules/skills/hooks. Present: no-op. Missing: exactly
1831
+ // one `aiwg use all` pass into the workdir (never elsewhere). A failed
1832
+ // ensure refuses as `blocked` with the typed diagnostic (never a
1833
+ // launch). Default on; `ensureWorkerContext: false` skips (synthetic
1834
+ // tests), a function value replaces the ensure.
1835
+ if (options.ensureWorkerContext !== false) {
1836
+ try {
1837
+ const ensure = typeof options.ensureWorkerContext === 'function'
1838
+ ? options.ensureWorkerContext
1839
+ : ensureWorkerContextForInvocation;
1840
+ await ensure({ invocation, config: snapshot.config });
1841
+ } catch (error) {
1842
+ return markTerminal({ status: 'blocked', termination: 'confirmed', diagnostics: [toWorkerContextDiagnostic(error)] });
1843
+ }
1844
+ }
1845
+
1748
1846
  // =========================================================================
1749
1847
  // #2915 round 8 (D6): ONE decision, immediately after the prefix, from
1750
1848
  // the launcher's own pure eligibility predicate,
@@ -1901,7 +1999,7 @@ export async function registerWorkerInvokeToolset(server, options = {}) {
1901
1999
  // diagnostic below. A duplicate-key/planted-marker probe showed
1902
2000
  // this exact kind of path can otherwise reflect attacker/worker-
1903
2001
  // controlled input text straight back out to the caller.
1904
- return markTerminal({ status: 'blocked', termination: 'confirmed', diagnostics: [typeof error?.code === 'string' ? error.code : 'OG_BRIEF_NOT_STRUCTURED'] });
2002
+ return markTerminal({ status: 'blocked', termination: 'confirmed', diagnostics: workerFailureDiagnostics(error, 'workerDirectBrief', 'OG_BRIEF_NOT_STRUCTURED') });
1905
2003
  }
1906
2004
  mcpBriefDigest = `sha256:${createHash('sha256').update(briefBytes).digest('hex')}`;
1907
2005
  }
@@ -2022,7 +2120,7 @@ export async function registerWorkerInvokeToolset(server, options = {}) {
2022
2120
  // parser error message, which can echo attacker/worker-controlled
2023
2121
  // input text back out (a duplicate-key probe reflected a planted
2024
2122
  // marker through this exact path before the fix).
2025
- return registry.markTerminal(entry.invocationId, { status: 'failed', termination: 'confirmed', diagnostics: [typeof error?.code === 'string' ? error.code : 'OG_WORKER_FINAL_INVALID'] });
2123
+ return registry.markTerminal(entry.invocationId, { status: 'failed', termination: 'confirmed', diagnostics: workerFailureDiagnostics(error, 'workerClaims', 'OG_WORKER_FINAL_INVALID') });
2026
2124
  }
2027
2125
  const assembled = await tryAssembleDirectResult({ request, invocation, observation, claims, root,
2028
2126
  briefDigest: mcpBriefDigest, requireStructuredBrief: invocation.transport === 'mcp' });
@@ -2228,7 +2326,7 @@ export async function registerWorkerInvokeToolset(server, options = {}) {
2228
2326
  // #2915 review 6 (finding 4): zero workers dispatched — same
2229
2327
  // proven-never-dispatched settlement as above.
2230
2328
  settleRecovery({ kind: 'uncertain', confirmedStop: true });
2231
- return markTerminal({ status: 'blocked', termination: 'confirmed', diagnostics: [typeof error?.code === 'string' ? error.code : 'OG_BRIEF_NOT_STRUCTURED'] });
2329
+ return markTerminal({ status: 'blocked', termination: 'confirmed', diagnostics: workerFailureDiagnostics(error, 'workerDirectBrief', 'OG_BRIEF_NOT_STRUCTURED') });
2232
2330
  }
2233
2331
  mcpBriefDigest = `sha256:${createHash('sha256').update(briefBytes).digest('hex')}`;
2234
2332
  }
@@ -2590,7 +2688,7 @@ export async function registerWorkerInvokeToolset(server, options = {}) {
2590
2688
  // parser error message, which can echo attacker/worker-controlled
2591
2689
  // input text back out (a duplicate-key probe reflected a planted
2592
2690
  // marker through this exact path before the fix).
2593
- return registry.markTerminal(entry.invocationId, { status: 'failed', termination: 'confirmed', diagnostics: [typeof error?.code === 'string' ? error.code : 'OG_WORKER_FINAL_INVALID'] });
2691
+ return registry.markTerminal(entry.invocationId, { status: 'failed', termination: 'confirmed', diagnostics: workerFailureDiagnostics(error, 'workerClaims', 'OG_WORKER_FINAL_INVALID') });
2594
2692
  }
2595
2693
  const assembled = await tryAssembleDirectResult({ request, invocation, observation, claims, root,
2596
2694
  briefDigest: mcpBriefDigest, requireStructuredBrief: invocation.transport === 'mcp' });
@@ -1,3 +1,4 @@
1
+ import { workerSemanticErrors, contractValidationPointer } from './worker-contract-diagnostics.js';
1
2
  import { createHash } from 'node:crypto';
2
3
  import { readFileSync } from 'node:fs';
3
4
  import { dirname, join, resolve } from 'node:path';
@@ -300,7 +301,7 @@ function loadWorkerValidators() {
300
301
  const validators = new Map();
301
302
  for (const definition of WORKER_DEFINITIONS)
302
303
  validators.set(definition, ajv.compile({ $ref: `${schema.$id}#/$defs/${definition}` }));
303
- cachedWorkerValidators = { schemaId: schema.$id, bundleDigest: workerBundleDigestFor(schema), validators };
304
+ cachedWorkerValidators = { schema, schemaId: schema.$id, bundleDigest: workerBundleDigestFor(schema), validators };
304
305
  }
305
306
  return cachedWorkerValidators;
306
307
  }
@@ -327,43 +328,9 @@ export function workerContractSchemaId() {
327
328
  // 05 §5.1: complete JSON payloads are bounded independently of field limits.
328
329
  export const WORKER_MAX_JSON_BYTES = 262_144;
329
330
  export const WORKER_MAX_NESTING_DEPTH = 16;
330
- function workerSemanticErrors(value) {
331
- const ancestors = new Set();
332
- function visit(node, depth) {
333
- if (!node || typeof node !== 'object')
334
- return undefined;
335
- if (depth > WORKER_MAX_NESTING_DEPTH)
336
- return `JSON nesting depth exceeds ${WORKER_MAX_NESTING_DEPTH}`;
337
- if (ancestors.has(node))
338
- return 'Worker JSON must not contain cycles';
339
- ancestors.add(node);
340
- for (const child of Object.values(node)) {
341
- const error = visit(child, depth + 1);
342
- if (error)
343
- return error;
344
- }
345
- ancestors.delete(node);
346
- return undefined;
347
- }
348
- // Root object/array is depth 1; scalar leaves do not add a nesting level.
349
- const depthError = visit(value, 1);
350
- if (depthError)
351
- return [depthError];
352
- try {
353
- const bytes = JSON.stringify(value);
354
- if (bytes === undefined)
355
- return ['Worker payload must be JSON serializable'];
356
- if (Buffer.byteLength(bytes, 'utf8') > WORKER_MAX_JSON_BYTES)
357
- return [`Serialized UTF-8 JSON exceeds ${WORKER_MAX_JSON_BYTES} bytes`];
358
- }
359
- catch {
360
- return ['Worker payload must be JSON serializable'];
361
- }
362
- return [];
363
- }
364
331
  /** Validate a worker payload against its bundled definition. Closed shapes reject unknown fields. */
365
332
  export function validateWorkerPayload(definition, value) {
366
- const semanticErrors = workerSemanticErrors(value);
333
+ const semanticErrors = workerSemanticErrors(value, WORKER_MAX_JSON_BYTES, WORKER_MAX_NESTING_DEPTH);
367
334
  if (semanticErrors.length)
368
335
  return { valid: false, errors: semanticErrors };
369
336
  const validate = loadWorkerValidators().validators.get(definition);
@@ -392,8 +359,11 @@ export function validateWorkerPayload(definition, value) {
392
359
  }
393
360
  export function assertValidWorkerPayload(definition, value) {
394
361
  const { valid, errors } = validateWorkerPayload(definition, value);
395
- if (!valid)
396
- throw new WorkerContractError('OG_CONTRACT_INVALID', `Invalid ${definition}: ${errors.join('; ') || 'shape mismatch'}.`);
362
+ if (!valid) {
363
+ const { schema, validators } = loadWorkerValidators();
364
+ throw Object.assign(new WorkerContractError('OG_CONTRACT_INVALID', `Invalid ${definition}: ${errors.join('; ') || 'shape mismatch'}.`), { pointer: contractValidationPointer(definition, workerSemanticErrors(value, WORKER_MAX_JSON_BYTES, WORKER_MAX_NESTING_DEPTH).length
365
+ ? null : validators.get(definition)?.errors, schema) });
366
+ }
397
367
  }
398
368
  /** The producer is supplied by trusted calling code, never read from payload data. */
399
369
  export function decodeWorkerProducedPayload(definition, producer, value) {
@@ -0,0 +1,62 @@
1
+ export function workerSemanticErrors(value, maxBytes, maxDepth) {
2
+ const ancestors = new Set();
3
+ function visit(node, depth) {
4
+ if (!node || typeof node !== 'object')
5
+ return undefined;
6
+ if (depth > maxDepth)
7
+ return `JSON nesting depth exceeds ${maxDepth}`;
8
+ if (ancestors.has(node))
9
+ return 'Worker JSON must not contain cycles';
10
+ ancestors.add(node);
11
+ for (const child of Object.values(node)) {
12
+ const error = visit(child, depth + 1);
13
+ if (error)
14
+ return error;
15
+ }
16
+ ancestors.delete(node);
17
+ return undefined;
18
+ }
19
+ // Root object/array is depth 1; scalar leaves do not add a nesting level.
20
+ const depthError = visit(value, 1);
21
+ if (depthError)
22
+ return [depthError];
23
+ try {
24
+ const bytes = JSON.stringify(value);
25
+ if (bytes === undefined)
26
+ return ['Worker payload must be JSON serializable'];
27
+ if (Buffer.byteLength(bytes, 'utf8') > maxBytes)
28
+ return [`Serialized UTF-8 JSON exceeds ${maxBytes} bytes`];
29
+ }
30
+ catch {
31
+ return ['Worker payload must be JSON serializable'];
32
+ }
33
+ return [];
34
+ }
35
+ export function contractValidationPointer(definition, errors, schema) {
36
+ const names = new Set();
37
+ function visit(node) {
38
+ if (!node || typeof node !== 'object')
39
+ return;
40
+ const record = node;
41
+ if (record.properties && typeof record.properties === 'object') {
42
+ for (const name of Object.keys(record.properties))
43
+ names.add(name);
44
+ }
45
+ for (const value of Object.values(record))
46
+ visit(value);
47
+ }
48
+ visit(schema);
49
+ const error = errors?.[0];
50
+ const parts = (error?.instancePath ?? '').split('/').slice(1);
51
+ if (error?.keyword === 'required')
52
+ parts.push(String(error.params.missingProperty));
53
+ const safe = [definition];
54
+ for (const part of parts) {
55
+ const name = part.replace(/~1/g, '/').replace(/~0/g, '~');
56
+ if (!names.has(name) && !/^(0|[1-9][0-9]{0,8})$/.test(name))
57
+ break;
58
+ safe.push(part);
59
+ }
60
+ return `/${safe.join('/')}`;
61
+ }
62
+ //# sourceMappingURL=worker-contract-diagnostics.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=worker-invocation-types.js.map
@@ -0,0 +1,8 @@
1
+ // Types for the portable conductor input guard.
2
+ import type { WorkerSelectionPolicy } from './types.js';
3
+ import type { ParallelismConfig } from '../config/aiwg-config.js';
4
+ export const LOCAL_ONLY_PATH_PATTERN: string;
5
+ export function validLocalOnlyPath(value: unknown): boolean;
6
+ export function collectLocalOnlyPaths(policy?: WorkerSelectionPolicy, parallelism?: ParallelismConfig): string[];
7
+ export function conductorToolDenial(event: { tool_name?: string; tool_input?: Record<string, unknown>; cwd?: string }, root: string, paths: string[]): string | null;
8
+ export function runConductorDataHook(root: string, paths: string[]): void;
@@ -0,0 +1,190 @@
1
+ // Refuse conductor references, glob reads and ancestor searches of declared local-only inputs.
2
+ import { realpathSync, readFileSync, statSync } from 'node:fs';
3
+ import { resolve, dirname, relative, isAbsolute, sep, matchesGlob, basename } from 'node:path';
4
+
5
+ // Literal project-relative files or directory subtrees; no globs, traversal or rule syntax.
6
+ export const LOCAL_ONLY_PATH_PATTERN = '^(?!.*(?:^|/)\\.{1,2}(?:/|$))[A-Za-z0-9_.@+,-]+(?:/[A-Za-z0-9_.@+,-]+)*$(?![\\s\\S])';
7
+ export const validLocalOnlyPath = value => typeof value === 'string' && new RegExp(LOCAL_ONLY_PATH_PATTERN).test(value);
8
+
9
+ export function collectLocalOnlyPaths(policy, parallelism) {
10
+ const policies = [policy?.dataPolicy, ...Object.values(policy?.dataClasses ?? {}),
11
+ ...Object.values(parallelism?.groupTemplates ?? {}).map(group => group.dataPolicy),
12
+ ...Object.values(parallelism?.groups ?? {}).map(group => group.dataPolicy)];
13
+ const paths = policies.flatMap(data => data?.localOnlyPaths ?? []);
14
+ if (!paths.every(validLocalOnlyPath)) throw new Error('Invalid dataPolicy.localOnlyPaths: expected literal project-relative paths');
15
+ return [...new Set(paths)].sort();
16
+ }
17
+
18
+ // Resolve existing symlink ancestors without opening file contents (also handles absent leaves).
19
+ function canonical(file) {
20
+ try { return realpathSync(file); } catch {
21
+ const parent = dirname(file);
22
+ return parent === file ? file : resolve(canonical(parent), relative(parent, file));
23
+ }
24
+ }
25
+ const contains = (base, file) => relative(base, file) === ''
26
+ || (!relative(base, file).startsWith('..' + sep) && relative(base, file) !== '..' && !isAbsolute(relative(base, file)));
27
+
28
+ // Retain quoting for glob characters, but never execute shell expansions or substitutions.
29
+ const literalPattern = value => value.replace(/[*?\[\]{}()!+@]/g, char => char === ']' ? ']' : `[${char}]`);
30
+ function shellCommands(command) {
31
+ const commands = [];
32
+ let words = [], value = '', pattern = '', glob = false, quote = null;
33
+ const word = () => {
34
+ if (value) words.push({ value, pattern, glob });
35
+ value = ''; pattern = ''; glob = false;
36
+ };
37
+ const end = () => { word(); if (words.length) commands.push(words); words = []; };
38
+ const append = (char, active) => {
39
+ value += char;
40
+ pattern += active ? char : literalPattern(char);
41
+ glob ||= active && /[*?[]/.test(char);
42
+ };
43
+ for (let index = 0; index < command.length; index++) {
44
+ const char = command[index];
45
+ if (char === '\\' && quote !== "'" && index + 1 < command.length
46
+ && (quote !== '"' || /["\\$`\n]/.test(command[index + 1]))) {
47
+ const next = command[++index];
48
+ if (next !== '\n') append(next, false);
49
+ } else if (quote) {
50
+ if (char === quote) quote = null;
51
+ else append(char, false);
52
+ } else if (char === "'" || char === '"') quote = char;
53
+ else if (/[;&|<>()`\n]/.test(char)) end();
54
+ else if (/\s/.test(char)) word();
55
+ else append(char, true);
56
+ }
57
+ end();
58
+ return commands;
59
+ }
60
+
61
+ function globComponents(token, cwd) {
62
+ const values = token.value.split('/'), patterns = token.pattern.split('/');
63
+ const parts = isAbsolute(token.value) ? [] : cwd.split('/').filter(Boolean)
64
+ .map(value => ({ value, pattern: literalPattern(value) }));
65
+ for (let index = 0; index < values.length; index++) {
66
+ const value = values[index];
67
+ if (!value || value === '.') continue;
68
+ if (value === '..') parts.pop();
69
+ else parts.push({ value, pattern: patterns[index] });
70
+ }
71
+ return parts;
72
+ }
73
+
74
+ function globOverlaps(parts, file, subtree) {
75
+ const names = file.split(sep).filter(Boolean), memo = new Map();
76
+ const match = (index, depth) => {
77
+ const key = `${index}:${depth}`;
78
+ if (memo.has(key)) return memo.get(key);
79
+ let result;
80
+ // A matched ancestor is conservative: readers can subsequently traverse it.
81
+ if (index === parts.length) result = true;
82
+ else if (depth === names.length) result = subtree || parts.slice(index).every(part => part === '**');
83
+ else if (parts[index] === '**') result = match(index + 1, depth)
84
+ || (!names[depth].startsWith('.') && match(index, depth + 1));
85
+ else result = matchesGlob(names[depth], parts[index]) && match(index + 1, depth + 1);
86
+ memo.set(key, result);
87
+ return result;
88
+ };
89
+ return match(0, 0);
90
+ }
91
+
92
+ function globReferences(token, cwd, protectedPaths) {
93
+ if (!token.glob) return false;
94
+ const parts = globComponents(token, cwd);
95
+ const firstGlob = parts.findIndex(part => /[*?[]/.test(part.pattern));
96
+ const prefix = firstGlob < 0 ? parts.length : firstGlob;
97
+ const canonicalPrefix = canonical(resolve(sep, ...parts.slice(0, prefix).map(part => part.value)));
98
+ const patterns = [parts.map(part => part.pattern), [
99
+ ...canonicalPrefix.split(sep).filter(Boolean).map(literalPattern),
100
+ ...parts.slice(prefix).map(part => part.pattern),
101
+ ]];
102
+ return protectedPaths.some(file => {
103
+ let subtree = true;
104
+ try { subtree = statSync(file).isDirectory(); } catch { /* Absent declarations still protect future subtrees. */ }
105
+ return patterns.some(pattern => globOverlaps(pattern, file, subtree));
106
+ });
107
+ }
108
+
109
+ // Recognize common recursive readers; option values and search patterns are not path operands.
110
+ function recursiveRoots(words) {
111
+ const index = words.findIndex(word => ['rg', 'grep', 'find'].includes(basename(word.value)));
112
+ if (index < 0) return [];
113
+ const name = basename(words[index].value), args = words.slice(index + 1).map(word => word.value);
114
+ if (name === 'grep' && !args.some(arg => /^-[^-]*[rR]/.test(arg)
115
+ || ['--recursive', '--dereference-recursive'].includes(arg))) return [];
116
+ if (name === 'find') {
117
+ const roots = [];
118
+ for (const arg of args) {
119
+ if (['-H', '-L', '-P'].includes(arg) && !roots.length) continue;
120
+ if (/^[-(!]/.test(arg)) break;
121
+ roots.push(arg);
122
+ }
123
+ return roots.length ? roots : ['.'];
124
+ }
125
+ const valueOptions = new Set(['-e', '--regexp', '-f', '--file', '-g', '--glob', '--iglob',
126
+ '-t', '--type', '-T', '--type-not', '-m', '--max-count', '-A', '-B', '-C', '--after-context',
127
+ '--before-context', '--context', '--include', '--exclude', '--exclude-dir', '--encoding', '-j', '--threads']);
128
+ let needsPattern = !args.includes('--files'), options = true;
129
+ const roots = [];
130
+ for (let cursor = 0; cursor < args.length; cursor++) {
131
+ const arg = args[cursor];
132
+ if (options && arg === '--') { options = false; continue; }
133
+ if (options && arg.startsWith('-') && arg !== '-') {
134
+ if (/^(?:-[ef].+|--(?:regexp|file)=)/.test(arg)) needsPattern = false;
135
+ if (valueOptions.has(arg)) {
136
+ if (['-e', '--regexp', '-f', '--file'].includes(arg)) needsPattern = false;
137
+ cursor++;
138
+ }
139
+ continue;
140
+ }
141
+ if (needsPattern) needsPattern = false;
142
+ else roots.push(arg);
143
+ }
144
+ return roots.length ? roots : ['.'];
145
+ }
146
+
147
+ export function conductorToolDenial(event, root, paths) {
148
+ const input = event.tool_input ?? {};
149
+ const cwd = typeof event.cwd === 'string' && isAbsolute(event.cwd) ? event.cwd : root;
150
+ const protectedPaths = paths.flatMap(file => [resolve(root, file), canonical(resolve(root, file))]);
151
+ const denied = candidate => {
152
+ if (typeof candidate !== 'string' || !candidate) return false;
153
+ const file = resolve(cwd, candidate);
154
+ return protectedPaths.some(base => contains(base, file) || contains(base, canonical(file)));
155
+ };
156
+ if ([input.file_path, input.path].some(denied)) return 'AIWG: local-only task input is unavailable to this conductor';
157
+ if (typeof input.command === 'string') {
158
+ // Covers literal/quoted paths, compound commands and common redirections. This is
159
+ // deliberately conservative, not a shell interpreter or a filesystem sandbox.
160
+ const command = input.command;
161
+ const tokens = command.match(/[^\s"'`;&|<>()]+/g) ?? [];
162
+ if (paths.some(file => command.includes(file)) || tokens.some(token => denied(token) || denied(token.split('=').at(-1)))) {
163
+ return 'AIWG: command references a local-only task input';
164
+ }
165
+ const commands = shellCommands(command);
166
+ if (commands.flat().some(token => globReferences(token, cwd, protectedPaths))
167
+ || commands.some(words => recursiveRoots(words).some(candidate => {
168
+ const ancestor = resolve(cwd, candidate);
169
+ return protectedPaths.some(file => contains(ancestor, file) || contains(canonical(ancestor), file));
170
+ }))) return 'AIWG: command may read a local-only task input through a glob or ancestor search';
171
+ }
172
+ return null;
173
+ }
174
+
175
+ export function runConductorDataHook(root, paths) {
176
+ try {
177
+ const raw = readFileSync(0, 'utf8');
178
+ if (Buffer.byteLength(raw) > 1024 * 1024) throw new Error('oversized event');
179
+ const event = JSON.parse(raw);
180
+ if (!event || typeof event !== 'object' || typeof event.tool_name !== 'string'
181
+ || !event.tool_input || typeof event.tool_input !== 'object') throw new Error('invalid event');
182
+ const reason = conductorToolDenial(event, root, paths);
183
+ if (reason) process.stdout.write(JSON.stringify({ hookSpecificOutput: {
184
+ hookEventName: 'PreToolUse', permissionDecision: 'deny', permissionDecisionReason: reason,
185
+ } }) + '\n');
186
+ } catch {
187
+ process.stderr.write('AIWG local-only guard: refusing malformed tool event\n');
188
+ process.exitCode = 2;
189
+ }
190
+ }
@@ -1,3 +1,4 @@
1
+ import { workerDispatchBlocked, isWorkerMaintenance, workerRecoveryDiagnostic } from '../config/worker-write.js';
1
2
  /**
2
3
  * Model Configuration Loader
3
4
  * Loads and merges model configurations from multiple sources with proper precedence
@@ -11,17 +12,20 @@ import { fileURLToPath } from 'node:url';
11
12
  import { homedir } from 'os';
12
13
  import { existsSync } from 'fs';
13
14
  import { findPackageRoot } from '../cli/find-package-root.js';
14
- import { workerConfigDigest, validateWorkerGroupReferences } from '../config/aiwg-config.js';
15
+ import { getConfigPath, validateParallelismConfig, workerConfigDigest, validateWorkerGroupReferences } from '../config/aiwg-config.js';
15
16
  import { DEFAULT_WORKER_SELECTION, WorkerRoleEffortError, mergeWorkerSelection, WorkerSelectionPolicySchema, validateUserProjectModelConfig } from './provider-policy.js';
17
+ import { expandWorkerGroupTemplates, mergeGroupWorkerPolicy } from '../config/worker-group-templates.js';
16
18
  /**
17
19
  * Loads and merges model configurations
18
20
  */
19
21
  export class ConfigLoader {
22
+ options;
20
23
  cache = null;
21
24
  cacheDigest = null;
22
25
  inputDigests = null;
23
26
  locations;
24
27
  constructor(projectPath, options) {
28
+ this.options = options;
25
29
  const aiwgHome = options?.aiwgHome || process.env.AIWG_HOME
26
30
  || findPackageRoot(dirname(fileURLToPath(import.meta.url)));
27
31
  if (!aiwgHome)
@@ -38,10 +42,17 @@ export class ConfigLoader {
38
42
  * Precedence: project > user > AIWG default
39
43
  */
40
44
  async load(parallelism) {
45
+ if (!isWorkerMaintenance() && workerDispatchBlocked(dirname(this.locations.projectConfig))) {
46
+ throw new Error(workerRecoveryDiagnostic(dirname(this.locations.projectConfig)));
47
+ }
48
+ const expansion = expandWorkerGroupTemplates(parallelism, getConfigPath(dirname(this.locations.projectConfig)));
49
+ const errors = validateParallelismConfig(parallelism);
50
+ if (errors.length)
51
+ throw new Error(errors.join('; '));
41
52
  // Load all configs
42
53
  const aiwgDefault = await this.loadAiwgDefault();
43
54
  const userConfig = await this.loadUserConfig();
44
- const projectConfig = await this.loadProjectConfig();
55
+ const projectConfig = this.options?.workerCandidate ? this.options.workerCandidate.projectModels : await this.loadProjectConfig();
45
56
  const digest = workerConfigDigest([aiwgDefault, userConfig, projectConfig, parallelism]);
46
57
  if (this.cache && this.cacheDigest === digest)
47
58
  return this.cache;
@@ -64,7 +75,7 @@ export class ConfigLoader {
64
75
  if (result.diagnostics.length)
65
76
  throw new Error(result.diagnostics.map(d => `${d.code}: ${d.pointer}: ${d.message}; origins=${JSON.stringify(d.origins ?? [])}`).join('; '));
66
77
  merged.defaults.workerSelection = result.policy;
67
- merged.workerPolicyOrigins = result.origins;
78
+ merged.workerPolicyOrigins = { ...result.origins, ...expansion.origins };
68
79
  }
69
80
  if (parallelism?.worker_dispatch) {
70
81
  // Exact wrapper pins remain explainable alongside worker restrictions.
@@ -83,6 +94,11 @@ export class ConfigLoader {
83
94
  });
84
95
  }
85
96
  }
97
+ for (const group of Object.values(expansion.parallelism?.groups ?? {})) {
98
+ const narrowed = mergeGroupWorkerPolicy(merged.defaults.workerSelection, group);
99
+ if (narrowed.diagnostics.length)
100
+ throw new Error(narrowed.diagnostics.map(d => `${d.code}: ${d.message}`).join('; '));
101
+ }
86
102
  const errors = validateWorkerGroupReferences(parallelism, merged.defaults.workerSelection);
87
103
  if (errors.length)
88
104
  throw new Error(errors.join('; '));