privacy: enforce query-hash secret in prod + purge rag_query_misses (PIA-2, PIA-4)#368
Merged
Merged
Conversation
Two low-risk hardening fixes from the privacy impact assessment (PIA-2, PIA-4). PIA-2 — RAG_QUERY_HASH_SECRET is now required at production startup. Without it, logged clinical-query hashes silently degrade from keyed HMAC-SHA256 to unsalted, dictionary-reversible SHA-256, defeating the redaction. requireQueryHashSecret() (src/lib/env.ts) is called from instrumentation.register(), which already fails closed on demo/no-auth/misconfig; unit-tested in tests/env-query-hash-secret.test.ts. PIA-4 — add a nightly pg_cron purge for rag_query_misses (90 days), which stored the same hash-redacted query telemetry as rag_queries/rag_retrieval_logs but had no retention bound. New migration only; schema.sql + drift-manifest are regenerated from live via `npm run drift:manifest` (Docker) as the usual follow-up. Not included: the fail-closed retrieval_owner_matches change (tenancy review §6 item 1) — it sits on the retrieval hot path with ~20 RPC sites and needs the live golden-eval to verify, which can't run without the Supabase key. Held as a separate, verified change. Verified: typecheck, eslint, prettier, and the relevant vitest suites (env, privacy, owner-scope, supabase-schema, drift-detection, check-runtime, api-route-coverage) pass. Live cron scheduling is verifiable only against the live project. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
|
Updates to Preview Branch (claude/privacy-tenancy-hardening) ↗︎
Tasks are run on every commit but only new migration files are pushed.
View logs for this Workflow Run ↗︎. |
The startup guard added in this branch (requireQueryHashSecret) makes the secret mandatory in production, so update .env.example from "strongly recommended" to "required", with a generation command. Placeholder only — no real value committed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
BigSimmo
enabled auto-merge (squash)
July 8, 2026 06:21
The new requireQueryHashSecret() gate made the existing "starts a fully configured production server" case throw. Supply RAG_QUERY_HASH_SECRET in that case and add a case asserting register() rejects when it is absent (ordered after the OpenAI check, matching instrumentation.ts). Full vitest suite green (1292 passed). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Implements the two low-risk, locally-verifiable fixes from the privacy impact assessment. Follow-up to the analysis in #330.
PIA-2 (High) — require
RAG_QUERY_HASH_SECRETin productionClinical query text is redacted to a keyed HMAC pseudonym before logging (
query-privacy.ts). Without the secret, the hash silently degrades to unsalted, dictionary-reversible SHA-256 — a reader of the log tables can hash candidate patient/drug strings offline and match rows. Nothing enforced the secret.requireQueryHashSecret()insrc/lib/env.ts, called frominstrumentation.register()— the existing production-only startup gate that already fails closed on demo/no-auth/misconfig. Missing secret now refuses to start with an actionable error.tests/env-query-hash-secret.test.ts.PIA-4 (Medium) — bound
rag_query_missesretentionIt stored the same hash-redacted query telemetry as
rag_queries(30d) andrag_retrieval_logs(90d) but had no purge, so rows accumulated indefinitely.20260708120000_rag_query_misses_retention.sql:purge_expired_rag_query_misses(90)+ nightlypg_cronjobpurge-rag-query-misses@ 03:45 UTC (installs the function even where pg_cron is absent, matching the existing retention migrations).schema.sql+drift-manifest.jsonare intentionally not hand-edited — they're regenerated from live vianpm run drift:manifest(Docker) as the standard follow-up after the migration applies.Deliberately not in this PR
The fail-closed
retrieval_owner_matcheschange (tenancy review §6, item 1). It's on the retrieval hot path with ~20 RPC call sites and internal default-nullcallers, and its only real verification is the live golden eval, which can't run without the Supabase key. Its production benefit is a future-regression backstop (prod already throws beforenullreaches the RPC). It'll come as its own verified change rather than shipped blind.Verification
typecheck,eslint,prettierclean; vitest green forenv-query-hash-secret,env-demo-mode,privacy,owner-scope,supabase-schema,drift-detection,check-runtime,api-route-coverage. Livepg_cronscheduling is only verifiable against the live project.Operator action
Set
RAG_QUERY_HASH_SECRET(random, ≥16 chars) on the live project before this deploys, or production startup will fail closed (by design).🤖 Generated with Claude Code