Files
PaperClipAI/packages/adapters/codex-local/CODEX-AUTH-CACHE.md
Nicky LeachandPaperclip 7e592c33fa feat(codex): add identity-keyed host credential cache (#10853)
## 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>
2026-08-04 16:33:54 -07:00

102 lines
5.2 KiB
Markdown

# 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_id` of 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`.