Skip to content

Manage User Consent and Opt-Out

This guide shows how to manage a single user’s privacy preferences against the per-workspace privacy-api: granting and revoking granular consent categories, setting scope-based opt-out preferences, and reading aggregate consent and enforcement stats.

privacy-api owns per-user consent grants and opt-out preferences (each workspace has its own consent database). Memory and PII-redaction enforcement in memory-api and session-api reads this state to decide what may be stored — so these are the knobs that drive per-user enforcement.

  • A Workspace with spec.privacy set. This is the trigger that provisions the privacy-api; without it there is no consent service to call.
  • The user’s pseudonymized identifier (virtual_user_id — the same PseudonymizeID value the facade writes for sessions and memory, never a raw email or user id). Consent, opt-out, and session data are all keyed by this pseudonym, so a raw id matches nothing.
  • Network access to privacy-api. Its JSON API is authenticated with a Kubernetes ServiceAccount token and is not exposed externally by default, so call it from inside the cluster with a valid token.
Terminal window
# Forward the workspace's privacy-api locally (service name is privacy-<workspace>).
kubectl port-forward -n <workspace-namespace> svc/privacy-<workspace> 8080:8080

All examples below assume $SA_TOKEN holds a valid ServiceAccount bearer token and the service is reachable at http://localhost:8080.

Consent is granted per category. The platform defines a fixed set of categories; two tiers exist:

Category Requires explicit grant?
memory:preferences No — granted by default
memory:context No — granted by default
memory:history No — granted by default
memory:identity Yes
memory:location Yes
memory:health Yes
analytics:aggregate Yes

Categories that do not require an explicit grant are treated as consented unless the user opts out; the sensitive categories (memory:identity, memory:location, memory:health, analytics:aggregate) are denied until explicitly granted.

PUT /api/v1/privacy/preferences/{userID}/consent applies grants and/or revocations in one call. The body has two lists — grants and revocations:

Terminal window
curl -sS -X PUT \
http://localhost:8080/api/v1/privacy/preferences/<pseudonymized-user-id>/consent \
-H "Authorization: Bearer $SA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"grants": ["memory:identity", "memory:location"],
"revocations": ["analytics:aggregate"]
}'

The response is the user’s resulting consent state:

{
"grants": ["memory:identity", "memory:location"],
"defaults": ["memory:preferences", "memory:context", "memory:history"],
"denied": ["memory:health", "analytics:aggregate"]
}
  • grants — categories the user has explicitly granted.
  • defaults — categories consented by default (no explicit grant needed).
  • denied — sensitive categories not currently granted.

Revoking a category that was never granted is a silent no-op. Revocations are also fanned out to downstream memory-api instances so already-stored memories in a revoked category can be enforced. Each grant and revocation is written to the privacy audit log (consent_granted / consent_revoked).

GET /api/v1/privacy/preferences/{userID}/consent returns the same grants / defaults / denied shape without mutating anything:

Terminal window
curl -sS \
http://localhost:8080/api/v1/privacy/preferences/<pseudonymized-user-id>/consent \
-H "Authorization: Bearer $SA_TOKEN"

Opt-out is separate from consent: it suppresses recording/enforcement at a scope rather than per category. POST /api/v1/privacy/opt-out sets one:

Terminal window
curl -sS -X POST http://localhost:8080/api/v1/privacy/opt-out \
-H "Authorization: Bearer $SA_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"userId": "<pseudonymized-user-id>",
"scope": "workspace",
"target": "my-workspace"
}'

Fields:

Field Required Values / notes
userId yes The pseudonymized subject id.
scope yes all, workspace, or agent.
target for workspace / agent The workspace or agent name. Not needed for all.

A successful set returns 204 No Content.

Remove an opt-out with the same body and DELETE /api/v1/privacy/opt-out:

Terminal window
curl -sS -X DELETE http://localhost:8080/api/v1/privacy/opt-out \
-H "Authorization: Bearer $SA_TOKEN" \
-H "Content-Type: application/json" \
-d '{"userId": "<pseudonymized-user-id>", "scope": "workspace", "target": "my-workspace"}'

Fetch a user’s current opt-out preferences with GET /api/v1/privacy/preferences/{userID}:

Terminal window
curl -sS http://localhost:8080/api/v1/privacy/preferences/<pseudonymized-user-id> \
-H "Authorization: Bearer $SA_TOKEN"

A 404 Not Found means the user has no stored preferences yet.

Both stats endpoints are workspace-scoped and require a workspace query parameter.

Enforcement stats — how much privacy enforcement has actually happened (derived from the central audit hub):

Terminal window
curl -sS "http://localhost:8080/api/v1/privacy/enforcement-stats?workspace=my-workspace" \
-H "Authorization: Bearer $SA_TOKEN"
{
"piiBlocked": 42,
"redactions": 128
}

piiBlocked counts opt-out write blocks; redactions counts PII redaction events. This is the same read path the dashboard’s enforcement-stats view uses.

Aggregate consent stats across users in the workspace:

Terminal window
curl -sS "http://localhost:8080/api/v1/privacy/consent/stats?workspace=my-workspace" \
-H "Authorization: Bearer $SA_TOKEN"

What happens when privacy-api is unavailable

Section titled “What happens when privacy-api is unavailable”

Opt-out enforcement depends on privacy-api being reachable. The behaviour when it is not depends on whether it was configured in the first place, and the two cases are deliberately opposite:

privacy-api Behaviour Rationale
Configured but unavailable The subject is treated as opted out — writes are dropped The service that knows the subject’s choice cannot be asked, so the conservative answer is assumed
Not configured at all The subject is treated as opted in — writes proceed spec.privacy is optional; an unconfigured component must not halt the product

The first case is why a privacy-api outage looks like a sudden stop in recorded sessions and memories rather than an error: the writes are being refused on purpose. It is logged at info level on the affected service:

session write dropped: privacy-api unavailable, treating subject as opted out

and counted separately from real opt-outs, so it can be alerted on:

# A privacy-api problem — page on this.
omnia_session_api_writes_dropped_total{reason="privacy-unavailable"} > 0
# Subjects exercising their rights — expected traffic, never alert.
omnia_session_api_writes_dropped_total{reason="user-opted-out"}

Writes dropped because privacy-api was unavailable are not written to the privacy audit hub and do not appear in enforcement-stats. Only a subject’s actual decision is recorded there, so the audit trail stays a record of consent decisions rather than of outages.

The second case is announced once at startup, because it means opt-out is not being enforced at all:

PRIVACY NOT ENFORCED: no privacy-api resolved for this workspace;
user opt-out is disabled and all session writes will be recorded

If you see that line on a deployment that is meant to honour opt-outs, set Workspace.spec.privacy and confirm the service group reports status.privacyURL.

Both postures are also reported on the Workspace itself, so you do not have to find the right pod to answer “is opt-out enforced here?”:

Terminal window
kubectl get workspace my-ws \
-o jsonpath='{.status.conditions[?(@.type=="OptOutEnforced")]}'

True with reason PrivacyEnforced means privacy-api is serving. The False reasons say which of the two postures you are in — PrivacyNotConfigured (no spec.privacy; recording proceeds) or PrivacyUnavailable (privacy-api wanted but not serving; writes are being dropped). See the Workspace CRD reference for the full table.

A configured workspace whose privacy-api has not come up yet

Section titled “A configured workspace whose privacy-api has not come up yet”

While spec.privacy is set but privacy-api is not resolvable, session-api and memory-api treat every subject as opted out and drop user-scoped writes. This is the “configured but unavailable” row above, and it applies during the first few seconds of a brand-new workspace as well as during an outage.

The services re-resolve periodically, so they start enforcing on their own once the operator publishes status.privacyURL — no restart is needed.