@aiwg/cli 2026.10.9 → 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/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/use-addon-profile.js +72 -0
- package/dist/src/cli/handlers/use.js +191 -230
- 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/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/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
|
@@ -1,220 +1,14 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Provider Adapter Base Class for External Ralph Loop
|
|
3
|
-
*
|
|
4
|
-
* Defines the interface that all CLI provider adapters must implement.
|
|
5
|
-
* Each adapter wraps a specific agentic coding CLI (claude, codex, etc.)
|
|
6
|
-
* and handles binary resolution, argument construction, model mapping,
|
|
7
|
-
* and output parsing.
|
|
8
|
-
*
|
|
9
|
-
* @implements Plan: Multi-Provider Support for External Ralph Loop
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
1
|
import { spawn } from 'child_process';
|
|
13
2
|
import { importImpl } from '../../_resolve-impl.mjs';
|
|
14
3
|
import { createWorkerHealthObserver, observeProviderHealthEvent } from './worker-health.mjs';
|
|
15
4
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
* map a scan failure to a specific contract-violation message without
|
|
19
|
-
* string-matching free-text.
|
|
20
|
-
*/
|
|
21
|
-
class StrictJsonScanError extends Error {
|
|
22
|
-
constructor(code, message) {
|
|
23
|
-
super(message);
|
|
24
|
-
this.code = code;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* RFC 8259 JSON parser used on every strict worker-final path (JSONL lines,
|
|
30
|
-
* `format: 'json'`, and the MCP `content[0].text`/top-level envelope) instead
|
|
31
|
-
* of `JSON.parse`. `JSON.parse` silently keeps the LAST value when an object
|
|
32
|
-
* repeats a member name — RFC 8259 §4 explicitly leaves this to the
|
|
33
|
-
* implementation, and V8 picks "last wins". That lets a forged worker final
|
|
34
|
-
* smuggle a second, hidden value past a first one: a trailing duplicate
|
|
35
|
-
* `"status":"completed"` can overwrite an earlier `"status":"failed"`, or a
|
|
36
|
-
* trailing duplicate `"type":"result"` can collapse a `"type":"message"`
|
|
37
|
-
* framing envelope into a bogus result in one line (#2890 BLOCKER 1+2).
|
|
38
|
-
*
|
|
39
|
-
* This scanner tracks member names per object independently at every
|
|
40
|
-
* nesting level (a sibling object may reuse a name the scanner already saw
|
|
41
|
-
* in its parent or an earlier sibling) and rejects any object that repeats
|
|
42
|
-
* one. Names are compared only after the scanner has fully decoded them —
|
|
43
|
-
* resolving `\"`, other single-character escapes, and `\uXXXX` escapes
|
|
44
|
-
* (including surrogate pairs, since JS string concatenation of two decoded
|
|
45
|
-
* UTF-16 code units forms the pair automatically) — so an escape-obfuscated
|
|
46
|
-
* collision (`"status"` vs `"status"`) is caught exactly like a literal
|
|
47
|
-
* one. A non-colliding unknown key, escaped or not, is left for the existing
|
|
48
|
-
* `additionalProperties: false` schema check to reject, unchanged.
|
|
49
|
-
*
|
|
50
|
-
* Nesting depth is bounded during the scan (not only afterward, by
|
|
51
|
-
* `boundedJson()`'s post-parse walk) so pathological depth fails fast with a
|
|
52
|
-
* clear `DEPTH` code instead of relying on a recursive-descent stack
|
|
53
|
-
* overflow to be caught as a generic syntax error.
|
|
54
|
-
*
|
|
55
|
-
* @param {string} text - Decoded JSON text (already UTF-8 validated by the caller)
|
|
56
|
-
* @param {number} maxDepth - Same bound as `WORKER_MAX_NESTING_DEPTH`; root value is depth 1
|
|
57
|
-
* @returns {unknown}
|
|
58
|
-
*/
|
|
59
|
-
function parseStrictWorkerJson(text, maxDepth) {
|
|
60
|
-
const length = text.length;
|
|
61
|
-
let index = 0;
|
|
62
|
-
|
|
63
|
-
function fail(code, message) {
|
|
64
|
-
throw new StrictJsonScanError(code, `${message} (offset ${index})`);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function skipWhitespace() {
|
|
68
|
-
while (index < length) {
|
|
69
|
-
const code = text.charCodeAt(index);
|
|
70
|
-
if (code === 0x20 || code === 0x09 || code === 0x0a || code === 0x0d) index += 1;
|
|
71
|
-
else break;
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function parseValue(depth) {
|
|
76
|
-
skipWhitespace();
|
|
77
|
-
if (index >= length) fail('SYNTAX', 'Unexpected end of JSON input');
|
|
78
|
-
const code = text.charCodeAt(index);
|
|
79
|
-
if (code === 0x7b) return parseObject(depth);
|
|
80
|
-
if (code === 0x5b) return parseArray(depth);
|
|
81
|
-
if (code === 0x22) return parseString();
|
|
82
|
-
if (code === 0x2d || (code >= 0x30 && code <= 0x39)) return parseNumber();
|
|
83
|
-
if (text.startsWith('true', index)) { index += 4; return true; }
|
|
84
|
-
if (text.startsWith('false', index)) { index += 5; return false; }
|
|
85
|
-
if (text.startsWith('null', index)) { index += 4; return null; }
|
|
86
|
-
fail('SYNTAX', 'Unexpected token');
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function parseObject(depth) {
|
|
90
|
-
if (depth > maxDepth) fail('DEPTH', 'Worker nesting depth exceeds bound');
|
|
91
|
-
index += 1; // {
|
|
92
|
-
const result = {};
|
|
93
|
-
const seen = new Set();
|
|
94
|
-
skipWhitespace();
|
|
95
|
-
if (text.charCodeAt(index) === 0x7d) { index += 1; return result; }
|
|
96
|
-
for (;;) {
|
|
97
|
-
skipWhitespace();
|
|
98
|
-
if (text.charCodeAt(index) !== 0x22) fail('SYNTAX', 'Expected object member name');
|
|
99
|
-
const key = parseString();
|
|
100
|
-
if (seen.has(key)) fail('DUPLICATE', `Duplicate object member name ${JSON.stringify(key)}`);
|
|
101
|
-
seen.add(key);
|
|
102
|
-
skipWhitespace();
|
|
103
|
-
if (text.charCodeAt(index) !== 0x3a) fail('SYNTAX', 'Expected colon after object member name');
|
|
104
|
-
index += 1;
|
|
105
|
-
// Define, never assign: assignment would invoke the inherited __proto__
|
|
106
|
-
// setter, hiding the member from own-key validation and diverging from
|
|
107
|
-
// JSON.parse, which creates an ordinary own property.
|
|
108
|
-
Object.defineProperty(result, key, { value: parseValue(depth + 1), enumerable: true, writable: true, configurable: true });
|
|
109
|
-
skipWhitespace();
|
|
110
|
-
const delimiter = text.charCodeAt(index);
|
|
111
|
-
if (delimiter === 0x2c) { index += 1; continue; }
|
|
112
|
-
if (delimiter === 0x7d) { index += 1; break; }
|
|
113
|
-
fail('SYNTAX', 'Expected , or } in object');
|
|
114
|
-
}
|
|
115
|
-
return result;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function parseArray(depth) {
|
|
119
|
-
if (depth > maxDepth) fail('DEPTH', 'Worker nesting depth exceeds bound');
|
|
120
|
-
index += 1; // [
|
|
121
|
-
const result = [];
|
|
122
|
-
skipWhitespace();
|
|
123
|
-
if (text.charCodeAt(index) === 0x5d) { index += 1; return result; }
|
|
124
|
-
for (;;) {
|
|
125
|
-
result.push(parseValue(depth + 1));
|
|
126
|
-
skipWhitespace();
|
|
127
|
-
const delimiter = text.charCodeAt(index);
|
|
128
|
-
if (delimiter === 0x2c) { index += 1; continue; }
|
|
129
|
-
if (delimiter === 0x5d) { index += 1; break; }
|
|
130
|
-
fail('SYNTAX', 'Expected , or ] in array');
|
|
131
|
-
}
|
|
132
|
-
return result;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
function parseString() {
|
|
136
|
-
index += 1; // opening quote
|
|
137
|
-
let value = '';
|
|
138
|
-
for (;;) {
|
|
139
|
-
if (index >= length) fail('SYNTAX', 'Unterminated string');
|
|
140
|
-
const code = text.charCodeAt(index);
|
|
141
|
-
if (code === 0x22) { index += 1; return value; }
|
|
142
|
-
if (code === 0x5c) {
|
|
143
|
-
index += 1;
|
|
144
|
-
const escape = text[index];
|
|
145
|
-
if (escape === undefined) fail('SYNTAX', 'Unterminated escape sequence');
|
|
146
|
-
switch (escape) {
|
|
147
|
-
case '"': value += '"'; index += 1; break;
|
|
148
|
-
case '\\': value += '\\'; index += 1; break;
|
|
149
|
-
case '/': value += '/'; index += 1; break;
|
|
150
|
-
case 'b': value += '\b'; index += 1; break;
|
|
151
|
-
case 'f': value += '\f'; index += 1; break;
|
|
152
|
-
case 'n': value += '\n'; index += 1; break;
|
|
153
|
-
case 'r': value += '\r'; index += 1; break;
|
|
154
|
-
case 't': value += '\t'; index += 1; break;
|
|
155
|
-
case 'u': {
|
|
156
|
-
const hex = text.slice(index + 1, index + 5);
|
|
157
|
-
if (!/^[0-9a-fA-F]{4}$/.test(hex)) fail('SYNTAX', 'Invalid unicode escape');
|
|
158
|
-
// Decoded code unit is appended as-is; two consecutive decoded
|
|
159
|
-
// surrogate halves recombine into one character automatically
|
|
160
|
-
// through normal JS UTF-16 string concatenation.
|
|
161
|
-
value += String.fromCharCode(parseInt(hex, 16));
|
|
162
|
-
index += 5;
|
|
163
|
-
break;
|
|
164
|
-
}
|
|
165
|
-
default: fail('SYNTAX', 'Invalid escape sequence');
|
|
166
|
-
}
|
|
167
|
-
} else if (code < 0x20) {
|
|
168
|
-
fail('SYNTAX', 'Unescaped control character in string');
|
|
169
|
-
} else {
|
|
170
|
-
value += text[index];
|
|
171
|
-
index += 1;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function parseNumber() {
|
|
177
|
-
const start = index;
|
|
178
|
-
if (text.charCodeAt(index) === 0x2d) index += 1;
|
|
179
|
-
if (text.charCodeAt(index) === 0x30) {
|
|
180
|
-
index += 1;
|
|
181
|
-
} else if (text.charCodeAt(index) >= 0x31 && text.charCodeAt(index) <= 0x39) {
|
|
182
|
-
index += 1;
|
|
183
|
-
while (text.charCodeAt(index) >= 0x30 && text.charCodeAt(index) <= 0x39) index += 1;
|
|
184
|
-
} else fail('SYNTAX', 'Invalid number');
|
|
185
|
-
if (text.charCodeAt(index) === 0x2e) {
|
|
186
|
-
index += 1;
|
|
187
|
-
if (!(text.charCodeAt(index) >= 0x30 && text.charCodeAt(index) <= 0x39)) fail('SYNTAX', 'Invalid number fraction');
|
|
188
|
-
while (text.charCodeAt(index) >= 0x30 && text.charCodeAt(index) <= 0x39) index += 1;
|
|
189
|
-
}
|
|
190
|
-
if (text.charCodeAt(index) === 0x65 || text.charCodeAt(index) === 0x45) {
|
|
191
|
-
index += 1;
|
|
192
|
-
if (text.charCodeAt(index) === 0x2b || text.charCodeAt(index) === 0x2d) index += 1;
|
|
193
|
-
if (!(text.charCodeAt(index) >= 0x30 && text.charCodeAt(index) <= 0x39)) fail('SYNTAX', 'Invalid number exponent');
|
|
194
|
-
while (text.charCodeAt(index) >= 0x30 && text.charCodeAt(index) <= 0x39) index += 1;
|
|
195
|
-
}
|
|
196
|
-
return Number(text.slice(start, index));
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const result = parseValue(1);
|
|
200
|
-
skipWhitespace();
|
|
201
|
-
if (index !== length) fail('SYNTAX', 'Unexpected trailing characters after JSON value');
|
|
202
|
-
return result;
|
|
203
|
-
}
|
|
5
|
+
import { parseStrictWorkerJson, StrictJsonScanError } from './strict-worker-json.mjs';
|
|
6
|
+
import { classifySessionAttempt } from './session-attempt.mjs';
|
|
204
7
|
|
|
205
8
|
const installedOwner = await importImpl(import.meta.url, 'models/worker-installation.mjs');
|
|
206
9
|
export const { workerNegativeFixtureSuite, workerNegativeFixtureDigest, registerProvider, createProvider, listProviders, hasProvider } = installedOwner;
|
|
207
10
|
|
|
208
|
-
/**
|
|
209
|
-
* Whether the optional muse exec Ralph adapter is registered.
|
|
210
|
-
* Enabled by default; set `AIWG_MUSE_RALPH_ENABLED` to `0`, `false`, `no`,
|
|
211
|
-
* or `off` to disable the adapter without affecting
|
|
212
|
-
* `aiwg use --provider muse` (the deploy/writer path lives in `src/` and
|
|
213
|
-
* never consults this registry). See
|
|
214
|
-
* `tools/ralph-external/lib/muse-adapter.mjs` (#230).
|
|
215
|
-
*
|
|
216
|
-
* @returns {boolean}
|
|
217
|
-
*/
|
|
11
|
+
/** The optional Muse adapter is independent of provider deployment. */
|
|
218
12
|
export function isMuseRalphEnabled() {
|
|
219
13
|
const raw = String(process.env.AIWG_MUSE_RALPH_ENABLED ?? '').trim().toLowerCase();
|
|
220
14
|
return !['0', 'false', 'no', 'off'].includes(raw);
|
|
@@ -661,24 +455,15 @@ export class ProviderAdapter {
|
|
|
661
455
|
return null;
|
|
662
456
|
}
|
|
663
457
|
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
* @param {string} [context] - Context for the warning message
|
|
669
|
-
* @returns {boolean}
|
|
670
|
-
*/
|
|
458
|
+
classifySessionAttempt(request) {
|
|
459
|
+
return classifySessionAttempt(this, request);
|
|
460
|
+
}
|
|
461
|
+
|
|
671
462
|
hasCapability(capability) {
|
|
672
463
|
const caps = this.getCapabilities();
|
|
673
464
|
return !!caps[capability];
|
|
674
465
|
}
|
|
675
466
|
|
|
676
|
-
/**
|
|
677
|
-
* Log a capability warning when a feature is not supported.
|
|
678
|
-
*
|
|
679
|
-
* @param {string} capability - Capability name
|
|
680
|
-
* @param {string} feature - Human-readable feature description
|
|
681
|
-
*/
|
|
682
467
|
warnUnsupported(capability, feature) {
|
|
683
468
|
if (!this.hasCapability(capability)) {
|
|
684
469
|
console.warn(
|
|
@@ -687,52 +472,18 @@ export class ProviderAdapter {
|
|
|
687
472
|
}
|
|
688
473
|
}
|
|
689
474
|
}
|
|
690
|
-
|
|
691
|
-
// ============================================================================
|
|
692
|
-
// Provider Registry & Factory
|
|
693
|
-
// ============================================================================
|
|
694
|
-
|
|
695
475
|
export async function loadInstalledWorkerAdapters(request) {
|
|
696
476
|
await ensureProvidersRegistered();
|
|
697
477
|
return installedOwner.loadInstalledWorkerAdapters(request);
|
|
698
478
|
}
|
|
699
479
|
|
|
700
|
-
//
|
|
701
|
-
// Auto-register built-in providers on import
|
|
702
|
-
// ============================================================================
|
|
703
|
-
|
|
704
|
-
// Lazy imports to avoid circular dependencies - adapters register themselves
|
|
705
|
-
// when their modules are loaded. We trigger that here.
|
|
480
|
+
// Adapters register themselves; keep imports lazy to avoid registry cycles.
|
|
706
481
|
async function registerBuiltinProviders() {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
try {
|
|
711
|
-
await import('./codex-adapter.mjs');
|
|
712
|
-
} catch { /* ignore if not found */ }
|
|
713
|
-
try {
|
|
714
|
-
await import('./opencode-adapter.mjs');
|
|
715
|
-
} catch { /* ignore if not found */ }
|
|
716
|
-
try {
|
|
717
|
-
await import('./factory-adapter.mjs');
|
|
718
|
-
} catch { /* ignore if not found */ }
|
|
719
|
-
try {
|
|
720
|
-
await import('./pi-adapter.mjs');
|
|
721
|
-
await import('./omp-adapter.mjs');
|
|
722
|
-
} catch { /* ignore if not found */ }
|
|
723
|
-
try {
|
|
724
|
-
await import('./deepseek-harness-adapter.mjs');
|
|
725
|
-
} catch { /* ignore if not found */ }
|
|
726
|
-
// #230: optional / post-experimental. Disabled via AIWG_MUSE_RALPH_ENABLED=0
|
|
727
|
-
// without affecting `aiwg use --provider muse`.
|
|
728
|
-
if (isMuseRalphEnabled()) {
|
|
729
|
-
try {
|
|
730
|
-
await import('./muse-adapter.mjs');
|
|
731
|
-
} catch { /* ignore if not found */ }
|
|
482
|
+
for (const name of ['claude', 'codex', 'opencode', 'factory', 'pi', 'omp', 'deepseek-harness',
|
|
483
|
+
...(isMuseRalphEnabled() ? ['muse'] : [])]) {
|
|
484
|
+
try { await import(`./${name}-adapter.mjs`); } catch { /* Optional adapter unavailable. */ }
|
|
732
485
|
}
|
|
733
486
|
}
|
|
734
|
-
|
|
735
|
-
// Run registration — store promise so callers can await it
|
|
736
487
|
const _registrationPromise = registerBuiltinProviders();
|
|
737
488
|
|
|
738
489
|
/**
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// Classify native continuation and retain the original execution assumptions.
|
|
2
|
+
import { randomUUID } from 'node:crypto';
|
|
3
|
+
import { writeFileSync } from 'node:fs';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import { importImpl } from '../../_resolve-impl.mjs';
|
|
6
|
+
|
|
7
|
+
const { qualificationDigest } = await importImpl(import.meta.url, 'models/qualification-digest.mjs');
|
|
8
|
+
const validSession = value => typeof value === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9._:/-]{0,255}$/.test(value);
|
|
9
|
+
|
|
10
|
+
export function changedBindings(original, current) {
|
|
11
|
+
if (!original) return [];
|
|
12
|
+
return [...new Set([...Object.keys(original), ...Object.keys(current)])].sort()
|
|
13
|
+
.filter(key => qualificationDigest(original[key] ?? null) !== qualificationDigest(current[key] ?? null))
|
|
14
|
+
.map(key => ({ binding: key, previous: original[key] ?? null, current: current[key] ?? null }));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function classifySessionAttempt(adapter, { sessionId, original, current, sessionValid = true } = {}) {
|
|
18
|
+
const changes = changedBindings(original, current ?? {});
|
|
19
|
+
const requested = sessionId !== null && sessionId !== undefined;
|
|
20
|
+
const reason = !requested ? 'no-session' : !adapter.hasCapability('sessionResume') ? 'resume-unsupported'
|
|
21
|
+
: !validSession(sessionId) || !sessionValid || !original ? 'invalid-session'
|
|
22
|
+
: changes.length ? 'bindings-changed' : 'session-bound';
|
|
23
|
+
return { kind: reason === 'session-bound' ? 'continuation' : 'fresh', reason,
|
|
24
|
+
requestedSessionId: validSession(sessionId) ? sessionId : null,
|
|
25
|
+
changes, originalBindings: original ?? null, bindings: current ?? {} };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function persistCycleAttempt(owner, state, outputDir, attempt) {
|
|
29
|
+
state.currentAttempt = attempt;
|
|
30
|
+
owner.stateManager.save(state);
|
|
31
|
+
writeFileSync(join(outputDir, 'session-attempt.json'), JSON.stringify(attempt, null, 2), { mode: 0o600 });
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function prepareNativeAttempt(owner, state, options) {
|
|
35
|
+
const adapter = owner.providerAdapter;
|
|
36
|
+
// Resolve again even when a requested continuation cannot be used.
|
|
37
|
+
const model = adapter.mapModel(options.model);
|
|
38
|
+
const prior = state.sessionBinding;
|
|
39
|
+
const sessionId = prior?.sessionId ?? (state.currentIteration > 1 ? state.sessionId : null);
|
|
40
|
+
const bindings = { provider: adapter.getName(), model, sessionId,
|
|
41
|
+
policyDigest: qualificationDigest({ budget: options.budget, maxTurns: options.maxTurns,
|
|
42
|
+
mcpConfig: options.mcpConfig, tools: options.tools, thinking: options.thinking,
|
|
43
|
+
workingDir: options.workingDir }) };
|
|
44
|
+
const attempt = { attemptId: randomUUID(), previousAttemptId: state.currentAttempt?.attemptId ?? null,
|
|
45
|
+
...adapter.classifySessionAttempt({ sessionId, original: prior?.originalBindings,
|
|
46
|
+
current: bindings, sessionValid: prior?.valid === true }), status: 'prepared' };
|
|
47
|
+
// Only Claude/Muse accept a caller-created session id. Other CLIs must
|
|
48
|
+
// report a native id before it can be used for continuation.
|
|
49
|
+
const nativeId = attempt.kind === 'continuation' ? sessionId
|
|
50
|
+
: ['claude', 'muse'].includes(adapter.getName()) ? randomUUID() : null;
|
|
51
|
+
attempt.bindings.sessionId = nativeId;
|
|
52
|
+
persistCycleAttempt(owner, state, options.outputDir, attempt);
|
|
53
|
+
return { ...options, model, sessionId: nativeId, resumeSession: attempt.kind === 'continuation' ? nativeId : null,
|
|
54
|
+
attempt };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function finishNativeAttempt(owner, state, options, result) {
|
|
58
|
+
let sessionId = options.sessionId;
|
|
59
|
+
let reportedId = null;
|
|
60
|
+
let conflictingSession = false;
|
|
61
|
+
let providerFailed = false;
|
|
62
|
+
// Session ids are accepted only from this attempt's captured provider output.
|
|
63
|
+
for (const line of String(result.stdoutBuffer ?? '').split('\n')) {
|
|
64
|
+
try {
|
|
65
|
+
const event = JSON.parse(line);
|
|
66
|
+
const observed = owner.providerAdapter.observeHealthEvent(event, { kind: 'stdout' }, {});
|
|
67
|
+
providerFailed ||= observed?.kind === 'error' || observed?.kind === 'terminal' && observed.success === false;
|
|
68
|
+
const candidate = observed?.sessionId ?? event.session_id ?? event.sessionId ?? event.stream?.id;
|
|
69
|
+
if (validSession(candidate)) {
|
|
70
|
+
conflictingSession ||= reportedId !== null && reportedId !== candidate
|
|
71
|
+
|| options.resumeSession != null && options.resumeSession !== candidate;
|
|
72
|
+
reportedId = candidate;
|
|
73
|
+
sessionId = candidate;
|
|
74
|
+
}
|
|
75
|
+
} catch { /* Non-JSON stdout has no native session binding. */ }
|
|
76
|
+
}
|
|
77
|
+
const bindings = { ...options.attempt.bindings, sessionId };
|
|
78
|
+
const valid = result.exitCode === 0 && !result.timedOut && !providerFailed && !conflictingSession && validSession(sessionId);
|
|
79
|
+
state.sessionBinding = { sessionId, valid,
|
|
80
|
+
originalBindings: options.attempt.kind === 'continuation' ? options.attempt.originalBindings : bindings };
|
|
81
|
+
options.attempt.status = 'finished';
|
|
82
|
+
options.attempt.bindings = bindings;
|
|
83
|
+
persistCycleAttempt(owner, state, options.outputDir, options.attempt);
|
|
84
|
+
result.attempt = options.attempt;
|
|
85
|
+
}
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
// Parse bounded worker JSON without accepting duplicate object keys.
|
|
2
|
+
/**
|
|
3
|
+
* Thrown by {@link parseStrictWorkerJson} with a stable `code` so callers can
|
|
4
|
+
* map a scan failure to a specific contract-violation message without
|
|
5
|
+
* string-matching free-text.
|
|
6
|
+
*/
|
|
7
|
+
export class StrictJsonScanError extends Error {
|
|
8
|
+
constructor(code, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.code = code;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* RFC 8259 JSON parser used on every strict worker-final path (JSONL lines,
|
|
16
|
+
* `format: 'json'`, and the MCP `content[0].text`/top-level envelope) instead
|
|
17
|
+
* of `JSON.parse`. `JSON.parse` silently keeps the LAST value when an object
|
|
18
|
+
* repeats a member name — RFC 8259 §4 explicitly leaves this to the
|
|
19
|
+
* implementation, and V8 picks "last wins". That lets a forged worker final
|
|
20
|
+
* smuggle a second, hidden value past a first one: a trailing duplicate
|
|
21
|
+
* `"status":"completed"` can overwrite an earlier `"status":"failed"`, or a
|
|
22
|
+
* trailing duplicate `"type":"result"` can collapse a `"type":"message"`
|
|
23
|
+
* framing envelope into a bogus result in one line (#2890 BLOCKER 1+2).
|
|
24
|
+
*
|
|
25
|
+
* This scanner tracks member names per object independently at every
|
|
26
|
+
* nesting level (a sibling object may reuse a name the scanner already saw
|
|
27
|
+
* in its parent or an earlier sibling) and rejects any object that repeats
|
|
28
|
+
* one. Names are compared only after the scanner has fully decoded them —
|
|
29
|
+
* resolving `\"`, other single-character escapes, and `\uXXXX` escapes
|
|
30
|
+
* (including surrogate pairs, since JS string concatenation of two decoded
|
|
31
|
+
* UTF-16 code units forms the pair automatically) — so an escape-obfuscated
|
|
32
|
+
* collision (`"status"` vs `"status"`) is caught exactly like a literal
|
|
33
|
+
* one. A non-colliding unknown key, escaped or not, is left for the existing
|
|
34
|
+
* `additionalProperties: false` schema check to reject, unchanged.
|
|
35
|
+
*
|
|
36
|
+
* Nesting depth is bounded during the scan (not only afterward, by
|
|
37
|
+
* `boundedJson()`'s post-parse walk) so pathological depth fails fast with a
|
|
38
|
+
* clear `DEPTH` code instead of relying on a recursive-descent stack
|
|
39
|
+
* overflow to be caught as a generic syntax error.
|
|
40
|
+
*
|
|
41
|
+
* @param {string} text - Decoded JSON text (already UTF-8 validated by the caller)
|
|
42
|
+
* @param {number} maxDepth - Same bound as `WORKER_MAX_NESTING_DEPTH`; root value is depth 1
|
|
43
|
+
* @returns {unknown}
|
|
44
|
+
*/
|
|
45
|
+
export function parseStrictWorkerJson(text, maxDepth) {
|
|
46
|
+
const length = text.length;
|
|
47
|
+
let index = 0;
|
|
48
|
+
|
|
49
|
+
function fail(code, message) {
|
|
50
|
+
throw new StrictJsonScanError(code, `${message} (offset ${index})`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function skipWhitespace() {
|
|
54
|
+
while (index < length) {
|
|
55
|
+
const code = text.charCodeAt(index);
|
|
56
|
+
if (code === 0x20 || code === 0x09 || code === 0x0a || code === 0x0d) index += 1;
|
|
57
|
+
else break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseValue(depth) {
|
|
62
|
+
skipWhitespace();
|
|
63
|
+
if (index >= length) fail('SYNTAX', 'Unexpected end of JSON input');
|
|
64
|
+
const code = text.charCodeAt(index);
|
|
65
|
+
if (code === 0x7b) return parseObject(depth);
|
|
66
|
+
if (code === 0x5b) return parseArray(depth);
|
|
67
|
+
if (code === 0x22) return parseString();
|
|
68
|
+
if (code === 0x2d || (code >= 0x30 && code <= 0x39)) return parseNumber();
|
|
69
|
+
if (text.startsWith('true', index)) { index += 4; return true; }
|
|
70
|
+
if (text.startsWith('false', index)) { index += 5; return false; }
|
|
71
|
+
if (text.startsWith('null', index)) { index += 4; return null; }
|
|
72
|
+
fail('SYNTAX', 'Unexpected token');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function parseObject(depth) {
|
|
76
|
+
if (depth > maxDepth) fail('DEPTH', 'Worker nesting depth exceeds bound');
|
|
77
|
+
index += 1; // {
|
|
78
|
+
const result = {};
|
|
79
|
+
const seen = new Set();
|
|
80
|
+
skipWhitespace();
|
|
81
|
+
if (text.charCodeAt(index) === 0x7d) { index += 1; return result; }
|
|
82
|
+
for (;;) {
|
|
83
|
+
skipWhitespace();
|
|
84
|
+
if (text.charCodeAt(index) !== 0x22) fail('SYNTAX', 'Expected object member name');
|
|
85
|
+
const key = parseString();
|
|
86
|
+
if (seen.has(key)) fail('DUPLICATE', `Duplicate object member name ${JSON.stringify(key)}`);
|
|
87
|
+
seen.add(key);
|
|
88
|
+
skipWhitespace();
|
|
89
|
+
if (text.charCodeAt(index) !== 0x3a) fail('SYNTAX', 'Expected colon after object member name');
|
|
90
|
+
index += 1;
|
|
91
|
+
// Define, never assign: assignment would invoke the inherited __proto__
|
|
92
|
+
// setter, hiding the member from own-key validation and diverging from
|
|
93
|
+
// JSON.parse, which creates an ordinary own property.
|
|
94
|
+
Object.defineProperty(result, key, { value: parseValue(depth + 1), enumerable: true, writable: true, configurable: true });
|
|
95
|
+
skipWhitespace();
|
|
96
|
+
const delimiter = text.charCodeAt(index);
|
|
97
|
+
if (delimiter === 0x2c) { index += 1; continue; }
|
|
98
|
+
if (delimiter === 0x7d) { index += 1; break; }
|
|
99
|
+
fail('SYNTAX', 'Expected , or } in object');
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function parseArray(depth) {
|
|
105
|
+
if (depth > maxDepth) fail('DEPTH', 'Worker nesting depth exceeds bound');
|
|
106
|
+
index += 1; // [
|
|
107
|
+
const result = [];
|
|
108
|
+
skipWhitespace();
|
|
109
|
+
if (text.charCodeAt(index) === 0x5d) { index += 1; return result; }
|
|
110
|
+
for (;;) {
|
|
111
|
+
result.push(parseValue(depth + 1));
|
|
112
|
+
skipWhitespace();
|
|
113
|
+
const delimiter = text.charCodeAt(index);
|
|
114
|
+
if (delimiter === 0x2c) { index += 1; continue; }
|
|
115
|
+
if (delimiter === 0x5d) { index += 1; break; }
|
|
116
|
+
fail('SYNTAX', 'Expected , or ] in array');
|
|
117
|
+
}
|
|
118
|
+
return result;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function parseString() {
|
|
122
|
+
index += 1; // opening quote
|
|
123
|
+
let value = '';
|
|
124
|
+
for (;;) {
|
|
125
|
+
if (index >= length) fail('SYNTAX', 'Unterminated string');
|
|
126
|
+
const code = text.charCodeAt(index);
|
|
127
|
+
if (code === 0x22) { index += 1; return value; }
|
|
128
|
+
if (code === 0x5c) {
|
|
129
|
+
index += 1;
|
|
130
|
+
const escape = text[index];
|
|
131
|
+
if (escape === undefined) fail('SYNTAX', 'Unterminated escape sequence');
|
|
132
|
+
switch (escape) {
|
|
133
|
+
case '"': value += '"'; index += 1; break;
|
|
134
|
+
case '\\': value += '\\'; index += 1; break;
|
|
135
|
+
case '/': value += '/'; index += 1; break;
|
|
136
|
+
case 'b': value += '\b'; index += 1; break;
|
|
137
|
+
case 'f': value += '\f'; index += 1; break;
|
|
138
|
+
case 'n': value += '\n'; index += 1; break;
|
|
139
|
+
case 'r': value += '\r'; index += 1; break;
|
|
140
|
+
case 't': value += '\t'; index += 1; break;
|
|
141
|
+
case 'u': {
|
|
142
|
+
const hex = text.slice(index + 1, index + 5);
|
|
143
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex)) fail('SYNTAX', 'Invalid unicode escape');
|
|
144
|
+
// Decoded code unit is appended as-is; two consecutive decoded
|
|
145
|
+
// surrogate halves recombine into one character automatically
|
|
146
|
+
// through normal JS UTF-16 string concatenation.
|
|
147
|
+
value += String.fromCharCode(parseInt(hex, 16));
|
|
148
|
+
index += 5;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
default: fail('SYNTAX', 'Invalid escape sequence');
|
|
152
|
+
}
|
|
153
|
+
} else if (code < 0x20) {
|
|
154
|
+
fail('SYNTAX', 'Unescaped control character in string');
|
|
155
|
+
} else {
|
|
156
|
+
value += text[index];
|
|
157
|
+
index += 1;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function parseNumber() {
|
|
163
|
+
const start = index;
|
|
164
|
+
if (text.charCodeAt(index) === 0x2d) index += 1;
|
|
165
|
+
if (text.charCodeAt(index) === 0x30) {
|
|
166
|
+
index += 1;
|
|
167
|
+
} else if (text.charCodeAt(index) >= 0x31 && text.charCodeAt(index) <= 0x39) {
|
|
168
|
+
index += 1;
|
|
169
|
+
while (text.charCodeAt(index) >= 0x30 && text.charCodeAt(index) <= 0x39) index += 1;
|
|
170
|
+
} else fail('SYNTAX', 'Invalid number');
|
|
171
|
+
if (text.charCodeAt(index) === 0x2e) {
|
|
172
|
+
index += 1;
|
|
173
|
+
if (!(text.charCodeAt(index) >= 0x30 && text.charCodeAt(index) <= 0x39)) fail('SYNTAX', 'Invalid number fraction');
|
|
174
|
+
while (text.charCodeAt(index) >= 0x30 && text.charCodeAt(index) <= 0x39) index += 1;
|
|
175
|
+
}
|
|
176
|
+
if (text.charCodeAt(index) === 0x65 || text.charCodeAt(index) === 0x45) {
|
|
177
|
+
index += 1;
|
|
178
|
+
if (text.charCodeAt(index) === 0x2b || text.charCodeAt(index) === 0x2d) index += 1;
|
|
179
|
+
if (!(text.charCodeAt(index) >= 0x30 && text.charCodeAt(index) <= 0x39)) fail('SYNTAX', 'Invalid number exponent');
|
|
180
|
+
while (text.charCodeAt(index) >= 0x30 && text.charCodeAt(index) <= 0x39) index += 1;
|
|
181
|
+
}
|
|
182
|
+
return Number(text.slice(start, index));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const result = parseValue(1);
|
|
186
|
+
skipWhitespace();
|
|
187
|
+
if (index !== length) fail('SYNTAX', 'Unexpected trailing characters after JSON value');
|
|
188
|
+
return result;
|
|
189
|
+
}
|
|
190
|
+
|