feat(consent): add the extraction consent kind and purge on withdrawal

A new receipt kind, ai_extraction, covers reading documents, lab report
scans and typed medication text with an external model. Switching the
document auto-read on now mints ai_extraction instead of ai_full, so it
no longer grants the Coach or the analysis as a side effect. Existing
ai_full receipts keep covering everything.

DELETE /api/consent/ai/latest now runs through withdrawConsent: the
revoke and, when it leaves no receipt covering the analysis, the
deletion of the text a model wrote under it commit in one transaction.
That is the status notes, the cached briefing, model-written period
narratives, reaction lines and workout paragraphs. Coach conversations,
facts and plans and document summaries stay. The purge is audit-logged
as consent.ai.purge with counts only.

Refs #975
This commit is contained in:
Marc-André Bombeck committed 2026-09-24 09:05:09 +02:00
1 parent 2710dfc041
commit 8325f3964d
14 files changed
+607 -185

No files matched your search

+38 -15
View File
@@ -7948,12 +7948,13 @@ paths:
Hard-set, idempotent, audit-logged. Rate-limited 60 / min per user.
Turning it ON does two further things a caller should expect. It is itself the standing consent act, so the
write appends an `ai_full` `ConsentReceipt` — the same receipt POST /api/consent/ai/web mints, and one DELETE
/api/consent/ai/latest revokes without touching this flag. And a genuine OFF→ON flip schedules a bounded
catch-up over the documents already in the vault, because the summary job is enqueued at upload time and would
otherwise only ever apply to future uploads. The catch-up is fire-and-forget and re-runs every consent and
budget gate per document.
Turning it ON does two further things a caller should expect. It is the consent act for reading documents, so
the write appends an `ai_extraction` `ConsentReceipt`: the narrow kind that covers documents, lab report scans
and typed medication text and nothing else (not the Coach, not the analysis). DELETE /api/consent/ai/latest
revokes it without touching this flag, and the revocation wins: document reading stops even while the flag stays
on. And a genuine OFF→ON flip schedules a bounded catch-up over the documents already in the vault, because the
summary job is enqueued at upload time and would otherwise only ever apply to future uploads. The catch-up is
fire-and-forget and re-runs every consent and budget gate per document.
The body cap here is 1 KB, far tighter than the 64 KB its siblings allow — a payload above it is refused with
@@ -12691,6 +12692,7 @@ paths:
- ai_full
- ai_insights_only
- ai_coach
- ai_extraction
description: Narrow to one consent kind. Omit for the full keyspace.
responses:
"200":
@@ -12702,8 +12704,9 @@ paths:
- $ref: "#/components/schemas/ConsentLatestForKindEnvelope"
- $ref: "#/components/schemas/ConsentLatestByKindEnvelope"
"400": &a22
description: "`kind` was present but is not one of `ai_full`, `ai_insights_only`, `ai_coach`. Multi-issue envelope;
nothing was read or revoked. The consent family answers 400 here where the rest of the API answers 422."
description: "`kind` was present but is not one of `ai_full`, `ai_insights_only`, `ai_coach`, `ai_extraction`.
Multi-issue envelope; nothing was read or revoked. The consent family answers 400 here where the rest of the
API answers 422."
content:
application/json:
schema:
@@ -12726,9 +12729,18 @@ paths:
each revocation is audit-logged.
Revoking `ai_full` does NOT switch off `documentsAutoAiRead`; that flag is written through PATCH
/api/auth/me/documents-auto-ai-read and turning it on mints a fresh receipt. A client offering both controls
should show them as the two separate things they are.
Revoking `ai_full` or `ai_extraction` does NOT switch off `documentsAutoAiRead`, but it does stop document
reading: the document gate reads the receipt, not the switch, so a revocation wins over a switch left on.
Turning the switch on again mints a fresh `ai_extraction` receipt through PATCH
/api/auth/me/documents-auto-ai-read. A client offering both controls should show them as the two separate things
they are.
When a revocation leaves no receipt covering the analysis (`ai_insights_only` or `ai_full`), the same
transaction deletes the text a model wrote under it that can be written again: per-metric status notes, the
cached briefing, model-written period narratives, reaction lines and workout paragraphs. Coach conversations,
facts and plans and document summaries are the person's records and stay. A deletion is audit-logged with counts
only (`consent.ai.purge`).
parameters:
- in: query
name: kind
@@ -12739,6 +12751,7 @@ paths:
- ai_full
- ai_insights_only
- ai_coach
- ai_extraction
description: Revoke one kind. Omit to revoke the latest active receipt across every kind.
responses:
"200":
@@ -23846,8 +23859,8 @@ components:
type: boolean
required:
- documentsAutoAiRead
description: Turn automatic AI reading of newly uploaded documents on or off. Turning it on also mints an `ai_full`
consent receipt and schedules a catch-up over the existing vault.
description: Turn automatic AI reading of newly uploaded documents on or off. Turning it on also mints an
`ai_extraction` consent receipt and schedules a catch-up over the existing vault.
InjectionSitePrefsPatchRequest:
type: object
properties:
@@ -26005,6 +26018,7 @@ components:
- ai_full
- ai_insights_only
- ai_coach
- ai_extraction
artefact:
type: string
minLength: 1
@@ -58019,6 +58033,7 @@ components:
- ai_full
- ai_insights_only
- ai_coach
- ai_extraction
signedAt:
type: string
revokedAt:
@@ -58115,6 +58130,7 @@ components:
- ai_full
- ai_insights_only
- ai_coach
- ai_extraction
receipt:
anyOf:
- $ref: "#/components/schemas/SerialisedConsentReceipt"
@@ -58137,6 +58153,7 @@ components:
- ai_full
- ai_insights_only
- ai_coach
- ai_extraction
signedAt:
type: string
format: date-time
@@ -58193,13 +58210,18 @@ components:
anyOf:
- $ref: "#/components/schemas/SerialisedConsentReceipt"
- type: "null"
ai_extraction:
anyOf:
- $ref: "#/components/schemas/SerialisedConsentReceipt"
- type: "null"
required:
- ai_full
- ai_insights_only
- ai_coach
- ai_extraction
additionalProperties: false
description: "The latest active receipt for every kind. All three keys are always present: a null value means the kind
is not granted, so a client never has to read absence as ambiguity."
description: "The latest active receipt for every kind. Every key is always present: a null value means the kind is not
granted, so a client never has to read absence as ambiguity."
ConsentRevokeForKindEnvelope:
type: object
properties:
@@ -58248,6 +58270,7 @@ components:
- ai_full
- ai_insights_only
- ai_coach
- ai_extraction
receipt:
$ref: "#/components/schemas/SerialisedConsentReceipt"
required: