diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 02344a0aa2..1d11ebe15b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -516,7 +516,7 @@ jobs: if [ "$suite_status" -eq 0 ]; then exit 0 fi - if ! grep -Eqi 'oh no: Bun has crashed|Segmentation fault at address|Illegal instruction|Bus error|Aborted \(core dumped\)' "$suite_log"; then + if ! grep -Eqi 'oh no: Bun has crashed|Internal assertion failure|Segmentation fault at address|Illegal instruction|Bus error|Aborted \(core dumped\)' "$suite_log"; then echo "::error::macOS suite failed on attempt ${attempt} (exit ${suite_status}); assertion failures are not retried." exit "$suite_status" fi @@ -553,7 +553,16 @@ jobs: # Sharded like the Linux legs. The single-leg run reached 30 minutes on a # green suite and was killed in cleanup; four shards put each leg inside the # same budget the Linux shards already hold. - timeout-minutes: 15 + # + # 15 was that Linux budget, and on this leg it truncated the evidence rather + # than bounding a hang: shard 1/4 of run 32340498394 was CANCELLED at exactly + # 15m12s while still executing tests, so its result was neither pass nor fail + # and the composed-acceptance cases it carries could not be read at all. The + # other shards finished in 14-15 minutes, which is the wrong side of the + # margin. 25 leaves the outer bound in place — a wedged shard still dies — + # while making a completed shard the normal outcome. The crash-retry below can + # double a shard's work, and this ceiling has to cover that second attempt too. + timeout-minutes: 25 strategy: fail-fast: false matrix: @@ -611,7 +620,31 @@ jobs: # the only one left on Bun's 5s default, and it is the slowest hardware on the board. # Three of its failures were the default firing on tests that had not hung — the # composed-acceptance cases spawn a real `ocx start` and were still working at 41s. - run: bun test --isolate --timeout 60000 tests --shard=${{ matrix.shard }}/4 + # + # The retry is the same one the macOS leg already carries, for the same reason: a Bun + # runtime panic is a crash in the interpreter, not a test result, and failing the shard + # on it reports a defect this repository does not have (#2152). An ordinary assertion + # failure returns its status immediately — only the crash signatures below are retried, + # and only once, so a genuinely broken build cannot be retried into green. + shell: bash + run: | + set +e + set -uo pipefail + suite_log="$(mktemp -t ocx-windows-suite.XXXXXX)" + for attempt in 1 2; do + bun test --isolate --timeout 60000 tests --shard=${{ matrix.shard }}/4 2>&1 | tee "$suite_log" + suite_status="${PIPESTATUS[0]}" + if [ "$suite_status" -eq 0 ]; then + exit 0 + fi + if ! grep -Eqi 'oh no: Bun has crashed|Internal assertion failure|Segmentation fault at address|Illegal instruction|Bus error|Aborted \(core dumped\)' "$suite_log"; then + echo "::error::Windows shard ${{ matrix.shard }}/4 failed on attempt ${attempt} (exit ${suite_status}); assertion failures are not retried." + exit "$suite_status" + fi + echo "::warning::Bun runtime crash in Windows shard ${{ matrix.shard }}/4 (exit ${suite_status}, attempt ${attempt})." + done + echo "::error::Bun runtime crash repeated on Windows shard ${{ matrix.shard }}/4; failing after one retry." + exit 1 - name: CLI help smoke run: bun run src/cli/index.ts help diff --git a/devlog/_plan/260820_bug_pr_backlog_consolidation/000_research_inventory.md b/devlog/_plan/260820_bug_pr_backlog_consolidation/000_research_inventory.md new file mode 100644 index 0000000000..1b68a3fe29 --- /dev/null +++ b/devlog/_plan/260820_bug_pr_backlog_consolidation/000_research_inventory.md @@ -0,0 +1,289 @@ +# 000 — Research: open bug-PR backlog inventory, rubric, and disposition + +Unit: 260820_bug_pr_backlog_consolidation +Work-phase: wp1 (docs-only roadmap cycle, LOOP-DOCS-FIRST-01) +Baseline: origin/dev = ceac592d7. Worktree branch codex/fix-subagent-roster-truncation (PR #2134). + +Evidence for every claim below came from six read-only xai/grok-4.6 investigation lanes that +read the actual PR diffs with `gh pr diff` and cross-read the runtime in this worktree. Code +edits stay in the main agent. + +## 1. Inventory + +27 open bug-labeled PRs; 25 authored by someone other than lidge-jun. 17 open bug issues. + +| PR | Author | Draft | Subsystem | Files | +|---|---|---|---|---| +| 2131 | bet4it | no | responses id backfill | server/responses | +| 2127 | agentHits | yes | antigravity thought_signature | adapters/google | +| 2115 | louis-tepe | no | adapter prompt nudge | adapters/* | +| 2110 | drakonkat | no | antigravity baseUrl override | providers/registry, lib/destination-policy | +| 2109 | drakonkat | no | anthropic baseUrl override | providers/registry, lib/destination-policy | +| 2105 | lilinxiong | no | claude shell hook | cli/index, server/system-env | +| 2104 | olddonkey | no | xai OAuth responses streaming | adapters/xai | +| 2102 | lilinxiong | no | gpt-5.6 prompt_cache_retention | adapters/openai-responses | +| 2101 | Ingwannu | no | account entitlement gating | codex/catalog | +| 2100 | ntdatt812 | no | routing capability evidence | routing/capability | +| 2099 | yzxcj797 | yes | gpt-5.6 prompt_cache_retention | adapters/openai-responses | +| 2091 | luvs01 | no | prompt_cache_retention (all forward) | adapters/openai-responses | +| 2082 | yzxcj797 | yes | AgentRouter language preamble | adapters | +| 2077 | ntdatt812 | no | lab behavior overrides | routing/compatibility/behavior | +| 2075 | olddonkey | no | Fast gate native chat (CONFLICTING) | adapters/openai-chat | +| 2067 | waw4303 | yes | opencode-free headers | providers/registry | +| 2063 | yzxcj797 | yes | K12 detail.code denials (CONFLICTING) | codex/quota-rejection | +| 2062 | yzxcj797 | yes | K12 short-window quota | codex/quota, codex/routing | +| 2056 | Ingwannu | no | K12 short-window quota | codex/quota, codex/routing | +| 2054 | keepitmello | yes | cursor checkpoints (CONFLICTING) | adapters/cursor | +| 2053 | Ingwannu | no | superseded OAuth commits | oauth/* | +| 2040 | Ingwannu | no | routed tool_search passthrough | server/responses | +| 2032 | yzxcj797 | yes | claude root bypass | cli/claude | +| 2029 | yzxcj797 | yes | probe session bus absent | service-manager-probe | +| 2027 | yzxcj797 | yes | opencode-go quota gating | providers/quota | + +## 2. Scoring rubric + +Score = severity (0-35) + blast radius (0-25) + evidence quality (0-20) + fix tractability (0-20). +Threshold for this campaign: **>= 60**. + +- severity: does it break a core path (routing, auth, streaming, config persistence) for a + default configuration, or is it peripheral/cosmetic? +- blast radius: how many users/configurations does the defect reach? +- evidence quality: deterministic reproduction with logs/curl, or assertion? +- fix tractability: is a correct, testable fix small and self-contained? + +## 3. Scores and disposition + +| Item | Score | Disposition | +|---|---|---| +| Issue #2132 bearer admission forces ChatGPT credential | 96 | ABSORB — no PR exists; highest-value gap in the backlog | +| Issue #2092 / PRs #2102,#2099,#2091 prompt_cache_retention | 86 | ABSORB #2102 as base; supersede #2099, #2091 | +| Issue #2114/#1939 / PR #2029 probe bus | 80 | SUPERSEDED by maintainer PR #2130 (already open) | +| PR #2131 responses output id backfill | 80 | ABSORB | +| PR #2100 routing capability evidence | 80 | ABSORB | +| PR #2047 / #2056 + #2062 K12 short-window quota | 72 | ABSORB #2056; supersede #2062 | +| PR #2053 superseded OAuth credential commits | 72 | KEEP — C4 auth, needs human security review (MAINTAINERS.md) | +| PRs #2109 + #2110 baseUrl override | 68 | HOLD — unresolved security gap, see §6 | +| PR #2101 account entitlement gating | 64 | KEEP — large (20 files), needs its own cycle | +| PR #2077 lab behavior overrides | 62 | ABSORB | +| PR #2040 routed tool_search passthrough | 62 | KEEP — 14 files, own cycle | +| PR #2105 claude shell hook | 60 | ABSORB | +| PR #2063 K12 detail.code | — | SUPERSEDED by already-merged #2055 | +| PR #2115 code mode nudge | 54 | BELOW THRESHOLD — contracts native-OpenAI detection; needs human adapter pass | +| PR #2082 AgentRouter language | 54 | BELOW THRESHOLD | +| PR #2027 opencode-go quota | 56 | BELOW THRESHOLD | +| PR #2067 opencode-free headers | 50 | BELOW THRESHOLD | +| PR #2054 cursor checkpoints | 46 | BELOW THRESHOLD — hypothesis pending wire trace | +| PR #2032 claude root bypass | 46 | BELOW THRESHOLD — maintainer already rejected the default | +| PR #2104, #2075, #2127 | n/a | Deferred: #2075 and #2054 are CONFLICTING; #2127 is an active draft by its author | + +## 4. Duplicate clusters (evidence-backed) + +**prompt_cache_retention (issue #2092).** #2102 gates on `forward && isCanonicalOpenAiForwardProvider` +and matches `gpt-5.6` / `gpt-5.6-*`. #2099 uses a looser `startsWith("gpt-5.6")` on ANY forward +provider and carries a stray package.json 2.24.2 -> 2.25.0 bump. #2091 strips the field for every +forward request and every model, which inverts the existing gpt-5.5 preserve pin at +tests/openai-responses-passthrough.test.ts:807 — the issue reporter explicitly withdrew the +global claim. #2102 is the correct contract. + +**K12 short-window quota (issue #2047).** #2056 is a strict superset of #2062: it adds +`snapshotHasShort`, partial-snapshot preservation, `updateAccountQuota` carry, and the +parse -> cache -> DTO path the issue requires. Both rewrite the same two functions and WOULD +conflict. #2062 also carries the same stray version bump. + +**Probe bus (issues #2114/#1939).** #2130's `busUnreachable()` is a superset of #2029's two +strings and adds the on-disk unit check that #2029's reviewer demanded. Landing #2029 on top of +#2130 would REGRESS the disk check back to unconditional `absent`. + +## 5. Structural finding: this backlog is not one stack + +DEV-STACK-01 permits stacking only when later parts consume earlier parts' output. Measured file +overlap across the absorb set: + +| Cluster | Files | +|---|---| +| PCR consolidation | src/adapters/openai-responses.ts | +| #2132 + #2131 | src/server/responses/core.ts (**shared**) | +| #2100 | src/routing/capability.ts | +| #2077 | src/routing/compatibility/behavior.ts | +| K12 | src/codex/quota.ts, src/codex/routing.ts | +| #2105 | src/cli/index.ts, src/server/system-env.ts | + +Exactly one real dependency edge exists: **#2132 and #2131 both modify +`src/server/responses/core.ts`**, so they must be ordered. Everything else is disjoint. + +Forcing 12 disjoint fixes into one 12-layer chain would violate DEV-STACK-01's independence +clause and the 2-4 depth guidance, and would impose a false merge order in which an unrelated +layer blocks every layer above it. The honest shape is therefore **one bounded stack rooted on +#2134 for the genuinely dependent Responses work, plus sibling PRs off dev for the disjoint +fixes**. That is recorded here rather than silently reshaped. + +## 6. Security holds (detail deliberately not recorded here) + +The baseUrl-override pair (#2109/#2110) has an unresolved gap already raised publicly in the +CodeRabbit thread on those PRs. Per AGENTS.md, pre-disclosure security reasoning does not go in +this public directory: the analysis lives in scratch only, and these PRs are HOLD, not absorb, +until a human security pass. #2053 is C4 OAuth and requires the security review MAINTAINERS.md +mandates; it is KEEP, not absorb. + +## 7. Attribution contract + +Every superseded PR gets (a) its author credited by @login in the superseding PR body, +(b) a courteous closing comment naming the replacement PR and what was carried over, +(c) no force-push and no edit to the contributor's own branch. + + +--- + +# P-phase amendment (A-gate self-audit, 2026-08-20): the stack premise in §5 was WRONG + +The A-phase auditor lane produced nothing across three wait cycles, so it was retired +(DISPATCH-RETIRE-01) and the load-bearing claims were verified directly. Two of them failed. + +## Correction 1 — #2131 does NOT touch `src/server/responses/core.ts` + +`gh pr diff 2131 --name-only` returns `src/server/responses/responses-field-backfill.ts`, +its test, and eight docs locales. `core.ts` already imports that module on `dev` +(`src/server/responses/core.ts:6-7`, called at :3098-3099); #2131 only changes the module's +internals and signature. It never edits `core.ts`. + +#2132's fix lives in `resolveResponsesCodexAuth` (`core.ts:1082-1114`), a different region of +a file #2131 does not modify at all. + +**Therefore the single dependency edge claimed in §5 does not exist.** The corrected file map: + +| Item | Files | Overlap | +|---|---|---| +| #2132 | src/server/responses/core.ts (auth resolution) | none | +| #2131 | src/server/responses/responses-field-backfill.ts | none | +| #2102 | src/adapters/openai-responses.ts | none | +| #2100 | src/routing/capability.ts | none | +| #2077 | src/routing/compatibility/behavior.ts | none | +| #2056 | src/codex/quota.ts, src/codex/routing.ts | none | +| #2105 | src/cli/index.ts, src/server/system-env.ts | none | + +Every absorbed item is disjoint. **There is no dependency-ordered chain in this backlog at all.** + +## Consequence: this work must NOT be stacked + +DEV-STACK-01 forbids stacking independent parts: "the parts are independent — open parallel PRs +off trunk instead, since a stack imposes a false merge order." Building the requested chain +would mean any layer's review blocking every layer above it, for zero dependency benefit, and +would violate the same rule the request asked to follow. + +Docs 010 and 020 are therefore **superseded**: both become siblings based on `dev`, not layers. +PR #2134 remains its own independent PR. The stack rooted on #2134 is cancelled and the reason +is recorded here rather than the plan being quietly reshaped. + +**One exception preserved:** if two absorbed items ever do touch one file, they stack. None do. + +## Correction 2 — issue #2132 is confirmed present, with a sharper mechanism than 010 assumed + +Verified in this worktree: +- `core.ts:1088`: `const substituteMainCredential = options.admission?.source === "bearer";` + keys on HOW the caller authenticated, never on WHERE the request routes. +- `auth-context.ts:542-548`: with `ctx.kind === "main"` and that flag, a missing/dead stored + main token throws `CodexMainSubstitutionUnavailableError`. +- `core.ts:1148-1153`: that becomes the reported 401. +- The `authCtx = { kind: "main" }` fallback at `core.ts:1105` is taken whenever + `route.codexAccountMode` is unset — which is every non-`openai` provider. + +So a key-auth routed provider reaches `kind: "main"` + `substituteMainCredential: true` and +fails, exactly as reported. The defect is real and 010's fix direction stands; only its stack +position changes. + +## Correction 3 — supersede claims re-verified + +`gh pr view 2055`: `MERGED` at 2026-08-19T00:11:27Z, merge commit `2648ffa879edf93e`. #2063's +supersede stands. + +## Revised work-phase map + +| WP | Doc | Branch | Base | Content | +|---|---|---|---|---| +| wp2 | 010 | codex/fix-bearer-admission-2132 | dev | issue #2132 (score 96) | +| wp3 | 030 | codex/consolidate-prompt-cache-retention | dev | absorb #2102; supersede #2099, #2091 | +| wp4 | 040 | codex/absorb-capability-evidence | dev | absorb #2100, #2077 | +| wp5 | 050 | codex/absorb-k12-short-window | dev | absorb #2056; supersede #2062, #2063 | +| wp6 | 020 | codex/absorb-responses-id-backfill | dev | absorb #2131 + unique-id correction | +| wp7 | 060 | — | — | close-outs with attribution | + +Ordered by score, not by dependency, because no dependency exists. Each is independently +reviewable and independently mergeable, which is what DEV-STACK-01 actually asks for. + + +## Correction 4 — #2130 merged mid-cycle; #2029 is now superseded in fact, not in prospect + +`gh pr view 2130`: `MERGED` at 2026-08-19T17:25:26Z. The probe lane's verdict was conditional +("SUPERSEDED, once #2130 merges"); that condition is now satisfied. + +Consequence for `060`: **#2029 (@yzxcj797)** moves from a prospective close to an immediate one. +`dev` now carries `busUnreachable()` — a superset of #2029's two stderr strings — plus the +on-disk unit check that #2029's own reviewer demanded. Landing #2029 on top would REGRESS that +disk check back to an unconditional `absent`. Nothing from #2029 needs to be carried over; its +one unique behavior (keeping `DBUS_SESSION_BUS_ADDRESS not set` as `unknown`) is precisely what +the merged disk check replaces. + +This also removes #2130 from the open-bug-PR set: the fresh count at wp1 close is 26 open bug +PRs, of which exactly one (#2134) is lidge-jun's and 25 are not. + +## wp1 close-out evidence + +`gh pr list --repo lidge-jun/opencodex --state open --label bug --limit 100` at close: +26 total, mine = [2134], non-mine = 25. Every one of those 25 numbers appears in §1/§3 of this +document (verified by a grep loop over the list, exit 0). No open bug PR is left without a +disposition. + + +--- + +# A-gate amendment 2 — retired auditor returned late with VERDICT: FAIL; findings adjudicated + +The adversarial lane retired under DISPATCH-RETIRE-01 (three empty wait cycles) delivered after +retirement. Its verdict is FAIL. It is adjudicated here rather than discarded, because a late +reviewer is still a reviewer. + +**Findings 1 and 2 — CONFIRMED, and already corrected.** It independently measured the same +`gh pr diff --name-only` evidence and reached the same conclusion as amendment 1: #2131 does not +touch `core.ts`, no dependency edge exists, and rooting a stack on #2134 (which only touches +`agent-settings-routes.ts`) is a second DEV-STACK-01 violation. Two independent measurements now +agree. Recorded as settled. + +**Finding 5 — CONFIRMED, and it is the sharpest catch.** Doc 010 said to gate substitution on +"the native ChatGPT **pool**". That would exclude `codexAccountMode: "direct"` and re-break +#1686, whose whole point is that Direct bearer admission is only safe *because* substitution +still runs. The implemented fix uses `route.codexAccountMode !== undefined`, which covers both +`pool` and `direct` and matches the issue reporter's own suggested gate. 010's prose is +superseded by this line; the code is correct. + +**Finding 3 — CONFIRMED and material.** Overlap was measured only inside the absorb set. Three +OTHER open PRs edit `src/server/responses/core.ts`: + +| PR | Overlap with the #2132 fix | +|---|---| +| #2104 (@olddonkey) | `src/server/responses/core.ts` — review-ready, MERGEABLE | +| #2101 (@Ingwannu) | `core.ts` + `compact.ts` + `auth-context.ts` — all three files this fix touches | +| #2040 (@Ingwannu) | `core.ts` | + +Verified by `gh pr diff --name-only`. The #2132 change is 5 lines across two files and does not +restructure either function, so a textual conflict is possible but small. This is a merge-order +hazard to state on the PR, not a reason to withhold the fix. #2104 is reclassified from `n/a` +to KEEP (review-ready, not a conflicting draft — the auditor is right that grouping it with +CONFLICTING #2075 and draft #2127 was an error, and its inventory row's "adapters/xai" file +attribution was wrong). + +**Finding 4 — CONFIRMED. #2105 would have been lost.** It is scored 60 ABSORB in §3, has no +decade doc, and appears in no row of 060. An above-threshold item with no execution path is +exactly how a contributor's work disappears without a close comment. Disposition corrected to +**KEEP — remains open**, because no replacement exists. It is not closed. + +**Nits accepted:** "strict superset" overstates #2056 vs #2062 (#2062 uniquely adds +`tests/rate-limit-reset-credits.test.ts`); #2130 has empty `closingIssuesReferences` so +#1939/#2114/#2108 will not auto-close; the rubric is recorded as a single integer, so the +component arithmetic is not independently auditable. + +## Net effect on the plan + +No absorbed item is dropped and no new one is added. Two dispositions change (#2104 n/a -> KEEP, +#2105 ABSORB -> KEEP), one prose invariant in 010 is superseded by the implemented predicate, +and one merge hazard is now stated. The sibling shape from amendment 1 stands, reinforced. + diff --git a/devlog/_plan/260820_bug_pr_backlog_consolidation/010_layer1_bearer_admission_2132.md b/devlog/_plan/260820_bug_pr_backlog_consolidation/010_layer1_bearer_admission_2132.md new file mode 100644 index 0000000000..991c3bb62a --- /dev/null +++ b/devlog/_plan/260820_bug_pr_backlog_consolidation/010_layer1_bearer_admission_2132.md @@ -0,0 +1,70 @@ +> **SUPERSEDED IN PART — read `000_research_inventory.md` amendments 1 and 2 first.** +> +> Two things in this document are wrong and were corrected after it was written: +> +> 1. **It is NOT a stack layer and does NOT root on #2134.** No dependency edge exists; +> the shipped PR (#2137) is based on `dev` as a sibling. +> 2. **The substitution predicate is NOT "native ChatGPT pool".** Pool-only would exclude +> `codexAccountMode: "direct"` and re-break #1686, whose Direct admission is only safe +> BECAUSE substitution still runs. The shipped predicate is +> `route.codexAccountMode !== undefined`, covering pool AND direct. Do not "correct" it back. + + +# 010 — Layer 1 (stack bottom): fix issue #2132, bearer admission must not force a ChatGPT credential + +Work-phase: wp2. Branch: `codex/fix-bearer-admission-2132`. Base: `codex/fix-subagent-roster-truncation` (PR #2134). +Absorbs: nothing (no PR exists). Closes: #2132. + +## Why this is the stack bottom + +It is the highest-scoring item in the backlog (96) and it shares `src/server/responses/core.ts` +with layer 2 (#2131). Layer 2 must be based on this, or the two edits to that file collide. + +## Defect + +Reported in #2132: after v2.23.0, a key-auth provider (Cloudflare/etc.) returns 401 +`No usable Codex main credential` when `~/.codex/auth.json` holds no ChatGPT token. Bearer +admission sets `substituteMainCredential` unconditionally, so a route that needs no ChatGPT +identity is still gated on one. + +## P-phase re-verification required (stale check) + +Before editing, confirm against the CURRENT tree — the lane read `dev`, not this branch: +1. `rg -n "substituteMainCredential" src/` — enumerate every producer and consumer. +2. Read `src/server/responses/core.ts`, `src/server/responses/compact.ts`, + `src/codex/auth-context.ts` and establish where the flag is set and where it is read. +3. Reproduce the admission decision in a unit context with a key-auth provider and an + auth.json containing no ChatGPT token. If the current code does NOT reproduce, stop and + amend this doc rather than writing a fix for a defect that is not there. + +## Intended change + +Make the substitution conditional on the resolved route actually requiring a native/ChatGPT +credential. A key-auth routed provider carries its own credential and must be admitted +without one. Exact call sites are fixed during the stale check above; the invariant is: +`substituteMainCredential` is set only when the route's credential source is the native +ChatGPT pool. + +Out of scope: changing what happens once a native route legitimately lacks a credential, +and any change to the pool/account selection itself. + +## Test plan (must fail RED first) + +New `tests/bearer-admission-key-auth.test.ts`: +1. key-auth routed provider + auth.json with NO ChatGPT token -> request is admitted (no 401). +2. native gpt route + no ChatGPT token -> still fails closed with the existing error. +3. key-auth provider + ChatGPT token present -> unchanged behavior (no regression). + +Drive the file against the unpatched tree first and record the failure output; a test that +passes before the fix does not prove anything. + +## Verification + +`bun run typecheck`; `bun test --isolate` on the new file plus the existing responses/auth +suites; full `bun test --isolate tests` before marking review-ready; `bun run privacy:scan`. + +## Standalone thesis (DEV-STACK-03) + +"A provider that carries its own key must not be gated on a ChatGPT credential." Builds and +passes its own tests at its own tip, independent of layer 2. + diff --git a/devlog/_plan/260820_bug_pr_backlog_consolidation/020_layer2_responses_id_backfill_2131.md b/devlog/_plan/260820_bug_pr_backlog_consolidation/020_layer2_responses_id_backfill_2131.md new file mode 100644 index 0000000000..bb5c088378 --- /dev/null +++ b/devlog/_plan/260820_bug_pr_backlog_consolidation/020_layer2_responses_id_backfill_2131.md @@ -0,0 +1,43 @@ +# 020 — Layer 2: absorb PR #2131, backfill missing Responses output ids + +Work-phase: wp3. Branch: `codex/absorb-responses-id-backfill`. Base: `codex/fix-bearer-admission-2132` (layer 1). +Absorbs: **PR #2131 by @bet4it**. Closes: PR #2131 as superseded, with attribution. + +## Dependency edge (the only real one in this backlog) + +#2131 adds `src/server/responses/responses-field-backfill.ts` and calls it from +`src/server/responses/core.ts` — the same file layer 1 edits. This is why it stacks rather +than sitting beside layer 1. + +## Defect + +Strict decoders (grok-build) reject Responses output items that omit `id` on +`message` / `reasoning` / `function_call`. #1941 landed earlier but some relays still omit it. + +## Change to carry over + +@bet4it's implementation, preserved in substance: synthesize stable `msg_ocx_N` / `rs_ocx_N` / +`fc_ocx_N` ids keyed on `output_index`, never overwriting an id that is already present. + +## Correction to apply on top (audit finding, lane: quality) + +An invalid or missing `output_index` collapses to `0`, so two unindexed items can both become +`msg_ocx_0` — duplicate ids, which is the exact class of bug this fixes. Replace the +collapse-to-zero fallback with a monotonic per-response counter so synthesized ids are unique +even when `output_index` is absent or malformed. Add the regression test that pins it. + +Docs: the locale files in #2131 are uneven (EN/FR rewritten, JA/KO/ZH/TR only first sentence). +Carry only the EN change in this layer; locale parity is not this layer's thesis. + +## Test plan (must fail RED first) + +Carry @bet4it's tests (SSE `response.completed`, `output_item.done` via `output_index`, JSON +passthrough, preserve-existing-id, inherited `toString` type) and ADD: +- two items with missing `output_index` receive DISTINCT ids (fails on #2131 as written). + +## Verification + +Same gate as layer 1, plus explicit confirmation that layer 2's branch contains layer 1's +commit (`git log --oneline ..` shows only layer-2 commits) and that the PR +base ref names layer 1's branch. + diff --git a/devlog/_plan/260820_bug_pr_backlog_consolidation/030_sibling_prompt_cache_retention.md b/devlog/_plan/260820_bug_pr_backlog_consolidation/030_sibling_prompt_cache_retention.md new file mode 100644 index 0000000000..fc66d105c9 --- /dev/null +++ b/devlog/_plan/260820_bug_pr_backlog_consolidation/030_sibling_prompt_cache_retention.md @@ -0,0 +1,38 @@ +# 030 — Sibling A: consolidate prompt_cache_retention (issue #2092) + +Work-phase: wp4. Branch: `codex/consolidate-prompt-cache-retention`. Base: **dev** (sibling, not a stack layer). +Absorbs: **PR #2102 by @lilinxiong** (base implementation). Supersedes: **#2099 by @yzxcj797**, **#2091 by @luvs01**. Closes #2092. + +## Why a sibling and not a layer + +It touches only `src/adapters/openai-responses.ts`, which no other absorbed item touches. It has +no dependency on layers 1-2, so stacking it would impose a false merge order (DEV-STACK-01). + +## Chosen contract + +@lilinxiong's #2102: strip `prompt_cache_retention` only when +`forward && isCanonicalOpenAiForwardProvider(provider)` AND the model is `gpt-5.6` or +`gpt-5.6-*`. This matches the issue's own correction — the reporter withdrew the "strip +everywhere" claim, and some non-5.6 deployments still honor the field. + +Rejected: #2091's blanket strip for every forward provider and every model (it inverts the +existing gpt-5.5 preserve pin at tests/openai-responses-passthrough.test.ts:807). +Rejected: #2099's `startsWith("gpt-5.6")`, which also matches `gpt-5.60`, and its stray +package.json 2.24.2 -> 2.25.0 bump. + +## Carried from the superseded PRs + +From @yzxcj797's #2099: the `Fixes #2092` issue link and the repro-shaped fixture +(`store:false`, streamed input array). From @luvs01's #2091: nothing — its key-auth preserve +case is already covered by #2102. + +## Tightening to apply + +Replace the string-prefix family match with the catalog/native-slug predicate if one exists +in the current tree (`rg -n "isGpt56NativeSlug|NATIVE_OPENAI_MODELS" src/`); otherwise keep +the exact `gpt-5.6` / `gpt-5.6-*` match and pin `gpt-5.60` as a NON-match in tests. + +## Test plan (must fail RED first) + +Carry #2102's tests; add `gpt-5.60` non-match; keep the gpt-5.5 preserve pin intact. + diff --git a/devlog/_plan/260820_bug_pr_backlog_consolidation/040_sibling_routing_capability.md b/devlog/_plan/260820_bug_pr_backlog_consolidation/040_sibling_routing_capability.md new file mode 100644 index 0000000000..62f0016582 --- /dev/null +++ b/devlog/_plan/260820_bug_pr_backlog_consolidation/040_sibling_routing_capability.md @@ -0,0 +1,31 @@ +# 040 — Sibling B: routing capability + lab behavior evidence + +Work-phase: wp5. Branch: `codex/absorb-capability-evidence`. Base: **dev**. +Absorbs: **PR #2100 and PR #2077, both by @ntdatt812**. Closes both as superseded. + +## Why these two together, and why a sibling + +#2100 touches `src/routing/capability.ts`; #2077 touches +`src/routing/compatibility/behavior.ts`. Disjoint files, one author, one thesis: *model-keyed +lookups must use the same resolution rules the runtime uses*. Neither depends on layers 1-2. + +Note: #2077 is Lab-adjacent. Verify `tests/core-lab-boundary.test.ts` stays green — the file +already imports Lab types, so this must not newly puncture the boundary. + +## Defects + +#2100: bare map lookups made `gpt-oss:120b` inherit the provider-wide 8k window instead of the +`gpt-oss` family's 131072, and `noVisionModels` was ignored. +#2077: `map[modelId]` missed family/case overrides, and `constructor` resolved to +`Object.prototype.constructor`, making `jcsStringify` throw and silently dropping Lab subjects. + +## Change + +Route both through `modelRecordValue` / `isModelTextOnly` as @ntdatt812 wrote them. Prototype-id +safety (`constructor`, `toString`) is the load-bearing part; keep those tests verbatim. + +## Test plan + +Carry both test files. Confirm the exact-own maps (`modelPreferHostedTools`, +`modelOpenRouterRouting`) still do NOT family-spread. + diff --git a/devlog/_plan/260820_bug_pr_backlog_consolidation/050_sibling_k12_short_window.md b/devlog/_plan/260820_bug_pr_backlog_consolidation/050_sibling_k12_short_window.md new file mode 100644 index 0000000000..524d543b5c --- /dev/null +++ b/devlog/_plan/260820_bug_pr_backlog_consolidation/050_sibling_k12_short_window.md @@ -0,0 +1,25 @@ +# 050 — Sibling C: K12 short-window quota (issue #2047) + +Work-phase: wp6. Branch: `codex/absorb-k12-short-window`. Base: **dev**. +Absorbs: **PR #2056 by @Ingwannu**. Supersedes: **#2062 by @yzxcj797**. Closes #2047. + +## Chosen base + +#2056 is a strict superset of #2062: `snapshotHasShort`, partial-snapshot preservation, +`updateAccountQuota` carry, and the parse -> cache -> DTO path #2047 actually requires. #2062 +drops short on a later weekly/monthly partial snapshot and carries a stray version bump. + +## Blocker to fix before this can land (raised by the maintainer on both PRs) + +A short-only snapshot with `shortPercent: 0` scores `0` instead of `CODEX_UNKNOWN_USAGE_SCORE`, +so `pickLowestUsageAmong` prefers an account whose long windows are unverified. Fix: +include `shortPercent` in `computeCodexUsageScore` only when the plan's governing long window +is finite; otherwise return `CODEX_UNKNOWN_USAGE_SCORE`. Add the short-only regression. + +This blocker is why #2056 is absorbed-and-corrected rather than simply approved. + +## Also close + +**#2063 by @yzxcj797** — superseded by ALREADY-MERGED #2055 (`2648ffa87`), which classifies +`detail.code` with a stricter own-property lookup. Close with attribution; fold nothing. + diff --git a/devlog/_plan/260820_bug_pr_backlog_consolidation/060_supersede_and_close_operations.md b/devlog/_plan/260820_bug_pr_backlog_consolidation/060_supersede_and_close_operations.md new file mode 100644 index 0000000000..ec55998dfc --- /dev/null +++ b/devlog/_plan/260820_bug_pr_backlog_consolidation/060_supersede_and_close_operations.md @@ -0,0 +1,37 @@ +# 060 — Close-out: supersede operations and attribution + +Work-phase: wp7. No code. GitHub state only. + +## Ordering rule + +A PR is closed ONLY after its replacement exists and is pushed. Never close first. + +## Operations + +| Close | Author | Replaced by | Carried over | +|---|---|---|---| +| #2131 | @bet4it | layer 2 (020) | full implementation + tests, plus unique-id correction | +| #2099 | @yzxcj797 | sibling A (030) | issue link, repro fixture | +| #2091 | @luvs01 | sibling A (030) | nothing; contract deliberately narrower | +| #2100 | @ntdatt812 | sibling B (040) | full implementation + tests | +| #2077 | @ntdatt812 | sibling B (040) | full implementation + tests | +| #2102 | @lilinxiong | sibling A (030) | full implementation + tests (base) | +| #2062 | @yzxcj797 | sibling C (050) | nothing; #2056 supersedes | +| #2063 | @yzxcj797 | merged #2055 | nothing | +| #2056 | @Ingwannu | sibling C (050) | full implementation + scorer correction | +| #2029 | @yzxcj797 | maintainer PR #2130 | nothing; #2130 is a superset | + +## Comment template + +> Thanks for this, @ — closing as superseded by #, which carries from your +> patch. Your work is credited in that PR's description. + +## NOT closed, with reasons stated publicly + +- **#2109 / #2110** (@drakonkat): unresolved security gap in the override gate; needs a human + security pass (AGENTS.md security boundary). +- **#2053** (@Ingwannu): C4 OAuth surface; MAINTAINERS.md mandates security review. +- **#2101, #2040**: large (20 and 14 files); each needs its own PABCD cycle. +- **#2115, #2082, #2027, #2067, #2054, #2032**: below the 60 threshold. +- **#2104, #2075, #2127**: #2075/#2054 CONFLICTING; #2127 is an active draft by its author. + diff --git a/devlog/_plan/260820_bug_pr_backlog_consolidation/070_execution_log.md b/devlog/_plan/260820_bug_pr_backlog_consolidation/070_execution_log.md new file mode 100644 index 0000000000..cd146a2c99 --- /dev/null +++ b/devlog/_plan/260820_bug_pr_backlog_consolidation/070_execution_log.md @@ -0,0 +1,293 @@ +# 070 — Execution log: what actually shipped + +Appended as work-phases close. This is the record of landed state, distinct from the plan. + +## wp2 — issue #2132 (score 96) + +**PR #2137**, branch `codex/fix-bearer-admission-2132`, base `dev`. + +`substituteMainCredential` was computed from how the caller authenticated and never from where +the request routes, so a key-authenticated provider was gated on a ChatGPT credential it cannot +use. The predicate is now +`options.admission?.source === "bearer" && route.codexAccountMode !== undefined` +at both `core.ts:1088` and `compact.ts:325`. + +It covers `pool` AND `direct`. Doc 010's "native ChatGPT pool" wording would have excluded +`direct` and re-broken #1686, whose Direct admission is only safe because substitution still +runs. 010 now carries a banner saying so. + +Evidence: `tests/bearer-admission-routed-provider.test.ts` driven RED (it reproduced the exact +reported 401), full suite 13516 pass / 0 fail, typecheck and privacy scan clean. Re-audit round 2 +by the same adversarial reviewer returned **VERDICT: PASS**. + +## wp3 — issue #2092 (score 86) + +**PR #2138**, branch `codex/consolidate-prompt-cache-retention`, base `dev`. + +Absorbs @lilinxiong's #2102 contract: strip `prompt_cache_retention` on canonical ChatGPT +forward for the `gpt-5.6` family only, with an exact-or-dashed-prefix match so a future +`gpt-5.60` is not swept up. The retired value is not translated into `prompt_cache_options`. + +Evidence: 5 of the new tests fail when only the adapter change is reverted; the two narrowness +guards stay green in both directions, which is what makes them guards rather than restatements. +Full suite 13537 pass / 0 fail. + +### Closed with attribution + +| PR | Author | Superseded by | Carried | +|---|---|---|---| +| #2102 | @lilinxiong | #2138 | the implementation itself | +| #2099 | @yzxcj797 | #2138 | issue link + repro fixture | +| #2091 | @luvs01 | #2138 | nothing; contract deliberately narrower | +| #2029 | @yzxcj797 | merged #2130 | nothing; #2130 adds the disk check review demanded | +| #2063 | @yzxcj797 | merged #2055 | nothing; #2055 is the stricter own-property lookup | + +Each carries a comment naming the replacement and the specific reason, so no contributor has to +guess why their work closed. + +## Still open by decision, not omission + +- #2109 / #2110 (@drakonkat) — unresolved security gap in the override gate; needs a human pass. +- #2053 (@Ingwannu) — C4 OAuth; MAINTAINERS.md mandates security review. +- #2105 (@lilinxiong) — above threshold but no replacement exists yet; closing it now would lose work. +- #2101, #2040 — 20 and 14 files; each needs its own cycle. +- #2104 (@olddonkey) — review-ready and MERGEABLE; reclassified out of the deferred bucket, it is a + KEEP that deserves review rather than supersession. + +## Remaining work-phases + +wp4 (#2100 + #2077 capability evidence), wp5 (#2056 K12 with the scorer correction), wp6 (#2131 +responses id backfill with the duplicate-id fix). Each is a sibling off `dev`; none depends on +another. + + +## wp6 — PR #2131 (@bet4it) + +**PR #2142**, branch `codex/absorb-responses-id-backfill`, base `dev`. + +Carries @bet4it's implementation and tests, plus one correction: an absent or malformed +`output_index` collapsed to 0, so two such items both synthesized `msg_ocx_0` — duplicate ids, +the exact defect the backfill prevents. Unusable indices now take a monotonic ordinal based far +above any plausible real index. + +Evidence worth naming: applying ONLY @bet4it's original source and running the new suite gives +15 pass / 1 fail, and the single failure is the duplicate-id guard. That is what makes it a guard +rather than a restatement of behavior. + +The inherited assertion `expect(parsed.item.id).toBe("msg_ocx_0")` was replaced, not deleted +quietly, and the replacement is disclosed in the PR body. + +# Campaign state at wp6 close + +Superseded and closed with attribution: #2102, #2099, #2091, #2029, #2063, #2100, #2077, #2056, +#2062, #2131 — ten PRs, each with a comment naming its replacement and the specific reason. + +Opened: #2137 (#2132), #2138 (#2092), #2140 (#2100+#2077), #2141 (#2047), #2142 (#2131), plus +the pre-existing #2134. + +Deliberately still open: #2109/#2110 (security gap), #2053 (C4 OAuth review), #2105 (no +replacement written yet), #2101/#2040 (each needs its own cycle), #2104 (review-ready, deserves +review not supersession), and the below-threshold set (#2115, #2082, #2027, #2067, #2054, #2032, +#2075, #2127). + + +## wp7 — PR #2105 (@lilinxiong) + +**PR #2144**, branch `codex/absorb-claude-shell-hook-gate`, base `dev`. + +Implementation and tests carried unchanged. The one addition is a comment on +`reconcileShellHook` recording that "installed" is answered from the calling process's PATH, so +a service context with a stripped PATH can remove a hook an interactive shell would keep — the +reversible direction, and the one this reconcile wants. + +This closes the finding the auditor raised at #2105: it was scored ABSORB with no execution path +and would have been lost. It now has one. + + +# Campaign close — CI state and honest end state + +All six shipped PRs are green on exact head and MERGEABLE: + +| PR | Fixes | Checks | +|---|---|---| +| #2137 | issue #2132 | 25 pass / 0 fail | +| #2138 | issue #2092 (absorbs #2102) | 25 pass / 0 fail | +| #2140 | absorbs #2100 + #2077 | 25 pass / 0 fail | +| #2141 | issue #2047 (absorbs #2056) | 25 pass / 0 fail | +| #2142 | absorbs #2131 | 23 pass / 0 fail | +| #2144 | absorbs #2105 | 29 pass / 0 fail | + +#2140 first showed `npm-global-smoke` failing on windows-latest with +`EBUSY: resource busy or locked, unlink ...bun.exe` during dependency install — a Windows file +lock during Bun installation, not a defect in the routing change. Rerunning the failed jobs +turned it green, which is the evidence that it was infrastructure rather than the patch. + +## Eleven PRs closed with attribution + +#2102, #2099, #2091, #2029, #2063, #2100, #2077, #2056, #2062, #2131, #2105. + +Each carries a comment naming its replacement, what was carried over, and what was deliberately +not. Where a contributor's own assertion had to be replaced (#2056's `shortPercent: 0` scorer +case, #2131's `msg_ocx_0` collapse case), the replacement is disclosed in both the closing +comment and the superseding PR body rather than done silently. + +## Fourteen PRs deliberately still open + +- **Security holds:** #2109, #2110 (override gate), #2053 (C4 OAuth, MAINTAINERS.md review). +- **Own-cycle scale:** #2101 (20 files), #2040 (14 files). +- **Deserves review, not supersession:** #2104 — review-ready, MERGEABLE, and touching + `core.ts` alongside #2137. +- **Below the 60 threshold:** #2115, #2082, #2027, #2067, #2054, #2032, #2075, #2127. + +Nothing here is an omission. Every one is a recorded decision with a reason. + +## Merging + +Not done. DEV-STACK-04 and DEV-GIT-PUSH-01 both put merge authorization with the user, and +nothing in this campaign changes that. + + +## wp9 — PR #2101 (@Ingwannu): the ONE real stack layer + +**PR #2146**, branch `codex/absorb-account-entitlement-stacked`, base **`codex/fix-bearer-admission-2132`** (the #2137 branch), not `dev`. + +This is the single genuine dependency edge in the entire backlog. #2101 passes +`substituteMainCredentialForDirect: substituteMainCredential` into `resolveCodexAuthContext` — +the exact value #2137 corrects. Landing it on `dev` alone would silently reintroduce #2132 for +every routed provider. Everything else absorbed in this campaign was disjoint and shipped as a +sibling; this one is stacked because the code says so, not because a plan said so. + +Three corrections on top of @Ingwannu's work: + +1. **Selector compact bypassed the wire rewrite** — `accountGatedCompactWireModel` came from + `raw.model`, which never matches the gated map for `side/gpt-daybreak-blue-latest`, so a + selector-form compact still hit the native endpoint. Now derived from `route.modelId`. +2. **Direct callers evicted catalog evidence** — one 64-entry LRU shared between per-credential + Direct keys and the main/Pool keys the catalog projects from. Split into two eviction classes; + pinned by a test verified to fail against the shared LRU. +3. **Comment rot** — `native-models.ts` claimed routing never collapses Daybreak into + `gpt-5.6-sol`, which the wire normalization does. + +Evidence: full suite 13554 pass / 0 fail at the stacked tip; the composition check +(`codex-model-entitlements` + `bearer-admission-routed-provider` + `codex-auth-context` + +`server-auth`) is 146 pass / 0 fail, which is what proves the two layers agree. +Stack integrity: `git log parent..layer` shows exactly 1 commit, and a stack map was added to +#2137 so a reviewer arriving at the parent sees the chain. + +Two gaps named in the PR rather than carried silently: Direct `/v1/models` can still advertise a +Pool-only grant (advertisement only; dispatch still checks the caller credential), and +same-account gated-400 retry stays Pool-only. + + +## wp12 — PR #2053 (@Ingwannu), the C4 OAuth hold + +**PR #2149**, branch `codex/absorb-oauth-superseded-commit`, base `dev`. + +Applied unchanged, rebased from 145 commits behind. The persist-boundary placement is the whole +design: `assertBeforePersist` runs inside the file lock, after `fn(store)` and before +`persist()`, so a superseded flow's in-memory mutation is discarded rather than written. +Ownership is identity-checked against the flow's own `AbortController`, not a timestamp. + +**This was a wp1 HOLD and it is resolved by shipping, not by absorbing quietly.** The PR states +plainly that MAINTAINERS.md mandates security review and asks that it not be merged on my +verification alone, and it names three residuals rather than letting the original claim stand: + +1. the description claimed reauth coverage; the diff wires the hook but adds no reauth test +2. `OAuthLoginSupersededError` is not in the public allowlist, so it projects to the generic string +3. a never-finishing Kiro rollback blocks all replacements, by design + +Evidence: reverting `src/oauth/` fails 2 tests including the cancel-then-replace round trip; +full suite 13536 pass / 0 fail. + + +## wp14 — PR #2075 (@olddonkey): the second false negative + +**PR #2151**, branch `codex/absorb-fastwire-native-chat`, base `dev`. Closes #1886. + +Same class of error as wp13, different cause. I saw `CONFLICTING` and treated it as a reason not +to read the diff. The rescore put it at **67**: native `/v1/chat/completions` decided +`service_tier` from `chatServiceTier` alone, so a `supportsServiceTier: false` declaration was +fail-open. The conflict was why it could not MERGE, not why it should score LOW — and resolving +it took one import line. + +Rebase, stated exactly: `src/adapters/openai-chat.ts` conflicted because `dev` added +`AdapterTierMetadata` while the PR adds `decideTier` and `ResolvedFastPolicy`. Both kept. +Everything else clean. Typecheck is what confirms the resolution. + +Evidence: reverting `src/` fails the characterization test the author had flipped from +documented-known-bug to passing assertion. Full suite 13552 pass / 0 fail. + +## Two scoring lessons, recorded together + +wp13 and wp14 were both my errors, from two different shortcuts: + +1. **Scoring from titles** — "preserve and replay thought signatures" reads like bookkeeping and + was a core provider 400. +2. **Reading merge state as value** — `CONFLICTING` says a patch cannot land today; it says + nothing about whether the defect matters. + +Both produce false negatives that are indistinguishable from correct low scores without opening +the diff. The rubric was fine; the inputs I fed it were not. + + +# Campaign close (final) + +## 13 PRs open, all green, all MERGEABLE + +| PR | Fixes | Credit | Base | +|---|---|---|---| +| #2137 | issue #2132 | new work | dev | +| #2138 | issue #2092 | @lilinxiong | dev | +| #2140 | #2100 + #2077 | @ntdatt812 | dev | +| #2141 | issue #2047 | @Ingwannu | dev | +| #2142 | #2131 | @bet4it | dev | +| #2144 | #2105 | @lilinxiong | dev | +| #2145 | issue #1950 | @Ingwannu | dev | +| #2146 | issue #2097 | @Ingwannu | **#2137 branch (stacked)** | +| #2147 | issue #1886 | @olddonkey | dev | +| #2148 | #2109 + #2110 | @drakonkat | dev | +| #2149 | #2053 | @Ingwannu | dev | +| #2150 | issue #2125 | @agentHits | dev | +| #2151 | issue #1886 | @olddonkey | dev | + +Plus #2134, which opened this session. + +## 16 PRs closed with attribution + +#2102, #2099, #2091, #2029, #2063, #2100, #2077, #2056, #2062, #2131, #2105, #2040, #2101, +#2104, #2109, #2110, #2053, #2127, #2075. + +Every one carries a comment naming its replacement, what was carried over, and what was +deliberately not. Where a contributor's own assertion had to be replaced — #2141's scorer case, +#2142's `msg_ocx_0` case — the replacement is disclosed in both the comment and the PR body. + +## 6 remain, independently verified below threshold + +#2115 (58), #2082 (46), #2067 (38), #2054 (58), #2032 (37), #2027 (51). + +These are not omissions. A rescore lane read every diff and scored them against the same rubric; +it found exactly two false negatives in my original triage (#2127 at 83, #2075 at 67) and both +were absorbed as wp13 and wp14. The remaining six are genuinely below the line, and four of them +are additionally blocked (draft, CONFLICTING, or CHANGES_REQUESTED). + +Two of them carry real bugs attached to unabsorbable patches: #2054's Cursor context collapse +(#1527) and #2027's Go quota gating (#1924). The right move for both is a clean reimplementation +on `dev`, not absorbing a 19-file conflicting draft. That is stated rather than silently skipped. + +## Corrections made on top of contributor work + +Eight PRs shipped with fixes the originals were missing, each pinned by a test verified to fail +against the contributor's own source: + +- #2141 short-only scorer returning 0 instead of UNKNOWN +- #2142 duplicate `msg_ocx_0` from a collapsed index +- #2145 history-only arming and non-atomic SSE overflow +- #2146 selector compact bypassing the wire rewrite, Direct callers evicting catalog cache +- #2148 `allowPrivateNetwork` bypassing the HTTPS gate for public hosts +- #2138 `gpt-5.60` near-miss match + +## Not merged + +DEV-STACK-04 and DEV-GIT-PUSH-01 both put merge authorization with the user. #2137 must land +before #2146. + diff --git a/devlog/_plan/260820_bug_pr_backlog_consolidation/080_residual_dispositions.md b/devlog/_plan/260820_bug_pr_backlog_consolidation/080_residual_dispositions.md new file mode 100644 index 0000000000..a35be68067 --- /dev/null +++ b/devlog/_plan/260820_bug_pr_backlog_consolidation/080_residual_dispositions.md @@ -0,0 +1,315 @@ +# 080 — Residual dispositions: #2115, #2054, #2032, #2067, #2082, #2027, #2155 + +Unit: 260820_bug_pr_backlog_consolidation +Work-phases: wp15 (this doc's first three), wp16-wp19 (one PR each), wp20 (closeout). +Baseline: origin/dev; worktree branch `codex/fix-subagent-roster-truncation`. + +The wp1 rubric put six PRs below the 60 threshold and left them open. The user reviewed +that reasoning and issued explicit per-PR dispositions, plus one new arrival (#2155). This +doc records the dispositions and the evidence each one rests on. Evidence came from five +read-only gpt-5.6-sol investigation lanes that read every diff and the surrounding runtime. + +## wp15 — the three that need no new code + +### #2115 @louis-tepe — CLOSE + +The PR is titled as Code Mode edit guidance, but `src/adapters/openai-chat.ts:548` swaps the +local "hostname is exactly api.openai.com" test for a new `isCanonicalNativeOpenAIRoute` +predicate in `src/adapters/tool-catalog-nudge.ts:118-132`. That predicate is not +prompt-only. Through `messagesToChatFormat` (`openai-chat.ts:629`) it decides whether +developer messages stay as ordered `role: "developer"` entries or fold into the leading +system message; through `toolChoiceToChatFormat` (`:1232-1246`) it decides whether a single +required tool becomes a forced named function; and through `buildRequest` (`:1333`) it +decides native `reasoning_effort` versus gateway-style `reasoning`. One predicate change +therefore moves three wire semantics that have nothing to do with edit guidance. + +Second defect, independent of the first: `codeModeExecName` is withheld when a bare shell +bridge is present (`tool-catalog-nudge.ts:203-207`), but the new suffix injection checks +only `codeModeExecTool` (`:214-218`). A freeform `exec` sitting beside a top-level +`exec_command` still receives "targeted code edits" guidance even though the repository's +own predicate classifies that catalog as not Code Mode — a contract pinned at +`tests/tool-catalog-nudge.test.ts:114-123`. + +Blast radius if the guidance predicate is wrong: every routed Code Mode request on +Anthropic, Google/Vertex/Antigravity, Command Code, and every OpenAI-compatible host +without a literal `openai`/`chatgpt` DNS label — gateways, DeepSeek, Groq, Ollama, vLLM, +LM Studio, custom routes. Kiro (`src/adapters/kiro.ts:466-469`) picks up the reworded +generic sentence without being able to receive the suffix at all. + +The underlying request is legitimate. The implementation is not absorbable as-is because +the correct version is a narrower Code Mode seam that does not redefine native route +identity. + +### #2054 @keepitmello — STAY OPEN, probe requested + +The PR stores Cursor's returned `ConversationStateStructure` and replays it as the next +`AgentRunRequest.conversation_state` instead of rebuilding history every turn +(`src/adapters/cursor/protobuf-request.ts:823` on dev is the full-replay path). The +hypothesis is that full replay defeats Cursor's own checkpoint cache and produces the +large-context collapse in #1527. + +The PR proves the request construction changed — smaller `rootBytes` — and states plainly +that it did not reproduce the `kimi-k3` collapse or the 429. So the causal link is exactly +the thing still missing, and it is cheap for the author to capture: a matched three-turn +baseline-vs-head run at the issue's 75k-95k token shape, recording per turn the request +`conversation_id`, a digest and byte length of `conversation_state`, its +`root_prompt_messages_json` count, `turns` count, and `token_details.used_tokens`, against +the same fields on the response's `conversation_checkpoint_update`. The decisive comparison +is whether turn N+1's request state equals turn N's returned checkpoint while +`conversation_id` holds. `ocx debug provider on` / `ocx debug provider logs -f` +(`docs-site/src/content/docs/reference/cli/agents.md:102`, `src/lib/debug.ts:15`) already +carries the construction mode; exact state equality needs payload-free digests added +locally. + +Checkpoint reuse WITHOUT the collapse disappearing would refute the causal claim, which is +why the request is worth making rather than guessing. + +### #2032 @yzxcj797 — CLOSE + +This is a decision that was already made by a human, not a scoring call. The maintainer's +CHANGES_REQUESTED review says it directly: "Passing --dangerously-skip-permissions does not +create an OS sandbox" and "A viable revision needs a real sandboxed launch path, or it must +leave the vendor root guard intact." + +The diff injects `IS_SANDBOX=1` whenever the flag appears in argv (PR head +`src/cli/claude.ts:126-131`, `:329-331`) with no UID check and no sandbox establishment, +so it suppresses Claude Code's root guard while the child keeps ordinary root filesystem and +process access. The added tests (`tests/claude-cli.test.ts:271-295`) assert environment +assembly, not an isolation boundary. It also carries an unrelated `package.json` version +bump to 2.25.0. + +On dev, opencodex does not drop, refuse, or warn about the flag: `src/cli/dispatch.ts:500` +forwards trailing args and `src/cli/claude.ts:338` passes them through unchanged. The +refusal comes from Claude Code itself. A user who has genuinely isolated their environment +can already export `IS_SANDBOX=1`, because `buildClaudeEnv` starts from the caller's +environment (`src/cli/claude.ts:76`) and the docs promise exported variables win +(`docs-site/src/content/docs/guides/claude-code.md:56`). + +## wp15 outcome (executed) + +| PR | Action | Receipt | +|---|---|---| +| #2115 | CLOSED with reason | `issuecomment-5349122248`, state CLOSED | +| #2054 | comment only, left OPEN | `issuecomment-5349122708`, state OPEN | +| #2032 | CLOSED with reason | `issuecomment-5349122937`, state CLOSED | + +Verified by `gh pr view --json state` after the fact, not from the write's own exit code. + +## wp16 — #2067 @waw4303: ABSORB, and the reason is external corroboration + +The user's instruction was to check how **omniroute** — a separate open-source project +brokering free quota against the same upstream — builds these headers, then decide. That +turned out to be the decisive evidence, and it moved the answer. + +The PR head changed while the lane was reading it. The original commit `a5183abb` sent +`opencode-cli/1.0.0` / `cli` / `default`; the current head `6a79c42e` sends only +`User-Agent: opencode` alongside the existing `x-opencode-client: desktop`. + +omniroute (`diegosouzapw/OmniRoute`, commit `3d7ed7aa`, 2026-08-19) resolves the same +headers in `open-sse/executors/opencode.ts:408-448`: + +```ts +userAgent: process.env[envUAKey]?.trim() || process.env.OPENCODE_USER_AGENT?.trim() || "opencode", +client: process.env.OPENCODE_CLIENT?.trim() || "desktop", +project: process.env.OPENCODE_PROJECT?.trim() || "global", +``` + +It does not fetch or derive an installed CLI version at runtime. It falls back to a bare +unversioned `opencode`, preserves a real incoming `opencode-cli/` when one exists, +and lets an operator override via env. + +Three-way comparison: + +| Header | ours today | #2067 head | omniroute | +|---|---|---|---| +| `User-Agent` | absent (uncontrolled runtime default) | `opencode` | `opencode`, configurable, preserves real `opencode-cli/` | +| `x-opencode-client` | `desktop` | `desktop` | `desktop`, configurable | +| `x-opencode-project` | absent | absent | `global`, configurable | +| `x-opencode-request` | absent | absent | fresh UUID | +| `x-opencode-session` | absent | absent | conversation-derived or UUID | + +The important finding is the one that reverses a wp1 assumption. wp1 scored this 38 partly +because a pinned CLI version marker has a short shelf life — and that criticism was correct +against `a5183abb`. omniroute made the same mistake and then deliberately backed it out: +its July implementation (`234956dd`) used exactly `opencode-cli/1.0.0` / `cli` / `default`, +and PR #10571 replaced them with `opencode` / `desktop` / `global`. So the version pin is +not corroborated by an independent implementation; the *revised* values are, and by one that +arrived at them by retreating from the pin. + +That removes the "value with a short lifetime" objection entirely. What remains is a real +defect: we send no `User-Agent` at all today (`src/providers/registry.ts:2427`), so the +runtime default goes out uncontrolled, which is what the reporter's 429 is attributed to. + +Precedent for pinning a client fingerprint already exists here — Anthropic +(`src/adapters/anthropic.ts:936`, asserted at `tests/client-fingerprint.test.ts:120`), xAI +(`src/providers/xai-transport.ts:7`, `tests/xai-transport.test.ts:55`), Command Code with a +configurable fallback (`src/adapters/command-code.ts:482`). And the value stays +operator-overridable through the existing case-insensitive provider header override at +`src/server/management/provider-routes.ts:288`. + +Decision: **ABSORB the revised shape**, not the original. + +```ts +staticHeaders: { + "User-Agent": "opencode", + "x-opencode-client": "desktop", +} +``` + +Deliberately NOT copied from omniroute: `x-opencode-project`, `x-opencode-request`, +`x-opencode-session`. None is needed to fix the demonstrated failure, and adding a +conversation-derived session identifier is a privacy-relevant change that needs its own +evidence rather than a sibling project's precedent. + +### wp16 outcome — and the bug the absorb uncovered + +**PR #2160**, branch `codex/absorb-opencode-free-static-headers`, base `dev`. #2067 closed +with attribution (`issuecomment-5349492578`). + +The audit is the interesting part. The plan as written — add the header to the registry row — +passed my own reading and FAILED the reviewer, correctly. `staticHeaders` is documented at +`registry.ts:149` as "merged into every upstream request for this provider", and that was +false. It was copied at seed time only: `providerConfigSeed` writes the block once, +`enrichProviderFromCatalog` fills it only when the whole block is absent, and nothing merged +it at request time. `rg -n 'headers' src/router.ts` returned zero hits. + +Reproduced directly before accepting the finding: + +| persisted config | `routedProviderConfig("opencode-free", ...).headers` | +|---|---| +| no headers block | `undefined` | +| `{x-opencode-client: desktop}` | unchanged — no UA | +| `{user-agent: custom-agent}` | unchanged — no client marker | + +So the contributor's one-line registry patch would have shipped a header that **no existing +install ever receives**. The management API strips a persisted block that exactly matches the +registry set, which means the most common on-disk state is "no headers at all" — and that +state gained nothing. + +Implementation, three parts: + +1. `mergeRegistryStaticHeaders(staticHeaders, userHeaders)` in `registry.ts` — registry values + fill only names the user has not claimed, compared **case-insensitively**. That last word is + load-bearing: HTTP header names are case-insensitive but object keys are not, so spreading a + registry `User-Agent` over a user's `user-agent` leaves both keys and `Headers` serializes + them as `"custom-agent, opencode"` — a corrupted request wearing the costume of an override. +2. `routedProviderConfig` (`router.ts`) merges at resolve time. +3. `buildModelsRequest` (`oauth/index.ts`) does the same, because a provider identified as + `opencode` when it completes but anonymous when it lists its own models reads as two + different clients to a rate limiter. + +Residual, stated rather than skipped: `validateApiKey` (`key-providers.ts:102`) still sends +only `Authorization`. It is an auth probe by design; widening an auth-path request shape is a +separate change with its own review burden. + +Evidence: 6 new regressions; reverting only `router.ts` + `oauth/index.ts` while keeping the +registry header fails exactly 5 of them (13 pass / 5 fail), which is what makes them delivery +tests rather than restatements of the registry constant. Full suite 13519 pass / 10 skip / +0 fail across 856 files; typecheck and privacy scan clean. + +One existing expectation moved: `tests/management-provider-validation.test.ts` "provider PATCH +clear keeps registry static headers" now asserts the two-header set. That is the same edit +#2067 made, and it is the correct one — the test pins the registry-owned set, which grew. + +## wp17-wp19 — the three that need new code + +Recorded here as each is decided; each is its own PABCD cycle. + +### wp17 — #2082 @yzxcj797: AgentRouter language framing + +**PR #2162**, branch `codex/absorb-agentrouter-language-framing`, base `dev`. #2082 closed +(`issuecomment-5349709140`). Fixes #2074. + +The diagnosis was the contributor's and it was correct: AgentRouter answers 400 +`content-blocked` on a non-English first user message while the same request in English +returns 200, and the filter reads that turn, so an Anthropic `system` string cannot reach it. + +Two corrections. + +**Host predicate.** `hostname.includes("agentrouter")` also matches `notagentrouter.example` +and `agentrouter.org.attacker.example`. This is a prompt mutation keyed on a provider's +identity, so the key has to be that identity exactly — otherwise an unrelated destination +quietly receives an injected instruction block. Now `agentrouter.org` or a real subdomain. + +**Where the marker goes.** The original spliced it into the user's string: +`firstUser.content = \`\${MARKER}\\n\\n\${firstUser.content}\``. That edits what the user +wrote, and every downstream reader then attributes a sentence to them that they never typed — +the hidden user-turn mutation named in #1804. The framing is now its own leading text block. +It still adds content to the user turn, which is unavoidable against a filter that reads the +first user message, but additive-and-visible is a different risk class than a silent rewrite. + +Idempotence is keyed on the LEADING block being exactly the marker, not a substring test: a +user who quotes the marker mid-prompt must not suppress their own framing. + +Evidence: 10 regressions; reverting only the adapter fails 7. The 3 that stay green are the +lookalike-host and direct-Anthropic cases — green on unpatched `dev` precisely because `dev` +frames nobody, which is what makes them guards against the substring predicate rather than +restatements of it. Full suite 13529 pass / 10 skip / 0 fail; typecheck and privacy clean. + +`CONFLICTING` was an inherited `package.json` bump alone; the Anthropic hunks merge cleanly. +No version change in the replacement. + +### wp18 — #2027 @yzxcj797: OpenCode Go quota, planned + +The investigation moved the answer here too. The real issue is #1924: sibling rows +(`opencode-go-2` … `-5`) show no quota in the dashboard and no rows in +`ocx provider quota --refresh --json`, because dispatch gates on the literal provider NAME at +`src/providers/quota.ts:2087`. + +The contributor's fix swaps that for a base-URL comparison. Closer, but it does not check the +adapter, so a row pointed at the canonical URL with a different adapter would be probed. + +The repository already has the exact predicate: `registryEntryForProviderDestination` +(`registry.ts:2678`) identifies a renamed fixed key provider by normalized endpoint + adapter ++ auth mode, and is already the convention for renamed rows +(`opencode-zen-rate-limit.ts:28-43`, `derive.ts:398-425`). + +Rejected alternative, recorded: `providerMatchesRegistryTransport("opencode-go", provider)` +would need `preserveCustomDestination: true` on the registry entry, which also changes ROUTING +for a same-named custom row (`router.ts:269-274` vs `:320-336`). That may be worth doing, but +not as a side effect of a quota fix. + +The defensive canonical-URL check inside `fetchOpenCodeGoQuota` (`quota.ts:485-494`) stays: it +is what stops an API key being sent to a non-canonical host, and it should not depend on the +dispatch predicate being correct. + +### wp18-wp20 — the rest of the chain + +**#2027 @yzxcj797 -> PR #2164.** Dispatch gated on the literal name `opencode-go`, so the +multi-account sibling rows in #1924 had no quota panel and no CLI report. The contributor's +base-URL swap is closer but does not check the adapter; `registryEntryForProviderDestination` +already answers exactly this question (endpoint + adapter + key auth) and is the existing +convention for renamed rows. Rejected: `providerMatchesRegistryTransport` would need +`preserveCustomDestination`, which also changes routing for same-named custom rows. + +**#2155 @waw4303 -> PR #2165.** Field validation ran before the pending-call lookup, so a +non-string repeat of an already-canonical field killed the turn with a 502. Two corrections: +`arguments` was gated on a canonical NAME (a name is not evidence about the arguments field, +so a real payload could be dropped) — now keyed on `sawArgumentsString`; and `id` stayed +unconditionally terminal. Diagnostics now come from the rejection site, because a stateless +rescan blamed call 0's accepted padding for call 1's real defect. + +**#2163 @Ingwannu -> PR #2166.** Scored 65. Backend attribution was correct; sanitization sat +at the one call site rather than in the logging layer, so `/api/logs` carried the raw +caller-supplied value. Moved into `addFinalRequestLog`. #2157 stays open: the GUI half is not +built, and closing it would claim an affordance that does not exist. + +### The stack + +Six layers, each rebased onto the current `dev` tip, base refs verified: + +#2134 -> #2160 -> #2162 -> #2164 -> #2165 -> #2166 + +Only one true dependency edge exists in the whole set (none of the six share files). They are +chained rather than opened as siblings because the user asked for one reviewable stack; that is +a review-workflow choice, stated rather than dressed up as a code constraint. + +A privacy-scan failure caught in CI and not locally: a test fixture API key over 24 characters +reads as a real bearer token to `scripts/privacy-scan.ts`. Fixed at the L4 commit. + +### End state + +`gh pr list --label bug --state open` returns only lidge-jun PRs plus #2054, which stays open +by explicit instruction and carries the wire-probe request. Nine contributor PRs closed with +attribution across this unit; none was closed without a named reason. + diff --git a/devlog/_plan/260820_bug_pr_backlog_consolidation/100_release_audit.md b/devlog/_plan/260820_bug_pr_backlog_consolidation/100_release_audit.md new file mode 100644 index 0000000000..f90fbc99a8 --- /dev/null +++ b/devlog/_plan/260820_bug_pr_backlog_consolidation/100_release_audit.md @@ -0,0 +1,198 @@ +# 100 — Release-safety audit of origin/main..origin/dev + +Unit: 260820_bug_pr_backlog_consolidation +Range: `8e01dd4e8..a584890f8` — 87 commits, 22 merges, 108 changed files. +Verification host: `ssh lidge:~/ci-wp3/opencodex`. Nothing heavy ran on the workstation. + +Most of this range landed hours earlier through an admin-override merge run, so it had never +been read as one body of work. Five passes, each with its own evidence. A pass that says "looks +fine" without its own artifact is not a pass. + +## Verdict + +**Ship-able after two fixes**, both of which are in this record with a RED-proven regression. +Neither was found by per-PR CI, and one of them could not have been: it only exists when two +independently-correct changes compose. + +## Pass 1 — security boundary (7 credential-touching PRs) + +| PR | Verdict | +|---|---| +| #2137 bearer admission | SAFE — narrows substitution. `admission.source === "bearer"` requires an exact match against `OPENCODEX_API_AUTH_TOKEN` or a configured key, so it is not caller-selectable by an unauthenticated party. | +| #2147 xAI OAuth 401 replay | SAFE — refresh is pinned to the rejected snapshot's account, one replay (an `if`, not a loop), refreshed token never logged. | +| #2149 OAuth commit ownership | SAFE — assertion runs under the lock with no `await` before persistence, fails closed, atomic 0600 rename. | +| #2164 OpenCode Go quota | SAFE — the widened predicate still requires exact URL + adapter + key auth, and the fetch ignores the configured URL entirely in favor of a compile-time constant with `redirect: "error"`. | +| #2166 shadow marker | **FINDING, FIXED** — see below. | +| #2146 entitlement discovery | FINDING, recorded not fixed — see "Deliberately left". | +| #2148 baseUrl override | FINDING, recorded not fixed — see "Deliberately left". | + +### FIXED — #2166: caller-controlled text reached a durable log + +The shadow-call intercept matches by **prefix**, so a caller can send `gpt-5.6-luna` plus +arbitrary trailing text and still be intercepted. The whole raw string was recorded as +`shadowCallRewrittenFrom`, which is persisted to `usage.jsonl` and served from `/api/logs`. + +The sanitizer on that path is not sufficient, for two independent reasons — both run against +the shipped code, not reasoned about: + +``` +"\n" -> control stripped, value redacted +"\n" -> control stripped, value INTACT +``` + +(Written as shapes rather than literals on purpose: `scripts/privacy-scan.ts` matches those +patterns wherever they appear, so pasting a real-looking transcript breaks the `gates` job for +every branch cut from `dev`. That is precisely what #2175 had to undo, and this record +reintroduced it — the scanner does not care that a credential is fake.) + +1. Control characters are stripped **before** redaction, so the newline that separated marker + from credential is gone by the time the `Bearer` rule looks for a word boundary. +2. The runtime redactor is a deny-list. An `AIza`-shaped Google key has no rule and survives. + +Fix (PR #2170): record the operator-configured prefix that matched, via +`shadowSourceModelPrefix()`. The field can then only hold a value the operator configured. +That removes the class instead of adding one more pattern to a deny-list — which matters, +because the next unrecognized credential family would reopen a pattern-based fix. + +RED proof: reverting the two source files fails exactly the two new tests, 15 pass / 2 fail. + +## Pass 2 — cross-PR interaction + +The shared-contract map found six files touched by 2+ PRs. Three interactions were reproduced; +two were already closed by the time of this audit, one was not. + +**Closed already, recorded for the history:** an admission bearer could escape through a +custom-named canonical transport, because #2137 decided substitution from the provider NAME +while the adapter recognized the same row by TRANSPORT. Two predicates answering one question. +Fixed by #2169, verified present at the current `dev` tip. + +**FIXED here — `tool_search_call` got the wrong id namespace.** #2145 restores a lowered +tool_search as `tool_search_call` with no id; #2142's universal backfill then names it. The +backfill's prefix table had no entry for the type, so it produced `item_ocx_0`: + +``` +{"type":"tool_search_call",...} -> id: "item_ocx_0" +{"type":"function_call",...} -> id: "fc_ocx_0" +``` + +Not cosmetic: `stripInvalidItemIds` deletes any id whose prefix does not match its type, and +it lists `tsc_` as the only valid prefix for that type. The id survived the turn that created +it and was silently dropped on the next one, leaving the client an item it could not correlate. +`custom_tool_call` had the identical gap. Fix: PR #2173, with the superset invariant between +the two tables written at the table. + +Both PRs' focused suites pass in isolation — neither composes restoration with backfill. This +is the whole argument for a cross-PR pass existing. + +**No interaction found**, each with its reason: #2160 static headers × #2148 overrides (neither +overridden provider has static headers); #2160 × #2164 (opencode-go owns no static headers); +#2166 log field × #2147 tier writers (independent fields, no last-writer overwrite); #2151 × +#2165 (request construction vs request-local parseStream state, no shared lifecycle); #2138 × +#2145 (canonical-forward vs noncanonical, activation sets do not intersect); #2149 × #2147 +(both account- and generation-guarded). + +## Pass 3 — default-install wire/behavior regression + +**PASS.** Byte-identical on both revisions for: OpenAI-compatible key provider, direct Anthropic +key provider, Google AI Studio, plain native Chat, unsupported caller `service_tier`, caller +reasoning `high`. Verified by SHA-256 over the built request. + +Six differences found, every one attributed to an intending PR: AgentRouter framing (#2162, +exact-host gated — a non-AgentRouter Anthropic request is byte-identical), `opencode-free` +static headers (#2160, the only registry row with them), `service_tier: priority` on an exact +Fast-capable model (#2151), `prompt_cache_retention` removal on canonical GPT-5.6 (#2138), +tool_search lowering on noncanonical upstreams (#2145), xAI OAuth Responses routing (#2147). + +`shadowCallRewrittenFrom` appears in `usage.jsonl` only when the explicitly-enabled shadow gate +matches; an ordinary request writes the same key set as before. + +## Pass 4 — repository invariants + +All green on `ssh lidge`: `tests/core-lab-boundary.test.ts` + `tests/repo-hygiene.test.ts` +24 pass / 0 fail; `bun run privacy:scan` passed; `bun x tsc --noEmit` exit 0; no `160000` +gitlink in the tree; no `src/lab/` file changed in the range and every protected core path +stayed transitively Lab-free. + +## Pass 5 — release mechanics + +Version line `2.27.0` on both `main` and `dev` — coherent, and this campaign did not move it. +No half-finished migration found. No behavior observed that depends on a particular branch +being checked out. + +## Deliberately left — not fixed, recorded with the reason + +**#2146 — entitlement discovery uses more accounts than the request needs.** An authenticated +`/v1/models`, or any account-gated request, enumerates main plus every pool row with a +syntactically valid id and queries each one's credential concurrently. A request bound to +account B can therefore cause account A's token to be refreshed, persisted, and sent to ChatGPT +discovery. Paused and needs-reauth accounts are not excluded. + +Each token stays paired with its own `chatgptAccountId`, so this is not credential +misbinding, and no token reaches a log or the cache (only model sets and SHA-256 fingerprints). +It is excessive credential use with a cross-account side effect. Narrowing the candidate set to +the accounts a request actually needs is a product decision about what `/v1/models` is meant +to enumerate — that is a NEEDS_HUMAN call, not something to infer from the code. + +**#2148 — an old config silently activates on upgrade.** `allowBaseUrlOverride` is registry-only +and never persisted, and the router honors any already-saved resolved `baseUrl` the moment the +flag appears. Reproduced: + +``` +routedProviderConfig("anthropic", {baseUrl:"https://relay.example.com/v1"}) -> honored +routedProviderConfig("google-antigravity", {baseUrl:"https://relay.example.com/v1"}) -> honored +``` + +So a custom URL that older releases accepted and ignored starts receiving the OAuth bearer +after upgrade, with no consent step. The transport gate does hold: public cleartext HTTP is +refused for these providers, and URL userinfo is rejected at config validation. + +Scope, measured rather than assumed: this range newly opts in exactly **two** providers +(`anthropic`, `google-antigravity`). Thirteen others already had the flag on `main`, so the +upgrade-activation shape is pre-existing behavior, not introduced here. Whether it needs a +persisted consent marker or a release note is a product decision. + +**link-local / unspecified addresses under `allowPrivateNetwork`.** `https://169.254.1.1` and +`https://0.0.0.0` are permitted when the flag is set, which reads oddly against the policy +comment. Confirmed **pre-existing**: the classification and the waiver are both unchanged in +this range. Recorded so it is not rediscovered as new, but it is not this release's regression. + +## Fixes pushed + +| Finding | PR | RED proof | +|---|---|---| +| #2166 caller-controlled marker reaches usage.jsonl and /api/logs | #2170 | 15 pass / 2 fail with the fix reverted | +| tool_search_call / custom_tool_call id namespace | #2173 | 20 pass / 2 fail with the fix reverted | + +Full suite at the tip carrying both: **13717 pass / 15 skip / 0 fail** across 866 files; +typecheck exit 0; privacy scan passed. All on `ssh lidge`. + + +## Closeout — the fixes are on dev + +All four landed in dependency order. The order was forced, not chosen: `privacy:scan` runs in +the `gates` job, so while `dev` itself was failing it, every branch cut from `dev` inherited +the failure. #2173 was red for exactly that reason and went green once #2175 landed. + +| PR | dev merge commit | What it fixes | +|---|---|---| +| #2175 | `5bcc91d0e` | the broken `privacy:scan` gate on `dev` itself | +| #2170 | `9eb6647d5` | caller-controlled marker reaching `usage.jsonl` and `/api/logs` | +| #2173 | `b2878f8e8` | `tool_search_call` / `custom_tool_call` id namespace | +| #2174 | `12c14d5c3` | this audit record | + +Verified at the `dev` tip on `ssh lidge`: + +- `bun run test` — **13719 pass / 15 skip / 0 fail** across 866 files. +- `bun x tsc --noEmit` — exit 0. +- `bun run privacy:scan` — passed. It **failed** on `dev` before #2175, which is the whole + reason that PR exists. +- GitHub CI run `32334852749` — completed **success** at `b2878f8e8`, the commit carrying both + code fixes. `12c14d5c3` above it is docs-only. +- Fixes present in `dev` source, not just in a merge commit: `shadowSourceModelPrefix` ×1, + `tool_search_call` ×3. + +The three findings under "Deliberately left" are unchanged and still open questions. Nothing in +this closeout resolves them; they need a product decision, not a patch. + +Release execution remains unauthorized: no `scripts/release.ts`, no publish, no tag, no change +to `main` or `preview`. diff --git a/devlog/_plan/260820_bug_pr_backlog_consolidation/100_release_safety_audit.md b/devlog/_plan/260820_bug_pr_backlog_consolidation/100_release_safety_audit.md new file mode 100644 index 0000000000..8ee286d849 --- /dev/null +++ b/devlog/_plan/260820_bug_pr_backlog_consolidation/100_release_safety_audit.md @@ -0,0 +1,122 @@ +# 100 — Release-safety audit of `main..dev` + +Unit: 260820_bug_pr_backlog_consolidation +Range audited: `origin/main` `8e01dd4e8` → `origin/dev`, 92 commits / 22 merges / 109 files. + +Most of this range landed in one admin-override merge run hours earlier. It had never been +read as a single body of work, which is the only reason this audit is worth its cost: a merge +run answers "does each PR pass" and this answers "is the result shippable". + +Four passes, four read-only lanes plus direct verification in the main agent. Every finding +below was reproduced before it was fixed; nothing was patched on a lane's word alone. + +## 1. Security boundary — ONE REAL LEAK, FIXED + +**An admission bearer reached ChatGPT.** `#2169`. + +Two predicates were answering one security question with different inputs: + +| Question | Predicate | Keyed on | +|---|---|---| +| "must we substitute the stored credential?" | `route.codexAccountMode !== undefined` | provider **name** | +| "may we forward the caller's Authorization?" | `isCanonicalOpenAiForwardProvider` | adapter + authMode + **base URL** | + +A provider row named anything other than `openai`, pointed at the canonical ChatGPT backend +with `authMode: "forward"`, satisfies the second and fails the first. Substitution was skipped +and the adapter forwarded our own proxy secret. Reproduced through the real adapter: + +``` +URL: https://chatgpt.com/backend-api/codex/responses +AUTH: Bearer +LEAKED: true +``` + +This violates the contract stated in `src/codex/auth-context.ts`: an admission bearer is +replaced with the stored main credential or the request fails before any I/O. Substitution now +consults the same authority the adapter does. + +Worth recording how nearly this was missed: **two of three lanes classified #2137 SOUND.** One +of them read `openai-responses.ts:1451` and concluded "only the canonical ChatGPT destination +may relay caller credentials" — true, and exactly the problem, because canonical-ness is +decided by URL while substitution was decided by name. A correct observation, a wrong verdict. +The majority was wrong and the minority had a reproduction; the reproduction won. + +**Recorded, deliberately not patched:** #2148 lets an OAuth bearer reach any operator-configured +HTTPS host, with no vendor allowlist. Both lanes agree this is operator authorization by +definition and it is the stated point of the PR. It is a WEAKENED-by-design boundary, not a +defect, and narrowing it is a product decision rather than an audit fix. + +## 2. Cross-PR interaction — PASS + +The merge run already proved this defect class exists here (the id backfill mutated the compact +endpoint's wire format; neither branch failed alone). A dedicated hunt over 21 same-file overlap +pairs plus cross-file wire/predicate overlaps found no second instance. 366 pass / 0 fail across +13 composed files. + +## 3. Default-install behavior — PASS + +Every ungated change traces to the fix that intended it. Specifically checked, because these +were the plausible accidents: + +- AgentRouter framing fires only for `agentrouter.org` and its real subdomains. +- `opencode-free` is the only registry row with `staticHeaders`, so no other provider's headers moved. +- The shell hook now installs **less** often than before, not more — it is gated on an installed CLI. +- The tool-catalog nudge is byte-identical in this range. + +One documented side effect: #2146's cold entitlement lookup adds an authenticated `/models` +fetch during catalog sync. Bounded, fail-closed, and no credentials means no fetch. + +## 4. Repository invariants — PASS, with TWO GUARD DEFECTS FIXED + +The invariants themselves hold: core reaches no Lab path from any of the three protected roots +(192 / 101 / 344 modules walked, dynamic edges followed), zero gitlinks, zero vendored clones, +privacy signature scan clean. + +But two of the mechanisms we rely on were quietly broken. `#2171`. + +**The core/Lab guard had a directory bypass.** It matched only `/lab/`, and `src/lab/index.ts` +exists — so `import("../lab")` resolves to the Lab entrypoint and matched nothing: + +``` +source : void import("../lab"); +Guard 1 detects : false +``` + +The self-test that claims to protect the guard re-declared its own copy of the regex. A copy +cannot fail when the original drifts, which is precisely how a guard rots while looking green. +Both now call one shared predicate. + +**The release helper's documented path could never complete.** A dry run bumps, commits, and +pushes deliberately, so the `--publish` re-run it instructs you to do hits +`npm version ` and exits `Version not changed` before dispatching. The bump and +push are now a desired end state rather than mandatory actions. + +Neither defect came from this range. Both would have been trusted by the next release. + +## Contributor PR merged during the audit + +`#2167` (@ntdatt812) — a background `/wham/usage` 200 was retracting a reauth quarantine that +Responses traffic had set. A 200 from the usage endpoint proves the token authenticates *there*; +it does not prove the account can serve Responses, which still answers 403 for a workspace the +token can no longer select. The account went straight back into rotation, failed identically, +and re-quarantined — so `needsReauth` never settled. Correct diagnosis, correct fix, and it +pins both directions. + +## Coverage limits, stated rather than implied + +`privacy:scan` is a **signature** scan. It detects `/Users//` paths, email shapes, long +`Bearer` literals, and selected `sk-`/`ghp_`/JWT forms in tracked text files. It does not see +`/home/...` paths, arbitrary key prefixes, short secrets, runtime dataflow, `usage.jsonl` +contents, or whether a new log field carries PII at runtime. A green scan is evidence about +literals in the tree, not about behavior. + +## Verdict + +The range is shippable **after** #2169. Before it, a specific configuration leaked a proxy +credential to a third party, and no test would have caught it. + +Version line on `dev` is `2.27.0`, equal to the published `latest` and the `v2.27.0` tag on +`main` — the release helper takes the next version as an argument, so this is the expected +pre-release state, not a half-finished bump. + +This remains a readiness record. No release was executed. diff --git a/devlog/_plan/260820_bug_pr_backlog_consolidation/assets/2157-logs-intercepted-badge.png b/devlog/_plan/260820_bug_pr_backlog_consolidation/assets/2157-logs-intercepted-badge.png new file mode 100644 index 0000000000..d2810138f7 Binary files /dev/null and b/devlog/_plan/260820_bug_pr_backlog_consolidation/assets/2157-logs-intercepted-badge.png differ diff --git a/devlog/_plan/260820_bug_pr_backlog_consolidation/assets/2157-logs-intercepted-filtered.png b/devlog/_plan/260820_bug_pr_backlog_consolidation/assets/2157-logs-intercepted-filtered.png new file mode 100644 index 0000000000..86de3cdfe5 Binary files /dev/null and b/devlog/_plan/260820_bug_pr_backlog_consolidation/assets/2157-logs-intercepted-filtered.png differ diff --git a/docs-site/src/content/docs/fr/guides/claude-code.md b/docs-site/src/content/docs/fr/guides/claude-code.md index 9579f84a9a..8a5ccf79b2 100644 --- a/docs-site/src/content/docs/fr/guides/claude-code.md +++ b/docs-site/src/content/docs/fr/guides/claude-code.md @@ -142,7 +142,10 @@ par le proxy sans nécessiter le wrapper `ocx claude`. Les shells déjà ouverts `ocx stop` et l'arrêt du proxy **suppriment les variables injectées** (ils ne restaurent pas les valeurs précédentes — seules les clés injectées par opencodex sont supprimées). Le proxy écrit également `~/.opencodex/claude-env.sh` ; -`ocx start` installe un hook source `.zshrc` qui le charge automatiquement. +`ocx start` installe le hook source `.zshrc` uniquement lorsqu’un exécutable Claude Code CLI est +présent dans le `PATH`. Au démarrage et avec `ocx ensure`, le hook appartenant à OpenCodex est supprimé +si aucun exécutable du CLI Claude Code n’est trouvé dans le `PATH` ou si l’intégration de l’environnement +système est inactive. Claude Desktop utilise son propre profil et ne déclenche pas l’installation du hook shell. Désactivez cette intégration avec `claudeCode.systemEnv: false` dans la configuration ou avec le commutateur de l'interface. La fonctionnalité est réservée à macOS ; sur les autres plateformes, utilisez `ocx claude`. diff --git a/docs-site/src/content/docs/guides/claude-code.md b/docs-site/src/content/docs/guides/claude-code.md index 8f5a305a44..7c3a86dea3 100644 --- a/docs-site/src/content/docs/guides/claude-code.md +++ b/docs-site/src/content/docs/guides/claude-code.md @@ -142,7 +142,10 @@ requiring the `ocx claude` wrapper. Already-open shells are unaffected and must `ocx stop` and proxy shutdown **unset the injected keys** (it does not restore previous values — only the keys opencodex injected are removed). The proxy also writes `~/.opencodex/claude-env.sh`; -`ocx start` installs a `.zshrc` source hook that loads it automatically. +`ocx start` installs a `.zshrc` source hook that loads it automatically only when an executable +Claude Code CLI is present on `PATH`. Startup and `ocx ensure` remove the OpenCodex-owned hook when +Claude Code is absent or system environment integration is inactive. Claude Desktop uses its +separate profile and does not cause shell-hook installation. Disable with `claudeCode.systemEnv: false` in the configuration or with the GUI toggle. This feature is macOS-only; on other platforms, use `ocx claude`. diff --git a/docs-site/src/content/docs/guides/codex-app-models.md b/docs-site/src/content/docs/guides/codex-app-models.md index 0a5008f6fa..b0910bd9d5 100644 --- a/docs-site/src/content/docs/guides/codex-app-models.md +++ b/docs-site/src/content/docs/guides/codex-app-models.md @@ -23,9 +23,17 @@ the bare or API-key model list. The row is matched on the field shape a real cat which filters malformed entries — it does not prove the id came from an upstream response, since the cache is a user-owned file. See [Exact Codex account selectors](/reference/configuration/routing/#exact-codex-account-selectors). -`gpt-daybreak-blue-latest` follows that observation-only rule for account-qualified rows and is not -added to the bare native allowlist. A separate, explicit `customModels` entry can expose the same -wire id as `openai/gpt-daybreak-blue-latest` through the canonical Codex-login forward provider: +`gpt-daybreak-blue-latest` is account-gated. opencodex checks each authenticated ChatGPT account's +own Codex model roster before advertising or routing it. In Pool mode, the bare row exists only when +at least one eligible Pool account reports the slug. In Direct mode, the bare row follows the main +account used by the local catalog, and each request also checks the forwarded caller credential (or +the stored main credential when an OpenCodex admission bearer is substituted). A +`/gpt-daybreak-blue-latest` row exists only when that selector's mapped account reports it. +Pool routing excludes unentitled accounts. If no roster can be confirmed, the gated row fails closed +instead of spending a prompt on an upstream 400. + +A separate, explicit `customModels` entry can expose the same wire id as +`openai/gpt-daybreak-blue-latest` through the canonical Codex-login forward provider: ```json { @@ -41,8 +49,8 @@ wire id as `openai/gpt-daybreak-blue-latest` through the canonical Codex-login f Only that exact provider, endpoint, and model id receive the pinned Sol capability snapshot: 922,000 context, 829,800 automatic compaction, the native reasoning ladder, and native Codex tool -metadata. The request still sends `gpt-daybreak-blue-latest`; opencodex does not rewrite it to Sol, -does not create a bare row, and does not grant account entitlement. The separately billed +metadata. The request still sends `gpt-daybreak-blue-latest`; opencodex does not rewrite it to Sol +or grant account entitlement. The separately billed `openai-apikey/daybreak-blue-latest` API row is a different route and its 1,050,000 / 922,000 limits are never copied into the Codex-login row. @@ -68,7 +76,7 @@ gpt-5.6-sol # bare Codex-login route via Pool or Direct /gpt-5.6-sol # stored Codex account mapped by that selector openai-apikey/gpt-5.6-sol # API key openai/gpt-daybreak-blue-latest # explicit Codex-forward custom row (922,000) -/gpt-daybreak-blue-latest # observed account-qualified native id, when available +/gpt-daybreak-blue-latest # account-qualified native id, only when that account reports it openai-apikey/daybreak-blue-latest # separate API-key route (1,050,000 / 922,000) ``` diff --git a/docs-site/src/content/docs/guides/grok-build.md b/docs-site/src/content/docs/guides/grok-build.md index 08a1073805..f1192b73e2 100644 --- a/docs-site/src/content/docs/guides/grok-build.md +++ b/docs-site/src/content/docs/guides/grok-build.md @@ -3,10 +3,10 @@ title: Grok Build description: Use any opencodex-routed model from xAI's Grok Build CLI — models are auto-registered into ~/.grok/config.toml while the proxy runs. --- -opencodex serves an OpenAI-compatible `POST /v1/chat/completions` (and `/v1/responses`) on its -local port, and Grok Build supports custom models against OpenAI-compatible servers. Starting -with this integration, opencodex registers its whole visible catalog into Grok Build -automatically — no manual config editing required. +opencodex serves an OpenAI-compatible `POST /v1/responses` on its local port, and Grok Build +supports custom models against OpenAI-compatible servers. Starting with this integration, +opencodex registers its whole visible catalog into Grok Build automatically — no manual config +editing required. ## Auto-registration @@ -18,7 +18,7 @@ into `~/.grok/config.toml`: [model.ocx-gpt-5-6-sol] model = "gpt-5.6-sol" base_url = "http://127.0.0.1:10100/v1" -api_backend = "chat_completions" +api_backend = "responses" api_key = "opencodex-loopback" name = "OCX gpt-5.6-sol" # ... one [model.ocx-*] table per visible model ... @@ -61,12 +61,11 @@ in Codex. Models with an empty tier list keep no effort control, matching Codex behavior. Native GPT-5.6 entries are separate: they preserve and expose their pinned upstream reasoning ladders rather than provider-configured routed metadata. -Grok Build talks to opencodex over Chat Completions and sends `reasoning_effort` when -the ladder is advertised. The Chat Completions inbound translator defaults the internal -Responses `reasoning.summary` to `auto` in that case, so thinking traces reach Grok as -`delta.reasoning_content` instead of being hidden. Set `include_reasoning: false` (or -`reasoning.summary: "none"`) if a client wants the model to think without returning the -trace. An explicit `reasoning.summary` wins when both knobs are present. +Grok Build talks to opencodex over the Responses API. When the route advertises a reasoning +ladder, the Responses passthrough forwards `reasoning.summary` as configured, so thinking +traces reach Grok natively as Responses reasoning items. Set `reasoning.summary: "none"` if +a client wants the model to think without returning the trace. An explicit `reasoning.summary` +wins over the route default. ## Authentication note diff --git a/docs-site/src/content/docs/ja/guides/claude-code.md b/docs-site/src/content/docs/ja/guides/claude-code.md index 4254e9a906..532419e23f 100644 --- a/docs-site/src/content/docs/ja/guides/claude-code.md +++ b/docs-site/src/content/docs/ja/guides/claude-code.md @@ -37,7 +37,9 @@ ocx claude `ocx stop` とプロキシ終了は**注入されたキーを解除します**。以前の値を復元せず、opencodex が 注入したキーのみ削除します。プロキシは `~/.opencodex/claude-env.sh` も書き出し、`ocx start` はこのファイルを -自動で読み込む `.zshrc` source hook をインストールします。 +自動で読み込む `.zshrc` source hook を、実行可能な Claude Code CLI が `PATH` にある場合にのみインストールします。 +Claude Code が存在しない場合、またはシステム環境連携が無効な場合、起動処理と `ocx ensure` は OpenCodex が追加した +hook を削除します。Claude Desktop は独立した profile を使用し、shell hook のインストールを引き起こしません。 設定で `claudeCode.systemEnv: false` に指定するか GUI トグルでオフにできます。この機能は macOS 専用で、他のプラットフォームでは `ocx claude` を使ってください。 diff --git a/docs-site/src/content/docs/ja/reference/adapters.md b/docs-site/src/content/docs/ja/reference/adapters.md index e637c4cc26..54bd07eef8 100644 --- a/docs-site/src/content/docs/ja/reference/adapters.md +++ b/docs-site/src/content/docs/ja/reference/adapters.md @@ -43,8 +43,13 @@ interface ProviderAdapter { ## `openai-responses` -**対象:** OpenAI **Responses API**。**`passthrough: true`** — 元のリクエスト本文をそのまま渡し、レスポンスを **変換せずに** ストリーミングします。 -**認証:** `forward`(呼び出し元ヘッダー中継)または `key`。 +**対象:** OpenAI **Responses API**。**`passthrough: true`** — 通常は元のリクエストとレスポンスをそのまま渡し、ルーティング先ゲートウェイに必要な限定的な互換変換だけを適用します。 +**認証:** canonical OpenAI `forward` は安全な呼び出し元ヘッダー許可リストだけを中継します。非 canonical な `forward` は呼び出し元の authorization を中継せず、設定済みの静的ヘッダーだけを使用します。`key` は設定済み provider key を使用します。 + +非 canonical な Responses ゲートウェイには、Codex のクライアント実行型 `tool_search` +宣言を既存の公開 function tool と衝突しない名前で送り、対応するリクエスト履歴と JSON/SSE +function call をクライアント向けの非公開 `tool_search` ライフサイクルに復元します。 +canonical OpenAI forward はネイティブな非公開型を維持します。 `key` 認証では、[`retryOn429`](/ja/reference/configuration/) もここに適用されます: プリストリームの 429 は、翻訳された `openai-chat` / Anthropic リクエスト経路と同様に、他の処理やフェイルオーバーに diff --git a/docs-site/src/content/docs/ko/guides/claude-code.md b/docs-site/src/content/docs/ko/guides/claude-code.md index a3bab95851..c5aae96d36 100644 --- a/docs-site/src/content/docs/ko/guides/claude-code.md +++ b/docs-site/src/content/docs/ko/guides/claude-code.md @@ -62,7 +62,9 @@ macOS의 자동 연결(`claudeCode.systemEnv`)도 같은 방식으로 판단하 `ocx stop`과 프록시 종료는 **주입된 키를 해제해요**. 이전 값을 복원하지는 않고 opencodex가 주입한 키만 제거해요. 프록시는 `~/.opencodex/claude-env.sh`도 작성하고, `ocx start`는 이 파일을 -자동으로 불러오는 `.zshrc` source hook을 설치해요. +자동으로 불러오는 `.zshrc` source hook을 실행 가능한 Claude Code CLI가 `PATH`에 있을 때만 설치해요. +Claude Code가 없거나 시스템 환경 연동이 비활성화되어 있으면 시작 과정과 `ocx ensure`가 OpenCodex가 추가한 +hook을 제거해요. Claude Desktop은 별도 profile을 사용하며 shell hook 설치를 유발하지 않아요. 설정에서 `claudeCode.systemEnv: false`로 지정하거나 GUI 토글로 끌 수 있어요. 이 기능은 macOS 전용이며, 다른 플랫폼에서는 `ocx claude`를 사용하세요. diff --git a/docs-site/src/content/docs/ko/reference/adapters.md b/docs-site/src/content/docs/ko/reference/adapters.md index 58c61cd7e4..06eedf2773 100644 --- a/docs-site/src/content/docs/ko/reference/adapters.md +++ b/docs-site/src/content/docs/ko/reference/adapters.md @@ -49,9 +49,16 @@ interface ProviderAdapter { ## `openai-responses` -**대상:** OpenAI **Responses API**. **`passthrough: true`** — 원본 요청 본문을 전달하고 응답을 -**변환하지 않은 채** 스트리밍합니다. -**인증:** `forward`(호출자 헤더 중계) 또는 `key`. +**대상:** OpenAI **Responses API**. **`passthrough: true`** — 일반적으로 원본 요청과 응답을 +그대로 전달하되, 라우팅된 게이트웨이에 필요한 좁은 호환성 변환만 적용합니다. +**인증:** 정규 OpenAI `forward`는 안전한 호출자 헤더 허용 목록만 중계합니다. 비정규 +`forward`는 호출자 authorization을 중계하지 않고 설정된 정적 헤더만 사용하며, `key`는 +설정된 provider 키를 사용합니다. + +비정규 Responses 게이트웨이에는 Codex의 클라이언트 실행형 `tool_search` 선언을 공개 function +도구와 충돌하지 않는 이름으로 전달합니다. 일치하는 요청 기록과 JSON/SSE function call은 +클라이언트용 비공개 `tool_search` 수명 주기로 복원합니다. 정규 OpenAI forward 경로는 +네이티브 비공개 타입을 그대로 유지합니다. `key` 인증에서는 [`retryOn429`](/ko/reference/configuration/)도 여기에 적용됩니다: 사전 스트림 429는 번역된 `openai-chat`/Anthropic 요청 경로와 동일하게 다른 처리나 페일오버보다 먼저 diff --git a/docs-site/src/content/docs/reference/adapters.md b/docs-site/src/content/docs/reference/adapters.md index 8a9bd05c45..0895ad07e8 100644 --- a/docs-site/src/content/docs/reference/adapters.md +++ b/docs-site/src/content/docs/reference/adapters.md @@ -49,9 +49,16 @@ provider — xAI, Kimi, DeepSeek, GLM, Groq, OpenRouter, Ollama (local & cloud), ## `openai-responses` -**Targets:** the OpenAI **Responses API**. **`passthrough: true`** — forwards the raw request body and -streams the response back **untranslated**. -**Auth:** `forward` (relay the caller's headers) or `key`. +**Targets:** the OpenAI **Responses API**. **`passthrough: true`** — normally forwards the raw request +body and response, with narrow compatibility rewrites for routed gateways. +**Auth:** canonical OpenAI `forward` relays only the safe caller-header allowlist; noncanonical +`forward` uses configured static headers without relaying caller authorization; `key` uses the +configured provider key. + +Noncanonical Responses gateways receive Codex's client-executed `tool_search` declaration as a +collision-safe public function tool. Matching request history and JSON/SSE function calls are +translated back to the private `tool_search` lifecycle for the client. Canonical OpenAI forward +keeps the native private type unchanged. For `key` auth, [`retryOn429`](/reference/configuration/) applies here too: a pre-stream 429 waits and replays the identical request on the same key before any other handling, exactly like diff --git a/docs-site/src/content/docs/ru/guides/claude-code.md b/docs-site/src/content/docs/ru/guides/claude-code.md index c8e88c1947..46bc060c40 100644 --- a/docs-site/src/content/docs/ru/guides/claude-code.md +++ b/docs-site/src/content/docs/ru/guides/claude-code.md @@ -39,7 +39,10 @@ ocx claude `ocx stop` и остановка прокси **снимают внедрённые ключи** (прежние значения не восстанавливаются — удаляются только ключи, внедрённые opencodex). Прокси также записывает `~/.opencodex/claude-env.sh`; -`ocx start` устанавливает source-хук в `.zshrc`, который загружает этот файл автоматически. +`ocx start` устанавливает source-хук в `.zshrc` только когда исполняемый Claude Code CLI найден в +`PATH`. При запуске и выполнении `ocx ensure` хук, принадлежащий OpenCodex, удаляется, если файл Claude +Code CLI не найден в `PATH`, не имеет права на выполнение или системная интеграция окружения выключена. +Claude Desktop использует отдельный профиль и не вызывает установку shell-хука. Отключить можно параметром `claudeCode.systemEnv: false` в конфигурации или переключателем в GUI. Функция доступна только на macOS; на других платформах используйте `ocx claude`. diff --git a/docs-site/src/content/docs/tr/guides/claude-code.md b/docs-site/src/content/docs/tr/guides/claude-code.md index 918ccaba62..e335bea33e 100644 --- a/docs-site/src/content/docs/tr/guides/claude-code.md +++ b/docs-site/src/content/docs/tr/guides/claude-code.md @@ -172,8 +172,11 @@ olan kabuklar etkilenmez ve yeniden açılmalıdır. `ocx stop` ve proxy'nin kapatılması **enjekte edilen anahtarları kaldırır** (önceki değerleri geri yüklemez — yalnızca opencodex'in enjekte ettiği anahtarlar kaldırılır). Proxy ayrıca `~/.opencodex/claude-env.sh` dosyasını -yazar; `ocx start`, bunu otomatik olarak yükleyen bir `.zshrc` kaynak kancası -kurar. +yazar; `ocx start`, bunu otomatik olarak yükleyen `.zshrc` kaynak kancasını +yalnızca çalıştırılabilir Claude Code CLI `PATH` içinde bulunduğunda kurar. +Claude Code yoksa veya sistem ortamı entegrasyonu etkin değilse başlangıç işlemi +ve `ocx ensure`, OpenCodex tarafından eklenen kancayı kaldırır. Claude Desktop ayrı +bir profil kullanır ve shell kancasının kurulmasını tetiklemez. Yapılandırmada `claudeCode.systemEnv: false` ile veya GUI anahtarıyla devre dışı bırakın. Bu özellik yalnızca macOS içindir; diğer platformlarda `ocx claude` @@ -605,4 +608,3 @@ aracının `model` argümanını değil, `` yönergelerin kullanır. Yönergenin hedeflenen rotayla eşleştiğinden emin olun. Model yer tutucusu olarak `"haiku"` iletin. - diff --git a/docs-site/src/content/docs/zh-cn/guides/claude-code.md b/docs-site/src/content/docs/zh-cn/guides/claude-code.md index 1bb5ef6acf..08fb965f27 100644 --- a/docs-site/src/content/docs/zh-cn/guides/claude-code.md +++ b/docs-site/src/content/docs/zh-cn/guides/claude-code.md @@ -37,7 +37,9 @@ shell 不受影响,必须重新打开。 `ocx stop` 和代理关闭操作会**取消设置已注入的键**(不会恢复之前的值——只会移除 opencodex 注入的键)。代理还会写入 `~/.opencodex/claude-env.sh`;`ocx start` 会安装一个 `.zshrc` -source hook,以自动加载该文件。 +source hook,以自动加载该文件,但仅限 `PATH` 中存在可执行的 Claude Code CLI。Claude Code +不存在或系统环境集成未启用时,启动过程和 `ocx ensure` 会移除 OpenCodex 自己写入的 hook。 +Claude Desktop 使用独立 profile,不会触发 shell hook 安装。 可以在配置中设置 `claudeCode.systemEnv: false`,或使用 GUI 开关来禁用。此功能仅适用于 macOS;在其他平台上,请使用 `ocx claude`。 diff --git a/docs-site/src/content/docs/zh-cn/reference/adapters.md b/docs-site/src/content/docs/zh-cn/reference/adapters.md index 07dbfcf441..5786952810 100644 --- a/docs-site/src/content/docs/zh-cn/reference/adapters.md +++ b/docs-site/src/content/docs/zh-cn/reference/adapters.md @@ -45,9 +45,14 @@ interface ProviderAdapter { ## `openai-responses` -**目标:** OpenAI **Responses API**。**`passthrough: true`** —— 转发原始请求 body,并把响应 -**不经转换**地流式传回。 -**认证:** `forward`(转发调用方 header)或 `key`。 +**目标:** OpenAI **Responses API**。**`passthrough: true`** —— 通常原样转发请求与响应,仅对 +路由网关应用范围有限的兼容性转换。 +**认证:** 规范 OpenAI `forward` 只转发安全的调用方 header allowlist;非规范 `forward` 不会 +转发调用方 authorization,只使用已配置的静态 header;`key` 使用已配置的 provider key。 + +对于非规范 Responses 网关,Codex 的客户端执行型 `tool_search` 声明会作为公共 function tool +以不与现有 function 名称冲突的方式发送;匹配的请求历史和 JSON/SSE function call 会恢复为 +客户端私有的 `tool_search` 生命周期。规范 OpenAI forward 路径仍保持原生私有类型不变。 使用 `key` 认证时,[`retryOn429`](/zh-cn/reference/configuration/) 同样适用:流开始前的 429 会等待并先于其他处理或故障转移,在相同 key 上重放完全相同请求,与翻译后的 diff --git a/docs-site/src/content/docs/zh-tw/guides/claude-code.md b/docs-site/src/content/docs/zh-tw/guides/claude-code.md index bc21c430f1..be06746fba 100644 --- a/docs-site/src/content/docs/zh-tw/guides/claude-code.md +++ b/docs-site/src/content/docs/zh-tw/guides/claude-code.md @@ -113,7 +113,9 @@ shell 不受影響,必須重新開啟。 `ocx stop` 和代理關閉操作會**取消設定已注入的鍵**(不會恢復之前的值——只會移除 opencodex 注入的鍵)。代理還會寫入 `~/.opencodex/claude-env.sh`;`ocx start` 會安裝一個 `.zshrc` -source hook,以自動載入該檔案。 +source hook,以自動載入該檔案,但僅限 `PATH` 中存在可執行的 Claude Code CLI。Claude Code +不存在或系統環境整合未啟用時,啟動程序和 `ocx ensure` 會移除 OpenCodex 自己寫入的 hook。 +Claude Desktop 使用獨立 profile,不會觸發 shell hook 安裝。 可以在設定中設定 `claudeCode.systemEnv: false`,或使用 GUI 開關來停用。此功能僅適用於 macOS;在其他平臺上,請使用 `ocx claude`。 diff --git a/gui/src/i18n/de.ts b/gui/src/i18n/de.ts index 113fc419d2..d6c503134a 100644 --- a/gui/src/i18n/de.ts +++ b/gui/src/i18n/de.ts @@ -638,6 +638,9 @@ export const de: Record = { "logs.filter.surface.claude": "Claude", "logs.filter.surface.codex": "Codex", "logs.filter.surface.grok": "Grok", + "logs.filter.interceptedHelpersOnly": "Nur abgefangene Helfer", + "logs.badge.interceptedHelper": "I · {model}", + "logs.badge.interceptedHelperTitle": "Abgefangene Helfer-Anfrage", "logs.filter.conversation.label": "Konversation", "logs.filter.conversation.placeholder": "Konversations-ID einfügen", "logs.filter.conversation.clear": "Löschen", diff --git a/gui/src/i18n/en.ts b/gui/src/i18n/en.ts index 997fcdc806..87892599ce 100644 --- a/gui/src/i18n/en.ts +++ b/gui/src/i18n/en.ts @@ -671,6 +671,9 @@ export const en = { "logs.filter.surface.claude": "Claude", "logs.filter.surface.codex": "Codex", "logs.filter.surface.grok": "Grok", + "logs.filter.interceptedHelpersOnly": "Intercepted helpers only", + "logs.badge.interceptedHelper": "I · {model}", + "logs.badge.interceptedHelperTitle": "Intercepted helper request", "logs.filter.conversation.label": "Conversation", "logs.filter.conversation.placeholder": "Paste conversation id", "logs.filter.conversation.clear": "Clear", diff --git a/gui/src/i18n/fr.ts b/gui/src/i18n/fr.ts index 112fd7ad2a..4da3bc33f1 100644 --- a/gui/src/i18n/fr.ts +++ b/gui/src/i18n/fr.ts @@ -652,6 +652,9 @@ export const fr: Record = { "logs.filter.surface.claude": "Claude", "logs.filter.surface.codex": "Codex", "logs.filter.surface.grok": "Grok", + "logs.filter.interceptedHelpersOnly": "Assistants interceptés uniquement", + "logs.badge.interceptedHelper": "I · {model}", + "logs.badge.interceptedHelperTitle": "Requête d'assistant interceptée", "logs.filter.conversation.label": "Conversation", "logs.filter.conversation.placeholder": "Coller l’ID de conversation", "logs.filter.conversation.clear": "Effacer", diff --git a/gui/src/i18n/ja.ts b/gui/src/i18n/ja.ts index 69fdac1c93..a65cc76d6b 100644 --- a/gui/src/i18n/ja.ts +++ b/gui/src/i18n/ja.ts @@ -614,6 +614,9 @@ export const ja: Record = { "logs.filter.surface.claude": "Claude", "logs.filter.surface.codex": "Codex", "logs.filter.surface.grok": "Grok", + "logs.filter.interceptedHelpersOnly": "インターセプトされたヘルパーのみ", + "logs.badge.interceptedHelper": "I · {model}", + "logs.badge.interceptedHelperTitle": "インターセプトされたヘルパー要求", "logs.filter.conversation.label": "会話", "logs.filter.conversation.placeholder": "会話 ID を貼り付け", "logs.filter.conversation.clear": "クリア", diff --git a/gui/src/i18n/ko.ts b/gui/src/i18n/ko.ts index 9f491a1efc..ba19d179e8 100644 --- a/gui/src/i18n/ko.ts +++ b/gui/src/i18n/ko.ts @@ -657,6 +657,9 @@ export const ko: Record = { "logs.filter.surface.claude": "Claude", "logs.filter.surface.codex": "Codex", "logs.filter.surface.grok": "Grok", + "logs.filter.interceptedHelpersOnly": "가로챈 헬퍼만", + "logs.badge.interceptedHelper": "I · {model}", + "logs.badge.interceptedHelperTitle": "가로챈 헬퍼 요청", "logs.filter.conversation.label": "대화", "logs.filter.conversation.placeholder": "대화 ID 붙여넣기", "logs.filter.conversation.clear": "지우기", diff --git a/gui/src/i18n/ru.ts b/gui/src/i18n/ru.ts index 00aa9cb06b..f41af16d00 100644 --- a/gui/src/i18n/ru.ts +++ b/gui/src/i18n/ru.ts @@ -655,6 +655,9 @@ export const ru: Record = { "logs.filter.surface.claude": "Claude", "logs.filter.surface.codex": "Codex", "logs.filter.surface.grok": "Grok", + "logs.filter.interceptedHelpersOnly": "Только перехваченные помощники", + "logs.badge.interceptedHelper": "I · {model}", + "logs.badge.interceptedHelperTitle": "Перехваченный запрос помощника", "logs.filter.conversation.label": "Диалог", "logs.filter.conversation.placeholder": "Вставьте ID диалога", "logs.filter.conversation.clear": "Сбросить", diff --git a/gui/src/i18n/tr.ts b/gui/src/i18n/tr.ts index 743be2af77..101dc684c4 100644 --- a/gui/src/i18n/tr.ts +++ b/gui/src/i18n/tr.ts @@ -662,6 +662,9 @@ export const tr: Record = { "logs.filter.surface.claude": "Claude", "logs.filter.surface.codex": "Codex", "logs.filter.surface.grok": "Grok", + "logs.filter.interceptedHelpersOnly": "Yalnizca yakalanan yardimcilar", + "logs.badge.interceptedHelper": "I · {model}", + "logs.badge.interceptedHelperTitle": "Yakalanan yardimci istegi", "logs.filter.conversation.label": "Sohbet", "logs.filter.conversation.placeholder": "Sohbet ID'sini yapıştırın", "logs.filter.conversation.clear": "Temizle", diff --git a/gui/src/i18n/zh-TW.ts b/gui/src/i18n/zh-TW.ts index c195aead79..89b891d603 100644 --- a/gui/src/i18n/zh-TW.ts +++ b/gui/src/i18n/zh-TW.ts @@ -509,6 +509,9 @@ export const zhTW: Record = { "logs.filter.surface.claude": "Claude", "logs.filter.surface.codex": "Codex", "logs.filter.surface.grok": "Grok", + "logs.filter.interceptedHelpersOnly": "僅已攔截的輔助請求", + "logs.badge.interceptedHelper": "I · {model}", + "logs.badge.interceptedHelperTitle": "已攔截的輔助請求", "logs.filter.conversation.label": "對話", "logs.filter.conversation.placeholder": "貼上對話 ID", "logs.filter.conversation.clear": "清除", diff --git a/gui/src/i18n/zh.ts b/gui/src/i18n/zh.ts index 7dd4beb860..c5d588efa0 100644 --- a/gui/src/i18n/zh.ts +++ b/gui/src/i18n/zh.ts @@ -650,6 +650,9 @@ export const zh: Record = { "logs.filter.surface.claude": "Claude", "logs.filter.surface.codex": "Codex", "logs.filter.surface.grok": "Grok", + "logs.filter.interceptedHelpersOnly": "仅已拦截的辅助请求", + "logs.badge.interceptedHelper": "I · {model}", + "logs.badge.interceptedHelperTitle": "已拦截的辅助请求", "logs.filter.conversation.label": "会话", "logs.filter.conversation.placeholder": "粘贴会话 ID", "logs.filter.conversation.clear": "清除", diff --git a/gui/src/pages/Logs.tsx b/gui/src/pages/Logs.tsx index 55a7ef634d..4ec0aede31 100644 --- a/gui/src/pages/Logs.tsx +++ b/gui/src/pages/Logs.tsx @@ -133,6 +133,15 @@ export interface LogEntry { provider: string; surface?: LogSurface; conversationId?: string; + /** + * The original helper model, when Shadow Call Intercept rewrote this request. + * + * Present ONLY for an intercepted request. A helper request that was not intercepted -- + * interception off, no replacement model, or a slug the matcher does not recognize -- is + * indistinguishable here from ordinary traffic, which is why the filter below says + * "intercepted" rather than "helper". + */ + shadowCallRewrittenFrom?: string; requestedEffort?: string; effectiveEffort?: string; reasoningWireField?: string; @@ -174,6 +183,7 @@ function validCachedLogs(cached: LogEntry[] | null): LogEntry[] | null { || typeof entry.provider !== "string" || typeof entry.status !== "number" || typeof entry.durationMs !== "number" + || (entry.shadowCallRewrittenFrom !== undefined && typeof entry.shadowCallRewrittenFrom !== "string") || !validCachedRouteDecision(entry.routeDecision) ) { return null; @@ -355,6 +365,7 @@ export default function Logs({ apiBase }: { apiBase: string }) { const [autoRefresh, setAutoRefresh] = useState(true); const [detail, setDetail] = useState(null); const [surfaceFilter, setSurfaceFilter] = useState("all"); + const [interceptedHelpersOnly, setInterceptedHelpersOnly] = useState(false); const [conversationFilter, setConversationFilter] = useState(""); const [conversationQueryHash, setConversationQueryHash] = useState(); const scrollContainerRef = useRef(null); @@ -472,6 +483,7 @@ export default function Logs({ apiBase }: { apiBase: string }) { const filteredLogs = logs.filter(log => ( logMatchesSurface(log, surfaceFilter) + && (!interceptedHelpersOnly || Boolean(log.shadowCallRewrittenFrom)) && (!conversationQuery || matchesLogConversationId(log.conversationId, conversationQuery, conversationQueryHash)) )); const conversationTotals = conversationQuery ? summarizeFilteredLogs(filteredLogs) : null; @@ -566,6 +578,20 @@ export default function Logs({ apiBase }: { apiBase: string }) { ))} + {/* + "Intercepted", not "helper". The marker only exists when Shadow Call Intercept + rewrote the request, so a helper request that was not intercepted looks exactly like + ordinary traffic here. A broader label would promise a classification this data + cannot support. + */} +