3.5 Capabilities and the sandbox (default-deny)
RS2's safety model rests on one principle: a service can reach only what it is explicitly granted, and nothing else. This is true for prebuilt services and, crucially, for the custom code you (or others) deploy.
Capabilities are the only way out
A service never reaches infrastructure directly — no opening files by absolute path, no arbitrary network. Instead the host hands it pre-scoped capability handles: a file store already bound to this tenant, a data store, an outbound HTTP client limited to certain hosts, durable state, logging. The service uses the handle; it cannot widen it.
For sandboxed custom code, every capability is named and reached through one
call (ctx.request(<name>, …) in JavaScript). There is no other exit from the
sandbox.
Default-deny
A capability that wasn't granted does not exist for that mount. An attempt to
use an ungranted name fails immediately with 403 capability_denied — before any
I/O happens. You opt in to each capability per mount; nothing is ambient.
A custom service's grants are declared on its mount:
{ "path": "/pay", "service": "code:stripe-wrapper@a1b2c3d4",
"config": {
"grants": {
"orders": { "prefix": "/data/orders" },
"fetch": { "type": "httpOut", "hosts": ["api.stripe.com"] }
}
} }
This service can read/write under /data/orders (and only there) and make
outbound calls to api.stripe.com (and only there). Everything else — other
data, other hosts, the filesystem — is denied by omission. Part 8 covers grants
in full.
Grants re-enter the runtime
An internal grant like {"prefix": "/data/orders"} doesn't bypass anything: the
guest's request is appended to the prefix and re-enters full dispatch — the
same routing, auth, limits, and idempotency an external caller gets. A grant is a
scoped door into the front, not a tunnel around the back.
Hard limits and containment
Sandboxing isn't only about what code can reach, but how much it can consume.
Every invocation runs under host-enforced limits — wall-clock time, memory,
outbound call budget, materialized body size. A runaway loop or allocation bomb
is terminated cleanly with 503 limit_exceeded (no process impact), and repeated
breaches trip a per-tenant circuit breaker so a pathological service can't
starve its neighbors. The defaults and the containment guarantees are detailed in
Part 9.
Why this design
Because the model is default-deny with named, re-entrant grants, you can run untrusted or experimental code — including AI-generated services — next to trusted ones without it escaping its mount. The host is the single place that decides what's reachable, which is the same throughline as routing and caching: cross-cutting policy lives in the host, not scattered through services.
Last in this part: what, exactly, fully describes a tenant — the instruction plane.
← Previous: 3.4 Patterns vs. facets · Manual home · Next: 3.6 The instruction plane →