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. Biscuit attenuation, audience, expiry, key rotation, cross-relation joins, and DP aggregation are explicit open problems (§6).
An agent's effective rights are context-driven: which principal it acts for, which task it was invoked under, which scope the caller granted. The same agent code, called by two principals — or by one principal under two tasks — must read two different views.
Traditional RBAC bakes a static identity → role → permission chain at provisioning time and cannot encode any of those axes. Per-engine RLS doesn't survive the ETL boundary; tenant segregation forfeits the cross-source joins that motivate the lakehouse. Policy has to move to the query plane, gated under the agent's task-scoped identity at query time.
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] -->|MCP plan + biscuit| gw{{Postern gateway}}
subgraph TCB[trusted base]
gw -->|verify| bc[biscuit verifier]
bc -->|principal| rw["plan rewriter
(Lean ref ↔ Rust mirror)"]
pol[(policy)] --> rw
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 the
postern-diff conformance harness (18/18 cases);
property-based differential testing is an open problem
(§6).
| theorem | file | status |
|---|---|---|
rewrite_sound | Postern.lean | proved |
rewrite_filter_sound | Postern.lean | proved |
eval_monotone | Datalog.lean | proof reduces to one sorry |
eval_sound | Datalog.lean | stated, sorry |
eval_terminates | Datalog.lean | stated, sorry |
Axioms of proven theorems:
{ propext, Quot.sound }.
eval_monotone's body is written but reduces
to herbrandBound_mono — a labelled
sorry we expose rather than hide. Eight
supporting iterator lemmas are proven.
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 — reference-conformance harness, asserts Rust output is byte-equal to the Lean reference on a hand-curated JSON corpus (18/18 cases: 15 accept, 3 refuse).postern-wasm — surface this site loads.The WASM module powering the demo is today the column-grant DSL evaluator; Biscuit-Datalog migration in progress.
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).
Paper §6 enumerates the full open-problem list.