import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it, vi } from "vitest"; import { CODEX_SYNC_ALLOWLIST, codexHomeHasUsableAuth, ensureSymlink, evaluateCodexCredentialReadiness, mergeManagedCodexMcpGateways, isManagedCodexHomePath, prepareManagedCodexHome, reconcileManagedCodexHome, seedManagedCodexHome, stageCodexHomeForSync, writeManagedCodexMcpConfig, } from "./codex-home.js"; describe("mergeManagedCodexMcpGateways", () => { it("keeps runtime gateways and appends non-overlapping context gateways", () => { expect( mergeManagedCodexMcpGateways( [{ name: "runtime", endpointPath: "/runtime", bearerToken: "runtime-token" }], [ { name: "runtime", endpointPath: "/stale", bearerToken: "stale-token" }, { name: "manual", endpointPath: "/manual", bearerToken: "manual-token" }, ], ), ).toEqual([ { name: "runtime", endpointPath: "/runtime", bearerToken: "runtime-token" }, { name: "manual", endpointPath: "/manual", bearerToken: "manual-token" }, ]); }); }); describe("codex managed home", () => { afterEach(() => { vi.restoreAllMocks(); }); it("treats a concurrently-created expected auth symlink as success", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-home-")); const sharedCodexHome = path.join(root, "shared-codex-home"); const paperclipHome = path.join(root, "paperclip-home"); const managedCodexHome = path.join( paperclipHome, "instances", "default", "companies", "company-1", "codex-home", ); const sharedAuth = path.join(sharedCodexHome, "auth.json"); const managedAuth = path.join(managedCodexHome, "auth.json"); await fs.mkdir(sharedCodexHome, { recursive: true }); await fs.writeFile(sharedAuth, '{"OPENAI_API_KEY":"shared"}\n', "utf8"); const originalSymlink = fs.symlink.bind(fs); vi.spyOn(fs, "symlink").mockImplementationOnce(async (source, target, type) => { await originalSymlink(source, target, type); const error = new Error("file already exists") as NodeJS.ErrnoException; error.code = "EEXIST"; throw error; }); try { await expect( prepareManagedCodexHome( { CODEX_HOME: sharedCodexHome, PAPERCLIP_HOME: paperclipHome, PAPERCLIP_INSTANCE_ID: "default", }, async () => {}, "company-1", ), ).resolves.toBe(managedCodexHome); expect((await fs.lstat(managedAuth)).isSymbolicLink()).toBe(true); expect(await fs.realpath(managedAuth)).toBe(await fs.realpath(sharedAuth)); } finally { await fs.rm(root, { recursive: true, force: true }); } }); it("still throws on EEXIST when a raced-in auth symlink points elsewhere", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-home-")); const sharedCodexHome = path.join(root, "shared-codex-home"); const paperclipHome = path.join(root, "paperclip-home"); const managedCodexHome = path.join( paperclipHome, "instances", "default", "companies", "company-1", "codex-home", ); const sharedAuth = path.join(sharedCodexHome, "auth.json"); const wrongAuth = path.join(sharedCodexHome, "other-auth.json"); const managedAuth = path.join(managedCodexHome, "auth.json"); await fs.mkdir(sharedCodexHome, { recursive: true }); await fs.writeFile(sharedAuth, '{"OPENAI_API_KEY":"shared"}\n', "utf8"); await fs.writeFile(wrongAuth, '{"token":"other"}\n', "utf8"); const originalSymlink = fs.symlink.bind(fs); vi.spyOn(fs, "symlink").mockImplementationOnce(async (_source, target, type) => { await originalSymlink(wrongAuth, target, type); const error = new Error("file already exists") as NodeJS.ErrnoException; error.code = "EEXIST"; throw error; }); try { await expect( prepareManagedCodexHome( { CODEX_HOME: sharedCodexHome, PAPERCLIP_HOME: paperclipHome, PAPERCLIP_INSTANCE_ID: "default", }, async () => {}, "company-1", ), ).rejects.toMatchObject({ code: "EEXIST" }); expect((await fs.lstat(managedAuth)).isSymbolicLink()).toBe(true); expect(await fs.readlink(managedAuth)).toBe(wrongAuth); } finally { await fs.rm(root, { recursive: true, force: true }); } }); // Regression for #5028: older Paperclip versions copied auth.json into the // managed home instead of symlinking. After upgrading to the symlink-based // logic, the stale regular file at the target stayed in place and every // subsequent codex_local run failed with refresh_token_reused as soon as the // source token rotated. `ensureSymlink` now heals the upgrade path by // unlinking the stale copy and creating a symlink to the live source. it("replaces a stale regular-file auth.json with a symlink to the live source (#5028)", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-home-")); try { const sharedCodexHome = path.join(root, "shared-codex-home"); const paperclipHome = path.join(root, "paperclip-home"); const managedCodexHome = path.join( paperclipHome, "instances", "default", "companies", "company-1", "codex-home", ); const sharedAuth = path.join(sharedCodexHome, "auth.json"); const managedAuth = path.join(managedCodexHome, "auth.json"); await fs.mkdir(sharedCodexHome, { recursive: true }); // The live source has rotated since the stale copy was written. await fs.writeFile(sharedAuth, '{"token":"fresh"}', "utf8"); // Simulate a stale copy left by a previous Paperclip version. await fs.mkdir(managedCodexHome, { recursive: true }); await fs.writeFile(managedAuth, '{"token":"stale-from-copy"}', "utf8"); await prepareManagedCodexHome( { CODEX_HOME: sharedCodexHome, PAPERCLIP_HOME: paperclipHome, PAPERCLIP_INSTANCE_ID: "default", }, async () => {}, "company-1", ); expect((await fs.lstat(managedAuth)).isSymbolicLink()).toBe(true); expect(await fs.readFile(managedAuth, "utf8")).toBe('{"token":"fresh"}'); } finally { await fs.rm(root, { recursive: true, force: true }); } }); // Direct unit coverage for the new ensureSymlink branch (#5028). The // regression test above goes through prepareManagedCodexHome, whose // pre-existing apikey-mode cleanup `fs.rm`s the stale auth.json before // ensureSymlink runs — so the heal branch never executes there. Call // ensureSymlink directly to prove the unlink-and-recreate path itself. it("ensureSymlink: unlinks a stale regular file and recreates the symlink", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-ensure-symlink-")); try { const source = path.join(root, "live-source.json"); const target = path.join(root, "stale-target.json"); await fs.writeFile(source, '{"token":"fresh"}', "utf8"); await fs.writeFile(target, '{"token":"stale-from-copy"}', "utf8"); await ensureSymlink(target, source); expect((await fs.lstat(target)).isSymbolicLink()).toBe(true); expect(await fs.readFile(target, "utf8")).toBe('{"token":"fresh"}'); } finally { await fs.rm(root, { recursive: true, force: true }); } }); // The isDirectory() guard added with the heal branch must keep an unexpected // directory in place rather than throwing EISDIR. We treat a directory at // this path as operator-owned, not a stale Paperclip copy. it("ensureSymlink: leaves an unexpected directory in place instead of throwing", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-ensure-symlink-dir-")); try { const source = path.join(root, "live-source.json"); const target = path.join(root, "unexpected-dir"); await fs.writeFile(source, '{"token":"fresh"}', "utf8"); await fs.mkdir(target); await fs.writeFile(path.join(target, "sentinel"), "keep-me", "utf8"); await expect(ensureSymlink(target, source)).resolves.toBeUndefined(); expect((await fs.lstat(target)).isDirectory()).toBe(true); expect(await fs.readFile(path.join(target, "sentinel"), "utf8")).toBe("keep-me"); } finally { await fs.rm(root, { recursive: true, force: true }); } }); }); describe("isManagedCodexHomePath", () => { const env = { PAPERCLIP_HOME: "/srv/paperclip", PAPERCLIP_INSTANCE_ID: "default", } satisfies NodeJS.ProcessEnv; const companyRoot = path.resolve( "/srv/paperclip/instances/default/companies/company-1", ); it("treats the per-agent managed home as managed", () => { expect( isManagedCodexHomePath( env, "company-1", path.join(companyRoot, "agents", "agent-7", "codex-home"), ), ).toBe(true); }); it("treats the shared company home as managed", () => { expect( isManagedCodexHomePath(env, "company-1", path.join(companyRoot, "codex-home")), ).toBe(true); }); it("treats a path outside the company tree as an external override", () => { expect(isManagedCodexHomePath(env, "company-1", "/home/dev/.codex")).toBe(false); expect( isManagedCodexHomePath( env, "company-1", path.resolve("/srv/paperclip/instances/default/companies/company-2/codex-home"), ), ).toBe(false); }); it("returns false without a companyId", () => { expect(isManagedCodexHomePath(env, undefined, path.join(companyRoot, "codex-home"))).toBe( false, ); }); }); describe("codexHomeHasUsableAuth", () => { it("is true for credential-bearing auth.json and false when missing", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-auth-")); try { expect(await codexHomeHasUsableAuth(root)).toBe(false); await fs.writeFile(path.join(root, "auth.json"), "{}", "utf8"); expect(await codexHomeHasUsableAuth(root)).toBe(false); await fs.writeFile(path.join(root, "auth.json"), '{"foo":"bar"}', "utf8"); expect(await codexHomeHasUsableAuth(root)).toBe(false); await fs.writeFile(path.join(root, "auth.json"), '{"token":"shared"}', "utf8"); expect(await codexHomeHasUsableAuth(root)).toBe(false); await fs.writeFile(path.join(root, "auth.json"), '{"access_token":"shared"}', "utf8"); expect(await codexHomeHasUsableAuth(root)).toBe(false); await fs.writeFile(path.join(root, "auth.json"), '{"OPENAI_API_KEY":"shared"}', "utf8"); expect(await codexHomeHasUsableAuth(root)).toBe(true); } finally { await fs.rm(root, { recursive: true, force: true }); } }); it("recognizes the Codex 0.143 AuthDotJson subscription shape", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-auth-modern-")); try { await fs.writeFile( path.join(root, "auth.json"), JSON.stringify({ tokens: { id_token: "synthetic-id-token", access_token: "synthetic-access-token", refresh_token: "synthetic-refresh-token", account_id: "acct-modern", }, last_refresh: "2026-07-09T00:00:00Z", }), "utf8", ); expect(await codexHomeHasUsableAuth(root)).toBe(true); } finally { await fs.rm(root, { recursive: true, force: true }); } }); it("treats subscription auth without account_id or token material as unusable", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-auth-modern-invalid-")); try { await fs.writeFile( path.join(root, "auth.json"), JSON.stringify({ tokens: { id_token: "synthetic-id-token", access_token: "synthetic-access-token", refresh_token: "synthetic-refresh-token", }, last_refresh: "2026-07-09T00:00:00Z", }), "utf8", ); expect(await codexHomeHasUsableAuth(root)).toBe(false); await fs.writeFile( path.join(root, "auth.json"), JSON.stringify({ tokens: { account_id: "acct-modern", }, last_refresh: "2026-07-09T00:00:00Z", }), "utf8", ); expect(await codexHomeHasUsableAuth(root)).toBe(false); } finally { await fs.rm(root, { recursive: true, force: true }); } }); it("is false for a dangling auth.json symlink", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-auth-dangling-")); try { await fs.symlink(path.join(root, "missing-source.json"), path.join(root, "auth.json")); expect(await codexHomeHasUsableAuth(root)).toBe(false); } finally { await fs.rm(root, { recursive: true, force: true }); } }); }); describe("seedManagedCodexHome", () => { it("symlinks auth.json from the shared source into an explicit per-agent home", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-seed-")); try { const sharedCodexHome = path.join(root, "shared-codex-home"); const agentHome = path.join( root, "instances", "default", "companies", "company-1", "agents", "agent-7", "codex-home", ); const sharedAuth = path.join(sharedCodexHome, "auth.json"); const agentAuth = path.join(agentHome, "auth.json"); await fs.mkdir(sharedCodexHome, { recursive: true }); await fs.writeFile(sharedAuth, '{"OPENAI_API_KEY":"shared"}', "utf8"); await seedManagedCodexHome(agentHome, { CODEX_HOME: sharedCodexHome }, async () => {}); expect((await fs.lstat(agentAuth)).isSymbolicLink()).toBe(true); expect(await fs.realpath(agentAuth)).toBe(await fs.realpath(sharedAuth)); } finally { await fs.rm(root, { recursive: true, force: true }); } }); it("writes an API-key auth.json into the home when an apiKey is supplied", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-seed-apikey-")); try { const agentHome = path.join(root, "agent-home"); const emptyShared = path.join(root, "empty-shared"); await fs.mkdir(emptyShared, { recursive: true }); await seedManagedCodexHome(agentHome, { CODEX_HOME: emptyShared }, async () => {}, { apiKey: "sk-test-123", }); const written = JSON.parse(await fs.readFile(path.join(agentHome, "auth.json"), "utf8")); expect(written).toEqual({ OPENAI_API_KEY: "sk-test-123" }); } finally { await fs.rm(root, { recursive: true, force: true }); } }); // A device-login promotion writes the company credential as a regular-file // subscription auth.json. Re-seeding must keep it, or the first Test probe or // run after a successful login silently signs the company out. The cases // below pin the identity- and freshness-anchored rule: keep a subscription // identity the shared source does not hold; keep a same-identity file the // shared source is not strictly fresher than (ties and unparseable freshness // included); still heal the same-identity stale copy once the shared source // has moved past it (#5028) and still remove apikey-mode residue. const subscriptionAuth = ( accountId: string, marker: string, lastRefresh: string | null = "2026-07-09T00:00:00Z", ) => JSON.stringify({ tokens: { id_token: `synthetic-id-token-${marker}`, access_token: `synthetic-access-token-${marker}`, refresh_token: `synthetic-refresh-token-${marker}`, account_id: accountId, }, ...(lastRefresh ? { last_refresh: lastRefresh } : {}), }); it("keeps a promoted subscription auth.json when the shared source has no auth", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-seed-promoted-")); try { const companyHome = path.join(root, "company-home"); const emptyShared = path.join(root, "empty-shared"); const promoted = subscriptionAuth("acct-promoted", "promoted"); await fs.mkdir(emptyShared, { recursive: true }); await fs.mkdir(companyHome, { recursive: true }); await fs.writeFile(path.join(companyHome, "auth.json"), promoted, "utf8"); await seedManagedCodexHome(companyHome, { CODEX_HOME: emptyShared }, async () => {}); const kept = path.join(companyHome, "auth.json"); expect((await fs.lstat(kept)).isSymbolicLink()).toBe(false); expect(await fs.readFile(kept, "utf8")).toBe(promoted); } finally { await fs.rm(root, { recursive: true, force: true }); } }); it("keeps a promoted subscription auth.json whose identity differs from the shared source", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-seed-foreign-")); try { const companyHome = path.join(root, "company-home"); const sharedCodexHome = path.join(root, "shared-codex-home"); const promoted = subscriptionAuth("acct-promoted", "promoted"); await fs.mkdir(sharedCodexHome, { recursive: true }); await fs.writeFile( path.join(sharedCodexHome, "auth.json"), subscriptionAuth("acct-host", "host"), "utf8", ); await fs.mkdir(companyHome, { recursive: true }); await fs.writeFile(path.join(companyHome, "auth.json"), promoted, "utf8"); await seedManagedCodexHome(companyHome, { CODEX_HOME: sharedCodexHome }, async () => {}); const kept = path.join(companyHome, "auth.json"); expect((await fs.lstat(kept)).isSymbolicLink()).toBe(false); expect(await fs.readFile(kept, "utf8")).toBe(promoted); } finally { await fs.rm(root, { recursive: true, force: true }); } }); it("still replaces a same-identity stale regular copy with the shared symlink once the source is strictly fresher (#5028)", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-seed-stale-")); try { const companyHome = path.join(root, "company-home"); const sharedCodexHome = path.join(root, "shared-codex-home"); // The live source has rotated since the stale copy was written, so its // last_refresh is strictly greater — the real #5028 shape. const fresh = subscriptionAuth("acct-same", "fresh", "2026-07-09T02:00:00Z"); await fs.mkdir(sharedCodexHome, { recursive: true }); await fs.writeFile(path.join(sharedCodexHome, "auth.json"), fresh, "utf8"); await fs.mkdir(companyHome, { recursive: true }); await fs.writeFile( path.join(companyHome, "auth.json"), subscriptionAuth("acct-same", "stale", "2026-07-09T01:00:00Z"), "utf8", ); await seedManagedCodexHome(companyHome, { CODEX_HOME: sharedCodexHome }, async () => {}); const healed = path.join(companyHome, "auth.json"); expect((await fs.lstat(healed)).isSymbolicLink()).toBe(true); expect(await fs.readFile(healed, "utf8")).toBe(fresh); } finally { await fs.rm(root, { recursive: true, force: true }); } }); it("keeps a same-identity promoted auth.json that is strictly newer than the shared source", async () => { // The device-login promotion mints a credential whose last_refresh is newer // than the host copy the user was failing with. Swapping it for the shared // symlink here would sign the company back in with that failing credential // right after the login that replaced it. const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-seed-newer-")); try { const companyHome = path.join(root, "company-home"); const sharedCodexHome = path.join(root, "shared-codex-home"); const promoted = subscriptionAuth("acct-same", "promoted", "2026-07-09T02:00:00Z"); await fs.mkdir(sharedCodexHome, { recursive: true }); await fs.writeFile( path.join(sharedCodexHome, "auth.json"), subscriptionAuth("acct-same", "host", "2026-07-09T01:00:00Z"), "utf8", ); await fs.mkdir(companyHome, { recursive: true }); await fs.writeFile(path.join(companyHome, "auth.json"), promoted, "utf8"); await seedManagedCodexHome(companyHome, { CODEX_HOME: sharedCodexHome }, async () => {}); const kept = path.join(companyHome, "auth.json"); expect((await fs.lstat(kept)).isSymbolicLink()).toBe(false); expect(await fs.readFile(kept, "utf8")).toBe(promoted); } finally { await fs.rm(root, { recursive: true, force: true }); } }); it("keeps a same-identity auth.json when freshness ties or cannot be compared", async () => { // Ties and unparseable timestamps keep the file: deleting a promoted // credential is irreversible, while a kept file self-corrects on the next // seed once the shared source has provably moved past it. const cases = [ // Tie: same last_refresh on both sides. { source: "2026-07-09T01:00:00Z", target: "2026-07-09T01:00:00Z" }, // The shared source carries no parseable last_refresh. { source: null, target: "2026-07-09T01:00:00Z" }, // The file carries no parseable last_refresh. { source: "2026-07-09T02:00:00Z", target: null }, ]; for (const { source, target } of cases) { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-seed-tie-")); try { const companyHome = path.join(root, "company-home"); const sharedCodexHome = path.join(root, "shared-codex-home"); const file = subscriptionAuth("acct-same", "file", target); await fs.mkdir(sharedCodexHome, { recursive: true }); await fs.writeFile( path.join(sharedCodexHome, "auth.json"), subscriptionAuth("acct-same", "host", source), "utf8", ); await fs.mkdir(companyHome, { recursive: true }); await fs.writeFile(path.join(companyHome, "auth.json"), file, "utf8"); await seedManagedCodexHome(companyHome, { CODEX_HOME: sharedCodexHome }, async () => {}); const kept = path.join(companyHome, "auth.json"); expect((await fs.lstat(kept)).isSymbolicLink()).toBe(false); expect(await fs.readFile(kept, "utf8")).toBe(file); } finally { await fs.rm(root, { recursive: true, force: true }); } } }); it("keeps the usable target when the shared source exists but cannot be read", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-seed-src-err-")); try { const companyHome = path.join(root, "company-home"); const sharedCodexHome = path.join(root, "shared-codex-home"); // A directory at the source auth.json path makes the read fail with // EISDIR — a deterministic present-but-unreadable source. Removal plus // the existence-only symlink pass would link the home to a source no // downstream reader can use, so the usable target must survive. await fs.mkdir(path.join(sharedCodexHome, "auth.json"), { recursive: true }); await fs.mkdir(companyHome, { recursive: true }); const target = subscriptionAuth("acct-unknown-source", "target"); await fs.writeFile(path.join(companyHome, "auth.json"), target, "utf8"); await seedManagedCodexHome(companyHome, { CODEX_HOME: sharedCodexHome }, async () => {}); const kept = path.join(companyHome, "auth.json"); expect((await fs.lstat(kept)).isSymbolicLink()).toBe(false); expect(await fs.readFile(kept, "utf8")).toBe(target); } finally { await fs.rm(root, { recursive: true, force: true }); } }); it("never symlinks or heals a credential-store entry's auth.json, even for a fresher same-identity source", async () => { // An agent can bind CODEX_HOME to a per-identity store entry through the // login's account-home secret, and this seeding pass runs before every // probe and execute. The entry's auth.json is the durable login the // promotion/vend/copy-back own — a strictly-fresher same-identity shared // source must NOT trigger the #5028 heal here, or the bound account is // silently swapped for the host login. const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-seed-store-")); try { const sharedCodexHome = path.join(root, "shared-codex-home"); const entryHome = path.join( root, "paperclip-home", "instances", "default", "companies", "company-1", "codex-auth-cache", "acct-bound", ); const env = { CODEX_HOME: sharedCodexHome, PAPERCLIP_HOME: path.join(root, "paperclip-home"), PAPERCLIP_INSTANCE_ID: "default", }; const stored = subscriptionAuth("acct-same", "stored", "2026-07-09T01:00:00Z"); await fs.mkdir(sharedCodexHome, { recursive: true }); await fs.writeFile( path.join(sharedCodexHome, "auth.json"), subscriptionAuth("acct-same", "host", "2026-07-09T02:00:00Z"), "utf8", ); await fs.writeFile(path.join(sharedCodexHome, "config.toml"), 'model = "gpt-5"\n', "utf8"); await fs.mkdir(entryHome, { recursive: true }); await fs.writeFile(path.join(entryHome, "auth.json"), stored, "utf8"); await seedManagedCodexHome(entryHome, env, async () => {}); const kept = path.join(entryHome, "auth.json"); expect((await fs.lstat(kept)).isSymbolicLink()).toBe(false); expect(await fs.readFile(kept, "utf8")).toBe(stored); // The static shared config still copies in, so a bound run gets the // same config a per-agent home gets. expect(await fs.readFile(path.join(entryHome, "config.toml"), "utf8")).toBe('model = "gpt-5"\n'); } finally { await fs.rm(root, { recursive: true, force: true }); } }); it("refuses an API-key rewrite of a credential-store entry's auth.json", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-seed-store-apikey-")); try { const sharedCodexHome = path.join(root, "shared-codex-home"); const entryHome = path.join( root, "paperclip-home", "instances", "default", "companies", "company-1", "codex-auth-cache", "acct-bound", ); const env = { CODEX_HOME: sharedCodexHome, PAPERCLIP_HOME: path.join(root, "paperclip-home"), PAPERCLIP_INSTANCE_ID: "default", }; const stored = subscriptionAuth("acct-bound-id", "stored", "2026-07-09T01:00:00Z"); await fs.mkdir(sharedCodexHome, { recursive: true }); await fs.mkdir(entryHome, { recursive: true }); await fs.writeFile(path.join(entryHome, "auth.json"), stored, "utf8"); const logs: string[] = []; await seedManagedCodexHome(entryHome, env, async (_stream, line) => { logs.push(line); }, { apiKey: "sk-configured" }); expect(await fs.readFile(path.join(entryHome, "auth.json"), "utf8")).toBe(stored); expect(logs.join("\n")).toContain("Refusing to write an API-key auth.json into credential-store entry"); } finally { await fs.rm(root, { recursive: true, force: true }); } }); it("still removes an apikey-mode auth.json so the chatgpt-mode symlink is restored", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-seed-apikey-residue-")); try { const companyHome = path.join(root, "company-home"); const sharedCodexHome = path.join(root, "shared-codex-home"); const shared = subscriptionAuth("acct-host", "host"); await fs.mkdir(sharedCodexHome, { recursive: true }); await fs.writeFile(path.join(sharedCodexHome, "auth.json"), shared, "utf8"); await fs.mkdir(companyHome, { recursive: true }); await fs.writeFile( path.join(companyHome, "auth.json"), '{"OPENAI_API_KEY":"stale-key"}', "utf8", ); await seedManagedCodexHome(companyHome, { CODEX_HOME: sharedCodexHome }, async () => {}); const healed = path.join(companyHome, "auth.json"); expect((await fs.lstat(healed)).isSymbolicLink()).toBe(true); expect(await fs.readFile(healed, "utf8")).toBe(shared); } finally { await fs.rm(root, { recursive: true, force: true }); } }); }); // Startup backfill for already-isolated managed homes. describe("reconcileManagedCodexHome", () => { async function makeFixture() { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-reconcile-")); const sharedCodexHome = path.join(root, "shared-codex-home"); const paperclipHome = path.join(root, "paperclip-home"); const agentHome = path.join( paperclipHome, "instances", "default", "companies", "company-1", "agents", "agent-7", "codex-home", ); const sharedAuth = path.join(sharedCodexHome, "auth.json"); const agentAuth = path.join(agentHome, "auth.json"); await fs.mkdir(sharedCodexHome, { recursive: true }); await fs.writeFile(sharedAuth, '{"OPENAI_API_KEY":"shared"}', "utf8"); const env = { CODEX_HOME: sharedCodexHome, PAPERCLIP_HOME: paperclipHome, PAPERCLIP_INSTANCE_ID: "default", } satisfies NodeJS.ProcessEnv; return { root, sharedCodexHome, sharedAuth, agentHome, agentAuth, env }; } it("seeds a previously-stranded managed home and is a no-op on re-run", async () => { const fx = await makeFixture(); try { // The isolation guard created the per-agent home with no auth.json. expect(await codexHomeHasUsableAuth(fx.agentHome)).toBe(false); const first = await reconcileManagedCodexHome({ companyId: "company-1", configuredCodexHome: fx.agentHome, env: fx.env, }); expect(first.status).toBe("seeded"); expect(first.home).toBe(fx.agentHome); expect((await fs.lstat(fx.agentAuth)).isSymbolicLink()).toBe(true); expect(await fs.realpath(fx.agentAuth)).toBe(await fs.realpath(fx.sharedAuth)); const second = await reconcileManagedCodexHome({ companyId: "company-1", configuredCodexHome: fx.agentHome, env: fx.env, }); expect(second.status).toBe("already_seeded"); expect((await fs.lstat(fx.agentAuth)).isSymbolicLink()).toBe(true); expect(await fs.realpath(fx.agentAuth)).toBe(await fs.realpath(fx.sharedAuth)); } finally { await fs.rm(fx.root, { recursive: true, force: true }); } }); it("reports source_auth_missing when shared auth is unavailable", async () => { const fx = await makeFixture(); try { await fs.rm(fx.sharedAuth, { force: true }); const result = await reconcileManagedCodexHome({ companyId: "company-1", configuredCodexHome: fx.agentHome, env: fx.env, }); expect(result.status).toBe("source_auth_missing"); await expect(fs.lstat(fx.agentAuth)).rejects.toThrow(); } finally { await fs.rm(fx.root, { recursive: true, force: true }); } }); it("leaves a genuine external override untouched", async () => { const fx = await makeFixture(); try { const external = path.join(fx.root, "user-codex"); await fs.mkdir(external, { recursive: true }); const result = await reconcileManagedCodexHome({ companyId: "company-1", configuredCodexHome: external, env: fx.env, }); expect(result.status).toBe("external_override"); expect(await codexHomeHasUsableAuth(external)).toBe(false); } finally { await fs.rm(fx.root, { recursive: true, force: true }); } }); it("reports no_managed_home when no CODEX_HOME is configured", async () => { const fx = await makeFixture(); try { const result = await reconcileManagedCodexHome({ companyId: "company-1", configuredCodexHome: null, env: fx.env, }); expect(result).toEqual({ status: "no_managed_home", home: null }); } finally { await fs.rm(fx.root, { recursive: true, force: true }); } }); it("preserves an existing API-key auth.json when the key is secret-bound", async () => { const fx = await makeFixture(); try { // A prior execute-time run resolved the secret and wrote a regular-file // auth.json containing the key. await fs.mkdir(fx.agentHome, { recursive: true }); await fs.writeFile( fx.agentAuth, JSON.stringify({ OPENAI_API_KEY: "sk-secret-resolved" }), { mode: 0o600 }, ); const result = await reconcileManagedCodexHome({ companyId: "company-1", configuredCodexHome: fx.agentHome, apiKeySecretBound: true, env: fx.env, }); expect(result.status).toBe("already_seeded"); expect((await fs.lstat(fx.agentAuth)).isSymbolicLink()).toBe(false); expect(JSON.parse(await fs.readFile(fx.agentAuth, "utf8"))).toEqual({ OPENAI_API_KEY: "sk-secret-resolved", }); } finally { await fs.rm(fx.root, { recursive: true, force: true }); } }); it("seeds the shared symlink for a secret-bound key when no auth exists yet", async () => { const fx = await makeFixture(); try { const result = await reconcileManagedCodexHome({ companyId: "company-1", configuredCodexHome: fx.agentHome, apiKeySecretBound: true, env: fx.env, }); expect(result.status).toBe("seeded"); expect((await fs.lstat(fx.agentAuth)).isSymbolicLink()).toBe(true); expect(await fs.realpath(fx.agentAuth)).toBe(await fs.realpath(fx.sharedAuth)); } finally { await fs.rm(fx.root, { recursive: true, force: true }); } }); it("writes an API-key auth.json into a managed home when an apiKey is supplied", async () => { const fx = await makeFixture(); try { const result = await reconcileManagedCodexHome({ companyId: "company-1", configuredCodexHome: fx.agentHome, apiKey: "sk-reconcile-1", env: fx.env, }); expect(result.status).toBe("seeded"); const written = JSON.parse(await fs.readFile(fx.agentAuth, "utf8")); expect(written).toEqual({ OPENAI_API_KEY: "sk-reconcile-1" }); const second = await reconcileManagedCodexHome({ companyId: "company-1", configuredCodexHome: fx.agentHome, apiKey: "sk-reconcile-1", env: fx.env, }); expect(second.status).toBe("already_seeded"); expect(JSON.parse(await fs.readFile(fx.agentAuth, "utf8"))).toEqual({ OPENAI_API_KEY: "sk-reconcile-1", }); } finally { await fs.rm(fx.root, { recursive: true, force: true }); } }); }); describe("evaluateCodexCredentialReadiness", () => { async function makeFixture() { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-readiness-")); const sharedCodexHome = path.join(root, "shared-codex-home"); const paperclipHome = path.join(root, "paperclip-home"); const companyRoot = path.join( paperclipHome, "instances", "default", "companies", "company-1", ); const managedCompanyHome = path.join(companyRoot, "codex-home"); const managedAgentHome = path.join(companyRoot, "agents", "agent-1", "codex-home"); const env: NodeJS.ProcessEnv = { CODEX_HOME: sharedCodexHome, PAPERCLIP_HOME: paperclipHome, PAPERCLIP_INSTANCE_ID: "default", }; await fs.mkdir(sharedCodexHome, { recursive: true }); return { root, sharedCodexHome, managedCompanyHome, managedAgentHome, env }; } async function writeUsableAuth(home: string) { await fs.mkdir(home, { recursive: true }); await fs.writeFile(path.join(home, "auth.json"), '{"OPENAI_API_KEY":"sk-live"}\n', "utf8"); } it("flags a managed home with no source auth and empty OPENAI_API_KEY as not ready", async () => { const fx = await makeFixture(); try { const result = await evaluateCodexCredentialReadiness({ env: fx.env, companyId: "company-1", configuredCodexHome: fx.managedAgentHome, configuredApiKey: "", }); expect(result).toMatchObject({ managed: true, authMode: "subscription", ready: false }); expect(result.effectiveHome).toBe(path.resolve(fx.managedAgentHome)); } finally { await fs.rm(fx.root, { recursive: true, force: true }); } }); it("treats a non-empty resolved OPENAI_API_KEY as ready without touching disk", async () => { const fx = await makeFixture(); try { const result = await evaluateCodexCredentialReadiness({ env: fx.env, companyId: "company-1", configuredCodexHome: fx.managedAgentHome, configuredApiKey: "sk-agent-key", }); expect(result).toMatchObject({ managed: true, authMode: "api", ready: true }); } finally { await fs.rm(fx.root, { recursive: true, force: true }); } }); it("is ready when the shared source home carries usable subscription auth", async () => { const fx = await makeFixture(); try { await writeUsableAuth(fx.sharedCodexHome); const result = await evaluateCodexCredentialReadiness({ env: fx.env, companyId: "company-1", configuredCodexHome: fx.managedAgentHome, configuredApiKey: "", }); expect(result).toMatchObject({ managed: true, authMode: "subscription", ready: true }); } finally { await fs.rm(fx.root, { recursive: true, force: true }); } }); it("is ready when the already-seeded effective home carries usable auth", async () => { const fx = await makeFixture(); try { await writeUsableAuth(fx.managedAgentHome); const result = await evaluateCodexCredentialReadiness({ env: fx.env, companyId: "company-1", configuredCodexHome: fx.managedAgentHome, configuredApiKey: "", }); expect(result).toMatchObject({ managed: true, authMode: "subscription", ready: true }); } finally { await fs.rm(fx.root, { recursive: true, force: true }); } }); it("defaults to the managed company home when no CODEX_HOME is configured", async () => { const fx = await makeFixture(); try { const result = await evaluateCodexCredentialReadiness({ env: fx.env, companyId: "company-1", configuredCodexHome: null, configuredApiKey: "", }); expect(result).toMatchObject({ managed: true, authMode: "subscription", ready: false }); expect(result.effectiveHome).toBe(path.resolve(fx.managedCompanyHome)); } finally { await fs.rm(fx.root, { recursive: true, force: true }); } }); it("treats an external/user-supplied CODEX_HOME override as self-managed and ready", async () => { const fx = await makeFixture(); try { const externalHome = path.join(fx.root, "user-codex-home"); await fs.mkdir(externalHome, { recursive: true }); const result = await evaluateCodexCredentialReadiness({ env: fx.env, companyId: "company-1", configuredCodexHome: externalHome, configuredApiKey: "", }); expect(result).toMatchObject({ managed: false, ready: true }); } finally { await fs.rm(fx.root, { recursive: true, force: true }); } }); it("replaces the managed MCP block and clears stale servers for an empty runtime set", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-mcp-config-")); try { const alphaHome = path.join(root, "agent-alpha"); const zeroHome = path.join(root, "agent-zero"); await writeManagedCodexMcpConfig({ codexHome: alphaHome, apiBaseUrl: "https://paperclip.example", gateways: [{ name: "alpha", endpointPath: "https://paperclip.example/api/tool-gateway/gateways/alpha/mcp", bearerToken: "alpha-token", }], }); await writeManagedCodexMcpConfig({ codexHome: zeroHome, apiBaseUrl: "https://paperclip.example", gateways: [{ name: "stale", endpointPath: "/api/tool-gateway/gateways/stale/mcp", bearerToken: "stale-token", }], }); await writeManagedCodexMcpConfig({ codexHome: zeroHome, apiBaseUrl: "https://paperclip.example", gateways: [], }); const alpha = await fs.readFile(path.join(alphaHome, "config.toml"), "utf8"); const zero = await fs.readFile(path.join(zeroHome, "config.toml"), "utf8"); expect(alpha).toContain('[mcp_servers."alpha"]'); expect(alpha).toContain('Authorization = "Bearer alpha-token"'); expect(zero).not.toContain("mcp_servers."); expect(zero).not.toContain("stale-token"); expect(alphaHome).not.toBe(zeroHome); } finally { await fs.rm(root, { recursive: true, force: true }); } }); it("restricts permissions on an existing managed MCP config", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-mcp-config-")); try { const configPath = path.join(root, "config.toml"); await fs.writeFile(configPath, "model = \"gpt-5\"\n", { mode: 0o644 }); await writeManagedCodexMcpConfig({ codexHome: root, apiBaseUrl: "https://paperclip.example", gateways: [], }); expect((await fs.stat(configPath)).mode & 0o777).toBe(0o600); } finally { await fs.rm(root, { recursive: true, force: true }); } }); }); describe("stageCodexHomeForSync", () => { afterEach(() => { vi.restoreAllMocks(); }); // Builds a fake managed CODEX_HOME containing the full allowlist (with // `auth.json` as a symlink into a separate source-bytes file and a populated // `skills/` symlink, mirroring the real managed home) plus decoy runtime // state the allowlist must NOT copy. async function buildFakeHome(root: string): Promise<{ home: string; authBytes: string; skillBytes: string }> { const home = path.join(root, "codex-home"); const authSource = path.join(root, "shared", "auth.json"); const skillSource = path.join(root, "shared", "skill-src.md"); const authBytes = '{"tokens":{"account_id":"acct","refresh_token":"r"}}\n'; const skillBytes = "# injected skill\n"; await fs.mkdir(path.join(root, "shared"), { recursive: true }); await fs.writeFile(authSource, authBytes, "utf8"); await fs.writeFile(skillSource, skillBytes, "utf8"); await fs.mkdir(home, { recursive: true }); // auth.json is a symlink into the shared source (single-use rotating tokens). await fs.symlink(authSource, path.join(home, "auth.json")); await fs.writeFile(path.join(home, "config.toml"), "model_provider = \"paperclip\"\n", "utf8"); await fs.writeFile(path.join(home, "config.json"), "{}\n", "utf8"); await fs.writeFile(path.join(home, "instructions.md"), "hi\n", "utf8"); // skills/ is a directory of symlinks. await fs.mkdir(path.join(home, "skills"), { recursive: true }); await fs.symlink(skillSource, path.join(home, "skills", "demo.md")); // Decoys: large runtime state the 4-name denylist missed. await fs.writeFile(path.join(home, "logs_2.sqlite"), "x", "utf8"); await fs.writeFile(path.join(home, "state_5.sqlite"), "x", "utf8"); await fs.mkdir(path.join(home, "plugins", "cache"), { recursive: true }); await fs.writeFile(path.join(home, "plugins", "cache", "x"), "x", "utf8"); await fs.mkdir(path.join(home, "sessions"), { recursive: true }); await fs.writeFile(path.join(home, "sessions", "y"), "x", "utf8"); await fs.mkdir(path.join(home, "tmp"), { recursive: true }); await fs.symlink("/usr/bin/env", path.join(home, "tmp", "arg0")); return { home, authBytes, skillBytes }; } it("stages exactly the allowlist, derefs auth.json to bytes, and excludes decoys", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-stage-")); let staged: string | null = null; try { const { home, authBytes, skillBytes } = await buildFakeHome(root); staged = await stageCodexHomeForSync(home, { runId: "run-1" }); const entries = (await fs.readdir(staged)).sort(); expect(entries).toEqual([...CODEX_SYNC_ALLOWLIST].sort()); // Decoys must be absent. for (const decoy of ["logs_2.sqlite", "state_5.sqlite", "plugins", "sessions", "tmp"]) { expect(entries).not.toContain(decoy); } // auth.json is a regular file (symlink dereferenced) whose bytes equal the target. const stagedAuth = path.join(staged, "auth.json"); expect((await fs.lstat(stagedAuth)).isSymbolicLink()).toBe(false); expect(await fs.readFile(stagedAuth, "utf8")).toBe(authBytes); // skills/ copied recursively with the symlink dereferenced to bytes. const stagedSkill = path.join(staged, "skills", "demo.md"); expect((await fs.lstat(stagedSkill)).isSymbolicLink()).toBe(false); expect(await fs.readFile(stagedSkill, "utf8")).toBe(skillBytes); // config.toml (post-rewrite state) carried through. expect(await fs.readFile(path.join(staged, "config.toml"), "utf8")).toContain("model_provider"); } finally { if (staged) await fs.rm(staged, { recursive: true, force: true }); await fs.rm(root, { recursive: true, force: true }); } }); // C1 — staged credential file must be mode 0600 (not the world-readable default). it("writes the staged auth.json with mode 0600", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-stage-mode-")); let staged: string | null = null; try { const { home } = await buildFakeHome(root); staged = await stageCodexHomeForSync(home, { runId: "run-mode" }); const mode = (await fs.stat(path.join(staged, "auth.json"))).mode & 0o777; expect(mode).toBe(0o600); } finally { if (staged) await fs.rm(staged, { recursive: true, force: true }); await fs.rm(root, { recursive: true, force: true }); } }); // config.toml carries the managed MCP `Authorization: Bearer …` header and is // secret-bearing; the staged copy must be 0600, not the world-readable default. it("writes the staged config.toml (managed MCP bearer header) with mode 0600", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-stage-toml-mode-")); let staged: string | null = null; try { const home = path.join(root, "codex-home"); await fs.mkdir(home, { recursive: true }); // Mirror the source writer: config.toml holds an MCP gateway bearer token // and is persisted 0600 on disk. await fs.writeFile( path.join(home, "config.toml"), "[mcp_servers.paperclip]\nheaders = { Authorization = \"Bearer secret-token\" }\n", { mode: 0o600 }, ); staged = await stageCodexHomeForSync(home, { runId: "run-toml-mode" }); const mode = (await fs.stat(path.join(staged, "config.toml"))).mode & 0o777; expect(mode).toBe(0o600); } finally { if (staged) await fs.rm(staged, { recursive: true, force: true }); await fs.rm(root, { recursive: true, force: true }); } }); // Least privilege: no staged regular file needs group/other read, so every // one (config.json, instructions.md — not just credentials) is staged 0600. it("writes every staged regular file with mode 0600", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-stage-all-mode-")); let staged: string | null = null; try { const { home } = await buildFakeHome(root); staged = await stageCodexHomeForSync(home, { runId: "run-all-mode" }); for (const entry of ["auth.json", "config.toml", "config.json", "instructions.md"]) { const mode = (await fs.stat(path.join(staged, entry))).mode & 0o777; expect(mode, `${entry} should be staged 0600`).toBe(0o600); } } finally { if (staged) await fs.rm(staged, { recursive: true, force: true }); await fs.rm(root, { recursive: true, force: true }); } }); // C2 — staged dir must be 0700 (mkdtemp guarantees this on POSIX). it("creates the staged dir with mode 0700", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-stage-dir-")); let staged: string | null = null; try { const { home } = await buildFakeHome(root); staged = await stageCodexHomeForSync(home, { runId: "run-dir" }); const mode = (await fs.stat(staged)).mode & 0o777; expect(mode).toBe(0o700); } finally { if (staged) await fs.rm(staged, { recursive: true, force: true }); await fs.rm(root, { recursive: true, force: true }); } }); it("skips absent optional entries without throwing", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-stage-absent-")); let staged: string | null = null; try { // Keyring-credential mode: no auth.json, no config.json. const home = path.join(root, "codex-home"); await fs.mkdir(home, { recursive: true }); await fs.writeFile(path.join(home, "config.toml"), "x\n", "utf8"); staged = await stageCodexHomeForSync(home, { runId: "run-absent" }); const entries = await fs.readdir(staged); expect(entries).toEqual(["config.toml"]); } finally { if (staged) await fs.rm(staged, { recursive: true, force: true }); await fs.rm(root, { recursive: true, force: true }); } }); it("treats a dangling auth.json symlink as absent (skips it, no throw)", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-stage-dangling-")); let staged: string | null = null; try { const home = path.join(root, "codex-home"); await fs.mkdir(home, { recursive: true }); await fs.symlink(path.join(root, "gone", "auth.json"), path.join(home, "auth.json")); await fs.writeFile(path.join(home, "config.toml"), "x\n", "utf8"); staged = await stageCodexHomeForSync(home, { runId: "run-dangling" }); expect(await fs.readdir(staged)).toEqual(["config.toml"]); } finally { if (staged) await fs.rm(staged, { recursive: true, force: true }); await fs.rm(root, { recursive: true, force: true }); } }); // C3 + C4 — an unexpected I/O error must reject (fail-closed, not partial) // AND remove the temp dir it created (cleanup on the error path). it("fails closed and removes the temp dir on an unexpected I/O error", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-stage-fail-")); try { const { home } = await buildFakeHome(root); let createdDir: string | null = null; const realMkdtemp = fs.mkdtemp.bind(fs); vi.spyOn(fs, "mkdtemp").mockImplementation(async (prefix: string, ...rest: unknown[]) => { const dir = await (realMkdtemp as typeof fs.mkdtemp)(prefix, ...(rest as [])); createdDir = dir as string; return dir; }); vi.spyOn(fs, "readFile").mockRejectedValue( Object.assign(new Error("boom"), { code: "EACCES" }), ); await expect(stageCodexHomeForSync(home, { runId: "run-fail" })).rejects.toThrow("boom"); expect(createdDir).not.toBeNull(); // The staged temp dir was cleaned up despite the failure. await expect(fs.access(createdDir as unknown as string)).rejects.toMatchObject({ code: "ENOENT" }); } finally { await fs.rm(root, { recursive: true, force: true }); } }); // Circular symlinks inside skills/ must be silently skipped (not throw ELOOP). // Skill symlinks that point OUTSIDE skills/ are intentional design (Paperclip // stores skill packages in a shared location) and are dereferenced normally; // all resulting files land 0600 inside the 0700 staged dir. it("skips circular skill symlinks (ELOOP) without throwing", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-stage-circular-")); let staged: string | null = null; try { const home = path.join(root, "codex-home"); await fs.mkdir(path.join(home, "skills"), { recursive: true }); // Self-referential symlink: would loop forever — must be skipped. const circularLink = path.join(home, "skills", "loop.md"); await fs.symlink(circularLink, circularLink); // A normal skill file — must still be staged. await fs.writeFile(path.join(home, "skills", "legit.md"), "# ok\n", "utf8"); staged = await stageCodexHomeForSync(home, { runId: "run-circular" }); const stagedSkillEntries = await fs.readdir(path.join(staged, "skills")); // Circular link must be absent. expect(stagedSkillEntries).not.toContain("loop.md"); // Normal skill file must be present. expect(stagedSkillEntries).toContain("legit.md"); } finally { if (staged) await fs.rm(staged, { recursive: true, force: true }); await fs.rm(root, { recursive: true, force: true }); } }); // Mode normalization: nested skill files must be staged 0600 regardless of // their source mode (0644 documents, 0755 scripts, etc.). it("writes nested skill files with mode 0600 regardless of source mode", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-stage-skill-mode-")); let staged: string | null = null; try { const home = path.join(root, "codex-home"); await fs.mkdir(path.join(home, "skills", "my-skill"), { recursive: true }); // Typical source modes: readable doc (0644) and executable script (0755); // both must land 0600 in the staged dir. await fs.writeFile(path.join(home, "skills", "my-skill", "SKILL.md"), "# skill\n", { mode: 0o644 }); await fs.writeFile(path.join(home, "skills", "my-skill", "run.sh"), "#!/bin/sh\n", { mode: 0o755 }); staged = await stageCodexHomeForSync(home, { runId: "run-skill-mode" }); for (const rel of ["my-skill/SKILL.md", "my-skill/run.sh"]) { const mode = (await fs.stat(path.join(staged, "skills", rel))).mode & 0o777; expect(mode, `skills/${rel} should be staged 0600`).toBe(0o600); } } finally { if (staged) await fs.rm(staged, { recursive: true, force: true }); await fs.rm(root, { recursive: true, force: true }); } }); // Finding 1 (Greptile): a directory symlink such as `back -> .` inside a skill // resolves to an ancestor directory (it does NOT raise ELOOP), so naive // recursion would traverse the same tree forever until disk/memory is // exhausted. Cycle detection must let staging finish while still copying the // real content and skipping the self-referential link. it("does not infinitely traverse an ancestor directory link (back -> .) inside a skill", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-stage-cycle-")); let staged: string | null = null; try { const home = path.join(root, "codex-home"); const skillDir = path.join(home, "skills", "my-skill"); await fs.mkdir(skillDir, { recursive: true }); await fs.writeFile(path.join(skillDir, "SKILL.md"), "# skill\n", "utf8"); // Directory symlink pointing at the skill's own dir — resolves to a // directory already on the active traversal path; must be skipped. await fs.symlink(".", path.join(skillDir, "back")); staged = await stageCodexHomeForSync(home, { runId: "run-cycle" }); // Completed without hanging; the real file is staged and the cyclic link // produced no runaway nested `back/back/…` chain (it is skipped entirely). expect(await fs.readdir(path.join(staged, "skills", "my-skill"))).toEqual(["SKILL.md"]); expect(await fs.readFile(path.join(staged, "skills", "my-skill", "SKILL.md"), "utf8")).toBe( "# skill\n", ); } finally { if (staged) await fs.rm(staged, { recursive: true, force: true }); await fs.rm(root, { recursive: true, force: true }); } }); // Finding 2 (Greptile): a symlink *inside* a skill that points to a host file // or directory OUTSIDE that skill (e.g. `~/.ssh/id_rsa`) must NOT be // dereferenced into the staged asset — otherwise a malformed/compromised skill // could smuggle host secrets past CODEX_SYNC_ALLOWLIST. it("does not stage a nested skill symlink that escapes the skill root", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-stage-escape-")); let staged: string | null = null; try { const home = path.join(root, "codex-home"); const skillDir = path.join(home, "skills", "my-skill"); await fs.mkdir(skillDir, { recursive: true }); await fs.writeFile(path.join(skillDir, "SKILL.md"), "# skill\n", "utf8"); // A host "secret" file and directory living OUTSIDE the skill dir. const secretFile = path.join(root, "host-secret.txt"); await fs.writeFile(secretFile, "TOP SECRET\n", "utf8"); const secretDir = path.join(root, "host-secret-dir"); await fs.mkdir(secretDir, { recursive: true }); await fs.writeFile(path.join(secretDir, "creds"), "creds\n", "utf8"); // Nested symlinks inside the skill escaping to those host paths. await fs.symlink(secretFile, path.join(skillDir, "stolen.txt")); await fs.symlink(secretDir, path.join(skillDir, "stolen-dir")); staged = await stageCodexHomeForSync(home, { runId: "run-escape" }); const stagedEntries = await fs.readdir(path.join(staged, "skills", "my-skill")); // The legit in-skill file is staged… expect(stagedEntries).toContain("SKILL.md"); // …but neither escaping link is followed into the staged asset. expect(stagedEntries).not.toContain("stolen.txt"); expect(stagedEntries).not.toContain("stolen-dir"); } finally { if (staged) await fs.rm(staged, { recursive: true, force: true }); await fs.rm(root, { recursive: true, force: true }); } }); // Defense-in-depth for finding 1: a degenerate top-level `skills/ -> ..` // link resolves to an ancestor of `skills/` (the home). It must be skipped, // never adopted as a containment root — otherwise the whole home // (`sessions/`, `*.sqlite`, …) would be dragged into the staged skills asset. it("skips a top-level skills entry that resolves to an ancestor of skills/", async () => { const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-codex-stage-ancestor-")); let staged: string | null = null; try { const home = path.join(root, "codex-home"); await fs.mkdir(path.join(home, "skills"), { recursive: true }); await fs.writeFile(path.join(home, "skills", "legit.md"), "# ok\n", "utf8"); // Runtime state in the home that must never reach the staged asset. await fs.writeFile(path.join(home, "logs.sqlite"), "x", "utf8"); // `up -> ..` resolves to the home dir (an ancestor of skills/). await fs.symlink("..", path.join(home, "skills", "up")); staged = await stageCodexHomeForSync(home, { runId: "run-ancestor" }); const stagedSkillEntries = await fs.readdir(path.join(staged, "skills")); expect(stagedSkillEntries).toContain("legit.md"); // The ancestor link is skipped, so the home's runtime state is not dragged in. expect(stagedSkillEntries).not.toContain("up"); } finally { if (staged) await fs.rm(staged, { recursive: true, force: true }); await fs.rm(root, { recursive: true, force: true }); } }); });