@desplega.ai/agent-swarm 2.0.0 → 2.1.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-sj2g0m84.js → acp-adapter-kr4abrsk.js} +10 -10
- package/dist/{actions-fegk44gr.js → actions-eh4g6w2r.js} +27 -27
- package/dist/{amp-adapter-ad8ft89x.js → amp-adapter-7h7wdt8q.js} +5 -5
- package/dist/{app-kfvgbvzy.js → app-7k8s9w5p.js} +9 -9
- package/dist/{assistant-z7zdy9ja.js → assistant-wswt2y7p.js} +28 -28
- package/dist/{boot-reembed-j8xa181e.js → boot-reembed-9apm9qw6.js} +7 -7
- package/dist/{boot-reembed-5c2z0mbe.js → boot-reembed-whzbdthj.js} +7 -7
- package/dist/{boot-scrub-sweep-hnkn3rgt.js → boot-scrub-sweep-ndzmvmgt.js} +8 -8
- package/dist/{boot-seeds-kwjq4d2z.js → boot-seeds-e9sta0yt.js} +13 -11
- package/dist/{claude-adapter-9vmvpvgd.js → claude-adapter-4mqd3r6r.js} +11 -11
- package/dist/{claude-managed-adapter-k9qae05j.js → claude-managed-adapter-9wswb52m.js} +3 -3
- package/dist/{claude-managed-setup-q54zx5q2.js → claude-managed-setup-tpe1effx.js} +4 -4
- package/dist/{claude-sdk-session-9jye6cxr.js → claude-sdk-session-q49ghmh9.js} +10340 -10197
- package/dist/{cli-ke896fcf.js → cli-01xvj1rq.js} +10 -10
- package/dist/{cli-tr4sxp68.js → cli-035vekcw.js} +2 -2
- package/dist/{cli-z5jjpaxw.js → cli-0pd8hzhc.js} +3 -3
- package/dist/{cli-n5vwg842.js → cli-0veb92a3.js} +6 -1
- package/dist/{cli-wayw93nt.js → cli-10jesr25.js} +3 -3
- package/dist/{cli-se8w6wny.js → cli-1y7yywmj.js} +1 -1
- package/dist/{cli-v1yb0rqb.js → cli-20krdhz5.js} +47 -19
- package/dist/{cli-d5f9ed5c.js → cli-29rja109.js} +39 -20
- package/dist/{cli-apx87jne.js → cli-2pwa0q4d.js} +15 -4
- package/dist/{cli-ee9gtz70.js → cli-4mengw46.js} +348 -25
- package/dist/{cli-03gedhng.js → cli-5twdke5x.js} +6 -6
- package/dist/{cli-6razj6t4.js → cli-711vwbxn.js} +2 -2
- package/dist/{cli-s2sp957d.js → cli-78vv7446.js} +1 -1
- package/dist/{cli-3fbmmawm.js → cli-7n6s5man.js} +4 -4
- package/dist/{cli-hnhtk6cj.js → cli-8wwxw8a3.js} +1 -1
- package/dist/{cli-hnmhtzvh.js → cli-9jzdj90z.js} +2 -0
- package/dist/{cli-skxvvpap.js → cli-cvv9k0hh.js} +28 -25
- package/dist/{cli-74ahz61x.js → cli-d9286m9h.js} +2 -2
- package/dist/{cli-4jxm2qtz.js → cli-gxzkg1de.js} +28 -16
- package/dist/{cli-zkpks3r3.js → cli-h1arqn31.js} +1 -1
- package/dist/{cli-r62a35ea.js → cli-hqr88ew6.js} +1 -1
- package/dist/{cli-ra0xzggj.js → cli-j67mh2vg.js} +2 -2
- package/dist/{cli-29jc5q7w.js → cli-ksphgh48.js} +2 -2
- package/dist/{cli-eejsgvm4.js → cli-mgz4nbgg.js} +4 -4
- package/dist/{cli-vatyhshy.js → cli-mz5as5ha.js} +4 -4
- package/dist/{cli-f7kr7svz.js → cli-n8ndw8j7.js} +2 -2
- package/dist/{cli-1e4b6bzh.js → cli-naqzr17j.js} +7 -7
- package/dist/{cli-q0ke1yww.js → cli-nm1p8jfr.js} +6 -6
- package/dist/{cli-m50t4309.js → cli-p3zbc1rw.js} +5 -5
- package/dist/{cli-cgtksawx.js → cli-pe05f8yk.js} +23 -3
- package/dist/{cli-vdach5a9.js → cli-qmzsbkvy.js} +4 -4
- package/dist/{cli-n3hzg89b.js → cli-qnm4e9e4.js} +57 -579
- package/dist/{cli-s4setq91.js → cli-qqy4q48b.js} +3 -3
- package/dist/{cli-k382tshb.js → cli-r35rw5de.js} +8 -7
- package/dist/{cli-k3dp5tpf.js → cli-r610p9bs.js} +2 -2
- package/dist/{cli-e6mpjhed.js → cli-rc6pf5kv.js} +1 -1
- package/dist/{cli-3dj8k4vy.js → cli-rdxm77k3.js} +1 -1
- package/dist/{cli-tqaazemc.js → cli-s69nfyeh.js} +1 -1
- package/dist/{cli-6jyj59nb.js → cli-sawv63e8.js} +2 -2
- package/dist/{cli-q50942rs.js → cli-sg19gbf0.js} +1 -1
- package/dist/{cli-p1x85d2t.js → cli-shk7r5cs.js} +12 -9
- package/dist/{cli-przpmdgr.js → cli-sksetb2q.js} +11 -1
- package/dist/{cli-d2qr7aj4.js → cli-t02z59f2.js} +1 -1
- package/dist/{cli-j0pfptdp.js → cli-v7g551te.js} +39 -43
- package/dist/{cli-bzszjf2p.js → cli-vgqzp4em.js} +519 -126
- package/dist/cli-wfnemr1t.js +86 -0
- package/dist/{cli-ne236afy.js → cli-wms6x9db.js} +8 -8
- package/dist/{cli-5hkm7whv.js → cli-x1r545x8.js} +1 -1
- package/dist/{cli-kt6zqg0e.js → cli-xczsgshn.js} +2 -2
- package/dist/{cli-ef1hg1wv.js → cli-y3167wt1.js} +430 -238
- package/dist/{cli-tsde1hdr.js → cli-yjmqpxt3.js} +12 -11
- package/dist/{cli-9nfx44eh.js → cli-z780ec7c.js} +1 -1
- package/dist/{cli-r2pnvj8g.js → cli-zm9t2vde.js} +2 -2
- package/dist/cli.js +24 -23
- package/dist/{codex-adapter-y81sssk0.js → codex-adapter-4z16v9n0.js} +8 -8
- package/dist/{codex-login-f2rcfpp9.js → codex-login-pkepy2j1.js} +2 -2
- package/dist/{codex-session-runner-8vx4j6a9.js → codex-session-runner-rvms9y2x.js} +8 -8
- package/dist/{commands-kcpvpcrr.js → commands-2a9rtyat.js} +8 -8
- package/dist/{controller-ywrmt6yd.js → controller-3k3madbn.js} +4 -4
- package/dist/{controllers-connect-kwpvj3rf.js → controllers-connect-v9yn8zb9.js} +30 -30
- package/dist/{cursor-adapter-cehv1qns.js → cursor-adapter-98gbhyzn.js} +4 -4
- package/dist/{db-ww5d7jq3.js → db-eywwsnjw.js} +9 -7
- package/dist/{db-k8ankj20.js → db-ncypk9np.js} +9 -9
- package/dist/{devin-adapter-x1022qb1.js → devin-adapter-zeq8b9gz.js} +2 -2
- package/dist/{dispatcher-e8gq5m68.js → dispatcher-w94kc575.js} +26 -26
- package/dist/{docker-ff6y1190.js → docker-b1e4cqab.js} +2 -2
- package/dist/{dsh-adapter-3epjm3af.js → dsh-adapter-47ztej36.js} +4 -4
- package/dist/{e2b-8ygwhg0f.js → e2b-bbt8ey45.js} +3 -3
- package/dist/{e2b-swhnpw3n.js → e2b-dp804sen.js} +3 -3
- package/dist/{event-bus-6axnngdj.js → event-bus-j2jdg3qb.js} +2 -2
- package/dist/{external-channel-h3359cwn.js → external-channel-zrkg8ar9.js} +8 -8
- package/dist/{grok-adapter-0d4x7gkx.js → grok-adapter-s9ncp9vy.js} +11 -11
- package/dist/{handlers-an1gzv21.js → handlers-kqftvjgw.js} +29 -29
- package/dist/{heartbeat-6dakeb61.js → heartbeat-9q96p8v9.js} +31 -31
- package/dist/{hook-6v6cv6d5.js → hook-56qe10dp.js} +7 -7
- package/dist/{hook-sa5xye4y.js → hook-yy6rw42k.js} +8 -8
- package/dist/{http-r7f3eky0.js → http-r2dnb8f8.js} +963 -75
- package/dist/{index-7ep4gzdk.js → index-3gtmkhc6.js} +26 -26
- package/dist/{index-v1v6n7hg.js → index-3k5hkz7r.js} +31 -31
- package/dist/{index-rmpw9a8q.js → index-5e101240.js} +26 -26
- package/dist/{index-s2wxgzr0.js → index-5wyfjc2c.js} +392 -15
- package/dist/{index-zk96aq1k.js → index-vxvhxxbn.js} +7 -7
- package/dist/{keepalive-be9vwy5h.js → keepalive-psj6j54c.js} +9 -9
- package/dist/{lead-0861cwrf.js → lead-hajz4688.js} +43 -43
- package/dist/{maintenance-mzzgc8bn.js → maintenance-ps02sb5m.js} +7 -7
- package/dist/{node-mn1zs7an.js → node-mwb5r697.js} +1 -1
- package/dist/{oauth-refresh-sweep-cpdnfwdj.js → oauth-refresh-sweep-0284h93b.js} +8 -8
- package/dist/{onboard-fh8qs4r3.js → onboard-rbf6bg01.js} +2 -2
- package/dist/{opencode-adapter-nm42wwj8.js → opencode-adapter-13dcatgq.js} +8 -8
- package/dist/{otel-impl-x6c1c9g4.js → otel-impl-zvqcbhp7.js} +2 -2
- package/dist/{pi-mono-adapter-9n2ya240.js → pi-mono-adapter-xxy47q4v.js} +7 -7
- package/dist/{pricing-refresh-qb0rv0ap.js → pricing-refresh-kefx9v3t.js} +9 -9
- package/dist/{rbac-roles-1gzdwbh1.js → rbac-roles-3m36rxm2.js} +8 -8
- package/dist/{rbac-roles-3mraey52.js → rbac-roles-z76xv0hq.js} +9 -9
- package/dist/{render-v2-agpa45zz.js → render-v2-wz73bs25.js} +9 -9
- package/dist/{resume-x0ydy9s9.js → resume-k4wzge8w.js} +14 -14
- package/dist/{setup-as9ga0d6.js → setup-2fp2pypp.js} +2 -2
- package/dist/{work-objects-y5g1y7ev.js → work-objects-6k0ywz3m.js} +29 -29
- package/dist/{worker-860c3e38.js → worker-5v3rxsdh.js} +43 -43
- package/dist/{x-m477fr0h.js → x-4mtdeym1.js} +2 -2
- package/openapi.json +777 -6
- package/package.json +8 -7
- package/src/agentmail/handlers.ts +21 -11
- package/src/apps/definition.ts +16 -0
- package/src/azure-devops/api.ts +52 -2
- package/src/azure-devops/boards-sync.ts +50 -20
- package/src/azure-devops/index.ts +1 -0
- package/src/azure-devops/outbound.ts +95 -0
- package/src/be/boot-seeds.ts +11 -0
- package/src/be/db/model-profiles.ts +315 -0
- package/src/be/db/tasks/read.ts +7 -0
- package/src/be/db.ts +53 -47
- package/src/be/migrations/219_relabel_pi_context_compactions.sql +22 -0
- package/src/be/migrations/220_model_profiles.sql +137 -0
- package/src/be/model-tier-resolution.ts +145 -11
- package/src/be/model-validation.ts +8 -1
- package/src/be/scripts/typecheck.ts +12 -0
- package/src/be/secret-registry.ts +6 -2
- package/src/be/seed-model-profiles.ts +41 -0
- package/src/be/seed-scripts/catalog/heartbeat-jev-gate.ts +368 -0
- package/src/be/seed-scripts/index.ts +9 -0
- package/src/be/swarm-config-guard.ts +1 -0
- package/src/commands/runner.ts +3 -17
- package/src/extensions/catalog.generated.json +17 -0
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/loader.ts +10 -5
- package/src/heartbeat/heartbeat.ts +2 -1
- package/src/http/agents.ts +49 -2
- package/src/http/all-routes.ts +1 -0
- package/src/http/apps.ts +30 -3
- package/src/http/config.ts +26 -2
- package/src/http/core.ts +4 -0
- package/src/http/index.ts +11 -1
- package/src/http/model-profiles.ts +303 -0
- package/src/http/pages-public.ts +11 -5
- package/src/http/poll.ts +16 -5
- package/src/http/script-runs.ts +1 -1
- package/src/http/scripts.ts +25 -2
- package/src/http/tasks.ts +18 -0
- package/src/http/webhooks.ts +1 -2
- package/src/pages/feedback-overlay.ts +65 -6
- package/src/providers/acp-targets.ts +1 -1
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/providers/pi-mono-extension.ts +17 -3
- package/src/providers/reasoning-effort.ts +10 -0
- package/src/rbac/legacy-policy.ts +8 -0
- package/src/rbac/permissions.ts +5 -0
- package/src/scheduler/deferred-task-waits.ts +18 -5
- package/src/scheduler/scheduler.ts +7 -1
- package/src/scripts-runtime/executors/quickjs-prelude.ts +75 -6
- package/src/scripts-runtime/executors/quickjs-runner.ts +85 -8
- package/src/scripts-runtime/executors/quickjs-web-shims.ts +1041 -0
- package/src/scripts-runtime/extract-args-schema.ts +9 -8
- package/src/scripts-runtime/quickjs-compat.ts +464 -0
- package/src/scripts-runtime/sdk-allowlist.ts +4 -0
- package/src/scripts-runtime/types/stdlib.d.ts +12 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +3 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +12 -0
- package/src/server.ts +10 -1
- package/src/slack/blocks.ts +4 -2
- package/src/slack/reaction-shortcode.ts +14 -0
- package/src/slack/render-v2.ts +12 -7
- package/src/slack/responses.ts +5 -4
- package/src/slack/thread-buffer.ts +41 -22
- package/src/tasks/worker-follow-up.ts +11 -2
- package/src/tests/agentmail-filters.test.ts +47 -0
- package/src/tests/apps-spike2.test.ts +35 -0
- package/src/tests/azure-devops-outbound.test.ts +420 -0
- package/src/tests/db-task-read-contract.test.ts +2 -0
- package/src/tests/db-task-write-contract.test.ts +1 -0
- package/src/tests/deferred-task-wake.test.ts +78 -1
- package/src/tests/docs-index-footprint.test.js +2 -2
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/env-flag.test.ts +1 -0
- package/src/tests/extensions-catalog.test.ts +1 -0
- package/src/tests/extensions-example-comms-adherence-gate.test.ts +413 -0
- package/src/tests/extensions-loader.test.ts +15 -0
- package/src/tests/heartbeat-jev-gate-credentials.test.ts +168 -0
- package/src/tests/heartbeat-jev-gate.test.ts +385 -0
- package/src/tests/model-profile-backcompat.test.ts +327 -0
- package/src/tests/model-profile-providers.test.ts +302 -0
- package/src/tests/model-profile-runtime-secrets.test.ts +223 -0
- package/src/tests/model-profiles-api.test.ts +614 -0
- package/src/tests/pages-feedback-keys.test.ts +121 -0
- package/src/tests/pages-feedback.test.ts +3 -0
- package/src/tests/pi-context-compaction.test.ts +193 -0
- package/src/tests/profile-claim.test.ts +269 -0
- package/src/tests/quickjs-compat.test.ts +213 -0
- package/src/tests/rbac-engine.test.ts +21 -0
- package/src/tests/script-connections.test.ts +4 -0
- package/src/tests/script-executor-conformance.test.ts +252 -1
- package/src/tests/script-executor-quickjs.test.ts +96 -0
- package/src/tests/script-runs-http.test.ts +89 -0
- package/src/tests/scripts-http.test.ts +16 -0
- package/src/tests/secret-scrubber.test.ts +10 -0
- package/src/tests/seed-model-profiles.test.ts +174 -0
- package/src/tests/seed-scripts.test.ts +2 -2
- package/src/tests/skill-invocations.test.ts +4 -2
- package/src/tests/slack-blocks.test.ts +36 -0
- package/src/tests/slack-inline-output.test.ts +14 -0
- package/src/tests/slack-reaction-config.test.ts +20 -0
- package/src/tests/slack-render-v2.test.ts +37 -0
- package/src/tests/slack-thread-buffer.test.ts +98 -1
- package/src/tests/workflow-llm.test.ts +61 -0
- package/src/tools/model-profiles/list-model-profiles.ts +54 -0
- package/src/tools/request-human-input.ts +11 -3
- package/src/tools/script-apis.ts +5 -1
- package/src/tools/script-upsert.ts +4 -1
- package/src/tools/send-task.ts +24 -1
- package/src/tools/system-one-decide.ts +104 -0
- package/src/tools/task-action.ts +22 -1
- package/src/tools/tool-config.ts +6 -0
- package/src/types.ts +36 -1
- package/src/utils/secret-scrubber.ts +19 -12
- package/src/utils/vcs-repo-match.ts +29 -0
- package/src/workflows/executors/raw-llm.ts +8 -3
- package/src/workflows/executors/system-one-decision.ts +21 -11
- package/src/workflows/executors/workflow-llm.ts +22 -0
- package/src/workflows/index.ts +21 -12
- package/templates/extensions/README.md +1 -0
- package/templates/extensions/comms-adherence-gate/README.md +88 -0
- package/templates/extensions/comms-adherence-gate/hooks.ts +489 -0
- package/templates/extensions/comms-adherence-gate/manifest.json +8 -0
- package/dist/cli-5t4f29ez.js +0 -33
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import {
|
|
3
|
+
checkQuickJSCompat,
|
|
4
|
+
type QuickJSCompatRule,
|
|
5
|
+
quickJSCompatWarnings,
|
|
6
|
+
} from "../scripts-runtime/quickjs-compat";
|
|
7
|
+
|
|
8
|
+
const wrap = (body: string) => `export default async (args, ctx) => {\n${body}\n};`;
|
|
9
|
+
|
|
10
|
+
// One positive (flagged) and one negative (clean, but close) fixture per rule.
|
|
11
|
+
const FIXTURES: Array<{ rule: QuickJSCompatRule; flagged: string; clean: string }> = [
|
|
12
|
+
{
|
|
13
|
+
rule: "bunGlobal",
|
|
14
|
+
flagged: wrap("return Bun.hash('x');"),
|
|
15
|
+
clean: wrap("const Bun = { hash: (s) => s }; return typeof globalThis.Bun;"),
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
rule: "process",
|
|
19
|
+
flagged: wrap("return process.env.HOME;"),
|
|
20
|
+
clean: wrap("const config = { process: 1 }; return config.process + args.process;"),
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
rule: "fsStdlib",
|
|
24
|
+
flagged: wrap("return ctx.stdlib.glob('*.ts');"),
|
|
25
|
+
clean: wrap("return ctx.stdlib.fetchJson('https://example.com');"),
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
rule: "require",
|
|
29
|
+
flagged: wrap("const fs = require('fs'); return fs;"),
|
|
30
|
+
clean: wrap("const require = (name) => name; return require('fs');"),
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
rule: "unsupportedGlobal",
|
|
34
|
+
flagged: wrap("return Buffer.from('aGk=', 'base64').toString();"),
|
|
35
|
+
clean: wrap("const bytes: Buffer | null = null; return { Buffer: bytes };"),
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
rule: "cryptoSubtle",
|
|
39
|
+
flagged: wrap("return crypto.subtle.digest('SHA-256', new Uint8Array());"),
|
|
40
|
+
clean: wrap("return crypto.randomUUID();"),
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
rule: "responseBody",
|
|
44
|
+
flagged: wrap("const res = await fetch('https://example.com'); return res.arrayBuffer();"),
|
|
45
|
+
clean: wrap("const res = await fetch('https://example.com'); return res.text();"),
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
rule: "localeFormat",
|
|
49
|
+
flagged: wrap("return new Date().toLocaleString('en-GB', { timeZone: 'Europe/Madrid' });"),
|
|
50
|
+
clean: wrap("return new Date().toISOString();"),
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
rule: "hostCredential",
|
|
54
|
+
flagged: wrap("return ctx.stdlib.Redacted.value(ctx.swarm.config.apiKey);"),
|
|
55
|
+
clean: wrap("return ctx.stdlib.Redacted.value(ctx.swarm.config.agentId);"),
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
rule: "privateHost",
|
|
59
|
+
flagged: wrap("return ctx.stdlib.fetchJson('http://intranet:8080/status');"),
|
|
60
|
+
clean: wrap("return ctx.stdlib.fetchJson('https://api.example.com/status');"),
|
|
61
|
+
},
|
|
62
|
+
];
|
|
63
|
+
|
|
64
|
+
describe("checkQuickJSCompat", () => {
|
|
65
|
+
for (const { rule, flagged, clean } of FIXTURES) {
|
|
66
|
+
test(`${rule}: flags the positive fixture`, () => {
|
|
67
|
+
const report = checkQuickJSCompat(flagged);
|
|
68
|
+
expect(report.compatible).toBe(false);
|
|
69
|
+
expect(report.findings.map((finding) => finding.rule)).toContain(rule);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test(`${rule}: passes the negative fixture`, () => {
|
|
73
|
+
expect(checkQuickJSCompat(clean)).toEqual({ compatible: true, findings: [] });
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
test("workspaceRw: flags the fsMode, not the source", () => {
|
|
78
|
+
const source = wrap("return 'workspace-rw';");
|
|
79
|
+
expect(checkQuickJSCompat(source, { fsMode: "workspace-rw" }).findings).toEqual([
|
|
80
|
+
{ rule: "workspaceRw", message: "fsMode workspace-rw is not available under quickjs" },
|
|
81
|
+
]);
|
|
82
|
+
expect(checkQuickJSCompat(source, { fsMode: "none" }).compatible).toBe(true);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("fsStdlib also catches stdlib imports and destructuring", () => {
|
|
86
|
+
const imported = `import { glob } from "stdlib";\n${wrap("return glob('*');")}`;
|
|
87
|
+
const destructured = wrap("const { grep } = ctx.stdlib; return grep('x', '.');");
|
|
88
|
+
expect(checkQuickJSCompat(imported).findings[0]?.rule).toBe("fsStdlib");
|
|
89
|
+
expect(checkQuickJSCompat(destructured).findings[0]?.rule).toBe("fsStdlib");
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("globals read off globalThis count, typeof feature checks do not", () => {
|
|
93
|
+
expect(checkQuickJSCompat(wrap("return globalThis.process.env;")).findings[0]?.rule).toBe(
|
|
94
|
+
"process",
|
|
95
|
+
);
|
|
96
|
+
expect(
|
|
97
|
+
checkQuickJSCompat(wrap("return (globalThis as any).Bun.version;")).findings[0]?.rule,
|
|
98
|
+
).toBe("bunGlobal");
|
|
99
|
+
expect(checkQuickJSCompat(wrap("return typeof process === 'undefined';")).compatible).toBe(
|
|
100
|
+
true,
|
|
101
|
+
);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("the shimmed web globals are compatible", () => {
|
|
105
|
+
const source = wrap(`const url = new URL("/x?a=1", "https://example.com");
|
|
106
|
+
url.searchParams.set("b", "2");
|
|
107
|
+
const bytes = new TextEncoder().encode(url.href);
|
|
108
|
+
const id = setInterval(() => {}, 10);
|
|
109
|
+
clearInterval(id);
|
|
110
|
+
await fetch(url, { method: "POST", body: bytes });
|
|
111
|
+
return [new TextDecoder().decode(bytes), crypto.randomUUID(), crypto.getRandomValues(new Uint8Array(4))];`);
|
|
112
|
+
expect(checkQuickJSCompat(source)).toEqual({ compatible: true, findings: [] });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("bindings resolve by lexical scope", () => {
|
|
116
|
+
// A local Buffer parameter in one function does not hide a global Buffer in another.
|
|
117
|
+
const shadowed = `function decode(Buffer: { from(x: string): string }) { return Buffer.from("x"); }
|
|
118
|
+
export default async () => [decode({ from: (x) => x }), Buffer.from("aGk=", "base64").toString()];`;
|
|
119
|
+
expect(checkQuickJSCompat(shadowed).findings).toEqual([
|
|
120
|
+
{ rule: "unsupportedGlobal", message: "Buffer is not available under quickjs", line: 2 },
|
|
121
|
+
]);
|
|
122
|
+
// Block scope too, and a shorthand property reads the binding it names.
|
|
123
|
+
const block = wrap("{ const process = 1; } return { process };");
|
|
124
|
+
expect(checkQuickJSCompat(block).findings[0]?.rule).toBe("process");
|
|
125
|
+
// Every use resolves locally: clean.
|
|
126
|
+
const local = `function decode(Buffer: { from(x: string): string }) { return Buffer.from("x"); }
|
|
127
|
+
export default async () => { const process = { env: {} }; return [decode({ from: (x) => x }), process.env]; };`;
|
|
128
|
+
expect(checkQuickJSCompat(local)).toEqual({ compatible: true, findings: [] });
|
|
129
|
+
// A declare-only binding defines nothing at run time, so the global still counts.
|
|
130
|
+
const ambient = `declare const Buffer: any;\n${wrap("return Buffer.from('x');")}`;
|
|
131
|
+
expect(checkQuickJSCompat(ambient).findings[0]?.rule).toBe("unsupportedGlobal");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("atob and btoa are shimmed; AbortSignal stays reported", () => {
|
|
135
|
+
expect(checkQuickJSCompat(wrap("return atob(btoa('hi'));"))).toEqual({
|
|
136
|
+
compatible: true,
|
|
137
|
+
findings: [],
|
|
138
|
+
});
|
|
139
|
+
expect(
|
|
140
|
+
checkQuickJSCompat(
|
|
141
|
+
wrap("return fetch('https://example.com', { signal: AbortSignal.timeout(5) });"),
|
|
142
|
+
).findings[0]?.message,
|
|
143
|
+
).toBe("AbortSignal is not available under quickjs");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("privateHost: literal fetch URLs to single-label or private hosts", () => {
|
|
147
|
+
const hosts = (body: string) =>
|
|
148
|
+
checkQuickJSCompat(wrap(body)).findings.filter((finding) => finding.rule === "privateHost");
|
|
149
|
+
const flagged = [
|
|
150
|
+
"fetch('http://api:3013/health')",
|
|
151
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: script source under test
|
|
152
|
+
"fetch(`http://localhost:8080/${args.path}`)",
|
|
153
|
+
"globalThis.fetch('http://10.0.0.5/x')",
|
|
154
|
+
"ctx.stdlib.fetch('http://192.168.1.10/')",
|
|
155
|
+
"ctx.stdlib.fetchJson('http://[::1]:9000/')",
|
|
156
|
+
"ctx.stdlib.fetchJson('http://0x7f.1/')",
|
|
157
|
+
"fetch('http://[fd00::1]/')",
|
|
158
|
+
"fetch('http://[::ffff:10.1.2.3]/')",
|
|
159
|
+
"fetch('https://svc.localhost/')",
|
|
160
|
+
"const { fetchJson } = ctx.stdlib; await fetchJson('http://intranet/')",
|
|
161
|
+
];
|
|
162
|
+
for (const call of flagged) expect(hosts(`return ${call};`)).toHaveLength(1);
|
|
163
|
+
const clean = [
|
|
164
|
+
"fetch('https://example.com/x')",
|
|
165
|
+
"fetch('https://8.8.8.8/')",
|
|
166
|
+
"fetch('http://[2001:db8::1]/')",
|
|
167
|
+
"fetch(args.url)",
|
|
168
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: script source under test
|
|
169
|
+
"fetch(`http://${args.host}/x`)",
|
|
170
|
+
"fetch('relative/path')",
|
|
171
|
+
"ctx.api.github.fetch('http://intranet/')",
|
|
172
|
+
"console.log('http://intranet/')",
|
|
173
|
+
];
|
|
174
|
+
for (const call of clean) expect(hosts(`return ${call};`)).toEqual([]);
|
|
175
|
+
// A const that holds the base URL fixes the host too; a let or a cut-off name does not.
|
|
176
|
+
const constBase = [
|
|
177
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: script source under test
|
|
178
|
+
"const API = 'http://api:3013'; return fetch(`${API}/health`);",
|
|
179
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: script source under test
|
|
180
|
+
"const API = 'http://api:3013'; return fetch(`${API}${args.path}`);",
|
|
181
|
+
"const API = 'http://api:3013'; return ctx.stdlib.fetch(API + '/x');",
|
|
182
|
+
"const API = `http://10.1.2.3`; return fetch(API);",
|
|
183
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: script source under test
|
|
184
|
+
"const API = 'http://api:3013'; const rows = `${API}/rows/${args.id}`; return fetch(`${rows}?limit=5`);",
|
|
185
|
+
];
|
|
186
|
+
for (const body of constBase) expect(hosts(body)).toHaveLength(1);
|
|
187
|
+
const dynamicBase = [
|
|
188
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: script source under test
|
|
189
|
+
"let API = 'http://api:3013'; API = args.base; return fetch(`${API}/x`);",
|
|
190
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: script source under test
|
|
191
|
+
"return fetch(`http://intra${args.suffix}/x`);",
|
|
192
|
+
"return fetch('http://intra' + args.suffix);",
|
|
193
|
+
// biome-ignore lint/suspicious/noTemplateCurlyInString: script source under test
|
|
194
|
+
"const API = 'https://api.example.com'; return fetch(`${API}/x`);",
|
|
195
|
+
];
|
|
196
|
+
for (const body of dynamicBase) expect(hosts(body)).toEqual([]);
|
|
197
|
+
expect(hosts("return fetch('http://api:3013/health');")[0]?.message).toBe(
|
|
198
|
+
"fetch to api:3013 (single-label host) depends on the egress policy: quickjs refuses private hosts unless the operator allows them",
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("reports each rule and name once, with its first line", () => {
|
|
203
|
+
const report = checkQuickJSCompat(wrap("Bun.sleep(1);\nBun.sleep(2);\nreturn process.pid;"));
|
|
204
|
+
expect(report.findings).toEqual([
|
|
205
|
+
{ rule: "bunGlobal", message: "Bun.* is not available under quickjs", line: 2 },
|
|
206
|
+
{ rule: "process", message: "process is not available under quickjs", line: 4 },
|
|
207
|
+
]);
|
|
208
|
+
expect(quickJSCompatWarnings(report, "actions.sync")).toEqual([
|
|
209
|
+
"actions.sync: quickjs compat (bunGlobal, line 2): Bun.* is not available under quickjs",
|
|
210
|
+
"actions.sync: quickjs compat (process, line 4): process is not available under quickjs",
|
|
211
|
+
]);
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -107,6 +107,9 @@ const OPERATOR_OR_USER_VERBS: PermissionVerb[] = [
|
|
|
107
107
|
|
|
108
108
|
const LEAD_OR_OPERATOR_VERBS: PermissionVerb[] = ["models.catalog.write"];
|
|
109
109
|
|
|
110
|
+
// The shared API key alone: dashboard users are denied too.
|
|
111
|
+
const API_KEY_OPERATOR_VERBS: PermissionVerb[] = ["models.profiles.write"];
|
|
112
|
+
|
|
110
113
|
const AGENT_OR_OPERATOR_VERBS: PermissionVerb[] = ["models.harness-support.write"];
|
|
111
114
|
|
|
112
115
|
const LEAD_OR_RESOURCE_OWNER_VERBS: PermissionVerb[] = [
|
|
@@ -211,6 +214,7 @@ describe("verb-group partition", () => {
|
|
|
211
214
|
...LEAD_OR_OPERATOR_OR_USER_VERBS,
|
|
212
215
|
...OPERATOR_OR_USER_VERBS,
|
|
213
216
|
...LEAD_OR_OPERATOR_VERBS,
|
|
217
|
+
...API_KEY_OPERATOR_VERBS,
|
|
214
218
|
...AGENT_OR_OPERATOR_VERBS,
|
|
215
219
|
];
|
|
216
220
|
expect(new Set(grouped).size).toBe(grouped.length);
|
|
@@ -325,6 +329,23 @@ describe("agent-or-operator verbs", () => {
|
|
|
325
329
|
}
|
|
326
330
|
});
|
|
327
331
|
|
|
332
|
+
describe("api-key operator verbs", () => {
|
|
333
|
+
const expected: Expected = {
|
|
334
|
+
lead: false,
|
|
335
|
+
worker: false,
|
|
336
|
+
ownerWorker: false,
|
|
337
|
+
creatorWorker: false,
|
|
338
|
+
userRequester: false,
|
|
339
|
+
foreignUser: false,
|
|
340
|
+
operator: true,
|
|
341
|
+
};
|
|
342
|
+
for (const verb of API_KEY_OPERATOR_VERBS) {
|
|
343
|
+
test(`${verb}: only the operator allowed`, () => {
|
|
344
|
+
expectDecisions(verb, { kind: "none" }, expected);
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
|
|
328
349
|
describe("lead-or-operator verbs", () => {
|
|
329
350
|
const expected: Expected = {
|
|
330
351
|
lead: true,
|
|
@@ -628,6 +628,10 @@ describe("script connections", () => {
|
|
|
628
628
|
markMigrationApplied(database, "213_script_connections_sql.sql");
|
|
629
629
|
// 215 alters swarm_config, which this migration-112-only fixture does not create.
|
|
630
630
|
markMigrationApplied(database, "215_swarm_config_export_to_env.sql");
|
|
631
|
+
// 219 rewrites task_context_snapshots, which this fixture does not create.
|
|
632
|
+
markMigrationApplied(database, "219_relabel_pi_context_compactions.sql");
|
|
633
|
+
// 220 reads agents and swarm_config, which this migration-112-only fixture does not create.
|
|
634
|
+
markMigrationApplied(database, "220_model_profiles.sql");
|
|
631
635
|
|
|
632
636
|
const id = crypto.randomUUID();
|
|
633
637
|
const now = new Date().toISOString();
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { describe, expect, test } from "bun:test";
|
|
1
|
+
import { afterAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { mkdtemp, rm } from "node:fs/promises";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
2
5
|
import { classifyExit, NativeScriptExecutor } from "../scripts-runtime/executors/native";
|
|
3
6
|
import { QuickJSScriptExecutor } from "../scripts-runtime/executors/quickjs";
|
|
4
7
|
import type {
|
|
@@ -8,6 +11,7 @@ import type {
|
|
|
8
11
|
} from "../scripts-runtime/executors/types";
|
|
9
12
|
import { DEFAULT_SCRIPT_RESOURCES } from "../scripts-runtime/executors/types";
|
|
10
13
|
import { SKIP_SANDBOX_SPAWN_TESTS } from "./sandbox-spawn-test-helpers";
|
|
14
|
+
import { CHILD_PROCESS_TEST_BUDGET_MS } from "./test-proc";
|
|
11
15
|
|
|
12
16
|
const payload = {
|
|
13
17
|
system: {
|
|
@@ -132,6 +136,253 @@ conformance("fake", () => new FakeScriptExecutor(), false);
|
|
|
132
136
|
// Worker threads and WASM, no Bun.spawn: runs even when the spawn probe fails.
|
|
133
137
|
conformance("quickjs", () => new QuickJSScriptExecutor(), false);
|
|
134
138
|
|
|
139
|
+
// Each probe is plain JavaScript, so the test process (Bun) can evaluate it too:
|
|
140
|
+
// both executors must return exactly what Bun's own web globals return.
|
|
141
|
+
const echoServer = Bun.serve({
|
|
142
|
+
port: 0,
|
|
143
|
+
async fetch(req) {
|
|
144
|
+
const bytes = new Uint8Array(await req.arrayBuffer());
|
|
145
|
+
return Response.json({
|
|
146
|
+
contentType: req.headers.get("content-type"),
|
|
147
|
+
bytes: Array.from(bytes),
|
|
148
|
+
});
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
afterAll(() => echoServer.stop(true));
|
|
152
|
+
|
|
153
|
+
const PARITY_PROBES: Record<string, string> = {
|
|
154
|
+
URL: `async () => {
|
|
155
|
+
const cases = [
|
|
156
|
+
["https://user:pa ss@Example.COM:443/a/./b/../c d?q=1 2&x='y'#frag ment"],
|
|
157
|
+
["http://example.com:8080"], ["../x?y#z", "https://a.b/c/d/e"], ["//other.host/p", "https://a.b/c"],
|
|
158
|
+
["?new", "https://a.b/c/d?q#h"], ["#h2", "https://a.b/c/d?q#h"], ["mailto:someone@example.com"],
|
|
159
|
+
["foo://EXAMPLE.com/a/../b"], ["http://0x7f.1/"], ["http://[0:0:0:0:0:0:0:1]:80/"],
|
|
160
|
+
["https://münchen.de/straße"], ["http://a.b/c\\\\d"], ["file:///etc/hosts"], ["http:foo", "http://a.b/c/d"],
|
|
161
|
+
["not a url"], ["https://a.b:99999/"], ["http://exa mple.com"], ["http://256.1.1.1/"],
|
|
162
|
+
];
|
|
163
|
+
const keys = ["href", "origin", "protocol", "username", "password", "host", "hostname", "port", "pathname", "search", "hash"];
|
|
164
|
+
const parsed = cases.map(([url, base]) => {
|
|
165
|
+
try {
|
|
166
|
+
const u = new URL(url, base);
|
|
167
|
+
return Object.fromEntries(keys.map((key) => [key, u[key]]).concat([["params", [...u.searchParams]]]));
|
|
168
|
+
} catch (error) {
|
|
169
|
+
return { error: error.name };
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
const u = new URL("https://a.b/p?x=1#h");
|
|
173
|
+
u.searchParams.append("y", "two words");
|
|
174
|
+
const linked = u.href;
|
|
175
|
+
u.search = "?z=3";
|
|
176
|
+
u.pathname = "/new path";
|
|
177
|
+
u.port = "8443";
|
|
178
|
+
u.hostname = "C.D";
|
|
179
|
+
u.hash = "";
|
|
180
|
+
return { parsed, linked, mutated: u.href, json: JSON.stringify({ u }), canParse: [URL.canParse("x"), URL.canParse("x", "https://a/")] };
|
|
181
|
+
}`,
|
|
182
|
+
"URL relative state": `async () => {
|
|
183
|
+
const base = "http://a/b?q#f";
|
|
184
|
+
const cases = [
|
|
185
|
+
"http:/x", "http:/", "http:", "http:?y", "http:#g", "http:x/../y", "http:\\\\x", "http://x", "http:///x",
|
|
186
|
+
"http:/\\\\x", "https:/x", "ws:/x", "foo:/x", "file:/x",
|
|
187
|
+
];
|
|
188
|
+
return cases.map((url) => {
|
|
189
|
+
try { return new URL(url, base).href; } catch (error) { return error.name; }
|
|
190
|
+
});
|
|
191
|
+
}`,
|
|
192
|
+
"URL setters": `async () => {
|
|
193
|
+
const run = (start, key, values) => values.map((value) => {
|
|
194
|
+
const u = new URL(start);
|
|
195
|
+
u[key] = value;
|
|
196
|
+
return u.href;
|
|
197
|
+
});
|
|
198
|
+
return {
|
|
199
|
+
hostname: run("http://a.b:81/p", "hostname", [
|
|
200
|
+
"[::1]", "[::1]:90", "example.net:90", "c.d/e", "c.d?x", "user@c.d", "", "exa mple", "C.D", "[::1", "0x7f.1",
|
|
201
|
+
]),
|
|
202
|
+
host: run("http://a.b:81/p", "host", [
|
|
203
|
+
"[::1]", "[::1]:90", "c.d:90", "c.d:", "c.d:80", "c.d:99999", "c.d:9x", "user@c.d", "user:pw@c.d:9", "", "c.d/e:1", ":90",
|
|
204
|
+
"user:pw@c.d", "c.d:1:2", "c.d:9/x:1", "c.d:080", "c.d:65536", "c.d:x", "[::1]:80",
|
|
205
|
+
]),
|
|
206
|
+
hostNoPort: run("http://a.b/p", "host", ["c.d:80", "c.d:99999"]),
|
|
207
|
+
nonSpecialHost: run("foo://a:5/p", "host", ["", "b:1", "[::1]", "b@c", "b:", "b:99999"]),
|
|
208
|
+
nonSpecialEmptyHost: run("foo://a/p", "host", [""]),
|
|
209
|
+
nonSpecialHostname: run("foo://u@a:5/p", "hostname", ["", "b"]),
|
|
210
|
+
protocol: run("http://a.b/p", "protocol", [
|
|
211
|
+
"https", "https:", "https:foo", "https://x", "ht tps", "https!", "1http", "foo", "ws:", "HTTPS", "\\thttps",
|
|
212
|
+
]),
|
|
213
|
+
fileProtocol: run("file:///p", "protocol", ["http"]),
|
|
214
|
+
opaqueHost: run("mailto:x@y", "host", ["z"]),
|
|
215
|
+
};
|
|
216
|
+
}`,
|
|
217
|
+
URLSearchParams: `async () => {
|
|
218
|
+
const p = new URLSearchParams("?b=2&a=1&a=3&sp=a+b%20c&uni=%C3%A9&bad=%E0%A4%A");
|
|
219
|
+
p.append("x y", "ä&=+");
|
|
220
|
+
p.sort();
|
|
221
|
+
const sorted = p.toString();
|
|
222
|
+
const all = p.getAll("a");
|
|
223
|
+
p.set("a", "9");
|
|
224
|
+
p.delete("b");
|
|
225
|
+
return {
|
|
226
|
+
sorted, all, size: p.size, after: p.toString(), entries: [...p],
|
|
227
|
+
has: [p.has("a"), p.has("a", "1"), p.has("zz")],
|
|
228
|
+
fromObject: new URLSearchParams({ q: "hello world", n: 5 }).toString(),
|
|
229
|
+
fromPairs: new URLSearchParams([["k", "v"], ["k", "w"]]).toString(),
|
|
230
|
+
};
|
|
231
|
+
}`,
|
|
232
|
+
"URLSearchParams live traversal": `async () => {
|
|
233
|
+
const appended = new URLSearchParams("a=1&b=2");
|
|
234
|
+
const keys = appended.keys();
|
|
235
|
+
appended.append("c", "3");
|
|
236
|
+
const keysAfterAppend = [...keys];
|
|
237
|
+
|
|
238
|
+
const deleted = new URLSearchParams("a=1&b=2&c=3");
|
|
239
|
+
const visited = [];
|
|
240
|
+
deleted.forEach((value, key) => {
|
|
241
|
+
visited.push(key + value);
|
|
242
|
+
if (key === "a") deleted.delete("c");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
const grown = new URLSearchParams("x=1");
|
|
246
|
+
const grownVisits = [];
|
|
247
|
+
grown.forEach((value, key) => {
|
|
248
|
+
grownVisits.push(key);
|
|
249
|
+
if (grownVisits.length < 3) grown.append("y" + grownVisits.length, value);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const shifted = new URLSearchParams("a=1&b=2&c=3");
|
|
253
|
+
const entries = shifted.entries();
|
|
254
|
+
const first = entries.next().value;
|
|
255
|
+
shifted.delete("a");
|
|
256
|
+
const rest = [...entries];
|
|
257
|
+
|
|
258
|
+
const reset = new URLSearchParams("a=1&b=2");
|
|
259
|
+
const values = reset.values();
|
|
260
|
+
values.next();
|
|
261
|
+
reset.set("a", "9");
|
|
262
|
+
reset.sort();
|
|
263
|
+
const drained = values.next();
|
|
264
|
+
reset.append("z", "0");
|
|
265
|
+
const afterDone = values.next();
|
|
266
|
+
|
|
267
|
+
const linked = new URL("https://h/?a=1&b=2");
|
|
268
|
+
const linkedKeys = linked.searchParams.keys();
|
|
269
|
+
linked.search = "?c=3&d=4&e=5";
|
|
270
|
+
const linkedRest = [...linkedKeys];
|
|
271
|
+
|
|
272
|
+
return { keysAfterAppend, visited, grownVisits, first, rest, drained, afterDone, linkedRest };
|
|
273
|
+
}`,
|
|
274
|
+
base64: `async () => {
|
|
275
|
+
const attempt = (run) => { try { return run(); } catch (error) { return error.name; } };
|
|
276
|
+
return {
|
|
277
|
+
encode: ["", "f", "fo", "foo", "foob", "fooba", "foobar", "\\u00ff\\u0000\\u0080", 123, null, undefined].map((value) => btoa(value)),
|
|
278
|
+
decode: ["", "Zg", "Zg==", "Zm8=", "Zm9v", " Zm 9v\\n", "Zm9vYg", "/+8=", "AP+A"].map((value) => atob(value)),
|
|
279
|
+
decodeBad: ["Z", "Zg=", "Zg===", "Z=g=", "Zm9v!", "Zm9v=", "===="].map((value) => attempt(() => atob(value))),
|
|
280
|
+
encodeBad: attempt(() => btoa("é👋")),
|
|
281
|
+
roundTrip: atob(btoa(String.fromCharCode(...Array.from({ length: 256 }, (_, i) => i)))).length,
|
|
282
|
+
};
|
|
283
|
+
}`,
|
|
284
|
+
TextEncoder: `async () => {
|
|
285
|
+
const encoder = new TextEncoder();
|
|
286
|
+
const bytes = encoder.encode("héllo 👋 \\uD800x");
|
|
287
|
+
const decoder = new TextDecoder();
|
|
288
|
+
const wave = encoder.encode("👋é");
|
|
289
|
+
const streamed = [decoder.decode(wave.subarray(0, 2), { stream: true }), decoder.decode(wave.subarray(2, 5), { stream: true }), decoder.decode(wave.subarray(5))];
|
|
290
|
+
let fatal = "accepted";
|
|
291
|
+
try { new TextDecoder("utf-8", { fatal: true }).decode(new Uint8Array([0xc3])); } catch (error) { fatal = error.name; }
|
|
292
|
+
const target = new Uint8Array(5);
|
|
293
|
+
return {
|
|
294
|
+
bytes: Array.from(bytes),
|
|
295
|
+
roundTrip: decoder.decode(bytes),
|
|
296
|
+
replaced: decoder.decode(new Uint8Array([0x61, 0xff, 0xe2, 0x82, 0x62, 0xf0, 0x9f, 0x91])),
|
|
297
|
+
streamed, fatal,
|
|
298
|
+
bom: decoder.decode(new Uint8Array([0xef, 0xbb, 0xbf, 0x41])),
|
|
299
|
+
fromBuffer: decoder.decode(new Uint8Array([104, 105]).buffer),
|
|
300
|
+
encodeInto: [encoder.encodeInto("aé👋", target), Array.from(target)],
|
|
301
|
+
};
|
|
302
|
+
}`,
|
|
303
|
+
setInterval: `async () => {
|
|
304
|
+
let ticks = 0;
|
|
305
|
+
await new Promise((resolve) => {
|
|
306
|
+
const id = setInterval((step) => {
|
|
307
|
+
ticks += step;
|
|
308
|
+
if (ticks === 3) { clearInterval(id); resolve(); }
|
|
309
|
+
}, 5, 1);
|
|
310
|
+
});
|
|
311
|
+
let crossCleared = 0;
|
|
312
|
+
const other = setInterval(() => { crossCleared++; }, 5);
|
|
313
|
+
clearTimeout(other);
|
|
314
|
+
await new Promise((resolve) => setTimeout(resolve, 40));
|
|
315
|
+
return { ticks, crossCleared };
|
|
316
|
+
}`,
|
|
317
|
+
crypto: `async () => {
|
|
318
|
+
const uuids = [crypto.randomUUID(), crypto.randomUUID()];
|
|
319
|
+
const words = new Uint32Array(8);
|
|
320
|
+
const same = crypto.getRandomValues(words) === words;
|
|
321
|
+
let quota = "accepted";
|
|
322
|
+
try { crypto.getRandomValues(new Uint8Array(65537)); } catch (error) { quota = error.name; }
|
|
323
|
+
return {
|
|
324
|
+
v4: uuids.every((id) => /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(id)),
|
|
325
|
+
distinct: uuids[0] !== uuids[1],
|
|
326
|
+
same,
|
|
327
|
+
filled: words.some((word) => word !== 0),
|
|
328
|
+
quota,
|
|
329
|
+
};
|
|
330
|
+
}`,
|
|
331
|
+
"fetch bodies": `async ({ base }) => {
|
|
332
|
+
const send = async (body, headers) => (await fetch(base, { method: "POST", body, headers })).json();
|
|
333
|
+
return {
|
|
334
|
+
uint8: await send(new Uint8Array([0, 1, 254, 255])),
|
|
335
|
+
view: await send(new Uint16Array([1, 2]).subarray(1)),
|
|
336
|
+
buffer: await send(new Uint8Array([7, 8, 9]).buffer, { "content-type": "application/octet-stream" }),
|
|
337
|
+
form: await send(new URLSearchParams({ a: "1 2", b: "é" })),
|
|
338
|
+
formTyped: await send(new URLSearchParams("x=1"), { "Content-Type": "text/plain" }),
|
|
339
|
+
};
|
|
340
|
+
}`,
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
// Imports the probe as a module, so Bun's own globals compute the expectation.
|
|
344
|
+
async function runInBun(probe: string, args: unknown): Promise<unknown> {
|
|
345
|
+
const dir = await mkdtemp(join(tmpdir(), "parity-probe-"));
|
|
346
|
+
try {
|
|
347
|
+
const file = join(dir, "probe.js");
|
|
348
|
+
await Bun.write(file, `export default ${probe};`);
|
|
349
|
+
const mod = (await import(file)) as { default: (args: unknown) => Promise<unknown> };
|
|
350
|
+
return await mod.default(args);
|
|
351
|
+
} finally {
|
|
352
|
+
await rm(dir, { recursive: true, force: true });
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
describe("quickjs parity shims match native and Bun", () => {
|
|
357
|
+
const args = { base: `http://127.0.0.1:${echoServer.port}/echo` };
|
|
358
|
+
const executors: Array<[string, () => ScriptExecutor, boolean]> = [
|
|
359
|
+
["native", () => new NativeScriptExecutor(), true],
|
|
360
|
+
["quickjs", () => new QuickJSScriptExecutor(), false],
|
|
361
|
+
];
|
|
362
|
+
for (const [name, probe] of Object.entries(PARITY_PROBES)) {
|
|
363
|
+
for (const [executorName, makeExecutor, spawns] of executors) {
|
|
364
|
+
// The native case spawns a bun subprocess with a 10s script budget, so
|
|
365
|
+
// the test timeout must outlast it (runbooks/testing.md hard rule 5).
|
|
366
|
+
test.skipIf(spawns && SKIP_SANDBOX_SPAWN_TESTS)(
|
|
367
|
+
`${name} (${executorName})`,
|
|
368
|
+
async () => {
|
|
369
|
+
const expected = await runInBun(probe, args);
|
|
370
|
+
const output = await makeExecutor().run(
|
|
371
|
+
input({
|
|
372
|
+
source: `export default ${probe};`,
|
|
373
|
+
args,
|
|
374
|
+
resources: { ...DEFAULT_SCRIPT_RESOURCES, memoryMb: 2048, wallClockMs: 10_000 },
|
|
375
|
+
}),
|
|
376
|
+
);
|
|
377
|
+
expect(output.error).toBeUndefined();
|
|
378
|
+
expect(output.result).toEqual(expected);
|
|
379
|
+
},
|
|
380
|
+
CHILD_PROCESS_TEST_BUDGET_MS,
|
|
381
|
+
);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
});
|
|
385
|
+
|
|
135
386
|
describe("native-only executor behavior", () => {
|
|
136
387
|
test.skipIf(SKIP_SANDBOX_SPAWN_TESTS)("timeout maps to timeout", async () => {
|
|
137
388
|
const output = await new NativeScriptExecutor().run(
|
|
@@ -69,6 +69,9 @@ const server = Bun.serve({
|
|
|
69
69
|
afterAll(() => server.stop(true));
|
|
70
70
|
|
|
71
71
|
const baseUrl = `http://127.0.0.1:${server.port}`;
|
|
72
|
+
// The documented binary body caps (scripts-runtime guide).
|
|
73
|
+
const MAX_BINARY_BODY_BYTES = 16 * 1024 * 1024;
|
|
74
|
+
const MAX_PENDING_BINARY_BODY_BYTES = 64 * 1024 * 1024;
|
|
72
75
|
const executor = new QuickJSScriptExecutor();
|
|
73
76
|
|
|
74
77
|
function input(source: string, overrides: Partial<ExecutorInput> = {}): ExecutorInput {
|
|
@@ -294,6 +297,35 @@ export default async (args: T) => explode(args);
|
|
|
294
297
|
expect(output.runtimeError?.message).toContain("fetch body must be a string");
|
|
295
298
|
});
|
|
296
299
|
|
|
300
|
+
// Parity with native lives in script-executor-conformance.test.ts. These
|
|
301
|
+
// are the documented limits of the shims.
|
|
302
|
+
test("shims refuse what they do not implement, with a clear error", async () => {
|
|
303
|
+
const output = await executor.run(
|
|
304
|
+
input(`export default async () => {
|
|
305
|
+
const attempt = (run) => { try { run(); return "ok"; } catch (e) { return e.name + ": " + e.message; } };
|
|
306
|
+
return {
|
|
307
|
+
latin1: attempt(() => new TextDecoder("latin1")),
|
|
308
|
+
floatRandom: attempt(() => crypto.getRandomValues(new Float64Array(2))),
|
|
309
|
+
subtle: typeof crypto.subtle,
|
|
310
|
+
};
|
|
311
|
+
};`),
|
|
312
|
+
);
|
|
313
|
+
expect(output.result).toEqual({
|
|
314
|
+
latin1: 'RangeError: quickjs executor: TextDecoder supports utf-8 only, not "latin1"',
|
|
315
|
+
floatRandom: "TypeError: crypto.getRandomValues: the argument must be an integer TypedArray",
|
|
316
|
+
subtle: "undefined",
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test("binary bodies reach the host through ctx.stdlib.fetch too", async () => {
|
|
321
|
+
const output = await executor.run(
|
|
322
|
+
input(
|
|
323
|
+
`export default async (_args, ctx) => (await ctx.stdlib.fetch("${baseUrl}/echo", { method: "POST", body: new TextEncoder().encode("é!") })).json();`,
|
|
324
|
+
),
|
|
325
|
+
);
|
|
326
|
+
expect(output.result).toMatchObject({ method: "POST", body: "é!" });
|
|
327
|
+
});
|
|
328
|
+
|
|
297
329
|
test("user config values stay redacted until unwrapped", async () => {
|
|
298
330
|
const output = await executor.run(
|
|
299
331
|
input(`export default async (_args, ctx) => {
|
|
@@ -436,6 +468,70 @@ export default async (args: T) => explode(args);
|
|
|
436
468
|
expect(output.result).toContain("Too many pending host calls");
|
|
437
469
|
});
|
|
438
470
|
|
|
471
|
+
// The sandbox controls bodyBase64, so these call the bridge directly, past
|
|
472
|
+
// the prelude's own encoding, and check that no request reaches the server.
|
|
473
|
+
const bridgeSend = `const send = async (bodyBase64: unknown) =>
|
|
474
|
+
JSON.parse(await (globalThis as any).__host_call("fetch", JSON.stringify([{ url: "${baseUrl}/sink", method: "POST", bodyBase64 }]))).error?.message ?? "sent";`;
|
|
475
|
+
|
|
476
|
+
test("rejects a malformed binary body before any fetch starts", async () => {
|
|
477
|
+
const before = requests.length;
|
|
478
|
+
const output = await executor.run(
|
|
479
|
+
input(`export default async () => {
|
|
480
|
+
${bridgeSend}
|
|
481
|
+
return [await send(123), await send({ length: 4 }), await send("!!!!"), await send("AAA"), await send("AA=A")];
|
|
482
|
+
};`),
|
|
483
|
+
);
|
|
484
|
+
expect(output.result).toEqual([
|
|
485
|
+
"fetch bodyBase64 must be a string",
|
|
486
|
+
"fetch bodyBase64 must be a string",
|
|
487
|
+
"fetch bodyBase64 is not valid base64",
|
|
488
|
+
"fetch bodyBase64 is not valid base64",
|
|
489
|
+
"fetch bodyBase64 is not valid base64",
|
|
490
|
+
]);
|
|
491
|
+
expect(requests.length).toBe(before);
|
|
492
|
+
});
|
|
493
|
+
|
|
494
|
+
test("rejects an oversized binary body before any fetch starts", async () => {
|
|
495
|
+
const before = requests.length;
|
|
496
|
+
const encodedLimit = Math.ceil(MAX_BINARY_BODY_BYTES / 3) * 4;
|
|
497
|
+
const output = await executor.run(
|
|
498
|
+
input(`export default async () => {
|
|
499
|
+
${bridgeSend}
|
|
500
|
+
return {
|
|
501
|
+
encoded: await send("A".repeat(${encodedLimit + 4})),
|
|
502
|
+
decoded: await send("A".repeat(${encodedLimit})),
|
|
503
|
+
atLimit: await send("A".repeat(${encodedLimit - 4}) + "AA=="),
|
|
504
|
+
};
|
|
505
|
+
};`),
|
|
506
|
+
);
|
|
507
|
+
const tooLarge = `fetch binary body exceeds the ${MAX_BINARY_BODY_BYTES}-byte limit of the quickjs executor`;
|
|
508
|
+
expect(output.result).toEqual({ encoded: tooLarge, decoded: tooLarge, atLimit: "sent" });
|
|
509
|
+
// Only the body at the limit reached the server.
|
|
510
|
+
expect(requests.length).toBe(before + 1);
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
test("caps the binary bodies one run can have pending", async () => {
|
|
514
|
+
const before = { ...hang };
|
|
515
|
+
const encodedLimit = Math.ceil(MAX_BINARY_BODY_BYTES / 3) * 4;
|
|
516
|
+
const output = await executor.run(
|
|
517
|
+
input(
|
|
518
|
+
`export default async () => {
|
|
519
|
+
// Decodes to exactly MAX_BINARY_BODY_BYTES; four fill the per-run budget.
|
|
520
|
+
const args = JSON.stringify([{ url: "${baseUrl}/hang", method: "POST", bodyBase64: "A".repeat(${encodedLimit - 4}) + "AA==" }]);
|
|
521
|
+
const call = async () => JSON.parse(await (globalThis as any).__host_call("fetch", args)).error?.message ?? "sent";
|
|
522
|
+
const held = [call(), call(), call(), call()];
|
|
523
|
+
return call();
|
|
524
|
+
};`,
|
|
525
|
+
{ resources: { ...DEFAULT_SCRIPT_RESOURCES, memoryMb: 1024, wallClockMs: 10_000 } },
|
|
526
|
+
),
|
|
527
|
+
);
|
|
528
|
+
expect(output.result).toBe(
|
|
529
|
+
`Pending fetch binary bodies in one script run exceed ${MAX_PENDING_BINARY_BODY_BYTES} bytes`,
|
|
530
|
+
);
|
|
531
|
+
expect(await waitFor(() => hang.started === before.started + 4)).toBe(true);
|
|
532
|
+
expect(await waitFor(() => hang.aborted === before.aborted + 4)).toBe(true);
|
|
533
|
+
}, 30_000);
|
|
534
|
+
|
|
439
535
|
test("queues runs beyond the pool size", async () => {
|
|
440
536
|
const outputs = await Promise.all(
|
|
441
537
|
Array.from({ length: 12 }, (_, i) =>
|