diff --git a/docs/branch-review-ledger.md b/docs/branch-review-ledger.md index 1d36ee39b..74e9fca0a 100644 --- a/docs/branch-review-ledger.md +++ b/docs/branch-review-ledger.md @@ -964,6 +964,8 @@ This file is append-only. Never rewrite or delete an existing review record; app | 2026-07-25 | PR #1190 / `remediate-dark-mode-audit` | `00eca49b9b0d7e5fbfa5703a15e9e930963984a6` | Cursor review+Bugbot+prlanded+debug (fresh pass, same HEAD) | DO NOT MERGE; NOT LANDED (OPEN, mergeable=CONFLICTING/DIRTY, 468 behind / 2 ahead). Reconfirmed P0: conflict resolution deleted `trustGatedAnswerForClinicalNotes` (0 hits on head; main L584/632/659/1075) — Clinical Notes consumes ungated answer. P1: `src/app/api/answer/route.ts:6` imports nonexistent `@/lib/rag` (tsc TS2307; stream correctly uses `@/lib/rag/rag`). P1: merge-tree conflicts on answer/upload/evidence-panels + 10 paths; literal `<<<<<<<` in docs audit plan; migration timestamp collision risk vs main. Intentional dark-mode delta is only commit `363672602` (~10 files). Salvage: `cursor/pr1190-dark-mode-salvage-f453` cherry-picks that commit onto current main, restores unused-manifest-import cleanup, keeps clinical gate. Close #1190 after salvage lands. | Bugbot; `git grep` gate/import/markers; merge-tree; `tsc` TS2307 proof; gh pr view/checks (PR policy fail). Salvage: tsc clean; visual-evidence+overlay tests 13/13; eslint on changed files. No provider/UI matrix. | | 2026-07-25 | execute-audit-remediation-plan (PR #1188) | 8b8639113925601e1687bfe4f1f29c44a4308b61 | Bugbot/diff-review: maintainability remediation tip | BLOCK: tip tree carries unresolved conflict markers (answer/upload APIs, clinical-dashboard, services, tests); invalid `async export function sha256Hex` in indexing-v3 utils; ClinicalDashboard still calls removed `renderSystemNotice`; merge-tree vs main conflicts in check-github-action-pins.mjs + ui-primitives.tsx. Intended notices extraction/dynamic imports look mostly sound; search-scope/migration not in three-dot product delta. | `git grep` conflict markers on tip (none on origin/main); `git show` for ClinicalDashboard:3824 + utils.ts:191; `git merge-tree --write-tree origin/main 8b8639113`; no provider-backed checks. | | 2026-07-25 | PR #1209 / `cursor/pr1186-audit-remediation-c94c` | `4cb22e45dfe46b1975fa15ddd028c7e14ceb5fab` | Close #1186 + babysit #1209 CI | DONE. Closed #1186 as superseded. Synced origin/main (MERGEABLE/CLEAN). Fixed PR-policy RAG impact line. Hosted PR required SUCCESS (Static/Safety/Unit/Build/Production UI/Advisory UI/containers) on pre-ledger tip; docs-only follow-up pushed. Ready to merge; auto-merge not enabled. | gh pr checks; local policy ok; no provider/eval runs. | +| 2026-07-25 | cursor/pr1197-fix-regressions-d06a (PR #1197 fix) | c1e9696de6000440ad5b44e4d2fb2738a858deae | Fix-forward after Bugbot do-not-merge review | FIXED for tip. Synced to origin/main; restored clinical-notes trust gate + boundary tests + summaryMode 400 contract by taking main; removed ISSUE-07 RAG pre-classifier (rag.ts matches main � RAG impact: no retrieval behaviour change). Kept only additive SQL remediation: migration 20260725000000 (real worker URL + ISSUE-05 revokes), schema.sql URL/[REDACTED] fix + ISSUE-05, regenerated drift-manifest. Dropped broken upload authority refs (undefined canonicalAuthority). | Vitest: summaryMode reject + ClinicalNotes boundary 4/4; rag-classifier-memo + rag-tail-latency 17/17; upload smart-title/cleanup 3/3; check:migration-role pass; drift:manifest regenerated via Docker. No provider-backed checks. | +| 2026-07-25 | execute-system-audit-remediation (PR #1197) | c7ae011683614c8de6027347b49e8b3fb79dfa34 | Merge-ready polish: grant reassert + CI green path | READY. Hardened migration/schema to reassert service_role EXECUTE after CREATE OR REPLACE / ISSUE-05 revokes; drift regenerated. Prior regressions remain fixed (trust gate, summaryMode, no RAG pre-classifier). Delta vs main: migration + schema + drift + ledger only. | check:migration-role; drift:manifest; supabase-schema + function-grants Vitest 83/83; awaiting hosted PR required on tip. | | 2026-07-25 | cursor/pr1190-dark-mode-salvage-f453 (PR #1214) | pending-ci-retrigger | CI unblock after bot sync | Hosted pr-branch-sync merged main onto tip (`68d0ceaab`), leaving CI `action_required` for bot-authored runs. Pushing agent commit to re-trigger non-bot CI before squash-merge; then close #1190. | Local pwa-manifest 8/8; trust gate present; prior unit failure fixed. No provider-backed checks. | | 2026-07-25 | cursor/pr1190-dark-mode-salvage-f453 (PR #1214) | pending-ci-retrigger-2 | CI unblock + skip-branch-sync | Repeated pr-branch-sync bot merges left CI `action_required`. Applied `skip-branch-sync` label and agent retrigger so required checks can finish for squash-merge; then close #1190. | Hosted CI pending on tip; no provider-backed checks. | diff --git a/supabase/drift-manifest.json b/supabase/drift-manifest.json index f731ca7db..6bedb04da 100644 --- a/supabase/drift-manifest.json +++ b/supabase/drift-manifest.json @@ -1,9 +1,9 @@ { - "generated_at": "2026-07-24T06:18:38.047Z", + "generated_at": "2026-07-25T15:45:24.778Z", "generator": "scripts/generate-drift-manifest.ts", - "postgres_image": "supabase/postgres:17.6.1.127", - "schema_sha256": "87e910024d52ea024f4b19c34f6e078ecc48160dc50a483fa785677740673db1", - "replay_seconds": 18, + "postgres_image": "supabase/postgres:17.6.1.127@sha256:be60aee15997daca475b710b734bc6bfe52cd544dcd7e9fd2ff58210b6747d83", + "schema_sha256": "81240cb662ec8a9da27eca60a3908587f565c3eb67981aa0c21961d6b333a3d5", + "replay_seconds": 15, "snapshot": { "views": [ { @@ -6775,11 +6775,12 @@ "signature": "public.correct_clinical_query_terms(text,real)" }, { - "schema": "public", - "name": "create_uploaded_document_with_ingestion_job", - "signature": "public.create_uploaded_document_with_ingestion_job(jsonb,integer)", - "language": "plpgsql", - "security_definer": true + "acl": [ + "postgres=X/postgres", + "service_role=X/postgres" + ], + "def_hash": "54f1ab21c9d7cee9555b187dec088371", + "signature": "public.create_uploaded_document_with_ingestion_job(jsonb,integer)" }, { "acl": [ @@ -6897,7 +6898,7 @@ "postgres=X/postgres", "service_role=X/postgres" ], - "def_hash": "4a8d38962265b8fde233dc8657773bf1", + "def_hash": "066ef1277bc11fa8092086fc66352d15", "signature": "public.invoke_ingestion_worker(integer)" }, { diff --git a/supabase/migrations/20260725000000_audit_security_remediation.sql b/supabase/migrations/20260725000000_audit_security_remediation.sql new file mode 100644 index 000000000..fb6b88053 --- /dev/null +++ b/supabase/migrations/20260725000000_audit_security_remediation.sql @@ -0,0 +1,82 @@ +-- Audit remediation: correct invoke_ingestion_worker URL fallback + harden grants. +-- +-- Context: migration 20260724130100 introduced the GUC-backed worker URL pattern +-- but landed with a literal `[REDACTED]` fallback (from an earlier audit export). +-- Hosted apply of that migration can leave cron ingestion pointing at an invalid +-- base URL when `app.ingestion_worker_base_url` is unset. This follow-up restores +-- the real Clinical KB Database project URL and reasserts fail-closed grants. +-- +-- ISSUE-05 / ISSUE-04 reasserts are idempotent with earlier migrations and with +-- supabase/schema.sql. Do not edit the already-applied 20260724130100 blob. + +-- ISSUE-08: set GUC + replace function with the real project URL fallback. +do $$ +begin + execute format('alter database %I set app.ingestion_worker_base_url = %L', + current_database(), 'https://sjrfecxgysukkwxsowpy.supabase.co'); +exception + when insufficient_privilege then + raise notice 'Skipping ALTER DATABASE SET app.ingestion_worker_base_url (insufficient privilege on hosted Supabase); invoke_ingestion_worker falls back to the hardcoded URL.'; +end +$$; + +create or replace function public.invoke_ingestion_worker(p_limit integer default 25) +returns bigint +language plpgsql +security definer +set search_path = public, extensions, vault, pg_temp +as $$ +declare + v_request_id bigint; + v_jwt text; + v_limit integer := greatest(1, least(coalesce("p_limit", 25), 200)); + v_base_url text; +begin + select "decrypted_secret" into v_jwt + from "vault"."decrypted_secrets" + where "name" = 'cron_ingestion_jwt' + limit 1; + + if v_jwt is null or length(trim(v_jwt)) = 0 then + raise exception 'Missing Vault secret: cron_ingestion_jwt'; + end if; + + -- Prefer the GUC; fall back to the hardcoded production URL so that + -- existing deployments that have not yet set the GUC continue to work. + v_base_url := coalesce( + nullif(current_setting('app.ingestion_worker_base_url', true), ''), + 'https://sjrfecxgysukkwxsowpy.supabase.co' + ); + + select "net"."http_post"( + url := v_base_url || '/functions/v1/ingestion-worker?limit=' || v_limit::text, + headers := jsonb_build_object( + 'Content-Type','application/json', + 'Authorization','Bearer ' || v_jwt + ), + body := jsonb_build_object('source','pg_cron','worker','ingestion-worker','ts', now()), + timeout_milliseconds := 60000 + ) + into v_request_id; + + return v_request_id; +end; +$$; + +-- CREATE OR REPLACE can leave grants ambiguous across platforms; reassert explicitly. +revoke execute on function public.invoke_ingestion_worker(integer) from public, anon, authenticated; +grant execute on function public.invoke_ingestion_worker(integer) to service_role; + +-- ISSUE-05: revoke PUBLIC / client-role execute on invoker RPCs (idempotent). +revoke execute on function public.detect_legacy_ivfflat_indexes() from public, anon, authenticated; +revoke execute on function public.document_summary_text(uuid) from public, anon, authenticated; +revoke execute on function public.search_document_chunks(uuid, text, integer, uuid) from public, anon, authenticated; +revoke execute on function public.set_document_embedding_field_content_hash() from public, anon, authenticated; +grant execute on function public.detect_legacy_ivfflat_indexes() to service_role; +grant execute on function public.document_summary_text(uuid) to service_role; +grant execute on function public.search_document_chunks(uuid, text, integer, uuid) to service_role; +grant execute on function public.set_document_embedding_field_content_hash() to service_role; + +-- ISSUE-04: Data API table/sequence fail-closed posture (idempotent with schema.sql). +revoke all privileges on all tables in schema public from anon, authenticated; +revoke all privileges on all sequences in schema public from anon, authenticated; diff --git a/supabase/schema.sql b/supabase/schema.sql index 62e84520d..0dde9afc4 100644 --- a/supabase/schema.sql +++ b/supabase/schema.sql @@ -1,4 +1,4 @@ --- Medical RAG Knowledge Base schema. +-- Medical RAG Knowledge Base schema. -- Run this in the Supabase SQL editor or with the Supabase CLI. -- Tables are RLS protected; the local Next.js API and worker use the service role. -- @@ -3476,7 +3476,7 @@ grant execute on function public.purge_expired_rag_query_misses(integer) to serv do $$ begin execute format('alter database %I set app.ingestion_worker_base_url = %L', - current_database(), '[REDACTED]'); + current_database(), 'https://sjrfecxgysukkwxsowpy.supabase.co'); exception when insufficient_privilege then raise notice 'Skipping ALTER DATABASE SET app.ingestion_worker_base_url (insufficient privilege on hosted Supabase).'; @@ -3506,7 +3506,7 @@ begin v_base_url := coalesce( nullif(current_setting('app.ingestion_worker_base_url', true), ''), - '[REDACTED]' + 'https://sjrfecxgysukkwxsowpy.supabase.co' ); select "net"."http_post"( @@ -9135,3 +9135,13 @@ begin end if; end; $$; + +-- Explicit function revokes (ISSUE-05) +revoke execute on function public.detect_legacy_ivfflat_indexes() from public, anon, authenticated; +revoke execute on function public.document_summary_text(uuid) from public, anon, authenticated; +revoke execute on function public.search_document_chunks(uuid, text, integer, uuid) from public, anon, authenticated; +revoke execute on function public.set_document_embedding_field_content_hash() from public, anon, authenticated; +grant execute on function public.detect_legacy_ivfflat_indexes() to service_role; +grant execute on function public.document_summary_text(uuid) to service_role; +grant execute on function public.search_document_chunks(uuid, text, integer, uuid) to service_role; +grant execute on function public.set_document_embedding_field_content_hash() to service_role;