martin-loop
Advanced tools
| import { spawnSync } from "node:child_process"; | ||
| export type CodexFlagScope = "global" | "exec"; | ||
| export type CodexPromptTransport = "stdin-dash" | "argv"; | ||
| export type CodexWriteStrategy = "sandbox+approval" | "sandbox" | "automation" | "approval" | "default"; | ||
| export interface CodexCapabilityFlag { | ||
| flag: string; | ||
| scope: CodexFlagScope; | ||
| } | ||
| export interface CodexSandboxCapability extends CodexCapabilityFlag { | ||
| values: string[]; | ||
| } | ||
| export interface CodexApprovalCapability extends CodexCapabilityFlag { | ||
| semantics: "approval-policy" | "automation-mode"; | ||
| value?: string; | ||
| } | ||
| export interface CodexCapabilityProfile { | ||
| binaryPath: string; | ||
| supportsExec: boolean; | ||
| probeSucceeded: boolean; | ||
| probeError?: string; | ||
| model?: CodexCapabilityFlag; | ||
| cwd?: CodexCapabilityFlag; | ||
| sandbox?: CodexSandboxCapability; | ||
| /** Preferred compatibility alias. `automation` and `approvalPolicy` are authoritative. */ | ||
| approval?: CodexApprovalCapability; | ||
| automation?: CodexApprovalCapability; | ||
| approvalPolicy?: CodexApprovalCapability; | ||
| json?: CodexCapabilityFlag; | ||
| color?: CodexCapabilityFlag & { | ||
| neverValue?: string; | ||
| }; | ||
| userConfigIsolation?: CodexCapabilityFlag; | ||
| promptTransport: CodexPromptTransport; | ||
| promptTransports?: CodexPromptTransport[]; | ||
| selectedWriteStrategy?: CodexWriteStrategy; | ||
| } | ||
| export interface CodexExecArgsOptions { | ||
| command?: string; | ||
| workingDirectory: string; | ||
| sandbox?: "read-only" | "workspace-write" | "danger-full-access"; | ||
| model?: string; | ||
| extraArgs?: string[]; | ||
| mode?: "prompt" | "probe"; | ||
| prompt?: string; | ||
| capabilityProfile?: CodexCapabilityProfile; | ||
| promptTransport?: CodexPromptTransport; | ||
| writeStrategy?: CodexWriteStrategy; | ||
| } | ||
| type SpawnSyncLike = typeof spawnSync; | ||
| export declare function clearCodexCapabilityCacheForTests(): void; | ||
| export declare function cacheCodexCapabilityProfile(profile: CodexCapabilityProfile, platform?: NodeJS.Platform): CodexCapabilityProfile; | ||
| export declare function probeCodexCapabilities(binaryPath: string, options?: { | ||
| platform?: NodeJS.Platform; | ||
| spawnSyncImpl?: SpawnSyncLike; | ||
| cache?: boolean; | ||
| }): CodexCapabilityProfile; | ||
| export declare function codexWriteStrategies(profile: CodexCapabilityProfile): CodexWriteStrategy[]; | ||
| export declare function buildCodexExecArgs(options: CodexExecArgsOptions): string[]; | ||
| export declare function buildCodexStdin(profile: CodexCapabilityProfile, prompt: string, transport?: CodexPromptTransport): string | undefined; | ||
| export {}; |
| import { spawnSync } from "node:child_process"; | ||
| import { extname } from "node:path"; | ||
| import { createSpawnPlan, resolveNpmShimScript } from "./cli-bridge.js"; | ||
| const capabilityCache = new Map(); | ||
| function escapeRegex(value) { | ||
| return value.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&"); | ||
| } | ||
| function flagPattern(flag) { | ||
| return new RegExp(`(?:^|\\s)${escapeRegex(flag)}(?=\\s|[=,<\\[]|$)`, "imu"); | ||
| } | ||
| function locateFlag(globalHelp, execHelp, candidates) { | ||
| for (const flag of candidates) { | ||
| if (flagPattern(flag).test(execHelp)) | ||
| return { flag, scope: "exec" }; | ||
| } | ||
| for (const flag of candidates) { | ||
| if (flagPattern(flag).test(globalHelp)) | ||
| return { flag, scope: "global" }; | ||
| } | ||
| return undefined; | ||
| } | ||
| function flagContext(help, flag) { | ||
| const lines = help.split(/\r?\n/u); | ||
| const index = lines.findIndex((line) => line.includes(flag)); | ||
| if (index < 0) | ||
| return ""; | ||
| return lines.slice(Math.max(0, index - 1), Math.min(lines.length, index + 3)).join("\n"); | ||
| } | ||
| function buildInjectedSpawnPlan(binaryPath, args, platform) { | ||
| if (platform !== "win32") | ||
| return { command: binaryPath, args }; | ||
| const extension = extname(binaryPath).toLowerCase(); | ||
| if (extension === ".cmd" || extension === ".bat" || extension === ".ps1") { | ||
| const script = resolveNpmShimScript(binaryPath); | ||
| if (script) | ||
| return { command: process.execPath, args: [script, ...args] }; | ||
| if (extension === ".ps1") { | ||
| return { | ||
| command: "powershell.exe", | ||
| args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", binaryPath, ...args] | ||
| }; | ||
| } | ||
| return { | ||
| command: process.env.ComSpec || "cmd.exe", | ||
| args: ["/d", "/c", binaryPath, ...args] | ||
| }; | ||
| } | ||
| return { command: binaryPath, args }; | ||
| } | ||
| function runHelpProbe(input) { | ||
| const plan = input.injected | ||
| ? buildInjectedSpawnPlan(input.binaryPath, input.args, input.platform) | ||
| : createSpawnPlan(input.binaryPath, input.args, process.cwd(), false); | ||
| const result = input.spawnSyncImpl(plan.command, plan.args, { | ||
| encoding: "utf8", | ||
| stdio: ["ignore", "pipe", "pipe"], | ||
| timeout: 8_000 | ||
| }); | ||
| return { | ||
| text: `${result.stdout ?? ""}\n${result.stderr ?? ""}`, | ||
| status: result.status, | ||
| ...(result.error ? { error: result.error.message } : {}) | ||
| }; | ||
| } | ||
| function parsePromptTransports(execHelp) { | ||
| const transports = []; | ||
| const stdinAdvertised = /\bstdin\b/iu.test(execHelp) || /(?:^|\s)-(?:\s|,|$).*prompt/imu.test(execHelp); | ||
| const argvAdvertised = /(?:\[|<)PROMPT(?:\]|>)/iu.test(execHelp) || /\bPROMPT\b/iu.test(execHelp); | ||
| if (stdinAdvertised) | ||
| transports.push("stdin-dash"); | ||
| if (argvAdvertised || !stdinAdvertised) | ||
| transports.push("argv"); | ||
| return [...new Set(transports)]; | ||
| } | ||
| export function clearCodexCapabilityCacheForTests() { | ||
| capabilityCache.clear(); | ||
| } | ||
| export function cacheCodexCapabilityProfile(profile, platform = process.platform) { | ||
| capabilityCache.set(`${platform}:${profile.binaryPath}`, profile); | ||
| return profile; | ||
| } | ||
| export function probeCodexCapabilities(binaryPath, options = {}) { | ||
| const platform = options.platform ?? process.platform; | ||
| const key = `${platform}:${binaryPath}`; | ||
| const useCache = options.cache ?? options.spawnSyncImpl === undefined; | ||
| if (useCache) { | ||
| const cached = capabilityCache.get(key); | ||
| if (cached) | ||
| return cached; | ||
| } | ||
| const spawnSyncImpl = options.spawnSyncImpl ?? spawnSync; | ||
| const injected = options.spawnSyncImpl !== undefined; | ||
| const globalProbe = runHelpProbe({ | ||
| binaryPath, | ||
| args: ["--help"], | ||
| platform, | ||
| spawnSyncImpl, | ||
| injected | ||
| }); | ||
| const execProbe = runHelpProbe({ | ||
| binaryPath, | ||
| args: ["exec", "--help"], | ||
| platform, | ||
| spawnSyncImpl, | ||
| injected | ||
| }); | ||
| const globalHelp = globalProbe.text; | ||
| const execHelp = execProbe.text; | ||
| const supportsExec = execProbe.status === 0 && | ||
| !/(unknown command|unrecognized subcommand|unexpected argument ['"]?exec)/iu.test(execHelp); | ||
| const sandboxFlag = locateFlag(globalHelp, execHelp, ["--sandbox", "-s"]); | ||
| const sandboxHelp = sandboxFlag | ||
| ? `${flagContext(globalHelp, sandboxFlag.flag)}\n${flagContext(execHelp, sandboxFlag.flag)}` | ||
| : ""; | ||
| const sandboxValues = ["read-only", "workspace-write", "danger-full-access"].filter((value) => new RegExp(`\\b${escapeRegex(value)}\\b`, "iu").test(sandboxHelp)); | ||
| const automationFlag = locateFlag(globalHelp, execHelp, ["--approve-for-me", "--full-auto"]); | ||
| const automation = automationFlag | ||
| ? { ...automationFlag, semantics: "automation-mode" } | ||
| : undefined; | ||
| const approvalFlag = locateFlag(globalHelp, execHelp, ["--ask-for-approval", "-a"]); | ||
| const approvalHelp = approvalFlag | ||
| ? `${flagContext(globalHelp, approvalFlag.flag)}\n${flagContext(execHelp, approvalFlag.flag)}` | ||
| : ""; | ||
| const approvalPolicy = approvalFlag && /\bnever\b/iu.test(approvalHelp) | ||
| ? { ...approvalFlag, semantics: "approval-policy", value: "never" } | ||
| : undefined; | ||
| const colorFlag = locateFlag(globalHelp, execHelp, ["--color"]); | ||
| const colorHelp = colorFlag | ||
| ? `${flagContext(globalHelp, colorFlag.flag)}\n${flagContext(execHelp, colorFlag.flag)}` | ||
| : ""; | ||
| const promptTransports = parsePromptTransports(execHelp); | ||
| const probeError = globalProbe.error ?? execProbe.error; | ||
| const profile = { | ||
| binaryPath, | ||
| supportsExec, | ||
| probeSucceeded: supportsExec && `${globalHelp}\n${execHelp}`.trim().length > 0, | ||
| ...(probeError ? { probeError } : {}), | ||
| ...(locateFlag(globalHelp, execHelp, ["--model", "-m"]) | ||
| ? { model: locateFlag(globalHelp, execHelp, ["--model", "-m"]) } | ||
| : {}), | ||
| ...(locateFlag(globalHelp, execHelp, ["--cd", "--cwd", "--working-dir", "-C"]) | ||
| ? { cwd: locateFlag(globalHelp, execHelp, ["--cd", "--cwd", "--working-dir", "-C"]) } | ||
| : {}), | ||
| ...(sandboxFlag ? { sandbox: { ...sandboxFlag, values: sandboxValues } } : {}), | ||
| ...(automation ? { automation, approval: automation } : {}), | ||
| ...(approvalPolicy | ||
| ? { | ||
| approvalPolicy, | ||
| ...(!automation ? { approval: approvalPolicy } : {}) | ||
| } | ||
| : {}), | ||
| ...(locateFlag(globalHelp, execHelp, ["--json"]) | ||
| ? { json: locateFlag(globalHelp, execHelp, ["--json"]) } | ||
| : {}), | ||
| ...(colorFlag | ||
| ? { | ||
| color: { | ||
| ...colorFlag, | ||
| ...(/\bnever\b/iu.test(colorHelp) ? { neverValue: "never" } : {}) | ||
| } | ||
| } | ||
| : {}), | ||
| ...(locateFlag(globalHelp, execHelp, ["--ignore-user-config", "--no-user-config"]) | ||
| ? { userConfigIsolation: locateFlag(globalHelp, execHelp, ["--ignore-user-config", "--no-user-config"]) } | ||
| : {}), | ||
| promptTransport: promptTransports[0] ?? "argv", | ||
| promptTransports | ||
| }; | ||
| if (useCache) | ||
| cacheCodexCapabilityProfile(profile, platform); | ||
| return profile; | ||
| } | ||
| function pushCapabilityArg(globalArgs, execArgs, capability, ...values) { | ||
| (capability.scope === "global" ? globalArgs : execArgs).push(capability.flag, ...values); | ||
| } | ||
| export function codexWriteStrategies(profile) { | ||
| const strategies = []; | ||
| const hasWorkspaceWrite = profile.sandbox?.values.includes("workspace-write") === true; | ||
| if (hasWorkspaceWrite && profile.approvalPolicy?.value === "never") | ||
| strategies.push("sandbox+approval"); | ||
| if (hasWorkspaceWrite) | ||
| strategies.push("sandbox"); | ||
| if (profile.automation) | ||
| strategies.push("automation"); | ||
| if (profile.approvalPolicy?.value === "never") | ||
| strategies.push("approval"); | ||
| strategies.push("default"); | ||
| return [...new Set(strategies)]; | ||
| } | ||
| export function buildCodexExecArgs(options) { | ||
| const profile = options.capabilityProfile ?? probeCodexCapabilities(options.command ?? "codex"); | ||
| if (!profile.supportsExec) { | ||
| throw new Error(`Resolved Codex binary ${profile.binaryPath} does not advertise a usable exec subcommand.`); | ||
| } | ||
| const globalArgs = []; | ||
| const execArgs = []; | ||
| const requestedSandbox = options.sandbox ?? "workspace-write"; | ||
| const strategy = options.writeStrategy ?? profile.selectedWriteStrategy ?? codexWriteStrategies(profile)[0] ?? "default"; | ||
| const promptTransport = options.promptTransport ?? profile.promptTransport; | ||
| if (profile.userConfigIsolation) | ||
| pushCapabilityArg(globalArgs, execArgs, profile.userConfigIsolation); | ||
| if (profile.cwd) | ||
| pushCapabilityArg(globalArgs, execArgs, profile.cwd, options.workingDirectory); | ||
| if (requestedSandbox === "workspace-write") { | ||
| if ((strategy === "sandbox" || strategy === "sandbox+approval") && profile.sandbox?.values.includes("workspace-write")) { | ||
| pushCapabilityArg(globalArgs, execArgs, profile.sandbox, "workspace-write"); | ||
| } | ||
| if (strategy === "automation" && profile.automation) { | ||
| pushCapabilityArg(globalArgs, execArgs, profile.automation); | ||
| } | ||
| if ((strategy === "approval" || strategy === "sandbox+approval") && profile.approvalPolicy?.value) { | ||
| pushCapabilityArg(globalArgs, execArgs, profile.approvalPolicy, profile.approvalPolicy.value); | ||
| } | ||
| } | ||
| else { | ||
| if (!profile.sandbox?.values.includes(requestedSandbox)) { | ||
| throw new Error(`Resolved Codex binary ${profile.binaryPath} does not advertise requested sandbox mode ${requestedSandbox}.`); | ||
| } | ||
| pushCapabilityArg(globalArgs, execArgs, profile.sandbox, requestedSandbox); | ||
| } | ||
| if (profile.json) | ||
| pushCapabilityArg(globalArgs, execArgs, profile.json); | ||
| if (profile.color?.neverValue) | ||
| pushCapabilityArg(globalArgs, execArgs, profile.color, profile.color.neverValue); | ||
| if (options.model) { | ||
| if (!profile.model) { | ||
| throw new Error(`Resolved Codex binary ${profile.binaryPath} does not advertise a model override flag.`); | ||
| } | ||
| pushCapabilityArg(globalArgs, execArgs, profile.model, options.model); | ||
| } | ||
| const prompt = options.prompt ?? ""; | ||
| const promptArgs = promptTransport === "stdin-dash" ? ["-"] : prompt ? [prompt] : []; | ||
| return [...globalArgs, "exec", ...execArgs, ...(options.extraArgs ?? []), ...promptArgs]; | ||
| } | ||
| export function buildCodexStdin(profile, prompt, transport = profile.promptTransport) { | ||
| return transport === "stdin-dash" ? prompt : undefined; | ||
| } | ||
| //# sourceMappingURL=codex-capabilities.js.map |
| import { type CodexCliAdapterOptions as LegacyCodexCliAdapterOptions } from "./claude-cli.js"; | ||
| import { type CodexCapabilityProfile } from "./codex-launcher.js"; | ||
| export interface CodexCliAdapterOptions extends Omit<LegacyCodexCliAdapterOptions, "command"> { | ||
| /** Exact resolved executable selected by the Codex launch probe. */ | ||
| command?: string; | ||
| /** | ||
| * Optional pre-probed capability profile. Production callers normally omit | ||
| * this because probeCodexCapabilities caches profiles by exact binary path | ||
| * for the process lifetime. Exposed for deterministic integrations/tests. | ||
| */ | ||
| capabilityProfile?: CodexCapabilityProfile; | ||
| } | ||
| /** | ||
| * Capability-driven Codex CLI adapter. | ||
| * | ||
| * Provider identity remains `codex` even when doctor/preflight selected an | ||
| * absolute native binary or npm shim. The selected binary's cached capability | ||
| * profile builds both the real argv and stdin transport, while the spawn router | ||
| * sends only the Codex subprocess to that exact executable. Git/verifier | ||
| * subprocesses retain their normal commands. | ||
| */ | ||
| export declare function createCodexCliAdapter(options?: CodexCliAdapterOptions): import("../core/index.js").MartinAdapter; |
| import { spawn } from "node:child_process"; | ||
| import { createAgentCliAdapter } from "./claude-cli.js"; | ||
| import { buildCodexExecArgs, buildCodexStdin, probeCodexCapabilities } from "./codex-launcher.js"; | ||
| import { createSpawnPlan } from "./cli-bridge.js"; | ||
| function createCodexSpawnRouter(input) { | ||
| if (input.injectedSpawn) { | ||
| return (command, args = [], options) => input.injectedSpawn?.(command === "codex" ? input.selectedBinary : command, args, options); | ||
| } | ||
| return (command, args = [], options) => { | ||
| const executable = command === "codex" ? input.selectedBinary : command; | ||
| const cwd = typeof options?.cwd === "string" ? options.cwd : input.workingDirectory; | ||
| const plan = createSpawnPlan(executable, [...args], cwd, false); | ||
| return spawn(plan.command, plan.args, options ?? {}); | ||
| }; | ||
| } | ||
| /** | ||
| * Capability-driven Codex CLI adapter. | ||
| * | ||
| * Provider identity remains `codex` even when doctor/preflight selected an | ||
| * absolute native binary or npm shim. The selected binary's cached capability | ||
| * profile builds both the real argv and stdin transport, while the spawn router | ||
| * sends only the Codex subprocess to that exact executable. Git/verifier | ||
| * subprocesses retain their normal commands. | ||
| */ | ||
| export function createCodexCliAdapter(options = {}) { | ||
| const workingDirectory = options.workingDirectory ?? process.cwd(); | ||
| const selectedBinary = options.command ?? "codex"; | ||
| const capabilityProfile = options.capabilityProfile ?? probeCodexCapabilities(selectedBinary); | ||
| const sandbox = options.sandbox ?? "workspace-write"; | ||
| const extraArgs = options.extraArgs ?? []; | ||
| const launchModel = options.model; | ||
| const spawnImpl = createCodexSpawnRouter({ | ||
| selectedBinary, | ||
| workingDirectory, | ||
| ...(options.spawnImpl ? { injectedSpawn: options.spawnImpl } : {}) | ||
| }); | ||
| return createAgentCliAdapter({ | ||
| // Keep semantic provider identity stable for usage parsing, pricing, | ||
| // diagnostics, and adapter metadata. The spawn router maps this to the | ||
| // exact selected binary at process launch time. | ||
| command: "codex", | ||
| adapterIdSuffix: "codex", | ||
| model: options.model, | ||
| label: options.label ?? "Codex CLI adapter", | ||
| workingDirectory, | ||
| timeoutMs: options.timeoutMs, | ||
| verifyTimeoutMs: options.verifyTimeoutMs, | ||
| supportsJsonOutput: false, | ||
| spawnImpl, | ||
| argsBuilder: (prompt) => buildCodexExecArgs({ | ||
| command: selectedBinary, | ||
| workingDirectory, | ||
| sandbox, | ||
| ...(launchModel ? { model: launchModel } : {}), | ||
| extraArgs, | ||
| mode: "prompt", | ||
| prompt, | ||
| capabilityProfile | ||
| }), | ||
| stdinBuilder: (prompt) => buildCodexStdin(capabilityProfile, prompt) | ||
| }); | ||
| } | ||
| //# sourceMappingURL=codex-cli.js.map |
| import { spawnSync } from "node:child_process"; | ||
| import { type CodexCapabilityProfile, type CodexPromptTransport, type CodexWriteStrategy } from "./codex-capabilities.js"; | ||
| export interface CliCommandAvailability { | ||
| command: string; | ||
| available: boolean; | ||
| locator: string; | ||
| detail: string; | ||
| resolvedPath?: string; | ||
| candidatePaths?: string[]; | ||
| } | ||
| export type CodexHostPlatform = "windows" | "linux" | "wsl" | "macos"; | ||
| export type CodexInstallKind = "missing" | "native" | "windows_shim" | "windows_mounted_path"; | ||
| export type CodexInvocationMode = "direct" | "cmd_shell" | "powershell"; | ||
| export interface CodexHostDiagnosis { | ||
| hostPlatform: CodexHostPlatform; | ||
| nativeInstallValid: boolean; | ||
| installKind: CodexInstallKind; | ||
| invocationMode: CodexInvocationMode; | ||
| sandboxMode: "workspace-write"; | ||
| sandboxCompatible: boolean; | ||
| resolvedPath?: string; | ||
| nativeDependencyStatus?: "unknown" | "missing"; | ||
| nativeDependencyPackage?: string; | ||
| warnings: string[]; | ||
| remediation?: string; | ||
| } | ||
| export interface CodexProbeCandidateResult { | ||
| path: string; | ||
| installKind: CodexInstallKind; | ||
| invocationMode: CodexInvocationMode; | ||
| nativeInstallValid: boolean; | ||
| sandboxCompatible: boolean; | ||
| launchReady: boolean; | ||
| summary: string; | ||
| remediation?: string; | ||
| nativeDependencyStatus?: "unknown" | "missing"; | ||
| nativeDependencyPackage?: string; | ||
| capabilityProfile?: CodexCapabilityProfile; | ||
| writeStrategy?: CodexWriteStrategy; | ||
| promptTransport?: CodexPromptTransport; | ||
| } | ||
| export interface CodexLaunchProbeResult { | ||
| ok: boolean; | ||
| summary: string; | ||
| availability: CliCommandAvailability; | ||
| diagnosis: CodexHostDiagnosis; | ||
| command: string; | ||
| args: string[]; | ||
| capabilityProfile?: CodexCapabilityProfile; | ||
| writeStrategy?: CodexWriteStrategy; | ||
| promptTransport?: CodexPromptTransport; | ||
| exitCode?: number; | ||
| stdout?: string; | ||
| stderr?: string; | ||
| candidateProbeResults?: CodexProbeCandidateResult[]; | ||
| } | ||
| type SpawnSyncLike = typeof spawnSync; | ||
| export interface CodexSandboxPreflightOk { | ||
| ok: true; | ||
| effectiveSandbox: "read-only" | "workspace-write"; | ||
| capabilitySource: "probe"; | ||
| writableRoot: string; | ||
| } | ||
| export interface CodexSandboxPreflightReadOnly { | ||
| ok: false; | ||
| code: "provider_sandbox_read_only"; | ||
| requestedCapability: "workspace-write"; | ||
| detectedCapability: "read-only"; | ||
| effectiveSandbox: "read-only"; | ||
| affectedPath: string; | ||
| writableRoot: string; | ||
| capabilitySource: "probe"; | ||
| remediation: string; | ||
| } | ||
| export type CodexSandboxPreflightOutcome = CodexSandboxPreflightOk | CodexSandboxPreflightReadOnly; | ||
| export declare function probeFilesystemWriteCapability(directory: string): { | ||
| writable: true; | ||
| } | { | ||
| writable: false; | ||
| reason: string; | ||
| }; | ||
| export declare function checkCodexSandboxPreflight(input: { | ||
| requestedSandbox: "read-only" | "workspace-write"; | ||
| workingDirectory: string; | ||
| }): CodexSandboxPreflightOutcome; | ||
| export declare function resolveCliCommandAvailability(command: string, options?: { | ||
| platform?: NodeJS.Platform; | ||
| env?: NodeJS.ProcessEnv; | ||
| spawnSyncImpl?: SpawnSyncLike; | ||
| }): CliCommandAvailability; | ||
| export declare function detectCodexHostPlatform(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): CodexHostPlatform; | ||
| export declare function diagnoseCodexHost(availability: CliCommandAvailability, options?: { | ||
| env?: NodeJS.ProcessEnv; | ||
| platform?: NodeJS.Platform; | ||
| }): CodexHostDiagnosis; | ||
| export declare function probeCodexLaunch(input: { | ||
| workingDirectory: string; | ||
| availability?: CliCommandAvailability; | ||
| env?: NodeJS.ProcessEnv; | ||
| platform?: NodeJS.Platform; | ||
| spawnSyncImpl?: SpawnSyncLike; | ||
| model?: string; | ||
| }): CodexLaunchProbeResult; | ||
| export {}; |
| import { spawnSync } from "node:child_process"; | ||
| import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs"; | ||
| import { dirname, extname, join, resolve } from "node:path"; | ||
| import { createSpawnPlan, resolveNpmShimScript } from "./cli-bridge.js"; | ||
| import { buildCodexExecArgs, buildCodexStdin, cacheCodexCapabilityProfile, codexWriteStrategies, probeCodexCapabilities } from "./codex-capabilities.js"; | ||
| const launchProbeCache = new Map(); | ||
| export function probeFilesystemWriteCapability(directory) { | ||
| try { | ||
| mkdirSync(directory, { recursive: true }); | ||
| } | ||
| catch (error) { | ||
| return { | ||
| writable: false, | ||
| reason: `Could not create directory ${directory}: ${error instanceof Error ? error.message : String(error)}` | ||
| }; | ||
| } | ||
| let tempDir; | ||
| try { | ||
| tempDir = mkdtempSync(join(directory, ".ml-write-probe-")); | ||
| const tempFile = join(tempDir, "capability.tmp"); | ||
| writeFileSync(tempFile, "\x01", { encoding: "binary", flag: "wx" }); | ||
| unlinkSync(tempFile); | ||
| return { writable: true }; | ||
| } | ||
| catch (error) { | ||
| return { writable: false, reason: error instanceof Error ? error.message : String(error) }; | ||
| } | ||
| finally { | ||
| if (tempDir) { | ||
| try { | ||
| rmSync(tempDir, { recursive: true, force: true }); | ||
| } | ||
| catch { | ||
| // best effort cleanup | ||
| } | ||
| } | ||
| } | ||
| } | ||
| export function checkCodexSandboxPreflight(input) { | ||
| const directory = resolve(input.workingDirectory); | ||
| if (input.requestedSandbox === "read-only") { | ||
| return { | ||
| ok: true, | ||
| effectiveSandbox: "read-only", | ||
| capabilitySource: "probe", | ||
| writableRoot: directory | ||
| }; | ||
| } | ||
| const result = probeFilesystemWriteCapability(directory); | ||
| if (result.writable) { | ||
| return { | ||
| ok: true, | ||
| effectiveSandbox: "workspace-write", | ||
| capabilitySource: "probe", | ||
| writableRoot: directory | ||
| }; | ||
| } | ||
| return { | ||
| ok: false, | ||
| code: "provider_sandbox_read_only", | ||
| requestedCapability: "workspace-write", | ||
| detectedCapability: "read-only", | ||
| effectiveSandbox: "read-only", | ||
| affectedPath: directory, | ||
| writableRoot: directory, | ||
| capabilitySource: "probe", | ||
| remediation: `The working directory ${directory} is not writable by the current process. ` + | ||
| "Launch MartinLoop in a session with write access to that directory, or use `--sandbox read-only` for inspection-only work." | ||
| }; | ||
| } | ||
| function normalizeCandidates(lines) { | ||
| return [...new Set(lines.map((line) => line.trim()).filter(Boolean))]; | ||
| } | ||
| function readLocatorCandidates(command, platform, spawnSyncImpl) { | ||
| const locator = platform === "win32" ? "where.exe" : "which"; | ||
| const result = spawnSyncImpl(locator, [command], { | ||
| encoding: "utf8", | ||
| stdio: ["ignore", "pipe", "pipe"] | ||
| }); | ||
| return { | ||
| locator, | ||
| candidates: result.status === 0 ? normalizeCandidates((result.stdout ?? "").split(/\r?\n/u)) : [] | ||
| }; | ||
| } | ||
| function discoverWindowsDesktopCodexCandidates(env) { | ||
| const localAppData = env.LOCALAPPDATA; | ||
| if (!localAppData) | ||
| return []; | ||
| const base = join(localAppData, "OpenAI", "Codex", "bin"); | ||
| if (!existsSync(base)) | ||
| return []; | ||
| const found = []; | ||
| const direct = join(base, "codex.exe"); | ||
| if (existsSync(direct)) | ||
| found.push({ path: direct, mtimeMs: statSync(direct).mtimeMs }); | ||
| for (const entry of readdirSync(base, { withFileTypes: true })) { | ||
| if (!entry.isDirectory()) | ||
| continue; | ||
| const candidate = join(base, entry.name, "codex.exe"); | ||
| if (existsSync(candidate)) | ||
| found.push({ path: candidate, mtimeMs: statSync(candidate).mtimeMs }); | ||
| } | ||
| found.sort((a, b) => b.mtimeMs - a.mtimeMs); | ||
| return normalizeCandidates(found.map((item) => item.path)); | ||
| } | ||
| export function resolveCliCommandAvailability(command, options = {}) { | ||
| const platform = options.platform ?? process.platform; | ||
| const env = options.env ?? process.env; | ||
| const spawnSyncImpl = options.spawnSyncImpl ?? spawnSync; | ||
| const discovery = readLocatorCandidates(command, platform, spawnSyncImpl); | ||
| if (discovery.candidates.length > 0) { | ||
| return { | ||
| command, | ||
| available: true, | ||
| locator: discovery.locator, | ||
| detail: `${command} is available on PATH.`, | ||
| resolvedPath: discovery.candidates[0], | ||
| candidatePaths: discovery.candidates | ||
| }; | ||
| } | ||
| const offPath = discoverCommandOffPath(command, platform, env); | ||
| if (offPath) { | ||
| return { | ||
| command, | ||
| available: true, | ||
| locator: "off-path-discovery", | ||
| detail: `${command} found at ${offPath} (not on PATH, auto-discovered).`, | ||
| resolvedPath: offPath, | ||
| candidatePaths: [offPath] | ||
| }; | ||
| } | ||
| return { | ||
| command, | ||
| available: false, | ||
| locator: discovery.locator, | ||
| detail: `${command} is not installed. ${suggestInstall(command)}` | ||
| }; | ||
| } | ||
| function discoverCommandOffPath(command, platform, env) { | ||
| const home = env.HOME ?? env.USERPROFILE ?? ""; | ||
| const directories = []; | ||
| if (platform === "win32") { | ||
| if (env.APPDATA) | ||
| directories.push(join(env.APPDATA, "npm")); | ||
| if (env.LOCALAPPDATA) | ||
| directories.push(join(env.LOCALAPPDATA, "OpenAI", "Codex", "bin")); | ||
| if (home) | ||
| directories.push(join(home, "scoop", "shims")); | ||
| } | ||
| else { | ||
| directories.push("/usr/local/bin", "/opt/homebrew/bin"); | ||
| if (home) { | ||
| directories.push(join(home, ".local", "bin"), join(home, ".npm-global", "bin"), join(home, ".bun", "bin"), join(home, ".cargo", "bin")); | ||
| } | ||
| if (env.NVM_DIR) | ||
| directories.push(join(env.NVM_DIR, "current", "bin")); | ||
| } | ||
| const extensions = platform === "win32" | ||
| ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((item) => item.trim().toLowerCase()).filter(Boolean) | ||
| : [""]; | ||
| for (const directory of directories) { | ||
| for (const extension of extensions) { | ||
| const candidate = extension ? join(directory, `${command}${extension}`) : join(directory, command); | ||
| if (existsSync(candidate)) | ||
| return candidate; | ||
| } | ||
| } | ||
| return undefined; | ||
| } | ||
| function suggestInstall(command) { | ||
| const installs = { | ||
| claude: "Install with: npm install -g @anthropic-ai/claude-code", | ||
| codex: "Install with: npm install -g @openai/codex", | ||
| gemini: "Install with: npm install -g @google/gemini-cli" | ||
| }; | ||
| return installs[command] ?? `Install ${command} and ensure it is available.`; | ||
| } | ||
| export function detectCodexHostPlatform(env = process.env, platform = process.platform) { | ||
| if (platform === "win32") | ||
| return "windows"; | ||
| if (platform === "darwin") | ||
| return "macos"; | ||
| if (env.WSL_DISTRO_NAME || env.WSL_INTEROP) | ||
| return "wsl"; | ||
| return "linux"; | ||
| } | ||
| function detectInstallKind(path, host) { | ||
| if (!path) | ||
| return "missing"; | ||
| const normalized = path.replace(/\\/gu, "/").toLowerCase(); | ||
| if ((host === "linux" || host === "wsl") && normalized.startsWith("/mnt/c/")) | ||
| return "windows_mounted_path"; | ||
| if (/\.(cmd|bat|ps1)$/iu.test(normalized) || normalized.includes("/appdata/roaming/npm/")) | ||
| return "windows_shim"; | ||
| return "native"; | ||
| } | ||
| function detectInvocationMode(path, platform) { | ||
| if (platform !== "win32" || !path) | ||
| return "direct"; | ||
| const extension = extname(path).toLowerCase(); | ||
| if (extension === ".ps1") | ||
| return "powershell"; | ||
| if (extension === ".cmd" || extension === ".bat") | ||
| return "cmd_shell"; | ||
| return "direct"; | ||
| } | ||
| export function diagnoseCodexHost(availability, options = {}) { | ||
| const platform = options.platform ?? process.platform; | ||
| const hostPlatform = detectCodexHostPlatform(options.env ?? process.env, platform); | ||
| const resolvedPath = availability.resolvedPath; | ||
| const installKind = detectInstallKind(resolvedPath, hostPlatform); | ||
| const invocationMode = detectInvocationMode(resolvedPath, platform); | ||
| if (!availability.available) { | ||
| return { | ||
| hostPlatform, | ||
| nativeInstallValid: false, | ||
| installKind, | ||
| invocationMode, | ||
| sandboxMode: "workspace-write", | ||
| sandboxCompatible: false, | ||
| warnings: [], | ||
| remediation: "Install or expose the Codex CLI on PATH before running governed Codex work." | ||
| }; | ||
| } | ||
| if ((hostPlatform === "linux" || hostPlatform === "wsl") && installKind === "windows_mounted_path") { | ||
| return { | ||
| hostPlatform, | ||
| nativeInstallValid: false, | ||
| installKind, | ||
| invocationMode, | ||
| sandboxMode: "workspace-write", | ||
| sandboxCompatible: false, | ||
| resolvedPath, | ||
| warnings: ["Codex resolves to a Windows-hosted install from Linux/WSL."], | ||
| remediation: "Install Codex natively inside this Linux/WSL environment before governed work." | ||
| }; | ||
| } | ||
| return { | ||
| hostPlatform, | ||
| nativeInstallValid: true, | ||
| installKind, | ||
| invocationMode, | ||
| sandboxMode: "workspace-write", | ||
| sandboxCompatible: true, | ||
| resolvedPath, | ||
| ...(hostPlatform === "linux" || hostPlatform === "wsl" ? { nativeDependencyStatus: "unknown" } : {}), | ||
| warnings: [] | ||
| }; | ||
| } | ||
| function candidatePreference(path, diagnosis, platform) { | ||
| const base = diagnosis.installKind === "native" ? 0 : diagnosis.installKind === "windows_shim" ? 20 : 30; | ||
| if (platform !== "win32" || diagnosis.installKind !== "windows_shim") | ||
| return base; | ||
| return base + (/\.(cmd|bat|ps1)$/iu.test(path) ? 0 : 1); | ||
| } | ||
| function buildCandidates(input) { | ||
| const pathCandidates = normalizeCandidates(input.availability.candidatePaths ?? [input.availability.resolvedPath ?? input.availability.command]); | ||
| const desktop = input.platform === "win32" && input.includeDesktopCandidates | ||
| ? discoverWindowsDesktopCodexCandidates(input.env).filter((path) => !pathCandidates.includes(path)) | ||
| : []; | ||
| return [...pathCandidates, ...desktop] | ||
| .map((path, discoveryIndex) => { | ||
| const diagnosis = diagnoseCodexHost({ ...input.availability, resolvedPath: path }, { env: input.env, platform: input.platform }); | ||
| return { | ||
| path, | ||
| diagnosis, | ||
| preference: candidatePreference(path, diagnosis, input.platform), | ||
| discoveryIndex | ||
| }; | ||
| }) | ||
| .sort((a, b) => a.preference === b.preference ? a.discoveryIndex - b.discoveryIndex : a.preference - b.preference); | ||
| } | ||
| function isInsideGitRepository(workingDirectory) { | ||
| let current = resolve(workingDirectory); | ||
| while (true) { | ||
| if (existsSync(resolve(current, ".git"))) | ||
| return true; | ||
| const parent = dirname(current); | ||
| if (parent === current) | ||
| return false; | ||
| current = parent; | ||
| } | ||
| } | ||
| function buildInjectedSpawnPlan(binaryPath, args, platform) { | ||
| if (platform !== "win32") | ||
| return { command: binaryPath, args, invocationMode: "direct" }; | ||
| const extension = extname(binaryPath).toLowerCase(); | ||
| if (extension === ".cmd" || extension === ".bat" || extension === ".ps1") { | ||
| const script = resolveNpmShimScript(binaryPath); | ||
| if (script) | ||
| return { command: process.execPath, args: [script, ...args], invocationMode: "direct" }; | ||
| if (extension === ".ps1") { | ||
| return { | ||
| command: "powershell.exe", | ||
| args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", binaryPath, ...args], | ||
| invocationMode: "powershell" | ||
| }; | ||
| } | ||
| return { | ||
| command: process.env.ComSpec || "cmd.exe", | ||
| args: ["/d", "/c", binaryPath, ...args], | ||
| invocationMode: "cmd_shell" | ||
| }; | ||
| } | ||
| return { command: binaryPath, args, invocationMode: "direct" }; | ||
| } | ||
| function buildMarkerPrompt(marker) { | ||
| const command = `node -e "require('node:fs').writeFileSync(process.argv[1],'MARTIN_CODEX_WRITE_OK')" ${marker}`; | ||
| return [ | ||
| "You are validating MartinLoop Codex host readiness.", | ||
| "Do not modify tracked files.", | ||
| "Use the shell command executor exactly once.", | ||
| `Run exactly: ${command}`, | ||
| "Do not use MCP tools or alternate tools.", | ||
| "After it succeeds, reply READY." | ||
| ].join("\n"); | ||
| } | ||
| function classifyFailure(stderr, stdout, diagnosis) { | ||
| const combined = `${stderr}\n${stdout}`; | ||
| if (/@openai\/codex-linux-x64|cannot find module ['"]@openai\/codex-linux-x64['"]/iu.test(combined)) { | ||
| return { | ||
| ...diagnosis, | ||
| nativeInstallValid: false, | ||
| sandboxCompatible: false, | ||
| nativeDependencyStatus: "missing", | ||
| nativeDependencyPackage: "@openai/codex-linux-x64", | ||
| warnings: [...diagnosis.warnings, "Codex native Linux package is missing."], | ||
| remediation: "Reinstall Codex natively in this Linux/WSL environment." | ||
| }; | ||
| } | ||
| if (/CreateProcessAsUserW failed:\s*5|windows sandbox: runner error/iu.test(combined)) { | ||
| return { | ||
| ...diagnosis, | ||
| sandboxCompatible: false, | ||
| warnings: [...diagnosis.warnings, "This Codex invocation could not create a writable subprocess on Windows."], | ||
| remediation: "Use another capability strategy advertised by this exact Codex binary, or repair the host sandbox." | ||
| }; | ||
| } | ||
| return diagnosis; | ||
| } | ||
| function probeCacheKey(input) { | ||
| return JSON.stringify({ | ||
| workingDirectory: resolve(input.workingDirectory), | ||
| platform: input.platform, | ||
| candidatePaths: input.candidatePaths, | ||
| model: input.model | ||
| }); | ||
| } | ||
| export function probeCodexLaunch(input) { | ||
| const availability = input.availability ?? resolveCliCommandAvailability("codex", { | ||
| ...(input.platform ? { platform: input.platform } : {}), | ||
| ...(input.env ? { env: input.env } : {}), | ||
| ...(input.spawnSyncImpl ? { spawnSyncImpl: input.spawnSyncImpl } : {}) | ||
| }); | ||
| const diagnosis = diagnoseCodexHost(availability, { | ||
| ...(input.platform ? { platform: input.platform } : {}), | ||
| ...(input.env ? { env: input.env } : {}) | ||
| }); | ||
| if (!availability.available) { | ||
| return { ok: false, summary: availability.detail, availability, diagnosis, command: availability.command, args: [] }; | ||
| } | ||
| if (!isInsideGitRepository(input.workingDirectory)) { | ||
| return { | ||
| ok: false, | ||
| summary: "Working directory is not inside a git repository. Codex exec requires a trusted repository for governed work.", | ||
| availability, | ||
| diagnosis, | ||
| command: availability.resolvedPath ?? availability.command, | ||
| args: [] | ||
| }; | ||
| } | ||
| const platform = input.platform ?? process.platform; | ||
| const env = input.env ?? process.env; | ||
| const spawnSyncImpl = input.spawnSyncImpl ?? spawnSync; | ||
| const candidates = buildCandidates({ | ||
| availability, | ||
| env, | ||
| platform, | ||
| includeDesktopCandidates: input.spawnSyncImpl === undefined | ||
| }); | ||
| const candidatePaths = candidates.map((candidate) => candidate.path); | ||
| const key = probeCacheKey({ | ||
| workingDirectory: input.workingDirectory, | ||
| platform, | ||
| candidatePaths, | ||
| ...(input.model ? { model: input.model } : {}) | ||
| }); | ||
| if (input.spawnSyncImpl === undefined) { | ||
| const cached = launchProbeCache.get(key); | ||
| if (cached) | ||
| return cached; | ||
| } | ||
| const candidateResults = []; | ||
| let selected; | ||
| let lastFailure; | ||
| candidateLoop: for (const candidate of candidates) { | ||
| if (!candidate.diagnosis.nativeInstallValid) { | ||
| candidateResults.push({ | ||
| path: candidate.path, | ||
| installKind: candidate.diagnosis.installKind, | ||
| invocationMode: candidate.diagnosis.invocationMode, | ||
| nativeInstallValid: false, | ||
| sandboxCompatible: false, | ||
| launchReady: false, | ||
| summary: candidate.diagnosis.remediation ?? "Codex installation is not valid for this host." | ||
| }); | ||
| continue; | ||
| } | ||
| const profile = probeCodexCapabilities(candidate.path, { | ||
| platform, | ||
| spawnSyncImpl, | ||
| cache: input.spawnSyncImpl === undefined | ||
| }); | ||
| if (!profile.supportsExec) { | ||
| candidateResults.push({ | ||
| path: candidate.path, | ||
| installKind: candidate.diagnosis.installKind, | ||
| invocationMode: candidate.diagnosis.invocationMode, | ||
| nativeInstallValid: true, | ||
| sandboxCompatible: false, | ||
| launchReady: false, | ||
| summary: "Resolved Codex binary does not advertise a usable exec subcommand.", | ||
| capabilityProfile: profile | ||
| }); | ||
| continue; | ||
| } | ||
| const transports = profile.promptTransports?.length ? profile.promptTransports : [profile.promptTransport]; | ||
| const strategies = codexWriteStrategies(profile); | ||
| let candidateSummary = "No advertised Codex invocation strategy proved writable execution."; | ||
| for (const strategy of strategies) { | ||
| for (const transport of transports) { | ||
| const marker = `.martin-codex-write-probe-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.tmp`; | ||
| const markerPath = join(input.workingDirectory, marker); | ||
| const prompt = buildMarkerPrompt(marker); | ||
| let args; | ||
| try { | ||
| args = buildCodexExecArgs({ | ||
| command: candidate.path, | ||
| workingDirectory: input.workingDirectory, | ||
| sandbox: "workspace-write", | ||
| ...(input.model ? { model: input.model } : {}), | ||
| mode: "probe", | ||
| prompt, | ||
| capabilityProfile: profile, | ||
| promptTransport: transport, | ||
| writeStrategy: strategy | ||
| }); | ||
| } | ||
| catch (error) { | ||
| candidateSummary = error instanceof Error ? error.message : String(error); | ||
| lastFailure = { candidate, profile, args: [], summary: candidateSummary }; | ||
| continue; | ||
| } | ||
| const plan = input.spawnSyncImpl | ||
| ? buildInjectedSpawnPlan(candidate.path, args, platform) | ||
| : { ...createSpawnPlan(candidate.path, args, input.workingDirectory, false), invocationMode: "direct" }; | ||
| const stdin = buildCodexStdin(profile, prompt, transport); | ||
| const result = spawnSyncImpl(plan.command, plan.args, { | ||
| cwd: input.workingDirectory, | ||
| encoding: "utf8", | ||
| stdio: [stdin === undefined ? "ignore" : "pipe", "pipe", "pipe"], | ||
| ...(stdin !== undefined ? { input: stdin } : {}) | ||
| }); | ||
| let markerVerified = false; | ||
| try { | ||
| markerVerified = existsSync(markerPath) && readFileSync(markerPath, "utf8") === "MARTIN_CODEX_WRITE_OK"; | ||
| } | ||
| finally { | ||
| try { | ||
| unlinkSync(markerPath); | ||
| } | ||
| catch { | ||
| // best effort cleanup | ||
| } | ||
| } | ||
| if (!result.error && result.status === 0 && markerVerified) { | ||
| const negotiated = cacheCodexCapabilityProfile({ ...profile, selectedWriteStrategy: strategy, promptTransport: transport }, platform); | ||
| selected = { | ||
| candidate: { | ||
| ...candidate, | ||
| diagnosis: { ...candidate.diagnosis, invocationMode: plan.invocationMode } | ||
| }, | ||
| profile: negotiated, | ||
| args, | ||
| strategy, | ||
| transport, | ||
| ...(result.status === null ? {} : { exitCode: result.status }), | ||
| stdout: result.stdout ?? "", | ||
| stderr: result.stderr ?? "" | ||
| }; | ||
| candidateResults.push({ | ||
| path: candidate.path, | ||
| installKind: candidate.diagnosis.installKind, | ||
| invocationMode: plan.invocationMode, | ||
| nativeInstallValid: true, | ||
| sandboxCompatible: true, | ||
| launchReady: true, | ||
| summary: `Codex capability negotiation passed with ${strategy} / ${transport}.`, | ||
| capabilityProfile: negotiated, | ||
| writeStrategy: strategy, | ||
| promptTransport: transport | ||
| }); | ||
| break candidateLoop; | ||
| } | ||
| const failureDiagnosis = classifyFailure(result.stderr ?? "", result.stdout ?? "", candidate.diagnosis); | ||
| candidateSummary = result.error | ||
| ? `Codex launch probe failed: ${result.error.message}` | ||
| : result.status !== 0 | ||
| ? `Codex launch probe exited non-zero: ${(result.stderr ?? result.stdout ?? "").trim() || String(result.status)}` | ||
| : `Codex ${strategy} / ${transport} invocation did not prove workspace writes.`; | ||
| lastFailure = { | ||
| candidate: { ...candidate, diagnosis: failureDiagnosis }, | ||
| profile, | ||
| args, | ||
| summary: candidateSummary, | ||
| ...(result.status === null ? {} : { exitCode: result.status }), | ||
| stdout: result.stdout ?? "", | ||
| stderr: result.stderr ?? "" | ||
| }; | ||
| } | ||
| } | ||
| candidateResults.push({ | ||
| path: candidate.path, | ||
| installKind: candidate.diagnosis.installKind, | ||
| invocationMode: candidate.diagnosis.invocationMode, | ||
| nativeInstallValid: candidate.diagnosis.nativeInstallValid, | ||
| sandboxCompatible: false, | ||
| launchReady: false, | ||
| summary: candidateSummary, | ||
| capabilityProfile: profile, | ||
| ...(candidate.diagnosis.remediation ? { remediation: candidate.diagnosis.remediation } : {}) | ||
| }); | ||
| } | ||
| if (selected) { | ||
| const result = { | ||
| ok: true, | ||
| summary: `Codex capability-driven workspace-write probe passed using ${selected.strategy} / ${selected.transport}.`, | ||
| availability: { ...availability, resolvedPath: selected.candidate.path, candidatePaths }, | ||
| diagnosis: { ...selected.candidate.diagnosis, resolvedPath: selected.candidate.path }, | ||
| command: selected.candidate.path, | ||
| args: selected.args, | ||
| capabilityProfile: selected.profile, | ||
| writeStrategy: selected.strategy, | ||
| promptTransport: selected.transport, | ||
| ...(selected.exitCode === undefined ? {} : { exitCode: selected.exitCode }), | ||
| ...(selected.stdout === undefined ? {} : { stdout: selected.stdout }), | ||
| ...(selected.stderr === undefined ? {} : { stderr: selected.stderr }), | ||
| candidateProbeResults: candidateResults | ||
| }; | ||
| if (input.spawnSyncImpl === undefined) | ||
| launchProbeCache.set(key, result); | ||
| return result; | ||
| } | ||
| const failed = lastFailure; | ||
| const result = { | ||
| ok: false, | ||
| summary: failed?.summary ?? diagnosis.remediation ?? "No Codex candidate proved governed writable execution.", | ||
| availability: { | ||
| ...availability, | ||
| ...(failed ? { resolvedPath: failed.candidate.path } : {}), | ||
| ...(candidatePaths.length ? { candidatePaths } : {}) | ||
| }, | ||
| diagnosis: failed | ||
| ? { ...failed.candidate.diagnosis, resolvedPath: failed.candidate.path } | ||
| : diagnosis, | ||
| command: failed?.candidate.path ?? availability.resolvedPath ?? availability.command, | ||
| args: failed?.args ?? [], | ||
| ...(failed?.profile ? { capabilityProfile: failed.profile } : {}), | ||
| ...(failed?.exitCode === undefined ? {} : { exitCode: failed.exitCode }), | ||
| ...(failed?.stdout === undefined ? {} : { stdout: failed.stdout }), | ||
| ...(failed?.stderr === undefined ? {} : { stderr: failed.stderr }), | ||
| candidateProbeResults: candidateResults | ||
| }; | ||
| if (input.spawnSyncImpl === undefined) | ||
| launchProbeCache.set(key, result); | ||
| return result; | ||
| } | ||
| //# sourceMappingURL=codex-host.js.map |
@@ -43,1 +43,13 @@ # MartinLoop Demo Sandbox | ||
| ``` | ||
| ## What to look for | ||
| The demo is a small way to see MartinLoop's larger execution-control model: | ||
| ```text | ||
| Definition of Done -> Controlled Run -> Verified Handoff | ||
| ``` | ||
| A live governed run should make the budget, verifier, attempts, final outcome, and receipt evidence inspectable. `--proof` is intentionally different: it runs real verifier checks without claiming a governed coding-agent edit. | ||
| For the current product and agent-facing definitions see [`../../README.md`](../../README.md), [`../../llms.txt`](../../llms.txt), and [`../../docs/for-agents.md`](../../docs/for-agents.md). |
@@ -1,138 +0,10 @@ | ||
| import { spawnSync } from "node:child_process"; | ||
| export interface CliCommandAvailability { | ||
| command: string; | ||
| available: boolean; | ||
| locator: string; | ||
| detail: string; | ||
| resolvedPath?: string; | ||
| candidatePaths?: string[]; | ||
| } | ||
| export type CodexHostPlatform = "windows" | "linux" | "wsl" | "macos"; | ||
| export type CodexInstallKind = "missing" | "native" | "windows_shim" | "windows_mounted_path"; | ||
| export type CodexInvocationMode = "direct" | "cmd_shell" | "powershell"; | ||
| export interface CodexHostDiagnosis { | ||
| hostPlatform: CodexHostPlatform; | ||
| nativeInstallValid: boolean; | ||
| installKind: CodexInstallKind; | ||
| invocationMode: CodexInvocationMode; | ||
| sandboxMode: "workspace-write"; | ||
| sandboxCompatible: boolean; | ||
| resolvedPath?: string; | ||
| nativeDependencyStatus?: "unknown" | "missing"; | ||
| nativeDependencyPackage?: string; | ||
| warnings: string[]; | ||
| remediation?: string; | ||
| } | ||
| export interface CodexLaunchProbeResult { | ||
| ok: boolean; | ||
| summary: string; | ||
| availability: CliCommandAvailability; | ||
| diagnosis: CodexHostDiagnosis; | ||
| command: string; | ||
| args: string[]; | ||
| exitCode?: number; | ||
| stdout?: string; | ||
| stderr?: string; | ||
| candidateProbeResults?: CodexProbeCandidateResult[]; | ||
| } | ||
| export interface CodexExecArgsOptions { | ||
| workingDirectory: string; | ||
| sandbox?: "read-only" | "workspace-write" | "danger-full-access"; | ||
| model?: string; | ||
| extraArgs?: string[]; | ||
| mode?: "prompt" | "probe"; | ||
| } | ||
| type SpawnSyncLike = typeof spawnSync; | ||
| import { type CodexExecArgsOptions } from "./codex-capabilities.js"; | ||
| /** | ||
| * Outcome when the preflight probe confirms the working directory is writable. | ||
| * capabilitySource is always "probe" — result is measured, not assumed. | ||
| * Compatibility facade for callers that still populate the original | ||
| * `approval` profile field. Internally automation modes and approval policies | ||
| * remain separate capabilities, but either shape drives the same dynamic | ||
| * builder without restoring any hard-coded flag assumption. | ||
| */ | ||
| export interface CodexSandboxPreflightOk { | ||
| ok: true; | ||
| effectiveSandbox: "read-only" | "workspace-write"; | ||
| capabilitySource: "probe"; | ||
| writableRoot: string; | ||
| } | ||
| /** | ||
| * Outcome when the working directory cannot be written but workspace-write | ||
| * was requested. This is a first-class typed failure — distinct from a | ||
| * provider-unavailable or environment-mismatch error. No model call has | ||
| * been attempted when this is returned. | ||
| */ | ||
| export interface CodexSandboxPreflightReadOnly { | ||
| ok: false; | ||
| code: "provider_sandbox_read_only"; | ||
| requestedCapability: "workspace-write"; | ||
| detectedCapability: "read-only"; | ||
| effectiveSandbox: "read-only"; | ||
| affectedPath: string; | ||
| writableRoot: string; | ||
| capabilitySource: "probe"; | ||
| remediation: string; | ||
| } | ||
| export type CodexSandboxPreflightOutcome = CodexSandboxPreflightOk | CodexSandboxPreflightReadOnly; | ||
| /** | ||
| * Probes whether the given directory is writable by the current process. | ||
| * | ||
| * Strategy: create a uniquely named temp file inside the directory, write a | ||
| * sentinel byte, then remove it. This is a real filesystem action — not an | ||
| * inference from binary metadata or launch-probe output. | ||
| * | ||
| * The probe leaves no file behind on either success or failure. | ||
| * | ||
| * Exported for unit testing with a real tmp directory. | ||
| */ | ||
| export declare function probeFilesystemWriteCapability(directory: string): { | ||
| writable: true; | ||
| } | { | ||
| writable: false; | ||
| reason: string; | ||
| }; | ||
| /** | ||
| * Checks whether the requested sandbox mode is achievable for the given | ||
| * working directory. The adapter receives `requestedSandbox` from CLI/core — | ||
| * it does not decide the mode itself. | ||
| * | ||
| * When `requestedSandbox` is "workspace-write" and the working directory is not | ||
| * writable, this function returns `provider_sandbox_read_only` before any model | ||
| * execution is attempted. | ||
| * | ||
| * When `requestedSandbox` is "read-only" no write probe is performed; the | ||
| * outcome is `ok: true, effectiveSandbox: "read-only"` immediately. | ||
| */ | ||
| export declare function checkCodexSandboxPreflight(input: { | ||
| requestedSandbox: "read-only" | "workspace-write"; | ||
| workingDirectory: string; | ||
| }): CodexSandboxPreflightOutcome; | ||
| export interface CodexProbeCandidateResult { | ||
| path: string; | ||
| installKind: CodexInstallKind; | ||
| invocationMode: CodexInvocationMode; | ||
| nativeInstallValid: boolean; | ||
| sandboxCompatible: boolean; | ||
| launchReady: boolean; | ||
| summary: string; | ||
| remediation?: string; | ||
| nativeDependencyStatus?: "unknown" | "missing"; | ||
| nativeDependencyPackage?: string; | ||
| } | ||
| export declare function buildCodexExecArgs(options: CodexExecArgsOptions): string[]; | ||
| export declare function resolveCliCommandAvailability(command: string, options?: { | ||
| platform?: NodeJS.Platform; | ||
| env?: NodeJS.ProcessEnv; | ||
| spawnSyncImpl?: SpawnSyncLike; | ||
| }): CliCommandAvailability; | ||
| export declare function detectCodexHostPlatform(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): CodexHostPlatform; | ||
| export declare function diagnoseCodexHost(availability: CliCommandAvailability, options?: { | ||
| env?: NodeJS.ProcessEnv; | ||
| platform?: NodeJS.Platform; | ||
| }): CodexHostDiagnosis; | ||
| export declare function probeCodexLaunch(input: { | ||
| workingDirectory: string; | ||
| availability?: CliCommandAvailability; | ||
| env?: NodeJS.ProcessEnv; | ||
| platform?: NodeJS.Platform; | ||
| spawnSyncImpl?: SpawnSyncLike; | ||
| model?: string; | ||
| }): CodexLaunchProbeResult; | ||
| export {}; | ||
| export { buildCodexStdin, cacheCodexCapabilityProfile, clearCodexCapabilityCacheForTests, codexWriteStrategies, probeCodexCapabilities, type CodexApprovalCapability, type CodexCapabilityFlag, type CodexCapabilityProfile, type CodexExecArgsOptions, type CodexFlagScope, type CodexPromptTransport, type CodexSandboxCapability, type CodexWriteStrategy } from "./codex-capabilities.js"; | ||
| export { checkCodexSandboxPreflight, detectCodexHostPlatform, diagnoseCodexHost, probeCodexLaunch, probeFilesystemWriteCapability, resolveCliCommandAvailability, type CliCommandAvailability, type CodexHostDiagnosis, type CodexHostPlatform, type CodexInstallKind, type CodexInvocationMode, type CodexLaunchProbeResult, type CodexProbeCandidateResult, type CodexSandboxPreflightOk, type CodexSandboxPreflightOutcome, type CodexSandboxPreflightReadOnly } from "./codex-host.js"; |
@@ -1,772 +0,23 @@ | ||
| import { spawnSync } from "node:child_process"; | ||
| import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs"; | ||
| import { dirname, extname, join, resolve } from "node:path"; | ||
| import { resolveNpmShimScript } from "./cli-bridge.js"; | ||
| const codexLaunchProbeCache = new Map(); | ||
| import { buildCodexExecArgs as buildCapabilityDrivenCodexExecArgs } from "./codex-capabilities.js"; | ||
| /** | ||
| * Probes whether the given directory is writable by the current process. | ||
| * | ||
| * Strategy: create a uniquely named temp file inside the directory, write a | ||
| * sentinel byte, then remove it. This is a real filesystem action — not an | ||
| * inference from binary metadata or launch-probe output. | ||
| * | ||
| * The probe leaves no file behind on either success or failure. | ||
| * | ||
| * Exported for unit testing with a real tmp directory. | ||
| * Compatibility facade for callers that still populate the original | ||
| * `approval` profile field. Internally automation modes and approval policies | ||
| * remain separate capabilities, but either shape drives the same dynamic | ||
| * builder without restoring any hard-coded flag assumption. | ||
| */ | ||
| export function probeFilesystemWriteCapability(directory) { | ||
| // Ensure the directory exists before probing. | ||
| try { | ||
| mkdirSync(directory, { recursive: true }); | ||
| } | ||
| catch (err) { | ||
| return { | ||
| writable: false, | ||
| reason: `Could not create directory ${directory}: ${err instanceof Error ? err.message : String(err)}` | ||
| }; | ||
| } | ||
| // Use mkdtempSync so the filename is guaranteed unique even under concurrent runs. | ||
| let tempDir; | ||
| try { | ||
| tempDir = mkdtempSync(join(directory, ".ml-write-probe-")); | ||
| const tempFile = join(tempDir, "capability.tmp"); | ||
| writeFileSync(tempFile, "\x01", { encoding: "binary", flag: "wx" }); | ||
| unlinkSync(tempFile); | ||
| return { writable: true }; | ||
| } | ||
| catch (err) { | ||
| return { | ||
| writable: false, | ||
| reason: err instanceof Error ? err.message : String(err) | ||
| }; | ||
| } | ||
| finally { | ||
| if (tempDir) { | ||
| try { | ||
| rmSync(tempDir, { recursive: true, force: true }); | ||
| } | ||
| catch { /* ignore cleanup errors */ } | ||
| } | ||
| } | ||
| } | ||
| /** | ||
| * Checks whether the requested sandbox mode is achievable for the given | ||
| * working directory. The adapter receives `requestedSandbox` from CLI/core — | ||
| * it does not decide the mode itself. | ||
| * | ||
| * When `requestedSandbox` is "workspace-write" and the working directory is not | ||
| * writable, this function returns `provider_sandbox_read_only` before any model | ||
| * execution is attempted. | ||
| * | ||
| * When `requestedSandbox` is "read-only" no write probe is performed; the | ||
| * outcome is `ok: true, effectiveSandbox: "read-only"` immediately. | ||
| */ | ||
| export function checkCodexSandboxPreflight(input) { | ||
| const dir = resolve(input.workingDirectory); | ||
| if (input.requestedSandbox === "read-only") { | ||
| return { | ||
| ok: true, | ||
| effectiveSandbox: "read-only", | ||
| capabilitySource: "probe", | ||
| writableRoot: dir | ||
| }; | ||
| } | ||
| // workspace-write: run the actual filesystem probe. | ||
| const probeResult = probeFilesystemWriteCapability(dir); | ||
| if (probeResult.writable) { | ||
| return { | ||
| ok: true, | ||
| effectiveSandbox: "workspace-write", | ||
| capabilitySource: "probe", | ||
| writableRoot: dir | ||
| }; | ||
| } | ||
| return { | ||
| ok: false, | ||
| code: "provider_sandbox_read_only", | ||
| requestedCapability: "workspace-write", | ||
| detectedCapability: "read-only", | ||
| effectiveSandbox: "read-only", | ||
| affectedPath: dir, | ||
| writableRoot: dir, | ||
| capabilitySource: "probe", | ||
| remediation: `The working directory ${dir} is not writable by the current process. ` + | ||
| "Launch MartinLoop in a session with write access to that directory, or use " + | ||
| "`--sandbox read-only` for inspection-only work." | ||
| }; | ||
| } | ||
| const CODEX_LAUNCH_PROBE_PROMPT = [ | ||
| "You are validating MartinLoop Codex host readiness.", | ||
| "Do not edit files.", | ||
| "Use the shell command executor exactly once to run: git status --short -- .", | ||
| "Do not use MCP tools, Node REPL, or any fallback tool if shell execution fails.", | ||
| "If the shell command succeeds, reply with READY only.", | ||
| "If it fails, reply with the exact failure in one sentence." | ||
| ].join("\n"); | ||
| function buildProbeCacheKey(input) { | ||
| return JSON.stringify({ | ||
| workingDirectory: resolve(input.workingDirectory), | ||
| platform: input.platform, | ||
| candidatePaths: input.candidatePaths, | ||
| model: input.model | ||
| }); | ||
| } | ||
| function isInsideGitRepository(workingDirectory) { | ||
| let current = resolve(workingDirectory); | ||
| while (true) { | ||
| if (existsSync(resolve(current, ".git"))) { | ||
| return true; | ||
| } | ||
| const parent = dirname(current); | ||
| if (parent === current) { | ||
| return false; | ||
| } | ||
| current = parent; | ||
| } | ||
| } | ||
| function normalizeCandidates(lines) { | ||
| return [...new Set(lines.map((line) => line.trim()).filter(Boolean))]; | ||
| } | ||
| function readLocatorCandidates(command, platform, spawnSyncImpl) { | ||
| const locator = platform === "win32" ? "where.exe" : "which"; | ||
| const result = spawnSyncImpl(locator, [command], { | ||
| encoding: "utf8", | ||
| stdio: ["ignore", "pipe", "pipe"] | ||
| }); | ||
| return { | ||
| locator, | ||
| candidates: result.status === 0 | ||
| ? normalizeCandidates((result.stdout ?? "").split(/\r?\n/u)) | ||
| : [], | ||
| foundOnPath: result.status === 0 | ||
| }; | ||
| } | ||
| function discoverWindowsDesktopCodexCandidates(env) { | ||
| const localAppData = env["LOCALAPPDATA"]; | ||
| if (!localAppData) { | ||
| return []; | ||
| } | ||
| const baseDirectory = join(localAppData, "OpenAI", "Codex", "bin"); | ||
| if (!existsSync(baseDirectory)) { | ||
| return []; | ||
| } | ||
| const candidates = []; | ||
| const directCandidate = join(baseDirectory, "codex.exe"); | ||
| if (existsSync(directCandidate)) { | ||
| candidates.push({ path: directCandidate, mtimeMs: statSync(directCandidate).mtimeMs }); | ||
| } | ||
| for (const entry of readdirSync(baseDirectory, { withFileTypes: true })) { | ||
| if (!entry.isDirectory()) { | ||
| continue; | ||
| } | ||
| const candidate = join(baseDirectory, entry.name, "codex.exe"); | ||
| if (!existsSync(candidate)) { | ||
| continue; | ||
| } | ||
| candidates.push({ path: candidate, mtimeMs: statSync(candidate).mtimeMs }); | ||
| } | ||
| candidates.sort((left, right) => right.mtimeMs - left.mtimeMs); | ||
| return normalizeCandidates(candidates.map((candidate) => candidate.path)); | ||
| } | ||
| function codexProbePreference(diagnosis) { | ||
| if (diagnosis.installKind === "native" && diagnosis.invocationMode === "direct") { | ||
| return 0; | ||
| } | ||
| if (diagnosis.installKind === "native") { | ||
| return 1; | ||
| } | ||
| if (diagnosis.installKind === "windows_shim") { | ||
| return 2; | ||
| } | ||
| return 3; | ||
| } | ||
| function codexProbeCandidatePreference(path, diagnosis, platform) { | ||
| const hostPreference = codexProbePreference(diagnosis) * 10; | ||
| if (platform !== "win32" || diagnosis.installKind !== "windows_shim") { | ||
| return hostPreference; | ||
| } | ||
| // `where codex` commonly returns npm's extensionless POSIX shim before | ||
| // `codex.cmd`. Node cannot spawn that text shim directly on Windows, while | ||
| // the .cmd/.ps1 shim has a supported invocation path (or can be unwrapped). | ||
| const extension = extname(path).toLowerCase(); | ||
| return hostPreference + (extension === ".cmd" || extension === ".bat" || extension === ".ps1" ? 0 : 1); | ||
| } | ||
| function buildProbeCandidates(input) { | ||
| const pathCandidates = normalizeCandidates(input.availability.candidatePaths ?? [input.availability.resolvedPath ?? input.availability.command]); | ||
| const desktopCandidates = input.platform === "win32" && input.includeDesktopCandidates | ||
| ? discoverWindowsDesktopCodexCandidates(input.env).filter((candidatePath) => !pathCandidates.includes(candidatePath)) | ||
| : []; | ||
| return [...pathCandidates, ...desktopCandidates] | ||
| .map((path, discoveryIndex) => { | ||
| const diagnosis = diagnoseCodexHost({ | ||
| ...input.availability, | ||
| resolvedPath: path | ||
| }, { | ||
| env: input.env, | ||
| platform: input.platform | ||
| }); | ||
| return { | ||
| path, | ||
| diagnosis, | ||
| preference: input.platform === "win32" | ||
| ? codexProbeCandidatePreference(path, diagnosis, input.platform) | ||
| : 0, | ||
| discoveryIndex | ||
| }; | ||
| }) | ||
| .sort((left, right) => left.preference === right.preference | ||
| ? left.discoveryIndex - right.discoveryIndex | ||
| : left.preference - right.preference); | ||
| } | ||
| function buildProbeCommand(command, args, platform) { | ||
| if (platform !== "win32") { | ||
| return { command, args, invocationMode: "direct" }; | ||
| } | ||
| const extension = extname(command).toLowerCase(); | ||
| switch (extension) { | ||
| case ".cmd": | ||
| case ".bat": | ||
| case ".ps1": { | ||
| // Bypass the npm shim's cmd.exe/powershell.exe wrapper hop when we can statically resolve | ||
| // the real `node <script>` target it wraps — keeps the live probe's process-nesting depth | ||
| // consistent with the real run's invocation (see createSpawnPlan in cli-bridge.ts). | ||
| const directScript = resolveNpmShimScript(command); | ||
| if (directScript !== undefined) { | ||
| return { command: process.execPath, args: [directScript, ...args], invocationMode: "direct" }; | ||
| } | ||
| if (extension === ".ps1") { | ||
| return { | ||
| command: "powershell.exe", | ||
| args: ["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", command, ...args], | ||
| invocationMode: "powershell" | ||
| }; | ||
| } | ||
| return { | ||
| command: process.env.ComSpec || "cmd.exe", | ||
| args: ["/d", "/c", command, ...args], | ||
| invocationMode: "cmd_shell" | ||
| }; | ||
| } | ||
| default: | ||
| return { command, args, invocationMode: "direct" }; | ||
| } | ||
| } | ||
| function detectInstallKind(resolvedPath, hostPlatform) { | ||
| if (!resolvedPath) { | ||
| return "missing"; | ||
| } | ||
| const normalizedPath = resolvedPath.replace(/\\/gu, "/").toLowerCase(); | ||
| const looksWindowsShim = normalizedPath.endsWith(".cmd") || | ||
| normalizedPath.endsWith(".bat") || | ||
| normalizedPath.endsWith(".ps1") || | ||
| normalizedPath.includes("/appdata/roaming/npm/"); | ||
| const looksMountedWindowsPath = normalizedPath.startsWith("/mnt/c/"); | ||
| if ((hostPlatform === "linux" || hostPlatform === "wsl") && looksMountedWindowsPath) { | ||
| return "windows_mounted_path"; | ||
| } | ||
| if (looksWindowsShim) { | ||
| return "windows_shim"; | ||
| } | ||
| return "native"; | ||
| } | ||
| function detectInvocationMode(resolvedPath, platform) { | ||
| if (platform !== "win32" || !resolvedPath) { | ||
| return "direct"; | ||
| } | ||
| const extension = extname(resolvedPath).toLowerCase(); | ||
| if (extension === ".ps1") { | ||
| return "powershell"; | ||
| } | ||
| if (extension === ".cmd" || extension === ".bat") { | ||
| return "cmd_shell"; | ||
| } | ||
| return "direct"; | ||
| } | ||
| function classifyProbeFailure(stderr, stdout, diagnosis) { | ||
| const combined = `${stderr}\n${stdout}`; | ||
| if (/MARTIN_REMOTE_TOKEN/iu.test(combined) || | ||
| /AuthRequired\(AuthRequiredError/iu.test(combined) || | ||
| /www_authenticate_header/iu.test(combined)) { | ||
| const warnings = [...diagnosis.warnings]; | ||
| const configCollisionWarning = "Codex inherited auth-sensitive MCP or plugin state from the operator's default user config."; | ||
| if (!warnings.includes(configCollisionWarning)) { | ||
| warnings.push(configCollisionWarning); | ||
| } | ||
| return { | ||
| 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.", | ||
| diagnosis: { | ||
| ...diagnosis, | ||
| warnings, | ||
| 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." | ||
| } | ||
| }; | ||
| } | ||
| if (/not supported when using Codex with a ChatGPT account/iu.test(combined) || | ||
| /gpt-5\.3-codex/iu.test(combined)) { | ||
| const warnings = [...diagnosis.warnings]; | ||
| const unsupportedModelWarning = "Codex tried to use a model that is not supported for ChatGPT-account authentication."; | ||
| if (!warnings.includes(unsupportedModelWarning)) { | ||
| warnings.push(unsupportedModelWarning); | ||
| } | ||
| return { | ||
| 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.", | ||
| diagnosis: { | ||
| ...diagnosis, | ||
| warnings, | ||
| remediation: "Override the Codex launch model to a ChatGPT-account-supported option before running governed Codex work." | ||
| } | ||
| }; | ||
| } | ||
| if (/@openai\/codex-linux-x64/iu.test(combined) || | ||
| /cannot find module ['"]@openai\/codex-linux-x64['"]/iu.test(combined)) { | ||
| const warnings = [...diagnosis.warnings]; | ||
| const missingDependencyWarning = "Codex is missing the native Linux package '@openai/codex-linux-x64' required for this host."; | ||
| if (!warnings.includes(missingDependencyWarning)) { | ||
| warnings.push(missingDependencyWarning); | ||
| } | ||
| return { | ||
| summary: "Codex native dependency '@openai/codex-linux-x64' is missing for this Linux/WSL environment.", | ||
| diagnosis: { | ||
| ...diagnosis, | ||
| nativeInstallValid: false, | ||
| sandboxCompatible: false, | ||
| warnings, | ||
| nativeDependencyStatus: "missing", | ||
| nativeDependencyPackage: "@openai/codex-linux-x64", | ||
| remediation: "Reinstall Codex natively inside this Linux/WSL environment so the '@openai/codex-linux-x64' package is present before running governed Codex work." | ||
| } | ||
| }; | ||
| } | ||
| if (/CreateProcessAsUserW failed:\s*5/iu.test(combined) || | ||
| /windows sandbox: runner error: CreateProcessAsUserW failed:\s*5/iu.test(combined) || | ||
| /spawn setup refresh/iu.test(combined)) { | ||
| const warnings = [...diagnosis.warnings]; | ||
| const sandboxFailureWarning = "Codex workspace-write sandbox could not launch subprocesses on this Windows host."; | ||
| if (!warnings.includes(sandboxFailureWarning)) { | ||
| warnings.push(sandboxFailureWarning); | ||
| } | ||
| return { | ||
| summary: "Codex workspace-write sandbox could not launch subprocesses on this Windows host.", | ||
| diagnosis: { | ||
| ...diagnosis, | ||
| sandboxCompatible: false, | ||
| warnings, | ||
| 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." | ||
| } | ||
| }; | ||
| } | ||
| if (/writing is blocked by read-only sandbox/iu.test(combined) || | ||
| /read-only filesystem sandbox/iu.test(combined) || | ||
| /approval is disabled/iu.test(combined)) { | ||
| const warnings = [...diagnosis.warnings]; | ||
| const readOnlySandboxWarning = "Codex stayed in a read-only or approval-disabled sandbox even though MartinLoop requested workspace-write."; | ||
| if (!warnings.includes(readOnlySandboxWarning)) { | ||
| warnings.push(readOnlySandboxWarning); | ||
| } | ||
| return { | ||
| summary: "Codex stayed in a read-only or approval-disabled sandbox even though MartinLoop requested workspace-write.", | ||
| diagnosis: { | ||
| ...diagnosis, | ||
| sandboxCompatible: false, | ||
| warnings, | ||
| 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." | ||
| } | ||
| }; | ||
| } | ||
| return {}; | ||
| } | ||
| export function buildCodexExecArgs(options) { | ||
| const sandbox = options.sandbox ?? "workspace-write"; | ||
| const modelArgs = options.model ? ["--model", options.model] : []; | ||
| const extraArgs = options.extraArgs ?? []; | ||
| const sandboxArgs = sandbox === "workspace-write" | ||
| // In Codex CLI, --approve-for-me is the write-enabled automatic-review | ||
| // mode and is mutually exclusive with --sandbox workspace-write. | ||
| ? ["--approve-for-me"] | ||
| : ["--sandbox", sandbox]; | ||
| return [ | ||
| "exec", | ||
| // Governed MartinLoop runs should not inherit auth-sensitive default MCP/plugin state. | ||
| "--ignore-user-config", | ||
| "--cd", | ||
| options.workingDirectory, | ||
| ...sandboxArgs, | ||
| "--json", | ||
| "--color", | ||
| "never", | ||
| ...modelArgs, | ||
| ...extraArgs, | ||
| "-" | ||
| ]; | ||
| } | ||
| function parseCodexProbeEvents(stdout) { | ||
| return stdout | ||
| .split(/\r?\n/u) | ||
| .map((line) => line.trim()) | ||
| .filter(Boolean) | ||
| .map((line) => { | ||
| try { | ||
| return JSON.parse(line); | ||
| } | ||
| catch { | ||
| return undefined; | ||
| } | ||
| }) | ||
| .filter((event) => event !== undefined); | ||
| } | ||
| function hasSuccessfulCommandExecution(events) { | ||
| return events.some((event) => event.type === "item.completed" && | ||
| event.item?.type === "command_execution" && | ||
| event.item?.status === "completed" && | ||
| event.item?.exit_code === 0); | ||
| } | ||
| export function resolveCliCommandAvailability(command, options = {}) { | ||
| const platform = options.platform ?? process.platform; | ||
| const env = options.env ?? process.env; | ||
| const spawnSyncImpl = options.spawnSyncImpl ?? spawnSync; | ||
| const discovery = readLocatorCandidates(command, platform, spawnSyncImpl); | ||
| if (discovery.candidates.length > 0) { | ||
| const resolvedPath = discovery.candidates[0]; | ||
| return { | ||
| command, | ||
| available: true, | ||
| locator: discovery.locator, | ||
| detail: `${command} is available on PATH.`, | ||
| ...(resolvedPath ? { resolvedPath } : {}), | ||
| candidatePaths: discovery.candidates | ||
| }; | ||
| const profile = options.capabilityProfile; | ||
| if (!profile?.approval || profile.automation || profile.approvalPolicy) { | ||
| return buildCapabilityDrivenCodexExecArgs(options); | ||
| } | ||
| // PATH didn't find it — search common install locations before giving up. | ||
| const offPathCandidate = discoverCommandOffPath(command, platform, env); | ||
| if (offPathCandidate) { | ||
| return { | ||
| command, | ||
| available: true, | ||
| locator: "off-path-discovery", | ||
| detail: `${command} found at ${offPathCandidate} (not on PATH, auto-discovered).`, | ||
| resolvedPath: offPathCandidate, | ||
| candidatePaths: [offPathCandidate] | ||
| }; | ||
| } | ||
| return { | ||
| command, | ||
| available: false, | ||
| locator: discovery.locator, | ||
| detail: `${command} is not installed. ${suggestInstall(command)}` | ||
| }; | ||
| } | ||
| function discoverCommandOffPath(command, platform, env) { | ||
| const home = env.HOME ?? env.USERPROFILE ?? ""; | ||
| const dirs = []; | ||
| if (platform === "win32") { | ||
| const appData = env.APPDATA; | ||
| if (appData) | ||
| dirs.push(join(appData, "npm")); | ||
| const localAppData = env.LOCALAPPDATA; | ||
| if (localAppData) | ||
| dirs.push(join(localAppData, "OpenAI", "Codex", "bin")); | ||
| if (home) | ||
| dirs.push(join(home, "scoop", "shims")); | ||
| } | ||
| else { | ||
| dirs.push("/usr/local/bin", "/opt/homebrew/bin"); | ||
| if (home) { | ||
| dirs.push(join(home, ".local", "bin"), join(home, ".npm-global", "bin"), join(home, ".bun", "bin"), join(home, ".cargo", "bin")); | ||
| } | ||
| const nvmDir = env.NVM_DIR; | ||
| if (nvmDir) | ||
| dirs.push(join(nvmDir, "current", "bin")); | ||
| } | ||
| const extensions = platform === "win32" | ||
| ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").map((e) => e.trim().toLowerCase()).filter(Boolean) | ||
| : [""]; | ||
| for (const dir of dirs) { | ||
| for (const ext of extensions) { | ||
| const candidate = ext ? join(dir, `${command}${ext}`) : join(dir, command); | ||
| if (existsSync(candidate)) { | ||
| return candidate; | ||
| } | ||
| } | ||
| } | ||
| return undefined; | ||
| } | ||
| function suggestInstall(command) { | ||
| const installs = { | ||
| claude: "Install with: npm install -g @anthropic-ai/claude-code", | ||
| codex: "Install with: npm install -g @openai/codex", | ||
| gemini: "Install with: npm install -g @google/gemini-cli" | ||
| }; | ||
| return installs[command] ?? `Install ${command} and ensure it is available.`; | ||
| } | ||
| export function detectCodexHostPlatform(env = process.env, platform = process.platform) { | ||
| if (platform === "win32") { | ||
| return "windows"; | ||
| } | ||
| if (platform === "darwin") { | ||
| return "macos"; | ||
| } | ||
| if (env["WSL_DISTRO_NAME"] || env["WSL_INTEROP"]) { | ||
| return "wsl"; | ||
| } | ||
| return "linux"; | ||
| } | ||
| export function diagnoseCodexHost(availability, options = {}) { | ||
| const hostPlatform = detectCodexHostPlatform(options.env ?? process.env, options.platform ?? process.platform); | ||
| const resolvedPath = availability.resolvedPath; | ||
| const installKind = detectInstallKind(resolvedPath, hostPlatform); | ||
| const invocationMode = detectInvocationMode(resolvedPath, options.platform ?? process.platform); | ||
| const warnings = []; | ||
| if (!availability.available) { | ||
| return { | ||
| hostPlatform, | ||
| nativeInstallValid: false, | ||
| installKind, | ||
| invocationMode, | ||
| sandboxMode: "workspace-write", | ||
| sandboxCompatible: false, | ||
| ...(resolvedPath ? { resolvedPath } : {}), | ||
| warnings, | ||
| remediation: "Install or expose the Codex CLI on PATH before running governed Codex work." | ||
| }; | ||
| } | ||
| if ((hostPlatform === "linux" || hostPlatform === "wsl") && | ||
| (installKind === "windows_shim" || installKind === "windows_mounted_path")) { | ||
| warnings.push("Codex resolves to a Windows-hosted install from a Linux/WSL environment."); | ||
| return { | ||
| hostPlatform, | ||
| nativeInstallValid: false, | ||
| installKind, | ||
| invocationMode, | ||
| sandboxMode: "workspace-write", | ||
| sandboxCompatible: false, | ||
| ...(resolvedPath ? { resolvedPath } : {}), | ||
| warnings, | ||
| remediation: "Install Codex natively inside this Linux/WSL environment instead of relying on a Windows PATH shim." | ||
| }; | ||
| } | ||
| return { | ||
| hostPlatform, | ||
| nativeInstallValid: true, | ||
| installKind, | ||
| invocationMode, | ||
| sandboxMode: "workspace-write", | ||
| sandboxCompatible: true, | ||
| ...(resolvedPath ? { resolvedPath } : {}), | ||
| ...(hostPlatform === "linux" || hostPlatform === "wsl" | ||
| ? { nativeDependencyStatus: "unknown" } | ||
| : {}), | ||
| warnings | ||
| }; | ||
| } | ||
| export function probeCodexLaunch(input) { | ||
| const availability = input.availability ?? | ||
| resolveCliCommandAvailability("codex", { | ||
| platform: input.platform, | ||
| spawnSyncImpl: input.spawnSyncImpl | ||
| }); | ||
| const diagnosis = diagnoseCodexHost(availability, { | ||
| env: input.env, | ||
| platform: input.platform | ||
| const normalizedProfile = profile.approval.semantics === "automation-mode" | ||
| ? { ...profile, automation: profile.approval } | ||
| : { ...profile, approvalPolicy: profile.approval }; | ||
| return buildCapabilityDrivenCodexExecArgs({ | ||
| ...options, | ||
| capabilityProfile: normalizedProfile | ||
| }); | ||
| const args = buildCodexExecArgs({ | ||
| workingDirectory: input.workingDirectory, | ||
| model: input.model, | ||
| mode: "probe" | ||
| }); | ||
| if (!availability.available) { | ||
| return { | ||
| ok: false, | ||
| summary: availability.detail, | ||
| availability, | ||
| diagnosis, | ||
| command: availability.command, | ||
| args | ||
| }; | ||
| } | ||
| if (!isInsideGitRepository(input.workingDirectory)) { | ||
| return { | ||
| ok: false, | ||
| summary: "Working directory is not inside a git repository. Codex exec requires a trusted repo unless --skip-git-repo-check is explicitly enabled.", | ||
| availability, | ||
| diagnosis, | ||
| command: availability.resolvedPath ?? availability.command, | ||
| args | ||
| }; | ||
| } | ||
| const spawnSyncImpl = input.spawnSyncImpl ?? spawnSync; | ||
| const platform = input.platform ?? process.platform; | ||
| const env = input.env ?? process.env; | ||
| const probeCandidates = buildProbeCandidates({ | ||
| availability, | ||
| env, | ||
| platform, | ||
| includeDesktopCandidates: input.spawnSyncImpl === undefined | ||
| }); | ||
| const candidatePaths = probeCandidates.map((candidate) => candidate.path); | ||
| const cacheKey = buildProbeCacheKey({ | ||
| workingDirectory: input.workingDirectory, | ||
| platform, | ||
| candidatePaths, | ||
| model: input.model | ||
| }); | ||
| if (input.spawnSyncImpl === undefined) { | ||
| const cached = codexLaunchProbeCache.get(cacheKey); | ||
| if (cached) { | ||
| return cached; | ||
| } | ||
| } | ||
| const candidateResults = []; | ||
| const probeCandidatePath = (candidate) => { | ||
| const candidateDiagnosis = candidate.diagnosis; | ||
| if (!candidateDiagnosis.nativeInstallValid) { | ||
| candidateResults.push({ | ||
| path: candidate.path, | ||
| diagnosis: candidateDiagnosis, | ||
| ok: false, | ||
| summary: candidateDiagnosis.remediation ?? "Codex host installation is not valid for this environment." | ||
| }); | ||
| return false; | ||
| } | ||
| const spawnPlan = buildProbeCommand(candidate.path, args, platform); | ||
| const result = spawnSyncImpl(spawnPlan.command, spawnPlan.args, { | ||
| cwd: input.workingDirectory, | ||
| encoding: "utf8", | ||
| stdio: ["pipe", "pipe", "pipe"], | ||
| input: CODEX_LAUNCH_PROBE_PROMPT | ||
| }); | ||
| const probedDiagnosis = { | ||
| ...candidateDiagnosis, | ||
| invocationMode: spawnPlan.invocationMode | ||
| }; | ||
| if (result.error) { | ||
| const classifiedFailure = classifyProbeFailure(result.stderr ?? "", result.stdout ?? "", probedDiagnosis); | ||
| candidateResults.push({ | ||
| path: candidate.path, | ||
| diagnosis: classifiedFailure.diagnosis ?? probedDiagnosis, | ||
| ok: false, | ||
| summary: classifiedFailure.summary ?? `Codex launch probe failed: ${result.error.message}`, | ||
| stderr: result.stderr ?? "", | ||
| stdout: result.stdout ?? "" | ||
| }); | ||
| return false; | ||
| } | ||
| if (result.status !== 0) { | ||
| const stderr = (result.stderr ?? "").trim(); | ||
| const classifiedFailure = classifyProbeFailure(result.stderr ?? "", result.stdout ?? "", probedDiagnosis); | ||
| candidateResults.push({ | ||
| path: candidate.path, | ||
| diagnosis: classifiedFailure.diagnosis ?? probedDiagnosis, | ||
| ok: false, | ||
| summary: classifiedFailure.summary ?? | ||
| (stderr.length > 0 ? `Codex launch probe failed: ${stderr}` : "Codex launch probe exited non-zero."), | ||
| exitCode: result.status ?? undefined, | ||
| stderr: result.stderr ?? "", | ||
| stdout: result.stdout ?? "" | ||
| }); | ||
| return false; | ||
| } | ||
| const events = parseCodexProbeEvents(result.stdout ?? ""); | ||
| if (!hasSuccessfulCommandExecution(events)) { | ||
| const classifiedFailure = classifyProbeFailure(result.stderr ?? "", result.stdout ?? "", probedDiagnosis); | ||
| candidateResults.push({ | ||
| path: candidate.path, | ||
| diagnosis: classifiedFailure.diagnosis ?? probedDiagnosis, | ||
| ok: false, | ||
| summary: classifiedFailure.summary ?? "Codex launch probe did not complete a shell command successfully.", | ||
| exitCode: result.status ?? undefined, | ||
| stderr: result.stderr ?? "", | ||
| stdout: result.stdout ?? "" | ||
| }); | ||
| return false; | ||
| } | ||
| candidateResults.push({ | ||
| path: candidate.path, | ||
| diagnosis: probedDiagnosis, | ||
| ok: true, | ||
| summary: "Codex exec prompt-and-shell probe passed for the current MartinLoop invocation shape.", | ||
| exitCode: result.status ?? undefined, | ||
| stderr: result.stderr ?? "", | ||
| stdout: result.stdout ?? "" | ||
| }); | ||
| return true; | ||
| }; | ||
| for (const candidate of probeCandidates) { | ||
| if (probeCandidatePath(candidate)) { | ||
| break; | ||
| } | ||
| } | ||
| const successfulCandidates = candidateResults.filter((candidate) => candidate.ok); | ||
| const selectedCandidate = successfulCandidates[0]; | ||
| const candidateProbeResults = candidateResults.map((candidate) => ({ | ||
| path: candidate.path, | ||
| installKind: candidate.diagnosis.installKind, | ||
| invocationMode: candidate.diagnosis.invocationMode, | ||
| nativeInstallValid: candidate.diagnosis.nativeInstallValid, | ||
| sandboxCompatible: candidate.diagnosis.sandboxCompatible, | ||
| launchReady: candidate.ok, | ||
| summary: candidate.summary, | ||
| ...(candidate.diagnosis.remediation ? { remediation: candidate.diagnosis.remediation } : {}), | ||
| ...(candidate.diagnosis.nativeDependencyStatus | ||
| ? { nativeDependencyStatus: candidate.diagnosis.nativeDependencyStatus } | ||
| : {}), | ||
| ...(candidate.diagnosis.nativeDependencyPackage | ||
| ? { nativeDependencyPackage: candidate.diagnosis.nativeDependencyPackage } | ||
| : {}) | ||
| })); | ||
| if (selectedCandidate) { | ||
| const successResult = { | ||
| ok: true, | ||
| summary: selectedCandidate.summary, | ||
| availability: { | ||
| ...availability, | ||
| resolvedPath: selectedCandidate.path, | ||
| candidatePaths | ||
| }, | ||
| diagnosis: { | ||
| ...selectedCandidate.diagnosis, | ||
| resolvedPath: selectedCandidate.path | ||
| }, | ||
| command: selectedCandidate.path, | ||
| args, | ||
| exitCode: selectedCandidate.exitCode, | ||
| stderr: selectedCandidate.stderr, | ||
| stdout: selectedCandidate.stdout, | ||
| candidateProbeResults | ||
| }; | ||
| if (input.spawnSyncImpl === undefined) { | ||
| codexLaunchProbeCache.set(cacheKey, successResult); | ||
| } | ||
| return successResult; | ||
| } | ||
| const bestFailure = platform === "win32" | ||
| ? candidateResults.find((candidate) => candidate.diagnosis.nativeInstallValid && candidate.diagnosis.installKind === "native") ?? | ||
| candidateResults.find((candidate) => candidate.diagnosis.nativeInstallValid) ?? | ||
| candidateResults[0] | ||
| : candidateResults[0]; | ||
| const failureResult = { | ||
| ok: false, | ||
| summary: bestFailure?.summary ?? diagnosis.remediation ?? "Codex launch probe failed.", | ||
| availability: { | ||
| ...availability, | ||
| ...(bestFailure ? { resolvedPath: bestFailure.path } : {}), | ||
| ...(candidatePaths.length ? { candidatePaths } : {}) | ||
| }, | ||
| diagnosis: bestFailure | ||
| ? { | ||
| ...bestFailure.diagnosis, | ||
| resolvedPath: bestFailure.path | ||
| } | ||
| : diagnosis, | ||
| command: bestFailure?.path ?? availability.resolvedPath ?? availability.command, | ||
| args, | ||
| exitCode: bestFailure?.exitCode, | ||
| stderr: bestFailure?.stderr, | ||
| stdout: bestFailure?.stdout, | ||
| candidateProbeResults | ||
| }; | ||
| if (input.spawnSyncImpl === undefined) { | ||
| codexLaunchProbeCache.set(cacheKey, failureResult); | ||
| } | ||
| return failureResult; | ||
| } | ||
| export { buildCodexStdin, cacheCodexCapabilityProfile, clearCodexCapabilityCacheForTests, codexWriteStrategies, probeCodexCapabilities } from "./codex-capabilities.js"; | ||
| export { checkCodexSandboxPreflight, detectCodexHostPlatform, diagnoseCodexHost, probeCodexLaunch, probeFilesystemWriteCapability, resolveCliCommandAvailability } from "./codex-host.js"; | ||
| //# sourceMappingURL=codex-launcher.js.map |
| export { createDirectProviderAdapter, type DirectProviderAdapterOptions } from "./direct-provider.js"; | ||
| export { createAgentCliAdapter, createClaudeCliAdapter, createCodexCliAdapter, createGeminiCliAdapter, type AgentCliAdapterOptions, type ClaudeCliAdapterOptions, type CodexCliAdapterOptions, type GeminiCliAdapterOptions, type CliArgsBuilder } from "./claude-cli.js"; | ||
| export { createAgentCliAdapter, createClaudeCliAdapter, createGeminiCliAdapter, type AgentCliAdapterOptions, type ClaudeCliAdapterOptions, type GeminiCliAdapterOptions, type CliArgsBuilder } from "./claude-cli.js"; | ||
| export { createCodexCliAdapter, type CodexCliAdapterOptions } from "./codex-cli.js"; | ||
| export { createOpenAiCompatibleAdapter, resolveOpenAiCompatibleRuntimeConfig, type OpenAiCompatibleAdapterOptions } from "./openai-compatible.js"; | ||
| export { createVerifierOnlyAdapter, type VerifierOnlyAdapterOptions } from "./verifier-only.js"; | ||
| export { detectCodexHostPlatform, diagnoseCodexHost, probeCodexLaunch, resolveCliCommandAvailability, type CliCommandAvailability, type CodexHostDiagnosis, type CodexHostPlatform, type CodexLaunchProbeResult, checkCodexSandboxPreflight, probeFilesystemWriteCapability, type CodexSandboxPreflightOk, type CodexSandboxPreflightOutcome, type CodexSandboxPreflightReadOnly } from "./codex-launcher.js"; | ||
| export { buildCodexExecArgs, buildCodexStdin, clearCodexCapabilityCacheForTests, detectCodexHostPlatform, diagnoseCodexHost, probeCodexCapabilities, probeCodexLaunch, resolveCliCommandAvailability, type CliCommandAvailability, type CodexApprovalCapability, type CodexCapabilityFlag, type CodexCapabilityProfile, type CodexFlagScope, type CodexHostDiagnosis, type CodexHostPlatform, type CodexLaunchProbeResult, type CodexPromptTransport, type CodexSandboxCapability, checkCodexSandboxPreflight, probeFilesystemWriteCapability, type CodexSandboxPreflightOk, type CodexSandboxPreflightOutcome, type CodexSandboxPreflightReadOnly } from "./codex-launcher.js"; | ||
| export { createSpawnPlan, type SpawnLike, type SpawnPlan, type SubprocessResult, type VerificationOutcome } from "./cli-bridge.js"; |
| export { createDirectProviderAdapter } from "./direct-provider.js"; | ||
| export { createAgentCliAdapter, createClaudeCliAdapter, createCodexCliAdapter, createGeminiCliAdapter } from "./claude-cli.js"; | ||
| export { createAgentCliAdapter, createClaudeCliAdapter, createGeminiCliAdapter } from "./claude-cli.js"; | ||
| export { createCodexCliAdapter } from "./codex-cli.js"; | ||
| export { createOpenAiCompatibleAdapter, resolveOpenAiCompatibleRuntimeConfig } from "./openai-compatible.js"; | ||
| export { createVerifierOnlyAdapter } from "./verifier-only.js"; | ||
| export { detectCodexHostPlatform, diagnoseCodexHost, probeCodexLaunch, resolveCliCommandAvailability, checkCodexSandboxPreflight, probeFilesystemWriteCapability } from "./codex-launcher.js"; | ||
| export { buildCodexExecArgs, buildCodexStdin, clearCodexCapabilityCacheForTests, detectCodexHostPlatform, diagnoseCodexHost, probeCodexCapabilities, probeCodexLaunch, resolveCliCommandAvailability, checkCodexSandboxPreflight, probeFilesystemWriteCapability } from "./codex-launcher.js"; | ||
| export { createSpawnPlan } from "./cli-bridge.js"; | ||
| //# sourceMappingURL=index.js.map |
| { | ||
| "name": "@martin/cli", | ||
| "version": "0.5.2", | ||
| "version": "0.5.3", | ||
| "type": "module", | ||
@@ -5,0 +5,0 @@ "description": "Open-source execution control for coding agents with verifier-gated completion, stop limits, rollback evidence, and Verified Handoffs.", |
+1
-1
| { | ||
| "name": "martin-loop", | ||
| "private": false, | ||
| "version": "0.5.2", | ||
| "version": "0.5.3", | ||
| "type": "module", | ||
@@ -6,0 +6,0 @@ "description": "Open-source command center for governed AI coding agents with built-in onboarding, hard gates, MCP, and shareable run receipts.", |
+88
-14
@@ -33,3 +33,3 @@ # MartinLoop | ||
| **Install** — run `npx -y martin-loop@0.5.2 start`, or install it globally with `npm install -g martin-loop@0.5.2`. | ||
| **Install** — run `npx -y martin-loop@0.5.3 start`, or install it globally with `npm install -g martin-loop@0.5.3`. | ||
@@ -44,3 +44,3 @@ **Governed run** — define an objective, verifier, budget, and iteration cap with `martin run`. | ||
| **MCP** — install `@martinloop/mcp@0.5.1` in a supported host or generate host configuration with `martin mcp print-config`. | ||
| **MCP** — install `@martinloop/mcp@0.5.3` in a supported host or generate host configuration with `martin mcp print-config`. | ||
@@ -51,2 +51,50 @@ **Documentation** — continue with the [quickstart](./docs/getting-started/quickstart.md), [CLI reference](./docs/reference/cli.md), or [MCP setup](./docs/getting-started/mcp.md). | ||
| ## For AI Agents | ||
| MartinLoop is the execution-control system around coding agents. The coding agent still writes the code. MartinLoop governs the run and makes completion prove itself. | ||
| Use MartinLoop when a coding task needs one or more of these controls: | ||
| - preflight readiness before agent spend | ||
| - hard budget and iteration limits | ||
| - allowed or denied edit scope | ||
| - controlled retries and stop conditions | ||
| - independent verifier commands | ||
| - recovery and rollback evidence | ||
| - a finite completion decision | ||
| - run receipts, dossiers, history, failure classification, and post-run analysis | ||
| - one control layer across Codex, Claude Code, and other coding-agent environments | ||
| Canonical lifecycle: | ||
| ```text | ||
| DEFINE | ||
| -> PREFLIGHT | ||
| -> CONTROL | ||
| -> VERIFY | ||
| -> RECOVER | ||
| -> PROVE | ||
| -> ANALYZE | ||
| ``` | ||
| The product-level flow is **Definition of Done -> Controlled Run -> Verified Handoff**. | ||
| For machine-readable context start with [`llms.txt`](./llms.txt), [`llms-full.txt`](./llms-full.txt), and [MartinLoop for AI Agents](./docs/for-agents.md). | ||
| ## One System Around the Run | ||
| Teams should not need to stitch together a separate script or point tool for every part of coding-agent execution. MartinLoop connects the control path around the agent from preflight through post-run evidence. | ||
| | Stage | MartinLoop role | | ||
| | --- | --- | | ||
| | Define | Capture the objective, verifier, budget, scope, and finish line. | | ||
| | Preflight | Check readiness and required workflow evidence before agent spend. | | ||
| | Control | Enforce budgets, attempts, path boundaries, policy, and stop conditions while the coding agent works. | | ||
| | Verify | Run configured checks and bind the evidence to the active run and workspace. | | ||
| | Recover | Preserve recovery and rollback state when another attempt or human review is required. | | ||
| | Prove | Produce the authoritative `VERIFIED`, `STOPPED`, or `NEEDS REVIEW` handoff plus receipts. | | ||
| | Analyze | Inspect run history, cost provenance, failure classes, dossiers, and shareable evidence after execution. | | ||
| MartinLoop does not replace Git, GitHub, CI, dedicated security scanners, observability platforms, code review, or the coding agent itself. It gives those workflows one governed execution record to inspect. | ||
| ## Why MartinLoop | ||
@@ -115,4 +163,20 @@ | ||
| Release notes for the current root package: [MartinLoop 0.5.2](./docs/release/OSS-0.5.2-RELEASE-NOTES.md). | ||
| Release notes for the current root package: [MartinLoop 0.5.3](./docs/release/OSS-0.5.3-RELEASE-NOTES.md). | ||
| ## The Run From Start to Handoff | ||
| MartinLoop's terminal presentation is built around the governed lifecycle, not around a single verifier command. | ||
| **Governed Run Plan** shows the configured finish line before work starts, including the task, budget posture, verifier plan, scope, and execution boundaries. | ||
| **Controlled Run** keeps the coding agent working inside those boundaries while MartinLoop tracks attempts, cost, stop conditions, and recovery state. | ||
| **Verified Handoff** closes the loop with one authoritative outcome: | ||
| - `VERIFIED` when the configured evidence supports the Definition of Done | ||
| - `STOPPED` when a configured hard boundary ends the run | ||
| - `NEEDS REVIEW` when completion cannot be established from the available evidence | ||
| The handoff can include verifier steps, scope state, attempt count, cost provenance, unresolved evidence, recovery state, receipt integrity, and the next safe action. The exact fields depend on what the run actually established. | ||
| ## Visual Proof | ||
@@ -132,2 +196,10 @@ | ||
| ## MartinLoop Arcade | ||
| Long governed runs do not have to mean staring at a spinner. In an interactive terminal, MartinLoop Arcade can be offered while the coding agent continues working in the background. | ||
| Arcade is presentation-only. It cannot change the agent, budget, verifier, policy decision, run outcome, or receipt evidence. It stays out of JSON, CI, non-interactive, and other machine-readable execution paths. | ||
| Use `--arcade` to offer Arcade immediately for a supported interactive run, or `--no-arcade` to suppress it for that run. | ||
| ## Proof Receipts | ||
@@ -158,13 +230,13 @@ | ||
| ```sh | ||
| npx -y martin-loop@0.5.2 --version | ||
| npx -y martin-loop@0.5.2 start | ||
| npx -y martin-loop@0.5.2 demo | ||
| npx -y martin-loop@0.5.3 --version | ||
| npx -y martin-loop@0.5.3 start | ||
| npx -y martin-loop@0.5.3 demo | ||
| cd martin-loop-demo | ||
| npm install | ||
| npx -y martin-loop@0.5.2 run "Summarize the demo workspace and prove tests still pass" --verify "npm test" --budget-usd 2 --max-iterations 1 --json | ||
| npx -y martin-loop@0.5.2 dossier --latest --json | ||
| npx -y martin-loop@0.5.2 share --latest --json | ||
| npx -y martin-loop@0.5.3 run "Summarize the demo workspace and prove tests still pass" --verify "npm test" --budget-usd 2 --max-iterations 1 --json | ||
| npx -y martin-loop@0.5.3 dossier --latest --json | ||
| npx -y martin-loop@0.5.3 share --latest --json | ||
| ``` | ||
| For deterministic installs, pin the package line (`martin-loop@0.5.2`) or use `martin-loop@latest`. Plain `npx martin-loop` can resolve a stale local cache on some machines. | ||
| For deterministic installs, pin the package line (`martin-loop@0.5.3`) or use `martin-loop@latest`. Plain `npx martin-loop` can resolve a stale local cache on some machines. | ||
@@ -254,6 +326,6 @@ Expected share bundle outputs: | ||
| <!-- Generated by scripts/generate-install-links.mjs. --> | ||
| <!-- MCP package: @martinloop/mcp@0.5.1 --> | ||
| <!-- MCP package: @martinloop/mcp@0.5.3 --> | ||
| [](vscode:mcp/install?%7B%22name%22%3A%22martin-loop%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40martinloop%2Fmcp%400.5.1%22%5D%7D) | ||
| [](cursor://anysphere.cursor-deeplink/mcp/install?name=martin-loop&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBtYXJ0aW5sb29wL21jcEAwLjUuMSJdfQ%3D%3D) | ||
| [](vscode:mcp/install?%7B%22name%22%3A%22martin-loop%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40martinloop%2Fmcp%400.5.3%22%5D%7D) | ||
| [](cursor://anysphere.cursor-deeplink/mcp/install?name=martin-loop&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBtYXJ0aW5sb29wL21jcEAwLjUuMyJdfQ%3D%3D) | ||
@@ -343,3 +415,3 @@ Common options: | ||
| The root `martin-loop` package is `0.5.2`; standalone `@martinloop/mcp` and MCPB remain at `0.5.1` for this root-only corrective release. Their version lines move independently. | ||
| The root `martin-loop` package, standalone `@martinloop/mcp` package, plugin metadata, and MCPB product version are aligned at `0.5.3`. The MCPB manifest schema remains `0.3`. | ||
@@ -354,2 +426,3 @@ The public MCP release train labels are: | ||
| - `0.3.1` review and handoff release | ||
| - `0.5.3` execution-control and host-compatibility release | ||
@@ -403,2 +476,3 @@ The standalone MCP registry/server identifier is `io.github.Keesan12/martin-loop`. | ||
| - [Examples](./docs/getting-started/examples.md) | ||
| - [MartinLoop for AI Agents](./docs/for-agents.md) | ||
| - [Agent Failure Atlas](./docs/agent-failure-atlas.md) | ||
@@ -405,0 +479,0 @@ - [Failure Taxonomy (13 Runtime Classes)](./docs/oss/FAILURE-TAXONOMY-13.md) |
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 3 instances
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
AI-detected potential code anomaly
Supply chain riskAI has identified unusual behaviors that may pose a security risk.
Found 2 instances
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
1373661
1.24%230
2.68%31155
0.61%583
14.54%111
1.83%28
12%