@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
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
import { createWorkloadMcpToolPort, mcpPublicErrorCode } from "@questpie/mcp";
|
|
2
|
+
|
|
3
|
+
//#region src/broker-wire.ts
|
|
4
|
+
/**
|
|
5
|
+
* One place for the sandbox broker's decoded payload and derived wire budgets.
|
|
6
|
+
*
|
|
7
|
+
* `http.fetch` bodies are binary-to-base64, then nested in JSON RPC envelopes.
|
|
8
|
+
* The transport limits therefore cannot equal the decoded body limit. Keep all
|
|
9
|
+
* supervisor, route, frame, and custom-tool caps derived here.
|
|
10
|
+
*/
|
|
11
|
+
/** Decoded request/response body limit promised by `http.fetch`. */
|
|
12
|
+
const BROKER_HTTP_BODY_CAP_BYTES = 5 * 1024 * 1024;
|
|
13
|
+
/** Exact maximum base64 length for a decoded HTTP body at the public cap. */
|
|
14
|
+
const BROKER_HTTP_BASE64_CAP_BYTES = 4 * Math.ceil(BROKER_HTTP_BODY_CAP_BYTES / 3);
|
|
15
|
+
/**
|
|
16
|
+
* Room for URL, method, bounded HTTP headers, status metadata, and JSON RPC
|
|
17
|
+
* envelope syntax around a maximum-size base64 body.
|
|
18
|
+
*/
|
|
19
|
+
const BROKER_HTTP_JSON_HEADROOM_BYTES = 128 * 1024;
|
|
20
|
+
/** Whole JSON request/response budget for the `http.fetch` method. */
|
|
21
|
+
const BROKER_HTTP_WIRE_CAP_BYTES = BROKER_HTTP_BASE64_CAP_BYTES + BROKER_HTTP_JSON_HEADROOM_BYTES;
|
|
22
|
+
/** Existing small native binding request budget, including its JSON envelope. */
|
|
23
|
+
const BROKER_NATIVE_REQUEST_WIRE_CAP_BYTES = 64 * 1024;
|
|
24
|
+
/** Maximum JSON value returned by native bindings before envelope overhead. */
|
|
25
|
+
const BROKER_NATIVE_RESULT_CAP_BYTES = 768 * 1024;
|
|
26
|
+
/** Custom-tool logical limits; shared with the host session implementation. */
|
|
27
|
+
const BROKER_CUSTOM_ARGUMENT_CAP_BYTES = 63 * 1024;
|
|
28
|
+
const BROKER_CUSTOM_RESULT_CAP_BYTES = 768 * 1024;
|
|
29
|
+
const BROKER_CUSTOM_LIST_CAP_BYTES = 768 * 1024;
|
|
30
|
+
const BROKER_JSON_ENVELOPE_HEADROOM_BYTES = 4 * 1024;
|
|
31
|
+
const BROKER_NATIVE_RESPONSE_WIRE_CAP_BYTES = BROKER_NATIVE_RESULT_CAP_BYTES + BROKER_JSON_ENVELOPE_HEADROOM_BYTES;
|
|
32
|
+
const BROKER_CUSTOM_REQUEST_WIRE_CAP_BYTES = BROKER_CUSTOM_ARGUMENT_CAP_BYTES + BROKER_JSON_ENVELOPE_HEADROOM_BYTES;
|
|
33
|
+
const BROKER_CUSTOM_RESPONSE_WIRE_CAP_BYTES = Math.max(BROKER_CUSTOM_RESULT_CAP_BYTES, BROKER_CUSTOM_LIST_CAP_BYTES) + BROKER_JSON_ENVELOPE_HEADROOM_BYTES;
|
|
34
|
+
/** Maximum body the app broker route will read before method-aware validation. */
|
|
35
|
+
const BROKER_MAX_REQUEST_WIRE_BYTES = Math.max(BROKER_HTTP_WIRE_CAP_BYTES, BROKER_NATIVE_REQUEST_WIRE_CAP_BYTES, BROKER_CUSTOM_REQUEST_WIRE_CAP_BYTES);
|
|
36
|
+
/** Maximum response the supervisor will read before method-aware validation. */
|
|
37
|
+
const BROKER_MAX_RESPONSE_WIRE_BYTES = Math.max(BROKER_HTTP_WIRE_CAP_BYTES, BROKER_NATIVE_RESPONSE_WIRE_CAP_BYTES, BROKER_CUSTOM_RESPONSE_WIRE_CAP_BYTES);
|
|
38
|
+
/** Prefix/newline room around the largest JSON RPC frame. */
|
|
39
|
+
const BROKER_FRAME_CAP_BYTES = Math.max(BROKER_MAX_REQUEST_WIRE_BYTES, BROKER_MAX_RESPONSE_WIRE_BYTES) + 1024;
|
|
40
|
+
/**
|
|
41
|
+
* A run may emit one maximum RPC request and still return a maximum-sized final
|
|
42
|
+
* frame. Repeated large calls remain bounded by this cumulative budget.
|
|
43
|
+
*/
|
|
44
|
+
const BROKER_TOTAL_OUTPUT_CAP_BYTES = BROKER_FRAME_CAP_BYTES * 2;
|
|
45
|
+
function brokerRequestWireCap(method) {
|
|
46
|
+
if (method === "http.fetch") return BROKER_HTTP_WIRE_CAP_BYTES;
|
|
47
|
+
if (method === "tools.call") return BROKER_CUSTOM_REQUEST_WIRE_CAP_BYTES;
|
|
48
|
+
if (method === "tools.list") return BROKER_JSON_ENVELOPE_HEADROOM_BYTES;
|
|
49
|
+
return BROKER_NATIVE_REQUEST_WIRE_CAP_BYTES;
|
|
50
|
+
}
|
|
51
|
+
function brokerResponseWireCap(method) {
|
|
52
|
+
if (method === "http.fetch") return BROKER_HTTP_WIRE_CAP_BYTES;
|
|
53
|
+
if (method === "tools.call" || method === "tools.list") return BROKER_CUSTOM_RESPONSE_WIRE_CAP_BYTES;
|
|
54
|
+
return BROKER_NATIVE_RESPONSE_WIRE_CAP_BYTES;
|
|
55
|
+
}
|
|
56
|
+
function brokerResultValueCap(method) {
|
|
57
|
+
if (method === "http.fetch") return BROKER_HTTP_WIRE_CAP_BYTES - BROKER_JSON_ENVELOPE_HEADROOM_BYTES;
|
|
58
|
+
if (method === "tools.call") return BROKER_CUSTOM_RESULT_CAP_BYTES;
|
|
59
|
+
if (method === "tools.list") return BROKER_CUSTOM_LIST_CAP_BYTES;
|
|
60
|
+
return BROKER_NATIVE_RESULT_CAP_BYTES;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/server-internals.ts
|
|
65
|
+
const MAX_RESULT_DEPTH = 32;
|
|
66
|
+
const MAX_RESULT_NODES = 1e4;
|
|
67
|
+
const MAX_RESULT_JSON_BYTES = 1048576;
|
|
68
|
+
const MAX_RESULT_LOGS = 1e3;
|
|
69
|
+
const MAX_RESULT_LOG_BYTES = 4e3;
|
|
70
|
+
const MAX_TOTAL_LOG_BYTES = 1048576;
|
|
71
|
+
function exactKeys(value, required, optional) {
|
|
72
|
+
const keys = Object.keys(value);
|
|
73
|
+
return required.every((key) => keys.includes(key)) && keys.every((key) => required.includes(key) || optional.includes(key));
|
|
74
|
+
}
|
|
75
|
+
function isBoundedJson(value, budget = {
|
|
76
|
+
nodes: 0,
|
|
77
|
+
bytes: 0
|
|
78
|
+
}, depth = 0, maximumBytes = MAX_RESULT_JSON_BYTES) {
|
|
79
|
+
if (depth > MAX_RESULT_DEPTH) return false;
|
|
80
|
+
budget.nodes += 1;
|
|
81
|
+
if (budget.nodes > MAX_RESULT_NODES) return false;
|
|
82
|
+
if (value === null || typeof value === "boolean") budget.bytes += 5;
|
|
83
|
+
else if (typeof value === "number") {
|
|
84
|
+
if (!Number.isFinite(value)) return false;
|
|
85
|
+
budget.bytes += 24;
|
|
86
|
+
} else if (typeof value === "string") budget.bytes += new TextEncoder().encode(value).byteLength;
|
|
87
|
+
else if (Array.isArray(value)) {
|
|
88
|
+
budget.bytes += 2;
|
|
89
|
+
for (const item of value) if (!isBoundedJson(item, budget, depth + 1, maximumBytes)) return false;
|
|
90
|
+
} else if (typeof value === "object" && value !== null) {
|
|
91
|
+
budget.bytes += 2;
|
|
92
|
+
for (const [key, item] of Object.entries(value)) {
|
|
93
|
+
budget.bytes += new TextEncoder().encode(key).byteLength;
|
|
94
|
+
if (!isBoundedJson(item, budget, depth + 1, maximumBytes)) return false;
|
|
95
|
+
}
|
|
96
|
+
} else return false;
|
|
97
|
+
return budget.bytes <= maximumBytes;
|
|
98
|
+
}
|
|
99
|
+
function parseSandboxRunResult(value) {
|
|
100
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
101
|
+
const result = value;
|
|
102
|
+
if (!exactKeys(result, ["ok", "logs"], [
|
|
103
|
+
"output",
|
|
104
|
+
"error",
|
|
105
|
+
"timedOut",
|
|
106
|
+
"ms"
|
|
107
|
+
]) || typeof result.ok !== "boolean" || !Array.isArray(result.logs) || result.logs.length > MAX_RESULT_LOGS || result.error !== void 0 && typeof result.error !== "string" || result.timedOut !== void 0 && typeof result.timedOut !== "boolean" || result.ms !== void 0 && (typeof result.ms !== "number" || !Number.isFinite(result.ms) || result.ms < 0)) return null;
|
|
108
|
+
if (result.ok === true && (Object.hasOwn(result, "error") || Object.hasOwn(result, "timedOut")) || result.ok === false && (typeof result.error !== "string" || Object.hasOwn(result, "output") || Object.hasOwn(result, "timedOut") && result.timedOut !== true)) return null;
|
|
109
|
+
let logBytes = 0;
|
|
110
|
+
for (const log of result.logs) {
|
|
111
|
+
if (typeof log !== "string") return null;
|
|
112
|
+
const bytes = new TextEncoder().encode(log).byteLength;
|
|
113
|
+
logBytes += bytes;
|
|
114
|
+
if (bytes > MAX_RESULT_LOG_BYTES || logBytes > MAX_TOTAL_LOG_BYTES) return null;
|
|
115
|
+
}
|
|
116
|
+
if (result.output !== void 0 && !isBoundedJson(result.output)) return null;
|
|
117
|
+
return {
|
|
118
|
+
ok: result.ok,
|
|
119
|
+
...result.output !== void 0 ? { output: result.output } : {},
|
|
120
|
+
...typeof result.error === "string" ? { error: result.error } : {},
|
|
121
|
+
logs: result.logs,
|
|
122
|
+
...typeof result.timedOut === "boolean" ? { timedOut: result.timedOut } : {},
|
|
123
|
+
...typeof result.ms === "number" ? { ms: result.ms } : {}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
const BROKER_ERROR_MESSAGES = Object.freeze({
|
|
127
|
+
bad_method: "invalid sandbox binding method",
|
|
128
|
+
bad_args: "invalid sandbox binding arguments",
|
|
129
|
+
unauthorized: "sandbox binding authorization failed",
|
|
130
|
+
forbidden: "sandbox binding operation forbidden",
|
|
131
|
+
not_implemented: "sandbox binding operation unavailable",
|
|
132
|
+
execution_error: "sandbox binding operation failed"
|
|
133
|
+
});
|
|
134
|
+
/** Normalize a broker URL for comparison (drop a trailing slash; ignore case of origin). */
|
|
135
|
+
function normalizeBrokerUrl(raw) {
|
|
136
|
+
try {
|
|
137
|
+
const u = new URL(raw);
|
|
138
|
+
if (u.protocol !== "http:" && u.protocol !== "https:" || u.username.length > 0 || u.password.length > 0 || u.hash.length > 0) return null;
|
|
139
|
+
const path = u.pathname.replace(/\/$/, "");
|
|
140
|
+
return `${u.origin}${path}${u.search}`;
|
|
141
|
+
} catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region src/custom-tools.ts
|
|
148
|
+
const DEFAULT_MAX_TOOLS = 64;
|
|
149
|
+
const HARD_MAX_TOOLS = 256;
|
|
150
|
+
const DEFAULT_MAX_LIST_BYTES = 256 * 1024;
|
|
151
|
+
const HARD_MAX_LIST_BYTES = BROKER_CUSTOM_LIST_CAP_BYTES;
|
|
152
|
+
const DEFAULT_MAX_ARGUMENT_BYTES = 32 * 1024;
|
|
153
|
+
const HARD_MAX_ARGUMENT_BYTES = BROKER_CUSTOM_ARGUMENT_CAP_BYTES;
|
|
154
|
+
const DEFAULT_MAX_RESULT_BYTES = 256 * 1024;
|
|
155
|
+
const HARD_MAX_RESULT_BYTES = BROKER_CUSTOM_RESULT_CAP_BYTES;
|
|
156
|
+
const DEFAULT_MAX_CALLS = 64;
|
|
157
|
+
const HARD_MAX_CALLS = 256;
|
|
158
|
+
const DEFAULT_TIMEOUT_MS = 5e3;
|
|
159
|
+
const HARD_MAX_TIMEOUT_MS = 3e4;
|
|
160
|
+
const DEFAULT_CONCURRENCY = 4;
|
|
161
|
+
const HARD_MAX_CONCURRENCY = 16;
|
|
162
|
+
const DEFAULT_EVIDENCE_TIMEOUT_MS = 250;
|
|
163
|
+
const HARD_MAX_EVIDENCE_TIMEOUT_MS = 1e3;
|
|
164
|
+
const MAX_ACTIVE_TOOL_SESSIONS = 1024;
|
|
165
|
+
const MAX_EXPIRED_SESSION_CLEANUP_SCAN = 64;
|
|
166
|
+
function sandboxCustomTools(config) {
|
|
167
|
+
if (!resolveLimits(config)) throw new Error("sandbox custom-tool limits are invalid");
|
|
168
|
+
return config;
|
|
169
|
+
}
|
|
170
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
171
|
+
const encoder = new TextEncoder();
|
|
172
|
+
function closeSession(token, session) {
|
|
173
|
+
session.revoked = true;
|
|
174
|
+
if (sessions.get(token) === session) sessions.delete(token);
|
|
175
|
+
session.port = void 0;
|
|
176
|
+
session.envelope = void 0;
|
|
177
|
+
}
|
|
178
|
+
function cleanupExpiredSessions(now) {
|
|
179
|
+
const candidates = [];
|
|
180
|
+
for (const [token, session] of sessions) {
|
|
181
|
+
candidates.push([token, session]);
|
|
182
|
+
if (candidates.length >= MAX_EXPIRED_SESSION_CLEANUP_SCAN) break;
|
|
183
|
+
}
|
|
184
|
+
for (const [token, session] of candidates) if (session.revoked || now >= session.expiresAt) closeSession(token, session);
|
|
185
|
+
else if (sessions.get(token) === session) {
|
|
186
|
+
sessions.delete(token);
|
|
187
|
+
sessions.set(token, session);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function boundedPositiveInteger(value, fallback, maximum) {
|
|
191
|
+
if (value === void 0) return fallback;
|
|
192
|
+
return Number.isSafeInteger(value) && value >= 1 && value <= maximum ? value : null;
|
|
193
|
+
}
|
|
194
|
+
function resolveLimits(config) {
|
|
195
|
+
const limits = config.limits;
|
|
196
|
+
const maxTools = boundedPositiveInteger(limits?.maxTools, DEFAULT_MAX_TOOLS, HARD_MAX_TOOLS);
|
|
197
|
+
const maxListBytes = boundedPositiveInteger(limits?.maxListBytes, DEFAULT_MAX_LIST_BYTES, HARD_MAX_LIST_BYTES);
|
|
198
|
+
const maxArgumentBytes = boundedPositiveInteger(limits?.maxArgumentBytes, DEFAULT_MAX_ARGUMENT_BYTES, HARD_MAX_ARGUMENT_BYTES);
|
|
199
|
+
const maxResultBytes = boundedPositiveInteger(limits?.maxResultBytes, DEFAULT_MAX_RESULT_BYTES, HARD_MAX_RESULT_BYTES);
|
|
200
|
+
const maxCalls = boundedPositiveInteger(limits?.maxCalls, DEFAULT_MAX_CALLS, HARD_MAX_CALLS);
|
|
201
|
+
const timeoutMs = boundedPositiveInteger(limits?.timeoutMs, DEFAULT_TIMEOUT_MS, HARD_MAX_TIMEOUT_MS);
|
|
202
|
+
const concurrency = boundedPositiveInteger(limits?.concurrency, DEFAULT_CONCURRENCY, HARD_MAX_CONCURRENCY);
|
|
203
|
+
const evidenceTimeoutMs = boundedPositiveInteger(config.evidenceTimeoutMs, DEFAULT_EVIDENCE_TIMEOUT_MS, HARD_MAX_EVIDENCE_TIMEOUT_MS);
|
|
204
|
+
if (maxTools === null || maxListBytes === null || maxArgumentBytes === null || maxResultBytes === null || maxCalls === null || timeoutMs === null || concurrency === null || evidenceTimeoutMs === null) return null;
|
|
205
|
+
return {
|
|
206
|
+
maxTools,
|
|
207
|
+
maxListBytes,
|
|
208
|
+
maxArgumentBytes,
|
|
209
|
+
maxResultBytes,
|
|
210
|
+
maxCalls,
|
|
211
|
+
timeoutMs,
|
|
212
|
+
concurrency,
|
|
213
|
+
evidenceTimeoutMs
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function error(code, message) {
|
|
217
|
+
return {
|
|
218
|
+
ok: false,
|
|
219
|
+
error: {
|
|
220
|
+
code,
|
|
221
|
+
message
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
function isRecord(value) {
|
|
226
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
227
|
+
}
|
|
228
|
+
function hasExactKeys(value, required, optional = []) {
|
|
229
|
+
const keys = Object.keys(value);
|
|
230
|
+
return required.every((key) => keys.includes(key)) && keys.every((key) => required.includes(key) || optional.includes(key));
|
|
231
|
+
}
|
|
232
|
+
function jsonBytes(value) {
|
|
233
|
+
try {
|
|
234
|
+
const serialized = JSON.stringify(value);
|
|
235
|
+
if (serialized === void 0) return null;
|
|
236
|
+
return encoder.encode(serialized).byteLength;
|
|
237
|
+
} catch {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
var DeadlineError = class extends Error {};
|
|
242
|
+
var CancelledError = class extends Error {};
|
|
243
|
+
async function within(work, timeoutMs, parent, onWorkSettled) {
|
|
244
|
+
const controller = new AbortController();
|
|
245
|
+
const cancel = () => controller.abort(new CancelledError());
|
|
246
|
+
if (parent?.aborted) cancel();
|
|
247
|
+
else parent?.addEventListener("abort", cancel, { once: true });
|
|
248
|
+
let timer;
|
|
249
|
+
const operation = Promise.resolve().then(() => work(controller.signal));
|
|
250
|
+
if (onWorkSettled) operation.then(onWorkSettled, onWorkSettled);
|
|
251
|
+
try {
|
|
252
|
+
return await Promise.race([operation, new Promise((_, reject) => {
|
|
253
|
+
timer = setTimeout(() => {
|
|
254
|
+
const deadline = new DeadlineError();
|
|
255
|
+
controller.abort(deadline);
|
|
256
|
+
reject(deadline);
|
|
257
|
+
}, timeoutMs);
|
|
258
|
+
controller.signal.addEventListener("abort", () => reject(controller.signal.reason ?? new CancelledError()), { once: true });
|
|
259
|
+
if (controller.signal.aborted) reject(controller.signal.reason ?? new CancelledError());
|
|
260
|
+
})]);
|
|
261
|
+
} finally {
|
|
262
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
263
|
+
parent?.removeEventListener("abort", cancel);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
async function emitEvidence(config, limits, event) {
|
|
267
|
+
if (!config.evidence) return;
|
|
268
|
+
const controller = new AbortController();
|
|
269
|
+
let timer;
|
|
270
|
+
try {
|
|
271
|
+
await Promise.race([Promise.resolve(config.evidence(Object.freeze(event), { signal: controller.signal })), new Promise((resolve) => {
|
|
272
|
+
timer = setTimeout(() => {
|
|
273
|
+
controller.abort();
|
|
274
|
+
resolve();
|
|
275
|
+
}, limits.evidenceTimeoutMs);
|
|
276
|
+
})]);
|
|
277
|
+
} catch {} finally {
|
|
278
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function registerSandboxCustomToolsSession(token, brokerUrl, envelope, ttlMs) {
|
|
282
|
+
const now = Date.now();
|
|
283
|
+
cleanupExpiredSessions(now);
|
|
284
|
+
const endpoint = normalizeBrokerUrl(brokerUrl);
|
|
285
|
+
if (typeof token !== "string" || token.length === 0 || encoder.encode(token).byteLength > 8192 || endpoint === null || !Number.isSafeInteger(ttlMs) || ttlMs <= 0 || sessions.has(token) || sessions.size >= MAX_ACTIVE_TOOL_SESSIONS) throw new Error("sandbox custom-tool session could not be registered");
|
|
286
|
+
const session = {
|
|
287
|
+
envelope,
|
|
288
|
+
endpoint,
|
|
289
|
+
expiresAt: now + Math.min(Math.round(ttlMs), 5 * 6e4),
|
|
290
|
+
calls: 0,
|
|
291
|
+
inFlight: 0,
|
|
292
|
+
revoked: false
|
|
293
|
+
};
|
|
294
|
+
sessions.set(token, session);
|
|
295
|
+
return { revoke() {
|
|
296
|
+
if (session.revoked) return;
|
|
297
|
+
closeSession(token, session);
|
|
298
|
+
} };
|
|
299
|
+
}
|
|
300
|
+
function resolveSession(token, requestUrl) {
|
|
301
|
+
if (!token) return null;
|
|
302
|
+
const session = sessions.get(token);
|
|
303
|
+
if (!session || session.revoked) return null;
|
|
304
|
+
if (normalizeBrokerUrl(requestUrl) !== session.endpoint) return null;
|
|
305
|
+
if (Date.now() >= session.expiresAt) {
|
|
306
|
+
closeSession(token, session);
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
return session;
|
|
310
|
+
}
|
|
311
|
+
function createToolPort(app, config, envelope) {
|
|
312
|
+
return createWorkloadMcpToolPort(app, {
|
|
313
|
+
envelope,
|
|
314
|
+
authorizer: config.authorizer,
|
|
315
|
+
contextBinder: config.contextBinder,
|
|
316
|
+
audit: config.authorizationAudit,
|
|
317
|
+
handoff: config.handoff
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
function mcpResultCode(result) {
|
|
321
|
+
const meta = result["_meta"];
|
|
322
|
+
const detail = isRecord(meta) && isRecord(meta["questpie/error"]) ? meta["questpie/error"] : void 0;
|
|
323
|
+
const descriptor = detail ? Object.getOwnPropertyDescriptor(detail, "code") : void 0;
|
|
324
|
+
const code = descriptor && "value" in descriptor ? descriptor.value : void 0;
|
|
325
|
+
return [
|
|
326
|
+
"access_denied",
|
|
327
|
+
"invalid_input",
|
|
328
|
+
"input_too_large",
|
|
329
|
+
"output_too_large",
|
|
330
|
+
"timeout",
|
|
331
|
+
"cancelled",
|
|
332
|
+
"busy",
|
|
333
|
+
"internal"
|
|
334
|
+
].includes(code) ? code : void 0;
|
|
335
|
+
}
|
|
336
|
+
async function handleSandboxCustomToolsRpc({ app, config, token, requestUrl, method, args, signal }) {
|
|
337
|
+
const session = resolveSession(token, requestUrl);
|
|
338
|
+
if (!session || !config) return error("unauthorized", "sandbox custom tool session is unavailable");
|
|
339
|
+
if (method !== "tools.list" && method !== "tools.call") return error("bad_method", "invalid sandbox custom tool method");
|
|
340
|
+
const limits = resolveLimits(config);
|
|
341
|
+
if (!limits) return error("execution_error", "sandbox custom tool configuration is invalid");
|
|
342
|
+
const phase = method === "tools.list" ? "discovery" : "call";
|
|
343
|
+
const started = performance.now();
|
|
344
|
+
let toolName;
|
|
345
|
+
const finish = async (result, evidenceResult, reason) => {
|
|
346
|
+
await emitEvidence(config, limits, {
|
|
347
|
+
boundary: "sandbox.tool",
|
|
348
|
+
phase,
|
|
349
|
+
...toolName ? { toolName } : {},
|
|
350
|
+
result: evidenceResult,
|
|
351
|
+
reason,
|
|
352
|
+
durationMs: Math.max(0, Math.round(performance.now() - started))
|
|
353
|
+
});
|
|
354
|
+
return result;
|
|
355
|
+
};
|
|
356
|
+
if (session.calls >= limits.maxCalls) return finish(error("forbidden", "sandbox custom tool limit exceeded"), "denied", "limit_exceeded");
|
|
357
|
+
session.calls += 1;
|
|
358
|
+
const requestId = session.calls;
|
|
359
|
+
if (session.inFlight >= limits.concurrency) return finish(error("forbidden", "sandbox custom tool limit exceeded"), "denied", "limit_exceeded");
|
|
360
|
+
if (method === "tools.list") {
|
|
361
|
+
if (!isRecord(args) || !hasExactKeys(args, [])) return finish(error("bad_args", "invalid sandbox custom tool arguments"), "denied", "invalid_request");
|
|
362
|
+
} else {
|
|
363
|
+
if (!isRecord(args) || !hasExactKeys(args, ["name"], ["arguments"]) || typeof args.name !== "string" || args.name.length === 0 || encoder.encode(args.name).byteLength > 256 || args.arguments !== void 0 && !isRecord(args.arguments)) return finish(error("bad_args", "invalid sandbox custom tool arguments"), "denied", "invalid_request");
|
|
364
|
+
toolName = args.name;
|
|
365
|
+
const argumentBytes = jsonBytes(args.arguments ?? {});
|
|
366
|
+
if (argumentBytes === null || argumentBytes > limits.maxArgumentBytes) return finish(error("bad_args", "sandbox custom tool arguments are too large"), "denied", "limit_exceeded");
|
|
367
|
+
}
|
|
368
|
+
session.inFlight += 1;
|
|
369
|
+
try {
|
|
370
|
+
const value = await within(async (operationSignal) => {
|
|
371
|
+
const port = session.port ?? (session.port = createToolPort(app, config, session.envelope));
|
|
372
|
+
if (method === "tools.list") {
|
|
373
|
+
const listed = await port.listCustomTools({
|
|
374
|
+
signal: operationSignal,
|
|
375
|
+
requestId
|
|
376
|
+
});
|
|
377
|
+
if (listed.tools.length > limits.maxTools || (jsonBytes(listed) ?? Number.POSITIVE_INFINITY) > limits.maxListBytes) throw new RangeError("tool list limit exceeded");
|
|
378
|
+
return listed;
|
|
379
|
+
}
|
|
380
|
+
return port.callCustomTool({
|
|
381
|
+
name: toolName,
|
|
382
|
+
input: args.arguments ?? {},
|
|
383
|
+
signal: operationSignal,
|
|
384
|
+
requestId
|
|
385
|
+
});
|
|
386
|
+
}, limits.timeoutMs, signal, () => {
|
|
387
|
+
session.inFlight = Math.max(0, session.inFlight - 1);
|
|
388
|
+
});
|
|
389
|
+
const callResult = value;
|
|
390
|
+
if (method === "tools.call" && callResult.isError === true) {
|
|
391
|
+
const code = mcpResultCode(callResult);
|
|
392
|
+
return finish(error(code === "invalid_input" || code === "input_too_large" ? "bad_args" : code === "access_denied" ? "forbidden" : "execution_error", code === "invalid_input" || code === "input_too_large" ? "invalid sandbox custom tool arguments" : code === "access_denied" ? "sandbox custom tool denied" : "sandbox custom tool failed"), code === "access_denied" || code === "invalid_input" || code === "input_too_large" || code === "busy" ? "denied" : "failed", code === "access_denied" ? "authorization_or_tool_denied" : code === "invalid_input" || code === "input_too_large" ? "invalid_request" : code === "timeout" || code === "cancelled" ? "timed_out" : code === "busy" || code === "output_too_large" ? "limit_exceeded" : "execution_failed");
|
|
393
|
+
}
|
|
394
|
+
const resultBytes = jsonBytes(value);
|
|
395
|
+
if (resultBytes === null || resultBytes > limits.maxResultBytes) return finish(error("execution_error", "sandbox custom tool result is too large"), "failed", "limit_exceeded");
|
|
396
|
+
return finish({
|
|
397
|
+
ok: true,
|
|
398
|
+
value
|
|
399
|
+
}, "succeeded", "completed");
|
|
400
|
+
} catch (caught) {
|
|
401
|
+
const publicCode = mcpPublicErrorCode(caught);
|
|
402
|
+
return finish(error(publicCode === "invalid_input" || publicCode === "input_too_large" ? "bad_args" : publicCode === "access_denied" || publicCode === "busy" ? "forbidden" : "execution_error", caught instanceof DeadlineError || publicCode === "timeout" ? "sandbox custom tool timed out" : caught instanceof CancelledError || publicCode === "cancelled" ? "sandbox custom tool cancelled" : publicCode === "invalid_input" || publicCode === "input_too_large" ? "invalid sandbox custom tool arguments" : publicCode === "access_denied" ? "sandbox custom tool denied" : publicCode === "busy" ? "sandbox custom tool limit exceeded" : "sandbox custom tool failed"), publicCode === "access_denied" || publicCode === "invalid_input" || publicCode === "input_too_large" || publicCode === "busy" ? "denied" : "failed", caught instanceof DeadlineError || caught instanceof CancelledError || publicCode === "timeout" || publicCode === "cancelled" ? "timed_out" : caught instanceof RangeError || publicCode === "busy" || publicCode === "output_too_large" ? "limit_exceeded" : publicCode === "invalid_input" || publicCode === "input_too_large" ? "invalid_request" : publicCode === "access_denied" ? "authorization_or_tool_denied" : "execution_failed");
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
//#endregion
|
|
407
|
+
export { BROKER_MAX_REQUEST_WIRE_BYTES as a, brokerResultValueCap as c, parseSandboxRunResult as i, registerSandboxCustomToolsSession as n, brokerRequestWireCap as o, sandboxCustomTools as r, brokerResponseWireCap as s, handleSandboxCustomToolsRpc as t };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { n as HttpSandboxAdapterOptions, r as
|
|
1
|
+
import { A as SandboxWorkloadAdmissionVerification, C as SandboxCapabilities, D as SandboxWorkloadAdmissionClaims, E as WORKLOAD_ADMISSION_HEADER, O as SandboxWorkloadAdmissionDenialReason, S as SandboxBindings, T as SandboxRunResult, _ as GuestResultMessage, a as SANDBOX_WORKLOAD_DENIAL_MESSAGE, b as HostRpcResultMessage, c as SandboxWorkloadAuthorizationContext, d as SandboxWorkloadDeniedError, f as SandboxWorkloadPolicy, g as GuestMessage, h as FRAME_MARKER, i as httpSandboxAdapter, k as SandboxWorkloadAdmissionKey, l as SandboxWorkloadAuthorizationPhase, m as BINDINGS_TOKEN_HEADER, n as HttpSandboxAdapterOptions, o as SandboxWorkloadAuditContext, p as SandboxWorkloadRunOptions, r as WorkloadHttpSandboxOptions, s as SandboxWorkloadAuditEvent, t as HttpSandboxAdapter, u as SandboxWorkloadAuthorizer, v as GuestRpcMessage, w as SandboxRunRequest, x as SandboxBindingError, y as HOST_ADMISSION_HEADER } from "./adapter-http-DDeL-Ym3.mjs";
|
|
2
|
+
import { a as SandboxCustomToolsRunOptions, i as SandboxCustomToolsLimits, n as SandboxCustomToolEvidenceEvent, o as sandboxCustomTools, r as SandboxCustomToolsConfig, t as SandboxCustomToolEvidenceContext } from "./custom-tools-CgfgyVNv.mjs";
|
|
2
3
|
|
|
3
4
|
//#region src/net-validation.d.ts
|
|
4
5
|
|
|
@@ -57,6 +58,8 @@ declare function classifyIpLiteral(ip: string): IpValidationResult;
|
|
|
57
58
|
*/
|
|
58
59
|
declare function validateHostEgress(entry: string, opts?: {
|
|
59
60
|
resolve?: DnsResolver;
|
|
61
|
+
signal?: AbortSignal;
|
|
62
|
+
timeoutMs?: number;
|
|
60
63
|
}): Promise<IpValidationResult>;
|
|
61
64
|
/**
|
|
62
65
|
* Validate ALL `net` + `import` hosts in a capability set.
|
|
@@ -64,121 +67,11 @@ declare function validateHostEgress(entry: string, opts?: {
|
|
|
64
67
|
*/
|
|
65
68
|
declare function validateEgressHosts(hosts: readonly string[], opts?: {
|
|
66
69
|
resolve?: DnsResolver;
|
|
70
|
+
signal?: AbortSignal;
|
|
71
|
+
timeoutMs?: number;
|
|
72
|
+
concurrency?: number;
|
|
67
73
|
}): Promise<IpValidationResult>;
|
|
68
74
|
//#endregion
|
|
69
|
-
//#region src/types.d.ts
|
|
70
|
-
/**
|
|
71
|
-
* Wire contract between the `executor` adapter (Bun/Node, main app) and the
|
|
72
|
-
* standalone Deno sandbox server. Kept intentionally minimal: the main app
|
|
73
|
-
* never imports Deno; it only speaks this JSON over HTTP.
|
|
74
|
-
*/
|
|
75
|
-
/**
|
|
76
|
-
* Per-request guest capabilities. `net` and `import` are INDEPENDENT axes
|
|
77
|
-
* (Deno enforces them separately — `--allow-net` gates `fetch`, `--allow-import`
|
|
78
|
-
* gates module imports). The richer app-binding capabilities (`data`, `services`,
|
|
79
|
-
* etc.) live on the core `ExecutorCapabilities` type and are NOT enforced by the
|
|
80
|
-
* sandbox server — they are the later bindings-broker's job.
|
|
81
|
-
*/
|
|
82
|
-
interface SandboxCapabilities {
|
|
83
|
-
/** Runtime `fetch()` allowlist as `host[:port]`. Empty = no network. */
|
|
84
|
-
net: string[];
|
|
85
|
-
/** Module import allowlist as `host[:port]` (e.g. `esm.sh:443`). Empty = no remote imports. */
|
|
86
|
-
import: string[];
|
|
87
|
-
/** Hard wall-clock timeout; the subprocess is force-killed on exceed. */
|
|
88
|
-
timeoutMs: number;
|
|
89
|
-
/** Hard per-guest memory bound (`--v8-flags=--max-old-space-size`). */
|
|
90
|
-
memoryMb: number;
|
|
91
|
-
}
|
|
92
|
-
/**
|
|
93
|
-
* App-bindings wiring for the UNTRUSTED path. When present, the supervisor
|
|
94
|
-
* exposes a capability-scoped `globalThis.questpie` proxy to the guest whose
|
|
95
|
-
* methods RPC (over STDIO, never the network) to the host broker at {@link url},
|
|
96
|
-
* authenticated by the per-run scoped {@link token}.
|
|
97
|
-
*
|
|
98
|
-
* The token is OPAQUE and is held by the SUPERVISOR only — it is never written
|
|
99
|
-
* into the guest subprocess, so guest code cannot read or forge it to widen
|
|
100
|
-
* scope. The guest only ever sees the typed proxy.
|
|
101
|
-
*
|
|
102
|
-
* Critically, the broker is reached SERVER-TO-SERVER (supervisor → app), NOT by
|
|
103
|
-
* the guest's `fetch`: the guest's egress allowlist rejects loopback/private IPs
|
|
104
|
-
* (the SSRF defense), so a localhost broker is unreachable from guest code by
|
|
105
|
-
* design — we do not poke a hole in that.
|
|
106
|
-
*/
|
|
107
|
-
interface SandboxBindings {
|
|
108
|
-
/** Absolute URL of the host broker endpoint (e.g. `https://app/sandbox/rpc`). */
|
|
109
|
-
url: string;
|
|
110
|
-
/** Per-run opaque scoped token. Supervisor-only; never exposed to the guest. */
|
|
111
|
-
token: string;
|
|
112
|
-
}
|
|
113
|
-
/** POST /run request body. */
|
|
114
|
-
interface SandboxRunRequest {
|
|
115
|
-
/** Guest TypeScript source. Must `export default` a `function(input)`. */
|
|
116
|
-
source: string;
|
|
117
|
-
/** Payload passed to the guest entry function. */
|
|
118
|
-
input: unknown;
|
|
119
|
-
capabilities: SandboxCapabilities;
|
|
120
|
-
/**
|
|
121
|
-
* Secrets injected into the guest (NOT in source). Surfaced to the guest as
|
|
122
|
-
* `globalThis.__secrets`. The sandbox never persists these.
|
|
123
|
-
*/
|
|
124
|
-
secrets?: Record<string, string>;
|
|
125
|
-
/**
|
|
126
|
-
* App-bindings broker wiring. When omitted, the guest gets no `questpie`
|
|
127
|
-
* proxy (the legacy compute-only path). When present, the supervisor brokers
|
|
128
|
-
* the guest's binding RPCs to the host. See {@link SandboxBindings}.
|
|
129
|
-
*/
|
|
130
|
-
bindings?: SandboxBindings;
|
|
131
|
-
}
|
|
132
|
-
/** POST /run response body — also the `ExecutorAdapter.run` result shape. */
|
|
133
|
-
interface SandboxRunResult {
|
|
134
|
-
ok: boolean;
|
|
135
|
-
output?: unknown;
|
|
136
|
-
logs: string[];
|
|
137
|
-
error?: string;
|
|
138
|
-
/** True when the run was killed by the wall-clock timeout. */
|
|
139
|
-
timedOut?: boolean;
|
|
140
|
-
/** Wall-clock duration in ms (server-measured). */
|
|
141
|
-
ms?: number;
|
|
142
|
-
}
|
|
143
|
-
/** Line prefix marking a framed protocol message on guest stdout. */
|
|
144
|
-
declare const FRAME_MARKER = "__QP_SANDBOX_MSG__";
|
|
145
|
-
/**
|
|
146
|
-
* Header carrying the per-run scoped token on the supervisor → broker request.
|
|
147
|
-
* Mirrors `questpie`'s `BINDINGS_TOKEN_HEADER`. Supervisor-only; never set by or
|
|
148
|
-
* exposed to guest code.
|
|
149
|
-
*/
|
|
150
|
-
declare const BINDINGS_TOKEN_HEADER = "x-questpie-sandbox-token";
|
|
151
|
-
/** Structured error returned by the broker (mirrors `questpie` `BindingError`). */
|
|
152
|
-
interface SandboxBindingError {
|
|
153
|
-
code: string;
|
|
154
|
-
message: string;
|
|
155
|
-
}
|
|
156
|
-
/** Guest → supervisor: a binding RPC awaiting a brokered result. */
|
|
157
|
-
interface GuestRpcMessage {
|
|
158
|
-
type: "rpc";
|
|
159
|
-
id: number;
|
|
160
|
-
method: string;
|
|
161
|
-
args: unknown;
|
|
162
|
-
}
|
|
163
|
-
/** Guest → supervisor: the run's final result. */
|
|
164
|
-
interface GuestResultMessage {
|
|
165
|
-
type: "result";
|
|
166
|
-
ok: boolean;
|
|
167
|
-
output?: unknown;
|
|
168
|
-
error?: string;
|
|
169
|
-
logs: string[];
|
|
170
|
-
}
|
|
171
|
-
/** Any message the guest emits on stdout (framed). */
|
|
172
|
-
type GuestMessage = GuestRpcMessage | GuestResultMessage;
|
|
173
|
-
/** Supervisor → guest: the brokered outcome of a prior {@link GuestRpcMessage}. */
|
|
174
|
-
interface HostRpcResultMessage {
|
|
175
|
-
type: "rpc-result";
|
|
176
|
-
id: number;
|
|
177
|
-
ok: boolean;
|
|
178
|
-
value?: unknown;
|
|
179
|
-
error?: SandboxBindingError;
|
|
180
|
-
}
|
|
181
|
-
//#endregion
|
|
182
75
|
//#region src/guest-bindings.d.ts
|
|
183
76
|
/**
|
|
184
77
|
* Guest bindings proxy — the UNTRUSTED-side typed surface a sandboxed mini-app
|
|
@@ -237,6 +130,33 @@ interface GuestStore {
|
|
|
237
130
|
where?: unknown;
|
|
238
131
|
}): Promise<unknown>;
|
|
239
132
|
}
|
|
133
|
+
/** Schema-preserving custom-tool descriptor returned by discovery. */
|
|
134
|
+
interface GuestToolDescriptor {
|
|
135
|
+
name: string;
|
|
136
|
+
title?: string;
|
|
137
|
+
description?: string;
|
|
138
|
+
inputSchema: Record<string, unknown>;
|
|
139
|
+
outputSchema?: Record<string, unknown>;
|
|
140
|
+
annotations?: Record<string, unknown>;
|
|
141
|
+
_meta?: Record<string, unknown>;
|
|
142
|
+
}
|
|
143
|
+
/** Protocol-safe custom-tool result returned to the guest. */
|
|
144
|
+
interface GuestToolResult {
|
|
145
|
+
content: Array<{
|
|
146
|
+
type: string;
|
|
147
|
+
[key: string]: unknown;
|
|
148
|
+
}>;
|
|
149
|
+
structuredContent?: Record<string, unknown>;
|
|
150
|
+
isError?: boolean;
|
|
151
|
+
_meta?: Record<string, unknown>;
|
|
152
|
+
}
|
|
153
|
+
/** Explicit custom-tool discovery/call surface. Authorization stays host-side. */
|
|
154
|
+
interface GuestTools {
|
|
155
|
+
list(): Promise<{
|
|
156
|
+
tools: GuestToolDescriptor[];
|
|
157
|
+
}>;
|
|
158
|
+
call(name: string, args?: Record<string, unknown>): Promise<GuestToolResult>;
|
|
159
|
+
}
|
|
240
160
|
/** The `globalThis.questpie` surface handed to untrusted guest code. */
|
|
241
161
|
interface GuestBindings {
|
|
242
162
|
/** Files (file-as-DB), scoped by the run's `capabilities.files`. */
|
|
@@ -265,6 +185,11 @@ interface GuestBindings {
|
|
|
265
185
|
* `collections.document_store.<op>` call.
|
|
266
186
|
*/
|
|
267
187
|
store: Record<string, GuestStore>;
|
|
188
|
+
/**
|
|
189
|
+
* Explicitly released custom MCP tools. Built-in CRUD/file/store operations
|
|
190
|
+
* never flow through this namespace.
|
|
191
|
+
*/
|
|
192
|
+
tools: GuestTools;
|
|
268
193
|
}
|
|
269
194
|
/**
|
|
270
195
|
* Build the guest bindings proxy over an injected {@link HostCall}.
|
|
@@ -277,4 +202,4 @@ interface GuestBindings {
|
|
|
277
202
|
*/
|
|
278
203
|
declare function buildGuestBindings(hostCall: HostCall): GuestBindings;
|
|
279
204
|
//#endregion
|
|
280
|
-
export { BINDINGS_TOKEN_HEADER, type DnsResolver, FRAME_MARKER, type GuestBindings, type GuestCollection, type GuestMessage, type GuestResultMessage, type GuestRpcMessage, type HostCall, type HostRpcResultMessage, HttpSandboxAdapter, type HttpSandboxAdapterOptions, type IpValidationResult, type ParsedHost, type SandboxBindingError, type SandboxBindings, type SandboxCapabilities, type SandboxRunRequest, type SandboxRunResult, buildGuestBindings, classifyIpLiteral, httpSandboxAdapter, parseHostEntry, validateEgressHosts, validateHostEgress };
|
|
205
|
+
export { BINDINGS_TOKEN_HEADER, type DnsResolver, FRAME_MARKER, type GuestBindings, type GuestCollection, type GuestMessage, type GuestResultMessage, type GuestRpcMessage, type GuestToolDescriptor, type GuestToolResult, type GuestTools, HOST_ADMISSION_HEADER, type HostCall, type HostRpcResultMessage, HttpSandboxAdapter, type HttpSandboxAdapterOptions, type IpValidationResult, type ParsedHost, SANDBOX_WORKLOAD_DENIAL_MESSAGE, type SandboxBindingError, type SandboxBindings, type SandboxCapabilities, type SandboxCustomToolEvidenceContext, type SandboxCustomToolEvidenceEvent, type SandboxCustomToolsConfig, type SandboxCustomToolsLimits, type SandboxCustomToolsRunOptions, type SandboxRunRequest, type SandboxRunResult, type SandboxWorkloadAdmissionClaims, type SandboxWorkloadAdmissionDenialReason, type SandboxWorkloadAdmissionKey, type SandboxWorkloadAdmissionVerification, type SandboxWorkloadAuditContext, type SandboxWorkloadAuditEvent, type SandboxWorkloadAuthorizationContext, type SandboxWorkloadAuthorizationPhase, type SandboxWorkloadAuthorizer, SandboxWorkloadDeniedError, type SandboxWorkloadPolicy, type SandboxWorkloadRunOptions, WORKLOAD_ADMISSION_HEADER, type WorkloadHttpSandboxOptions, buildGuestBindings, classifyIpLiteral, httpSandboxAdapter, parseHostEntry, sandboxCustomTools, validateEgressHosts, validateHostEgress };
|
package/dist/index.mjs
CHANGED
|
@@ -1,16 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { r as sandboxCustomTools } from "./custom-tools-DxI7JvUn.mjs";
|
|
2
|
+
import { a as BINDINGS_TOKEN_HEADER, c as WORKLOAD_ADMISSION_HEADER, d as validateEgressHosts, f as validateHostEgress, i as SandboxWorkloadDeniedError, l as classifyIpLiteral, n as httpSandboxAdapter, o as FRAME_MARKER, r as SANDBOX_WORKLOAD_DENIAL_MESSAGE, s as HOST_ADMISSION_HEADER, t as HttpSandboxAdapter, u as parseHostEntry } from "./adapter-http-Coy4jqsj.mjs";
|
|
2
3
|
|
|
3
|
-
//#region src/types.ts
|
|
4
|
-
/** Line prefix marking a framed protocol message on guest stdout. */
|
|
5
|
-
const FRAME_MARKER = "__QP_SANDBOX_MSG__";
|
|
6
|
-
/**
|
|
7
|
-
* Header carrying the per-run scoped token on the supervisor → broker request.
|
|
8
|
-
* Mirrors `questpie`'s `BINDINGS_TOKEN_HEADER`. Supervisor-only; never set by or
|
|
9
|
-
* exposed to guest code.
|
|
10
|
-
*/
|
|
11
|
-
const BINDINGS_TOKEN_HEADER = "x-questpie-sandbox-token";
|
|
12
|
-
|
|
13
|
-
//#endregion
|
|
14
4
|
//#region src/guest-bindings.ts
|
|
15
5
|
/**
|
|
16
6
|
* Build the guest bindings proxy over an injected {@link HostCall}.
|
|
@@ -61,9 +51,16 @@ function buildGuestBindings(hostCall) {
|
|
|
61
51
|
update: (args) => ds("update", withStoreWhere(args ?? {})),
|
|
62
52
|
delete: (args = {}) => ds("delete", withStoreWhere(args))
|
|
63
53
|
};
|
|
64
|
-
} })
|
|
54
|
+
} }),
|
|
55
|
+
tools: {
|
|
56
|
+
list: async () => await hostCall("tools.list", {}),
|
|
57
|
+
call: async (name, args) => await hostCall("tools.call", {
|
|
58
|
+
name,
|
|
59
|
+
arguments: args ?? {}
|
|
60
|
+
})
|
|
61
|
+
}
|
|
65
62
|
};
|
|
66
63
|
}
|
|
67
64
|
|
|
68
65
|
//#endregion
|
|
69
|
-
export { BINDINGS_TOKEN_HEADER, FRAME_MARKER, HttpSandboxAdapter, buildGuestBindings, classifyIpLiteral, httpSandboxAdapter, parseHostEntry, validateEgressHosts, validateHostEgress };
|
|
66
|
+
export { BINDINGS_TOKEN_HEADER, FRAME_MARKER, HOST_ADMISSION_HEADER, HttpSandboxAdapter, SANDBOX_WORKLOAD_DENIAL_MESSAGE, SandboxWorkloadDeniedError, WORKLOAD_ADMISSION_HEADER, buildGuestBindings, classifyIpLiteral, httpSandboxAdapter, parseHostEntry, sandboxCustomTools, validateEgressHosts, validateHostEgress };
|