audit fixes (P4 + P6): make the dormant Clinical Ask and Caring Contacts surfaces genuinely dormant - #2611
Conversation
…(M16) Defect: POST /api/speech/transcribe never consulted the Clinical Ask master flag, so an anonymous caller could drive the paid transcription provider on a feature the operator regards as switched off, and the browser policy still granted microphone=(self) with no dictation control in the product. Trigger: any HTTP client POSTs multipart audio to /api/speech/transcribe while CLINICAL_ASK_ENABLED is false (the default). Fix: the route checks the new clinicalAskEnabled() helper first and answers a no-store 404 before the auth lookup, the durable rate limiter, or the provider; Permissions-Policy now carries microphone=(). Proof: tests/speech-transcription-route.test.ts "answers 404 before access, the rate limiter, or the provider when Clinical Ask is off"; tests/security-headers.test.ts "keeps microphone capture disabled while Clinical Ask dictation has no user entry point". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FSPY4VSqg7WVukCvmHQP9t
…flag is off (L33) Defect: POST /api/clinical-ask/stream parsed the body, performed the Supabase auth lookup and wrote a durable rate-limit row before checking CLINICAL_ASK_ENABLED, and then answered a switched-off feature with an HTTP 200 event-stream carrying a mode_unavailable frame. Trigger: any POST to /api/clinical-ask/stream while CLINICAL_ASK_ENABLED is false (the default). Fix: the route checks clinicalAskEnabled() first and returns a JSON 404 before body parsing, authentication, or the limiter. The per-mode emergency denylist keeps its SSE mode_unavailable frame. Proof: tests/clinical-ask-route.test.ts "answers 404 before authentication or the durable rate limiter when the master flag is off" and "keeps the SSE mode_unavailable frame for a mode on the emergency denylist". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FSPY4VSqg7WVukCvmHQP9t
… (M13) Defect: the Tools catalogue listed Caring Contacts as "ready" with an Open action unconditionally, while every /caring-contacts route and API fails closed (404) in a production process. A signed-in clinician on the live launcher pressed Open and reached a dead link on a suicide-prevention surface. Trigger: any production process (NODE_ENV=production without the isolated Playwright offline server's NEXT_PUBLIC_DEMO_MODE=true). Fix: toolCatalogRecordsForSession now filters the card through isCaringContactsToolListed(), which reads only what the client bundle inlines (NODE_ENV, NEXT_PUBLIC_DEMO_MODE) so server and client agree, and is proved to match isCaringContactsDemoEnabled() in every environment instrumentation.ts lets a server start in. The isolated Playwright production server keeps its entry point. The record itself stays in the catalogue for the mockup fixtures and the category identity registry. Proof: tests/tools-catalog.test.ts, "the Caring Contacts card follows the workspace's production lock" (four cases, including the environment matrix against isCaringContactsDemoEnabled). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FSPY4VSqg7WVukCvmHQP9t
…y search kind (L113) Defect: two silent fallbacks in the mode and tool registries. toolCatalogRecordById returned the first record (PsychSift Search) for any id the catalogue did not know, so a typo rendered the wrong tool card instead of failing. isSearchableAppMode enumerated all twelve AppModeSearchKind values, so it could never return false for a defined mode and named a distinction that SearchableAppModeId (= AppModeId) does not allow. Trigger: any caller passing a misspelt or retired tool id; any reader taking the kind list as a real distinction. Fix: toolCatalogRecordById throws "Unknown tool catalogue id: <id>" (its one caller is a mockup fixture built at module load, whose ids the fixture test already pins). isSearchableAppMode delegates to isAppModeId, with the reason recorded beside it. Proof: tests/tools-catalog.test.ts "throws for an unknown tool id instead of substituting the first tool"; tests/app-modes.test.ts "rejects unknown ids from isSearchableAppMode exactly as isAppModeId does" and "does not re-enumerate every search kind inside isSearchableAppMode". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FSPY4VSqg7WVukCvmHQP9t
Defect: the one variable that switches the Caring Contacts workspace from
its in-memory demo store to Postgres was documented only in the database
test runner, CI and archived plan documents. It was absent from
.env.example and docs/deployment-architecture.md, so check:env-parity could
not know the name, nothing said which service reads it, and the prerequisite
that the login role be a member of caring_contacts_app (migration 0001
grants membership only to the migrating role) was stated nowhere an
operator would read before every `set local role` failed.
Trigger: an operator configuring the workspace's database for a deployment.
Fix: a commented entry in .env.example (which check:env-parity parses) and
one paragraph in docs/deployment-architecture.md §4 naming the reading
service, the fail-closed default, the two project-separation guards, the
role-membership prerequisite, and that no deployment sets it today.
Proof: `node scripts/check-env-parity.mjs` reports the name as known
("Environment parity OK"); docs link check passes.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FSPY4VSqg7WVukCvmHQP9t
Defect: the catalogue tier substituted the whole catalogue whenever the question matched no record (differentials, formulation, specifiers and therapy-compass; services, forms and dsm carried the same inert pattern), so the orchestrator never saw an empty tier and always paid the synthesis call, the governance retry, and coverage annotation over unrelated records. Trigger: any Clinical Ask question in those modes whose text matches no catalogue record. Fix: every mode returns exactly its matches; specifiers now match on the ranked specifier search (the mode's own signal) plus verbatim short-label hits instead of a whole-sentence substring that never matched. An empty tier lets the orchestrator's evidence-gap path fire before any provider call. Proof: tests/clinical-ask-catalogue-evidence.test.ts "returns no $mode evidence when nothing matches instead of the whole catalogue" (all seven modes); tests/clinical-ask-orchestrator.test.ts "reports an evidence gap without synthesis when no tier returns evidence". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FSPY4VSqg7WVukCvmHQP9t
Defect: the Phase 2B handover told a future session to catch up the trunk and then merge four feature branches, and the master progress ledger called Phase 2B "Not started; needs its own plan" and Phase 3 "Not started" — five days after Phase 2B landed on main as 17df388 (#2451, 2026-08-29), with none of the four branches left on origin. Trigger: any session reading these two files as the map for the programme. Fix: a dated banner at the top of both files stating the merge, marking the branch table historical and pointing at the build record rulings; the phase table, session history and task table updated to match. No content below the banners was removed. Proof: `git show -s 17df388` and `git branch -r | grep caring` are the sources of the banner's claims; `npm run docs:check-links` passes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FSPY4VSqg7WVukCvmHQP9t
… the client stream (L17) Defect: streamClinicalAsk treated every non-OK response as internal_error, so the route's JSON 429 (with its Retry-After window) and 401 collapsed into "Clinical Ask stream could not be read." with a retryable flag, inviting immediate retries that kept hitting the limiter; the contract's rate_limited and unauthorized codes existed only in the schema. Trigger: the stream route answers 401 or 429 before the event stream opens. Fix: non-OK responses map by status: 401 becomes unauthorized (not retryable, "Sign in to use Clinical Ask."); 429 becomes rate_limited with the server envelope message plus the Retry-After window (header first, then details.retryAfterSeconds); every other status stays a generic internal_error whose body is never surfaced. Proof: tests/clinical-ask-stream-contract.test.ts "surfaces a 429 as rate_limited with the server retry window instead of internal_error", "surfaces a 401 as unauthorized and not retryable", and "keeps other non-OK statuses as a generic retryable failure without echoing the body". Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FSPY4VSqg7WVukCvmHQP9t
…ed (M34) Defect: the privacy impact assessment said the shared composer can route a question-like typed input into Smart Clinical Ask, but PR #2360 removed the composer controls, so a reviewer reading the PIA would assess a data flow the product cannot start. Trigger: an operator or privacy reviewer reads the "Mode-aware Clinical Ask privacy boundary" section while deciding whether to set CLINICAL_ASK_ENABLED=true. Fix: the paragraph now states that Clinical Ask is gated by the CLINICAL_ASK_ENABLED flag, has had no user entry point since PR #2360, and that its routes answer 404 with the microphone denied while the flag is off. No other PIA section changed. Proof: prose only; tests/privacy-readiness-contract.test.ts and tests/maturity-governance-docs.test.ts still pass. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FSPY4VSqg7WVukCvmHQP9t
The Caring Contacts catalogue test iterated a plain string array into isCaringContactsDemoEnabled, whose environment parameter is the NODE_ENV union; the source typecheck rejected it. Narrowed with `as const`; the test's assertions are unchanged. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FSPY4VSqg7WVukCvmHQP9t
… M16)
Both master-flag 404s called jsonError() with its default log:true, so every
anonymous probe of a switched-off route wrote an error-level "API request
failed" line, which the Sentry Logs allowlist forwards. For the stream route
this was a regression against the previous errorStream() path, which logged
nothing. A designed 404 is not a server fault: pass { log: false }, the
repo's existing precedent for a designed 404/401. Both route tests now pin
that no error or warn log is written on the dormant path (the clinical-ask
route test gains a logger mock for this).
One commit for one defect: the pre-commit documentation hook requires all
touched inputs staged together, so the two routes travel as a single change.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FSPY4VSqg7WVukCvmHQP9t
Traced all three ClinicalAskResponse construction sites (orchestrator.ts and response-governance.ts) and confirmed every one routes evidence through publicClinicalAskEvidence, which blanks extract before it reaches the client, so the disclosure could never show content in production. Removed it rather than gating, since no real code path can ever supply a non-empty extract. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DHSyfuC6mS98ystWFiitAR
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Team Run ID: 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 |
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_e4bf2fe5-e4e3-4a72-8e88-399586db40c1) |
CI triageCI failed on this PR. Automated classification of the 2 failed job(s):
Compared with main CI run #15583 (failure). Classification is evidence routing, not permission to ignore a failure. Exact quarantined Playwright identities remain governed by the flake ledger. |
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_9060e4be-93eb-4920-b1f2-82623aec1693) |
Summary
Two audit remediation packages bundled into one pull request, from
docs/audit/full-repository-audit-2026-09-02.md(PR #2573). They share a theme — a feature that is supposed to be switched off still behaving as if it were on — and touch no file in common, so they gate and review together. Branchclaude/audit-fix-p6is folded in here and is superseded; it needs no pull request of its own.P4 — Dormant Clinical Ask. Findings
M16,L33,L16,L17,M34, plusL124(reassigned here).The substantive change is that
CLINICAL_ASK_ENABLED=falsenow actually means off. Both provider-backed routes consultclinicalAskEnabled()as their first statement and answer a no-store JSON 404 before body parsing, the Supabase auth lookup, the durable rate-limiter write, or any provider client is constructed. Previously a disabled feature still authenticated callers, consumed rate-limiter budget and could reach OpenAI.Permissions-Policynow denies the microphone while the flag is off, and the pinned expectation intests/security-headers.test.tsmoved in the same commit.P6 — Caring Contacts live surface. Findings
M13,L113,L50,L74.L113is the one with teeth beyond the card:toolCatalogRecordByIdreturned the first tool for an unknown id rather than nothing, so a typo or stale id silently resolved to an unrelated tool.L124was verified before it was acted on rather than taken from the audit: all three server paths that build a Clinical Ask response callpublicClinicalAskEvidence, which setsextract: ""before the response leaves the server, so the disclosure could never render content. It was removed rather than gated, since gating on a value that can never arrive is dead code.Decisions needed from the owner:
CARING_CONTACTS_DATABASE_URLis documented as set on no deployment. If a hosted Caring Contacts database is intended before enterprise sign-on exists, the role-membership grant and the Railway service placement need an operator decision and acheck:env-parityentry.RAG impact: none — Clinical Ask is a separate, flag-gated path and the Tools catalogue is not a retrieval surface; no ranking, ordering, comparator, selection or retrieval RPC change, and no file under
src/lib/rag/**was touched.Verification
npm run verify:pr-local—- completed: check:runtime, check:installed-lock-parity, format:changed, check:diff-integrity, sitemap:check, check:repo-awareness-snapshot, docs:check-index, docs:check-inventory, docs:check-scripts, docs:check-links, check:branch-review-ledger, check:outstanding-issues, check:ledger-write-discipline, lint, typecheck, test, build, check:rag:fixtures, check:medication-interactions, check:medication-lexicon-report·- failed: (none)·- not reached: (none)·Test Files 1127 passed | 1 skipped (1128)·Tests 14983 passed | 2 expected fail | 3 skipped (14988)·✓ Compiled successfully in 85snpm run check:diff-integrity—[diff-integrity] PASS — 9 changed test file(s), 86 -> 101 test case(s), against base 51ddfcd83.No test deleted, skipped or weakened; the case count rose by 15.L124proved red-then-green before commit: the new assertion failed against the previous code (found <summary>Review extract</summary>) and passes after —Test Files 1 passed (1) / Tests 4 passed (4).Verification not run:
npm run verify:ui— browser proof left to CI; the pinned Chromium is not installed in this container, and no narrowed browser run is claimed as the full gate.Verification not run:
npm run verify:release— no release or handoff confidence is claimed.Verification not run: provider-backed gates (
check:production-readiness, the liveeval:*commands) — nothing here reaches OpenAI, Supabase, Railway or Sentry at run time; all work was offline. The offlinecheck:rag:fixturesstep did run inside the gate and passed.Risk and rollout
mode_unavailableframe by design, so only the master flag produces the hard 404. ForM13, the card is hidden using a catalogue-local predicate that readsNODE_ENVandNEXT_PUBLIC_DEMO_MODE, because the real server-side predicate also readsPLAYWRIGHT_OFFLINE_MODE, which never reaches the client bundle. The two are proved equal by test in every environmentsrc/instrumentation.tspermits a server to start in, and the test pins the instrumentation refusal string — but it is a mirror, not a shared implementation, and would need revisiting if a new environment shape were introduced.Clinical Governance Preflight
Each item confirmed against this pull request's diff. The checked line is the exact policy item; the note beneath it is the evidence.
unchanged; no citation or verification path touched.
none introduced or expanded; the Caring Contacts card is withdrawn from the launcher, narrowing exposure.
Clinical KB Database(sjrfecxgysukkwxsowpy)no Supabase env value, migration target or configured project changes.
unchanged; the routes now refuse before constructing any Supabase client.
reinforced: the Caring Contacts demonstration surface no longer appears as an openable tool outside demo mode.
unchanged.
reviewed; no decision-support behaviour is added — a dormant path is closed and a demonstration card withdrawn.
Notes
L124was implemented and verified in this session.origin/mainwas merged into this branch (not rebased) before gating; the branch was level withorigin/mainat push time.🤖 Generated with Claude Code
https://claude.ai/code/session_01DHSyfuC6mS98ystWFiitAR
Generated by Claude Code
Note
Medium Risk
Touches API gates, security headers, and Tools launcher visibility for suicide-prevention and provider-backed paths; behavior is heavily tested but Clinical Ask’s client/server lock predicates remain mirrored rather than shared.
Overview
Audit remediation (P4 + P6) makes two “off” surfaces behave as off instead of still doing work or advertising dead links.
Clinical Ask (
CLINICAL_ASK_ENABLED=false)./api/clinical-ask/streamand/api/speech/transcribenow callclinicalAskEnabled()first and return a no-store JSON 404 (without error-level logging) before body parsing, Supabase auth, durable rate limits, or OpenAI. Per-mode disable still uses the existing SSEmode_unavailablepath when the master flag is on. Permissions-Policy denies the microphone while there is no governed dictation UI. Catalogue retrieval no longer backfills with an empty query when a question matches nothing—empty evidence lets the orchestrator hit evidence_gap without synthesis. The client stream maps 401 and 429 tounauthorized/rate_limitedwith retry hints. The always-empty “Review extract” disclosure is removed from the answer surface. Privacy copy documents that Clinical Ask has no composer entry point when dormant.Caring Contacts launcher.
toolCatalogRecordsForSessionhides the caring-contacts card in production unlessNEXT_PUBLIC_DEMO_MODE=true(client-safe mirror of the workspace lock).toolCatalogRecordByIdthrows on unknown ids instead of silently returning the first catalogue entry. Operators get documentedCARING_CONTACTS_DATABASE_URLin.env.exampleand deployment architecture; Caring Contacts progress/handover docs record Phase 2B merged onmain.Minor:
isSearchableAppModedelegates toisAppModeIdinstead of re-listing every search kind.Reviewed by Cursor Bugbot for commit 6fe074f. Configure here.