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 <noreply@paperclip.ing>
This commit is contained in:
Nicky LeachandPaperclip authored and GitHub committed 2026-11-10 18:57:03 +00:00
1 parent 86cc3e3a11
commit 19e938415a
2 files changed
+15 -6

No files matched your search

@@ -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<string, string> = {
"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");
@@ -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 = [