Skip to content

audit fixes (P4 + P6): make the dormant Clinical Ask and Caring Contacts surfaces genuinely dormant - #2611

Merged
BigSimmo merged 20 commits into
mainfrom
claude/audit-fix-p4
Sep 4, 2026
Merged

audit fixes (P4 + P6): make the dormant Clinical Ask and Caring Contacts surfaces genuinely dormant#2611
BigSimmo merged 20 commits into
mainfrom
claude/audit-fix-p4

Conversation

@BigSimmo

@BigSimmo BigSimmo commented Sep 4, 2026

Copy link
Copy Markdown
Owner

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. Branch claude/audit-fix-p6 is folded in here and is superseded; it needs no pull request of its own.

P4 — Dormant Clinical Ask. Findings M16, L33, L16, L17, M34, plus L124 (reassigned here).

  • b19a05e speech: answer 404 and deny the microphone while Clinical Ask is off (M16)
  • 8c58bed clinical-ask: answer 404 before auth and the limiter when the master flag is off (L33)
  • f5af132 clinical-ask: return no catalogue evidence when nothing matches (L16)
  • 551bd66 clinical-ask: surface 401 and 429 as unauthorized and rate_limited in the client stream (L17)
  • 770d1ee docs: state that Clinical Ask has no user entry point and is flag-gated (M34)
  • 52fe0f1 clinical-ask: remove always-empty "Review extract" disclosure (L124)

The substantive change is that CLINICAL_ASK_ENABLED=false now actually means off. Both provider-backed routes consult clinicalAskEnabled() 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-Policy now denies the microphone while the flag is off, and the pinned expectation in tests/security-headers.test.ts moved in the same commit.

P6 — Caring Contacts live surface. Findings M13, L113, L50, L74.

L113 is the one with teeth beyond the card: toolCatalogRecordById returned the first tool for an unknown id rather than nothing, so a typo or stale id silently resolved to an unrelated tool.

L124 was verified before it was acted on rather than taken from the audit: all three server paths that build a Clinical Ask response call publicClinicalAskEvidence, which sets extract: "" 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:

  • P6/M13: the card is now hidden entirely when the workspace is locked. The package notes permitted either that or a visible "demonstration only" card with no Open action; hiding was chosen as the smaller change. Say if you would rather it stayed visible and disabled.
  • P6/L50: CARING_CONTACTS_DATABASE_URL is 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 a check:env-parity entry.
  • P4/M34 follow-on (product, not taken here): whether Clinical Ask stays dormant, gets a governed composer action reinstated behind the flag, or has its client layer retired.

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 85s
  • npm 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.
  • L124 proved 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 live eval:* commands) — nothing here reaches OpenAI, Supabase, Railway or Sentry at run time; all work was offline. The offline check:rag:fixtures step did run inside the gate and passed.

Risk and rollout

  • Risk: medium — the change makes two provider-backed routes refuse earlier and removes a card from the Tools launcher; both are pinned by new tests and adversarially reviewed. Residual risk: the per-mode denylist still answers over the 200 SSE mode_unavailable frame by design, so only the master flag produces the hard 404. For M13, the card is hidden using a catalogue-local predicate that reads NODE_ENV and NEXT_PUBLIC_DEMO_MODE, because the real server-side predicate also reads PLAYWRIGHT_OFFLINE_MODE, which never reaches the client bundle. The two are proved equal by test in every environment src/instrumentation.ts permits 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.
  • Rollback: revert this pull request's commits; each is one finding and reverts independently.
  • Provider or production effects: None. The 404 path removes provider calls rather than adding any.
  • RAG impact: none.

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.

  • Source-backed claims still require linked source verification before clinical use
    unchanged; no citation or verification path touched.
  • No patient-identifiable document workflow was introduced or expanded without explicit governance approval
    none introduced or expanded; the Caring Contacts card is withdrawn from the launcher, narrowing exposure.
  • Supabase target remains Clinical KB Database (sjrfecxgysukkwxsowpy)
    no Supabase env value, migration target or configured project changes.
  • Service-role keys and private document access remain server-only
    unchanged; the routes now refuse before constructing any Supabase client.
  • Demo/synthetic content remains clearly separated from real clinical sources
    reinforced: the Caring Contacts demonstration surface no longer appears as an openable tool outside demo mode.
  • Source metadata, review status, and outdated/unknown-source behavior remain conservative
    unchanged.
  • Deployment classification/TGA SaMD impact was checked when clinical decision-support behavior changed
    reviewed; no decision-support behaviour is added — a dormant path is closed and a demonstration card withdrawn.

Notes

  • P6's adversarial review passed; P4's passed after one fix round. L124 was implemented and verified in this session.
  • origin/main was merged into this branch (not rebased) before gating; the branch was level with origin/main at push time.
  • One owner per file across the remediation programme: no other open remediation pull request edits these files. Files in common between P4 and P6: none.

🤖 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/stream and /api/speech/transcribe now call clinicalAskEnabled() 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 SSE mode_unavailable path 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 to unauthorized / rate_limited with 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. toolCatalogRecordsForSession hides the caring-contacts card in production unless NEXT_PUBLIC_DEMO_MODE=true (client-safe mirror of the workspace lock). toolCatalogRecordById throws on unknown ids instead of silently returning the first catalogue entry. Operators get documented CARING_CONTACTS_DATABASE_URL in .env.example and deployment architecture; Caring Contacts progress/handover docs record Phase 2B merged on main.

Minor: isSearchableAppMode delegates to isAppModeId instead of re-listing every search kind.

Reviewed by Cursor Bugbot for commit 6fe074f. Configure here.

…(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
@supabase

supabase Bot commented Sep 4, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project sjrfecxgysukkwxsowpy because there are no changes detected in supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Team

Run ID: 57773d4f-36b0-4b22-ad23-7b1a492c9721


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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@BigSimmo
BigSimmo marked this pull request as ready for review September 4, 2026 12:48
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

@cursor

cursor Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Bugbot couldn't run - usage limit reached

Bugbot 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)

@BigSimmo
BigSimmo enabled auto-merge (squash) September 4, 2026 13:37
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

CI triage

CI failed on this PR. Automated classification of the 2 failed job(s):

  • Production UI (3)needs investigation: inspect the failing step and uploaded diagnostics; rerun only after classifying the cause.
  • PR requiredmain-side: the same job also failed on the latest completed main CI run.

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.

@cursor

cursor Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Bugbot couldn't run - usage limit reached

Bugbot 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)

@BigSimmo
BigSimmo merged commit ea6bf19 into main Sep 4, 2026
33 checks passed
@BigSimmo
BigSimmo deleted the claude/audit-fix-p4 branch September 4, 2026 15:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants