Issue Codex subscription tokens through a chatgpt.com entry (DRU-476) - #487
Merged
czpython merged 1 commit intoSep 8, 2026
Merged
Conversation
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Automations to automatically generate PRs for you. |
A Codex subscription run holds a placeholder in CODEX_SUBSCRIPTION_TOKEN. The box's custom entry swaps it on every chatgpt.com request. The issuer answers the stored access token with its JWT exp as the expiry. It uses the gate, the row lock, and the refresh requests of the Claude path. The Codex run wrapper writes ~/.codex/auth.json in the box from that variable, before the command. The file inlines the account id, a sentinel refresh token, and an unsigned id token with the stored claims. Codex needs both keys present; without them it switches to API-key mode. The one refresh Codex attempts after a 401 fails on the sentinel, and the turn ends. Druks refreshes the subscription; the box never does. Druks no longer uploads .codex/auth.json. The subscription row stays out of invocation construction. A profile carries the subscription's non-secret facts as its identity, and build_invocation takes that identity in place of the row. Fixes DRU-476. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
czpython
force-pushed
the
commonzenpython/dru-476-codex-subscription-contract
branch
from
September 8, 2026 18:36
173b1da to
6dcf870
Compare
czpython
deleted the
commonzenpython/dru-476-codex-subscription-contract
branch
September 8, 2026 18:43
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
A Codex subscription run holds a placeholder in
CODEX_SUBSCRIPTION_TOKEN.CodexHarness.get_secret_refsdeclares the custom entry: hostchatgpt.com, headerAuthorization, prefixBearer, refresh1h. The proxy swaps the placeholder on everychatgpt.comrequest, the WebSocket upgrade included.The Codex run wrapper writes
~/.codex/auth.jsonin the box before the command, mode 600. Druks builds the JSON with$CODEX_SUBSCRIPTION_TOKENas the access token. The wrapper emits it in a double-quoted shell word, so the box shell expands the one variable. The file carriesOPENAI_API_KEY: null, the account id, and the sentinel refresh tokendruks-placeholder. It also carries an unsigned id token with the stored claims, andlast_refreshat build time. Codex needs the id token and a refresh token key present; without them it switches to API-key mode. The one refresh Codex attempts after a 401 fails on the sentinel, and the turn ends. Druks refreshes the subscription; the box never does.The issuer answers the stored access token with its JWT
expas the expiry. The gate, the row lock, and the refresh requests are the generic ones; the provider has no branch.The subscription row stays out of invocation construction.
Provider.get_identity(subscription)returns the row's non-secret facts.OpenAiProvideraddsaccount_idfrom the stored tokens andplanfrom the id token claim.Profile.identitycarries them, andbuild_invocation(identity=)replacessubscription=on Claude, Codex, Pi, and OpenCode.Host.run_agentandHost.run_promptpassidentity=profile.identity. NoHomeFileforauth.jsonremains. API-key mode is unchanged. No migration, and nothing in Drukbox.Docs: the Harnesses paragraph and table in
docs/configuration.md, and one sentence pair indocs/concepts.md.Where this differs from the ticket
get_identityomits an absent fact instead of a null. A row whose id token carries no plan claim yields noplankey, and the id token then carries only the claims present.docs/concepts.mdnever named Claude in the identities section. One place now names both CLIs.test_harness_reasoning_flags.pylost its autouse fixture.build_invocationreads no row now, so those tests run without the database.conftest.make_jwtserves three test files instead of three inline copies.unsigned. The proof kit usedx. Codex reads only the payload segment.Names
codex_subscription_token: the ref name, and the box variableCODEX_SUBSCRIPTION_TOKENProvider.get_identity,Profile.identity,build_invocation(identity=)OPENAI_AUTH_CLAIM: public now, socodex.pyimports it_auth_file,_CHATGPT_HOST,_SUBSCRIPTION_TOKENincodex.pymake_jwtinconftest.pyRemoved tests
None.
test_credentials_builders_read_their_harness_config_directoriesasserted theauth.jsonHomeFile; it now asserts noHomeFileat all. The reasoning-flags autouse fixture is gone; the tests it served remain.Gates
Run from the worktree root, each as its own command.
uv run ruff check backend: passeduv run ruff format --check backend: passeduv run pytest backend/: 1709 passedReview
Start with
_auth_fileand theloginfragment inbackend/druks/harnesses/codex.py, thenOpenAiProvider.get_identityinproviders.py. The secrecy tests aretest_codex_subscription_token_stays_on_the_serverandtest_the_codex_wrapper_writes_its_login_around_the_placeholder. The bed checks are yours. They are one box turn through the real exchange and issuer, one rotation while Codex is idle, and the turn after a revocation. This PR claims none of them.Fixes DRU-476.
🤖 Generated with Claude Code