QUESTPIE

Brokered app access

How a sandboxed guest reaches your collections and files without importing your app, which route carries the calls, why the guest loses its sockets when you turn this on, and the operations the broker still refuses.

View markdown

A compute-only guest is easy to reason about. It gets input, it gets a host allowlist, it returns JSON. The moment it needs your data the question becomes who holds the credentials, and the answer here is that the guest never does.

The three pieces

Pass appBindings and a brokerUrl to run() and the executor mints a short-lived token bound to that pair of capabilities and target, hands the supervisor the broker coordinates, and revokes the token the moment the run settles. The guest sees globalThis.questpie, whose every method call is one RPC out over stdio.

const result = await ctx.executor.run({
	isolation: "sandboxed",
	source: entrySource,
	capabilities: {
		data: { collections: { posts: ["read"] } },
		timeoutMs: 5_000,
	},
	appBindings: target, // the capability-scoped surface you built
	brokerUrl, // from config, never from request input
});
// inside the guest:
//   const posts = await questpie.collections.posts.find({ limit: 10 });

The broker URL comes from config, always

It is the address the supervisor calls carrying the run token. Sourcing it from an inbound request's Host or origin lets a spoofed header mail that token to a host the caller picked. Read it from executor.brokerUrl or SANDBOX_BROKER_URL.

Register the route

The broker endpoint is a route in a module, so it exists only once you add that module. Codegen picks it up from modules.ts like any other.

src/questpie/server/modules.ts
import { sandboxModule } from "@questpie/sandbox/modules/sandbox";

const modules = [sandboxModule] as const;
export default modules;

Run questpie generate and the route lands at sandbox/rpc under your handler's base path, so /api/sandbox/rpc in the starters. That path is what brokerUrl and the supervisor's SANDBOX_BROKER_URL both have to name. Calls arrive with the per-run token in the x-questpie-sandbox-token header, and the broker rechecks the run's capabilities on every one of them.

What the guest can call

CallReaches
questpie.files.read/write/listFiles, scoped by capabilities.files path globs
questpie.collections.<name>.<op>find, findOne, create, update, delete
questpie.store.<name>.<op>The same collection ops against one document_store namespace
questpie.tools.list/callCustom MCP tools you explicitly released

questpie.store.posts.create(x) is exactly questpie.collections.document_store.create({ ...x, store: "posts" }). The sugar saves the argument, it is not a second enforcement path, and the host still clamps the call to the stores the manifest granted.

What it refuses

The manifest types services, jobs and workflows, and the broker will capability-check them, but none of them dispatches. They answer not_implemented, which is a deferral rather than a silent success. Globals are refused earlier still: the four namespaces in the table above are the whole proxy, so guest code has no questpie.globals to call. The broker would refuse a guest's globals.set anyway, because a global is a singleton with no per-tenant boundary to clamp a write against.

Everything else depends on the target you built. The broker dispatches only to handlers present on it, so an operation the manifest allows but the target never wired still resolves to not_implemented. Capability enforcement runs before dispatch, so a missing handler can only narrow access, never widen it.

The guest loses its sockets

Turning bindings on changes the network story completely. The subprocess is spawned with no --allow-net flag at all, which in Deno means no sockets, and the guest's native fetch is replaced by a shim that relays an http.fetch RPC over the same framed channel as the questpie proxy. Your declared net hosts stay on the host side and gate the broker's fetch instead.

That is what buys the stronger egress guarantee. The broker resolves the host itself, validates every A and AAAA record against the private, loopback, link-local, CGNAT and metadata policy, rejects the host outright if any one address fails, then pins the socket to a validated IP literal while keeping the original hostname for SNI and the Host header. No DNS resolution happens at connect time. Redirects are not auto-followed, so every hop is re-parsed, re-resolved, re-validated and re-pinned, and a hop leaving the run's allowlist is blocked mid-chain.

A plain `net` run does not get the pin

A compute-only guest holds a real Deno network grant, and its hosts are validated once at manifest time. Continuous validation is a property of the brokered path, where the guest cannot open a socket in the first place.

Next

Sandbox covers the adapters, the capability manifest and the run result.

On this page