@questpie/sandbox 3.16.0 → 3.17.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,15 +1,18 @@
1
1
  # @questpie/sandbox
2
2
 
3
- Hardened sandboxed code execution for QUESTPIE executor workloads. The package ships the HTTP adapter used by a QUESTPIE app and the Deno supervisor source that executes untrusted code in a separate process with capability-scoped network, import, file, and app bindings.
3
+ Hardened isolated-script execution for QUESTPIE. The package ships the HTTP
4
+ adapter used by a QUESTPIE app and the Deno supervisor source that executes each
5
+ untrusted script in a fresh, capability-scoped subprocess.
4
6
 
5
7
  ## Features
6
8
 
7
- - **HTTP executor adapter** - connect `ctx.executor.run()` to a standalone sandbox service.
8
- - **Deno supervisor** - run guest code outside the main Bun/Node app process.
9
- - **Capability-scoped bindings** - expose only declared collections, stores, files, and fetch access.
10
- - **SSRF protection** - validate literal IPs, DNS rebinding, redirects, and private/metadata ranges.
11
- - **Network isolation support** - Linux network namespace/firewall planning for defense in depth.
12
- - **Structured failures** - timeouts, non-JSON responses, and network failures return predictable results.
9
+ - HTTP `ExecutorAdapter` for `ctx.executor.run()`
10
+ - fresh Deno subprocess with bounded wall time and memory
11
+ - capability-scoped collections, stores, files, and network bindings
12
+ - explicitly released custom MCP tools through a separate guest namespace
13
+ - SSRF, redirect, private-address, and metadata-address protection
14
+ - optional Linux network namespace/firewall defense in depth
15
+ - generic, consumer-supplied workload authorization
13
16
 
14
17
  ## Installation
15
18
 
@@ -17,10 +20,9 @@ Hardened sandboxed code execution for QUESTPIE executor workloads. The package s
17
20
  bun add @questpie/sandbox
18
21
  ```
19
22
 
20
- ## Configure The Adapter
23
+ ## Trusted framework execution
21
24
 
22
25
  ```ts
23
- // config/app.ts
24
26
  import { httpSandboxAdapter } from "@questpie/sandbox/adapter";
25
27
  import { appConfig } from "questpie";
26
28
 
@@ -28,30 +30,213 @@ export default appConfig({
28
30
  executor: {
29
31
  sandboxed: httpSandboxAdapter({
30
32
  url: process.env.SANDBOX_URL,
33
+ hostAdmissionSecret: process.env.SANDBOX_HOST_ADMISSION_SECRET,
31
34
  }),
32
35
  },
33
36
  });
34
37
  ```
35
38
 
36
- Guest bindings require a host-side broker route in the app or control-plane layer. Compute-only sandboxed execution only needs the adapter above.
39
+ The adapter and supervisor must share the same random
40
+ `SANDBOX_HOST_ADMISSION_SECRET` of at least 32 bytes. Omitting the credential
41
+ fails closed. `SANDBOX_URL` must be a canonical HTTP(S) origin with no userinfo,
42
+ fragment, query, or path prefix. The adapter never follows `/run` redirects, so
43
+ the host admission credential cannot cross origins. Broker coordinates are still minted by QUESTPIE's core executor
44
+ and remain capability-scoped. Any execution with bindings also requires the
45
+ supervisor's canonical `SANDBOX_BROKER_URL`; missing, malformed, or non-matching
46
+ protocol, host, effective port, path, or query is denied before spawn. Userinfo
47
+ and fragments are forbidden; only one trailing path slash is treated as
48
+ equivalent. The supervisor fetches the validated canonical URL, never the raw
49
+ request spelling.
50
+
51
+ Bindings guests always run with `--allow-net=[]`. Their declared `net`
52
+ allowlist remains host-side and is used only by the resolve/validate/pin HTTP
53
+ broker. The backwards-compatible compute-only path may still receive a direct
54
+ Deno network grant.
55
+
56
+ ## Generic workload authorization
57
+
58
+ A remote consumer supplies its own opaque envelope and authorizer. QUESTPIE does
59
+ not prescribe, inspect, or persist the envelope. The authorizer returns the
60
+ complete sandbox policy for one execution; no source, input, process capability,
61
+ secret, or broker binding can be supplied beside the envelope.
37
62
 
38
- ## Run The Supervisor
63
+ ```ts
64
+ import { httpSandboxAdapter } from "@questpie/sandbox";
65
+
66
+ const adapter = httpSandboxAdapter({
67
+ url: process.env.SANDBOX_URL,
68
+ workload: {
69
+ admission: {
70
+ keyId: process.env.SANDBOX_WORKLOAD_ADMISSION_KEY_ID!,
71
+ secret: new TextEncoder().encode(
72
+ process.env.SANDBOX_WORKLOAD_ADMISSION_SECRET!,
73
+ ),
74
+ instanceId: process.env.SANDBOX_INSTANCE_ID!,
75
+ },
76
+ authorize: async (opaqueEnvelope, { phase, signal }) => {
77
+ const authorization = await consumerAuthority.authorize(opaqueEnvelope, {
78
+ phase,
79
+ signal,
80
+ });
81
+ if (!authorization) return null;
82
+
83
+ return {
84
+ source: authorization.source,
85
+ input: authorization.input,
86
+ capabilities: {
87
+ net: authorization.networkHosts,
88
+ import: authorization.importHosts,
89
+ timeoutMs: 5_000,
90
+ memoryMb: 128,
91
+ },
92
+ secrets: {},
93
+ validUntil: authorization.validUntil,
94
+ };
95
+ },
96
+ audit: writeRedactedSandboxDecision,
97
+ },
98
+ });
99
+
100
+ const result = await adapter.runWorkload({ envelope: consumerEnvelope });
101
+ ```
102
+
103
+ The authorizer is called at preparation and immediately before dispatch. Both
104
+ normalized policies must be identical. Authorization and audit callbacks have
105
+ hard deadlines, caller cancellation is propagated, and malformed/missing
106
+ authorization fails closed. Policy input, secrets, bindings, source, combined
107
+ egress hosts, wall time, memory, and the final request body all have hard bounds.
108
+ Cancellation after dispatch aborts broker relays and terminates the guest with a
109
+ bounded `SIGTERM`/`SIGKILL` sequence.
110
+
111
+ The subprocess boundary uses one 5 MiB decoded HTTP body budget. Its
112
+ method-aware request, response, frame, and cumulative-output limits are derived
113
+ from the exact base64 expansion plus bounded JSON-envelope headroom. Native
114
+ binding results and custom-tool results remain capped at 768 KiB. Stderr is
115
+ independently capped at 256 KiB, binding calls at 256 per run, and concurrent
116
+ broker relays at 16. Crossing a limit kills the guest and aborts outstanding
117
+ relays. Broker and supervisor HTTP responses are streamed through byte caps and
118
+ parsed against exact schemas; broker-provided error text is replaced by
119
+ supervisor-owned stable messages before it can enter the guest. Broker and
120
+ `/run` redirects are rejected. Any broker denial or malformed response latches
121
+ a terminal run failure; guest `catch` code cannot turn it back into success. A
122
+ valid result frame is terminal immediately, and a result racing an in-flight
123
+ RPC is rejected.
124
+
125
+ The adapter then signs a minimal, product-neutral transport admission. It is
126
+ bound to the exact request body and one supervisor instance, expires within five
127
+ seconds, and is consumed once. The supervisor rejects missing, forged, expired,
128
+ replayed, wrong-instance, and body-mismatched admissions before spawning a
129
+ subprocess. Consumer audit events distinguish authorization decisions from the
130
+ terminal transport outcome and include only decision plus phase/reason. The
131
+ supervisor's transport log additionally includes its own instance id. Neither
132
+ surface contains the opaque envelope, source, inputs, secrets, or broker token.
133
+
134
+ `SANDBOX_INSTANCE_ID` is a process incarnation, not a deployment or replica id.
135
+ It must be newly generated for every supervisor process and restart. The
136
+ adapter's `SANDBOX_URL` must route directly to the one process named by its
137
+ configured instance id; do not put this admission path behind an unpinned
138
+ round-robin load balancer. Reusing one instance id across replicas is forbidden.
139
+ Replay state is deliberately process-local because the admission is
140
+ cryptographically bound to that unique process incarnation.
141
+
142
+ ## Custom tools
143
+
144
+ Custom `mcpTool(...)` definitions can be exposed to a sandbox without turning
145
+ MCP CRUD into the sandbox data API. Collections, files, and stores keep using
146
+ their native broker primitives; only explicitly released custom tools enter the
147
+ separate `questpie.tools` namespace.
148
+
149
+ Configure the host-side consumer authorization port in `questpie.config.ts`:
150
+
151
+ ```ts
152
+ import { sandboxCustomTools } from "@questpie/sandbox";
153
+ import { runtimeConfig } from "questpie/app";
154
+
155
+ export default runtimeConfig({
156
+ // db, executor, …
157
+ sandboxCustomTools: sandboxCustomTools({
158
+ authorizer: consumerToolAuthority,
159
+ contextBinder: consumerQuestpieContextBinder,
160
+ evidence: async (event) => {
161
+ // Product-neutral: phase, tool name, result, reason, duration only.
162
+ await evidenceSink.write(event);
163
+ },
164
+ }),
165
+ });
166
+ ```
167
+
168
+ The tool itself must be in the released MCP custom-tool catalog and declare an
169
+ explicit workload requirement. Omitted exposure or workload policy remains
170
+ invisible and uncallable. Every call repeats consumer authorization, binds a
171
+ fresh user-mode QUESTPIE context, rechecks the tool access/scope policy, parses
172
+ the original input schema, and invokes the original handler.
173
+
174
+ The host supplies only an opaque envelope for the run:
175
+
176
+ ```ts
177
+ const result = await ctx.executor.run({
178
+ isolation: "sandboxed",
179
+ source: `
180
+ export default async () => {
181
+ const { tools } = await globalThis.questpie.tools.list();
182
+ return globalThis.questpie.tools.call("reports.generate", {
183
+ period: "week",
184
+ });
185
+ }
186
+ `,
187
+ brokerUrl: "https://app.internal/api/sandbox/rpc",
188
+ sandboxTools: { envelope: consumerEnvelope },
189
+ capabilities: {
190
+ net: [],
191
+ import: [],
192
+ timeoutMs: 5_000,
193
+ memoryMb: 128,
194
+ },
195
+ });
196
+ ```
197
+
198
+ The adapter keeps the envelope in the app host. The broker endpoint and token
199
+ are sent only to the trusted supervisor through the existing bindings
200
+ coordinate; none of them is exposed to guest code or `globalThis.questpie`.
201
+ Discovery, arguments, results, operation count, wall time, and concurrency are
202
+ bounded. Evidence callbacks receive no envelope, arguments, result body,
203
+ bearer credential, app instance, database handle, or request context.
204
+
205
+ Defaults/hard maxima are: 64/256 discovered tools, 256 KiB/768 KiB discovery
206
+ payload, 32 KiB/63 KiB arguments, 256 KiB/768 KiB result, 64/256 operations,
207
+ 5 s/30 s per operation, 4/16 concurrent operations, and 250 ms/1 s for
208
+ observational evidence. Overrides must be positive safe integers at or below
209
+ the hard maximum; `sandboxCustomTools(...)` rejects invalid configuration.
210
+ Denied operations consume the call budget. A timed-out handler keeps its
211
+ concurrency slot until the underlying MCP call settles, even if it ignores
212
+ cancellation.
39
213
 
40
- The Deno supervisor source is published with the package:
214
+ ## Run the supervisor
41
215
 
42
216
  ```bash
43
- deno run --allow-net --allow-env --allow-run --allow-read --allow-write=$TMPDIR \
217
+ export SANDBOX_WORKLOAD_ADMISSION_KEY_ID=sandbox-workload-v1
218
+ export SANDBOX_WORKLOAD_ADMISSION_SECRET='replace-with-at-least-32-random-bytes'
219
+ export SANDBOX_HOST_ADMISSION_SECRET='replace-with-another-32-byte-secret'
220
+ # Generate a new value for every supervisor process/restart.
221
+ export SANDBOX_INSTANCE_ID=sandbox_instance_01HXYZ
222
+ # Required whenever a policy can receive QUESTPIE broker bindings.
223
+ export SANDBOX_BROKER_URL='https://app.internal/api/sandbox/rpc'
224
+
225
+ deno run --allow-net --allow-env --allow-run --allow-read \
226
+ --allow-write=$TMPDIR \
44
227
  node_modules/@questpie/sandbox/src/sandbox-server.ts
45
228
  ```
46
229
 
47
- Set `SANDBOX_URL` in the app to the supervisor URL.
230
+ Set `SANDBOX_URL` in the app to the directly addressed URL of that supervisor
231
+ instance.
48
232
 
49
233
  ## Exports
50
234
 
51
- | Entry Point | Purpose |
52
- | --------------------------- | ------------------------------------------ |
53
- | `@questpie/sandbox` | Adapter, validation, and binding utilities |
54
- | `@questpie/sandbox/adapter` | `httpSandboxAdapter()` only |
235
+ | Entry point | Purpose |
236
+ | ----------------------------------- | --------------------------------------------- |
237
+ | `@questpie/sandbox` | Adapter, authorization, validation, bindings |
238
+ | `@questpie/sandbox/adapter` | HTTP adapter and workload authorization types |
239
+ | `@questpie/sandbox/modules/sandbox` | Generated broker route module |
55
240
 
56
241
  ## License
57
242