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:
- The operator’s grant — what you allow at run time, via
--grant/--allow/--deny, a profile, or the config file. - 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:
| Mode | Behaviour |
|---|---|
ask (default) | Prompt on first access, remember the answer for the session |
deny | Refuse every operation |
allowlist | Permit only operations matching an allow constraint, minus any deny |
open | Permit anything the component’s declared ceiling allows |
What ask actually does
Section titled “What ask actually does”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:
audit: ⚠ declared ask, no prompt channel — every access will be denied: wasi:filesystemGranting from the CLI
Section titled “Granting from the CLI”Three flags, uniform across run, call, and info:
# Open a whole class, up to the component's declared ceilingact run <component> --mcp --allow wasi:http
# Refuse a class outright — including a semantic class the component declaresact run <component> --mcp --deny db:drop
# Full control: a JSON grant, repeatable and mergedact 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.
Shorthand: one rule per flag
Section titled “Shorthand: one rule per flag”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:
act run actpkg.dev/library/sqlite --mcp --allow 'fs=/data/**' # read-writeact run <component> --mcp --allow 'fs=~/notes/**:ro' # read-onlyact run <component> --mcp --allow 'http=https://api.example.com' # one host + schemeact run <component> --mcp --allow 'sockets=db.local:5432/tcp' # host, port, protocolact run <component> --mcp --allow 'db:drop=test_*' # semantic class, by keyact run <component> --mcp --allow 'fs=/data/**' --deny 'fs=/data/secret/**'| Class | Alias | Shorthand |
|---|---|---|
wasi:filesystem | fs | <glob>[:ro|:rw] — read-write unless :ro |
wasi:http | http | [scheme://]host[:port] |
wasi:sockets | sockets | host[:port][/tcp|/udp], or a CIDR |
act:credentials | creds | none — --allow creds |
| any semantic class | — | <key-glob> |
- Aliases work wherever an id does: in flags, in
--grantkeys 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— becausefe80::1:8080is 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
--allowkeeps the deny rules your profile or config set for that class. - A constrained
--denynarrows whatever the profile and config granted and never grants. --allow fsand--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.
Capability ids and their constraints
Section titled “Capability ids and their constraints”Grants are keyed by capability id. The built-in classes:
wasi:filesystem
Section titled “wasi:filesystem”{"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.
wasi:http
Section titled “wasi:http”{"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.
wasi:sockets
Section titled “wasi:sockets”{"host": "db.internal", "ports": [5432], "protocols": ["tcp"]}Same network-rule vocabulary as wasi:http (host, ports, cidr, except-ports), plus
protocols.
act:credentials
Section titled “act:credentials”The host-provided credential store. See act secret and act login for putting values in it.
Semantic classes
Section titled “Semantic classes”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.
Layering
Section titled “Layering”Grants merge, later winning:
global [policy] < profile [policy] < CLI --grant/--allow/--denyWithin 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.
Reading the decisions
Section titled “Reading the decisions”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:
audit: actpkg.dev/library/sqlite sha256:cb7715 │ act:credentials=deny wasi:filesystem=allowlist wasi:http=deny wasi:sockets=denyaudit: ✓ allow wasi:filesystem write /data/app.sqlite mode:allowlist under /data/**audit: ✗ deny wasi:filesystem read /dev/urandom outside ceiling mode:allowlistoutside 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).
Quick recipes
Section titled “Quick recipes”Lock everything down
Section titled “Lock everything down”act run <component> --mcp --grant '{"*":"deny"}'One database file
Section titled “One database file”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”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"}]}}'Trust the component’s own declaration
Section titled “Trust the component’s own declaration”act run <component> --mcp --allow wasi:http --allow wasi:filesystemCap memory while you’re at it
Section titled “Cap memory while you’re at it”act run <component> --mcp --max-memory 512MiBDeclaring the ceiling
Section titled “Declaring the ceiling”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.