@celestea/tools 2.7.1
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/LICENSE +21 -0
- package/dist/args.d.ts +15 -0
- package/dist/args.js +55 -0
- package/dist/attachments/image-header.d.ts +28 -0
- package/dist/attachments/image-header.js +148 -0
- package/dist/attachments/store.d.ts +56 -0
- package/dist/attachments/store.js +148 -0
- package/dist/browser/cdp.d.ts +162 -0
- package/dist/browser/cdp.js +269 -0
- package/dist/browser/launch.d.ts +92 -0
- package/dist/browser/launch.js +232 -0
- package/dist/browser/memory-guard.d.ts +55 -0
- package/dist/browser/memory-guard.js +210 -0
- package/dist/browser/session.d.ts +146 -0
- package/dist/browser/session.js +371 -0
- package/dist/browser/snapshot.d.ts +69 -0
- package/dist/browser/snapshot.js +163 -0
- package/dist/browser/types.d.ts +51 -0
- package/dist/browser/types.js +8 -0
- package/dist/builtin.d.ts +54 -0
- package/dist/builtin.js +77 -0
- package/dist/desc.d.ts +20 -0
- package/dist/desc.js +22 -0
- package/dist/disclosure.d.ts +113 -0
- package/dist/disclosure.js +141 -0
- package/dist/env.d.ts +10 -0
- package/dist/env.js +33 -0
- package/dist/errors.d.ts +25 -0
- package/dist/errors.js +60 -0
- package/dist/exposure.d.ts +145 -0
- package/dist/exposure.js +244 -0
- package/dist/fn-tool.d.ts +14 -0
- package/dist/fn-tool.js +14 -0
- package/dist/fs/file-io.d.ts +78 -0
- package/dist/fs/file-io.js +239 -0
- package/dist/guard/path-guard.d.ts +144 -0
- package/dist/guard/path-guard.js +289 -0
- package/dist/guard/paths.d.ts +35 -0
- package/dist/guard/paths.js +100 -0
- package/dist/http/errors.d.ts +24 -0
- package/dist/http/errors.js +64 -0
- package/dist/http/headers.d.ts +19 -0
- package/dist/http/headers.js +62 -0
- package/dist/http/redirects.d.ts +31 -0
- package/dist/http/redirects.js +76 -0
- package/dist/http/ssrf.d.ts +105 -0
- package/dist/http/ssrf.js +272 -0
- package/dist/http/transport.d.ts +50 -0
- package/dist/http/transport.js +130 -0
- package/dist/index.d.ts +114 -0
- package/dist/index.js +129 -0
- package/dist/memory/log.d.ts +75 -0
- package/dist/memory/log.js +157 -0
- package/dist/memory/store.d.ts +47 -0
- package/dist/memory/store.js +61 -0
- package/dist/platform/exec.d.ts +79 -0
- package/dist/platform/exec.js +218 -0
- package/dist/platform/index.d.ts +12 -0
- package/dist/platform/index.js +12 -0
- package/dist/platform/paths.d.ts +51 -0
- package/dist/platform/paths.js +60 -0
- package/dist/platform/quote.d.ts +72 -0
- package/dist/platform/quote.js +102 -0
- package/dist/plugin.d.ts +96 -0
- package/dist/plugin.js +101 -0
- package/dist/process/buffers.d.ts +33 -0
- package/dist/process/buffers.js +86 -0
- package/dist/process/registry.d.ts +98 -0
- package/dist/process/registry.js +282 -0
- package/dist/registry.d.ts +52 -0
- package/dist/registry.js +161 -0
- package/dist/run-code/broker.d.ts +68 -0
- package/dist/run-code/broker.js +465 -0
- package/dist/run-code/limits.d.ts +69 -0
- package/dist/run-code/limits.js +88 -0
- package/dist/run-code/lines.d.ts +69 -0
- package/dist/run-code/lines.js +199 -0
- package/dist/run-code/sdk-ts.d.ts +34 -0
- package/dist/run-code/sdk-ts.js +276 -0
- package/dist/run-code/sdk.d.ts +39 -0
- package/dist/run-code/sdk.js +294 -0
- package/dist/sandbox/async.d.ts +10 -0
- package/dist/sandbox/async.js +26 -0
- package/dist/sandbox/bwrap-argv.d.ts +62 -0
- package/dist/sandbox/bwrap-argv.js +113 -0
- package/dist/sandbox/bwrap.d.ts +94 -0
- package/dist/sandbox/bwrap.js +159 -0
- package/dist/sandbox/child.d.ts +38 -0
- package/dist/sandbox/child.js +98 -0
- package/dist/sandbox/config.d.ts +89 -0
- package/dist/sandbox/config.js +149 -0
- package/dist/sandbox/fake-sandbox.d.ts +57 -0
- package/dist/sandbox/fake-sandbox.js +110 -0
- package/dist/sandbox/launch.d.ts +51 -0
- package/dist/sandbox/launch.js +134 -0
- package/dist/sandbox/limits.d.ts +63 -0
- package/dist/sandbox/limits.js +113 -0
- package/dist/sandbox/probe.d.ts +46 -0
- package/dist/sandbox/probe.js +102 -0
- package/dist/sandbox/provider.d.ts +83 -0
- package/dist/sandbox/provider.js +126 -0
- package/dist/sandbox/rlimit.d.ts +60 -0
- package/dist/sandbox/rlimit.js +76 -0
- package/dist/sandbox/seccomp.d.ts +48 -0
- package/dist/sandbox/seccomp.js +115 -0
- package/dist/sandbox/userspace.d.ts +65 -0
- package/dist/sandbox/userspace.js +107 -0
- package/dist/sandbox/workdir.d.ts +13 -0
- package/dist/sandbox/workdir.js +44 -0
- package/dist/schema.d.ts +20 -0
- package/dist/schema.js +135 -0
- package/dist/testing/platform-gates.d.ts +54 -0
- package/dist/testing/platform-gates.js +62 -0
- package/dist/tool-failure.d.ts +13 -0
- package/dist/tool-failure.js +19 -0
- package/dist/tools/ask-user.d.ts +32 -0
- package/dist/tools/ask-user.js +145 -0
- package/dist/tools/browser.d.ts +24 -0
- package/dist/tools/browser.js +132 -0
- package/dist/tools/http-request.d.ts +31 -0
- package/dist/tools/http-request.js +117 -0
- package/dist/tools/list-dir.d.ts +9 -0
- package/dist/tools/list-dir.js +45 -0
- package/dist/tools/load-skill.d.ts +37 -0
- package/dist/tools/load-skill.js +76 -0
- package/dist/tools/memory.d.ts +46 -0
- package/dist/tools/memory.js +131 -0
- package/dist/tools/process-control.d.ts +14 -0
- package/dist/tools/process-control.js +49 -0
- package/dist/tools/read-file.d.ts +11 -0
- package/dist/tools/read-file.js +81 -0
- package/dist/tools/read-image.d.ts +33 -0
- package/dist/tools/read-image.js +144 -0
- package/dist/tools/run-code.d.ts +48 -0
- package/dist/tools/run-code.js +115 -0
- package/dist/tools/run-shell.d.ts +22 -0
- package/dist/tools/run-shell.js +81 -0
- package/dist/tools/write-file.d.ts +8 -0
- package/dist/tools/write-file.js +31 -0
- package/package.json +28 -0
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `run_code` parent broker (`crates/tools/src/run_code.rs:562-978`).
|
|
3
|
+
*
|
|
4
|
+
* One `run_code` call = one round trip. The assembled program — TypeScript by
|
|
5
|
+
* default since W774, Python on request — runs in the sandbox; its sub-calls
|
|
6
|
+
* arrive as one-line JSON on stdout and the parent answers on stdin after
|
|
7
|
+
* dispatching each one through the **same** registry pipeline (schema → guards →
|
|
8
|
+
* execute) the model itself would use. Only `main()`'s return value travels back
|
|
9
|
+
* as the tool result.
|
|
10
|
+
*
|
|
11
|
+
* The protocol is language-neutral (it is byte-identical for both SDKs), so the
|
|
12
|
+
* language only decides two things: the script file's extension and the
|
|
13
|
+
* interpreter that runs it.
|
|
14
|
+
*
|
|
15
|
+
* Invariants:
|
|
16
|
+
* - every limit is enforced here, never in the child: the 21st sub-call is
|
|
17
|
+
* refused before dispatch, the wall clock is enforced while waiting for a
|
|
18
|
+
* line, the sub-call output ledger is charged per reply;
|
|
19
|
+
* - every infrastructure failure is a structured `run_code: code=… msg="…"`
|
|
20
|
+
* (invalid_arg | registry | config | spawn | protocol | timeout | aborted);
|
|
21
|
+
* a program exception is that exception's text plus a bounded log tail;
|
|
22
|
+
* - the child is killed on timeout, on cancel and on protocol failure — never
|
|
23
|
+
* left behind — and its script file is removed on every exit path.
|
|
24
|
+
*/
|
|
25
|
+
import { existsSync } from "node:fs";
|
|
26
|
+
import { mkdir, rm, writeFile } from "node:fs/promises";
|
|
27
|
+
import { join } from "node:path";
|
|
28
|
+
import { errorCode, errorText } from "../errors.js";
|
|
29
|
+
import { stringArg } from "../args.js";
|
|
30
|
+
import { resolveShellKind } from "../platform/exec.js";
|
|
31
|
+
import { pythonCandidates, runCodeCommand } from "../platform/quote.js";
|
|
32
|
+
import { whichSync } from "../sandbox/probe.js";
|
|
33
|
+
import { TIMED_OUT, withTimeout } from "../sandbox/async.js";
|
|
34
|
+
import { readCapped, REAP_GRACE_MS } from "../sandbox/launch.js";
|
|
35
|
+
import { ToolFailure } from "../tool-failure.js";
|
|
36
|
+
import { EXIT_GRACE_MS, MAX_LINE_BYTES, RUN_CODE_ERROR_PREFIX, SDK_TOOLS, STDIN_WRITE_TIMEOUT_MS, resolveTimeoutMs, runCodeFailure, } from "./limits.js";
|
|
37
|
+
import { LineReader, appendBounded, jsonByteLength, tail, truncateValue } from "./lines.js";
|
|
38
|
+
import { assembleProgram, DEFAULT_RUN_CODE_LANGUAGE } from "./sdk.js";
|
|
39
|
+
let scriptSeq = 0;
|
|
40
|
+
/**
|
|
41
|
+
* Absolute interpreter path of a TypeScript program (W774): the Node that runs
|
|
42
|
+
* THIS process, which is the same binary the sandbox can see (`--ro-bind / /`
|
|
43
|
+
* mounts the host root read-only) and never depends on the child's PATH.
|
|
44
|
+
* `/usr/bin/node` is preferred because it is the host's system-wide install;
|
|
45
|
+
* `process.execPath` is the honest fallback (nvm/volta hosts).
|
|
46
|
+
*
|
|
47
|
+
* W885: the constant is kept for compatibility, but the interpreter actually
|
|
48
|
+
* used is resolved per run (`resolveInterpreter`), which checks the platform
|
|
49
|
+
* PATH first — `/usr/bin/node` is a POSIX convention that simply does not
|
|
50
|
+
* exist on Windows (W883 B9/B15).
|
|
51
|
+
*/
|
|
52
|
+
export const TS_PROGRAM_RUNTIME = existsSync("/usr/bin/node") ? "/usr/bin/node" : process.execPath;
|
|
53
|
+
/** One full run_code round trip: the program's final value + its render. */
|
|
54
|
+
export async function brokerRun(ctx, args) {
|
|
55
|
+
const source = programSource(args);
|
|
56
|
+
const timeoutMs = resolveTimeoutMs(readArg(args, "timeout_ms"), ctx.config);
|
|
57
|
+
// W880: the program is written to <CELESTEA_HOME>/.../run-code, NOT into the
|
|
58
|
+
// workspace. The sandbox config owns that absolute path; the interpreter is
|
|
59
|
+
// invoked with the absolute path so no cwd-relative lookup is involved.
|
|
60
|
+
const script = await placeProgram(ctx.sandbox.config.programDir, source);
|
|
61
|
+
const state = newRunState();
|
|
62
|
+
try {
|
|
63
|
+
await executeProgram(ctx, script.path, source.language, timeoutMs, state);
|
|
64
|
+
}
|
|
65
|
+
catch (e) {
|
|
66
|
+
throw withLogs(e, ctx, state);
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
await script.cleanup();
|
|
70
|
+
}
|
|
71
|
+
return outcomeOf(ctx, state);
|
|
72
|
+
}
|
|
73
|
+
/** `code` must be a non-empty program; `language` defaults to TypeScript (W774). */
|
|
74
|
+
function programSource(args) {
|
|
75
|
+
const code = stringArg(args, "code");
|
|
76
|
+
if (code.trim() === "")
|
|
77
|
+
throw runCodeFailure("invalid_arg", "'code' must be a non-empty program");
|
|
78
|
+
const raw = readArg(args, "language");
|
|
79
|
+
if (raw === undefined || raw === null)
|
|
80
|
+
return { code, language: DEFAULT_RUN_CODE_LANGUAGE };
|
|
81
|
+
if (raw === "typescript" || raw === "python")
|
|
82
|
+
return { code, language: raw };
|
|
83
|
+
// The spec's enum refuses anything else before dispatch; this is the floor.
|
|
84
|
+
throw runCodeFailure("invalid_arg", `'language' must be 'typescript' or 'python' (got ${JSON.stringify(raw)})`);
|
|
85
|
+
}
|
|
86
|
+
function readArg(args, key) {
|
|
87
|
+
if (typeof args !== "object" || args === null)
|
|
88
|
+
return undefined;
|
|
89
|
+
return args[key];
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Write `SDK + user code + runner` into
|
|
93
|
+
* `<programDir>/run_code_<pid>_<n>.{ts,py}` — the extension is the ONLY thing the
|
|
94
|
+
* language changes about placement. `programDir` is the sandbox config's
|
|
95
|
+
* absolute `<CELESTEA_HOME>/workspaces/<ws>/run-code`; the host has full-disk
|
|
96
|
+
* access so it writes directly, and the bwrap provider binds that dir into the
|
|
97
|
+
* namespace so the child can read it.
|
|
98
|
+
*/
|
|
99
|
+
async function placeProgram(programDir, source) {
|
|
100
|
+
try {
|
|
101
|
+
await mkdir(programDir, { recursive: true });
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
throw runCodeFailure("config", `cannot create '${programDir}': ${errorText(e)}`);
|
|
105
|
+
}
|
|
106
|
+
const suffix = source.language === "python" ? "py" : "ts";
|
|
107
|
+
const name = `run_code_${process.pid}_${scriptSeq++}.${suffix}`;
|
|
108
|
+
const path = join(programDir, name);
|
|
109
|
+
try {
|
|
110
|
+
await writeFile(path, assembleProgram(source.code, source.language), "utf8");
|
|
111
|
+
}
|
|
112
|
+
catch (e) {
|
|
113
|
+
throw runCodeFailure("spawn", `cannot write program file '${path}': ${errorText(e)}`);
|
|
114
|
+
}
|
|
115
|
+
return { path, cleanup: () => rm(path, { force: true }).catch(() => undefined) };
|
|
116
|
+
}
|
|
117
|
+
// ---- child lifecycle ---------------------------------------------------------
|
|
118
|
+
/**
|
|
119
|
+
* The interpreter of one `run_code` program (W885).
|
|
120
|
+
*
|
|
121
|
+
* TypeScript runs under the Node that runs THIS process — the one binary the
|
|
122
|
+
* sandbox is guaranteed to see through `--ro-bind / /` — with the host's PATH
|
|
123
|
+
* consulted first ONLY when that yields a Node (`node` is `node.exe` on
|
|
124
|
+
* Windows, and `process.execPath` there is routinely `C:\\Program Files\\…`
|
|
125
|
+
* with a space in it). Python keeps the historical `python3` on POSIX and
|
|
126
|
+
* falls back to the names Windows actually ships (`python`, `py`).
|
|
127
|
+
*
|
|
128
|
+
* Anything `whichSync` cannot find becomes the bare name, so a missing
|
|
129
|
+
* interpreter still surfaces as the interpreter's own "not found" instead of a
|
|
130
|
+
* silent wrong one.
|
|
131
|
+
*/
|
|
132
|
+
export function resolveInterpreter(language, platform = process.platform, env = process.env) {
|
|
133
|
+
if (language === "typescript") {
|
|
134
|
+
const onPath = whichSync("node", env, platform);
|
|
135
|
+
if (onPath !== null)
|
|
136
|
+
return onPath;
|
|
137
|
+
return process.execPath;
|
|
138
|
+
}
|
|
139
|
+
for (const candidate of pythonCandidates(platform)) {
|
|
140
|
+
const found = whichSync(candidate, env, platform);
|
|
141
|
+
if (found !== null)
|
|
142
|
+
return found;
|
|
143
|
+
}
|
|
144
|
+
return pythonCandidates(platform)[0] ?? "python3";
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The interpreter command line, quoted for the shell that will run it.
|
|
148
|
+
*
|
|
149
|
+
* On POSIX the bytes are exactly what they always were — `python3 -uB '<path>'`
|
|
150
|
+
* / `<node> '<path>'` — because `kind` is `posix` there and both quoting
|
|
151
|
+
* helpers reproduce the historical single-quote rule verbatim.
|
|
152
|
+
*/
|
|
153
|
+
function interpreterCommand(language, scriptPath, input = {}) {
|
|
154
|
+
const kind = resolveShellKind(input).kind;
|
|
155
|
+
return runCodeCommand(kind, language, resolveInterpreter(language), scriptPath);
|
|
156
|
+
}
|
|
157
|
+
async function spawnProgram(sandbox, scriptPath, language) {
|
|
158
|
+
let spawned;
|
|
159
|
+
try {
|
|
160
|
+
spawned = await sandbox.spawn({ command: interpreterCommand(language, scriptPath, sandbox.shell) });
|
|
161
|
+
}
|
|
162
|
+
catch (e) {
|
|
163
|
+
throw runCodeFailure("spawn", errorText(e));
|
|
164
|
+
}
|
|
165
|
+
if (spawned.child.stdin === null)
|
|
166
|
+
throw runCodeFailure("spawn", "no stdin pipe (reply channel)");
|
|
167
|
+
return spawned.child;
|
|
168
|
+
}
|
|
169
|
+
/** Spawn, pump the protocol to completion, then settle (and always clean up). */
|
|
170
|
+
async function executeProgram(ctx, scriptPath, language, timeoutMs, state) {
|
|
171
|
+
const child = await spawnProgram(ctx.sandbox, scriptPath, language);
|
|
172
|
+
const stderr = readCapped(child.stderr, ctx.config.maxLogBytes);
|
|
173
|
+
// W833 (R3 B1 / W812 P1-1): the wall clock is enforced HERE, not only while
|
|
174
|
+
// waiting for the next stdout line. A slow sub-call (run_shell itself allows
|
|
175
|
+
// 300s) or a reply write parked on a full pipe lives INSIDE pumpLines, where
|
|
176
|
+
// the reader's own deadline cannot be consulted; this timer kills the child
|
|
177
|
+
// and records code=timeout no matter which await the pump is parked on.
|
|
178
|
+
let wallTimer;
|
|
179
|
+
let timedOut = false;
|
|
180
|
+
let pumpError = null;
|
|
181
|
+
const wallFired = new Promise((resolve) => {
|
|
182
|
+
wallTimer = setTimeout(() => {
|
|
183
|
+
timedOut = true;
|
|
184
|
+
if (state.infraError === null)
|
|
185
|
+
state.infraError = timeoutMessage(child, timeoutMs, state);
|
|
186
|
+
child.kill();
|
|
187
|
+
resolve();
|
|
188
|
+
}, timeoutMs);
|
|
189
|
+
});
|
|
190
|
+
const pump = pumpLines(ctx, child, timeoutMs, state).catch((error) => {
|
|
191
|
+
// Park the rejection: after the wall clock has already won, the pump may
|
|
192
|
+
// still fail (its write lands on a destroyed stdin) and must not surface as
|
|
193
|
+
// an unhandled rejection. The outcome is decided below.
|
|
194
|
+
pumpError = error;
|
|
195
|
+
});
|
|
196
|
+
try {
|
|
197
|
+
await Promise.race([pump, wallFired]);
|
|
198
|
+
}
|
|
199
|
+
finally {
|
|
200
|
+
clearTimeout(wallTimer);
|
|
201
|
+
endStdin(child.stdin);
|
|
202
|
+
}
|
|
203
|
+
if (pumpError !== null && !timedOut) {
|
|
204
|
+
child.kill();
|
|
205
|
+
throw pumpError;
|
|
206
|
+
}
|
|
207
|
+
const settled = await settleChild(child, EXIT_GRACE_MS);
|
|
208
|
+
const captured = await stderr;
|
|
209
|
+
state.settle = { ...settled, stderrText: captured.text, stderrTruncated: captured.truncated };
|
|
210
|
+
}
|
|
211
|
+
/** Wait for a natural exit within `graceMs`; on expiry kill the tree. */
|
|
212
|
+
async function settleChild(child, graceMs) {
|
|
213
|
+
const exit = await withTimeout(child.wait(), graceMs);
|
|
214
|
+
if (exit !== TIMED_OUT)
|
|
215
|
+
return { exitCode: exit.code, killed: false };
|
|
216
|
+
child.kill();
|
|
217
|
+
await withTimeout(child.wait(), REAP_GRACE_MS);
|
|
218
|
+
return { exitCode: null, killed: true };
|
|
219
|
+
}
|
|
220
|
+
/** Close our end of the reply channel so a blocked bridge call sees EOF. */
|
|
221
|
+
function endStdin(stdin) {
|
|
222
|
+
if (stdin === null)
|
|
223
|
+
return;
|
|
224
|
+
stdin.on("error", () => undefined);
|
|
225
|
+
try {
|
|
226
|
+
stdin.end();
|
|
227
|
+
}
|
|
228
|
+
catch {
|
|
229
|
+
// already closed: nothing to release
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// ---- the broker loop ---------------------------------------------------------
|
|
233
|
+
/** Read protocol lines until `__final__` / `__error__` / EOF / the deadline. */
|
|
234
|
+
async function pumpLines(ctx, child, timeoutMs, state) {
|
|
235
|
+
const reader = new LineReader(child.stdout, MAX_LINE_BYTES);
|
|
236
|
+
const deadline = Date.now() + timeoutMs;
|
|
237
|
+
try {
|
|
238
|
+
for (;;) {
|
|
239
|
+
const line = await reader.next(deadline - Date.now());
|
|
240
|
+
if (line === TIMED_OUT) {
|
|
241
|
+
state.infraError = timeoutMessage(child, timeoutMs, state);
|
|
242
|
+
child.kill();
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (line === null)
|
|
246
|
+
return;
|
|
247
|
+
if ((await handleLine(ctx, child, line, state)) === "stop")
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
finally {
|
|
252
|
+
reader.stop();
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/** Classify one stdout line: final / error / request / log. */
|
|
256
|
+
async function handleLine(ctx, child, line, state) {
|
|
257
|
+
const trimmed = line.text.trimEnd();
|
|
258
|
+
if (line.truncated) {
|
|
259
|
+
logLine(state, ctx.config, trimmed, true);
|
|
260
|
+
return "continue";
|
|
261
|
+
}
|
|
262
|
+
const decoded = decodeObject(trimmed);
|
|
263
|
+
if (decoded === null) {
|
|
264
|
+
logLine(state, ctx.config, trimmed);
|
|
265
|
+
return "continue";
|
|
266
|
+
}
|
|
267
|
+
if ("__final__" in decoded) {
|
|
268
|
+
state.hasFinal = true;
|
|
269
|
+
state.finalValue = decoded["__final__"];
|
|
270
|
+
return "stop";
|
|
271
|
+
}
|
|
272
|
+
if ("__error__" in decoded) {
|
|
273
|
+
state.programError = typeof decoded["__error__"] === "string" ? decoded["__error__"] : "unknown program error";
|
|
274
|
+
return "stop";
|
|
275
|
+
}
|
|
276
|
+
const request = requestOf(decoded);
|
|
277
|
+
if (request === null) {
|
|
278
|
+
logLine(state, ctx.config, trimmed);
|
|
279
|
+
return "continue";
|
|
280
|
+
}
|
|
281
|
+
await answerSubCall(ctx, child, request, state);
|
|
282
|
+
return "continue";
|
|
283
|
+
}
|
|
284
|
+
/** Objects only: a JSON scalar / array on stdout is a log line (parity). */
|
|
285
|
+
function decodeObject(text) {
|
|
286
|
+
if (!text.startsWith("{"))
|
|
287
|
+
return null;
|
|
288
|
+
try {
|
|
289
|
+
const decoded = JSON.parse(text);
|
|
290
|
+
if (typeof decoded !== "object" || decoded === null || Array.isArray(decoded))
|
|
291
|
+
return null;
|
|
292
|
+
return decoded;
|
|
293
|
+
}
|
|
294
|
+
catch {
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
/** A request needs an integer `id`, a string `tool` and an object `args`. */
|
|
299
|
+
function requestOf(decoded) {
|
|
300
|
+
const { id, tool, args } = decoded;
|
|
301
|
+
if (typeof id !== "number" || !Number.isInteger(id))
|
|
302
|
+
return null;
|
|
303
|
+
if (typeof tool !== "string")
|
|
304
|
+
return null;
|
|
305
|
+
if (typeof args !== "object" || args === null || Array.isArray(args))
|
|
306
|
+
return null;
|
|
307
|
+
return { id, tool, args: args };
|
|
308
|
+
}
|
|
309
|
+
// ---- sub-call dispatch -------------------------------------------------------
|
|
310
|
+
async function answerSubCall(ctx, child, request, state) {
|
|
311
|
+
const reply = await buildReply(ctx, request, state);
|
|
312
|
+
try {
|
|
313
|
+
await writeReply(child.stdin, encodeReply(reply, request.id));
|
|
314
|
+
}
|
|
315
|
+
catch (e) {
|
|
316
|
+
if (e instanceof ToolFailure && e.kind === "timeout") {
|
|
317
|
+
// W833 (R3 B1): a reply the child never drains is a wall-clock failure,
|
|
318
|
+
// not a protocol error. Record it, kill the child and let the pump stop.
|
|
319
|
+
if (state.infraError === null)
|
|
320
|
+
state.infraError = e.message;
|
|
321
|
+
child.kill();
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
throw runCodeFailure("protocol", `cannot write reply to the program (stdin closed): ${errorText(e)}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
/** Whitelist → sub-call budget → dispatch through the shared registry. */
|
|
328
|
+
async function buildReply(ctx, request, state) {
|
|
329
|
+
if (!SDK_TOOLS.includes(request.tool)) {
|
|
330
|
+
return { id: request.id, ok: false, error: `tool '${request.tool}' not exposed in run_code SDK` };
|
|
331
|
+
}
|
|
332
|
+
if (state.dispatched >= ctx.config.maxSubCalls) {
|
|
333
|
+
return {
|
|
334
|
+
id: request.id,
|
|
335
|
+
ok: false,
|
|
336
|
+
error: `${RUN_CODE_ERROR_PREFIX}: sub-call limit exceeded (max ${ctx.config.maxSubCalls})`,
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
state.dispatched += 1;
|
|
340
|
+
const subCallId = `${ctx.parentId}:c${request.id}`;
|
|
341
|
+
const input = { call_id: subCallId, name: request.tool, args: request.args };
|
|
342
|
+
ctx.events?.({ type: "tool_call", id: subCallId, name: request.tool, args: request.args, parent_id: ctx.parentId });
|
|
343
|
+
const out = await ctx.registry.dispatch(input);
|
|
344
|
+
ctx.events?.({ type: "tool_result", id: subCallId, value: out.value, error: out.error, parent_id: ctx.parentId });
|
|
345
|
+
if (out.error !== null)
|
|
346
|
+
return { id: request.id, ok: false, error: out.error };
|
|
347
|
+
return valueReply(ctx.config, request.id, out.value, state);
|
|
348
|
+
}
|
|
349
|
+
/** Charge the sub-call output ledger; oversized values are cut with a warning. */
|
|
350
|
+
function valueReply(config, id, value, state) {
|
|
351
|
+
const size = jsonByteLength(value);
|
|
352
|
+
const room = Math.max(config.maxSubOutputBytes - state.subOutputBytes, 0);
|
|
353
|
+
if (size !== null && size <= room) {
|
|
354
|
+
state.subOutputBytes += size;
|
|
355
|
+
return { id, ok: true, value };
|
|
356
|
+
}
|
|
357
|
+
const cut = truncateValue(value, room);
|
|
358
|
+
const cutSize = jsonByteLength(cut) ?? 0;
|
|
359
|
+
state.subOutputBytes += cutSize;
|
|
360
|
+
state.subOutputDropped += Math.max((size ?? 0) - cutSize, 0);
|
|
361
|
+
return {
|
|
362
|
+
id,
|
|
363
|
+
ok: true,
|
|
364
|
+
value: cut,
|
|
365
|
+
truncated: true,
|
|
366
|
+
warning: `sub-call output budget (${config.maxSubOutputBytes} bytes) exceeded; value truncated`,
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
function encodeReply(reply, id) {
|
|
370
|
+
try {
|
|
371
|
+
const encoded = JSON.stringify(reply);
|
|
372
|
+
if (encoded !== undefined)
|
|
373
|
+
return encoded;
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
// fall through: the value cannot cross the wire, tell the program why
|
|
377
|
+
}
|
|
378
|
+
return JSON.stringify({ id, ok: false, error: "reply not serializable" });
|
|
379
|
+
}
|
|
380
|
+
async function writeReply(stdin, line) {
|
|
381
|
+
const written = new Promise((resolve, reject) => {
|
|
382
|
+
stdin.write(`${line}\n`, (error) => (error === null || error === undefined ? resolve() : reject(error)));
|
|
383
|
+
});
|
|
384
|
+
if ((await withTimeout(written, STDIN_WRITE_TIMEOUT_MS)) === TIMED_OUT) {
|
|
385
|
+
throw runCodeFailure("timeout", `reply was not written after ${STDIN_WRITE_TIMEOUT_MS}ms (the program stopped reading stdin)`);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
// ---- logs, render, outcome ---------------------------------------------------
|
|
389
|
+
/**
|
|
390
|
+
* Append one log line to the bounded stdout log. Lines append back to
|
|
391
|
+
* back (no separator): the budget is a byte ledger, not a pretty printer.
|
|
392
|
+
*/
|
|
393
|
+
function logLine(state, config, text, truncated = false) {
|
|
394
|
+
const merged = appendBounded(state.logs, text, config.maxLogBytes);
|
|
395
|
+
state.logs = merged.text;
|
|
396
|
+
state.logsTruncated =
|
|
397
|
+
state.logsTruncated || merged.truncated || truncated || Buffer.byteLength(text, "utf8") > config.maxLogBytes;
|
|
398
|
+
}
|
|
399
|
+
function timeoutMessage(child, timeoutMs, state) {
|
|
400
|
+
const pid = child.pid === null ? "?" : String(child.pid);
|
|
401
|
+
const captured = Buffer.byteLength(state.logs, "utf8");
|
|
402
|
+
return runCodeFailure("timeout", `killed pid ${pid} after ${timeoutMs}ms (wall clock; stdout_log_captured_bytes=${captured})`)
|
|
403
|
+
.message;
|
|
404
|
+
}
|
|
405
|
+
function abortedMessage(state) {
|
|
406
|
+
const settled = state.settle;
|
|
407
|
+
const code = settled === null || settled.exitCode === null ? "?" : String(settled.exitCode);
|
|
408
|
+
const killed = settled?.killed === true;
|
|
409
|
+
return runCodeFailure("aborted", `program exited code=${code} (killed=${killed}) without a final line`).message;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Human rendering: stdout logs + stderr tail + budget warnings (bounded).
|
|
413
|
+
*
|
|
414
|
+
* W833 (R3 B1 / W812 P2-2): the numbers come from the EFFECTIVE config, not the
|
|
415
|
+
* module constants — a caller that injects a smaller budget must see its own
|
|
416
|
+
* value in the render or the model is told a threshold that does not apply.
|
|
417
|
+
*/
|
|
418
|
+
function composeRender(config, state) {
|
|
419
|
+
const parts = [];
|
|
420
|
+
const settled = state.settle;
|
|
421
|
+
if (state.logs !== "")
|
|
422
|
+
parts.push(state.logs);
|
|
423
|
+
if (state.logsTruncated)
|
|
424
|
+
parts.push(`[run_code] stdout logs truncated at ${config.maxLogBytes} bytes`);
|
|
425
|
+
if (settled !== null && settled.stderrText !== "")
|
|
426
|
+
parts.push(`[stderr]\n${settled.stderrText}`);
|
|
427
|
+
if (settled !== null && settled.stderrTruncated)
|
|
428
|
+
parts.push(`[run_code] stderr truncated at ${config.maxLogBytes} bytes`);
|
|
429
|
+
if (state.subOutputDropped > 0) {
|
|
430
|
+
parts.push(`[run_code] warning: sub-call output budget (${config.maxSubOutputBytes} bytes) exceeded — ${state.subOutputDropped} bytes dropped`);
|
|
431
|
+
}
|
|
432
|
+
return parts.length === 0 ? null : parts.join("\n");
|
|
433
|
+
}
|
|
434
|
+
/** The canonical value, or the structured error (infra > program > aborted). */
|
|
435
|
+
function outcomeOf(ctx, state) {
|
|
436
|
+
const render = composeRender(ctx.config, state);
|
|
437
|
+
const error = state.infraError ?? state.programError ?? (state.hasFinal ? null : abortedMessage(state));
|
|
438
|
+
if (error !== null)
|
|
439
|
+
throw new ToolFailure(errorCode(error) ?? RUN_CODE_ERROR_PREFIX, withLogsText(error, render));
|
|
440
|
+
return { value: state.hasFinal ? state.finalValue : null, render };
|
|
441
|
+
}
|
|
442
|
+
/** Attach the bounded render tail to a failure (legacy `FailureCtx`). */
|
|
443
|
+
function withLogsText(error, render) {
|
|
444
|
+
if (render === null || render === "")
|
|
445
|
+
return error;
|
|
446
|
+
return `${error}\n[run_code] logs:\n${tail(render, 2048)}`;
|
|
447
|
+
}
|
|
448
|
+
function withLogs(error, ctx, state) {
|
|
449
|
+
const text = withLogsText(errorText(error), composeRender(ctx.config, state));
|
|
450
|
+
return error instanceof ToolFailure ? new ToolFailure(error.kind, text) : new ToolFailure(RUN_CODE_ERROR_PREFIX, text);
|
|
451
|
+
}
|
|
452
|
+
function newRunState() {
|
|
453
|
+
return {
|
|
454
|
+
logs: "",
|
|
455
|
+
logsTruncated: false,
|
|
456
|
+
subOutputBytes: 0,
|
|
457
|
+
subOutputDropped: 0,
|
|
458
|
+
dispatched: 0,
|
|
459
|
+
hasFinal: false,
|
|
460
|
+
finalValue: null,
|
|
461
|
+
programError: null,
|
|
462
|
+
infraError: null,
|
|
463
|
+
settle: null,
|
|
464
|
+
};
|
|
465
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `run_code` hard limits + tuning knobs (W255 parity:
|
|
3
|
+
* `crates/tools/src/run_code.rs:39-153`).
|
|
4
|
+
*
|
|
5
|
+
* The limits are parent-side and non-negotiable: the child program cannot talk
|
|
6
|
+
* its way past them, because the broker never dispatches what they forbid.
|
|
7
|
+
* - ≤[`MAX_SUB_CALLS`] sub-calls (the next one is refused, not dispatched);
|
|
8
|
+
* - wall clock ≤[`MAX_TIMEOUT_MS`] (per-call `timeout_ms` may only lower it;
|
|
9
|
+
* [`ENV_RUN_CODE_TIMEOUT_MS`] tunes the *default*, never the cap);
|
|
10
|
+
* - sub-call output ledger ≤[`MAX_SUB_OUTPUT_BYTES`] (truncate + warning);
|
|
11
|
+
* - program stdout/stderr logs ≤[`MAX_LOG_BYTES`] (render only).
|
|
12
|
+
*/
|
|
13
|
+
import { ToolFailure } from "../tool-failure.js";
|
|
14
|
+
/** Stable prefix of every structured `run_code` infrastructure error. */
|
|
15
|
+
export declare const RUN_CODE_ERROR_PREFIX = "run_code";
|
|
16
|
+
/**
|
|
17
|
+
* The SDK whitelist — the only tool names the parent dispatches for a sub-call.
|
|
18
|
+
* `spawn_worker` / `send_message` / `process_control` / `http_request` /
|
|
19
|
+
* `run_code` are deliberately excluded (eval §6.5).
|
|
20
|
+
*/
|
|
21
|
+
export declare const SDK_TOOLS: readonly string[];
|
|
22
|
+
/** Hard sub-call budget: the 21st dispatched sub-call is refused. */
|
|
23
|
+
export declare const MAX_SUB_CALLS = 20;
|
|
24
|
+
/** Hard output ledger for sub-call results: 256 KiB (truncate + warn). */
|
|
25
|
+
export declare const MAX_SUB_OUTPUT_BYTES: number;
|
|
26
|
+
/** Hard budget for the program's stdout/stderr log lines (non-protocol): 64 KiB. */
|
|
27
|
+
export declare const MAX_LOG_BYTES: number;
|
|
28
|
+
/** Upper bound for one stdout line (protocol requests included): 1 MiB. */
|
|
29
|
+
export declare const MAX_LINE_BYTES: number;
|
|
30
|
+
/** Default whole-run wall clock: 120s. */
|
|
31
|
+
export declare const DEFAULT_TIMEOUT_MS = 120000;
|
|
32
|
+
/** Hard cap for `timeout_ms`: 120s. */
|
|
33
|
+
export declare const MAX_TIMEOUT_MS = 120000;
|
|
34
|
+
/** Grace period for the child to exit after its final/error line. */
|
|
35
|
+
export declare const EXIT_GRACE_MS = 2000;
|
|
36
|
+
/**
|
|
37
|
+
* W833 (R3 B1 / W812 P1-1): bounded wait for ONE protocol reply to reach the
|
|
38
|
+
* child's stdin. A program that requests a sub-call and then stops reading
|
|
39
|
+
* stdin fills the OS pipe buffer; without this bound the parent would park on
|
|
40
|
+
* the write callback forever (the wall clock only fires once the pump returns
|
|
41
|
+
* to the reader). A wedged write is a wall-clock failure: kill + code=timeout.
|
|
42
|
+
*/
|
|
43
|
+
export declare const STDIN_WRITE_TIMEOUT_MS = 5000;
|
|
44
|
+
/** Env var: default whole-run wall clock in ms (clamped to [1, 120000]). */
|
|
45
|
+
export declare const ENV_RUN_CODE_TIMEOUT_MS = "CELAESTEA_RUN_CODE_TIMEOUT_MS";
|
|
46
|
+
/** Tuning knobs for the broker (legacy `RunCodeConfig`). */
|
|
47
|
+
export interface RunCodeConfig {
|
|
48
|
+
/** Default wall clock; a per-call `timeout_ms` is bounded by [`MAX_TIMEOUT_MS`]. */
|
|
49
|
+
timeoutMs: number;
|
|
50
|
+
/** Sub-call budget; the next call is refused. */
|
|
51
|
+
maxSubCalls: number;
|
|
52
|
+
/** Sub-call output ledger in bytes. */
|
|
53
|
+
maxSubOutputBytes: number;
|
|
54
|
+
/** Program stdout/stderr log budget in bytes. */
|
|
55
|
+
maxLogBytes: number;
|
|
56
|
+
}
|
|
57
|
+
/** Defaults: 120s wall clock, 20 sub-calls, 256KiB sub-call output, 64KiB logs. */
|
|
58
|
+
export declare function runCodeConfig(overrides?: Partial<RunCodeConfig>): RunCodeConfig;
|
|
59
|
+
/** [`runCodeConfig`] after applying `CELAESTEA_RUN_CODE_TIMEOUT_MS` (clamped). */
|
|
60
|
+
export declare function runCodeConfigFromEnv(env?: NodeJS.ProcessEnv): RunCodeConfig;
|
|
61
|
+
/** Clamp a wall clock to [1ms, 120000ms] — the cap is hard. */
|
|
62
|
+
export declare function clampTimeoutMs(ms: number): number;
|
|
63
|
+
/** Structured infrastructure error (`run_code: code=<code> msg="<quoted>"`). */
|
|
64
|
+
export declare function runCodeFailure(code: string, message: string): ToolFailure;
|
|
65
|
+
/**
|
|
66
|
+
* The effective wall clock: `config.timeoutMs` unless the call passes
|
|
67
|
+
* `timeout_ms`, which must be an integer in [1, 120000] (legacy `invalid_arg`).
|
|
68
|
+
*/
|
|
69
|
+
export declare function resolveTimeoutMs(raw: unknown, config: RunCodeConfig): number;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `run_code` hard limits + tuning knobs (W255 parity:
|
|
3
|
+
* `crates/tools/src/run_code.rs:39-153`).
|
|
4
|
+
*
|
|
5
|
+
* The limits are parent-side and non-negotiable: the child program cannot talk
|
|
6
|
+
* its way past them, because the broker never dispatches what they forbid.
|
|
7
|
+
* - ≤[`MAX_SUB_CALLS`] sub-calls (the next one is refused, not dispatched);
|
|
8
|
+
* - wall clock ≤[`MAX_TIMEOUT_MS`] (per-call `timeout_ms` may only lower it;
|
|
9
|
+
* [`ENV_RUN_CODE_TIMEOUT_MS`] tunes the *default*, never the cap);
|
|
10
|
+
* - sub-call output ledger ≤[`MAX_SUB_OUTPUT_BYTES`] (truncate + warning);
|
|
11
|
+
* - program stdout/stderr logs ≤[`MAX_LOG_BYTES`] (render only).
|
|
12
|
+
*/
|
|
13
|
+
import { envInt } from "../env.js";
|
|
14
|
+
import { contractError } from "../errors.js";
|
|
15
|
+
import { ToolFailure } from "../tool-failure.js";
|
|
16
|
+
/** Stable prefix of every structured `run_code` infrastructure error. */
|
|
17
|
+
export const RUN_CODE_ERROR_PREFIX = "run_code";
|
|
18
|
+
/**
|
|
19
|
+
* The SDK whitelist — the only tool names the parent dispatches for a sub-call.
|
|
20
|
+
* `spawn_worker` / `send_message` / `process_control` / `http_request` /
|
|
21
|
+
* `run_code` are deliberately excluded (eval §6.5).
|
|
22
|
+
*/
|
|
23
|
+
export const SDK_TOOLS = ["read_file", "write_file", "list_dir", "run_shell"];
|
|
24
|
+
/** Hard sub-call budget: the 21st dispatched sub-call is refused. */
|
|
25
|
+
export const MAX_SUB_CALLS = 20;
|
|
26
|
+
/** Hard output ledger for sub-call results: 256 KiB (truncate + warn). */
|
|
27
|
+
export const MAX_SUB_OUTPUT_BYTES = 256 * 1024;
|
|
28
|
+
/** Hard budget for the program's stdout/stderr log lines (non-protocol): 64 KiB. */
|
|
29
|
+
export const MAX_LOG_BYTES = 64 * 1024;
|
|
30
|
+
/** Upper bound for one stdout line (protocol requests included): 1 MiB. */
|
|
31
|
+
export const MAX_LINE_BYTES = 1024 * 1024;
|
|
32
|
+
/** Default whole-run wall clock: 120s. */
|
|
33
|
+
export const DEFAULT_TIMEOUT_MS = 120_000;
|
|
34
|
+
/** Hard cap for `timeout_ms`: 120s. */
|
|
35
|
+
export const MAX_TIMEOUT_MS = 120_000;
|
|
36
|
+
/** Grace period for the child to exit after its final/error line. */
|
|
37
|
+
export const EXIT_GRACE_MS = 2_000;
|
|
38
|
+
/**
|
|
39
|
+
* W833 (R3 B1 / W812 P1-1): bounded wait for ONE protocol reply to reach the
|
|
40
|
+
* child's stdin. A program that requests a sub-call and then stops reading
|
|
41
|
+
* stdin fills the OS pipe buffer; without this bound the parent would park on
|
|
42
|
+
* the write callback forever (the wall clock only fires once the pump returns
|
|
43
|
+
* to the reader). A wedged write is a wall-clock failure: kill + code=timeout.
|
|
44
|
+
*/
|
|
45
|
+
export const STDIN_WRITE_TIMEOUT_MS = 5_000;
|
|
46
|
+
/** Env var: default whole-run wall clock in ms (clamped to [1, 120000]). */
|
|
47
|
+
export const ENV_RUN_CODE_TIMEOUT_MS = "CELAESTEA_RUN_CODE_TIMEOUT_MS";
|
|
48
|
+
/** Defaults: 120s wall clock, 20 sub-calls, 256KiB sub-call output, 64KiB logs. */
|
|
49
|
+
export function runCodeConfig(overrides = {}) {
|
|
50
|
+
return {
|
|
51
|
+
timeoutMs: overrides.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
52
|
+
maxSubCalls: overrides.maxSubCalls ?? MAX_SUB_CALLS,
|
|
53
|
+
maxSubOutputBytes: overrides.maxSubOutputBytes ?? MAX_SUB_OUTPUT_BYTES,
|
|
54
|
+
maxLogBytes: overrides.maxLogBytes ?? MAX_LOG_BYTES,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** [`runCodeConfig`] after applying `CELAESTEA_RUN_CODE_TIMEOUT_MS` (clamped). */
|
|
58
|
+
export function runCodeConfigFromEnv(env = process.env) {
|
|
59
|
+
const ms = envInt(env, ENV_RUN_CODE_TIMEOUT_MS);
|
|
60
|
+
return runCodeConfig(ms === undefined ? {} : { timeoutMs: clampTimeoutMs(ms) });
|
|
61
|
+
}
|
|
62
|
+
/** Clamp a wall clock to [1ms, 120000ms] — the cap is hard. */
|
|
63
|
+
export function clampTimeoutMs(ms) {
|
|
64
|
+
if (!Number.isFinite(ms))
|
|
65
|
+
return DEFAULT_TIMEOUT_MS;
|
|
66
|
+
return Math.min(Math.max(Math.trunc(ms), 1), MAX_TIMEOUT_MS);
|
|
67
|
+
}
|
|
68
|
+
/** Structured infrastructure error (`run_code: code=<code> msg="<quoted>"`). */
|
|
69
|
+
export function runCodeFailure(code, message) {
|
|
70
|
+
return new ToolFailure(code, contractError(RUN_CODE_ERROR_PREFIX, code, message));
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The effective wall clock: `config.timeoutMs` unless the call passes
|
|
74
|
+
* `timeout_ms`, which must be an integer in [1, 120000] (legacy `invalid_arg`).
|
|
75
|
+
*/
|
|
76
|
+
export function resolveTimeoutMs(raw, config) {
|
|
77
|
+
if (raw === undefined || raw === null)
|
|
78
|
+
return config.timeoutMs;
|
|
79
|
+
if (typeof raw !== "number" || !Number.isInteger(raw)) {
|
|
80
|
+
throw runCodeFailure("invalid_arg", "timeout_ms must be an integer");
|
|
81
|
+
}
|
|
82
|
+
if (raw < 1)
|
|
83
|
+
throw runCodeFailure("invalid_arg", `timeout_ms must be >= 1, got ${raw}`);
|
|
84
|
+
if (raw > MAX_TIMEOUT_MS) {
|
|
85
|
+
throw runCodeFailure("invalid_arg", `timeout_ms=${raw} exceeds the run_code maximum ${MAX_TIMEOUT_MS}ms`);
|
|
86
|
+
}
|
|
87
|
+
return raw;
|
|
88
|
+
}
|