# Resource Limits

Every agentOS VM runs with **per-VM resource and runtime caps**. These caps contain runaway or malicious guest work to its VM and give the host an explicit failure instead of silent data loss.

- **Secure defaults**: unset fields fall back to built-in defaults that match the runtime's historical constants. Optional execution budgets such as WASM fuel explicitly document when their default has no additional budget.
- **Per-VM**: every VM gets its own budget. Limits are not shared across VMs.
- **Enforced by the sidecar/runtime**: a guest that exceeds a cap fails inside the VM (out-of-memory, `EMFILE`, `EAGAIN`, runtime timeout, etc.) instead of consuming past the configured budget.
- **Operator-raisable**: the operator (the trusted process that creates the VM) may raise any cap for trusted workloads. Guest code can never raise its own caps.

## Setting limits

Set caps on the `limits` object in the `agentOS` config. Limits are grouped by subsystem (`resources`, `process`, `jsRuntime`, `python`, `wasm`, and more). Omitted limits keep their secure default.

examples/resource-limits/server.ts:

```ts
import pi from "@agentos-software/pi";
import { agentOS, setup } from "@rivet-dev/agentos";

const vm = agentOS({
	software: [pi],
	limits: {
		resources: {
			maxProcesses: 64, // concurrent processes
			maxOpenFds: 256, // open file descriptors
			maxSockets: 128, // open sockets
			maxFilesystemBytes: 256 * 1024 * 1024, // VFS storage budget
			maxWasmFuel: 30_000, // WASM execution budget
			maxWasmMemoryBytes: 128 * 1024 * 1024, // WASM linear memory
			maxWasmStackBytes: 4 * 1024 * 1024, // WASM call-stack ceiling
		},
		process: {
			pendingStdinBytes: 64 * 1024 * 1024, // stdin waiting on a kernel pipe
			pendingEventCount: 10_000, // queued process/runtime events per stage
			pendingEventBytes: 64 * 1024 * 1024, // queued event payload per stage
		},
		jsRuntime: {
			v8HeapLimitMb: 128, // JS isolate heap
			cpuTimeLimitMs: 30_000, // active JS CPU time
			wallClockLimitMs: 0, // 0 disables elapsed wall-clock cutoff
			importCacheMaterializeTimeoutMs: 30_000, // Node import-cache setup
			syncRpcWaitTimeoutMs: 30_000, // host sync-RPC wait
		},
		python: {
			executionTimeoutMs: 300_000, // Python wall-clock execution
			maxOldSpaceMb: 0, // 0 keeps the Pyodide runner default
		},
		wasm: {
			prewarmTimeoutMs: 30_000, // WASM compile-cache warmup
			runnerHeapLimitMb: 2048, // trusted WASI runner V8 heap
		},
	},
});

export const registry = setup({ use: { vm } });
registry.start();
```

## Available caps

| Limit | Controls | Notes |
|---|---|---|
| `resources.maxProcesses` | Concurrent processes in the VM process table | Caps fork bombs and runaway spawning. New spawns fail with `EAGAIN`. |
| `resources.maxOpenFds` | Open file descriptors | Exhausting the table fails with `EMFILE` / `ENFILE`. |
| `resources.maxSockets` | Open sockets in the socket table | Bounds concurrent connections; excess `connect`/`accept` fail. |
| `resources.maxFilesystemBytes` | Total bytes stored in the virtual filesystem | Bounds VFS storage; writes past the budget fail with a no-space error. |
| `resources.maxInodeCount` | Inodes retained by the virtual filesystem | Default is `16384`; creating another file or directory fails with a no-space error. This is the expected upper bound for filesystem-schema sizing and benchmarks. |
| `resources.maxWasmFuel` | WASM execution budget | Bounds WASM execution work; unset means no explicit fuel budget. |
| `resources.maxWasmMemoryBytes` | WASM linear memory, in bytes | Default is `128 MiB`. |
| `resources.maxWasmStackBytes` | Maximum WASM call-stack size, in bytes | Deep recursion fails with a stack overflow instead of crashing the VM. |
| `resources.maxBlockingReadMs` | AgentOS safety backstop for otherwise-blocking guest operations | Default is `30000`. Socket waits, poll, and contended `F_SETLKW` warn near the limit and fail with `ETIMEDOUT` if it expires; raise it for workloads that intentionally wait longer. Linux has no equivalent global backstop. |
| `process.pendingStdinBytes` | Stdin accepted by the sidecar but not yet written into kernel pipes | Default is `64 MiB` per process and across the VM. Sibling processes share the same aggregate envelope, so this is a tighter bound for multi-process workloads. A non-draining process rejects further writes with an error naming `limits.process.pendingStdinBytes`. |
| `process.pendingEventCount` | Event count at each bounded VM/process delivery-queue stage | Default is `10000`. The crossing event is rejected with an error naming `limits.process.pendingEventCount`; it is never silently dropped. |
| `process.pendingEventBytes` | Retained process-event bytes at each bounded delivery-queue stage | Default is `64 MiB` per process and across all process queues in the VM. Sibling processes share the VM-wide envelope. Large stdout/stderr bursts are rejected with an error naming `limits.process.pendingEventBytes`, independently of event count. |
| `acp.maxSessionsPerVm` | Durable sessions retained in one VM SQLite database | Default is `10000`. Opening another session fails with a typed error naming this field. |
| `acp.maxPromptsPerSession` | Prompt and idempotency records retained for one durable session | Default is `100000`; it must not exceed `acp.maxPromptsPerVm`. |
| `acp.maxPromptsPerVm` | Prompt and idempotency records retained across one VM | Default is `1000000`. |
| `acp.maxPendingPermissionsPerSession` | Actionable ACP permission requests for one session | Default is `1000`; it must not exceed `acp.maxPendingPermissionsPerVm`. |
| `acp.maxPendingPermissionsPerVm` | Actionable ACP permission requests across one VM | Default is `10000`. |
| `acp.maxPermissionOutcomesPerSession` | Terminal ACP permission outcomes retained for one session | Default is `10000`; it must not exceed `acp.maxPermissionOutcomesPerVm`. |
| `acp.maxPermissionOutcomesPerVm` | Terminal ACP permission outcomes retained across one VM | Default is `100000`. Old outcomes are bounded independently from pending requests. |
| `jsRuntime.v8HeapLimitMb` | Guest JavaScript V8 heap, in MiB | Default is `128`. |
| `jsRuntime.cpuTimeLimitMs` | Active JavaScript CPU time | Default is `30000`; `0` disables the CPU watchdog. |
| `jsRuntime.wallClockLimitMs` | JavaScript elapsed wall-clock backstop | Default is `0`, disabled. Use this for finite commands, not long-lived adapters. |
| `jsRuntime.importCacheMaterializeTimeoutMs` | Node import-cache materialization timeout | Default is `30000`. |
| `jsRuntime.syncRpcWaitTimeoutMs` | JavaScript sync host-RPC wait | Unset keeps the engine default, currently `30000`. |
| `python.executionTimeoutMs` | Python execution wall-clock timeout | Default is `300000`. |
| `python.maxOldSpaceMb` | Pyodide runner V8 old-space heap, in MiB | Default is `0`, which keeps the engine default. |
| `wasm.prewarmTimeoutMs` | WASM compile-cache warmup timeout | Default is `30000`. |
| `wasm.runnerHeapLimitMb` | Trusted WASI/WASM runner V8 heap, in MiB | Default is `2048`; this is not guest linear memory. |
| `wasm.runnerCpuTimeLimitMs` | Trusted WASI/WASM runner active-CPU budget | Default is `30000`; `0` disables this budget for trusted configurations. |
| `process.maxSpawnFileActions` | File actions decoded for one `posix_spawn` call | Default is `4096`; excess actions fail with `E2BIG`. |
| `process.maxSpawnFileActionBytes` | Serialized file-action bytes for one `posix_spawn` call | Default is `1 MiB`; excess input fails with `E2BIG`. |

## Behavior at the limit

- **WASM stack**: deep recursion throws a stack-overflow error in the guest, never a host crash.
- **JavaScript CPU time**: CPU-bound loops terminate with a CPU-budget error once active JS CPU exceeds `jsRuntime.cpuTimeLimitMs`.
- **JavaScript wall time**: awaiting or blocked JS terminates only when you set `jsRuntime.wallClockLimitMs`; the default is disabled for long-lived adapters.
- **Filesystem bytes**: writing past the VFS budget fails with a no-space error to the guest.
- **Counts (fds / processes / sockets)**: hitting a table cap returns the standard POSIX errno appropriate to that cap (`EMFILE`/`ENFILE`, `EAGAIN`, etc.).
- **Durable ACP collections**: session, prompt, pending-permission, and terminal-outcome caps fail atomically with typed errors naming the exact `limits.acp.*` field to raise. Per-session caps are validated not to exceed their corresponding per-VM cap.

### WASM memory residency calls

V8 owns the physical backing pages for WASM linear memory, and the runtime
cannot currently pin those pages against host swapping. Nonempty `mlock()` and
valid `mlockall()` requests therefore return `ENOTSUP` rather than falsely
claiming that secrets or other guest memory were pinned. `munlock()` and
`munlockall()` succeed because no guest lock can be established and the memory
is already unlocked.

Non-destructive `madvise()` access-pattern calls are accepted as best-effort
hints, which Linux is also permitted to ignore. Advice that would discard data
or change fork, core-dump, or host VM mapping policy returns `ENOTSUP` because
the runtime cannot apply it.

## Sidecar liveness

Separate from the guest caps above, the host detects a dead or wedged sidecar
process by silence, not by per-request deadlines. The sidecar emits a liveness
heartbeat every 10 seconds from a dedicated thread — so it keeps beating even
mid-way through a long turn — and the host treats 30 seconds with no inbound
frames at all as a dead sidecar: it kills the process and fails in-flight
requests with a typed `SidecarSilenceTimeout` error.

Because liveness is silence-based, individual requests have no time limit of
their own: an agent turn may legitimately run for many minutes without being
torn down. Neither the heartbeat cadence nor the silence window is
configurable — they are fixed protocol constants.

## Warnings & observability

Limits are observable, not just enforced. Live resource gauges and internal
bounded queues are tracked in a central limit registry that:

- **Warns before the limit is hit.** As usage crosses ~80% of a cap, the runtime
  emits a structured warning (once per crossing, re-armed only after it recovers),
  so a slow consumer or a runaway guest is visible *before* it fails.
- **Never drops data silently.** Internal queues either apply backpressure or
  fail with a typed error naming the exhausted limit and the setting used to
  raise it. A rejected event is not popped and forgotten.
- **Surfaces through logs.** The agentOS sidecar logs to stderr (stdout is the wire
  protocol); set `AGENTOS_LOG=warn` (the default) to see near-limit warnings
  or `AGENTOS_LOG=debug` for live per-limit usage snapshots.

See [Limits & Observability](/agentos/docs/architecture/limits-and-observability) for the
full architecture.
