# 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](/agentos/docs/approvals).

## Defaults and merge semantics

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

```ts
{
  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.

examples/permissions/server.ts:

```ts
// 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` | Outbound connections: `fetch`, HTTP, DNS, and inbound `listen` | `deny` |
| `childProcess` | Spawning child processes | `allow` |
| `process` | Process-control operations | `allow` |
| `env` | Environment variable access | `allow` |
| `binding` | Invoking bindings registered with the runtime | `deny`* |

\* 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.

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

examples/permissions/bind-policy.ts:

```ts
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.

```ts
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](/agentos/docs/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.

examples/permissions/fs-rules.ts:

```ts
// 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:

examples/permissions/fs-rules.ts:

```ts
// 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 is a host (or `host:port`), and the operations are `fetch`, `http`, `dns`, and `listen`.

examples/permissions/server.ts:

```ts
// Deny the network by default, allow only api.example.com.
const allowOneHost = {
	network: {
		default: "deny",
		rules: [{ mode: "allow", operations: ["*"], patterns: ["api.example.com"] }],
	},
} satisfies Permissions;
```

## 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`.

examples/permissions/server.ts:

```ts
// Deny all bindings by default, allow only the "add" binding by name.
const allowOneBinding = {
	binding: {
		default: "deny",
		rules: [{ mode: "allow", operations: ["*"], 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: ["*"]`.

## Combine policies and see denials

Each policy above sets one scope, so you can spread several into one `permissions` object and bind them together.

examples/permissions/combine.ts:

```ts
import { agentOS, setup } from "@rivet-dev/agentos";
import type { Permissions } 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: ["api.example.com"] }],
	},
} satisfies Permissions;

// Deny all bindings by default, allow only the "add" binding by name.
const allowOneBinding = {
	binding: {
		default: "deny",
		rules: [{ mode: "allow", operations: ["*"], patterns: ["add"] }],
	},
} satisfies Permissions;

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
```
