Xo is experimental. The source code is not public yet; until then, try Xo in your browser.
Menu · Dependencies and capabilities

Docs

Dependencies and capabilities

A dependency in Xo can only do what you let it. This page shows the tools, in the order you are likely to use them, with the output the toolchain prints. Every command and message below was run with the current toolchain; only the long file paths are shortened.

The running example is a shop that depends on a billing library, github.com/acme/billing.

Capabilities: authority starts at main

A program gets its authority as values: main(os: Os) receives the network, files, clock, environment, and processes, and a library can use one only if it is handed it. There is no global open or dial.

pub fn sync(net: Net) -> Bool uses net.dial {
  match net.dial("metrics.example.net:443") {
    Ok(_) => true
    Err(_) => false
  }
}

Every pub function declares its effects (uses net.dial) and the compiler checks the body against them, so a library’s signature tells you what it can do. A function with no uses clause is pure. Writing a log record is the log effect, and every call into Go is the go effect.

Effect ceilings in xo.mod

A uses clause on a require line bounds what the dependency may do:

module example.com/shop
edition preview

require github.com/acme/billing v1.2.0 uses net.dial

uses none admits nothing, uses net admits net.dial and net.listen. A call into the dependency whose function declares more is an error at the call:

$ xo check .
main/main.xo:4:20: error XO1309: `region` of github.com/acme/billing v1.2.0 performs `go`, above the effect ceiling `uses net.dial` of that dependency in xo.mod
  note: xo.mod:4:1: the ceiling is declared here
  note: review why github.com/acme/billing needs `go`; then raise the ceiling, or require a version within it (decision 0086)

Ceilings are opt-in. A require line without uses has none, which is where the effect lock comes in.

The effect lock in xo.lock

xo get and xo mod tidy record, next to each module’s hashes, the effects its exported API can reach:

github.com/acme/billing v1.0.0 h1:gtG1lIS42/HaOPBMfYVCNbdaHfqlfYaS65G16dWPVAw=
github.com/acme/billing v1.0.0/xo.mod h1:auD5srgbLtmoFQa9b8RUSQb31JWBxoGs3RQZoaW4FqE=
github.com/acme/billing effects none

When an update reaches further, the update fails and nothing is written:

$ xo get github.com/acme/billing@v1.1.0
xo.lock:6:1: error XO1317: github.com/acme/billing v1.1.0 reaches effects that xo.lock does not record for it: `net.dial` (locked: none, now: net.dial)
  note: the API of the selected version declares effects the version locked before did not; review why it needs them (decision 0156)
  note: to accept them: `xo mod accept-effects github.com/acme/billing` (or `xo get --accept-effects github.com/acme/billing@v1.1.0`); otherwise stay on the previous version

This needs no setup: the lock is on for every module xo get records. After reviewing the new version, accept it, and xo get prints what changed in its API:

$ xo get --accept-effects github.com/acme/billing@v1.1.0
github.com/acme/billing v1.0.0 => v1.1.0
  github.com/acme/billing v1.0.0 => v1.1.0:
    added fn github.com/acme/billing.sync

xo check and xo build never widen the lock; only xo mod accept-effects and xo get --accept-effects do. On the publishing side, xo publish rejects a minor or patch release that adds an effect to an export (XO1310).

Vouches for Go and C code

Go code (use go of a package that is not pure) and C code (use c) can reach files and the network without a capability, so the checker cannot see what they do. In a dependency, each of these escape hatches needs a vouch line in your xo.mod, for the exact version you reviewed. xo get refuses an update that brings an escape hatch you have not vouched for, before it writes xo.mod or xo.lock:

$ xo get --accept-effects github.com/acme/billing@v1.2.0
cache/github.com/acme/billing@v1.2.0/billing.xo:1:1: error XO1316: `use go "os"` of github.com/acme/billing v1.2.0 is not vouched for by the consuming module: Go code reaches files, the network, and processes without a capability
  note: after reviewing the code, add to xo.mod: vouch github.com/acme/billing v1.2.0 go "os"

--accept-effects accepts new effects only; it never stands in for a vouch. After reviewing the code, add the line and run xo get again:

require github.com/acme/billing v1.1.0

vouch github.com/acme/billing v1.2.0 go "os"
$ xo get --accept-effects github.com/acme/billing@v1.2.0
github.com/acme/billing v1.1.0 => v1.2.0
  github.com/acme/billing v1.1.0 => v1.2.0:
    added fn github.com/acme/billing.region

A dependency’s own vouch lines are ignored: it cannot vouch for itself. A C binding can carry effects in the vouch (vouch <module> <version> c "zlib.h" uses none); an unvouched C binding is charged ffi, whatever its own uses says. There is no automatic fix for XO1316: a vouch is a human review, and an upgrade needs a new one. xo check and the build report it too, for example after xo mod tidy, which records versions without checking vouches.

Narrowed capabilities

You can hand a library less than the whole capability. A narrowed capability has the same type, and anything outside it fails with Denied before any effect:

fn main(os: Os) {
  let api = os.net.only(["api.example.com"])
  match api.dial("metrics.example.net:443") {
    Ok(_) => os.stdio.println("connected")
    Err(e) => os.stdio.println("refused: ${e}")
  }
}
$ xo run ./main
refused: Denied(addr: "metrics.example.net:443")

The narrowing methods are Net.only(hosts), Env.only(names), Proc.only(commands), and for files Fs.sub(dir), read_only(), and write_only(). Net, Env, and Proc narrowing is checked at run time on the Go backend; the native backends are planned.

xo audit

xo audit prints one report of every dependency: its effects and lock state, its escape hatches and the vouches that cover them, the capabilities you narrowed, and which capabilities you pass into dependencies. On a terminal it prints text; with --json, or when its output is piped, it prints JSON (abridged):

$ xo audit --json .
{
  "module": "example.com/shop",
  "dependencies": [
    {
      "path": "github.com/acme/billing",
      "version": "v1.2.0",
      "effects": ["go", "net.dial"],
      "locked_effects": ["go", "net.dial"],
      "lock": "ok",
      "escape_hatches": [
        { "kind": "go", "target": "os", "line": 1, "vouched": true, "effects": ["go"] }
      ]
    }
  ],
  "attenuations": [
    { "file": "main/main.xo", "line": 4, "capability": "Net", "method": "only",
      "args": ["metrics.example.net"], "constant": true }
  ],
  "capabilities_passed": [
    { "file": "main/main.xo", "line": 5, "callee": "sync",
      "dependency": "github.com/acme/billing@v1.2.0", "capability": "Net",
      "attenuated": true, "via": "Net.only(metrics.example.net)" }
  ],
  "summary": { "dependencies": 1, "unvouched_escape_hatches": 0, "widened": 0,
               "unlocked": 0, "unattenuated_capabilities_passed": 0, "errors": 0 }
}

It exits 1 when the check has errors, so it can gate CI.

Fetching and installing

No code runs when a module is fetched or built: Xo has no install hooks, no build scripts, and no code that runs on import (a top-level statement is a compile error). xo.lock pins the content hash of every module version, and with XO_SUMDB set, the first fetch of each version is checked against a signed checksum log.

What this does not cover

  • Opt-in hardening. Without a ceiling or a narrowed capability, a new dependency you hand os.net can use it. It shows in its signatures, in xo.lock, and in xo audit, but it is not blocked.
  • Your vouch is trusted. Xo cannot see inside C or Go code. A vouch that says uses none for C that does I/O is not caught.
  • No effect, no check. A function that returns a wrong answer, or loops forever, performs no effect. Tests, contracts, and timeouts are the defense there.
  • Not a sandbox. This is checking at compile time plus capabilities at run time, inside one process.

Measured against 15 attack scenarios from the Backstabber’s Knife Collection taxonomy, these defenses block 10; the post on supply-chain safety has the table. The full rules are in the specification: xo.mod, xo.lock, and the diagnostic pages for XO1309, XO1316, and XO1317.