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 →