diff --git a/docs/adapters/claude-local.md b/docs/adapters/claude-local.md index c61b8ca604..76cb4b337b 100644 --- a/docs/adapters/claude-local.md +++ b/docs/adapters/claude-local.md @@ -8,7 +8,8 @@ The `claude_local` adapter runs Anthropic's Claude Code CLI locally. It supports ## Prerequisites - Claude Code CLI installed (`claude` command available) -- `ANTHROPIC_API_KEY` set in the environment or agent config +- Either `ANTHROPIC_API_KEY` in adapter env/host env, or a Claude Code + subscription login available to the execution target ## Configuration Fields @@ -69,6 +70,33 @@ On-call checklist if you see this in production: The adapter creates a temporary directory with symlinks to Paperclip skills and passes it via `--add-dir`. This makes skills discoverable without polluting the agent's working directory. +## Remote credential ownership + +`claude_local` uses a snapshot-owns-auth topology for managed sandbox execution +targets. When the run uses a sandbox execution target and no explicit +`CLAUDE_CONFIG_DIR` is configured, Paperclip creates a remote +`CLAUDE_CONFIG_DIR` under the run's Claude runtime directory. It uploads +sanitized host-side settings such as `settings.json` and `CLAUDE.md`, but the +managed seed does not upload host Claude credential files. + +After the seed is copied, the remote materialization command checks the +execution target's own `$HOME/.claude` directory. For each missing credential +file, it copies `.credentials.json` or `credentials.json` from that remote home +into the managed `CLAUDE_CONFIG_DIR`. That means credentials baked into the +sandbox image win for managed remote Claude runs. + +Worked example: a sandbox image contains `$HOME/.claude/.credentials.json` from +its own Claude Code login. Paperclip starts a managed remote `claude_local` run, +uploads only the sanitized config seed, and sets `CLAUDE_CONFIG_DIR` to the +remote runtime config path. Because the managed config has no credential file, +the adapter copies the sandbox image's `$HOME/.claude/.credentials.json` into +that path before invoking Claude. The sandbox snapshot owns the credential for +the run. + +This differs from [`codex_local`](/adapters/codex-local), where a +Paperclip-managed sandbox run uploads a host-owned `CODEX_HOME/auth.json` and +therefore shadows any Codex login already present inside the sandbox image. + For manual local CLI usage outside heartbeat runs (for example running as `claudecoder` directly), use: ```sh diff --git a/docs/adapters/codex-local.md b/docs/adapters/codex-local.md index e1bd06b17c..5a539f150e 100644 --- a/docs/adapters/codex-local.md +++ b/docs/adapters/codex-local.md @@ -8,7 +8,11 @@ The `codex_local` adapter runs OpenAI's Codex CLI locally. It supports session p ## Prerequisites - Codex CLI installed (`codex` command available) -- `OPENAI_API_KEY` set in the environment or agent config +- Either a host Codex login with `~/.codex/auth.json`, or a per-agent + `OPENAI_API_KEY` configured in adapter env (Paperclip materializes this into + `$CODEX_HOME/auth.json` for managed homes; Codex CLI reads credentials from + `auth.json`, not directly from the process environment — for a self-managed + external `CODEX_HOME`, write `auth.json` there directly instead) ## Configuration Fields @@ -55,6 +59,83 @@ A managed home is created empty, so the adapter must provision auth into it befo - **Genuine external overrides** (a `CODEX_HOME` outside the Paperclip-managed company tree) are treated as self-managed and are never seeded or overwritten. - **Fail-fast guard:** if a managed home ends up with no usable `auth.json` and no configured API key, the run fails with an explicit `adapter_failed` ("no Codex credentials provisioned for managed home …") rather than emitting an unauthenticated request. +### Auth ownership and precedence + +`codex_local` is host-owns-auth when Paperclip owns the effective +`CODEX_HOME`. The winning credential file is: + +1. **Per-agent API key:** when adapter env contains a non-empty + `OPENAI_API_KEY`, Paperclip writes `$CODEX_HOME/auth.json` with only + `{ "OPENAI_API_KEY": "..." }`. This overwrites any existing file or symlink + at that path. Codex CLI versions that Paperclip supports read the key from + `auth.json`, not directly from the process environment. +2. **Host ChatGPT-subscription login:** when no per-agent key is configured, + Paperclip symlinks `auth.json` from the shared host Codex home into the + managed home. The symlink keeps rotating/single-use refresh tokens live + instead of copying a stale token into the managed home. +3. **External `CODEX_HOME`:** if adapter env points `CODEX_HOME` outside the + Paperclip-managed company tree, that home is self-managed. Paperclip does + not seed or overwrite it, so its own `auth.json` wins. + +For sandbox or SSH execution, Paperclip uploads the effective managed +`CODEX_HOME` and launches Codex with `CODEX_HOME` pointing at that uploaded +directory. Any `auth.json` already baked into the sandbox image is shadowed in +managed-home mode. If the host has no usable `auth.json` and no per-agent +`OPENAI_API_KEY`, the managed run fails fast instead of falling back to an +in-sandbox login. + +Worked example: a worker runs in a sandbox image that already has +`$HOME/.codex/auth.json`, and the Paperclip host is logged in with a ChatGPT +subscription. For a managed `codex_local` agent, Paperclip symlinks the host +`auth.json` into the agent's managed home, uploads that home to the sandbox, +and sets `CODEX_HOME` to the uploaded path. Codex reads the host-owned +uploaded file, so the sandbox image login does not win. + +For high-concurrency sandbox fleets, prefer a per-agent `OPENAI_API_KEY` over a +shared ChatGPT-subscription login. API-key mode produces a standalone +`auth.json` for each managed home and avoids many concurrent sandboxes sharing +one rotating subscription credential. The tradeoff is billing: API-key mode is +metered per token through the OpenAI API, while ChatGPT-subscription auth uses +the subscription's flat-plan economics and quota behavior. Pick the mode +deliberately for the fleet's cost and concurrency profile. + +### Deferred config-validation warning spec + +This section specifies a warning that is not implemented yet. The warning should +help operators notice the host-owns-auth topology before they run a sandbox +fleet with ChatGPT-subscription credentials. + +- **Source fields:** resolved Codex auth mode (`api` vs `subscription`) and + execution target kind (`local`, `remote:ssh`, or `remote:sandbox`). Infer the + auth mode from the final managed `$CODEX_HOME/auth.json` shape after seeding: + `{ "OPENAI_API_KEY": ... }` means API-key mode; subscription-token-shaped + host auth, including the symlinked host file, means ChatGPT-subscription + mode. Use top-level shape only; do not read credential values into the + warning. The target kind comes from `AdapterExecutionTarget`. +- **Transformations:** during config preparation or home seeding for a run, + classify `(subscription auth mode AND remote/sandbox execution target)` as a + warning condition. This is classification only. Do not read credential values + into the warning. +- **Sink fields:** emit a warning log line through `onLog("stderr", ...)` and, + when the config-validation surface supports it, a surfaced validation warning. + The sink may include only the auth-mode label and target type. +- **Retention:** warning text may appear in ephemeral run logs or validation + results. Do not persist auth material. +- **Attacker-observable IDs:** none new. The warning must not print token + values, email addresses, or `auth.json` contents. +- **Hook point:** wire the check at the `codex_local` execute/config-prep path + around managed-home seeding: `execute.ts` already has `executionTarget` / + target transport in scope, calls `seedManagedCodexHome`, and calls + `evaluateCodexCredentialReadiness` before uploading `CODEX_HOME`. If the + warning is factored into `codex-home.ts`, pass the resolved target + classification in rather than making `codex-home.ts` inspect execution + targets on its own. + +Because the warning touches authentication behavior, implementation must go +through the security-review gate. Treat this docs section as the follow-up +implementation spec, not as authorization to add the warning in a docs-only +change. + ## Manual Local CLI For manual local CLI usage outside heartbeat runs (for example running as `codexcoder` directly), use: diff --git a/docs/adapters/overview.md b/docs/adapters/overview.md index ac153d2937..739c1c18ed 100644 --- a/docs/adapters/overview.md +++ b/docs/adapters/overview.md @@ -31,6 +31,28 @@ When a heartbeat fires, Paperclip: | [Process](/adapters/process) | `process` | Executes arbitrary shell commands | | [HTTP](/adapters/http) | `http` | Sends webhooks to external agents | +## Credential ownership for sandbox targets + +Local CLI adapters can run on the Paperclip host, SSH targets, or managed +sandbox targets. The adapter decides which credential home is authoritative +before the CLI starts: + +| Adapter | Credential topology | Which credential file wins on managed sandbox targets | +|---------|---------------------|-------------------------------------------------------| +| [`codex_local`](/adapters/codex-local) | Host-owns-auth for Paperclip-managed `CODEX_HOME` | A host-owned `auth.json` is symlinked into the managed `CODEX_HOME` and uploaded to the sandbox. If a per-agent `OPENAI_API_KEY` is configured, Paperclip writes an API-key `auth.json` instead and that file wins. A login baked into the sandbox image is shadowed because Codex runs with Paperclip's uploaded `CODEX_HOME`. | +| [`claude_local`](/adapters/claude-local) | Snapshot-owns-auth for managed remote Claude config | Paperclip uploads only sanitized settings and skill/runtime assets. When the remote managed config has no Claude credential files, it copies `.credentials.json` or `credentials.json` from the sandbox image's own `$HOME/.claude`, so the image's login wins. | + +Worked examples: + +- **Codex sandbox with host ChatGPT login:** the host `~/.codex/auth.json` + is symlinked into the managed home, then uploaded as the sandbox + `CODEX_HOME`. Codex reads that uploaded file and does not use any + `auth.json` already present inside the sandbox image. +- **Claude sandbox with image login:** Paperclip materializes a remote + `CLAUDE_CONFIG_DIR`, then fills missing `.credentials.json` / + `credentials.json` from the sandbox image's own `$HOME/.claude`. The + snapshot's Claude login is the credential source for the run. + ### Hermes local vs gateway Use `hermes_local` when Paperclip should start the local `hermes` CLI on the