@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.
- package/dist/package-version.d.ts +1 -1
- package/dist/package-version.js +1 -1
- package/dist/vendor/adapters/claude-cli.d.ts +16 -1
- package/dist/vendor/adapters/claude-cli.js +286 -71
- package/dist/vendor/adapters/cli-bridge.d.ts +12 -3
- package/dist/vendor/adapters/cli-bridge.js +129 -149
- package/dist/vendor/adapters/codex-capabilities.d.ts +4 -1
- package/dist/vendor/adapters/codex-capabilities.js +36 -4
- package/dist/vendor/adapters/codex-cli.d.ts +10 -3
- package/dist/vendor/adapters/codex-cli.js +21 -24
- package/dist/vendor/adapters/codex-host.js +150 -132
- package/dist/vendor/adapters/index.d.ts +3 -2
- package/dist/vendor/adapters/index.js +3 -2
- package/dist/vendor/adapters/openai-compatible.js +88 -6
- package/dist/vendor/adapters/process-supervisor.d.ts +60 -0
- package/dist/vendor/adapters/process-supervisor.js +541 -0
- package/dist/vendor/adapters/verifier-only.d.ts +10 -1
- package/dist/vendor/adapters/verifier-only.js +43 -1
- package/dist/vendor/contracts/external-outcome.d.ts +102 -0
- package/dist/vendor/contracts/external-outcome.js +226 -0
- package/dist/vendor/contracts/index.d.ts +12 -1
- package/dist/vendor/contracts/index.js +2 -0
- package/dist/vendor/contracts/swarm-hosted.d.ts +204 -0
- package/dist/vendor/contracts/swarm-hosted.js +629 -0
- package/dist/vendor/contracts/swarm.d.ts +449 -0
- package/dist/vendor/contracts/swarm.js +550 -0
- package/dist/vendor/core/external-outcome.d.ts +34 -0
- package/dist/vendor/core/external-outcome.js +515 -0
- package/dist/vendor/core/grounding.js +1 -1
- package/dist/vendor/core/index.d.ts +52 -1
- package/dist/vendor/core/index.js +55 -3
- package/dist/vendor/core/leash.d.ts +1 -0
- package/dist/vendor/core/leash.js +13 -0
- package/dist/vendor/core/persistence/integrity.d.ts +4 -0
- package/dist/vendor/core/persistence/integrity.js +19 -6
- package/dist/vendor/core/persistence/swarm-integrity.d.ts +63 -0
- package/dist/vendor/core/persistence/swarm-integrity.js +309 -0
- package/dist/vendor/core/policy.d.ts +3 -0
- package/dist/vendor/core/policy.js +13 -0
- package/dist/vendor/core/swarm/candidates.d.ts +78 -0
- package/dist/vendor/core/swarm/candidates.js +810 -0
- package/dist/vendor/core/swarm/evidence-projection.d.ts +87 -0
- package/dist/vendor/core/swarm/evidence-projection.js +128 -0
- package/dist/vendor/core/swarm/evidence.d.ts +40 -0
- package/dist/vendor/core/swarm/evidence.js +575 -0
- package/dist/vendor/core/swarm/hosted-export.d.ts +48 -0
- package/dist/vendor/core/swarm/hosted-export.js +362 -0
- package/dist/vendor/core/swarm/hosted-projection.d.ts +5 -0
- package/dist/vendor/core/swarm/hosted-projection.js +258 -0
- package/dist/vendor/core/swarm/index.d.ts +110 -0
- package/dist/vendor/core/swarm/index.js +1060 -0
- package/dist/vendor/core/swarm/integration.d.ts +76 -0
- package/dist/vendor/core/swarm/integration.js +615 -0
- package/dist/vendor/core/swarm/lifecycle.d.ts +72 -0
- package/dist/vendor/core/swarm/lifecycle.js +252 -0
- package/dist/vendor/core/swarm/live-runtime.d.ts +78 -0
- package/dist/vendor/core/swarm/live-runtime.js +1482 -0
- package/dist/vendor/core/swarm/live-store.d.ts +99 -0
- package/dist/vendor/core/swarm/live-store.js +860 -0
- package/dist/vendor/core/swarm/operational-reader.d.ts +17 -0
- package/dist/vendor/core/swarm/operational-reader.js +96 -0
- package/dist/vendor/core/swarm/operations.d.ts +29 -0
- package/dist/vendor/core/swarm/operations.js +128 -0
- package/dist/vendor/core/swarm/parent-receipt.d.ts +13 -0
- package/dist/vendor/core/swarm/parent-receipt.js +489 -0
- package/dist/vendor/core/swarm/receipt-projection.d.ts +26 -0
- package/dist/vendor/core/swarm/receipt-projection.js +120 -0
- package/dist/vendor/core/swarm/scheduler.d.ts +92 -0
- package/dist/vendor/core/swarm/scheduler.js +555 -0
- package/dist/vendor/core/swarm/verification.d.ts +70 -0
- package/dist/vendor/core/swarm/verification.js +295 -0
- package/dist/vendor/core/swarm/workspaces.d.ts +77 -0
- package/dist/vendor/core/swarm/workspaces.js +488 -0
- package/dist/vendor/core/verified-handoff.d.ts +4 -0
- package/dist/vendor/core/verified-handoff.js +4 -0
- package/package.json +1 -1
- 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
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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, {
|
|
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
|
-
|
|
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
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
|
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) =>
|
|
248
|
-
|
|
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
|
|
20
|
-
* sends only the Codex subprocess to that exact
|
|
21
|
-
* subprocesses retain
|
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
|
22
|
-
* sends only the Codex subprocess to that exact
|
|
23
|
-
* subprocesses retain
|
|
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.
|
|
50
|
-
//
|
|
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
|
-
|
|
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,
|