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