@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.
Files changed (89) hide show
  1. package/README.md +44 -2
  2. package/dist/agents/claude/sdk-message-projector.js +5 -0
  3. package/dist/agents/claude/sdk-options.d.ts +1 -2
  4. package/dist/agents/claude.js +2 -1
  5. package/dist/agents/codex-app-server/agent.js +89 -52
  6. package/dist/agents/codex-app-server/scenario-mount.d.ts +40 -0
  7. package/dist/agents/codex-app-server/scenario-mount.js +238 -0
  8. package/dist/agents/codex.js +1 -0
  9. package/dist/agents/cursor-scenario.d.ts +20 -0
  10. package/dist/agents/cursor-scenario.js +96 -0
  11. package/dist/agents/cursor.js +21 -0
  12. package/dist/agents/opencode/scenario.d.ts +25 -0
  13. package/dist/agents/opencode/scenario.js +195 -0
  14. package/dist/agents/opencode.js +54 -2
  15. package/dist/commands/report.d.ts +10 -2
  16. package/dist/commands/report.js +25 -4
  17. package/dist/internal/direct-mcp-v2/acp-author-projector.d.ts +20 -0
  18. package/dist/internal/direct-mcp-v2/acp-author-projector.js +147 -0
  19. package/dist/internal/direct-mcp-v2/acp-author-session.d.ts +39 -0
  20. package/dist/internal/direct-mcp-v2/acp-author-session.js +201 -0
  21. package/dist/internal/direct-mcp-v2/acp-client.d.ts +70 -0
  22. package/dist/internal/direct-mcp-v2/acp-client.js +310 -0
  23. package/dist/internal/direct-mcp-v2/acp-direct-mcp.d.ts +38 -0
  24. package/dist/internal/direct-mcp-v2/acp-direct-mcp.js +326 -0
  25. package/dist/internal/direct-mcp-v2/claude-direct-mcp.d.ts +68 -0
  26. package/dist/internal/direct-mcp-v2/claude-direct-mcp.js +456 -0
  27. package/dist/internal/direct-mcp-v2/claude-profile.d.ts +9 -0
  28. package/dist/internal/direct-mcp-v2/claude-profile.js +97 -0
  29. package/dist/internal/direct-mcp-v2/cleanup.d.ts +62 -0
  30. package/dist/internal/direct-mcp-v2/cleanup.js +182 -0
  31. package/dist/internal/direct-mcp-v2/codex-direct-mcp.d.ts +41 -0
  32. package/dist/internal/direct-mcp-v2/codex-direct-mcp.js +436 -0
  33. package/dist/internal/direct-mcp-v2/codex-profile.d.ts +12 -0
  34. package/dist/internal/direct-mcp-v2/codex-profile.js +81 -0
  35. package/dist/internal/direct-mcp-v2/compiler.d.ts +12 -0
  36. package/dist/internal/direct-mcp-v2/compiler.js +449 -0
  37. package/dist/internal/direct-mcp-v2/cursor-direct-mcp.d.ts +14 -0
  38. package/dist/internal/direct-mcp-v2/cursor-direct-mcp.js +60 -0
  39. package/dist/internal/direct-mcp-v2/cursor-profile.d.ts +38 -0
  40. package/dist/internal/direct-mcp-v2/cursor-profile.js +79 -0
  41. package/dist/internal/direct-mcp-v2/evidence.d.ts +50 -0
  42. package/dist/internal/direct-mcp-v2/evidence.js +147 -0
  43. package/dist/internal/direct-mcp-v2/host-reducer.d.ts +61 -0
  44. package/dist/internal/direct-mcp-v2/host-reducer.js +214 -0
  45. package/dist/internal/direct-mcp-v2/json.d.ts +9 -0
  46. package/dist/internal/direct-mcp-v2/json.js +220 -0
  47. package/dist/internal/direct-mcp-v2/materialize.d.ts +39 -0
  48. package/dist/internal/direct-mcp-v2/materialize.js +22 -0
  49. package/dist/internal/direct-mcp-v2/opencode-direct-mcp.d.ts +14 -0
  50. package/dist/internal/direct-mcp-v2/opencode-direct-mcp.js +71 -0
  51. package/dist/internal/direct-mcp-v2/opencode-profile.d.ts +8 -0
  52. package/dist/internal/direct-mcp-v2/opencode-profile.js +52 -0
  53. package/dist/internal/direct-mcp-v2/projection.d.ts +40 -0
  54. package/dist/internal/direct-mcp-v2/projection.js +46 -0
  55. package/dist/internal/direct-mcp-v2/public-scenario-runtime.d.ts +51 -0
  56. package/dist/internal/direct-mcp-v2/public-scenario-runtime.js +353 -0
  57. package/dist/internal/direct-mcp-v2/runtime-lock.d.ts +51 -0
  58. package/dist/internal/direct-mcp-v2/runtime-lock.js +203 -0
  59. package/dist/internal/direct-mcp-v2/runtime-profile.d.ts +28 -0
  60. package/dist/internal/direct-mcp-v2/runtime-profile.js +163 -0
  61. package/dist/internal/direct-mcp-v2/scenario-http-host.d.ts +24 -0
  62. package/dist/internal/direct-mcp-v2/scenario-http-host.js +254 -0
  63. package/dist/internal/direct-mcp-v2/types.d.ts +233 -0
  64. package/dist/internal/direct-mcp-v2/types.js +1 -0
  65. package/dist/pathgrade.js +22 -1
  66. package/dist/reporters/github-comment.d.ts +12 -3
  67. package/dist/reporters/github-comment.js +80 -9
  68. package/dist/reporting/artifacts.js +5 -2
  69. package/dist/sdk/agent.js +56 -4
  70. package/dist/sdk/evaluate.d.ts +2 -0
  71. package/dist/sdk/evaluate.js +20 -12
  72. package/dist/sdk/index.d.ts +6 -0
  73. package/dist/sdk/index.js +4 -0
  74. package/dist/sdk/lifecycle.js +8 -3
  75. package/dist/sdk/managed-session.d.ts +6 -0
  76. package/dist/sdk/managed-session.js +157 -50
  77. package/dist/sdk/scenario-evidence.d.ts +27 -0
  78. package/dist/sdk/scenario-evidence.js +94 -0
  79. package/dist/sdk/scenario-machine-v2.d.ts +9 -0
  80. package/dist/sdk/scenario-machine-v2.js +37 -0
  81. package/dist/sdk/snapshots.d.ts +3 -0
  82. package/dist/sdk/snapshots.js +2 -0
  83. package/dist/sdk/types.d.ts +11 -1
  84. package/dist/tool-event-results.d.ts +1 -1
  85. package/dist/tool-event-results.js +2 -1
  86. package/dist/tool-events.d.ts +5 -0
  87. package/dist/tool-events.js +5 -0
  88. package/dist/types.d.ts +8 -0
  89. 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?: McpServersObject;
34
+ mcpServers?: Options['mcpServers'];
36
35
  /** Per-turn cancellation controller linked to PathGrade's turn timeout. */
37
36
  abortController?: AbortController;
38
37
  }
@@ -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?.scriptedMcpHost?.claudeServers
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 sensitiveValues = collectSensitiveEnvValues(runtimeEnv);
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
- if (process.env.PATHGRADE_CODEX_DEBUG) {
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
- scriptedHost?.failProtocol('turn/start did not return an authoritative turn id');
339
- turn.signalFailure?.('turn/start did not return an authoritative turn id');
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
- await closeUncorrelatedTurnTransport({
403
- turnStartCanReuseTransport,
404
- authoritativeTurnId: turn.authoritativeTurnId,
405
- retireTransport,
406
- currentHandle: handle,
407
- turnHandle,
408
- reset: () => {
409
- sessionRetired ||= retireTransport;
410
- handle = null;
411
- threadId = null;
412
- authoritativeTurnIds.clear();
413
- closeInfo = null;
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
- ? message
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 ? message : '',
603
- visibleAssistantMessage: exitCode === 0 ? message : '',
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
+ }
@@ -25,6 +25,7 @@ export class CodexAgent extends TranscriptAgent {
25
25
  exitCode: result.exitCode,
26
26
  traceOutput: rawOutput,
27
27
  toolEvents,
28
+ resolvedModel: resolveCodexModel(options?.model),
28
29
  };
29
30
  }
30
31
  }