Permissions
Define scoped agentOS kernel permissions that authorize guest filesystem, process, and network operations for each VM and session.
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,hostFunction. - Each scope is a mode (
"allow"or"deny"), or a rule set. - A denied operation is rejected with
EACCESbefore 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.
Defaults and merge semantics
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",
hostFunction: "allow",
network: { // VM-local listeners and loopback only
default: "deny",
rules: [
{ mode: "allow", operations: ["listen"], patterns: ["tcp://**", "unix:**"] },
{
mode: "allow",
operations: ["http"],
patterns: ["tcp://127.0.0.1:*", "tcp://localhost:*", "tcp://::1:*", "unix:**"],
},
],
},
}
fs/childProcess/process/envare allowed because they are fully virtualized (the guest sees only the VM, never the host) and are required to run a program at all.hostFunctionis allowed so the host functions you register can be called.- Guest listeners and loopback connections work because they stay inside the VM. External DNS and network connections are denied until you opt in; model providers have no special exception.
- The sidecar owns this baseline, so the TypeScript and Rust clients behave identically.
- 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" } satisfies Permissions;
Permission scopes
| Scope | Controls | Default |
|---|---|---|
fs | Filesystem reads, writes, and metadata operations | allow |
network | External connections, DNS, VM-local connections, and listen | VM-local only |
childProcess | Spawning child processes | allow |
process | Process-control operations | allow |
env | Environment variable access | allow |
hostFunction | Invoking host functions registered with the runtime | deny* |
* The hostFunction scope is auto-granted to allow when you register host functions and set no hostFunction policy of your own. Pass a hostFunction policy to gate individual host functions.
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
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".
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/**"] }],
},
} satisfies Permissions;
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/**"] }],
},
} satisfies Permissions;
Allow only specific network hosts
Every non-fs scope matches by patterns instead of paths. For network, a pattern matches a resource, not a bare hostname: dns://<host> for name resolution and tcp://<host>:<port> for the connection. Allowing a host needs both, and * matches any port. The operations are fetch, http, dns, and listen.
A bare hostname such as api.example.com matches nothing, so the host stays denied.
// Deny the network by default, allow only api.example.com.
const allowOneHost = {
network: {
default: "deny",
rules: [
{
mode: "allow",
operations: ["*"],
patterns: ["dns://api.example.com", "tcp://api.example.com:*"],
},
],
},
} satisfies Permissions;
Allow only specific host functions
Host functions registered with the runtime are gated by the hostFunction scope and matched by name via patterns. Use ["invoke"] for operations.
// Deny all host functions by default, then allow only "add" by name.
const allowOneHostFunction = {
hostFunction: {
default: "deny",
rules: [{ mode: "allow", operations: ["invoke"], patterns: ["add"] }],
},
} satisfies Permissions;
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: ["*"]. Setting childProcess: "deny" blocks subprocesses created by guest code; it does not block the trusted runtime from starting the VM’s requested root program.
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 type { Permissions } from "@rivet-dev/agentos";
import { agentOS, setup } from "@rivet-dev/agentos";
// Allow the filesystem everywhere, but deny anything under /home/agentos/vault.
const denyVault = {
fs: {
default: "allow",
rules: [
{ mode: "deny", operations: ["*"], paths: ["/home/agentos/vault/**"] },
],
},
} satisfies Permissions;
// Deny the network by default, allow only api.example.com.
const allowOneHost = {
network: {
default: "deny",
rules: [
{
mode: "allow",
operations: ["*"],
patterns: ["dns://api.example.com", "tcp://api.example.com:*"],
},
],
},
} satisfies Permissions;
// Deny all host functions by default, then allow only "add" by name.
const allowOneHostFunction = {
hostFunction: {
default: "deny",
rules: [{ mode: "allow", operations: ["invoke"], patterns: ["add"] }],
},
} satisfies Permissions;
const vm = agentOS({
permissions: {
...denyVault,
...allowOneHost,
...allowOneHostFunction,
},
});
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
Embedded API
Pass the same permission policy to AgentOs.create().
import { AgentOs, type Permissions } from "@rivet-dev/agentos-core";
// The kernel permission policy is the same object the actor takes. Pass it to
// AgentOs.create() instead of agentOS().
const permissions = {
network: {
default: "deny",
rules: [
{
mode: "allow",
operations: ["*"],
patterns: ["dns://api.example.com", "tcp://api.example.com:*"],
},
],
},
} satisfies Permissions;
const vm = await AgentOs.create({ permissions });
await vm.dispose();
Read more in the embedded API quickstart.