From a0eb776c5865e0d00b4353e4f6d01c47ba8146d0 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Wed, 19 Aug 2026 09:08:24 +0900 Subject: [PATCH 1/3] docs(devlog): 260819 triage-execution campaign roadmap (r2-audited) --- .../_plan/260819_triage_execution/000_plan.md | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 devlog/_plan/260819_triage_execution/000_plan.md diff --git a/devlog/_plan/260819_triage_execution/000_plan.md b/devlog/_plan/260819_triage_execution/000_plan.md new file mode 100644 index 0000000000..5743a2662f --- /dev/null +++ b/devlog/_plan/260819_triage_execution/000_plan.md @@ -0,0 +1,63 @@ +# 000 — 260819 triage-execution campaign plan + +Baseline: 4-lane sol-medium triage (2026-08-19) over 53 open PRs + 75 open +issues. This unit EXECUTES the verdicts. All merges via gh pr merge --admin +(user pre-approved). Issue-closure rule: a merged PR that resolves an issue +closes that issue in the same work-phase. + +Live-state reverify (wp0, 2026-08-19): all 14 merge candidates OPEN, +MERGEABLE, base=dev, zero FAILED checks — but NOT all CI-proven: #2061 #2066 +#2042 #2072 #2068 #1903 #2075 have no Cross-platform CI run on their exact +heads (only hygiene/enforce-target). mergeStateStatus=BLOCKED is the +review-requirement ruleset; admin merge passes. #1885 CONFLICTING (close +target anyway), #1498 draft+CONFLICTING+red hygiene (close target). + +**Pre-merge validation rule (r1 audit fold-back):** a PR without a +Cross-platform CI conclusion on its exact head must NOT be merged on +mergeability alone. Before merging such a PR: scratch-merge its head onto +current dev in a lidge worktree and run the focused suites the diff touches +(plus tsc); only a clean scratch-merge run authorizes the admin merge. The +post-merge push CI on the merge SHA remains the decisive gate; a red +post-merge CI triggers immediate fix-forward or revert of that one merge. + +## Work-phase map (dependency-ordered) + +| wp | scope | PRs / issues | gate | +|---|---|---|---| +| wp1 | batch-quota | merge #2056 #2055 -> close #2047 #2046 | sol review lane per PR, then admin merge, CI on merge SHA | +| wp2 | batch-small-fixes | merge #2053 #2045 #2061 #2066 -> close #2065 | same | +| wp3 | batch-lab-chat | merge #2042 #2059; re-diff #2075 vs #2042 (prefix-matching claim); #2044 test-gap decision (merge with follow-up test or request change) | same + 2075 contradiction resolution | +| wp4 | batch-features | SERIALIZED: merge #2072 first, then re-diff/re-review/scratch-validate #1903 against post-2072 dev before merging it (both touch src/codex/catalog/provider-fetch.ts + provider docs + 9 GUI locales); then #2068 #2057; close #1885 (superseded by #2072) #1498 (stale/dont-merge) | same + serialization gate | +| wp5 | redesign #2073 | injector env_http_headers -> env_key (codex 0.146+) | C3: wp5's P WRITES 010_env_key_contract.md (contract proof from codex-rs source/release notes via cxc-search) BEFORE impl; impl+tests, PR, admin merge, close #2073 | +| wp6 | redesign #2064 | Remote raw-thinking on empty summary[] | C3: wp6's P WRITES 020_remote_reasoning_leak_rca.md (root-cause in OUR relay; model-side intermittent exposure is out of scope) BEFORE impl; impl+tests, PR, merge, close #2064 | +| wp7 | redesign #1926 | tsig credential scope | C4 security: design 051_tsig_credential_scope.md (fin unit), impl+tests, PR, merge, close #1926 | +| wp8 | redesign #1942 | Windows transactional update rollback | C4: base design 090_transactional_update_rollback.md has a KNOWN path-structure defect (staging/backup as CHILDREN of makes the live-prefix swap move them with it, and live cannot move into its own subdirectory) — wp8's P MUST amend the design to sibling-of-prefix staging/backup paths (e.g. .ocx-staging-) before impl; impl+tests, PR, merge, close #1942 | +| wp9 | closeout | final dev-head CI verify, ledger, unit disposition | push-CI green or pre-existing-red classified | + +## Review-lane contract (every merge batch) + +- One sol-medium read-only reviewer per PR (parallel), packet includes + $codexclaw:cxc-dev + $codexclaw:cxc-search mentions, full diff read, + verdict line MERGE-OK | BLOCK(reason). +- Main agent merges only MERGE-OK PRs; BLOCK verdicts downgrade the PR to + NEEDS-WORK with an evidence comment. +- Merge method: squash when the branch has fixup/noise commits, merge + otherwise. +- Suites: lidge dispatch without long blocking waits; decisive gate is the + Cross-platform CI push run on the merged SHA. + +## Known risks + +- Seven merge candidates lack head CI (see pre-merge validation rule above); + scratch-merge lidge validation is mandatory for them. +- wp4 #2072/#1903 file overlap (provider-fetch.ts, provider docs, locales): + serialized merge with re-validation between. +- #1942 base design path defect: fixed at wp8 P via design amendment. +- #2075 verdict conflict: 1st-pass ADOPT-NOW vs sol NEEDS-WORK (claims it + reintroduces prefix matching #2042 fixes). Resolve by diffing #2075 head + against #2042 semantics AFTER #2042 lands. +- #2064: reasoning leakage can be intermittent model-side behavior; fix only + what our relay provably does wrong (persisting/rendering raw reasoning when + summary[] is empty). +- Windows dispatch CI leg is known-red pre-campaign (Log Guard families) — + not a gate for these merges. From 3da2c2b957b5d60357b494b2980b6d4b3e049897 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Wed, 19 Aug 2026 09:39:03 +0900 Subject: [PATCH 2/3] fix(codex): emit modern env_key admission line on non-loopback inject codex-cli 0.146+ documents env_key as the bearer auth form for custom model providers: it sends Authorization: Bearer $VAR and hard-errors on a missing variable instead of silently omitting the header. The server side already admits and substitutes this bearer (#1686 chain), so the injector was the stale half. Loopback Design B still emits no auth line; the #2061 orphan sub-table cleanup keeps handling historic env_http_headers configs. Closes #2073 --- .../010_env_key_contract.md | 61 +++++++++++++++++++ .../docs/fr/guides/codex-integration.md | 2 +- .../content/docs/guides/codex-integration.md | 2 +- .../docs/ja/guides/codex-integration.md | 2 +- .../docs/ko/guides/codex-integration.md | 2 +- .../docs/ru/guides/codex-integration.md | 2 +- .../docs/tr/guides/codex-integration.md | 2 +- .../docs/zh-cn/guides/codex-integration.md | 2 +- .../docs/zh-tw/guides/codex-integration.md | 2 +- src/codex/inject.ts | 10 ++- structure/02_config-and-codex-home.md | 2 +- tests/codex-inject.test.ts | 10 ++- tests/loopback-listener-admission.test.ts | 1 + tests/loopback-listener-integration.test.ts | 1 + 14 files changed, 86 insertions(+), 15 deletions(-) create mode 100644 devlog/_plan/260819_triage_execution/010_env_key_contract.md diff --git a/devlog/_plan/260819_triage_execution/010_env_key_contract.md b/devlog/_plan/260819_triage_execution/010_env_key_contract.md new file mode 100644 index 0000000000..7855373d9f --- /dev/null +++ b/devlog/_plan/260819_triage_execution/010_env_key_contract.md @@ -0,0 +1,61 @@ +# 010 — #2073: injector env_key contract (codex-cli 0.146+) + +## Contract facts (verified against openai/codex rust-v0.146.0 source, wp5 research lane) + +- env_key reads the named env var and sends Authorization: Bearer + (model-provider-info/src/lib.rs#L263-281). Missing/empty var = HARD error + (CodexErr::EnvVar), never an empty bearer. +- env_key + requires_openai_auth = true is valid; env_key WINS for wire auth + (first-party scoped auth disabled), requires_openai_auth keeps login/account + UX (provider.rs#L153-174, auth.rs#L162-178). +- env_http_headers is still honored in 0.146 (not removed), but bearer auth is + the documented modern form. + +## Server-side prerequisite (already landed) + +#1686 chain (22d5492b2, acfedae0a, f848b4997): /v1/responses admits our +admission secret via Authorization: Bearer and SUBSTITUTES stored main auth +upstream (materializeCodexUpstreamAuth). So an env_key client works end to end +on dev today. The injector is the only stale half (this issue). + +## Change + +src/codex/inject.ts buildProviderTableBlock (legacy/non-loopback mode only — +loopback Design B emits no auth line at all): + +- BEFORE: env_http_headers = { "x-opencodex-api-key" = "OPENCODEX_API_AUTH_TOKEN" } +- AFTER: env_key = "OPENCODEX_API_AUTH_TOKEN" + +requires_openai_auth = true stays (login UX; env_key wins wire auth). +Sub-table strip logic from #2061 (env_http_headers orphan cleanup) STAYS — it +cleans historic configs regenerated by the app. stripExistingModelProvider +already removes the whole marker-owned block, so old env_http_headers lines +from prior injections are replaced on next inject (idempotent). + +## Behavior deltas (accepted) + +- Missing OPENCODEX_API_AUTH_TOKEN in the codex process env: was silent header + omission -> 401; now a clear codex-side hard error. Better diagnosability; + the unauthenticated-loopback-listener case already suppresses the auth line + entirely (shouldInjectApiAuthHeader), so the known no-var environment never + sees env_key. +- The client's own ChatGPT bearer is no longer forwarded on this path (env_key + replaces Authorization); the proxy substitutes stored main auth — exactly the + #1686 design. Hand-configured env_http_headers remains honored by the runtime + for users who want the legacy form. + +## Tests + +- tests/codex-inject.test.ts:44,194 — flip to env_key expectation + add a + regression that env_http_headers is NOT emitted. +- tests/loopback-listener-admission.test.ts:199 / integration:617 — extend the + not-contains to env_key (loopback emits no auth line). +- Keep #2061 sub-table strip tests untouched. + +## Docs + +- structure/02_config-and-codex-home.md provider-block sample: update line. +- docs-site reference mentions of env_http_headers (if any) updated. + +Verifier: bun test tests/codex-inject.test.ts tests/loopback-listener-admission.test.ts tests/loopback-listener-integration.test.ts + bun x tsc --noEmit. + diff --git a/docs-site/src/content/docs/fr/guides/codex-integration.md b/docs-site/src/content/docs/fr/guides/codex-integration.md index ce9778c9b6..52dc472be2 100644 --- a/docs-site/src/content/docs/fr/guides/codex-integration.md +++ b/docs-site/src/content/docs/fr/guides/codex-integration.md @@ -125,7 +125,7 @@ name = "OpenCodex Proxy" base_url = "http://your-host:10100/v1" wire_api = "responses" requires_openai_auth = true -env_http_headers = { "x-opencodex-api-key" = "OPENCODEX_API_AUTH_TOKEN" } +env_key = "OPENCODEX_API_AUTH_TOKEN" # supports_websockets = true # only when config.websockets is true ``` diff --git a/docs-site/src/content/docs/guides/codex-integration.md b/docs-site/src/content/docs/guides/codex-integration.md index 39e33fc0c4..a9c7a140ba 100644 --- a/docs-site/src/content/docs/guides/codex-integration.md +++ b/docs-site/src/content/docs/guides/codex-integration.md @@ -121,7 +121,7 @@ name = "OpenCodex Proxy" base_url = "http://your-host:10100/v1" wire_api = "responses" requires_openai_auth = true -env_http_headers = { "x-opencodex-api-key" = "OPENCODEX_API_AUTH_TOKEN" } +env_key = "OPENCODEX_API_AUTH_TOKEN" # supports_websockets = true # only when config.websockets is true ``` diff --git a/docs-site/src/content/docs/ja/guides/codex-integration.md b/docs-site/src/content/docs/ja/guides/codex-integration.md index d4d2bfa20e..ced7a0a112 100644 --- a/docs-site/src/content/docs/ja/guides/codex-integration.md +++ b/docs-site/src/content/docs/ja/guides/codex-integration.md @@ -87,7 +87,7 @@ name = "OpenCodex Proxy" base_url = "http://your-host:10100/v1" wire_api = "responses" requires_openai_auth = true -env_http_headers = { "x-opencodex-api-key" = "OPENCODEX_API_AUTH_TOKEN" } +env_key = "OPENCODEX_API_AUTH_TOKEN" # supports_websockets = true # only when config.websockets is true ``` diff --git a/docs-site/src/content/docs/ko/guides/codex-integration.md b/docs-site/src/content/docs/ko/guides/codex-integration.md index 002ba14e08..b1a15ea4c2 100644 --- a/docs-site/src/content/docs/ko/guides/codex-integration.md +++ b/docs-site/src/content/docs/ko/guides/codex-integration.md @@ -79,7 +79,7 @@ name = "OpenCodex Proxy" base_url = "http://your-host:10100/v1" wire_api = "responses" requires_openai_auth = true -env_http_headers = { "x-opencodex-api-key" = "OPENCODEX_API_AUTH_TOKEN" } +env_key = "OPENCODEX_API_AUTH_TOKEN" # supports_websockets = true # only when config.websockets is true ``` diff --git a/docs-site/src/content/docs/ru/guides/codex-integration.md b/docs-site/src/content/docs/ru/guides/codex-integration.md index 3c89ef2d08..118c663870 100644 --- a/docs-site/src/content/docs/ru/guides/codex-integration.md +++ b/docs-site/src/content/docs/ru/guides/codex-integration.md @@ -127,7 +127,7 @@ name = "OpenCodex Proxy" base_url = "http://your-host:10100/v1" wire_api = "responses" requires_openai_auth = true -env_http_headers = { "x-opencodex-api-key" = "OPENCODEX_API_AUTH_TOKEN" } +env_key = "OPENCODEX_API_AUTH_TOKEN" # supports_websockets = true # only when config.websockets is true ``` diff --git a/docs-site/src/content/docs/tr/guides/codex-integration.md b/docs-site/src/content/docs/tr/guides/codex-integration.md index 8ef1aa6169..ba513713f6 100644 --- a/docs-site/src/content/docs/tr/guides/codex-integration.md +++ b/docs-site/src/content/docs/tr/guides/codex-integration.md @@ -140,7 +140,7 @@ name = "OpenCodex Proxy" base_url = "http://your-host:10100/v1" wire_api = "responses" requires_openai_auth = true -env_http_headers = { "x-opencodex-api-key" = "OPENCODEX_API_AUTH_TOKEN" } +env_key = "OPENCODEX_API_AUTH_TOKEN" # supports_websockets = true # yalnızca config.websockets true olduğunda ``` diff --git a/docs-site/src/content/docs/zh-cn/guides/codex-integration.md b/docs-site/src/content/docs/zh-cn/guides/codex-integration.md index 76b785dcfb..ef693616f6 100644 --- a/docs-site/src/content/docs/zh-cn/guides/codex-integration.md +++ b/docs-site/src/content/docs/zh-cn/guides/codex-integration.md @@ -115,7 +115,7 @@ name = "OpenCodex Proxy" base_url = "http://your-host:10100/v1" wire_api = "responses" requires_openai_auth = true -env_http_headers = { "x-opencodex-api-key" = "OPENCODEX_API_AUTH_TOKEN" } +env_key = "OPENCODEX_API_AUTH_TOKEN" # supports_websockets = true # only when config.websockets is true ``` diff --git a/docs-site/src/content/docs/zh-tw/guides/codex-integration.md b/docs-site/src/content/docs/zh-tw/guides/codex-integration.md index a0349e97fb..e12850aec1 100644 --- a/docs-site/src/content/docs/zh-tw/guides/codex-integration.md +++ b/docs-site/src/content/docs/zh-tw/guides/codex-integration.md @@ -113,7 +113,7 @@ name = "OpenCodex Proxy" base_url = "http://your-host:10100/v1" wire_api = "responses" requires_openai_auth = true -env_http_headers = { "x-opencodex-api-key" = "OPENCODEX_API_AUTH_TOKEN" } +env_key = "OPENCODEX_API_AUTH_TOKEN" # supports_websockets = true # 僅當 config.websockets 為 true ``` diff --git a/src/codex/inject.ts b/src/codex/inject.ts index 5701f7c004..e137d1567b 100644 --- a/src/codex/inject.ts +++ b/src/codex/inject.ts @@ -227,9 +227,13 @@ export function buildProviderTableBlock( "requires_openai_auth = true", ]; if (includeApiAuthHeader) { - lines.push( - 'env_http_headers = { "x-opencodex-api-key" = "OPENCODEX_API_AUTH_TOKEN" }', - ); + // codex-cli 0.146+ contract (#2073): env_key sends Authorization: Bearer $VAR and + // hard-errors on a missing/empty variable instead of silently omitting auth. It + // coexists with requires_openai_auth (env_key wins wire auth; the flag keeps the + // login/account UX), and the server substitutes stored main auth for our admission + // bearer (#1686), so the modern form is strictly better than the legacy + // env_http_headers table this line used to emit. + lines.push('env_key = "OPENCODEX_API_AUTH_TOKEN"'); } if (supportsWebsockets) lines.push("supports_websockets = true"); return lines.join("\n") + "\n"; diff --git a/structure/02_config-and-codex-home.md b/structure/02_config-and-codex-home.md index b1ec14a828..6054c211d4 100644 --- a/structure/02_config-and-codex-home.md +++ b/structure/02_config-and-codex-home.md @@ -196,7 +196,7 @@ name = "OpenCodex Proxy" base_url = "http://:/v1" wire_api = "responses" requires_openai_auth = true -env_http_headers = { "x-opencodex-api-key" = "OPENCODEX_API_AUTH_TOKEN" } +env_key = "OPENCODEX_API_AUTH_TOKEN" ``` Root TOML keys must be written before the first `[table]`. Re-injection strips the stale form of diff --git a/tests/codex-inject.test.ts b/tests/codex-inject.test.ts index 66f33a2745..1d9b86a00f 100644 --- a/tests/codex-inject.test.ts +++ b/tests/codex-inject.test.ts @@ -38,10 +38,13 @@ describe("Codex config injection", () => { expect(block).toContain("supports_websockets = true"); }); - test("can inject Codex provider API auth header from environment for non-loopback proxy mode", () => { + test("non-loopback proxy mode injects the modern env_key admission line (#2073)", () => { const block = buildProviderTableBlock(10100, false, true); - expect(block).toContain('env_http_headers = { "x-opencodex-api-key" = "OPENCODEX_API_AUTH_TOKEN" }'); + expect(block).toContain('env_key = "OPENCODEX_API_AUTH_TOKEN"'); + // The legacy header table must not come back: codex 0.146+ documents env_key as + // the bearer form, and #1686's server-side substitution is keyed to it. + expect(block).not.toContain("env_http_headers"); }); test("injected base_url matches the actual bind: literal 127.0.0.1 for loopback/wildcard (Windows resolves localhost to ::1 first)", () => { @@ -191,7 +194,8 @@ describe("Codex config injection", () => { expect(profile).toContain('model_catalog_json = "/tmp/opencodex-catalog.json"'); expect(profile).toContain("supports_websockets = true"); - expect(profile).toContain('env_http_headers = { "x-opencodex-api-key" = "OPENCODEX_API_AUTH_TOKEN" }'); + expect(profile).toContain('env_key = "OPENCODEX_API_AUTH_TOKEN"'); + expect(profile).not.toContain("env_http_headers"); }); test("honors an explicit unavailable catalog decision", () => { diff --git a/tests/loopback-listener-admission.test.ts b/tests/loopback-listener-admission.test.ts index ea7baea3a4..f9858b5b08 100644 --- a/tests/loopback-listener-admission.test.ts +++ b/tests/loopback-listener-admission.test.ts @@ -197,5 +197,6 @@ describe("injected Codex provider block", () => { const block = buildProviderTableBlock(10200, false, false, "0.0.0.0"); expect(block).toContain('base_url = "http://127.0.0.1:10200/v1"'); expect(block).not.toContain("env_http_headers"); + expect(block).not.toContain("env_key"); }); }); diff --git a/tests/loopback-listener-integration.test.ts b/tests/loopback-listener-integration.test.ts index 8429479132..7ed51757b2 100644 --- a/tests/loopback-listener-integration.test.ts +++ b/tests/loopback-listener-integration.test.ts @@ -615,6 +615,7 @@ describe("Codex injection targets the loopback listener", () => { expect(written).toContain("http://127.0.0.1:10200/v1"); expect(written).not.toContain("http://127.0.0.1:10100/v1"); expect(written).not.toContain("env_http_headers"); + expect(written).not.toContain("env_key"); } finally { rmSync(root, { recursive: true, force: true }); } From f7c6122437589a7cee5b964fa64f052259f68382 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Wed, 19 Aug 2026 09:43:27 +0900 Subject: [PATCH 3/3] test(codex): pin non-loopback legacy env_http_headers -> env_key upgrade --- tests/codex-inject-integration.test.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/codex-inject-integration.test.ts b/tests/codex-inject-integration.test.ts index 1a6637856d..a544a98bb9 100644 --- a/tests/codex-inject-integration.test.ts +++ b/tests/codex-inject-integration.test.ts @@ -92,6 +92,31 @@ describe("injectCodexConfig integration (Design B)", () => { expect(config.match(/Auto-injected by opencodex/g)?.length).toBe(1); }); + test("upgrade path: a non-loopback legacy env_http_headers config converts to env_key (#2073)", () => { + writeFileSync(join(codexHome, "config.toml"), [ + 'model_provider = "opencodex"', + "", + "# Auto-injected by opencodex", + "[model_providers.opencodex]", + 'name = "OpenCodex Proxy"', + 'base_url = "http://192.168.1.50:10100/v1"', + 'wire_api = "responses"', + "requires_openai_auth = true", + 'env_http_headers = { "x-opencodex-api-key" = "OPENCODEX_API_AUTH_TOKEN" }', + "", + ].join("\n"), "utf8"); + + const r = runInject(codexHome, ocxHome, JSON.stringify({ hostname: "192.168.1.50" })); + expect(r.status).toBe(0); + expect(JSON.parse(r.stdout).success).toBe(true); + + const config = readFileSync(join(codexHome, "config.toml"), "utf8"); + expect(config).toContain('env_key = "OPENCODEX_API_AUTH_TOKEN"'); + expect(config).not.toContain("env_http_headers"); + // Still exactly one provider block, no duplicate accumulation. + expect(config.match(/\[model_providers\.opencodex]/g)?.length).toBe(1); + }); + test("re-inject over a Design B config is idempotent", () => { writeFileSync(join(codexHome, "config.toml"), 'model = "gpt-5.5"\n', "utf8");