@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
@@ -0,0 +1,436 @@
1
+ import { copyFile, mkdir, mkdtemp, realpath, rm } from 'node:fs/promises';
2
+ import { homedir, tmpdir } from 'node:os';
3
+ import { dirname, join } from 'node:path';
4
+ import { randomBytes } from 'node:crypto';
5
+ import { compileScenario } from './compiler.js';
6
+ import { IdempotentTrialCleaner, persistTrialArtifact, TrialExecutionError, TrialResourceRegistry } from './cleanup.js';
7
+ import { PrivateReplayVault, replayPrivateEvidence, verifyEvidenceChain } from './evidence.js';
8
+ import { ScenarioHostReducer } from './host-reducer.js';
9
+ import { materializeProjection } from './materialize.js';
10
+ import { projectEndpoints } from './projection.js';
11
+ import { admitRuntimeProfile } from './runtime-profile.js';
12
+ import { CODEX_DIRECT_MCP_DARWIN_ARM64_PROFILE, CODEX_DIRECT_MCP_NORMALIZED_HANDSHAKE } from './codex-profile.js';
13
+ import { createInvocationBarrier, startScenarioHttpHost } from './scenario-http-host.js';
14
+ import { spawnAppServerTransport } from '../../agents/codex-app-server/transport.js';
15
+ import { mergeCodexScenarioConfigs } from '../../agents/codex-app-server/scenario-mount.js';
16
+ export const CODEX_DIRECT_MCP_FLAG = 'PATHGRADE_INTERNAL_CODEX_DIRECT_MCP_V2';
17
+ const disabledProfiles = new Map();
18
+ export function isCodexDirectMcpSelected(environment = process.env) {
19
+ const fingerprint = CODEX_DIRECT_MCP_DARWIN_ARM64_PROFILE.profileFingerprint;
20
+ return environment[CODEX_DIRECT_MCP_FLAG] === '1' && !disabledProfiles.has(fingerprint);
21
+ }
22
+ export function codexDirectMcpDisableReason() {
23
+ return disabledProfiles.get(CODEX_DIRECT_MCP_DARWIN_ARM64_PROFILE.profileFingerprint);
24
+ }
25
+ /** Test-only reset for proving the fail-closed process latch. */
26
+ // oxlint-disable-next-line inhuman/no-empty-wrappers -- This test seam exposes Map.clear without exposing mutable production state.
27
+ export function __resetCodexDirectMcpDisablementForTesting() {
28
+ disabledProfiles.clear();
29
+ }
30
+ export async function runCodexDirectMcpTrial(options) {
31
+ const environment = options.environment ?? process.env;
32
+ const profile = CODEX_DIRECT_MCP_DARWIN_ARM64_PROFILE;
33
+ if (!isCodexDirectMcpSelected(environment)) {
34
+ const reason = disabledProfiles.get(profile.profileFingerprint);
35
+ throw new Error(reason ? `Codex direct MCP disabled for admitted profile: ${reason}` : `${CODEX_DIRECT_MCP_FLAG} is disabled`);
36
+ }
37
+ try {
38
+ return await executeTrial({ ...options, environment });
39
+ }
40
+ catch (error) {
41
+ const reason = error instanceof Error ? error.message : String(error);
42
+ disabledProfiles.set(profile.profileFingerprint, reason.slice(0, 500));
43
+ throw error;
44
+ }
45
+ }
46
+ async function executeTrial(options) {
47
+ const compiled = compileScenario(options.scenario);
48
+ if (!compiled.ok)
49
+ throw new Error(`Scenario compilation failed: ${JSON.stringify(compiled.diagnostics)}`);
50
+ const profile = CODEX_DIRECT_MCP_DARWIN_ARM64_PROFILE;
51
+ const barrierCase = findEffectfulSuccessCase(compiled.artifact);
52
+ const createdScratchParent = await mkdtemp(join(tmpdir(), 'pathgrade-codex-direct-mcp-'));
53
+ let scratchParent;
54
+ try {
55
+ scratchParent = await realpath(createdScratchParent);
56
+ }
57
+ catch (error) {
58
+ await rm(createdScratchParent, { recursive: true, force: true }).catch(() => undefined);
59
+ throw error;
60
+ }
61
+ const scratchPath = join(scratchParent, 'trial');
62
+ const workspacePath = join(scratchPath, 'workspace');
63
+ const codexHome = join(scratchPath, 'codex-home');
64
+ try {
65
+ await mkdir(workspacePath, { recursive: true });
66
+ await mkdir(codexHome, { recursive: true });
67
+ }
68
+ catch (error) {
69
+ await rm(scratchParent, { recursive: true, force: true }).catch(() => undefined);
70
+ throw error;
71
+ }
72
+ const trialId = `codex-${randomBytes(12).toString('hex')}`;
73
+ const macKey = randomBytes(32);
74
+ const replayVault = new PrivateReplayVault(randomBytes(32));
75
+ const reducer = new ScenarioHostReducer(trialId, compiled.artifact, macKey, replayVault);
76
+ reducer.markCompiled();
77
+ const barrier = createInvocationBarrier(barrierCase.caseId);
78
+ const host = await startScenarioHttpHost({ artifact: compiled.artifact, reducer, barrier }).catch(async (error) => {
79
+ macKey.fill(0);
80
+ replayVault.dispose();
81
+ reducer.dispose();
82
+ await rm(scratchParent, { recursive: true, force: true }).catch(() => undefined);
83
+ throw error;
84
+ });
85
+ const registry = new TrialResourceRegistry();
86
+ let handle;
87
+ let transport;
88
+ let hostOpen = true;
89
+ let controlPlaneOpen = false;
90
+ let hostQuiescentBeforeCleanup = false;
91
+ registry.register({ opaqueId: 'scenario-host', kind: 'listener', close: async () => { await host.dispose(); hostOpen = false; }, isOpen: () => hostOpen });
92
+ registry.register({ opaqueId: 'scenario-transports', kind: 'transport', close: () => host.dispose(), isOpen: () => host.activeTransportCount() !== 0 });
93
+ let persisted = false;
94
+ const artifactDirectory = options.artifactDirectory;
95
+ const cleaner = new IdempotentTrialCleaner({
96
+ registry,
97
+ scanRoots: [workspacePath, codexHome],
98
+ scratchPath,
99
+ approvedScratchParent: scratchParent,
100
+ bearer: host.bearer.toString(),
101
+ persistRedacted: async (report, phase) => {
102
+ if (!artifactDirectory) {
103
+ if (phase === 'final')
104
+ persisted = true;
105
+ return;
106
+ }
107
+ const path = join(artifactDirectory, 'trial-evidence.json');
108
+ await mkdir(dirname(path), { recursive: true });
109
+ const value = { artifactDigest: compiled.artifact.artifactDigest, evidence: reducer.ledger.events(), cleanup: { ...report, scans: report.scans.map((entry) => ({ ...entry, path: entry.path.replace(scratchParent, '<scratch>') })) } };
110
+ await persistTrialArtifact(path, `${JSON.stringify(value)}\n`, phase);
111
+ if (phase === 'final')
112
+ persisted = true;
113
+ },
114
+ zeroBearer: () => { host.bearer.fill(0); macKey.fill(0); replayVault.dispose(); },
115
+ });
116
+ try {
117
+ const runtimeEnv = await prepareCodexEnvironment(options.environment, codexHome);
118
+ const admission = await admitRuntimeProfile(profile, {
119
+ provider: 'codex',
120
+ platform: process.platform,
121
+ architecture: process.arch,
122
+ environment: runtimeEnv,
123
+ forbiddenSecrets: [host.bearer.toString(), `Bearer ${host.bearer.toString()}`],
124
+ initializeControlPlane: async () => {
125
+ handle = options.createControlPlane?.({ cwd: workspacePath, env: runtimeEnv, binary: profile.command.absolutePath })
126
+ ?? spawnAppServerTransport({
127
+ binary: profile.command.absolutePath,
128
+ args: profile.command.args,
129
+ cwd: workspacePath,
130
+ env: runtimeEnv,
131
+ logStderrOnExit: false,
132
+ });
133
+ controlPlaneOpen = true;
134
+ transport = handle.transport;
135
+ try {
136
+ const initialized = await transport.sendRequest('initialize', {
137
+ clientInfo: { name: 'pathgrade-direct-mcp', version: '2.0.0', title: null },
138
+ capabilities: { experimentalApi: true, optOutNotificationMethods: null },
139
+ });
140
+ const admittedVersion = profile.expectedVersion.replace(/^codex-cli\s+/, '');
141
+ if (!initialized.userAgent?.includes(admittedVersion))
142
+ throw new Error('Codex initialize userAgent did not match admitted version');
143
+ transport.sendNotification('initialized', null);
144
+ return {
145
+ normalizedHandshake: CODEX_DIRECT_MCP_NORMALIZED_HANDSHAKE,
146
+ capabilities: profile.requiredCapabilities,
147
+ close: async () => { await handle?.close(); controlPlaneOpen = false; },
148
+ };
149
+ }
150
+ catch (error) {
151
+ await handle.close().catch(() => undefined);
152
+ controlPlaneOpen = false;
153
+ throw error;
154
+ }
155
+ },
156
+ });
157
+ if (!admission.ok || !transport || !handle) {
158
+ throw new Error(`RUNTIME_NOT_ADMITTED: ${admission.ok ? 'control plane unavailable' : admission.reason}`);
159
+ }
160
+ const admittedTransport = transport;
161
+ const admittedHandle = handle;
162
+ registry.register({ opaqueId: 'codex-app-server', kind: 'process', close: async () => { await admittedHandle.close(); controlPlaneOpen = false; }, isOpen: () => controlPlaneOpen });
163
+ reducer.markAdmitted();
164
+ const projected = projectEndpoints(host.endpoints, admission.profile);
165
+ if (!Array.isArray(projected))
166
+ throw new Error(`Projection rejected: ${projected.reason}`);
167
+ const configs = projected.map((plan) => materializeProjection(plan, {
168
+ resolve(secretRef) {
169
+ if (secretRef !== host.secretRef)
170
+ throw new Error(`Unknown secret reference: ${secretRef}`);
171
+ return `Bearer ${host.bearer.toString()}`;
172
+ },
173
+ }));
174
+ const mcpConfig = mergeCodexScenarioConfigs(configs);
175
+ reducer.markReady();
176
+ const started = await admittedTransport.sendRequest('thread/start', {
177
+ cwd: workspacePath,
178
+ approvalPolicy: 'never',
179
+ sandbox: 'read-only',
180
+ ephemeral: true,
181
+ model: options.model ?? options.environment.PATHGRADE_DIRECT_MCP_CODEX_MODEL ?? 'gpt-5.6-luna',
182
+ config: mcpConfig,
183
+ });
184
+ const threadId = started.thread.id;
185
+ const serverId = host.endpoints[0]?.serverId;
186
+ if (!serverId)
187
+ throw new Error('Scenario produced no Codex MCP endpoint');
188
+ reducer.beginTurn('direct-call', 'codex');
189
+ const directResult = await admittedTransport.sendRequest('mcpServer/tool/call', {
190
+ threadId, server: serverId, tool: barrierCase.pureToolId, arguments: barrierCase.pureArgs,
191
+ meta: { source: 'pathgrade-direct-mcp-contract' },
192
+ });
193
+ if (directResult.isError === true)
194
+ throw new Error('Direct MCP success call returned isError');
195
+ reducer.finalize('settled');
196
+ reducer.beginTurn('structured-error', 'codex');
197
+ const errorResult = await admittedTransport.sendRequest('mcpServer/tool/call', {
198
+ threadId, server: serverId, tool: barrierCase.effectfulToolId, arguments: barrierCase.errorArgs,
199
+ meta: { source: 'pathgrade-direct-mcp-contract' },
200
+ });
201
+ if (errorResult.isError !== true || !errorResult.structuredContent?.pathgrade?.code)
202
+ throw new Error('Codex did not preserve the structured scenario tool error');
203
+ reducer.finalize('settled');
204
+ reducer.beginTurn('cancelled-turn', 'codex');
205
+ const providerSignals = [];
206
+ const turnCompletions = createTurnCompletionMonitor(admittedTransport);
207
+ const stopCollectingSignals = admittedTransport.onNotification((notification) => {
208
+ // SAFETY: only optional diagnostic fields are read from the exact-pinned Codex notification schema.
209
+ const params = notification.params;
210
+ if (notification.method === 'item/completed')
211
+ providerSignals.push(`item:${params?.item?.type ?? 'unknown'}:${params?.item?.status ?? 'unknown'}:${params?.item?.server ?? ''}:${params?.item?.tool ?? ''}`);
212
+ if (notification.method === 'turn/completed')
213
+ providerSignals.push(`turn:${params?.turn?.status ?? params?.status ?? 'unknown'}`);
214
+ if (notification.method === 'mcpServer/startupStatus/updated')
215
+ providerSignals.push(`startup:${params?.name ?? 'unknown'}:${params?.status ?? 'unknown'}:${params?.error ? 'error' : 'clean'}`);
216
+ });
217
+ let observingProvider = true;
218
+ registry.register({
219
+ opaqueId: 'codex-provider-observers',
220
+ kind: 'descriptor',
221
+ close: async () => {
222
+ if (!observingProvider)
223
+ return;
224
+ stopCollectingSignals();
225
+ turnCompletions.dispose();
226
+ observingProvider = false;
227
+ },
228
+ isOpen: () => observingProvider,
229
+ });
230
+ const turnStarted = await admittedTransport.sendRequest('turn/start', {
231
+ threadId,
232
+ input: [{
233
+ type: 'text',
234
+ text: `Mandatory contract action: invoke the ${serverId}.${barrierCase.effectfulToolId} MCP tool exactly once with arguments ${JSON.stringify(barrierCase.successArgs)}. Do not answer with text before the tool finishes, do not call another tool, and do not use shell commands or edit files. A response without this MCP invocation is invalid.`,
235
+ text_elements: [],
236
+ }],
237
+ });
238
+ const turnId = turnStarted.turn?.id ?? turnStarted.turnId;
239
+ if (!turnId)
240
+ throw new Error('Codex turn/start returned no turn id');
241
+ const terminal = turnCompletions.waitFor(turnId, 120_000);
242
+ const entered = await withTimeout(Promise.race([
243
+ barrier.reached().then(() => 'barrier'),
244
+ terminal.then((status) => `terminal:${status}`),
245
+ ]), 90_000, 'Codex produced neither a blocking MCP invocation nor a terminal');
246
+ if (entered !== 'barrier')
247
+ throw new Error(`Codex reached ${entered} before the blocking MCP transition (${providerSignals.join(',')})`);
248
+ reducer.fence();
249
+ await withTimeout(admittedTransport.sendRequest('turn/interrupt', { threadId, turnId }), 15_000, 'Timed out sending Codex turn interrupt');
250
+ reducer.abortUncommitted('provider-cancelled');
251
+ barrier.release();
252
+ const [cancellationStatus] = await Promise.all([terminal, host.waitForQuiescence(30_000)]);
253
+ hostQuiescentBeforeCleanup = host.activeTransportCount() === 0;
254
+ if (!hostQuiescentBeforeCleanup)
255
+ throw new Error('Scenario host was not quiescent after cancellation');
256
+ stopCollectingSignals();
257
+ turnCompletions.dispose();
258
+ observingProvider = false;
259
+ reducer.observeProviderTerminal();
260
+ if (cancellationStatus !== 'interrupted')
261
+ throw new Error(`Codex cancellation terminal was ${cancellationStatus}`);
262
+ reducer.finalize('cancelled-clean');
263
+ reducer.beginTurn('post-cancel-reuse', 'codex');
264
+ const reused = await admittedTransport.sendRequest('mcpServer/tool/call', {
265
+ threadId, server: serverId, tool: barrierCase.effectfulToolId, arguments: barrierCase.successArgs,
266
+ meta: { source: 'pathgrade-direct-mcp-contract' },
267
+ });
268
+ if (reused.isError === true)
269
+ throw new Error('Post-cancel direct MCP reuse failed');
270
+ reducer.finalize('settled');
271
+ const replay = replayPrivateEvidence({ artifact: compiled.artifact, evidence: reducer.ledger.events(), vault: replayVault, macKey });
272
+ if (!replay.ok || replay.poisoned || replay.finalState !== reducer.state)
273
+ throw new Error(`Private replay failed: ${replay.ok ? 'state mismatch' : replay.reason}`);
274
+ const chain = verifyEvidenceChain(reducer.ledger.events(), compiled.artifact.artifactDigest);
275
+ if (!chain.ok)
276
+ throw new Error(`Evidence chain failed: ${chain.reason}`);
277
+ reducer.dispose();
278
+ const cleanup = await cleaner.dispose();
279
+ await rm(scratchParent, { recursive: true, force: true });
280
+ if (!persisted || cleanup.failures.length > 0 || cleanup.bearerFound || !cleanup.scratchDeleted)
281
+ throw new Error(`CLEANUP_FAILED: ${JSON.stringify(cleanup.failures)}`);
282
+ return {
283
+ selected: true,
284
+ profileFingerprint: profile.profileFingerprint,
285
+ artifact: compiled.artifact,
286
+ evidence: reducer.ledger.events(),
287
+ checks: {
288
+ runtimeAdmitted: true,
289
+ secretFreeProject: true,
290
+ directCall: true,
291
+ structuredToolError: true,
292
+ fencedCancellation: true,
293
+ providerTerminal: true,
294
+ hostQuiescent: hostQuiescentBeforeCleanup,
295
+ postCancelReuse: true,
296
+ privateReplay: true,
297
+ cleanup: true,
298
+ },
299
+ cleanup,
300
+ cancellationStatus,
301
+ };
302
+ }
303
+ catch (error) {
304
+ barrier.release();
305
+ try {
306
+ if (reducer.phase === 'ACTIVE')
307
+ reducer.fence();
308
+ if (reducer.phase === 'FENCING')
309
+ reducer.abortUncommitted('trial-failed');
310
+ if (reducer.phase !== 'DISPOSED' && reducer.phase !== 'NEW' && reducer.phase !== 'COMPILED' && reducer.phase !== 'ADMITTED')
311
+ reducer.finalize('poisoned', 'trial-failed');
312
+ }
313
+ catch (lifecycleFailure) {
314
+ // Preserve the primary failure; the cleaner reports any remaining resources.
315
+ void lifecycleFailure;
316
+ }
317
+ reducer.dispose();
318
+ let cleanupError;
319
+ try {
320
+ const report = await cleaner.dispose();
321
+ if (report.failures.length > 0 || report.bearerFound || !report.scratchDeleted)
322
+ cleanupError = new Error(`CLEANUP_FAILED: ${JSON.stringify(report.failures)}`);
323
+ }
324
+ catch (failure) {
325
+ cleanupError = failure instanceof Error ? failure : new Error(String(failure));
326
+ }
327
+ await rm(scratchParent, { recursive: true, force: true }).catch(() => undefined);
328
+ if (cleanupError)
329
+ throw new TrialExecutionError(error instanceof Error ? error.message : 'Codex direct MCP trial failed', { primary: error, secondary: [cleanupError] });
330
+ throw error;
331
+ }
332
+ }
333
+ async function prepareCodexEnvironment(environment, codexHome) {
334
+ const result = mapCodexProviderEnvironment(environment, codexHome);
335
+ if (!result.OPENAI_API_KEY) {
336
+ const source = join(environment.CODEX_HOME || join(homedir(), '.codex'), 'auth.json');
337
+ const destination = join(codexHome, 'auth.json');
338
+ try {
339
+ await copyFile(source, destination);
340
+ }
341
+ catch (error) {
342
+ throw new Error(`Codex live cancellation requires OPENAI_API_KEY or readable managed auth at ${source}: ${error instanceof Error ? error.message : String(error)}`);
343
+ }
344
+ }
345
+ return result;
346
+ }
347
+ export function mapCodexProviderEnvironment(environment, codexHome) {
348
+ const { APP_OPENAI_API_KEY: appApiKey, APP_OPENAI_BASE_URL: appBaseUrl, ...providerEnvironment } = environment;
349
+ const result = { ...providerEnvironment, CODEX_HOME: codexHome };
350
+ const apiKey = environment.OPENAI_API_KEY ?? appApiKey;
351
+ const baseUrl = environment.OPENAI_BASE_URL ?? appBaseUrl;
352
+ if (apiKey)
353
+ result.OPENAI_API_KEY = apiKey;
354
+ if (baseUrl)
355
+ result.OPENAI_BASE_URL = baseUrl;
356
+ return result;
357
+ }
358
+ function findEffectfulSuccessCase(artifact) {
359
+ const state = artifact.canonicalIr.states[artifact.canonicalIr.initialState];
360
+ const entries = Object.entries(state.tools);
361
+ const effectful = entries.find(([, tool]) => tool.effectful && tool.cases.some((entry) => entry.outcome.kind === 'success' && 'exactArgs' in entry.when && entry.transition));
362
+ const error = entries.find(([, tool]) => tool.cases.some((entry) => entry.outcome.kind === 'tool-error' && 'exactArgs' in entry.when));
363
+ const pure = entries.find(([, tool]) => !tool.effectful && tool.cases.some((entry) => entry.outcome.kind === 'success' && 'exactArgs' in entry.when || 'all' in entry.when));
364
+ if (!effectful || !error || !pure)
365
+ throw new Error('Codex vertical requires an effectful success, structured error, and pure success in the initial state');
366
+ const successCase = effectful[1].cases.find((entry) => entry.outcome.kind === 'success' && 'exactArgs' in entry.when && entry.transition);
367
+ const errorCase = error[1].cases.find((entry) => entry.outcome.kind === 'tool-error' && 'exactArgs' in entry.when);
368
+ const pureCase = pure[1].cases.find((entry) => entry.outcome.kind === 'success');
369
+ return {
370
+ caseId: successCase.id,
371
+ effectfulToolId: effectful[0].split('/')[1],
372
+ pureToolId: pure[0].split('/')[1],
373
+ successArgs: 'exactArgs' in successCase.when ? successCase.when.exactArgs : {},
374
+ errorArgs: 'exactArgs' in errorCase.when ? errorCase.when.exactArgs : {},
375
+ pureArgs: 'exactArgs' in pureCase.when ? pureCase.when.exactArgs : deriveArgs(pureCase.when.all),
376
+ };
377
+ }
378
+ function deriveArgs(predicates) {
379
+ const result = {};
380
+ for (const predicate of predicates) {
381
+ if (predicate.pointer.startsWith('/') && 'equals' in predicate)
382
+ result[predicate.pointer.slice(1)] = predicate.equals;
383
+ }
384
+ return result;
385
+ }
386
+ function createTurnCompletionMonitor(transport) {
387
+ const completed = new Map();
388
+ const waiters = new Map();
389
+ const off = transport.onNotification((notification) => {
390
+ if (notification.method !== 'turn/completed')
391
+ return;
392
+ // SAFETY: the exact-pinned Codex schema permits nested and legacy flattened terminal fields.
393
+ const params = notification.params;
394
+ const turnId = params?.turn?.id ?? params?.turnId;
395
+ if (!turnId)
396
+ return;
397
+ const status = params?.turn?.status ?? params?.status ?? 'unknown';
398
+ const waiter = waiters.get(turnId);
399
+ if (waiter) {
400
+ waiters.delete(turnId);
401
+ clearTimeout(waiter.timer);
402
+ waiter.resolve(status);
403
+ }
404
+ else
405
+ completed.set(turnId, status);
406
+ });
407
+ return {
408
+ waitFor(turnId, timeoutMs) {
409
+ const status = completed.get(turnId);
410
+ if (status !== undefined) {
411
+ completed.delete(turnId);
412
+ return Promise.resolve(status);
413
+ }
414
+ return new Promise((resolve, reject) => {
415
+ const timer = setTimeout(() => {
416
+ waiters.delete(turnId);
417
+ reject(new Error('Timed out waiting for Codex turn terminal'));
418
+ }, timeoutMs);
419
+ waiters.set(turnId, { resolve, timer });
420
+ });
421
+ },
422
+ dispose() {
423
+ off();
424
+ completed.clear();
425
+ for (const waiter of waiters.values())
426
+ clearTimeout(waiter.timer);
427
+ waiters.clear();
428
+ },
429
+ };
430
+ }
431
+ async function withTimeout(promise, timeoutMs, message) {
432
+ let timer;
433
+ return Promise.race([promise, new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(message)), timeoutMs); })])
434
+ .finally(() => { if (timer)
435
+ clearTimeout(timer); });
436
+ }
@@ -0,0 +1,12 @@
1
+ import type { RuntimeLock } from './runtime-lock.js';
2
+ import type { JsonObject, RuntimeCapabilityProfile } from './types.js';
3
+ export declare const CODEX_DIRECT_MCP_NORMALIZED_HANDSHAKE: JsonObject;
4
+ /** Reviewed macOS arm64 pin captured by RFC 0006. Other platforms fail closed. */
5
+ export declare const CODEX_DIRECT_MCP_DARWIN_ARM64_PROFILE: RuntimeCapabilityProfile;
6
+ /** Code-owned relocatable identity lock for the admitted public Codex slice. */
7
+ export declare const CODEX_DIRECT_MCP_DARWIN_ARM64_LOCK: RuntimeLock;
8
+ /**
9
+ * Resolve one Codex candidate before admission. Selection is deterministic;
10
+ * identity failure never falls through to another process.
11
+ */
12
+ export declare function resolveCodexScenarioExecutable(environment: Readonly<Record<string, string | undefined>>): Promise<string>;
@@ -0,0 +1,81 @@
1
+ import { access, realpath } from 'node:fs/promises';
2
+ import { constants } from 'node:fs';
3
+ import { delimiter, join } from 'node:path';
4
+ export const CODEX_DIRECT_MCP_NORMALIZED_HANDSHAKE = {
5
+ normalizerVersion: 1,
6
+ provider: 'codex',
7
+ controlPlane: 'app-server',
8
+ streamableHttp: true,
9
+ controlPlaneHeaders: true,
10
+ cancellation: 'turn-interrupt',
11
+ protocolMethods: ['mcpServer/tool/call', 'thread/start', 'turn/interrupt'],
12
+ };
13
+ /** Reviewed macOS arm64 pin captured by RFC 0006. Other platforms fail closed. */
14
+ export const CODEX_DIRECT_MCP_DARWIN_ARM64_PROFILE = {
15
+ format: 'pathgrade-direct-mcp-runtime-profile-v1',
16
+ provider: 'codex',
17
+ platform: 'darwin',
18
+ architecture: 'arm64',
19
+ controlPlane: 'app-server',
20
+ command: {
21
+ absolutePath: '/Applications/ChatGPT.app/Contents/Resources/codex',
22
+ args: [],
23
+ versionArgs: ['--version'],
24
+ },
25
+ expectedVersion: 'codex-cli 0.151.0-alpha.7.1',
26
+ artifacts: [{
27
+ label: 'codex-cli-0.151.0-alpha.7.1',
28
+ absolutePath: '/Applications/ChatGPT.app/Contents/Resources/codex',
29
+ sha256: 'a5976fd714dc0801a6f40e0e2b3051f64f1e0468f6c0f279b7d2a7afb7623f43',
30
+ }],
31
+ requiredCapabilities: {
32
+ streamableHttp: true,
33
+ controlPlaneHeaders: true,
34
+ cancellation: 'turn-interrupt',
35
+ },
36
+ profileFingerprint: '79ed563e36c65404b385e2a8ebd1c02e33e4c6f86dac589a94b32d4a0cf7f169',
37
+ runtimeCapabilityFingerprint: '2ea3ca12b39fb6de97464ab539be3986d5fe2f9fda51b718198128a55ba11f27',
38
+ };
39
+ /** Code-owned relocatable identity lock for the admitted public Codex slice. */
40
+ export const CODEX_DIRECT_MCP_DARWIN_ARM64_LOCK = {
41
+ format: 'pathgrade-direct-mcp-runtime-lock-v1',
42
+ provider: 'codex',
43
+ platform: 'darwin',
44
+ architecture: 'arm64',
45
+ controlPlane: 'app-server',
46
+ version: CODEX_DIRECT_MCP_DARWIN_ARM64_PROFILE.expectedVersion,
47
+ commandArtifact: 'codex-cli-0.151.0-alpha.7.1',
48
+ args: CODEX_DIRECT_MCP_DARWIN_ARM64_PROFILE.command.args,
49
+ versionArgs: CODEX_DIRECT_MCP_DARWIN_ARM64_PROFILE.command.versionArgs,
50
+ artifacts: [{
51
+ id: 'codex-cli-0.151.0-alpha.7.1',
52
+ relativePath: 'codex',
53
+ sha256: CODEX_DIRECT_MCP_DARWIN_ARM64_PROFILE.artifacts[0].sha256,
54
+ }],
55
+ capabilities: CODEX_DIRECT_MCP_DARWIN_ARM64_PROFILE.requiredCapabilities,
56
+ normalizedHandshakeSha256: CODEX_DIRECT_MCP_DARWIN_ARM64_PROFILE.runtimeCapabilityFingerprint,
57
+ };
58
+ /**
59
+ * Resolve one Codex candidate before admission. Selection is deterministic;
60
+ * identity failure never falls through to another process.
61
+ */
62
+ export async function resolveCodexScenarioExecutable(environment) {
63
+ const bundled = CODEX_DIRECT_MCP_DARWIN_ARM64_PROFILE.command.absolutePath;
64
+ if (await isExecutable(bundled))
65
+ return realpath(bundled);
66
+ for (const directory of (environment.PATH ?? '').split(delimiter).filter(Boolean)) {
67
+ const candidate = join(directory, 'codex');
68
+ if (await isExecutable(candidate))
69
+ return realpath(candidate);
70
+ }
71
+ throw new Error('RUNTIME_NOT_ADMITTED: no Codex executable was discovered');
72
+ }
73
+ async function isExecutable(path) {
74
+ try {
75
+ await access(path, constants.X_OK);
76
+ return true;
77
+ }
78
+ catch {
79
+ return false;
80
+ }
81
+ }
@@ -0,0 +1,12 @@
1
+ import type { JsonObject, MatchExpr, ScenarioArtifact, ScenarioDiagnostic } from './types.js';
2
+ export type CompileScenarioResult = {
3
+ ok: true;
4
+ artifact: ScenarioArtifact;
5
+ } | {
6
+ ok: false;
7
+ diagnostics: ScenarioDiagnostic[];
8
+ };
9
+ export declare function matchExpression(args: JsonObject, expression: MatchExpr): boolean;
10
+ export declare function compileScenario(source: unknown): CompileScenarioResult;
11
+ export declare function compileScenarioText(text: string): CompileScenarioResult;
12
+ export declare function isSha256(value: unknown): value is string;