fix: separate Codex login and ARM host qualification

Keep daemon ABI admission separate from CLI login and preserve restricted Docker installation checks before native host startup.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-12 11:20:36 -05:00
1 parent 695506ad39
commit 123a188374
7 files changed
+152 -14

No files matched your search

@@ -62,6 +62,37 @@ ARM startup cause, then require fresh hosted qualification, CI, and review. Keep
the current branch, PR, test-drive, budget holds, and no-Codex-bundling boundary.
No merge or production deployment is authorized.
Checkpoint `695506ad3912fa63604ca784fda50416fba983d6` is pushed and passes
ordinary CI (53 successful checks, four conditional skips). The restarted
test-drive serves `2026.1005.0+348.git.695506ad39`; four complete agent execution
configurations and both historical native runs survive unchanged, with the old
experimental flag false. Normal staging selects the verified Mac target and
reports `needsBuild: false`; there is no binary override or local Cargo build.
Receipt: `/private/tmp/pr2-final-preservation-after.json`.
The hosted [ARM diagnostic](https://github.com/paperclipai/paperclip/actions/runs/38273898742)
used tooling `695506ad` and the literal `dcdf09a` package archives. It recovered
the primary failure: Codex's bubblewrap cannot create a namespace inside the
restricted Docker fixture. Daemon/CLI installation and ABI were not the cause.
The existing consumer now retains that Docker install/lifecycle proof and
uses the same installed graph on the actual ARM host for unchanged read-only
startup. Lock, daemon, and official Codex command hashes must match across
Docker/host and before/after. Twenty-one controls and independent review pass.
No external-sandbox substitution, sudo, sysctl, namespace flags, or product
permission policy changes are used. Actual host capability still needs proof.
Receipt: `/private/tmp/pr2-arm-host-split-owned-receipt.json`.
Fresh review also found that the daemon's new libc gate had accidentally sent
older Linux browser login to PATH-only lookup. A two-file fix restores owned
Codex CLI resolution independently of daemon eligibility. One hundred twenty-four
focused cases and server typecheck pass; nine regressions fail before the fix.
Creation's libc floor remains unchanged. Receipt:
`/private/tmp/codex-default-login-libc-decoupling-receipt.json`.
Next action: freeze this corrected tooling and run one remote ARM host treatment
against the same verified archives. Require actual host startup before the final
four-target qualification; retain literal product/tool source identities.
## Current addition: Linux ARM64
The user now requires ARM Linux support as well. Treat ARM Linux as ARM64
+10 -2
View File
@@ -66,8 +66,8 @@ are allowed. Relative PATH entries use the selected process working directory.
Compatibility is established by the actual protocol or login attempt.
Missing executables and real protocol failures still return actionable errors.
Package identity and executable containment remain checked. Browser login keeps
its existing provider-specific credential home and uses the native Codex resolver
on supported Linux and macOS targets, including Linux ARM64.
its provider-specific credential home and resolves the installed Codex CLI on
64-bit Linux and macOS independently of the daemon's libc requirements.
Exact dependency pins remain release and ACPX artifact-qualification checks.
They make published builds reproducible and verify sandbox artifacts; they do not
@@ -203,6 +203,14 @@ Apple Silicon and Intel hosts. Actual task and follow-up acceptance is performed
on Apple Silicon; Intel qualification covers build, installation, and protocol
startup.
On Linux, Codex's sandboxed filesystem modes also require the host to permit its
unprivileged namespaces. A no-privilege Docker container can prohibit nested
namespace creation even when installation and binary compatibility pass.
Qualification therefore retains the restricted Docker install/lifecycle checks
and exercises the same installed graph's read-only native startup on the actual
ARM host. This does not qualify arbitrary container or managed setup targets;
their selected runtime must pass setup in that environment.
Automatic creation selects legacy execution when a Linux host has musl, unknown
libc, or a measured GNU libc version below the baseline. Existing agents retain
their stored runner. Windows, 32-bit ARM, and Alpine/musl native qualification
@@ -7,13 +7,31 @@ import { join } from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
import { isWorkspacePackage, parseWorkspaceEntries } from '../build-standalone-public-packages.mjs';
import { GROK_PUBLIC_INSTALL_IMAGE, GROK_PUBLIC_INSTALL_LIFECYCLE, assertInstalledCodexPlatformProvenance, assertNoBundledCodexPayloads, grokConsumerDockerArgs, installedCodexProbeSource, installedDaemonProbeSource, macNodeHeadersDirectory, macPublicInstallLifecycleProfile, macInstalledStartupProbeSource } from '../grok-public-install-sandbox.mjs';
import { GROK_PUBLIC_INSTALL_IMAGE, GROK_PUBLIC_INSTALL_LIFECYCLE, assertInstalledCodexPlatformProvenance, assertInstalledNativeConsumerContinuity, assertNoBundledCodexPayloads, grokConsumerDockerArgs, installedCodexProbeSource, installedDaemonProbeSource, macNodeHeadersDirectory, macPublicInstallLifecycleProfile, macInstalledStartupProbeSource } from '../grok-public-install-sandbox.mjs';
import { verifyPiPackagedDaemon } from '../pi-public-install-probe.mjs';
import { runnerBinaryTarget } from '../../packages/paperclip-runner/src/live/runner-binary.ts';
const paths = { assets: '/private/staging/assets', consumer: '/private/staging/consumer', cache: '/private/staging/cache', uid: 1001, gid: 1001 };
const values = (args, flag) => args.flatMap((value, index) => value === flag ? [args[index + 1]] : []);
test('native host startup preserves the consumer lock and both executable byte identities', () => {
const before = { lockSha256: 'a'.repeat(64), runnerSha256: 'b'.repeat(64), codexCommandSha256: 'c'.repeat(64) };
assert.doesNotThrow(() => assertInstalledNativeConsumerContinuity(before, { ...before }));
for (const field of Object.keys(before)) {
assert.throws(() => assertInstalledNativeConsumerContinuity(before, { ...before, [field]: 'd'.repeat(64) }), new RegExp(field));
assert.throws(() => assertInstalledNativeConsumerContinuity({ ...before, [field]: '' }, { ...before, [field]: '' }), new RegExp(field));
}
const source = readFileSync(new URL('../verify-grok-npm-install.mjs', import.meta.url), 'utf8');
const arm = source.split('async function verifyLinuxArmConsumer')[1].split("if (process.argv[2] === '--linux-arm64-consumer')")[0];
assert.match(arm, /const hostEnv = \{ PATH: .*HOME: home, TMPDIR: hostTmp, LC_ALL: 'C', TZ: 'UTC' \}/);
assert.match(arm, /execFileSync\(process.execPath, \[file\], \{ cwd: consumer, env: hostEnv/);
assert.ok(arm.indexOf('isolated(GROK_PUBLIC_INSTALL_LIFECYCLE)') < arm.indexOf("hostProbe('host-startup-probe.mjs'"));
assert.ok(arm.indexOf('const before =') < arm.indexOf("hostProbe('host-startup-probe.mjs'"));
assert.ok(arm.indexOf("hostProbe('host-startup-probe.mjs'") < arm.indexOf('const after ='));
assert.match(arm, /assertInstalledNativeConsumerContinuity\(before, after\)/);
assert.doesNotMatch(arm, /externallySandboxed|externalSandbox|sudo|sysctl|--privileged|seccomp=unconfined|unshare/);
});
test('Mac offline lifecycle uses only the matching executing Node SDK', () => {
const root = realpathSync(mkdtempSync(join(tmpdir(), 'mac-node-headers-test-')));
try {
@@ -266,6 +284,7 @@ test('the installed Codex probe exercises the public export and rejects incomple
assert.equal(JSON.parse(result.stdout).pinnedCodexCommandVerified, true);
assert.equal(JSON.parse(result.stdout).codexPlatformPackageSource, 'official npm');
assert.equal(JSON.parse(result.stdout).codexConsumerPlatform, target);
assert.equal(JSON.parse(result.stdout).codexCommandSha256, createHash('sha256').update(readFileSync(command)).digest('hex'));
assert.equal(JSON.parse(result.stdout).codexQualifiedInstallationVerified, true);
assert.equal(JSON.parse(result.stdout).codexCommandLeaseVerified, true);
assert.equal(JSON.parse(result.stdout).codexNativeSandboxResourcesVerified, true);
@@ -438,7 +457,9 @@ test('native ARM clean consumers keep the existing container and installed-runti
assert.match(arm, /isolated\(GROK_PUBLIC_INSTALL_LIFECYCLE\)/);
assert.match(arm, /installedCodexProbeSource\(installed \+ '\/index.js', '\/consumer', undefined, \{ nativeCodex: true \}\)/);
assert.match(arm, /tests\/runner-e2e\/installed-cli.ts/);
assert.match(arm, /temporarySizeMiB: 2048, temporaryExecutable: true/);
assert.match(arm, /process.versions.node.split\('\.'\)\[0\], '24'/);
assert.match(arm, /macInstalledStartupProbeSource\(consumer, hostServer, daemon.sha256, join\(assets, 'installed-cli.ts'\)\)/);
assert.match(arm, /startupEnvironment: 'native host, same installed graph, isolated empty homes'/);
assert.doesNotMatch(arm, /stage-runner|runner\/target|PAPERCLIP_RUNNER_BINARY|prerequisite:/);
});
+11
View File
@@ -91,6 +91,7 @@ export function installedCodexProbeSource(indexPath, consumerRoot, expectedVersi
return `
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import { createRequire } from 'node:module';
import { accessSync, constants, existsSync, lstatSync, readFileSync, readdirSync, realpathSync, statSync } from 'node:fs';
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path';
@@ -174,6 +175,7 @@ export function installedCodexProbeSource(indexPath, consumerRoot, expectedVersi
const lease = await installation.openCommand();
await lease.close();`}
console.log(JSON.stringify({ pinnedCodexCommandVerified: true, installedCodexVersion: version,
codexCommandSha256: createHash('sha256').update(readFileSync(command)).digest('hex'),
...provenance, codexSelectedPlatformPath: selectedPlatform.path, codexSelectedPlatformVersion: selectedMetadata.version,
codexNativeSandboxResourcesVerified: true,
${nativeCodex ? '' : `pinnedCodexVersion: ${JSON.stringify(expectedVersion)}, codexQualifiedInstallationVerified: true, codexCommandLeaseVerified: true,`}
@@ -181,6 +183,15 @@ export function installedCodexProbeSource(indexPath, consumerRoot, expectedVersi
`;
}
/** Installed bytes and consumer resolution must survive host protocol startup. */
export function assertInstalledNativeConsumerContinuity(before, after) {
for (const field of ['lockSha256', 'runnerSha256', 'codexCommandSha256']) {
if (!/^[a-f0-9]{64}$/.test(before[field]) || before[field] !== after[field]) {
throw new Error(`Installed native host startup changed ${field}`);
}
}
}
/** The installed consumer must select its packaged host daemon, never PATH. */
export function installedDaemonProbeSource(outputRoot, consumerRoot, sourceRevision) {
return `
+30 -3
View File
@@ -13,7 +13,7 @@ import { basename, dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import { packConcurrency, packStagedPackages, packTimings } from './pack-staged-packages.mjs';
import { materializePublishManifest } from './prepare-bundled-package.mjs';
import { GROK_PUBLIC_INSTALL_IMAGE, GROK_PUBLIC_INSTALL_LIFECYCLE, assertNoBundledCodexPayloads, grokConsumerDockerArgs, installedCodexProbeSource, installedDaemonProbeSource, macNodeHeadersDirectory, macPublicInstallLifecycleProfile, macInstalledStartupProbeSource } from './grok-public-install-sandbox.mjs';
import { GROK_PUBLIC_INSTALL_IMAGE, GROK_PUBLIC_INSTALL_LIFECYCLE, assertInstalledNativeConsumerContinuity, assertNoBundledCodexPayloads, grokConsumerDockerArgs, installedCodexProbeSource, installedDaemonProbeSource, macNodeHeadersDirectory, macPublicInstallLifecycleProfile, macInstalledStartupProbeSource } from './grok-public-install-sandbox.mjs';
import { startPrefetch, stopPrefetches } from './prefetch-child.mjs';
import { retainRunnerQualificationPackages } from './retain-runner-qualification-packages.mjs';
import { bundledInstallTimings, startBundledInstalls } from './stage-bundled-packages.mjs';
@@ -99,6 +99,7 @@ async function verifyMacConsumer(packageDirectory, sourceRevision, receiptPath)
async function verifyLinuxArmConsumer(packageDirectory, sourceRevision, receiptPath) {
assert.equal(process.env.GITHUB_ACTIONS, 'true', 'Native clean consumers run only on disposable hosted CI');
assert.equal(process.platform + '-' + process.arch, 'linux-arm64', 'ARM qualification requires a native ARM host');
assert.equal(process.versions.node.split('.')[0], '24', 'ARM host startup requires the qualified Node24 runtime');
assert.match(sourceRevision, /^[a-f0-9]{40}$/);
const packages = realpathSync(packageDirectory);
const manifest = JSON.parse(readFileSync(join(packages, 'manifest.json'), 'utf8'));
@@ -150,12 +151,38 @@ async function verifyLinuxArmConsumer(packageDirectory, sourceRevision, receiptP
// production imports still resolve from the installed consumer graph.
cpSync(join(repo, 'tests/runner-e2e/installed-cli.ts'), join(assets, 'installed-cli.ts'));
writeFileSync(join(assets, 'types.js'), stripTypeScriptTypes(readFileSync(join(repo, 'tests/runner-e2e/types.ts'), 'utf8')));
const startup = probe('startup-probe.mjs', macInstalledStartupProbeSource('/consumer', server, daemon.sha256, '/packages/installed-cli.ts'),
{ temporarySizeMiB: 2048, temporaryExecutable: true });
// The restricted install container deliberately forbids nested namespaces.
// Keep its lifecycle/no-network proof, then test the exact same bind-mounted
// graph on the native host using Codex's normal read-only namespace policy.
const home = join(root, 'host-home'), hostTmp = join(root, 'host-tmp');
for (const directory of [home, hostTmp]) mkdirSync(directory, { mode: 0o700 });
const hostEnv = { PATH: `${dirname(process.execPath)}:/usr/bin:/bin:/usr/sbin:/sbin`, HOME: home, TMPDIR: hostTmp, LC_ALL: 'C', TZ: 'UTC' };
const hostProbe = (name, source) => {
const file = join(assets, name); writeFileSync(file, source);
return JSON.parse(execFileSync(process.execPath, [file], { cwd: consumer, env: hostEnv,
timeout: 120_000, maxBuffer: 1024 * 1024, encoding: 'utf8' }).trim());
};
const hostServer = join(consumer, 'node_modules/@paperclipai/server'), hostInstalled = join(hostServer, 'dist/vendor/paperclip-runner');
const hostDaemon = () => hostProbe('host-daemon-probe.mjs', installedDaemonProbeSource(hostInstalled, consumer, sourceRevision));
const hostCodex = () => hostProbe('host-codex-probe.mjs', installedCodexProbeSource(join(hostInstalled, 'index.js'), consumer, undefined, { nativeCodex: true }));
const digest = value => createHash('sha256').update(value).digest('hex');
const before = { lockSha256: digest(lock), runnerSha256: hostDaemon().sha256.slice(7), codexCommandSha256: hostCodex().codexCommandSha256 };
assert.equal(before.runnerSha256, daemon.sha256.slice(7), 'Docker and host must select the same packaged daemon bytes');
assert.equal(before.codexCommandSha256, codex.codexCommandSha256, 'Docker and host must select the same official Codex command bytes');
const hostHeader = process.report.getReport().header;
assert.match(hostHeader.glibcVersionRuntime || '', /^\d+\.\d+$/, 'Native host startup must report its actual GNU libc ABI');
const startup = hostProbe('host-startup-probe.mjs', macInstalledStartupProbeSource(consumer, hostServer, daemon.sha256, join(assets, 'installed-cli.ts')));
const after = { lockSha256: digest(readFileSync(join(consumer, 'package-lock.json'))), runnerSha256: hostDaemon().sha256.slice(7), codexCommandSha256: hostCodex().codexCommandSha256 };
assertInstalledNativeConsumerContinuity(before, after);
assert.equal(startup.runnerSha256, before.runnerSha256);
assert.equal(startup.codexSha256, before.codexCommandSha256);
const receipt = { schema: 'paperclip.runner.linux-arm64-public-install.v1', sourceRevision, platform: process.platform, architecture: process.arch,
cleanNpmInstall: true, packageCount: manifest.archives.length, codexPackedPayloadsOmitted: true,
consumerImage: GROK_PUBLIC_INSTALL_IMAGE, consumerImagePlatform: platform,
consumerLockPreserved: true, lifecycleSentinelVerified: true, lifecycleNetwork: 'none',
startupEnvironment: 'native host, same installed graph, isolated empty homes',
startupHost: { nodeVersion: process.versions.node, nodeModulesAbi: process.versions.modules, glibcVersionRuntime: hostHeader.glibcVersionRuntime, kernelRelease: hostHeader.osRelease },
installedIdentityBefore: before, installedIdentityAfter: after,
...daemon, ...codex, startup, providerCalls: 0 };
writeFileSync(receiptPath, JSON.stringify(receipt, null, 2) + '\n');
console.log(JSON.stringify(receipt));
@@ -43,12 +43,13 @@ function targetPlatform(platform: string, architecture: string, glibc = true, gl
}
describe.skipIf(process.platform === "win32")("local browser subscription login", () => {
it.each(["native", "arm32", "musl", "glibc-2.38", "glibc-3.0"])("uses the selected Codex command with its credential home and ambient HOME/cwd (%s)", async platform => {
const legacy = ["arm32", "musl", "glibc-2.38"].includes(platform);
it.each(["native", "arm32", "musl", "glibc-2.38", "glibc-3.0", "unknown"])("uses the selected Codex command with its credential home and ambient HOME/cwd (%s)", async platform => {
const legacy = platform === "arm32";
if (platform === "arm32") targetPlatform("linux", "arm");
if (platform === "musl") targetPlatform("linux", "arm64", false);
if (platform === "glibc-2.38") targetPlatform("linux", "arm64", true, "2.38");
if (platform === "glibc-3.0") targetPlatform("linux", "arm64", true, "3.0");
if (platform === "unknown") targetPlatform("linux", "x64", true, "unknown");
const home = await fakeCli("codex", [
'[ "$1 $2" = "login --device-auth" ] || exit 11',
'[ "$HOME" = "$CLAUDE_CONFIG_DIR" ] && [ "$HOME" != "$CODEX_HOME" ] && [ "$CODEX_HOME" = "$PAPERCLIP_TEST_LOGIN_CODEX_HOME" ] || exit 12',
@@ -66,12 +67,51 @@ describe.skipIf(process.platform === "win32")("local browser subscription login"
for (const key of ["OPENAI_API_KEY", "CODEX_API_KEY", "ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN", "CLAUDE_CODE_OAUTH_TOKEN"])
vi.stubEnv(key, "unrelated-host-value");
const login = startLocalBrowserLogin("openai", home);
await vi.waitFor(() => expect(login.outcome).toBe("success"), { timeout: 5000 });
await vi.waitFor(() => expect(login.outcome).toBeDefined(), { timeout: 5000 });
expect(login.outcome).toBe("success");
if (legacy) expect(resolveCodexCommand).not.toHaveBeenCalled();
else expect(resolveCodexCommand).toHaveBeenCalledOnce();
expect(login.code).toBe("ABCD-EFGHJ");
});
it.each(["x64", "arm64"].flatMap(architecture => ["2.38", "musl", "unknown"].map(libc => ({ architecture, libc }))))("uses the installed Codex dependency for login on Linux $architecture/$libc without a global CLI", async ({ architecture, libc }) => {
targetPlatform("linux", architecture, libc !== "musl", libc);
const home = await fakeCli("codex", [
'echo owned > "$CODEX_HOME/owned-cli"',
'printf "1. Open this link in your browser and sign in to your account\\nhttps://auth.openai.com/codex/device\\n2. Enter this one-time code (expires in 15 minutes)\\nABCD-EFGHJ\\n"',
].join("\n"));
const dependency = path.join(root!, "node_modules/@openai/codex");
await mkdir(path.join(dependency, "bin"), { recursive: true });
await writeFile(path.join(dependency, "package.json"), JSON.stringify({ name: "@openai/codex", version: "0.1.0", bin: { codex: "bin/codex" } }));
await writeFile(path.join(dependency, "bin/codex"), await readFile(path.join(root!, "bin/codex")), { mode: 0o700 });
const actual = await vi.importActual<Pick<typeof import("../vendor/paperclip-runner/index.js"), "resolveCodexCommand">>("../../../packages/paperclip-runner/src/drivers/codex/codex-command.ts");
vi.mocked(resolveCodexCommand).mockImplementation(() => actual.resolveCodexCommand(path.join(root!, "issuer.js"), process.env, home));
process.env.PATH = "/usr/bin:/bin";
const login = startLocalBrowserLogin("openai", home);
await vi.waitFor(() => expect(login.outcome).toBeDefined(), { timeout: 5000 });
expect(login.outcome).toBe("success");
expect(await readFile(path.join(home, "owned-cli"), "utf8")).toBe("owned\n");
expect(resolveCodexCommand).toHaveBeenCalledOnce();
expect(process.report.getReport).not.toHaveBeenCalled();
});
it.each(["2.38", "musl", "unknown"])("reports a real owned-dependency containment failure on Linux/%s without falling back to another CLI", async libc => {
targetPlatform("linux", "arm64", libc !== "musl", libc);
const home = await fakeCli("codex", 'echo invoked > "$CODEX_HOME/unreviewed-cli"');
const dependency = path.join(root!, "node_modules/@openai/codex");
await mkdir(dependency, { recursive: true });
await writeFile(path.join(dependency, "package.json"), JSON.stringify({ name: "@openai/codex", version: "0.1.0", bin: { codex: "../outside-codex" } }));
const actual = await vi.importActual<Pick<typeof import("../vendor/paperclip-runner/index.js"), "resolveCodexCommand">>("../../../packages/paperclip-runner/src/drivers/codex/codex-command.ts");
vi.mocked(resolveCodexCommand).mockImplementation(() => actual.resolveCodexCommand(path.join(root!, "issuer.js"), process.env, home));
const login = startLocalBrowserLogin("openai", home);
await vi.waitFor(() => expect(login.outcome).toBeDefined(), { timeout: 5000 });
expect(login.outcome).toBe("failure");
expect(login.error).toMatch(/requires an installed Codex CLI.*Install Codex/);
expect(JSON.stringify(login)).not.toContain(root!);
await expect(readFile(path.join(home, "unreviewed-cli"))).rejects.toMatchObject({ code: "ENOENT" });
expect(resolveCodexCommand).toHaveBeenCalledOnce();
});
it.each([["linux", "x64"], ["linux", "arm64"], ["darwin", "arm64"], ["darwin", "x64"]] as const)("reports an unavailable Codex CLI on %s/%s without revealing private details", async (platform, architecture) => {
targetPlatform(platform, architecture);
const home = await fakeCli("codex", 'echo invoked > "$CODEX_HOME/unreviewed-cli"');
@@ -5,7 +5,6 @@ import path from "node:path";
import { runSetupTokenLogin } from "@paperclipai/adapter-claude-local/server";
import { runDeviceLogin } from "@paperclipai/adapter-codex-local/server";
import { ensureCommandResolvable, resolveCommandForLogs } from "@paperclipai/adapter-utils/server-utils";
import { localPaperclipRunnerSupported } from "../lib/agent-runner-platform.js";
import { resolveCodexCommand } from "../vendor/paperclip-runner/index.js";
export type LocalBrowserLoginState = {
@@ -57,9 +56,10 @@ export function startLocalBrowserLogin(provider: "anthropic" | "openai", home: s
async start(_command: string, onData: (chunk: string) => void): Promise<{ exitCode: number | null }> {
let executable = "claude";
if (provider === "openai") {
// Native targets use the same owned Codex resolution as execution.
// Other platforms retain the legacy CLI lookup.
const legacyPlatform = !localPaperclipRunnerSupported();
// The installed Codex CLI is independent of the runner daemon's glibc floor.
// Supported CLI hosts use owned resolution; other platforms keep PATH lookup.
const legacyPlatform = !((process.platform === "linux" || process.platform === "darwin")
&& (process.arch === "x64" || process.arch === "arm64"));
try {
if (legacyPlatform) {
await ensureCommandResolvable("codex", home, process.env);