Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/codex/auth-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1160,6 +1160,7 @@ export function materializeCodexUpstreamAuth(
if (!stored?.accessToken || !isMainAccountTokenLive()) {
throw new CodexMainSubstitutionUnavailableError();
}
selected.delete("chatgpt-account-id");
Comment thread
luvs01 marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update every document that owns src/codex

This authentication change modifies src/codex/auth-context.ts, but checking structure/INDEX.md shows eight documents mapped to src/codex/, while this commit updates only structure/providers/openai-tiers.md; runtime.md, config.md, codex-home.md, catalog.md, subagents.md, gui-and-management-api.md, and ops/docs-and-release.md remain unchanged. Update every mapped document in the same change, or narrow the ownership map if those documents genuinely do not own this contract.

AGENTS.md reference: src/AGENTS.md:L11-L11

Useful? React with 👍 / 👎.

selected.set("authorization", `Bearer ${stored.accessToken}`);
if (stored.chatgptAccountId) selected.set("chatgpt-account-id", stored.chatgptAccountId);
observeSelectedMainCredential(stored, writer);
Expand Down Expand Up @@ -1237,6 +1238,7 @@ export async function materializeCodexUpstreamAuthAsync(
...(options.nativeMainRefreshDependencies ?? {}),
});
if (!stored?.accessToken) throw new CodexMainSubstitutionUnavailableError();
selected.delete("chatgpt-account-id");
selected.set("authorization", `Bearer ${stored.accessToken}`);
if (stored.chatgptAccountId) selected.set("chatgpt-account-id", stored.chatgptAccountId);
observeSelectedMainCredential(stored, writer);
Expand Down
3 changes: 2 additions & 1 deletion structure/data-planes/inbound-compat.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ is scoped to canonical ChatGPT Responses forwarding; other source-area behavior
`src/server/audio-transcriptions.ts` owns `POST /v1/audio/transcriptions`, independently of
Responses and Chat conversion. `src/server/audio-upstream.ts` resolves explicit data-plane keys
on both listeners and substitutes stored OpenAI credentials. Direct stored-main access claims
the enclosing admission lease; Pool uses the existing sidecar account resolver. A selected
the enclosing admission lease and derives its account header only from that stored credential;
caller-supplied account selection is never retained. Pool uses the existing sidecar account resolver. A selected
ChatGPT authentication failure never falls through to the paid OpenAI provider.

The bounded multipart input accepts one nonempty file up to 25,000,000 bytes within a 32 MiB
Expand Down
3 changes: 2 additions & 1 deletion structure/providers/openai-tiers.md
Original file line number Diff line number Diff line change
Expand Up @@ -415,7 +415,8 @@ sidecar candidate and cannot hide a failed Codex credential with separately bill

`src/server/audio-upstream.ts` uses the same selection for standalone transcription. Explicit
native Direct auth remains caller-owned; proxy-key-only Direct claims stored main before
materialization. `src/providers/openai-sidecar.ts` releases quota-probe ownership on every
materialization, replacing both bearer and account identity exclusively from that credential.
`src/providers/openai-sidecar.ts` releases quota-probe ownership on every
materialization or usability failure before transferring a resolved context to its caller.
Audio reports one terminal upstream outcome after validating the response body; redirects remain
neutral and client/shutdown cancellation does not manufacture an account failure.
Expand Down
27 changes: 27 additions & 0 deletions tests/codex-integration/codex-auth-context.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
cooldownErrorResponse,
headersForCodexAuthContext,
materializeCodexUpstreamAuth,
materializeCodexUpstreamAuthAsync,
CodexMainSubstitutionUnavailableError,
isCodexAuthContextUsable,
resolveCodexAuthContext,
Expand Down Expand Up @@ -1664,6 +1665,32 @@ describe("Codex auth context", () => {
expect(headers.get("openai-beta")).toBe("responses=experimental");
});

test.each([
["absent", undefined, null],
["present", "stored_main_acc", "stored_main_acc"],
])("async stored Direct substitution owns account identity when %s", async (_label, accountId, expectedAccountId) => {
const storedCredential = liveJwt();
writeFileSync(join(testDir, "auth.json"), JSON.stringify({
tokens: { access_token: storedCredential, account_id: accountId },
}));
const inbound = new Headers({
authorization: "Bearer ocx_data_localsecret",
"chatgpt-account-id": "caller-account",
"openai-beta": "responses=experimental",
});

const headers = await materializeCodexUpstreamAuthAsync(
inbound,
{ kind: "main", accountId: null },
{ substituteMainCredential: true },
);

expect(headers.get("authorization")).toBe(`Bearer ${storedCredential}`);
expect(headers.get("chatgpt-account-id")).toBe(expectedAccountId);
expect(headers.get("openai-beta")).toBe("responses=experimental");
expect(inbound.get("chatgpt-account-id")).toBe("caller-account");
});

test("substitution fails closed when no usable main credential exists (#1686)", () => {
// Falling through here would forward the admission secret upstream, which is exactly
// the leak the forward guard exists to prevent. Throw before any I/O instead.
Expand Down
21 changes: 21 additions & 0 deletions tests/server/audio-transcriptions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,27 @@ describe("standalone transcription API", () => {
expect((await captured[0]!.formData()).get("model")).toBeNull();
});

test("stored Direct credentials never inherit a caller account ID", async () => {
writeFileSync(join(codex.path, "auth.json"), JSON.stringify({ tokens: { access_token: "fixture-main-access" } }));
clearMainAccountInfoCache();
const cfg = config();
cfg.defaultProvider = "openai";
cfg.providers = { openai: { adapter: "openai-responses", baseUrl: "https://chatgpt.com/backend-api/codex", authMode: "forward", codexAccountMode: "direct" } };
saveConfig(cfg);

for (const headers of [
{ authorization: "", "x-opencodex-api-key": KEY, "chatgpt-account-id": "caller-workspace" },
{ authorization: "", "x-api-key": KEY, "chatgpt-account-id": "caller-workspace" },
]) {
expect((await request(form(), headers)).status).toBe(200);
}
expect(captured).toHaveLength(2);
for (const upstream of captured) {
expect(upstream.headers.get("authorization")).toBe("Bearer fixture-main-access");
expect(upstream.headers.get("chatgpt-account-id")).toBeNull();
}
});

test("a missing stored Direct credential fails without paid-provider fallback", async () => {
const cfg = config();
cfg.providers.openai = { adapter: "openai-responses", baseUrl: "https://chatgpt.com/backend-api/codex", authMode: "forward", codexAccountMode: "direct" };
Expand Down
Loading