Repository navigation
fix(ci): security-tier gate must honor ALWAYS_PROTECTED_API_PATTERNS too (+ file-size rebaseline) - #12605
Conversation
…ine four merged-growth file caps Two base-reds on release/v3.8.51 (#12581), both drained at the source. 1) check:openapi-security-tiers reported six CORRECTLY annotated routes as unprotected and demanded the removal of their x-loopback-only annotation — pushing the fix in the unsafe direction. The gate re-reads routeGuard.ts as text (it cannot import the module: routeGuard pulls the server runtime and the gate runs on plain node), but it only read the FIRST half of isLocalOnlyPath(): LOCAL_ONLY_API_PREFIXES.some(...) || LOCAL_ONLY_API_PATTERNS.some(...) so every route gated by a regex (/api/providers/volcengine-plan/connect/*) or by an imported constant (VNC_ROUTE_PREFIX, which the text parse turned into the literal string "VNC_ROUTE_PREFIX") looked open. Proven with isLocalOnlyPath() at runtime: all six return true; the control /api/providers/{id}/refresh stays false. New scripts/check/routeGuardConstants.mjs reads BOTH arrays, resolves imported identifiers by following the import, and THROWS on an unresolvable token instead of silently degrading it into a literal. Its array scanner is hand-rolled because regex literals carry the brackets and commas a \[([^\]]+)\] capture plus a naive comma split break on ([^/] and {1,3}). The reverse pass (missing-annotation warnings) now uses the same predicate. 2) check:file-size: four frozen files grew past their cap through merged PRs — chat.ts +10 (#12427/#12503 video-transcript redaction, derived from the post-guardrail payload at the single dispatch point) and stream.ts / accountFallback.ts / codex.ts +17 total (#12179 hot-path regex hoisting, bounded caches, quadratic-buffering fix). All cohesive at existing chokepoints; rebaselined with the rationale recorded in the baseline file. Refs #12581
…e-v3.8.51-basereds-openapi-tiers # Conflicts: # scripts/check/check-openapi-security-tiers.mjs
#12350 fixed the LOCAL_ONLY half of the checker (prefixes + patterns + imported consts). isAlwaysProtectedPath() is two-armed the same way: ALWAYS_PROTECTED_API_PATHS.some(...) || ALWAYS_PROTECTED_API_PATTERNS.some(...) but the checker still read only the path array, so the four credential routes gated by the GHSA-5926-2w35-7h4q pattern (#12600) — /api/providers/{id}/{claude,codex}-auth/{export,apply-local} — reported as 'has x-always-protected but is NOT in ALWAYS_PROTECTED_API_PATHS', asking for the removal of a CORRECT annotation on a credential-export route. Verified with the real predicate: all four isAlwaysProtectedPath() → true; control /api/providers/{id}/models → false. tests/unit/openapi-security-tiers.test.ts already checks BOTH arrays (#12600 updated the test but not the gate script) and stays green — this commit makes the gate agree with the test and with the runtime. Also carries the file-size rebaseline for four caps grown by merged PRs (chat.ts +10 from #12427/#12503; stream.ts / accountFallback.ts / codex.ts +17 from #12179), rationale recorded in the baseline file. Refs #12581
…2 -> 313) The check:public-creds allowlist pins each frozen literal by FILE:LINE, so #12179 (hot-path regex hoisting in the same file) shifted the ZCode handshake id from L302 to L313 and broke the gate twice over: the old entry went stale ('a violação foi corrigida; REMOVA a entrada') while the literal itself, now at L313, was no longer covered. The literal is unchanged and still not a credential: `omniroute-${process.pid}` is a per-process handshake id for the local ZCode app-server, already audited and frozen with that justification. Only the anchor moves. Refs #12581
|
Cheguei neste mesmo base-red (#12581) por outro caminho e parei ao ver que esta PR já cobre o núcleo do fix — o gate passando a honrar Não toquei nesta branch (Hard Rule #22b). Deixo aqui dois deltas que apurei e que não estão nesta PR, para você decidir se quer dobrar aqui ou se prefere que eu abra follow-up depois do merge:
Se preferir, mando o patch dos dois itens; se preferir manter o escopo desta PR, abro follow-up assim que ela mergear. |
…te entry The allowlist key is file:LINE:value, so the synthetic source in this test pads to the exact line the entry pins. Re-anchoring the entry 302 -> 313 (previous commit) without moving the padding left the test asserting the old line — caught by Unit Tests fast-path (4/4) on #12605. Both halves now sit at 313, and the test still proves the allowlist does NOT weaken detection: swapping the value for 'upstream-client-' is still flagged. Refs #12581
|
Refreshed onto current Conflicts resolved:
Local: |
|
Second refresh onto current tip |
…e-v3.8.51-basereds-openapi-tiers
The base drained fast while this PR was open. Re-verified on 008da6d and dropped everything already covered there: - check-public-creds.mjs: the base already re-anchors the ZCode entry to L313 (my commit only added a comment on top) -> reverted to the base version. - file-size-baseline.json: the base rebaselined chat.ts/codex.ts/ accountFallback.ts to HIGHER caps than mine, and stream.ts measures 3064 against the base cap of 3072 — my 3078 bump would have loosened a cap for no reason -> reverted to the base version. What the base still does NOT have, verified on its current tip: node scripts/check/check-openapi-security-tiers.mjs -> EXIT=1, 4 mismatches so the ALWAYS_PROTECTED_API_PATTERNS half stays, plus its changelog entry. Refs #12581
|
Merging with Reproduced on the tip (not in this PR):
No new ESLint warnings has been Passing on this head: Vitest, Docs Gates, Change Classification, semgrep, CodeQL, file-size, public-creds, complexity (0 changed source files). |
…cuting gate test (#12581) The ALWAYS_PROTECTED two-arm read itself landed in #12605; what was still missing is a regression guard. This runs the real gate and asserts it exits 0 with no "NOT covered" line, so the LOCAL_ONLY-arm defect (#12350) cannot silently reappear on the ALWAYS_PROTECTED arm. Also fails the parse guard when ALWAYS_PROTECTED_API_PATTERNS comes back empty, instead of reporting every regex-covered route as an annotation mismatch.
…cuting gate test (#12581) (#12652) Merged as a reduced diff, and worth recording why. The two-arm `ALWAYS_PROTECTED` read this PR proposed had already landed in #12605 while this branch was open — the tip carries `coveredByAlwaysProtected()` with both arms and the new error wording. I ran the gate on the current tip to be sure: `PASS — all security tier annotations match routeGuard.ts`. Merging the whole branch would have reintroduced the same logic under a different comment. What was genuinely missing, and is what merged: - **`tests/unit/openapi-security-tiers-gate.test.ts`** — executes the real gate and asserts exit 0 with no "NOT covered" line. #12605 fixed the defect but left no guard, so the LOCAL_ONLY-arm bug (#12350) could reappear on the ALWAYS_PROTECTED arm exactly as it did the first time. 1/1 green. - **The parse guard** — `ALWAYS_PROTECTED_PATTERNS.length === 0` now fails the constant-parse check with its own count in the message. Without it, a regex array that stops parsing degrades into "every pattern-covered route is an annotation mismatch" instead of saying so. A note for the record: my first read of this PR was wrong. I ran the gate in the main checkout, which was 11 commits behind `origin/release/v3.8.51`, saw the pre-#12605 failure, and classified this as fixing a live red. It was not — the checkout was stale. Corrected before anything was merged.
…too (+ file-size rebaseline) (cheaper-inference#12605) * fix(ci): mirror isLocalOnlyPath in the security-tier gate and rebaseline four merged-growth file caps Two base-reds on release/v3.8.51 (cheaper-inference#12581), both drained at the source. 1) check:openapi-security-tiers reported six CORRECTLY annotated routes as unprotected and demanded the removal of their x-loopback-only annotation — pushing the fix in the unsafe direction. The gate re-reads routeGuard.ts as text (it cannot import the module: routeGuard pulls the server runtime and the gate runs on plain node), but it only read the FIRST half of isLocalOnlyPath(): LOCAL_ONLY_API_PREFIXES.some(...) || LOCAL_ONLY_API_PATTERNS.some(...) so every route gated by a regex (/api/providers/volcengine-plan/connect/*) or by an imported constant (VNC_ROUTE_PREFIX, which the text parse turned into the literal string "VNC_ROUTE_PREFIX") looked open. Proven with isLocalOnlyPath() at runtime: all six return true; the control /api/providers/{id}/refresh stays false. New scripts/check/routeGuardConstants.mjs reads BOTH arrays, resolves imported identifiers by following the import, and THROWS on an unresolvable token instead of silently degrading it into a literal. Its array scanner is hand-rolled because regex literals carry the brackets and commas a \[([^\]]+)\] capture plus a naive comma split break on ([^/] and {1,3}). The reverse pass (missing-annotation warnings) now uses the same predicate. 2) check:file-size: four frozen files grew past their cap through merged PRs — chat.ts +10 (cheaper-inference#12427/cheaper-inference#12503 video-transcript redaction, derived from the post-guardrail payload at the single dispatch point) and stream.ts / accountFallback.ts / codex.ts +17 total (cheaper-inference#12179 hot-path regex hoisting, bounded caches, quadratic-buffering fix). All cohesive at existing chokepoints; rebaselined with the rationale recorded in the baseline file. Refs cheaper-inference#12581 * fix(ci): security-tier gate must honor ALWAYS_PROTECTED_API_PATTERNS too cheaper-inference#12350 fixed the LOCAL_ONLY half of the checker (prefixes + patterns + imported consts). isAlwaysProtectedPath() is two-armed the same way: ALWAYS_PROTECTED_API_PATHS.some(...) || ALWAYS_PROTECTED_API_PATTERNS.some(...) but the checker still read only the path array, so the four credential routes gated by the GHSA-5926-2w35-7h4q pattern (cheaper-inference#12600) — /api/providers/{id}/{claude,codex}-auth/{export,apply-local} — reported as 'has x-always-protected but is NOT in ALWAYS_PROTECTED_API_PATHS', asking for the removal of a CORRECT annotation on a credential-export route. Verified with the real predicate: all four isAlwaysProtectedPath() → true; control /api/providers/{id}/models → false. tests/unit/openapi-security-tiers.test.ts already checks BOTH arrays (cheaper-inference#12600 updated the test but not the gate script) and stays green — this commit makes the gate agree with the test and with the runtime. Also carries the file-size rebaseline for four caps grown by merged PRs (chat.ts +10 from cheaper-inference#12427/cheaper-inference#12503; stream.ts / accountFallback.ts / codex.ts +17 from cheaper-inference#12179), rationale recorded in the baseline file. Refs cheaper-inference#12581 * fix(ci): re-anchor the zcodeProtocol public-creds allowlist entry (302 -> 313) The check:public-creds allowlist pins each frozen literal by FILE:LINE, so cheaper-inference#12179 (hot-path regex hoisting in the same file) shifted the ZCode handshake id from L302 to L313 and broke the gate twice over: the old entry went stale ('a violação foi corrigida; REMOVA a entrada') while the literal itself, now at L313, was no longer covered. The literal is unchanged and still not a credential: `omniroute-${process.pid}` is a per-process handshake id for the local ZCode app-server, already audited and frozen with that justification. Only the anchor moves. Refs cheaper-inference#12581 * test(ci): re-anchor the ZCode allowlist test to L313 alongside the gate entry The allowlist key is file:LINE:value, so the synthetic source in this test pads to the exact line the entry pins. Re-anchoring the entry 302 -> 313 (previous commit) without moving the padding left the test asserting the old line — caught by Unit Tests fast-path (4/4) on cheaper-inference#12605. Both halves now sit at 313, and the test still proves the allowlist does NOT weaken detection: swapping the value for 'upstream-client-' is still flagged. Refs cheaper-inference#12581 * docs(ci): changelog fragment for cheaper-inference#12605 * chore(ci): trim cheaper-inference#12605 to the one fix the base still needs The base drained fast while this PR was open. Re-verified on 9960e46 and dropped everything already covered there: - check-public-creds.mjs: the base already re-anchors the ZCode entry to L313 (my commit only added a comment on top) -> reverted to the base version. - file-size-baseline.json: the base rebaselined chat.ts/codex.ts/ accountFallback.ts to HIGHER caps than mine, and stream.ts measures 3064 against the base cap of 3072 — my 3078 bump would have loosened a cap for no reason -> reverted to the base version. What the base still does NOT have, verified on its current tip: node scripts/check/check-openapi-security-tiers.mjs -> EXIT=1, 4 mismatches so the ALWAYS_PROTECTED_API_PATTERNS half stays, plus its changelog entry. Refs cheaper-inference#12581
…cuting gate test (cheaper-inference#12581) (cheaper-inference#12652) Merged as a reduced diff, and worth recording why. The two-arm `ALWAYS_PROTECTED` read this PR proposed had already landed in cheaper-inference#12605 while this branch was open — the tip carries `coveredByAlwaysProtected()` with both arms and the new error wording. I ran the gate on the current tip to be sure: `PASS — all security tier annotations match routeGuard.ts`. Merging the whole branch would have reintroduced the same logic under a different comment. What was genuinely missing, and is what merged: - **`tests/unit/openapi-security-tiers-gate.test.ts`** — executes the real gate and asserts exit 0 with no "NOT covered" line. cheaper-inference#12605 fixed the defect but left no guard, so the LOCAL_ONLY-arm bug (cheaper-inference#12350) could reappear on the ALWAYS_PROTECTED arm exactly as it did the first time. 1/1 green. - **The parse guard** — `ALWAYS_PROTECTED_PATTERNS.length === 0` now fails the constant-parse check with its own count in the message. Without it, a regex array that stops parsing degrades into "every pattern-covered route is an annotation mismatch" instead of saying so. A note for the record: my first read of this PR was wrong. I ran the gate in the main checkout, which was 11 commits behind `origin/release/v3.8.51`, saw the pre-cheaper-inference#12605 failure, and classified this as fixing a live red. It was not — the checkout was stale. Corrected before anything was merged.
Base-red drain for #12581 (
fix/release-vX.Y.Z-basereds-*convention).1. The checker's other half is still blind
#12350 taught the checker both arms of
isLocalOnlyPath(). ButisAlwaysProtectedPath()is two-armed in exactly the same way:and the checker still read only the path array. So the four credential routes gated by the GHSA-5926-2w35-7h4q pattern (#12600, merged today) failed the gate:
The gate was asking for the removal of a correct annotation on a credential-export route — the same unsafe direction #12350 called out.
Verified against the real predicate, not the parse:
Fix:
coveredByAlwaysProtected()mirrorsisAlwaysProtectedPath()— both arms, same order — reusing theparsePatterns()helper #12350 already added. 13 changed lines.Why no new test:
tests/unit/openapi-security-tiers.test.tsalready checks both arrays against the real imported constants — #12600 updated the test but not the gate script, which is exactly how the two drifted apart. That test is the regression guard; this commit makes the gate agree with it (and with the runtime). It stays green: 6/6.2.
check:file-size— four merged-growth capsThe Release-Green report listed only
chat.ts; the gate actually flags four, all cohesive growth from already-merged PRs at existing chokepoints:src/sse/handlers/chat.tsopen-sse/utils/stream.tsopen-sse/services/accountFallback.tsopen-sse/executors/codex.tsRationale recorded in
file-size-baseline.json(_rebaseline_2026_09_03_basereds_v3851_merged_growth).Status of the other #12581 hard failures (not this PR)
check:licenses(@eloqnt/config)a2aLifecycleREMOVED_PROVIDERS.md)Validation
node scripts/check/check-openapi-security-tiers.mjs→ PASS (was FAIL with 4 mismatches on this tip)npm run check:file-size→ OK (was 4 ✗)tests/unit/openapi-security-tiers.test.ts→ 6/6Refs #12581