@desplega.ai/agent-swarm 2.6.0 → 2.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/{acp-adapter-dgfakfpw.js → acp-adapter-8dn5tq56.js} +6 -6
- package/dist/{actions-fbqma8aa.js → actions-3r6gw7dy.js} +18 -18
- package/dist/{amp-adapter-en0jy6hs.js → amp-adapter-wjtq413p.js} +3 -3
- package/dist/{anthropic-messages-txs190dd.js → anthropic-messages-n4cby1rz.js} +4 -4
- package/dist/{app-k8a5cgpp.js → app-d53yhykk.js} +3 -3
- package/dist/{assistant-a16433gr.js → assistant-ksrawj3k.js} +20 -20
- package/dist/{boot-reembed-6c1sjfnr.js → boot-reembed-fxfw17m2.js} +11 -3
- package/dist/{boot-reembed-f4nn561n.js → boot-reembed-k5y5xbj0.js} +15 -8
- package/dist/{boot-scrub-sweep-d4w9gajf.js → boot-scrub-sweep-se41j9fd.js} +3 -3
- package/dist/{boot-seeds-9bmkspyw.js → boot-seeds-r3kwwv8b.js} +4 -4
- package/dist/{claude-adapter-nys1kjxa.js → claude-adapter-ahmyf231.js} +8 -8
- package/dist/{claude-managed-adapter-55207973.js → claude-managed-adapter-5knamegc.js} +3 -3
- package/dist/{claude-managed-setup-3cm35gey.js → claude-managed-setup-mdc7dy6b.js} +3 -3
- package/dist/{claude-sdk-session-nw5yd8cp.js → claude-sdk-session-n5wen9zk.js} +8729 -8600
- package/dist/{cli-683y0b3a.js → cli-0dqtv2jg.js} +2 -2
- package/dist/{cli-a34pr3w1.js → cli-0tww7dnz.js} +2 -2
- package/dist/{cli-w2656ka1.js → cli-0w4e4v58.js} +7 -7
- package/dist/{cli-5sckpqcx.js → cli-2sjm3t4r.js} +1 -1
- package/dist/{cli-gf2850q7.js → cli-3cywb44q.js} +18 -1
- package/dist/{cli-8wkb09q5.js → cli-3k9jej0c.js} +2 -2
- package/dist/{cli-3aqd4jaa.js → cli-3v2cyqb8.js} +3 -3
- package/dist/{cli-emhjcj2y.js → cli-5qt9vzmq.js} +89 -20
- package/dist/{cli-abkm71vr.js → cli-5rp1w4cr.js} +1 -1
- package/dist/{cli-gf95q3bm.js → cli-6vwjsetx.js} +4 -4
- package/dist/{cli-k21gx5ht.js → cli-6w2yrrc0.js} +10 -10
- package/dist/{cli-m70bajzr.js → cli-75tdydgv.js} +1 -1
- package/dist/{cli-5rj83bam.js → cli-8feswf2v.js} +47 -36
- package/dist/{cli-sxgtf5fw.js → cli-8wd3mbyj.js} +1 -1
- package/dist/{cli-ca6banwz.js → cli-9csfthbw.js} +10 -8
- package/dist/{cli-dqygeeh5.js → cli-b41p764a.js} +31 -3
- package/dist/{cli-m5a9m0ak.js → cli-c1t533hh.js} +3 -3
- package/dist/{cli-2sjhpsns.js → cli-c28jxpcv.js} +1 -1
- package/dist/{cli-bqztfyve.js → cli-cb50g9cd.js} +1 -1
- package/dist/{cli-ag2yrnt5.js → cli-d8gaphtr.js} +2 -2
- package/dist/{cli-9abdgn5t.js → cli-dgtfysne.js} +3 -3
- package/dist/{cli-nq677csy.js → cli-dscnhn18.js} +1 -1
- package/dist/{cli-3fwqy71j.js → cli-dv11pgpv.js} +39 -32
- package/dist/{cli-v49fkghp.js → cli-ekptgk2m.js} +6 -6
- package/dist/{cli-c2habvsf.js → cli-f3mzy662.js} +17 -17
- package/dist/{cli-zqne1qcj.js → cli-fk7k8s9x.js} +2 -2
- package/dist/{cli-1tem6ns9.js → cli-gt4h8cxw.js} +3 -2
- package/dist/{cli-vbhgkw9h.js → cli-h3e7bhvn.js} +3 -3
- package/dist/{cli-ffvsj2qw.js → cli-hkxqdmnb.js} +3 -3
- package/dist/{cli-a2rr0fmg.js → cli-hpkvgdad.js} +7 -4
- package/dist/{cli-art7c1zh.js → cli-hq2v4rr5.js} +37 -9
- package/dist/{cli-sqma6vw1.js → cli-k1zb7raw.js} +5 -5
- package/dist/{cli-4rvr7zfx.js → cli-k4awgvzm.js} +5 -0
- package/dist/{cli-3ndmpxxv.js → cli-k7d3hvya.js} +2 -2
- package/dist/{cli-3gfvmzht.js → cli-kbtdftbw.js} +74 -26
- package/dist/{cli-hfeb68z4.js → cli-kj69b9tw.js} +34 -6
- package/dist/{cli-az25fn5f.js → cli-kk9jy4nm.js} +2 -2
- package/dist/{cli-39aet655.js → cli-m52t4cta.js} +1 -1
- package/dist/{cli-cz37jqss.js → cli-mdadewqp.js} +5 -5
- package/dist/{cli-esakngh8.js → cli-nf6k60sn.js} +1 -1
- package/dist/{cli-j6rktc91.js → cli-nzdf09js.js} +1 -1
- package/dist/{cli-rr7zx19j.js → cli-prcyzecg.js} +12 -12
- package/dist/{cli-4hxrf0p4.js → cli-pzafyqtk.js} +393 -62
- package/dist/{cli-g8ka5j4j.js → cli-qvaa1gjp.js} +173 -52
- package/dist/{cli-d2dpf95n.js → cli-rg4b2g6j.js} +1 -1
- package/dist/{cli-kf4jree9.js → cli-rm66vqdz.js} +1 -1
- package/dist/{cli-4rf0f5vg.js → cli-s504c3w7.js} +5 -3
- package/dist/{cli-nn6vx2mq.js → cli-swkx9jm2.js} +7 -5
- package/dist/{cli-g20jjwt4.js → cli-thsaq4pd.js} +16 -16
- package/dist/{cli-aj8djyqt.js → cli-twjnyvs8.js} +2 -2
- package/dist/{cli-03gg9pd1.js → cli-typdpe8g.js} +4 -4
- package/dist/{cli-mtnj4fwa.js → cli-vg8pwmsd.js} +6 -6
- package/dist/{cli-gnsb1zxa.js → cli-vhpckzvp.js} +1 -1
- package/dist/{cli-7enjwya9.js → cli-vz78s5wm.js} +1 -1
- package/dist/{cli-dv97kdzv.js → cli-wdxgsdz5.js} +2 -2
- package/dist/{cli-7a6p4e65.js → cli-wtjg6je3.js} +1 -1
- package/dist/{cli-6y51ryva.js → cli-x3cngb1h.js} +44 -12
- package/dist/{cli-n7asmce6.js → cli-xbz6mcg4.js} +3 -3
- package/dist/{cli-24j1jeeb.js → cli-y4tdxsqb.js} +3 -3
- package/dist/{cli-mvjz5an3.js → cli-yqrsp3ny.js} +7 -7
- package/dist/cli.js +31 -21
- package/dist/{codex-adapter-d95q4qsn.js → codex-adapter-7wepzq15.js} +6 -6
- package/dist/{codex-hook-brpnycn5.js → codex-hook-pbdh4xhx.js} +2 -2
- package/dist/{codex-session-runner-gdwd8h0c.js → codex-session-runner-02sb49e4.js} +6 -6
- package/dist/{commands-jqmgk54b.js → commands-3rcspxct.js} +2 -2
- package/dist/{controller-z3jq589f.js → controller-8ar4avd6.js} +3 -3
- package/dist/{controllers-connect-r2za39ef.js → controllers-connect-ajvh9h0v.js} +19 -19
- package/dist/{cursor-adapter-4mr4jgkw.js → cursor-adapter-gd0sa7bh.js} +1 -1
- package/dist/{db-w36wwb5g.js → db-dv9j7ts5.js} +2 -2
- package/dist/{db-ppv2tze7.js → db-z26vgdvj.js} +3 -3
- package/dist/{dispatcher-pv5h77vf.js → dispatcher-8m0htjq8.js} +17 -17
- package/dist/{dsh-adapter-d4pdpb3r.js → dsh-adapter-y8gyk130.js} +1 -1
- package/dist/{e2b-fn3zacvt.js → e2b-74vg9b7e.js} +1 -1
- package/dist/{e2b-222hsajc.js → e2b-jyay8wzn.js} +1 -1
- package/dist/{easter-egg-3d-4sp4dvmc.js → easter-egg-3d-28f880xx.js} +1 -1
- package/dist/{execute-jev839y1.js → execute-9seb79vh.js} +1 -1
- package/dist/{external-channel-y6nkzhnn.js → external-channel-smz7jyth.js} +2 -2
- package/dist/{getMachineId-bsd-brn0xym3.js → getMachineId-bsd-whfre6gr.js} +3 -3
- package/dist/{getMachineId-darwin-t339sfxm.js → getMachineId-darwin-5zjynb19.js} +3 -3
- package/dist/{getMachineId-linux-5eq95d8b.js → getMachineId-linux-8mksrrsq.js} +3 -3
- package/dist/{getMachineId-unsupported-qavrtc7j.js → getMachineId-unsupported-fxtqsdt2.js} +3 -3
- package/dist/{getMachineId-win-q7fe1wx7.js → getMachineId-win-nzfmz857.js} +3 -3
- package/dist/{grok-adapter-t08b9ntm.js → grok-adapter-z0nnvtbn.js} +7 -7
- package/dist/handlers-pspryh6g.js +41 -0
- package/dist/{heartbeat-vn31xwdk.js → heartbeat-f928aqyp.js} +20 -20
- package/dist/{hook-cpjs8q51.js → hook-d7c605r4.js} +7 -7
- package/dist/{hook-6d0zbvvt.js → hook-q9hv5xf9.js} +5 -5
- package/dist/{http-9c6medda.js → http-q6j4k989.js} +1000 -251
- package/dist/{index-k0yt1tvq.js → index-5nb266jr.js} +17 -17
- package/dist/{index-ysd45kgc.js → index-8fxt5eah.js} +10 -10
- package/dist/{index-zgvqzjsw.js → index-cekzwhez.js} +5 -5
- package/dist/{index-38t35h0n.js → index-ch0c1xff.js} +17 -17
- package/dist/{index-pkma1yf8.js → index-ebnjhxh1.js} +1 -1
- package/dist/{index-yqgbfgaj.js → index-ed8dy8rn.js} +20 -20
- package/dist/{index-qhjf2htm.js → index-ejqdkkqx.js} +262 -96
- package/dist/{index-dr94zbnh.js → index-farm1zsn.js} +32661 -29443
- package/dist/{index-g6k9tctj.js → index-yn1yxg85.js} +1 -1
- package/dist/{keepalive-em0117bp.js → keepalive-zzc3hks1.js} +3 -3
- package/dist/lead-c0pjcaw4.js +79 -0
- package/dist/{maintenance-kbme3qy9.js → maintenance-kqsxzpzx.js} +1 -1
- package/dist/{oauth-refresh-sweep-xysj4yse.js → oauth-refresh-sweep-7qt0g55z.js} +8 -3
- package/dist/{onboard-p6q0vz97.js → onboard-k7x13fqg.js} +121 -8
- package/dist/{opencode-adapter-esv3k9yn.js → opencode-adapter-tqvrtter.js} +5 -5
- package/dist/{otel-impl-54qhqh1z.js → otel-impl-sqkf6k3b.js} +97 -97
- package/dist/{pi-mono-adapter-mymbwhj0.js → pi-mono-adapter-m2qnafxm.js} +89 -14
- package/dist/{pricing-refresh-yb75wv33.js → pricing-refresh-wkpwhgvs.js} +3 -3
- package/dist/{rbac-roles-dexhc9x8.js → rbac-roles-dpn0c0wj.js} +7 -4
- package/dist/{rbac-roles-1vp2mw4p.js → rbac-roles-wk7xd6a6.js} +2 -2
- package/dist/{render-v2-tjay5hhf.js → render-v2-ex2m4g2k.js} +3 -3
- package/dist/{resume-8ayf37g5.js → resume-725g9b0b.js} +7 -7
- package/dist/{runtime-hqe5cqcd.js → runtime-kajas15g.js} +1 -1
- package/dist/{session-access-fdhp987s.js → session-access-0j2ezjje.js} +8 -4
- package/dist/{session-source-backfill-qgtc86z1.js → session-source-backfill-62202awe.js} +2 -2
- package/dist/{setup-4rrmqcq3.js → setup-9sj11ax6.js} +1 -1
- package/dist/{virtual-modules-52km00d6.js → virtual-modules-c6fcq6zr.js} +2 -2
- package/dist/{work-objects-p28fgd70.js → work-objects-0y07495y.js} +20 -20
- package/dist/worker-d4q9sqbc.js +79 -0
- package/openapi.json +1536 -407
- package/package.json +7 -5
- package/src/be/automation-preflight.ts +2 -0
- package/src/be/connector-codes.ts +65 -22
- package/src/be/db/api-keys.ts +9 -0
- package/src/be/db/workflow-runs.ts +36 -5
- package/src/be/db.ts +80 -22
- package/src/be/memory/boot-reembed.ts +4 -1
- package/src/be/memory/constants.ts +6 -0
- package/src/be/memory/embedding-recovery.ts +48 -0
- package/src/be/memory/index-content.ts +5 -2
- package/src/be/memory/providers/openai-embedding.ts +65 -14
- package/src/be/memory/task-learning.ts +257 -0
- package/src/be/memory/types.ts +13 -2
- package/src/be/migrations/245_script_executor.sql +3 -0
- package/src/be/migrations/246_connector_code_kind.sql +11 -0
- package/src/be/oauth-refresh-sweep.ts +2 -1
- package/src/be/onboarding.ts +125 -15
- package/src/be/rbac-roles.ts +4 -1
- package/src/be/scripts/boot-reembed.ts +2 -0
- package/src/be/scripts/db.ts +14 -4
- package/src/be/scripts/embeddings.ts +4 -2
- package/src/be/scripts/run-saved.ts +1 -0
- package/src/be/scripts/typecheck.ts +9 -3
- package/src/be/seed/agent-fs-provision.ts +75 -14
- package/src/be/seed-skills/index.ts +7 -0
- package/src/be/swarm-config-guard.ts +3 -0
- package/src/cli.tsx +9 -0
- package/src/comb/config.ts +6 -1
- package/src/commands/onboard/agent-fs-version.ts +5 -0
- package/src/commands/onboard/compose-generator.ts +117 -4
- package/src/commands/onboard/env-generator.ts +9 -0
- package/src/commands/onboard/tei-image.ts +12 -0
- package/src/commands/onboard/types.ts +7 -0
- package/src/commands/onboard.tsx +11 -1
- package/src/commands/provider-credentials.ts +2 -1
- package/src/commands/runner.ts +14 -1
- package/src/heartbeat/heartbeat.ts +14 -1
- package/src/heartbeat/script-runner-probe.ts +47 -0
- package/src/http/agentfs-proxy.ts +369 -0
- package/src/http/all-routes.ts +1 -0
- package/src/http/api-keys.ts +24 -9
- package/src/http/codex-oauth-device.ts +8 -1
- package/src/http/fs.ts +64 -10
- package/src/http/idempotency-key.ts +1 -1
- package/src/http/inbox-state.ts +27 -1
- package/src/http/index.ts +7 -4
- package/src/http/memory.ts +7 -4
- package/src/http/onboarding.ts +139 -11
- package/src/http/pages-public.ts +38 -5
- package/src/http/scripts.ts +27 -0
- package/src/http/session-access.ts +24 -0
- package/src/http/sessions.ts +10 -6
- package/src/http/tasks.ts +85 -11
- package/src/http/telemetry-ui.ts +1 -1
- package/src/http/user-roles.ts +4 -1
- package/src/http/users.ts +272 -3
- package/src/http/utils.ts +22 -3
- package/src/http/workflows.ts +10 -0
- package/src/http/x.ts +5 -0
- package/src/prompts/base-prompt.ts +45 -10
- package/src/prompts/session-templates.ts +43 -5
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +6 -1
- package/src/providers/cursor-adapter.ts +1 -1
- package/src/providers/dsh-adapter.ts +1 -1
- package/src/providers/pi-mono-adapter.ts +99 -14
- package/src/rbac/legacy-policy.ts +1 -0
- package/src/rbac/permissions.ts +4 -0
- package/src/scheduler/scheduler.ts +2 -1
- package/src/scripts-runtime/eval-harness.ts +8 -1
- package/src/scripts-runtime/executors/native.ts +22 -16
- package/src/scripts-runtime/executors/quickjs-prelude.ts +103 -1
- package/src/scripts-runtime/executors/quickjs-runner.ts +2 -1
- package/src/scripts-runtime/executors/quickjs.ts +7 -0
- package/src/scripts-runtime/executors/registry.ts +32 -0
- package/src/scripts-runtime/executors/types.ts +7 -0
- package/src/scripts-runtime/host-validated-args.ts +133 -0
- package/src/scripts-runtime/loader.ts +30 -3
- package/src/scripts-runtime/swarm-sdk.ts +9 -2
- package/src/scripts-runtime/types/stdlib.d.ts +9 -1
- package/src/scripts-runtime/types/swarm-sdk.d.ts +9 -1
- package/src/tests/agent-fs-provision-seeder.test.ts +261 -92
- package/src/tests/agentfs-proxy.test.ts +1106 -0
- package/src/tests/api-key-status-by-suffix.test.ts +242 -0
- package/src/tests/base-prompt.test.ts +139 -4
- package/src/tests/codex-adapter.test.ts +46 -8
- package/src/tests/codex-oauth-device.test.ts +12 -1
- package/src/tests/comb-config-guard.test.ts +20 -0
- package/src/tests/comb-links.test.ts +79 -1
- package/src/tests/comb-status.test.ts +44 -0
- package/src/tests/community-linear-delivery-loop.test.ts +2099 -0
- package/src/tests/credential-check.test.ts +4 -2
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/docs-index.test.ts +135 -0
- package/src/tests/dsh-adapter.test.ts +1 -1
- package/src/tests/fetch-resolved-env.test.ts +16 -2
- package/src/tests/fixtures/onboard/baseline-states.ts +68 -0
- package/src/tests/fixtures/onboard/content.compose.yml.txt +190 -0
- package/src/tests/fixtures/onboard/content.env.txt +29 -0
- package/src/tests/fixtures/onboard/dev.compose.yml.txt +155 -0
- package/src/tests/fixtures/onboard/dev.env.txt +28 -0
- package/src/tests/fixtures/onboard/full-all.compose.yml.txt +582 -0
- package/src/tests/fixtures/onboard/full-all.env.txt +59 -0
- package/src/tests/fixtures/onboard/full.compose.yml.txt +435 -0
- package/src/tests/fixtures/onboard/full.env.txt +36 -0
- package/src/tests/fixtures/onboard/research.compose.yml.txt +155 -0
- package/src/tests/fixtures/onboard/research.env.txt +28 -0
- package/src/tests/fixtures/onboard/solo.compose.yml.txt +85 -0
- package/src/tests/fixtures/onboard/solo.env.txt +26 -0
- package/src/tests/heartbeat-checklist.test.ts +89 -0
- package/src/tests/heartbeat-script-runner-probe.test.ts +83 -0
- package/src/tests/http-log-scrubbing.test.ts +34 -1
- package/src/tests/http-task-write-ownership.test.ts +5 -1
- package/src/tests/invites-http.test.ts +464 -0
- package/src/tests/memory-embedding-input-cap.test.ts +122 -1
- package/src/tests/memory-embedding-recovery.test.ts +137 -0
- package/src/tests/oauth-refresh-sweep.test.ts +34 -1
- package/src/tests/onboard-compose.test.ts +162 -0
- package/src/tests/onboard-env.test.ts +24 -0
- package/src/tests/onboard-non-interactive.test.ts +29 -0
- package/src/tests/onboard-tei-image.test.ts +36 -0
- package/src/tests/onboarding.test.ts +451 -4
- package/src/tests/openai-embedding-provider.test.ts +105 -2
- package/src/tests/opencode-adapter.test.ts +2 -1
- package/src/tests/pages-frame-ancestors.test.ts +162 -0
- package/src/tests/pi-codemode.test.ts +2 -1
- package/src/tests/pi-mono-adapter.test.ts +54 -0
- package/src/tests/pi-mono-session-cost-on-abort.test.ts +270 -0
- package/src/tests/pi-native-mcp.test.ts +13 -1
- package/src/tests/pi-tool-exposure.test.ts +8 -8
- package/src/tests/preload-env.test.ts +4 -0
- package/src/tests/preload.ts +4 -0
- package/src/tests/probe-tool-schema-tokens.test.ts +144 -0
- package/src/tests/prompt-template-session.test.ts +20 -3
- package/src/tests/rbac-admission-e2e.test.ts +22 -6
- package/src/tests/rbac-admission.test.ts +342 -4
- package/src/tests/rbac-engine.test.ts +1 -0
- package/src/tests/rbac-roles.test.ts +9 -1
- package/src/tests/request-composition-derive.test.ts +329 -0
- package/src/tests/script-connections.test.ts +2 -0
- package/src/tests/script-executor-conformance.test.ts +36 -1
- package/src/tests/script-executor-quickjs-pool.test.ts +50 -0
- package/src/tests/script-executor-quickjs.test.ts +140 -0
- package/src/tests/script-host-validated-args-native.test.ts +196 -0
- package/src/tests/script-per-script-executor.test.ts +415 -0
- package/src/tests/scripts-external-api.test.ts +45 -0
- package/src/tests/sdk-allowlist.test.ts +3 -2
- package/src/tests/self-improvement.test.ts +4 -4
- package/src/tests/send-task-requester-gate.test.ts +64 -3
- package/src/tests/sessions-move.test.ts +39 -1
- package/src/tests/store-progress-learning.test.ts +381 -0
- package/src/tests/ui-logs-parser.test.ts +180 -0
- package/src/tests/whoami-route.test.ts +41 -0
- package/src/tests/workflow-engine-v2.test.ts +116 -1
- package/src/tests/workflow-mcp-trigger-schema.test.ts +209 -0
- package/src/tests/workflow-run-finished-event.test.ts +263 -0
- package/src/tests/workflow-swarm-script.test.ts +34 -0
- package/src/tools/inject-learning.ts +1 -1
- package/src/tools/memory-edit.ts +1 -1
- package/src/tools/memory-search.ts +1 -1
- package/src/tools/script-common.ts +1 -0
- package/src/tools/script-run.ts +7 -0
- package/src/tools/script-upsert.ts +7 -0
- package/src/tools/store-progress.ts +71 -1
- package/src/tools/utils.ts +12 -1
- package/src/tools/workflows/trigger-workflow.ts +104 -10
- package/src/types.ts +12 -1
- package/src/utils/constants.ts +39 -0
- package/src/utils/credentials.ts +5 -2
- package/src/utils/task-attachment-links.ts +15 -3
- package/src/workflows/engine.ts +18 -16
- package/src/workflows/executors/swarm-script.ts +5 -0
- package/src/workflows/foreach-driver.ts +5 -11
- package/src/workflows/node-timeout.ts +30 -0
- package/templates/community/linear-delivery-loop/PLAYBOOK.md +323 -0
- package/templates/community/linear-delivery-loop/README.md +38 -0
- package/templates/community/linear-delivery-loop/config.example.json +31 -0
- package/templates/community/linear-delivery-loop/lead-prompt.md +34 -0
- package/templates/community/linear-delivery-loop/schedule.json +10 -0
- package/templates/community/linear-delivery-loop/scripts/linear-delivery-setup.ts +148 -0
- package/templates/community/linear-delivery-loop/scripts/linear-delivery-tick.ts +1781 -0
- package/templates/skills/swarm-docs/config.json +14 -0
- package/templates/skills/swarm-docs/content.md +144 -0
- package/dist/handlers-eqsnmv95.js +0 -41
- package/dist/lead-23e7z59t.js +0 -79
- package/dist/worker-e3w7vye1.js +0 -79
- package/dist/{azure-openai-responses-9kfwhmn7.js → azure-openai-responses-43asa23s.js} +0 -0
- package/dist/{node-vxx896fa.js → node-t28cc56b.js} +0 -0
- package/dist/{openai-codex-responses-79s8xnbh.js → openai-codex-responses-jhr3sjtm.js} +3 -3
- package/dist/{openai-completions-tdbyts0x.js → openai-completions-gs6yn37k.js} +3 -3
- /package/dist/{openai-responses-dm3t5vqp.js → openai-responses-jqm9vwwk.js} +0 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import {
|
|
6
|
+
bareToolName,
|
|
7
|
+
controlReached,
|
|
8
|
+
countSwarmTools,
|
|
9
|
+
hasControlResult,
|
|
10
|
+
inputTokens,
|
|
11
|
+
toolNames,
|
|
12
|
+
turnOne,
|
|
13
|
+
withSessionDirs,
|
|
14
|
+
} from "../../scripts/probe-tool-schema-tokens";
|
|
15
|
+
|
|
16
|
+
describe("probe-tool-schema-tokens", () => {
|
|
17
|
+
let dir = "";
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
if (dir) rmSync(dir, { recursive: true, force: true });
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("matches swarm tools under every harness naming", () => {
|
|
23
|
+
const swarm = new Set(["get_swarm", "store_progress"]);
|
|
24
|
+
expect(bareToolName("mcp__agent_swarm__get_swarm")).toBe("get_swarm");
|
|
25
|
+
expect(bareToolName("agent-swarm_store-progress")).toBe("store_progress");
|
|
26
|
+
expect(
|
|
27
|
+
countSwarmTools(["get-swarm", "mcp__agent_swarm__store_progress", "bash", "read"], swarm),
|
|
28
|
+
).toBe(2);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("lists tools in chat-completions and namespaced responses bodies", () => {
|
|
32
|
+
expect(toolNames({ tools: [{ type: "function", function: { name: "get-swarm" } }] })).toEqual([
|
|
33
|
+
"get-swarm",
|
|
34
|
+
]);
|
|
35
|
+
const responses = {
|
|
36
|
+
input: [
|
|
37
|
+
{
|
|
38
|
+
type: "additional_tools",
|
|
39
|
+
tools: [{ type: "namespace", name: "functions", tools: [{ name: "exec" }] }],
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
tools: [{ name: "wait" }],
|
|
43
|
+
};
|
|
44
|
+
expect(toolNames(responses).sort()).toEqual(["exec", "wait"]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("reads the last reported input token count from a stream", () => {
|
|
48
|
+
const sse = 'data: {"usage":null}\n\ndata: {"usage":{"prompt_tokens":90776}}\n';
|
|
49
|
+
expect(inputTokens(sse)).toBe(90776);
|
|
50
|
+
expect(inputTokens('{"response":{"usage":{"input_tokens":16629}}}')).toBe(16629);
|
|
51
|
+
expect(inputTokens("data: [DONE]")).toBeNull();
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("turn one skips side requests for other models", () => {
|
|
55
|
+
dir = mkdtempSync(join(tmpdir(), "probe-tool-schema-"));
|
|
56
|
+
writeFileSync(join(dir, "req-1.json"), JSON.stringify({ model: "openai/gpt-6-luna" }));
|
|
57
|
+
writeFileSync(join(dir, "res-1.txt"), '{"usage":{"prompt_tokens":453}}');
|
|
58
|
+
writeFileSync(
|
|
59
|
+
join(dir, "req-2.json"),
|
|
60
|
+
JSON.stringify({ model: "deepseek/deepseek-v4.1-flash", tools: [{ name: "bash" }] }),
|
|
61
|
+
);
|
|
62
|
+
writeFileSync(join(dir, "res-2.txt"), '{"usage":{"prompt_tokens":33115}}');
|
|
63
|
+
expect(turnOne(dir, "openrouter/deepseek/deepseek-v4.1-flash")).toEqual({
|
|
64
|
+
tokens: 33115,
|
|
65
|
+
names: ["bash"],
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const found = "Found 5 agent(s) in the swarm. Requested by session: s1";
|
|
70
|
+
|
|
71
|
+
test("control passes only on a get-swarm tool output", () => {
|
|
72
|
+
dir = mkdtempSync(join(tmpdir(), "probe-tool-schema-"));
|
|
73
|
+
// The schema alone carries "in the swarm" in get-swarm's description.
|
|
74
|
+
const schemaOnly = {
|
|
75
|
+
model: "m",
|
|
76
|
+
tools: [
|
|
77
|
+
{
|
|
78
|
+
type: "function",
|
|
79
|
+
function: { name: "agent-swarm_get-swarm", description: "Lists agents in the swarm." },
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
messages: [{ role: "user", content: "Call get-swarm. Found 5 agent(s) in the swarm." }],
|
|
83
|
+
};
|
|
84
|
+
expect(hasControlResult(schemaOnly)).toBe(false);
|
|
85
|
+
writeFileSync(join(dir, "req-1.json"), JSON.stringify(schemaOnly));
|
|
86
|
+
expect(controlReached(dir)).toBe(false);
|
|
87
|
+
|
|
88
|
+
const chat = {
|
|
89
|
+
...schemaOnly,
|
|
90
|
+
messages: [
|
|
91
|
+
{
|
|
92
|
+
role: "assistant",
|
|
93
|
+
tool_calls: [{ id: "c1", type: "function", function: { name: "agent-swarm_get-swarm" } }],
|
|
94
|
+
},
|
|
95
|
+
{ role: "tool", tool_call_id: "c1", content: [{ type: "text", text: found }] },
|
|
96
|
+
],
|
|
97
|
+
};
|
|
98
|
+
expect(hasControlResult(chat)).toBe(true);
|
|
99
|
+
writeFileSync(join(dir, "req-2.json"), JSON.stringify(chat));
|
|
100
|
+
expect(controlReached(dir)).toBe(true);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("control reads Responses items and rejects other tools and failed calls", () => {
|
|
104
|
+
const responses = (name: string, output: string) => ({
|
|
105
|
+
input: [
|
|
106
|
+
{ type: "function_call", name, call_id: "call_1", arguments: "{}" },
|
|
107
|
+
{ type: "function_call_output", call_id: "call_1", output },
|
|
108
|
+
],
|
|
109
|
+
});
|
|
110
|
+
expect(hasControlResult(responses("mcp__agent_swarm__get_swarm", found))).toBe(true);
|
|
111
|
+
expect(hasControlResult(responses("mcp__agent_swarm__get_tasks", found))).toBe(false);
|
|
112
|
+
expect(hasControlResult(responses("mcp__agent_swarm__get_swarm", "Agent ID required."))).toBe(
|
|
113
|
+
false,
|
|
114
|
+
);
|
|
115
|
+
// An output for a different call id does not count.
|
|
116
|
+
const mismatched = responses("mcp__agent_swarm__get_swarm", found);
|
|
117
|
+
mismatched.input[1] = { type: "function_call_output", call_id: "call_2", output: found };
|
|
118
|
+
expect(hasControlResult(mismatched)).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("session dirs: home is removed on success and on error, captures stay", async () => {
|
|
122
|
+
dir = mkdtempSync(join(tmpdir(), "probe-tool-schema-"));
|
|
123
|
+
let home = "";
|
|
124
|
+
const capDir = await withSessionDirs(dir, "ok", async (cap, h) => {
|
|
125
|
+
home = h;
|
|
126
|
+
writeFileSync(join(h, "config.toml"), "x");
|
|
127
|
+
writeFileSync(join(cap, "req-1.json"), "{}");
|
|
128
|
+
return cap;
|
|
129
|
+
});
|
|
130
|
+
expect(existsSync(home)).toBe(false);
|
|
131
|
+
expect(existsSync(join(capDir, "req-1.json"))).toBe(true);
|
|
132
|
+
|
|
133
|
+
await expect(
|
|
134
|
+
withSessionDirs(dir, "boom", async (cap, h) => {
|
|
135
|
+
home = h;
|
|
136
|
+
writeFileSync(join(h, "config.toml"), "x");
|
|
137
|
+
writeFileSync(join(cap, "req-1.json"), "{}");
|
|
138
|
+
throw new Error("codex exec exited 1");
|
|
139
|
+
}),
|
|
140
|
+
).rejects.toThrow("codex exec exited 1");
|
|
141
|
+
expect(existsSync(home)).toBe(false);
|
|
142
|
+
expect(existsSync(join(dir, "boom-cap", "req-1.json"))).toBe(true);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -25,6 +25,7 @@ const SYSTEM_TEMPLATES = [
|
|
|
25
25
|
"system.agent.context.bound_note",
|
|
26
26
|
"system.agent.cursor.first_message",
|
|
27
27
|
"system.agent.lead",
|
|
28
|
+
"system.agent.lead.docs",
|
|
28
29
|
"system.agent.memory",
|
|
29
30
|
"system.agent.memory.remote",
|
|
30
31
|
"system.agent.outputs",
|
|
@@ -40,6 +41,8 @@ const SYSTEM_TEMPLATES = [
|
|
|
40
41
|
"system.agent.tool_preload",
|
|
41
42
|
"system.agent.tool_discovery.search",
|
|
42
43
|
"system.agent.tool_discovery.amp",
|
|
44
|
+
"system.agent.tool_discovery.codex",
|
|
45
|
+
"system.agent.tool_discovery.opencode",
|
|
43
46
|
"system.agent.tool_discovery.direct",
|
|
44
47
|
"system.agent.tools_skills",
|
|
45
48
|
"system.agent.worker",
|
|
@@ -118,11 +121,11 @@ describe("Session templates: registration", () => {
|
|
|
118
121
|
expect(sessionSystemEventTypes("session")).toEqual([...SESSION_TEMPLATES].sort());
|
|
119
122
|
});
|
|
120
123
|
|
|
121
|
-
test("registers
|
|
124
|
+
test("registers 35 system and session templates in total", () => {
|
|
122
125
|
const all = getAllTemplateDefinitions();
|
|
123
126
|
const sessionSystem = all.filter((d) => d.category === "system" || d.category === "session");
|
|
124
|
-
//
|
|
125
|
-
expect(sessionSystem.length).toBe(
|
|
127
|
+
// 30 system blocks + 5 session composites.
|
|
128
|
+
expect(sessionSystem.length).toBe(35);
|
|
126
129
|
});
|
|
127
130
|
|
|
128
131
|
test("drops the v1 blocks that prompt v2 deleted", () => {
|
|
@@ -245,6 +248,20 @@ describe("Session templates: MUST pointers", () => {
|
|
|
245
248
|
expect(result.text).toContain("`implementing` skill");
|
|
246
249
|
expect(result.text).not.toContain("/researching");
|
|
247
250
|
});
|
|
251
|
+
|
|
252
|
+
test("the outputs block takes its file-link base from fileLinkBase", () => {
|
|
253
|
+
const definition = getAllTemplateDefinitions().find(
|
|
254
|
+
(d) => d.eventType === "system.agent.outputs",
|
|
255
|
+
);
|
|
256
|
+
expect(definition?.variables.map((v) => v.name)).toEqual(["fileLinkBase"]);
|
|
257
|
+
const result = resolveTemplate("system.agent.outputs", {
|
|
258
|
+
fileLinkBase: "http://localhost:5274",
|
|
259
|
+
});
|
|
260
|
+
expect(result.text).toContain(
|
|
261
|
+
"Give people the live app link `http://localhost:5274/file/~/<org>/<drive>/<path>`.",
|
|
262
|
+
);
|
|
263
|
+
expect(result.unresolved).toEqual([]);
|
|
264
|
+
});
|
|
248
265
|
});
|
|
249
266
|
|
|
250
267
|
// ============================================================================
|
|
@@ -247,12 +247,20 @@ describe("RBAC admission over real HTTP", () => {
|
|
|
247
247
|
|
|
248
248
|
rewriteUserToRequester(dbPath, userId);
|
|
249
249
|
|
|
250
|
-
|
|
250
|
+
// A requester creates its own task (`task.create.own`); a verb-less write
|
|
251
|
+
// stays operator-only.
|
|
252
|
+
const requesterTaskCreate = await api(server.base, "POST", "/api/tasks", {
|
|
251
253
|
bearer: userToken,
|
|
252
|
-
body: { task: "rbac admission
|
|
254
|
+
body: { task: "rbac admission requester task" },
|
|
253
255
|
});
|
|
254
|
-
expect(
|
|
255
|
-
|
|
256
|
+
expect(requesterTaskCreate.status).toBe(201);
|
|
257
|
+
|
|
258
|
+
const deniedDbQuery = await api(server.base, "POST", "/api/db-query", {
|
|
259
|
+
bearer: userToken,
|
|
260
|
+
body: { sql: "select 1" },
|
|
261
|
+
});
|
|
262
|
+
expect(deniedDbQuery.status).toBe(403);
|
|
263
|
+
expect(deniedDbQuery.body.error).toContain("admission: route has no permission verb");
|
|
256
264
|
|
|
257
265
|
// The ranking state carries the private claim: a requester cannot read it.
|
|
258
266
|
const deniedRanking = await api(server.base, "GET", "/api/ranking", { bearer: userToken });
|
|
@@ -378,8 +386,8 @@ describe("RBAC admission over real HTTP", () => {
|
|
|
378
386
|
"GET /api/ranking",
|
|
379
387
|
"GET /api/scripts/{id}/apis/{endpointId}/secret",
|
|
380
388
|
"PATCH /api/assets/app/{id}/key",
|
|
389
|
+
"POST /api/db-query",
|
|
381
390
|
"POST /api/scripts/{id}/apis",
|
|
382
|
-
"POST /api/tasks",
|
|
383
391
|
].every((resourceId) =>
|
|
384
392
|
userRows.some((row) => row.resourceId === resourceId && row.decision === "deny"),
|
|
385
393
|
) &&
|
|
@@ -410,9 +418,17 @@ describe("RBAC admission over real HTTP", () => {
|
|
|
410
418
|
"GET /api/ranking",
|
|
411
419
|
"GET /api/scripts/{id}/apis/{endpointId}/secret",
|
|
412
420
|
"PATCH /api/assets/app/{id}/key",
|
|
421
|
+
"POST /api/db-query",
|
|
413
422
|
"POST /api/scripts/{id}/apis",
|
|
414
|
-
"POST /api/tasks",
|
|
415
423
|
]);
|
|
424
|
+
expect(
|
|
425
|
+
userHttpRows.some(
|
|
426
|
+
(row) =>
|
|
427
|
+
row.resourceId === "POST /api/tasks" &&
|
|
428
|
+
row.verb === "task.create.own" &&
|
|
429
|
+
row.decision === "allow",
|
|
430
|
+
),
|
|
431
|
+
).toBe(true);
|
|
416
432
|
expect(
|
|
417
433
|
denyRows.some(
|
|
418
434
|
(row) =>
|
|
@@ -16,9 +16,15 @@ import {
|
|
|
16
16
|
import { closeDb, createAgent, createUser, getDbClient, initDb } from "../be/db";
|
|
17
17
|
import { flushAuditBuffer } from "../be/rbac-audit";
|
|
18
18
|
import { attachRole, detachRole, ensureRbacSeedsSynced } from "../be/rbac-roles";
|
|
19
|
+
import {
|
|
20
|
+
resetAgentFsProvisionFetchForTests,
|
|
21
|
+
setAgentFsProvisionFetchForTests,
|
|
22
|
+
} from "../be/seed/agent-fs-provision";
|
|
19
23
|
import { type IdentityActor, mintToken } from "../be/users";
|
|
20
24
|
import { handleCore } from "../http/core";
|
|
25
|
+
import { handleDbQuery } from "../http/db-query";
|
|
21
26
|
import { handleFs } from "../http/fs";
|
|
27
|
+
import { handleInboxState } from "../http/inbox-state";
|
|
22
28
|
import { handleMcpOAuth } from "../http/mcp-oauth";
|
|
23
29
|
import { handleRealtimeTicket } from "../http/realtime";
|
|
24
30
|
import { handleTasks } from "../http/tasks";
|
|
@@ -61,6 +67,8 @@ function createTestServer(): Server {
|
|
|
61
67
|
if (await handleCore(req, res, myAgentId, API_KEY)) return;
|
|
62
68
|
if (await handleTasks(req, res, pathSegments, queryParams, myAgentId)) return;
|
|
63
69
|
if (await handleFs(req, res, pathSegments, queryParams, myAgentId)) return;
|
|
70
|
+
if (await handleInboxState(req, res, pathSegments, queryParams)) return;
|
|
71
|
+
if (await handleDbQuery(req, res, pathSegments, queryParams)) return;
|
|
64
72
|
if (await handleMcpOAuth(req, res, pathSegments, queryParams)) return;
|
|
65
73
|
if (await handleRealtimeTicket(req, res, pathSegments, queryParams)) return;
|
|
66
74
|
|
|
@@ -426,14 +434,20 @@ describe("handleCore admission wiring", () => {
|
|
|
426
434
|
expect(await admissionAuditRows()).toEqual([]);
|
|
427
435
|
});
|
|
428
436
|
|
|
429
|
-
test("flag on requester grant
|
|
437
|
+
test("flag on requester grant admits declared verbs and reads, denies verb-less writes, and audits each decision", async () => {
|
|
430
438
|
process.env.RBAC_ENABLED = "true";
|
|
431
439
|
const { userId, plaintext } = await createTokenForUser("Requester User");
|
|
432
440
|
await narrowUserToRequester(userId);
|
|
433
441
|
|
|
434
|
-
const
|
|
442
|
+
const created = await api(port, "POST", "/api/tasks", {
|
|
443
|
+
bearer: plaintext,
|
|
444
|
+
body: { task: "requester creates its own task" },
|
|
445
|
+
});
|
|
446
|
+
expect(created.status).toBe(201);
|
|
447
|
+
|
|
448
|
+
const denied = await api(port, "POST", "/api/db-query", {
|
|
435
449
|
bearer: plaintext,
|
|
436
|
-
body: {
|
|
450
|
+
body: { sql: "select 1" },
|
|
437
451
|
});
|
|
438
452
|
expect(denied.status).toBe(403);
|
|
439
453
|
expect(denied.body).toEqual({
|
|
@@ -454,9 +468,17 @@ describe("handleCore admission wiring", () => {
|
|
|
454
468
|
|
|
455
469
|
await flushAuditBuffer();
|
|
456
470
|
const rows = await admissionAuditRows();
|
|
457
|
-
expect(rows).toHaveLength(
|
|
471
|
+
expect(rows).toHaveLength(4);
|
|
458
472
|
|
|
459
473
|
expect(rows.find((row) => row.resourceId === "POST /api/tasks")).toMatchObject({
|
|
474
|
+
principalType: "user",
|
|
475
|
+
principalId: userId,
|
|
476
|
+
verb: "task.create.own",
|
|
477
|
+
decision: "allow",
|
|
478
|
+
reason: null,
|
|
479
|
+
source: "http",
|
|
480
|
+
});
|
|
481
|
+
expect(rows.find((row) => row.resourceId === "POST /api/db-query")).toMatchObject({
|
|
460
482
|
principalType: "user",
|
|
461
483
|
principalId: userId,
|
|
462
484
|
verb: "(admission:no-verb)",
|
|
@@ -485,6 +507,322 @@ describe("handleCore admission wiring", () => {
|
|
|
485
507
|
});
|
|
486
508
|
});
|
|
487
509
|
|
|
510
|
+
describe("requester and viewer task lifecycle", () => {
|
|
511
|
+
async function createTask(bearer: string, task: string): Promise<string> {
|
|
512
|
+
const res = await api(port, "POST", "/api/tasks", { bearer, body: { task } });
|
|
513
|
+
expect(res.status).toBe(201);
|
|
514
|
+
return (res.body as { id: string }).id;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async function setStatus(taskId: string, status: string): Promise<void> {
|
|
518
|
+
await getDbClient().run("UPDATE agent_tasks SET status = ? WHERE id = ?", [status, taskId]);
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async function lifecycle(bearer: string, taskId: string): Promise<Record<string, number>> {
|
|
522
|
+
await setStatus(taskId, "in_progress");
|
|
523
|
+
const pause = await api(port, "POST", `/api/tasks/${taskId}/pause`, { bearer });
|
|
524
|
+
await setStatus(taskId, "paused");
|
|
525
|
+
const resume = await api(port, "POST", `/api/tasks/${taskId}/resume`, { bearer });
|
|
526
|
+
const steer = await api(port, "POST", `/api/tasks/${taskId}/steer`, {
|
|
527
|
+
bearer,
|
|
528
|
+
body: { message: "requester steer" },
|
|
529
|
+
});
|
|
530
|
+
const title = await api(port, "PATCH", `/api/tasks/${taskId}/title`, {
|
|
531
|
+
bearer,
|
|
532
|
+
body: { title: "requester title" },
|
|
533
|
+
});
|
|
534
|
+
const cancel = await api(port, "POST", `/api/tasks/${taskId}/cancel`, { bearer });
|
|
535
|
+
return {
|
|
536
|
+
pause: pause.status,
|
|
537
|
+
resume: resume.status,
|
|
538
|
+
steer: steer.status,
|
|
539
|
+
title: title.status,
|
|
540
|
+
cancel: cancel.status,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
test("a requester creates, pauses, resumes, steers, renames and cancels its own task", async () => {
|
|
545
|
+
process.env.RBAC_ENABLED = "true";
|
|
546
|
+
const { userId, plaintext } = await createTokenForUser("Lifecycle Requester");
|
|
547
|
+
await narrowUserToRequester(userId);
|
|
548
|
+
|
|
549
|
+
const taskId = await createTask(plaintext, "requester lifecycle");
|
|
550
|
+
const owner = await getDbClient().get<{ requestedByUserId: string | null }>(
|
|
551
|
+
"SELECT requestedByUserId FROM agent_tasks WHERE id = ?",
|
|
552
|
+
[taskId],
|
|
553
|
+
);
|
|
554
|
+
expect(owner?.requestedByUserId).toBe(userId);
|
|
555
|
+
|
|
556
|
+
expect(await lifecycle(plaintext, taskId)).toEqual({
|
|
557
|
+
pause: 200,
|
|
558
|
+
resume: 200,
|
|
559
|
+
steer: 200,
|
|
560
|
+
title: 200,
|
|
561
|
+
cancel: 200,
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
test("the same calls on another user's task hit the session wall, not admission", async () => {
|
|
566
|
+
process.env.RBAC_ENABLED = "true";
|
|
567
|
+
const other = await createTokenForUser("Task Owner");
|
|
568
|
+
const foreignTaskId = await createTask(other.plaintext, "someone else's task");
|
|
569
|
+
const { userId, plaintext } = await createTokenForUser("Foreign Requester");
|
|
570
|
+
await narrowUserToRequester(userId);
|
|
571
|
+
|
|
572
|
+
const calls: [string, string, unknown][] = [
|
|
573
|
+
["POST", `/api/tasks/${foreignTaskId}/pause`, undefined],
|
|
574
|
+
["POST", `/api/tasks/${foreignTaskId}/resume`, undefined],
|
|
575
|
+
["POST", `/api/tasks/${foreignTaskId}/steer`, { message: "not mine" }],
|
|
576
|
+
["PATCH", `/api/tasks/${foreignTaskId}/title`, { title: "not mine" }],
|
|
577
|
+
["POST", `/api/tasks/${foreignTaskId}/cancel`, undefined],
|
|
578
|
+
];
|
|
579
|
+
for (const [method, path, body] of calls) {
|
|
580
|
+
const res = await api(port, method, path, { bearer: plaintext, body });
|
|
581
|
+
const error = (res.body as { error?: string }).error ?? "";
|
|
582
|
+
expect({ path, status: res.status, admission: error.includes("admission:") }).toEqual({
|
|
583
|
+
path,
|
|
584
|
+
status: 403,
|
|
585
|
+
admission: false,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
const task = await getDbClient().get<{ status: string; title: string | null }>(
|
|
589
|
+
"SELECT status, title FROM agent_tasks WHERE id = ?",
|
|
590
|
+
[foreignTaskId],
|
|
591
|
+
);
|
|
592
|
+
expect(task?.status).not.toBe("cancelled");
|
|
593
|
+
expect(task?.title).not.toBe("not mine");
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
// Regression: the session wall reads an ownerless root (Slack, schedules,
|
|
597
|
+
// operator key) as everyone's, so without the handler ownership check a
|
|
598
|
+
// requester could cancel, pause, resume or rename any such task.
|
|
599
|
+
test("a requester cannot change an ownerless task, an admin user still can", async () => {
|
|
600
|
+
process.env.RBAC_ENABLED = "true";
|
|
601
|
+
const { userId, plaintext } = await createTokenForUser("Ownerless Requester");
|
|
602
|
+
await narrowUserToRequester(userId);
|
|
603
|
+
const admin = await createTokenForUser("Ownerless Admin");
|
|
604
|
+
|
|
605
|
+
const taskId = await createTask(API_KEY, "operator task with no owner");
|
|
606
|
+
const owner = await getDbClient().get<{ requestedByUserId: string | null }>(
|
|
607
|
+
"SELECT requestedByUserId FROM agent_tasks WHERE id = ?",
|
|
608
|
+
[taskId],
|
|
609
|
+
);
|
|
610
|
+
expect(owner?.requestedByUserId).toBeNull();
|
|
611
|
+
|
|
612
|
+
await setStatus(taskId, "in_progress");
|
|
613
|
+
const pause = await api(port, "POST", `/api/tasks/${taskId}/pause`, { bearer: plaintext });
|
|
614
|
+
await setStatus(taskId, "paused");
|
|
615
|
+
const resume = await api(port, "POST", `/api/tasks/${taskId}/resume`, { bearer: plaintext });
|
|
616
|
+
const title = await api(port, "PATCH", `/api/tasks/${taskId}/title`, {
|
|
617
|
+
bearer: plaintext,
|
|
618
|
+
body: { title: "not mine" },
|
|
619
|
+
});
|
|
620
|
+
const cancel = await api(port, "POST", `/api/tasks/${taskId}/cancel`, { bearer: plaintext });
|
|
621
|
+
expect({
|
|
622
|
+
pause: pause.status,
|
|
623
|
+
resume: resume.status,
|
|
624
|
+
title: title.status,
|
|
625
|
+
cancel: cancel.status,
|
|
626
|
+
}).toEqual({ pause: 403, resume: 403, title: 403, cancel: 403 });
|
|
627
|
+
const task = await getDbClient().get<{ status: string; title: string | null }>(
|
|
628
|
+
"SELECT status, title FROM agent_tasks WHERE id = ?",
|
|
629
|
+
[taskId],
|
|
630
|
+
);
|
|
631
|
+
expect(task?.status).toBe("paused");
|
|
632
|
+
expect(task?.title).not.toBe("not mine");
|
|
633
|
+
|
|
634
|
+
const adminTitle = await api(port, "PATCH", `/api/tasks/${taskId}/title`, {
|
|
635
|
+
bearer: admin.plaintext,
|
|
636
|
+
body: { title: "admin title" },
|
|
637
|
+
});
|
|
638
|
+
const adminCancel = await api(port, "POST", `/api/tasks/${taskId}/cancel`, {
|
|
639
|
+
bearer: admin.plaintext,
|
|
640
|
+
});
|
|
641
|
+
expect({ title: adminTitle.status, cancel: adminCancel.status }).toEqual({
|
|
642
|
+
title: 200,
|
|
643
|
+
cancel: 200,
|
|
644
|
+
});
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
test("a viewer cannot create a task", async () => {
|
|
648
|
+
process.env.RBAC_ENABLED = "true";
|
|
649
|
+
const { userId, plaintext } = await createTokenForUser("Viewer User");
|
|
650
|
+
await detachRole(userId, "admin");
|
|
651
|
+
await attachRole(userId, "viewer");
|
|
652
|
+
|
|
653
|
+
const res = await api(port, "POST", "/api/tasks", {
|
|
654
|
+
bearer: plaintext,
|
|
655
|
+
body: { task: "viewer should not create" },
|
|
656
|
+
});
|
|
657
|
+
expect(res.status).toBe(403);
|
|
658
|
+
expect(res.body).toEqual({
|
|
659
|
+
error: "Forbidden: admission: missing permission 'task.create.own'",
|
|
660
|
+
});
|
|
661
|
+
});
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
describe("per-user inbox state", () => {
|
|
665
|
+
const item = { itemType: "approval", itemId: "inbox-guard", status: "done" };
|
|
666
|
+
|
|
667
|
+
test("a requester writes its own inbox state but not another user's", async () => {
|
|
668
|
+
process.env.RBAC_ENABLED = "true";
|
|
669
|
+
const other = await createUser({ name: "Inbox Other" });
|
|
670
|
+
const { userId, plaintext } = await createTokenForUser("Inbox Requester");
|
|
671
|
+
await narrowUserToRequester(userId);
|
|
672
|
+
|
|
673
|
+
const own = await api(port, "PATCH", "/api/inbox-state", {
|
|
674
|
+
bearer: plaintext,
|
|
675
|
+
body: { ...item, userId },
|
|
676
|
+
});
|
|
677
|
+
expect(own.status).toBe(200);
|
|
678
|
+
|
|
679
|
+
const foreign = await api(port, "PATCH", "/api/inbox-state", {
|
|
680
|
+
bearer: plaintext,
|
|
681
|
+
body: { ...item, userId: other.id },
|
|
682
|
+
});
|
|
683
|
+
expect(foreign.status).toBe(403);
|
|
684
|
+
|
|
685
|
+
const foreignBulk = await api(port, "PATCH", "/api/inbox-state/bulk", {
|
|
686
|
+
bearer: plaintext,
|
|
687
|
+
body: { userId: other.id, items: [item] },
|
|
688
|
+
});
|
|
689
|
+
expect(foreignBulk.status).toBe(403);
|
|
690
|
+
|
|
691
|
+
const rows = await getDbClient().query<{ userId: string }>(
|
|
692
|
+
"SELECT userId FROM inbox_item_state WHERE itemId = 'inbox-guard'",
|
|
693
|
+
);
|
|
694
|
+
expect(rows.map((row) => row.userId)).toEqual([userId]);
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
test("the operator key and an admin user still write any user's inbox state", async () => {
|
|
698
|
+
process.env.RBAC_ENABLED = "true";
|
|
699
|
+
const other = await createUser({ name: "Inbox Target" });
|
|
700
|
+
const admin = await createTokenForUser("Inbox Admin");
|
|
701
|
+
|
|
702
|
+
const operator = await api(port, "PATCH", "/api/inbox-state", {
|
|
703
|
+
body: { ...item, userId: other.id },
|
|
704
|
+
});
|
|
705
|
+
expect(operator.status).toBe(200);
|
|
706
|
+
|
|
707
|
+
const adminCall = await api(port, "PATCH", "/api/inbox-state/bulk", {
|
|
708
|
+
bearer: admin.plaintext,
|
|
709
|
+
body: { userId: other.id, items: [{ ...item, itemId: "inbox-guard-2" }] },
|
|
710
|
+
});
|
|
711
|
+
expect(adminCall.status).toBe(200);
|
|
712
|
+
});
|
|
713
|
+
});
|
|
714
|
+
|
|
715
|
+
describe("join the shared agent-fs drive", () => {
|
|
716
|
+
const AGENT_FS_ENV_KEYS = [
|
|
717
|
+
"AGENT_FS_API_URL",
|
|
718
|
+
"API_AGENT_FS_API_KEY",
|
|
719
|
+
"AGENT_FS_API_KEY",
|
|
720
|
+
"AGENT_FS_DEFAULT_ORG_ID",
|
|
721
|
+
"AGENT_FS_SHARED_ORG_ID",
|
|
722
|
+
"AGENT_FS_DEFAULT_DRIVE_ID",
|
|
723
|
+
"AGENT_FS_REGISTER_EMAIL",
|
|
724
|
+
];
|
|
725
|
+
let invites: { email: string; role: string }[] = [];
|
|
726
|
+
|
|
727
|
+
beforeEach(() => {
|
|
728
|
+
invites = [];
|
|
729
|
+
for (const key of AGENT_FS_ENV_KEYS) delete process.env[key];
|
|
730
|
+
process.env.AGENT_FS_API_URL = "https://agent-fs.example.test";
|
|
731
|
+
process.env.API_AGENT_FS_API_KEY = "example-afs-admin-key";
|
|
732
|
+
process.env.AGENT_FS_REGISTER_EMAIL = "admin@example.test";
|
|
733
|
+
setAgentFsProvisionFetchForTests((async (input, init) => {
|
|
734
|
+
const url = new URL(String(input));
|
|
735
|
+
const method = init?.method ?? "GET";
|
|
736
|
+
if (url.pathname === "/auth/me") return Response.json({ userId: "admin-user" });
|
|
737
|
+
if (url.pathname === "/orgs" && method === "GET") {
|
|
738
|
+
return Response.json({ orgs: [{ id: "shared-org", name: "swarm" }] });
|
|
739
|
+
}
|
|
740
|
+
if (url.pathname === "/orgs/shared-org/drives" && method === "GET") {
|
|
741
|
+
return Response.json({ drives: [{ id: "shared-drive", name: "shared" }] });
|
|
742
|
+
}
|
|
743
|
+
if (url.pathname === "/orgs/shared-org/members" && method === "GET") {
|
|
744
|
+
return Response.json({ members: [] });
|
|
745
|
+
}
|
|
746
|
+
if (url.pathname === "/orgs/shared-org/members/invite" && method === "POST") {
|
|
747
|
+
invites.push(JSON.parse(String(init?.body)));
|
|
748
|
+
return Response.json({ ok: true });
|
|
749
|
+
}
|
|
750
|
+
return Response.json({ error: "unexpected route" }, { status: 500 });
|
|
751
|
+
}) as typeof fetch);
|
|
752
|
+
});
|
|
753
|
+
|
|
754
|
+
afterEach(() => {
|
|
755
|
+
resetAgentFsProvisionFetchForTests();
|
|
756
|
+
for (const key of AGENT_FS_ENV_KEYS) delete process.env[key];
|
|
757
|
+
});
|
|
758
|
+
|
|
759
|
+
async function tokenWithRole(name: string, email: string, role: string) {
|
|
760
|
+
const user = await createUser({ name, email });
|
|
761
|
+
const { plaintext } = await mintToken(user.id, "admission", ACTOR);
|
|
762
|
+
if (role !== "admin") {
|
|
763
|
+
await detachRole(user.id, "admin");
|
|
764
|
+
await attachRole(user.id, role);
|
|
765
|
+
}
|
|
766
|
+
return plaintext;
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
test("a requester cannot invite another email", async () => {
|
|
770
|
+
process.env.RBAC_ENABLED = "true";
|
|
771
|
+
const bearer = await tokenWithRole("Join Requester", "Req.User@example.test", "requester");
|
|
772
|
+
|
|
773
|
+
const res = await api(port, "POST", "/api/fs/members/invite", {
|
|
774
|
+
bearer,
|
|
775
|
+
body: { email: "someone-else@example.test", role: "admin" },
|
|
776
|
+
});
|
|
777
|
+
expect(res.status).toBe(403);
|
|
778
|
+
expect(invites).toEqual([]);
|
|
779
|
+
});
|
|
780
|
+
|
|
781
|
+
test("a requester joins with its own email as editor, whatever role it asks for", async () => {
|
|
782
|
+
process.env.RBAC_ENABLED = "true";
|
|
783
|
+
const bearer = await tokenWithRole("Join Requester", "Req.User@example.test", "requester");
|
|
784
|
+
|
|
785
|
+
const res = await api(port, "POST", "/api/fs/members/invite", {
|
|
786
|
+
bearer,
|
|
787
|
+
body: { email: "req.user@EXAMPLE.test", role: "admin" },
|
|
788
|
+
});
|
|
789
|
+
expect(res.status).toBe(200);
|
|
790
|
+
expect(res.body).toEqual({ orgId: "shared-org", invited: true, status: "added" });
|
|
791
|
+
expect(invites).toEqual([{ email: "Req.User@example.test", role: "editor" }]);
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
test("a viewer joins with its own email as viewer", async () => {
|
|
795
|
+
process.env.RBAC_ENABLED = "true";
|
|
796
|
+
const bearer = await tokenWithRole("Join Viewer", "viewer@example.test", "viewer");
|
|
797
|
+
|
|
798
|
+
const res = await api(port, "POST", "/api/fs/members/invite", {
|
|
799
|
+
bearer,
|
|
800
|
+
body: { email: "viewer@example.test" },
|
|
801
|
+
});
|
|
802
|
+
expect(res.status).toBe(200);
|
|
803
|
+
expect(invites).toEqual([{ email: "viewer@example.test", role: "viewer" }]);
|
|
804
|
+
});
|
|
805
|
+
|
|
806
|
+
test("the operator key and an admin user invite any email at the requested role", async () => {
|
|
807
|
+
process.env.RBAC_ENABLED = "true";
|
|
808
|
+
const bearer = await tokenWithRole("Join Admin", "admin-user@example.test", "admin");
|
|
809
|
+
|
|
810
|
+
const operator = await api(port, "POST", "/api/fs/members/invite", {
|
|
811
|
+
body: { email: "teammate@example.test", role: "admin" },
|
|
812
|
+
});
|
|
813
|
+
expect(operator.status).toBe(200);
|
|
814
|
+
const admin = await api(port, "POST", "/api/fs/members/invite", {
|
|
815
|
+
bearer,
|
|
816
|
+
body: { email: "another@example.test" },
|
|
817
|
+
});
|
|
818
|
+
expect(admin.status).toBe(200);
|
|
819
|
+
expect(invites).toEqual([
|
|
820
|
+
{ email: "teammate@example.test", role: "admin" },
|
|
821
|
+
{ email: "another@example.test", role: "editor" },
|
|
822
|
+
]);
|
|
823
|
+
});
|
|
824
|
+
});
|
|
825
|
+
|
|
488
826
|
describe("realtime ticket admission", () => {
|
|
489
827
|
async function grantOnly(userId: string, verbs: PermissionVerb[]): Promise<void> {
|
|
490
828
|
const roleId = `rbac-role-test-${crypto.randomUUID()}`;
|
|
@@ -16,7 +16,9 @@ import type { PermissionVerb } from "../rbac";
|
|
|
16
16
|
const TEST_DB_PATH = "./test-rbac-roles.sqlite";
|
|
17
17
|
const REQUESTER_ROLE_ID = "rbac-role-requester";
|
|
18
18
|
const REQUESTER_VERBS = [
|
|
19
|
+
"comb.presence",
|
|
19
20
|
"favorite.write.own",
|
|
21
|
+
"fs.join.own",
|
|
20
22
|
"session.presence",
|
|
21
23
|
"session.share",
|
|
22
24
|
"task.action.own",
|
|
@@ -24,6 +26,7 @@ const REQUESTER_VERBS = [
|
|
|
24
26
|
"task.create.own",
|
|
25
27
|
"task.fs.mutate",
|
|
26
28
|
"task.read.own",
|
|
29
|
+
"task.steer.own",
|
|
27
30
|
] satisfies PermissionVerb[];
|
|
28
31
|
|
|
29
32
|
async function removeDbFiles() {
|
|
@@ -194,7 +197,12 @@ describe("ensureRbacSeedsSynced", () => {
|
|
|
194
197
|
ensureRbacSeedsSynced({ quiet: true });
|
|
195
198
|
|
|
196
199
|
const viewerVerbs = await roleVerbs("rbac-role-viewer");
|
|
197
|
-
expect(viewerVerbs).toEqual([
|
|
200
|
+
expect(viewerVerbs).toEqual([
|
|
201
|
+
"comb.presence",
|
|
202
|
+
"favorite.write.own",
|
|
203
|
+
"fs.join.own",
|
|
204
|
+
"session.presence",
|
|
205
|
+
]);
|
|
198
206
|
for (const verb of viewerVerbs) expect(REQUESTER_VERBS).toContain(verb);
|
|
199
207
|
});
|
|
200
208
|
|