Permissions
The sandbox permission policy is the kernel-level enforcement layer. Every guest syscall the agent’s sandboxed code makes is checked against a per-scope policy before any host resource is touched.
- Six scopes, configured independently:
fs,network,childProcess,process,env,binding. - Each scope is a mode (
"allow"or"deny"), or a rule set. - A denied operation is rejected with
EACCESbefore any host resource is touched. - Not merged over a baseline. Pass no policy and every scope defaults to
"allow"; pass a policy and every scope you omit is denied.
For the higher-level agent tool-approval layer (human-in-the-loop, auto-approve), see Approvals.
Defaults and merge semantics
Section titled “Defaults and merge semantics”The policy is not merged over a baseline. There are two distinct modes:
1. No policy — everything is allowed. When you pass no permissions object, every scope defaults to "allow", including network:
{ fs: "allow", // virtualized in-memory filesystem only childProcess: "allow", process: "allow", env: "allow", network: "allow", // network egress is on by default binding: "allow",}2. A policy is provided — every omitted scope is denied. The moment you pass a permissions object it is not layered over the all-allow default. The sidecar treats any scope you leave out as "deny". So { network: "allow" } grants the network but also denies fs, childProcess, process, and env — the guest can no longer run a normal program.
To keep the execution essentials on while opting into the network, list every scope you want allowed:
// Grant the network AND keep the execution essentials — omitted scopes are denied.const grantNetwork = { network: "allow", fs: "allow", childProcess: "allow", process: "allow", env: "allow",};Permission scopes
Section titled “Permission scopes”The No-policy default column is the mode each scope takes when you pass no permissions object at all. If you pass any permissions object, every scope you omit is denied instead (see Defaults and merge semantics).
| Scope | Controls | No-policy default |
|---|---|---|
fs | Filesystem reads, writes, and metadata operations | allow |
network | Outbound connections: fetch, HTTP, DNS, and inbound listen | allow |
childProcess | Spawning child processes | allow |
process | Process-control operations | allow |
env | Environment variable access | allow |
binding | Invoking bindings registered with the runtime | allow |
Bind a policy to the VM
Section titled “Bind a policy to the VM”A policy is a plain object keyed by scope. Pass it as permissions to agentOS(...) and it gates every guest syscall on that VM.
import { agentOS, setup } from "@rivet-dev/agentos";
const vm = agentOS({ permissions: { network: "allow", fs: "deny", },});
export const registry = setup({ use: { vm } });registry.start();Grant or deny a whole scope
Section titled “Grant or deny a whole scope”The simplest value for a scope is a single mode string. "allow" permits every operation in the scope; "deny" rejects every one with EACCES. Remember that once you pass a permissions object, every scope you omit is denied — so list every scope you want allowed, not just the ones you want to change.
const permissions = { network: "allow", // turn on network egress fs: "deny", // turn off all filesystem access // childProcess, process, and env are omitted here, so they are denied.};There is no typed "ask" mode. Interactive, human-in-the-loop approval lives in the higher-level Approvals layer, not the kernel policy. To block at the kernel level, use "deny".
Allow only specific filesystem paths
Section titled “Allow only specific filesystem paths”For finer control, a scope can be a rule set instead of a bare mode: a default mode plus an ordered list of rules. The fs scope matches by paths (filesystem globs). Each rule names its operations (read, write, stat, readdir, create_dir, rm, rename, symlink, readlink, chmod, truncate, mount_sensitive, or ["*"] for all). Last matching rule wins; if no rule matches, default applies.
// Allow the filesystem everywhere, but deny anything under /home/agentos/vault.const denyVault = { fs: { default: "allow", rules: [{ mode: "deny", operations: ["*"], paths: ["/home/agentos/vault/**"] }], },};To invert it, flip default to "deny" and allow just one subtree:
// Deny the filesystem by default, allow only reads under /home/agentos/data.const allowOnlyData = { fs: { default: "deny", rules: [{ mode: "allow", operations: ["read", "readdir", "stat"], paths: ["/home/agentos/data/**"] }], },};Allow only specific network hosts
Section titled “Allow only specific network hosts”Every non-fs scope matches by patterns instead of paths. For network, a pattern is a host (or host:port), and the operations are fetch, http, dns, and listen.
// Deny the network by default, allow only api.example.com.const allowOneHost = { network: { default: "deny", rules: [{ mode: "allow", operations: ["*"], patterns: ["api.example.com"] }], },};Allow only specific bindings
Section titled “Allow only specific bindings”Bindings registered with the runtime are gated by the binding scope, matched by name via patterns. Bindings have no sub-operations, so pass ["*"] for operations.
// Deny all bindings by default, allow only the "add" binding by name.const allowOneBinding = { binding: { default: "deny", rules: [{ mode: "allow", operations: ["*"], patterns: ["add"] }], },};The childProcess, process, and env scopes work the same way: childProcess patterns match the command (operations: ["spawn"]), env patterns match the variable name (operations: ["read", "write"]), and process is matched by pattern with operations: ["*"].
Combine policies and see denials
Section titled “Combine policies and see denials”Each policy above sets one scope, so you can spread several into one permissions object and bind them together.
import { agentOS, setup } from "@rivet-dev/agentos";
const vm = agentOS({ permissions: { ...denyVault, ...allowOneHost, ...allowOneBinding, },});
export const registry = setup({ use: { vm } });registry.start();When a scope or matching rule denies an operation, the kernel rejects it with EACCES before any host resource is touched. For example, with network: "deny", an outbound fetch() inside the guest throws:
EACCES: permission denied, tcp://example.com:80: blocked by network.http policy