fix(web): declare the three remaining implicit return types (#1381 review residue) - #1402
Conversation
|
Important Review skippedAuto reviews are limited based on label configuration. 🏷️ Required labels (at least one) (1)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository YAML (base), Repository UI (inherited), Organization UI (inherited) Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Agent Completion Truth Gate: BLOCKEDReasons: Machine-readable verdict{
"details": {
"collection_errors": [
"incomplete_linked_issue_contract",
"missing_closing_issue_reference",
"linked_issue_not_agent_task",
"missing_intent_snapshot",
"missing_agent_run_id",
"missing_agent_login"
],
"invalid_fields": [
"policy.agent_login",
"policy.run_id"
]
},
"reasons": [
"invalid_payload"
],
"verdict": "blocked"
} |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
|
@coderabbitai review (Auto-review was skipped for lack of a qualifying label; Generated by Claude Code |
|
|
…ile fallback Absorbs the three items #1402 carried that this branch did not, so the two lines of work converge here rather than diverging: - `getOpenAiHeaders` declares `: Record<string, string> | null` - `sdpRequest` declares `: Request`, `upstreamResponse` declares `: Response` - a test for `verifyTurnstileToken` returning `{ ok: false }` with no `error`, which the optional-field fallback added in 5e69b7e handles but nothing exercised #1402's `fetchUpstream` rewrite is deliberately not taken. It guards only the `fetch` call, leaving `await upstream.text()` outside the try — so a connection reset mid-body still escapes the route. `upstreamUnreachable` (ae2be83) wraps both, which is the stricter guarantee. `npm test` 272 passed / 1 failed — the pre-existing `billing-chat-gating` timeout (#1116), identical on the base commit. `npm run lint` and `npm run type-check` clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DSkV9zjdhpkfUSMeLhrFGy
Residue from CodeRabbit's review of #1381 after ae2be83 and 5e69b7e landed the substantive findings. Those two commits typed `upstreamFailure`, `upstreamUnreachable`, and the `activateRequest` / `webhookRequest` / `transcribeRequest` helpers; these three were missed: - `getOpenAiHeaders` -> `Record<string, string> | null` - `sdpRequest` -> `Request` - `upstreamResponse` -> `Response` Types only — no behavior, no response shape, no test expectation changes. `getOpenAiHeaders` already returned exactly this union on both paths, so the annotation is descriptive rather than a narrowing. Verified at this head: tsc clean, eslint clean, web suite 271 passed / 1 failed — the pre-existing billing-chat-gating timeout (#1116), matching 5e69b7e exactly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0138bgDsN77ZFnktuYw26Pqa
f9f964e to
855dcec
Compare
|
Closing as superseded — the work landed directly on this PR's own base branch instead. Two sessions picked up the same five CodeRabbit threads on #1381 within minutes of each other. This PR branched from Absorbed from this PR (in
Already on the branch, so not re-applied: the full One difference, deliberate. try { return await fetch(url, init); } catch { … }
// …then, outside the guard:
const body = await upstream.text();
Two small contract choices also differ, both in the branch's favour:
Nothing is lost by closing; reopen if you disagree with any of the above. Verified on Generated by Claude Code |
|
Closing as superseded by #1381 — the two branches independently implemented the same CodeRabbit remediation on top of Why this PR is redundant. Both heads address the same three findings. Comparing
The one behavioural difference is the first row, and it favours #1381. This PR's Test coverage is otherwise a superset on #1381: it carries the rejected-GET and rejected-POST cases this PR adds, plus the body-read case, and folds the missing-key/invalid-SDP Concrete reason to close rather than leave open. The Two open PRs claim One thing worth keeping from this branch. Nothing is lost by closing: reopen if the comparison above is wrong on any row. Generated by Claude Code |
|
Correction to my closing comment above — it was written against My comment compared the original head (the Here is the state as of now.
That leaves this PR's diff against its current base as a net removal: The only change remaining is deleting The structurally-unsatisfiable Separately, this unblocks #1381: its Reopen if the Generated by Claude Code |
…1381) * fix(web): stop leaking upstream and Stripe error details to clients Two information-disclosure issues were returning third-party error text verbatim to callers. 1. /api/realtime/session (GET + POST) returned the raw OpenAI response body as `details` while mirroring `upstream.status`. OpenAI bodies echo org/project IDs, quota state, and on 401 a partial key (`Incorrect API key provided: sk-proj-****ABCD`). Both handlers now log the body server-side and return a static message with a fixed 502. 2. Five billing routes returned raw Stripe SDK `err.message`. All five are on the unauthenticated PUBLIC_API_EXACT allowlist, so any internet caller could trigger and read them. Each now returns a static string plus a machine-readable code; the raw message stays in console.error and kaizenObserve. /api/transcribe had the same raw-upstream pattern in its JSON-parse and catch-all branches and is fixed the same way. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> * docs(web): correct two inaccurate security comments flagged in review Applies the two fixes agreed in the Copilot review threads on #1381. Both are comment-only; no behavior, status code, or response body changes. transcribe/route.ts — the docblock claimed all error responses are static and machine-readable. The four `!result.success` branches return `fetchTranscript`'s message verbatim and carry no `code`, and the billing branch still returns a static `details` hint. Those messages are app-authored (every `error` in transcription-service.ts is a literal, a numeric status, or our own SSRF-guard text), so the CWE-209 objective holds — but the docblock overstated it. Scoped it to the two sanitized paths and described the rest accurately. billing/webhook/route.ts — the rationale was wrong on two counts. `constructEvent` verifies Stripe-Signature against the webhook signing secret, not the API key, so an API-key fragment cannot forge a signature; and StripeSignatureVerificationError describes tolerance/scheme/digests without echoing key material. Rewrote it to state the real risk: on an unauthenticated route, verification internals are an oracle for tuning replay/timestamp attacks. Verified at this head: type-check clean, eslint clean, the six route test files pass (17 tests), full web suite 261 passed / 1 failed — the failure being the pre-existing billing-chat-gating timeout tracked in #1116, which fails identically on the base commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jsfy3ts3U1ggGsJqFB2KSC * fix(web): return the static 502 when the Realtime upstream is unreachable CodeRabbit flagged this on #1381 and it reproduces: neither GET nor POST wrapped its `fetch`, so a transport-level failure — DNS, TLS, connection reset, timeout — rejected before `upstreamFailure` could run. The throw escaped to Next.js's own unstructured 500 instead of the `{ error, code }` JSON the route promises, `use-realtime-voice.ts` got a payload it cannot parse, and no `[realtime]` line was logged for the operator. Both handlers now wrap the fetch and the body read in try/catch and route transport failures through a new `upstreamUnreachable` helper, which logs the cause server-side and returns the same fixed 502 as a non-OK upstream response. The cause is an Error from our own transport rather than upstream text, so logging it introduces no disclosure. `await upstream.text()` is inside the try as well — a truncated body rejects there for the same reason. The gap predates this PR, but this PR is what establishes the contract that these routes always answer with a static structured error, so it belongs here. Three regression tests, each verified non-vacuous — they fail against the unwrapped route and pass against the fix: GET with a rejected fetch (ENOTFOUND), POST with a rejected fetch (socket hang up), and GET with a response whose body read rejects mid-stream. Verified at this head: tsc clean, eslint clean, full web suite 264 passed / 1 failed — the pre-existing billing-chat-gating timeout tracked in #1116, identical on the base commit. Up from 261 passed by exactly these 3 tests. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jsfy3ts3U1ggGsJqFB2KSC * fix(web): complete the error-code contract across the patched routes CodeRabbit's review of 1adbed6 found the `code` field was added only to the branches that were sanitized for CWE-209, leaving the rest of each route's error surface without a machine-readable key. A client cannot branch on a contract that only half the branches honour. Every remaining error branch on the six patched routes now returns `code` alongside its existing `error` value: - realtime/session — `realtime_not_configured` (GET and POST missing-key), `invalid_sdp_offer` - billing/activate — `invalid_json`, `session_id_required`, `not_eligible` - billing/checkout — `invalid_json`, `turnstile_rejected` - billing/renew — `invalid_json` - billing/webhook — `webhook_not_configured`, `missing_signature` `error` values are unchanged, so no client contract breaks. The checkout Turnstile branch additionally gains a fallback: `turnstile.error` is optional on `TurnstileVerifyResult`, so the previous `{ error: turnstile.error }` could serialize to `{}`. Its values are all our own literals, never Cloudflare response text, so returning it verbatim remains correct. Also from the same review: - `upstreamFailure` and `upstreamUnreachable` declare `: Response`. - `activateRequest`/`webhookRequest` declare `: NextRequest`, `transcribeRequest` declares `: Request`. The review's remaining finding — that a rejected `fetch()` skips `upstreamFailure` entirely — was already fixed in ae2be83, which landed after the review ran. +9 tests covering each newly-coded branch. `npm test` 271 passed / 1 failed; the single failure is the pre-existing `billing-chat-gating` 5000 ms timeout (#1116), identical on the base commit. `npm run lint` and `npm run type-check` clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DSkV9zjdhpkfUSMeLhrFGy * fix(web): annotate remaining helper return types and cover the Turnstile fallback Absorbs the three items #1402 carried that this branch did not, so the two lines of work converge here rather than diverging: - `getOpenAiHeaders` declares `: Record<string, string> | null` - `sdpRequest` declares `: Request`, `upstreamResponse` declares `: Response` - a test for `verifyTurnstileToken` returning `{ ok: false }` with no `error`, which the optional-field fallback added in 5e69b7e handles but nothing exercised #1402's `fetchUpstream` rewrite is deliberately not taken. It guards only the `fetch` call, leaving `await upstream.text()` outside the try — so a connection reset mid-body still escapes the route. `upstreamUnreachable` (ae2be83) wraps both, which is the stricter guarantee. `npm test` 272 passed / 1 failed — the pre-existing `billing-chat-gating` timeout (#1116), identical on the base commit. `npm run lint` and `npm run type-check` clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DSkV9zjdhpkfUSMeLhrFGy * fix(web): stop verifyTurnstileToken rejecting into an unstructured 500 CodeRabbit's review of the previous head found this and it reproduces. verifyTurnstileToken returns a TurnstileVerifyResult on every other exit, so its sole caller — the unauthenticated /api/billing/checkout route — awaits it outside any try/catch. Two awaits inside it break that contract: the siteverify fetch rejects on a transport failure, and res.json() rejects when Cloudflare answers 2xx with a truncated or non-JSON body. Either escaped POST as an unstructured framework 500 with no kaizenObserve trace and no `code` — the same disclosure-shaped hole ae2be83 closed on the realtime route. Fixed at the source rather than at the call site: the function now honours its own Promise<TurnstileVerifyResult> signature, so every present and future caller is covered. The reject reason is logged server-side — undici puts the resolved host and port in it (`connect ECONNREFUSED 10.0.3.14:443`) — and collapsed into the app-authored literal `turnstile_verification_unavailable`. The route's existing 403 branch handles it unchanged, so no status code or response shape moves. Note the 403 is now also reachable when Cloudflare is merely unreachable rather than rejecting; keeping the existing status was the narrower choice, and the distinction is visible to operators in the log and to clients in the `error` literal. Five tests, verified non-vacuous — reverting the guard fails three of them: rejected fetch, rejected res.json(), and a non-rejection contract check, plus the accept and non-2xx paths. They assert the resolved host/port never rides out on the result while the reason still reaches console.error. Verified at this head: tsc clean, eslint clean, full web suite 277 passed / 1 failed — the pre-existing billing-chat-gating 5000 ms timeout (#1116), which is also intermittently joined by a second timeout under load. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Jsfy3ts3U1ggGsJqFB2KSC * fix(web): complete the error-code contract on the transcribe route CodeRabbit flagged the `!result.success` branches of /api/transcribe as a CWE-209 leak. Traced every value `fetchTranscript` can put in `error` before changing anything: each one is an app-authored literal (transcription-service 75, 296, 303, 314), a numeric HTTP status (287), our own SSRF-guard message (278, and every throw in ssrf-guard interpolates only the caller's own URL), or a conditional literal (363). Upstream throws are swallowed at console.warn and never reach `error`. So the leak half of that finding does not hold. The contract half does. This PR established `error` + a stable machine-readable `code` on every error branch of the five billing routes and realtime/session; transcribe was the last surface still missing it. The docblock documented the gap instead of closing it. All five error branches now carry a code — input_required, rate_limited, billing_not_configured, transcription_unavailable, alongside the existing invalid_json and transcription_failed. The `error` strings are untouched, so the human-facing contract is unchanged, matching the precedent already accepted for the Turnstile branch: dynamic message, pinned code. The `details` hint on the billing branch is kept. It is an app-authored operator string, not upstream text; the docblock no longer claims otherwise. Tests: +5. One asserting the missing-input branch short-circuits before fetchTranscript, and four table-driven cases pinning each resolved-failure branch's status and code while asserting no provider-shaped token reaches the client. Also added vi.clearAllMocks() to the afterEach — restoreAllMocks only restores spies, so the module-factory vi.fn() was leaking call history between tests in this file. apps/web: type-check clean, eslint clean, 282 passed / 1 failed — the pre-existing billing-chat-gating timeout tracked in #1116. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Y1xcdTagvCU4K7ZwTgGQF6 * fix(web): stop the SSRF guard leaking DNS resolver text to the caller CodeRabbit found the last CWE-209 path on this PR's surface, and it is a real one. `assertPublicHttpUrl` throws app-authored literals at every exit except `dns.lookup`, which was awaited bare: ssrf-guard.ts:121 await dns.lookup(host, { all: true }) -> rejects `getaddrinfo ENOTFOUND <submitted-hostname>` transcription-service.ts:273-280 -> `Rejected audioUrl: ${guardErr.message}` transcribe/route.ts:104-113 -> returns that verbatim as `error` in a 503 So a caller could name any host and read back the server's resolver verdict on it. That is system error text reaching a client, which is this PR's whole objective — but the sharper problem is that it is a DNS oracle. ENOTFOUND vs EAI_AGAIN vs the existing 'Host resolves to a private address' distinguishes "no such name" from "exists but private" from "exists and public", letting a caller map internal names from the server's network position. That is exactly the reconnaissance this guard exists to block, leaking out of the guard. Fixed at the source rather than at either call site, so present and future callers of `assertPublicHttpUrl` are covered: the lookup is wrapped, the reject reason is logged server-side, and every resolver failure collapses into 'Host does not resolve' — the literal a zero-result lookup already threw, so the oracle closes in both directions. This also corrects a claim made earlier on this PR. The transcribe docblock thread asserted every `result.error` value was app-authored and listed line 278 as "our own SSRF-guard message". That held for the five explicit `throw new Error(...)` sites but not for the `dns.lookup` rejection, which is Node's. The docblock scoping stands; the reasoning behind it was incomplete. Severity is bounded: `/api/transcribe` is not on `PUBLIC_API_EXACT`, so this needs an authenticated caller, unlike the billing routes. Four tests, verified non-vacuous — reverting the try/catch fails exactly the two leakage tests (resolver text suppressed but still logged; ENOTFOUND and EAI_AGAIN indistinguishable from an empty result) while the two behaviour tests still pass, since private-address blocking and the public-host allow path do not depend on the catch. Verified at this head: tsc clean, eslint clean, web suite 286 passed / 1 failed — the pre-existing billing-chat-gating 5000 ms timeout (#1116). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DSkV9zjdhpkfUSMeLhrFGy * fix(web): close the last DNS oracle in the SSRF guard Vercel's review bot found this on 8628b5e and it is correct — 36645ad closed only half the hole it described. That commit collapsed the resolver-failure cases (ENOTFOUND, EAI_AGAIN, zero results) into one literal, but left the private-address branch throwing a distinct one: Host does not resolve <- the name does not exist Host resolves to a private address <- the name EXISTS, and is internal That difference is the sharpest oracle of the set. `fetchTranscript` interpolates the message into `Rejected audioUrl: ${guardErr.message}` (transcription-service.ts:278) and the transcribe route returns it verbatim on the `!result.success` path, so an unauthenticated caller could guess internal hostnames and read back whether each one exists and points at internal infrastructure — the reconnaissance this guard exists to prevent. 36645ad's own comment names "Host resolves to a private address" as part of the oracle and then does not unify it. All four non-public outcomes now throw one literal, `Host does not resolve to a public address`, which is true of every one of them. The resolved address is logged for operators instead. The IP-literal branch keeps its own distinct message deliberately: the caller supplied that address, so naming it private reveals nothing they did not already know, and no hostname is confirmed or denied. Two tests, verified non-vacuous — restoring the old literal fails both: one asserts the private address never rides out on the rejection while still reaching console.error, the other feeds the same hostname down the does-not-exist and exists-but-internal paths and asserts the caller cannot tell them apart. npm test 287 passed / 1 failed — the pre-existing billing-chat-gating timeout (#1116). lint and type-check clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DSkV9zjdhpkfUSMeLhrFGy * fix(web): stop /api/transcribe leaking probe status and key config #1381 sanitized the thrown and JSON-parse paths of /api/transcribe but left the `!result.success` branches returning `fetchTranscript`'s message verbatim, on the stated grounds that every such value is an app-authored literal. Two were not, and both are reachable by a caller who controls the request body: - `Failed to fetch audio: ${audioResponse.status}` echoed the HTTP status of the caller-supplied `audioUrl`. The SSRF guard admits public hosts, so this returned a cross-origin read (401 vs 403 vs 404 vs 500) that the browser's same-origin policy would otherwise deny — a probe oracle, not upstream text, which is why it survived the original pass. - The all-strategies-failed message branched on whether provider keys were set, disclosing server configuration and naming both the variables and the hosting platform. Both are fixed at the source in transcription-service.ts rather than masked at the route, so any future caller of fetchTranscript inherits the fix. The real status and the real key state are logged for operators, where they were the only thing actionable. Also drops the residual `details` field from the billing branch, which named this deployment's cloud and model vendors — #1381 stated `details` was gone from this route, and this is the one that was left. The route docblock carried the claim these values disproved; it now states the invariant and what enforces it. Verification (head e9bf62d, apps/web): - 3 new tests in transcription-error-disclosure.test.ts; all 3 fail against the unfixed service and pass with it — the status test asserts four distinct upstream statuses collapse to one message, so a partial fix cannot pass it. - vitest run: 1 failed | 290 passed (291). The single failure, billing-chat-gating "blocks free tier after daily quota", is pre-existing and unrelated — confirmed failing identically on the untouched e9bf62d with this change stashed. Tracked in #1116; #1230 makes the suite hermetic against it. - npm run type-check: clean. npm run lint: clean. - package-lock.json drift from npm install reverted, per #1381's convention. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EX3tLZJJfbDUM6XXi6Uefm * test: use the repo's standard fixture video ID The new disclosure tests used dQw4w9WgXcQ. `.github/copilot-instructions.md` requires auJzb1D-fag for all test data, and 60 files follow it against 6 stragglers — this file was one I had just added to that tail. Flagged by Vercel VADE review on #1440. The ID is inert here: fetch is stubbed, so the URL is never dereferenced and only reaches the strategy dispatch. 3/3 tests still pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EX3tLZJJfbDUM6XXi6Uefm * fix(web): stop forwarding the SSRF guard's rejection reason Raised by CodeRabbit on #1440 (P2), and correct. The previous commit's docblock asserted that every `fetchTranscript` error value reaching the client is a fixed app-authored literal. That was false: the guard branch returned `Rejected audioUrl: ${guardErr.message}` verbatim. `assertPublicHttpUrl` throws five distinguishable messages — `Invalid URL`, `Blocked URL scheme: <protocol>`, `Blocked host`, `Blocked private IP literal`, and `Host does not resolve to a public address`. Forwarding them told the caller WHICH rule fired. `Blocked host` confirms a hostname-blocklist match; the resolution message confirms only that DNS returned nothing public. That difference is a policy oracle, and it sharpens as BLOCKED_HOSTNAMES grows. All five rejections now return one fixed `Rejected audioUrl`; the real reason is logged. With this, all seven of the service's error values are fixed literals — verified by enumeration, not assumed. The docblock overstated in the same way #1381's did, which is what this PR was opened to correct, so it has been rewritten to state what is actually true and to name what it does NOT claim: the choice among literals still tells a caller whether their host cleared the guard. That residue is inherent to a guard that refuses some inputs and attempts others, is far coarser than naming the rule, and the route is session-gated — accepted, and now written down rather than glossed. Complementary to #1428, which fixes this at the guard level with SsrfGuardError (public message + private reason). This is the call-site boundary and does not touch ssrf-guard.ts, so the two do not conflict. Verification (head 8d756b6 + this, apps/web): - New test drives all five real guard messages through fetchTranscript and asserts the caller-visible error is identical. Fails without the fix (`expected 5 to be 1`), passes with it. - vitest run: 1 failed | 291 passed (292). The failure, billing-chat-gating "blocks free tier after daily quota", is pre-existing and unrelated — confirmed earlier on the untouched base. Tracked in #1116. - npm run type-check: clean. npm run lint: clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EX3tLZJJfbDUM6XXi6Uefm --------- Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
Canonical issue
Closes #1383 (shares the canonical issue of its base PR #1381 — stacked follow-up, not a competing implementation).
Base is
groupthinking-fix-upstream-error-leakage, notmain.Outcome
Three helpers still had implicit return types after
ae2be83and5e69b7e. Those commits typedupstreamFailure,upstreamUnreachable, and theactivateRequest/webhookRequest/transcribeRequesthelpers; these were missed:getOpenAiHeaders(realtime/session/route.ts)Record<string, string> | nullsdpRequest(realtime-session-route.test.ts)RequestupstreamResponse(realtime-session-route.test.ts)ResponseThis closes out CodeRabbit's strict-mode path instruction ("flag implicit any, missing return types") for the routes #1381 touches.
getOpenAiHeadersalready returned exactly that union on both of its paths, so the annotation is descriptive, not a narrowing.Scope
ae2be83/5e69b7e, and the "add thecopilot-rabbitlabel and obtain an explicit Copilot approval" thread — a repository process gate, not a code change, and not something an agent should self-grant.Why the original commit was dropped rather than merged
Both implementations were compared line by line before discarding mine. Theirs is better on every overlapping point:
upstreamUnreachablewraps the fetch andawait upstream.text(), so a body read that rejects mid-stream is caught too. MyfetchUpstreamonly wrapped the call itself and would have let that case escape. Their version also carries a third regression test for exactly that path.code: 'turnstile_rejected'while keepingerrorverbatim. Mine madecodemirror the dynamicturnstile.error, which is a weaker contract — a machine-readable key that varies isn't a stable key.Merging the original would have regressed both. Dropping it was the correct call, not a concession.
Risk
Verification
Run against head
855dcec.271 passed / 1 failed matches
5e69b7eexactly — this PR adds no tests and changes no counts. The single failure isbilling-chat-gating.test.ts:35"blocks free tier after daily quota", 5000 ms timeout: pre-existing, unrelated, tracked in #1116.agent-completion/truth-gateis structurally unsatisfiable here; see belowae2be83/5e69b7e; this is the residueProduction evidence
Not applicable: type-only change stacked on an unmerged PR, so there is no independent deployment surface and no runtime behavior to observe. Production evidence belongs to #1381.
Known blocker —
agent-completion/truth-gateCannot pass on this PR and is not worth retrying.
agentTaskApplicable(.github/workflows/pr-checks.yml:646-649) engages the agent-task policy on anyclaude/*head branch; the gate then requires a GitHub-native closing-issue reference, which GitHub only creates for PRs targeting the default branch. This PR targetsgroupthinking-fix-upstream-error-leakage, so no issue resolves andpolicy.agent_login/policy.run_id— read from the linked issue body atpr-checks.yml:2162-2165— come back empty. Full analysis in this comment.Merging this into
groupthinking-fix-upstream-error-leakagemoots it: the change is then covered by #1381's own gate, which is green.Agent handoff
ae2be83/5e69b7e