@martinloop/mcp 0.5.1 → 0.5.5
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/README.md +52 -14
- package/dist/server-validation.js +2 -0
- package/dist/server.js +30 -3
- package/dist/tools/run-loop.d.ts +1 -0
- package/dist/tools/run-loop.js +25 -4
- package/dist/tools/tool-response.d.ts +9 -1
- package/dist/tools/tool-response.js +7 -5
- package/dist/tools/tool-support.js +62 -271
- package/dist/vendor/adapters/claude-cli.d.ts +9 -0
- package/dist/vendor/adapters/claude-cli.js +19 -2
- package/dist/vendor/adapters/codex-capabilities.d.ts +69 -0
- package/dist/vendor/adapters/codex-capabilities.js +265 -0
- package/dist/vendor/adapters/codex-cli.d.ts +23 -0
- package/dist/vendor/adapters/codex-cli.js +72 -0
- package/dist/vendor/adapters/codex-host.d.ts +107 -0
- package/dist/vendor/adapters/codex-host.js +595 -0
- package/dist/vendor/adapters/codex-launcher.d.ts +7 -135
- package/dist/vendor/adapters/codex-launcher.js +17 -766
- package/dist/vendor/adapters/index.d.ts +4 -2
- package/dist/vendor/adapters/index.js +4 -2
- package/dist/vendor/contracts/agent-execution.d.ts +9 -0
- package/dist/vendor/contracts/agent-execution.js +14 -0
- package/dist/vendor/contracts/index.d.ts +9 -0
- package/dist/vendor/contracts/index.js +1 -0
- package/dist/vendor/core/grounding.js +42 -7
- package/dist/vendor/core/index.d.ts +5 -2
- package/dist/vendor/core/index.js +12 -3
- package/dist/vendor/presentation/governed-plan.d.ts +33 -0
- package/dist/vendor/presentation/governed-plan.js +146 -0
- package/dist/vendor/presentation/index.d.ts +7 -0
- package/dist/vendor/presentation/index.js +7 -0
- package/dist/vendor/presentation/motion.d.ts +12 -0
- package/dist/vendor/presentation/motion.js +123 -0
- package/dist/vendor/presentation/status.d.ts +8 -0
- package/dist/vendor/presentation/status.js +11 -0
- package/dist/vendor/presentation/table.d.ts +11 -0
- package/dist/vendor/presentation/table.js +47 -0
- package/dist/vendor/presentation/text.d.ts +6 -0
- package/dist/vendor/presentation/text.js +30 -0
- package/dist/vendor/presentation/theme.d.ts +31 -0
- package/dist/vendor/presentation/theme.js +82 -0
- package/dist/vendor/presentation/verified-handoff.d.ts +15 -0
- package/dist/vendor/presentation/verified-handoff.js +151 -0
- package/package.json +3 -2
- package/server.json +2 -2
|
@@ -1,771 +1,22 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { dirname, extname, join, resolve } from "node:path";
|
|
4
|
-
import { resolveNpmShimScript } from "./cli-bridge.js";
|
|
5
|
-
const codexLaunchProbeCache = new Map();
|
|
1
|
+
import { buildCodexExecArgs as buildCapabilityDrivenCodexExecArgs } from "./codex-capabilities.js";
|
|
6
2
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* inference from binary metadata or launch-probe output.
|
|
12
|
-
*
|
|
13
|
-
* The probe leaves no file behind on either success or failure.
|
|
14
|
-
*
|
|
15
|
-
* Exported for unit testing with a real tmp directory.
|
|
3
|
+
* Compatibility facade for callers that still populate the original
|
|
4
|
+
* `approval` profile field. Internally automation modes and approval policies
|
|
5
|
+
* remain separate capabilities, but either shape drives the same dynamic
|
|
6
|
+
* builder without restoring any hard-coded flag assumption.
|
|
16
7
|
*/
|
|
17
|
-
export function probeFilesystemWriteCapability(directory) {
|
|
18
|
-
// Ensure the directory exists before probing.
|
|
19
|
-
try {
|
|
20
|
-
mkdirSync(directory, { recursive: true });
|
|
21
|
-
}
|
|
22
|
-
catch (err) {
|
|
23
|
-
return {
|
|
24
|
-
writable: false,
|
|
25
|
-
reason: `Could not create directory ${directory}: ${err instanceof Error ? err.message : String(err)}`
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
// Use mkdtempSync so the filename is guaranteed unique even under concurrent runs.
|
|
29
|
-
let tempDir;
|
|
30
|
-
try {
|
|
31
|
-
tempDir = mkdtempSync(join(directory, ".ml-write-probe-"));
|
|
32
|
-
const tempFile = join(tempDir, "capability.tmp");
|
|
33
|
-
writeFileSync(tempFile, "\x01", { encoding: "binary", flag: "wx" });
|
|
34
|
-
unlinkSync(tempFile);
|
|
35
|
-
return { writable: true };
|
|
36
|
-
}
|
|
37
|
-
catch (err) {
|
|
38
|
-
return {
|
|
39
|
-
writable: false,
|
|
40
|
-
reason: err instanceof Error ? err.message : String(err)
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
finally {
|
|
44
|
-
if (tempDir) {
|
|
45
|
-
try {
|
|
46
|
-
rmSync(tempDir, { recursive: true, force: true });
|
|
47
|
-
}
|
|
48
|
-
catch { /* ignore cleanup errors */ }
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
/**
|
|
53
|
-
* Checks whether the requested sandbox mode is achievable for the given
|
|
54
|
-
* working directory. The adapter receives `requestedSandbox` from CLI/core —
|
|
55
|
-
* it does not decide the mode itself.
|
|
56
|
-
*
|
|
57
|
-
* When `requestedSandbox` is "workspace-write" and the working directory is not
|
|
58
|
-
* writable, this function returns `provider_sandbox_read_only` before any model
|
|
59
|
-
* execution is attempted.
|
|
60
|
-
*
|
|
61
|
-
* When `requestedSandbox` is "read-only" no write probe is performed; the
|
|
62
|
-
* outcome is `ok: true, effectiveSandbox: "read-only"` immediately.
|
|
63
|
-
*/
|
|
64
|
-
export function checkCodexSandboxPreflight(input) {
|
|
65
|
-
const dir = resolve(input.workingDirectory);
|
|
66
|
-
if (input.requestedSandbox === "read-only") {
|
|
67
|
-
return {
|
|
68
|
-
ok: true,
|
|
69
|
-
effectiveSandbox: "read-only",
|
|
70
|
-
capabilitySource: "probe",
|
|
71
|
-
writableRoot: dir
|
|
72
|
-
};
|
|
73
|
-
}
|
|
74
|
-
// workspace-write: run the actual filesystem probe.
|
|
75
|
-
const probeResult = probeFilesystemWriteCapability(dir);
|
|
76
|
-
if (probeResult.writable) {
|
|
77
|
-
return {
|
|
78
|
-
ok: true,
|
|
79
|
-
effectiveSandbox: "workspace-write",
|
|
80
|
-
capabilitySource: "probe",
|
|
81
|
-
writableRoot: dir
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
return {
|
|
85
|
-
ok: false,
|
|
86
|
-
code: "provider_sandbox_read_only",
|
|
87
|
-
requestedCapability: "workspace-write",
|
|
88
|
-
detectedCapability: "read-only",
|
|
89
|
-
effectiveSandbox: "read-only",
|
|
90
|
-
affectedPath: dir,
|
|
91
|
-
writableRoot: dir,
|
|
92
|
-
capabilitySource: "probe",
|
|
93
|
-
remediation: `The working directory ${dir} is not writable by the current process. ` +
|
|
94
|
-
"Launch MartinLoop in a session with write access to that directory, or use " +
|
|
95
|
-
"`--sandbox read-only` for inspection-only work."
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
const CODEX_LAUNCH_PROBE_PROMPT = [
|
|
99
|
-
"You are validating MartinLoop Codex host readiness.",
|
|
100
|
-
"Do not edit files.",
|
|
101
|
-
"Use the shell command executor exactly once to run: git status --short -- .",
|
|
102
|
-
"Do not use MCP tools, Node REPL, or any fallback tool if shell execution fails.",
|
|
103
|
-
"If the shell command succeeds, reply with READY only.",
|
|
104
|
-
"If it fails, reply with the exact failure in one sentence."
|
|
105
|
-
].join("\n");
|
|
106
|
-
function buildProbeCacheKey(input) {
|
|
107
|
-
return JSON.stringify({
|
|
108
|
-
workingDirectory: resolve(input.workingDirectory),
|
|
109
|
-
platform: input.platform,
|
|
110
|
-
candidatePaths: input.candidatePaths,
|
|
111
|
-
model: input.model
|
|
112
|
-
});
|
|
113
|
-
}
|
|
114
|
-
function isInsideGitRepository(workingDirectory) {
|
|
115
|
-
let current = resolve(workingDirectory);
|
|
116
|
-
while (true) {
|
|
117
|
-
if (existsSync(resolve(current, ".git"))) {
|
|
118
|
-
return true;
|
|
119
|
-
}
|
|
120
|
-
const parent = dirname(current);
|
|
121
|
-
if (parent === current) {
|
|
122
|
-
return false;
|
|
123
|
-
}
|
|
124
|
-
current = parent;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
function normalizeCandidates(lines) {
|
|
128
|
-
return [...new Set(lines.map((line) => line.trim()).filter(Boolean))];
|
|
129
|
-
}
|
|
130
|
-
function readLocatorCandidates(command, platform, spawnSyncImpl) {
|
|
131
|
-
const locator = platform === "win32" ? "where.exe" : "which";
|
|
132
|
-
const result = spawnSyncImpl(locator, [command], {
|
|
133
|
-
encoding: "utf8",
|
|
134
|
-
stdio: ["ignore", "pipe", "pipe"]
|
|
135
|
-
});
|
|
136
|
-
return {
|
|
137
|
-
locator,
|
|
138
|
-
candidates: result.status === 0
|
|
139
|
-
? normalizeCandidates((result.stdout ?? "").split(/\r?\n/u))
|
|
140
|
-
: [],
|
|
141
|
-
foundOnPath: result.status === 0
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
function discoverWindowsDesktopCodexCandidates(env) {
|
|
145
|
-
const localAppData = env["LOCALAPPDATA"];
|
|
146
|
-
if (!localAppData) {
|
|
147
|
-
return [];
|
|
148
|
-
}
|
|
149
|
-
const baseDirectory = join(localAppData, "OpenAI", "Codex", "bin");
|
|
150
|
-
if (!existsSync(baseDirectory)) {
|
|
151
|
-
return [];
|
|
152
|
-
}
|
|
153
|
-
const candidates = [];
|
|
154
|
-
const directCandidate = join(baseDirectory, "codex.exe");
|
|
155
|
-
if (existsSync(directCandidate)) {
|
|
156
|
-
candidates.push({ path: directCandidate, mtimeMs: statSync(directCandidate).mtimeMs });
|
|
157
|
-
}
|
|
158
|
-
for (const entry of readdirSync(baseDirectory, { withFileTypes: true })) {
|
|
159
|
-
if (!entry.isDirectory()) {
|
|
160
|
-
continue;
|
|
161
|
-
}
|
|
162
|
-
const candidate = join(baseDirectory, entry.name, "codex.exe");
|
|
163
|
-
if (!existsSync(candidate)) {
|
|
164
|
-
continue;
|
|
165
|
-
}
|
|
166
|
-
candidates.push({ path: candidate, mtimeMs: statSync(candidate).mtimeMs });
|
|
167
|
-
}
|
|
168
|
-
candidates.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
|
169
|
-
return normalizeCandidates(candidates.map((candidate) => candidate.path));
|
|
170
|
-
}
|
|
171
|
-
function codexProbePreference(diagnosis) {
|
|
172
|
-
if (diagnosis.installKind === "native" && diagnosis.invocationMode === "direct") {
|
|
173
|
-
return 0;
|
|
174
|
-
}
|
|
175
|
-
if (diagnosis.installKind === "native") {
|
|
176
|
-
return 1;
|
|
177
|
-
}
|
|
178
|
-
if (diagnosis.installKind === "windows_shim") {
|
|
179
|
-
return 2;
|
|
180
|
-
}
|
|
181
|
-
return 3;
|
|
182
|
-
}
|
|
183
|
-
function codexProbeCandidatePreference(path, diagnosis, platform) {
|
|
184
|
-
const hostPreference = codexProbePreference(diagnosis) * 10;
|
|
185
|
-
if (platform !== "win32" || diagnosis.installKind !== "windows_shim") {
|
|
186
|
-
return hostPreference;
|
|
187
|
-
}
|
|
188
|
-
// `where codex` commonly returns npm's extensionless POSIX shim before
|
|
189
|
-
// `codex.cmd`. Node cannot spawn that text shim directly on Windows, while
|
|
190
|
-
// the .cmd/.ps1 shim has a supported invocation path (or can be unwrapped).
|
|
191
|
-
const extension = extname(path).toLowerCase();
|
|
192
|
-
return hostPreference + (extension === ".cmd" || extension === ".bat" || extension === ".ps1" ? 0 : 1);
|
|
193
|
-
}
|
|
194
|
-
function buildProbeCandidates(input) {
|
|
195
|
-
const pathCandidates = normalizeCandidates(input.availability.candidatePaths ?? [input.availability.resolvedPath ?? input.availability.command]);
|
|
196
|
-
const desktopCandidates = input.platform === "win32" && input.includeDesktopCandidates
|
|
197
|
-
? discoverWindowsDesktopCodexCandidates(input.env).filter((candidatePath) => !pathCandidates.includes(candidatePath))
|
|
198
|
-
: [];
|
|
199
|
-
return [...pathCandidates, ...desktopCandidates]
|
|
200
|
-
.map((path, discoveryIndex) => {
|
|
201
|
-
const diagnosis = diagnoseCodexHost({
|
|
202
|
-
...input.availability,
|
|
203
|
-
resolvedPath: path
|
|
204
|
-
}, {
|
|
205
|
-
env: input.env,
|
|
206
|
-
platform: input.platform
|
|
207
|
-
});
|
|
208
|
-
return {
|
|
209
|
-
path,
|
|
210
|
-
diagnosis,
|
|
211
|
-
preference: input.platform === "win32"
|
|
212
|
-
? codexProbeCandidatePreference(path, diagnosis, input.platform)
|
|
213
|
-
: 0,
|
|
214
|
-
discoveryIndex
|
|
215
|
-
};
|
|
216
|
-
})
|
|
217
|
-
.sort((left, right) => left.preference === right.preference
|
|
218
|
-
? left.discoveryIndex - right.discoveryIndex
|
|
219
|
-
: left.preference - right.preference);
|
|
220
|
-
}
|
|
221
|
-
function buildProbeCommand(command, args, platform) {
|
|
222
|
-
if (platform !== "win32") {
|
|
223
|
-
return { command, args, invocationMode: "direct" };
|
|
224
|
-
}
|
|
225
|
-
const extension = extname(command).toLowerCase();
|
|
226
|
-
switch (extension) {
|
|
227
|
-
case ".cmd":
|
|
228
|
-
case ".bat":
|
|
229
|
-
case ".ps1": {
|
|
230
|
-
// Bypass the npm shim's cmd.exe/powershell.exe wrapper hop when we can statically resolve
|
|
231
|
-
// the real `node <script>` target it wraps — keeps the live probe's process-nesting depth
|
|
232
|
-
// consistent with the real run's invocation (see createSpawnPlan in cli-bridge.ts).
|
|
233
|
-
const directScript = resolveNpmShimScript(command);
|
|
234
|
-
if (directScript !== undefined) {
|
|
235
|
-
return { command: process.execPath, args: [directScript, ...args], invocationMode: "direct" };
|
|
236
|
-
}
|
|
237
|
-
if (extension === ".ps1") {
|
|
238
|
-
return {
|
|
239
|
-
command: "powershell.exe",
|
|
240
|
-
args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", command, ...args],
|
|
241
|
-
invocationMode: "powershell"
|
|
242
|
-
};
|
|
243
|
-
}
|
|
244
|
-
return {
|
|
245
|
-
command: process.env.ComSpec || "cmd.exe",
|
|
246
|
-
args: ["/d", "/c", command, ...args],
|
|
247
|
-
invocationMode: "cmd_shell"
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
default:
|
|
251
|
-
return { command, args, invocationMode: "direct" };
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
function detectInstallKind(resolvedPath, hostPlatform) {
|
|
255
|
-
if (!resolvedPath) {
|
|
256
|
-
return "missing";
|
|
257
|
-
}
|
|
258
|
-
const normalizedPath = resolvedPath.replace(/\\/gu, "/").toLowerCase();
|
|
259
|
-
const looksWindowsShim = normalizedPath.endsWith(".cmd") ||
|
|
260
|
-
normalizedPath.endsWith(".bat") ||
|
|
261
|
-
normalizedPath.endsWith(".ps1") ||
|
|
262
|
-
normalizedPath.includes("/appdata/roaming/npm/");
|
|
263
|
-
const looksMountedWindowsPath = normalizedPath.startsWith("/mnt/c/");
|
|
264
|
-
if ((hostPlatform === "linux" || hostPlatform === "wsl") && looksMountedWindowsPath) {
|
|
265
|
-
return "windows_mounted_path";
|
|
266
|
-
}
|
|
267
|
-
if (looksWindowsShim) {
|
|
268
|
-
return "windows_shim";
|
|
269
|
-
}
|
|
270
|
-
return "native";
|
|
271
|
-
}
|
|
272
|
-
function detectInvocationMode(resolvedPath, platform) {
|
|
273
|
-
if (platform !== "win32" || !resolvedPath) {
|
|
274
|
-
return "direct";
|
|
275
|
-
}
|
|
276
|
-
const extension = extname(resolvedPath).toLowerCase();
|
|
277
|
-
if (extension === ".ps1") {
|
|
278
|
-
return "powershell";
|
|
279
|
-
}
|
|
280
|
-
if (extension === ".cmd" || extension === ".bat") {
|
|
281
|
-
return "cmd_shell";
|
|
282
|
-
}
|
|
283
|
-
return "direct";
|
|
284
|
-
}
|
|
285
|
-
function classifyProbeFailure(stderr, stdout, diagnosis) {
|
|
286
|
-
const combined = `${stderr}\n${stdout}`;
|
|
287
|
-
if (/MARTIN_REMOTE_TOKEN/iu.test(combined) ||
|
|
288
|
-
/AuthRequired\(AuthRequiredError/iu.test(combined) ||
|
|
289
|
-
/www_authenticate_header/iu.test(combined)) {
|
|
290
|
-
const warnings = [...diagnosis.warnings];
|
|
291
|
-
const configCollisionWarning = "Codex inherited auth-sensitive MCP or plugin state from the operator's default user config.";
|
|
292
|
-
if (!warnings.includes(configCollisionWarning)) {
|
|
293
|
-
warnings.push(configCollisionWarning);
|
|
294
|
-
}
|
|
295
|
-
return {
|
|
296
|
-
summary: "Codex inherited auth-sensitive MCP or plugin state from the operator's default user config. Governed Codex runs should launch with `exec --ignore-user-config` so missing remote tokens or third-party auth do not abort MartinLoop work.",
|
|
297
|
-
diagnosis: {
|
|
298
|
-
...diagnosis,
|
|
299
|
-
warnings,
|
|
300
|
-
remediation: "Retry the governed Codex launch with `exec --ignore-user-config`, or remove auth-sensitive MCP/plugin dependencies from the default Codex user config before running MartinLoop."
|
|
301
|
-
}
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
if (/not supported when using Codex with a ChatGPT account/iu.test(combined) ||
|
|
305
|
-
/gpt-5\.3-codex/iu.test(combined)) {
|
|
306
|
-
const warnings = [...diagnosis.warnings];
|
|
307
|
-
const unsupportedModelWarning = "Codex tried to use a model that is not supported for ChatGPT-account authentication.";
|
|
308
|
-
if (!warnings.includes(unsupportedModelWarning)) {
|
|
309
|
-
warnings.push(unsupportedModelWarning);
|
|
310
|
-
}
|
|
311
|
-
return {
|
|
312
|
-
summary: "Codex launched with a model that is not supported for ChatGPT-account authentication. Pass an explicit model that your ChatGPT account supports for governed Codex work.",
|
|
313
|
-
diagnosis: {
|
|
314
|
-
...diagnosis,
|
|
315
|
-
warnings,
|
|
316
|
-
remediation: "Override the Codex launch model to a ChatGPT-account-supported option before running governed Codex work."
|
|
317
|
-
}
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
if (/@openai\/codex-linux-x64/iu.test(combined) ||
|
|
321
|
-
/cannot find module ['"]@openai\/codex-linux-x64['"]/iu.test(combined)) {
|
|
322
|
-
const warnings = [...diagnosis.warnings];
|
|
323
|
-
const missingDependencyWarning = "Codex is missing the native Linux package '@openai/codex-linux-x64' required for this host.";
|
|
324
|
-
if (!warnings.includes(missingDependencyWarning)) {
|
|
325
|
-
warnings.push(missingDependencyWarning);
|
|
326
|
-
}
|
|
327
|
-
return {
|
|
328
|
-
summary: "Codex native dependency '@openai/codex-linux-x64' is missing for this Linux/WSL environment.",
|
|
329
|
-
diagnosis: {
|
|
330
|
-
...diagnosis,
|
|
331
|
-
nativeInstallValid: false,
|
|
332
|
-
sandboxCompatible: false,
|
|
333
|
-
warnings,
|
|
334
|
-
nativeDependencyStatus: "missing",
|
|
335
|
-
nativeDependencyPackage: "@openai/codex-linux-x64",
|
|
336
|
-
remediation: "Reinstall Codex natively inside this Linux/WSL environment so the '@openai/codex-linux-x64' package is present before running governed Codex work."
|
|
337
|
-
}
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
if (/CreateProcessAsUserW failed:\s*5/iu.test(combined) ||
|
|
341
|
-
/windows sandbox: runner error: CreateProcessAsUserW failed:\s*5/iu.test(combined) ||
|
|
342
|
-
/spawn setup refresh/iu.test(combined)) {
|
|
343
|
-
const warnings = [...diagnosis.warnings];
|
|
344
|
-
const sandboxFailureWarning = "Codex workspace-write sandbox could not launch subprocesses on this Windows host.";
|
|
345
|
-
if (!warnings.includes(sandboxFailureWarning)) {
|
|
346
|
-
warnings.push(sandboxFailureWarning);
|
|
347
|
-
}
|
|
348
|
-
return {
|
|
349
|
-
summary: "Codex workspace-write sandbox could not launch subprocesses on this Windows host.",
|
|
350
|
-
diagnosis: {
|
|
351
|
-
...diagnosis,
|
|
352
|
-
sandboxCompatible: false,
|
|
353
|
-
warnings,
|
|
354
|
-
remediation: "MartinLoop already invokes Codex's underlying binary directly when it can resolve the npm shim (bypassing the cmd.exe/PowerShell wrapper hop), so this failure persisted even at the shallowest invocation depth available. Update or reinstall Codex on this Windows host until `codex exec --sandbox workspace-write` can launch a simple shell command before running governed Codex work."
|
|
355
|
-
}
|
|
356
|
-
};
|
|
357
|
-
}
|
|
358
|
-
if (/writing is blocked by read-only sandbox/iu.test(combined) ||
|
|
359
|
-
/read-only filesystem sandbox/iu.test(combined) ||
|
|
360
|
-
/approval is disabled/iu.test(combined)) {
|
|
361
|
-
const warnings = [...diagnosis.warnings];
|
|
362
|
-
const readOnlySandboxWarning = "Codex stayed in a read-only or approval-disabled sandbox even though MartinLoop requested workspace-write.";
|
|
363
|
-
if (!warnings.includes(readOnlySandboxWarning)) {
|
|
364
|
-
warnings.push(readOnlySandboxWarning);
|
|
365
|
-
}
|
|
366
|
-
return {
|
|
367
|
-
summary: "Codex stayed in a read-only or approval-disabled sandbox even though MartinLoop requested workspace-write.",
|
|
368
|
-
diagnosis: {
|
|
369
|
-
...diagnosis,
|
|
370
|
-
sandboxCompatible: false,
|
|
371
|
-
warnings,
|
|
372
|
-
remediation: "Launch governed Codex runs with `codex exec --ignore-user-config --sandbox workspace-write`. If the session still reports a read-only sandbox, treat it as a host/runtime mismatch and repair or relocate the affected Codex workspace before resuming governed work."
|
|
373
|
-
}
|
|
374
|
-
};
|
|
375
|
-
}
|
|
376
|
-
return {};
|
|
377
|
-
}
|
|
378
8
|
export function buildCodexExecArgs(options) {
|
|
379
|
-
const
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
// Governed MartinLoop runs should not inherit auth-sensitive default MCP/plugin state.
|
|
390
|
-
"--ignore-user-config",
|
|
391
|
-
"--cd",
|
|
392
|
-
options.workingDirectory,
|
|
393
|
-
...sandboxArgs,
|
|
394
|
-
"--json",
|
|
395
|
-
"--color",
|
|
396
|
-
"never",
|
|
397
|
-
...modelArgs,
|
|
398
|
-
...extraArgs,
|
|
399
|
-
"-"
|
|
400
|
-
];
|
|
401
|
-
}
|
|
402
|
-
function parseCodexProbeEvents(stdout) {
|
|
403
|
-
return stdout
|
|
404
|
-
.split(/\r?\n/u)
|
|
405
|
-
.map((line) => line.trim())
|
|
406
|
-
.filter(Boolean)
|
|
407
|
-
.map((line) => {
|
|
408
|
-
try {
|
|
409
|
-
return JSON.parse(line);
|
|
410
|
-
}
|
|
411
|
-
catch {
|
|
412
|
-
return undefined;
|
|
413
|
-
}
|
|
414
|
-
})
|
|
415
|
-
.filter((event) => event !== undefined);
|
|
416
|
-
}
|
|
417
|
-
function hasSuccessfulCommandExecution(events) {
|
|
418
|
-
return events.some((event) => event.type === "item.completed" &&
|
|
419
|
-
event.item?.type === "command_execution" &&
|
|
420
|
-
event.item?.status === "completed" &&
|
|
421
|
-
event.item?.exit_code === 0);
|
|
422
|
-
}
|
|
423
|
-
export function resolveCliCommandAvailability(command, options = {}) {
|
|
424
|
-
const platform = options.platform ?? process.platform;
|
|
425
|
-
const env = options.env ?? process.env;
|
|
426
|
-
const spawnSyncImpl = options.spawnSyncImpl ?? spawnSync;
|
|
427
|
-
const discovery = readLocatorCandidates(command, platform, spawnSyncImpl);
|
|
428
|
-
if (discovery.candidates.length > 0) {
|
|
429
|
-
const resolvedPath = discovery.candidates[0];
|
|
430
|
-
return {
|
|
431
|
-
command,
|
|
432
|
-
available: true,
|
|
433
|
-
locator: discovery.locator,
|
|
434
|
-
detail: `${command} is available on PATH.`,
|
|
435
|
-
...(resolvedPath ? { resolvedPath } : {}),
|
|
436
|
-
candidatePaths: discovery.candidates
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
// PATH didn't find it — search common install locations before giving up.
|
|
440
|
-
const offPathCandidate = discoverCommandOffPath(command, platform, env);
|
|
441
|
-
if (offPathCandidate) {
|
|
442
|
-
return {
|
|
443
|
-
command,
|
|
444
|
-
available: true,
|
|
445
|
-
locator: "off-path-discovery",
|
|
446
|
-
detail: `${command} found at ${offPathCandidate} (not on PATH, auto-discovered).`,
|
|
447
|
-
resolvedPath: offPathCandidate,
|
|
448
|
-
candidatePaths: [offPathCandidate]
|
|
449
|
-
};
|
|
450
|
-
}
|
|
451
|
-
return {
|
|
452
|
-
command,
|
|
453
|
-
available: false,
|
|
454
|
-
locator: discovery.locator,
|
|
455
|
-
detail: `${command} is not installed. ${suggestInstall(command)}`
|
|
456
|
-
};
|
|
457
|
-
}
|
|
458
|
-
function discoverCommandOffPath(command, platform, env) {
|
|
459
|
-
const home = env.HOME ?? env.USERPROFILE ?? "";
|
|
460
|
-
const dirs = [];
|
|
461
|
-
if (platform === "win32") {
|
|
462
|
-
const appData = env.APPDATA;
|
|
463
|
-
if (appData)
|
|
464
|
-
dirs.push(join(appData, "npm"));
|
|
465
|
-
const localAppData = env.LOCALAPPDATA;
|
|
466
|
-
if (localAppData)
|
|
467
|
-
dirs.push(join(localAppData, "OpenAI", "Codex", "bin"));
|
|
468
|
-
if (home)
|
|
469
|
-
dirs.push(join(home, "scoop", "shims"));
|
|
470
|
-
}
|
|
471
|
-
else {
|
|
472
|
-
dirs.push("/usr/local/bin", "/opt/homebrew/bin");
|
|
473
|
-
if (home) {
|
|
474
|
-
dirs.push(join(home, ".local", "bin"), join(home, ".npm-global", "bin"), join(home, ".bun", "bin"), join(home, ".cargo", "bin"));
|
|
475
|
-
}
|
|
476
|
-
const nvmDir = env.NVM_DIR;
|
|
477
|
-
if (nvmDir)
|
|
478
|
-
dirs.push(join(nvmDir, "current", "bin"));
|
|
479
|
-
}
|
|
480
|
-
const extensions = platform === "win32"
|
|
481
|
-
? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim().toLowerCase()).filter(Boolean)
|
|
482
|
-
: [""];
|
|
483
|
-
for (const dir of dirs) {
|
|
484
|
-
for (const ext of extensions) {
|
|
485
|
-
const candidate = ext ? join(dir, `${command}${ext}`) : join(dir, command);
|
|
486
|
-
if (existsSync(candidate)) {
|
|
487
|
-
return candidate;
|
|
488
|
-
}
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
return undefined;
|
|
492
|
-
}
|
|
493
|
-
function suggestInstall(command) {
|
|
494
|
-
const installs = {
|
|
495
|
-
claude: "Install with: npm install -g @anthropic-ai/claude-code",
|
|
496
|
-
codex: "Install with: npm install -g @openai/codex",
|
|
497
|
-
gemini: "Install with: npm install -g @google/gemini-cli"
|
|
498
|
-
};
|
|
499
|
-
return installs[command] ?? `Install ${command} and ensure it is available.`;
|
|
500
|
-
}
|
|
501
|
-
export function detectCodexHostPlatform(env = process.env, platform = process.platform) {
|
|
502
|
-
if (platform === "win32") {
|
|
503
|
-
return "windows";
|
|
504
|
-
}
|
|
505
|
-
if (platform === "darwin") {
|
|
506
|
-
return "macos";
|
|
507
|
-
}
|
|
508
|
-
if (env["WSL_DISTRO_NAME"] || env["WSL_INTEROP"]) {
|
|
509
|
-
return "wsl";
|
|
510
|
-
}
|
|
511
|
-
return "linux";
|
|
512
|
-
}
|
|
513
|
-
export function diagnoseCodexHost(availability, options = {}) {
|
|
514
|
-
const hostPlatform = detectCodexHostPlatform(options.env ?? process.env, options.platform ?? process.platform);
|
|
515
|
-
const resolvedPath = availability.resolvedPath;
|
|
516
|
-
const installKind = detectInstallKind(resolvedPath, hostPlatform);
|
|
517
|
-
const invocationMode = detectInvocationMode(resolvedPath, options.platform ?? process.platform);
|
|
518
|
-
const warnings = [];
|
|
519
|
-
if (!availability.available) {
|
|
520
|
-
return {
|
|
521
|
-
hostPlatform,
|
|
522
|
-
nativeInstallValid: false,
|
|
523
|
-
installKind,
|
|
524
|
-
invocationMode,
|
|
525
|
-
sandboxMode: "workspace-write",
|
|
526
|
-
sandboxCompatible: false,
|
|
527
|
-
...(resolvedPath ? { resolvedPath } : {}),
|
|
528
|
-
warnings,
|
|
529
|
-
remediation: "Install or expose the Codex CLI on PATH before running governed Codex work."
|
|
530
|
-
};
|
|
531
|
-
}
|
|
532
|
-
if ((hostPlatform === "linux" || hostPlatform === "wsl") &&
|
|
533
|
-
(installKind === "windows_shim" || installKind === "windows_mounted_path")) {
|
|
534
|
-
warnings.push("Codex resolves to a Windows-hosted install from a Linux/WSL environment.");
|
|
535
|
-
return {
|
|
536
|
-
hostPlatform,
|
|
537
|
-
nativeInstallValid: false,
|
|
538
|
-
installKind,
|
|
539
|
-
invocationMode,
|
|
540
|
-
sandboxMode: "workspace-write",
|
|
541
|
-
sandboxCompatible: false,
|
|
542
|
-
...(resolvedPath ? { resolvedPath } : {}),
|
|
543
|
-
warnings,
|
|
544
|
-
remediation: "Install Codex natively inside this Linux/WSL environment instead of relying on a Windows PATH shim."
|
|
545
|
-
};
|
|
546
|
-
}
|
|
547
|
-
return {
|
|
548
|
-
hostPlatform,
|
|
549
|
-
nativeInstallValid: true,
|
|
550
|
-
installKind,
|
|
551
|
-
invocationMode,
|
|
552
|
-
sandboxMode: "workspace-write",
|
|
553
|
-
sandboxCompatible: true,
|
|
554
|
-
...(resolvedPath ? { resolvedPath } : {}),
|
|
555
|
-
...(hostPlatform === "linux" || hostPlatform === "wsl"
|
|
556
|
-
? { nativeDependencyStatus: "unknown" }
|
|
557
|
-
: {}),
|
|
558
|
-
warnings
|
|
559
|
-
};
|
|
560
|
-
}
|
|
561
|
-
export function probeCodexLaunch(input) {
|
|
562
|
-
const availability = input.availability ??
|
|
563
|
-
resolveCliCommandAvailability("codex", {
|
|
564
|
-
platform: input.platform,
|
|
565
|
-
spawnSyncImpl: input.spawnSyncImpl
|
|
566
|
-
});
|
|
567
|
-
const diagnosis = diagnoseCodexHost(availability, {
|
|
568
|
-
env: input.env,
|
|
569
|
-
platform: input.platform
|
|
9
|
+
const profile = options.capabilityProfile;
|
|
10
|
+
if (!profile?.approval || profile.automation || profile.approvalPolicy) {
|
|
11
|
+
return buildCapabilityDrivenCodexExecArgs(options);
|
|
12
|
+
}
|
|
13
|
+
const normalizedProfile = profile.approval.semantics === "automation-mode"
|
|
14
|
+
? { ...profile, automation: profile.approval }
|
|
15
|
+
: { ...profile, approvalPolicy: profile.approval };
|
|
16
|
+
return buildCapabilityDrivenCodexExecArgs({
|
|
17
|
+
...options,
|
|
18
|
+
capabilityProfile: normalizedProfile
|
|
570
19
|
});
|
|
571
|
-
const args = buildCodexExecArgs({
|
|
572
|
-
workingDirectory: input.workingDirectory,
|
|
573
|
-
model: input.model,
|
|
574
|
-
mode: "probe"
|
|
575
|
-
});
|
|
576
|
-
if (!availability.available) {
|
|
577
|
-
return {
|
|
578
|
-
ok: false,
|
|
579
|
-
summary: availability.detail,
|
|
580
|
-
availability,
|
|
581
|
-
diagnosis,
|
|
582
|
-
command: availability.command,
|
|
583
|
-
args
|
|
584
|
-
};
|
|
585
|
-
}
|
|
586
|
-
if (!isInsideGitRepository(input.workingDirectory)) {
|
|
587
|
-
return {
|
|
588
|
-
ok: false,
|
|
589
|
-
summary: "Working directory is not inside a git repository. Codex exec requires a trusted repo unless --skip-git-repo-check is explicitly enabled.",
|
|
590
|
-
availability,
|
|
591
|
-
diagnosis,
|
|
592
|
-
command: availability.resolvedPath ?? availability.command,
|
|
593
|
-
args
|
|
594
|
-
};
|
|
595
|
-
}
|
|
596
|
-
const spawnSyncImpl = input.spawnSyncImpl ?? spawnSync;
|
|
597
|
-
const platform = input.platform ?? process.platform;
|
|
598
|
-
const env = input.env ?? process.env;
|
|
599
|
-
const probeCandidates = buildProbeCandidates({
|
|
600
|
-
availability,
|
|
601
|
-
env,
|
|
602
|
-
platform,
|
|
603
|
-
includeDesktopCandidates: input.spawnSyncImpl === undefined
|
|
604
|
-
});
|
|
605
|
-
const candidatePaths = probeCandidates.map((candidate) => candidate.path);
|
|
606
|
-
const cacheKey = buildProbeCacheKey({
|
|
607
|
-
workingDirectory: input.workingDirectory,
|
|
608
|
-
platform,
|
|
609
|
-
candidatePaths,
|
|
610
|
-
model: input.model
|
|
611
|
-
});
|
|
612
|
-
if (input.spawnSyncImpl === undefined) {
|
|
613
|
-
const cached = codexLaunchProbeCache.get(cacheKey);
|
|
614
|
-
if (cached) {
|
|
615
|
-
return cached;
|
|
616
|
-
}
|
|
617
|
-
}
|
|
618
|
-
const candidateResults = [];
|
|
619
|
-
const probeCandidatePath = (candidate) => {
|
|
620
|
-
const candidateDiagnosis = candidate.diagnosis;
|
|
621
|
-
if (!candidateDiagnosis.nativeInstallValid) {
|
|
622
|
-
candidateResults.push({
|
|
623
|
-
path: candidate.path,
|
|
624
|
-
diagnosis: candidateDiagnosis,
|
|
625
|
-
ok: false,
|
|
626
|
-
summary: candidateDiagnosis.remediation ?? "Codex host installation is not valid for this environment."
|
|
627
|
-
});
|
|
628
|
-
return false;
|
|
629
|
-
}
|
|
630
|
-
const spawnPlan = buildProbeCommand(candidate.path, args, platform);
|
|
631
|
-
const result = spawnSyncImpl(spawnPlan.command, spawnPlan.args, {
|
|
632
|
-
cwd: input.workingDirectory,
|
|
633
|
-
encoding: "utf8",
|
|
634
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
635
|
-
input: CODEX_LAUNCH_PROBE_PROMPT
|
|
636
|
-
});
|
|
637
|
-
const probedDiagnosis = {
|
|
638
|
-
...candidateDiagnosis,
|
|
639
|
-
invocationMode: spawnPlan.invocationMode
|
|
640
|
-
};
|
|
641
|
-
if (result.error) {
|
|
642
|
-
const classifiedFailure = classifyProbeFailure(result.stderr ?? "", result.stdout ?? "", probedDiagnosis);
|
|
643
|
-
candidateResults.push({
|
|
644
|
-
path: candidate.path,
|
|
645
|
-
diagnosis: classifiedFailure.diagnosis ?? probedDiagnosis,
|
|
646
|
-
ok: false,
|
|
647
|
-
summary: classifiedFailure.summary ?? `Codex launch probe failed: ${result.error.message}`,
|
|
648
|
-
stderr: result.stderr ?? "",
|
|
649
|
-
stdout: result.stdout ?? ""
|
|
650
|
-
});
|
|
651
|
-
return false;
|
|
652
|
-
}
|
|
653
|
-
if (result.status !== 0) {
|
|
654
|
-
const stderr = (result.stderr ?? "").trim();
|
|
655
|
-
const classifiedFailure = classifyProbeFailure(result.stderr ?? "", result.stdout ?? "", probedDiagnosis);
|
|
656
|
-
candidateResults.push({
|
|
657
|
-
path: candidate.path,
|
|
658
|
-
diagnosis: classifiedFailure.diagnosis ?? probedDiagnosis,
|
|
659
|
-
ok: false,
|
|
660
|
-
summary: classifiedFailure.summary ??
|
|
661
|
-
(stderr.length > 0 ? `Codex launch probe failed: ${stderr}` : "Codex launch probe exited non-zero."),
|
|
662
|
-
exitCode: result.status ?? undefined,
|
|
663
|
-
stderr: result.stderr ?? "",
|
|
664
|
-
stdout: result.stdout ?? ""
|
|
665
|
-
});
|
|
666
|
-
return false;
|
|
667
|
-
}
|
|
668
|
-
const events = parseCodexProbeEvents(result.stdout ?? "");
|
|
669
|
-
if (!hasSuccessfulCommandExecution(events)) {
|
|
670
|
-
const classifiedFailure = classifyProbeFailure(result.stderr ?? "", result.stdout ?? "", probedDiagnosis);
|
|
671
|
-
candidateResults.push({
|
|
672
|
-
path: candidate.path,
|
|
673
|
-
diagnosis: classifiedFailure.diagnosis ?? probedDiagnosis,
|
|
674
|
-
ok: false,
|
|
675
|
-
summary: classifiedFailure.summary ?? "Codex launch probe did not complete a shell command successfully.",
|
|
676
|
-
exitCode: result.status ?? undefined,
|
|
677
|
-
stderr: result.stderr ?? "",
|
|
678
|
-
stdout: result.stdout ?? ""
|
|
679
|
-
});
|
|
680
|
-
return false;
|
|
681
|
-
}
|
|
682
|
-
candidateResults.push({
|
|
683
|
-
path: candidate.path,
|
|
684
|
-
diagnosis: probedDiagnosis,
|
|
685
|
-
ok: true,
|
|
686
|
-
summary: "Codex exec prompt-and-shell probe passed for the current MartinLoop invocation shape.",
|
|
687
|
-
exitCode: result.status ?? undefined,
|
|
688
|
-
stderr: result.stderr ?? "",
|
|
689
|
-
stdout: result.stdout ?? ""
|
|
690
|
-
});
|
|
691
|
-
return true;
|
|
692
|
-
};
|
|
693
|
-
for (const candidate of probeCandidates) {
|
|
694
|
-
if (probeCandidatePath(candidate)) {
|
|
695
|
-
break;
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
const successfulCandidates = candidateResults.filter((candidate) => candidate.ok);
|
|
699
|
-
const selectedCandidate = successfulCandidates[0];
|
|
700
|
-
const candidateProbeResults = candidateResults.map((candidate) => ({
|
|
701
|
-
path: candidate.path,
|
|
702
|
-
installKind: candidate.diagnosis.installKind,
|
|
703
|
-
invocationMode: candidate.diagnosis.invocationMode,
|
|
704
|
-
nativeInstallValid: candidate.diagnosis.nativeInstallValid,
|
|
705
|
-
sandboxCompatible: candidate.diagnosis.sandboxCompatible,
|
|
706
|
-
launchReady: candidate.ok,
|
|
707
|
-
summary: candidate.summary,
|
|
708
|
-
...(candidate.diagnosis.remediation ? { remediation: candidate.diagnosis.remediation } : {}),
|
|
709
|
-
...(candidate.diagnosis.nativeDependencyStatus
|
|
710
|
-
? { nativeDependencyStatus: candidate.diagnosis.nativeDependencyStatus }
|
|
711
|
-
: {}),
|
|
712
|
-
...(candidate.diagnosis.nativeDependencyPackage
|
|
713
|
-
? { nativeDependencyPackage: candidate.diagnosis.nativeDependencyPackage }
|
|
714
|
-
: {})
|
|
715
|
-
}));
|
|
716
|
-
if (selectedCandidate) {
|
|
717
|
-
const successResult = {
|
|
718
|
-
ok: true,
|
|
719
|
-
summary: selectedCandidate.summary,
|
|
720
|
-
availability: {
|
|
721
|
-
...availability,
|
|
722
|
-
resolvedPath: selectedCandidate.path,
|
|
723
|
-
candidatePaths
|
|
724
|
-
},
|
|
725
|
-
diagnosis: {
|
|
726
|
-
...selectedCandidate.diagnosis,
|
|
727
|
-
resolvedPath: selectedCandidate.path
|
|
728
|
-
},
|
|
729
|
-
command: selectedCandidate.path,
|
|
730
|
-
args,
|
|
731
|
-
exitCode: selectedCandidate.exitCode,
|
|
732
|
-
stderr: selectedCandidate.stderr,
|
|
733
|
-
stdout: selectedCandidate.stdout,
|
|
734
|
-
candidateProbeResults
|
|
735
|
-
};
|
|
736
|
-
if (input.spawnSyncImpl === undefined) {
|
|
737
|
-
codexLaunchProbeCache.set(cacheKey, successResult);
|
|
738
|
-
}
|
|
739
|
-
return successResult;
|
|
740
|
-
}
|
|
741
|
-
const bestFailure = platform === "win32"
|
|
742
|
-
? candidateResults.find((candidate) => candidate.diagnosis.nativeInstallValid && candidate.diagnosis.installKind === "native") ??
|
|
743
|
-
candidateResults.find((candidate) => candidate.diagnosis.nativeInstallValid) ??
|
|
744
|
-
candidateResults[0]
|
|
745
|
-
: candidateResults[0];
|
|
746
|
-
const failureResult = {
|
|
747
|
-
ok: false,
|
|
748
|
-
summary: bestFailure?.summary ?? diagnosis.remediation ?? "Codex launch probe failed.",
|
|
749
|
-
availability: {
|
|
750
|
-
...availability,
|
|
751
|
-
...(bestFailure ? { resolvedPath: bestFailure.path } : {}),
|
|
752
|
-
...(candidatePaths.length ? { candidatePaths } : {})
|
|
753
|
-
},
|
|
754
|
-
diagnosis: bestFailure
|
|
755
|
-
? {
|
|
756
|
-
...bestFailure.diagnosis,
|
|
757
|
-
resolvedPath: bestFailure.path
|
|
758
|
-
}
|
|
759
|
-
: diagnosis,
|
|
760
|
-
command: bestFailure?.path ?? availability.resolvedPath ?? availability.command,
|
|
761
|
-
args,
|
|
762
|
-
exitCode: bestFailure?.exitCode,
|
|
763
|
-
stderr: bestFailure?.stderr,
|
|
764
|
-
stdout: bestFailure?.stdout,
|
|
765
|
-
candidateProbeResults
|
|
766
|
-
};
|
|
767
|
-
if (input.spawnSyncImpl === undefined) {
|
|
768
|
-
codexLaunchProbeCache.set(cacheKey, failureResult);
|
|
769
|
-
}
|
|
770
|
-
return failureResult;
|
|
771
20
|
}
|
|
21
|
+
export { buildCodexStdin, cacheCodexCapabilityProfile, clearCodexCapabilityCacheForTests, codexWriteStrategies, probeCodexCapabilities, resolveCodexAutonomyCandidates } from "./codex-capabilities.js";
|
|
22
|
+
export { checkCodexSandboxPreflight, detectCodexHostPlatform, diagnoseCodexHost, probeCodexLaunch, probeFilesystemWriteCapability, resolveCliCommandAvailability } from "./codex-host.js";
|