mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 16:27:18 +02:00
## Thinking Path > - Paperclip is the open source control plane for agent companies. > - People choose a harness when creating an agent. > - The server should resolve that harness to a qualified runner. > - Codex is the first harness in the staged default rollout. > - Saved agents must retain their execution choices. > - New Codex agents therefore use Paperclip Runner on qualified macOS/Linux targets, with an explicit Legacy override. ## Linked Issues or Issue Description Refs #16028, #15555, #15673, #15422, #15631, #15926, #15932, #16149. The runtime/install prerequisite and release repair are merged. This slice owns Codex creation, setup, configuration and preservation. Other harness defaults remain unchanged. It adds no database migration; incoming master migrations, required maps and regression coverage are retained. ## What Changed - Resolve `runner: auto | paperclip | legacy` at the common creation boundary across onboarding, API/CLI, hires, built-ins, plugins and imports. Freeze pending hires and export resolved execution choices. - Preserve existing runners on upgrades, unrelated edits and import updates, including compatible accounts, workspace settings, Codex effort and intentional clears. - Use the underlying harness picker and branding; put the runner override in Advanced. Use the shared dropdown and update production-component stories and creation contracts. - Support qualified Apple Silicon, Intel Mac, Linux x64 and Linux ARM64. Linux requires GNU libc 2.39+. Disabled, external, unsupported and unqualified combinations retain their applicable Legacy execution. - Test the selected runtime/account/model/environment. Ordinary Connect probes eligible immutable managed targets and refreshes scoped discovery. Creation/discovery do not provision. Auth, dependency, incompatible-setting and cleanup failures remain actionable without changing runners. - Keep setup probes read-only, preserve local/SSH isolation and resolve packaged assets before staging credentials. Bound cleanup reads by the remaining absolute deadline; physically close every owned ingress socket before endpoint release. Unconfirmed cleanup retains recovery state. - Preserve validated completed-onboarding callbacks through startup recovery. First file-producing work belongs to the child task and uses the existing dependency/block handoff and task-scoped registration. - Reuse existing E2E fixtures, artifact checks, reporting and controls. Keep Docker/Cargo/native compilation remote. Paperclip tarballs contain no Codex payloads; official optional npm platform dependencies remain. ## Verification Current tested head: `1c03a8815672d82c264a9b08d4ea1984e8fae107`. **Merge-ready: required CI, fresh review, package qualification, Mac core acceptance and Test task/follow-up acceptance pass.** The existing attachment and native-continuity oracles independently verify both Test downloads, run attribution and unchanged native/provider/workspace identities. The maintainer authorized a code-owner-review exception for this PR only. The first Test setup failure and normal Board completion-review friction are retained below. Actual post-merge packages and private Cloud distribution remain separate qualification gates. - [Current CI](https://github.com/paperclipai/paperclip/actions/runs/38564241625): all 48 ordinary jobs and required verify/E2E pass. Attempt 1 retained six simultaneous host-shutdown cancellations. One failed-only retry passes those six lanes and two aggregates, preserving 40 successes. Actual integration checkout `53104c1` combines this head with master `c80`; it is not relabeled as the branch tree. Fresh exact-head Greptile 5/5, 30 resolved threads, no actionable findings. - [Exact-source package qualification](https://github.com/paperclipai/paperclip/actions/runs/38565287239): nine applicable jobs pass, nine conditional jobs skip. Four remote native producers; Linux x64 clean install; Intel Mac, Apple Silicon and Linux ARM64 native startup/physical cleanup controls. All 18 tarballs/four daemon source/hash/manifest bindings and 343 migrations through 0344 verify. No Codex payload is packed. These hosted tarballs are not a published npm canary. - Normal Apple Silicon installation and credential-free native startup/cleanup pass using the packaged daemon, with no source build or provider turn. Fresh installed DB initialization passes. Upgrade 651a→1c03 preserves all three complete execution configs, accounts, ten recorded run profiles, paused budgets, active incidents, cost summaries and two historical unpriced events. Historical recovery state is retained. - Cleanup regressions: four delayed-read baseline failures become 15 passing deadline/authority controls. No-acknowledgement and prior-error socket ownership regressions become 12 passing controls. Direct runtime/server types, maps: 35 areas / 176 IDs, diff checks and independent reviews pass. Permission/auth/reconnect policy and the 10-second setup budget remain intact. - Existing source coverage includes creation/selection/imports, pending approvals, recovery, account/lifecycle handling, mounted forms, dropdowns and E2E support. Full typechecks/tests/build/token and applicable Storybook checks pass in the recorded current CI; no duplicate suite or weakened grader. - Literal 9546 live Mac proof: ordinary Auto Codex onboarding, useful document, real completed-child callback/reply, seven successful native runs, manual follow-up and independently downloaded1,101-byte child file. Existing Legacy title edit/reload preserves execution; two real Legacy runs produce independently checked15/34-byte files. These remain historical source records. - Current literal 1c03 Mac core acceptance passes independent checks: ordinary Auto native Codex onboarding without runner override; only timeout 180 seconds/concurrency 1 saved; six successful native runs and six serialized turns (maximum overlap 1); both same-task continuity checks and five mechanical callback checks; actual rendered parent reply and exact child downloads. The useful handoff advances from revision 1 to 2. Both files are 48 bytes, SHA-256 `8b742bd4e8123a7c078f0a8d9f3449fbfa8a320d7d90be641a2818db3c53d92c` and `f42db9774fa06e804ab7d596fc8b922a2b638921c418b87d0111151d290a6afe`; exactly one byte changes and old history remains. The agent is paused with zero active runs/errors. Both artifact work products have `isPrimary=false`, and descriptive titles fail a stricter title-helper assumption; these retained results are scoped editorial follow-up, not new merge gates. Six source-bound screenshots are uploaded and verified in the private primary QA report. - [Literal 9546 managed proof](https://d1p6rlowie26tp.cloudfront.net/runner-e2e/campaigns/gha-38485488621-1/index.html): original 19 assertions and completed judge pass, four native turns, child-bound240-byte file and session/workspace continuity. Checkout reports a verified generated target-lock mutation; full final porcelain was not retained. Signed execution-image identity remains separate from Core source. - Actual 651a Test deployment succeeds on one owned/pinned target. One ordinary Auto API-key Connect then fails native cleanup before hire. Controller turn/drain/suspend completion and service-recorded environment cleanup are observed; they do not certify physical deletion or native authority retirement. Failure/recovery/unknown holds are retained. No Legacy switch or unchanged paid retry. The two source repairs above require actual treatment. - [Current immutable Core preview](https://github.com/paperclipai/paperclip/actions/runs/38570365725) and matching DB/shared artifacts pass. One owned, pinned Test target serves exact `1c03`; identical migration history correctly skips the migrator. Ordinary Auto Codex Connect succeeds and hires native; saved native selection and QA bounds survive reload. The initial child preparation attempt fails before provider work, with actual stdout identifying `renewal_deadline`; its trigger remains unknown and the failure is retained. Normal Board **Try again** and **Resume** recover the child to Done, producing a browser-verified 44-byte file, SHA-256 `0a8458b1efcc2d7a0d815689458c4caab039a46c7c7a3e5805de6bc8b1a96ecc`. The actual parent callback reports the completed child document/file. The existing validator requires a parent-owned attachment, so recovery enters normal Review; after byte checking, normal **Approve completion** reaches parent Done. No forced status or duplicate parent file is used. The validator is unchanged from integrated and current master. Autonomous initial callback-to-Done is not proven. The same-child follow-up completes document revision 2 and publishes a second independently downloaded 44-byte file, SHA-256 `22022257300fae762784632d08d3bbe0bab61e2ce81f91864a14dcd7aa464591`; only the revision digit changes. The unchanged existing native-continuity and attachment oracles pass. All ten recorded runs are terminal: nine native successes and the retained pre-provider setup failure. The QA agent is paused with zero active runs. The pre-existing parent evidence scope mismatch is a separate follow-up, with ordinary completion review exercised rather than a forced status or relaxed validation. This unsigned Core preview remains distinct from post-merge private Cloud composition. ## Risks - Saved agents are not migrated. Explicit runner changes use normal configuration/session rules; historical runs retain their recorded execution. - Native readiness depends on verified assets and server-owned target qualification. A Legacy probe, HTTP200 or successful account save cannot prove native readiness. - Recovery exceptions remain restricted to validated completed-onboarding reports; process-loss, ownership and terminal-state safeguards remain active. Unexecuted live journeys stay open. - Linux ARM paid tasks, Windows, Alpine and 32-bit ARM remain deferred. Cold Cargo cache misses are recorded; no warm-cache speedup is claimed. - Historical invoices/physical cleanup remain unconfirmed and held. After merge, exact published npm canary, signed standard image/migrator, private composition and serving/live acceptance still require qualification. Production deployment/default promotion is not authorized. ## Model Used OpenAI Codex, GPT-6 family, with reasoning, tools, code execution and parallel agents. Exact serving snapshot and context-window size are not exposed. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used with available version and capability details - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have linked public issues or PRs above - [x] I have not referenced internal/instance-local Paperclip issues or links - [x] My branch name describes the change and contains no internal ticket id - [x] I have run the stated focused tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation - [x] I have considered and documented risks - [x] All Paperclip CI gates are green on the current candidate - [x] Greptile is 5/5 with no open findings on the current candidate - [x] I will address all reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
529 lines
43 KiB
TypeScript
529 lines
43 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import { chmod, lstat, mkdir, mkdtemp, readFile, readlink, readdir, realpath, rm, stat, symlink, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join, posix } from "node:path";
|
|
import { ADAPTER_AUTH_MISSING_CHECK_CODE } from "@paperclipai/shared";
|
|
import * as localCredentialFile from "../local-ai-credential-file.js";
|
|
import * as codexCredentials from "@paperclipai/adapter-codex-local/server";
|
|
const { prepareIsolatedCodexHome } = await import(new URL("../../../../packages/paperclip-runner/src/drivers/runtime-context-materializer.ts", import.meta.url).href);
|
|
const { execute, nativeProbe, selectedRunner, selectedRemoteRunner, sshExecute, sshRunner, nativeArtifacts, remoteLauncher, registerPrp, ingressConnect } = vi.hoisted(() => ({ execute: vi.fn(), nativeProbe: vi.fn(), selectedRunner: vi.fn(), selectedRemoteRunner: vi.fn(), sshExecute: vi.fn(), sshRunner: vi.fn(), nativeArtifacts: vi.fn(), remoteLauncher: vi.fn(), registerPrp: vi.fn(), ingressConnect: vi.fn() }));
|
|
const nativeProbeRuntimeDirectories = vi.hoisted(() => [] as string[]);
|
|
vi.mock("node:fs/promises", async original => {
|
|
const filesystem = await original<typeof import("node:fs/promises")>();
|
|
return { ...filesystem, mkdtemp: async (...args: Parameters<typeof filesystem.mkdtemp>) => {
|
|
const directory = await filesystem.mkdtemp(...args);
|
|
if (typeof args[0] === "string" && args[0].endsWith("paperclip-native-setup-") && typeof directory === "string") nativeProbeRuntimeDirectories.push(directory);
|
|
return directory;
|
|
} };
|
|
});
|
|
vi.mock("@paperclipai/adapter-utils/execution-target", () => ({ runAdapterExecutionTargetShellCommand: execute }));
|
|
vi.mock("./native-ssh-command-runner.js", () => ({ createNativeSshCommandRunner: sshRunner }));
|
|
vi.mock("../../realtime/runner-prp-ws.js", () => ({ registerRunnerPrpAuthority: registerPrp }));
|
|
vi.mock("../../realtime/runner-prp-outbound.js", () => ({ connectRunnerPrpIngress: ingressConnect }));
|
|
vi.mock("./native-codex-runner.js", () => ({ resolvePaperclipRunnerBinary: selectedRunner, resolveRemotePaperclipRunnerBinary: selectedRemoteRunner }));
|
|
vi.mock("../../vendor/paperclip-runner/index.js", async original => ({ ...await original<typeof import("../../vendor/paperclip-runner/index.js")>(), probeNativeRunnerEnvironment: nativeProbe }));
|
|
vi.mock("./native-session-executor.js", async original => ({ ...await original<typeof import("./native-session-executor.js")>(), createRemoteNativeArtifactPreparation: nativeArtifacts, createRemoteRunnerProcessLauncher: remoteLauncher }));
|
|
import { testNativeRunnerAuthentication } from "./setup-readiness.js";
|
|
const context = {
|
|
companyId: "company", adapterType: "paperclip_runner", config: { externallySandboxed: false, env: { PAPERCLIP_RUNNER_EXTERNAL_SANDBOX: "false" } },
|
|
executionTarget: { kind: "remote" as const, transport: "sandbox" as const, providerKey: "test", remoteCwd: "/workspace", runner: { execute: vi.fn() } },
|
|
};
|
|
describe("Codex selected native account verification", () => {
|
|
const receipt = (provider: "codex", model: string | null) => ({ provider, effectiveModel: model, providerDriver: "codex_app_server", helloProbePassed: true, cleanupConfirmed: true });
|
|
beforeEach(() => {
|
|
execute.mockReset(); nativeProbe.mockReset(); nativeProbeRuntimeDirectories.splice(0);
|
|
sshExecute.mockReset().mockResolvedValue({ exitCode: 0, timedOut: false, stdout: "", stderr: "" });
|
|
sshRunner.mockReset().mockReturnValue({ execute: sshExecute });
|
|
nativeArtifacts.mockReset().mockReturnValue({ prepare: vi.fn().mockResolvedValue(undefined) });
|
|
remoteLauncher.mockReset().mockReturnValue(vi.fn());
|
|
registerPrp.mockReset().mockImplementation(async () => ({ release: vi.fn() }));
|
|
vi.stubEnv("PAPERCLIP_RUNNER_PUBLIC_URL", "wss://paperclip.example.test");
|
|
vi.stubEnv("PAPERCLIP_RUNNER_CA_BUNDLE_PATH", "");
|
|
vi.stubEnv("PAPERCLIP_RUNNER_REMOTE_CODEX_PATH", "");
|
|
vi.stubEnv("PAPERCLIP_RUNNER_REMOTE_CODEX_NPM_SPEC", "");
|
|
selectedRunner.mockReset().mockReturnValue("/fixture/bin/paperclip-runnerd");
|
|
selectedRemoteRunner.mockReset().mockResolvedValue("/fixture/bin/paperclip-runnerd");
|
|
vi.stubEnv("PAPERCLIP_RUNNER_REMOTE_PROVIDER_PACK_PATH", "");
|
|
vi.stubEnv("PAPERCLIP_RUNNER_REMOTE_BINARY_PATH", "");
|
|
});
|
|
afterEach(async () => {
|
|
vi.unstubAllGlobals();
|
|
vi.unstubAllEnvs();
|
|
for (const root of nativeProbeRuntimeDirectories) await rm(root, { recursive: true, force: true });
|
|
});
|
|
it.each([["codex", "gpt-6.1-sol", "OPENAI_API_KEY"]] as const)("requires the native %s host and only the selected credential", async (provider, model, key) => {
|
|
vi.stubEnv(key, "ambient-account");
|
|
vi.stubEnv("ANTHROPIC_API_KEY", "ambient-anthropic");
|
|
nativeProbe.mockImplementation(async input => { await input.onCleanupConfirmed(); return receipt(provider, model); });
|
|
const result = await testNativeRunnerAuthentication({ companyId: "company", adapterType: "paperclip_runner", config: { externallySandboxed: true, env: { [key]: "bound-account", PAPERCLIP_RUNNER_EXTERNAL_SANDBOX: "true" }, modelReasoningEffort: "low" } }, provider, model);
|
|
expect(result.status).toBe("pass");
|
|
const input = nativeProbe.mock.calls[0][0];
|
|
expect(input).toMatchObject({ provider, model, timeoutMs: 45_000, environment: { [key]: "bound-account" } });
|
|
expect(input.transportOptions.runnerBinary).toBe("/fixture/bin/paperclip-runnerd");
|
|
expect(input.transportOptions.externallySandboxed).toBe(false);
|
|
expect(selectedRunner).toHaveBeenCalledOnce();
|
|
expect(input.environment.ANTHROPIC_API_KEY).toBeUndefined();
|
|
if (provider === "codex") expect(input.reasoningEffort).toBe("low");
|
|
else expect(input.reasoningEffort).toBeUndefined();
|
|
await expect(stat(input.runtimeDirectory)).rejects.toThrow();
|
|
expect(execute).not.toHaveBeenCalled();
|
|
});
|
|
it("rejects a missing local daemon before creating private credential staging", async () => {
|
|
const home = await realpath(await mkdtemp(join(tmpdir(), "native-preflight-daemon-")));
|
|
const auth = '{"tokens":{"account_id":"fixture-account","access_token":"fixture-access"}}';
|
|
const readCredential = vi.spyOn(localCredentialFile, "readLocalAiCredentialFile");
|
|
try {
|
|
await writeFile(join(home, "account-store.json"), auth, { mode: 0o600 });
|
|
await symlink(join(home, "account-store.json"), join(home, "auth.json"));
|
|
selectedRunner.mockImplementation(() => { throw new Error("native executable is missing"); });
|
|
const result = await testNativeRunnerAuthentication({ companyId: "company", adapterType: "paperclip_runner", managedAiCredentialHome: home, config: {} }, "codex", "gpt-6.1-sol");
|
|
expect(result.status).toBe("fail");
|
|
expect(result.checks[0].message).toBe("native executable is missing");
|
|
expect(readCredential).toHaveBeenCalledOnce();
|
|
expect(nativeProbeRuntimeDirectories).toEqual([]);
|
|
expect(nativeProbe).not.toHaveBeenCalled();
|
|
expect(await readFile(join(home, "account-store.json"), "utf8")).toBe(auth);
|
|
expect(await readlink(join(home, "auth.json"))).toBe(join(home, "account-store.json"));
|
|
expect((await readdir(home)).sort()).toEqual(["account-store.json", "auth.json"]);
|
|
} finally { readCredential.mockRestore(); await rm(home, { recursive: true, force: true }); }
|
|
});
|
|
it.each(["local", "remote"])("cleans its own private root when %s credential staging fails before probe handoff", async transport => {
|
|
const home = await realpath(await mkdtemp(join(tmpdir(), "native-preflight-staging-")));
|
|
const auth = '{"tokens":{"account_id":"fixture-account","access_token":"fixture-access"}}';
|
|
try {
|
|
await writeFile(join(home, "account-store.json"), auth, { mode: 0o600 });
|
|
await symlink(join(home, "account-store.json"), join(home, "auth.json"));
|
|
await writeFile(join(home, "config.toml"), "x".repeat(1024 * 1024 + 1), { mode: 0o600 });
|
|
const selectedContext = transport === "remote" ? sshContext : { companyId: "company", adapterType: "paperclip_runner", config: {} };
|
|
const result = await testNativeRunnerAuthentication({ ...selectedContext, managedAiCredentialHome: home }, "codex", "gpt-6.1-sol");
|
|
expect(result.status).toBe("fail");
|
|
expect(result.checks[0].message).toContain("configuration exceeds its size limit");
|
|
expect(nativeProbe).not.toHaveBeenCalled();
|
|
expect(sshExecute).not.toHaveBeenCalled();
|
|
expect(nativeProbeRuntimeDirectories).toHaveLength(1);
|
|
await expect(stat(nativeProbeRuntimeDirectories[0]!)).rejects.toMatchObject({ code: "ENOENT" });
|
|
expect(await readFile(join(home, "account-store.json"), "utf8")).toBe(auth);
|
|
expect((await stat(join(home, "config.toml"))).size).toBe(1024 * 1024 + 1);
|
|
expect(await readlink(join(home, "auth.json"))).toBe(join(home, "account-store.json"));
|
|
} finally { await rm(home, { recursive: true, force: true }); }
|
|
});
|
|
it.each(["host-home", "host-codex-home", "configured-home", "managed-home"] as const)("reuses the task's authorized local Codex home before isolation (%s)", async mode => {
|
|
vi.stubEnv("HOME", "/host-home");
|
|
vi.stubEnv("CODEX_HOME", mode === "host-home" ? "" : "/host-codex");
|
|
vi.stubEnv("OPENAI_API_KEY", "ambient-key-is-not-authorized");
|
|
nativeProbe.mockResolvedValue(receipt("codex", "observed-model"));
|
|
const config = mode === "configured-home" || mode === "managed-home" ? { env: { CODEX_HOME: "/configured-codex" } } : {};
|
|
expect((await testNativeRunnerAuthentication({ companyId: "company", adapterType: "paperclip_runner", config,
|
|
...(mode === "managed-home" ? { managedAiCredentialHome: "/managed-codex" } : {}) }, "codex", null)).status).toBe("pass");
|
|
const input = nativeProbe.mock.calls[0][0];
|
|
expect(input.transportOptions.sourceCodexHome).toBe({ "host-home": "/host-home/.codex", "host-codex-home": "/host-codex", "configured-home": "/configured-codex", "managed-home": "/managed-codex" }[mode]);
|
|
expect(input.environment.OPENAI_API_KEY).toBeUndefined();
|
|
expect(input.environment.HOME).toBeUndefined();
|
|
expect(input.runtimeDirectory).not.toContain("host-home");
|
|
});
|
|
it.each(["same-account", "another-account", "bound-key", "invalid-handoff"] as const)("preserves the server-selected login refresh before local cleanup (%s)", async mode => {
|
|
const home = await realpath(await mkdtemp(join(tmpdir(), "native-local-login-")));
|
|
const auth = (account: string, marker: string, age: number) => JSON.stringify({ tokens: { account_id: account, id_token: `id-${marker}`, access_token: `access-${marker}`, refresh_token: `refresh-${marker}` }, last_refresh: new Date(Date.now() - age).toISOString() });
|
|
const store = join(home, "account.json");
|
|
try {
|
|
await writeFile(store, auth("qa-account", "old", 120_000), { mode: 0o600 });
|
|
await symlink(store, join(home, "auth.json"));
|
|
nativeProbe.mockImplementation(async input => {
|
|
const privateHome = join(input.runtimeDirectory, "codex-home");
|
|
await mkdir(privateHome, { mode: 0o700 });
|
|
const filename = join(privateHome, "auth.json");
|
|
await writeFile(filename, auth(mode === "another-account" ? "other-account" : "qa-account", "new", 60_000), { mode: 0o600 });
|
|
if (mode === "bound-key") expect(input.onCodexCredentialRefresh).toBeUndefined();
|
|
else await input.onCodexCredentialRefresh(mode === "invalid-handoff" ? store : filename);
|
|
await input.onCleanupConfirmed();
|
|
return receipt("codex", "gpt-6.1-sol");
|
|
});
|
|
const result = await testNativeRunnerAuthentication({ companyId: "company", adapterType: "paperclip_runner", managedAiCredentialHome: home, config: { env: { CODEX_HOME: home, ...(mode === "bound-key" ? { OPENAI_API_KEY: "selected-key" } : {}) } } }, "codex", "gpt-6.1-sol");
|
|
expect(result.status).toBe(mode === "invalid-handoff" ? "fail" : "pass");
|
|
expect(JSON.parse(await readFile(store, "utf8")).tokens.access_token).toBe(mode === "same-account" ? "access-new" : "access-old");
|
|
expect((await stat(store)).mode & 0o777).toBe(0o600);
|
|
expect(await realpath(join(home, "auth.json"))).toBe(store);
|
|
expect(JSON.stringify(result)).not.toContain("access-new");
|
|
if (mode !== "invalid-handoff") await expect(stat(nativeProbe.mock.calls[0][0].runtimeDirectory)).rejects.toMatchObject({ code: "ENOENT" });
|
|
} finally { await rm(home, { recursive: true, force: true }); }
|
|
});
|
|
const sshContext = { companyId: "company", adapterType: "paperclip_runner", config: {}, executionTarget: {
|
|
kind: "remote" as const, transport: "ssh" as const, remoteCwd: "/qa-workspace", spec: { host: "qa-host", username: "qa-user", port: 22, remoteCwd: "/qa-workspace", remoteWorkspacePath: "/qa-workspace", privateKey: null, knownHosts: null, strictHostKeyChecking: true },
|
|
} };
|
|
it.each(["local", "remote"].flatMap(transport => ["configured", "operator"].flatMap(source => [false, true].map(linked => ({ transport, source, linked })))))
|
|
("matches actual task auth seeding before $transport setup (source=$source, linked=$linked)", async ({ transport, source, linked }) => {
|
|
const home = await realpath(await mkdtemp(join(tmpdir(), "native-unbound-auth-")));
|
|
const auth = '{"tokens":{"account_id":"fixture-unbound-account","access_token":"fixture-unbound-access","refresh_token":"fixture-unbound-refresh","id_token":"fixture-unbound-id"}}';
|
|
try {
|
|
const stored = join(home, linked ? "account-store.json" : "auth.json");
|
|
await writeFile(stored, auth, { mode: 0o600 });
|
|
if (linked) await symlink(stored, join(home, "auth.json"));
|
|
const taskHome = join(home, "actual-task-home");
|
|
await prepareIsolatedCodexHome({ context: null, codexHome: taskHome, sourceCodexHome: home });
|
|
const taskAuth = await readFile(join(taskHome, "auth.json"), "utf8").catch(() => null);
|
|
expect(taskAuth).toBe(linked ? null : auth);
|
|
vi.stubEnv("CODEX_HOME", source === "operator" ? home : "");
|
|
nativeProbe.mockImplementation(async input => {
|
|
const probeHome = join(input.runtimeDirectory, "codex-home");
|
|
await prepareIsolatedCodexHome({ context: null, codexHome: probeHome, sourceCodexHome: input.transportOptions.sourceCodexHome });
|
|
expect(await readFile(join(probeHome, "auth.json"), "utf8")).toBe(auth);
|
|
await input.onCleanupConfirmed();
|
|
return receipt("codex", "gpt-6.1-sol");
|
|
});
|
|
const testContext = transport === "remote" ? sshContext : { companyId: "company", adapterType: "paperclip_runner", config: {} };
|
|
const result = await testNativeRunnerAuthentication({ ...testContext, config: {
|
|
managedAiCredentialHome: home,
|
|
env: source === "configured" ? { CODEX_HOME: home } : {},
|
|
} }, "codex", "gpt-6.1-sol");
|
|
expect(result.status).toBe(linked ? "fail" : "pass");
|
|
if (linked) {
|
|
expect(nativeProbe).not.toHaveBeenCalled();
|
|
expect(sshExecute).not.toHaveBeenCalled();
|
|
expect(result.checks[0].message).toContain(source === "configured" ? "env.CODEX_HOME" : "operator CODEX_HOME");
|
|
expect(result.checks[0].message).toContain("Legacy runner");
|
|
expect(result.checks[0].message).toContain("managed Codex Connection");
|
|
} else expect(nativeProbe).toHaveBeenCalledOnce();
|
|
expect(await readFile(stored, "utf8")).toBe(auth);
|
|
expect(JSON.stringify(result)).not.toContain("fixture-unbound-access");
|
|
} finally { await rm(home, { recursive: true, force: true }); }
|
|
});
|
|
it.each(["local", "remote"].flatMap(transport => ["OPENAI_API_KEY", "CODEX_API_KEY"].flatMap(key => ["valid", "dangling", "invalid-store", "probe-error"].map(authState => ({ transport, key, authState })))))
|
|
("keeps the configured $key task credential authoritative over $authState linked auth during $transport setup", async ({ transport, key, authState }) => {
|
|
const home = await realpath(await mkdtemp(join(tmpdir(), "native-key-linked-auth-")));
|
|
const auth = '{"tokens":{"account_id":"unused-subscription","access_token":"unused-subscription-access"}}';
|
|
const stored = join(home, "account-store.json");
|
|
const readCredential = vi.spyOn(localCredentialFile, "readLocalAiCredentialFile");
|
|
const copyBack = vi.spyOn(codexCredentials, "copyBackCodexAuth");
|
|
try {
|
|
if (authState === "valid" || authState === "probe-error") await writeFile(stored, auth, { mode: 0o600 });
|
|
else if (authState === "invalid-store") await mkdir(stored, { mode: 0o700 });
|
|
await symlink(stored, join(home, "auth.json"));
|
|
const taskHome = join(home, "actual-key-task-home");
|
|
await prepareIsolatedCodexHome({ context: null, codexHome: taskHome, sourceCodexHome: home, apiKey: "configured-fixture-key" });
|
|
expect(JSON.parse(await readFile(join(taskHome, "auth.json"), "utf8"))).toEqual({ OPENAI_API_KEY: "configured-fixture-key" });
|
|
nativeProbe.mockImplementation(async input => {
|
|
expect(input.onCodexCredentialRefresh).toBeUndefined();
|
|
expect(input.transportOptions.sourceCodexHome).toBe(home);
|
|
const probeHome = join(input.runtimeDirectory, "codex-home");
|
|
await prepareIsolatedCodexHome({ context: null, codexHome: probeHome, sourceCodexHome: input.transportOptions.sourceCodexHome,
|
|
apiKey: input.environment.CODEX_API_KEY ?? input.environment.OPENAI_API_KEY });
|
|
expect(JSON.parse(await readFile(join(probeHome, "auth.json"), "utf8"))).toEqual({ OPENAI_API_KEY: "configured-fixture-key" });
|
|
await input.onCleanupConfirmed();
|
|
if (authState === "probe-error") throw new Error("authentication failed: configured-fixture-key");
|
|
return receipt("codex", "gpt-6.1-sol");
|
|
});
|
|
const testContext = transport === "remote" ? sshContext : { companyId: "company", adapterType: "paperclip_runner", config: {} };
|
|
const result = await testNativeRunnerAuthentication({ ...testContext, config: { env: { CODEX_HOME: home, [key]: "configured-fixture-key" } } }, "codex", "gpt-6.1-sol");
|
|
expect(result.status).toBe(authState === "probe-error" ? "fail" : "pass");
|
|
if (authState === "probe-error") expect(result.checks[0]).toMatchObject({ code: "codex_hello_probe_auth_required", message: "authentication failed: [REDACTED]" });
|
|
expect(nativeProbe).toHaveBeenCalledOnce();
|
|
expect(readCredential).not.toHaveBeenCalled();
|
|
expect(copyBack).not.toHaveBeenCalled();
|
|
expect((await lstat(join(home, "auth.json"))).isSymbolicLink()).toBe(true);
|
|
expect(await readlink(join(home, "auth.json"))).toBe(stored);
|
|
if (authState === "valid" || authState === "probe-error") expect(await readFile(stored, "utf8")).toBe(auth);
|
|
else if (authState === "invalid-store") expect(await readdir(stored)).toEqual([]);
|
|
else await expect(lstat(stored)).rejects.toMatchObject({ code: "ENOENT" });
|
|
expect(JSON.stringify(result)).not.toContain("configured-fixture-key");
|
|
} finally { readCredential.mockRestore(); copyBack.mockRestore(); await rm(home, { recursive: true, force: true }); }
|
|
});
|
|
it.each(["local", "remote"].flatMap(transport => [false, true].map(linked => ({ transport, linked }))))
|
|
("seeds the selected subscription before isolated $transport setup (linked=$linked)", async ({ transport, linked }) => {
|
|
const home = await realpath(await mkdtemp(join(tmpdir(), "native-selected-auth-")));
|
|
const auth = JSON.stringify({ tokens: { account_id: "fixture-account", id_token: "fixture-id", access_token: "fixture-access", refresh_token: "fixture-refresh" }, last_refresh: "2026-01-01T00:00:00.000Z" });
|
|
const stored = join(home, linked ? "account-store.json" : "auth.json");
|
|
let seeded = false;
|
|
try {
|
|
await writeFile(stored, auth, { mode: 0o600 });
|
|
if (linked) await symlink(stored, join(home, "auth.json"));
|
|
await writeFile(join(home, "config.toml"), 'cli_auth_credentials_store = "file"\n[mcp_servers.host_only]\ncommand = "untrusted-host-tool"\n', { mode: 0o600 });
|
|
await writeFile(join(home, "unassigned-private-file"), "must-not-copy");
|
|
nativeProbe.mockImplementation(async input => {
|
|
expect(input.environment._PAPERCLIP_NATIVE_SETUP_CODEX_AUTH_JSON_SECRET).toBeUndefined();
|
|
const codexHome = join(input.runtimeDirectory, "codex-home");
|
|
await prepareIsolatedCodexHome({ context: null, codexHome, sourceCodexHome: input.transportOptions.sourceCodexHome,
|
|
apiKey: input.environment.CODEX_API_KEY ?? input.environment.OPENAI_API_KEY });
|
|
const privateAuth = await readFile(join(codexHome, "auth.json"), "utf8").catch(() => null);
|
|
seeded = privateAuth === auth;
|
|
expect((await readFile(join(codexHome, "config.toml"), "utf8"))).not.toContain("untrusted-host-tool");
|
|
if (!seeded) throw new Error("authentication required");
|
|
if (linked) {
|
|
const source = join(input.runtimeDirectory, "codex-credential-source");
|
|
expect(input.transportOptions.sourceCodexHome).toBe(source);
|
|
expect((await stat(source)).mode & 0o777).toBe(0o700);
|
|
expect((await stat(join(source, "auth.json"))).mode & 0o777).toBe(0o600);
|
|
expect((await readdir(source)).sort()).toEqual(["auth.json", "config.toml"]);
|
|
}
|
|
if (transport === "remote") {
|
|
await remoteLauncher.mock.calls[0][0].ensureArtifact();
|
|
const upload = sshExecute.mock.calls.find(([value]) => value.args?.[1]?.includes("/codex-home/auth.json"))?.[0];
|
|
expect(upload).toBeDefined();
|
|
expect(Buffer.from(upload!.stdin, "base64").toString("utf8")).toBe(auth);
|
|
expect(upload!.args[1]).toContain("chmod 600");
|
|
}
|
|
await input.onCleanupConfirmed();
|
|
return receipt("codex", "gpt-6.1-sol");
|
|
});
|
|
const testContext = transport === "remote" ? sshContext : { companyId: "company", adapterType: "paperclip_runner", config: {} };
|
|
const result = await testNativeRunnerAuthentication({ ...testContext, config: { env: { CODEX_HOME: "/untrusted-configured-home", _PAPERCLIP_NATIVE_SETUP_CODEX_AUTH_JSON_SECRET: "spoofed-auth" } }, managedAiCredentialHome: home }, "codex", "gpt-6.1-sol");
|
|
expect(result.status).toBe("pass");
|
|
expect(seeded).toBe(true);
|
|
expect(await readFile(stored, "utf8")).toBe(auth);
|
|
expect(JSON.stringify(result)).not.toContain("fixture-access");
|
|
await expect(stat(nativeProbe.mock.calls[0][0].runtimeDirectory)).rejects.toMatchObject({ code: "ENOENT" });
|
|
expect(execute).not.toHaveBeenCalled();
|
|
} finally { await rm(home, { recursive: true, force: true }); }
|
|
});
|
|
it.each(["local", "remote"].flatMap(transport => ["dangling", "foreign-owner", "unsafe-mode", "oversize"].map(mode => ({ transport, mode }))))
|
|
("rejects $mode linked subscription before $transport provider admission", async ({ transport, mode }) => {
|
|
const home = await realpath(await mkdtemp(join(tmpdir(), "native-invalid-link-")));
|
|
const stored = join(home, "account-store.json");
|
|
let owner: ReturnType<typeof vi.spyOn> | undefined;
|
|
try {
|
|
if (mode !== "dangling") await writeFile(stored, mode === "oversize" ? "x".repeat(65_537) : '{"tokens":{"account_id":"fixture-account"}}', { mode: 0o600 });
|
|
await symlink(stored, join(home, "auth.json"));
|
|
if (mode === "unsafe-mode") await chmod(stored, 0o644);
|
|
if (mode === "foreign-owner") owner = vi.spyOn(process, "getuid").mockReturnValue(process.getuid!() + 1);
|
|
nativeProbe.mockResolvedValue(receipt("codex", "gpt-6.1-sol"));
|
|
const testContext = transport === "remote" ? sshContext : { companyId: "company", adapterType: "paperclip_runner", config: {} };
|
|
const result = await testNativeRunnerAuthentication({ ...testContext, managedAiCredentialHome: home }, "codex", "gpt-6.1-sol");
|
|
expect(result.status).toBe("fail");
|
|
expect(nativeProbe).not.toHaveBeenCalled();
|
|
expect(sshExecute).not.toHaveBeenCalled();
|
|
if (mode === "dangling") expect(result.checks.some(check => check.code === ADAPTER_AUTH_MISSING_CHECK_CODE)).toBe(true);
|
|
} finally { owner?.mockRestore(); await rm(home, { recursive: true, force: true }); }
|
|
});
|
|
it.each(["local", "remote"])("redacts checked file credentials from %s native probe errors without exporting them to env", async transport => {
|
|
const home = await realpath(await mkdtemp(join(tmpdir(), "native-file-redaction-")));
|
|
const auth = JSON.stringify({ tokens: { account_id: "fixture-account", id_token: "fixture-id", access_token: "fixture-access", refresh_token: "fixture-refresh" } });
|
|
try {
|
|
await writeFile(join(home, "account-store.json"), auth, { mode: 0o600 });
|
|
await symlink(join(home, "account-store.json"), join(home, "auth.json"));
|
|
nativeProbe.mockImplementation(async input => {
|
|
expect(input.environment._PAPERCLIP_NATIVE_SETUP_CODEX_AUTH_JSON_SECRET).toBeUndefined();
|
|
throw new Error("authentication failed: " + auth + " fixture-access fixture-refresh");
|
|
});
|
|
const testContext = transport === "remote" ? sshContext : { companyId: "company", adapterType: "paperclip_runner", config: {} };
|
|
const result = await testNativeRunnerAuthentication({ ...testContext, managedAiCredentialHome: home }, "codex", "gpt-6.1-sol");
|
|
expect(result.status).toBe("fail");
|
|
expect(JSON.stringify(result)).not.toMatch(/fixture-account|fixture-access|fixture-refresh|fixture-id/);
|
|
expect(result.checks[0].message).toContain("[REDACTED]");
|
|
} finally { await rm(home, { recursive: true, force: true }); }
|
|
});
|
|
it.each(["local", "remote"].flatMap(transport => [false, true].map(confirmed => ({ transport, confirmed }))))
|
|
("settles private linked credentials only after confirmed canceled $transport setup (confirmed=$confirmed)", async ({ transport, confirmed }) => {
|
|
const home = await realpath(await mkdtemp(join(tmpdir(), "native-linked-cancel-")));
|
|
const auth = '{"tokens":{"account_id":"fixture-account","access_token":"fixture-access"}}';
|
|
try {
|
|
await writeFile(join(home, "account-store.json"), auth, { mode: 0o600 });
|
|
await symlink(join(home, "account-store.json"), join(home, "auth.json"));
|
|
nativeProbe.mockImplementation(async input => {
|
|
expect(await readFile(join(input.transportOptions.sourceCodexHome, "auth.json"), "utf8")).toBe(auth);
|
|
if (confirmed) await input.onCleanupConfirmed();
|
|
throw new Error("Native provider setup canceled");
|
|
});
|
|
const testContext = transport === "remote" ? sshContext : { companyId: "company", adapterType: "paperclip_runner", config: {} };
|
|
expect((await testNativeRunnerAuthentication({ ...testContext, managedAiCredentialHome: home }, "codex", "gpt-6.1-sol")).status).toBe("fail");
|
|
const root = nativeProbe.mock.calls[0][0].runtimeDirectory;
|
|
if (confirmed) await expect(stat(root)).rejects.toMatchObject({ code: "ENOENT" });
|
|
else expect(await readFile(join(root, "codex-credential-source", "auth.json"), "utf8")).toBe(auth);
|
|
expect(await readFile(join(home, "account-store.json"), "utf8")).toBe(auth);
|
|
} finally { await rm(home, { recursive: true, force: true }); }
|
|
});
|
|
it.each(["linux-arm64", "linux-x64"])("uses task staging and the selected %s SSH daemon without a provider pack", async target => {
|
|
const selectedBinary = `/fixture/bin/${target}/paperclip-runnerd`;
|
|
selectedRemoteRunner.mockResolvedValue(selectedBinary);
|
|
let released = false;
|
|
registerPrp.mockResolvedValue({ release: async () => { released = true; } });
|
|
nativeProbe.mockImplementation(async input => {
|
|
const registration = await input.transportOptions.controlPlaneRegistration({ marker: "native-authority" });
|
|
expect(registration.connection).toMatchObject({ mode: "connect", connectUrl: expect.stringMatching(/^wss:\/\/paperclip\.example\.test\/api\/runner\/v1\/connect\//) });
|
|
await registration.release();
|
|
await input.onCleanupConfirmed();
|
|
return receipt("codex", "gpt-6.1-sol");
|
|
});
|
|
expect((await testNativeRunnerAuthentication({ ...sshContext, config: { externallySandboxed: true, env: { OPENAI_API_KEY: "company-account", PAPERCLIP_RUNNER_EXTERNAL_SANDBOX: "true" }, modelReasoningEffort: "low" } }, "codex", "gpt-6.1-sol")).status).toBe("pass");
|
|
const input = nativeProbe.mock.calls[0][0];
|
|
expect(input).toMatchObject({ reasoningEffort: "low", environment: { OPENAI_API_KEY: "company-account" }, workingDirectory: expect.stringMatching(/^\/qa-workspace\/.paperclip-runtime\/paperclip-native-setup-.*\/filesystem\/workspace$/) });
|
|
expect(input.transportOptions).toMatchObject({ runnerBinary: selectedBinary, codexCommand: expect.stringMatching(/\/bin\/codex$/), runnerFilesystemRoot: expect.stringMatching(/\/filesystem$/), externallySandboxed: false });
|
|
expect(nativeArtifacts).toHaveBeenCalledWith(expect.objectContaining({ controllerRunnerBinary: selectedBinary, model: "gpt-6.1-sol", targetPlatformArtifactVerified: true }));
|
|
expect(selectedRemoteRunner).toHaveBeenCalledExactlyOnceWith({ runner: { execute: sshExecute }, remoteCwd: "/qa-workspace", configuredPath: null });
|
|
expect(selectedRunner).not.toHaveBeenCalled();
|
|
expect(nativeArtifacts.mock.results[0]!.value.prepare).toHaveBeenCalledWith("dial_wss");
|
|
expect(registerPrp).toHaveBeenCalledWith({ companyId: "company", runId: input.transportOptions.prpIdentity.runId, authority: { marker: "native-authority" } });
|
|
expect(remoteLauncher).toHaveBeenCalledWith(expect.objectContaining({ runnerInstanceId: input.transportOptions.prpIdentity.runnerInstanceId }));
|
|
expect(released).toBe(true);
|
|
expect(sshExecute.mock.calls.at(-1)![0]).toMatchObject({ command: "rm", args: ["-rf", "--", expect.stringContaining("paperclip-native-setup-")] });
|
|
expect(sshExecute.mock.calls.some(([value]) => JSON.stringify(value).includes("company-account"))).toBe(false);
|
|
expect(execute).not.toHaveBeenCalled();
|
|
});
|
|
it.each(["runtime missing", "authentication failed: selected-key", "no native completion", "model mismatch"])("fails SSH native setup without a legacy fallback (%s)", async message => {
|
|
nativeProbe.mockRejectedValue(new Error(message));
|
|
const result = await testNativeRunnerAuthentication({ ...sshContext, config: { env: { OPENAI_API_KEY: "selected-key" } } }, "codex", "gpt-6.1-sol");
|
|
expect(result.status).toBe("fail"); expect(JSON.stringify(result)).not.toContain("selected-key");
|
|
expect(nativeProbe).toHaveBeenCalledOnce(); expect(execute).not.toHaveBeenCalled();
|
|
expect(sshExecute.mock.calls.some(([value]) => value.command === "rm")).toBe(false);
|
|
expect((await stat(nativeProbe.mock.calls[0][0].runtimeDirectory)).isDirectory()).toBe(true);
|
|
});
|
|
it("fails SSH transport eligibility before staging or opening the native provider", async () => {
|
|
vi.stubEnv("PAPERCLIP_RUNNER_PUBLIC_URL", "");
|
|
expect((await testNativeRunnerAuthentication(sshContext, "codex", null)).status).toBe("fail");
|
|
expect(sshExecute).not.toHaveBeenCalled(); expect(nativeArtifacts).not.toHaveBeenCalled(); expect(nativeProbe).not.toHaveBeenCalled();
|
|
});
|
|
it("reports an unavailable SSH target daemon before probing credentials or switching runtimes", async () => {
|
|
selectedRemoteRunner.mockRejectedValue(new Error("runner_remote_artifact_unavailable: install the matching linux-arm64 Paperclip release"));
|
|
const result = await testNativeRunnerAuthentication(sshContext, "codex", null);
|
|
expect(result.status).toBe("fail"); expect(result.checks[0].message).toContain("matching linux-arm64 Paperclip release");
|
|
expect(nativeArtifacts).not.toHaveBeenCalled(); expect(nativeProbe).not.toHaveBeenCalled();
|
|
expect(selectedRunner).not.toHaveBeenCalled(); expect(execute).not.toHaveBeenCalled();
|
|
});
|
|
it.each([true, false])("removes SSH probe state only after confirmed teardown (%s)", async confirmed => {
|
|
nativeProbe.mockImplementation(async input => {
|
|
const registration = await input.transportOptions.controlPlaneRegistration({});
|
|
await registration.release();
|
|
if (confirmed) await input.onCleanupConfirmed();
|
|
throw new Error("authentication failed");
|
|
});
|
|
expect((await testNativeRunnerAuthentication(sshContext, "codex", null)).status).toBe("fail");
|
|
expect(sshExecute.mock.calls.some(([value]) => value.command === "rm")).toBe(confirmed);
|
|
const root = nativeProbe.mock.calls[0][0].runtimeDirectory;
|
|
if (confirmed) await expect(stat(root)).rejects.toThrow(); else expect((await stat(root)).isDirectory()).toBe(true);
|
|
});
|
|
it.each(["same-account", "another-account", "read-failure", "uncertain-close"] as const)("preserves managed SSH account refresh and confirmed cleanup (%s)", async mode => {
|
|
const home = await mkdtemp(join(tmpdir(), "native-ssh-managed-account-"));
|
|
const auth = (account: string, marker: string, age: number) => JSON.stringify({ tokens: { account_id: account, id_token: `id-${marker}`, access_token: `access-${marker}`, refresh_token: `refresh-${marker}` }, last_refresh: new Date(Date.now() - age).toISOString() });
|
|
try {
|
|
await writeFile(join(home, "account-store.json"), auth("qa-account", "old", 120_000), { mode: 0o600 });
|
|
await symlink(join(home, "account-store.json"), join(home, "auth.json"));
|
|
const refreshed = auth(mode === "another-account" ? "different-account" : "qa-account", "new", 60_000);
|
|
sshExecute.mockImplementation(async input => ({ exitCode: input.args?.[2] === "paperclip-native-refresh" && mode === "read-failure" ? 1 : 0, timedOut: false,
|
|
stdout: input.args?.[2] === "paperclip-native-refresh" ? Buffer.from(refreshed).toString("base64") : "", stderr: "" }));
|
|
nativeProbe.mockImplementation(async input => {
|
|
const registration = await input.transportOptions.controlPlaneRegistration({});
|
|
await registration.release();
|
|
const original = new Error("authentication failed in native SSH" + (mode === "uncertain-close" ? "; daemon teardown was not confirmed" : ""));
|
|
try { await input.onCodexCredentialRefresh(posix.join(input.transportOptions.runnerFilesystemRoot, "codex-home", "auth.json")); }
|
|
catch (error) { throw new AggregateError([original, error], original.message + "; " + (error as Error).message, { cause: original }); }
|
|
if (mode !== "uncertain-close") await input.onCleanupConfirmed();
|
|
throw original;
|
|
});
|
|
const result = await testNativeRunnerAuthentication({ ...sshContext, managedAiCredentialHome: home }, "codex", "gpt-6.1-sol");
|
|
expect(result.status).toBe("fail"); expect(result.checks[0].message).toContain("authentication failed in native SSH");
|
|
expect(JSON.parse(await readFile(join(home, "auth.json"), "utf8")).tokens.access_token).toBe(mode === "same-account" || mode === "uncertain-close" ? "access-new" : "access-old");
|
|
expect(sshExecute.mock.calls.some(([input]) => input.command === "rm")).toBe(mode === "same-account" || mode === "another-account");
|
|
expect(JSON.stringify(result)).not.toContain("access-new");
|
|
const read = sshExecute.mock.calls.find(([input]) => input.args?.[2] === "paperclip-native-refresh")![0];
|
|
expect(read.command).toBe("sh"); expect(read.args[1]).toContain("head -c 65537"); expect(read.args[1]).toContain("test ! -L");
|
|
expect(nativeProbe.mock.calls[0][0].transportOptions.sourceCodexHome).toBe(join(nativeProbe.mock.calls[0][0].runtimeDirectory, "codex-credential-source"));
|
|
if (mode === "read-failure") expect(result.checks[0].message).toContain("refresh handoff unavailable");
|
|
if (mode === "uncertain-close") expect(result.checks[0].message).toContain("teardown was not confirmed");
|
|
} finally { await rm(home, { recursive: true, force: true }); }
|
|
});
|
|
it.each([
|
|
["authentication failed: bound-account", "auth_required"], ["Native provider hello probe timed out.", "timeout"],
|
|
["Model is unavailable", "failed"], ["native executable is missing", "failed"], ["native provider returned no response", "failed"],
|
|
])("fails closed and redacts native failures (%s)", async (message, code) => {
|
|
nativeProbe.mockRejectedValue(new Error(message));
|
|
const result = await testNativeRunnerAuthentication({ companyId: "company", adapterType: "paperclip_runner", config: { env: { OPENAI_API_KEY: "bound-account" } } }, "codex", "gpt-6.1-sol");
|
|
expect(result.status).toBe("fail");
|
|
expect(result.checks[0]).toMatchObject({ code: `codex_hello_probe_${code}`, level: "error" });
|
|
expect(result.checks.some(check => check.code === ADAPTER_AUTH_MISSING_CHECK_CODE)).toBe(code === "auth_required");
|
|
expect(JSON.stringify(result)).not.toContain("bound-account");
|
|
expect(nativeProbe).toHaveBeenCalledOnce();
|
|
expect(execute).not.toHaveBeenCalled();
|
|
});
|
|
it.each([
|
|
["authentication required: selected-account", true],
|
|
["Model is unavailable", false],
|
|
["native executable is missing", false],
|
|
])("projects the canonical sandbox login check only for authentication failures (%s)", async (message, authentication) => {
|
|
nativeProbe.mockRejectedValue(new Error(message));
|
|
const result = await testNativeRunnerAuthentication(context, "codex", "gpt-6.1-sol");
|
|
expect(result.status).toBe("fail");
|
|
expect(result.checks[0].code).toBe(`codex_hello_probe_${authentication ? "auth_required" : "failed"}`);
|
|
expect(result.checks.some(check => check.code === ADAPTER_AUTH_MISSING_CHECK_CODE)).toBe(authentication);
|
|
if (authentication) expect(result.checks).toContainEqual(expect.objectContaining({ code: ADAPTER_AUTH_MISSING_CHECK_CODE, level: "error" }));
|
|
expect(nativeProbe).toHaveBeenCalledOnce();
|
|
expect(execute).not.toHaveBeenCalled();
|
|
});
|
|
it.each([
|
|
{ helloProbePassed: undefined }, { effectiveModel: "another-model" }, { effectiveModel: null }, { effectiveModel: "" }, { provider: "opencode" }, { providerDriver: "codex_cli" },
|
|
])("rejects installation-only, legacy, or mismatched receipts", async override => {
|
|
nativeProbe.mockResolvedValue({ ...receipt("codex", "gpt-6.1-sol"), ...override });
|
|
expect((await testNativeRunnerAuthentication({ companyId: "company", adapterType: "paperclip_runner", config: {} }, "codex", "gpt-6.1-sol")).status).toBe("fail");
|
|
});
|
|
it("accepts an automatic Codex model only when the native provider observed a nonempty model", async () => {
|
|
nativeProbe.mockResolvedValue(receipt("codex", "gpt-6.1-sol"));
|
|
expect((await testNativeRunnerAuthentication({ companyId: "company", adapterType: "paperclip_runner", config: {} }, "codex", null)).status).toBe("pass");
|
|
expect(nativeProbe.mock.calls[0][0].model).toBeNull();
|
|
nativeProbe.mockResolvedValue(receipt("codex", null));
|
|
expect((await testNativeRunnerAuthentication({ companyId: "company", adapterType: "paperclip_runner", config: {} }, "codex", null)).status).toBe("fail");
|
|
nativeProbe.mockResolvedValue(receipt("codex", "unknown"));
|
|
expect((await testNativeRunnerAuthentication({ companyId: "company", adapterType: "paperclip_runner", config: {} }, "codex", null)).status).toBe("fail");
|
|
});
|
|
it.each([true, false])("removes failed local probe state only after confirmed teardown (%s)", async confirmed => {
|
|
nativeProbe.mockImplementation(async input => {
|
|
if (confirmed) await input.onCleanupConfirmed();
|
|
throw new Error("authentication failed");
|
|
});
|
|
expect((await testNativeRunnerAuthentication({ companyId: "company", adapterType: "paperclip_runner", config: {} }, "codex", null)).status).toBe("fail");
|
|
const root = nativeProbe.mock.calls[0][0].runtimeDirectory;
|
|
if (confirmed) await expect(stat(root)).rejects.toThrow();
|
|
else expect((await stat(root)).isDirectory()).toBe(true);
|
|
});
|
|
it.each(["arm64", "x64"].flatMap(controllerArch => ["arm64", "x64"].flatMap(targetArch => [false, true].map(explicit => ({ controllerArch, targetArch, explicit })))))
|
|
("uses target $targetArch artifacts and controller PRP from Mac $controllerArch for sandbox setup (override=$explicit)", async ({ controllerArch, targetArch, explicit }) => {
|
|
vi.stubGlobal("process", { ...process, platform: "darwin", arch: controllerArch });
|
|
const selectedBinary = `/controller/artifacts/linux-${targetArch}/paperclip-runnerd`;
|
|
selectedRemoteRunner.mockResolvedValue(selectedBinary);
|
|
if (explicit) vi.stubEnv("PAPERCLIP_RUNNER_REMOTE_BINARY_PATH", selectedBinary);
|
|
nativeProbe.mockImplementation(async input => {
|
|
const registration = await input.transportOptions.controlPlaneRegistration({});
|
|
expect(registration.connection.mode).toBe("connect");
|
|
await registration.release(); await input.onCleanupConfirmed();
|
|
return receipt("codex", "gpt-6.1-sol");
|
|
});
|
|
const target = { ...context.executionTarget, runner: { execute: sshExecute } };
|
|
expect((await testNativeRunnerAuthentication({ ...context, executionTarget: target }, "codex", "gpt-6.1-sol")).status).toBe("pass");
|
|
expect(sshRunner).not.toHaveBeenCalled();
|
|
expect(selectedRemoteRunner).toHaveBeenCalledExactlyOnceWith({ runner: target.runner, remoteCwd: target.remoteCwd, configuredPath: explicit ? selectedBinary : null });
|
|
expect(selectedRunner).not.toHaveBeenCalled();
|
|
expect(nativeArtifacts).toHaveBeenCalledWith(expect.objectContaining({ target, runner: target.runner, controllerRunnerBinary: selectedBinary, targetPlatformArtifactVerified: true }));
|
|
expect(nativeProbe.mock.calls[0][0].transportOptions.runnerBinary).toBe(selectedBinary);
|
|
expect(nativeProbe.mock.calls[0][0].transportOptions.externallySandboxed).toBe(true);
|
|
expect(nativeArtifacts.mock.results[0].value.prepare).toHaveBeenCalledWith("dial_wss");
|
|
expect(execute).not.toHaveBeenCalled();
|
|
});
|
|
it.each(["runner_remote_artifact_unavailable", "runner_remote_artifact_platform_mismatch"])("fails sandbox %s before setup or a controller fallback", async code => {
|
|
selectedRemoteRunner.mockRejectedValue(new Error(code));
|
|
const target = { ...context.executionTarget, runner: { execute: sshExecute } };
|
|
const result = await testNativeRunnerAuthentication({ ...context, executionTarget: target }, "codex", "gpt-6.1-sol");
|
|
expect(result.status).toBe("fail");
|
|
expect(result.checks[0].message).toContain(code);
|
|
expect(selectedRunner).not.toHaveBeenCalled(); expect(nativeArtifacts).not.toHaveBeenCalled();
|
|
expect(nativeProbe).not.toHaveBeenCalled(); expect(sshExecute).not.toHaveBeenCalled();
|
|
expect(execute).not.toHaveBeenCalled();
|
|
});
|
|
it("prepares listener artifacts before acquiring sandbox ingress and owns its release", async () => {
|
|
const order: string[] = [];
|
|
const close = vi.fn();
|
|
const endpoint = { kind: "authenticated_websocket" as const, websocketUrl: "wss://ingress.test", secretHeaders: [], generation: "generation", refresh: vi.fn(), close };
|
|
const getRunnerIngressEndpoint = vi.fn(async () => { order.push("ingress"); return endpoint; });
|
|
nativeArtifacts.mockReturnValue({ prepare: vi.fn(async mode => { expect(mode).toBe("listen_ws"); order.push("artifacts"); }) });
|
|
const connectionClose = vi.fn();
|
|
ingressConnect.mockImplementation(() => { order.push("connect"); return { ready: Promise.resolve(), close: connectionClose }; });
|
|
nativeProbe.mockImplementation(async input => {
|
|
const registration = await input.transportOptions.controlPlaneRegistration({});
|
|
expect(registration.connection.mode).toBe("listen");
|
|
remoteLauncher.mock.calls[0][0].onRunnerProcessSpawned();
|
|
await registration.activate(); await registration.ready(); await registration.release();
|
|
await input.onCleanupConfirmed(); return receipt("codex", "gpt-6.1-sol");
|
|
});
|
|
const target = { ...context.executionTarget, providerKey: "daytona", runner: { execute: sshExecute }, leaseId: "lease", effectiveCapabilities: { reusableLeases: false, nativeSyncIn: false, nativeSyncOut: false, persistentProcessSessions: false, independentControlCommands: true, incrementalSessionOutput: false, concurrentSyncOperations: false, duplexCommandStream: false, runnerWebSocketIngress: true }, getRunnerIngressEndpoint };
|
|
expect((await testNativeRunnerAuthentication({ ...context, executionTarget: target }, "codex", "gpt-6.1-sol")).status).toBe("pass");
|
|
expect(order).toEqual(["artifacts", "ingress", "connect"]);
|
|
expect(nativeProbe.mock.calls[0][0].transportOptions.externallySandboxed).toBe(true);
|
|
expect(connectionClose).toHaveBeenCalledOnce(); expect(registerPrp).not.toHaveBeenCalled();
|
|
});
|
|
});
|