## Thinking Path > - Paperclip manages agent work as issues and pull requests. > - This change lives in the Codex local adapter path. > - The host auth flow needs a stable cache per identity. > - The cache must not change the copy-back path or the default store overwrite. > - This pull request adds that cache and keeps the existing flow intact. > - The benefit is repeatable host auth with safer identity scoping. ## Linked Issues or Issue Description **Subsystem affected** - packages/adapters: Codex local adapter and host auth flow **Problem or motivation** - The host auth flow needs one usable credential per identity. - The current flow does not keep that identity state in a separate cache. **Proposed solution** - Add an identity-keyed host credential cache. - Keep the cache company scoped. - Add an opt-in seed mode for the merge decision helper. - Write the cache at copy-back time without changing the default store overwrite. **Alternatives considered** - Store the cache in the instance-global root. - Seed the host default store through an environment flag. - Both choices weaken isolation or caller control, so I did not use them. **Roadmap alignment** - I checked `ROADMAP.md`. - I found no direct overlap with an active roadmap item. - The change fits the core auth and secrets direction. **Additional context** - Local tests passed before I opened this pull request. - I found no direct duplicate pull request for this branch. ## What Changed - Added `codex-auth-cache.ts` for company scoped cache storage and identity anchored vending. - Added `codex-auth-merge-decision.cjs` support for a leading `--seed-if-dest-absent` flag. - Updated `codex-auth-copyback.ts` to write the cache at teardown under the merge lock. - Wired `execute.ts` to use the identity anchored vend before the managed home seed step. - Added `CODEX-AUTH-CACHE.md` for the cache rules, directions, state matrix, off switch, and clear action. - Added and updated tests for cache behavior, merge decision flow, and copy-back flow. ## Verification - `pnpm --filter @paperclipai/adapter-codex-local exec vitest run`. - `node_modules/.bin/vitest run --project @paperclipai/adapter-utils workspace-restore-merge`. - `tsc --noEmit` in `packages/adapters/codex-local`. - `git log --oneline origin/master..origin/feat/codex-auth-identity-cache` shows the expected commits. ## Risks - This change touches credential storage. - A mistake in the cache path could break host auth for one identity. - The tests reduce this risk, but the surface stays sensitive. ## Model Used - OpenAI Codex, GPT-5, tool use enabled. ## 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 linked existing issues with `Fixes:` / `Closes:` / `Refs:` or described the issue in the pull request body - [x] I have not referenced internal or instance-local Paperclip issues or links - [x] My branch name describes the change 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>
5.2 KiB
Codex identity-keyed host credential cache
This document describes the host credential cache for Codex authentication. The
cache keeps one usable subscription credential per identity (account_id) in a
separate host store. The cache is additive. It does not change the copy-back
path, the fail-closed decision predicate, or the host default store overwrite.
Where the cache lives
The cache root is company-scoped, under the same isolation boundary as the managed Codex home:
<instanceRoot>/companies/<companyId>/codex-auth-cache/<account_id>/auth.json
The root sits outside the shared Codex home (resolveSharedCodexHomeDir) and
outside the symlink allowlist. The account_id is sanitized to one safe path
segment. Each cache root and each identity directory is private (mode 0700).
The two directions
The board set one rule for both directions. A side that has no credential can receive one, but only when a real credential on one side names the expected identity. The harness never picks a credential from the cache at random.
- Host to sandbox (inbound). A sandbox that starts with no Codex credential takes the host credential. The cache does not change this. At provision the cache also refreshes the host credential with a strictly-newer cached copy of the same identity (the vend, below).
- Sandbox to host (copy-back). A host that already holds an identity keeps a strictly-newer same-identity credential from the sandbox. The cache does not change this. At teardown the cache also writes the sandbox credential into its per-identity slot (the cache write, below).
The identity anchor rule
The identity anchor rule is the load-bearing constraint:
- The cache vend only refreshes an identity the host already holds in the
shared home. It replaces the staged host credential with a strictly-newer
cached credential of the same
account_id. It never introduces a new identity. - When the host shared home holds no credential, the vend does nothing. The harness never selects a cache entry to seed an empty host. This is the "no random pick from the cache when the host is empty" rule.
- The cache write keys each entry by the real
account_idof the credential that flows back from the sandbox. It writes to a per-identity cache slot, never to the host default store. The cache write is best-effort: it runs after the host copy-back finishes, so a cache-write failure never replaces the successful copy-back result. The failure is logged with its errno code and the next teardown re-attempts the write.
The host never learns an identity from the cache. The host only refreshes an identity a real credential already states.
State matrix
"Host has auth" means the shared source store resolveSharedCodexHomeDir(env)/auth.json
holds a usable subscription credential. "Sandbox has auth" means the run's
sandbox auth.json holds a usable credential at teardown. X and Y are two
different subscription identities (account_id).
| # | Host store | Sandbox cred | Inbound: sandbox home gets | Copy-back: host store | Cache write | Cache vend | Identity anchor |
|---|---|---|---|---|---|---|---|
| 1a | HAS X |
HAS X, newer |
fresher of the two (X) |
overwrite with newer X |
write slot X |
may stage a strictly-newer cached X |
host and sandbox both name X |
| 1b | HAS X |
HAS Y (Y != X) |
host X (predicate rejects Y) |
keep host X |
write slot Y (per identity) |
may stage a strictly-newer cached X |
host names X; Y is cached, never adopted |
| 2 | HAS X |
NONE | host X |
keep host (sandbox absent) | no write (no source) | may stage a strictly-newer cached X |
host names X |
| 3 | NONE | HAS Y |
image-login fallback; host store never seeded | keep host empty (never seed) | write slot Y |
none (host empty, no random pick) | sandbox names Y; host is silent |
| 4 | NONE | NONE | image-login fallback, or the run fails | keep host empty | no write | none | no side names an identity |
The cache changes an outcome only in the "stage a strictly-newer cached copy of an identity the host already holds" cases (rows 1a, 1b, 2, vend column). It never changes which identity a side uses. It never seeds an empty host store (rows 3, 4).
Off-switch
The cache is on by default. Set the environment flag PAPERCLIP_CODEX_AUTH_CACHE
to an explicit falsy value (0, false, no, or off) to turn it off. When
off, the teardown cache write and the provision vend become no-ops. The host
default overwrite is unchanged in both states.
Cache-clear action
Two operator actions remove cached credentials:
clearCodexAuthCacheEntry(env, accountId, companyId)removes exactly one identity slot.clearCodexAuthCache(env, companyId)removes every slot in the company-scoped cache root.
To disable the cache without a code revert, use the off-switch. To remove a
single cached identity, use clearCodexAuthCacheEntry. To remove every cached
credential, delete the cache root or use clearCodexAuthCache. Neither action
affects the host default store.
No secret in logs
The cache logs the decision and the outcome only. It never logs token bytes and
never logs a raw account_id.