@desplega.ai/agent-swarm 1.165.0 → 1.166.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/dist/{acp-adapter-w6zws6me.js → acp-adapter-yzfxn8vj.js} +4 -4
- package/dist/{actions-jna0b3wm.js → actions-qz3m692w.js} +20 -24
- package/dist/{amp-adapter-0eve81pz.js → amp-adapter-j1xbmsqz.js} +4 -4
- package/dist/{app-75hm5cfa.js → app-43vrdmv8.js} +6 -7
- package/dist/{assistant-yzd51j33.js → assistant-ertffmka.js} +21 -25
- package/dist/{boot-reembed-z158xq32.js → boot-reembed-998jtpdg.js} +9 -12
- package/dist/{boot-reembed-2ex2mcsh.js → boot-reembed-s7k7c6v2.js} +10 -12
- package/dist/boot-scrub-sweep-nepsk4t1.js +211 -0
- package/dist/{boot-seeds-2bmnh18p.js → boot-seeds-vvzs1zhk.js} +8 -8
- package/dist/{claude-adapter-jbj1mycz.js → claude-adapter-2zvmekrp.js} +9 -11
- package/dist/{claude-managed-adapter-17ggz09t.js → claude-managed-adapter-s49wbk8t.js} +3 -3
- package/dist/{claude-managed-setup-ptm1nzm2.js → claude-managed-setup-cvhqq8qn.js} +4 -4
- package/dist/{claude-sdk-session-9re1g7r2.js → claude-sdk-session-kkbxtw5q.js} +2618 -2600
- package/dist/{cli-k6xx3pve.js → cli-0nhdwxd5.js} +4 -4
- package/dist/{cli-n9ehc28t.js → cli-0qjcsxgz.js} +4 -4
- package/dist/{cli-zn2v3t8v.js → cli-3ddybbbx.js} +1 -1
- package/dist/{cli-5anb0ev3.js → cli-3qn4r334.js} +71 -14
- package/dist/{cli-1kzfvd1z.js → cli-4r1sjfc4.js} +9 -5
- package/dist/{cli-ba66wyhy.js → cli-66zh26sp.js} +6 -6
- package/dist/{cli-9xb89xca.js → cli-68r1j4sg.js} +6 -6
- package/dist/{cli-xnzrzets.js → cli-8kgvmkak.js} +2 -2
- package/dist/{cli-28smfvbn.js → cli-8t229cmr.js} +5 -5
- package/dist/{cli-9866bd07.js → cli-aqcey741.js} +1 -1
- package/dist/{cli-90v942q1.js → cli-cpfmqz3n.js} +8 -8
- package/dist/{cli-50eg13rj.js → cli-df56tqdw.js} +459 -282
- package/dist/{cli-8gpbwy09.js → cli-dw2q9f7g.js} +5 -5
- package/dist/{cli-0n4fasz0.js → cli-ecknp7p1.js} +15 -15
- package/dist/{cli-xb3c6aat.js → cli-f2n17zhp.js} +1 -1
- package/dist/{cli-ftry7975.js → cli-feh3dkah.js} +1 -1
- package/dist/{cli-amwhs8h2.js → cli-fn697bnt.js} +2 -2
- package/dist/{cli-prt9emct.js → cli-gt8azyet.js} +3 -3
- package/dist/{cli-59jn5twa.js → cli-h3x613ze.js} +2 -2
- package/dist/{cli-mmzwwptd.js → cli-hf5s9pgf.js} +1 -1
- package/dist/cli-j2jbr8af.js +516 -0
- package/dist/{cli-wfajtmf7.js → cli-jhm6hzjn.js} +593 -10
- package/dist/{cli-xtgtyafv.js → cli-jvj6a0cv.js} +190 -439
- package/dist/{cli-hpryr6em.js → cli-mb75wpxf.js} +1 -1
- package/dist/{cli-420bdgw0.js → cli-mv2520nm.js} +1 -1
- package/dist/{cli-pjxnr1jn.js → cli-mwegs4af.js} +3 -3
- package/dist/{cli-5b2fjjzh.js → cli-p4ztb80h.js} +2 -2
- package/dist/{cli-e7t4snpa.js → cli-rggdq8wq.js} +4 -4
- package/dist/{cli-bdv39kgj.js → cli-rsy933yz.js} +5 -5
- package/dist/{cli-gh5yqzpe.js → cli-v2a8x45d.js} +2 -2
- package/dist/{cli-bns5c8k9.js → cli-vzc73wet.js} +43 -47
- package/dist/cli-wt7ccy4r.js +2654 -0
- package/dist/{cli-j0pdrmwg.js → cli-x7kpr72a.js} +3 -3
- package/dist/{cli-x7afk5wm.js → cli-xvc3q9br.js} +1 -1
- package/dist/{cli-9s3h836g.js → cli-xvdgasgy.js} +1 -1
- package/dist/{cli-zvckv750.js → cli-y59h2pk9.js} +2 -2
- package/dist/{cli-qdv8kgz7.js → cli-yshaazs1.js} +13884 -3241
- package/dist/{cli-nfygw434.js → cli-ysnyhws9.js} +2 -2
- package/dist/{cli-fw6vdcq5.js → cli-zp67qg5x.js} +34 -1
- package/dist/{cli-ma3zvh39.js → cli-zy7vwsne.js} +6066 -5829
- package/dist/cli.js +24 -20
- package/dist/{codex-adapter-3qnxshje.js → codex-adapter-dw1wxqt8.js} +7 -7
- package/dist/{codex-login-brkxhhdg.js → codex-login-terbdsqq.js} +2 -2
- package/dist/{codex-session-runner-612wnapp.js → codex-session-runner-3cgb8y1g.js} +7 -7
- package/dist/{commands-2ybfcxkj.js → commands-mhxam6z2.js} +6 -6
- package/dist/{cursor-adapter-p3jgdehv.js → cursor-adapter-2qda3st7.js} +3 -3
- package/dist/{db-f4ans0jj.js → db-sjks418k.js} +8 -6
- package/dist/{devin-adapter-dhzt01ff.js → devin-adapter-bffjj5n4.js} +2 -2
- package/dist/{dispatcher-pk182apd.js → dispatcher-1es759rn.js} +18 -22
- package/dist/{dsh-adapter-7twaeps1.js → dsh-adapter-bek4cyw9.js} +3 -3
- package/dist/{e2b-y8t3jzyz.js → e2b-nd50eccg.js} +1 -1
- package/dist/{event-bus-w7ac4n3c.js → event-bus-vnfthafg.js} +2 -2
- package/dist/{grok-adapter-yt9pab3k.js → grok-adapter-ar9cw9jg.js} +5 -5
- package/dist/{handlers-g2wnr4t9.js → handlers-1gkbhktr.js} +21 -25
- package/dist/{hook-f7jt9vnm.js → hook-ds2zxq9v.js} +14 -16
- package/dist/{hook-bbdpnkb4.js → hook-vxa3tcsy.js} +8 -10
- package/dist/{http-w40q4w94.js → http-3h21z9md.js} +247 -164
- package/dist/{index-056j5kzn.js → index-9b65j1m5.js} +8 -8
- package/dist/{index-9bfssf54.js → index-bn6cy8ps.js} +18 -22
- package/dist/{index-cj1r9q83.js → index-dkfxpkmm.js} +19 -23
- package/dist/{index-7n70jwbb.js → index-f8xnqrye.js} +19 -21
- package/dist/{index-fdk1t4d6.js → index-n0rxa4ca.js} +11962 -9900
- package/dist/{index-k8xc083e.js → index-n99fycq7.js} +18 -22
- package/dist/{keepalive-1tj10tf4.js → keepalive-3xspa9jw.js} +11 -8
- package/dist/{lead-st0bg7v4.js → lead-6qphc124.js} +34 -38
- package/dist/{maintenance-nz71zats.js → maintenance-0teem1bb.js} +7 -9
- package/dist/{node-f0ykebbv.js → node-9sg7ajby.js} +25 -7
- package/dist/{oauth-refresh-sweep-aefzwvvq.js → oauth-refresh-sweep-sh9q25xp.js} +10 -7
- package/dist/{onboard-kgpr9mbn.js → onboard-54qmnqxe.js} +2 -2
- package/dist/{opencode-adapter-0c8e70dk.js → opencode-adapter-wjfa2wvd.js} +2 -2
- package/dist/{otel-impl-x8zp4y5a.js → otel-impl-ycqzp2a1.js} +2 -2
- package/dist/{pi-mono-adapter-t280ah5c.js → pi-mono-adapter-e77e9zst.js} +4 -4
- package/dist/{pricing-refresh-9e0q7cwk.js → pricing-refresh-atnkngqs.js} +7 -7
- package/dist/{rbac-roles-kcp6zge2.js → rbac-roles-9pjphh9q.js} +6 -6
- package/dist/{rbac-roles-790j3ga8.js → rbac-roles-j78wdksm.js} +6 -6
- package/dist/{render-v2-tch4nefj.js → render-v2-pxjs49fx.js} +6 -7
- package/dist/{setup-rcm303ns.js → setup-ymej0xtx.js} +2 -2
- package/dist/{work-objects-ctg7t70w.js → work-objects-3nh4ymzb.js} +22 -26
- package/dist/{worker-5dp0xm55.js → worker-qa9fkz5s.js} +34 -38
- package/dist/{x-v3yw66zs.js → x-ma4gnfgy.js} +2 -2
- package/openapi.json +5 -2
- package/package.json +9 -5
- package/src/be/automation-preflight.ts +9 -4
- package/src/be/boot-scrub-sweep.ts +302 -0
- package/src/be/crypto/key-bootstrap.ts +1 -1
- package/src/be/db/approvals.ts +15 -1
- package/src/be/db/codex-oauth-device-flows.ts +40 -0
- package/src/be/db/config-secrets.ts +31 -0
- package/src/be/db/runtime.ts +4 -0
- package/src/be/db/script-runs.ts +15 -0
- package/src/be/db/tasks/write.ts +1 -1
- package/src/be/db-queries/mcp-oauth.ts +5 -0
- package/src/be/db-queries/oauth.ts +77 -0
- package/src/be/db.ts +162 -59
- package/src/be/events.ts +17 -1
- package/src/be/memory/index-content.ts +2 -2
- package/src/be/memory/providers/sqlite-store.ts +63 -11
- package/src/be/memory/types.ts +14 -0
- package/src/be/migrations/201_workflow_replay_columns.sql +10 -0
- package/src/be/migrations/202_codex_oauth_device_flows.sql +24 -0
- package/src/be/scripts/db.ts +28 -4
- package/src/be/scripts/source-secrets.ts +37 -0
- package/src/be/scrub-json.ts +32 -0
- package/src/be/sealed-json.ts +59 -0
- package/src/be/secret-registry.ts +131 -0
- package/src/be/workflow-replay.ts +77 -0
- package/src/http/codex-oauth-device.ts +8 -10
- package/src/http/db-query-bounded.ts +3 -0
- package/src/http/db-query-shared.ts +107 -15
- package/src/http/db-query.ts +10 -2
- package/src/http/index.ts +23 -6
- package/src/http/script-runs.ts +9 -1
- package/src/http/scripts.ts +15 -0
- package/src/http/workflows.ts +3 -2
- package/src/oauth/mcp-wrapper.ts +1 -1
- package/src/providers/acp-adapter.ts +25 -11
- package/src/providers/acp-target-catalog.ts +35 -1
- package/src/providers/acp-targets.ts +69 -3
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/cursor-adapter.ts +1 -1
- package/src/script-workflows/supervisor.ts +11 -2
- package/src/scripts-runtime/eval-harness.ts +2 -12
- package/src/scripts-runtime/executors/quickjs-bundle.ts +152 -0
- package/src/scripts-runtime/executors/quickjs-prelude.ts +285 -0
- package/src/scripts-runtime/executors/quickjs-runner.ts +580 -0
- package/src/scripts-runtime/executors/quickjs-worker.ts +81 -0
- package/src/scripts-runtime/executors/quickjs.ts +196 -0
- package/src/scripts-runtime/executors/registry.ts +2 -0
- package/src/scripts-runtime/executors/sourcemap.ts +79 -0
- package/src/scripts-runtime/script-error-hints.ts +14 -0
- package/src/server.ts +3 -1
- package/src/tests/acp-adapter.test.ts +127 -2
- package/src/tests/boot-scrub-sweep.test.ts +390 -0
- package/src/tests/check-text-columns.test.ts +145 -0
- package/src/tests/claude-sdk-process.fixture.ts +13 -2
- package/src/tests/codex-oauth-device.test.ts +23 -37
- package/src/tests/db-query-denylist.test.ts +202 -0
- package/src/tests/db-task-read-contract.test.ts +1 -0
- package/src/tests/db-task-write-contract.test.ts +1 -0
- package/src/tests/entrypoint-acp-target-binary.test.ts +31 -1
- package/src/tests/events-scrub.test.ts +88 -0
- package/src/tests/fixtures/secret-corpus.ts +320 -0
- package/src/tests/script-connections.test.ts +5 -0
- package/src/tests/script-executor-conformance.test.ts +3 -0
- package/src/tests/script-executor-quickjs.test.ts +442 -0
- package/src/tests/script-executor-registry.test.ts +6 -1
- package/src/tests/script-runs-http.test.ts +88 -1
- package/src/tests/script-runs-scrub.test.ts +275 -0
- package/src/tests/scripts-http.test.ts +82 -1
- package/src/tests/scrubbed-text-12e.test.ts +289 -0
- package/src/tests/sealed-key-guard.test.ts +166 -0
- package/src/tests/secret-canary-sweep.test.ts +958 -0
- package/src/tests/secret-registry.test.ts +355 -0
- package/src/tests/secret-rules.test.ts +230 -0
- package/src/tests/secret-scrubber.test.ts +3 -2
- package/src/tests/workflow-replay.test.ts +518 -0
- package/src/tests/workflow-scrub.test.ts +242 -0
- package/src/tools/db-query.ts +1 -1
- package/src/tools/inject-learning.ts +1 -1
- package/src/tools/workflows/get-workflow-run.ts +3 -2
- package/src/types.ts +1 -1
- package/src/utils/secret-rules.generated.ts +2055 -0
- package/src/utils/secret-scrubber.ts +363 -29
- package/src/workflows/engine.ts +39 -4
- package/src/workflows/recovery.ts +10 -1
- package/src/workflows/resume.ts +62 -3
- package/src/workflows/retry-poller.ts +3 -0
- package/dist/boot-scrub-logs-246yn076.js +0 -99
- package/dist/cli-3a4m1aws.js +0 -572
- package/dist/cli-a87b04k8.js +0 -951
- package/dist/cli-cvk6yzw5.js +0 -40
- package/dist/cli-h1spv8v4.js +0 -427
- package/dist/cli-jqt9hrnz.js +0 -168
- package/dist/cli-yhmwmv0g.js +0 -9281
- package/src/be/boot-scrub-logs.ts +0 -132
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
QuickJSContext,
|
|
3
|
+
QuickJSDeferredPromise,
|
|
4
|
+
QuickJSHandle,
|
|
5
|
+
QuickJSWASMModule,
|
|
6
|
+
} from "quickjs-emscripten-core";
|
|
7
|
+
import { buildCtx, type RuntimeCtx } from "../ctx";
|
|
8
|
+
import { Redacted, type Redacted as RedactedValue } from "../redacted";
|
|
9
|
+
import { SCRIPT_SDK_RESPONSE_LIMIT_BYTES } from "../response-limit";
|
|
10
|
+
import { ctxSignatureHintFor } from "../script-error-hints";
|
|
11
|
+
import { runtimeFetch } from "../stdlib/fetch";
|
|
12
|
+
import { table } from "../stdlib/table";
|
|
13
|
+
import { SwarmConfig } from "../swarm-config";
|
|
14
|
+
import { bundleForQuickJS, USER_SCRIPT_FILE } from "./quickjs-bundle";
|
|
15
|
+
import { SANDBOX_EPILOGUE, SANDBOX_PRELUDE } from "./quickjs-prelude";
|
|
16
|
+
import type { SourceMapReader } from "./sourcemap";
|
|
17
|
+
import type {
|
|
18
|
+
ExecutorOutput,
|
|
19
|
+
ScriptExecutorError,
|
|
20
|
+
ScriptResourcePolicy,
|
|
21
|
+
ScriptRuntimeError,
|
|
22
|
+
ScriptStackFrame,
|
|
23
|
+
SwarmConfigPayload,
|
|
24
|
+
} from "./types";
|
|
25
|
+
|
|
26
|
+
export type QuickJSJob = {
|
|
27
|
+
source: string;
|
|
28
|
+
args: unknown;
|
|
29
|
+
configPayload: SwarmConfigPayload;
|
|
30
|
+
resources: ScriptResourcePolicy;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const BUNDLE_FILE = USER_SCRIPT_FILE.replace(/\.ts$/, ".js");
|
|
34
|
+
// Host calls (fetch, ctx.swarm, ctx.api, ctx.mcp) for one run: at most this
|
|
35
|
+
// many run at once, and the rest wait. The native executor has a similar
|
|
36
|
+
// bound from its file-descriptor ulimit (64).
|
|
37
|
+
const MAX_CONCURRENT_HOST_CALLS = 16;
|
|
38
|
+
// Host calls and timers that one run can have in flight or queued.
|
|
39
|
+
const MAX_PENDING_HOST_CALLS = 1024;
|
|
40
|
+
// After a run ends, its host calls are aborted. Wait this long for them to settle.
|
|
41
|
+
const HOST_CALL_SETTLE_MS = 1_000;
|
|
42
|
+
|
|
43
|
+
export type QuickJSJobResult = {
|
|
44
|
+
output: ExecutorOutput;
|
|
45
|
+
/** Host calls still running after the settle wait. The worker must not be reused. */
|
|
46
|
+
leakedHostCalls: number;
|
|
47
|
+
};
|
|
48
|
+
const MAX_STACK_BYTES = 1024 * 1024;
|
|
49
|
+
const TIMEOUT_EXIT_CODE = 124;
|
|
50
|
+
|
|
51
|
+
type ErrorPayload = { name: string; message: string; stack: string };
|
|
52
|
+
type Outcome = { ok: true; result: unknown } | { ok: false; error: ErrorPayload };
|
|
53
|
+
|
|
54
|
+
class CappedLog {
|
|
55
|
+
private readonly parts: string[] = [];
|
|
56
|
+
private bytes = 0;
|
|
57
|
+
truncated = false;
|
|
58
|
+
|
|
59
|
+
constructor(private readonly limit: number) {}
|
|
60
|
+
|
|
61
|
+
append(text: string): void {
|
|
62
|
+
if (this.truncated) return;
|
|
63
|
+
const size = Buffer.byteLength(text);
|
|
64
|
+
if (this.bytes + size <= this.limit) {
|
|
65
|
+
this.parts.push(text);
|
|
66
|
+
this.bytes += size;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const room = this.limit - this.bytes;
|
|
70
|
+
if (room > 0) this.parts.push(Buffer.from(text).subarray(0, room).toString());
|
|
71
|
+
this.bytes = this.limit;
|
|
72
|
+
this.truncated = true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get text(): string {
|
|
76
|
+
return this.parts.join("");
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function errorPayload(error: unknown): ErrorPayload {
|
|
81
|
+
if (error instanceof Error) {
|
|
82
|
+
return { name: error.name || "Error", message: error.message, stack: error.stack ?? "" };
|
|
83
|
+
}
|
|
84
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
85
|
+
const record = error as Record<string, unknown>;
|
|
86
|
+
return {
|
|
87
|
+
name: typeof record.name === "string" ? record.name : "Error",
|
|
88
|
+
message: String(record.message),
|
|
89
|
+
stack: typeof record.stack === "string" ? record.stack : "",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
return { name: "Error", message: String(error), stack: "" };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function classify(error: ErrorPayload): ScriptExecutorError {
|
|
96
|
+
if (error.name === "InternalError" && error.message === "interrupted") return "timeout";
|
|
97
|
+
if (error.message === "out of memory") return "oom";
|
|
98
|
+
return "eval_error";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function readTextCapped(response: Response, operation: string): Promise<string> {
|
|
102
|
+
const limit = SCRIPT_SDK_RESPONSE_LIMIT_BYTES;
|
|
103
|
+
if (!response.body) return "";
|
|
104
|
+
const reader = response.body.getReader();
|
|
105
|
+
const decoder = new TextDecoder();
|
|
106
|
+
const parts: string[] = [];
|
|
107
|
+
let received = 0;
|
|
108
|
+
while (true) {
|
|
109
|
+
const { done, value } = await reader.read();
|
|
110
|
+
if (done) break;
|
|
111
|
+
received += value.byteLength;
|
|
112
|
+
if (received > limit) {
|
|
113
|
+
await reader.cancel().catch(() => {});
|
|
114
|
+
throw new Error(
|
|
115
|
+
`${operation} response exceeded the ${limit}-byte limit of the quickjs executor`,
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
parts.push(decoder.decode(value, { stream: true }));
|
|
119
|
+
}
|
|
120
|
+
parts.push(decoder.decode());
|
|
121
|
+
return parts.join("");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
type SandboxRequest = {
|
|
125
|
+
url: string;
|
|
126
|
+
method?: string;
|
|
127
|
+
headers?: [string, string][];
|
|
128
|
+
body?: string;
|
|
129
|
+
retries?: number;
|
|
130
|
+
timeoutMs?: number;
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
function requestInit(request: SandboxRequest): RequestInit {
|
|
134
|
+
return { method: request.method, headers: request.headers, body: request.body };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function serializeResponse(response: Response, operation: string) {
|
|
138
|
+
// Keep `headers` before `body`: see the data: URL note in dispatchCall.
|
|
139
|
+
return {
|
|
140
|
+
status: response.status,
|
|
141
|
+
statusText: response.statusText,
|
|
142
|
+
ok: response.ok,
|
|
143
|
+
url: response.url,
|
|
144
|
+
headers: Array.from(response.headers.entries()),
|
|
145
|
+
body: await readTextCapped(response, operation),
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function callable(target: unknown, name: string): (...args: unknown[]) => Promise<unknown> {
|
|
150
|
+
if (typeof target !== "function") throw new TypeError(`${name} is not a function`);
|
|
151
|
+
return target as (...args: unknown[]) => Promise<unknown>;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Runs an async host call that the sandbox requested. Only these paths exist. */
|
|
155
|
+
async function dispatchCall(
|
|
156
|
+
hostCtx: RuntimeCtx,
|
|
157
|
+
path: string,
|
|
158
|
+
args: unknown[],
|
|
159
|
+
signal: AbortSignal,
|
|
160
|
+
): Promise<unknown> {
|
|
161
|
+
if (path === "fetch" || path === "stdlib.fetch" || path === "stdlib.fetchJson") {
|
|
162
|
+
const request = args[0] as SandboxRequest;
|
|
163
|
+
const response =
|
|
164
|
+
path === "fetch"
|
|
165
|
+
? await fetch(request.url, { ...requestInit(request), signal })
|
|
166
|
+
: await runtimeFetch(request.url, {
|
|
167
|
+
...requestInit(request),
|
|
168
|
+
retries: request.retries,
|
|
169
|
+
timeoutMs: request.timeoutMs,
|
|
170
|
+
signal,
|
|
171
|
+
});
|
|
172
|
+
if (path !== "stdlib.fetchJson") return serializeResponse(response, path);
|
|
173
|
+
// Read the header first. Bun 1.4 returns null headers for a data: URL
|
|
174
|
+
// response whose body was streamed before the headers were first read.
|
|
175
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
176
|
+
const text = await readTextCapped(response, path);
|
|
177
|
+
return contentType.includes("application/json") ? JSON.parse(text) : text;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// The path comes from sandbox code, so only own members are reachable:
|
|
181
|
+
// never `constructor`, `__proto__`, or other inherited properties.
|
|
182
|
+
const own = (target: unknown, name: string): unknown =>
|
|
183
|
+
target && typeof target === "object" && Object.hasOwn(target, name)
|
|
184
|
+
? (target as Record<string, unknown>)[name]
|
|
185
|
+
: undefined;
|
|
186
|
+
const [kind, first, ...rest] = path.split(".");
|
|
187
|
+
if (kind === "swarm" && first === "room" && rest.length === 1) {
|
|
188
|
+
const name = rest[0] as string;
|
|
189
|
+
return callable(own(hostCtx.swarm.room, name), `ctx.swarm.room.${name}`)(...args);
|
|
190
|
+
}
|
|
191
|
+
if (kind === "swarm" && first && rest.length === 0) {
|
|
192
|
+
// ctx.swarm is a Proxy that maps any tool name to a call (allowlisted inside).
|
|
193
|
+
const method = first in Object.prototype ? undefined : hostCtx.swarm[first];
|
|
194
|
+
return callable(method, `ctx.swarm.${first}`)(...args);
|
|
195
|
+
}
|
|
196
|
+
if ((kind === "api" || kind === "mcp") && first && rest.length > 0) {
|
|
197
|
+
const name = rest.join(".");
|
|
198
|
+
const client = own(hostCtx[kind], first);
|
|
199
|
+
return callable(own(client, name), `ctx.${kind}.${first}.${name}`)(...args);
|
|
200
|
+
}
|
|
201
|
+
throw new Error(`unknown quickjs host call: ${path}`);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function redactedFor(config: SwarmConfig, hostId: unknown): RedactedValue<unknown> {
|
|
205
|
+
if (typeof hostId !== "string") throw new Error("invalid Redacted handle");
|
|
206
|
+
const system: Record<string, RedactedValue<string> | undefined> = {
|
|
207
|
+
"system:apiKey": config.apiKey,
|
|
208
|
+
"system:agentId": config.agentId,
|
|
209
|
+
"system:mcpBaseUrl": config.mcpBaseUrl,
|
|
210
|
+
"system:runtimeInstanceId": config.runtimeInstanceId,
|
|
211
|
+
};
|
|
212
|
+
const value = hostId.startsWith("user:") ? config.get(hostId.slice(5)) : system[hostId];
|
|
213
|
+
if (!value) throw new Error("Redacted value was not in registry");
|
|
214
|
+
return value;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Runs a sync host call that the sandbox requested. */
|
|
218
|
+
function dispatchSync(config: SwarmConfig, op: string, value: unknown): unknown {
|
|
219
|
+
switch (op) {
|
|
220
|
+
case "redacted.value":
|
|
221
|
+
return Redacted.value(redactedFor(config, value));
|
|
222
|
+
case "redacted.meta":
|
|
223
|
+
return Redacted.meta(redactedFor(config, value));
|
|
224
|
+
case "config.has":
|
|
225
|
+
return typeof value === "string" && config.get(value) !== undefined;
|
|
226
|
+
case "table":
|
|
227
|
+
return table(value as Array<Record<string, unknown>>);
|
|
228
|
+
default:
|
|
229
|
+
throw new Error(`unknown quickjs sync host call: ${op}`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const FRAME_RE = /\s+at\s+(?:(\S+)\s+\()?([^\s()]+):(\d+):(\d+)\)?/g;
|
|
234
|
+
|
|
235
|
+
function structuredError(
|
|
236
|
+
error: ErrorPayload,
|
|
237
|
+
map: SourceMapReader | undefined,
|
|
238
|
+
): ScriptRuntimeError & { ctxSignatureHint?: string } {
|
|
239
|
+
const userFrames: ScriptStackFrame[] = [];
|
|
240
|
+
const lines: string[] = [];
|
|
241
|
+
for (const match of error.stack.matchAll(FRAME_RE)) {
|
|
242
|
+
const [raw, fn, file, line, column] = match;
|
|
243
|
+
if (!file || !line || !column) continue;
|
|
244
|
+
const original =
|
|
245
|
+
file === BUNDLE_FILE ? map?.originalPositionFor(Number(line), Number(column)) : undefined;
|
|
246
|
+
if (original?.source.endsWith(USER_SCRIPT_FILE)) {
|
|
247
|
+
const location = `${USER_SCRIPT_FILE}:${original.line}:${original.column}`;
|
|
248
|
+
const text = fn ? `at ${fn} (${location})` : `at ${location}`;
|
|
249
|
+
userFrames.push({
|
|
250
|
+
file: USER_SCRIPT_FILE,
|
|
251
|
+
line: original.line,
|
|
252
|
+
column: original.column,
|
|
253
|
+
raw: text,
|
|
254
|
+
});
|
|
255
|
+
lines.push(` ${text}`);
|
|
256
|
+
} else {
|
|
257
|
+
lines.push(` ${raw.trim()}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
const hint = ctxSignatureHintFor(error.message);
|
|
261
|
+
return {
|
|
262
|
+
name: error.name,
|
|
263
|
+
message: error.message,
|
|
264
|
+
stack: [`${error.name}: ${error.message}`, ...lines].join("\n"),
|
|
265
|
+
userFrames,
|
|
266
|
+
userScriptLine: userFrames[0]?.line,
|
|
267
|
+
userScriptColumn: userFrames[0]?.column,
|
|
268
|
+
...(hint ? { ctxSignatureHint: hint } : {}),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function errorStderr(runtimeError: ScriptRuntimeError): string {
|
|
273
|
+
if (runtimeError.userFrames.length === 0) return `${runtimeError.stack}\n`;
|
|
274
|
+
const frames = runtimeError.userFrames.map((frame) => ` ${frame.raw}`).join("\n");
|
|
275
|
+
return `${runtimeError.name}: ${runtimeError.message}\n${frames}\n`;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Owns the host side of one run: an abort signal on every host fetch
|
|
280
|
+
* (including the ones inside ctx.swarm, ctx.api and ctx.mcp), a per-run
|
|
281
|
+
* concurrency budget, and teardown that aborts and settles every host call
|
|
282
|
+
* before the worker can take another job.
|
|
283
|
+
*/
|
|
284
|
+
class HostCallScope {
|
|
285
|
+
private readonly controller = new AbortController();
|
|
286
|
+
private readonly ops = new Set<Promise<unknown>>();
|
|
287
|
+
private readonly waiters: Array<() => void> = [];
|
|
288
|
+
private readonly outerFetch = globalThis.fetch;
|
|
289
|
+
private active = 0;
|
|
290
|
+
|
|
291
|
+
constructor() {
|
|
292
|
+
const outer = this.outerFetch;
|
|
293
|
+
const runSignal = this.controller.signal;
|
|
294
|
+
// The worker runs one job at a time, so scoping the global fetch is safe.
|
|
295
|
+
globalThis.fetch = Object.assign(
|
|
296
|
+
(input: string | URL | Request, init?: RequestInit) =>
|
|
297
|
+
outer(input, {
|
|
298
|
+
...init,
|
|
299
|
+
signal: init?.signal ? AbortSignal.any([init.signal, runSignal]) : runSignal,
|
|
300
|
+
}),
|
|
301
|
+
{ preconnect: outer.preconnect },
|
|
302
|
+
) as typeof fetch;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
get signal(): AbortSignal {
|
|
306
|
+
return this.controller.signal;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** Track a host call. `limited` calls share the per-run concurrency budget. */
|
|
310
|
+
call<T>(run: () => Promise<T>, limited: boolean): Promise<T> {
|
|
311
|
+
if (this.ops.size >= MAX_PENDING_HOST_CALLS) {
|
|
312
|
+
return Promise.reject(
|
|
313
|
+
new Error(
|
|
314
|
+
`Too many pending host calls in one script run (limit ${MAX_PENDING_HOST_CALLS})`,
|
|
315
|
+
),
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
const op = limited ? this.limited(run) : run();
|
|
319
|
+
this.ops.add(op);
|
|
320
|
+
void op.then(
|
|
321
|
+
() => this.ops.delete(op),
|
|
322
|
+
() => this.ops.delete(op),
|
|
323
|
+
);
|
|
324
|
+
return op;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
sleep(ms: number): Promise<null> {
|
|
328
|
+
return this.call(
|
|
329
|
+
() =>
|
|
330
|
+
new Promise<null>((resolve) => {
|
|
331
|
+
const done = () => {
|
|
332
|
+
clearTimeout(timer);
|
|
333
|
+
resolve(null);
|
|
334
|
+
};
|
|
335
|
+
const timer = setTimeout(done, ms);
|
|
336
|
+
this.signal.addEventListener("abort", done, { once: true });
|
|
337
|
+
}),
|
|
338
|
+
false,
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
private async limited<T>(run: () => Promise<T>): Promise<T> {
|
|
343
|
+
while (this.active >= MAX_CONCURRENT_HOST_CALLS) {
|
|
344
|
+
await new Promise<void>((resolve) => this.waiters.push(resolve));
|
|
345
|
+
}
|
|
346
|
+
this.active++;
|
|
347
|
+
try {
|
|
348
|
+
this.signal.throwIfAborted();
|
|
349
|
+
return await run();
|
|
350
|
+
} finally {
|
|
351
|
+
this.active--;
|
|
352
|
+
this.waiters.shift()?.();
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/** Abort every host call, wait for them to settle, and restore fetch. */
|
|
357
|
+
async close(): Promise<number> {
|
|
358
|
+
this.controller.abort(new Error("The script run ended"));
|
|
359
|
+
for (const wake of this.waiters.splice(0)) wake();
|
|
360
|
+
await Promise.race([Promise.allSettled([...this.ops]), Bun.sleep(HOST_CALL_SETTLE_MS)]);
|
|
361
|
+
globalThis.fetch = this.outerFetch;
|
|
362
|
+
return this.ops.size;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Evaluates one script in a fresh QuickJS runtime. The caller owns the
|
|
368
|
+
* process-level setup (one job at a time, egress fetch patch).
|
|
369
|
+
*/
|
|
370
|
+
export async function runQuickJSJob(
|
|
371
|
+
QuickJS: QuickJSWASMModule,
|
|
372
|
+
job: QuickJSJob,
|
|
373
|
+
): Promise<QuickJSJobResult> {
|
|
374
|
+
const host = new HostCallScope();
|
|
375
|
+
let output: ExecutorOutput;
|
|
376
|
+
try {
|
|
377
|
+
output = await evaluateJob(QuickJS, job, host);
|
|
378
|
+
} catch (error) {
|
|
379
|
+
await host.close();
|
|
380
|
+
throw error;
|
|
381
|
+
}
|
|
382
|
+
return { output, leakedHostCalls: await host.close() };
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
async function evaluateJob(
|
|
386
|
+
QuickJS: QuickJSWASMModule,
|
|
387
|
+
job: QuickJSJob,
|
|
388
|
+
host: HostCallScope,
|
|
389
|
+
): Promise<ExecutorOutput> {
|
|
390
|
+
const start = performance.now();
|
|
391
|
+
const stdout = new CappedLog(job.resources.maxStdoutBytes);
|
|
392
|
+
const stderr = new CappedLog(job.resources.maxStdoutBytes);
|
|
393
|
+
const output = (
|
|
394
|
+
fields: Omit<ExecutorOutput, "stdout" | "stderr" | "truncated" | "durationMs">,
|
|
395
|
+
) => ({
|
|
396
|
+
...fields,
|
|
397
|
+
stdout: stdout.text,
|
|
398
|
+
stderr: stderr.text,
|
|
399
|
+
truncated: { stdout: stdout.truncated, stderr: stderr.truncated },
|
|
400
|
+
durationMs: Math.round(performance.now() - start),
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
const bundle = await bundleForQuickJS(job.source);
|
|
404
|
+
if (!bundle.ok) {
|
|
405
|
+
stderr.append(`${bundle.diagnostic}\n`);
|
|
406
|
+
return output({
|
|
407
|
+
result: undefined,
|
|
408
|
+
exitCode: 1,
|
|
409
|
+
error: "eval_error",
|
|
410
|
+
runtimeError: {
|
|
411
|
+
name: "BuildError",
|
|
412
|
+
message: bundle.diagnostic,
|
|
413
|
+
stack: bundle.diagnostic,
|
|
414
|
+
userFrames: [],
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
const config = new SwarmConfig(job.configPayload);
|
|
420
|
+
const hostCtx = buildCtx({
|
|
421
|
+
swarmConfig: config,
|
|
422
|
+
apiConnections: job.configPayload.apiConnections,
|
|
423
|
+
mcpConnections: job.configPayload.mcpConnections,
|
|
424
|
+
});
|
|
425
|
+
const shape = {
|
|
426
|
+
hasRuntimeInstanceId: config.runtimeInstanceId !== undefined,
|
|
427
|
+
api: Object.fromEntries(Object.entries(hostCtx.api).map(([slug, c]) => [slug, Object.keys(c)])),
|
|
428
|
+
mcp: Object.fromEntries(Object.entries(hostCtx.mcp).map(([slug, c]) => [slug, Object.keys(c)])),
|
|
429
|
+
};
|
|
430
|
+
// Accept both shapes, like eval-harness: callers may pass already-serialized JSON.
|
|
431
|
+
const args = typeof job.args === "string" ? JSON.parse(job.args) : (job.args ?? null);
|
|
432
|
+
|
|
433
|
+
const runtime = QuickJS.newRuntime();
|
|
434
|
+
runtime.setMemoryLimit(job.resources.memoryMb * 1024 * 1024);
|
|
435
|
+
runtime.setMaxStackSize(MAX_STACK_BYTES);
|
|
436
|
+
const deadline = Date.now() + job.resources.wallClockMs;
|
|
437
|
+
runtime.setInterruptHandler(() => Date.now() > deadline);
|
|
438
|
+
const ctx: QuickJSContext = runtime.newContext();
|
|
439
|
+
|
|
440
|
+
let alive = true;
|
|
441
|
+
let finish: (outcome: Outcome) => void = () => {};
|
|
442
|
+
const done = new Promise<Outcome>((resolve) => {
|
|
443
|
+
finish = resolve;
|
|
444
|
+
});
|
|
445
|
+
const fail = (error: unknown) => finish({ ok: false, error: errorPayload(error) });
|
|
446
|
+
|
|
447
|
+
const drain = () => {
|
|
448
|
+
if (!alive) return;
|
|
449
|
+
const jobs = runtime.executePendingJobs();
|
|
450
|
+
if (jobs.error) {
|
|
451
|
+
const dumped = ctx.dump(jobs.error);
|
|
452
|
+
jobs.error.dispose();
|
|
453
|
+
fail(dumped);
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
|
|
457
|
+
const pending = new Set<QuickJSDeferredPromise>();
|
|
458
|
+
const bridge = (run: () => Promise<unknown>): QuickJSHandle => {
|
|
459
|
+
const deferred = ctx.newPromise();
|
|
460
|
+
pending.add(deferred);
|
|
461
|
+
void run()
|
|
462
|
+
.then(
|
|
463
|
+
(value) => ({ value: value === undefined ? null : value }),
|
|
464
|
+
(error) => ({ error: errorPayload(error) }),
|
|
465
|
+
)
|
|
466
|
+
.then((envelope) => {
|
|
467
|
+
if (!alive) return;
|
|
468
|
+
let json: string;
|
|
469
|
+
try {
|
|
470
|
+
json = JSON.stringify(envelope);
|
|
471
|
+
} catch (error) {
|
|
472
|
+
json = JSON.stringify({ error: errorPayload(error) });
|
|
473
|
+
}
|
|
474
|
+
const handle = ctx.newString(json);
|
|
475
|
+
deferred.resolve(handle);
|
|
476
|
+
handle.dispose();
|
|
477
|
+
pending.delete(deferred);
|
|
478
|
+
deferred.dispose();
|
|
479
|
+
drain();
|
|
480
|
+
})
|
|
481
|
+
// A failure while handing the result back to QuickJS ends the run.
|
|
482
|
+
.catch(fail);
|
|
483
|
+
return deferred.handle;
|
|
484
|
+
};
|
|
485
|
+
|
|
486
|
+
const define = (name: string, fn: (...handles: QuickJSHandle[]) => QuickJSHandle | undefined) => {
|
|
487
|
+
const handle = ctx.newFunction(name, fn);
|
|
488
|
+
ctx.setProp(ctx.global, name, handle);
|
|
489
|
+
handle.dispose();
|
|
490
|
+
};
|
|
491
|
+
define("__host_call", (pathHandle, argsHandle) => {
|
|
492
|
+
const path = ctx.getString(pathHandle);
|
|
493
|
+
const callArgs = JSON.parse(ctx.getString(argsHandle)) as unknown[];
|
|
494
|
+
return bridge(() => host.call(() => dispatchCall(hostCtx, path, callArgs, host.signal), true));
|
|
495
|
+
});
|
|
496
|
+
define("__host_sync", (opHandle, valueHandle) => {
|
|
497
|
+
let envelope: unknown;
|
|
498
|
+
try {
|
|
499
|
+
const value = dispatchSync(
|
|
500
|
+
config,
|
|
501
|
+
ctx.getString(opHandle),
|
|
502
|
+
JSON.parse(ctx.getString(valueHandle)),
|
|
503
|
+
);
|
|
504
|
+
envelope = { value: value === undefined ? null : value };
|
|
505
|
+
} catch (error) {
|
|
506
|
+
envelope = { error: errorPayload(error) };
|
|
507
|
+
}
|
|
508
|
+
return ctx.newString(JSON.stringify(envelope));
|
|
509
|
+
});
|
|
510
|
+
define("__host_log", (streamHandle, textHandle) => {
|
|
511
|
+
const text = ctx.getString(textHandle);
|
|
512
|
+
(ctx.getString(streamHandle) === "stderr" ? stderr : stdout).append(text);
|
|
513
|
+
return undefined;
|
|
514
|
+
});
|
|
515
|
+
define("__host_sleep", (msHandle) => {
|
|
516
|
+
const ms = Math.max(0, ctx.getNumber(msHandle));
|
|
517
|
+
return bridge(() => host.sleep(ms));
|
|
518
|
+
});
|
|
519
|
+
define("__host_done", (jsonHandle) => {
|
|
520
|
+
finish(JSON.parse(ctx.getString(jsonHandle)) as Outcome);
|
|
521
|
+
return undefined;
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
const evaluate = (code: string, filename: string): boolean => {
|
|
525
|
+
const result = ctx.evalCode(code, filename);
|
|
526
|
+
if (result.error) {
|
|
527
|
+
const dumped = ctx.dump(result.error);
|
|
528
|
+
result.error.dispose();
|
|
529
|
+
fail(dumped);
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
532
|
+
result.value.dispose();
|
|
533
|
+
return true;
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
537
|
+
try {
|
|
538
|
+
const init = `globalThis.__swarm_shape = ${JSON.stringify(shape)};\nglobalThis.__swarm_args = ${JSON.stringify(args)};`;
|
|
539
|
+
const ok =
|
|
540
|
+
evaluate(init, "swarm-init.js") &&
|
|
541
|
+
evaluate(SANDBOX_PRELUDE, "swarm-prelude.js") &&
|
|
542
|
+
evaluate(bundle.code, BUNDLE_FILE) &&
|
|
543
|
+
evaluate(SANDBOX_EPILOGUE, "swarm-epilogue.js");
|
|
544
|
+
if (ok) drain();
|
|
545
|
+
|
|
546
|
+
// The interrupt handler bounds synchronous code. This timer bounds the
|
|
547
|
+
// whole run, including awaited host calls.
|
|
548
|
+
const timeout = new Promise<Outcome>((resolve) => {
|
|
549
|
+
timer = setTimeout(
|
|
550
|
+
() =>
|
|
551
|
+
resolve({
|
|
552
|
+
ok: false,
|
|
553
|
+
error: { name: "InternalError", message: "interrupted", stack: "" },
|
|
554
|
+
}),
|
|
555
|
+
Math.max(0, deadline - Date.now()),
|
|
556
|
+
);
|
|
557
|
+
});
|
|
558
|
+
const outcome = await Promise.race([done, timeout]);
|
|
559
|
+
|
|
560
|
+
if (outcome.ok) return output({ result: outcome.result, exitCode: 0 });
|
|
561
|
+
const error = classify(outcome.error);
|
|
562
|
+
if (error === "timeout") {
|
|
563
|
+
stderr.append(`Script exceeded its ${job.resources.wallClockMs} ms wall-clock limit\n`);
|
|
564
|
+
return output({ result: undefined, exitCode: TIMEOUT_EXIT_CODE, error });
|
|
565
|
+
}
|
|
566
|
+
const runtimeError = structuredError(outcome.error, bundle.map);
|
|
567
|
+
stderr.append(errorStderr(runtimeError));
|
|
568
|
+
return output({ result: undefined, exitCode: 1, error, runtimeError });
|
|
569
|
+
} finally {
|
|
570
|
+
alive = false;
|
|
571
|
+
clearTimeout(timer);
|
|
572
|
+
for (const deferred of pending) deferred.dispose();
|
|
573
|
+
try {
|
|
574
|
+
ctx.dispose();
|
|
575
|
+
runtime.dispose();
|
|
576
|
+
} catch (error) {
|
|
577
|
+
console.warn("[scripts-runtime] quickjs teardown failed:", errorPayload(error).message);
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Worker thread entry for the quickjs executor. Each worker loads the QuickJS
|
|
3
|
+
* WASM module once and then runs one job at a time, each in a fresh QuickJS
|
|
4
|
+
* runtime. The worker keeps the API event loop free while a script runs
|
|
5
|
+
* synchronous code, and its own `globalThis.fetch` lets the egress patch stay
|
|
6
|
+
* per job instead of per API process.
|
|
7
|
+
*
|
|
8
|
+
* The compiled API binary must list this file as an extra entrypoint
|
|
9
|
+
* (see Dockerfile).
|
|
10
|
+
*/
|
|
11
|
+
import variant from "@jitl/quickjs-singlefile-mjs-release-sync";
|
|
12
|
+
import { newQuickJSWASMModuleFromVariant } from "quickjs-emscripten-core";
|
|
13
|
+
import { patchFetchWithEgressSubstitution } from "../egress-secrets";
|
|
14
|
+
import { type QuickJSJob, type QuickJSJobResult, runQuickJSJob } from "./quickjs-runner";
|
|
15
|
+
|
|
16
|
+
declare const self: Worker;
|
|
17
|
+
|
|
18
|
+
export type QuickJSWorkerRequest = { id: number; job: QuickJSJob };
|
|
19
|
+
export type QuickJSWorkerMessage =
|
|
20
|
+
| { type: "ready" }
|
|
21
|
+
| {
|
|
22
|
+
type: "result";
|
|
23
|
+
id: number;
|
|
24
|
+
output: QuickJSJobResult["output"];
|
|
25
|
+
/** Size of the worker's WASM linear memory after the job, when known. */
|
|
26
|
+
wasmHeapBytes?: number;
|
|
27
|
+
/** Host calls that did not settle after the run was aborted. */
|
|
28
|
+
leakedHostCalls: number;
|
|
29
|
+
}
|
|
30
|
+
| { type: "fatal"; id?: number; message: string };
|
|
31
|
+
|
|
32
|
+
const pristineFetch = globalThis.fetch;
|
|
33
|
+
const modulePromise = newQuickJSWASMModuleFromVariant(variant);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* WASM linear memory grows to the peak of any job and never shrinks, even
|
|
37
|
+
* after the QuickJS runtime is disposed. The pool reads this to recycle a
|
|
38
|
+
* worker whose memory grew. `module` is not public API, so this is best effort.
|
|
39
|
+
*/
|
|
40
|
+
function wasmHeapBytes(QuickJS: unknown): number | undefined {
|
|
41
|
+
const heap = (QuickJS as { module?: { HEAPU8?: Uint8Array } }).module?.HEAPU8;
|
|
42
|
+
return heap instanceof Uint8Array ? heap.byteLength : undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
self.onmessage = async (event: MessageEvent<QuickJSWorkerRequest>) => {
|
|
46
|
+
const { id, job } = event.data;
|
|
47
|
+
try {
|
|
48
|
+
const QuickJS = await modulePromise;
|
|
49
|
+
// Re-install from the pristine fetch so egress patches never stack across jobs.
|
|
50
|
+
globalThis.fetch = pristineFetch;
|
|
51
|
+
patchFetchWithEgressSubstitution(
|
|
52
|
+
job.configPayload.egressSecrets ?? [],
|
|
53
|
+
job.configPayload.failedBindings ?? [],
|
|
54
|
+
);
|
|
55
|
+
const { output, leakedHostCalls } = await runQuickJSJob(QuickJS, job);
|
|
56
|
+
postMessage({
|
|
57
|
+
type: "result",
|
|
58
|
+
id,
|
|
59
|
+
output,
|
|
60
|
+
leakedHostCalls,
|
|
61
|
+
wasmHeapBytes: wasmHeapBytes(QuickJS),
|
|
62
|
+
} satisfies QuickJSWorkerMessage);
|
|
63
|
+
} catch (error) {
|
|
64
|
+
postMessage({
|
|
65
|
+
type: "fatal",
|
|
66
|
+
id,
|
|
67
|
+
message: error instanceof Error ? error.message : String(error),
|
|
68
|
+
} satisfies QuickJSWorkerMessage);
|
|
69
|
+
} finally {
|
|
70
|
+
globalThis.fetch = pristineFetch;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
modulePromise.then(
|
|
75
|
+
() => postMessage({ type: "ready" } satisfies QuickJSWorkerMessage),
|
|
76
|
+
(error) =>
|
|
77
|
+
postMessage({
|
|
78
|
+
type: "fatal",
|
|
79
|
+
message: `failed to load QuickJS: ${error instanceof Error ? error.message : String(error)}`,
|
|
80
|
+
} satisfies QuickJSWorkerMessage),
|
|
81
|
+
);
|