Schubert
Replace boolean allow/deny with geometric access control. Quantitative decisions. Impossibility detection. Continuous trust.
Schubert is a Rust library that reimagines access control through Schubert calculus — a
branch of algebraic geometry. Instead of returning true or false, Schubert tells you
how many valid configurations exist for a given set of capabilities. When conditions are
geometrically impossible to satisfy together, Schubert catches conflicts that traditional
boolean AND checks would silently approve.
Why Schubert?
Traditional access control gives you a boolean. You either can or you can't. This breaks down in complex systems:
- Two capabilities conflict but individually are fine — a boolean AND approves. Schubert detects the geometrical impossibility.
- Trust degrades over time — boolean systems can't express partial trust. Schubert models continuous trust with wall-crossing analysis.
- Cross-domain access is guesswork — can a capability in one domain translate to another? Schubert's Schubert intersection answers exactly.
- Rate limiting is arbitrary — Schubert scales rate limits by intersection numbers, giving higher-trust principals more throughput.
The Killer Feature: Impossibility Detection
Consider a user with write (σ₂) and internal-audit (σ₁₁) capabilities in Gr(2,4). Each capability is individually valid. Together? They're geometrically impossible — no subspace of ℝ⁴ can simultaneously satisfy both conditions.
A traditional RBAC system with boolean AND would approve. Schubert returns
AccessDecision::Impossible and tells you exactly which capabilities conflict.
What Schubert Is Not
- Not an authentication system — identity belongs to your OAuth/OIDC provider
- Not a network service — Schubert is a library you embed
- Not a policy server — no REST API, no gRPC, no wire protocol
- Not a single Grassmannian —
MultiControllermanages cross-domain access
The Industrial Algebra Ecosystem
Schubert depends on three sibling projects:
| Crate | Version | Role |
|---|---|---|
| Amari | 0.23 | Schubert calculus engine — Grassmannians, intersection numbers |
| Karpal | 0.5 | Formal verification — type-level proofs, SMT/Lean obligations |
| Minuet | 0.3 | Holographic memory — cosine-similarity access patterns |
License
Schubert is dual-licensed under AGPL-3.0-only and a commercial license. See LICENSE and LICENSE-COMMERCIAL for details.
Getting Started
Installation
Add Schubert to your Cargo.toml:
[dependencies]
schubert = "0.1"
Schubert requires nightly Rust (the Industrial Algebra ecosystem standard):
rustup toolchain install nightly
rustup default nightly
Your First Access Controller
use schubert::{ AccessController, Capability, CapabilityKind, AccessDecision, PrincipalId, }; fn main() -> Result<(), Box<dyn std::error::Error>> { // Create a controller for Gr(2,4) — the standard RBAC space let mut acl = AccessController::new(2, 4)?; // Register capabilities (Schubert conditions) acl.register_capability(Capability::new( "read:data", "Read data access", vec![1], // σ₁ — codimension 1 CapabilityKind::ReadLike, ))?; acl.register_capability(Capability::new( "write:data", "Write data access", vec![2], // σ₂ — codimension 2 CapabilityKind::WriteLike, ))?; // Create a principal and grant capabilities let alice = acl.create_principal("alice")?; acl.grant(&alice, "read:data")?; acl.grant(&alice, "write:data")?; // Check access — returns a quantitative decision match acl.check(&alice, &["read:data", "write:data"])? { AccessDecision::Granted { configurations } => { println!("Granted with {configurations} configurations"); } AccessDecision::Impossible { conflicting } => { println!("Geometrically impossible: {conflicting:?}"); } AccessDecision::Denied => { println!("Access denied (overconstrained)"); } AccessDecision::Underconstrained { dimension } => { println!("Policy too permissive (dimension {dimension})"); } } Ok(()) }
Understanding the Output
| Decision | Meaning |
|---|---|
Granted { configurations: n } | Access allowed in exactly n ways |
Impossible { conflicting } | Conditions are geometrically incompatible |
Denied | Too many conditions for the policy space |
Underconstrained { dimension } | Not enough conditions (policy is loose) |
Choosing a Grassmannian
| Gr(k,n) | Dimension k(n−k) | Use Case |
|---|---|---|
| Gr(2,4) | 4 | Standard RBAC (recommended starting point) |
| Gr(3,6) | 9 | Complex multi-tenant policies |
| Gr(4,8) | 16 | Enterprise-scale policy space |
Larger Grassmannians support more distinct capabilities but have higher computational cost.
Next Steps
- Mathematical Foundation — understand the geometry
- Capabilities as Schubert Conditions — designing capabilities
- Feature Flags — enabling optional features
- Installation & Configuration — production setup
Mathematical Foundation
Schubert uses Schubert calculus — a branch of algebraic geometry — to make access control decisions. You don't need to be a mathematician, but understanding the core concepts helps design better policies.
The Grassmannian as Policy Space
A Grassmannian Gr(k,n) is the space of all k-dimensional subspaces of an n-dimensional vector space. In Schubert, we use it as the policy space — each point represents a possible access configuration.
The dimension of Gr(k,n) is k(n−k). This is the maximum number of independent Schubert conditions you can impose before the space collapses:
| Gr(k,n) | Dimension | Max Independent Conditions |
|---|---|---|
| Gr(2,4) | 4 | 4 |
| Gr(3,6) | 9 | 9 |
| Gr(4,8) | 16 | 16 |
Schubert Conditions
A Schubert condition is a geometric constraint defined by a partition — a
weakly decreasing sequence of integers like [1], [2,1], or [2,2]. Each partition
corresponds to a specific subspace constraint.
The codimension of a condition is the sum of the partition entries. Higher codimension = more restrictive:
| Partition | Codimension | Typical Use |
|---|---|---|
[1] | 1 | Read access |
[2] | 2 | Write access |
[1,1] | 2 | Read + audit |
[2,1] | 3 | Manage |
[2,2] | 4 | Admin (point class) |
Schubert Intersection
When you check multiple capabilities, Schubert computes their Schubert intersection. The intersection number (Littlewood-Richardson coefficient) tells you how many configurations satisfy all conditions simultaneously:
- Positive integer: access is granted with that many configurations
- Zero: the conditions are geometrically impossible together (the killer feature)
- Too many conditions (> dimension): overconstrained — access denied
Key Mathematical Properties (Verified)
- σ₁⁴ = 2 in Gr(2,4) — four read-like conditions yield exactly 2 configurations
- σ₂ · σ₁₁ = 0 — write + internal-audit is geometrically impossible
- Composition is commutative — grant order doesn't matter
- Grant-revoke identity — grant then revoke = no net change
External References
- Grassmannian — Wikipedia
- Schubert calculus — Wikipedia
- Littlewood-Richardson rule — Wikipedia
- Schubert variety — Wolfram MathWorld
- Capability-based security — Wikipedia
Capabilities as Schubert Conditions
Every capability in Schubert is a Schubert condition — a geometric constraint with a partition, a kind, and a label.
Defining a Capability
#![allow(unused)] fn main() { use schubert::{Capability, CapabilityKind}; let read = Capability::new( "read:data", // unique ID "Read data access", // human-readable label vec![1], // partition: σ₁ (codimension 1) CapabilityKind::ReadLike, ); let write = Capability::new( "write:data", "Write data access", vec![2], // partition: σ₂ (codimension 2) CapabilityKind::WriteLike, ); let admin = Capability::new( "admin", "Full administrative access", vec![2, 2], // partition: σ₂₂ (codimension 4, point class) CapabilityKind::AdminLike, ); }
CapabilityKind
The CapabilityKind affects how capabilities behave under trust degradation:
| Kind | Trust Sensitivity | Examples |
|---|---|---|
ReadLike | Low — stable even at low trust | Read, list, view |
WriteLike | Medium — degrades at moderate trust | Write, update, delete |
AdminLike | High — degrades rapidly under trust loss | Admin, manage, configure |
Custom | Application-defined | Any custom semantic |
Higher-codimension AdminLike capabilities are the first to become unstable as trust erodes. This models the real-world principle that powerful capabilities should require more trust.
Partition Design
Partitions determine how capabilities interact. Key rules:
- Partitions must be weakly decreasing:
[2,1]is valid,[1,2]is not - Codimension = sum of entries:
[2,1]has codimension 3 - One element = one row:
[1]is a single-row condition - Equal partitions = same restriction: two capabilities with
[1]are equivalent in their geometric constraint
Temporal Capabilities
Capabilities can have an expiry time:
#![allow(unused)] fn main() { let temp = Capability::new("temp", "Temporary access", vec![1], CapabilityKind::ReadLike) .with_expiry(now + 3_600_000); // 1 hour from now acl.register_capability(temp)?; acl.grant(&principal, "temp")?; // Later: acl.check_temporal(&principal, &["temp"], now)?; // OK acl.check_temporal(&principal, &["temp"], later)?; // Denied }
Registration and Grants
Capabilities must be registered with the controller before they can be granted:
#![allow(unused)] fn main() { // 1. Register acl.register_capability(read)?; // 2. Grant to principal acl.grant(&alice, "read:data")?; // 3. Check acl.check(&alice, &["read:data"])?; }
Registration defines the capability's geometry. Granting assigns it to a principal. Checking evaluates the intersection.
Access Decisions
When you call acl.check(), Schubert returns an AccessDecision — not a boolean,
but a quantitative result with four variants.
The Four Decisions
#![allow(unused)] fn main() { pub enum AccessDecision { /// Access allowed with exactly this many configurations Granted { configurations: usize }, /// Conditions are geometrically incompatible (Littlewood-Richardson = 0) Impossible { conflicting: Vec<String> }, /// Too many conditions for the policy space (overconstrained) Denied, /// Too few conditions — policy is loose Underconstrained { dimension: usize }, } }
Granted
The intersection number is positive. The principal can access the resource in
configurations distinct ways.
#![allow(unused)] fn main() { acl.grant(&alice, "read")?; acl.grant(&alice, "write")?; let result = acl.check(&alice, &["read", "write"])?; match result { AccessDecision::Granted { configurations } => { // configurations is the Littlewood-Richardson coefficient // for σ₁ ∩ σ₂ in this Grassmannian } _ => {} } }
Impossible
The killer feature. Individual capabilities are valid but together they're geometrically impossible. A traditional boolean AND would approve.
#![allow(unused)] fn main() { // σ₂ (write) and σ₁₁ (internal audit) in Gr(2,4) acl.grant(&principal, "write")?; acl.grant(&principal, "internal_audit")?; let result = acl.check(&principal, &["write", "internal_audit"])?; // AccessDecision::Impossible { conflicting: ["write", "internal_audit"] } }
The Littlewood-Richardson coefficient σ₂ · σ₁₁ = 0 in Gr(2,4). No subspace can simultaneously satisfy both conditions.
Denied
Too many independent conditions — the intersection is empty because the total codimension exceeds the Grassmannian dimension.
#![allow(unused)] fn main() { // Gr(2,4) has dimension 4 — can't impose 5 independent conditions acl.check(&principal, &["c1", "c2", "c3", "c4", "c5"])?; // AccessDecision::Denied }
Underconstrained
Too few conditions — the policy doesn't pin down a specific configuration. The remaining dimension tells you how loose the policy is.
#![allow(unused)] fn main() { // Only one condition in Gr(2,4) with dimension 4 // Remaining dimension = 4 - 1 = 3 acl.check(&principal, &["read"])?; // AccessDecision::Underconstrained { dimension: 3 } }
Computation Paths
Schubert supports four computation engines for computing intersection numbers:
| Path | When to use |
|---|---|
LR | Default — balanced performance, exact results |
Localization | When you need geometric insight into why |
Tropical | Large-scale batch operations (>1000 principals) |
Matroid | When parallel evaluation is enabled |
#![allow(unused)] fn main() { use schubert::ComputationPath; acl.set_computation_path(ComputationPath::Tropical); }
Trust and Stability
Schubert models trust as a continuous value from 0.0 to 1.0, and analyzes how capabilities degrade as trust erodes.
Continuous Trust
Unlike boolean access control (trusted / not trusted), Schubert supports continuous trust:
#![allow(unused)] fn main() { use schubert::AccessContext; let ctx = AccessContext { resource: Some("customer-data".into()), time_budget_ms: Some(500), required_trust: 0.85, // 85% trust required }; acl.check_with_context(&principal, &["read:data"], &ctx)?; }
Wall-Crossing Stability
The wall-crossing engine (analyze_stability()) finds the trust levels where
capabilities become unstable:
#![allow(unused)] fn main() { use schubert::analyze_stability; let report = analyze_stability(&acl, &principal)?; // report.phase_diagram — breakpoints where stability changes // report.walls — individual stability walls per capability // report.most_sensitive — which capability degrades first }
A phase diagram shows how many configurations are available at each trust level. As trust drops, capabilities cross stability walls — higher-codimension (AdminLike) capabilities cross first.
Trust Sensitivity by Kind
| CapabilityKind | Degradation Pattern |
|---|---|
| ReadLike | Degrades below ~0.3 trust |
| WriteLike | Degrades below ~0.5 trust |
| AdminLike | Degrades below ~0.7 trust |
This models the security principle that powerful operations require higher trust.
Surreal Trust Levels
For applications requiring exact arithmetic on infinitesimal trust differences,
enable the surreal feature:
#![allow(unused)] fn main() { // Requires: features = ["surreal"] use schubert::surreal_trust::SurrealTrust; let trust = SurrealTrust::new(rational_surreal_value); }
The surreal trust module uses Amari's RationalSurreal (v0.23) for exact arithmetic
on trust values, including infinitesimal ε and ε². This enables:
- Exact comparison of arbitrarily close trust levels
- Infinitesimal trust recovery after temporary degradation
- Provable trust monotonicity for formal verification
#![allow(unused)] fn main() { // Compare infinitesimal trust levels let a = SurrealTrust::from_epsilon(1); // ε (infinitesimal) let b = SurrealTrust::from_epsilon(2); // 2ε (twice infinitesimal) assert!(a < b); // Exact trust composition let combined = a.compose_with(b)?; }
For a deep dive, see Surreal Trust Levels.
Composition and Composability
Schubert supports operadic composition — combining capabilities across principals to model service chains, delegation, and capability translation.
Operadic Composition
Two capabilities are composable if their Schubert intersection is non-empty. The result includes a multiplicity — how many configurations survive the composition:
#![allow(unused)] fn main() { use schubert::compose; let result = compose(&acl, &producer, "output", &consumer, "input")?; match result { CompositionResult::Composable { multiplicity } => { println!("{multiplicity} configurations survive composition"); } CompositionResult::NotComposable { reason } => { println!("Cannot compose: {reason}"); } } }
Service Chain Model
Composition models real-world service chains:
Service A (produces "report") ─┐
├─► Compose? Multiplicity?
Service B (consumes "report") ─┘
If Service A's output capability and Service B's input capability are composable with multiplicity > 0, the service chain is valid.
Mathematical Properties
- Commutativity: Grant order doesn't affect composition result
- Associativity:
(a ∘ b) ∘ c = a ∘ (b ∘ c) - Identity: Grant then revoke = no net change
- Impossibility is symmetric: If
a ∘ bis impossible,b ∘ ais too
Cross-Domain Composition
For multi-Grassmannian setups, use MultiController:
#![allow(unused)] fn main() { use schubert::MultiController; let mut mc = MultiController::new(); let rbac = mc.add_domain(2, 4)?; // RBAC domain let tenant = mc.add_domain(3, 6)?; // Multi-tenant domain mc.create_principal("alice", &rbac)?; mc.grant_in_domain(&alice, "read", &rbac)?; // Check if an RBAC capability works in the tenant domain: mc.check_cross_domain(&alice, &["read"], &rbac, &tenant)?; }
Cross-domain checks use Schubert intersection to determine if capabilities translate between Grassmannians.
Checking Composability
Before attempting composition, check if capabilities are composable:
#![allow(unused)] fn main() { if are_composable(&acl, "read", "write")? { let result = compose(&acl, &alice, "read", &bob, "write")?; // ... } }
The are_composable() check is cheaper than full composition — it only checks
whether the Littlewood-Richardson coefficient is non-zero.
Installing and Configuring
Cargo.toml
[dependencies]
schubert = "0.1"
Feature Flags
Enable additional features as needed:
[dependencies]
schubert = { version = "0.1", features = ["serde", "policy", "crypto"] }
| Feature | Enables |
|---|---|
std (default) | HashMap, SystemTime, thread-safe audit |
serde | Serialization on all types, JSON I/O |
karpal | Type-level proofs (Proven, Rewrite) |
parallel | Batch operations via rayon |
policy | TOML policy language |
wasm | WebAssembly JS bindings |
crypto | Ed25519 capability tokens |
karpal-verify | Formal verification (SMT/Lean) |
surreal | Exact surreal trust arithmetic |
holographic | Minuet holographic memory |
All features compose — enable any combination.
no_std Support
[dependencies]
schubert = { version = "0.1", default-features = false }
When std is disabled: HashMap → BTreeMap, InMemoryAudit is single-threaded,
AuditSink trait is unavailable.
Production Configuration
[dependencies]
schubert = { version = "0.1", features = [
"serde", # Serialization for policy persistence
"policy", # TOML policy files
"crypto", # Signed capability tokens
"parallel", # Batch operations
] }
For high-assurance systems, add karpal, karpal-verify, and surreal.
Feature Flags
Schubert uses additive feature gates — enabling a feature adds functionality without breaking existing API.
Available Features
| Feature | What It Enables |
|---|---|
std (default) | HashMap, SystemTime, thread-safe Mutex audit |
serde | Serialize/Deserialize on all types, JSON I/O |
karpal | proof module: Proven, Property, Rewrite, law checks |
parallel | check_batch(), stability_batch(), compose_batch() via rayon |
policy | policy module: TOML parsing, validate, roundtrip |
wasm | wasm module: WasmController with JS bindings |
crypto | crypto module: Ed25519 CapabilityToken, Issuer, Verifier |
karpal-verify | verify module: SMT/Lean proof obligations, Certified trust boundary |
surreal | surreal_trust module: RationalSurreal + EpsilonPolynomial |
holographic | holographic module: Minuet integration |
Common Combinations
# Production with crypto tokens and policy loading
cargo build --features serde,policy,crypto
# Research with proofs and verification
cargo build --features karpal,karpal-verify,surreal
# Browser with wasm bindings
cargo build --target wasm32-unknown-unknown --features wasm
# Everything (for development)
cargo build --all-features
All features are designed to compose freely. Enable only what you need — each feature adds compile time and binary size.
Adding Your Own Feature
Feature gates follow the IA ecosystem convention:
- Add to
[features]inCargo.toml - Use
#[cfg(feature = "my-feature")]on module declarations - Use
#[cfg(feature = "my-feature")]on impl blocks and functions - Document in
src/lib.rs
Policy Language (TOML)
Schubert supports declarative policies via TOML files. Enable with the policy feature.
Policy File Format
# policy.toml
[grassmannian]
k = 2
n = 4
[capabilities.read]
partition = [1]
kind = "ReadLike"
label = "Read access"
[capabilities.write]
partition = [2]
kind = "WriteLike"
label = "Write access"
[capabilities.admin]
partition = [2, 2]
kind = "AdminLike"
label = "Full administration"
[principals.alice]
grants = ["read", "write"]
[principals.bob]
grants = ["read"]
[principals.admin_user]
grants = ["admin"]
Loading Policies
#![allow(unused)] fn main() { use schubert::AccessController; let toml_str = std::fs::read_to_string("policy.toml")?; let acl = AccessController::from_policy_toml(&toml_str)?; // Use the loaded controller let alice = acl.get_principal("alice")?; acl.check(&alice, &["read", "write"])?; }
Exporting Policies
#![allow(unused)] fn main() { let toml_str = acl.to_policy_toml()?; std::fs::write("exported-policy.toml", toml_str)?; }
Validation
Policies are validated on load:
- Grassmannian dimensions must satisfy 0 < k < n
- Partitions must be weakly decreasing
- Capability IDs must be unique
- Principal grants must reference registered capabilities
- CapabilityKind must be a valid variant
Invalid policies return descriptive errors with context.
Multi-Domain Access
MultiController manages access across multiple Grassmannian domains with
cross-domain capability translation.
Setup
#![allow(unused)] fn main() { use schubert::MultiController; let mut mc = MultiController::new(); // Register domains let rbac_domain = mc.add_domain_named(2, 4, "rbac")?; let tenant_domain = mc.add_domain_named(3, 6, "multi-tenant")?; // Create principal in a domain let alice = mc.create_principal("alice", &rbac_domain)?; // Grant capabilities within a domain mc.grant_in_domain(&alice, "read", &rbac_domain)?; mc.grant_in_domain(&alice, "write", &rbac_domain)?; }
Same-Domain Check
#![allow(unused)] fn main() { let result = mc.check_in_domain(&alice, &["read", "write"], &rbac_domain)?; }
Cross-Domain Check
Translates capabilities between Grassmannians using Schubert intersection:
#![allow(unused)] fn main() { // Check if RBAC read/write capabilities work in the tenant domain let result = mc.check_cross_domain( &alice, &["read", "write"], &rbac_domain, // from this domain &tenant_domain, // to this domain )?; }
Domain Discovery
#![allow(unused)] fn main() { // Find domains that accept a given partition let domains = mc.domains_for_partition(&[1])?; // List capabilities translatable between domains let translatable = mc.translatable_capabilities(&rbac_domain, &tenant_domain)?; }
Rate Limiting
Schubert's RateLimiter uses a token-bucket algorithm scaled by Schubert intersection
numbers — higher-trust principals get proportionally more throughput.
Basic Rate Limiter
#![allow(unused)] fn main() { use schubert::RateLimiter; // 10 tokens per second, burst capacity of 20 tokens let mut rl = RateLimiter::new(10.0, 1.0); // Per-request rate check if rl.try_consume("alice").is_err() { return Err("rate limit exceeded"); } }
Configure from Access Decision
#![allow(unused)] fn main() { let granted = acl.check(&alice, &["read", "write"])?; let mut rl = RateLimiter::new(10.0, 1.0); // Scale the rate limiter based on access decision: // Higher intersection numbers → more tokens rl.configure_from_decision("alice", &granted)?; }
Bucket State Queries
#![allow(unused)] fn main() { let available = rl.tokens_available("alice"); let capacity = rl.capacity("alice"); let fill_rate = rl.refill_rate(); }
How It Works
The token bucket is parameterized by the Schubert intersection number from the access decision. A principal with more valid configurations (higher intersection number) gets a larger token bucket — this models the principle that higher-trust, multi-capability access should have proportionally more throughput.
When trust degrades and the intersection number drops, the rate limit tightens automatically.
Schubert Routing
Geometric network routing — paths through Schubert conditions.
RouteTable
#![allow(unused)] fn main() { use schubert::RouteTable; let mut table = RouteTable::new(2, 4); // Advertise a route as a Schubert condition table.advertise("service-a", vec![1])?; table.advertise("service-b", vec![2])?; // Find a path through the route table let path = table.find_path(&["service-a", "service-b"])?; }
Route Advertisements
Each route advertisement is a Schubert condition with a partition. Routes are compatible if their Schubert intersection is non-empty:
#![allow(unused)] fn main() { // Advertise with different codimensions table.advertise("gateway", vec![1])?; // σ₁ — lightweight route table.advertise("database", vec![2, 1])?; // σ₂₁ — restricted route // Routes compose if intersection > 0 let route = table.find_path(&["gateway", "database"])?; }
Congestion Detection
If the intersection number drops (fewer valid paths), the route table detects congestion:
#![allow(unused)] fn main() { if let Some(congested) = table.check_congestion(&["gateway", "database"])? { println!("Route congested: {congested:?}"); } }
Distributed CRDTs
Eventually-consistent access grants using Conflict-Free Replicated Data Types (CRDTs). Multiple nodes can independently grant/revoke capabilities and merge.
CrdtState
#![allow(unused)] fn main() { use schubert::crdt::{CrdtState, CrdtGrant, VersionVector}; let mut node_a = CrdtState::new(); let mut node_b = CrdtState::new(); // Node A grants a capability node_a.apply(CrdtGrant::grant("alice", "read"))?; // Node B grants a capability (concurrently) node_b.apply(CrdtGrant::grant("alice", "write"))?; // Merge — both grants survive node_a.merge(&node_b)?; assert!(node_a.has_grant("alice", "read")); assert!(node_a.has_grant("alice", "write")); }
Version Vectors
Each grant carries a version vector tracking causal history:
#![allow(unused)] fn main() { let grant = CrdtGrant::grant("alice", "read"); println!("Version: {:?}", grant.version()); }
Last-Write-Wins
Conflicting grants (same principal, same capability) resolve via last-write-wins:
#![allow(unused)] fn main() { // Node A grants, Node B revokes concurrently let grant = CrdtGrant::grant("alice", "read"); let revoke = CrdtGrant::revoke("alice", "read"); // Merge resolves to the operation with the higher timestamp node_a.apply(grant)?; node_a.merge(&node_b)?; // state_b has the revoke with higher timestamp }
Merge Properties
- Commutative:
a.merge(b) == b.merge(a) - Associative:
(a.merge(b)).merge(c) == a.merge(b.merge(c)) - Idempotent:
a.merge(a) == a
CLI Discovery Tool
Schubert includes a lightweight CLI for LLM agents to discover and use its API. Three subcommands cover the full lifecycle.
Install
cargo install schubert
schubert discover — API Catalog
Compact JSON schema of the full API surface (~200-500 tokens).
# Full catalog
schubert discover
# Filter by feature
schubert discover --feature crypto
# Filter by module
schubert discover --module routing
# Markdown output
schubert discover --format md
schubert recommend — Config Recommender
# Interactive mode
schubert recommend
# Batch mode (LLM automation)
schubert recommend --input constraints.toml
Recommends optimal Gr(k,n), computation path, and feature flags given constraints like number of roles, domains, audit requirements, and trust model.
schubert explore — Decision Sandbox
# REPL mode
schubert explore
# One-shot evaluator (LLM tool-calling)
schubert explore --eval '{"action":"create","k":2,"n":4}'
schubert explore --eval '{"action":"check","principal":"alice","capabilities":["read","write"]}'
Supports actions: create, grant, check, stability, compose, revoke, list.
For the full guide, see CLI Guide.
AccessController
The main entry point for all access control operations.
Construction
#![allow(unused)] fn main() { use schubert::AccessController; let mut acl = AccessController::new(2, 4)?; // Gr(2,4) }
Principal Management
#![allow(unused)] fn main() { // Create a principal let alice = acl.create_principal("alice")?; // Get an existing principal let bob = acl.get_principal("bob")?; // List all principals let principals = acl.principals(); }
Capability Management
#![allow(unused)] fn main() { use schubert::{Capability, CapabilityKind}; // Register a capability acl.register_capability(Capability::new( "read:data", "Read data access", vec![1], CapabilityKind::ReadLike, ))?; // List registered capabilities let capabilities = acl.capabilities(); // Check if a capability is registered if acl.has_capability("read:data") { // ... } }
Grant / Revoke
#![allow(unused)] fn main() { acl.grant(&alice, "read:data")?; acl.grant(&alice, "write:data")?; // Revoke acl.revoke(&alice, "write:data")?; // Check what a principal holds let held = acl.held_by(&alice); }
Access Check
#![allow(unused)] fn main() { let result = acl.check(&alice, &["read:data"])?; match result { AccessDecision::Granted { configurations } => { /* ... */ } AccessDecision::Impossible { conflicting } => { /* ... */ } AccessDecision::Denied => { /* ... */ } AccessDecision::Underconstrained { dimension } => { /* ... */ } } }
Context-Aware Check
#![allow(unused)] fn main() { use schubert::AccessContext; let ctx = AccessContext { resource: Some("customer-data".into()), time_budget_ms: Some(500), required_trust: 0.85, }; acl.check_with_context(&alice, &["read:data"], &ctx)?; }
Batch Operations (parallel feature)
#![allow(unused)] fn main() { let queries = vec![ (alice.clone(), vec!["read:data"]), (bob.clone(), vec!["write:data"]), ]; let results = acl.check_batch(&queries)?; }
Computation Path
#![allow(unused)] fn main() { use schubert::ComputationPath; acl.set_computation_path(ComputationPath::LR); acl.set_computation_path(ComputationPath::Tropical); }
Audit Sink
#![allow(unused)] fn main() { use schubert::audit::InMemoryAudit; acl.set_audit_sink(Box::new(InMemoryAudit::new())); // Every check() call now records to the sink }
Capability & Principal
Capability
A Schubert condition with a partition, kind, and label.
#![allow(unused)] fn main() { use schubert::{Capability, CapabilityKind}; let cap = Capability::new( "read:data", // unique ID "Read data access", // human label vec![1], // partition (Schubert condition) CapabilityKind::ReadLike, ); }
Fields
| Field | Type | Description |
|---|---|---|
id | &str | Unique capability identifier |
label | &str | Human-readable description |
partition | Vec<usize> | Schubert partition (weakly decreasing) |
kind | CapabilityKind | Semantic category affecting trust sensitivity |
expires_at | Option<u64> | Optional expiry timestamp (milliseconds) |
Methods
#![allow(unused)] fn main() { // Temporal capabilities let temp = cap.with_expiry(now + 3_600_000); // 1 hour let remaining = temp.time_remaining_at(check_time); let is_expired = temp.is_expired_at(check_time); // Codimension (sum of partition entries) let codim = cap.codimension(); // 1 for [1], 3 for [2,1] }
CapabilityKind
#![allow(unused)] fn main() { pub enum CapabilityKind { ReadLike, // Low trust sensitivity WriteLike, // Medium trust sensitivity AdminLike, // High trust sensitivity Custom, // Application-defined } }
PrincipalId
An opaque identity wrapper. Schubert never authenticates — identity is provided by your external auth system.
#![allow(unused)] fn main() { use schubert::PrincipalId; let alice = PrincipalId::new("alice"); let from_jwt = PrincipalId::new(jwt_claims.sub); }
PrincipalId implements Clone, Eq, Hash, Debug, and with serde:
Serialize/Deserialize.
Decision & Context
AccessDecision
The quantitative result of an access check.
#![allow(unused)] fn main() { pub enum AccessDecision { Granted { configurations: usize }, Impossible { conflicting: Vec<String> }, Denied, Underconstrained { dimension: usize }, } }
Decision Logic
| Condition | Decision |
|---|---|
| Intersection number > 0 | Granted { configurations } |
| Intersection number = 0 | Impossible { conflicting } |
| Total codimension > Gr dimension | Denied |
| Total codimension < Gr dimension | Underconstrained { dimension } |
Methods
#![allow(unused)] fn main() { impl AccessDecision { pub fn is_granted(&self) -> bool; pub fn is_impossible(&self) -> bool; pub fn is_denied(&self) -> bool; pub fn is_underconstrained(&self) -> bool; pub fn grant_count(&self) -> Option<usize>; } }
ComputationPath
Four engines for computing Schubert intersections:
#![allow(unused)] fn main() { pub enum ComputationPath { LR, // Default — balanced performance Localization, // Geometric insight into *why* Tropical, // Large-scale batch operations Matroid, // Parallel evaluation } }
AccessContext
Context-aware access with resource scoping and trust requirements:
#![allow(unused)] fn main() { pub struct AccessContext { /// Optional resource identifier for scoping pub resource: Option<String>, /// Time budget for the check (milliseconds) pub time_budget_ms: Option<u64>, /// Minimum trust level required (0.0–1.0) pub required_trust: f64, } }
Used with check_with_context() for time-aware, resource-scoped, trust-gated
access decisions.
Composition Engine
Operadic composition of principals through shared capabilities.
compose()
#![allow(unused)] fn main() { use schubert::compose; let result = compose(&acl, &producer, "output", &consumer, "input")?; }
CompositionResult
#![allow(unused)] fn main() { pub enum CompositionResult { Composable { multiplicity: usize }, NotComposable { reason: String }, } }
- Composable:
multiplicityconfigurations survive the composition - NotComposable: Geometrically incompatible capabilities
are_composable()
Cheaper pre-check before full composition:
#![allow(unused)] fn main() { if are_composable(&acl, "read", "write")? { let result = compose(&acl, &alice, "read", &bob, "write")?; } }
Use Cases
- Service chaining: Service A produces output that Service B consumes
- Delegation: Principal delegates a capability to another principal
- Cross-domain translation: Translate capabilities between Grassmannians
- Composability checking: Verify two services can interoperate
Properties
- Commutative: Order of composition doesn't affect result
- Associative:
(a ∘ b) ∘ c = a ∘ (b ∘ c) - Zero-preserving: If either side is impossible, composition is impossible
Stability Analysis
Wall-crossing analysis of capability stability under trust degradation.
analyze_stability()
#![allow(unused)] fn main() { use schubert::analyze_stability; let report = analyze_stability(&acl, &principal)?; }
StabilityReport
#![allow(unused)] fn main() { pub struct StabilityReport { /// Breakpoints where stability changes pub phase_diagram: Vec<(f64, usize)>, /// Individual stability walls per capability pub walls: Vec<StabilityWall>, /// Which capability degrades first pub most_sensitive: String, /// Current stability at trust = 1.0 pub at_full_trust: usize, /// Current stability at trust = 0.0 pub at_zero_trust: usize, } }
StabilityWall
#![allow(unused)] fn main() { pub struct StabilityWall { pub capability: String, pub cap_kind: CapabilityKind, /// Trust level where this capability crosses its stability wall pub trust_threshold: f64, } }
How It Works
- For each granted capability, compute its stability as a function of trust
- Higher-codimension capabilities cross stability walls at higher trust levels
- AdminLike capabilities cross first, ReadLike last
- The phase diagram shows total viable configurations at each trust level
Batch Stability (parallel feature)
#![allow(unused)] fn main() { let principals = vec![alice, bob, carol]; let reports = analyze_stability_batch(&acl, &principals)?; }
Audit & Error
AuditSink Trait
Pluggable audit interface for recording access decisions.
#![allow(unused)] fn main() { use schubert::audit::{AuditSink, DecisionRecord}; struct DatabaseAudit { pool: PgPool } impl AuditSink for DatabaseAudit { fn record(&self, record: &DecisionRecord) -> schubert::Result<()> { // Write to database, file, log, etc. Ok(()) } } acl.set_audit_sink(Box::new(DatabaseAudit { pool })); }
InMemoryAudit
Built-in audit sink that stores records in memory:
#![allow(unused)] fn main() { use schubert::audit::InMemoryAudit; let sink = InMemoryAudit::new(); acl.set_audit_sink(Box::new(sink)); // After checks... let records = sink.records(); let filtered = sink.records_for_principal(&alice); sink.clear(); }
Audit Design
- Fire-and-forget: Failing sinks never block access decisions
- Feature-gated:
AuditSinkrequires thestdfeature - No_std:
InMemoryAuditusesRefCell, no thread safety
SchubertError
All errors use SchubertError with 11 variants:
| Variant | When |
|---|---|
InvalidGrassmannian | k ≥ n or k = 0 |
CapabilityNotFound | Referenced capability not registered |
PrincipalNotFound | Referenced principal doesn't exist |
AlreadyHolds | Duplicate grant |
DoesNotHold | Revoke on unheld capability |
InvalidPartition | Partition not weakly decreasing |
ImpossibleComposition | Geometric incompatibility |
Underconstrained | Too few conditions |
Overconstrained | Too many conditions |
SerializationError | serde I/O failure |
VerificationError | Karpal proof obligation failed |
Proof-Carrying Tokens
Cryptographic capability tokens using Ed25519 signatures. Enable the crypto feature.
Issuing Tokens
#![allow(unused)] fn main() { use schubert::crypto::{CapabilityIssuer, CapabilityToken}; // Generate a key pair let issuer = CapabilityIssuer::generate(); // Issue a token let token = issuer.issue("alice", "read:data")?; let serialized = token.serialize()?; // send over the wire }
Verifying Tokens
#![allow(unused)] fn main() { use schubert::crypto::CapabilityVerifier; let verifier = CapabilityVerifier::new(issuer.public_key()); // Verify and extract claims verifier.verify(&token)?; let (principal, capability) = verifier.verify_and_extract(&token)?; // Grant in your controller acl.grant(&principal, capability.as_str())?; }
Token Structure
#![allow(unused)] fn main() { pub struct CapabilityToken { pub principal: String, pub capability: String, pub issued_at: u64, pub expires_at: Option<u64>, pub signature: [u8; 64], } }
Security Properties
- Ed25519 signatures: 128-bit security, 64-byte signatures
- Tamper detection: Modified tokens fail signature verification
- Expiry support: Tokens can include an expiration timestamp
- No replay protection: Tokens are stateless — the verifier tracks usage
- Key rotation: Generate new
CapabilityIssuerand distribute public key
WebAssembly
JavaScript bindings for in-browser access control. Enable the wasm feature.
Build
cargo build --target wasm32-unknown-unknown --features wasm
WasmController
import init, { WasmController } from 'schubert';
await init();
const acl = new WasmController(2, 4);
acl.register_capability("read", [1], "ReadLike", "Read data");
acl.register_capability("write", [2], "WriteLike", "Write data");
const alice = acl.create_principal("alice");
acl.grant(alice, "read");
acl.grant(alice, "write");
const decision = acl.check(alice, ["read", "write"]);
// { kind: "Granted", configurations: 1 }
Note:
AuditSinkis not available on wasm32 since it requiresstd.
CapabilityKind Values
| JS String | Variant |
|---|---|
"ReadLike" | Read-like capability |
"WriteLike" | Write-like capability |
"AdminLike" | Admin-like capability |
"Custom" | Custom capability |
Limitations
- Single-threaded (browser context)
- No audit sink (requires std)
- No parallel batch operations (requires rayon/threads)
Verification Integration (Karpal)
Schubert integrates with Karpal (v0.5) for formal verification of access control
properties. Enable the karpal-verify feature.
Architecture
AccessControl ──► verify.rs ──► karpal-verify (SMT/Lean)
│
├── ObligationBundle ──► Proof obligations
├── Certified<T> ──► Trust boundary
└── VerificationResult ──► Pass/Fail/Caveat
Obligation Bundles
Five obligation bundles verify key properties:
| Bundle | Property |
|---|---|
grant_check_consistency | If granted, check must return Granted |
revoke_removes_access | Revoke → check returns Denied or Impossible |
grant_revoke_identity | Grant then revoke = no net change |
composition_associativity | (a ∘ b) ∘ c = a ∘ (b ∘ c) |
impossibility_symmetry | a impossible with b ⇔ b impossible with a |
Certified Trust Boundary
#![allow(unused)] fn main() { use schubert::verify::Certified; // Wrap a value in a proof obligation let certified_decision: Certified<AccessDecision> = verify.check_certified(&acl, &principal, &["read"])?; }
Certified<T> carries a formal proof that T satisfies specified properties.
At the boundary, the proof is discharged or rejected.
Verification Levels
| Level | Backend | Guarantee |
|---|---|---|
QuickCheck | Property testing | Statistical confidence |
SMT | Z3/CVC4 | Symbolic model checking |
Lean | Lean 4 | Full formal proof |
Integration
#![allow(unused)] fn main() { use schubert::verify; use karpal_verify::Verifier; let verifier = Verifier::new(); let obligations = verify::build_obligations(&acl)?; for obligation in &obligations { let result = verifier.verify(obligation)?; match result { verify::VerificationResult::Pass => {}, verify::VerificationResult::Fail(reason) => { eprintln!("Verification failed: {reason}"); }, verify::VerificationResult::Caveat(msg) => { println!("Caveat: {msg}"); }, } } }
Surreal Trust Levels
Exact arithmetic on trust values using Amari's RationalSurreal. Enable the surreal
feature.
Motivation
Floating-point trust (f64) has rounding errors that accumulate in chained trust operations. Surreal numbers provide exact arithmetic with infinitesimal resolution.
RationalSurreal
A surreal number represented as a rational with infinitesimal extensions:
#![allow(unused)] fn main() { use schubert::surreal_trust::SurrealTrust; // Standard trust values let full = SurrealTrust::new(RationalSurreal::from_f64(1.0)); let half = SurrealTrust::new(RationalSurreal::from_f64(0.5)); // Exact comparison assert!(half < full); }
EpsilonPolynomial
Infinitesimal trust resolution using ε (epsilon) and its powers:
#![allow(unused)] fn main() { use schubert::surreal_trust::EpsilonPolynomial; let eps = EpsilonPolynomial::epsilon(); let two_eps = EpsilonPolynomial::epsilon() * 2; let eps_sq = EpsilonPolynomial::epsilon_squared(); // ε² < ε < 1 (infinitesimal ordering) assert!(eps_sq < eps); assert!(eps < 1.0); // Compare infinitesimal trust levels assert!(SurrealTrust::from_epsilon(1) < SurrealTrust::from_epsilon(2)); }
Use Cases
- Exact trust comparison: Arbitrarily close trust levels are distinct
- Infinitesimal recovery: Gradual trust restoration in ε increments
- Provable monotonicity: Trust never spontaneously increases
- Compositional trust: Exact trust arithmetic across service chains
Comparison
EpsilonPolynomial lacks PartialOrd — use compare_infinitesimal():
#![allow(unused)] fn main() { use schubert::surreal_trust::compare_infinitesimal; let a = EpsilonPolynomial::epsilon_squared(); let b = EpsilonPolynomial::epsilon(); assert_eq!(compare_infinitesimal(&a, &b), std::cmp::Ordering::Less); }
Comparison first checks valuations (degree of smallest ε term), then compares coefficients.
Holographic Memory (Minuet)
Cosine-similarity-based access patterns via Minuet (v0.3). Enable the holographic
feature.
HolographicAccessControl
#![allow(unused)] fn main() { use schubert::holographic::HolographicAccessControl; let mut holo = HolographicAccessControl::new(); // Encode a principal's access pattern holo.encode("alice", &["read", "write"])?; holo.encode("bob", &["read"])?; // Query by similarity let similar = holo.query_similar(&["read", "write"], 5)?; // Returns principals with similar access patterns, ranked by cosine similarity }
How It Works
- Access patterns are encoded as vectors via FNV hash
- Cosine similarity measures how close two access patterns are
- Schubert intersection provides geometric validation of similarity
- Results are ranked by combined similarity + intersection score
Use Cases
- Anomaly detection: Flag access patterns unlike any known principal
- Role discovery: Cluster principals by access pattern similarity
- Privilege escalation detection: Sudden change in access pattern vector
- Audit forensics: Find principals with similar access to a known attacker
Query
#![allow(unused)] fn main() { // Find top-K similar principals let results = holo.query_similar(&["read:data"], 10)?; for (principal, similarity) in results { println!("{principal}: {similarity:.4}"); } }
Limitations
- Cosine similarity is approximate, not exact match
- Encoding uses FNV hash (fast but not cryptographic)
- Not a full Minuet algebra binding — simplified for access control
- Memory-only storage (no persistence)
Security Considerations
Identity Model
Schubert never authenticates. PrincipalId is an opaque string provided by your
external identity system (OAuth, OIDC, JWT, mTLS). Schubert authorizes based on
identities you provide.
You are responsible for:
- Authenticating users
- Mapping authenticated identities to
PrincipalIdvalues - Ensuring identity consistency across service boundaries
Capability Design
- Partition collisions: Two capabilities with the same partition are geometrically equivalent. Design partitions to be distinct.
- Over-granting: Granting too many capabilities may create impossible
combinations. Use
analyze_stability()to detect this. - Admin capabilities:
[2,2](point class) is maximally restrictive. Grant admin capabilities sparingly.
Trust Boundaries
Certified<T>: A formal proof boundary. Values crossing this boundary have been verified by Karpal. Rejection means the proof obligation failed.AuditSink: Audit records are best-effort. A failing sink does not block access decisions.CapabilityToken: Ed25519 signatures provide integrity and authenticity but not confidentiality. Token contents are plaintext.
Known Limitations
- No built-in persistence: Policy state is in-memory. Use
serde+ your own storage for durability. - No network protocol: Schubert is purely a library. Implement wire protocols yourself.
- Cosine similarity: Holographic queries use approximate similarity, not exact access matching.
- No revocation propagation: CRDT revocations are eventually consistent, not immediately globally visible.
Cryptographic Tokens
- Key management:
CapabilityIssuergenerates keys. Store and rotate keys according to your security policy. - No replay protection: Tokens are stateless. Track used tokens in the verifier if replay is a concern.
- Signature verification: Always verify before extracting claims. Never trust unverified token contents.
Performance Considerations
- Grassmannian scaling: Larger Grassmannians (Gr(3,6), Gr(4,8)) have higher computational cost for intersection calculations.
- Batch operations: Use
check_batch()with theparallelfeature for high-throughput scenarios. - Tropical path: Switch to
ComputationPath::Tropicalfor >1000 concurrent principals.
Critique & Future Work
v0.1.0 Snapshot — This is an honest assessment of the project at its initial public release. Several points have been addressed since the original critique; we track the remainder as future work.
Addressed Since Original Critique
| Concern | Resolution |
|---|---|
| No CLI tooling | ✅ CLI with discover, recommend, explore subcommands |
| AGPL-only licensing | ✅ Dual-licensed AGPL-3.0 + commercial |
| Sparse documentation | ✅ User guide, book, API reference, cookbook |
| Feature-flag complexity | ✅ Feature flag guide with common combinations |
Ongoing Challenges
Learning Curve
Schubert calculus is not standard security engineering knowledge. We recommend:
- Start with Gr(2,4) — the standard RBAC space
- Use the Getting Started walkthrough
- Use
schubert discoverto explore the API surface - Read Mathematical Foundation for the geometry
Performance Benchmarks
No published benchmarks yet. We expect:
- Single check: <1ms for Gr(2,4)
- Batch check (100 principals): <10ms with
parallelfeature - Tropical path: linear scaling in number of conditions
Benchmarks are tracked for a future release.
Persistence
No built-in storage layer. All state is in-memory. Use serde serialization +
your database of choice for persistence.
Real-World Adoption
Schubert is a new project. We welcome:
- Production deployment reports
- Bug reports and edge cases
- Integration examples with common stacks (PostgreSQL, Redis, Kubernetes)
Future Directions
See the full Roadmap for speculative directions including:
- Persistent backends (SQLite, PostgreSQL)
- gRPC policy distribution protocol
- Policy diff and incremental updates
- Visualization of Schubert varieties
- Integration with OpenFGA / Rego policy languages
Roadmap & History
v0.1.0 Snapshot — All 14 core roadmap items are complete. Speculative directions are explorations for the research community, not commitments.
v0.1.0 — Foundation Complete
Core Infrastructure
- AccessController with principal management, capability registry, grant/revoke
- Quantitative AccessDecision (Granted{n}, Impossible, Denied, Underconstrained)
- 4 computation paths: LR, Localization, Tropical, Matroid
- Operadic composition, stability analysis, pluggable audit sinks
Feature-Gated Modules
serde— Serialization, JSON I/O, roundtrippolicy— TOML policy language with validationwasm— WasmController with JS bindingscrypto— Ed25519 capability tokenskarpal— Type-level proofs (Proven, Rewrite, law checks)karpal-verify— SMT/Lean proof obligations, Certified trust boundarysurreal— RationalSurreal + EpsilonPolynomial trust arithmeticholographic— Minuet cosine-similarity access patterns
Advanced Features
- Context-aware decisions (resource scoping, time-aware trust)
- MultiController with cross-domain capability translation
- Temporal access control (expiry, time-remaining)
- Rate limiting scaled by intersection numbers
- Schubert routing with geometric path computation
- Distributed CRDTs with version vectors
Quality
- 128 unit tests + 18 CLI tests = 146 total
- Zero clippy warnings (all feature combinations)
- 7 example programs
- CI/CD: fmt, clippy, test matrix (5 combos), docs, wasm build, verification
Speculative Directions
These are research explorations, not commitments:
- Persistent backends — SQLite, PostgreSQL, Redis storage layers
- gRPC policy distribution — Wire protocol for multi-node policy sync
- Policy diff engine — Incremental policy updates with minimal recomputation
- Visualization — SVG/WebGL rendering of Schubert varieties
- OpenFGA/Rego bridge — Translation between Schubert policies and standard DSLs
- Holographic persistence — Full Minuet store integration with cosine indexing
- Async runtime — tokio-based async AccessController
- Policy fuzzing — Automated discovery of impossible capability combinations
- Benchmark suite — Standardized workloads with published results
- WASM Component Model — WIT-based interface definitions
Role-Based Access Control (RBAC)
Traditional RBAC with quantitative access decisions.
Source: examples/rbac.rs
Setup
use schubert::{AccessController, Capability, CapabilityKind, AccessDecision}; fn main() -> Result<(), Box<dyn std::error::Error>> { let mut acl = AccessController::new(2, 4)?;
Capabilities
#![allow(unused)] fn main() { acl.register_capability(Capability::new( "read", "Read access", vec![1], CapabilityKind::ReadLike, ))?; acl.register_capability(Capability::new( "write", "Write access", vec![2], CapabilityKind::WriteLike, ))?; acl.register_capability(Capability::new( "admin", "Admin access", vec![2, 1], CapabilityKind::AdminLike, ))?; }
Principals and Grants
#![allow(unused)] fn main() { let alice = acl.create_principal("alice")?; acl.grant(&alice, "read")?; acl.grant(&alice, "write")?; let bob = acl.create_principal("bob")?; acl.grant(&bob, "read")?; }
Access Checks
#![allow(unused)] fn main() { // Alice can read and write match acl.check(&alice, &["read", "write"])? { AccessDecision::Granted { configurations } => { println!("Alice: granted with {configurations} configurations"); } _ => unreachable!(), } // Bob cannot write match acl.check(&bob, &["read", "write"])? { AccessDecision::Granted { .. } => unreachable!(), _ => println!("Bob: cannot read and write"), } Ok(()) } }
Key Takeaway
RBAC is the simplest pattern — define roles as capability sets, grant them to principals, and check. The quantitative nature of Schubert shines when roles overlap or conflict.
Row-Level Security
Tenant-scoped capabilities for database row-level security.
Source: examples/row_security.rs
Pattern
Create tenant-specific capabilities and check cross-tenant access for geometric impossibility detection:
#![allow(unused)] fn main() { // Tenant-scoped capabilities acl.register_capability(Capability::new( "read:tenant_a", "Read tenant A", vec![1], ReadLike, ))?; acl.register_capability(Capability::new( "read:tenant_b", "Read tenant B", vec![1], ReadLike, ))?; acl.register_capability(Capability::new( "read:tenant_c", "Read tenant C", vec![1], ReadLike, ))?; // Multi-tenant principal acl.grant(&principal, "read:tenant_a")?; acl.grant(&principal, "read:tenant_b")?; // Three tenant reads in Gr(2,4) — too many conditions let result = acl.check(&principal, &[ "read:tenant_a", "read:tenant_b", "read:tenant_c", ])?; // AccessDecision::Denied (overconstrained) }
Key Takeaway
Cross-tenant access patterns that a boolean system would approve are caught as overconstrained by Schubert's geometric analysis.
API Gateway
Pattern for an API gateway using Schubert for authorization.
Source: examples/api_gateway.rs
Pattern
The API gateway authenticates (external) and uses Schubert to authorize:
#![allow(unused)] fn main() { fn handle_request( acl: &AccessController, token: &str, endpoint: &str, ) -> Result<bool> { // 1. Authenticate (external — JWT, OAuth, etc.) let principal = authenticate(token)?; // 2. Map endpoint to capabilities let required = match endpoint { "/api/data" => &["read:data"], "/api/admin" => &["admin"], _ => return Ok(false), }; // 3. Authorize via Schubert match acl.check(&principal, required)? { AccessDecision::Granted { .. } => Ok(true), _ => Ok(false), } } }
Key Takeaway
Schubert is a library, not a network service. Embed it in your gateway, middleware, or sidecar — Schubert handles authorization, your infrastructure handles authentication and transport.
Cross-Domain Access
Capability translation between Grassmannians.
Source: examples/cross_domain.rs
Pattern
#![allow(unused)] fn main() { let mut mc = MultiController::new(); // Two domains with different policy spaces let rbac = mc.add_domain_named(2, 4, "rbac")?; // dim 4 let tenant = mc.add_domain_named(3, 6, "multi-tenant")?; // dim 9 let alice = mc.create_principal("alice", &rbac)?; mc.grant_in_domain(&alice, "read", &rbac)?; mc.grant_in_domain(&alice, "write", &rbac)?; // Check if RBAC capabilities translate to tenant domain let result = mc.check_cross_domain( &alice, &["read", "write"], &rbac, &tenant )?; }
Key Takeaway
Capabilities aren't globally meaningful — they live in a specific Grassmannian.
check_cross_domain() uses Schubert intersection to determine if a capability
set in one domain is valid in another.
Context-Aware Decisions
Resource-scoped, time-aware, trust-gated access checks.
Source: examples/context_aware.rs
Pattern
#![allow(unused)] fn main() { use schubert::AccessContext; // Time-critical operation with high trust requirement let ctx = AccessContext { resource: Some("/api/critical".into()), time_budget_ms: Some(100), required_trust: 0.95, }; let result = acl.check_with_context(&alice, &["admin"], &ctx)?; // Low-trust read with generous time budget let ctx = AccessContext { resource: Some("/api/public".into()), time_budget_ms: Some(5000), required_trust: 0.3, }; let result = acl.check_with_context(&alice, &["read"], &ctx)?; }
Key Takeaway
Not all access checks are equal. High-trust, time-critical operations should
be more restrictive — AccessContext captures all three dimensions
(resource, time, trust) in one struct.
Policy Loader
Loading an access controller from a TOML policy file.
Source: examples/policy_loader.rs
Pattern
#![allow(unused)] fn main() { let toml_str = std::fs::read_to_string("policy.toml")?; let acl = AccessController::from_policy_toml(&toml_str)?; // Use the loaded controller let alice = acl.get_principal("alice")?; let result = acl.check(&alice, &["read"])?; // Export current state let exported = acl.to_policy_toml()?; std::fs::write("exported.toml", exported)?; }
Key Takeaway
Policies-as-code enable version control, code review, and CI validation of access control configurations.
Rate Limiter
Token-bucket rate limiting scaled by Schubert intersection numbers.
Source: examples/rate_limiter.rs
Pattern
#![allow(unused)] fn main() { use schubert::RateLimiter; let mut rl = RateLimiter::new(10.0, 1.0); // 10 tokens/sec // Check access first let granted = acl.check(&alice, &["read", "write"])?; // Scale rate limiter by intersection number rl.configure_from_decision("alice", &granted)?; // Per-request check for _ in 0..100 { if rl.try_consume("alice").is_err() { println!("Rate limit reached"); break; } } }
Key Takeaway
Higher-trust principals (higher intersection numbers) get proportionally more throughput because the rate limiter is scaled by the access decision.