@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.
- package/agentic/code/providers/model-capabilities.v1.json +197 -3
- package/bin/aiwg.mjs +8 -0
- package/dist/src/artifacts/cli.js +9 -5
- package/dist/src/auth/config.js +3 -8
- package/dist/src/auth/resource-credentials.js +35 -2
- package/dist/src/cli/handlers/diagnose.js +13 -0
- package/dist/src/cli/handlers/mc-admission.js +39 -0
- package/dist/src/cli/handlers/mc.js +2 -31
- package/dist/src/cli/handlers/models-help.js +52 -0
- package/dist/src/cli/handlers/models.js +11 -81
- package/dist/src/cli/handlers/resource-versions.js +8 -5
- package/dist/src/cli/handlers/script-runner.js +13 -1
- package/dist/src/cli/handlers/use-addon-profile.js +72 -0
- package/dist/src/cli/handlers/use.js +209 -233
- package/dist/src/cli/ui.js +1 -0
- package/dist/src/config/aiwg-config.js +38 -184
- package/dist/src/config/cli-help.js +54 -0
- package/dist/src/config/cli.js +5 -69
- package/dist/src/config/config-normalization.js +10 -0
- package/dist/src/config/worker-activation-gate.js +26 -0
- package/dist/src/config/worker-adoption-cli.js +102 -0
- package/dist/src/config/worker-adoption.js +368 -0
- package/dist/src/config/worker-config-digest.js +14 -0
- package/dist/src/config/worker-group-templates.js +150 -0
- package/dist/src/config/worker-guidance-warning.js +28 -0
- package/dist/src/config/worker-owner-lock.js +152 -0
- package/dist/src/config/worker-parallelism.js +154 -0
- package/dist/src/config/worker-write.js +75 -0
- package/dist/src/decision/telemetry/exporter.js +2 -1
- package/dist/src/decision/telemetry/index.js +1 -0
- package/dist/src/decision/telemetry/mapping.js +43 -0
- package/dist/src/decision/telemetry/otlp-http.js +5 -2
- package/dist/src/decision/telemetry/redaction.js +6 -1
- package/dist/src/decision/telemetry/types.js +3 -0
- package/dist/src/decision/telemetry/worker.js +69 -0
- package/dist/src/lint/rule-activation.js +15 -7
- package/dist/src/lint/runner.js +2 -2
- package/dist/src/mcp/server.mjs +2 -11
- package/dist/src/mcp/stdio-lifecycle.mjs +27 -0
- package/dist/src/mcp/tools/worker-failure-diagnostics.mjs +9 -0
- package/dist/src/mcp/tools/worker-invoke.mjs +6 -6
- package/dist/src/mission-protocol/codecs.js +8 -38
- package/dist/src/mission-protocol/worker-contract-diagnostics.js +62 -0
- package/dist/src/mission-protocol/worker-invocation-types.js +2 -0
- package/dist/src/models/conductor-data-policy.d.mts +8 -0
- package/dist/src/models/conductor-data-policy.mjs +190 -0
- package/dist/src/models/config-loader.js +19 -3
- package/dist/src/models/config-writer.js +27 -0
- package/dist/src/models/model-capabilities.v1.json +197 -3
- package/dist/src/models/provider-policy.js +7 -155
- package/dist/src/models/resolver.js +2 -293
- package/dist/src/models/worker-artifact-binding.mjs +12 -0
- package/dist/src/models/worker-direct-readiness-policy.js +155 -0
- package/dist/src/models/worker-direct-readiness.js +9 -218
- package/dist/src/models/worker-health-readiness.js +105 -26
- package/dist/src/models/worker-installation.mjs +12 -14
- package/dist/src/models/worker-member-references.js +64 -0
- package/dist/src/models/worker-plan.js +317 -0
- package/dist/src/models/worker-selection-policy.js +217 -0
- package/dist/src/models/wrapper-deployment.js +1 -1
- package/dist/src/resources/web-release.js +76 -65
- package/dist/src/serve/shared-host-admission-checks.js +116 -0
- package/dist/src/serve/shared-host-scheduler.js +25 -107
- package/dist/src/serve/shared-host-starvation.js +73 -0
- package/dist/src/serve/worker-artifacts.d.mts +4 -0
- package/dist/src/serve/worker-artifacts.mjs +56 -0
- package/dist/src/serve/worker-command-request.js +68 -0
- package/dist/src/serve/worker-invocation-paths.js +144 -0
- package/dist/src/serve/worker-invocation-preparation.js +169 -0
- package/dist/src/serve/worker-invocation.js +24 -291
- package/dist/src/serve/worker-stdin-frame.d.mts +2 -2
- package/dist/src/serve/worker-stdin-frame.mjs +6 -6
- package/dist/src/smiths/context-pipeline/parallelism-section.js +19 -7
- package/dist/src/storage/backends/fortemi.js +33 -29
- package/dist/src/storage/backends/logseq.js +16 -3
- package/dist/src/storage/backends/postgres.js +74 -33
- package/dist/src/storage/backends/postgrest.js +37 -13
- package/dist/src/storage/config.js +6 -1
- package/dist/src/uhp/config.js +4 -0
- package/package.json +9 -1
- package/schemas/mission-v1.schema.json +20 -19
- package/schemas/models/model-config-compatibility.v1.schema.json +16 -0
- package/tools/agents/providers/claude-post-deploy.mjs +35 -0
- package/tools/agents/providers/claude.mjs +3 -30
- package/tools/agents/providers/codex-post-deploy.mjs +13 -0
- package/tools/agents/providers/codex.mjs +2 -8
- package/tools/agents/providers/conductor-data-guard.mjs +61 -0
- package/tools/agents/providers/rule-delivery.mjs +3 -16
- package/tools/agents/providers/rule-presentation.mjs +38 -0
- package/tools/plugin/package-plugins.mjs +1 -1
- package/tools/ralph-external/lib/claude-adapter.mjs +2 -2
- package/tools/ralph-external/lib/codex-adapter.mjs +3 -3
- package/tools/ralph-external/lib/managed-worker-attempt.mjs +86 -0
- package/tools/ralph-external/lib/provider-adapter.mjs +11 -260
- package/tools/ralph-external/lib/session-attempt.mjs +85 -0
- package/tools/ralph-external/lib/strict-worker-json.mjs +190 -0
- package/tools/ralph-external/lib/worker-artifacts.mjs +209 -0
- package/tools/ralph-external/lib/worker-final.mjs +143 -0
- package/tools/ralph-external/lib/worker-inputs.mjs +55 -0
- package/tools/ralph-external/lib/worker-isolation.mjs +2 -44
- package/tools/ralph-external/lib/worker-launch-cli.mjs +47 -0
- package/tools/ralph-external/lib/worker-native-sandbox.mjs +83 -0
- package/tools/ralph-external/session-launcher.mjs +73 -217
- package/vscode-extension/schemas/aiwg.config.v1.json +161 -2
package/dist/src/mcp/server.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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,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
|