Capability Grants
A constructor executes inside a WASI sandbox that can reach nothing by
default — no filesystem, no network, no environment. The consumer (not the
author) widens it, per instance, with grants. The same constructor code can
therefore be run wide-open by one workflow and fully sealed by another.
The grammar is identical on every consumer surface — Rust constructor!,
Rust #[reactor(constructor = ...)], and Python cloaca.constructor(...):
grants = {
fs = ["ro:/data", "rw:/scratch"],
env = ["API_REGION"],
http = ["api.example.com", "*.internal:8443"],
tcp = ["db.internal:5432"],
}
grants={"fs": ["ro:/data"], "env": ["API_REGION"]}
| Kind | Pattern | Effect |
|---|---|---|
fs |
ro:<path> / rw:<path> |
Pre-opens the directory read-only / read-write. Everything outside stays invisible. A bare path with no ro:/rw: prefix is a hard error — the load fails closed. |
env |
<NAME> |
Passes the host’s value of that variable through by name (skipped silently if unset). Literal values are not supported. |
http |
<host>[:port][/path-glob], * globs |
Grants the http capability plus a per-request egress policy matching host/port/path. |
tcp |
<host>:<port>, *:<port>, * |
Grants raw sockets plus a per-connection policy. A DNS host is resolved once at load and matched by (ip, port). |
secrets |
<name> |
Allow-lists the named secrets the member may resolve through the secret store; anything not listed is denied (NotGranted, distinct from NotFound). |
- Default-closed, fail-closed. No grant → deny. A malformed grant aborts the load rather than silently widening access.
- Symlinks cannot escape. A path inside a granted tree that symlinks
outside it is refused by the sandbox (
Operation not permitted). Grant the real target, or point at a regular file — e.g. prefer/etc/hostnameover/etc/os-release, which is a symlink into/usr/libon Debian images. - Denial surfaces at the operation, not at load: an ungranted read fails inside the member (a task node fails, a trigger simply never fires) with the WASI error naming the path.
- Load-time lint. If the provider package declares a capability intent the consumer didn’t grant, the loader logs a warning at load — the mismatch will deny at runtime, so it’s surfaced early rather than as a mystery failure.
Grant enforcement applies only to WASM providers. A provider packaged with
runtime = native runs trusted and unsandboxed, in-process — no WASI
sandbox, no capability allow-list, no egress policy
(ProviderRuntime::grants_enforced() returns true only for Wasm; the native
load path takes no grants at all). Any grants you write at a native
constructor’s instantiation site are advisory documentation, not a security
boundary. Treat installing a native provider like adding a normal Rust
dependency: review it, pin it, and trust it — or use a WASM provider instead.
cloacinactl constructor package --native prints this trust tier at packaging
time.
Because runtime is an emission target, a provider can ship as WASM in one
version and native in the next — which would quietly turn your grants from a
control into decoration. Two rules make that impossible:
-
Declaring
grantsagainst a provider that resolves NATIVE is a load error, unless you acknowledge the trust tier explicitly. The message names all three ways out:grants are not enforced on native providers; either pin
runtime = "wasm", acknowledge withruntime = "native", or remove grants -
You can pin the tier with an optional
runtimeon the consumption site. If the resolved provider disagrees, the load fails and names both runtimes:constructor!( id = "read_config", from = "cloacina-provider-fs@0.1", constructor = "read_file", config = { path = "/etc/app.toml" }, grants = { fs = ["ro:/etc"] }, runtime = "wasm", // fail the load if this provider ever goes native );#[reactor(.., runtime = "..")]takes the same pin. Writingruntime = "native"is the explicit acknowledgement: the node loads, the grants stay advisory, and the loader logs a warning saying so.
Grants-free native loads are unaffected — no error, no new requirement. On the
producer side, changing a provider’s runtime is a MAJOR/breaking change, checked
against providers/COMPAT.toml by the provider wave guard.
The runnable examples/constructor-contract/fs-grant-demo demonstrates all
three outcomes side by side: a granted read, the same read denied without the
grant, and a granted write through a second suite member.