Research artifact · 2026
First Lean-mechanised soundness theorem for a plan-level access-control rewriter in an LLM-agent-facing data lakehouse.
Two layers behind one gateway: a rewriter (inspired by Cedar) bounds what data reaches the agent, and a capability-bounded data flow layer (inspired by Odersky et al.) bounds what the agent's code may do with released values. 13 rewriter theorems are mechanised: cross-relation joins and abstract-DP aggregation now ship as scaffolds (Theorems 11–12) with three Join-arm residuals; biscuit attenuation, audience, expiry, key rotation, and a concrete DP-mechanism instantiation remain explicit open problems (§6).
Agent rights are context-driven — principal, task, scope — and the same agent code may legitimately serve different views on different calls. Static identity → role → permission can't encode that; per-engine RLS doesn't survive ETL; tenant segregation forfeits cross-source joins. Policy has to move to the query plane.
flowchart LR
classDef ok fill:#eef,stroke:#446,color:#333
classDef agent fill:#fec,stroke:#b15c2e,color:#333
classDef bad fill:#fee,stroke:#a33,color:#333,stroke-dasharray:4
subgraph rbac["RBAC (stable, fits humans)"]
direction LR
u[user]:::ok --> rl[role]:::ok --> pm[perms]:::ok
end
subgraph reality["Agent (context-driven, per call)"]
direction LR
pr[principal]:::agent --> ag[agent]:::agent
tk[task] -.-> ag
sc[scope] -.-> ag
ag --> vv{{"view varies per call"}}:::bad
end
Adapted from paper §Design (Layer 1 rewriter) and §4 (Layer 2 capability-bounded data flow).
flowchart LR
agent[LLM agent] -->|plan + biscuit| gw{{Postern gateway}}
subgraph TCB[trusted base]
gw -->|sig verify| bc[biscuit-auth]
bc -->|principal + token facts| ev["biscuit Datalog eval
(planned)"]
pol[(policy: right facts)] --> ev
ev -->|allow-set| rw["plan rewriter
(Lean ref ↔ Rust mirror)"]
cat[(catalog)] --> rw
rw -->|Option Plan| exe[DuckDB / Polars]
exe --> guard["capability-bounded sink
Cap·Tagged · opaque receipt"]
end
guard -->|receipt| agent
Layer 1 (rewriter) constrains what
data reaches the agent; Layer 2
(capability-bounded sink) constrains what the agent's
code may do with released values. Rust mirror is
hand-written and pinned to the Lean reference by two
conformance harnesses: postern-diff (31/31
rewriter cases, Lean reference ↔ Rust impl) and
postern-datalog-diff (9/9 Datalog cases, Lean
Program.allowed ↔ biscuit_auth::datalog::World).
Property-based differential testing is an open problem
(§6).
Surface syntax (left) is sugar over ground
right(principal, relation, column) Datalog
facts (right) — the in-scope fragment of biscuit-auth's
Datalog dialect. Attenuation, expiry, audience, and key
rotation are open problems
(paper §6).
grant CRM on users_data
{ id, name, region, age }
grant CardOps on cards_data
{ card_id, card_type, limit, activated }
grant FraudRisk on transactions_data
{ txn_id, card_id, amount, merchant, timestamp }
grant FraudRisk on users_data
{ id, region } right("CRM", "users_data", "id");
right("CRM", "users_data", "name");
right("CRM", "users_data", "region");
right("CRM", "users_data", "age");
right("CardOps", "cards_data", "card_id");
right("CardOps", "cards_data", "card_type");
…
right("FraudRisk", "users_data", "id");
right("FraudRisk", "users_data", "region");
Multiple grants on the same (p, r)
flat-union — the policy language is monotone grant-only,
so review is additive: a new grant can only widen.
Anything outside the union is denied (fail-closed).
| theorem | file | status |
|---|---|---|
T1–9 rewriter core (rewrite_sound, rewrite_filter_sound, …) | Postern.lean | proved |
T10 bridge_allowed — column-grant ≡ Datalog | Bridge.lean | proved |
T11 rewrite_sound_join + join-key leak | Postern.lean | proved |
T12 rewrite_sound_aggregate (abstract DP boundary) | Postern.lean | proved |
T13 rewrite_filter_coverage — predicate-level coverage | Postern.lean | proved |
T5, T6, T9 on Join arm (idempotent / monotone / forbidden-filter) | Postern.lean | 3 sorryAx residuals |
eval_monotone, herbrandBound_mono | Datalog.lean | proved |
eval_sound, eval_terminates | Datalog.lean | stated, sorry |
Axioms of proven theorems are bounded by
{ propext, Quot.sound } — Lean 4's
foundational set; no Classical.choice, no
user-supplied axioms. Five residuals isolated as sorryAx
in CheckAxioms.lean: three on the Join
arm of Theorems 5, 6, 9; two Datalog headlines (eval_sound,
eval_terminates). Eight Datalog support lemmas and
the combinatorial helpers underpinning herbrandBound_mono
all proved from Init stdlib, no Mathlib.
postern-core mirrors the Lean rewriter one-for-one.
The gateway evaluates policies through
biscuit_auth::datalog::World behind a Cargo
feature flag.
postern-core — Plan IR, policy, rewrite().postern-guardrail — paper §4's capability-bounded data flow: branded Cap<'sc, C>, invariant 'sc, opaque-receipt sinks (three compile_fail doctests pin the lexical escape attempts).postern-diff — rewriter conformance harness, asserts Rust output is byte-equal to the Lean reference on a hand-curated JSON corpus (31/31 cases across Project, Filter(Pred), Join, Aggregate).postern-datalog-diff — Datalog conformance harness, asserts Lean Program.allowed mem-set-equals biscuit_auth::datalog::World on a second corpus (9/9 cases, ground facts + Horn rules).postern-wasm — small WASM bundle this site loads.
The gateway's native build evaluates policies through
biscuit_auth::datalog::World directly
(postern-core::datalog, behind the
datalog-biscuit Cargo feature). The browser-side WASM
bundle powering the demo still ships the
column-grant DSL evaluator only — pulling
biscuit-auth through wasm-bindgen would
bloat the bundle, so the WASM-only migration stays on the §6
list.
The rewriter constrains what data reaches the agent. A separate layer constrains what the agent's code may do with the values released. Inspired by Odersky et al.'s Scala 3 capture-checking; Rust has none, so we mechanise a weaker analog out of three pure-Rust constructions.
pub fn to_llm<'sc, T, C, S>(
cap: Cap<'sc, C>,
data: Tagged<'sc, T, C>,
serialize: S,
) -> LlmAck
where S: FnOnce(T) -> String; // T never escapes the sink | bypass attempt | mechanism | pinned by |
|---|---|---|
forge a Cap | sealed private constructor | compile_fail |
read Tagged::value | private value field, sinks only | compile_fail |
escape brand 'sc | invariant PhantomData + for<'sc> scope combinator | compile_fail |
side-channel via T | opaque receipts (only serialised length escapes) | type signature |
Construction is the one used by ghost-cell for branded
references. Crate is no_std-compatible (paper §4).
sorryAx on T5, T6, T9 over the Join arm: per-leg idempotence composition; widening through a two-leg key check; cross-leg forbidden-filter refusal.aggAllowed predicate; closing it under a specific mechanism (ε-budget, Laplace, Gaussian, k-anonymity) is the next step.eval_sound and eval_terminates remain stated with sorryAx; both reduce to a body-grounding lemma and a finite-Herbrand pigeonhole.Paper §6 enumerates the full open-problem list.