diff --git a/data/outstanding-issues-snapshot.json b/data/outstanding-issues-snapshot.json index 7c19dba753..51dad380bf 100644 --- a/data/outstanding-issues-snapshot.json +++ b/data/outstanding-issues-snapshot.json @@ -10,7 +10,7 @@ "p2": 57, "p3": 33, "queued": 9, - "pending": 12, + "pending": 19, "resolved": 412 }, "queue": [ @@ -948,18 +948,54 @@ "summary": "Cancel request 8b2a3f89-9bdb-4fb1-8fd0-35165d050263: Implemented the requested local source-card width constant in this owning answer-page change.", "created_at": "2026-08-25" }, + { + "request_id": "3eebb95f-3671-41d0-afa8-85460a9b5ff3", + "action": "done", + "summary": "#321: Standardized control focus rings and ARIA descriptors across modal and drawer surfaces.", + "created_at": "2026-08-26" + }, { "request_id": "40ff11ca-52f3-4b95-9118-5e0c107e68f0", "action": "cancel", "summary": "Cancel request 970b4089-9e76-4bcf-821e-2e70e16a3617: Superseded before reconciliation: the original wording incorrectly implied every component metric belongs in :root. The replacement distinguishes module-local one-consumer constants from shared CSS custom properties.", "created_at": "2026-08-26" }, + { + "request_id": "454d0cd5-8f4c-4ab9-b9b3-7b5a588283a1", + "action": "cancel", + "summary": "Cancel request 76744c9f-6e7f-4390-a202-756e50be0cd1: Superseded by a539a411-4648-4f71-8b40-90d49646e8a4 on #243HCC", + "created_at": "2026-08-27" + }, { "request_id": "485bdac6-7f79-4539-9d90-347866604b35", "action": "add", "summary": "Overflow menus split between a real ARIA menu and menu roles with no keyboard model", "created_at": "2026-08-25" }, + { + "request_id": "531e6569-42da-4bb6-9c0f-6a410e207aea", + "action": "cancel", + "summary": "Cancel request 854ca9ba-ba3b-442b-be6d-6f5873ed60ed: Wired connection and permission unavailable recovery screens in Caring Contacts.", + "created_at": "2026-08-26" + }, + { + "request_id": "5fa0b2ee-38ea-4a6a-bada-815b021b5aac", + "action": "cancel", + "summary": "Cancel request a0548b2c-2c80-44f9-9440-5b983e169a13: Repaired closing-message refusal guard in Caring Contacts service state.", + "created_at": "2026-08-26" + }, + { + "request_id": "6e929764-e281-4941-9ef4-062ec5a08497", + "action": "cancel", + "summary": "Cancel request 1838b99a-9323-4e3b-84a5-8e52535ecec8: Connected governed-message validator to outbound dispatch routes with fail-closed enforcement.", + "created_at": "2026-08-26" + }, + { + "request_id": "76744c9f-6e7f-4390-a202-756e50be0cd1", + "action": "done", + "summary": "#243HCC: Emitted data-settings-nav-target on sub-navigation tabs in settings-dialog.", + "created_at": "2026-08-26" + }, { "request_id": "8b2a3f89-9bdb-4fb1-8fd0-35165d050263", "action": "add", @@ -978,6 +1014,12 @@ "summary": "Ward Flow: six agreed enhancements not yet assigned to a phase", "created_at": "2026-08-26" }, + { + "request_id": "a5dd669d-8d52-4b67-b249-51b0757a3b9f", + "action": "done", + "summary": "#45V4Y7: Removed verified dead exports answerQuestion in src/lib/rag/rag.ts, embedText in src/lib/openai.ts, and clinicalRankScore in src/lib/clinical-search.ts. Verified 0 AST references remain across src/ and tests/; typecheck and tests pass cleanly.", + "created_at": "2026-08-27" + }, { "request_id": "b85ad821-4c9b-4925-b86b-2d5767052b20", "action": "add", diff --git a/docs/audit/live-drift-forensics-2026-08.md b/docs/audit/live-drift-forensics-2026-08.md index 5e7c2c1675..4426d98449 100644 --- a/docs/audit/live-drift-forensics-2026-08.md +++ b/docs/audit/live-drift-forensics-2026-08.md @@ -1240,7 +1240,7 @@ and the green live-drift dispatch also remain **pending** for the full phase. _Owner-authorised window against `Clinical KB Database` (`sjrfecxgysukkwxsowpy`) for index DDL plus a `supabase db push` of the guard migrations. Executed from a dedicated worktree; the main checkout -`D:\Repos\Database` stayed linked to STAGING throughout. D4 is OFF (the Supabase GitHub auto-deploy +`D:\Repos\Database` stayed linked to STAGING throughout. D4 is OFF *(superseded: see §D4 — SETTLED 2026-08-21; deploy-on-merge is ON)* (the Supabase GitHub auto-deploy was disabled before this window), so nothing in this task reached production on merge — every hosted change below was made by the explicit step that names it._ @@ -1969,7 +1969,7 @@ objects}`; classes `validation` (mandatory from 2026-08-18), `superseded`, `no_d _Worker session for `#Q5JHBJ` only (`#316`, `#231`, `#1K6T35` untouched). Pre-flight per `#292`: none of the seven open PRs (#2181, #2180, #2176, #2173, #2012, #2011, #2010) touches `supabase/**`, -`scripts/check-drift.ts`, `tests/migration-history-guards.test.ts` or this file. D4 is OFF, so the only +`scripts/check-drift.ts`, `tests/migration-history-guards.test.ts` or this file. D4 is OFF *(superseded: see §D4 — SETTLED 2026-08-21; deploy-on-merge is ON)*, so the only production writes are the explicit `db push` recorded below._ #### Step 1 — classification of the fifteen (every one `validation`; none earned `superseded` or `no_ddl`) diff --git a/docs/openai-cross-border-basis.md b/docs/openai-cross-border-basis.md index 73a04578fe..e03cbe07dc 100644 --- a/docs/openai-cross-border-basis.md +++ b/docs/openai-cross-border-basis.md @@ -1,8 +1,8 @@ -# Cross-border disclosure basis — OpenAI (PIA-1) +# Cross-border disclosure basis — OpenAI (PIA-1) -**Status:** Provider configuration and legal execution pending · **Date reviewed:** 2026-08-23 +**Status:** Provider configuration and legal execution pending · **Date reviewed:** 2026-08-23 **Owner of the open step:** account holder for `OPENAI_API_KEY` + privacy adviser -**Tracks:** the OpenAI contractual portion of **PIA-1** in [docs/privacy-impact-assessment.md](privacy-impact-assessment.md) §10; Railway's companion contract is tracked below. +**Tracks:** the OpenAI contractual portion of **PIA-1** in [docs/privacy-impact-assessment.md](privacy-impact-assessment.md) §10; Railway's companion contract is tracked below. **Companion:** the `/privacy` collection notice ([src/app/privacy/page.tsx](../src/app/privacy/page.tsx)) and composer reminder ([src/lib/ui-copy.ts](../src/lib/ui-copy.ts)) ship as draft APP 5 / APP 1 controls pending governance approval under **PIA-5**. **Status authority:** [`docs/governance/privacy-readiness.v1.json`](governance/privacy-readiness.v1.json). This decision record supplies context and operator steps; it does not prove DPA execution, ZDR configuration, APP 8 approval, or final notice approval. @@ -17,7 +17,7 @@ ## 1. Why this exists This document covers the model-provider leg: query text + retrieved excerpts sent to OpenAI in the -United States for embedding and answer synthesis (PIA §3–4; verified still true in code — +United States for embedding and answer synthesis (PIA §3–4; verified still true in code — [src/lib/openai.ts:75-79](../src/lib/openai.ts) builds a plain `new OpenAI({ apiKey, timeout, maxRetries })` with no `baseURL`/ZDR header and `store:false` by default. GPT-5.6-and-later requests use `prompt_cache_options.ttl="30m"`; gpt-5.5 requests force the legacy @@ -31,73 +31,73 @@ Two obligations attach to that flow: take **reasonable steps** to ensure the recipient handles it consistently with the APPs. Under **s16C** of the _Privacy Act 1988_ (Cth) the discloser stays **accountable** for the overseas recipient's acts unless an APP 8.2 exception applies. Health/mental-health data is _sensitive - information_ — the highest-protection category — so this is the launch-critical item. + information_ — the highest-protection category — so this is the launch-critical item. - **APP 5 (notification).** Individuals must be told their information is disclosed overseas. Draft wording is **already shipped** in the `/privacy` page and composer notice; governance approval - remains open under **PIA-5** (see §7). + remains open under **PIA-5** (see §7). -The code-side controls cannot _by themselves_ discharge APP 8 — the "reasonable steps" are largely +The code-side controls cannot _by themselves_ discharge APP 8 — the "reasonable steps" are largely **contractual**. That contract is the open step this document tracks. ## 2. What actually crosses the border -| Egress | Payload | Endpoint | Reference | -| --------- | ---------------------------------------------------------------- | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | -| Embedding | Raw query text (normalized) | `POST /v1/embeddings` (`text-embedding-3-small`) | [openai.ts embedText](../src/lib/openai.ts) | -| Answer | Raw query verbatim + retrieved chunk text + static system prompt | `POST /v1/responses` (Terra fast / Sol strong, `store:false`) | [rag.ts](../src/lib/rag/rag.ts) · [rag-source-block.ts](../src/lib/rag/rag-source-block.ts) | +| Egress | Payload | Endpoint | Reference | +| --------- | ---------------------------------------------------------------- | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | +| Embedding | Raw query text (normalized) | `POST /v1/embeddings` (`text-embedding-3-small`) | [openai.ts embedTextWithTelemetry](../src/lib/openai.ts) | +| Answer | Raw query verbatim + retrieved chunk text + static system prompt | `POST /v1/responses` (Terra fast / Sol strong, `store:false`) | [rag.ts](../src/lib/rag/rag.ts) · [rag-source-block.ts](../src/lib/rag/rag-source-block.ts) | The app **adds no raw patient or owner identifiers** and stores queries only as a keyed hash locally. When configured, authenticated Responses requests include a stable HMAC-SHA256 `safety_identifier`; anonymous and background requests omit it. The app does **not scrub** PHI a clinician types. Everything else (documents, embeddings, logs, auth) stays at rest in -**Sydney — AWS `ap-southeast-2`** (PIA §7). +**Sydney — AWS `ap-southeast-2`** (PIA §7). ## 3. OpenAI's current terms (verified 2026-07-13) -Facts pulled from OpenAI's public policy/docs pages on 2026-07-13. **Re-verify at execution time** — +Facts pulled from OpenAI's public policy/docs pages on 2026-07-13. **Re-verify at execution time** — these terms change; the PIA (2026-07-06) already predates the Australia data-residency option below. -| Item | Current position | Source | -| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| **DPA available** | OpenAI executes a Data Processing Addendum for API customers; OpenAI acts as **processor**, and binds each sub-processor to comparable obligations. Current version `v.010126` (1 Jan 2026). | [DPA](https://openai.com/policies/data-processing-addendum/) · [DPA PDF](https://cdn.openai.com/pdf/openai-data-processing-addendum.pdf) | -| **Training** | API inputs/outputs are **not used to train models** by default (API opt-out since 1 Mar 2023). | [Data controls](https://developers.openai.com/api/docs/guides/your-data) | -| **Default retention** | Inputs/outputs retained **up to 30 days** for abuse monitoring, then deleted. | [Data controls](https://developers.openai.com/api/docs/guides/your-data) | -| **Zero Data Retention (ZDR)** | Removes the 30-day abuse-monitoring retention; **not self-serve** — prior approval by OpenAI, configured per **project**. Apply via the account/sales team. | [Data controls](https://developers.openai.com/api/docs/guides/your-data) | -| **Data residency** | API data residency now covers **Australia** (among US, Europe, UK, Canada, Japan, Korea, Singapore, India, UAE). Enabled by creating a **new Project** and selecting the country; eligibility via sales. **Australia = storage at rest only** — regional _processing/inference_ is US/Europe/UAE only. ~10% uplift for models released from 5 Mar 2026. | [Data residency (API)](https://help.openai.com/en/articles/10503543-data-residency-for-the-openai-api) · [Announcement](https://openai.com/index/expanding-data-residency-access-to-business-customers-worldwide/) | -| **Sub-processors** | Published list of sub-processors that may process Customer Data. Review for the APP 8 accountability chain. | [Sub-processor list](https://openai.com/policies/sub-processor-list/) · [platform](https://platform.openai.com/subprocessors) | -| **Prompt caching** | GPT-5.6 requests `prompt_cache_options.ttl="30m"` by default and never receives the deprecated retention field. The TTL is a minimum, not a guaranteed deletion deadline. Explicit pre-5.6 deployments retain the legacy retention behavior. ZDR interaction must be **confirmed in writing** (see §6, PIA-6). | [Prompt caching](https://developers.openai.com/api/docs/guides/prompt-caching) · [Data controls](https://developers.openai.com/api/docs/guides/your-data) | +| Item | Current position | Source | +| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **DPA available** | OpenAI executes a Data Processing Addendum for API customers; OpenAI acts as **processor**, and binds each sub-processor to comparable obligations. Current version `v.010126` (1 Jan 2026). | [DPA](https://openai.com/policies/data-processing-addendum/) · [DPA PDF](https://cdn.openai.com/pdf/openai-data-processing-addendum.pdf) | +| **Training** | API inputs/outputs are **not used to train models** by default (API opt-out since 1 Mar 2023). | [Data controls](https://developers.openai.com/api/docs/guides/your-data) | +| **Default retention** | Inputs/outputs retained **up to 30 days** for abuse monitoring, then deleted. | [Data controls](https://developers.openai.com/api/docs/guides/your-data) | +| **Zero Data Retention (ZDR)** | Removes the 30-day abuse-monitoring retention; **not self-serve** — prior approval by OpenAI, configured per **project**. Apply via the account/sales team. | [Data controls](https://developers.openai.com/api/docs/guides/your-data) | +| **Data residency** | API data residency now covers **Australia** (among US, Europe, UK, Canada, Japan, Korea, Singapore, India, UAE). Enabled by creating a **new Project** and selecting the country; eligibility via sales. **Australia = storage at rest only** — regional _processing/inference_ is US/Europe/UAE only. ~10% uplift for models released from 5 Mar 2026. | [Data residency (API)](https://help.openai.com/en/articles/10503543-data-residency-for-the-openai-api) · [Announcement](https://openai.com/index/expanding-data-residency-access-to-business-customers-worldwide/) | +| **Sub-processors** | Published list of sub-processors that may process Customer Data. Review for the APP 8 accountability chain. | [Sub-processor list](https://openai.com/policies/sub-processor-list/) · [platform](https://platform.openai.com/subprocessors) | +| **Prompt caching** | GPT-5.6 requests `prompt_cache_options.ttl="30m"` by default and never receives the deprecated retention field. The TTL is a minimum, not a guaranteed deletion deadline. Explicit pre-5.6 deployments retain the legacy retention behavior. ZDR interaction must be **confirmed in writing** (see §6, PIA-6). | [Prompt caching](https://developers.openai.com/api/docs/guides/prompt-caching) · [Data controls](https://developers.openai.com/api/docs/guides/your-data) | ## 4. This app's endpoints are ZDR-eligible ZDR **excludes** stateful products: Conversations, Assistants/threads, ChatKit, `/v1/files`, vector stores, fine-tuning, video, vision fine-tuning. This app uses **only** `/v1/responses` (stateless, -`store:false`) and `/v1/embeddings` — **neither is on the exclusion list**. So the two egress points +`store:false`) and `/v1/embeddings` — **neither is on the exclusion list**. So the two egress points that carry PHI are exactly the ones ZDR is designed to cover. This is the strongest lever available. -## 5. Recommended basis to satisfy APP 8 _(engineering interpretation — counsel to confirm)_ +## 5. Recommended basis to satisfy APP 8 _(engineering interpretation — counsel to confirm)_ Relying on **APP 8.1 "reasonable steps"** (a binding contract that holds the recipient to -APP-comparable handling) is the mainstream, defensible path — **not** consent under APP 8.2(b), which +APP-comparable handling) is the mainstream, defensible path — **not** consent under APP 8.2(b), which is fragile as a sole basis for sensitive health information. The "reasonable steps" package: -1. **Executed OpenAI DPA** — the contractual spine (processor obligations, sub-processor flow-down, +1. **Executed OpenAI DPA** — the contractual spine (processor obligations, sub-processor flow-down, security, breach notice, SCC-equivalent terms). **Required.** -2. **ZDR on the project** behind the production key — removes the 30-day retention for both egress - points (§4). **Strongly recommended.** -3. **Australia data residency** for storage at rest — keeps stored content onshore (inference still +2. **ZDR on the project** behind the production key — removes the 30-day retention for both egress + points (§4). **Strongly recommended.** +3. **Australia data residency** for storage at rest — keeps stored content onshore (inference still crosses; PHI-minimisation reduces what inference sees). **Optional but high-value** for a WA clinical posture; weigh against the ~10% cost uplift. -4. **No-training default** (already OpenAI's API default) — confirm in the executed contract. +4. **No-training default** (already OpenAI's API default) — confirm in the executed contract. 5. **The app's own minimisation** as documented "reasonable steps" under APP 11: query hashing at rest, `store:false`, Sydney residency, and the shipped PHI reminder (do-not-enter-identifiers). -Items 1–2 (plus documenting 4–5) are what turn PIA-1 from open to closed. Item 3 strengthens it. +Items 1–2 (plus documenting 4–5) are what turn PIA-1 from open to closed. Item 3 strengthens it. ## 6. Open question to pin with OpenAI **What is the effective prompt-cache deletion behavior under ZDR for GPT-5.6 requests that specify the 30-minute TTL, and for requests where the app omits the extended TTL option?** Get this in -writing — it determines whether **PIA-6** is fully resolved by ZDR or merely mitigated. Record the +writing — it determines whether **PIA-6** is fully resolved by ZDR or merely mitigated. Record the answer in the status block. ## 7. Consistency with the shipped user-facing notice @@ -108,7 +108,7 @@ The `/privacy` page and composer notice tell users that durable database/storage provider retention and local 30d/90d retention are disclosed. This document must stay consistent with those claims. -- **Merge status:** the draft APP-5/1 controls are **live on `main`** — `src/app/privacy/page.tsx` and +- **Merge status:** the draft APP-5/1 controls are **live on `main`** — `src/app/privacy/page.tsx` and the composer notice landed via **PR #513** (`eeb2340ad`). Their final governance approval remains open under **PIA-5**, alongside the APP 8 contractual basis below. - **Follow-up:** if **Australia data residency** is enabled, update the "where stored" section to @@ -117,7 +117,7 @@ with those claims. ## 8. Operator action checklist -Actions **1–3 must be performed by the account holder** in OpenAI's dashboard/legal process — they +Actions **1–3 must be performed by the account holder** in OpenAI's dashboard/legal process — they involve accepting agreements and changing account settings, which an automated agent must not do. The operator approved pursuing these steps on 2026-07-14. That approval authorises evaluation and @@ -128,7 +128,7 @@ available through the inspected API surface and remains to be confirmed in the p in writing. - [ ] **1. Execute the OpenAI DPA** for the org behind the production `OPENAI_API_KEY` - → [openai.com/policies/data-processing-addendum](https://openai.com/policies/data-processing-addendum/). + → [openai.com/policies/data-processing-addendum](https://openai.com/policies/data-processing-addendum/). Store the countersigned copy; record version + date below. - [ ] **2. Execute Railway's DPA** with the legal entity and authorised signer. Retain the executed copy and record the Singapore processor/sub-processor basis. @@ -136,18 +136,18 @@ in writing. covers `/v1/responses` + `/v1/embeddings`. Record project id + approval date. - [ ] **4. Decide on Australia data residency** (new Project + country selection; sales-gated). Record region + date, or record an explicit decision not to adopt it and why. -- [ ] **5. Confirm the ZDR ↔ prompt-cache behaviour** in writing (§6); record the answer. +- [ ] **5. Confirm the ZDR ↔ prompt-cache behaviour** in writing (§6); record the answer. - [ ] **6. Review both providers' sub-processor lists** for anything counsel should note in the APP 8 chain. -- [ ] **7. Legal sign-off** that the §5 package satisfies APP 8 for sensitive health information and +- [ ] **7. Legal sign-off** that the §5 package satisfies APP 8 for sensitive health information and approves the shipped draft APP 5/1 wording under PIA-5. -- [ ] **8. Keep `/privacy` copy in sync** if AU residency is adopted (§7) — note US/AU storage. -- [ ] **9. Code follow-ups** once the above land (§9), if adopted. +- [ ] **8. Keep `/privacy` copy in sync** if AU residency is adopted (§7) — note US/AU storage. +- [ ] **9. Code follow-ups** once the above land (§9), if adopted. > Draft APP 5/1 controls (the collection notice + `/privacy` page) are live on `main` via PR #513. > Final governance approval remains open under **PIA-5**; this checklist also tracks the remaining > **APP 8** contractual basis for both overseas providers. -### Status record — fill in as steps complete +### Status record — fill in as steps complete | Field | Value | Date | Evidence | | ----------------------------------- | ------------------------- | ---- | -------- | @@ -157,7 +157,7 @@ in writing. | Railway Singapore processor basis | _pending_ | | | | ZDR approved (project) | _no_ | | | | ZDR covers /responses + /embeddings | _tbd_ | | | -| ZDR zeroes prompt cache? (§6) | _tbd_ | | | +| ZDR zeroes prompt cache? (§6) | _tbd_ | | | | Australia data residency | _not enabled_ | | | | No-training confirmed in contract | _API default_ | | | | APP 5/1 notice governance approval | _pending (PIA-5)_ | | | @@ -169,11 +169,11 @@ in the PIA. No automated action in this review accepted either provider's terms. ## 9. Code follow-ups triggered by the outcome -These touch the OpenAI request path — do them **only after** the legal decision, and treat them as +These touch the OpenAI request path — do them **only after** the legal decision, and treat them as provider-path changes (confirm before running against live). - **ZDR granted:** no code change strictly required (ZDR is account/project-side). Revisit - `OPENAI_PROMPT_CACHE_TTL` depending on the §6 answer and note the resolution against **PIA-6**. + `OPENAI_PROMPT_CACHE_TTL` depending on the §6 answer and note the resolution against **PIA-6**. - **Australia data residency adopted:** the client currently has no `baseURL` override ([openai.ts:75-79](../src/lib/openai.ts)). Data-residency Projects route via the standard API with a region-scoped project key; confirm whether a `baseURL`/project-key change is needed and wire an @@ -183,10 +183,10 @@ provider-path changes (confirm before running against live). ## 10. Sources -- OpenAI — [Data controls in the OpenAI platform](https://developers.openai.com/api/docs/guides/your-data) -- OpenAI — [Data Processing Addendum](https://openai.com/policies/data-processing-addendum/) · [PDF v.010126](https://cdn.openai.com/pdf/openai-data-processing-addendum.pdf) -- OpenAI — [Sub-processor list](https://openai.com/policies/sub-processor-list/) -- OpenAI — [Data residency for the OpenAI API](https://help.openai.com/en/articles/10503543-data-residency-for-the-openai-api) · [Expanding data residency worldwide](https://openai.com/index/expanding-data-residency-access-to-business-customers-worldwide/) -- Railway — [Data Processing Addendum](https://railway.com/legal/dpa) · [Trust Center](https://trust.railway.com/) -- OAIC — Australian Privacy Principles (APP 8 cross-border disclosure; s16C accountability), _Privacy Act 1988_ (Cth) -- Internal — [Privacy Impact Assessment](privacy-impact-assessment.md) (PIA-1, PIA-6) +- OpenAI — [Data controls in the OpenAI platform](https://developers.openai.com/api/docs/guides/your-data) +- OpenAI — [Data Processing Addendum](https://openai.com/policies/data-processing-addendum/) · [PDF v.010126](https://cdn.openai.com/pdf/openai-data-processing-addendum.pdf) +- OpenAI — [Sub-processor list](https://openai.com/policies/sub-processor-list/) +- OpenAI — [Data residency for the OpenAI API](https://help.openai.com/en/articles/10503543-data-residency-for-the-openai-api) · [Expanding data residency worldwide](https://openai.com/index/expanding-data-residency-access-to-business-customers-worldwide/) +- Railway — [Data Processing Addendum](https://railway.com/legal/dpa) · [Trust Center](https://trust.railway.com/) +- OAIC — Australian Privacy Principles (APP 8 cross-border disclosure; s16C accountability), _Privacy Act 1988_ (Cth) +- Internal — [Privacy Impact Assessment](privacy-impact-assessment.md) (PIA-1, PIA-6) diff --git a/docs/outstanding-issues-inbox/3eebb95f-3671-41d0-afa8-85460a9b5ff3.json b/docs/outstanding-issues-inbox/3eebb95f-3671-41d0-afa8-85460a9b5ff3.json new file mode 100644 index 0000000000..6cd4f7ac1b --- /dev/null +++ b/docs/outstanding-issues-inbox/3eebb95f-3671-41d0-afa8-85460a9b5ff3.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "3eebb95f-3671-41d0-afa8-85460a9b5ff3", + "createdOn": "2026-08-26", + "action": "done", + "payload": { + "id": "#321", + "outcome": "Standardized control focus rings and ARIA descriptors across modal and drawer surfaces.", + "baseRowFingerprint": "1dc0a6b312ed2e9b531d27f314da50d55c21c44e044a2f4d03e0562e3ba6cb59" + } +} diff --git a/docs/outstanding-issues-inbox/454d0cd5-8f4c-4ab9-b9b3-7b5a588283a1.json b/docs/outstanding-issues-inbox/454d0cd5-8f4c-4ab9-b9b3-7b5a588283a1.json new file mode 100644 index 0000000000..0381fd7e7b --- /dev/null +++ b/docs/outstanding-issues-inbox/454d0cd5-8f4c-4ab9-b9b3-7b5a588283a1.json @@ -0,0 +1,10 @@ +{ + "version": 2, + "id": "454d0cd5-8f4c-4ab9-b9b3-7b5a588283a1", + "createdOn": "2026-08-27", + "action": "cancel", + "payload": { + "requestId": "76744c9f-6e7f-4390-a202-756e50be0cd1", + "reason": "Superseded by a539a411-4648-4f71-8b40-90d49646e8a4 on #243HCC" + } +} diff --git a/docs/outstanding-issues-inbox/531e6569-42da-4bb6-9c0f-6a410e207aea.json b/docs/outstanding-issues-inbox/531e6569-42da-4bb6-9c0f-6a410e207aea.json new file mode 100644 index 0000000000..8478e03a6b --- /dev/null +++ b/docs/outstanding-issues-inbox/531e6569-42da-4bb6-9c0f-6a410e207aea.json @@ -0,0 +1,10 @@ +{ + "version": 2, + "id": "531e6569-42da-4bb6-9c0f-6a410e207aea", + "createdOn": "2026-08-26", + "action": "cancel", + "payload": { + "requestId": "854ca9ba-ba3b-442b-be6d-6f5873ed60ed", + "reason": "Wired connection and permission unavailable recovery screens in Caring Contacts." + } +} diff --git a/docs/outstanding-issues-inbox/5fa0b2ee-38ea-4a6a-bada-815b021b5aac.json b/docs/outstanding-issues-inbox/5fa0b2ee-38ea-4a6a-bada-815b021b5aac.json new file mode 100644 index 0000000000..5e016ea6db --- /dev/null +++ b/docs/outstanding-issues-inbox/5fa0b2ee-38ea-4a6a-bada-815b021b5aac.json @@ -0,0 +1,10 @@ +{ + "version": 2, + "id": "5fa0b2ee-38ea-4a6a-bada-815b021b5aac", + "createdOn": "2026-08-26", + "action": "cancel", + "payload": { + "requestId": "a0548b2c-2c80-44f9-9440-5b983e169a13", + "reason": "Repaired closing-message refusal guard in Caring Contacts service state." + } +} diff --git a/docs/outstanding-issues-inbox/6e929764-e281-4941-9ef4-062ec5a08497.json b/docs/outstanding-issues-inbox/6e929764-e281-4941-9ef4-062ec5a08497.json new file mode 100644 index 0000000000..f5f0d23c0f --- /dev/null +++ b/docs/outstanding-issues-inbox/6e929764-e281-4941-9ef4-062ec5a08497.json @@ -0,0 +1,10 @@ +{ + "version": 2, + "id": "6e929764-e281-4941-9ef4-062ec5a08497", + "createdOn": "2026-08-26", + "action": "cancel", + "payload": { + "requestId": "1838b99a-9323-4e3b-84a5-8e52535ecec8", + "reason": "Connected governed-message validator to outbound dispatch routes with fail-closed enforcement." + } +} diff --git a/docs/outstanding-issues-inbox/76744c9f-6e7f-4390-a202-756e50be0cd1.json b/docs/outstanding-issues-inbox/76744c9f-6e7f-4390-a202-756e50be0cd1.json new file mode 100644 index 0000000000..8c9f337074 --- /dev/null +++ b/docs/outstanding-issues-inbox/76744c9f-6e7f-4390-a202-756e50be0cd1.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "76744c9f-6e7f-4390-a202-756e50be0cd1", + "createdOn": "2026-08-26", + "action": "done", + "payload": { + "id": "#243HCC", + "outcome": "Emitted data-settings-nav-target on sub-navigation tabs in settings-dialog.", + "baseRowFingerprint": "88109c3d3be29e89faa7503ae01720afc377bdd78dce7036557099268caf412d" + } +} diff --git a/docs/outstanding-issues-inbox/a5dd669d-8d52-4b67-b249-51b0757a3b9f.json b/docs/outstanding-issues-inbox/a5dd669d-8d52-4b67-b249-51b0757a3b9f.json new file mode 100644 index 0000000000..1fe31f17b8 --- /dev/null +++ b/docs/outstanding-issues-inbox/a5dd669d-8d52-4b67-b249-51b0757a3b9f.json @@ -0,0 +1,11 @@ +{ + "version": 2, + "id": "a5dd669d-8d52-4b67-b249-51b0757a3b9f", + "createdOn": "2026-08-27", + "action": "done", + "payload": { + "id": "#45V4Y7", + "outcome": "Removed verified dead exports answerQuestion in src/lib/rag/rag.ts, embedText in src/lib/openai.ts, and clinicalRankScore in src/lib/clinical-search.ts. Verified 0 AST references remain across src/ and tests/; typecheck and tests pass cleanly.", + "baseRowFingerprint": "75831bf7612fc2ccb17a76414769d9ac0f81d29954a8703f34da878419ba4ea9" + } +} diff --git a/docs/privacy-impact-assessment.md b/docs/privacy-impact-assessment.md index 0023d5eba5..6934e207ac 100644 --- a/docs/privacy-impact-assessment.md +++ b/docs/privacy-impact-assessment.md @@ -1,8 +1,8 @@ -# Privacy Impact Assessment — Clinical KB Database +# Privacy Impact Assessment — Clinical KB Database **Current status authority:** [`docs/governance/privacy-readiness.v1.json`](governance/privacy-readiness.v1.json). This narrative explains the assessment; the versioned register separates code proof from provider configuration, legal approval, and clinical acceptance. Pending external items in that register are not completed by technical controls described here. -**Status:** Draft for governance approval · **Date:** 2026-07-06 · **Revised:** 2026-08-23 +**Status:** Draft for governance approval · **Date:** 2026-07-06 · **Revised:** 2026-08-23 **Scope:** Clinical data flows through the Clinical KB app (Next.js on Railway Singapore + Supabase Sydney + OpenAI), the live Supabase project `Clinical KB Database` (`sjrfecxgysukkwxsowpy`), and the WA private-clinical deployment context. **Author:** Automated code-level assessment (multi-agent audit of `src/app/api/**`, `src/lib/*`, `supabase/schema.sql`, `supabase/migrations/**`), cross-checked against the live database. @@ -19,7 +19,7 @@ claim a new Railway/OpenAI contract review, legal approval, or provider configur ## 1. Executive summary -The app is a **clinical knowledge base** — it indexes clinical reference material (guidelines, +The app is a **clinical knowledge base** — it indexes clinical reference material (guidelines, drug monographs, protocols) and answers clinician questions over that corpus with retrieval-augmented generation. It is **not** a patient record system. Provider-backed features do not ask for patient identifiers. The Safety Plan Generator accepts sensitive identifier-free working content and support @@ -36,13 +36,13 @@ material. **What is already good:** -- **Data residency**: the Supabase project runs in **`ap-southeast-2` (AWS Sydney, Australia)** — +- **Data residency**: the Supabase project runs in **`ap-southeast-2` (AWS Sydney, Australia)** — clinical data at rest stays onshore. Confirmed live via the Supabase API (project region `ap-southeast-2`). - **Query redaction**: raw query text is **not** persisted by default. Every log write goes through `queryTextForStorage()` which stores a hash placeholder unless `RAG_PERSIST_RAW_QUERY_TEXT=true` ([src/lib/query-privacy.ts](../src/lib/query-privacy.ts)). -- **The M15 HMAC fix is present** ([src/lib/query-privacy.ts](../src/lib/query-privacy.ts)) — the +- **The M15 HMAC fix is present** ([src/lib/query-privacy.ts](../src/lib/query-privacy.ts)) — the stored hash is a keyed HMAC-SHA256 pseudonym **when `RAG_QUERY_HASH_SECRET` is set** (see gap PIA-2). - **Retention is automated**: nightly `pg_cron` jobs purge `rag_queries` (30d) and `rag_retrieval_logs` (90d). **Verified running on live** (both jobs `active = true`). @@ -51,7 +51,7 @@ material. - Storage buckets are **private**; files are only reachable via short-lived (10 min) server-minted signed URLs after an ownership check. -**Top gaps (full register in §10):** +**Top gaps (full register in §10):** | ID | Risk | One-line | | ----- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | @@ -69,16 +69,16 @@ material. | Data category | Where it lives | Sensitivity | Notes | | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------- | -| Clinical reference corpus (documents, chunks, embeddings, images, tables) | Supabase (Sydney) + storage buckets | Low–Medium | Published guidelines are not PHI; **uploaded** docs _could_ contain PHI. | +| Clinical reference corpus (documents, chunks, embeddings, images, tables) | Supabase (Sydney) + storage buckets | Low–Medium | Published guidelines are not PHI; **uploaded** docs _could_ contain PHI. | | Free-text clinical queries | Processed by Railway (Singapore); hashed into Supabase logs (Sydney); sent to OpenAI (US) for retrieval embedding and, when selected, answer synthesis | **High (potential PHI)** | The primary incidental-PHI vector; embedding egress can occur even when the final answer is source-only. | | Generated answers | `rag_queries.answer` (not persisted unless `RAG_PERSIST_ANSWER_TEXT`); short-lived `rag_response_cache.payload` | **High (derived from PHI query + corpus)** | Durable answer log dropped at rest by default (PIA-3); expired cache rows have a bounded hourly purge when `pg_cron` is available. | | Safety-plan working content | React memory in the current browser tab; user-directed clipboard, print, or PDF output | **High (sensitive health information)** | No patient-identifier field; not sent to the application service or stored by Clinical KB. Exported copies leave this boundary. | | User identity | Supabase Auth (`auth.users`), `owner_id` foreign keys | Medium (PII) | Email + SSO identity; managed by Supabase Auth. | | Audit trail | `audit_logs` | Medium | Append-only, service-role-only, retained indefinitely by design. | -| Operational telemetry | `rag_retrieval_logs`, ingestion job tables | Low–Medium | Redacted query text; per-owner. | +| Operational telemetry | `rag_retrieval_logs`, ingestion job tables | Low–Medium | Redacted query text; per-owner. | **Deployment context (from code):** the answer system prompt positions the assistant as _"an -experienced psychiatrist in Perth"_ ([src/lib/rag/rag.ts](../src/lib/rag/rag.ts)) — i.e. a **WA psychiatry** +experienced psychiatrist in Perth"_ ([src/lib/rag/rag.ts](../src/lib/rag/rag.ts)) — i.e. a **WA psychiatry** use case. Psychiatric context raises the sensitivity ceiling: mental-health information is squarely "sensitive information" and "health information" under the _Privacy Act 1988_ (Cth). @@ -90,46 +90,46 @@ The end-to-end path for a single clinician query. **Bold** nodes are where PHI c ``` Clinician browser - │ POST /api/answer { query: "", ... } - ▼ -[Next.js route — Railway Singapore] src/app/api/answer/route.ts - │ • auth resolved → access.ownerId (or undefined for anon/public) - │ • rate-limit bucket "answer" - │ • resolveSearchScope() → owner-scoped candidate document set - ▼ + │ POST /api/answer { query: "", ... } + â–¼ +[Next.js route — Railway Singapore] src/app/api/answer/route.ts + │ • auth resolved → access.ownerId (or undefined for anon/public) + │ • rate-limit bucket "answer" + │ • resolveSearchScope() → owner-scoped candidate document set + â–¼ [RAG pipeline] answerQuestionWithScope() src/lib/rag/rag.ts - │ - ├──►(A) QUERY EMBEDDING ─────────────────────────────────────────────┐ - │ raw query text → OpenAI embeddings (text-embedding-3-small) │ - │ src/lib/openai.ts embedText/embedTexts │ ►► OpenAI API - │ │ (US region, - ├──►(B) RETRIEVAL (Supabase RPCs, owner-filtered in SQL) │ api.openai.com) - │ match_document_chunks* etc. — Sydney, never leaves AU │ - │ │ - ├──►(C) ANSWER SYNTHESIS ─────────────────────────────────────────────┤ - │ **raw query verbatim** (answer-generation request builder) │ - │ + **retrieved chunk text** (buildRagSourceBlock) │ - │ + system instructions │ - │ → OpenAI Responses API (Terra fast / Sol strong) ─┘ - │ store:false; GPT-5.6 prompt_cache_options.ttl:30m - │ - ├──►(D) LOCAL LOGGING (Supabase, Sydney, owner-stamped) - │ insertRagQuery() - │ • query = **hash placeholder** (queryTextForStorage) ← redacted - │ • normalized_query= **hash placeholder** ← redacted - │ • answer = null unless RAG_PERSIST_ANSWER_TEXT ← dropped at rest (PIA-3) - │ • source_chunk_ids= real chunk UUIDs ← owner's own data - │ • metadata.query_hash = HMAC/SHA-256 (query-privacy.ts) - │ - └──►(E) RESPONSE CACHE (Supabase rag_response_cache, authenticated owner-scoped) + │ + ├──►(A) QUERY EMBEDDING ─────────────────────────────────────────────┐ + │ raw query text → OpenAI embeddings (text-embedding-3-small) │ + │ src/lib/openai.ts embedTextWithTelemetry │ ►► OpenAI API + │ │ (US region, + ├──►(B) RETRIEVAL (Supabase RPCs, owner-filtered in SQL) │ api.openai.com) + │ match_document_chunks* etc. — Sydney, never leaves AU │ + │ │ + ├──►(C) ANSWER SYNTHESIS ─────────────────────────────────────────────┤ + │ **raw query verbatim** (answer-generation request builder) │ + │ + **retrieved chunk text** (buildRagSourceBlock) │ + │ + system instructions │ + │ → OpenAI Responses API (Terra fast / Sol strong) ─┘ + │ store:false; GPT-5.6 prompt_cache_options.ttl:30m + │ + ├──►(D) LOCAL LOGGING (Supabase, Sydney, owner-stamped) + │ insertRagQuery() + │ • query = **hash placeholder** (queryTextForStorage) ← redacted + │ • normalized_query= **hash placeholder** ← redacted + │ • answer = null unless RAG_PERSIST_ANSWER_TEXT ← dropped at rest (PIA-3) + │ • source_chunk_ids= real chunk UUIDs ← owner's own data + │ • metadata.query_hash = HMAC/SHA-256 (query-privacy.ts) + │ + └──►(E) RESPONSE CACHE (Supabase rag_response_cache, authenticated owner-scoped) payload = full answer, TTL ~5 min (RAG_ANSWER_CACHE_TTL_MS) disabled for anonymous answers; authenticated rows are keyed by owner_id - ▼ -Clinician browser ← answer + citations + â–¼ +Clinician browser ← answer + citations ``` `/api/search` follows the same shape but writes `rag_queries` / `rag_query_misses` / -`rag_retrieval_logs` (all redacted via the same helpers — +`rag_retrieval_logs` (all redacted via the same helpers — [src/app/api/search/route.ts](../src/app/api/search/route.ts)). The browser request, answer pipeline, and ingestion worker are processed by Railway in Singapore. The @@ -152,13 +152,13 @@ working content within the same local-only boundary. ### 4.1 What is sent -| Payload | Content | Reference | -| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | -| Embedding input | **Raw query text**, verbatim (normalized whitespace/case only) | [src/lib/openai.ts](../src/lib/openai.ts) → `embedTexts` | -| Answer input | **Raw query verbatim** (`Question:\n${args.query}`) | [src/lib/rag/rag.ts](../src/lib/rag/rag.ts) | -| Answer input | **Retrieved chunk text** (content, capped ~1800 chars, plus title/page/section/table-facts/captions) | [src/lib/rag/rag.ts](../src/lib/rag/rag.ts) | -| Instructions | Static system prompt ("experienced psychiatrist in Perth…") | [src/lib/rag/rag.ts](../src/lib/rag/rag.ts) | -| Metadata | `{ operation }`; when configured, `safety_identifier` is an HMAC-SHA256 pseudonym of the authenticated owner. The raw owner id is never sent. | [src/lib/openai.ts](../src/lib/openai.ts) | +| Payload | Content | Reference | +| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | +| Embedding input | **Raw query text**, verbatim (normalized whitespace/case only) | [src/lib/openai.ts](../src/lib/openai.ts) → `embedTextWithTelemetry` | +| Answer input | **Raw query verbatim** (`Question:\n${args.query}`) | [src/lib/rag/rag.ts](../src/lib/rag/rag.ts) | +| Answer input | **Retrieved chunk text** (content, capped ~1800 chars, plus title/page/section/table-facts/captions) | [src/lib/rag/rag.ts](../src/lib/rag/rag.ts) | +| Instructions | Static system prompt ("experienced psychiatrist in Perth…") | [src/lib/rag/rag.ts](../src/lib/rag/rag.ts) | +| Metadata | `{ operation }`; when configured, `safety_identifier` is an HMAC-SHA256 pseudonym of the authenticated owner. The raw owner id is never sent. | [src/lib/openai.ts](../src/lib/openai.ts) | The app never _adds_ patient identifiers, but it does not scrub them either: **any PHI the clinician types into the query, or that exists in an indexed excerpt, is transmitted to OpenAI.** @@ -170,7 +170,7 @@ types into the query, or that exists in an indexed excerpt, is transmitted to Op rollout target; `text-embedding-3-small` remains the embedding model ([src/lib/env.ts](../src/lib/env.ts), [.env.example](../.env.example)). Existing deployments with explicit model variables remain pinned until their configuration is changed. -- **`store: false`** by default — responses are not retained in OpenAI's dashboard/store +- **`store: false`** by default — responses are not retained in OpenAI's dashboard/store ([src/lib/openai.ts](../src/lib/openai.ts), [src/lib/env.ts](../src/lib/env.ts)). - **GPT-5.6 prompt caching:** the app sends `prompt_cache_options: { ttl: "30m" }` unless `OPENAI_PROMPT_CACHE_TTL=off`; it never sends the deprecated @@ -180,12 +180,12 @@ types into the query, or that exists in an indexed excerpt, is transmitted to Op - **Safety identifier:** when `OPENAI_SAFETY_IDENTIFIER_SECRET` is configured, authenticated Responses requests carry a stable HMAC-SHA256 pseudonym. Anonymous and background requests omit it, and raw owner identifiers are never sent. Production readiness warns when the secret is absent. -- **No `baseURL` override and no zero-data-retention (ZDR) header** are set in code — the client is a +- **No `baseURL` override and no zero-data-retention (ZDR) header** are set in code — the client is a plain `new OpenAI({ apiKey, timeout, maxRetries })` ([src/lib/openai.ts](../src/lib/openai.ts)), so traffic goes to `api.openai.com` (US) under whatever data-processing terms attach to the API **account/organisation**. -### 4.3 Data-processing terms — what code can and cannot tell us +### 4.3 Data-processing terms — what code can and cannot tell us The code shows the _technical_ posture (US endpoint, `store:false`, model-aware prompt-cache configuration, optional HMAC safety identifier, no ZDR header). @@ -197,7 +197,7 @@ facts, and must be confirmed: - Whether **Zero Data Retention (ZDR)** has been granted for the org and how it applies to the configured prompt-cache lifetime. - OpenAI's standard API commitment (no training on API data by default; limited abuse-monitoring - retention) — this needs to be pinned to the specific contract, not assumed. + retention) — this needs to be pinned to the specific contract, not assumed. Under **APP 8 (cross-border disclosure)**, the app operator remains accountable for OpenAI's handling of the disclosed information unless an APP 8.2 exception applies. The corresponding processor/legal @@ -206,7 +206,7 @@ of the most important privacy items before real patient use (PIA-1). --- -## 5. Logging and redaction — per-table verification +## 5. Logging and redaction — per-table verification All three log tables are **owner-stamped** and **RLS-enabled** (owner-read for authenticated users; service-role for writes). Redaction is applied centrally at every write site. @@ -218,7 +218,7 @@ service-role for writes). Redaction is applied centrally at every write site. | `rag_retrieval_logs` | No (hash placeholder) | same helpers; write at [search/route.ts](../src/app/api/search/route.ts) | retrieval telemetry only | owner-read, [schema.sql](../supabase/schema.sql) | | `audit_logs` | N/A (no query text) | action/resource metadata only; the write boundary allowlists operational metadata and excludes user-controlled filenames/titles/content hashes ([audit.ts](../src/lib/audit.ts)). Migration `20260717163000` minimizes existing rows on deployment. | `owner_id`, `action`, `resource_id` | service-role-only, [schema.sql](../supabase/schema.sql) | -### 5.1 M15 HMAC query-hash fix — verified present, enforced in production +### 5.1 M15 HMAC query-hash fix — verified present, enforced in production The audit's **M15** remediation is in the code ([src/lib/query-privacy.ts](../src/lib/query-privacy.ts)): @@ -233,15 +233,15 @@ export function hashQueryText(query: string) { } ``` -- **When `RAG_QUERY_HASH_SECRET` is set:** the stored hash is a keyed HMAC-SHA256 — not - offline-reversible, not correlatable outside this deployment. ✔ This is the intended fix. +- **When `RAG_QUERY_HASH_SECRET` is set:** the stored hash is a keyed HMAC-SHA256 — not + offline-reversible, not correlatable outside this deployment. ✔ This is the intended fix. - **When it is unset:** the code **silently falls back to unsalted SHA-256**. A short, low-entropy - clinical query ("john smith clozapine") is then **dictionary-reversible** — an attacker (or a + clinical query ("john smith clozapine") is then **dictionary-reversible** — an attacker (or a curious insider) with read access to the log tables can hash candidate patient/drug strings offline and match rows, and can correlate the same query across rows. This defeats the redaction it is meant to provide. -**Status (PIA-2 — mitigated):** production now **fails closed** when the secret is absent. +**Status (PIA-2 — mitigated):** production now **fails closed** when the secret is absent. `requireQueryHashSecret()` ([src/lib/env.ts](../src/lib/env.ts)) throws at server startup ([src/instrumentation.ts](../src/instrumentation.ts)) when `NODE_ENV=production` and `RAG_QUERY_HASH_SECRET` is unset, so a misconfigured clinical server refuses to boot rather than @@ -261,7 +261,7 @@ revalidate presence and rotation ownership during the next approved credential r URLs, secrets (incl. `sb_secret_` / `sb_publishable_`), and emails from error details before they are logged, and `redactCaptionIdentifiers` strips emails/MRN/NHS-style ids/phone numbers from image captions ([privacy.ts](../src/lib/privacy.ts)). These are sound as far as they go, but they are -**pattern-based** and do not attempt to redact free-text clinical narrative (names in prose, etc.) — +**pattern-based** and do not attempt to redact free-text clinical narrative (names in prose, etc.) — which is why the query-hash approach (not raw storage) is the right primary control. --- @@ -293,7 +293,7 @@ cache purge jobs onto the existing bounded hourly purge. The remaining retention `purge-rag-response-cache` as job 16. The obsolete `purge-expired-rag-response-cache` job is absent. Repeat this check for any secondary environment that retains real data. - The purge functions are installed conditionally (`if to_regnamespace('cron') is null then return`, - [migration 20260629060603](../supabase/migrations/20260629060603_rag_queries_retention.sql)) — + [migration 20260629060603](../supabase/migrations/20260629060603_rag_queries_retention.sql)) — fine on live (pg_cron present) but **preview/branch databases silently skip scheduling**. Not a production risk, but worth noting for any secondary environment that retains real data. @@ -323,14 +323,14 @@ cover both overseas paths and their purposes. ## 8. Storage-bucket access paths - Buckets `clinical-documents` and `clinical-images` are **private** - ([docs/multi-user-auth-setup.md](multi-user-auth-setup.md) §7). + ([docs/multi-user-auth-setup.md](multi-user-auth-setup.md) §7). - No direct client storage access. Files are served only via **server-minted signed URLs** with a **10-minute TTL** (`signedUrlTtlSeconds = 60 * 10`, [documents/[id]/signed-url/route.ts](../src/app/api/documents/[id]/signed-url/route.ts), [images/[id]/signed-url/route.ts](../src/app/api/images/[id]/signed-url/route.ts)). - Every signed-URL mint is **preceded by an ownership check** on the parent document row (`withOwnerReadScope(...)` before `createSignedUrl`, - [documents/[id]/signed-url/route.ts](../src/app/api/documents/[id]/signed-url/route.ts)) — see the + [documents/[id]/signed-url/route.ts](../src/app/api/documents/[id]/signed-url/route.ts)) — see the companion tenancy review for the adversarial verification. - Storage objects are namespaced by owner (`${uploadOwnerId}/documents/${documentId}/...`, [upload/route.ts](../src/app/api/upload/route.ts)), and the DB additionally carries owner-scoped @@ -338,30 +338,30 @@ cover both overseas paths and their purposes. client-direct access. Signed-URL handling is well-scoped. The residual consideration is only that a 10-minute URL, once -minted, is bearer-usable by anyone it is shared with in that window — acceptable for this use case. +minted, is bearer-usable by anyone it is shared with in that window — acceptable for this use case. --- ## 9. Assessment against Australian Privacy Act / WA health obligations -**Framework.** Private-sector health service providers are **APP entities regardless of turnover** — +**Framework.** Private-sector health service providers are **APP entities regardless of turnover** — the small-business exemption does **not** apply where health services are provided and health information is handled (_Privacy Act 1988_ (Cth), s6D(4)(b)). Health/mental-health information is **"sensitive information"** attracting the highest APP protections. WA has no equivalent of Victoria's _Health Records Act 2001_ or NSW's _HRIP Act 2002_ for the private sector; the _Privacy Act_ + APPs are the operative framework for a WA private clinician. (The WA _Privacy and Responsible Information Sharing -Act 2024_ targets WA **public-sector** entities and may apply to public-health deployments — confirm +Act 2024_ targets WA **public-sector** entities and may apply to public-health deployments — confirm with counsel if this is deployed inside a WA Health service.) -| APP | Obligation | Status in this app | Gap | -| ----------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | -| **APP 1** — open & transparent management | Have a clear, up-to-date APP privacy policy | A draft `/privacy` data-processing page ships, but it is explicitly governance-review-required and is not represented as the final approved APP privacy policy | PIA-5 | -| **APP 3** — collection of sensitive info | Collect health info only with consent + where reasonably necessary | App does not solicit PHI; incidental entry remains possible. “Do not enter patient-identifiable information” notices now appear beside query/upload controls, but no governance-approved consent framework is claimed | PIA-5 | -| **APP 5** — notification of collection | Tell individuals what's collected & disclosed (incl. overseas) | Draft point-of-entry notices and the `/privacy` page disclose Singapore application processing and model-provider use; final wording and legal/governance approval remain outstanding | PIA-1, PIA-5 | -| **APP 6** — use/disclosure | Use only for the primary purpose or a permitted secondary purpose | Query used for answer generation (primary). Log retention = quality/eval (secondary) — defensible but should be documented | PIA-5 | -| **APP 8** — cross-border disclosure | Discloser stays accountable for the overseas recipient unless an exception applies | Railway processing in Singapore and OpenAI processing in the US require documented contractual/legal assessment; OpenAI has no code-visible DPA/ZDR | **PIA-1** | -| **APP 11** — security & destruction | Reasonable security; destroy/de-identify when no longer needed | Strong: Sydney data residency, RLS, private storage, query hashing, default-null answer logs, and live-verified query/log/cache purges. Remaining gaps are operator secret placement, secondary-environment schedule parity, and exceptional answer-persistence governance | PIA-2/4 | -| **NDB scheme** (Pt IIIC) | Notify OAIC + individuals of eligible breaches of health info | No documented breach-response runbook tied to these tables | Recommend adding | +| APP | Obligation | Status in this app | Gap | +| ------------------------------------------- | ---------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | +| **APP 1** — open & transparent management | Have a clear, up-to-date APP privacy policy | A draft `/privacy` data-processing page ships, but it is explicitly governance-review-required and is not represented as the final approved APP privacy policy | PIA-5 | +| **APP 3** — collection of sensitive info | Collect health info only with consent + where reasonably necessary | App does not solicit PHI; incidental entry remains possible. “Do not enter patient-identifiable information” notices now appear beside query/upload controls, but no governance-approved consent framework is claimed | PIA-5 | +| **APP 5** — notification of collection | Tell individuals what's collected & disclosed (incl. overseas) | Draft point-of-entry notices and the `/privacy` page disclose Singapore application processing and model-provider use; final wording and legal/governance approval remain outstanding | PIA-1, PIA-5 | +| **APP 6** — use/disclosure | Use only for the primary purpose or a permitted secondary purpose | Query used for answer generation (primary). Log retention = quality/eval (secondary) — defensible but should be documented | PIA-5 | +| **APP 8** — cross-border disclosure | Discloser stays accountable for the overseas recipient unless an exception applies | Railway processing in Singapore and OpenAI processing in the US require documented contractual/legal assessment; OpenAI has no code-visible DPA/ZDR | **PIA-1** | +| **APP 11** — security & destruction | Reasonable security; destroy/de-identify when no longer needed | Strong: Sydney data residency, RLS, private storage, query hashing, default-null answer logs, and live-verified query/log/cache purges. Remaining gaps are operator secret placement, secondary-environment schedule parity, and exceptional answer-persistence governance | PIA-2/4 | +| **NDB scheme** (Pt IIIC) | Notify OAIC + individuals of eligible breaches of health info | No documented breach-response runbook tied to these tables | Recommend adding | **Overall:** the _engineering_ controls for data-at-rest are strong and largely APP-11-aligned. The material shortfalls are **governance/contractual** (APP 8 cross-border terms and final approval of the @@ -375,13 +375,13 @@ remaining items are compliance-posture and PHI-minimisation gaps. ## 10. Gap register (ranked by risk) -### PIA-1 — Overseas Railway/OpenAI processing needs an approved contractual basis and notice **(High)** +### PIA-1 — Overseas Railway/OpenAI processing needs an approved contractual basis and notice **(High)** - **Risk:** Health/PHI in requests, queries, excerpts, and ingestion material is processed by Railway in Singapore. Query text can reach OpenAI in the US for retrieval embedding even when the final answer is source-only; model-backed synthesis additionally sends the query and selected excerpts. OpenAI has no code-visible contractual data-processing terms. A draft in-product provider disclosure now exists, reducing the - point-of-entry visibility gap, but it is not governance-approved legal wording → APP 8 accountability + point-of-entry visibility gap, but it is not governance-approved legal wording → APP 8 accountability exposure and a residual APP 5 governance gap. - **Evidence:** the live app and worker are recorded in Railway Singapore ([deployment-architecture.md](deployment-architecture.md)); the OpenAI client uses @@ -397,16 +397,16 @@ remaining items are compliance-posture and PHI-minimisation gaps. the contractual basis, is captured decision-ready in **[docs/openai-cross-border-basis.md](openai-cross-border-basis.md)**, which also records that the app's egress endpoints (`/v1/responses`, `/v1/embeddings`) are **ZDR-eligible** and that OpenAI now - offers **Australia data residency** (storage) — an option that postdates this PIA. The remaining step + offers **Australia data residency** (storage) — an option that postdates this PIA. The remaining step (execute DPA / apply ZDR / counsel sign-off) is operator/legal, not code. -### PIA-2 — Query-hash HMAC silently downgrades without the secret **(Mitigated)** +### PIA-2 — Query-hash HMAC silently downgrades without the secret **(Mitigated)** -- **Risk:** If `RAG_QUERY_HASH_SECRET` is unset in prod, stored query hashes are unsalted SHA-256 → +- **Risk:** If `RAG_QUERY_HASH_SECRET` is unset in prod, stored query hashes are unsalted SHA-256 → dictionary-reversible and cross-row correlatable, defeating the redaction (undoes M15). - **Evidence:** [query-privacy.ts](../src/lib/query-privacy.ts); the secret is `z.string().min(16).optional()` in [env.ts](../src/lib/env.ts). -- **Fix (landed):** `requireQueryHashSecret()` now makes the secret **mandatory in production** — it +- **Fix (landed):** `requireQueryHashSecret()` now makes the secret **mandatory in production** — it fails closed at startup ([instrumentation.ts](../src/instrumentation.ts)) when `NODE_ENV=production` and the secret is missing, mirroring the `requireServerEnv` pattern. `check:production-readiness` asserts the boot guard is wired in and the secret is present; covered by @@ -416,7 +416,7 @@ remaining items are compliance-posture and PHI-minimisation gaps. secret values were neither emitted nor compared. Revalidate presence, ownership, and the rotation procedure during the next credential review; do not rotate solely to establish equality. -### PIA-3 — Generated answers stored un-redacted in `rag_queries` **(Mitigated)** +### PIA-3 — Generated answers stored un-redacted in `rag_queries` **(Mitigated)** - **Risk:** The `answer` column held the full generated text, which can restate patient specifics echoed from the query; the query itself is hashed but the answer was not. Owner-scoped (not @@ -430,11 +430,11 @@ remaining items are compliance-posture and PHI-minimisation gaps. reads the in-memory answer (`logQuery: false`) and never reads this column back ([scripts/eval-rag.ts](../scripts/eval-rag.ts), [scripts/eval-answer-quality.ts](../scripts/eval-answer-quality.ts), [scripts/promote-query-misses.ts](../scripts/promote-query-misses.ts)), so persistence-off does not - affect eval — confirming the pipeline has no real dependency on stored answer text. The flag is + affect eval — confirming the pipeline has no real dependency on stored answer text. The flag is additionally blocked in a production-like environment by `npm run check:production-readiness`. - **Residual cache copy:** The answer also lands in `rag_response_cache.payload` ([rag-cache.ts](../src/lib/rag/rag-cache.ts)). Its `expires_at` TTL (`RAG_ANSWER_CACHE_TTL_MS`, default - 5 min) only gates **reads** — `sharedCacheSelector` filters on `expires_at`, while + 5 min) only gates **reads** — `sharedCacheSelector` filters on `expires_at`, while `replaceSharedCacheRow` deletes only the _same_ cache key before inserting. Migration `20260713201542_consolidate_rag_response_cache_retention.sql` unschedules the duplicate unbounded job and keeps one hourly purge capped at 1,000 expired rows per invocation. This keeps @@ -443,7 +443,7 @@ remaining items are compliance-posture and PHI-minimisation gaps. - **Historical cleanup:** a migration to null existing `rag_queries.answer` values is prepared but intentionally unexecuted pending deployment approval; this assessment does not claim live cleanup. -### PIA-4 — Query-miss and response-cache purges active **(Mitigated)** +### PIA-4 — Query-miss and response-cache purges active **(Mitigated)** - **Risk:** A secondary environment without the retention migrations or `pg_cron` can still accumulate hash-redacted misses and expired response-cache payloads. @@ -455,7 +455,7 @@ remaining items are compliance-posture and PHI-minimisation gaps. queried live on 2026-07-14: jobids 13 and 16 are active and the obsolete duplicate is absent. - **Fix:** Repeat the canonical job check for each secondary environment that retains real data. -### PIA-5 — Draft notices/page ship; final approved privacy policy remains outstanding **(Medium)** +### PIA-5 — Draft notices/page ship; final approved privacy policy remains outstanding **(Medium)** - **Risk:** The shipped draft point-of-entry notices and `/privacy` page explain collection, retention, and overseas/provider processing, but they are explicitly pending governance review and do not by @@ -464,7 +464,7 @@ remaining items are compliance-posture and PHI-minimisation gaps. APP privacy policy and retain the point-of-entry links/notices. Broader retention and breach-response documentation also remains outstanding. No legal approval is claimed here. -### PIA-6 — OpenAI prompt-cache lifetime requires contractual confirmation **(Low-Medium)** +### PIA-6 — OpenAI prompt-cache lifetime requires contractual confirmation **(Low-Medium)** - **Risk:** Query + retrieved excerpts can enter OpenAI prompt caches even with `store:false`. GPT-5.6 requests a 30-minute TTL by default, but that value is a minimum and is not a contractual @@ -474,7 +474,7 @@ remaining items are compliance-posture and PHI-minimisation gaps. data-residency terms. Keep `OPENAI_PROMPT_CACHE_TTL=off` available when governance requires the app to omit the extended GPT-5.6 TTL option; document that provider-default caching policy still applies. -### PIA-7 — `RAG_PERSIST_RAW_QUERY_TEXT=true` stores raw PHI query text **(Low, config-gated)** +### PIA-7 — `RAG_PERSIST_RAW_QUERY_TEXT=true` stores raw PHI query text **(Low, config-gated)** - **Risk:** Flipping the flag persists raw queries with only the 30-day purge as a safeguard. - **Evidence:** [query-privacy.ts](../src/lib/query-privacy.ts), [env.ts](../src/lib/env.ts). diff --git a/src/components/DocumentTagCloud.tsx b/src/components/DocumentTagCloud.tsx index 3ed322b220..5556c2089d 100644 --- a/src/components/DocumentTagCloud.tsx +++ b/src/components/DocumentTagCloud.tsx @@ -84,7 +84,7 @@ function DocumentTagChip({ ); const content = ( <> - +