paperclip/packages/adapters/codex-local/src/server/codex-home.test.ts

1413 lines
59 KiB
TypeScript

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/<x> -> ..`
// 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 });
}
});
});