Skip to content

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.

PortPurpose
8080JSON API
8081GET /healthz, and GET /readyz (readiness pings Postgres)
9090Prometheus metrics

The Service is named privacy-<workspace> and lives in the workspace namespace. The Workspace publishes its base URL in status.privacyURL.

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.

CategoryExplicit grant required
memory:preferencesNo
memory:contextNo
memory:historyNo
memory:identityYes
memory:locationYes
memory:healthYes

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.

MethodPathResult
PUT/api/v1/privacy/preferences/{userID}/consentApplies grants, then revocations, and returns the resulting state
GET/api/v1/privacy/preferences/{userID}/consentReturns the state

Request body of the PUT:

FieldTypeDescription
grantslist of categoriesCategories to grant
revocationslist of categoriesCategories to revoke

A category outside the table above returns 400 with unknown consent category: <value>.

Response body of both routes:

FieldDescription
grantsCategories the subject has granted
defaultsCategories that need no grant
deniedCategories 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.

MethodPathSuccess
POST/api/v1/privacy/opt-out204
DELETE/api/v1/privacy/opt-out204; 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:

FieldRequiredDescription
virtualUserIdYesThe pseudonymous subject id
scopeYesall, workspace or agent
targetFor workspace and agentThe 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.

Both routes require the workspace query parameter and return 400 without it.

MethodPathResponse fields
GET/api/v1/privacy/enforcement-statspiiBlocked, redactions, policyDenied, aiDisclosureMissing
GET/api/v1/privacy/consent/statstotalUsers, 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.

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.

MethodPathResult
POST/api/v1/privacy/deletion-requestCreates 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-requestsLists a subject’s requests, newest first. Takes virtualUserId; virtual_user_id is an alias.
GET/api/v1/privacy/receiptsLists tool-call receipts held by the audit hub

Body of the POST:

{
"virtualUserId": "<pseudonymized-user-id>",
"reason": "gdpr_erasure",
"scope": "all"
}
FieldRequiredValues
virtualUserIdYesThe pseudonymized subject id. An empty value returns 400; a request never deletes all users.
reasonYesgdpr_erasure, ccpa_delete or user_request
scopeNo, default allall or workspace
workspaceWith workspaceThe 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:

  • scope date_range, or a dateFrom or dateTo field. memory-api cannot erase by date.
  • scope workspace without workspace, or workspace with scope all.

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:

ParameterMeaning
sessionIdOnly receipts of this session
callIdOnly this tool call. Requires sessionId, because a call id is unique only within a session.
since, untilRFC 3339 bounds. until must not be before since.
limitA positive integer
cursorA 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.

The dashboard serves these routes and proxies them to privacy-api. They require at least the viewer role in the workspace.

MethodPathProxies to
GET, PUT/api/workspaces/{workspace}/privacy/consentThe consent routes
GET, POST, DELETE/api/workspaces/{workspace}/privacy/opt-outThe opt-out routes
GET/api/workspaces/{workspace}/privacy/enforcement-statsenforcement-stats
GET/api/workspaces/{workspace}/privacy/consent/statsconsent/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.

Workspace statesession-apimemory-api
spec.privacy unsetWrites proceedWrites proceed
spec.privacy set, privacy-api unresolved or failingTreats the subject as opted outTreats 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.

ServiceLevelMessage
session-apiInfo (once per agent)session content suppressed: privacy-api unavailable, treating subject as opted out
memory-apiInfomemory write suppressed with reason user-opted-out, privacy-unavailable, consent-not-granted or unknown-category
session-api, memory-apiInfo (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-apiInfo (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
MetricLabelsMeaning
omnia_session_api_content_suppressed_totalreason: user-opted-out, privacy-unavailableSession writes whose content was suppressed
omnia_session_api_writes_dropped_totalreason: redaction-failed, suppression-failedSession writes refused by the privacy middleware
omnia_memory_writes_suppressed_totalcategory, 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.