@questpie/sandbox 3.16.0 → 3.18.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-CCmYxOrN.d.mts +259 -0
- package/dist/adapter-http-Coy4jqsj.mjs +865 -0
- package/dist/adapter.d.mts +3 -2
- package/dist/adapter.mjs +3 -2
- package/dist/custom-tools-CbaRJ1ei.d.mts +53 -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 +39 -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
|
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { a as SandboxCustomToolsRunOptions } from "./custom-tools-CbaRJ1ei.mjs";
|
|
2
|
+
import { ExecutorAdapter, ExecutorRunOptions, ExecutorRunResult } from "questpie/executor";
|
|
3
|
+
|
|
4
|
+
//#region src/workload-admission.d.ts
|
|
5
|
+
interface SandboxWorkloadAdmissionClaims {
|
|
6
|
+
readonly kind: "sandbox_workload_admission";
|
|
7
|
+
readonly version: 1;
|
|
8
|
+
readonly admissionId: string;
|
|
9
|
+
readonly supervisorInstanceId: string;
|
|
10
|
+
readonly expiresAt: string;
|
|
11
|
+
readonly requestSha256: string;
|
|
12
|
+
}
|
|
13
|
+
interface SandboxWorkloadAdmissionKey {
|
|
14
|
+
readonly keyId: string;
|
|
15
|
+
readonly secret: Uint8Array;
|
|
16
|
+
/** Unique per supervisor process; rotate on every restart. */
|
|
17
|
+
readonly instanceId: string;
|
|
18
|
+
}
|
|
19
|
+
type SandboxWorkloadAdmissionDenialReason = "invalid" | "wrong_instance" | "expired" | "body_mismatch";
|
|
20
|
+
type SandboxWorkloadAdmissionVerification = {
|
|
21
|
+
readonly ok: true;
|
|
22
|
+
readonly claims: SandboxWorkloadAdmissionClaims;
|
|
23
|
+
} | {
|
|
24
|
+
readonly ok: false;
|
|
25
|
+
readonly reason: SandboxWorkloadAdmissionDenialReason;
|
|
26
|
+
readonly claims?: SandboxWorkloadAdmissionClaims;
|
|
27
|
+
};
|
|
28
|
+
//#endregion
|
|
29
|
+
//#region src/types.d.ts
|
|
30
|
+
/**
|
|
31
|
+
* Wire contract between the `executor` adapter (Bun/Node, main app) and the
|
|
32
|
+
* standalone Deno sandbox server. Kept intentionally minimal: the main app
|
|
33
|
+
* never imports Deno; it only speaks this JSON over HTTP.
|
|
34
|
+
*/
|
|
35
|
+
/**
|
|
36
|
+
* Per-request guest capabilities. `net` and `import` are INDEPENDENT axes
|
|
37
|
+
* (Deno enforces them separately — `--allow-net` gates `fetch`, `--allow-import`
|
|
38
|
+
* gates module imports). The richer app-binding capabilities (`data`, `services`,
|
|
39
|
+
* etc.) live on the core `ExecutorCapabilities` type and are NOT enforced by the
|
|
40
|
+
* sandbox server — they are the later bindings-broker's job.
|
|
41
|
+
*/
|
|
42
|
+
interface SandboxCapabilities {
|
|
43
|
+
/** Runtime `fetch()` allowlist as `host[:port]`. Empty = no network. */
|
|
44
|
+
net: string[];
|
|
45
|
+
/** Module import allowlist as `host[:port]` (e.g. `esm.sh:443`). Empty = no remote imports. */
|
|
46
|
+
import: string[];
|
|
47
|
+
/** Hard wall-clock timeout; the subprocess is force-killed on exceed. */
|
|
48
|
+
timeoutMs: number;
|
|
49
|
+
/** Hard per-guest memory bound (`--v8-flags=--max-old-space-size`). */
|
|
50
|
+
memoryMb: number;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* App-bindings wiring for the UNTRUSTED path. When present, the supervisor
|
|
54
|
+
* exposes a capability-scoped `globalThis.questpie` proxy to the guest whose
|
|
55
|
+
* methods RPC (over STDIO, never the network) to the host broker at {@link url},
|
|
56
|
+
* authenticated by the per-run scoped {@link token}.
|
|
57
|
+
*
|
|
58
|
+
* The token is OPAQUE and is held by the SUPERVISOR only — it is never written
|
|
59
|
+
* into the guest subprocess, so guest code cannot read or forge it to widen
|
|
60
|
+
* scope. The guest only ever sees the typed proxy.
|
|
61
|
+
*
|
|
62
|
+
* Critically, the broker is reached SERVER-TO-SERVER (supervisor → app), NOT by
|
|
63
|
+
* the guest's `fetch`: the guest's egress allowlist rejects loopback/private IPs
|
|
64
|
+
* (the SSRF defense), so a localhost broker is unreachable from guest code by
|
|
65
|
+
* design — we do not poke a hole in that.
|
|
66
|
+
*/
|
|
67
|
+
interface SandboxBindings {
|
|
68
|
+
/** Absolute URL of the host broker endpoint (e.g. `https://app/sandbox/rpc`). */
|
|
69
|
+
url: string;
|
|
70
|
+
/** Per-run opaque scoped token. Supervisor-only; never exposed to the guest. */
|
|
71
|
+
token: string;
|
|
72
|
+
}
|
|
73
|
+
/** POST /run request body. */
|
|
74
|
+
interface SandboxRunRequest {
|
|
75
|
+
/** Explicit transport admission. The supervisor rejects omitted/unknown modes. */
|
|
76
|
+
mode: "workload" | "host";
|
|
77
|
+
/** Guest TypeScript source. Must `export default` a `function(input)`. */
|
|
78
|
+
source: string;
|
|
79
|
+
/** Payload passed to the guest entry function. */
|
|
80
|
+
input: unknown;
|
|
81
|
+
capabilities: SandboxCapabilities;
|
|
82
|
+
/**
|
|
83
|
+
* Secrets injected into the guest (NOT in source). Surfaced to the guest as
|
|
84
|
+
* `globalThis.__secrets`. The sandbox never persists these.
|
|
85
|
+
*/
|
|
86
|
+
secrets?: Record<string, string>;
|
|
87
|
+
/**
|
|
88
|
+
* App-bindings broker wiring. When omitted, the guest gets no `questpie`
|
|
89
|
+
* proxy (the legacy compute-only path). When present, the supervisor brokers
|
|
90
|
+
* the guest's binding RPCs to the host. See {@link SandboxBindings}.
|
|
91
|
+
*/
|
|
92
|
+
bindings?: SandboxBindings;
|
|
93
|
+
}
|
|
94
|
+
/** POST /run response body — also the `ExecutorAdapter.run` result shape. */
|
|
95
|
+
type SandboxRunResult = {
|
|
96
|
+
ok: true;
|
|
97
|
+
output?: unknown;
|
|
98
|
+
logs: string[];
|
|
99
|
+
error?: never;
|
|
100
|
+
timedOut?: never;
|
|
101
|
+
/** Wall-clock duration in ms (server-measured). */
|
|
102
|
+
ms?: number;
|
|
103
|
+
} | {
|
|
104
|
+
ok: false;
|
|
105
|
+
error: string;
|
|
106
|
+
logs: string[];
|
|
107
|
+
output?: never;
|
|
108
|
+
timedOut?: never;
|
|
109
|
+
/** Wall-clock duration in ms (server-measured). */
|
|
110
|
+
ms?: number;
|
|
111
|
+
} | {
|
|
112
|
+
ok: false;
|
|
113
|
+
error: string;
|
|
114
|
+
logs: string[];
|
|
115
|
+
output?: never;
|
|
116
|
+
/** True when the run was killed by the wall-clock timeout. */
|
|
117
|
+
timedOut: true;
|
|
118
|
+
/** Wall-clock duration in ms (server-measured). */
|
|
119
|
+
ms?: number;
|
|
120
|
+
};
|
|
121
|
+
/** Line prefix marking a framed protocol message on guest stdout. */
|
|
122
|
+
declare const FRAME_MARKER = "__QP_SANDBOX_MSG__";
|
|
123
|
+
/**
|
|
124
|
+
* Header carrying the per-run scoped token on the supervisor → broker request.
|
|
125
|
+
* Mirrors `questpie`'s `BINDINGS_TOKEN_HEADER`. Supervisor-only; never set by or
|
|
126
|
+
* exposed to guest code.
|
|
127
|
+
*/
|
|
128
|
+
declare const BINDINGS_TOKEN_HEADER$1 = "x-questpie-sandbox-token";
|
|
129
|
+
/** Host-issued admission binding an authorized workload to one exact request. */
|
|
130
|
+
declare const WORKLOAD_ADMISSION_HEADER = "x-questpie-workload-admission";
|
|
131
|
+
/** Trusted service identity for the structurally separate direct-host path. */
|
|
132
|
+
declare const HOST_ADMISSION_HEADER = "x-questpie-sandbox-host-admission";
|
|
133
|
+
/** Structured error returned by the broker (mirrors `questpie` `BindingError`). */
|
|
134
|
+
interface SandboxBindingError {
|
|
135
|
+
code: string;
|
|
136
|
+
message: string;
|
|
137
|
+
correlationId?: string;
|
|
138
|
+
}
|
|
139
|
+
/** Guest → supervisor: a binding RPC awaiting a brokered result. */
|
|
140
|
+
interface GuestRpcMessage {
|
|
141
|
+
type: "rpc";
|
|
142
|
+
id: number;
|
|
143
|
+
method: string;
|
|
144
|
+
args: unknown;
|
|
145
|
+
}
|
|
146
|
+
/** Guest → supervisor: the run's final result. */
|
|
147
|
+
interface GuestResultMessage {
|
|
148
|
+
type: "result";
|
|
149
|
+
ok: boolean;
|
|
150
|
+
output?: unknown;
|
|
151
|
+
error?: string;
|
|
152
|
+
logs: string[];
|
|
153
|
+
}
|
|
154
|
+
/** Any message the guest emits on stdout (framed). */
|
|
155
|
+
type GuestMessage = GuestRpcMessage | GuestResultMessage;
|
|
156
|
+
/** Supervisor → guest: the brokered outcome of a prior {@link GuestRpcMessage}. */
|
|
157
|
+
interface HostRpcResultMessage {
|
|
158
|
+
type: "rpc-result";
|
|
159
|
+
id: number;
|
|
160
|
+
ok: boolean;
|
|
161
|
+
value?: unknown;
|
|
162
|
+
error?: SandboxBindingError;
|
|
163
|
+
}
|
|
164
|
+
//#endregion
|
|
165
|
+
//#region src/workload.d.ts
|
|
166
|
+
declare const SANDBOX_WORKLOAD_DENIAL_MESSAGE = "The workload is not authorized for this sandbox operation.";
|
|
167
|
+
declare class SandboxWorkloadDeniedError extends Error {
|
|
168
|
+
readonly code: "sandbox_authority_denied";
|
|
169
|
+
constructor();
|
|
170
|
+
}
|
|
171
|
+
interface SandboxWorkloadPolicy {
|
|
172
|
+
readonly source: string;
|
|
173
|
+
readonly input?: unknown;
|
|
174
|
+
readonly capabilities: SandboxCapabilities;
|
|
175
|
+
readonly secrets?: Readonly<Record<string, string>>;
|
|
176
|
+
readonly bindings?: SandboxBindings;
|
|
177
|
+
/** Host-only custom-tool authority. Never serialized to the supervisor. */
|
|
178
|
+
readonly sandboxTools?: SandboxCustomToolsRunOptions;
|
|
179
|
+
/** Optional consumer deadline; transport admission is additionally capped at 5s. */
|
|
180
|
+
readonly validUntil?: string;
|
|
181
|
+
}
|
|
182
|
+
type SandboxWorkloadAuthorizationPhase = "prepare" | "dispatch";
|
|
183
|
+
interface SandboxWorkloadAuthorizationContext {
|
|
184
|
+
readonly phase: SandboxWorkloadAuthorizationPhase;
|
|
185
|
+
readonly signal: AbortSignal;
|
|
186
|
+
}
|
|
187
|
+
type SandboxWorkloadAuthorizer = (envelope: unknown, context: SandboxWorkloadAuthorizationContext) => SandboxWorkloadPolicy | null | Promise<SandboxWorkloadPolicy | null>;
|
|
188
|
+
interface SandboxWorkloadAuditEvent {
|
|
189
|
+
readonly boundary: "sandbox.workload";
|
|
190
|
+
readonly phase: SandboxWorkloadAuthorizationPhase | "transport";
|
|
191
|
+
readonly decision: "allowed" | "denied";
|
|
192
|
+
readonly reason: "authorization_failed" | "authorization_timed_out" | "cancelled" | "egress_denied" | "policy_changed" | "policy_invalid" | "authorization_succeeded" | "transport_completed" | "transport_denied" | "transport_cancelled" | "transport_timed_out" | "transport_invalid_response" | "transport_failed";
|
|
193
|
+
}
|
|
194
|
+
interface SandboxWorkloadAuditContext {
|
|
195
|
+
readonly signal: AbortSignal;
|
|
196
|
+
}
|
|
197
|
+
interface SandboxWorkloadRunOptions {
|
|
198
|
+
/** Consumer-owned opaque value. QUESTPIE never parses or persists it. */
|
|
199
|
+
readonly envelope: unknown;
|
|
200
|
+
readonly signal?: AbortSignal;
|
|
201
|
+
}
|
|
202
|
+
//#endregion
|
|
203
|
+
//#region src/adapter-http.d.ts
|
|
204
|
+
interface WorkloadHttpSandboxOptions {
|
|
205
|
+
readonly authorize: SandboxWorkloadAuthorizer;
|
|
206
|
+
readonly admission: SandboxWorkloadAdmissionKey;
|
|
207
|
+
/** Hard bound around each consumer authorization read. Default 2s, max 5s. */
|
|
208
|
+
readonly authorizationTimeoutMs?: number;
|
|
209
|
+
/** Redacted audit seam. Allowed decisions fail closed when it cannot settle. */
|
|
210
|
+
readonly audit?: (event: SandboxWorkloadAuditEvent, context: SandboxWorkloadAuditContext) => void | Promise<void>;
|
|
211
|
+
/** Hard bound around the audit callback. Default 250ms, max 1s. */
|
|
212
|
+
readonly auditTimeoutMs?: number;
|
|
213
|
+
}
|
|
214
|
+
interface HttpSandboxAdapterOptions {
|
|
215
|
+
/**
|
|
216
|
+
* Base URL of the standalone Deno sandbox service (`sandbox-server.ts`).
|
|
217
|
+
* Defaults to `process.env.SANDBOX_URL`. Throws on `run` if neither is set.
|
|
218
|
+
*/
|
|
219
|
+
url?: string;
|
|
220
|
+
/**
|
|
221
|
+
* Per-request HTTP fetch timeout (ms). A safety margin above the guest
|
|
222
|
+
* wall-timeout so the server's own kill fires first; if the server is
|
|
223
|
+
* unreachable this bounds the client wait. Default: guest timeout + 10s.
|
|
224
|
+
*/
|
|
225
|
+
fetchTimeoutMs?: number;
|
|
226
|
+
/** Optional custom `fetch` (e.g. for auth headers / mTLS). */
|
|
227
|
+
fetch?: typeof fetch;
|
|
228
|
+
/**
|
|
229
|
+
* Validate net/import hosts against the private-IP policy BEFORE sending to
|
|
230
|
+
* the sandbox (defense-in-depth; the server validates too). Default: true.
|
|
231
|
+
*/
|
|
232
|
+
validateEgress?: boolean;
|
|
233
|
+
/** Consumer-owned fail-closed admission for opaque remote workloads. */
|
|
234
|
+
workload?: WorkloadHttpSandboxOptions;
|
|
235
|
+
/** Trusted host credential for the direct framework `run()` path. */
|
|
236
|
+
hostAdmissionSecret?: string;
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Production sandboxed `ExecutorAdapter` — POSTs the run to the standalone Deno
|
|
240
|
+
* sandbox service over HTTP. The main app (Bun/Node) holds NO Deno; it only
|
|
241
|
+
* speaks this JSON contract.
|
|
242
|
+
*
|
|
243
|
+
* Use as the `sandboxed` adapter in the executor config:
|
|
244
|
+
* executor: { sandboxed: httpSandboxAdapter({ url: process.env.SANDBOX_URL }) }
|
|
245
|
+
*/
|
|
246
|
+
declare class HttpSandboxAdapter implements ExecutorAdapter {
|
|
247
|
+
private readonly options;
|
|
248
|
+
private readonly fetchImpl;
|
|
249
|
+
private readonly validateEgress;
|
|
250
|
+
constructor(options?: HttpSandboxAdapterOptions);
|
|
251
|
+
private resolveUrl;
|
|
252
|
+
private postRun;
|
|
253
|
+
run(options: ExecutorRunOptions): Promise<ExecutorRunResult>;
|
|
254
|
+
runWorkload(options: SandboxWorkloadRunOptions): Promise<ExecutorRunResult>;
|
|
255
|
+
}
|
|
256
|
+
/** Factory for the production sandboxed adapter. */
|
|
257
|
+
declare function httpSandboxAdapter(options?: HttpSandboxAdapterOptions): HttpSandboxAdapter;
|
|
258
|
+
//#endregion
|
|
259
|
+
export { SandboxWorkloadAdmissionVerification as A, SandboxCapabilities as C, SandboxWorkloadAdmissionClaims as D, WORKLOAD_ADMISSION_HEADER as E, SandboxWorkloadAdmissionDenialReason as O, SandboxBindings as S, SandboxRunResult as T, GuestResultMessage as _, SANDBOX_WORKLOAD_DENIAL_MESSAGE as a, HostRpcResultMessage as b, SandboxWorkloadAuthorizationContext as c, SandboxWorkloadDeniedError as d, SandboxWorkloadPolicy as f, GuestMessage as g, FRAME_MARKER as h, httpSandboxAdapter as i, SandboxWorkloadAdmissionKey as k, SandboxWorkloadAuthorizationPhase as l, BINDINGS_TOKEN_HEADER$1 as m, HttpSandboxAdapterOptions as n, SandboxWorkloadAuditContext as o, SandboxWorkloadRunOptions as p, WorkloadHttpSandboxOptions as r, SandboxWorkloadAuditEvent as s, HttpSandboxAdapter as t, SandboxWorkloadAuthorizer as u, GuestRpcMessage as v, SandboxRunRequest as w, SandboxBindingError as x, HOST_ADMISSION_HEADER as y };
|