@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
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
import { pathToFileURL } from "node:url";
|
|
11
11
|
import { toJSONSchema } from "zod";
|
|
12
12
|
|
|
13
|
-
async function createShims(
|
|
13
|
+
async function createShims(moduleDir: string): Promise<void> {
|
|
14
14
|
const runtimeDir = process.env.SCRIPT_RUNTIME_DIR;
|
|
15
15
|
const shims: [string, URL][] = [
|
|
16
16
|
[
|
|
@@ -35,7 +35,7 @@ async function createShims(tmpdir: string): Promise<void> {
|
|
|
35
35
|
]),
|
|
36
36
|
];
|
|
37
37
|
for (const [name, url] of shims) {
|
|
38
|
-
const dir = `${
|
|
38
|
+
const dir = `${moduleDir}/node_modules/${name}`;
|
|
39
39
|
await Bun.$`mkdir -p ${dir}`.quiet();
|
|
40
40
|
await Bun.write(`${dir}/package.json`, JSON.stringify({ type: "module", main: "index.ts" }));
|
|
41
41
|
await Bun.write(`${dir}/index.ts`, `export * from ${JSON.stringify(url.href)};\n`);
|
|
@@ -52,14 +52,15 @@ if (!sourceFile || !resultFile || !tmpdir) {
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
try {
|
|
55
|
-
|
|
55
|
+
// Subdirectory, not the tmpdir itself: this harness runs with cwd = tmpdir,
|
|
56
|
+
// and Bun (>= 1.3.12) snapshots the cwd listing at startup — a file or
|
|
57
|
+
// node_modules written into cwd after launch is invisible to the module
|
|
58
|
+
// resolver. See the same fix in eval-harness.ts.
|
|
59
|
+
const moduleDir = `${tmpdir}/user-module`;
|
|
60
|
+
await createShims(moduleDir);
|
|
56
61
|
|
|
57
62
|
const source = await Bun.file(sourceFile).text();
|
|
58
|
-
|
|
59
|
-
// and Bun (>= 1.3.12) snapshots the cwd listing at startup — a file written
|
|
60
|
-
// into cwd after launch is invisible to the module resolver. See the same
|
|
61
|
-
// fix in eval-harness.ts.
|
|
62
|
-
const userModulePath = `${tmpdir}/user-module/user-script.ts`;
|
|
63
|
+
const userModulePath = `${moduleDir}/user-script.ts`;
|
|
63
64
|
await Bun.write(userModulePath, source);
|
|
64
65
|
|
|
65
66
|
let mod: Record<string, unknown>;
|
|
@@ -0,0 +1,464 @@
|
|
|
1
|
+
import ts from "typescript";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Flags script source that would fail, or silently differ, under the quickjs
|
|
5
|
+
* executor. It is advisory: callers attach the findings as warnings and never
|
|
6
|
+
* reject on them.
|
|
7
|
+
*/
|
|
8
|
+
export type QuickJSCompatRule =
|
|
9
|
+
| "bunGlobal"
|
|
10
|
+
| "process"
|
|
11
|
+
| "fsStdlib"
|
|
12
|
+
| "require"
|
|
13
|
+
| "workspaceRw"
|
|
14
|
+
| "unsupportedGlobal"
|
|
15
|
+
| "cryptoSubtle"
|
|
16
|
+
| "responseBody"
|
|
17
|
+
| "localeFormat"
|
|
18
|
+
| "hostCredential"
|
|
19
|
+
| "privateHost";
|
|
20
|
+
|
|
21
|
+
export type QuickJSCompatFinding = {
|
|
22
|
+
rule: QuickJSCompatRule;
|
|
23
|
+
message: string;
|
|
24
|
+
/** 1-based line in the script source, when the finding has one. */
|
|
25
|
+
line?: number;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type QuickJSCompatReport = {
|
|
29
|
+
compatible: boolean;
|
|
30
|
+
findings: QuickJSCompatFinding[];
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// Globals that Bun provides and the quickjs sandbox does not (measured against
|
|
34
|
+
// the QuickJS build plus quickjs-prelude.ts and quickjs-web-shims.ts).
|
|
35
|
+
const UNSUPPORTED_GLOBALS = new Set([
|
|
36
|
+
"AbortController",
|
|
37
|
+
"AbortSignal",
|
|
38
|
+
"Blob",
|
|
39
|
+
"Buffer",
|
|
40
|
+
"Event",
|
|
41
|
+
"EventTarget",
|
|
42
|
+
"File",
|
|
43
|
+
"FormData",
|
|
44
|
+
"Intl",
|
|
45
|
+
"ReadableStream",
|
|
46
|
+
"Request",
|
|
47
|
+
"TransformStream",
|
|
48
|
+
"WebSocket",
|
|
49
|
+
"WritableStream",
|
|
50
|
+
"clearImmediate",
|
|
51
|
+
"navigator",
|
|
52
|
+
"performance",
|
|
53
|
+
"queueMicrotask",
|
|
54
|
+
"setImmediate",
|
|
55
|
+
"structuredClone",
|
|
56
|
+
]);
|
|
57
|
+
const RESPONSE_BODY_METHODS = new Set(["arrayBuffer", "blob", "formData", "bytes"]);
|
|
58
|
+
const LOCALE_METHODS = new Set(["toLocaleString", "toLocaleDateString", "toLocaleTimeString"]);
|
|
59
|
+
const FS_STDLIB = new Set(["glob", "grep"]);
|
|
60
|
+
// ctx.swarm.config values that quickjs keeps host-side (see quickjs-runner.ts).
|
|
61
|
+
const HOST_ONLY_CONFIG = new Set(["apiKey", "runtimeInstanceId"]);
|
|
62
|
+
const MAX_FINDINGS = 20;
|
|
63
|
+
|
|
64
|
+
const SCRIPT_FILE = "user-script.ts";
|
|
65
|
+
|
|
66
|
+
/** The initializer of the `const` binding a name reads; null for anything else. */
|
|
67
|
+
type ConstInitializer = (node: ts.Identifier) => ts.Expression | null;
|
|
68
|
+
// Follows at most this many consts, as in `const API = "..."; const url = `${API}/x``.
|
|
69
|
+
const MAX_CONST_DEPTH = 4;
|
|
70
|
+
|
|
71
|
+
/** True inside a `declare` statement, namespace or global block. */
|
|
72
|
+
function isAmbient(node: ts.Node): boolean {
|
|
73
|
+
for (let current: ts.Node | undefined = node; current; current = current.parent) {
|
|
74
|
+
if (
|
|
75
|
+
ts.canHaveModifiers(current) &&
|
|
76
|
+
ts.getModifiers(current)?.some((modifier) => modifier.kind === ts.SyntaxKind.DeclareKeyword)
|
|
77
|
+
) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Parses and binds the script with the TypeScript checker (no lib, no imports),
|
|
86
|
+
* so a name resolves by lexical scope: a `Buffer` parameter in one function does
|
|
87
|
+
* not hide a global `Buffer` read in another. A name declared only with
|
|
88
|
+
* `declare` (ambient) still counts as a global, because nothing defines it at run time.
|
|
89
|
+
*/
|
|
90
|
+
function createScope(source: string): {
|
|
91
|
+
sourceFile: ts.SourceFile;
|
|
92
|
+
isLocal: (node: ts.Identifier) => boolean;
|
|
93
|
+
constInitializer: ConstInitializer;
|
|
94
|
+
} {
|
|
95
|
+
const sourceFile = ts.createSourceFile(SCRIPT_FILE, source, ts.ScriptTarget.Latest, true);
|
|
96
|
+
const host: ts.CompilerHost = {
|
|
97
|
+
getSourceFile: (name) => (name === SCRIPT_FILE ? sourceFile : undefined),
|
|
98
|
+
getDefaultLibFileName: () => "lib.d.ts",
|
|
99
|
+
writeFile: () => {},
|
|
100
|
+
getCurrentDirectory: () => "/",
|
|
101
|
+
getCanonicalFileName: (name) => name,
|
|
102
|
+
useCaseSensitiveFileNames: () => true,
|
|
103
|
+
getNewLine: () => "\n",
|
|
104
|
+
fileExists: (name) => name === SCRIPT_FILE,
|
|
105
|
+
readFile: () => undefined,
|
|
106
|
+
};
|
|
107
|
+
const program = ts.createProgram({
|
|
108
|
+
rootNames: [SCRIPT_FILE],
|
|
109
|
+
options: { noLib: true, noResolve: true, types: [], target: ts.ScriptTarget.Latest },
|
|
110
|
+
host,
|
|
111
|
+
});
|
|
112
|
+
const checker = program.getTypeChecker();
|
|
113
|
+
const isLocal = (node: ts.Identifier) => {
|
|
114
|
+
const symbol =
|
|
115
|
+
ts.isShorthandPropertyAssignment(node.parent) && node.parent.name === node
|
|
116
|
+
? checker.getShorthandAssignmentValueSymbol(node.parent)
|
|
117
|
+
: checker.getSymbolAtLocation(node);
|
|
118
|
+
return Boolean(
|
|
119
|
+
symbol?.declarations?.some(
|
|
120
|
+
(declaration) => declaration.getSourceFile() === sourceFile && !isAmbient(declaration),
|
|
121
|
+
),
|
|
122
|
+
);
|
|
123
|
+
};
|
|
124
|
+
const constInitializer: ConstInitializer = (node) => {
|
|
125
|
+
const declaration = checker.getSymbolAtLocation(node)?.valueDeclaration;
|
|
126
|
+
if (!declaration || !ts.isVariableDeclaration(declaration) || !declaration.initializer) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
return ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const
|
|
130
|
+
? declaration.initializer
|
|
131
|
+
: null;
|
|
132
|
+
};
|
|
133
|
+
return { sourceFile, isLocal, constInitializer };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** True when `node` is read as a value, not as a property name or declaration. */
|
|
137
|
+
function isValueReference(node: ts.Identifier): boolean {
|
|
138
|
+
const parent = node.parent;
|
|
139
|
+
if (ts.isPropertyAccessExpression(parent) && parent.name === node) return false;
|
|
140
|
+
if (ts.isShorthandPropertyAssignment(parent)) return true;
|
|
141
|
+
if ("name" in parent && (parent as { name?: ts.Node }).name === node) return false;
|
|
142
|
+
if (ts.isTypeOfExpression(parent)) return false;
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function isTypeOnly(node: ts.Node): boolean {
|
|
147
|
+
return (
|
|
148
|
+
ts.isTypeNode(node) ||
|
|
149
|
+
ts.isInterfaceDeclaration(node) ||
|
|
150
|
+
ts.isTypeAliasDeclaration(node) ||
|
|
151
|
+
(ts.isImportDeclaration(node) && Boolean(node.importClause?.isTypeOnly))
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function stdlibImportNames(node: ts.ImportDeclaration): string[] {
|
|
156
|
+
if (!ts.isStringLiteral(node.moduleSpecifier) || node.moduleSpecifier.text !== "stdlib")
|
|
157
|
+
return [];
|
|
158
|
+
const bindings = node.importClause?.namedBindings;
|
|
159
|
+
if (!bindings || !ts.isNamedImports(bindings)) return [];
|
|
160
|
+
return bindings.elements
|
|
161
|
+
.map((element) => (element.propertyName ?? element.name).text)
|
|
162
|
+
.filter((name) => FS_STDLIB.has(name));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Strips parentheses, `as`, `!` and `satisfies`: `(globalThis as any)` is `globalThis`. */
|
|
166
|
+
function unwrap(node: ts.Expression): ts.Expression {
|
|
167
|
+
let current = node;
|
|
168
|
+
while (
|
|
169
|
+
ts.isParenthesizedExpression(current) ||
|
|
170
|
+
ts.isAsExpression(current) ||
|
|
171
|
+
ts.isNonNullExpression(current) ||
|
|
172
|
+
ts.isTypeAssertionExpression(current) ||
|
|
173
|
+
ts.isSatisfiesExpression(current)
|
|
174
|
+
) {
|
|
175
|
+
current = current.expression;
|
|
176
|
+
}
|
|
177
|
+
return current;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function isGlobalThis(node: ts.Expression): boolean {
|
|
181
|
+
const inner = unwrap(node);
|
|
182
|
+
return ts.isIdentifier(inner) && inner.text === "globalThis";
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** `globalThis.X` or `X` (free), as the global's name; otherwise null. */
|
|
186
|
+
function globalName(node: ts.Expression, isLocal: (node: ts.Identifier) => boolean): string | null {
|
|
187
|
+
const inner = unwrap(node);
|
|
188
|
+
if (ts.isIdentifier(inner)) return isLocal(inner) ? null : inner.text;
|
|
189
|
+
if (ts.isPropertyAccessExpression(inner) && isGlobalThis(inner.expression)) {
|
|
190
|
+
return inner.name.text;
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function isPrivateIPv4([a = 0, b = 0, c = 0]: number[]): boolean {
|
|
196
|
+
return (
|
|
197
|
+
a === 0 ||
|
|
198
|
+
a === 10 ||
|
|
199
|
+
a === 127 ||
|
|
200
|
+
(a === 100 && b >= 64 && b <= 127) ||
|
|
201
|
+
(a === 169 && b === 254) ||
|
|
202
|
+
(a === 172 && b >= 16 && b <= 31) ||
|
|
203
|
+
(a === 192 && b === 168) ||
|
|
204
|
+
(a === 192 && b === 0 && c === 0) ||
|
|
205
|
+
(a === 198 && (b === 18 || b === 19)) ||
|
|
206
|
+
a >= 224
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// `text` is a WHATWG-serialized IPv6 address without brackets: hex groups, at most one "::".
|
|
211
|
+
function isPrivateIPv6(text: string): boolean {
|
|
212
|
+
const [head = "", tail = ""] = text.split("::");
|
|
213
|
+
const left = head ? head.split(":") : [];
|
|
214
|
+
const right = text.includes("::") && tail ? tail.split(":") : [];
|
|
215
|
+
const fill = text.includes("::") ? 8 - left.length - right.length : 0;
|
|
216
|
+
const g = [...left, ...Array<string>(fill).fill("0"), ...right].map((part) => parseInt(part, 16));
|
|
217
|
+
const g0 = g[0] ?? 0;
|
|
218
|
+
if (g.slice(0, 6).every((part) => part === 0)) return true; // ::/96: ::, ::1
|
|
219
|
+
if (g.slice(0, 5).every((part) => part === 0) && g[5] === 0xffff) {
|
|
220
|
+
const [g6 = 0, g7 = 0] = g.slice(6);
|
|
221
|
+
return isPrivateIPv4([g6 >> 8, g6 & 255, g7 >> 8]); // IPv4-mapped
|
|
222
|
+
}
|
|
223
|
+
return (
|
|
224
|
+
(g0 & 0xfe00) === 0xfc00 || // fc00::/7 unique local
|
|
225
|
+
(g0 & 0xffc0) === 0xfe80 || // fe80::/10 link-local
|
|
226
|
+
(g0 & 0xffc0) === 0xfec0 || // fec0::/10 site-local
|
|
227
|
+
(g0 & 0xff00) === 0xff00 // ff00::/8 multicast
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* How the default egress policy sees a parsed URL hostname, statically:
|
|
233
|
+
* "private" for loopback names and private, link-local or reserved IP literals,
|
|
234
|
+
* "single-label" for a name with no dot (usually an internal service name that
|
|
235
|
+
* resolves to a private address). Null for anything else, including public names
|
|
236
|
+
* that resolve privately: only the runtime check sees DNS.
|
|
237
|
+
*/
|
|
238
|
+
function staticHostClass(hostname: string): "private" | "single-label" | null {
|
|
239
|
+
const host = hostname.toLowerCase().replace(/\.$/, "");
|
|
240
|
+
if (host === "localhost" || host.endsWith(".localhost")) return "private";
|
|
241
|
+
if (host.startsWith("[")) return isPrivateIPv6(host.slice(1, -1)) ? "private" : null;
|
|
242
|
+
const v4 = /^(\d+)\.(\d+)\.(\d+)\.(\d+)$/.exec(host);
|
|
243
|
+
if (v4) return isPrivateIPv4(v4.slice(1).map(Number)) ? "private" : null;
|
|
244
|
+
return host !== "" && !host.includes(".") ? "single-label" : null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** The fetch-like call this callee names: fetch, globalThis.fetch, ctx.stdlib.fetch or fetchJson. */
|
|
248
|
+
function fetchCallName(callee: ts.Expression): string | null {
|
|
249
|
+
const inner = unwrap(callee);
|
|
250
|
+
if (ts.isIdentifier(inner)) {
|
|
251
|
+
return inner.text === "fetch" || inner.text === "fetchJson" ? inner.text : null;
|
|
252
|
+
}
|
|
253
|
+
if (!ts.isPropertyAccessExpression(inner)) return null;
|
|
254
|
+
const method = inner.name.text;
|
|
255
|
+
if (method !== "fetch" && method !== "fetchJson") return null;
|
|
256
|
+
const owner = unwrap(inner.expression);
|
|
257
|
+
if (method === "fetch" && isGlobalThis(owner)) return "fetch";
|
|
258
|
+
const ownerName = ts.isIdentifier(owner)
|
|
259
|
+
? owner.text
|
|
260
|
+
: ts.isPropertyAccessExpression(owner)
|
|
261
|
+
? owner.name.text
|
|
262
|
+
: null;
|
|
263
|
+
return ownerName === "stdlib" ? `ctx.stdlib.${method}` : null;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* The fixed start of a URL argument: a literal, a const holding one, or a
|
|
268
|
+
* template or `+` concatenation that starts with them, through up to
|
|
269
|
+
* MAX_CONST_DEPTH consts. `cut` is true when the
|
|
270
|
+
* text stops at a dynamic part that could still extend the host, as in
|
|
271
|
+
* `http://intra${suffix}`; a dynamic part right after a const value cannot.
|
|
272
|
+
*/
|
|
273
|
+
function staticPrefix(
|
|
274
|
+
node: ts.Expression,
|
|
275
|
+
resolve: ConstInitializer,
|
|
276
|
+
depth = 0,
|
|
277
|
+
): { text: string; cut: boolean } | null {
|
|
278
|
+
const inner = unwrap(node);
|
|
279
|
+
if (ts.isStringLiteral(inner) || ts.isNoSubstitutionTemplateLiteral(inner)) {
|
|
280
|
+
return { text: inner.text, cut: false };
|
|
281
|
+
}
|
|
282
|
+
if (ts.isIdentifier(inner)) {
|
|
283
|
+
const initializer = depth < MAX_CONST_DEPTH ? resolve(inner) : null;
|
|
284
|
+
return initializer ? staticPrefix(initializer, resolve, depth + 1) : null;
|
|
285
|
+
}
|
|
286
|
+
if (ts.isTemplateExpression(inner)) {
|
|
287
|
+
let text = inner.head.text;
|
|
288
|
+
let boundary = false;
|
|
289
|
+
for (const span of inner.templateSpans) {
|
|
290
|
+
const part = staticPrefix(span.expression, resolve, depth);
|
|
291
|
+
if (part === null) return { text, cut: !boundary || text === "" };
|
|
292
|
+
if (part.cut) return { text: text + part.text, cut: true };
|
|
293
|
+
text += part.text + span.literal.text;
|
|
294
|
+
boundary = span.literal.text === "";
|
|
295
|
+
}
|
|
296
|
+
return { text, cut: false };
|
|
297
|
+
}
|
|
298
|
+
if (ts.isBinaryExpression(inner) && inner.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
299
|
+
const left = staticPrefix(inner.left, resolve, depth);
|
|
300
|
+
if (left === null || left.cut) return left;
|
|
301
|
+
const right = staticPrefix(inner.right, resolve, depth);
|
|
302
|
+
if (right === null) return { text: left.text, cut: !ts.isIdentifier(unwrap(inner.left)) };
|
|
303
|
+
return { text: left.text + right.text, cut: right.cut };
|
|
304
|
+
}
|
|
305
|
+
return null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** The URL argument's host when the source fixes it, parsed with the host URL parser. */
|
|
309
|
+
function literalUrl(arg: ts.Expression | undefined, resolve: ConstInitializer): URL | null {
|
|
310
|
+
if (!arg) return null;
|
|
311
|
+
const prefix = staticPrefix(arg, resolve);
|
|
312
|
+
if (!prefix) return null;
|
|
313
|
+
// A cut prefix fixes the host only once the authority has ended.
|
|
314
|
+
if (prefix.cut && !/^[a-z][a-z0-9+.-]*:\/\/[^/?#]*[/?#]/i.test(prefix.text)) return null;
|
|
315
|
+
try {
|
|
316
|
+
return new URL(prefix.text);
|
|
317
|
+
} catch {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export function checkQuickJSCompat(
|
|
323
|
+
source: string,
|
|
324
|
+
options: { fsMode?: string } = {},
|
|
325
|
+
): QuickJSCompatReport {
|
|
326
|
+
const findings: QuickJSCompatFinding[] = [];
|
|
327
|
+
const seen = new Set<string>();
|
|
328
|
+
const { sourceFile, isLocal, constInitializer } = createScope(source);
|
|
329
|
+
const add = (rule: QuickJSCompatRule, message: string, node?: ts.Node) => {
|
|
330
|
+
if (seen.has(`${rule}:${message}`)) return;
|
|
331
|
+
seen.add(`${rule}:${message}`);
|
|
332
|
+
const line = node
|
|
333
|
+
? sourceFile.getLineAndCharacterOfPosition(node.getStart(sourceFile)).line + 1
|
|
334
|
+
: undefined;
|
|
335
|
+
findings.push(line === undefined ? { rule, message } : { rule, message, line });
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
if (options.fsMode === "workspace-rw") {
|
|
339
|
+
add("workspaceRw", "fsMode workspace-rw is not available under quickjs");
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const reportGlobal = (name: string, node: ts.Node) => {
|
|
343
|
+
if (name === "Bun") add("bunGlobal", "Bun.* is not available under quickjs", node);
|
|
344
|
+
else if (name === "process") add("process", "process is not available under quickjs", node);
|
|
345
|
+
else if (UNSUPPORTED_GLOBALS.has(name)) {
|
|
346
|
+
add("unsupportedGlobal", `${name} is not available under quickjs`, node);
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const visit = (node: ts.Node) => {
|
|
351
|
+
if (isTypeOnly(node)) return;
|
|
352
|
+
|
|
353
|
+
if (
|
|
354
|
+
ts.isIdentifier(node) &&
|
|
355
|
+
(node.text === "Bun" || node.text === "process" || UNSUPPORTED_GLOBALS.has(node.text)) &&
|
|
356
|
+
isValueReference(node) &&
|
|
357
|
+
!isLocal(node)
|
|
358
|
+
) {
|
|
359
|
+
reportGlobal(node.text, node);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
363
|
+
const member = node.name.text;
|
|
364
|
+
const owner = globalName(node.expression, isLocal);
|
|
365
|
+
if (isGlobalThis(node.expression) && !ts.isTypeOfExpression(node.parent)) {
|
|
366
|
+
reportGlobal(member, node);
|
|
367
|
+
}
|
|
368
|
+
const parent = unwrap(node.expression);
|
|
369
|
+
if (
|
|
370
|
+
HOST_ONLY_CONFIG.has(member) &&
|
|
371
|
+
ts.isPropertyAccessExpression(parent) &&
|
|
372
|
+
parent.name.text === "config"
|
|
373
|
+
) {
|
|
374
|
+
add(
|
|
375
|
+
"hostCredential",
|
|
376
|
+
`ctx.swarm.config.${member} is host-only under quickjs; ctx.swarm calls use it for you`,
|
|
377
|
+
node,
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
if (member === "subtle" && owner === "crypto") {
|
|
381
|
+
add("cryptoSubtle", "crypto.subtle is not available under quickjs", node);
|
|
382
|
+
}
|
|
383
|
+
if (
|
|
384
|
+
FS_STDLIB.has(member) &&
|
|
385
|
+
ts.isPropertyAccessExpression(unwrap(node.expression)) &&
|
|
386
|
+
(unwrap(node.expression) as ts.PropertyAccessExpression).name.text === "stdlib"
|
|
387
|
+
) {
|
|
388
|
+
add("fsStdlib", `ctx.stdlib.${member} needs filesystem access, which quickjs lacks`, node);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
if (ts.isImportDeclaration(node)) {
|
|
393
|
+
for (const name of stdlibImportNames(node)) {
|
|
394
|
+
add("fsStdlib", `stdlib ${name} needs filesystem access, which quickjs lacks`, node);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
if (
|
|
399
|
+
ts.isVariableDeclaration(node) &&
|
|
400
|
+
ts.isObjectBindingPattern(node.name) &&
|
|
401
|
+
node.initializer &&
|
|
402
|
+
ts.isPropertyAccessExpression(node.initializer) &&
|
|
403
|
+
node.initializer.name.text === "stdlib"
|
|
404
|
+
) {
|
|
405
|
+
for (const element of node.name.elements) {
|
|
406
|
+
const name = (element.propertyName ?? element.name).getText(sourceFile);
|
|
407
|
+
if (FS_STDLIB.has(name)) {
|
|
408
|
+
add("fsStdlib", `ctx.stdlib.${name} needs filesystem access, which quickjs lacks`, node);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (ts.isCallExpression(node)) {
|
|
414
|
+
const callee = node.expression;
|
|
415
|
+
const fetchName = fetchCallName(callee);
|
|
416
|
+
const url = fetchName ? literalUrl(node.arguments[0], constInitializer) : null;
|
|
417
|
+
const hostClass =
|
|
418
|
+
url && (url.protocol === "http:" || url.protocol === "https:")
|
|
419
|
+
? staticHostClass(url.hostname)
|
|
420
|
+
: null;
|
|
421
|
+
if (url && hostClass) {
|
|
422
|
+
add(
|
|
423
|
+
"privateHost",
|
|
424
|
+
`${fetchName} to ${url.host} (${hostClass} host) depends on the egress policy: quickjs refuses private hosts unless the operator allows them`,
|
|
425
|
+
node,
|
|
426
|
+
);
|
|
427
|
+
}
|
|
428
|
+
if (ts.isIdentifier(callee) && callee.text === "require" && !isLocal(callee)) {
|
|
429
|
+
add("require", "require() is not supported; use an import", node);
|
|
430
|
+
}
|
|
431
|
+
if (ts.isPropertyAccessExpression(callee)) {
|
|
432
|
+
const method = callee.name.text;
|
|
433
|
+
if (RESPONSE_BODY_METHODS.has(method) && node.arguments.length === 0) {
|
|
434
|
+
add(
|
|
435
|
+
"responseBody",
|
|
436
|
+
`.${method}() on a fetch Response is not available under quickjs (text() and json() are)`,
|
|
437
|
+
node,
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
if (LOCALE_METHODS.has(method) && node.arguments.length > 0) {
|
|
441
|
+
add(
|
|
442
|
+
"localeFormat",
|
|
443
|
+
`${method}(locale, options) ignores its arguments under quickjs (no Intl)`,
|
|
444
|
+
node,
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
ts.forEachChild(node, visit);
|
|
451
|
+
};
|
|
452
|
+
visit(sourceFile);
|
|
453
|
+
|
|
454
|
+
return { compatible: findings.length === 0, findings: findings.slice(0, MAX_FINDINGS) };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** One human-readable line per finding, for `warnings` arrays. */
|
|
458
|
+
export function quickJSCompatWarnings(report: QuickJSCompatReport, label?: string): string[] {
|
|
459
|
+
const prefix = label ? `${label}: ` : "";
|
|
460
|
+
return report.findings.map(
|
|
461
|
+
(finding) =>
|
|
462
|
+
`${prefix}quickjs compat (${finding.rule}${finding.line ? `, line ${finding.line}` : ""}): ${finding.message}`,
|
|
463
|
+
);
|
|
464
|
+
}
|
|
@@ -89,6 +89,7 @@ export const SDK_TOOL_NAME_MAP = {
|
|
|
89
89
|
// ── model catalog ──
|
|
90
90
|
modelCatalog_refresh: "model-catalog-refresh", // external: fetches models.dev
|
|
91
91
|
modelCatalog_overlayUpsert: "model-catalog-overlay-upsert",
|
|
92
|
+
modelProfile_list: "list-model-profiles",
|
|
92
93
|
|
|
93
94
|
// ── slack ──
|
|
94
95
|
slack_read: "slack-read",
|
|
@@ -189,6 +190,9 @@ export const SDK_TOOL_NAME_MAP = {
|
|
|
189
190
|
// ── human input ──
|
|
190
191
|
request_humanInput: "request-human-input",
|
|
191
192
|
approval_cancel: "cancel-approval-request", // destructive
|
|
193
|
+
|
|
194
|
+
// ── decisions ──
|
|
195
|
+
systemOne_decide: "system-one-decide",
|
|
192
196
|
} as const;
|
|
193
197
|
|
|
194
198
|
export const SDK_ALLOWLIST = Object.keys(SDK_TOOL_NAME_MAP) as Array<
|
|
@@ -329,6 +329,8 @@ declare module "swarm-sdk" {
|
|
|
329
329
|
key?: string;
|
|
330
330
|
includeSecrets?: boolean;
|
|
331
331
|
}): Promise<unknown>;
|
|
332
|
+
// --- model profiles ---
|
|
333
|
+
modelProfile_list(args?: { harness?: string }): Promise<unknown>;
|
|
332
334
|
// --- slack ---
|
|
333
335
|
slack_read(args?: {
|
|
334
336
|
inboxMessageId?: string;
|
|
@@ -678,6 +680,16 @@ declare module "swarm-sdk" {
|
|
|
678
680
|
// --- human input ---
|
|
679
681
|
request_humanInput(args: Record<string, unknown>): Promise<unknown>;
|
|
680
682
|
approval_cancel(args: { requestId: string; reason?: string }): Promise<unknown>;
|
|
683
|
+
|
|
684
|
+
// --- decisions ---
|
|
685
|
+
systemOne_decide(args: {
|
|
686
|
+
provider?: "typesafe" | "openrouter" | "laya" | "openai" | "cloudflare";
|
|
687
|
+
model?: string;
|
|
688
|
+
state: string | string[] | Record<string, unknown>;
|
|
689
|
+
questions: Record<string, Record<string, unknown>>;
|
|
690
|
+
timeoutMs?: number;
|
|
691
|
+
maxRetries?: number;
|
|
692
|
+
}): Promise<unknown>;
|
|
681
693
|
}
|
|
682
694
|
|
|
683
695
|
export interface ScriptStdlib {
|
|
@@ -60,6 +60,7 @@ declare module "swarm-extension" {
|
|
|
60
60
|
model?: string | undefined;
|
|
61
61
|
modelTier?: "smol" | "regular" | "smart" | "ultra" | undefined;
|
|
62
62
|
effort?: "off" | "low" | "medium" | "high" | "xhigh" | "max" | undefined;
|
|
63
|
+
profile?: string | undefined;
|
|
63
64
|
scheduleId?: string | undefined;
|
|
64
65
|
workflowRunId?: string | undefined;
|
|
65
66
|
workflowRunStepId?: string | undefined;
|
|
@@ -194,6 +195,8 @@ declare module "swarm-extension" {
|
|
|
194
195
|
modelSource?: string | undefined;
|
|
195
196
|
modelAlias?: string | undefined;
|
|
196
197
|
resolvedEffort?: "off" | "low" | "medium" | "high" | "xhigh" | "max" | undefined;
|
|
198
|
+
profile?: string | undefined;
|
|
199
|
+
resolvedProfile?: string | undefined;
|
|
197
200
|
scheduleId?: string | undefined;
|
|
198
201
|
workflowRunId?: string | null | undefined;
|
|
199
202
|
workflowRunStepId?: string | null | undefined;
|
|
@@ -311,6 +311,8 @@ declare module "swarm-sdk" {
|
|
|
311
311
|
key?: string;
|
|
312
312
|
includeSecrets?: boolean;
|
|
313
313
|
}): Promise<unknown>;
|
|
314
|
+
// --- model profiles ---
|
|
315
|
+
modelProfile_list(args?: { harness?: string }): Promise<unknown>;
|
|
314
316
|
// --- slack ---
|
|
315
317
|
slack_read(args?: {
|
|
316
318
|
inboxMessageId?: string;
|
|
@@ -660,6 +662,16 @@ declare module "swarm-sdk" {
|
|
|
660
662
|
// --- human input ---
|
|
661
663
|
request_humanInput(args: Record<string, unknown>): Promise<unknown>;
|
|
662
664
|
approval_cancel(args: { requestId: string; reason?: string }): Promise<unknown>;
|
|
665
|
+
|
|
666
|
+
// --- decisions ---
|
|
667
|
+
systemOne_decide(args: {
|
|
668
|
+
provider?: "typesafe" | "openrouter" | "laya" | "openai" | "cloudflare";
|
|
669
|
+
model?: string;
|
|
670
|
+
state: string | string[] | Record<string, unknown>;
|
|
671
|
+
questions: Record<string, Record<string, unknown>>;
|
|
672
|
+
timeoutMs?: number;
|
|
673
|
+
maxRetries?: number;
|
|
674
|
+
}): Promise<unknown>;
|
|
663
675
|
}
|
|
664
676
|
|
|
665
677
|
export interface ScriptStdlib {
|
package/src/server.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import pkg from "../package.json";
|
|
3
|
-
import { ensurePricingSeeded, ensureRbacSeeded } from "./be/boot-seeds";
|
|
3
|
+
import { ensureModelProfilesSeeded, ensurePricingSeeded, ensureRbacSeeded } from "./be/boot-seeds";
|
|
4
4
|
import { initDb } from "./be/db";
|
|
5
5
|
import { startPricingRefreshLoop } from "./be/pricing-refresh";
|
|
6
6
|
import { isSteeringEnabled } from "./be/steering";
|
|
@@ -81,6 +81,7 @@ import {
|
|
|
81
81
|
registerModelCatalogOverlayUpsertTool,
|
|
82
82
|
registerModelCatalogRefreshTool,
|
|
83
83
|
} from "./tools/model-catalog";
|
|
84
|
+
import { registerListModelProfilesTool } from "./tools/model-profiles/list-model-profiles";
|
|
84
85
|
import { registerMyAgentInfoTool } from "./tools/my-agent-info";
|
|
85
86
|
import { registerGetOauthAccessTokenTool } from "./tools/oauth-access-token";
|
|
86
87
|
import { registerPollTaskTool } from "./tools/poll-task";
|
|
@@ -176,6 +177,7 @@ import {
|
|
|
176
177
|
} from "./tools/swarm-config";
|
|
177
178
|
import { registerSwarmXTool } from "./tools/swarm-x";
|
|
178
179
|
// Task pool capability
|
|
180
|
+
import { registerSystemOneDecideTool } from "./tools/system-one-decide";
|
|
179
181
|
import { registerTaskActionTool } from "./tools/task-action";
|
|
180
182
|
// Tracker capability
|
|
181
183
|
import {
|
|
@@ -414,6 +416,11 @@ export async function createServer(
|
|
|
414
416
|
// manual-override constants for runtime-fee / ACU pricing.
|
|
415
417
|
ensurePricingSeeded();
|
|
416
418
|
startPricingRefreshLoop();
|
|
419
|
+
try {
|
|
420
|
+
await ensureModelProfilesSeeded();
|
|
421
|
+
} catch (err) {
|
|
422
|
+
console.error("[server] Failed to seed model profiles:", err);
|
|
423
|
+
}
|
|
417
424
|
|
|
418
425
|
try {
|
|
419
426
|
ensureRbacSeeded();
|
|
@@ -477,6 +484,7 @@ export async function createServer(
|
|
|
477
484
|
registerGetTasksTool(server);
|
|
478
485
|
registerGetMetricsTool(server);
|
|
479
486
|
registerSendTaskTool(server);
|
|
487
|
+
registerListModelProfilesTool(server);
|
|
480
488
|
registerGetTaskDetailsTool(server);
|
|
481
489
|
registerStoreProgressTool(server);
|
|
482
490
|
registerMyAgentInfoTool(server);
|
|
@@ -611,6 +619,7 @@ export async function createServer(
|
|
|
611
619
|
registerGetWorkflowRunTool(server);
|
|
612
620
|
registerRetryWorkflowRunTool(server);
|
|
613
621
|
registerCancelWorkflowRunTool(server);
|
|
622
|
+
registerSystemOneDecideTool(server);
|
|
614
623
|
registerRequestHumanInputTool(server);
|
|
615
624
|
registerCancelApprovalRequestTool(server);
|
|
616
625
|
}
|
package/src/slack/blocks.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import type { AgentTaskStatus, TaskAttachment } from "../types";
|
|
9
9
|
import { getAppUrl } from "../utils/constants";
|
|
10
10
|
import { taskAttachmentDisplayUrl } from "../utils/task-attachment-links";
|
|
11
|
+
import { completedStatusIcon } from "./reaction-shortcode";
|
|
11
12
|
|
|
12
13
|
// Slack limits section text to 3000 chars; we use 2900 for safety.
|
|
13
14
|
export const MAX_SECTION_LENGTH = 2900;
|
|
@@ -294,7 +295,7 @@ export function buildCompletedBlocks(opts: {
|
|
|
294
295
|
trailer?: string;
|
|
295
296
|
}): SlackBlock[] {
|
|
296
297
|
const taskLink = getTaskLink(opts.taskId);
|
|
297
|
-
let line =
|
|
298
|
+
let line = `${completedStatusIcon()} *${opts.agentName}* (${taskLink})`;
|
|
298
299
|
if (opts.duration) line += ` · ${opts.duration}`;
|
|
299
300
|
|
|
300
301
|
const blocks: SlackBlock[] = [sectionBlock(line)];
|
|
@@ -479,7 +480,8 @@ export function isTreeOutputTruncated(text: string): boolean {
|
|
|
479
480
|
* Render a single node line: icon + bold name + task link + optional duration.
|
|
480
481
|
*/
|
|
481
482
|
function renderNodeLine(node: TreeNode): string {
|
|
482
|
-
const icon =
|
|
483
|
+
const icon =
|
|
484
|
+
node.status === "completed" ? completedStatusIcon() : (STATUS_ICON[node.status] ?? "•");
|
|
483
485
|
const taskLink = getTaskLink(node.taskId);
|
|
484
486
|
let line = `${icon} *${node.agentName}* (${taskLink})`;
|
|
485
487
|
if (node.duration) line += ` · ${node.duration}`;
|
|
@@ -43,3 +43,17 @@ export function reactionName(event: SlackReactionEvent): string {
|
|
|
43
43
|
const normalized = normalizeSlackReactionShortcode(raw);
|
|
44
44
|
return normalized ?? SLACK_REACTION_DEFAULTS[event];
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
export const SLACK_STATUS_ICON_COMPLETED_KEY = "SLACK_STATUS_ICON_COMPLETED";
|
|
48
|
+
export const SLACK_STATUS_ICON_COMPLETED_DEFAULT = "✅";
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The "done" glyph in thread text (task tree, thread header, outcome cards).
|
|
52
|
+
* Read per call so a global config write, which reloads `process.env`, takes
|
|
53
|
+
* effect without a restart. Unlike a reaction there is no `invalid_name`
|
|
54
|
+
* fallback in text: a name the workspace lacks renders as literal `:name:`.
|
|
55
|
+
*/
|
|
56
|
+
export function completedStatusIcon(): string {
|
|
57
|
+
const normalized = normalizeSlackReactionShortcode(process.env[SLACK_STATUS_ICON_COMPLETED_KEY]);
|
|
58
|
+
return normalized ? `:${normalized}:` : SLACK_STATUS_ICON_COMPLETED_DEFAULT;
|
|
59
|
+
}
|