Codex CLI Pro 20x runtime: durable host auth via #22 secret ingestion #26

Open
opened 2026-08-29 17:38:14 +09:00 by floot · 0 comments
Owner

Floot on Tokyo should get a Codex CLI runtime billed to ChatGPT Pro (20x), cloned from claude-cli. The login must go through the secret-ingestion path from #22 / #25 — not through a Floot transcript, and not through a secrets.env value that is re-applied on every daemon start.

#22 (requestSecret / writeSecret, PR 25, not yet deployed) is the right ingestion UI. It is the wrong lifetime for ChatGPT-managed Codex auth.

Two different secrets

Session secret (#22 as shipped) Codex Pro auth.json
Who uses it One Floot guest, one task Every codex-cli sandbox on the box
Shape bearer token or basic password JSON bundle: access + refresh + account id
Lifetime write-once; material is gone after daemon restart must survive restart and be rewritten on refresh
Who may see bytes nobody in the transcript Codex CLI only, via CODEX_HOME
Official rule n/a seed once; persist the refreshed file; do not re-seed from the original blob

writeSecret today only unwraps token or password and writes that string. A Codex bundle is a file, and Codex must write it back. A bearer cap cannot do that.

Claude already has the sidecar pattern: setup-hosted copies ENDO_CLAUDE_OAUTH_TOKEN into ~/.endo-claude-credentials/*.key because in-memory git credentials do not survive restart. Codex needs the same idea, plus writeback. Official ChatGPT-managed auth is ~/.codex/auth.json with cli_auth_credentials_store = "file"; Codex refreshes that file in place. Do not call the OAuth token endpoint ourselves.

How to wire them

  1. Deploy #22 first. Codex CLI must speak the same MCP tools as Claude (requestSecret, writeSecret). Otherwise a Codex session will ask the operator to paste into chat again — the #22 failure mode.

  2. Do not put auth.json in secrets.env as a reload-every-start value. That is how Claude's long-lived subscription token works. Doing it for Codex throws away refreshes. Durable path is /var/lib/endo/codex-home/auth.json (0600, endo:endo), seeded once.

  3. Add one credential kind #22 does not have: codexAuth (or a generic file). Mint it the same way as bearer:

    • Floot UI paste box, or a Chat inbox form with secret: true + asCredential + secretKind: 'codexAuth'
    • Host provideCodexAuthCredential(name, { audience, json })
    • Persist to the sidecar if and only if no file exists yet
    • Receipt to the model: { petName, kind: 'codexAuth', audience, byteLength } — never the JSON

    That reuses the #22 UI and the form-field opt-in. It does not reuse provideBearerCredential.

  4. Give the provisioner the cap, not the session guest. setup-hosted should hold codex-creds and hand each codex-client-<sessionId> a mount of CODEX_HOME, not a copy of the bytes in tool args. Sessions share one Pro 20x login. A session that called requestSecret and stored the cap under its own petname would vanish with that session and could not refresh for the next one.

  5. Refresh writes back into the sidecar, not into a Floot cap. Bind /var/lib/endo/codex-home read-write into the container. Do not have writeSecret overwrite the host file from a stale cap after a session.

  6. Keep #22 for everything else the Codex agent needs. Platform API keys, git tokens, service-account JSON: requestSecret → cap → writeSecret onto the session workspace. Same prompts: never paste in chat, never put in exec.

What not to do

  • Operator pastes auth.json into a Codex turn and the agent writeSecrets it into the workspace. That is a session-scoped copy; refresh is lost; the next session is blind.
  • Seed ENDO_CODEX_AUTH_JSON from secrets.env on every endo-daemon start. That resets last_refresh.
  • Teach the model the refresh token. The receipt must stay metadata-only.
  • Share one auth.json across Tokyo and a laptop. Official rule is one file per machine; concurrent refresh steals the token.
  • Point Floot at an unofficial ChatGPT web endpoint. The supported subscription path is Codex CLI signed in with ChatGPT.

Work to do

Endo

  • @endo/codex-sandbox cloned from @endo/claude-sandbox (factory, 9P, per-session client, hosted provisioner).
  • OCI image with Codex CLI (localhost/codex:latest).
  • Floot runtime id codex-cli next to claude-cli. Leave claude-cli as the default until Codex is proven.
  • codexAuth credential kind + durable sidecar + CODEX_HOME bind.
  • MCP secret tools on the Codex runtime (same as Claude after #22).
  • Daemon allowEnvPass should keep CODEX_* the way it already keeps CLAUDE_*.

Host (floot/nixos-config)

  • services.endo.codexSandbox.enable (Claude stays on; they coexist).
  • endo-codex-image.service, /var/lib/endo/codex-home, /var/lib/endo/codex-mounts.
  • ENDO_EXTRA: setup-host / setup-peer / setup-hosted for the new package.
  • Document seed-once. Do not re-apply auth.json from secrets.env on daemon start.

Operator (cannot be automated)

  • codex login or codex login --device-auth on a trusted machine with the Pro account.
  • Confirm auth_mode is chatgpt and a refresh token is present (metadata only; do not paste the file into chat, Forgejo, or the Nix store).
  • Copy onto Tokyo as /var/lib/endo/codex-home/auth.json, or complete device-auth as endo after the image exists.

Sequence

  1. Land #22 / PR 25 on Tokyo so the paste box exists.
  2. Codex package: codexAuth + sidecar + CODEX_HOME bind + MCP secret tools.
  3. Hosted setup: inbox form or one-shot requestSecret to the factory / machine-admin, then persist and forget the paste.
  4. Operator seeds auth.json once; after that the file on disk is the source of truth.
  5. One test session: confirm billing is ChatGPT Pro, not platform API.

The subscription login is host infrastructure that happens to use #22's ingestion UI. Per-task secrets stay on the #22 path unchanged.

Related: #22, #25

Floot on Tokyo should get a Codex CLI runtime billed to ChatGPT Pro (20x), cloned from `claude-cli`. The login must go through the secret-ingestion path from #22 / https://git.goooooo.ooo/floot/endo/pulls/25 — not through a Floot transcript, and not through a `secrets.env` value that is re-applied on every daemon start. #22 (`requestSecret` / `writeSecret`, PR 25, not yet deployed) is the right ingestion UI. It is the wrong lifetime for ChatGPT-managed Codex auth. ## Two different secrets | | Session secret (#22 as shipped) | Codex Pro `auth.json` | | --- | --- | --- | | Who uses it | One Floot guest, one task | Every `codex-cli` sandbox on the box | | Shape | bearer token or basic password | JSON bundle: access + refresh + account id | | Lifetime | write-once; material is gone after daemon restart | must survive restart and be rewritten on refresh | | Who may see bytes | nobody in the transcript | Codex CLI only, via `CODEX_HOME` | | Official rule | n/a | seed once; persist the refreshed file; do not re-seed from the original blob | `writeSecret` today only unwraps `token` or `password` and writes that string. A Codex bundle is a file, and Codex must write it back. A bearer cap cannot do that. Claude already has the sidecar pattern: `setup-hosted` copies `ENDO_CLAUDE_OAUTH_TOKEN` into `~/.endo-claude-credentials/*.key` because in-memory git credentials do not survive restart. Codex needs the same idea, plus writeback. Official ChatGPT-managed auth is `~/.codex/auth.json` with `cli_auth_credentials_store = "file"`; Codex refreshes that file in place. Do not call the OAuth token endpoint ourselves. ## How to wire them 1. **Deploy #22 first.** Codex CLI must speak the same MCP tools as Claude (`requestSecret`, `writeSecret`). Otherwise a Codex session will ask the operator to paste into chat again — the #22 failure mode. 2. **Do not put `auth.json` in `secrets.env` as a reload-every-start value.** That is how Claude's long-lived subscription token works. Doing it for Codex throws away refreshes. Durable path is `/var/lib/endo/codex-home/auth.json` (`0600`, `endo:endo`), seeded once. 3. **Add one credential kind #22 does not have: `codexAuth` (or a generic `file`).** Mint it the same way as bearer: - Floot UI paste box, or a Chat inbox form with `secret: true` + `asCredential` + `secretKind: 'codexAuth'` - Host `provideCodexAuthCredential(name, { audience, json })` - Persist to the sidecar **if and only if** no file exists yet - Receipt to the model: `{ petName, kind: 'codexAuth', audience, byteLength }` — never the JSON That reuses the #22 UI and the form-field opt-in. It does not reuse `provideBearerCredential`. 4. **Give the provisioner the cap, not the session guest.** `setup-hosted` should hold `codex-creds` and hand each `codex-client-<sessionId>` a mount of `CODEX_HOME`, not a copy of the bytes in tool args. Sessions share one Pro 20x login. A session that called `requestSecret` and stored the cap under its own petname would vanish with that session and could not refresh for the next one. 5. **Refresh writes back into the sidecar, not into a Floot cap.** Bind `/var/lib/endo/codex-home` read-write into the container. Do not have `writeSecret` overwrite the host file from a stale cap after a session. 6. **Keep #22 for everything else the Codex agent needs.** Platform API keys, git tokens, service-account JSON: `requestSecret` → cap → `writeSecret` onto the session workspace. Same prompts: never paste in chat, never put in `exec`. ## What not to do - Operator pastes `auth.json` into a Codex turn and the agent `writeSecret`s it into the workspace. That is a session-scoped copy; refresh is lost; the next session is blind. - Seed `ENDO_CODEX_AUTH_JSON` from `secrets.env` on every `endo-daemon` start. That resets `last_refresh`. - Teach the model the refresh token. The receipt must stay metadata-only. - Share one `auth.json` across Tokyo and a laptop. Official rule is one file per machine; concurrent refresh steals the token. - Point Floot at an unofficial ChatGPT web endpoint. The supported subscription path is Codex CLI signed in with ChatGPT. ## Work to do **Endo** - `@endo/codex-sandbox` cloned from `@endo/claude-sandbox` (factory, 9P, per-session client, hosted provisioner). - OCI image with Codex CLI (`localhost/codex:latest`). - Floot runtime id `codex-cli` next to `claude-cli`. Leave `claude-cli` as the default until Codex is proven. - `codexAuth` credential kind + durable sidecar + `CODEX_HOME` bind. - MCP secret tools on the Codex runtime (same as Claude after #22). - Daemon `allowEnvPass` should keep `CODEX_*` the way it already keeps `CLAUDE_*`. **Host (`floot/nixos-config`)** - `services.endo.codexSandbox.enable` (Claude stays on; they coexist). - `endo-codex-image.service`, `/var/lib/endo/codex-home`, `/var/lib/endo/codex-mounts`. - `ENDO_EXTRA`: setup-host / setup-peer / setup-hosted for the new package. - Document seed-once. Do not re-apply `auth.json` from `secrets.env` on daemon start. **Operator (cannot be automated)** - `codex login` or `codex login --device-auth` on a trusted machine with the Pro account. - Confirm `auth_mode` is `chatgpt` and a refresh token is present (metadata only; do not paste the file into chat, Forgejo, or the Nix store). - Copy onto Tokyo as `/var/lib/endo/codex-home/auth.json`, or complete device-auth as `endo` after the image exists. ## Sequence 1. Land #22 / PR 25 on Tokyo so the paste box exists. 2. Codex package: `codexAuth` + sidecar + `CODEX_HOME` bind + MCP secret tools. 3. Hosted setup: inbox form or one-shot `requestSecret` **to the factory / machine-admin**, then persist and forget the paste. 4. Operator seeds `auth.json` once; after that the file on disk is the source of truth. 5. One test session: confirm billing is ChatGPT Pro, not platform API. The subscription login is host infrastructure that happens to use #22's ingestion UI. Per-task secrets stay on the #22 path unchanged. Related: #22, https://git.goooooo.ooo/floot/endo/pulls/25
Sign in to join this conversation.
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
floot/endo#26
No description provided.