@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
|
@@ -0,0 +1,595 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, extname, join, resolve } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { normalizeProviderExecutionTimeoutMs } from "../contracts/index.js";
|
|
6
|
+
import { createSpawnPlan, resolveNpmShimScript } from "./cli-bridge.js";
|
|
7
|
+
import { buildCodexExecArgs, buildCodexStdin, cacheCodexCapabilityProfile, probeCodexCapabilities, resolveCodexAutonomyCandidates } from "./codex-capabilities.js";
|
|
8
|
+
const launchProbeCache = new Map();
|
|
9
|
+
export function probeFilesystemWriteCapability(directory) {
|
|
10
|
+
try {
|
|
11
|
+
mkdirSync(directory, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
catch (error) {
|
|
14
|
+
return {
|
|
15
|
+
writable: false,
|
|
16
|
+
reason: `Could not create directory ${directory}: ${error instanceof Error ? error.message : String(error)}`
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
let tempDir;
|
|
20
|
+
try {
|
|
21
|
+
tempDir = mkdtempSync(join(directory, ".ml-write-probe-"));
|
|
22
|
+
const tempFile = join(tempDir, "capability.tmp");
|
|
23
|
+
writeFileSync(tempFile, "\x01", { encoding: "binary", flag: "wx" });
|
|
24
|
+
unlinkSync(tempFile);
|
|
25
|
+
return { writable: true };
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
return { writable: false, reason: error instanceof Error ? error.message : String(error) };
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
if (tempDir) {
|
|
32
|
+
try {
|
|
33
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// best effort cleanup
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export function checkCodexSandboxPreflight(input) {
|
|
42
|
+
const directory = resolve(input.workingDirectory);
|
|
43
|
+
if (input.requestedSandbox === "read-only") {
|
|
44
|
+
return {
|
|
45
|
+
ok: true,
|
|
46
|
+
effectiveSandbox: "read-only",
|
|
47
|
+
capabilitySource: "probe",
|
|
48
|
+
writableRoot: directory
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const result = probeFilesystemWriteCapability(directory);
|
|
52
|
+
if (result.writable) {
|
|
53
|
+
return {
|
|
54
|
+
ok: true,
|
|
55
|
+
effectiveSandbox: "workspace-write",
|
|
56
|
+
capabilitySource: "probe",
|
|
57
|
+
writableRoot: directory
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
return {
|
|
61
|
+
ok: false,
|
|
62
|
+
code: "provider_sandbox_read_only",
|
|
63
|
+
requestedCapability: "workspace-write",
|
|
64
|
+
detectedCapability: "read-only",
|
|
65
|
+
effectiveSandbox: "read-only",
|
|
66
|
+
affectedPath: directory,
|
|
67
|
+
writableRoot: directory,
|
|
68
|
+
capabilitySource: "probe",
|
|
69
|
+
remediation: `The working directory ${directory} is not writable by the current process. ` +
|
|
70
|
+
"Launch MartinLoop in a session with write access to that directory, or use `--sandbox read-only` for inspection-only work."
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function normalizeCandidates(lines) {
|
|
74
|
+
return [...new Set(lines.map((line) => line.trim()).filter(Boolean))];
|
|
75
|
+
}
|
|
76
|
+
function readLocatorCandidates(command, platform, spawnSyncImpl) {
|
|
77
|
+
const locator = platform === "win32" ? "where.exe" : "which";
|
|
78
|
+
const result = spawnSyncImpl(locator, [command], {
|
|
79
|
+
encoding: "utf8",
|
|
80
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
81
|
+
});
|
|
82
|
+
return {
|
|
83
|
+
locator,
|
|
84
|
+
candidates: result.status === 0 ? normalizeCandidates((result.stdout ?? "").split(/\r?\n/u)) : []
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function discoverWindowsDesktopCodexCandidates(env) {
|
|
88
|
+
const localAppData = env.LOCALAPPDATA;
|
|
89
|
+
if (!localAppData)
|
|
90
|
+
return [];
|
|
91
|
+
const base = join(localAppData, "OpenAI", "Codex", "bin");
|
|
92
|
+
if (!existsSync(base))
|
|
93
|
+
return [];
|
|
94
|
+
const found = [];
|
|
95
|
+
const direct = join(base, "codex.exe");
|
|
96
|
+
if (existsSync(direct))
|
|
97
|
+
found.push({ path: direct, mtimeMs: statSync(direct).mtimeMs });
|
|
98
|
+
for (const entry of readdirSync(base, { withFileTypes: true })) {
|
|
99
|
+
if (!entry.isDirectory())
|
|
100
|
+
continue;
|
|
101
|
+
const candidate = join(base, entry.name, "codex.exe");
|
|
102
|
+
if (existsSync(candidate))
|
|
103
|
+
found.push({ path: candidate, mtimeMs: statSync(candidate).mtimeMs });
|
|
104
|
+
}
|
|
105
|
+
found.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
106
|
+
return normalizeCandidates(found.map((item) => item.path));
|
|
107
|
+
}
|
|
108
|
+
export function resolveCliCommandAvailability(command, options = {}) {
|
|
109
|
+
const platform = options.platform ?? process.platform;
|
|
110
|
+
const env = options.env ?? process.env;
|
|
111
|
+
const spawnSyncImpl = options.spawnSyncImpl ?? spawnSync;
|
|
112
|
+
const discovery = readLocatorCandidates(command, platform, spawnSyncImpl);
|
|
113
|
+
if (discovery.candidates.length > 0) {
|
|
114
|
+
return {
|
|
115
|
+
command,
|
|
116
|
+
available: true,
|
|
117
|
+
locator: discovery.locator,
|
|
118
|
+
detail: `${command} is available on PATH.`,
|
|
119
|
+
resolvedPath: discovery.candidates[0],
|
|
120
|
+
candidatePaths: discovery.candidates
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
const offPath = discoverCommandOffPath(command, platform, env);
|
|
124
|
+
if (offPath) {
|
|
125
|
+
return {
|
|
126
|
+
command,
|
|
127
|
+
available: true,
|
|
128
|
+
locator: "off-path-discovery",
|
|
129
|
+
detail: `${command} found at ${offPath} (not on PATH, auto-discovered).`,
|
|
130
|
+
resolvedPath: offPath,
|
|
131
|
+
candidatePaths: [offPath]
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
command,
|
|
136
|
+
available: false,
|
|
137
|
+
locator: discovery.locator,
|
|
138
|
+
detail: `${command} is not installed. ${suggestInstall(command)}`
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
function discoverCommandOffPath(command, platform, env) {
|
|
142
|
+
const home = env.HOME ?? env.USERPROFILE ?? "";
|
|
143
|
+
const directories = [];
|
|
144
|
+
if (platform === "win32") {
|
|
145
|
+
if (env.APPDATA)
|
|
146
|
+
directories.push(join(env.APPDATA, "npm"));
|
|
147
|
+
if (env.LOCALAPPDATA)
|
|
148
|
+
directories.push(join(env.LOCALAPPDATA, "OpenAI", "Codex", "bin"));
|
|
149
|
+
if (home)
|
|
150
|
+
directories.push(join(home, "scoop", "shims"));
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
directories.push("/usr/local/bin", "/opt/homebrew/bin");
|
|
154
|
+
if (home) {
|
|
155
|
+
directories.push(join(home, ".local", "bin"), join(home, ".npm-global", "bin"), join(home, ".bun", "bin"), join(home, ".cargo", "bin"));
|
|
156
|
+
}
|
|
157
|
+
if (env.NVM_DIR)
|
|
158
|
+
directories.push(join(env.NVM_DIR, "current", "bin"));
|
|
159
|
+
}
|
|
160
|
+
const extensions = platform === "win32"
|
|
161
|
+
? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((item) => item.trim().toLowerCase()).filter(Boolean)
|
|
162
|
+
: [""];
|
|
163
|
+
for (const directory of directories) {
|
|
164
|
+
for (const extension of extensions) {
|
|
165
|
+
const candidate = extension ? join(directory, `${command}${extension}`) : join(directory, command);
|
|
166
|
+
if (existsSync(candidate))
|
|
167
|
+
return candidate;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
function suggestInstall(command) {
|
|
173
|
+
const installs = {
|
|
174
|
+
claude: "Install with: npm install -g @anthropic-ai/claude-code",
|
|
175
|
+
codex: "Install with: npm install -g @openai/codex",
|
|
176
|
+
gemini: "Install with: npm install -g @google/gemini-cli"
|
|
177
|
+
};
|
|
178
|
+
return installs[command] ?? `Install ${command} and ensure it is available.`;
|
|
179
|
+
}
|
|
180
|
+
export function detectCodexHostPlatform(env = process.env, platform = process.platform) {
|
|
181
|
+
if (platform === "win32")
|
|
182
|
+
return "windows";
|
|
183
|
+
if (platform === "darwin")
|
|
184
|
+
return "macos";
|
|
185
|
+
if (env.WSL_DISTRO_NAME || env.WSL_INTEROP)
|
|
186
|
+
return "wsl";
|
|
187
|
+
return "linux";
|
|
188
|
+
}
|
|
189
|
+
function detectInstallKind(path, host) {
|
|
190
|
+
if (!path)
|
|
191
|
+
return "missing";
|
|
192
|
+
const normalized = path.replace(/\\/gu, "/").toLowerCase();
|
|
193
|
+
if ((host === "linux" || host === "wsl") && normalized.startsWith("/mnt/c/"))
|
|
194
|
+
return "windows_mounted_path";
|
|
195
|
+
if (/\.(cmd|bat|ps1)$/iu.test(normalized) || normalized.includes("/appdata/roaming/npm/"))
|
|
196
|
+
return "windows_shim";
|
|
197
|
+
return "native";
|
|
198
|
+
}
|
|
199
|
+
function detectInvocationMode(path, platform) {
|
|
200
|
+
if (platform !== "win32" || !path)
|
|
201
|
+
return "direct";
|
|
202
|
+
const extension = extname(path).toLowerCase();
|
|
203
|
+
if (extension === ".ps1")
|
|
204
|
+
return "powershell";
|
|
205
|
+
if (extension === ".cmd" || extension === ".bat")
|
|
206
|
+
return "cmd_shell";
|
|
207
|
+
return "direct";
|
|
208
|
+
}
|
|
209
|
+
export function diagnoseCodexHost(availability, options = {}) {
|
|
210
|
+
const platform = options.platform ?? process.platform;
|
|
211
|
+
const hostPlatform = detectCodexHostPlatform(options.env ?? process.env, platform);
|
|
212
|
+
const resolvedPath = availability.resolvedPath;
|
|
213
|
+
const installKind = detectInstallKind(resolvedPath, hostPlatform);
|
|
214
|
+
const invocationMode = detectInvocationMode(resolvedPath, platform);
|
|
215
|
+
if (!availability.available) {
|
|
216
|
+
return {
|
|
217
|
+
hostPlatform,
|
|
218
|
+
nativeInstallValid: false,
|
|
219
|
+
installKind,
|
|
220
|
+
invocationMode,
|
|
221
|
+
sandboxMode: "workspace-write",
|
|
222
|
+
sandboxCompatible: false,
|
|
223
|
+
warnings: [],
|
|
224
|
+
remediation: "Install or expose the Codex CLI on PATH before running governed Codex work."
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
if ((hostPlatform === "linux" || hostPlatform === "wsl") && installKind === "windows_mounted_path") {
|
|
228
|
+
return {
|
|
229
|
+
hostPlatform,
|
|
230
|
+
nativeInstallValid: false,
|
|
231
|
+
installKind,
|
|
232
|
+
invocationMode,
|
|
233
|
+
sandboxMode: "workspace-write",
|
|
234
|
+
sandboxCompatible: false,
|
|
235
|
+
resolvedPath,
|
|
236
|
+
warnings: ["Codex resolves to a Windows-hosted install from Linux/WSL."],
|
|
237
|
+
remediation: "Install Codex natively inside this Linux/WSL environment before governed work."
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
hostPlatform,
|
|
242
|
+
nativeInstallValid: true,
|
|
243
|
+
installKind,
|
|
244
|
+
invocationMode,
|
|
245
|
+
sandboxMode: "workspace-write",
|
|
246
|
+
sandboxCompatible: true,
|
|
247
|
+
resolvedPath,
|
|
248
|
+
...(hostPlatform === "linux" || hostPlatform === "wsl" ? { nativeDependencyStatus: "unknown" } : {}),
|
|
249
|
+
warnings: []
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
function candidatePreference(path, diagnosis, platform) {
|
|
253
|
+
const base = diagnosis.installKind === "native" ? 0 : diagnosis.installKind === "windows_shim" ? 20 : 30;
|
|
254
|
+
if (platform !== "win32" || diagnosis.installKind !== "windows_shim")
|
|
255
|
+
return base;
|
|
256
|
+
return base + (/\.(cmd|bat|ps1)$/iu.test(path) ? 0 : 1);
|
|
257
|
+
}
|
|
258
|
+
function buildCandidates(input) {
|
|
259
|
+
const pathCandidates = normalizeCandidates(input.availability.candidatePaths ?? [input.availability.resolvedPath ?? input.availability.command]);
|
|
260
|
+
const desktop = input.platform === "win32" && input.includeDesktopCandidates
|
|
261
|
+
? discoverWindowsDesktopCodexCandidates(input.env).filter((path) => !pathCandidates.includes(path))
|
|
262
|
+
: [];
|
|
263
|
+
return [...pathCandidates, ...desktop]
|
|
264
|
+
.map((path, discoveryIndex) => {
|
|
265
|
+
const diagnosis = diagnoseCodexHost({ ...input.availability, resolvedPath: path }, { env: input.env, platform: input.platform });
|
|
266
|
+
return {
|
|
267
|
+
path,
|
|
268
|
+
diagnosis,
|
|
269
|
+
preference: candidatePreference(path, diagnosis, input.platform),
|
|
270
|
+
discoveryIndex
|
|
271
|
+
};
|
|
272
|
+
})
|
|
273
|
+
.sort((a, b) => a.preference === b.preference ? a.discoveryIndex - b.discoveryIndex : a.preference - b.preference);
|
|
274
|
+
}
|
|
275
|
+
function isInsideGitRepository(workingDirectory) {
|
|
276
|
+
let current = resolve(workingDirectory);
|
|
277
|
+
while (true) {
|
|
278
|
+
if (existsSync(resolve(current, ".git")))
|
|
279
|
+
return true;
|
|
280
|
+
const parent = dirname(current);
|
|
281
|
+
if (parent === current)
|
|
282
|
+
return false;
|
|
283
|
+
current = parent;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
function buildInjectedSpawnPlan(binaryPath, args, platform) {
|
|
287
|
+
if (platform !== "win32")
|
|
288
|
+
return { command: binaryPath, args, invocationMode: "direct" };
|
|
289
|
+
const extension = extname(binaryPath).toLowerCase();
|
|
290
|
+
if (extension === ".cmd" || extension === ".bat" || extension === ".ps1") {
|
|
291
|
+
const script = resolveNpmShimScript(binaryPath);
|
|
292
|
+
if (script)
|
|
293
|
+
return { command: process.execPath, args: [script, ...args], invocationMode: "direct" };
|
|
294
|
+
if (extension === ".ps1") {
|
|
295
|
+
return {
|
|
296
|
+
command: "powershell.exe",
|
|
297
|
+
args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", binaryPath, ...args],
|
|
298
|
+
invocationMode: "powershell"
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
command: process.env.ComSpec || "cmd.exe",
|
|
303
|
+
args: ["/d", "/c", binaryPath, ...args],
|
|
304
|
+
invocationMode: "cmd_shell"
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
return { command: binaryPath, args, invocationMode: "direct" };
|
|
308
|
+
}
|
|
309
|
+
function buildMarkerPrompt(marker, outsideMarkerPath) {
|
|
310
|
+
const command = `node -e "require('node:fs').writeFileSync(process.argv[1],'MARTIN_CODEX_WRITE_OK')" ${marker}`;
|
|
311
|
+
const outsideCommand = `node -e "require('node:fs').writeFileSync(process.argv[1],'MARTIN_CODEX_OUTSIDE_BAD')" "${outsideMarkerPath}"`;
|
|
312
|
+
return [
|
|
313
|
+
"You are validating MartinLoop Codex host readiness.",
|
|
314
|
+
"Do not modify tracked files.",
|
|
315
|
+
"First attempt this outside-workspace command and confirm that it is denied:",
|
|
316
|
+
outsideCommand,
|
|
317
|
+
"Then use the shell command executor for the allowed workspace marker.",
|
|
318
|
+
`Run exactly: ${command}`,
|
|
319
|
+
"Do not use MCP tools or alternate tools.",
|
|
320
|
+
"After it succeeds, reply READY."
|
|
321
|
+
].join("\n");
|
|
322
|
+
}
|
|
323
|
+
function classifyFailure(stderr, stdout, diagnosis) {
|
|
324
|
+
const combined = `${stderr}\n${stdout}`;
|
|
325
|
+
if (/@openai\/codex-linux-x64|cannot find module ['"]@openai\/codex-linux-x64['"]/iu.test(combined)) {
|
|
326
|
+
return {
|
|
327
|
+
...diagnosis,
|
|
328
|
+
nativeInstallValid: false,
|
|
329
|
+
sandboxCompatible: false,
|
|
330
|
+
nativeDependencyStatus: "missing",
|
|
331
|
+
nativeDependencyPackage: "@openai/codex-linux-x64",
|
|
332
|
+
warnings: [...diagnosis.warnings, "Codex native Linux package is missing."],
|
|
333
|
+
remediation: "Reinstall Codex natively in this Linux/WSL environment."
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
if (/CreateProcessAsUserW failed:\s*5|windows sandbox: runner error/iu.test(combined)) {
|
|
337
|
+
return {
|
|
338
|
+
...diagnosis,
|
|
339
|
+
sandboxCompatible: false,
|
|
340
|
+
warnings: [...diagnosis.warnings, "This Codex invocation could not create a writable subprocess on Windows."],
|
|
341
|
+
remediation: "Use another capability strategy advertised by this exact Codex binary, or repair the host sandbox."
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
return diagnosis;
|
|
345
|
+
}
|
|
346
|
+
function probeCacheKey(input) {
|
|
347
|
+
return JSON.stringify({
|
|
348
|
+
workingDirectory: resolve(input.workingDirectory),
|
|
349
|
+
platform: input.platform,
|
|
350
|
+
candidatePaths: input.candidatePaths,
|
|
351
|
+
model: input.model
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
export function probeCodexLaunch(input) {
|
|
355
|
+
const availability = input.availability ?? resolveCliCommandAvailability("codex", {
|
|
356
|
+
...(input.platform ? { platform: input.platform } : {}),
|
|
357
|
+
...(input.env ? { env: input.env } : {}),
|
|
358
|
+
...(input.spawnSyncImpl ? { spawnSyncImpl: input.spawnSyncImpl } : {})
|
|
359
|
+
});
|
|
360
|
+
const diagnosis = diagnoseCodexHost(availability, {
|
|
361
|
+
...(input.platform ? { platform: input.platform } : {}),
|
|
362
|
+
...(input.env ? { env: input.env } : {})
|
|
363
|
+
});
|
|
364
|
+
if (!availability.available) {
|
|
365
|
+
return { ok: false, summary: availability.detail, availability, diagnosis, command: availability.command, args: [] };
|
|
366
|
+
}
|
|
367
|
+
if (!isInsideGitRepository(input.workingDirectory)) {
|
|
368
|
+
return {
|
|
369
|
+
ok: false,
|
|
370
|
+
summary: "Working directory is not inside a git repository. Codex exec requires a trusted repository for governed work.",
|
|
371
|
+
availability,
|
|
372
|
+
diagnosis,
|
|
373
|
+
command: availability.resolvedPath ?? availability.command,
|
|
374
|
+
args: []
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
const platform = input.platform ?? process.platform;
|
|
378
|
+
const env = input.env ?? process.env;
|
|
379
|
+
const spawnSyncImpl = input.spawnSyncImpl ?? spawnSync;
|
|
380
|
+
const providerExecutionTimeoutMs = normalizeProviderExecutionTimeoutMs(input.providerExecutionTimeoutMs);
|
|
381
|
+
const candidates = buildCandidates({
|
|
382
|
+
availability,
|
|
383
|
+
env,
|
|
384
|
+
platform,
|
|
385
|
+
includeDesktopCandidates: input.spawnSyncImpl === undefined
|
|
386
|
+
});
|
|
387
|
+
const candidatePaths = candidates.map((candidate) => candidate.path);
|
|
388
|
+
const key = probeCacheKey({
|
|
389
|
+
workingDirectory: input.workingDirectory,
|
|
390
|
+
platform,
|
|
391
|
+
candidatePaths,
|
|
392
|
+
...(input.model ? { model: input.model } : {})
|
|
393
|
+
});
|
|
394
|
+
if (input.spawnSyncImpl === undefined) {
|
|
395
|
+
const cached = launchProbeCache.get(key);
|
|
396
|
+
if (cached)
|
|
397
|
+
return cached;
|
|
398
|
+
}
|
|
399
|
+
const candidateResults = [];
|
|
400
|
+
let selected;
|
|
401
|
+
let lastFailure;
|
|
402
|
+
candidateLoop: for (const candidate of candidates) {
|
|
403
|
+
if (!candidate.diagnosis.nativeInstallValid) {
|
|
404
|
+
candidateResults.push({
|
|
405
|
+
path: candidate.path,
|
|
406
|
+
installKind: candidate.diagnosis.installKind,
|
|
407
|
+
invocationMode: candidate.diagnosis.invocationMode,
|
|
408
|
+
nativeInstallValid: false,
|
|
409
|
+
sandboxCompatible: false,
|
|
410
|
+
launchReady: false,
|
|
411
|
+
summary: candidate.diagnosis.remediation ?? "Codex installation is not valid for this host."
|
|
412
|
+
});
|
|
413
|
+
continue;
|
|
414
|
+
}
|
|
415
|
+
const profile = probeCodexCapabilities(candidate.path, {
|
|
416
|
+
platform,
|
|
417
|
+
spawnSyncImpl,
|
|
418
|
+
cache: input.spawnSyncImpl === undefined
|
|
419
|
+
});
|
|
420
|
+
if (!profile.supportsExec) {
|
|
421
|
+
candidateResults.push({
|
|
422
|
+
path: candidate.path,
|
|
423
|
+
installKind: candidate.diagnosis.installKind,
|
|
424
|
+
invocationMode: candidate.diagnosis.invocationMode,
|
|
425
|
+
nativeInstallValid: true,
|
|
426
|
+
sandboxCompatible: false,
|
|
427
|
+
launchReady: false,
|
|
428
|
+
summary: "Resolved Codex binary does not advertise a usable exec subcommand.",
|
|
429
|
+
capabilityProfile: profile
|
|
430
|
+
});
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
433
|
+
const transports = profile.promptTransports?.length ? profile.promptTransports : [profile.promptTransport];
|
|
434
|
+
const resolutions = resolveCodexAutonomyCandidates(profile);
|
|
435
|
+
let candidateSummary = "No advertised Codex invocation strategy proved writable execution.";
|
|
436
|
+
for (const resolution of resolutions) {
|
|
437
|
+
for (const transport of transports) {
|
|
438
|
+
const marker = `.martin-codex-write-probe-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.tmp`;
|
|
439
|
+
const markerPath = join(input.workingDirectory, marker);
|
|
440
|
+
const outsideRoot = mkdtempSync(join(tmpdir(), "martin-codex-outside-probe-"));
|
|
441
|
+
const outsideMarkerPath = join(outsideRoot, ".martin-codex-outside-probe.tmp");
|
|
442
|
+
const prompt = buildMarkerPrompt(marker, outsideMarkerPath);
|
|
443
|
+
let args;
|
|
444
|
+
try {
|
|
445
|
+
args = buildCodexExecArgs({
|
|
446
|
+
command: candidate.path,
|
|
447
|
+
workingDirectory: input.workingDirectory,
|
|
448
|
+
sandbox: "workspace-write",
|
|
449
|
+
...(input.model ? { model: input.model } : {}),
|
|
450
|
+
mode: "probe",
|
|
451
|
+
prompt,
|
|
452
|
+
capabilityProfile: profile,
|
|
453
|
+
promptTransport: transport,
|
|
454
|
+
writeStrategy: resolution.strategy,
|
|
455
|
+
autonomyResolution: resolution
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
catch (error) {
|
|
459
|
+
rmSync(outsideRoot, { recursive: true, force: true });
|
|
460
|
+
candidateSummary = error instanceof Error ? error.message : String(error);
|
|
461
|
+
lastFailure = { candidate, profile, args: [], summary: candidateSummary };
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
const plan = input.spawnSyncImpl
|
|
465
|
+
? buildInjectedSpawnPlan(candidate.path, args, platform)
|
|
466
|
+
: { ...createSpawnPlan(candidate.path, args, input.workingDirectory, false), invocationMode: "direct" };
|
|
467
|
+
const stdin = buildCodexStdin(profile, prompt, transport);
|
|
468
|
+
const result = spawnSyncImpl(plan.command, plan.args, {
|
|
469
|
+
cwd: input.workingDirectory,
|
|
470
|
+
encoding: "utf8",
|
|
471
|
+
stdio: [stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"],
|
|
472
|
+
...(stdin !== undefined ? { input: stdin } : {}),
|
|
473
|
+
timeout: providerExecutionTimeoutMs
|
|
474
|
+
});
|
|
475
|
+
let markerVerified = false;
|
|
476
|
+
let outsideDenied = false;
|
|
477
|
+
try {
|
|
478
|
+
markerVerified = existsSync(markerPath) && readFileSync(markerPath, "utf8") === "MARTIN_CODEX_WRITE_OK";
|
|
479
|
+
outsideDenied = !existsSync(outsideMarkerPath);
|
|
480
|
+
}
|
|
481
|
+
finally {
|
|
482
|
+
try {
|
|
483
|
+
unlinkSync(markerPath);
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
// best effort cleanup
|
|
487
|
+
}
|
|
488
|
+
rmSync(outsideRoot, { recursive: true, force: true });
|
|
489
|
+
}
|
|
490
|
+
if (!result.error && result.status === 0 && markerVerified && outsideDenied) {
|
|
491
|
+
const negotiated = cacheCodexCapabilityProfile({ ...profile, promptTransport: transport }, platform);
|
|
492
|
+
selected = {
|
|
493
|
+
candidate: {
|
|
494
|
+
...candidate,
|
|
495
|
+
diagnosis: { ...candidate.diagnosis, invocationMode: plan.invocationMode }
|
|
496
|
+
},
|
|
497
|
+
profile: negotiated,
|
|
498
|
+
args,
|
|
499
|
+
resolution,
|
|
500
|
+
transport,
|
|
501
|
+
...(result.status === null ? {} : { exitCode: result.status }),
|
|
502
|
+
stdout: result.stdout ?? "",
|
|
503
|
+
stderr: result.stderr ?? ""
|
|
504
|
+
};
|
|
505
|
+
candidateResults.push({
|
|
506
|
+
path: candidate.path,
|
|
507
|
+
installKind: candidate.diagnosis.installKind,
|
|
508
|
+
invocationMode: plan.invocationMode,
|
|
509
|
+
nativeInstallValid: true,
|
|
510
|
+
sandboxCompatible: true,
|
|
511
|
+
launchReady: true,
|
|
512
|
+
summary: `Codex capability negotiation passed with ${resolution.strategy} / ${transport}.`,
|
|
513
|
+
capabilityProfile: negotiated,
|
|
514
|
+
writeStrategy: resolution.strategy,
|
|
515
|
+
autonomyResolution: resolution,
|
|
516
|
+
promptTransport: transport
|
|
517
|
+
});
|
|
518
|
+
break candidateLoop;
|
|
519
|
+
}
|
|
520
|
+
const failureDiagnosis = classifyFailure(result.stderr ?? "", result.stdout ?? "", candidate.diagnosis);
|
|
521
|
+
candidateSummary = result.error
|
|
522
|
+
? `Codex launch probe failed: ${result.error.message}`
|
|
523
|
+
: result.status !== 0
|
|
524
|
+
? `Codex launch probe exited non-zero: ${(result.stderr ?? result.stdout ?? "").trim() || String(result.status)}`
|
|
525
|
+
: !outsideDenied
|
|
526
|
+
? `Codex ${resolution.strategy} / ${transport} escaped the workspace boundary.`
|
|
527
|
+
: `Codex ${resolution.strategy} / ${transport} invocation did not prove workspace writes.`;
|
|
528
|
+
lastFailure = {
|
|
529
|
+
candidate: { ...candidate, diagnosis: failureDiagnosis },
|
|
530
|
+
profile,
|
|
531
|
+
args,
|
|
532
|
+
summary: candidateSummary,
|
|
533
|
+
...(result.status === null ? {} : { exitCode: result.status }),
|
|
534
|
+
stdout: result.stdout ?? "",
|
|
535
|
+
stderr: result.stderr ?? ""
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
candidateResults.push({
|
|
540
|
+
path: candidate.path,
|
|
541
|
+
installKind: candidate.diagnosis.installKind,
|
|
542
|
+
invocationMode: candidate.diagnosis.invocationMode,
|
|
543
|
+
nativeInstallValid: candidate.diagnosis.nativeInstallValid,
|
|
544
|
+
sandboxCompatible: false,
|
|
545
|
+
launchReady: false,
|
|
546
|
+
summary: candidateSummary,
|
|
547
|
+
capabilityProfile: profile,
|
|
548
|
+
...(candidate.diagnosis.remediation ? { remediation: candidate.diagnosis.remediation } : {})
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
if (selected) {
|
|
552
|
+
const result = {
|
|
553
|
+
ok: true,
|
|
554
|
+
summary: `Codex capability-driven workspace-write probe passed using ${selected.resolution.strategy} / ${selected.transport}.`,
|
|
555
|
+
availability: { ...availability, resolvedPath: selected.candidate.path, candidatePaths },
|
|
556
|
+
diagnosis: { ...selected.candidate.diagnosis, resolvedPath: selected.candidate.path },
|
|
557
|
+
command: selected.candidate.path,
|
|
558
|
+
args: selected.args,
|
|
559
|
+
capabilityProfile: selected.profile,
|
|
560
|
+
writeStrategy: selected.resolution.strategy,
|
|
561
|
+
autonomyResolution: selected.resolution,
|
|
562
|
+
promptTransport: selected.transport,
|
|
563
|
+
...(selected.exitCode === undefined ? {} : { exitCode: selected.exitCode }),
|
|
564
|
+
...(selected.stdout === undefined ? {} : { stdout: selected.stdout }),
|
|
565
|
+
...(selected.stderr === undefined ? {} : { stderr: selected.stderr }),
|
|
566
|
+
candidateProbeResults: candidateResults
|
|
567
|
+
};
|
|
568
|
+
if (input.spawnSyncImpl === undefined)
|
|
569
|
+
launchProbeCache.set(key, result);
|
|
570
|
+
return result;
|
|
571
|
+
}
|
|
572
|
+
const failed = lastFailure;
|
|
573
|
+
const result = {
|
|
574
|
+
ok: false,
|
|
575
|
+
summary: failed?.summary ?? diagnosis.remediation ?? "No Codex candidate proved governed writable execution.",
|
|
576
|
+
availability: {
|
|
577
|
+
...availability,
|
|
578
|
+
...(failed ? { resolvedPath: failed.candidate.path } : {}),
|
|
579
|
+
...(candidatePaths.length ? { candidatePaths } : {})
|
|
580
|
+
},
|
|
581
|
+
diagnosis: failed
|
|
582
|
+
? { ...failed.candidate.diagnosis, resolvedPath: failed.candidate.path }
|
|
583
|
+
: diagnosis,
|
|
584
|
+
command: failed?.candidate.path ?? availability.resolvedPath ?? availability.command,
|
|
585
|
+
args: failed?.args ?? [],
|
|
586
|
+
...(failed?.profile ? { capabilityProfile: failed.profile } : {}),
|
|
587
|
+
...(failed?.exitCode === undefined ? {} : { exitCode: failed.exitCode }),
|
|
588
|
+
...(failed?.stdout === undefined ? {} : { stdout: failed.stdout }),
|
|
589
|
+
...(failed?.stderr === undefined ? {} : { stderr: failed.stderr }),
|
|
590
|
+
candidateProbeResults: candidateResults
|
|
591
|
+
};
|
|
592
|
+
if (input.spawnSyncImpl === undefined)
|
|
593
|
+
launchProbeCache.set(key, result);
|
|
594
|
+
return result;
|
|
595
|
+
}
|