Skip to content

Access & capabilities

Every read through /osl/query is limited by a capability + permission model: who is asking (a delegation chain of subjects), what each policy grants them over the semantic graph, and how those grants compose. It is fail-closed by construction — the default is deny, and anything unresolved is dropped, never guessed.

A subject is one principal in a delegation chain. It has one of three descriptive types:

Type Is
human A user (username / email)
agent An AI agent (with a vendor tag: Claude, ChatGPT, Gemini, …, or custom)
machine A workload / service account

Access is evaluated over a chain of principals, root → leaf. The acting subject is the last link. Composition has two levels:

Within one principal

All policies binding that principal combine: grants union, the strongest obligation wins (deny > mask > redact), an explicit deny overrides, and conditions (IP range, time window) gate ALLOW only — a deny is unconditional.

Across the chain

The links combine by MEET (intersection): an entity or column is granted only if every link grants it. The tier cap is the minimum (most restrictive) and the required redaction is the union of what each link requires. An empty or un-authored link collapses the meet to deny.

Link 0 (manager): { client, email: cleartext }
Link 1 (employee): { client, email: mask }
────────────────────────────────────────────── MEET
Effective: { client, email: mask } ← the column survives, obligated to mask

A policy’s semantic scope is a set of bindings, each keyed by a domain-map nodeId (e.g. entity:Customer, metric:aum). A binding declares:

Field Meaning
attributeGrants[] Per-attribute grant: cleartext, mask, or deny
filters[] (+ filtersBool) Row predicates that scope which rows are visible
grantedMetrics[] Standalone metric grants
facetGrants[] Per-facet content grant (see redaction versions)

Grants are positive: a column that isn’t granted is redacted, never returned as NULL or silently dropped from the schema. deny removes a column from the projection entirely; a policy-global deny overrides any other binding’s grant.

Content facets are governed by named redaction versions — a decoupling of a human-facing name from a physical, byte-stable identity:

  • A RedactionVersion has a name (the console handle, e.g. "PII básica") and a set of tags (the content classes it redacts, e.g. {email, phone}).
  • The tags are the physical identity (byte-unchanged, used as the table key); the name is only for authoring.
  • The implicit original version (no tags) is the base table.
  • A facetGrant binds a required version name per facet. Selection is exact-or-deny: the engine serves the stored version whose tags exactly match, or denies the facet — it never falls back to a more- or less-redacted version.
  • Across a chain, the required versions compose by union of tag-sets (more redaction is always safe).

Enforcement is server-side, in the cell. The outcomes:

Situation Value returned In the result?
Granted cleartext original yes
mask obligation *** yes
redact obligation ⟦redacted⟧ yes (survives for ORDER BY/GROUP BY)
deny / not granted dropped from the projection

The ⟦redacted⟧ sentinel means the column survives with a governed value (so downstream shape is stable), rather than becoming NULL or vanishing — which is what makes cross-modal joins fail-closed and the model composable.

The chain isn’t a trusted header — it’s a signed token. The cell mints and verifies an HS256 JWT whose payload contains the chain itself, so a caller can’t shorten or forge it without breaking the signature (an absent/invalid chain → deny). See Authentication.

Two dry-run operations compute the real composition without serving data (Enterprise console surfaces them; the APIs are in the cell):

  • :simulate — preview one policy against a query as a single subject.
  • :simulate-chain — compile the tenant’s full authored policy set over a root → leaf chain (the MEET), returning the composed scope plus a per-link contribution breakdown. An un-authored link is flagged; the meet empties to a fail-closed empty_meet.