Skip to content

Policy & sandbox

Nothing a component does to the outside world happens unasked. Every capability — filesystem, outbound HTTP, raw sockets, credentials, and provider-defined semantic classes — is gated by a policy engine, and two things shape the result:

  1. The operator’s grant — what you allow at run time, via --grant / --allow / --deny, a profile, or the config file.
  2. The component’s ceiling — what the component declared in its act.toml ([std.capabilities.*]). An undeclared capability is a hard deny regardless of what you grant.

The effective policy is grant ∩ ceiling. A permissive operator can’t escalate past a component’s stated intent, and a component can’t silently exceed the operator’s grant.

Every capability resolves to one of four modes:

ModeBehaviour
ask (default)Prompt on first access, remember the answer for the session
denyRefuse every operation
allowlistPermit only operations matching an allow constraint, minus any deny
openPermit anything the component’s declared ceiling allows

ask needs a channel to ask on. Interactive act call prompts on the TTY; act run --mcp (stdio or HTTP) asks the connected MCP client through MCP elicitation. Where no prompt channel exists — a headless CI run, a non-TTY pipe — ask degrades to deny. That is why scripted invocations pass grants explicitly, and why the host warns you when it happens:

Terminal window
audit: ⚠ declared ask, no prompt channel — every access will be denied: wasi:filesystem

Three flags, uniform across run, call, and info:

Terminal window
# Open a whole class, up to the component's declared ceiling
act run <component> --mcp --allow wasi:http
# Refuse a class outright — including a semantic class the component declares
act run <component> --mcp --deny db:drop
# Full control: a JSON grant, repeatable and merged
act run <component> --mcp --grant '{
"wasi:filesystem": {"mode":"allowlist","allow":[{"path":"/data/**","mode":"rw"}]},
"wasi:http": {"mode":"allowlist","allow":[{"host":"api.example.com","scheme":"https"}]}
}'

A grant’s value is either the string "open" or an object with mode, allow, and deny. allow and deny hold provider-defined constraints — the shapes differ per capability.

For the common case — one directory, one host — --allow and --deny take a rule after =. Each shorthand is exactly one --grant rule, parsed by the capability’s provider, so it means nothing a JSON grant couldn’t say:

Terminal window
act run actpkg.dev/library/sqlite --mcp --allow 'fs=/data/**' # read-write
act run <component> --mcp --allow 'fs=~/notes/**:ro' # read-only
act run <component> --mcp --allow 'http=https://api.example.com' # one host + scheme
act run <component> --mcp --allow 'sockets=db.local:5432/tcp' # host, port, protocol
act run <component> --mcp --allow 'db:drop=test_*' # semantic class, by key
act run <component> --mcp --allow 'fs=/data/**' --deny 'fs=/data/secret/**'
ClassAliasShorthand
wasi:filesystemfs<glob>[:ro|:rw] — read-write unless :ro
wasi:httphttp[scheme://]host[:port]
wasi:socketssocketshost[:port][/tcp|/udp], or a CIDR
act:credentialscredsnone — --allow creds
any semantic class—<key-glob>
  • Aliases work wherever an id does: in flags, in --grant keys and in [policy]. The audit trail always prints the full id.
  • A rule never goes past the component’s ceiling: fs=/data/** on a component that declared /data/** read-only is read-only.
  • IPv6 goes in brackets — http=[::1]:8080 — because fe80::1:8080 is itself a valid address.
  • --allow 'http=…' takes hosts, not CIDR ranges: an http allow rule without a host never survives the intersection with the component’s declared hosts, so a range would grant nothing. A range can be denied — --deny 'http=169.254.0.0/16' blocks the metadata service whatever name resolves to it.
  • A rule is only ever one class, not a pattern: --deny 'db:*=…' is refused, because it would not reach a class granted by its own name.
  • A constrained --allow keeps the deny rules your profile or config set for that class.
  • A constrained --deny narrows whatever the profile and config granted and never grants.
  • --allow fs and --allow 'fs=…' for the same class together is an error: the first opens the whole ceiling, the second only one rule.

act run --help lists every built-in class with its alias and shorthand.

Grants are keyed by capability id. The built-in classes:

{"path": "/data/**", "mode": "rw"}

path is a glob; mode is ro or rw. Guest paths are rewritten through the component’s declared mount-root / mounts, so a component that expects /data can be pointed at any host directory.

Ancestor traversal. WASI stats every intermediate directory when opening a nested file, so an entry for /tmp/work/db.sqlite implicitly permits traversing /tmp/work and /tmp — sibling files stay denied. You don’t list each parent. But do watch the sidecars: SQLite, for example, also writes a .lock file next to the database, so grant the directory rather than the single file.

{"host": "*.example.com", "scheme": "https", "methods": ["GET","HEAD"], "ports": [443]}
{"cidr": "169.254.169.254/32"}

host is exact or a *.suffix wildcard; scheme, methods, ports narrow further and are optional. A rule may instead match by cidr (IPv4 or IPv6), with except-ports carving ports back out — useful for deny rules like “block loopback except port 3000”. Use cidr in deny: an allow rule with no host is dropped when it is intersected with the component’s declared hosts.

Requests are checked on five axes — host, scheme, method, port, and DNS-resolved IP. A CIDR-denied destination surfaces to the component as a DNS error rather than a refused connection. Every hop of a redirect chain is re-checked against the same policy, so a 302 to a denied host fails mid-chain instead of quietly succeeding.

{"host": "db.internal", "ports": [5432], "protocols": ["tcp"]}

Same network-rule vocabulary as wasi:http (host, ports, cidr, except-ports), plus protocols.

The host-provided credential store. See act secret and act login for putting values in it.

A component may declare classes of its own — db:drop, browser:navigate, whatever it wants to be held to — and enforce them internally. They’re granted and denied exactly like the built-in ids, which is what makes --deny db:drop work.

Two rules come from the spec rather than from taste. A declaration must name a concrete class: "db:*" is not valid in act.toml, because a reader could not tell what the artifact is able to ask for. And a class should separate irreversible actions from routine ones — db:drop apart from db:ddl — so the destructive case can be refused without refusing the rest. A *-suffix pattern is fine on the granting side, in the config file ("db:*" = "deny"), where it narrows rather than hides.

Grants merge, later winning:

global [policy] < profile [policy] < CLI --grant/--allow/--deny

Within one layer, an id resolves by exact id > longest *-prefix > default. An unset default inherits from the layer below; absent everywhere, it is ask. See Profiles & config.

The audit trail reports the resolved policy at startup and every decision as it happens — it goes to stderr and is not governed by RUST_LOG:

Terminal window
audit: actpkg.dev/library/sqlite sha256:cb7715 │ act:credentials=deny wasi:filesystem=allowlist wasi:http=deny wasi:sockets=deny
audit: ✓ allow wasi:filesystem write /data/app.sqlite mode:allowlist under /data/**
audit: ✗ deny wasi:filesystem read /dev/urandom outside ceiling mode:allowlist

outside ceiling means the component never declared it; a plain denial means your grant didn’t cover it. Turn the trail off with --no-audit (nothing else silences it).

Terminal window
act run <component> --mcp --grant '{"*":"deny"}'
Terminal window
act run actpkg.dev/library/sqlite --mcp \
--session-args '{"database_path":"/data/app.db"}' \
--allow 'fs=/data/**'

One external API, nothing else — and no metadata service

Section titled “One external API, nothing else — and no metadata service”
Terminal window
act run <component> --mcp --grant '{"wasi:http":{"mode":"allowlist",
"allow":[{"host":"api.openai.com","scheme":"https","methods":["POST"]}],
"deny":[{"cidr":"169.254.169.254/32"},{"cidr":"10.0.0.0/8"}]}}'
Terminal window
act run <component> --mcp --allow wasi:http --allow wasi:filesystem
Terminal window
act run <component> --mcp --max-memory 512MiB

The other half of the model is the component’s own declaration — see Manifest reference. act-build pack validates it at build time and the host re-validates at load, rejecting a component whose declaration is malformed or which declares an empty allow where one is required.