@aiwg/cli 2026.10.8 → 2026.10.10

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 (104) hide show
  1. package/agentic/code/providers/model-capabilities.v1.json +197 -3
  2. package/bin/aiwg.mjs +8 -0
  3. package/dist/src/artifacts/cli.js +9 -5
  4. package/dist/src/auth/config.js +3 -8
  5. package/dist/src/auth/resource-credentials.js +35 -2
  6. package/dist/src/cli/handlers/diagnose.js +13 -0
  7. package/dist/src/cli/handlers/mc-admission.js +39 -0
  8. package/dist/src/cli/handlers/mc.js +2 -31
  9. package/dist/src/cli/handlers/models-help.js +52 -0
  10. package/dist/src/cli/handlers/models.js +11 -81
  11. package/dist/src/cli/handlers/resource-versions.js +8 -5
  12. package/dist/src/cli/handlers/script-runner.js +13 -1
  13. package/dist/src/cli/handlers/use-addon-profile.js +72 -0
  14. package/dist/src/cli/handlers/use.js +209 -233
  15. package/dist/src/cli/ui.js +1 -0
  16. package/dist/src/config/aiwg-config.js +38 -184
  17. package/dist/src/config/cli-help.js +54 -0
  18. package/dist/src/config/cli.js +5 -69
  19. package/dist/src/config/config-normalization.js +10 -0
  20. package/dist/src/config/worker-activation-gate.js +26 -0
  21. package/dist/src/config/worker-adoption-cli.js +102 -0
  22. package/dist/src/config/worker-adoption.js +368 -0
  23. package/dist/src/config/worker-config-digest.js +14 -0
  24. package/dist/src/config/worker-group-templates.js +150 -0
  25. package/dist/src/config/worker-guidance-warning.js +28 -0
  26. package/dist/src/config/worker-owner-lock.js +152 -0
  27. package/dist/src/config/worker-parallelism.js +154 -0
  28. package/dist/src/config/worker-write.js +75 -0
  29. package/dist/src/decision/telemetry/exporter.js +2 -1
  30. package/dist/src/decision/telemetry/index.js +1 -0
  31. package/dist/src/decision/telemetry/mapping.js +43 -0
  32. package/dist/src/decision/telemetry/otlp-http.js +5 -2
  33. package/dist/src/decision/telemetry/redaction.js +6 -1
  34. package/dist/src/decision/telemetry/types.js +3 -0
  35. package/dist/src/decision/telemetry/worker.js +69 -0
  36. package/dist/src/lint/rule-activation.js +15 -7
  37. package/dist/src/lint/runner.js +2 -2
  38. package/dist/src/mcp/server.mjs +2 -11
  39. package/dist/src/mcp/stdio-lifecycle.mjs +27 -0
  40. package/dist/src/mcp/tools/worker-failure-diagnostics.mjs +9 -0
  41. package/dist/src/mcp/tools/worker-invoke.mjs +6 -6
  42. package/dist/src/mission-protocol/codecs.js +8 -38
  43. package/dist/src/mission-protocol/worker-contract-diagnostics.js +62 -0
  44. package/dist/src/mission-protocol/worker-invocation-types.js +2 -0
  45. package/dist/src/models/conductor-data-policy.d.mts +8 -0
  46. package/dist/src/models/conductor-data-policy.mjs +190 -0
  47. package/dist/src/models/config-loader.js +19 -3
  48. package/dist/src/models/config-writer.js +27 -0
  49. package/dist/src/models/model-capabilities.v1.json +197 -3
  50. package/dist/src/models/provider-policy.js +7 -155
  51. package/dist/src/models/resolver.js +2 -293
  52. package/dist/src/models/worker-artifact-binding.mjs +12 -0
  53. package/dist/src/models/worker-direct-readiness-policy.js +155 -0
  54. package/dist/src/models/worker-direct-readiness.js +9 -218
  55. package/dist/src/models/worker-health-readiness.js +105 -26
  56. package/dist/src/models/worker-installation.mjs +12 -14
  57. package/dist/src/models/worker-member-references.js +64 -0
  58. package/dist/src/models/worker-plan.js +317 -0
  59. package/dist/src/models/worker-selection-policy.js +217 -0
  60. package/dist/src/models/wrapper-deployment.js +1 -1
  61. package/dist/src/resources/web-release.js +76 -65
  62. package/dist/src/serve/shared-host-admission-checks.js +116 -0
  63. package/dist/src/serve/shared-host-scheduler.js +25 -107
  64. package/dist/src/serve/shared-host-starvation.js +73 -0
  65. package/dist/src/serve/worker-artifacts.d.mts +4 -0
  66. package/dist/src/serve/worker-artifacts.mjs +56 -0
  67. package/dist/src/serve/worker-command-request.js +68 -0
  68. package/dist/src/serve/worker-invocation-paths.js +144 -0
  69. package/dist/src/serve/worker-invocation-preparation.js +169 -0
  70. package/dist/src/serve/worker-invocation.js +24 -291
  71. package/dist/src/serve/worker-stdin-frame.d.mts +2 -2
  72. package/dist/src/serve/worker-stdin-frame.mjs +6 -6
  73. package/dist/src/smiths/context-pipeline/parallelism-section.js +19 -7
  74. package/dist/src/storage/backends/fortemi.js +33 -29
  75. package/dist/src/storage/backends/logseq.js +16 -3
  76. package/dist/src/storage/backends/postgres.js +74 -33
  77. package/dist/src/storage/backends/postgrest.js +37 -13
  78. package/dist/src/storage/config.js +6 -1
  79. package/dist/src/uhp/config.js +4 -0
  80. package/package.json +9 -1
  81. package/schemas/mission-v1.schema.json +20 -19
  82. package/schemas/models/model-config-compatibility.v1.schema.json +16 -0
  83. package/tools/agents/providers/claude-post-deploy.mjs +35 -0
  84. package/tools/agents/providers/claude.mjs +3 -30
  85. package/tools/agents/providers/codex-post-deploy.mjs +13 -0
  86. package/tools/agents/providers/codex.mjs +2 -8
  87. package/tools/agents/providers/conductor-data-guard.mjs +61 -0
  88. package/tools/agents/providers/rule-delivery.mjs +3 -16
  89. package/tools/agents/providers/rule-presentation.mjs +38 -0
  90. package/tools/plugin/package-plugins.mjs +1 -1
  91. package/tools/ralph-external/lib/claude-adapter.mjs +2 -2
  92. package/tools/ralph-external/lib/codex-adapter.mjs +3 -3
  93. package/tools/ralph-external/lib/managed-worker-attempt.mjs +86 -0
  94. package/tools/ralph-external/lib/provider-adapter.mjs +11 -260
  95. package/tools/ralph-external/lib/session-attempt.mjs +85 -0
  96. package/tools/ralph-external/lib/strict-worker-json.mjs +190 -0
  97. package/tools/ralph-external/lib/worker-artifacts.mjs +209 -0
  98. package/tools/ralph-external/lib/worker-final.mjs +143 -0
  99. package/tools/ralph-external/lib/worker-inputs.mjs +55 -0
  100. package/tools/ralph-external/lib/worker-isolation.mjs +2 -44
  101. package/tools/ralph-external/lib/worker-launch-cli.mjs +47 -0
  102. package/tools/ralph-external/lib/worker-native-sandbox.mjs +83 -0
  103. package/tools/ralph-external/session-launcher.mjs +73 -217
  104. package/vscode-extension/schemas/aiwg.config.v1.json +161 -2
@@ -691,17 +691,8 @@ If retry fails, prepare structured escalation:
691
691
  export async function startServer() {
692
692
  const server = await createServer();
693
693
  const transport = new StdioServerTransport();
694
-
695
- console.error('[AIWG MCP] Starting server...');
696
- await server.connect(transport);
697
- console.error('[AIWG MCP] Server connected via stdio transport');
698
-
699
- // Handle graceful shutdown
700
- process.on('SIGINT', async () => {
701
- console.error('[AIWG MCP] Shutting down...');
702
- await server.close();
703
- process.exit(0);
704
- });
694
+ const { serveStdioUntilClosed } = await import('./stdio-lifecycle.mjs');
695
+ await serveStdioUntilClosed(server, transport);
705
696
  }
706
697
 
707
698
  // Run if executed directly
@@ -0,0 +1,27 @@
1
+ // Keep the CLI dispatch pending for the lifetime of its stdio MCP transport.
2
+ export async function serveStdioUntilClosed(server, transport, { input = process.stdin, signals = process } = {}) {
3
+ let finish;
4
+ let fail;
5
+ const closed = new Promise((resolve, reject) => { finish = resolve; fail = reject; });
6
+ const previousClose = server.server.onclose;
7
+ const onClose = () => {
8
+ try { previousClose?.(); } finally { finish(); }
9
+ };
10
+ const shutdown = () => { Promise.resolve().then(() => server.close()).catch(fail); };
11
+ server.server.onclose = onClose;
12
+ input.once('end', shutdown);
13
+ signals.once('SIGINT', shutdown);
14
+ signals.once('SIGTERM', shutdown);
15
+ try {
16
+ console.error('[AIWG MCP] Starting server...');
17
+ await server.connect(transport);
18
+ console.error('[AIWG MCP] Server connected via stdio transport');
19
+ if (input.readableEnded) shutdown();
20
+ await closed;
21
+ } finally {
22
+ input.removeListener('end', shutdown);
23
+ signals.removeListener('SIGINT', shutdown);
24
+ signals.removeListener('SIGTERM', shutdown);
25
+ if (server.server.onclose === onClose) server.server.onclose = previousClose;
26
+ }
27
+ }
@@ -0,0 +1,9 @@
1
+ // Keep contract failures actionable without publishing parser messages or payload values.
2
+ export function workerFailureDiagnostics(error, definition, fallback) {
3
+ const code = typeof error?.code === 'string' ? error.code : fallback;
4
+ if (code !== 'OG_CONTRACT_INVALID') return [code];
5
+ const pointer = typeof error.pointer === 'string' && error.pointer.startsWith(`/${definition}/`)
6
+ && error.pointer.length <= 1024 && /^\/[A-Za-z0-9_~/-]+$/.test(error.pointer)
7
+ ? error.pointer : `/${definition}`;
8
+ return [code, { code, pointer, message: 'Worker contract validation failed at this field.' }];
9
+ }
@@ -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
 
@@ -1901,7 +1901,7 @@ export async function registerWorkerInvokeToolset(server, options = {}) {
1901
1901
  // diagnostic below. A duplicate-key/planted-marker probe showed
1902
1902
  // this exact kind of path can otherwise reflect attacker/worker-
1903
1903
  // 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'] });
1904
+ return markTerminal({ status: 'blocked', termination: 'confirmed', diagnostics: workerFailureDiagnostics(error, 'workerDirectBrief', 'OG_BRIEF_NOT_STRUCTURED') });
1905
1905
  }
1906
1906
  mcpBriefDigest = `sha256:${createHash('sha256').update(briefBytes).digest('hex')}`;
1907
1907
  }
@@ -2022,7 +2022,7 @@ export async function registerWorkerInvokeToolset(server, options = {}) {
2022
2022
  // parser error message, which can echo attacker/worker-controlled
2023
2023
  // input text back out (a duplicate-key probe reflected a planted
2024
2024
  // 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'] });
2025
+ return registry.markTerminal(entry.invocationId, { status: 'failed', termination: 'confirmed', diagnostics: workerFailureDiagnostics(error, 'workerClaims', 'OG_WORKER_FINAL_INVALID') });
2026
2026
  }
2027
2027
  const assembled = await tryAssembleDirectResult({ request, invocation, observation, claims, root,
2028
2028
  briefDigest: mcpBriefDigest, requireStructuredBrief: invocation.transport === 'mcp' });
@@ -2228,7 +2228,7 @@ export async function registerWorkerInvokeToolset(server, options = {}) {
2228
2228
  // #2915 review 6 (finding 4): zero workers dispatched — same
2229
2229
  // proven-never-dispatched settlement as above.
2230
2230
  settleRecovery({ kind: 'uncertain', confirmedStop: true });
2231
- return markTerminal({ status: 'blocked', termination: 'confirmed', diagnostics: [typeof error?.code === 'string' ? error.code : 'OG_BRIEF_NOT_STRUCTURED'] });
2231
+ return markTerminal({ status: 'blocked', termination: 'confirmed', diagnostics: workerFailureDiagnostics(error, 'workerDirectBrief', 'OG_BRIEF_NOT_STRUCTURED') });
2232
2232
  }
2233
2233
  mcpBriefDigest = `sha256:${createHash('sha256').update(briefBytes).digest('hex')}`;
2234
2234
  }
@@ -2590,7 +2590,7 @@ export async function registerWorkerInvokeToolset(server, options = {}) {
2590
2590
  // parser error message, which can echo attacker/worker-controlled
2591
2591
  // input text back out (a duplicate-key probe reflected a planted
2592
2592
  // 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'] });
2593
+ return registry.markTerminal(entry.invocationId, { status: 'failed', termination: 'confirmed', diagnostics: workerFailureDiagnostics(error, 'workerClaims', 'OG_WORKER_FINAL_INVALID') });
2594
2594
  }
2595
2595
  const assembled = await tryAssembleDirectResult({ request, invocation, observation, claims, root,
2596
2596
  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('; '));
@@ -0,0 +1,27 @@
1
+ // Validates and serializes writes through the existing models.json owner.
2
+ import { promises as fs } from 'node:fs';
3
+ import path from 'node:path';
4
+ import { validateUserProjectModelConfig } from './provider-policy.js';
5
+ import { atomicConfigWrite as atomicWrite, withWorkerWriteLock } from '../config/worker-write.js';
6
+ export async function updateJson(file, mutate, dryRun) {
7
+ const update = async () => {
8
+ let value = {};
9
+ try {
10
+ value = JSON.parse(await fs.readFile(file, 'utf8'));
11
+ }
12
+ catch (error) {
13
+ if (error?.code !== 'ENOENT')
14
+ throw error;
15
+ }
16
+ mutate(value);
17
+ const validation = validateUserProjectModelConfig(value);
18
+ if (!validation.valid)
19
+ throw new Error(validation.diagnostics.map(item => item.message).join('; '));
20
+ const content = `${JSON.stringify(value, null, 2)}\n`;
21
+ if (!dryRun)
22
+ await atomicWrite(file, content);
23
+ return { file, beforeWrite: value, dryRun };
24
+ };
25
+ return dryRun ? update() : withWorkerWriteLock(path.dirname(file), update);
26
+ }
27
+ //# sourceMappingURL=config-writer.js.map