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 →