Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Capability System

The capability system is Crush’s security model. All interactions with the outside world (I/O, filesystem, network) require explicit capability permissions.

What are Capabilities?

Capabilities are permissions that grant access to external resources:

  • io.print - Print to stdout
  • io.read - Read from stdin
  • fs.read - Read files
  • fs.write - Write files
  • net.http - Make HTTP requests
  • sys.exec - Execute commands

Capability Calls

Use the @ prefix to call capabilities:

io.print("Hello, World!");

With Arguments

fs.write("file.txt", "content");
let content = fs.read("file.txt");

Storing Results

let user_input = io.read();
let file_data = fs.read("config.json");

Declaring Permissions

Capabilities must be declared in the program manifest:

{
  "manifest": {
    "permissions": {
      "io.print": true,
      "io.read": true,
      "fs.read": ["./data", "./config"],
      "fs.write": ["./output"],
      "net.connect": ["https://api.example.com"]
    }
  }
}

Permission Scoping:

  • true - Full access to that capability
  • [paths] - Restricted to specific paths or URLs
  • Omitted - Denied (default deny-by-default)

Without the permission, the capability call will fail at runtime.

Standard Capabilities

Host-provided, not bundled. crush-vm registers io.print, io.eprint, and io.read as built-in primitives. All other capabilities listed here (fs.*, net.*, sys.*) are host-provided — they must be registered by the embedding host process. The bare crush-ast crates define the capability interface; the implementation comes from the host (e.g. exosphere’s corecaps, or a custom host registration). If a cap is not registered, the call fails at runtime regardless of what the manifest declares.

I/O Capabilities

CapabilityDescriptionArgumentsReturnsScope
io.printPrint to stdoutmessage: StringnullN/A
io.readRead from stdinnoneStringN/A
io.eprintPrint to stderrmessage: StringnullN/A

Example:

io.print("Enter your name:");
let name = io.read();
io.print("Hello, " + name);

Filesystem Capabilities

CapabilityDescriptionArgumentsReturnsScope
fs.readRead filepath: StringStringPath list
fs.writeWrite filepath: String, content: StringnullPath list
fs.existsCheck if existspath: StringBoolPath list
fs.deleteDelete filepath: StringnullPath list
fs.listList directorypath: StringArray<String>Path list

Example:

if fs.exists("config.json") {
    let config = fs.read("config.json");
    io.print(config);
} else {
    fs.write("config.json", "{}");
}

System Capabilities

CapabilityDescriptionArgumentsReturns
sys.execExecute commandcmd: StringString
sys.envGet env variablename: StringString
sys.argsGet CLI argsnoneArray<String>
sys.exitExit programcode: Intnever

Example:

let args = sys.args();
if args.length < 2 {
    io.eprint("Usage: program <file>");
    sys.exit(1);
}

Network Capabilities

CapabilityDescriptionArgumentsReturns
net.httpHTTP requesturl: StringString
net.getHTTP GETurl: StringString
net.postHTTP POSTurl: String, body: StringString

Example:

let response = net.get("https://api.example.com/data");
io.print(response);

Security Model

Principle of Least Privilege

Only request capabilities you actually use:

// Good: Minimal permissions
{
  "permissions": ["io.print"]
}

// Bad: Excessive permissions
{
  "permissions": ["io.print", "fs.read", "fs.write", "net.http", "sys.exec"]
}

No Ambient Authority

Unlike traditional OS permissions, capabilities are:

  • Explicit: Must be declared in manifest
  • Granular: fs.read vs fs.write vs fs.delete
  • Auditable: Easy to see what a program can do

Capability Denial

If a program calls a capability it doesn’t have:

// Manifest: {"permissions": ["io.print"]}

fn main() {
    io.print("Hello");  // ✓ Allowed
    fs.read("file.txt");  // ✗ Runtime error: Permission denied
}

Best Practices

1. Declare Only What You Need

// Good
{
  "permissions": ["io.print", "fs.read"]
}

// Avoid
{
  "permissions": ["io.*", "fs.*", "net.*"]
}

2. Check Before Using

if fs.exists("config.json") {
    let config = fs.read("config.json");
} else {
    io.print("Config not found");
}

3. Handle Errors

// Future: try-catch
try {
    let data = fs.read("file.txt");
} catch (error) {
    io.eprint("Failed to read file: " + error);
}

Capability Composition

Capabilities can be composed:

fn read_and_print(filename: String) {
    let content = fs.read(filename);
    io.print(content);
}

// Requires both fs.read and io.print

Next Steps

What is actually implemented today

The chapters above describe the capability model. This section records, separately, which capabilities the runtime currently ships — because the two have drifted, and a guide that documents a capability the runtime does not have is worse than one that stays silent.

Verified by running each call against crush-run (built with --features stdlib):

Available

capabilitynotes
io.printbuilt-in, always available
str.concat, str.lenbuilt-in
str.trim, str.to_upper, str.substringrequire --stdlib
conv.to_intrequires --stdlib
math.*requires --stdlib
fs.read, fs.write, fs.exists, fs.listrequire --fs
env.getrequires --env
time.nowrequires --time

Run crush-run caps for the authoritative list on your build — and note that capability groups behind a Cargo feature (--stdlib, --net, --db, --graphics) will warn if that feature was not compiled in. A “not enabled in this build” warning is not the same as “does not exist.”

Not implemented — documented in this guide but NOT in the runtime

io.eprint · io.read · fs.delete · sys.exit · sys.args · sys.env · sys.exec · type.of · console.print · array.push · array.pop · array.length · map.keys · map.has_key

Examples using these will not run. They are retained here as intent, not as documentation of behaviour. If you hit one, that is a gap in the runtime, not a mistake in your code.

A note on syntax: what @ means in Crush

Capability calls are written unprefixed: io.print("hi"), not @io.print("hi"). Earlier revisions of this guide used an @ sigil; the parser has never accepted it in expression position, and every example here has been corrected.

That correction is narrow, and it is important not to over-generalise it. @ is a real and load-bearing sigil in Crush — it just does not introduce a capability call. It is overloaded across several distinct constructs, and stripping it blindly will corrupt them:

formexamplewhat it is
polyglot block@python { ... }, @javascript { ... }embed another language; the @ is required
compiler directive@gpu, @kernel, @targetsteer the backend (e.g. the PTX/GPU path)
AST / AI annotation@invariant, @decision, @covers, @writes, @synthesizetyped metadata attached to CAST nodes
capability call@io.print(...) → io.print(...)no sigil. This is the one that was wrong.

The lexer emits a single AtIdent token for all of these; what a given @name means is decided by the parser from context, not by the sigil.

So: if you are writing a tool that rewrites Crush source, do not treat @ as a single construct. Note in particular that some annotations carry a dot (@wip.started_by), so a “strip @ from anything shaped like @x.y” rule will silently eat them.