8.5 Capability grants: internal prefixes, outbound HTTP, and sockets
A custom service can reach only what its mount grants — default-deny (3.5).
Grants are declared in the mount config under grants, keyed by the capability
name the code passes to ctx.request (or "fetch" for the global fetch).
The three grant kinds
Internal prefix — reach another mount
"grants": { "orders": { "prefix": "/data/orders" } }
The guest calls ctx.request("orders", { url: "/42" }); the runtime appends the
guest's path to the prefix (/data/orders/42) and re-enters full dispatch —
authz, limits, and idempotency all apply. The guest cannot escape the prefix:
there's no path it can construct that reaches outside /data/orders.
Use this to let a service read/write a specific slice of your data (or call another service) without handing it the whole tenant.
Outbound HTTP — reach the internet
"grants": { "fetch": { "type": "httpOut", "hosts": ["api.stripe.com", "*.example.com"] } }
This backs fetch (and any named httpOut capability). The host allowlist is
exact or *.suffix (matching the apex too). A disallowed host fails
capability_denied before any I/O — the request never leaves the box.
This requires the deployment to have wired an outbound HTTP adapter (the standard
server does, via the http feature); without one, an httpOut grant yields
501 at request time.
httpOut grants are honored on pipeline and wrapper mounts too: a call
step with an absolute http(s):// URL goes out through the mount's grants,
same allowlist, same capability_denied-before-I/O semantics, with the grant's
inject credential applied host-side (7.3). Without any httpOut grant, a
pipeline cannot call out at all.
Socket — speak a non-HTTP wire protocol (JS only)
"grants": { "db": { "type": "socket", "hosts": ["db.internal:5432", "*.cache.acme:6379"] } }
When fetch isn't enough — you need to speak Postgres, Redis, MongoDB, or any
raw TCP/TLS protocol — a socket grant opens a gated outbound connection. In the
JS sandbox this backs the RS2Socket global (8.3):
const s = RS2Socket.connect("db.internal", 5432, { tls: true });
s.write(bytes); // bytes or a string
const reply = s.read(); // a Uint8Array, or null at EOF
s.close();
The host allowlist entries are host:port, host-only (any port), or
*.suffix[:port] (matching the apex too). A disallowed target fails
capability_denied before the connection is made. connect/read/write
are synchronous from the guest's point of view, like ctx.request.
This is the lower-level building block behind loadable storage adapters
(8.9), where a deployed bundle backs a data/file/query mount by talking to
a database over a socket. Sockets are a JavaScript-engine capability; the Wasm
engine exposes only request/log/state.
Full mount example
{ "path": "/pay", "service": "code:stripe-wrapper@a1b2c3d4",
"config": {
"grants": {
"orders": { "prefix": "/data/orders" },
"fetch": { "type": "httpOut", "hosts": ["api.stripe.com"] }
},
"access": { "read": "all", "write": "A" }
} }
This service can read/write under /data/orders, call api.stripe.com, and
nothing else. Note access (5.4) gates who can call the service; grants
gate what the service can reach. They're independent and you'll usually set
both.
An httpOut grant can also carry inject, which attaches a credential at
the host boundary without exposing it to the guest. Use "infra:<name>" for an
operator-owned credential, or an inline strategy whose secret:<name> leaves
draw from tenant secrets granted to the mount. Supported strategies include
bearer, header, basic, query, HMAC, and AWS SigV4. If all you need is a fixed
forwarding surface with injected auth, the prebuilt proxy service (8.10) is
simpler than custom code.
Names must match
The grant key must match the capability name the code uses. A mismatch
(ctx.request("order", …) against a grant named orders) is treated as
ungranted → capability_denied. The error's capability field tells you which
name was denied — the first thing to check when a grant "isn't working" (8.8).
Why grants re-enter dispatch
Because an internal grant routes back through the front door, a custom service can't use it to bypass auth or limits. If a principal is attached it propagates, so a service acting for a user is held to that user's access. Grants widen reach within the rules, never around them.
Next: actually building, bundling, and deploying a service.
← Previous: 8.4 The WebAssembly contract · Manual home · Next: 8.6 Building, bundling, deploying →