mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 08:22:20 +02:00
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:
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
|
||||
|
||||
@@ -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:/);
|
||||
});
|
||||
|
||||
|
||||
@@ -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 `
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user