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

Capabilities

A capability is an explicit permission for a workflow step to perform a side effect. No capability, no side effect — the VM enforces this unconditionally.

The eleven capabilities

The 1.0 capability set is frozen in crates/llmbc/src/capability.rs::Capability::ALL. A capability_set_hash, derived from the (name, version) tuples of this set, gives the capability surface a stable identity — reported by the boruna_capability_list MCP tool for compatibility checks.

CapabilityEffectExample use
net.fetchHTTP requestsCalling external APIs, webhooks
llm.callLLM inferenceGPT-4, Claude, local models (BYOH — see guides/llm-integration.md)
time.nowCurrent timestampTimestamping records
randomRandom numbersSampling, tie-breaking
fs.readFile system readsLoading documents, configs
fs.writeFile system writesWriting reports, outputs
db.queryDatabase accessReading/writing records
ui.renderUI surface renderingFramework view output (Elm-architecture apps)
actor.spawnActor creationSpawning parallel agents
actor.sendInter-actor messagingCoordinating actor state
step.inputRead a workflow step’s resolved inputsPer-step step_input("upstream_step") builtin

Declaring capabilities

Capabilities are declared on functions using the !{...} annotation:

fn call_model(prompt: String) -> String !{llm.call} {
    // live mode: calls LLM
}

fn fetch_and_parse(url: String) -> String !{net.fetch} {
    // live mode: makes HTTP request
}

A function without a capability annotation is pure — it cannot perform I/O and its output depends only on its inputs.

Policies

A policy is a set of allowed capabilities. It is specified at runtime, not in the workflow definition. This separation means the same workflow can run in restricted mode during testing and with full capabilities in production.

Built-in policies:

PolicyAllowed capabilities
allow-allAll 11 capabilities
deny-allNone
defaultNone (same as deny-all)

Pass a policy on the CLI:

boruna workflow run my-workflow/ --policy allow-all

In demo mode (no --live flag), capability calls are stubbed or skipped. In live mode, the policy is enforced against every capability call.

Capability enforcement in the VM

Every capability call in compiled bytecode goes through the CapabilityGateway:

  1. The VM encounters a capability opcode.
  2. The gateway checks the active policy.
  3. If the capability is not allowed, the VM returns a CapabilityDenied error immediately.
  4. If allowed, the call is dispatched to the registered handler (real HTTP, LLM client, etc.).
  5. The call and its result are written to the EventLog.

This enforcement happens at the bytecode level, before any handler executes. There is no way to bypass it from .ax code.

Capabilities in workflow definitions

Workflow steps declare their required capabilities in workflow.json. This makes the capability surface area visible before execution:

{
  "steps": [
    {
      "id": "analyze",
      "source": "steps/analyze.ax",
      "capabilities": ["llm.call"]
    }
  ]
}

The workflow validator checks that declared capabilities are consistent with the policy before the workflow runs.

Capabilities in package manifests

Standard libraries that require capabilities declare them in package.ax.json:

{
  "name": "std-http",
  "capabilities_required": ["net.fetch"]
}

This gives visibility into the transitive capability requirements of a dependency graph.

See also: Determinism, Policies