@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.
- package/agentic/code/providers/model-capabilities.v1.json +275 -17
- package/bin/aiwg.mjs +8 -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/sessions.js +198 -15
- package/dist/src/cli/handlers/use-addon-profile.js +72 -0
- package/dist/src/cli/handlers/use.js +203 -234
- 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-context.js +337 -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 +114 -16
- 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 +275 -17
- package/dist/src/models/provider-policy.js +14 -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/policy/authorization.js +29 -1
- package/dist/src/process/aiwg-probe-command.d.mts +15 -0
- package/dist/src/process/aiwg-probe-command.mjs +37 -0
- 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/sessions/adapters/grokbot.js +7 -0
- package/dist/src/sessions/batch-import.js +34 -6
- package/dist/src/sessions/contracts.js +19 -0
- package/dist/src/sessions/coverage-preflight.js +7 -1
- package/dist/src/sessions/fortemi-export-mapping.js +33 -3
- package/dist/src/sessions/host-catalog.js +157 -0
- package/dist/src/sessions/image-blobs.js +141 -0
- package/dist/src/sessions/importer.js +23 -3
- package/dist/src/sessions/index.js +2 -0
- package/dist/src/sessions/refresh.js +25 -2
- package/dist/src/sessions/repository.js +164 -22
- package/dist/src/sessions/workspace-discovery.js +5 -2
- package/dist/src/smiths/context-pipeline/parallelism-section.js +19 -7
- package/package.json +15 -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/muse-adapter.mjs +10 -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-context.mjs +237 -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-invocation-ancestry.mjs +132 -0
- package/tools/ralph-external/lib/worker-invocation-processes.mjs +123 -0
- package/tools/ralph-external/lib/worker-isolation-status.mjs +48 -0
- package/tools/ralph-external/lib/worker-isolation.mjs +19 -73
- package/tools/ralph-external/lib/worker-launch-cli.mjs +49 -0
- package/tools/ralph-external/lib/worker-native-sandbox.mjs +83 -0
- package/tools/ralph-external/lib/worker-overflow.mjs +48 -13
- package/tools/ralph-external/lib/worker-retry-evidence.mjs +103 -0
- package/tools/ralph-external/lib/worker-termination.mjs +82 -0
- package/tools/ralph-external/session-launcher.mjs +124 -313
- 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
|
|
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.
|
|
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.
|
|
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
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
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.
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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('; '));
|