Skip to content
GitHub Get Started
Operating System

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 EACCES before any host resource is touched.
  • Merged over a secure default, so partial policies work.

For the higher-level agent tool-approval layer (human-in-the-loop, auto-approve), see Approvals.

The sandbox is deny-by-default for outward-facing capabilities. When you pass no policy, this baseline applies:

{
fs: "allow", // virtualized in-memory filesystem only
childProcess: "allow",
process: "allow",
env: "allow",
network: "deny", // no network egress until you opt in
}
  • fs/childProcess/process/env are allowed because they are fully virtualized (the guest sees only the VM, never the host) and are required to run a program at all.
  • network is denied: guest code cannot reach the network until you opt in.
  • Your policy is merged over this baseline. Omitted scopes keep their default; they are not denied. So { network: "allow" } grants the network while keeping the execution essentials.
// Grant the network, leave everything else at the secure default.
const grantNetwork = { network: "allow" };

See Full Example

ScopeControlsDefault
fsFilesystem reads, writes, and metadata operationsallow
networkOutbound connections: fetch, HTTP, DNS, and inbound listendeny
childProcessSpawning child processesallow
processProcess-control operationsallow
envEnvironment variable accessallow
bindingInvoking bindings registered with the runtimedeny*

* The binding scope is auto-granted to allow when you register bindings and set no binding policy of your own. Pass a binding policy to gate individual bindings.

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();

See Full Example

The simplest value for a scope is a single mode string. "allow" permits every operation in the scope; "deny" rejects every one with EACCES. Omitted scopes keep their secure default, so you only list what you want to change.

const permissions = {
network: "allow", // turn on network egress
fs: "deny", // turn off all filesystem access
};

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".

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/**"] }],
},
};

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"] }],
},
};

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: ["*"].

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();

See Full Example

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