@wix/pathgrade 1.0.25 → 1.0.27
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/README.md +44 -2
- package/dist/agents/claude/sdk-message-projector.js +5 -0
- package/dist/agents/claude/sdk-options.d.ts +1 -2
- package/dist/agents/claude.js +2 -1
- package/dist/agents/codex-app-server/agent.js +89 -52
- package/dist/agents/codex-app-server/scenario-mount.d.ts +40 -0
- package/dist/agents/codex-app-server/scenario-mount.js +238 -0
- package/dist/agents/codex.js +1 -0
- package/dist/agents/cursor-scenario.d.ts +20 -0
- package/dist/agents/cursor-scenario.js +96 -0
- package/dist/agents/cursor.js +21 -0
- package/dist/agents/opencode/scenario.d.ts +25 -0
- package/dist/agents/opencode/scenario.js +195 -0
- package/dist/agents/opencode.js +54 -2
- package/dist/commands/report.d.ts +10 -2
- package/dist/commands/report.js +25 -4
- package/dist/internal/direct-mcp-v2/acp-author-projector.d.ts +20 -0
- package/dist/internal/direct-mcp-v2/acp-author-projector.js +147 -0
- package/dist/internal/direct-mcp-v2/acp-author-session.d.ts +39 -0
- package/dist/internal/direct-mcp-v2/acp-author-session.js +201 -0
- package/dist/internal/direct-mcp-v2/acp-client.d.ts +70 -0
- package/dist/internal/direct-mcp-v2/acp-client.js +310 -0
- package/dist/internal/direct-mcp-v2/acp-direct-mcp.d.ts +38 -0
- package/dist/internal/direct-mcp-v2/acp-direct-mcp.js +326 -0
- package/dist/internal/direct-mcp-v2/claude-direct-mcp.d.ts +68 -0
- package/dist/internal/direct-mcp-v2/claude-direct-mcp.js +456 -0
- package/dist/internal/direct-mcp-v2/claude-profile.d.ts +9 -0
- package/dist/internal/direct-mcp-v2/claude-profile.js +97 -0
- package/dist/internal/direct-mcp-v2/cleanup.d.ts +62 -0
- package/dist/internal/direct-mcp-v2/cleanup.js +182 -0
- package/dist/internal/direct-mcp-v2/codex-direct-mcp.d.ts +41 -0
- package/dist/internal/direct-mcp-v2/codex-direct-mcp.js +436 -0
- package/dist/internal/direct-mcp-v2/codex-profile.d.ts +12 -0
- package/dist/internal/direct-mcp-v2/codex-profile.js +81 -0
- package/dist/internal/direct-mcp-v2/compiler.d.ts +12 -0
- package/dist/internal/direct-mcp-v2/compiler.js +449 -0
- package/dist/internal/direct-mcp-v2/cursor-direct-mcp.d.ts +14 -0
- package/dist/internal/direct-mcp-v2/cursor-direct-mcp.js +60 -0
- package/dist/internal/direct-mcp-v2/cursor-profile.d.ts +38 -0
- package/dist/internal/direct-mcp-v2/cursor-profile.js +79 -0
- package/dist/internal/direct-mcp-v2/evidence.d.ts +50 -0
- package/dist/internal/direct-mcp-v2/evidence.js +147 -0
- package/dist/internal/direct-mcp-v2/host-reducer.d.ts +61 -0
- package/dist/internal/direct-mcp-v2/host-reducer.js +214 -0
- package/dist/internal/direct-mcp-v2/json.d.ts +9 -0
- package/dist/internal/direct-mcp-v2/json.js +220 -0
- package/dist/internal/direct-mcp-v2/materialize.d.ts +39 -0
- package/dist/internal/direct-mcp-v2/materialize.js +22 -0
- package/dist/internal/direct-mcp-v2/opencode-direct-mcp.d.ts +14 -0
- package/dist/internal/direct-mcp-v2/opencode-direct-mcp.js +71 -0
- package/dist/internal/direct-mcp-v2/opencode-profile.d.ts +8 -0
- package/dist/internal/direct-mcp-v2/opencode-profile.js +52 -0
- package/dist/internal/direct-mcp-v2/projection.d.ts +40 -0
- package/dist/internal/direct-mcp-v2/projection.js +46 -0
- package/dist/internal/direct-mcp-v2/public-scenario-runtime.d.ts +51 -0
- package/dist/internal/direct-mcp-v2/public-scenario-runtime.js +353 -0
- package/dist/internal/direct-mcp-v2/runtime-lock.d.ts +51 -0
- package/dist/internal/direct-mcp-v2/runtime-lock.js +203 -0
- package/dist/internal/direct-mcp-v2/runtime-profile.d.ts +28 -0
- package/dist/internal/direct-mcp-v2/runtime-profile.js +163 -0
- package/dist/internal/direct-mcp-v2/scenario-http-host.d.ts +24 -0
- package/dist/internal/direct-mcp-v2/scenario-http-host.js +254 -0
- package/dist/internal/direct-mcp-v2/types.d.ts +233 -0
- package/dist/internal/direct-mcp-v2/types.js +1 -0
- package/dist/pathgrade.js +22 -1
- package/dist/reporters/github-comment.d.ts +12 -3
- package/dist/reporters/github-comment.js +80 -9
- package/dist/reporting/artifacts.js +5 -2
- package/dist/sdk/agent.js +56 -4
- package/dist/sdk/evaluate.d.ts +2 -0
- package/dist/sdk/evaluate.js +20 -12
- package/dist/sdk/index.d.ts +6 -0
- package/dist/sdk/index.js +4 -0
- package/dist/sdk/lifecycle.js +8 -3
- package/dist/sdk/managed-session.d.ts +6 -0
- package/dist/sdk/managed-session.js +157 -50
- package/dist/sdk/scenario-evidence.d.ts +27 -0
- package/dist/sdk/scenario-evidence.js +94 -0
- package/dist/sdk/scenario-machine-v2.d.ts +9 -0
- package/dist/sdk/scenario-machine-v2.js +37 -0
- package/dist/sdk/snapshots.d.ts +3 -0
- package/dist/sdk/snapshots.js +2 -0
- package/dist/sdk/types.d.ts +11 -1
- package/dist/tool-event-results.d.ts +1 -1
- package/dist/tool-event-results.js +2 -1
- package/dist/tool-events.d.ts +5 -0
- package/dist/tool-events.js +5 -0
- package/dist/types.d.ts +8 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -375,6 +375,48 @@ Use `getMcpApproval()` for the simulated-user decision and
|
|
|
375
375
|
observed. Mock responses are fixed fixtures; this mode does not model stateful
|
|
376
376
|
mutation or make later reads observe earlier writes.
|
|
377
377
|
|
|
378
|
+
### Direct MCP scenario machine v2
|
|
379
|
+
|
|
380
|
+
`ScenarioMachineV2` is the versioned stateful MCP contract. Its parent-owned
|
|
381
|
+
host controls matching, transitions, cancellation, evidence, and replay. Cases
|
|
382
|
+
are unordered, disjoint JSON constraints; callbacks and regexes are rejected.
|
|
383
|
+
Concurrent calls involving a transition-capable tool poison the scenario. This
|
|
384
|
+
safety behavior is a fixed V2 invariant and is not author-configurable.
|
|
385
|
+
|
|
386
|
+
```ts
|
|
387
|
+
import {
|
|
388
|
+
compileScenario,
|
|
389
|
+
getFinalScenarioState,
|
|
390
|
+
getScenarioStateTimeline,
|
|
391
|
+
wasScenarioCaseMatched,
|
|
392
|
+
type ScenarioMachineV2,
|
|
393
|
+
} from '@wix/pathgrade';
|
|
394
|
+
|
|
395
|
+
declare const scenario: ScenarioMachineV2;
|
|
396
|
+
const compiled = compileScenario(scenario);
|
|
397
|
+
if (!compiled.ok) throw new Error(JSON.stringify(compiled.diagnostics));
|
|
398
|
+
declare const evidence: Parameters<typeof getFinalScenarioState>[0];
|
|
399
|
+
wasScenarioCaseMatched(evidence, { caseId: 'CreateDocument' });
|
|
400
|
+
getScenarioStateTimeline(evidence);
|
|
401
|
+
getFinalScenarioState(evidence);
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Evidence helpers validate the hash chain and reject empty, corrupt, or
|
|
405
|
+
cross-trial evidence. Provider lifecycle events are corroboration only.
|
|
406
|
+
|
|
407
|
+
#### Legacy migration window
|
|
408
|
+
Existing `mcpMock` descriptors remain supported with no scheduled removal. They
|
|
409
|
+
retain ordered regexes, fallback behavior, and receipts. V2 migration is
|
|
410
|
+
explicit; receipt settlement has no v2 authority or new features. Removing the
|
|
411
|
+
legacy executor would require a new owner-approved ADR and a major release.
|
|
412
|
+
|
|
413
|
+
The public live author path requires an explicit `agent`. It supports Claude,
|
|
414
|
+
Codex app-server, OpenCode ACP, and Cursor ACP when the checked-in exact runtime
|
|
415
|
+
lock for the current provider, platform, architecture, and version is admitted.
|
|
416
|
+
Codex `exec` is unsupported. OpenCode requires its existing absolute
|
|
417
|
+
`opencodeExecutable` option and uses the default API-key model for Scenario V2;
|
|
418
|
+
provider-specific scenario flags and profile controls are not public.
|
|
419
|
+
|
|
378
420
|
## CLI
|
|
379
421
|
|
|
380
422
|
```bash
|
|
@@ -386,14 +428,14 @@ pathgrade analyze [--skill=<name>] [--dir=<path>]
|
|
|
386
428
|
pathgrade affected [--since=<ref>] [--changed-files=<path>] [--explain] [--json]
|
|
387
429
|
pathgrade preview [browser] [--last=N] [--filter=text]
|
|
388
430
|
pathgrade preview-reactions --snapshot <run-snapshot.json> --reactions <file.ts>
|
|
389
|
-
pathgrade report [--results-path=<path>] [--no-comment] [--comment-id=<id>]
|
|
431
|
+
pathgrade report [--results-path=<path>] [--no-comment] [--comment-id=<id>] [--details-url=<url>] [--expected-head-sha=<sha>] [--strict]
|
|
390
432
|
```
|
|
391
433
|
|
|
392
434
|
Useful details:
|
|
393
435
|
|
|
394
436
|
- `pathgrade run --changed` computes affected evals first, writes selection metadata to `.pathgrade/selection.json`, and only then launches the selected runner adapter. Vitest is the default adapter; Jest is selected with `runner.adapter: 'jest'` or `--adapter=jest`.
|
|
395
437
|
- `pathgrade preview browser` starts a local viewer on `http://localhost:3847`.
|
|
396
|
-
- `pathgrade report` posts or updates a PR comment in GitHub Actions; locally it prints the markdown report and then the numeric pass rate.
|
|
438
|
+
- `pathgrade report` posts or updates a PR comment in GitHub Actions; locally it prints the markdown report and then the numeric pass rate. Provider orchestrators can add a `--details-url`, suppress stale updates with `--expected-head-sha`, and opt into surfaced API failures with `--strict`.
|
|
397
439
|
- `pathgrade validate --affected` is a strict mode for CI: every discovered eval must either live under a `SKILL.md` anchor or export valid `__pathgradeMeta`.
|
|
398
440
|
|
|
399
441
|
Run `pathgrade --help` for the full help text.
|
|
@@ -30,6 +30,7 @@ import { parseClaudeSdkMcpToolName } from './mcp-tool-name.js';
|
|
|
30
30
|
import { applyObservedToolResult, extractObservedToolResults, } from './tool-results.js';
|
|
31
31
|
export function projectSdkMessages(input) {
|
|
32
32
|
let sessionId;
|
|
33
|
+
let resolvedModel;
|
|
33
34
|
let initSkills;
|
|
34
35
|
let assistantText = '';
|
|
35
36
|
let resultText = '';
|
|
@@ -52,6 +53,9 @@ export function projectSdkMessages(input) {
|
|
|
52
53
|
sessionId = sid;
|
|
53
54
|
const sub = msg.subtype;
|
|
54
55
|
if (sub === 'init') {
|
|
56
|
+
const model = msg.model;
|
|
57
|
+
if (typeof model === 'string' && model)
|
|
58
|
+
resolvedModel = model;
|
|
55
59
|
const skills = msg.skills;
|
|
56
60
|
if (Array.isArray(skills))
|
|
57
61
|
initSkills = skills.filter((s) => typeof s === 'string');
|
|
@@ -153,6 +157,7 @@ export function projectSdkMessages(input) {
|
|
|
153
157
|
traceOutput,
|
|
154
158
|
toolEvents: finalToolEvents,
|
|
155
159
|
runtimePoliciesApplied: [],
|
|
160
|
+
...(resolvedModel ? { resolvedModel } : {}),
|
|
156
161
|
inputTokens,
|
|
157
162
|
outputTokens,
|
|
158
163
|
...(cacheCreationInputTokens !== undefined ? { cacheCreationInputTokens } : {}),
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { CanUseTool, Options, SpawnedProcess, SpawnOptions as SdkSpawnOptions } from '@anthropic-ai/claude-agent-sdk';
|
|
2
|
-
import type { McpServersObject } from '../../providers/mcp-runtime-mounting.js';
|
|
3
2
|
export interface ClaudeSdkOptionsInputs {
|
|
4
3
|
/** Per-trial workspace; cwd for the SDK so project-staged skills resolve. */
|
|
5
4
|
workspacePath: string;
|
|
@@ -32,7 +31,7 @@ export interface ClaudeSdkOptionsInputs {
|
|
|
32
31
|
*/
|
|
33
32
|
resume?: string;
|
|
34
33
|
/** MCP servers in the SDK's object form, from `mountMcpForClaudeSdk`. */
|
|
35
|
-
mcpServers?:
|
|
34
|
+
mcpServers?: Options['mcpServers'];
|
|
36
35
|
/** Per-turn cancellation controller linked to PathGrade's turn timeout. */
|
|
37
36
|
abortController?: AbortController;
|
|
38
37
|
}
|
package/dist/agents/claude.js
CHANGED
|
@@ -102,7 +102,8 @@ export class ClaudeAgent extends BaseAgent {
|
|
|
102
102
|
mcpSafety: sessionOptions?.mcpSafety,
|
|
103
103
|
});
|
|
104
104
|
await assertStdioMcpServersStartForClaudeSdk(mcpMountOptions);
|
|
105
|
-
const mcpServers = sessionOptions?.
|
|
105
|
+
const mcpServers = sessionOptions?.scenarioMcpServers
|
|
106
|
+
?? sessionOptions?.scriptedMcpHost?.claudeServers
|
|
106
107
|
?? await mountMcpForClaudeSdk(mcpMountOptions);
|
|
107
108
|
let priorSessionId;
|
|
108
109
|
let turnNumber = 0;
|
|
@@ -40,6 +40,29 @@ function projectMcpStartupStatusIntoTurn(params, turn) {
|
|
|
40
40
|
failTurn(turn, message);
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
+
function dispatchCodexNotification(notification, turn, sensitiveValues) {
|
|
44
|
+
if (process.env.PATHGRADE_CODEX_DEBUG) {
|
|
45
|
+
const debugParams = sanitizePersistenceValue(notification.params, sensitiveValues);
|
|
46
|
+
console.error(`[codex app-server] notification method=${notification.method} params=${JSON.stringify(debugParams).slice(0, 300)}`);
|
|
47
|
+
}
|
|
48
|
+
if (notification.method === 'mcpServer/startupStatus/updated') {
|
|
49
|
+
if (turn) {
|
|
50
|
+
projectMcpStartupStatusIntoTurn((notification.params ?? {}), turn);
|
|
51
|
+
}
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (!turn)
|
|
55
|
+
return;
|
|
56
|
+
const params = notification.params;
|
|
57
|
+
if (notification.method === 'item/started') {
|
|
58
|
+
if (params)
|
|
59
|
+
turn.itemLifecycle?.receiveStarted(params);
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (notification.method === 'item/completed' && params) {
|
|
63
|
+
turn.itemLifecycle?.receiveCompleted(params);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
43
66
|
function failTurn(turn, message) {
|
|
44
67
|
turn.turnFailed = true;
|
|
45
68
|
turn.failureMessage = message;
|
|
@@ -79,7 +102,11 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
79
102
|
const askBus = requireAskBusForLiveBatches(options, 'CodexAppServerAgent');
|
|
80
103
|
const workspacePath = getWorkspacePath(runtime);
|
|
81
104
|
const runtimeEnv = getRuntimeEnv(runtime);
|
|
82
|
-
const
|
|
105
|
+
const scenarioMount = options?.codexScenarioMount;
|
|
106
|
+
const sensitiveValues = [...new Set([
|
|
107
|
+
...collectSensitiveEnvValues(runtimeEnv),
|
|
108
|
+
...(scenarioMount?.sensitiveValues ?? []),
|
|
109
|
+
])];
|
|
83
110
|
const model = resolveCodexModel(options?.model);
|
|
84
111
|
const sandboxMode = this.deps.sandboxMode ?? 'workspace-write';
|
|
85
112
|
await assertUnsupportedLiveMcpSafetyRuntime({
|
|
@@ -112,6 +139,28 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
112
139
|
throw new Error('CodexAppServerAgent session disposed');
|
|
113
140
|
if (handle)
|
|
114
141
|
return handle.transport;
|
|
142
|
+
if (scenarioMount) {
|
|
143
|
+
handle = scenarioMount.handle;
|
|
144
|
+
const transport = handle.transport;
|
|
145
|
+
transport.onServerRequest((req) => this.dispatchServerRequest(req, {
|
|
146
|
+
transport,
|
|
147
|
+
askBus,
|
|
148
|
+
activeTurn: () => activeTurn,
|
|
149
|
+
mcpSafety: options?.mcpSafety,
|
|
150
|
+
scriptedHost,
|
|
151
|
+
correlator,
|
|
152
|
+
managedAuth,
|
|
153
|
+
}));
|
|
154
|
+
transport.onClose((info) => {
|
|
155
|
+
if (handle === scenarioMount.handle)
|
|
156
|
+
closeInfo = info;
|
|
157
|
+
});
|
|
158
|
+
transport.onNotification((n) => {
|
|
159
|
+
dispatchCodexNotification(n, activeTurn, sensitiveValues);
|
|
160
|
+
});
|
|
161
|
+
await managedAuth.login(transport);
|
|
162
|
+
return transport;
|
|
163
|
+
}
|
|
115
164
|
const factory = this.deps.createTransport
|
|
116
165
|
?? (async (ctx) => spawnAppServerTransport({ cwd: ctx.workspacePath, env: ctx.env,
|
|
117
166
|
args: scriptedHost ? ['--enable', 'tool_call_mcp_elicitation'] : [] }));
|
|
@@ -132,30 +181,7 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
132
181
|
closeInfo = info;
|
|
133
182
|
});
|
|
134
183
|
transport.onNotification((n) => {
|
|
135
|
-
|
|
136
|
-
const debugParams = sanitizePersistenceValue(n.params, sensitiveValues);
|
|
137
|
-
console.error(`[codex app-server] notification method=${n.method} params=${JSON.stringify(debugParams).slice(0, 300)}`);
|
|
138
|
-
}
|
|
139
|
-
if (n.method === 'mcpServer/startupStatus/updated') {
|
|
140
|
-
const turn = activeTurn;
|
|
141
|
-
if (turn) {
|
|
142
|
-
projectMcpStartupStatusIntoTurn((n.params ?? {}), turn);
|
|
143
|
-
}
|
|
144
|
-
return;
|
|
145
|
-
}
|
|
146
|
-
const turn = activeTurn;
|
|
147
|
-
if (!turn)
|
|
148
|
-
return;
|
|
149
|
-
const params = n.params;
|
|
150
|
-
if (n.method === 'item/started') {
|
|
151
|
-
if (params)
|
|
152
|
-
turn.itemLifecycle?.receiveStarted(params);
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
if (n.method !== 'item/completed')
|
|
156
|
-
return;
|
|
157
|
-
if (params)
|
|
158
|
-
turn.itemLifecycle?.receiveCompleted(params);
|
|
184
|
+
dispatchCodexNotification(n, activeTurn, sensitiveValues);
|
|
159
185
|
});
|
|
160
186
|
const initialize = async () => {
|
|
161
187
|
const initialized = await transport.sendRequest('initialize', {
|
|
@@ -179,6 +205,7 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
179
205
|
const runTurn = async (message) => {
|
|
180
206
|
const t = await ensureTransport();
|
|
181
207
|
const turnHandle = handle;
|
|
208
|
+
scenarioMount?.prepareProviderTurn();
|
|
182
209
|
turnCounter += 1;
|
|
183
210
|
const turn = {
|
|
184
211
|
turnNumber: turnCounter,
|
|
@@ -202,7 +229,7 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
202
229
|
env: runtimeEnv,
|
|
203
230
|
});
|
|
204
231
|
}
|
|
205
|
-
const mcpConfig = scriptedHost?.codexConfig ?? (options?.mcpConfigPath
|
|
232
|
+
const mcpConfig = scenarioMount?.config ?? scriptedHost?.codexConfig ?? (options?.mcpConfigPath
|
|
206
233
|
? await mountMcpForCodexAppServer({
|
|
207
234
|
workspacePath,
|
|
208
235
|
mcpConfigPath: options.mcpConfigPath,
|
|
@@ -335,12 +362,15 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
335
362
|
}
|
|
336
363
|
if (!authoritativeTurnId) {
|
|
337
364
|
if (!turn.authoritativeTurnId) {
|
|
338
|
-
|
|
339
|
-
|
|
365
|
+
const message = 'turn/start did not return an authoritative turn id';
|
|
366
|
+
scriptedHost?.failProtocol(message);
|
|
367
|
+
scenarioMount?.failProviderTurn(new Error(message));
|
|
368
|
+
turn.signalFailure?.(message);
|
|
340
369
|
}
|
|
341
370
|
return;
|
|
342
371
|
}
|
|
343
372
|
turnStartCanReuseTransport = true;
|
|
373
|
+
scenarioMount?.registerProviderTurn(authoritativeThreadId, authoritativeTurnId);
|
|
344
374
|
markAuthoritativeTurn(authoritativeTurnId);
|
|
345
375
|
})
|
|
346
376
|
.catch((err) => {
|
|
@@ -352,6 +382,7 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
352
382
|
: typeof err === 'string'
|
|
353
383
|
? err
|
|
354
384
|
: 'turn/start failed';
|
|
385
|
+
scenarioMount?.failProviderTurn(new Error(msg));
|
|
355
386
|
turn.signalFailure?.(msg);
|
|
356
387
|
});
|
|
357
388
|
try {
|
|
@@ -366,7 +397,7 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
366
397
|
message: info.message ?? 'app-server exited',
|
|
367
398
|
pid: info.pid,
|
|
368
399
|
signal: info.signal,
|
|
369
|
-
sensitiveValues,
|
|
400
|
+
sensitiveValues, resolvedModel: model,
|
|
370
401
|
});
|
|
371
402
|
}
|
|
372
403
|
if (turn.turnFailed) {
|
|
@@ -375,14 +406,14 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
375
406
|
activeTurn: turn,
|
|
376
407
|
exitCode: 1,
|
|
377
408
|
message: turn.failureMessage ?? 'turn failed',
|
|
378
|
-
sensitiveValues,
|
|
409
|
+
sensitiveValues, resolvedModel: model,
|
|
379
410
|
});
|
|
380
411
|
}
|
|
381
412
|
await authoritativeTurnReady;
|
|
382
413
|
if (turn.turnFailed) {
|
|
383
414
|
return assembleTurnResult({
|
|
384
415
|
askBus, activeTurn: turn, exitCode: 1,
|
|
385
|
-
message: turn.failureMessage ?? 'turn failed', sensitiveValues,
|
|
416
|
+
message: turn.failureMessage ?? 'turn failed', sensitiveValues, resolvedModel: model,
|
|
386
417
|
});
|
|
387
418
|
}
|
|
388
419
|
return assembleTurnResult({
|
|
@@ -390,7 +421,7 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
390
421
|
activeTurn: turn,
|
|
391
422
|
exitCode: 0,
|
|
392
423
|
message: turn.assistantMessageParts.join('\n\n'),
|
|
393
|
-
sensitiveValues,
|
|
424
|
+
sensitiveValues, resolvedModel: model,
|
|
394
425
|
});
|
|
395
426
|
}
|
|
396
427
|
finally {
|
|
@@ -399,20 +430,25 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
399
430
|
activeTurn = null;
|
|
400
431
|
correlator?.endTurn();
|
|
401
432
|
const retireTransport = shouldRetireAuthoritativeTurnTransport(authoritativeTurnIds);
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
handle
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
433
|
+
if (scenarioMount) {
|
|
434
|
+
sessionRetired ||= retireTransport;
|
|
435
|
+
}
|
|
436
|
+
else {
|
|
437
|
+
await closeUncorrelatedTurnTransport({
|
|
438
|
+
turnStartCanReuseTransport,
|
|
439
|
+
authoritativeTurnId: turn.authoritativeTurnId,
|
|
440
|
+
retireTransport,
|
|
441
|
+
currentHandle: handle,
|
|
442
|
+
turnHandle,
|
|
443
|
+
reset: () => {
|
|
444
|
+
sessionRetired ||= retireTransport;
|
|
445
|
+
handle = null;
|
|
446
|
+
threadId = null;
|
|
447
|
+
authoritativeTurnIds.clear();
|
|
448
|
+
closeInfo = null;
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
}
|
|
416
452
|
}
|
|
417
453
|
};
|
|
418
454
|
const dispose = async () => {
|
|
@@ -424,7 +460,7 @@ export class CodexAppServerAgent extends BaseAgent {
|
|
|
424
460
|
turn.signalFailure?.('session disposed');
|
|
425
461
|
}
|
|
426
462
|
activeTurn = null;
|
|
427
|
-
if (handle) {
|
|
463
|
+
if (handle && !scenarioMount) {
|
|
428
464
|
try {
|
|
429
465
|
await handle.close();
|
|
430
466
|
}
|
|
@@ -576,7 +612,7 @@ function shouldEnableMcpElicitations(options) {
|
|
|
576
612
|
return options?.mcpConfigOrigin === 'generated_mock';
|
|
577
613
|
}
|
|
578
614
|
function assembleTurnResult(args) {
|
|
579
|
-
const { askBus, activeTurn, exitCode, message, pid, signal, sensitiveValues = [] } = args;
|
|
615
|
+
const { askBus, activeTurn, exitCode, message, pid, signal, sensitiveValues = [], resolvedModel } = args;
|
|
580
616
|
for (const pending of activeTurn.pendingMcpDenials.splice(0)) {
|
|
581
617
|
activeTurn.nonAskToolEvents.push(buildPolicyDeniedMcpToolEvent(activeTurn.turnNumber, pending));
|
|
582
618
|
}
|
|
@@ -587,8 +623,9 @@ function assembleTurnResult(args) {
|
|
|
587
623
|
.map((s) => toAskUserToolEvent(s));
|
|
588
624
|
const toolEvents = enrichSkillEvents([...askEvents, ...activeTurn.nonAskToolEvents])
|
|
589
625
|
.map((event) => attachToolEventSensitiveValues(event, sensitiveValues));
|
|
626
|
+
const sanitizedMessage = sanitizePersistenceValue(message, sensitiveValues);
|
|
590
627
|
const rawOutput = sanitizePersistenceValue(exitCode === 0
|
|
591
|
-
?
|
|
628
|
+
? sanitizedMessage
|
|
592
629
|
: [
|
|
593
630
|
message,
|
|
594
631
|
pid !== undefined ? `pid=${pid}` : '',
|
|
@@ -599,12 +636,12 @@ function assembleTurnResult(args) {
|
|
|
599
636
|
.join(' '), sensitiveValues);
|
|
600
637
|
return attachTurnResultSensitiveValues({
|
|
601
638
|
rawOutput,
|
|
602
|
-
assistantMessage: exitCode === 0 ?
|
|
603
|
-
visibleAssistantMessage: exitCode === 0 ?
|
|
639
|
+
assistantMessage: exitCode === 0 ? sanitizedMessage : '',
|
|
640
|
+
visibleAssistantMessage: exitCode === 0 ? sanitizedMessage : '',
|
|
604
641
|
visibleAssistantMessageSource: 'assistant_message',
|
|
605
642
|
exitCode,
|
|
606
643
|
traceOutput: rawOutput,
|
|
607
|
-
toolEvents,
|
|
644
|
+
toolEvents, resolvedModel,
|
|
608
645
|
...(exitCode !== 0
|
|
609
646
|
? {
|
|
610
647
|
crashInfo: {
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { type MaterializedProjection } from '../../internal/direct-mcp-v2/materialize.js';
|
|
2
|
+
import { type PublicScenarioRuntime } from '../../internal/direct-mcp-v2/public-scenario-runtime.js';
|
|
3
|
+
import { type RuntimeLock } from '../../internal/direct-mcp-v2/runtime-lock.js';
|
|
4
|
+
import type { ScenarioArtifact } from '../../internal/direct-mcp-v2/types.js';
|
|
5
|
+
import type { CodexAppServerMcpConfig } from '../../providers/mcp-runtime-mounting.js';
|
|
6
|
+
import { type AppServerSessionHandle } from './transport.js';
|
|
7
|
+
/**
|
|
8
|
+
* Parent-owned, already-admitted Codex app-server attachment for one public
|
|
9
|
+
* ScenarioMachineV2 session. The driver may use this handle, but it must not
|
|
10
|
+
* replace or close it; the scenario coordinator owns provider cleanup.
|
|
11
|
+
*/
|
|
12
|
+
export interface CodexScenarioMount {
|
|
13
|
+
readonly handle: AppServerSessionHandle;
|
|
14
|
+
readonly config: CodexAppServerMcpConfig;
|
|
15
|
+
readonly sensitiveValues: readonly string[];
|
|
16
|
+
prepareProviderTurn(): void;
|
|
17
|
+
registerProviderTurn(threadId: string, turnId: string): void;
|
|
18
|
+
failProviderTurn(error: Error): void;
|
|
19
|
+
}
|
|
20
|
+
type CodexScenarioRuntimeOptions = {
|
|
21
|
+
artifact: ScenarioArtifact;
|
|
22
|
+
workspacePath: string;
|
|
23
|
+
environment: Readonly<Record<string, string | undefined>>;
|
|
24
|
+
onEvidence?(evidence: readonly import('../../internal/direct-mcp-v2/types.js').EvidenceEnvelope[]): void;
|
|
25
|
+
};
|
|
26
|
+
type CodexScenarioRuntimeDependencies = {
|
|
27
|
+
lock: RuntimeLock;
|
|
28
|
+
resolveExecutable(environment: Readonly<Record<string, string | undefined>>): Promise<string>;
|
|
29
|
+
spawn(context: {
|
|
30
|
+
binary: string;
|
|
31
|
+
args: readonly string[];
|
|
32
|
+
cwd: string;
|
|
33
|
+
env: NodeJS.ProcessEnv;
|
|
34
|
+
}): AppServerSessionHandle;
|
|
35
|
+
commandVersion(path: string, args: readonly string[]): Promise<string>;
|
|
36
|
+
};
|
|
37
|
+
/** Coordinator-backed public Codex ScenarioMachineV2 adapter. */
|
|
38
|
+
export declare function startPublicCodexScenarioRuntime(options: CodexScenarioRuntimeOptions, dependencies?: Partial<CodexScenarioRuntimeDependencies>): Promise<PublicScenarioRuntime>;
|
|
39
|
+
export declare function mergeCodexScenarioConfigs(configs: readonly MaterializedProjection[]): CodexAppServerMcpConfig;
|
|
40
|
+
export {};
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { materializeProjection } from '../../internal/direct-mcp-v2/materialize.js';
|
|
2
|
+
import { projectEndpoints } from '../../internal/direct-mcp-v2/projection.js';
|
|
3
|
+
import { startScenarioCoordinator, } from '../../internal/direct-mcp-v2/public-scenario-runtime.js';
|
|
4
|
+
import { admitRuntimeLock } from '../../internal/direct-mcp-v2/runtime-lock.js';
|
|
5
|
+
import { CODEX_DIRECT_MCP_DARWIN_ARM64_LOCK, CODEX_DIRECT_MCP_NORMALIZED_HANDSHAKE, resolveCodexScenarioExecutable, } from '../../internal/direct-mcp-v2/codex-profile.js';
|
|
6
|
+
import { spawnAppServerTransport, } from './transport.js';
|
|
7
|
+
const defaultDependencies = {
|
|
8
|
+
lock: CODEX_DIRECT_MCP_DARWIN_ARM64_LOCK,
|
|
9
|
+
resolveExecutable: resolveCodexScenarioExecutable,
|
|
10
|
+
spawn: ({ binary, args, cwd, env }) => spawnAppServerTransport({
|
|
11
|
+
binary,
|
|
12
|
+
args,
|
|
13
|
+
cwd,
|
|
14
|
+
env,
|
|
15
|
+
logStderrOnExit: false,
|
|
16
|
+
}),
|
|
17
|
+
commandVersion: async (path, args) => {
|
|
18
|
+
const { execFile } = await import('node:child_process');
|
|
19
|
+
return await new Promise((resolve, reject) => {
|
|
20
|
+
execFile(path, [...args], { timeout: 15_000, maxBuffer: 1024 * 1024 }, (error, stdout) => {
|
|
21
|
+
if (error)
|
|
22
|
+
reject(error);
|
|
23
|
+
else
|
|
24
|
+
resolve(stdout.trim());
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
/** Coordinator-backed public Codex ScenarioMachineV2 adapter. */
|
|
30
|
+
export async function startPublicCodexScenarioRuntime(options, dependencies = {}) {
|
|
31
|
+
const deps = { ...defaultDependencies, ...dependencies };
|
|
32
|
+
const coordinator = await startScenarioCoordinator({
|
|
33
|
+
...options,
|
|
34
|
+
provider: 'codex',
|
|
35
|
+
async attach(context) {
|
|
36
|
+
const bearer = context.resolveSecret('public-scenario-v2-bearer');
|
|
37
|
+
let handle;
|
|
38
|
+
const resolvedExecutable = await deps.resolveExecutable(options.environment);
|
|
39
|
+
const admission = await admitRuntimeLock(deps.lock, {
|
|
40
|
+
provider: 'codex',
|
|
41
|
+
platform: process.platform,
|
|
42
|
+
architecture: process.arch,
|
|
43
|
+
resolvedExecutable,
|
|
44
|
+
environment: options.environment,
|
|
45
|
+
forbiddenSecrets: [bearer, bearer.slice('Bearer '.length)],
|
|
46
|
+
commandVersion: deps.commandVersion,
|
|
47
|
+
initializeControlPlane: async (verifiedCommandPath, codeOwnedArgs) => {
|
|
48
|
+
handle = deps.spawn({
|
|
49
|
+
binary: verifiedCommandPath,
|
|
50
|
+
args: codeOwnedArgs,
|
|
51
|
+
cwd: options.workspacePath,
|
|
52
|
+
env: { ...options.environment },
|
|
53
|
+
});
|
|
54
|
+
try {
|
|
55
|
+
const initialized = await handle.transport.sendRequest('initialize', {
|
|
56
|
+
clientInfo: { name: 'pathgrade', version: '0.5.0', title: null },
|
|
57
|
+
capabilities: { experimentalApi: true, optOutNotificationMethods: null },
|
|
58
|
+
});
|
|
59
|
+
assertCodexHandshake(initialized, deps.lock.version);
|
|
60
|
+
handle.transport.sendNotification('initialized', null);
|
|
61
|
+
return {
|
|
62
|
+
normalizedHandshake: CODEX_DIRECT_MCP_NORMALIZED_HANDSHAKE,
|
|
63
|
+
capabilities: deps.lock.capabilities,
|
|
64
|
+
close: () => handle.close(),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
await handle.close().catch(() => undefined);
|
|
69
|
+
throw error;
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
if (!handle)
|
|
74
|
+
throw new Error('RUNTIME_NOT_ADMITTED: control plane unavailable');
|
|
75
|
+
try {
|
|
76
|
+
const plans = projectEndpoints(context.endpoints, admission.profile);
|
|
77
|
+
if (!Array.isArray(plans))
|
|
78
|
+
throw new Error(`RUNTIME_NOT_ADMITTED: ${plans.reason}`);
|
|
79
|
+
const config = mergeCodexScenarioConfigs(plans.map((plan) => materializeProjection(plan, {
|
|
80
|
+
resolve: context.resolveSecret,
|
|
81
|
+
})));
|
|
82
|
+
const tracker = createCodexScenarioTurnTracker(handle.transport);
|
|
83
|
+
const sensitiveValues = [...new Set([
|
|
84
|
+
bearer,
|
|
85
|
+
bearer.slice('Bearer '.length),
|
|
86
|
+
...context.endpoints.map((endpoint) => endpoint.url),
|
|
87
|
+
])];
|
|
88
|
+
const mount = {
|
|
89
|
+
handle,
|
|
90
|
+
config,
|
|
91
|
+
sensitiveValues,
|
|
92
|
+
prepareProviderTurn: tracker.prepare,
|
|
93
|
+
registerProviderTurn: tracker.register,
|
|
94
|
+
failProviderTurn: tracker.fail,
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
value: mount,
|
|
98
|
+
interrupt: tracker.interrupt,
|
|
99
|
+
waitForTerminal: tracker.waitForTerminal,
|
|
100
|
+
async close() {
|
|
101
|
+
tracker.dispose();
|
|
102
|
+
await handle?.close();
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
await handle.close().catch(() => undefined);
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
return {
|
|
113
|
+
configureSession(sessionOptions) {
|
|
114
|
+
sessionOptions.codexScenarioMount = coordinator.attachment;
|
|
115
|
+
},
|
|
116
|
+
beginTurn: coordinator.beginTurn,
|
|
117
|
+
settleTurn: coordinator.settleTurn,
|
|
118
|
+
failTurn: coordinator.failTurn,
|
|
119
|
+
evidence: coordinator.evidence,
|
|
120
|
+
dispose: coordinator.dispose,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
export function mergeCodexScenarioConfigs(configs) {
|
|
124
|
+
const mcpServers = {};
|
|
125
|
+
for (const config of configs) {
|
|
126
|
+
if (config.kind !== 'codex-app-server') {
|
|
127
|
+
throw new Error('Codex projection materialized as another provider');
|
|
128
|
+
}
|
|
129
|
+
Object.assign(mcpServers, config.mcp_servers);
|
|
130
|
+
}
|
|
131
|
+
return { mcp_servers: mcpServers };
|
|
132
|
+
}
|
|
133
|
+
function assertCodexHandshake(initialized, expectedVersion) {
|
|
134
|
+
const version = expectedVersion.replace(/^codex-cli\s+/, '');
|
|
135
|
+
if (typeof initialized.userAgent !== 'string' || !initialized.userAgent.includes(version)) {
|
|
136
|
+
throw new Error('Codex initialize userAgent did not match admitted version');
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function createCodexScenarioTurnTracker(transport) {
|
|
140
|
+
let active;
|
|
141
|
+
let resolveActive;
|
|
142
|
+
let rejectActive;
|
|
143
|
+
let terminalError;
|
|
144
|
+
let activeError;
|
|
145
|
+
const createActiveReady = () => {
|
|
146
|
+
const ready = new Promise((resolve, reject) => {
|
|
147
|
+
resolveActive = resolve;
|
|
148
|
+
rejectActive = reject;
|
|
149
|
+
});
|
|
150
|
+
void ready.catch(() => undefined);
|
|
151
|
+
return ready;
|
|
152
|
+
};
|
|
153
|
+
let activeReady = createActiveReady();
|
|
154
|
+
const completed = new Map();
|
|
155
|
+
const waiters = new Map();
|
|
156
|
+
const off = transport.onNotification((notification) => {
|
|
157
|
+
if (notification.method !== 'turn/completed')
|
|
158
|
+
return;
|
|
159
|
+
const params = notification.params;
|
|
160
|
+
const turnId = params?.turn?.id ?? params?.turnId;
|
|
161
|
+
if (!turnId)
|
|
162
|
+
return;
|
|
163
|
+
const status = params?.turn?.status ?? params?.status ?? 'unknown';
|
|
164
|
+
const waiter = waiters.get(turnId);
|
|
165
|
+
if (waiter) {
|
|
166
|
+
waiters.delete(turnId);
|
|
167
|
+
waiter.resolve(status);
|
|
168
|
+
}
|
|
169
|
+
else {
|
|
170
|
+
completed.set(turnId, status);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
const closeOff = transport.onClose((info) => {
|
|
174
|
+
terminalError = Object.assign(new Error('Codex app-server closed before the matching turn terminal'), {
|
|
175
|
+
exitCode: info.exitCode,
|
|
176
|
+
signal: info.signal,
|
|
177
|
+
pid: info.pid,
|
|
178
|
+
});
|
|
179
|
+
rejectActive?.(terminalError);
|
|
180
|
+
for (const waiter of waiters.values())
|
|
181
|
+
waiter.reject(terminalError);
|
|
182
|
+
waiters.clear();
|
|
183
|
+
});
|
|
184
|
+
const providerTurn = async (_turn) => {
|
|
185
|
+
if (active)
|
|
186
|
+
return active;
|
|
187
|
+
if (activeError)
|
|
188
|
+
throw activeError;
|
|
189
|
+
if (terminalError)
|
|
190
|
+
throw terminalError;
|
|
191
|
+
return activeReady;
|
|
192
|
+
};
|
|
193
|
+
return {
|
|
194
|
+
prepare() {
|
|
195
|
+
active = undefined;
|
|
196
|
+
activeError = undefined;
|
|
197
|
+
if (terminalError)
|
|
198
|
+
return;
|
|
199
|
+
activeReady = createActiveReady();
|
|
200
|
+
},
|
|
201
|
+
register(threadId, turnId) {
|
|
202
|
+
const turn = { threadId, turnId };
|
|
203
|
+
active = turn;
|
|
204
|
+
resolveActive?.(turn);
|
|
205
|
+
},
|
|
206
|
+
fail(error) {
|
|
207
|
+
activeError = error;
|
|
208
|
+
rejectActive?.(error);
|
|
209
|
+
},
|
|
210
|
+
async interrupt(turn) {
|
|
211
|
+
if (terminalError)
|
|
212
|
+
throw terminalError;
|
|
213
|
+
const provider = await providerTurn(turn);
|
|
214
|
+
await transport.sendRequest('turn/interrupt', provider);
|
|
215
|
+
},
|
|
216
|
+
async waitForTerminal(turn) {
|
|
217
|
+
const provider = await providerTurn(turn);
|
|
218
|
+
if (completed.has(provider.turnId)) {
|
|
219
|
+
completed.delete(provider.turnId);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (terminalError)
|
|
223
|
+
throw terminalError;
|
|
224
|
+
await new Promise((resolve, reject) => waiters.set(provider.turnId, { resolve, reject }));
|
|
225
|
+
},
|
|
226
|
+
dispose() {
|
|
227
|
+
off();
|
|
228
|
+
closeOff();
|
|
229
|
+
const error = terminalError ?? new Error('Codex scenario turn tracker disposed');
|
|
230
|
+
rejectActive?.(error);
|
|
231
|
+
for (const waiter of waiters.values())
|
|
232
|
+
waiter.reject(error);
|
|
233
|
+
active = undefined;
|
|
234
|
+
completed.clear();
|
|
235
|
+
waiters.clear();
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|