Skip to content
Closed
79 changes: 79 additions & 0 deletions devlog/_plan/260904_bug_stack_train/000_research.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# 000 — Live manifest and disposition research

Snapshot taken 2026-09-04, base `origin/dev` = `b5777aa2d642`.
Worktree: `/Users/jun/.codex/worktrees/9d5b/opencodex`.

All findings below were produced by four parallel read-only research lanes and
re-checked against the current tree. Every disposition names its evidence.

## Open bug-labelled PRs (10)

| PR | Author | Verdict | Basis |
|----|--------|---------|-------|
| #3335 | x3M3x | LAND_AS_IS | GUI hardcodes 2 of 5 strategies at `gui/src/components/combo-workspace-controls.tsx:24-50`; canonical set already has 5 at `gui/src/combo-workspace-data.ts:11-30`. Test is RED without the fix. |
| #3333 | blackjune67 | LAND_AS_IS | Models panels are persistent and toggle `hidden` (`gui/src/pages/Models.tsx:2228-2312`); scoping to the visible panel id stops width leakage. Test asserts selectors absent on dev. |
| #3322 | luvs01 | LAND_AS_IS | Head already implements the exact requested message at `src/cli/observe.ts:75-77`. The `CHANGES_REQUESTED` review is stale against the corrected head. |
| #3357 | huaiqing-afk | LAND_AS_IS (draft) | One global previous-text slot at `src/adapters/cursor/protobuf-request.ts:303-381` lets every tool result reset narration detection. PR tracks roles independently. Strong RED regression. |
| #3325 | luvs01 | BLOCKED_ON_POLICY | Code correct, but `.github/workflows/` is a restricted surface (`.github/scripts/pr-sponsored-surface.cjs:24-27`); hygiene fails `unsponsored_surface` without the `maintainer-sponsored` label. The second "failure" is a cancelled `enforce-target` run, not a real failure. |
| #3364 | lidge-jun | LAND_WITH_FIX | Exact-head CI green. Missing a direct `parseResponse()` non-stream regression even though production parse calls the same extractor (`src/adapters/openai-responses.ts:2450-2481`). |
| #3361 | lidge-jun | LAND_AS_IS | Exact-head CI green; marker/journal ownership preserved per key; `startServer` stays synchronous. Touches unauthenticated loopback admission, so it needs explicit maintainer security sign-off. |
| #3332 | full999 | LAND_WITH_FIX | Writes an OUTPUT limit into an INPUT field: `ModelMetadata.maxTokens` is output (`src/generated/model-metadata.ts:4-12`) but lands in `maxInputTokens`. Would shrink Claude 1M input models to 64K/128K. |
| #3348 | RHODIZSECURITY | DEFER | 2,248 lines / 34 files across failover, credentials, persistence, shutdown, and the core response path. Confirmed blocker: generic HTTP 410/413 become retryable hops, so an oversized or invalid request is replayed to the next provider. |
| #3312 | RHODIZSECURITY | DEFER (superseded) | Functionally the same work as #3348 with the same 410/413 blocker; currently CONFLICTING/DIRTY. Not an ancestry successor, but #3348 supersedes it. |

## Open bug-labelled issues (6)

None are safely fixable from the evidence currently attached. Detail:

- **#3352** (GPT-5.6 401) — NEEDS_REPORTER_EVIDENCE. Mechanism is established end to end:
gating at `src/codex/catalog/native-models.ts:5`, roster fetch at
`src/codex/model-entitlements.ts:185`, unconfirmed-evidence fallback at `:548`,
granted-only projection at `:958`/`:1024`, and the exact 401 at
`src/codex/auth-context.ts:435`. The reported `0.142.2` floor theory is already
ruled out — the code enforces `0.144.0` at `:75`. Letting `unknown` through would
be a security-policy change, not a bug fix.
- **#3320** (Windows non-ASCII scheduler) — NEEDS_REPORTER_EVIDENCE. Production XML
writes a locale-independent SID (`src/service.ts:1841,1912`); exact `<UserId>`
matching is deliberate (`:2117`) because folding two non-ASCII identities to `???`
could adopt another account's task. Needs redacted live XML before any patch.
- **#3279** (GUI 401) — NEEDS_REPORTER_EVIDENCE. Each page load mints a session from
its own Host-derived origin (`src/server/gui-session.ts:166`); exact origin checks
are the admission boundary (`:417`); expiry is deterministic at 5 minutes (`:62`).
Canonicalizing localhost/IPv4/IPv6 would weaken auth without proving cause.
- **#3255** (capability vs speed) — PRODUCT_DECISION. The two dimensions are already
independent (`src/reasoning-effort.ts:5` vs `src/codex/catalog/effort.ts:160`), and
there is no Ultra-fast wire tier to pass through.
- **#3245** (stream disconnect) — NEEDS_REPORTER_EVIDENCE. 426 is intentional
(`src/server/index.ts:1107`) and the 426-then-POST path is already covered
(`tests/server-auth.test.ts:1384`). The reporter saw no subsequent POST, which puts
the failure before the Responses bridge.
- **#1527** (Cursor large context) — NEEDS_REPORTER_EVIDENCE. Every known defect in
this path is already fixed; a matched current-dev trace is required.

## Issue #3366 — deviceauth (the implementation target)

Key correction to the issue's premise: `chatgpt` is deliberately excluded from the
generic OAuth surface (`src/oauth/index.ts:284-297`, `tests/oauth-public-surface.test.ts:77-111`)
and `openai|codex|chatgpt` route through the separate Codex-auth API. Returning
`deviceCode` from `src/oauth/` alone therefore does NOT light up the existing UI —
the Codex-auth layer discards it today at `src/codex/auth-api.ts:2199-2209`.

Upstream wire flow, confirmed against `codex-rs/login/src/device_code_auth.rs`:
15-minute poll window, only 403/404 mean pending, server-issued `code_verifier`,
and `redirect_uri=https://auth.openai.com/deviceauth/callback`.

Non-fabrication note: the issue claims a `codex_cli_rs` User-Agent is required.
Upstream actually builds a raw auth client with no Codex default headers
(`device_code_auth.rs:165-171`), and its real UA is dynamic. We do not hard-code
client impersonation; we send no custom UA and let the platform default stand.

## Stack plan

Dependency-ordered, bottom-up (DEV-STACK-01):

1. `codex/deviceauth-core` — the grant itself in `src/oauth/` (010)
2. `codex/deviceauth-surface` — Codex-auth API + CLI + docs (020)
3. `codex/bug-carry` — carried contributor fixes with attribution (030)

Deferred out of the stack with recorded reasons: #3348, #3312, #3325, and all six
bug issues. Documented in 040.
72 changes: 72 additions & 0 deletions devlog/_plan/260904_bug_stack_train/010_wp2_deviceauth_core.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# 010 — wp2: deviceauth grant core (stack layer 1)

Branch: `codex/deviceauth-core`, based on `origin/dev` `b5777aa2d642`.
Thesis: implement the OpenAI deviceauth grant as a self-contained module and let
`loginChatGPT` select it. Nothing outside `src/oauth/` changes in this layer.

## Files

- ADD `src/oauth/chatgpt-device.ts` — the grant.
- MODIFY `src/oauth/chatgpt.ts` — export `credsFromToken` for reuse; add the
`flow` option to `loginChatGPT`.
- MODIFY `src/oauth/index.ts` — thread `flow` through the `chatgpt` registry entry.
- ADD `tests/chatgpt-device-auth.test.ts`.
- MODIFY `tests/oauth-device-code-contract.test.ts` — extend the shared contract to chatgpt.

## Wire protocol (from codex-rs device_code_auth.rs)

1. `POST https://auth.openai.com/api/accounts/deviceauth/usercode`
JSON `{ client_id }` -> `{ device_auth_id, user_code, interval? }`
2. `POST https://auth.openai.com/api/accounts/deviceauth/token`
JSON `{ device_auth_id, user_code }`; 403/404 = pending; 200 =
`{ authorization_code, code_verifier }`
3. `POST https://auth.openai.com/oauth/token` form-encoded
`grant_type=authorization_code`, `client_id`, `code`, `code_verifier`,
`redirect_uri=https://auth.openai.com/deviceauth/callback`

Poll window 15 minutes; default interval 5s; the interval field may arrive as a
string, so coerce numerically and floor at 1s.

## Signatures

```ts
export type ChatGPTLoginFlow = "browser" | "device";
export async function loginChatGPTDevice(ctrl: OAuthController): Promise<OAuthCredentials>;
export async function loginChatGPT(
ctrl: OAuthController,
opts?: { forceLogin?: boolean; flow?: ChatGPTLoginFlow },
): Promise<OAuthCredentials>;
```

`onAuth` publishes `{ url: "https://auth.openai.com/codex/device", deviceCode: user_code,
instructions }` — matching the kimi/nous/copilot contract where `deviceCode` carries the
HUMAN code, never the opaque polling handle.

## Credential boundary

- Never log `device_auth_id`, `authorization_code`, `code_verifier`, or any token.
- Do NOT reuse `safeErrorDescription` from the callback flow: it reflects upstream
body text. Device errors carry status only.
- Bound the success payload; reject non-string `authorization_code`/`code_verifier`.

## Tests (red-then-green)

`tests/chatgpt-device-auth.test.ts`, stubbing `globalThis.fetch` by URL in the
established style of `tests/oauth-device-code-contract.test.ts:16-63`:

1. requests a user code and surfaces the fixed verification URL + human code
2. treats only 403/404 as pending and honors the returned interval
3. exchanges the server-issued `authorization_code`/`code_verifier` at the device callback URI
4. rejects a malformed success payload without reflecting the body
5. aborts promptly on signal
6. surfaces `accountId`/`email` from a realistic device-token `id_token`, because Codex
pool admission rejects a credential with no account id (`src/codex/auth-api.ts:2221`).
Wire success alone is not proof the credential is usable.

Focused command: `bun test tests/chatgpt-device-auth.test.ts tests/oauth-device-code-contract.test.ts tests/chatgpt-oauth.test.ts`

## Security review gate

`src/oauth/` is a restricted authentication surface
(`.github/scripts/pr-sponsored-surface.cjs:24`); `MAINTAINERS.md:60` requires explicit
security review. The PR description states this; it does not merge as routine work.
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# 020 — wp3: deviceauth surface (stack layer 2)

Branch: `codex/deviceauth-surface`, based on `codex/deviceauth-core`.
Thesis: make the grant reachable. Without this layer the core is unreachable from any
user-facing path, because `openai|codex|chatgpt` go through the Codex-auth API, which
drops `deviceCode` from the start DTO at `src/codex/auth-api.ts:2440` and opens the
authorization URL at `:2206` (conditional on `shouldOpenBrowserForLogin`, which already
honors an explicit/configured false at `src/oauth/open-browser-choice.ts:20` — the gap is
that a device flow is not itself a reason to skip the open).

## Files

- MODIFY `src/codex/auth-api.ts` — accept `device?: boolean` on login start, pass
`flow: "device"` into the chatgpt login, return `deviceCode` in the start DTO, and
suppress the server-side browser open when `deviceCode` is present (mirroring
`src/server/management/oauth-account-routes.ts:185`).
- MODIFY `src/cli/account-auth.ts` — add `--device`; include it in the login body;
print `Device code: <code>` in the Codex pre-poll block; preserve it under
`--no-wait --json`.
- MODIFY `src/cli/capabilities.ts` — declare the flag.
- MODIFY `gui/src/components/use-add-codex-account-oauth.ts` — keep `deviceCode` and
`instructions` on the start DTO (dropped today at `:148`) and request device mode.
- MODIFY `gui/src/components/add-codex-account-reducer.ts` — carry both fields in state.
- MODIFY `gui/src/components/add-codex-account-waiting-step.tsx` — pass them to
`LoginHint` (today it passes only `url` at `:38`). The shared renderer at
`gui/src/components/login-url-block.tsx:42-47,73-107` is already device-capable, so no
new UI component is needed.
- REGENERATE `skills/ocx/references/01_management_surface.md` via `bun run skill:surface`
(gated by `tests/skill-ocx.test.ts`).
- MODIFY `docs-site/` provider/account docs (English source; do not let locales contradict).

## Poll budget (audit blocker 2)

The device grant lives 15 minutes, but both existing poll budgets stop at five:
Codex-auth polls 150 x 2s and then records an error (`src/codex/auth-api.ts:2214,2414`),
and the CLI independently stops at the same 150 x 2s (`src/cli/account-auth.ts:123`).
Shipping the grant without widening these would advertise a 15-minute window that
dies at minute five — exactly the headless case this feature exists for, where the
operator walks to another device to enter the code.

Both budgets are raised for the device flow, and a test proves a login completing
after minute five still succeeds (fake timers; no real waiting).

## Security review gate (audit finding 4)

`src/oauth/`, `src/codex/auth-api.ts`, and `src/cli/account-auth.ts` are restricted
authentication surfaces (`.github/scripts/pr-sponsored-surface.cjs:24`) and require
explicit security review per `MAINTAINERS.md:60`. Both deviceauth PRs carry that
requirement in their description; neither is merged as routine.

Explicitly NOT done: repurposing `--code` as device user-code input. The device user
code is entered at `auth.openai.com`, while `account code` submits callback
authorization material to a different endpoint (`src/codex/auth-api.ts:2457-2472`).
Conflating them would silently break the existing paste fallback.

## Tests

- `tests/codex-auth-api.test.ts`: device login returns `deviceCode` and does not open a URL.
- `tests/cli-account.test.ts`: `--device` prints URL + device code + flow id; `--no-wait --json` preserves it.
- `tests/codex-auth-api.test.ts`: a device login that completes after minute five still succeeds.
- `tests/cli-account.test.ts`: with fake timers, a normal (polling) `--device` login completes
after minute five. The `--no-wait` case bypasses polling and does not cover this.
- `gui/tests/add-codex-account-device.test.tsx`: the start request carries `device: true`,
and the waiting step renders the device code and verification URL.

Focused: `bun test tests/codex-auth-api.test.ts tests/cli-account.test.ts tests/skill-ocx.test.ts`
plus the single focused GUI test file. No repository-wide suite.
39 changes: 39 additions & 0 deletions devlog/_plan/260904_bug_stack_train/030_wp4_bug_carry.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# 030 — wp4: carried contributor fixes (PARALLEL, not stacked)

Corrected after plan audit. These were originally drafted as a third stack layer above
deviceauth. That was wrong: none of the four consumes deviceauth and none consumes
another, so stacking them would impose a false merge order. DEV-STACK-01 says
independent parts open as parallel PRs off trunk, and DEV-STACK-03 says one thesis per
layer — four unrelated theses in one layer violates both.

Each fix therefore gets its own branch off `dev`, merged independently:
`codex/carry-3335`, `codex/carry-3333`, `codex/carry-3322`, `codex/carry-3357`.

Four PRs were judged root-correct with RED-without-fix regressions. Each is carried as
its own independent PR with a `Co-authored-by` trailer in its commit, so the
contributor graph records the author (AGENTS.md; `CREDITS.md` exists because 27
landings previously lost attribution).

| Source PR | Author trailer | Scope |
|-----------|----------------|-------|
| #3335 | `Co-authored-by: x3M3x <amroeid1999@gmail.com>` | GUI combo strategy selector: render all five |
| #3333 | `Co-authored-by: hajune <june@smartix.co.kr>` | Models tab spacing + Combos layout stability |
| #3322 | `Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>` | `logs --follow` capability contract |
| #3357 | `Co-authored-by: huaiqing-afk <huaiqing-afk@users.noreply.github.com>` | Cursor repeated-narration breaker |

Carry method: fetch the PR head and re-apply its source/test hunks onto a fresh branch
off `dev`, one commit per source PR, each carrying its trailer. Do not pipe
`gh pr diff` straight into `git apply` for #3335 — GitHub emits binary PNG hunks
without full index data, so the whole-patch check fails on the two
`docs/pr-assets/*.png` files even though every source hunk applies cleanly.

Focused verification, per branch — each branch runs only its own tests:

| Branch | Command |
|--------|---------|
| `codex/carry-3335` | `cd gui && bun test tests/combo-strategy-selector.test.tsx` |
| `codex/carry-3333` | `cd gui && bun test tests/models-tab-layout.test.ts` |
| `codex/carry-3322` | `bun test tests/cli-usage-report.test.ts tests/cli-capabilities.test.ts` |
| `codex/carry-3357` | `bun test tests/cursor-repetition-breaker.test.ts` |

No repository-wide suite (explicit user constraint).
30 changes: 30 additions & 0 deletions devlog/_plan/260904_bug_stack_train/040_deferrals.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# 040 — Recorded deferrals

Deferring is a disposition, not an omission. Each item below stays open with a
stated reason rather than being force-landed.

## #3348 / #3312 — combos failover hardening

Both carry the same confirmed correctness blocker: generic HTTP 410 and 413 are
classified as retryable hops (`src/combos/failover.ts:563-617` on both heads), so an
oversized or invalid request would be replayed to the next provider. Their own tests
encode the wrong expectation. #3348 functionally supersedes #3312 (30 shared files,
near-identical source diffs; #3312 is additionally CONFLICTING/DIRTY).

At 2,248 lines across 34 files spanning failover, credential rotation, durable
cooldown persistence, shutdown, and the core response path, this is not reviewable
inside a mixed campaign. It needs its own split stack.

## #3325 — dev bump guard fork filter

The code is correct, but `.github/workflows/` is a restricted surface
(`.github/scripts/pr-sponsored-surface.cjs:24-27`) and the hygiene gate fails
`unsponsored_surface` without a maintainer sponsorship decision. That is a policy
action for a human, not a patch. Note the second red check is a cancelled
`enforce-target` run that `gh pr checks` renders as a failure.

## All six bug issues

See 000. Every one needs reporter evidence or a product decision. Three of them
(#3352, #3320, #3279) would require weakening an auth or identity boundary to
"fix" without a reproduction, which is the wrong trade.
Loading
Loading