@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 +203 -18
- package/dist/adapter-http-Coy4jqsj.mjs +865 -0
- package/dist/adapter-http-DDeL-Ym3.d.mts +259 -0
- package/dist/adapter.d.mts +3 -2
- package/dist/adapter.mjs +3 -2
- package/dist/custom-tools-CgfgyVNv.d.mts +55 -0
- package/dist/custom-tools-DxI7JvUn.mjs +407 -0
- package/dist/index.d.mts +40 -115
- package/dist/index.mjs +11 -14
- package/dist/modules/sandbox.d.mts +40 -0
- package/dist/modules/sandbox.mjs +211 -0
- package/package.json +20 -5
- package/src/broker-wire.ts +94 -0
- package/src/egress-firewall.ts +9 -3
- package/src/guest-bindings.ts +43 -1
- package/src/guest-entry.ts +132 -22
- package/src/guest-runtime-source.ts +38 -0
- package/src/net-validation.ts +204 -51
- package/src/sandbox-server.ts +860 -144
- package/src/server-internals.ts +195 -12
- package/src/types.ts +38 -10
- package/src/workload-admission.ts +289 -0
- package/src/workload.ts +441 -0
- package/dist/adapter-http-7ypWHBck.d.mts +0 -43
- package/dist/adapter-http-w7SjAfcC.mjs +0 -298
package/README.md
CHANGED
|
@@ -1,15 +1,18 @@
|
|
|
1
1
|
# @questpie/sandbox
|
|
2
2
|
|
|
3
|
-
Hardened
|
|
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
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
214
|
+
## Run the supervisor
|
|
41
215
|
|
|
42
216
|
```bash
|
|
43
|
-
|
|
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
|
|
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
|
|
52
|
-
|
|
|
53
|
-
| `@questpie/sandbox`
|
|
54
|
-
| `@questpie/sandbox/adapter`
|
|
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
|
|