@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,40 @@
|
|
|
1
|
+
import "../custom-tools-CgfgyVNv.mjs";
|
|
2
|
+
import * as questpie0 from "questpie";
|
|
3
|
+
import { routeApp } from "questpie";
|
|
4
|
+
import "questpie/executor";
|
|
5
|
+
import { RouteDefinition, RouteParamsFromKey } from "questpie/types";
|
|
6
|
+
|
|
7
|
+
//#region src/server/modules/sandbox/routes/sandbox/rpc.d.ts
|
|
8
|
+
|
|
9
|
+
declare const sandboxRpcRoute: questpie0.RawRouteDefinition<questpie0.JsonRouteParams>;
|
|
10
|
+
//#endregion
|
|
11
|
+
//#region src/server/modules/sandbox/.generated/module.d.ts
|
|
12
|
+
type SandboxCollections = Record<never, never>;
|
|
13
|
+
type SandboxChannels = Record<never, never>;
|
|
14
|
+
type SandboxGlobals = Record<never, never>;
|
|
15
|
+
type SandboxJobs = Record<never, never>;
|
|
16
|
+
type SandboxRoutes = {
|
|
17
|
+
"sandbox/rpc": typeof sandboxRpcRoute extends {
|
|
18
|
+
__brand: "route";
|
|
19
|
+
} ? RouteDefinition<unknown, unknown, RouteParamsFromKey<"sandbox/rpc">> : typeof sandboxRpcRoute;
|
|
20
|
+
};
|
|
21
|
+
type SandboxFieldTypes = Record<never, never>;
|
|
22
|
+
type SandboxModule = {
|
|
23
|
+
name: "questpie-sandbox";
|
|
24
|
+
routes: SandboxRoutes;
|
|
25
|
+
collections: SandboxCollections;
|
|
26
|
+
channels: SandboxChannels;
|
|
27
|
+
globals: SandboxGlobals;
|
|
28
|
+
jobs: SandboxJobs;
|
|
29
|
+
fieldTypes: SandboxFieldTypes;
|
|
30
|
+
messages: Record<never, never>;
|
|
31
|
+
services: Record<never, never>;
|
|
32
|
+
emails: Record<never, never>;
|
|
33
|
+
migrations: readonly unknown[];
|
|
34
|
+
seeds: readonly unknown[];
|
|
35
|
+
};
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/server/modules/sandbox/index.d.ts
|
|
38
|
+
declare const sandboxModule: SandboxModule;
|
|
39
|
+
//#endregion
|
|
40
|
+
export { sandboxModule as default, sandboxModule };
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { a as BROKER_MAX_REQUEST_WIRE_BYTES, c as brokerResultValueCap, o as brokerRequestWireCap, s as brokerResponseWireCap, t as handleSandboxCustomToolsRpc } from "../custom-tools-DxI7JvUn.mjs";
|
|
2
|
+
import { route, routeApp } from "questpie";
|
|
3
|
+
import { BINDINGS_TOKEN_HEADER, snapshotBoundedBrokerValue } from "questpie/executor";
|
|
4
|
+
|
|
5
|
+
//#region src/server/modules/sandbox/routes/sandbox/rpc.ts
|
|
6
|
+
const MAX_METHOD_BYTES = 256;
|
|
7
|
+
const BINDING_ERROR_CODES = new Set([
|
|
8
|
+
"bad_method",
|
|
9
|
+
"bad_args",
|
|
10
|
+
"unauthorized",
|
|
11
|
+
"forbidden",
|
|
12
|
+
"not_implemented",
|
|
13
|
+
"execution_error"
|
|
14
|
+
]);
|
|
15
|
+
function json(body, status) {
|
|
16
|
+
return new Response(JSON.stringify(body), {
|
|
17
|
+
status,
|
|
18
|
+
headers: { "content-type": "application/json" }
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function errorResponse(status, code, message) {
|
|
22
|
+
return json({
|
|
23
|
+
ok: false,
|
|
24
|
+
error: {
|
|
25
|
+
code,
|
|
26
|
+
message
|
|
27
|
+
}
|
|
28
|
+
}, status);
|
|
29
|
+
}
|
|
30
|
+
async function readBoundedJson(request) {
|
|
31
|
+
const contentLength = request.headers.get("content-length");
|
|
32
|
+
if (contentLength !== null && (!/^\d+$/.test(contentLength) || Number(contentLength) > BROKER_MAX_REQUEST_WIRE_BYTES)) return {
|
|
33
|
+
ok: false,
|
|
34
|
+
response: errorResponse(413, "bad_args", "sandbox broker request body is too large")
|
|
35
|
+
};
|
|
36
|
+
if (!request.body) return {
|
|
37
|
+
ok: false,
|
|
38
|
+
response: errorResponse(400, "bad_args", "invalid JSON body")
|
|
39
|
+
};
|
|
40
|
+
const reader = request.body.getReader();
|
|
41
|
+
const chunks = [];
|
|
42
|
+
let size = 0;
|
|
43
|
+
try {
|
|
44
|
+
while (true) {
|
|
45
|
+
const part = await reader.read();
|
|
46
|
+
if (part.done) break;
|
|
47
|
+
size += part.value.byteLength;
|
|
48
|
+
if (size > BROKER_MAX_REQUEST_WIRE_BYTES) {
|
|
49
|
+
await reader.cancel();
|
|
50
|
+
return {
|
|
51
|
+
ok: false,
|
|
52
|
+
response: errorResponse(413, "bad_args", "sandbox broker request body is too large")
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
chunks.push(part.value);
|
|
56
|
+
}
|
|
57
|
+
} catch {
|
|
58
|
+
return {
|
|
59
|
+
ok: false,
|
|
60
|
+
response: errorResponse(400, "bad_args", "invalid JSON body")
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const bytes = new Uint8Array(size);
|
|
64
|
+
let offset = 0;
|
|
65
|
+
for (const chunk of chunks) {
|
|
66
|
+
bytes.set(chunk, offset);
|
|
67
|
+
offset += chunk.byteLength;
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
return {
|
|
71
|
+
ok: true,
|
|
72
|
+
value: JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)),
|
|
73
|
+
bytes: size
|
|
74
|
+
};
|
|
75
|
+
} catch {
|
|
76
|
+
return {
|
|
77
|
+
ok: false,
|
|
78
|
+
response: errorResponse(400, "bad_args", "invalid JSON body")
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
function parseRequestBody(value) {
|
|
83
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return {
|
|
84
|
+
ok: false,
|
|
85
|
+
response: errorResponse(400, "bad_args", "invalid request body")
|
|
86
|
+
};
|
|
87
|
+
const body = value;
|
|
88
|
+
if (Object.keys(body).some((key) => key !== "method" && key !== "args")) return {
|
|
89
|
+
ok: false,
|
|
90
|
+
response: errorResponse(400, "bad_args", "invalid request body")
|
|
91
|
+
};
|
|
92
|
+
if (typeof body.method !== "string" || body.method.length === 0 || new TextEncoder().encode(body.method).byteLength > MAX_METHOD_BYTES) return {
|
|
93
|
+
ok: false,
|
|
94
|
+
response: errorResponse(400, "bad_method", "invalid method")
|
|
95
|
+
};
|
|
96
|
+
return {
|
|
97
|
+
ok: true,
|
|
98
|
+
method: body.method,
|
|
99
|
+
args: body.args
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function invalidBrokerResult() {
|
|
103
|
+
return {
|
|
104
|
+
ok: false,
|
|
105
|
+
error: {
|
|
106
|
+
code: "execution_error",
|
|
107
|
+
message: "sandbox broker returned an invalid result",
|
|
108
|
+
correlationId: crypto.randomUUID()
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function normalizeBrokerResult(method, value) {
|
|
113
|
+
const bounded = snapshotBoundedBrokerValue(value, brokerResponseWireCap(method));
|
|
114
|
+
if (!bounded.ok || !bounded.value || Array.isArray(bounded.value)) return null;
|
|
115
|
+
const result = bounded.value;
|
|
116
|
+
const resultKeys = Object.keys(result);
|
|
117
|
+
if (result.ok === true) {
|
|
118
|
+
if (resultKeys.length !== 2 || !resultKeys.includes("ok") || !resultKeys.includes("value")) return null;
|
|
119
|
+
const normalizedValue = snapshotBoundedBrokerValue(result.value, brokerResultValueCap(method));
|
|
120
|
+
if (!normalizedValue.ok) return null;
|
|
121
|
+
return {
|
|
122
|
+
ok: true,
|
|
123
|
+
value: normalizedValue.value
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
if (result.ok !== false || resultKeys.length !== 2 || !resultKeys.includes("error") || !result.error || typeof result.error !== "object" || Array.isArray(result.error)) return null;
|
|
127
|
+
const error = result.error;
|
|
128
|
+
const errorKeys = Object.keys(error);
|
|
129
|
+
if (!errorKeys.every((key) => [
|
|
130
|
+
"code",
|
|
131
|
+
"message",
|
|
132
|
+
"correlationId"
|
|
133
|
+
].includes(key)) || !errorKeys.includes("code") || !errorKeys.includes("message") || typeof error.code !== "string" || !BINDING_ERROR_CODES.has(error.code) || typeof error.message !== "string" || error.correlationId !== void 0 && typeof error.correlationId !== "string") return null;
|
|
134
|
+
const correlationId = typeof error.correlationId === "string" && /^[\w-]{1,128}$/.test(error.correlationId) ? error.correlationId : void 0;
|
|
135
|
+
if (error.code === "execution_error") return {
|
|
136
|
+
ok: false,
|
|
137
|
+
error: {
|
|
138
|
+
code: "execution_error",
|
|
139
|
+
message: "sandbox binding operation failed",
|
|
140
|
+
correlationId: correlationId ?? crypto.randomUUID()
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
error: {
|
|
146
|
+
code: error.code,
|
|
147
|
+
message: error.message,
|
|
148
|
+
...correlationId ? { correlationId } : {}
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
async function handleSandboxBrokerRequest(request, broker, customTools) {
|
|
153
|
+
const decoded = await readBoundedJson(request);
|
|
154
|
+
if (!decoded.ok) return decoded.response;
|
|
155
|
+
const body = parseRequestBody(decoded.value);
|
|
156
|
+
if (!body.ok) return body.response;
|
|
157
|
+
if (decoded.bytes > brokerRequestWireCap(body.method)) return errorResponse(413, "bad_args", "sandbox broker request body is too large");
|
|
158
|
+
let result;
|
|
159
|
+
try {
|
|
160
|
+
result = body.method.startsWith("tools.") ? await handleSandboxCustomToolsRpc({
|
|
161
|
+
app: customTools?.app,
|
|
162
|
+
config: customTools?.config,
|
|
163
|
+
token: request.headers.get(BINDINGS_TOKEN_HEADER),
|
|
164
|
+
requestUrl: request.url,
|
|
165
|
+
method: body.method,
|
|
166
|
+
args: body.args,
|
|
167
|
+
signal: request.signal
|
|
168
|
+
}) : await broker.handleRpc(request.headers.get(BINDINGS_TOKEN_HEADER), body.method, body.args);
|
|
169
|
+
} catch {
|
|
170
|
+
return errorResponse(500, "execution_error", "sandbox broker request failed");
|
|
171
|
+
}
|
|
172
|
+
result = normalizeBrokerResult(body.method, result) ?? invalidBrokerResult();
|
|
173
|
+
const status = result.ok ? 200 : result.error.code === "unauthorized" ? 401 : result.error.code === "forbidden" ? 403 : result.error.code === "not_implemented" ? 501 : result.error.code === "execution_error" ? 500 : 400;
|
|
174
|
+
return json(result, status);
|
|
175
|
+
}
|
|
176
|
+
const sandboxRpcRoute = route().post().raw().access(true).handler((ctx) => {
|
|
177
|
+
const app = routeApp(ctx);
|
|
178
|
+
const broker = app.executor?.broker;
|
|
179
|
+
if (!broker) return errorResponse(503, "execution_error", "sandbox broker unavailable");
|
|
180
|
+
return handleSandboxBrokerRequest(ctx.request, broker, {
|
|
181
|
+
app,
|
|
182
|
+
config: app.state.sandboxCustomTools
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
var rpc_default = sandboxRpcRoute;
|
|
186
|
+
|
|
187
|
+
//#endregion
|
|
188
|
+
//#region src/server/modules/sandbox/.generated/module.ts
|
|
189
|
+
const _module = {
|
|
190
|
+
name: "questpie-sandbox",
|
|
191
|
+
routes: { "sandbox/rpc": rpc_default },
|
|
192
|
+
collections: {},
|
|
193
|
+
channels: {},
|
|
194
|
+
globals: {},
|
|
195
|
+
jobs: {},
|
|
196
|
+
messages: {},
|
|
197
|
+
services: {},
|
|
198
|
+
emails: {},
|
|
199
|
+
migrations: [],
|
|
200
|
+
seeds: [],
|
|
201
|
+
fieldTypes: {}
|
|
202
|
+
};
|
|
203
|
+
var module_default = _module;
|
|
204
|
+
|
|
205
|
+
//#endregion
|
|
206
|
+
//#region src/server/modules/sandbox/index.ts
|
|
207
|
+
const sandboxModule = module_default;
|
|
208
|
+
var sandbox_default = sandboxModule;
|
|
209
|
+
|
|
210
|
+
//#endregion
|
|
211
|
+
export { sandbox_default as default, sandboxModule };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@questpie/sandbox",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.17.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -10,12 +10,16 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"dist",
|
|
12
12
|
"src/sandbox-server.ts",
|
|
13
|
+
"src/broker-wire.ts",
|
|
13
14
|
"src/guest-entry.ts",
|
|
14
15
|
"src/net-validation.ts",
|
|
15
16
|
"src/server-internals.ts",
|
|
16
17
|
"src/egress-firewall.ts",
|
|
17
18
|
"src/types.ts",
|
|
18
|
-
"src/guest-bindings.ts"
|
|
19
|
+
"src/guest-bindings.ts",
|
|
20
|
+
"src/guest-runtime-source.ts",
|
|
21
|
+
"src/workload-admission.ts",
|
|
22
|
+
"src/workload.ts"
|
|
19
23
|
],
|
|
20
24
|
"type": "module",
|
|
21
25
|
"sideEffects": false,
|
|
@@ -31,6 +35,10 @@
|
|
|
31
35
|
"types": "./dist/adapter.d.mts",
|
|
32
36
|
"default": "./dist/adapter.mjs"
|
|
33
37
|
},
|
|
38
|
+
"./modules/sandbox": {
|
|
39
|
+
"types": "./dist/modules/sandbox.d.mts",
|
|
40
|
+
"default": "./dist/modules/sandbox.mjs"
|
|
41
|
+
},
|
|
34
42
|
"./package.json": {
|
|
35
43
|
"types": "./package.json",
|
|
36
44
|
"default": "./package.json"
|
|
@@ -47,6 +55,10 @@
|
|
|
47
55
|
"types": "./dist/adapter.d.mts",
|
|
48
56
|
"default": "./dist/adapter.mjs"
|
|
49
57
|
},
|
|
58
|
+
"./modules/sandbox": {
|
|
59
|
+
"types": "./dist/modules/sandbox.d.mts",
|
|
60
|
+
"default": "./dist/modules/sandbox.mjs"
|
|
61
|
+
},
|
|
50
62
|
"./package.json": {
|
|
51
63
|
"types": "./package.json",
|
|
52
64
|
"default": "./package.json"
|
|
@@ -56,16 +68,19 @@
|
|
|
56
68
|
"scripts": {
|
|
57
69
|
"build": "tsdown",
|
|
58
70
|
"check-types": "tsc --noEmit",
|
|
71
|
+
"questpie:generate": "bun questpie generate --verbose",
|
|
59
72
|
"test": "bun test"
|
|
60
73
|
},
|
|
61
74
|
"dependencies": {
|
|
62
|
-
"questpie": "^3.
|
|
75
|
+
"@questpie/mcp": "^3.17.0",
|
|
76
|
+
"questpie": "^3.17.0"
|
|
63
77
|
},
|
|
64
78
|
"devDependencies": {
|
|
65
79
|
"bun-types": "latest",
|
|
66
|
-
"tsdown": "^0.18.3"
|
|
80
|
+
"tsdown": "^0.18.3",
|
|
81
|
+
"zod": "^4.2.1"
|
|
67
82
|
},
|
|
68
83
|
"peerDependencies": {
|
|
69
|
-
"questpie": "^3.
|
|
84
|
+
"questpie": "^3.17.0"
|
|
70
85
|
}
|
|
71
86
|
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One place for the sandbox broker's decoded payload and derived wire budgets.
|
|
3
|
+
*
|
|
4
|
+
* `http.fetch` bodies are binary-to-base64, then nested in JSON RPC envelopes.
|
|
5
|
+
* The transport limits therefore cannot equal the decoded body limit. Keep all
|
|
6
|
+
* supervisor, route, frame, and custom-tool caps derived here.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/** Decoded request/response body limit promised by `http.fetch`. */
|
|
10
|
+
export const BROKER_HTTP_BODY_CAP_BYTES = 5 * 1024 * 1024;
|
|
11
|
+
|
|
12
|
+
/** Exact maximum base64 length for a decoded HTTP body at the public cap. */
|
|
13
|
+
export const BROKER_HTTP_BASE64_CAP_BYTES =
|
|
14
|
+
4 * Math.ceil(BROKER_HTTP_BODY_CAP_BYTES / 3);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Room for URL, method, bounded HTTP headers, status metadata, and JSON RPC
|
|
18
|
+
* envelope syntax around a maximum-size base64 body.
|
|
19
|
+
*/
|
|
20
|
+
export const BROKER_HTTP_JSON_HEADROOM_BYTES = 128 * 1024;
|
|
21
|
+
|
|
22
|
+
/** Whole JSON request/response budget for the `http.fetch` method. */
|
|
23
|
+
export const BROKER_HTTP_WIRE_CAP_BYTES =
|
|
24
|
+
BROKER_HTTP_BASE64_CAP_BYTES + BROKER_HTTP_JSON_HEADROOM_BYTES;
|
|
25
|
+
|
|
26
|
+
/** Existing small native binding request budget, including its JSON envelope. */
|
|
27
|
+
export const BROKER_NATIVE_REQUEST_WIRE_CAP_BYTES = 64 * 1024;
|
|
28
|
+
|
|
29
|
+
/** Maximum JSON value returned by native bindings before envelope overhead. */
|
|
30
|
+
export const BROKER_NATIVE_RESULT_CAP_BYTES = 768 * 1024;
|
|
31
|
+
|
|
32
|
+
/** Custom-tool logical limits; shared with the host session implementation. */
|
|
33
|
+
export const BROKER_CUSTOM_ARGUMENT_CAP_BYTES = 63 * 1024;
|
|
34
|
+
export const BROKER_CUSTOM_RESULT_CAP_BYTES = 768 * 1024;
|
|
35
|
+
export const BROKER_CUSTOM_LIST_CAP_BYTES = 768 * 1024;
|
|
36
|
+
|
|
37
|
+
const BROKER_JSON_ENVELOPE_HEADROOM_BYTES = 4 * 1024;
|
|
38
|
+
|
|
39
|
+
export const BROKER_NATIVE_RESPONSE_WIRE_CAP_BYTES =
|
|
40
|
+
BROKER_NATIVE_RESULT_CAP_BYTES + BROKER_JSON_ENVELOPE_HEADROOM_BYTES;
|
|
41
|
+
export const BROKER_CUSTOM_REQUEST_WIRE_CAP_BYTES =
|
|
42
|
+
BROKER_CUSTOM_ARGUMENT_CAP_BYTES + BROKER_JSON_ENVELOPE_HEADROOM_BYTES;
|
|
43
|
+
export const BROKER_CUSTOM_RESPONSE_WIRE_CAP_BYTES =
|
|
44
|
+
Math.max(BROKER_CUSTOM_RESULT_CAP_BYTES, BROKER_CUSTOM_LIST_CAP_BYTES) +
|
|
45
|
+
BROKER_JSON_ENVELOPE_HEADROOM_BYTES;
|
|
46
|
+
|
|
47
|
+
/** Maximum body the app broker route will read before method-aware validation. */
|
|
48
|
+
export const BROKER_MAX_REQUEST_WIRE_BYTES = Math.max(
|
|
49
|
+
BROKER_HTTP_WIRE_CAP_BYTES,
|
|
50
|
+
BROKER_NATIVE_REQUEST_WIRE_CAP_BYTES,
|
|
51
|
+
BROKER_CUSTOM_REQUEST_WIRE_CAP_BYTES,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
/** Maximum response the supervisor will read before method-aware validation. */
|
|
55
|
+
export const BROKER_MAX_RESPONSE_WIRE_BYTES = Math.max(
|
|
56
|
+
BROKER_HTTP_WIRE_CAP_BYTES,
|
|
57
|
+
BROKER_NATIVE_RESPONSE_WIRE_CAP_BYTES,
|
|
58
|
+
BROKER_CUSTOM_RESPONSE_WIRE_CAP_BYTES,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
/** Prefix/newline room around the largest JSON RPC frame. */
|
|
62
|
+
export const BROKER_FRAME_CAP_BYTES =
|
|
63
|
+
Math.max(BROKER_MAX_REQUEST_WIRE_BYTES, BROKER_MAX_RESPONSE_WIRE_BYTES) +
|
|
64
|
+
1024;
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A run may emit one maximum RPC request and still return a maximum-sized final
|
|
68
|
+
* frame. Repeated large calls remain bounded by this cumulative budget.
|
|
69
|
+
*/
|
|
70
|
+
export const BROKER_TOTAL_OUTPUT_CAP_BYTES = BROKER_FRAME_CAP_BYTES * 2;
|
|
71
|
+
|
|
72
|
+
export function brokerRequestWireCap(method: string): number {
|
|
73
|
+
if (method === "http.fetch") return BROKER_HTTP_WIRE_CAP_BYTES;
|
|
74
|
+
if (method === "tools.call") return BROKER_CUSTOM_REQUEST_WIRE_CAP_BYTES;
|
|
75
|
+
if (method === "tools.list") return BROKER_JSON_ENVELOPE_HEADROOM_BYTES;
|
|
76
|
+
return BROKER_NATIVE_REQUEST_WIRE_CAP_BYTES;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function brokerResponseWireCap(method: string): number {
|
|
80
|
+
if (method === "http.fetch") return BROKER_HTTP_WIRE_CAP_BYTES;
|
|
81
|
+
if (method === "tools.call" || method === "tools.list") {
|
|
82
|
+
return BROKER_CUSTOM_RESPONSE_WIRE_CAP_BYTES;
|
|
83
|
+
}
|
|
84
|
+
return BROKER_NATIVE_RESPONSE_WIRE_CAP_BYTES;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function brokerResultValueCap(method: string): number {
|
|
88
|
+
if (method === "http.fetch") {
|
|
89
|
+
return BROKER_HTTP_WIRE_CAP_BYTES - BROKER_JSON_ENVELOPE_HEADROOM_BYTES;
|
|
90
|
+
}
|
|
91
|
+
if (method === "tools.call") return BROKER_CUSTOM_RESULT_CAP_BYTES;
|
|
92
|
+
if (method === "tools.list") return BROKER_CUSTOM_LIST_CAP_BYTES;
|
|
93
|
+
return BROKER_NATIVE_RESULT_CAP_BYTES;
|
|
94
|
+
}
|
package/src/egress-firewall.ts
CHANGED
|
@@ -161,7 +161,9 @@ function looksIpv4(ip: string): boolean {
|
|
|
161
161
|
const parts = ip.split(".");
|
|
162
162
|
return (
|
|
163
163
|
parts.length === 4 &&
|
|
164
|
-
parts.every(
|
|
164
|
+
parts.every(
|
|
165
|
+
(p) => /^\d{1,3}$/.test(p) && Number(p) >= 0 && Number(p) <= 255,
|
|
166
|
+
)
|
|
165
167
|
);
|
|
166
168
|
}
|
|
167
169
|
|
|
@@ -182,7 +184,9 @@ function looksIpv4(ip: string): boolean {
|
|
|
182
184
|
export function buildNftRuleset(allow: readonly AllowedEndpoint[]): string {
|
|
183
185
|
const lines: string[] = [];
|
|
184
186
|
lines.push(`# QUESTPIE sandbox egress firewall (defense-in-depth, per-run).`);
|
|
185
|
-
lines.push(
|
|
187
|
+
lines.push(
|
|
188
|
+
`# Default-deny: only the resolved allowlist + broker may egress.`,
|
|
189
|
+
);
|
|
186
190
|
lines.push(`table inet ${NFT_TABLE} {`);
|
|
187
191
|
lines.push(`\tchain output {`);
|
|
188
192
|
lines.push(`\t\ttype filter hook output priority 0; policy drop;`);
|
|
@@ -228,7 +232,9 @@ export function buildNftRuleset(allow: readonly AllowedEndpoint[]): string {
|
|
|
228
232
|
* default-deny netns, the correct posture for the brokered (`--allow-net=[]`)
|
|
229
233
|
* path. The guest has nothing to reach over raw sockets; the kernel enforces it.
|
|
230
234
|
*/
|
|
231
|
-
export function planEgressFirewall(
|
|
235
|
+
export function planEgressFirewall(
|
|
236
|
+
input: EgressFirewallPlanInput,
|
|
237
|
+
): EgressFirewallPlan {
|
|
232
238
|
if (!isLinux(input.os)) {
|
|
233
239
|
return {
|
|
234
240
|
applied: false,
|
package/src/guest-bindings.ts
CHANGED
|
@@ -54,6 +54,31 @@ export interface GuestStore {
|
|
|
54
54
|
delete(args?: { where?: unknown }): Promise<unknown>;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
+
/** Schema-preserving custom-tool descriptor returned by discovery. */
|
|
58
|
+
export interface GuestToolDescriptor {
|
|
59
|
+
name: string;
|
|
60
|
+
title?: string;
|
|
61
|
+
description?: string;
|
|
62
|
+
inputSchema: Record<string, unknown>;
|
|
63
|
+
outputSchema?: Record<string, unknown>;
|
|
64
|
+
annotations?: Record<string, unknown>;
|
|
65
|
+
_meta?: Record<string, unknown>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Protocol-safe custom-tool result returned to the guest. */
|
|
69
|
+
export interface GuestToolResult {
|
|
70
|
+
content: Array<{ type: string; [key: string]: unknown }>;
|
|
71
|
+
structuredContent?: Record<string, unknown>;
|
|
72
|
+
isError?: boolean;
|
|
73
|
+
_meta?: Record<string, unknown>;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Explicit custom-tool discovery/call surface. Authorization stays host-side. */
|
|
77
|
+
export interface GuestTools {
|
|
78
|
+
list(): Promise<{ tools: GuestToolDescriptor[] }>;
|
|
79
|
+
call(name: string, args?: Record<string, unknown>): Promise<GuestToolResult>;
|
|
80
|
+
}
|
|
81
|
+
|
|
57
82
|
/** The `globalThis.questpie` surface handed to untrusted guest code. */
|
|
58
83
|
export interface GuestBindings {
|
|
59
84
|
/** Files (file-as-DB), scoped by the run's `capabilities.files`. */
|
|
@@ -76,6 +101,11 @@ export interface GuestBindings {
|
|
|
76
101
|
* `collections.document_store.<op>` call.
|
|
77
102
|
*/
|
|
78
103
|
store: Record<string, GuestStore>;
|
|
104
|
+
/**
|
|
105
|
+
* Explicitly released custom MCP tools. Built-in CRUD/file/store operations
|
|
106
|
+
* never flow through this namespace.
|
|
107
|
+
*/
|
|
108
|
+
tools: GuestTools;
|
|
79
109
|
}
|
|
80
110
|
|
|
81
111
|
/**
|
|
@@ -142,5 +172,17 @@ export function buildGuestBindings(hostCall: HostCall): GuestBindings {
|
|
|
142
172
|
},
|
|
143
173
|
) as Record<string, GuestStore>;
|
|
144
174
|
|
|
145
|
-
|
|
175
|
+
const tools: GuestTools = {
|
|
176
|
+
list: async () =>
|
|
177
|
+
(await hostCall("tools.list", {})) as {
|
|
178
|
+
tools: GuestToolDescriptor[];
|
|
179
|
+
},
|
|
180
|
+
call: async (name, args) =>
|
|
181
|
+
(await hostCall("tools.call", {
|
|
182
|
+
name,
|
|
183
|
+
arguments: args ?? {},
|
|
184
|
+
})) as GuestToolResult,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
return { files, collections, store, tools };
|
|
146
188
|
}
|