Capabilities as Typed Middleware

Functions are pure by default. To do I/O, you declare the capabilities you need. The compiler verifies you don't use anything else. Each capability is middleware with a policy. Policies are M-of-N governed. The type system IS the sandbox.

The three tiers of effects

-E

Pure. No I/O at all. Memoizable, parallelizable, replayable. The default for every function.

+E(caps)

Controlled I/O through named capabilities only. The compiler rejects any I/O outside the declared capabilities. Each capability has a policy.

+E

Unrestricted I/O. Reserved for the executor — runs only after M-of-N policy approval. Never given to untrusted code.

What a sandboxed function looks like

// This function can read users and send email. Nothing else. // The whitelist IS the type — StreamRead has no .delete(). f+E(db, email) handle_order( order: Order, db: StreamRead, email: EmailCap ) -> [Action] customer = db.get(order.customer_id) // OK — StreamRead has .get() // db.delete("users:1") ← COMPILE ERROR: no .delete() // File.write("x") ← COMPILE ERROR: not in +E(db, email) r => [Action({ type: "charge", amount: order.total })]

The +E(users, email) annotation tells the compiler: every +E call in this body must trace back to users or email. No ambient File, Network, or Process access. The function reads and processes data freely through its capabilities — it can await reads, think, read more — but it cannot reach outside the declared aperture.

Capabilities are middleware

A capability wraps an effectful operation with a policy. Every invocation goes through the policy first. Capabilities compose — add logging, rate limiting, or validation by wrapping:

d Capability name: S -M governance: Governance -M d StreamRead : Capability f+E get(key: S) -> Tree +N ! PolicyDenied f+E list(prefix: S) -> [S] ! PolicyDenied // No put(). No delete(). The whitelist IS the type. // Hooks attach via .on() with options: db.on(.get, rate_limiter, { timing: "before", governance: sec_gov }) db.on(.get, audit_logger, { timing: "after", async: true }) db.on(.*, error_counter, { timing: "error", batch: 5 })

Policies are M-of-N governed

Nobody — not the tool, not the developer, not a single admin — can change a policy unilaterally. Updating a policy requires M-of-N cryptographic signatures from authorized auditors.

d Policy rules: [Rule] -M required_signatures: I -M = 2 // M-of-N signers: [PublicKey] -M /// Update requires M-of-N signatures f+E update(new_rules: [Rule], sigs: [Signature]) -> none ! InsufficientSignatures valid = sigs.filter(sig => t.signers.contains(sig.key)) valid.len < t.required_signatures ? x InsufficientSignatures() t.rules = new_rules none

The Safebox execution cycle

// 1. SANDBOX — reads through capabilities, proposes writes f+E(db) process(input: S, db: Capability[Query, [Row]]) -> [Action] customer = db.invoke(Query({ table: "users", id: input })) r => [Action({ type: "charge", amount: customer.balance })] // 2. POLICY — pure, checks M-of-N signatures f-E approve(actions: [Action], sigs: [Signature], policy: Policy) -> L valid = sigs.filter(sig => policy.signers.contains(sig.key)) r => valid.len >= policy.required_signatures // 3. EXECUTOR — unrestricted, runs only after approval f+E execute(actions: [Action]) -> none actions.on(action => Payments.charge(action.data)) none

The sandbox function reads freely through its capabilities but can only propose writes. The policy function is pure — it just checks signatures. The executor is fully effectful but runs only after M-of-N approval. If the sandbox throws, MVCC rollback — proposed actions revert. Clean.

Why this beats runtime sandboxes

JavaScript sandboxes (DeterministicJS) and WASM memory isolation work at runtime — they can't tell you WHAT the code will do until it runs. U's +E(capabilities) is a compile-time guarantee: the compiler proves the function only uses its declared capabilities before the code ever executes. You know the attack surface from the type signature, not from running the code in a cage.

This is how Safebots works. The modifier system replaces DeterministicJS, the WASM sandbox, the capability injection framework, the write-intent queue, and the policy enforcement layer — with three modifier annotations. The type system IS the sandbox. Read the full Safebots story.