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

The sandbox

In the auto mode, each shell command of the agent runs at once in a kernel sandbox. The sandbox has three layers: bubblewrap (namespaces and mounts), Landlock (file and socket rights) and seccomp (system calls that fail). It holds the command and every program that the command starts: build scripts, tests, git hooks and package install scripts.

The sandbox does not judge the command. The kernel blocks the effects that are not allowed. A command that tries such an effect fails, usually with Read-only file system, Permission denied or Network is unreachable.

Before you start

  1. Check that the sandbox can run on your machine:

    efr sandbox check
    

    auto needs Linux 7.1 or newer (Landlock ABI 9) and bubblewrap 0.10.0 or newer, not setuid. The last line says auto: ready when every check passed.

  2. Register the project, so the sandbox can write it:

    cd ~/p/app
    efr project add
    

    Without a registered project, the agent writes only in $SCRATCH.

  3. Choose the mode: ,mode auto for this terminal, or efr config set permissions.mode auto for every terminal.

What a command can do

WhatIn the sandbox
Writesonly in the write roots (below). Everything else is read-only.
Readsas for you, except secrets and the read masks (below).
Networkthe hosts of your allow list through efr's network proxy. See The network proxy. Loopback works, so a test can start a server and talk to it.
Sockets and D-Busno connection to a socket that the call did not make: no docker.sock, no session bus, no ssh-agent. Read-only system queries get a filtered bus.
More rightsnone. sudo fails with The "no new privileges" flag is set.
Other processesno signals to your processes. ps still shows them.
Background processes&, nohup and setsid stop when the call ends. A server that must run on goes in a background job.
/tmpprivate to the conversation. Share files through $SCRATCH.

The lines that you type in your own shell never run in the sandbox.

Write roots

A command can write here:

  • the turn's project, when the turn runs in a registered project;
  • the registered projects that the command names, such as a path or a cd target of the line (sandbox.write_projects = "named", the default);
  • $SCRATCH of the conversation;
  • the private /tmp and /var/tmp;
  • private copies of the tool caches;
  • the paths of sandbox.write_roots;
  • exactly the path of an approved exit, for one call.

Your home directory is never a write root. A turn in a project at ~ runs as cautious, with the reason auto cannot use your home directory as a project. Register a narrower project, such as efr project add ~/dotfiles.

Some paths stay read-only also inside a write root: your shell startup files, efr's config, and the project's git config and hooks.

Tool caches

The sandbox reads your tool caches (~/.cargo, ~/.rustup, ~/.cache, ~/.npm and the others of sandbox.caches) through an overlay. Its writes go to a private layer, and your own builds never see them. With sandbox.cache_mode = "tmp" (the default), the writes go away after each call. sandbox.keep_downloads = true keeps the downloads of a conversation, so a second cargo build does not download the crates again. Each call then takes about 3 to 4 ms more.

Read masks

A masked directory reads as empty, and a masked file reads as an empty file. The masks hold secrets (~/.ssh, ~/.gnupg, password stores, cloud and registry credentials), browser and messenger profiles, shell histories, efr's own state, your runtime directory, and the .env files of each write root. Secret-like environment variables, such as names with API_KEY, are removed. sandbox.mask adds your own paths.

Exits

An action that leaves the sandbox is an exit. Examples are a write outside the write roots, a host that is not on your allow list, a socket, a device, a masked file such as a project .env, sudo, git push and a change that runs later, such as ~/.zshrc or crontab.

Your request is the permission. A reviewer model decides most exits from your messages, so you see few questions:

  • It allows an action that fits what you asked.
  • It refuses an action that nothing you said relates to.
  • It hands an unsure case to you.

While it works, the status row shows reviewing: <command>. Its answer stays as one dim line, such as:

reviewer allowed: write ~/.local/share/app for this call (low risk; you asked to run the generator)

An allow opens exactly that path, host, socket or device for one call. The next call asks again, unless an always rule covers it. After the reviewer refused 3 actions in a row, or 10 in one turn, it pauses, and efr asks you for the rest of the turn.

Each review is one more request to your provider. classifier.enabled = false asks you about every exit instead.

What asks only you

Some exits never go to the reviewer: sudo and other ways to more rights, a masked file, a synced folder such as ~/Dropbox, a desktop command that types or starts programs, and the whole session bus. An upload, a delete above the project or a change that runs later asks you when no message of yours names it.

The question shows the whole line and every program:

? allow outside the sandbox
│ sudo ./scripts/setup.sh
│ runs with your full rights: files, secrets, network
│ why: sudo (you may need to type your password)
│ programs: sudo, ./scripts/setup.sh ~/p/app/scripts/setup.sh (untrusted: written in the sandbox; in a write root, changed this turn)
│ efr: only you can allow this
│ y allow · n deny

A program that the sandbox wrote is marked untrusted: it can do anything with your rights. Lines that start with efr: are facts that efr found itself.

What efr refuses

A secret, a write of efr's config, of efr's own programs or of efr's own dirs is denied with no question. The call's result says why, such as ✗ refused: efr's config (floor).

Deletes and undo

A delete in the project, such as git reset --hard or rm -r build, runs at once when efr's snapshot holds every file that it can touch, because efr undo brings them back. At the end, the turn says changed 4 files (+12 -3) in <project>; efr undo reverts this turn. shred, git clean -x and a file that the snapshot left out still go to the reviewer. See Undo and diff.

Files that run code later

When a command plants git settings that run programs, efr moves them to quarantine and asks keep it? y = yes, n = no before the next command. At the end of the turn, efr names the files that run code later outside the sandbox, such as a Makefile or build.rs. Check them before you run the project yourself.

Always-allow rules

At some exit questions, a saves a standing answer. The question shows the rule in words, such as y allow · n deny · a always: write under ~/notes. After a, efr asks save rule "write under ~/notes"? y/n, and only y saves the rule and runs the call.

The rule goes to [[sandbox.allow]] at the end of config.toml:

[[sandbox.allow]]
kind = "write"
under = "~/notes"
project = "~/p/app"

There are three kinds: host (one host and port through the proxy), write (a directory and, if you want, only in one project) and socket (one socket). A later call that a rule covers runs with no question and no review, and shows a dim line such as allowed by your rule sandbox.allow[2]: write under ~/notes.

A rule never covers sudo, an upload, a change that runs later, a masked read, the bus or a file tool. A rule that crosses a floor stays in the file but is disabled.

efr sandbox allow           # list the rules with their index
efr sandbox allow remove 2  # remove rule 2 after a question

When the sandbox cannot run

auto needs every part. When one part is missing, an auto turn runs as cautious. There is no partial sandbox, and efr never runs a contained call outside the sandbox.

  • ,mode auto says at once: efr: auto needs the sandbox; turns run as cautious: Landlock ABI 6 found; auto needs 9 (Linux 7.1). efr sandbox check shows more.
  • Each turn that asked for auto starts with a dim line, such as auto is not available here; this turn runs as cautious: Landlock ABI 6 found.
  • efr status shows the sandbox line, such as unavailable: bubblewrap is not installed; auto runs as cautious.

Common causes and fixes:

ReasonFix
bubblewrap is not installedArch: pacman -S bubblewrap; Debian and Ubuntu: apt install bubblewrap; Fedora: dnf install bubblewrap
bubblewrap is setuid; efr needs the unprivileged buildinstall the build that is not setuid
Landlock ABI N found; auto needs 9 (Linux 7.1)a newer kernel
Landlock is not enabledadd landlock to the lsm= boot parameter
AppArmor blocks user namespaces for bwrap (...)an AppArmor profile for bwrap with userns,, or set the sysctl to 0
efr-sbx is not installed next to efrdinstall efr-sbx next to efrd or in ../lib/efr/
auto cannot use your home directory as a projectregister a narrower project
sandbox.enabled = falseset sandbox.enabled = true

efrd runs its probe again after a change of [sandbox] or of the projects, after a call whose sandbox could not start, and before an auto prompt while the last probe failed. So a fix, such as a new package or a sysctl, needs no restart. The auto sandbox lists every reason and what to expect on some systems.

Commands

efr sandbox check runs the probe now and prints each check. A failed check has a fix: line below it, and the command exits with 1 when the sandbox is not available. The end of the output looks like this:

ok    self-test: 16 checks passed
launch 3.6 ms
netview yes
network proxy
auto: ready

efr sandbox explain PATH tells what a contained command can do with one path, from the current directory:

$ efr sandbox explain ~/.zshrc
~/.zshrc, from ~/p/app in auto:
  read   yes
  write  no: a shell startup file (floor); a write goes to the reviewer when a message of yours names the file, and efr copies the file first, so efr undo reverts it; else you decide; a write is a persistence exit

At the end of a turn in which a call ran in the sandbox, one dim line says where the sandbox could write, such as sandbox: writes in the project, $SCRATCH, private /tmp; network through efr's proxy. With network.mode = "none", it ends with no network. efr history --verbose shows the record of each exit and who judged it.

Settings

KeyDefaultWhat it does
sandbox.enabledtruefalse makes auto run as cautious
sandbox.write_projects"named"which registered projects a call may write: turn, named or all
sandbox.write_roots[]more write roots for every call
sandbox.cache_mode"tmp"tmp, overlay or readonly
sandbox.keep_downloadsfalsekeep the downloads of a conversation in the tmp mode
sandbox.mask[]more paths that read as empty
sandbox.protect[]more paths that stay read-only in a write root
sandbox.allow[]the always-allow rules
classifier.enabledtruefalse asks you about every exit

config.toml reference lists every key. The auto sandbox has the full details, and ADR 0006 records the decision.