Contextfulv0.23.0

Docs / Specification

contract authority · 50-authority.md · 51-enforcement.md · guide · card

Enforcement and inference placement

Three layers stand between a stored row and a caller: removal at write time, a redistribution bound at push, and a compiled relation at query time. Together they form the reference monitor. This file states what each layer does, the order the compiled relation applies, column classification and masking, and the grammar of an inference zone.

The three layers of the reference monitor and the contracts feeding each:

Flowchart diagram Flowchart, top to bottom. 14 nodes, 13 edges. source system → writer [source values] admitted authority → registered relation [grants] caller zone → registered relation [per request] write-time removal: writer → credential-shaped value? [each value] write-time removal: credential-shaped value? → refused write [yes]; → columnar parts [no, redacted per pipeline] store: columnar parts → redistribution flag cleared? [push]; → registered relation [rows] redistribution bound: redistribution flag cleared? → withheld table [yes: withheld]; → bucket [no] query-time restriction: registered relation → read surfaces [filter rows, mask, place] disclosure: permission semi-join → registered relation [permitted subjects] read: read surfaces bucket → serving edge [filtered by puller grants] write-time removal each value writer credential-shaped value? store columnar parts redistribution bound redistribution flag cleared? query-time restriction registered relation disclosure permission semi-join read read surfaces source values yes no, redacted per pipeline push yes: withheld no filtered by puller grants rows grants permitted subjects per request filter rows, mask, place source system admitted authority caller zone bucket serving edge refused write withheld table

redact

Removal and transformation inside the writer, ahead of columnar bytes and the run path’s durable record.

compose

The three layers as one reference monitor, and the order a registered relation applies.

  • complete-mediationtestEvery read of stored rows traverses the layers. A path reaching stored rows outside a registered relation is a defect, never a documented limitation.
    WhyP5
  • protection-is-a-rewritetestProtection is a rewrite in the data plane: predicates compiled into the statement, rewritten projections, a per-credential filter at the sync edge, zone floors. Admission decides entry alone.
    WhyP5
  • registered-relationtestThe compiler emits one relation per granted table, bound to the table’s bare name for the session’s lifetime. Every retrieval arm reads through it.
    WhyP5
  • relation-ordertesttestA registered relation applies, in order: mirrored permission semi-join, tenant equality, credential predicate, table policy, project default, table zone, then the column projection carrying masks and zone nulls.
    WhyP5
  • conjunctive-narrowingtestSteps conjoin: a surviving row survives each step alone, and appending a step removes rows and adds none.
    WhyP5
  • before-the-cuttestEvery step, zone exclusion included, completes before a ranked result is cut to its requested size; visible positions carry no trace of a withheld row.
    WhyP5
  • vector-armtestA sidecar vector index returns candidate identifiers, which re-join through the registered relation before contributing to a result.
    WhyP5
  • session-buildtestA session builds its relations at admission and rebuilds them when a revocation or a manifest change invalidates a grant it holds.
    WhyP5
  • relations-per-sessiontestA session carries at most 1024 entries in its relation set.
    Whybecause a larger grant set is a mis-scoped credential

filter-rows

The typed row-predicate grammar, subject binding, the tenant equality and the project default.

  • grammartestA row predicate is the typed boolean subset of SQL in Shapes: column references, comparisons, conjunction, disjunction, negation, membership lists, pattern matching, null tests and declared-safe scalar functions.
    WhyP1
  • outside-grammartestPredicates parse at manifest load; a node outside the grammar raises EnforcePredicateOutsideGrammar, naming the node.
    WhyP1
  • subject-relationtestSubject claims reach a predicate as prepared-statement parameters through a session-scoped subject relation. A claim value carrying SQL syntax occupies a parameter slot and reaches no parser.
    WhyP1
  • exceptiontestAn exception condition is a boolean expression over subject fields in the same grammar, bound the same way.
    WhyP1
  • overridetestA table-policy predicate overriding for a matching subject condition replaces that one predicate; it reaches neither the tenant equality, the mirrored join nor the credential predicate.
    WhyP5
  • default-denytestA table reached with no matching grant yields no rows.
    WhyP5
  • tenant-equalitytestA tenant-scoped grant compiles a bound-parameter byte equality on the tenant column, conjoined by the engine; no consumer statement carries it.
    WhyP5
  • predicate-sizetestA declared predicate holds at most 4 KiB of source text.
    Whybecause a longer predicate is unreviewable policy
  • membership-listtestA membership list inside a predicate holds at most 512 entries.
    Whybecause a longer list belongs in a mirrored table

mask

Column classes, strategies and combines, guardrails over an exhaustible value space, the pepper, and masked behavior under caller SQL.

keyed digest

hash and tokenize digest under one pepper, truncated within the class’s crowd.

refuse

The two shapes an unauthorized read takes, the scope guard, and what a refusal discloses.

  • scope-deniedtestA tenant-scoped credential reading another tenant’s rows on a granted table raises EnforceScopeDenied, as wire code scope_denied, HTTP 403 or an in-band tool error, naming the table and both scopes.
    WhyP2
  • not-emptytestA scope refusal is a typed error, never an empty result.
    WhyP2
  • echotestThe requested scope in a refusal echoes the statement’s value, never a stored value.
    WhyP2
  • ungranted-tabletestA relation the caller holds no grant on, whether or not a table of that name exists, raises EnforceUnknownRelation and is absent from listings and descriptions.
    WhyP2
  • payloadtestA refusal payload carries at most 512 B.
    WhyP2
  • scope-guardtestThe scope guard walks the engine’s parse once, deciding literal equalities and membership lists over the tenant column and bound template parameters destined for it.
    WhyP2
  • top-leveltestThe guard reads top-level conjuncts of a statement whose own FROM names a scoped table.
    WhyP2
  • guard-walktestThe guard visits at most 4096 entries of one parse tree.
    WhyP2
  • undecidabletestA constraint the guard cannot settle composes as an ordinary conjunct; the engine-applied equality isolates.
    WhyP5
  • drifted-scopetestA tenant value matching no partition reads empty, like a tenant that wrote nothing; the read path repairs no transformed scope value.
    WhyA-authority#one-credential

bound-redistribution

The per-table licence bound applied at push, the root-object allowlist, and the per-credential edge filter.

place

Inference zones: grammar, composition across grains, floors, incognito, and the serve-time outcome.

unsettled: Does the restriction block also carry the rows a row predicate removes and the column names a mask policy nulls, given both disclose how much of a table the caller’s scope withholds? owner: authority affects: authority.place

resist

The paths that stay closed to an injected instruction.

unsettled: Does a source writer refuse a credential-shaped value per value, landing the surrounding rows, and which operator-declared exemption admits a source carrying such text? owner: authority affects: authority.resist

Shapes

The class registry. The width ceiling assumes the default crowd:

class   exhaustible   domain   hash truncate ceiling
phi     no            -        -
ssn     yes           1e9      4
phone   yes           1e10     5
email   yes           1e10     5
mrn     yes           1e8      4
prompt  no            -        -
completion no         -        -

A column policy using optional health-data classes (mrn, medical record number, and phi, protected health information), and a zone declaration:

[pipeline.tables.policy.columns]
record_number   = { class = "mrn",   strategy = "hash", combine = "truncate:4" }
contact_email = { class = "email", strategy = "tokenize", combine = "truncate:4" }
restricted_text    = { class = "phi",   summarize_only = true }
value_band   = { strategy = "bucket:10000" }
internal_id   = { strategy = "hash" }

[pipeline.tables.policy.zone]
allow = ["local:device", "on-prem:*"]
protected_class_override = []

[pipeline.tables.policy.rows]
predicate = "region = subject.region AND classification <= subject.clearance"

  [[pipeline.tables.policy.rows.exception]]
  when      = "subject.role = 'auditor'"
  predicate = "true"

[pipeline.tables.policy.redistribution]
replicate = false

[[principal]]
id   = "reader-loop"
zone = { allow = ["local:device"] }

[project.zone]
default = "on-prem:hq"
users_may_widen = false
incognito_default = true

The row-predicate grammar:

predicate     = disjunction ;
disjunction   = conjunction , { "OR" , conjunction } ;
conjunction   = negation , { "AND" , negation } ;
negation      = [ "NOT" ] , primary ;
primary       = comparison | membership | pattern | null-test | "(" predicate ")" ;
comparison    = operand , ( "=" | "<>" | "<" | "<=" | ">" | ">=" ) , operand ;
membership    = column , [ "NOT" ] , "IN" , "(" , literal , { "," , literal } , ")" ;
pattern       = column , [ "NOT" ] , "LIKE" , string ;
null-test     = column , "IS" , [ "NOT" ] , "NULL" ;
operand       = column | subject-field | literal | safe-call ;
subject-field = "subject" , "." , identifier ;
safe-call     = declared-function , "(" , [ operand , { "," , operand } ] , ")" ;

The root-object allowlist while a table is withheld:

tables/<withheld>/    withheld: no object, no manifest entry
access/               ships: mirrored permission tables
nodes/                ships: per-node run state, no rows
control/              ships: the applied control chain, no rows
stale_memory.idx      ships
manifest.json         ships, naming no withheld table
catalog.db            withheld
records/              withheld: verbatim pull records
memory.db             withheld

The serve-time outcome and the response envelope:

{
  "row": { "drop": false, "masked_columns": ["restricted_text"] },
  "envelope": {
    "rows_removed_by_predicate": 41,
    "rows_removed_by_zone": 7,
    "columns_masked_by_policy": ["record_number", "contact_email"],
    "columns_masked_by_zone": ["restricted_text"],
    "asserted_zone": "on-prem:team-a",
    "incognito": true
  }
}

The two refusals on the wire:

{ "error": { "code": "scope_denied", "http": 403, "table": "records", "granted_scope": "team-a", "requested_scope": "team-b" } }
{ "error": { "code": "unknown_relation", "http": 404, "relation": "restricted_records" } }