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 GrassmannianMultiController manages cross-domain access

The Industrial Algebra Ecosystem

Schubert depends on three sibling projects:

CrateVersionRole
Amari0.23Schubert calculus engine — Grassmannians, intersection numbers
Karpal0.5Formal verification — type-level proofs, SMT/Lean obligations
Minuet0.3Holographic 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

DecisionMeaning
Granted { configurations: n }Access allowed in exactly n ways
Impossible { conflicting }Conditions are geometrically incompatible
DeniedToo 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)4Standard RBAC (recommended starting point)
Gr(3,6)9Complex multi-tenant policies
Gr(4,8)16Enterprise-scale policy space

Larger Grassmannians support more distinct capabilities but have higher computational cost.

Next Steps

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)DimensionMax Independent Conditions
Gr(2,4)44
Gr(3,6)99
Gr(4,8)1616

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:

PartitionCodimensionTypical Use
[1]1Read access
[2]2Write access
[1,1]2Read + audit
[2,1]3Manage
[2,2]4Admin (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

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:

KindTrust SensitivityExamples
ReadLikeLow — stable even at low trustRead, list, view
WriteLikeMedium — degrades at moderate trustWrite, update, delete
AdminLikeHigh — degrades rapidly under trust lossAdmin, manage, configure
CustomApplication-definedAny 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:

PathWhen to use
LRDefault — balanced performance, exact results
LocalizationWhen you need geometric insight into why
TropicalLarge-scale batch operations (>1000 principals)
MatroidWhen 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

CapabilityKindDegradation Pattern
ReadLikeDegrades below ~0.3 trust
WriteLikeDegrades below ~0.5 trust
AdminLikeDegrades 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 ∘ b is impossible, b ∘ a is 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"] }
FeatureEnables
std (default)HashMap, SystemTime, thread-safe audit
serdeSerialization on all types, JSON I/O
karpalType-level proofs (Proven, Rewrite)
parallelBatch operations via rayon
policyTOML policy language
wasmWebAssembly JS bindings
cryptoEd25519 capability tokens
karpal-verifyFormal verification (SMT/Lean)
surrealExact surreal trust arithmetic
holographicMinuet holographic memory

All features compose — enable any combination.

no_std Support

[dependencies]
schubert = { version = "0.1", default-features = false }

When std is disabled: HashMapBTreeMap, 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

FeatureWhat It Enables
std (default)HashMap, SystemTime, thread-safe Mutex audit
serdeSerialize/Deserialize on all types, JSON I/O
karpalproof module: Proven, Property, Rewrite, law checks
parallelcheck_batch(), stability_batch(), compose_batch() via rayon
policypolicy module: TOML parsing, validate, roundtrip
wasmwasm module: WasmController with JS bindings
cryptocrypto module: Ed25519 CapabilityToken, Issuer, Verifier
karpal-verifyverify module: SMT/Lean proof obligations, Certified trust boundary
surrealsurreal_trust module: RationalSurreal + EpsilonPolynomial
holographicholographic 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:

  1. Add to [features] in Cargo.toml
  2. Use #[cfg(feature = "my-feature")] on module declarations
  3. Use #[cfg(feature = "my-feature")] on impl blocks and functions
  4. 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

FieldTypeDescription
id&strUnique capability identifier
label&strHuman-readable description
partitionVec<usize>Schubert partition (weakly decreasing)
kindCapabilityKindSemantic category affecting trust sensitivity
expires_atOption<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

ConditionDecision
Intersection number > 0Granted { configurations }
Intersection number = 0Impossible { conflicting }
Total codimension > Gr dimensionDenied
Total codimension < Gr dimensionUnderconstrained { 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: multiplicity configurations 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

  1. For each granted capability, compute its stability as a function of trust
  2. Higher-codimension capabilities cross stability walls at higher trust levels
  3. AdminLike capabilities cross first, ReadLike last
  4. 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: AuditSink requires the std feature
  • No_std: InMemoryAudit uses RefCell, no thread safety

SchubertError

All errors use SchubertError with 11 variants:

VariantWhen
InvalidGrassmanniank ≥ n or k = 0
CapabilityNotFoundReferenced capability not registered
PrincipalNotFoundReferenced principal doesn't exist
AlreadyHoldsDuplicate grant
DoesNotHoldRevoke on unheld capability
InvalidPartitionPartition not weakly decreasing
ImpossibleCompositionGeometric incompatibility
UnderconstrainedToo few conditions
OverconstrainedToo many conditions
SerializationErrorserde I/O failure
VerificationErrorKarpal 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 CapabilityIssuer and 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: AuditSink is not available on wasm32 since it requires std.

CapabilityKind Values

JS StringVariant
"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:

BundleProperty
grant_check_consistencyIf granted, check must return Granted
revoke_removes_accessRevoke → check returns Denied or Impossible
grant_revoke_identityGrant then revoke = no net change
composition_associativity(a ∘ b) ∘ c = a ∘ (b ∘ c)
impossibility_symmetrya 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

LevelBackendGuarantee
QuickCheckProperty testingStatistical confidence
SMTZ3/CVC4Symbolic model checking
LeanLean 4Full 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

  1. Access patterns are encoded as vectors via FNV hash
  2. Cosine similarity measures how close two access patterns are
  3. Schubert intersection provides geometric validation of similarity
  4. 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 PrincipalId values
  • 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: CapabilityIssuer generates 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 the parallel feature for high-throughput scenarios.
  • Tropical path: Switch to ComputationPath::Tropical for >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

ConcernResolution
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:

Performance Benchmarks

No published benchmarks yet. We expect:

  • Single check: <1ms for Gr(2,4)
  • Batch check (100 principals): <10ms with parallel feature
  • 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, roundtrip
  • policy — TOML policy language with validation
  • wasm — WasmController with JS bindings
  • crypto — Ed25519 capability tokens
  • karpal — Type-level proofs (Proven, Rewrite, law checks)
  • karpal-verify — SMT/Lean proof obligations, Certified trust boundary
  • surreal — RationalSurreal + EpsilonPolynomial trust arithmetic
  • holographic — 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:

  1. Persistent backends — SQLite, PostgreSQL, Redis storage layers
  2. gRPC policy distribution — Wire protocol for multi-node policy sync
  3. Policy diff engine — Incremental policy updates with minimal recomputation
  4. Visualization — SVG/WebGL rendering of Schubert varieties
  5. OpenFGA/Rego bridge — Translation between Schubert policies and standard DSLs
  6. Holographic persistence — Full Minuet store integration with cosine indexing
  7. Async runtime — tokio-based async AccessController
  8. Policy fuzzing — Automated discovery of impossible capability combinations
  9. Benchmark suite — Standardized workloads with published results
  10. 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.