From 19e938415ad702688dab40e7f5dec1b71199e518 Mon Sep 17 00:00:00 2001 From: Nicky Leach Date: Tue, 10 Nov 2026 10:57:03 -0800 Subject: [PATCH] fix(runtime): omit Codex refresh handoffs from backups (#20614) ## Thinking Path > - Managed ACPX Codex retains its final refresh after provider exit so Core can save it. > - Shutdown saves a checkpoint before Core removes the live credential handoff. > - Checkpoint profiles must exclude the handoff while retaining resumable session history. ## Linked Issues or Issue Description Follow-up to #20580 and its credential-backup review finding. Refs #20457. ## What Changed - Exclude `auth-refresh.json` and `auth-refresh.json.tmp` from the shared Codex checkpoint profile, covering native and ACPX homes. - Extend real checkpoint-copy coverage to ACPX Codex, both handoff filenames, and native Codex. Keep the live files and session history intact. ## Verification - A real ACPX checkpoint-copy test failed before the exclusion fix; Grok passed. - Five targeted real-copy/profile tests pass, including after applying the change to current master. - `git diff --check` and local secret/PII diff review pass. - Full regression coverage runs in GitHub CI at the maintainer's request. No live provider credentials were used. ## Risks - Two entries added to an existing exclusion list; no runtime lifecycle, schema, persisted identity, or session format changes. Saved sessions remain readable. Existing backup restoration uses the same exclusion profile. - Provider credential cleanup still follows final writeback; excluding a backup entry does not delete the live handoff. ## Model Used - OpenAI GPT-6 through Codex, with tool-assisted implementation and verification. The runtime does not expose a more specific model ID or context-window size. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] All Paperclip CI gates are green - [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge Co-authored-by: Paperclip --- .../native-session-executor.test.ts | 19 +++++++++++++------ .../native-runtime/native-session-executor.ts | 2 ++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/server/src/services/native-runtime/native-session-executor.test.ts b/server/src/services/native-runtime/native-session-executor.test.ts index 299495dd1a8..b7fc8561436 100644 --- a/server/src/services/native-runtime/native-session-executor.test.ts +++ b/server/src/services/native-runtime/native-session-executor.test.ts @@ -3163,6 +3163,8 @@ describe("native harness persistence profiles", () => { ".tmp", "auth.json", "config.toml", + "auth-refresh.json", + "auth-refresh.json.tmp", ".remote-plugin-install-staging", "cache/remote_plugin_catalog", "plugins/.remote-plugin-install-staging", @@ -3209,6 +3211,8 @@ describe("native harness persistence profiles", () => { `acpx/${sessionDirectory}/codex-home/.tmp`, `acpx/${sessionDirectory}/codex-home/auth.json`, `acpx/${sessionDirectory}/codex-home/config.toml`, + `acpx/${sessionDirectory}/codex-home/auth-refresh.json`, + `acpx/${sessionDirectory}/codex-home/auth-refresh.json.tmp`, `acpx/${sessionDirectory}/codex-home/.remote-plugin-install-staging`, `acpx/${sessionDirectory}/codex-home/cache/remote_plugin_catalog`, `acpx/${sessionDirectory}/codex-home/plugins/.remote-plugin-install-staging`, @@ -3920,22 +3924,23 @@ describe("split durable provider checkpoint identity", () => { }); describe("remote provider checkpoint snapshots", () => { - it("excludes Grok credentials and diagnostic logs from real checkpoint copies while retaining sessions", async () => { - const root = await mkdtemp(join(tmpdir(), "grok-checkpoint-")); + it.each(["codex", "grok"] as const)("excludes %s credentials and handoffs from real checkpoint copies while retaining sessions", async provider => { + const root = await mkdtemp(join(tmpdir(), `${provider}-checkpoint-`)); try { const source = join(root, "source"); const target = join(root, "backup"); const sessionDirectory = acpxRuntimeSessionDirectoryName("session"); - const relativeHome = join("acpx", sessionDirectory, "grok-home"); + const relativeHome = join("acpx", sessionDirectory, `${provider}-home`); const home = join(source, relativeHome); await mkdir(join(home, "logs"), { recursive: true }); await mkdir(join(home, "sessions")); await writeFile(join(home, "logs", "unified.jsonl"), "AUTH_SENTINEL"); await writeFile(join(home, "auth.json"), "AUTH_SENTINEL"); await writeFile(join(home, "auth-refresh.json"), "AUTH_SENTINEL"); + await writeFile(join(home, "auth-refresh.json.tmp"), "AUTH_SENTINEL"); await writeFile(join(home, "sessions", "session.json"), "resume-state"); const profile = resolveNativeHarnessPersistenceProfile({ - provider: { kind: "acpx", agent: "grok" }, + provider: { kind: "acpx", agent: provider }, session: { normalizedSessionId: "session", driverKind: "acpx_runtime" }, } as unknown as NativeExecutionInputV1); const execute = async ({ command, args }: { command: string; args: string[] }) => ({ @@ -3948,7 +3953,7 @@ describe("remote provider checkpoint snapshots", () => { await syncRemoteRunnerDirectoryOut({ runner: { execute, syncOut } as never, sourcePath: source, targetPath: target, mode: 0o700, excludeEntries: profile.directories.find(directory => directory.name === "acpx")!.excludeEntries }); - for (const entry of ["auth.json", "auth-refresh.json", "logs"]) + for (const entry of ["auth.json", "auth-refresh.json", "auth-refresh.json.tmp", ...(provider === "grok" ? ["logs"] : [])]) await expect(access(join(target, relativeHome, entry))).rejects.toMatchObject({ code: "ENOENT" }); expect(await readFile(join(target, relativeHome, "sessions", "session.json"), "utf8")).toBe("resume-state"); expect(await readFile(join(home, "logs", "unified.jsonl"), "utf8")).toBe("AUTH_SENTINEL"); @@ -4054,6 +4059,8 @@ describe("remote provider checkpoint snapshots", () => { const source = join(root, "codex-home"); const target = join(root, "backup"); const files: Record = { + "auth-refresh.json": "AUTH_SENTINEL", + "auth-refresh.json.tmp": "AUTH_SENTINEL", "sessions/2026/10/20/rollout.jsonl": "resume-state", "state_5.sqlite": "state", "cache/codex_apps_tools/tools.json": "apps", @@ -4086,7 +4093,7 @@ describe("remote provider checkpoint snapshots", () => { for (const kept of ["sessions/2026/10/20/rollout.jsonl", "state_5.sqlite", "cache/codex_apps_tools/tools.json", "plugins/data/sites/data.json", "plugins/cache/openai-bundled/browser/1.0.0/plugin.json"]) expect(await readFile(join(target, kept), "utf8")).toBe(files[kept]); - for (const omitted of ["cache/remote_plugin_catalog", "plugins/cache/openai-curated-remote", + for (const omitted of ["auth-refresh.json", "auth-refresh.json.tmp", "cache/remote_plugin_catalog", "plugins/cache/openai-curated-remote", "plugins/cache/workspace-shared-with-me", "plugins/.remote-plugin-install-staging", ".remote-plugin-install-staging"]) await expect(access(join(target, omitted))).rejects.toMatchObject({ code: "ENOENT" }); expect(await readFile(join(source, "plugins/cache/openai-curated-remote/sites/1.0.2/plugin.json"), "utf8")).toBe("remote"); diff --git a/server/src/services/native-runtime/native-session-executor.ts b/server/src/services/native-runtime/native-session-executor.ts index f8c429a6245..18a02d097b5 100644 --- a/server/src/services/native-runtime/native-session-executor.ts +++ b/server/src/services/native-runtime/native-session-executor.ts @@ -558,6 +558,8 @@ const CODEX_HOME_REDOWNLOADABLE_ENTRIES = [ ] as const; const CODEX_HOME_CHECKPOINT_EXCLUDED_ENTRIES = [ ...CODEX_HOME_NON_PERSISTENT_ENTRIES, + "auth-refresh.json", + "auth-refresh.json.tmp", ...CODEX_HOME_REDOWNLOADABLE_ENTRIES, ] as const; const OPENCODE_RUNTIME_CHECKPOINT_EXCLUDED_ENTRIES = [