@wix/pathgrade 1.0.25 → 1.0.26

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 (75) hide show
  1. package/README.md +42 -0
  2. package/dist/agents/claude/sdk-options.d.ts +1 -2
  3. package/dist/agents/claude.js +2 -1
  4. package/dist/agents/codex-app-server/agent.js +83 -46
  5. package/dist/agents/codex-app-server/scenario-mount.d.ts +40 -0
  6. package/dist/agents/codex-app-server/scenario-mount.js +238 -0
  7. package/dist/agents/cursor-scenario.d.ts +20 -0
  8. package/dist/agents/cursor-scenario.js +96 -0
  9. package/dist/agents/cursor.js +20 -0
  10. package/dist/agents/opencode/scenario.d.ts +25 -0
  11. package/dist/agents/opencode/scenario.js +195 -0
  12. package/dist/agents/opencode.js +50 -0
  13. package/dist/internal/direct-mcp-v2/acp-author-projector.d.ts +20 -0
  14. package/dist/internal/direct-mcp-v2/acp-author-projector.js +147 -0
  15. package/dist/internal/direct-mcp-v2/acp-author-session.d.ts +39 -0
  16. package/dist/internal/direct-mcp-v2/acp-author-session.js +201 -0
  17. package/dist/internal/direct-mcp-v2/acp-client.d.ts +70 -0
  18. package/dist/internal/direct-mcp-v2/acp-client.js +310 -0
  19. package/dist/internal/direct-mcp-v2/acp-direct-mcp.d.ts +38 -0
  20. package/dist/internal/direct-mcp-v2/acp-direct-mcp.js +326 -0
  21. package/dist/internal/direct-mcp-v2/claude-direct-mcp.d.ts +68 -0
  22. package/dist/internal/direct-mcp-v2/claude-direct-mcp.js +456 -0
  23. package/dist/internal/direct-mcp-v2/claude-profile.d.ts +9 -0
  24. package/dist/internal/direct-mcp-v2/claude-profile.js +97 -0
  25. package/dist/internal/direct-mcp-v2/cleanup.d.ts +62 -0
  26. package/dist/internal/direct-mcp-v2/cleanup.js +182 -0
  27. package/dist/internal/direct-mcp-v2/codex-direct-mcp.d.ts +41 -0
  28. package/dist/internal/direct-mcp-v2/codex-direct-mcp.js +436 -0
  29. package/dist/internal/direct-mcp-v2/codex-profile.d.ts +12 -0
  30. package/dist/internal/direct-mcp-v2/codex-profile.js +81 -0
  31. package/dist/internal/direct-mcp-v2/compiler.d.ts +12 -0
  32. package/dist/internal/direct-mcp-v2/compiler.js +449 -0
  33. package/dist/internal/direct-mcp-v2/cursor-direct-mcp.d.ts +14 -0
  34. package/dist/internal/direct-mcp-v2/cursor-direct-mcp.js +60 -0
  35. package/dist/internal/direct-mcp-v2/cursor-profile.d.ts +38 -0
  36. package/dist/internal/direct-mcp-v2/cursor-profile.js +79 -0
  37. package/dist/internal/direct-mcp-v2/evidence.d.ts +50 -0
  38. package/dist/internal/direct-mcp-v2/evidence.js +147 -0
  39. package/dist/internal/direct-mcp-v2/host-reducer.d.ts +61 -0
  40. package/dist/internal/direct-mcp-v2/host-reducer.js +214 -0
  41. package/dist/internal/direct-mcp-v2/json.d.ts +9 -0
  42. package/dist/internal/direct-mcp-v2/json.js +220 -0
  43. package/dist/internal/direct-mcp-v2/materialize.d.ts +39 -0
  44. package/dist/internal/direct-mcp-v2/materialize.js +22 -0
  45. package/dist/internal/direct-mcp-v2/opencode-direct-mcp.d.ts +14 -0
  46. package/dist/internal/direct-mcp-v2/opencode-direct-mcp.js +71 -0
  47. package/dist/internal/direct-mcp-v2/opencode-profile.d.ts +8 -0
  48. package/dist/internal/direct-mcp-v2/opencode-profile.js +52 -0
  49. package/dist/internal/direct-mcp-v2/projection.d.ts +40 -0
  50. package/dist/internal/direct-mcp-v2/projection.js +46 -0
  51. package/dist/internal/direct-mcp-v2/public-scenario-runtime.d.ts +51 -0
  52. package/dist/internal/direct-mcp-v2/public-scenario-runtime.js +353 -0
  53. package/dist/internal/direct-mcp-v2/runtime-lock.d.ts +51 -0
  54. package/dist/internal/direct-mcp-v2/runtime-lock.js +203 -0
  55. package/dist/internal/direct-mcp-v2/runtime-profile.d.ts +28 -0
  56. package/dist/internal/direct-mcp-v2/runtime-profile.js +163 -0
  57. package/dist/internal/direct-mcp-v2/scenario-http-host.d.ts +24 -0
  58. package/dist/internal/direct-mcp-v2/scenario-http-host.js +254 -0
  59. package/dist/internal/direct-mcp-v2/types.d.ts +233 -0
  60. package/dist/internal/direct-mcp-v2/types.js +1 -0
  61. package/dist/sdk/agent.js +49 -2
  62. package/dist/sdk/evaluate.js +6 -3
  63. package/dist/sdk/index.d.ts +4 -0
  64. package/dist/sdk/index.js +2 -0
  65. package/dist/sdk/managed-session.d.ts +6 -0
  66. package/dist/sdk/managed-session.js +157 -50
  67. package/dist/sdk/scenario-evidence.d.ts +27 -0
  68. package/dist/sdk/scenario-evidence.js +94 -0
  69. package/dist/sdk/scenario-machine-v2.d.ts +9 -0
  70. package/dist/sdk/scenario-machine-v2.js +37 -0
  71. package/dist/sdk/snapshots.d.ts +3 -0
  72. package/dist/sdk/snapshots.js +2 -0
  73. package/dist/sdk/types.d.ts +9 -1
  74. package/dist/types.d.ts +6 -0
  75. 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
@@ -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 {
@@ -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
  }
@@ -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,8 +636,8 @@ 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,
@@ -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
+ }
@@ -0,0 +1,20 @@
1
+ import type { AgentSessionOptions } from '../types.js';
2
+ import type { AcpToolNameNormalizer } from '../internal/direct-mcp-v2/acp-author-projector.js';
3
+ import { attachAcpAuthorSession, type AcpAuthorSession } from '../internal/direct-mcp-v2/acp-author-session.js';
4
+ import { startScenarioCoordinator, type PublicScenarioRuntimeFactory } from '../internal/direct-mcp-v2/public-scenario-runtime.js';
5
+ import { resolveCursorExecutable } from '../internal/direct-mcp-v2/cursor-profile.js';
6
+ export declare function setCursorScenarioSession(options: AgentSessionOptions, session: AcpAuthorSession): void;
7
+ export declare function getCursorScenarioSession(options: AgentSessionOptions | undefined): AcpAuthorSession | undefined;
8
+ /** Cursor's admitted ACP build exposes MCP calls only as the stable title `MCP: tool`. */
9
+ export declare const normalizeCursorAcpToolName: AcpToolNameNormalizer;
10
+ type CursorScenarioRuntimeDependencies = {
11
+ attachAcpAuthorSession: typeof attachAcpAuthorSession;
12
+ resolveCursorExecutable: typeof resolveCursorExecutable;
13
+ startScenarioCoordinator: typeof startScenarioCoordinator;
14
+ };
15
+ export type CursorScenarioRuntimeFactoryOptions = {
16
+ requestedModel?: string;
17
+ };
18
+ export declare function createPublicCursorScenarioRuntimeFactory(factoryOptions?: CursorScenarioRuntimeFactoryOptions, dependencyOverrides?: Partial<CursorScenarioRuntimeDependencies>): PublicScenarioRuntimeFactory;
19
+ export declare const startPublicCursorScenarioRuntime: typeof import("../internal/direct-mcp-v2/public-scenario-runtime.js").startPublicClaudeScenarioRuntime;
20
+ export {};