# PRD 0029: Provider auth credentials through egress

- **Status:** Active
- **Author:** didericis-codex
- **Created:** 2026-05-29
- **Issue:** #109

## Summary

Allow provider bottles to inject host credentials into the egress
sidecar without exposing them to the agent. Codex uses
`agent_provider.forward_host_credentials` for ChatGPT/device-login
access tokens. Claude uses `agent_provider.auth_token` to name the host
env var holding its OAuth token, which egress injects on
`api.anthropic.com` requests.

## Problem

Codex bottles can reach OpenAI hosts after they are added to egress, but
requests to Codex's ChatGPT-backed API endpoints still fail with HTTP
403 when the egress route is unauthenticated. The egress proxy strips
agent-originated `Authorization` headers and only re-injects auth for
routes that declare an egress-owned token. Bare `api.openai.com` or
`chatgpt.com` routes therefore forward Codex requests without the
ChatGPT bearer token.

Copying the host `~/.codex/auth.json` into the agent would solve auth
mode detection but would also put access and refresh material inside the
agent sandbox. That cuts against bot-bottle's credential minimization
model: provider credentials should live in the sidecar boundary when
possible, not in the agent.

## Goals / Success Criteria

- A Codex bottle with host ChatGPT auth can call Codex's
  `api.openai.com` and `chatgpt.com` endpoints through egress.
- Host credential forwarding happens only when the bottle declares
  `agent_provider.forward_host_credentials: true`.
- The agent container does not receive `OPENAI_API_KEY`,
  `CODEX_ACCESS_TOKEN`, or real `tokens.access_token` /
  `tokens.refresh_token` values.
- The agent container receives only a dummy Codex `auth.json` that
  preserves the host auth-mode shape, keeps the selected ChatGPT
  account id, and replaces credential values with placeholders.
- Egress route files remain non-secret: they contain only host/path/auth
  slot metadata, never token values.
- Missing, API-key, malformed, or expired host Codex auth fails
  launch with a clear operator-facing message.
- Existing Claude OAuth placeholder behavior remains unchanged.

## Non-goals

- Refreshing Codex tokens in the sidecar. The first cut reads the host's
  current access token at launch; operators can restart after host Codex
  refreshes auth.
- Copying host `~/.codex/auth.json` credentials into the agent.
- Allowing arbitrary host credential forwarding beyond the two providers
  covered here (Codex ChatGPT/device-login and Claude OAuth).
- Hot-applying new authenticated Codex routes to an existing running
  sidecar. The current hot-apply path cannot safely populate new token
  env slots in an already-running container.

## Scope

### In scope

- Add `agent_provider.forward_host_credentials` to the bottle manifest
  schema, defaulting to `false`.
- Support the flag for `agent_provider.template: codex`.
- Add `agent_provider.auth_token` to the bottle manifest schema.
- Support the field for `agent_provider.template: claude`: the named
  host env var is forwarded only into the egress sidecar as the Bearer
  token for `api.anthropic.com`, and a placeholder
  `CLAUDE_CODE_OAUTH_TOKEN` is set in the agent so the Claude Code CLI
  starts without a real credential.
- Remove the `claude_code_oauth` egress route role, which previously
  required operators to declare the OAuth route manually. The provisioner
  now injects it from `auth_token`.
- Read host Codex auth from `$CODEX_HOME/auth.json` when `CODEX_HOME` is
  set, otherwise from `~/.codex/auth.json`.
- Extract only `tokens.access_token` for egress injection.
- Generate a dummy agent-side `auth.json` from the host auth file's
  mode and key shape, without copying real token values.
- Validate that host auth is not API-key mode and the access token is
  present, JWT-shaped, and not expired.
- Add or upgrade `api.openai.com` and `chatgpt.com` egress routes to
  inject that access token via a shared `EGRESS_TOKEN_N` sidecar env
  slot.
- Pass the extracted token only into the sidecar compose/run
  environment, alongside other egress token values.

### Out of scope

- Sidecar-owned refresh using `tokens.refresh_token`.
- Sharing full Codex auth state with the agent.
- Supporting host credential forwarding for non-Codex providers.

## Design

### Manifest

Extend `agent_provider`:

```yaml
agent_provider:
  template: codex
  forward_host_credentials: true
```

The field defaults to `false`. If set on a non-Codex provider, manifest
validation should reject it until that provider has a concrete,
credential-minimizing implementation.

### Host auth extraction

At prepare/launch time, when the flag is enabled for Codex:

1. Resolve the host Codex home directory from `$CODEX_HOME`, falling
   back to `~/.codex`.
2. Parse `auth.json`.
3. Require user/device auth mode rather than API-key auth.
4. Require a non-empty `tokens.access_token`.
5. Parse the JWT payload enough to require an `exp` claim in the future.
6. Return only the access token value to the launch path.

Errors should name the missing or invalid condition and point the
operator at `codex login --device-auth`, without printing token values.

### Egress route

When forwarding host Codex credentials, the effective egress route table
should contain authenticated `api.openai.com` and `chatgpt.com` routes.
If the bottle already declares either host as a bare-pass route, upgrade
it in the effective route table rather than requiring a duplicate
manifest entry. If the bottle already declares an authenticated route for
either host, fail rather than guessing whether to override
operator-provided auth, unless that route already uses the synthetic
Codex host credential token reference.

The rendered route should look like any other egress-owned auth route:

```yaml
routes:
  - host: "api.openai.com"
    auth_scheme: "Bearer"
    token_env: "EGRESS_TOKEN_N"
  - host: "chatgpt.com"
    auth_scheme: "Bearer"
    token_env: "EGRESS_TOKEN_N"
```

The access token value is supplied through the sidecar process
environment for that `EGRESS_TOKEN_N` slot. It must not be written to
`routes.yaml`, compose files, env files, logs, or user-facing output.

### Data flow

```mermaid
flowchart LR
    H["Host ~/.codex/auth.json"] --> L["bot-bottle launch"]
    L -->|access token only| S["egress sidecar env"]
    L -->|dummy auth.json only| A
    A["Codex agent"] -->|HTTPS via proxy, auth stripped| E["egress"]
    E -->|Bearer injected from env| C["api.openai.com / chatgpt.com"]
```

## Implementation chunks

1. **PRD first.** Land this document as the first commit on the feature
   branch.
2. **Manifest schema.** Add `forward_host_credentials`, validation, and
   unit tests.
3. **Host Codex auth reader.** Add a small stdlib-only helper for
   parsing and validating host Codex auth without printing values.
4. **Effective egress route.** Add/upgrade the Codex API routes when the
   flag is enabled, and add tests for bare route upgrade,
   missing-route insertion, and authenticated-route conflict.
5. **Agent auth marker.** Provision a dummy Codex `auth.json` into the
   agent home so Codex selects the host's user/device auth branch while
   real credentials stay in egress.
6. **Launch wiring.** Pass the host access token into the egress sidecar
   env for Docker and smolmachines without exposing it to the agent.
7. **Docs and tests.** Update README examples and run the unit suite.

## Open questions

- Should a later version support sidecar refresh using the host refresh
  token, or should restart-on-expiry remain the policy?
- Should telemetry hosts such as `ab.chatgpt.com` stay blocked by
  default even when Codex ChatGPT auth is enabled?

## References

- Gitea issue #109: Codex ChatGPT auth should inject host access token
  via egress.
- PRD 0017: Egress-proxy — universal MITM with path filtering + auth
  injection.
- PRD 0026: Agent provider templates.
