Privacy API
The privacy-api is the per-workspace service that stores each subject’s consent
grants and opt-out preferences. session-api and memory-api read that state on
every user-scoped write. The operator deploys it when the Workspace sets
spec.privacy. The routes below require
Enterprise; without it the service serves only /healthz. A license is not
required: Enterprise without a license runs in dev mode. See
Licensing.
Service endpoints
Section titled “Service endpoints”| Port | Purpose |
|---|---|
8080 | JSON API |
8081 | GET /healthz, and GET /readyz (readiness pings Postgres) |
9090 | Prometheus metrics |
The Service is named privacy-<workspace> and lives in the workspace
namespace. The Workspace publishes its base URL in
status.privacyURL.
Authentication
Section titled “Authentication”When service authentication is enabled, callers send a Kubernetes ServiceAccount bearer token. The service validates it with TokenReview against its subject and namespace allowlists. With authentication disabled, the service logs a warning at startup and accepts unauthenticated requests.
Every route is keyed on the subject’s pseudonymous id, virtualUserId on the
wire (the {userID} segment of the preference routes). The
service stores whatever key it receives, so a raw user id records state for a
subject that matches no session or memory, and returns no error.
Consent categories
Section titled “Consent categories”| Category | Explicit grant required |
|---|---|
memory:preferences | No |
memory:context | No |
memory:history | No |
memory:identity | Yes |
memory:location | Yes |
memory:health | Yes |
A memory write in a category that needs a grant is refused until the subject grants it. A write in any other category proceeds unless the subject has opted out. A write naming a category outside this table is refused.
Consent routes
Section titled “Consent routes”| Method | Path | Result |
|---|---|---|
PUT | /api/v1/privacy/preferences/{userID}/consent | Applies grants, then revocations, and returns the resulting state |
GET | /api/v1/privacy/preferences/{userID}/consent | Returns the state |
Request body of the PUT:
| Field | Type | Description |
|---|---|---|
grants | list of categories | Categories to grant |
revocations | list of categories | Categories to revoke |
A category outside the table above returns 400 with
unknown consent category: <value>.
Response body of both routes:
| Field | Description |
|---|---|
grants | Categories the subject has granted |
defaults | Categories that need no grant |
denied | Categories that need a grant and have none |
Revoking a category that was never granted changes nothing and returns 200.
A revocation of a granted category is stored in an outbox and pushed to the
workspace’s memory-api instances. Delivery is best-effort: the PUT returns
200 whatever the delivery outcome. memory-api then applies the MemoryPolicy
consentRevocation.action to the memories already stored in that category.
Each grant and revocation writes a consent_granted or consent_revoked row
to the audit hub with source_service = privacy-api-consent. The subject is
in the virtual_user_id column and the category is in the row metadata. A revocation
of a category that was not granted also writes a row.
Opt-out routes
Section titled “Opt-out routes”| Method | Path | Success |
|---|---|---|
POST | /api/v1/privacy/opt-out | 204 |
DELETE | /api/v1/privacy/opt-out | 204; 404 when the subject has no stored preferences |
GET | /api/v1/privacy/preferences/{userID} | 200 with the preferences; 404 when the subject has none |
Body of POST and DELETE:
| Field | Required | Description |
|---|---|---|
virtualUserId | Yes | The pseudonymous subject id |
scope | Yes | all, workspace or agent |
target | For workspace and agent | The workspace or agent name |
A missing field or an unknown scope returns 400.
The preferences response has the fields virtualUserId, optOutAll,
optOutWorkspaces, optOutAgents, consentGrants, createdAt and
updatedAt.
Stats routes
Section titled “Stats routes”Both routes require the workspace query parameter and return 400 without it.
| Method | Path | Response fields |
|---|---|---|
GET | /api/v1/privacy/enforcement-stats | piiBlocked, redactions, policyDenied, aiDisclosureMissing |
GET | /api/v1/privacy/consent/stats | totalUsers, optedOutAll, grantsByCategory |
piiBlocked counts memory writes blocked by opt-out or a missing grant.
redactions counts PII redaction events. policyDenied counts ToolPolicy
denials in enforce mode. aiDisclosureMissing counts AI disclosure obligations
that a facade did not meet. All four come from the audit hub, filtered on
the workspace parameter, which must be the Workspace UID
(metadata.uid). A Workspace name matches no rows, so every count reads 0.
The consent stats route requires the workspace parameter but ignores its
value. The stats cover every subject in the workspace’s privacy database.
totalUsers counts subjects with a stored preferences row.
DSAR routes
Section titled “DSAR routes”privacy-api owns the erasure lifecycle. It records each request in its
deletion_requests table and fans the erasure out to every service group in the
workspace. session-api deletes the subject’s sessions and their media
(delete-by-user), and memory-api deletes their memories in batches.
privacy-api also erases the subject’s browser sessions and git credentials,
which it holds itself.
| Method | Path | Result |
|---|---|---|
POST | /api/v1/privacy/deletion-request | Creates a request and returns 202 with the request; processing continues in the background |
GET | /api/v1/privacy/deletion-request/{id} | Returns one request |
GET | /api/v1/privacy/deletion-requests | Lists a subject’s requests, newest first. Takes virtualUserId; virtual_user_id is an alias. |
GET | /api/v1/privacy/receipts | Lists tool-call receipts held by the audit hub |
Body of the POST:
{ "virtualUserId": "<pseudonymized-user-id>", "reason": "gdpr_erasure", "scope": "all"}| Field | Required | Values |
|---|---|---|
virtualUserId | Yes | The pseudonymized subject id. An empty value returns 400; a request never deletes all users. |
reason | Yes | gdpr_erasure, ccpa_delete or user_request |
scope | No, default all | all or workspace |
workspace | With workspace | The workspace this privacy-api serves. Any other name returns 400. |
Both scopes erase every session, media object and memory the subject has in the
workspace. privacy-api serves one workspace, so workspace names the same data
as all.
The service returns 400 for a request it cannot honour on every store:
scopedate_range, or adateFromordateTofield. memory-api cannot erase by date.scopeworkspacewithoutworkspace, orworkspacewith scopeall.
A request that reaches no service group ends failed, with an error that the
Workspace status lists no session-api or memory-api URL.
A request has the fields id, virtualUserId, reason, scope, workspace,
status, createdAt, startedAt, completedAt,
sessionsDeleted and errors. status moves from pending to in_progress
and ends at completed or failed. Each errors entry names the service group
and the tier that failed, for example group <name> sessions: ....
The receipts route takes these query parameters:
| Parameter | Meaning |
|---|---|
sessionId | Only receipts of this session |
callId | Only this tool call. Requires sessionId, because a call id is unique only within a session. |
since, until | RFC 3339 bounds. until must not be before since. |
limit | A positive integer |
cursor | A cursor the route issued |
The route is scoped to the workspace the privacy-api serves and accepts no
workspace parameter. An invalid parameter returns 400.
privacy-api writes deletion_requested to the audit hub when it accepts a
request and deletion_completed or deletion_failed when it ends, with
source_service = privacy-api-dsar.
Workspace routes
Section titled “Workspace routes”The dashboard serves these routes and proxies them to privacy-api. They require
at least the viewer role in the workspace.
| Method | Path | Proxies to |
|---|---|---|
GET, PUT | /api/workspaces/{workspace}/privacy/consent | The consent routes |
GET, POST, DELETE | /api/workspaces/{workspace}/privacy/opt-out | The opt-out routes |
GET | /api/workspaces/{workspace}/privacy/enforcement-stats | enforcement-stats |
GET | /api/workspaces/{workspace}/privacy/consent/stats | consent/stats |
The consent and opt-out routes pseudonymise the user id before they call
privacy-api. For a signed-in user they act on the session user and ignore the
userId query parameter. For an anonymous request they use userId. The
POST and DELETE bodies take scope and target; the route adds
virtualUserId.
Behaviour when privacy-api cannot answer
Section titled “Behaviour when privacy-api cannot answer”| Workspace state | session-api | memory-api |
|---|---|---|
spec.privacy unset | Writes proceed | Writes proceed |
spec.privacy set, privacy-api unresolved or failing | Treats the subject as opted out | Treats the subject as opted out |
For an opted-out subject, session-api stores the write with its content fields
replaced by the [OPTED_OUT] sentinel and tags the session content:suppressed.
Tokens, cost, model, latency and verdicts are stored unchanged. memory-api
refuses the write with 204.
The services re-resolve the privacy-api URL periodically and recover without a
restart once the Workspace publishes status.privacyURL. A failure to reach
privacy-api writes no row to the audit hub.
Log lines
Section titled “Log lines”| Service | Level | Message |
|---|---|---|
| session-api | Info (once per agent) | session content suppressed: privacy-api unavailable, treating subject as opted out |
| memory-api | Info | memory write suppressed with reason user-opted-out, privacy-unavailable, consent-not-granted or unknown-category |
| session-api, memory-api | Info (once) | PRIVACY NOT ENFORCED: no privacy-api configured for this workspace; user opt-out is disabled and all writes will be recorded |
| session-api, memory-api | Info (once per outage) | PRIVACY UNAVAILABLE: spec.privacy is set but privacy-api could not be resolved; user-scoped writes are being DROPPED until it is reachable |
Metrics
Section titled “Metrics”| Metric | Labels | Meaning |
|---|---|---|
omnia_session_api_content_suppressed_total | reason: user-opted-out, privacy-unavailable | Session writes whose content was suppressed |
omnia_session_api_writes_dropped_total | reason: redaction-failed, suppression-failed | Session writes refused by the privacy middleware |
omnia_memory_writes_suppressed_total | category, reason (as in the memory-api log line) | Memory writes refused before storage |
A privacy-unavailable value on either suppression counter marks a platform
fault. A user-opted-out value marks a subject’s choice.
Related pages
Section titled “Related pages”- Manage user consent and opt-out
- Handle data subject erasure (DSAR)
- Workspace: the
OptOutEnforcedcondition - Memory API
- Session API