@martinloop/mcp 0.7.1 → 0.8.1

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 (77) hide show
  1. package/dist/package-version.d.ts +1 -1
  2. package/dist/package-version.js +1 -1
  3. package/dist/vendor/adapters/claude-cli.d.ts +16 -1
  4. package/dist/vendor/adapters/claude-cli.js +286 -71
  5. package/dist/vendor/adapters/cli-bridge.d.ts +12 -3
  6. package/dist/vendor/adapters/cli-bridge.js +129 -149
  7. package/dist/vendor/adapters/codex-capabilities.d.ts +4 -1
  8. package/dist/vendor/adapters/codex-capabilities.js +36 -4
  9. package/dist/vendor/adapters/codex-cli.d.ts +10 -3
  10. package/dist/vendor/adapters/codex-cli.js +21 -24
  11. package/dist/vendor/adapters/codex-host.js +150 -132
  12. package/dist/vendor/adapters/index.d.ts +3 -2
  13. package/dist/vendor/adapters/index.js +3 -2
  14. package/dist/vendor/adapters/openai-compatible.js +88 -6
  15. package/dist/vendor/adapters/process-supervisor.d.ts +60 -0
  16. package/dist/vendor/adapters/process-supervisor.js +541 -0
  17. package/dist/vendor/adapters/verifier-only.d.ts +10 -1
  18. package/dist/vendor/adapters/verifier-only.js +43 -1
  19. package/dist/vendor/contracts/external-outcome.d.ts +102 -0
  20. package/dist/vendor/contracts/external-outcome.js +226 -0
  21. package/dist/vendor/contracts/index.d.ts +12 -1
  22. package/dist/vendor/contracts/index.js +2 -0
  23. package/dist/vendor/contracts/swarm-hosted.d.ts +204 -0
  24. package/dist/vendor/contracts/swarm-hosted.js +629 -0
  25. package/dist/vendor/contracts/swarm.d.ts +449 -0
  26. package/dist/vendor/contracts/swarm.js +550 -0
  27. package/dist/vendor/core/external-outcome.d.ts +34 -0
  28. package/dist/vendor/core/external-outcome.js +515 -0
  29. package/dist/vendor/core/grounding.js +1 -1
  30. package/dist/vendor/core/index.d.ts +52 -1
  31. package/dist/vendor/core/index.js +55 -3
  32. package/dist/vendor/core/leash.d.ts +1 -0
  33. package/dist/vendor/core/leash.js +13 -0
  34. package/dist/vendor/core/persistence/integrity.d.ts +4 -0
  35. package/dist/vendor/core/persistence/integrity.js +19 -6
  36. package/dist/vendor/core/persistence/swarm-integrity.d.ts +63 -0
  37. package/dist/vendor/core/persistence/swarm-integrity.js +309 -0
  38. package/dist/vendor/core/policy.d.ts +3 -0
  39. package/dist/vendor/core/policy.js +13 -0
  40. package/dist/vendor/core/swarm/candidates.d.ts +78 -0
  41. package/dist/vendor/core/swarm/candidates.js +810 -0
  42. package/dist/vendor/core/swarm/evidence-projection.d.ts +87 -0
  43. package/dist/vendor/core/swarm/evidence-projection.js +128 -0
  44. package/dist/vendor/core/swarm/evidence.d.ts +40 -0
  45. package/dist/vendor/core/swarm/evidence.js +575 -0
  46. package/dist/vendor/core/swarm/hosted-export.d.ts +48 -0
  47. package/dist/vendor/core/swarm/hosted-export.js +362 -0
  48. package/dist/vendor/core/swarm/hosted-projection.d.ts +5 -0
  49. package/dist/vendor/core/swarm/hosted-projection.js +258 -0
  50. package/dist/vendor/core/swarm/index.d.ts +110 -0
  51. package/dist/vendor/core/swarm/index.js +1060 -0
  52. package/dist/vendor/core/swarm/integration.d.ts +76 -0
  53. package/dist/vendor/core/swarm/integration.js +615 -0
  54. package/dist/vendor/core/swarm/lifecycle.d.ts +72 -0
  55. package/dist/vendor/core/swarm/lifecycle.js +252 -0
  56. package/dist/vendor/core/swarm/live-runtime.d.ts +78 -0
  57. package/dist/vendor/core/swarm/live-runtime.js +1482 -0
  58. package/dist/vendor/core/swarm/live-store.d.ts +99 -0
  59. package/dist/vendor/core/swarm/live-store.js +860 -0
  60. package/dist/vendor/core/swarm/operational-reader.d.ts +17 -0
  61. package/dist/vendor/core/swarm/operational-reader.js +96 -0
  62. package/dist/vendor/core/swarm/operations.d.ts +29 -0
  63. package/dist/vendor/core/swarm/operations.js +128 -0
  64. package/dist/vendor/core/swarm/parent-receipt.d.ts +13 -0
  65. package/dist/vendor/core/swarm/parent-receipt.js +489 -0
  66. package/dist/vendor/core/swarm/receipt-projection.d.ts +26 -0
  67. package/dist/vendor/core/swarm/receipt-projection.js +120 -0
  68. package/dist/vendor/core/swarm/scheduler.d.ts +92 -0
  69. package/dist/vendor/core/swarm/scheduler.js +555 -0
  70. package/dist/vendor/core/swarm/verification.d.ts +70 -0
  71. package/dist/vendor/core/swarm/verification.js +295 -0
  72. package/dist/vendor/core/swarm/workspaces.d.ts +77 -0
  73. package/dist/vendor/core/swarm/workspaces.js +488 -0
  74. package/dist/vendor/core/verified-handoff.d.ts +4 -0
  75. package/dist/vendor/core/verified-handoff.js +4 -0
  76. package/package.json +1 -1
  77. package/server.json +2 -2
@@ -1,146 +1,23 @@
1
- import { spawn } from "node:child_process";
2
1
  import { basename, delimiter, dirname, extname, isAbsolute, join, resolve } from "node:path";
3
2
  import { existsSync, readFileSync } from "node:fs";
4
3
  import { diffStatsFromNumstat } from "./runtime-support.js";
4
+ import { spawnSupervisedProcess } from "./process-supervisor.js";
5
5
  const gitRepositoryRootCache = new Map();
6
6
  export async function runSubprocess(command, args, options) {
7
- return new Promise((resolve) => {
8
- let timedOut = false;
9
- let outputCapped = false;
10
- let terminationReason;
11
- let settled = false;
12
- let exited = false;
13
- let outputBytes = 0;
14
- const stdoutChunks = [];
15
- const stderrChunks = [];
16
- const stdinMode = options.stdinData !== undefined ? "pipe" : "ignore";
17
- const resolveOnce = (result) => {
18
- if (settled) {
19
- return;
20
- }
21
- settled = true;
22
- resolve({ ...result, timedOut, outputCapped, ...(terminationReason ? { terminationReason } : {}) });
23
- };
24
- let proc;
25
- try {
26
- const spawnPlan = createSpawnPlan(command, args, options.cwd, options.spawnImpl !== undefined);
27
- proc = (options.spawnImpl ?? spawn)(spawnPlan.command, spawnPlan.args, {
28
- cwd: options.cwd,
29
- stdio: [stdinMode, "pipe", "pipe"],
30
- env: process.env
31
- });
32
- }
33
- catch (error) {
34
- const message = error instanceof Error ? error.message : String(error);
35
- resolveOnce({ exitCode: 1, stdout: "", stderr: message, launched: false, completed: false, crashed: true });
36
- return;
37
- }
38
- const trackOutput = (chunks, chunk) => {
39
- if (outputCapped || timedOut || terminationReason) {
40
- return;
41
- }
42
- chunks.push(chunk);
43
- outputBytes += chunk.byteLength;
44
- if (options.maxOutputBytes !== undefined && outputBytes > options.maxOutputBytes) {
45
- outputCapped = true;
46
- proc.kill("SIGTERM");
47
- }
48
- };
49
- const terminateEarly = (reason) => {
50
- if (terminationReason || timedOut || outputCapped) {
51
- return;
52
- }
53
- terminationReason = reason;
54
- proc.kill("SIGTERM");
55
- };
56
- // Honour the harness abort signal — kill the subprocess immediately
57
- if (options.signal !== undefined) {
58
- const sig = options.signal;
59
- if (sig.aborted) {
60
- proc.kill("SIGTERM");
61
- }
62
- else {
63
- const onAbort = () => { proc.kill("SIGTERM"); };
64
- sig.addEventListener("abort", onAbort, { once: true });
65
- proc.on("close", () => { sig.removeEventListener("abort", onAbort); });
66
- }
67
- }
68
- proc.stdout?.on("data", (chunk) => {
69
- if (outputCapped || timedOut || terminationReason) {
70
- return;
71
- }
72
- trackOutput(stdoutChunks, chunk);
73
- if (options.onStdoutChunk) {
74
- try {
75
- options.onStdoutChunk(chunk, terminateEarly);
76
- }
77
- catch (error) {
78
- terminateEarly(`stdout inspector error: ${error instanceof Error ? error.message : String(error)}`);
79
- }
80
- }
81
- });
82
- proc.stderr?.on("data", (chunk) => {
83
- if (outputCapped || timedOut || terminationReason) {
84
- return;
85
- }
86
- trackOutput(stderrChunks, chunk);
87
- });
88
- proc.stdin?.on("error", (error) => {
89
- // Some CLIs exit before consuming stdin in tests and on fast-fail paths.
90
- // Treat the closed pipe as a handled subprocess lifecycle condition.
91
- if (error.code === "EPIPE") {
92
- return;
93
- }
94
- stderrChunks.push(Buffer.from(`${error.message}\n`, "utf8"));
95
- });
96
- const timer = setTimeout(() => {
97
- if (settled || exited || proc.exitCode !== null) {
98
- return;
99
- }
100
- timedOut = true;
101
- proc.kill("SIGTERM");
102
- }, options.timeoutMs);
103
- proc.on("exit", () => {
104
- exited = true;
105
- });
106
- proc.on("error", (error) => {
107
- clearTimeout(timer);
108
- resolveOnce({ exitCode: 1, stdout: "", stderr: error.message, launched: false, completed: false, crashed: true });
109
- });
110
- proc.on("close", (code) => {
111
- clearTimeout(timer);
112
- const completed = code !== null && !timedOut && !outputCapped && !terminationReason;
113
- resolveOnce({
114
- exitCode: code ?? 1,
115
- stdout: Buffer.concat(stdoutChunks).toString("utf8"),
116
- stderr: Buffer.concat(stderrChunks).toString("utf8"),
117
- launched: true,
118
- completed,
119
- crashed: !completed && !timedOut && !outputCapped && !terminationReason,
120
- });
121
- });
122
- if (options.stdinData !== undefined && proc.stdin) {
123
- try {
124
- proc.stdin.end(options.stdinData, "utf8");
125
- }
126
- catch (error) {
127
- const stdinError = error;
128
- if (stdinError.code !== "EPIPE") {
129
- clearTimeout(timer);
130
- resolveOnce({
131
- exitCode: 1,
132
- stdout: Buffer.concat(stdoutChunks).toString("utf8"),
133
- stderr: stdinError.message,
134
- launched: false,
135
- completed: false,
136
- crashed: true,
137
- });
138
- }
139
- }
140
- }
7
+ const spawnPlan = createSpawnPlan(command, args, options.cwd, options.spawnImpl !== undefined);
8
+ return spawnSupervisedProcess(spawnPlan.command, spawnPlan.args, {
9
+ cwd: options.cwd,
10
+ timeoutMs: options.timeoutMs,
11
+ ...(options.env ? { env: options.env } : {}),
12
+ ...(options.spawnImpl ? { spawnImpl: options.spawnImpl } : {}),
13
+ ...(options.stdinData !== undefined ? { stdinData: options.stdinData } : {}),
14
+ ...(options.maxOutputBytes !== undefined ? { maxOutputBytes: options.maxOutputBytes } : {}),
15
+ ...(options.onStdoutChunk ? { onStdoutChunk: options.onStdoutChunk } : {}),
16
+ ...(options.signal ? { signal: options.signal } : {}),
17
+ ...(options.requireTreeClosureOnSuccess ? { requireTreeClosureOnSuccess: true } : {})
141
18
  });
142
19
  }
143
- export async function runVerification(commands, cwd, timeoutMs, verificationStack, spawnImpl, binding) {
20
+ export async function runVerification(commands, cwd, timeoutMs, verificationStack, spawnImpl, binding, signal) {
144
21
  const steps = verificationStack && verificationStack.length > 0
145
22
  ? verificationStack.map((step) => ({
146
23
  command: step.command,
@@ -150,12 +27,29 @@ export async function runVerification(commands, cwd, timeoutMs, verificationStac
150
27
  const executionBinding = {
151
28
  runId: binding?.runId ?? "unbound",
152
29
  workspaceId: binding?.workspaceId ?? "unbound",
30
+ ...(binding?.attemptId ? { attemptId: binding.attemptId } : {}),
153
31
  cwd: binding?.cwd ?? cwd,
32
+ ...(binding?.runsRoot ? { runsRoot: binding.runsRoot } : {}),
33
+ ...(binding?.executionProfile ? { executionProfile: binding.executionProfile } : {}),
34
+ ...(binding?.allowedNetworkDomains?.length ? { allowedNetworkDomains: [...binding.allowedNetworkDomains] } : {}),
154
35
  commands: steps.map((step) => step.command),
155
36
  };
37
+ const verifierEnv = {
38
+ ...process.env,
39
+ MARTIN_RUN_ID: executionBinding.runId,
40
+ MARTIN_WORKSPACE_ID: executionBinding.workspaceId,
41
+ ...(executionBinding.attemptId ? { MARTIN_ATTEMPT_ID: executionBinding.attemptId } : {}),
42
+ MARTIN_VERIFIER_CWD: executionBinding.cwd,
43
+ ...(executionBinding.runsRoot ? { MARTIN_RUNS_DIR: executionBinding.runsRoot } : {}),
44
+ ...(executionBinding.executionProfile ? { MARTIN_EXECUTION_PROFILE: executionBinding.executionProfile } : {}),
45
+ ...(executionBinding.allowedNetworkDomains?.length
46
+ ? { MARTIN_ALLOWED_NETWORK_DOMAINS: JSON.stringify(executionBinding.allowedNetworkDomains) }
47
+ : {}),
48
+ };
156
49
  if (steps.length === 0) {
157
50
  return {
158
51
  passed: false,
52
+ processCloseState: "not_required",
159
53
  summary: "No verification commands specified; execution is not VERIFIED.",
160
54
  steps: [],
161
55
  warnings: ["Execution completed without verifier evidence."],
@@ -165,6 +59,7 @@ export async function runVerification(commands, cwd, timeoutMs, verificationStac
165
59
  const failedSteps = [];
166
60
  const stepOutcomes = [];
167
61
  const warnings = [];
62
+ let processCloseState = "not_required";
168
63
  for (const step of steps) {
169
64
  let bin;
170
65
  let args;
@@ -193,8 +88,17 @@ export async function runVerification(commands, cwd, timeoutMs, verificationStac
193
88
  if (!bin) {
194
89
  continue;
195
90
  }
196
- const result = await runSubprocess(bin, args, { cwd, timeoutMs, spawnImpl });
91
+ const result = await runSubprocess(bin, args, {
92
+ cwd,
93
+ timeoutMs,
94
+ env: verifierEnv,
95
+ spawnImpl,
96
+ ...(signal ? { signal } : {}),
97
+ requireTreeClosureOnSuccess: true
98
+ });
197
99
  const detail = truncate(result.stderr.trim() || result.stdout.trim(), 500);
100
+ processCloseState = combineProcessCloseState(processCloseState, result.cleanup.state);
101
+ const evidence = parseExternalOutcomeEvidenceReference(result.stdout);
198
102
  stepOutcomes.push({
199
103
  command: step.command,
200
104
  launched: result.launched,
@@ -203,11 +107,23 @@ export async function runVerification(commands, cwd, timeoutMs, verificationStac
203
107
  exitCode: result.exitCode,
204
108
  timedOut: result.timedOut,
205
109
  fastFail: step.fastFail,
206
- ...(detail ? { detail } : {})
110
+ ...(detail ? { detail } : {}),
111
+ ...(evidence ? { evidence } : {})
207
112
  });
113
+ if (result.cleanup.state === "failed") {
114
+ return {
115
+ passed: false,
116
+ processCloseState,
117
+ summary: `Verifier process-tree closure failed: ${step.command}`,
118
+ steps: stepOutcomes,
119
+ binding: executionBinding,
120
+ warnings: [result.cleanup.message]
121
+ };
122
+ }
208
123
  if (result.timedOut) {
209
124
  return {
210
125
  passed: false,
126
+ processCloseState,
211
127
  summary: `Verification timed out: ${step.command}`,
212
128
  steps: stepOutcomes,
213
129
  binding: executionBinding,
@@ -220,7 +136,7 @@ export async function runVerification(commands, cwd, timeoutMs, verificationStac
220
136
  warnings.push(`Verifier never launched: ${step.command}`);
221
137
  }
222
138
  if (step.fastFail) {
223
- return { passed: false, summary, steps: stepOutcomes, binding: executionBinding, ...(warnings.length ? { warnings } : {}) };
139
+ return { passed: false, processCloseState, summary, steps: stepOutcomes, binding: executionBinding, ...(warnings.length ? { warnings } : {}) };
224
140
  }
225
141
  failedSteps.push(step.command);
226
142
  }
@@ -228,6 +144,7 @@ export async function runVerification(commands, cwd, timeoutMs, verificationStac
228
144
  if (failedSteps.length > 0) {
229
145
  return {
230
146
  passed: false,
147
+ processCloseState,
231
148
  summary: `Failed steps: ${failedSteps.join(", ")}`,
232
149
  steps: stepOutcomes,
233
150
  binding: executionBinding,
@@ -236,30 +153,93 @@ export async function runVerification(commands, cwd, timeoutMs, verificationStac
236
153
  }
237
154
  return {
238
155
  passed: true,
156
+ processCloseState,
239
157
  summary: `All ${String(steps.length)} verification step(s) passed.`,
240
158
  steps: stepOutcomes,
241
159
  binding: executionBinding,
242
160
  ...(warnings.length ? { warnings } : {})
243
161
  };
244
162
  }
245
- export async function readGitExecutionArtifacts(repoRoot, timeoutMs, spawnImpl) {
163
+ function parseExternalOutcomeEvidenceReference(stdout) {
164
+ const trimmed = stdout.trim();
165
+ if (!trimmed.startsWith("{") || !trimmed.endsWith("}"))
166
+ return undefined;
167
+ try {
168
+ const parsed = JSON.parse(trimmed);
169
+ const evidence = parsed.evidence;
170
+ if (evidence?.kind !== "external_outcome"
171
+ || typeof evidence.contractId !== "string"
172
+ || typeof evidence.path !== "string"
173
+ || typeof evidence.sha256 !== "string"
174
+ || !/^[a-f0-9]{64}$/u.test(evidence.sha256))
175
+ return undefined;
176
+ return {
177
+ kind: "external_outcome",
178
+ contractId: evidence.contractId,
179
+ path: evidence.path,
180
+ sha256: evidence.sha256,
181
+ };
182
+ }
183
+ catch {
184
+ return undefined;
185
+ }
186
+ }
187
+ function combineProcessCloseState(current, next) {
188
+ if (current === "failed" || next === "failed")
189
+ return "failed";
190
+ if (current === "closed" || next === "closed")
191
+ return "closed";
192
+ return "not_required";
193
+ }
194
+ export async function readGitExecutionArtifacts(repoRoot, timeoutMs, spawnImpl, requestedChangedFiles) {
246
195
  if (!resolveGitRepositoryRoot(repoRoot)) {
247
196
  return {};
248
197
  }
249
- const changedFilesResult = await runSubprocess("git", ["diff", "--name-only", "HEAD"], { cwd: repoRoot, timeoutMs, spawnImpl });
250
- const numstatResult = await runSubprocess("git", ["diff", "--numstat", "HEAD"], { cwd: repoRoot, timeoutMs, spawnImpl });
251
- const changedFiles = changedFilesResult.exitCode === 0
252
- ? changedFilesResult.stdout
253
- .split(/\r?\n/u)
254
- .map((entry) => entry.trim())
255
- .filter(Boolean)
256
- : [];
257
- const diffStats = numstatResult.exitCode === 0 ? diffStatsFromNumstat(numstatResult.stdout) : undefined;
198
+ const observedChangedFiles = requestedChangedFiles
199
+ ? [...requestedChangedFiles]
200
+ : await readGitChangedFiles(repoRoot, timeoutMs, spawnImpl);
201
+ const changedFiles = observedChangedFiles.filter(isSafeRepoRelativeGitPath);
202
+ const patchParts = [];
203
+ const numstatParts = [];
204
+ const nullDevice = process.platform === "win32" ? "NUL" : "/dev/null";
205
+ for (const file of changedFiles) {
206
+ const trackedPatch = await runSubprocess("git", ["diff", "--binary", "--no-ext-diff", "HEAD", "--", file], { cwd: repoRoot, timeoutMs, spawnImpl });
207
+ const trackedNumstat = await runSubprocess("git", ["diff", "--numstat", "HEAD", "--", file], { cwd: repoRoot, timeoutMs, spawnImpl });
208
+ if (trackedPatch.exitCode === 0 && trackedPatch.stdout.length > 0) {
209
+ patchParts.push(trackedPatch.stdout);
210
+ if (trackedNumstat.exitCode === 0 && trackedNumstat.stdout.length > 0) {
211
+ numstatParts.push(trackedNumstat.stdout);
212
+ }
213
+ continue;
214
+ }
215
+ // Git does not include untracked files in `git diff HEAD`. Diff each such
216
+ // path against the null tree so the parent runtime receives its content,
217
+ // not merely a synthetic filename-only patch.
218
+ const untrackedPatch = await runSubprocess("git", ["diff", "--binary", "--no-ext-diff", "--no-index", "--", nullDevice, file], { cwd: repoRoot, timeoutMs, spawnImpl });
219
+ if ((untrackedPatch.exitCode === 0 || untrackedPatch.exitCode === 1) && untrackedPatch.stdout.length > 0) {
220
+ patchParts.push(untrackedPatch.stdout);
221
+ }
222
+ const untrackedNumstat = await runSubprocess("git", ["diff", "--numstat", "--no-index", "--", nullDevice, file], { cwd: repoRoot, timeoutMs, spawnImpl });
223
+ if ((untrackedNumstat.exitCode === 0 || untrackedNumstat.exitCode === 1) && untrackedNumstat.stdout.length > 0) {
224
+ numstatParts.push(untrackedNumstat.stdout);
225
+ }
226
+ }
227
+ const patch = patchParts.join("\n");
228
+ const numstat = numstatParts.join("\n");
229
+ const diffStats = numstat.length > 0 ? diffStatsFromNumstat(numstat) : undefined;
258
230
  return {
259
231
  ...(changedFiles.length > 0 ? { changedFiles } : {}),
232
+ ...(patch.length > 0 ? { patch } : {}),
260
233
  ...(diffStats ? { diffStats } : {})
261
234
  };
262
235
  }
236
+ function isSafeRepoRelativeGitPath(file) {
237
+ if (file.length === 0 || file.includes("\u0000") || isAbsolute(file)) {
238
+ return false;
239
+ }
240
+ const segments = file.replace(/\\/gu, "/").split("/");
241
+ return segments.every((segment) => segment.length > 0 && segment !== "." && segment !== "..");
242
+ }
263
243
  export async function readGitChangedFiles(repoRoot, timeoutMs, spawnImpl) {
264
244
  if (!resolveGitRepositoryRoot(repoRoot)) {
265
245
  return [];
@@ -1,6 +1,6 @@
1
1
  import { spawnSync } from "node:child_process";
2
2
  import { type AgentExecutionIntent } from "../contracts/index.js";
3
- export type CodexFlagScope = "global" | "exec";
3
+ export type CodexFlagScope = "global" | "exec" | "sandbox";
4
4
  export type CodexPromptTransport = "stdin-dash" | "argv";
5
5
  export type CodexWriteStrategy = "sandbox+approval" | "automation";
6
6
  export interface CodexCapabilityFlag {
@@ -26,6 +26,8 @@ export interface CodexCapabilityProfile {
26
26
  supportsExec: boolean;
27
27
  probeSucceeded: boolean;
28
28
  probeError?: string;
29
+ config?: CodexCapabilityFlag;
30
+ sandboxConfig?: CodexCapabilityFlag;
29
31
  model?: CodexCapabilityFlag;
30
32
  cwd?: CodexCapabilityFlag;
31
33
  sandbox?: CodexSandboxCapability;
@@ -44,6 +46,7 @@ export interface CodexCapabilityProfile {
44
46
  export interface CodexExecArgsOptions {
45
47
  command?: string;
46
48
  workingDirectory: string;
49
+ platform?: NodeJS.Platform;
47
50
  sandbox?: "read-only" | "workspace-write" | "danger-full-access";
48
51
  model?: string;
49
52
  extraArgs?: string[];
@@ -134,8 +134,18 @@ export function probeCodexCapabilities(binaryPath, options = {}) {
134
134
  spawnSyncImpl,
135
135
  injected
136
136
  });
137
+ const sandboxProbe = platform === "win32"
138
+ ? runHelpProbe({
139
+ binaryPath,
140
+ args: ["sandbox", "--help"],
141
+ platform,
142
+ spawnSyncImpl,
143
+ injected
144
+ })
145
+ : { text: "", status: null };
137
146
  const globalHelp = globalProbe.text;
138
147
  const execHelp = execProbe.text;
148
+ const sandboxCommandHelp = sandboxProbe.text;
139
149
  const supportsExec = execProbe.status === 0 &&
140
150
  !/(unknown command|unrecognized subcommand|unexpected argument ['"]?exec)/iu.test(execHelp);
141
151
  const sandboxFlag = locateFlag(globalHelp, execHelp, ["--sandbox", "-s"]);
@@ -160,7 +170,8 @@ export function probeCodexCapabilities(binaryPath, options = {}) {
160
170
  ? `${flagContext(globalHelp, colorFlag.flag)}\n${flagContext(execHelp, colorFlag.flag)}`
161
171
  : "";
162
172
  const promptTransports = parsePromptTransports(execHelp);
163
- const probeError = globalProbe.error ?? execProbe.error;
173
+ const probeError = globalProbe.error ?? execProbe.error ?? sandboxProbe.error;
174
+ const sandboxConfigFlag = ["--config", "-c"].find((flag) => flagPattern(flag).test(sandboxCommandHelp));
164
175
  const profile = {
165
176
  binaryPath,
166
177
  supportsExec,
@@ -169,6 +180,12 @@ export function probeCodexCapabilities(binaryPath, options = {}) {
169
180
  ...(locateFlag(globalHelp, execHelp, ["--model", "-m"])
170
181
  ? { model: locateFlag(globalHelp, execHelp, ["--model", "-m"]) }
171
182
  : {}),
183
+ ...(locateFlag(globalHelp, execHelp, ["--config", "-c"])
184
+ ? { config: locateFlag(globalHelp, execHelp, ["--config", "-c"]) }
185
+ : {}),
186
+ ...(sandboxConfigFlag
187
+ ? { sandboxConfig: { flag: sandboxConfigFlag, scope: "sandbox" } }
188
+ : {}),
172
189
  ...(locateFlag(globalHelp, execHelp, ["--cd", "--cwd", "--working-dir", "-C"])
173
190
  ? { cwd: locateFlag(globalHelp, execHelp, ["--cd", "--cwd", "--working-dir", "-C"]) }
174
191
  : {}),
@@ -242,18 +259,33 @@ export function buildCodexExecArgs(options) {
242
259
  if (options.mode !== "probe" && !resolution) {
243
260
  throw new Error(`Resolved Codex binary ${profile.binaryPath} has no negotiated governed-autonomous execution resolution.`);
244
261
  }
245
- const permissionFlags = [profile.sandbox?.flag, profile.automation?.flag, profile.approvalPolicy?.flag]
262
+ const controlledFlags = [
263
+ profile.sandbox?.flag,
264
+ profile.automation?.flag,
265
+ profile.approvalPolicy?.flag,
266
+ profile.config?.flag
267
+ ]
246
268
  .filter((value) => Boolean(value));
247
- if ((options.extraArgs ?? []).some((arg) => arg === "danger-full-access" || permissionFlags.some((flag) => arg === flag || arg.startsWith(`${flag}=`)))) {
248
- throw new Error("Permission and sandbox controls cannot be supplied through extraArgs.");
269
+ if ((options.extraArgs ?? []).some((arg) => {
270
+ const isReservedConfig = arg === "--config" ||
271
+ arg.startsWith("--config=") ||
272
+ arg === "-c" ||
273
+ (arg.startsWith("-c") && arg.length > 2);
274
+ return arg === "danger-full-access" || isReservedConfig || controlledFlags.some((flag) => arg === flag || arg.startsWith(`${flag}=`));
275
+ })) {
276
+ throw new Error("Permission, sandbox, and configuration controls cannot be supplied through extraArgs.");
249
277
  }
250
278
  const globalArgs = [];
251
279
  const execArgs = [];
280
+ const platform = options.platform ?? process.platform;
252
281
  const requestedSandbox = options.sandbox ?? "workspace-write";
253
282
  const strategy = resolution?.strategy ?? options.writeStrategy;
254
283
  const promptTransport = options.promptTransport ?? profile.promptTransport;
255
284
  if (profile.userConfigIsolation)
256
285
  pushCapabilityArg(globalArgs, execArgs, profile.userConfigIsolation);
286
+ if (platform === "win32" && profile.userConfigIsolation && profile.config) {
287
+ pushCapabilityArg(globalArgs, execArgs, profile.config, 'windows.sandbox="elevated"');
288
+ }
257
289
  if (profile.cwd)
258
290
  pushCapabilityArg(globalArgs, execArgs, profile.cwd, options.workingDirectory);
259
291
  if (requestedSandbox === "workspace-write") {
@@ -1,5 +1,12 @@
1
1
  import { type CodexCliAdapterOptions as LegacyCodexCliAdapterOptions } from "./claude-cli.js";
2
2
  import { type CodexAutonomyResolution, type CodexCapabilityProfile } from "./codex-launcher.js";
3
+ /**
4
+ * Conservative admission floor for one Codex CLI turn. Codex reports token
5
+ * usage in turn.completed, after the first turn has already consumed context,
6
+ * so smaller explicit caps cannot be enforced before spend.
7
+ */
8
+ export declare const CODEX_MINIMUM_VIABLE_TOKEN_BUDGET = 128000;
9
+ export declare const CODEX_TOKEN_BUDGET_PREFLIGHT_BASIS = "codex_first_turn_usage_reports_after_completion";
3
10
  export interface CodexCliAdapterOptions extends Omit<LegacyCodexCliAdapterOptions, "command"> {
4
11
  /** Exact resolved executable selected by the Codex launch probe. */
5
12
  command?: string;
@@ -16,8 +23,8 @@ export interface CodexCliAdapterOptions extends Omit<LegacyCodexCliAdapterOption
16
23
  *
17
24
  * Provider identity remains `codex` even when doctor/preflight selected an
18
25
  * absolute native binary or npm shim. The selected binary's cached capability
19
- * profile builds both the real argv and stdin transport, while the spawn router
20
- * sends only the Codex subprocess to that exact executable. Git/verifier
21
- * subprocesses retain their normal commands.
26
+ * profile builds both the real argv and stdin transport. The generic adapter's
27
+ * execution-command boundary sends only the Codex subprocess to that exact
28
+ * executable, while Git/verifier subprocesses retain normal OS-owned spawning.
22
29
  */
23
30
  export declare function createCodexCliAdapter(options?: CodexCliAdapterOptions): import("../core/index.js").MartinAdapter;
@@ -1,26 +1,20 @@
1
- import { spawn } from "node:child_process";
2
1
  import { createAgentCliAdapter } from "./claude-cli.js";
3
2
  import { buildCodexExecArgs, buildCodexStdin, isCodexAutonomyResolutionVerifiedByLaunchProbe, probeCodexCapabilities } from "./codex-launcher.js";
4
- import { createSpawnPlan } from "./cli-bridge.js";
5
- function createCodexSpawnRouter(input) {
6
- if (input.injectedSpawn) {
7
- return (command, args = [], options) => input.injectedSpawn?.(command === "codex" ? input.selectedBinary : command, args, options);
8
- }
9
- return (command, args = [], options) => {
10
- const executable = command === "codex" ? input.selectedBinary : command;
11
- const cwd = typeof options?.cwd === "string" ? options.cwd : input.workingDirectory;
12
- const plan = createSpawnPlan(executable, [...args], cwd, false);
13
- return spawn(plan.command, plan.args, options ?? {});
14
- };
15
- }
3
+ /**
4
+ * Conservative admission floor for one Codex CLI turn. Codex reports token
5
+ * usage in turn.completed, after the first turn has already consumed context,
6
+ * so smaller explicit caps cannot be enforced before spend.
7
+ */
8
+ export const CODEX_MINIMUM_VIABLE_TOKEN_BUDGET = 128_000;
9
+ export const CODEX_TOKEN_BUDGET_PREFLIGHT_BASIS = "codex_first_turn_usage_reports_after_completion";
16
10
  /**
17
11
  * Capability-driven Codex CLI adapter.
18
12
  *
19
13
  * Provider identity remains `codex` even when doctor/preflight selected an
20
14
  * absolute native binary or npm shim. The selected binary's cached capability
21
- * profile builds both the real argv and stdin transport, while the spawn router
22
- * sends only the Codex subprocess to that exact executable. Git/verifier
23
- * subprocesses retain their normal commands.
15
+ * profile builds both the real argv and stdin transport. The generic adapter's
16
+ * execution-command boundary sends only the Codex subprocess to that exact
17
+ * executable, while Git/verifier subprocesses retain normal OS-owned spawning.
24
18
  */
25
19
  export function createCodexCliAdapter(options = {}) {
26
20
  const workingDirectory = options.workingDirectory ?? process.cwd();
@@ -39,16 +33,12 @@ export function createCodexCliAdapter(options = {}) {
39
33
  const sandbox = options.sandbox ?? "workspace-write";
40
34
  const extraArgs = options.extraArgs ?? [];
41
35
  const launchModel = options.model;
42
- const spawnImpl = createCodexSpawnRouter({
43
- selectedBinary,
44
- workingDirectory,
45
- ...(options.spawnImpl ? { injectedSpawn: options.spawnImpl } : {})
46
- });
47
36
  return createAgentCliAdapter({
48
37
  // Keep semantic provider identity stable for usage parsing, pricing,
49
- // diagnostics, and adapter metadata. The spawn router maps this to the
50
- // exact selected binary at process launch time.
38
+ // diagnostics, and adapter metadata. executionCommand selects the exact
39
+ // binary without disguising production OS spawning as a test injection.
51
40
  command: "codex",
41
+ executionCommand: selectedBinary,
52
42
  adapterIdSuffix: "codex",
53
43
  model: options.model,
54
44
  label: options.label ?? "Codex CLI adapter",
@@ -58,7 +48,14 @@ export function createCodexCliAdapter(options = {}) {
58
48
  providerExecutionTimeoutMs: options.providerExecutionTimeoutMs,
59
49
  verifyTimeoutMs: options.verifyTimeoutMs,
60
50
  supportsJsonOutput: false,
61
- spawnImpl,
51
+ streamingUsageCap: true,
52
+ streamingTokenCap: true,
53
+ streamingUsageDetailsIncludedInTotals: true,
54
+ budgetPreflight: {
55
+ minimumViableTokens: CODEX_MINIMUM_VIABLE_TOKEN_BUDGET,
56
+ basis: CODEX_TOKEN_BUDGET_PREFLIGHT_BASIS
57
+ },
58
+ spawnImpl: options.spawnImpl,
62
59
  argsBuilder: (prompt) => buildCodexExecArgs({
63
60
  command: selectedBinary,
64
61
  workingDirectory,