Skip to main content
A profile decides what one session sees and may run. Profiles live on the workspace; a session binds one at creation, for life. The policy engine enforces it; the route policy picks runtimes.
In code: Workspace(mounts, profiles=...) and ws.create_session("agent_a", profile="guarded") (TypeScript: new Workspace(mounts, { profiles }), ws.createSession('agent_a', { profile: 'guarded' })).
  • No profile means profiles.default if defined, else unrestricted. The workspace’s default session follows the same rule, so default also governs bare ws.shell(...) and ws.vfs.
  • ws.session("agent_a", profile="guarded") returns one handle for both doors, handle.shell(line) and handle.vfs.read(path). It creates the session or adopts an existing one. Session(ws, "agent_a") only adopts.
  • A line sets the session; a VFS call inherits it. ws.vfs.read(path, session_id=...) applies only outside a running line.

How rules are read

  • Command axis: rules without paths, by verb: deny before ask. allow decides whether a word is a command at all.
  • Path axis: rules with paths, and hides, by anchor depth (literal components before the first wildcard). The deeper entry wins whatever its verb: /runbook/frozen/* beats /runbook/*. Ties break by verb.
  • Hiding is not refusing. A hidden path is ENOENT and leaves no record; a denied path is listed and fails when used.

commands

allow is the session’s tool set. A word not in it is command not found (exit 127). Omitted means everything, empty means nothing, and builtins count: allow: [cat] leaves no echo.
A pattern is a prefix of the line: git push covers git push origin main, * matches one word, a bare * every command. The reason is only on the result’s refusal record. Text surfaces append policy denied: <reason> or requires approval: <reason> (ask <id>). A rule also covers what a command reaches on its own: every entry of a walk (grep -r, tar, cp -r, find -delete), files a command opens (awk print >, sed w) and cached bytes. Only rules with paths and no command reach the VFS-only doors (FUSE, ws.vfs, SFTP). Not covered yet: file access inside a CLI or interpreter (git, python3) and in getfattr, setfattr, ln, readlink; backends that search on their own side (grep -r on a GitHub mount).

paths and vars

hide takes exact paths (with their subtree) or patterns. show reopens a deeper subtree with a mode. A hide’s reason is for operators only. vars.hide makes variables read as unset.

mounts

Narrows one mount: a lower mode, patterns anchored at the mount, and ask / deny rules for lines working inside it by cwd or operand.

cwd, env, policy

cwd and env seed the session. policy is a script defining any of pre_command(ctx), pre_vfs(ctx) and pre_session(ctx) (preCommand, preVfs, preSession in JavaScript). pre_command returns None, {"deny": reason} or {"ask": reason}; the others allow or deny. A file defining no hook fails closed. The script may read files, through the same gate.
ask_curl.py

Asks

An ask holds the line. The agent reads rm: Permission denied at exit 126 with a pending record:
The question waits in ws.decisions until a host answers:
  • allow, scope once (default): the retry runs and spends it.
  • allow, scope session: every line the rule covers, from now on.
  • deny: the retry is refused; running the line again asks again.
An ask with paths and no command also asks from a file tool or session.vfs when no line is running. allow once then covers one tool call.

From the CLI

Over REST

A record has id, session_id, agent_id, command, argv, cwd, paths, reason, outcome (null while pending), scope and note. An unknown id is 404, an answered one 409, and deny with scope session 422.

In code

ws.decisions has pending(session_id), list(session_id) and answer(ask_id, outcome, scope, note). Pass on_ask (onAsk) to answer inline while the line waits. How a grant is spent:
  • Every command of a line is judged before it runs. A grant belongs to one place on the line, so a command spelled twice asks twice, and a loop body is one place.
  • Grants are spent when the line ends, whether or not the command ran.
  • Nested lines ($( ), eval, source, xargs) use the outer line’s grants: echo $(cat secret.txt) asks once.
  • A background job keeps its line’s grants until it ends.
  • A refused retry spends the refusal; the next run asks again.
A command only expansion reveals (cat $F, xargs cat, find -exec, a path after a cd to a variable) is asked at its own gate, possibly after earlier commands ran; a pending answer does not undo them.

Explain

To see what a profile would do to a line or a VFS call without running it, use Explain.

Examples

Their output is pinned in CI.