@desplega.ai/agent-swarm 2.7.4 → 2.7.6
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 +1 -1
- package/dist/acp-adapter-bp91d37f.js +15 -0
- package/dist/{actions-gfrr9vb7.js → actions-yj0eeaqh.js} +19 -18
- package/dist/{amp-adapter-0s58gm46.js → amp-adapter-4pg9h34q.js} +3 -3
- package/dist/{app-stvq692g.js → app-909e97cr.js} +3 -3
- package/dist/{assistant-5s37fr9n.js → assistant-qdyb5fyy.js} +32 -23
- package/dist/{boot-reembed-yzy1841n.js → boot-reembed-fk0s9z18.js} +1 -1
- package/dist/{boot-reembed-cv82gz7y.js → boot-reembed-gs1rzrnh.js} +1 -1
- package/dist/{boot-scrub-sweep-8k4q95ap.js → boot-scrub-sweep-4qfd4kfx.js} +4 -4
- package/dist/{boot-seeds-ftt9htah.js → boot-seeds-yr00k13d.js} +4 -4
- package/dist/{boxd-515v1t8z.js → boxd-vdmfssd9.js} +5 -5
- package/dist/{claude-adapter-t00wrzeh.js → claude-adapter-qta1f1xn.js} +7 -7
- package/dist/{claude-managed-adapter-g6vmjccb.js → claude-managed-adapter-dvs2xw5n.js} +2 -2
- package/dist/{claude-managed-setup-afpa82f7.js → claude-managed-setup-sc6j4dm0.js} +3 -3
- package/dist/{claude-sdk-session-vj0c2azx.js → claude-sdk-session-zgrz6yvc.js} +9175 -9076
- package/dist/{cli-gjr2y2xk.js → cli-0agcv1v5.js} +4 -1
- package/dist/{cli-edmm9d9y.js → cli-0at8w34w.js} +7 -7
- package/dist/{cli-a81qevyg.js → cli-0qatpy8q.js} +20 -36
- package/dist/{cli-bcz074zx.js → cli-0yv5z2pg.js} +1 -1
- package/dist/{cli-a4btqqft.js → cli-1b7btd38.js} +1 -1
- package/dist/{cli-a2jw6ee5.js → cli-1p89xny0.js} +1 -1
- package/dist/{cli-v726afdm.js → cli-2pnswgh8.js} +1 -1
- package/dist/{cli-jkt52mgt.js → cli-386tyxxg.js} +2 -2
- package/dist/{cli-az0bpf8p.js → cli-3ypk9vk1.js} +3 -3
- package/dist/{cli-wk9md5px.js → cli-436532ck.js} +4 -4
- package/dist/{cli-7n5sbw0z.js → cli-4necwtsv.js} +5 -5
- package/dist/cli-54t00gt8.js +151 -0
- package/dist/{cli-655xm9ms.js → cli-583frbyr.js} +1 -1
- package/dist/{cli-4r12t6w6.js → cli-5c4hjzne.js} +101 -4
- package/dist/{cli-hf6zt5m6.js → cli-69szn7w7.js} +17 -1
- package/dist/{cli-rtk9g7av.js → cli-73jzwksr.js} +6 -2
- package/dist/{cli-jg0nfda6.js → cli-75gkpjvw.js} +52 -21
- package/dist/{cli-wcmnan2g.js → cli-89knwags.js} +5 -5
- package/dist/{cli-pwyevs29.js → cli-8bc1w0tw.js} +22 -21
- package/dist/{cli-t282jgab.js → cli-8snvgs5r.js} +41 -16
- package/dist/{cli-vqgnyh76.js → cli-98rw5dt5.js} +15 -3
- package/dist/{cli-9cctx1tn.js → cli-a22h291q.js} +1 -1
- package/dist/{cli-ycm6180s.js → cli-a7v854rf.js} +5 -3
- package/dist/{cli-98pke4vk.js → cli-ae7mrdf9.js} +2 -2
- package/dist/{cli-je8yeyc5.js → cli-bjb3z51z.js} +5 -5
- package/dist/{cli-mc97xy2s.js → cli-c60vetmm.js} +16 -4
- package/dist/{cli-j4kw3rsx.js → cli-cm5yqn35.js} +18 -0
- package/dist/{cli-wndv9p7g.js → cli-decfb6v8.js} +6 -6
- package/dist/{cli-h55y2bjc.js → cli-dhar9qay.js} +2 -2
- package/dist/{cli-8yy525ad.js → cli-drr5v6av.js} +2 -2
- package/dist/{cli-5gr1g711.js → cli-dxae2yg7.js} +2 -2
- package/dist/{cli-pe09p6dv.js → cli-e3r2neck.js} +8 -7
- package/dist/{cli-13jvmew3.js → cli-ekwks2vs.js} +7 -7
- package/dist/{cli-jg4p5t36.js → cli-es6va8jk.js} +121 -51
- package/dist/{cli-j4kt1mpb.js → cli-faxe6thg.js} +3 -3
- package/dist/{cli-tzrpv5sc.js → cli-ftyxyspd.js} +1 -1
- package/dist/cli-fw2w0kva.js +47 -0
- package/dist/{cli-2dfbv8qr.js → cli-fy6x31ps.js} +1 -0
- package/dist/{cli-aq58fk5m.js → cli-g83s9ewh.js} +7 -1
- package/dist/cli-gdhpcbhf.js +55 -0
- package/dist/{cli-82jg7fp9.js → cli-he5nxaa8.js} +1 -1
- package/dist/{cli-8veytvbh.js → cli-he9wchme.js} +1 -1
- package/dist/{cli-k76tgq03.js → cli-hgaazk69.js} +2 -2
- package/dist/{cli-d2pzqej1.js → cli-hr2e2fft.js} +741 -7
- package/dist/{cli-dae4sbgy.js → cli-jm2vfpfd.js} +2 -2
- package/dist/{cli-1s1dqn1j.js → cli-k2gzwsj2.js} +297 -140
- package/dist/{cli-b1afgtj3.js → cli-kqdg1bsb.js} +3 -3
- package/dist/cli-m2z8jagn.js +12078 -0
- package/dist/{cli-mvckssyq.js → cli-m48g6p95.js} +5 -5
- package/dist/{cli-bwty35gx.js → cli-mthwvjre.js} +1 -1
- package/dist/{cli-41z8xwj0.js → cli-nhrvpaj7.js} +34 -8
- package/dist/{cli-ahd90bwt.js → cli-nwj0ee2w.js} +6 -6
- package/dist/{cli-kvtc7aez.js → cli-p5gp8pcx.js} +44 -14
- package/dist/{cli-avgcz4ra.js → cli-pd7vagab.js} +436 -171
- package/dist/{cli-zn15c46z.js → cli-q0k1w5zd.js} +1 -1
- package/dist/{cli-bq2pcphn.js → cli-rmmmrjkd.js} +1 -1
- package/dist/{cli-2qpvmmkw.js → cli-s0qnd4wv.js} +1 -1
- package/dist/{cli-kg8hchyg.js → cli-sa02jsqt.js} +1 -1
- package/dist/{cli-1yekbyqm.js → cli-shpbx5ee.js} +283 -82
- package/dist/{cli-9y354g3j.js → cli-srye3vdt.js} +1 -1
- package/dist/{cli-byqfbnjw.js → cli-svj5090z.js} +9 -9
- package/dist/{cli-gcbdjt47.js → cli-vfym7wmz.js} +6 -6
- package/dist/{cli-v1yw3wha.js → cli-vvpx3gsx.js} +4 -4
- package/dist/{cli-a1fhj4n9.js → cli-w6dt9ged.js} +2 -2
- package/dist/{cli-1gm8jcyr.js → cli-waqbxg08.js} +5232 -68
- package/dist/{cli-5hjced2v.js → cli-wp9sp1qb.js} +6 -6
- package/dist/{cli-eny54typ.js → cli-wx3rhnm0.js} +1 -1
- package/dist/{cli-s0exq74r.js → cli-xbbndvjy.js} +3 -3
- package/dist/{cli-f255yaqn.js → cli-xds5f77d.js} +3 -3
- package/dist/{cli-0sbkss7h.js → cli-xnqgpshy.js} +2 -2
- package/dist/{cli-0fgr1w9b.js → cli-xnrwazav.js} +20 -3
- package/dist/{cli-gf5hbmdr.js → cli-yjdgea32.js} +3 -3
- package/dist/{cli-cq1pf336.js → cli-yn4kxs06.js} +124 -11
- package/dist/{cli-224epwwf.js → cli-yx4kfngt.js} +3 -3
- package/dist/{cli-eqx6k993.js → cli-yy3rhsdw.js} +47 -13
- package/dist/{cli-36g23z21.js → cli-zbbwez0r.js} +4 -4
- package/dist/{cli-ktmvb20s.js → cli-zm72anhg.js} +12 -12
- package/dist/cli.js +26 -25
- package/dist/{codex-adapter-g0gctw29.js → codex-adapter-ayd1fb6h.js} +5 -5
- package/dist/{codex-hook-vrzz2xhr.js → codex-hook-9efm2x1m.js} +1 -1
- package/dist/{codex-login-0gg46ygn.js → codex-login-y0pmff1c.js} +1 -1
- package/dist/{codex-session-runner-dr2cxe1c.js → codex-session-runner-jesa32xc.js} +6 -6
- package/dist/{commands-1p07cvff.js → commands-hanewgf2.js} +9 -3
- package/dist/{controller-z0s7sv6r.js → controller-dt1n3c1q.js} +9 -4
- package/dist/{controllers-connect-x9kvvhyk.js → controllers-connect-xx8x64n5.js} +22 -21
- package/dist/{cursor-adapter-m5b98pvk.js → cursor-adapter-9s4dwwdw.js} +2 -2
- package/dist/{db-capk2raj.js → db-3vttcbv4.js} +3 -3
- package/dist/{db-rsh04kh3.js → db-9dv0g1ab.js} +12 -4
- package/dist/{devin-adapter-yxm87fe9.js → devin-adapter-r9phbnc9.js} +1 -1
- package/dist/{devtools-82fzqra8.js → devtools-qxfgbsqz.js} +1 -1
- package/dist/{dispatcher-304q6fwa.js → dispatcher-tx9hd1sv.js} +18 -17
- package/dist/{docker-7rxt0rry.js → docker-c1a3gqmf.js} +91 -24
- package/dist/{dsh-adapter-btewem0y.js → dsh-adapter-007y4ejs.js} +2 -2
- package/dist/{e2b-renc0k8q.js → e2b-a19d28rz.js} +4 -4
- package/dist/{e2b-7fc9b9bd.js → e2b-we6hs6q5.js} +6 -6
- package/dist/{easter-egg-3d-fm1p3n4d.js → easter-egg-3d-rgbrqb8s.js} +1 -1
- package/dist/{embedding-space-s68v49ct.js → embedding-space-ny1e2940.js} +6 -6
- package/dist/{embeddings-4ms3604w.js → embeddings-2t386t13.js} +1 -1
- package/dist/{event-bus-dfjx2qdh.js → event-bus-g0ka3pbw.js} +1 -1
- package/dist/{execute-mcdgccjs.js → execute-pksh0ceh.js} +1 -1
- package/dist/{external-channel-9dxwfztn.js → external-channel-phgmhmd7.js} +2 -2
- package/dist/fx-adapter-fy8rr921.js +18 -0
- package/dist/{google-generative-ai-yvmdxyxa.js → google-generative-ai-8y3m3k4b.js} +1 -1
- package/dist/{google-vertex-jrtdh9gs.js → google-vertex-w3v98n7b.js} +1 -1
- package/dist/{grok-adapter-05ecv4h0.js → grok-adapter-q0abvpaj.js} +8 -8
- package/dist/handlers-by3sfxyx.js +32 -0
- package/dist/{heartbeat-3n94heh3.js → heartbeat-kadyek8p.js} +21 -20
- package/dist/{hook-aadrn69x.js → hook-5zs3msf6.js} +6 -6
- package/dist/{hook-zxwje86c.js → hook-9k551xnj.js} +4 -4
- package/dist/{http-xzxk9mst.js → http-mhz9vw1f.js} +272 -103
- package/dist/index-4f2pvxae.js +37 -0
- package/dist/index-98gz7tmp.js +30 -0
- package/dist/{index-5rk6rfzr.js → index-fvzke5kx.js} +1 -1
- package/dist/index-k08z2cak.js +33 -0
- package/dist/{index-yqhz88yy.js → index-pxn30mse.js} +8 -8
- package/dist/{index-d34f6r87.js → index-q361cg2f.js} +1 -1
- package/dist/{index-9j4zwcht.js → index-rvr00sg7.js} +168 -675
- package/dist/{index-qeb82z37.js → index-vdygbq10.js} +1 -1
- package/dist/{keepalive-w4aakbyk.js → keepalive-w56f32cg.js} +4 -4
- package/dist/lead-enq35nqp.js +70 -0
- package/dist/{maintenance-hbptpk6p.js → maintenance-7mdgxcea.js} +1 -1
- package/dist/{oauth-refresh-sweep-217semrv.js → oauth-refresh-sweep-29txy9vk.js} +3 -3
- package/dist/{onboard-vz2s5df3.js → onboard-kpsrkarj.js} +5 -5
- package/dist/{opencode-adapter-gtfd7d7v.js → opencode-adapter-j25vm3rr.js} +5 -5
- package/dist/{otel-impl-aefave37.js → otel-impl-6kkct7a8.js} +2 -2
- package/dist/{pi-mono-adapter-f8h5yseh.js → pi-mono-adapter-exhkzhn2.js} +117 -23
- package/dist/{pricing-refresh-m1yhh9ya.js → pricing-refresh-n2x7jh6k.js} +3 -3
- package/dist/{rbac-roles-rrw8twdz.js → rbac-roles-81ypx6es.js} +1 -1
- package/dist/{rbac-roles-svj72cdf.js → rbac-roles-b5htkjby.js} +2 -2
- package/dist/{render-v2-amwbnfxh.js → render-v2-qn6jmpk4.js} +3 -3
- package/dist/{resume-9cd6wf6z.js → resume-wd30wx23.js} +7 -7
- package/dist/{runtime-fp4vrx7m.js → runtime-4hsg7h60.js} +1 -1
- package/dist/{session-access-21t6yjyg.js → session-access-5qvfevhk.js} +4 -4
- package/dist/{session-source-backfill-pxbzcsc6.js → session-source-backfill-n1czavbr.js} +3 -3
- package/dist/{setup-4592tqz4.js → setup-dnb56qy5.js} +3 -3
- package/dist/{virtual-modules-gzfnpmnz.js → virtual-modules-76xxv9fs.js} +2 -2
- package/dist/{work-objects-t9tcf174.js → work-objects-4n21tfkm.js} +21 -20
- package/dist/{worker-follow-up-q32jh4yk.js → worker-follow-up-77hvzxww.js} +18 -17
- package/dist/worker-gg1576j3.js +70 -0
- package/dist/{x-b6wxntdt.js → x-vmmye1kb.js} +2 -2
- package/openapi.json +189 -32
- package/package.json +8 -7
- package/src/be/db/script-runs.ts +23 -0
- package/src/be/db/session-summaries.ts +8 -2
- package/src/be/db/session-tree.ts +174 -61
- package/src/be/db/session-visibility.ts +5 -4
- package/src/be/db/tasks/read.ts +122 -3
- package/src/be/db-client.ts +8 -5
- package/src/be/db.ts +23 -8
- package/src/be/memory/providers/sqlite-store.ts +49 -12
- package/src/be/migrations/259_session_summaries_open_tasks.sql +111 -0
- package/src/be/migrations/260_runtime_pool_cache.sql +20 -0
- package/src/be/migrations/262_session_visibility_indexes.sql +18 -0
- package/src/be/migrations/263_task_list_filter_index.sql +11 -0
- package/src/be/migrations/264_slack_thread_cover_index.sql +9 -0
- package/src/be/migrations/266_script_runs_source_task.sql +11 -0
- package/src/be/model-validation.ts +5 -1
- package/src/be/pricing-normalize.ts +7 -2
- package/src/be/runtime-pools/cache-cleanup.ts +109 -0
- package/src/be/runtime-pools/db.ts +65 -6
- package/src/be/runtime-pools/remote-controller.ts +5 -0
- package/src/be/runtime-pools/supervisor.ts +33 -2
- package/src/be/scripts/typecheck.ts +23 -0
- package/src/be/seed/schedules-seeder.ts +135 -9
- package/src/be/seed/workflows-seeder.ts +65 -1
- package/src/be/seed-pricing.ts +23 -0
- package/src/be/seed-scripts/catalog/task-failure-audit.ts +5 -1
- package/src/be/steering.ts +50 -3
- package/src/be/swarm-config-guard.ts +1 -0
- package/src/commands/controller.ts +4 -0
- package/src/commands/entrypoint-api-readiness.ts +74 -0
- package/src/commands/provider-credentials.ts +29 -2
- package/src/commands/runner.ts +86 -5
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/extensions/contract.ts +27 -1
- package/src/extensions/dispatcher.ts +4 -0
- package/src/http/agents.ts +1 -0
- package/src/http/index.ts +1 -1
- package/src/http/list-total-cache.ts +14 -7
- package/src/http/mcp-bridge.ts +4 -0
- package/src/http/memory.ts +38 -9
- package/src/http/script-runs.ts +174 -5
- package/src/http/status.ts +10 -2
- package/src/http/tasks.ts +4 -2
- package/src/integrations/chat-sdk/inbound.ts +15 -5
- package/src/integrations/chat-sdk/steering.ts +3 -1
- package/src/kv-reserved-namespaces.ts +2 -0
- package/src/prompts/base-prompt.ts +4 -1
- package/src/prompts/session-templates.ts +26 -0
- package/src/providers/acp-adapter.ts +190 -26
- package/src/providers/acp-targets.ts +306 -6
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/codex-adapter.ts +9 -44
- package/src/providers/codex-oauth/flow.ts +24 -3
- package/src/providers/codex-oauth/storage.ts +56 -17
- package/src/providers/codex-oauth/subscription.ts +67 -0
- package/src/providers/fx-adapter.ts +211 -0
- package/src/providers/index.ts +5 -1
- package/src/providers/mcp-loopback-proxy.ts +94 -0
- package/src/providers/pi-mono-adapter.ts +98 -13
- package/src/providers/pricing-sources.md +8 -0
- package/src/providers/reasoning-effort.ts +17 -1
- package/src/providers/types.ts +2 -1
- package/src/runtime-providers/controller-protocol.ts +14 -1
- package/src/runtime-providers/controller.ts +5 -0
- package/src/runtime-providers/docker.ts +108 -23
- package/src/runtime-providers/types.ts +16 -0
- package/src/scheduler/scheduler.ts +36 -3
- package/src/script-workflows/workflow-ctx.ts +8 -0
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +27 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +33 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +27 -0
- package/src/server-user.ts +1 -0
- package/src/server.ts +7 -0
- package/src/slack/assistant.ts +17 -1
- package/src/slack/commands.ts +5 -1
- package/src/slack/handlers.ts +53 -5
- package/src/slack/left-threads.ts +88 -0
- package/src/slack/steering.ts +3 -0
- package/src/slack/thread-buffer.ts +40 -16
- package/src/tasks/follow-up-redelegation-guard.ts +155 -0
- package/src/tests/acp-adapter.test.ts +79 -0
- package/src/tests/base-prompt.test.ts +12 -0
- package/src/tests/chat-sdk-inbound.test.ts +30 -0
- package/src/tests/chatgpt-subscription-model-groups.test.ts +62 -0
- package/src/tests/chatgpt-subscription-pi.test.ts +170 -0
- package/src/tests/chatgpt-subscription-session-bench.test.ts +198 -0
- package/src/tests/claude-artifacts-read-script.test.ts +209 -0
- package/src/tests/codex-oauth-credential-info.test.ts +40 -0
- package/src/tests/codex-oauth-refresh-egress.test.ts +194 -0
- package/src/tests/context-window.test.ts +7 -0
- package/src/tests/db-client.test.ts +21 -0
- package/src/tests/db-task-read-contract.test.ts +8 -2
- package/src/tests/db-task-write-contract.test.ts +5 -1
- package/src/tests/entrypoint-api-readiness.test.ts +416 -210
- package/src/tests/explicit-model-validation.test.ts +113 -8
- package/src/tests/extensions-run-context.test.ts +11 -0
- package/src/tests/extensions-steering.test.ts +353 -0
- package/src/tests/extensions-validate.test.ts +5 -0
- package/src/tests/fixtures/extensions/steering-policy/hooks.ts +39 -0
- package/src/tests/fixtures/extensions/steering-policy/manifest.json +7 -0
- package/src/tests/fixtures/fake-fx.ts +167 -0
- package/src/tests/fixtures/fx/acp-responses-recorded.json +157 -0
- package/src/tests/fixtures/fx/acp-updates-recorded.jsonl +15 -0
- package/src/tests/follow-up-redelivery-guard.test.ts +432 -202
- package/src/tests/fx-adapter.test.ts +771 -0
- package/src/tests/grok-adapter.test.ts +36 -0
- package/src/tests/list-total-cache.test.ts +27 -2
- package/src/tests/mcp-loopback-proxy.test.ts +193 -0
- package/src/tests/memory-fts-populate.test.ts +11 -9
- package/src/tests/memory-retrieval-purge.test.ts +80 -0
- package/src/tests/migration-260-runtime-pool-cache.test.ts +118 -0
- package/src/tests/model-tier-resolution.test.ts +3 -3
- package/src/tests/pricing-refresh.test.ts +5 -5
- package/src/tests/prompt-template-session.test.ts +5 -3
- package/src/tests/provider-registration.test.ts +2 -1
- package/src/tests/query-audit-plans.test.ts +30 -2
- package/src/tests/runtime-cache-cleanup.test.ts +367 -0
- package/src/tests/runtime-e2b-pools.test.ts +41 -0
- package/src/tests/runtime-pools-db.test.ts +98 -0
- package/src/tests/runtime-pools-http.test.ts +29 -0
- package/src/tests/runtime-providers-docker.test.ts +253 -0
- package/src/tests/runtime-remote-controller.test.ts +66 -2
- package/src/tests/runtime-supervisor.test.ts +66 -2
- package/src/tests/script-connections.test.ts +13 -0
- package/src/tests/script-runs-http.test.ts +684 -2
- package/src/tests/seed-model-profiles.test.ts +2 -2
- package/src/tests/seed-schedules.test.ts +138 -0
- package/src/tests/seed-scripts.test.ts +26 -0
- package/src/tests/session-cost-recompute-unit.test.ts +39 -0
- package/src/tests/session-costs-golden.test.ts +26 -0
- package/src/tests/session-summaries-plans.test.ts +120 -0
- package/src/tests/session-summaries.test.ts +72 -12
- package/src/tests/sessions-tree.test.ts +11 -3
- package/src/tests/slack-leave-thread.test.ts +571 -0
- package/src/tests/slack-render-v2-query-plans.test.ts +24 -2
- package/src/tests/status.test.ts +11 -0
- package/src/tests/tasks-readable-row.test.ts +155 -0
- package/src/tests/tool-annotations.test.ts +2 -1
- package/src/tests/workflow-engine-v2.test.ts +42 -0
- package/src/tests/workflow-script-principal.test.ts +207 -7
- package/src/tools/send-task.ts +35 -47
- package/src/tools/slack-leave-thread.ts +104 -0
- package/src/tools/steer-task.ts +3 -2
- package/src/tools/task-dedup.ts +45 -21
- package/src/tools/tool-config.ts +2 -1
- package/src/types.ts +57 -0
- package/src/utils/codex-auth-failure.ts +30 -0
- package/src/utils/context-window.ts +4 -1
- package/src/utils/credentials.ts +7 -0
- package/src/utils/fx-models.ts +43 -0
- package/src/workflows/cooldown.ts +3 -2
- package/src/workflows/engine.ts +24 -9
- package/templates/schedules/dreaming-daily/config.json +1 -1
- package/templates/schedules/dreaming-daily/content.md +6 -14
- package/templates/skills/composio-google-docs/content.md +10 -0
- package/templates/skills/script-workflows/content.md +19 -1
- package/templates/skills/swarm-docs/content.md +2 -0
- package/templates/skills/swarm-scripts/SKILL.md +2 -0
- package/templates/skills/swarm-scripts/content.md +2 -0
- package/templates/workflows/dreaming/content.md +5 -1
- package/dist/acp-adapter-96dc64m2.js +0 -15
- package/dist/cli-dznrgaep.js +0 -30
- package/dist/cli-w3hcrxht.js +0 -6985
- package/dist/handlers-jg7v74ty.js +0 -31
- package/dist/index-8c1zbz59.js +0 -29
- package/dist/index-8k8tmfxd.js +0 -32
- package/dist/index-hq617xdj.js +0 -36
- package/dist/lead-567kw7x3.js +0 -68
- package/dist/worker-fjdqra6q.js +0 -68
|
@@ -112,10 +112,15 @@ afterAll(async () => {
|
|
|
112
112
|
await removeDbFiles(TEST_DB_PATH);
|
|
113
113
|
});
|
|
114
114
|
|
|
115
|
-
async function api(
|
|
115
|
+
async function api(
|
|
116
|
+
method: string,
|
|
117
|
+
path: string,
|
|
118
|
+
body: unknown,
|
|
119
|
+
headers: Record<string, string> = {},
|
|
120
|
+
) {
|
|
116
121
|
const res = await fetch(`${baseUrl}${path}`, {
|
|
117
122
|
method,
|
|
118
|
-
headers: { "content-type": "application/json" },
|
|
123
|
+
headers: { "content-type": "application/json", ...headers },
|
|
119
124
|
body: JSON.stringify(body),
|
|
120
125
|
});
|
|
121
126
|
const text = await res.text();
|
|
@@ -331,6 +336,44 @@ describe("grok model namespace", () => {
|
|
|
331
336
|
});
|
|
332
337
|
});
|
|
333
338
|
|
|
339
|
+
describe("fx model routes", () => {
|
|
340
|
+
test("an unrouted namespace, a bare non-OpenAI id or latest:anthropic is refused, even with allowCustomModel", async () => {
|
|
341
|
+
for (const [model, reason] of [
|
|
342
|
+
["nan/glm5.3-flash", "InconsistentFinishReason"],
|
|
343
|
+
["helmcode/qwen3.6", "Helmcode is not verified on fx"],
|
|
344
|
+
["anthropic/claude-opus-5-5", "Anthropic Messages API"],
|
|
345
|
+
["claude-opus-5-5", "is in the anthropic catalog"],
|
|
346
|
+
["latest:anthropic/opus", "latest:anthropic alias"],
|
|
347
|
+
] as const) {
|
|
348
|
+
for (const allowCustomModel of [false, true]) {
|
|
349
|
+
const error = await explicitModelError({ model, harnessProvider: "fx", allowCustomModel });
|
|
350
|
+
expect(error).toContain(`Model "${model}" does not run on the fx harness`);
|
|
351
|
+
expect(error).toContain(reason);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("the OpenRouter, AI Gateway and OpenAI routes pass", async () => {
|
|
357
|
+
for (const model of [
|
|
358
|
+
"openrouter/deepseek/deepseek-v4.1-flash",
|
|
359
|
+
"vercel/anthropic/claude-opus-5.5",
|
|
360
|
+
"gpt-5-mini",
|
|
361
|
+
"latest:openrouter/deepseek/deepseek-v4.1-*",
|
|
362
|
+
]) {
|
|
363
|
+
expect(await explicitModelError({ model, harnessProvider: "fx" })).toBeNull();
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
test("MODEL_TIER_FX_<TIER> refuses a model fx cannot route", () => {
|
|
368
|
+
expect(validateTierConfigValue("MODEL_TIER_FX_SMART", "nan/glm5.3-flash")).toContain(
|
|
369
|
+
'model "nan/glm5.3-flash" does not run on the fx harness',
|
|
370
|
+
);
|
|
371
|
+
expect(
|
|
372
|
+
validateTierConfigValue("MODEL_TIER_FX_SMOL", "openrouter/deepseek/deepseek-v4.1-flash"),
|
|
373
|
+
).toBeNull();
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
334
377
|
describe("MODEL_TIER_<PROVIDER>_<TIER> values", () => {
|
|
335
378
|
test("a model from another harness family is rejected on write", () => {
|
|
336
379
|
expect(validateTierConfigValue("MODEL_TIER_CODEX_SMART", "claude-opus-5-5")).toBe(
|
|
@@ -574,12 +617,13 @@ describe("harness compatibility at every create entry point", () => {
|
|
|
574
617
|
test("internal script-run agent-task route: a cross-harness model is a 400", async () => {
|
|
575
618
|
const runId = crypto.randomUUID();
|
|
576
619
|
await createScriptRun({ id: runId, agentId: workerId, source: "inline", args: {} });
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
model: "claude-opus-5-5",
|
|
582
|
-
|
|
620
|
+
// The run's own harness calls this route with the run's agent id.
|
|
621
|
+
const res = await api(
|
|
622
|
+
"POST",
|
|
623
|
+
`/api/internal/script-runs/${runId}/agent-task`,
|
|
624
|
+
{ stepKey: "step-1", task: "script step", agentId: codexId, model: "claude-opus-5-5" },
|
|
625
|
+
{ "X-Agent-ID": workerId },
|
|
626
|
+
);
|
|
583
627
|
expect(res.status).toBe(400);
|
|
584
628
|
expect(String(res.body.error)).toContain("does not run on the codex harness");
|
|
585
629
|
});
|
|
@@ -766,3 +810,64 @@ describe("nan and helmcode catalog models", () => {
|
|
|
766
810
|
expect(res.status).toBe(200);
|
|
767
811
|
});
|
|
768
812
|
});
|
|
813
|
+
|
|
814
|
+
describe("ChatGPT subscription models (openai-codex/<id>)", () => {
|
|
815
|
+
let piId = "";
|
|
816
|
+
|
|
817
|
+
beforeAll(async () => {
|
|
818
|
+
piId = (
|
|
819
|
+
await createAgent({
|
|
820
|
+
name: "validation-pi-subscription",
|
|
821
|
+
isLead: false,
|
|
822
|
+
status: "idle",
|
|
823
|
+
harnessProvider: "pi",
|
|
824
|
+
})
|
|
825
|
+
).id;
|
|
826
|
+
});
|
|
827
|
+
|
|
828
|
+
test("known when the bare id is an openai catalog model", () => {
|
|
829
|
+
const catalog = { openai: { models: { "gpt-5.6-sol": {} } } };
|
|
830
|
+
expect(isKnownCatalogModel("openai-codex/gpt-5.6-sol", catalog)).toBe(true);
|
|
831
|
+
expect(isKnownCatalogModel("openai-codex/gpt-9", catalog)).toBe(false);
|
|
832
|
+
expect(isKnownCatalogModel("openai-codex/openai/gpt-5.6-sol", catalog)).toBe(false);
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
test("pi takes it; opencode, codex, claude and grok get a mismatch", async () => {
|
|
836
|
+
const model = "openai-codex/gpt-5.6-sol";
|
|
837
|
+
expect(await explicitModelError({ model, harnessProvider: "pi" })).toBeNull();
|
|
838
|
+
for (const harnessProvider of ["opencode", "codex", "claude", "grok"]) {
|
|
839
|
+
const error = await explicitModelError({ model, harnessProvider, allowCustomModel: true });
|
|
840
|
+
expect(error).toContain(`does not run on the ${harnessProvider} harness`);
|
|
841
|
+
}
|
|
842
|
+
expect(await explicitModelError({ model, harnessProvider: "codex" })).toContain(
|
|
843
|
+
'On codex, use "gpt-5.6-sol"',
|
|
844
|
+
);
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
test("send-task to a pi agent and the agent runtime PATCH accept it", async () => {
|
|
848
|
+
const sender = ownerCtx({ agentId: leadId });
|
|
849
|
+
const accepted = await sendTaskHandler(sender, {
|
|
850
|
+
task: "run on the chatgpt subscription",
|
|
851
|
+
offerMode: false,
|
|
852
|
+
allowDuplicate: false,
|
|
853
|
+
agentId: piId,
|
|
854
|
+
model: "openai-codex/gpt-5.6-sol",
|
|
855
|
+
routingReason: "human_pinned",
|
|
856
|
+
routingNote: "pinned to check the subscription model id",
|
|
857
|
+
});
|
|
858
|
+
expect({ ok: accepted.ok, message: accepted.message }).toEqual({
|
|
859
|
+
ok: true,
|
|
860
|
+
message: expect.any(String),
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
const res = await api("PATCH", `/api/agents/${piId}/runtime`, {
|
|
864
|
+
harness_provider: "pi",
|
|
865
|
+
model: "openai-codex/gpt-5.6-sol",
|
|
866
|
+
reasoning_effort: "high",
|
|
867
|
+
});
|
|
868
|
+
expect({ status: res.status, body: res.body }).toEqual({
|
|
869
|
+
status: 200,
|
|
870
|
+
body: expect.anything(),
|
|
871
|
+
});
|
|
872
|
+
});
|
|
873
|
+
});
|
|
@@ -24,6 +24,17 @@ describe("runContext", () => {
|
|
|
24
24
|
expect(context.subject?.includes("sk-ant-")).toBe(false);
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
+
test("steering records its source and sender agent without message content", () => {
|
|
28
|
+
expect(
|
|
29
|
+
runContext("pre.task.steer", {
|
|
30
|
+
task: { id: "task-1", agentId: "worker", task: "Private task" },
|
|
31
|
+
source: "mcp",
|
|
32
|
+
sender: { kind: "agent", agentId: "lead" },
|
|
33
|
+
message: "Private correction",
|
|
34
|
+
}),
|
|
35
|
+
).toEqual({ agentId: "lead", subject: "mcp: task task-1" });
|
|
36
|
+
});
|
|
37
|
+
|
|
27
38
|
test("post task events record the task's agent", () => {
|
|
28
39
|
expect(
|
|
29
40
|
runContext("post.task.completed", {
|
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { afterAll, beforeAll, beforeEach, describe, expect, spyOn, test } from "bun:test";
|
|
2
|
+
import { createServer, type Server } from "node:http";
|
|
3
|
+
import {
|
|
4
|
+
closeDb,
|
|
5
|
+
createAgent,
|
|
6
|
+
createTaskExtended,
|
|
7
|
+
createUser,
|
|
8
|
+
getChildTasks,
|
|
9
|
+
getDbClient,
|
|
10
|
+
getKv,
|
|
11
|
+
getLogsByTaskId,
|
|
12
|
+
getSteeringMessagesForTask,
|
|
13
|
+
getTaskById,
|
|
14
|
+
initDb,
|
|
15
|
+
pauseTask,
|
|
16
|
+
startTask,
|
|
17
|
+
} from "../be/db";
|
|
18
|
+
import { getExtensionByName, installExtension, listExtensionRuns } from "../be/extensions/db";
|
|
19
|
+
import { requestSteering, SteeringBlockedError } from "../be/steering";
|
|
20
|
+
import type { TaskSteerEvent } from "../extensions/contract";
|
|
21
|
+
import { enableExtension, stopExtensionRuntime } from "../extensions/lifecycle";
|
|
22
|
+
import { handleMcpBridge } from "../http/mcp-bridge";
|
|
23
|
+
import { handleTasks } from "../http/tasks";
|
|
24
|
+
import { SwarmConfig } from "../scripts-runtime/swarm-config";
|
|
25
|
+
import { createSwarmSdk } from "../scripts-runtime/swarm-sdk";
|
|
26
|
+
import * as slackApp from "../slack/app";
|
|
27
|
+
import { requestSlackThreadSteering } from "../slack/steering";
|
|
28
|
+
import { bufferThreadMessage, instantFlush } from "../slack/thread-buffer";
|
|
29
|
+
import { steerTaskHandler } from "../tools/steer-task";
|
|
30
|
+
import { ownerCtx } from "../tools/task-tool-ctx";
|
|
31
|
+
import { loadBundleFixture } from "./fixtures/extensions/load";
|
|
32
|
+
import { listenOnFreePort } from "./test-net";
|
|
33
|
+
|
|
34
|
+
const TEST_DB_PATH = `./test-extensions-steering-${process.pid}.sqlite`;
|
|
35
|
+
const ENV_KEYS = [
|
|
36
|
+
"STEERING_ENABLED",
|
|
37
|
+
"SLACK_THREAD_STEERING",
|
|
38
|
+
"SLACK_THREAD_STEERING_MODE",
|
|
39
|
+
"SLACK_RENDER_V2",
|
|
40
|
+
];
|
|
41
|
+
const savedEnv = new Map(ENV_KEYS.map((key) => [key, process.env[key]]));
|
|
42
|
+
let leadId: string;
|
|
43
|
+
let server: Server;
|
|
44
|
+
let baseUrl: string;
|
|
45
|
+
|
|
46
|
+
async function removeDbFiles() {
|
|
47
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
48
|
+
await Bun.file(TEST_DB_PATH + suffix)
|
|
49
|
+
.delete()
|
|
50
|
+
.catch(() => {});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function installPolicy(config: Record<string, unknown>, name = "steering-policy") {
|
|
55
|
+
const bundle = await loadBundleFixture("steering-policy");
|
|
56
|
+
const installed = await installExtension({
|
|
57
|
+
...bundle,
|
|
58
|
+
manifest: { ...bundle.manifest, name },
|
|
59
|
+
config,
|
|
60
|
+
});
|
|
61
|
+
return await enableExtension(installed.extension.id);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function runningTask() {
|
|
65
|
+
const task = await createTaskExtended("Existing task", {
|
|
66
|
+
agentId: leadId,
|
|
67
|
+
source: "slack",
|
|
68
|
+
slackChannelId: "C_STEER_POLICY",
|
|
69
|
+
slackThreadTs: "1000.0001",
|
|
70
|
+
});
|
|
71
|
+
await startTask(task.id);
|
|
72
|
+
return task;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function lastEvent(name = "steering-policy"): Promise<TaskSteerEvent | undefined> {
|
|
76
|
+
const extension = await getExtensionByName(name);
|
|
77
|
+
return (await getKv(`task:agent:${extension?.agentId}`, `ext:${name}:last`))?.value as
|
|
78
|
+
| TaskSteerEvent
|
|
79
|
+
| undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function expectNoSteering(taskId: string) {
|
|
83
|
+
expect(await getSteeringMessagesForTask(taskId)).toEqual([]);
|
|
84
|
+
expect(
|
|
85
|
+
(await getLogsByTaskId(taskId)).filter((log) => log.eventType === "task_steering"),
|
|
86
|
+
).toEqual([]);
|
|
87
|
+
expect(await getChildTasks(taskId)).toEqual([]);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
describe("extension steering boundary", () => {
|
|
91
|
+
beforeAll(async () => {
|
|
92
|
+
process.env.STEERING_ENABLED = "true";
|
|
93
|
+
process.env.SLACK_THREAD_STEERING = "lead";
|
|
94
|
+
process.env.SLACK_THREAD_STEERING_MODE = "queue";
|
|
95
|
+
process.env.SLACK_RENDER_V2 = "false";
|
|
96
|
+
await removeDbFiles();
|
|
97
|
+
initDb(TEST_DB_PATH);
|
|
98
|
+
server = createServer(async (req, res) => {
|
|
99
|
+
res.setHeader("Content-Type", "application/json");
|
|
100
|
+
const path = new URL(req.url ?? "/", "http://localhost").pathname.split("/").filter(Boolean);
|
|
101
|
+
if (await handleMcpBridge(req, res, path, new URLSearchParams(), leadId)) return;
|
|
102
|
+
await handleTasks(req, res, path, new URLSearchParams(), leadId);
|
|
103
|
+
});
|
|
104
|
+
baseUrl = `http://127.0.0.1:${await listenOnFreePort(server)}`;
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
beforeEach(async () => {
|
|
108
|
+
await stopExtensionRuntime();
|
|
109
|
+
const client = getDbClient();
|
|
110
|
+
await client.run("DELETE FROM agent_tasks");
|
|
111
|
+
await client.run("DELETE FROM extensions");
|
|
112
|
+
await client.run("DELETE FROM kv_entries WHERE key LIKE 'ext:%'");
|
|
113
|
+
await client.run("DELETE FROM agents");
|
|
114
|
+
leadId = (
|
|
115
|
+
await createAgent({
|
|
116
|
+
name: "Steering policy lead",
|
|
117
|
+
isLead: true,
|
|
118
|
+
status: "busy",
|
|
119
|
+
maxTasks: 10,
|
|
120
|
+
harnessProvider: "pi",
|
|
121
|
+
})
|
|
122
|
+
).id;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
afterAll(async () => {
|
|
126
|
+
await stopExtensionRuntime();
|
|
127
|
+
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
128
|
+
closeDb();
|
|
129
|
+
await removeDbFiles();
|
|
130
|
+
for (const [key, value] of savedEnv) {
|
|
131
|
+
if (value === undefined) delete process.env[key];
|
|
132
|
+
else process.env[key] = value;
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("continue records the task, source, mode and sender before queuing the original text", async () => {
|
|
137
|
+
await installPolicy({ action: "continue" });
|
|
138
|
+
const task = await runningTask();
|
|
139
|
+
const result = await requestSteering({
|
|
140
|
+
taskId: task.id,
|
|
141
|
+
message: "Add the charts",
|
|
142
|
+
mode: "steer",
|
|
143
|
+
source: "mcp",
|
|
144
|
+
createdByKind: "agent",
|
|
145
|
+
createdByAgentId: leadId,
|
|
146
|
+
});
|
|
147
|
+
expect(result.outcome).toBe("steered");
|
|
148
|
+
expect(await lastEvent()).toMatchObject({
|
|
149
|
+
task: { id: task.id, status: "in_progress" },
|
|
150
|
+
message: "Add the charts",
|
|
151
|
+
mode: "steer",
|
|
152
|
+
source: "mcp",
|
|
153
|
+
sender: { kind: "agent", agentId: leadId },
|
|
154
|
+
});
|
|
155
|
+
expect((await getSteeringMessagesForTask(task.id))[0]?.body).toBe("Add the charts");
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test("valid modifications flow through successive handlers and are the only text delivered", async () => {
|
|
159
|
+
await installPolicy({ action: "modify", prefix: "first: " });
|
|
160
|
+
await installPolicy({ action: "modify", prefix: "second: " }, "steering-policy-z");
|
|
161
|
+
const task = await runningTask();
|
|
162
|
+
await requestSteering({ taskId: task.id, message: "Add the charts" });
|
|
163
|
+
expect((await lastEvent("steering-policy-z"))?.message).toBe("first: Add the charts");
|
|
164
|
+
expect((await getSteeringMessagesForTask(task.id))[0]?.body).toBe(
|
|
165
|
+
"second: first: Add the charts",
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("block returns an actionable refusal without steering rows, logs or follow-up tasks", async () => {
|
|
170
|
+
const extension = await installPolicy({ action: "block" });
|
|
171
|
+
const task = await runningTask();
|
|
172
|
+
try {
|
|
173
|
+
await requestSteering({ taskId: task.id, message: "Do not deliver" });
|
|
174
|
+
throw new Error("Expected steering to be blocked");
|
|
175
|
+
} catch (error) {
|
|
176
|
+
expect(error).toBeInstanceOf(SteeringBlockedError);
|
|
177
|
+
expect(error).toMatchObject({
|
|
178
|
+
statusCode: 403,
|
|
179
|
+
extension: { id: extension.id },
|
|
180
|
+
reason: "Steering denied by policy",
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
await expectNoSteering(task.id);
|
|
184
|
+
expect((await listExtensionRuns(extension.id)).map((run) => run.action)).toEqual(["block"]);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
test("a blocked paused task is not resumed", async () => {
|
|
188
|
+
await installPolicy({ action: "block" });
|
|
189
|
+
const task = await runningTask();
|
|
190
|
+
await pauseTask(task.id);
|
|
191
|
+
await expect(
|
|
192
|
+
requestSteering({ taskId: task.id, message: "Do not resume" }),
|
|
193
|
+
).rejects.toBeInstanceOf(SteeringBlockedError);
|
|
194
|
+
expect((await getTaskById(task.id))?.status).toBe("paused");
|
|
195
|
+
await expectNoSteering(task.id);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
for (const action of ["empty", "retarget", "throw"]) {
|
|
199
|
+
test(`${action} fails open and records an extension error`, async () => {
|
|
200
|
+
const extension = await installPolicy({ action });
|
|
201
|
+
const task = await runningTask();
|
|
202
|
+
await requestSteering({ taskId: task.id, message: "Keep the original" });
|
|
203
|
+
expect((await getSteeringMessagesForTask(task.id))[0]?.body).toBe("Keep the original");
|
|
204
|
+
expect((await listExtensionRuns(extension.id))[0]?.action).toBe("error");
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
test("valid rewritten text is secret-scrubbed before storage", async () => {
|
|
209
|
+
const secret = `sk-ant-${"a".repeat(40)}`;
|
|
210
|
+
await installPolicy({ action: "modify", prefix: `${secret} ` });
|
|
211
|
+
const task = await runningTask();
|
|
212
|
+
await requestSteering({ taskId: task.id, message: "Keep this" });
|
|
213
|
+
const body = (await getSteeringMessagesForTask(task.id))[0]?.body;
|
|
214
|
+
expect(body).not.toContain(secret);
|
|
215
|
+
expect(body).toContain("Keep this");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("extension-originated steering skips its own handler", async () => {
|
|
219
|
+
const extension = await installPolicy({ action: "block" });
|
|
220
|
+
const task = await runningTask();
|
|
221
|
+
await requestSteering({
|
|
222
|
+
taskId: task.id,
|
|
223
|
+
message: "From the extension",
|
|
224
|
+
createdByKind: "agent",
|
|
225
|
+
createdByAgentId: extension.agentId!,
|
|
226
|
+
});
|
|
227
|
+
expect(await lastEvent()).toBeUndefined();
|
|
228
|
+
expect((await getSteeringMessagesForTask(task.id))[0]?.body).toBe("From the extension");
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("HTTP steering returns 403 for a policy block", async () => {
|
|
232
|
+
await installPolicy({ action: "block" });
|
|
233
|
+
const task = await runningTask();
|
|
234
|
+
const response = await fetch(`${baseUrl}/api/tasks/${task.id}/steer`, {
|
|
235
|
+
method: "POST",
|
|
236
|
+
headers: { "Content-Type": "application/json", "X-Agent-ID": leadId },
|
|
237
|
+
body: JSON.stringify({ message: "Do not deliver" }),
|
|
238
|
+
});
|
|
239
|
+
expect(response.status).toBe(403);
|
|
240
|
+
expect(await response.text()).toContain("Steering denied by policy");
|
|
241
|
+
await expectNoSteering(task.id);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("MCP steering reports a policy block as a tool error", async () => {
|
|
245
|
+
await installPolicy({ action: "block" });
|
|
246
|
+
const task = await runningTask();
|
|
247
|
+
const result = await steerTaskHandler(ownerCtx({ agentId: leadId }), {
|
|
248
|
+
taskId: task.id,
|
|
249
|
+
message: "Do not deliver",
|
|
250
|
+
});
|
|
251
|
+
expect(result.ok).toBe(false);
|
|
252
|
+
expect(result.message).toContain("Steering denied by policy");
|
|
253
|
+
expect(await lastEvent()).toMatchObject({ source: "mcp" });
|
|
254
|
+
await expectNoSteering(task.id);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("script SDK steering reaches the hook through the bridge with the script source", async () => {
|
|
258
|
+
await installPolicy({ action: "continue" });
|
|
259
|
+
const task = await runningTask();
|
|
260
|
+
const sdk = createSwarmSdk(
|
|
261
|
+
new SwarmConfig({
|
|
262
|
+
system: {
|
|
263
|
+
apiKey: { value: "steering-sdk-test-key", isSecret: true },
|
|
264
|
+
agentId: { value: leadId, isSecret: false },
|
|
265
|
+
mcpBaseUrl: { value: baseUrl, isSecret: false },
|
|
266
|
+
},
|
|
267
|
+
user: {},
|
|
268
|
+
}),
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
const result = await sdk.task_steer({ taskId: task.id, message: "From the script SDK" });
|
|
272
|
+
|
|
273
|
+
expect(result).toMatchObject({
|
|
274
|
+
success: true,
|
|
275
|
+
status: 200,
|
|
276
|
+
data: { success: true, outcome: "queued" },
|
|
277
|
+
});
|
|
278
|
+
expect(await lastEvent()).toMatchObject({
|
|
279
|
+
task: { id: task.id },
|
|
280
|
+
message: "From the script SDK",
|
|
281
|
+
mode: "queue",
|
|
282
|
+
source: "script",
|
|
283
|
+
sender: { kind: "agent", agentId: leadId },
|
|
284
|
+
});
|
|
285
|
+
expect(await getSteeringMessagesForTask(task.id)).toEqual([
|
|
286
|
+
expect.objectContaining({ source: "script", body: "From the script SDK" }),
|
|
287
|
+
]);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("Slack supplies message timestamps and sender metadata without acknowledgement log rows on block", async () => {
|
|
291
|
+
await installPolicy({ action: "block" });
|
|
292
|
+
const task = await runningTask();
|
|
293
|
+
const user = await createUser({ name: "Steering sender" });
|
|
294
|
+
await expect(
|
|
295
|
+
requestSlackThreadSteering({
|
|
296
|
+
channelId: "C_STEER_POLICY",
|
|
297
|
+
threadTs: "1000.0001",
|
|
298
|
+
message: "Do not deliver",
|
|
299
|
+
messageTimestamps: ["1000.0002", "1000.0003"],
|
|
300
|
+
requestedByUserId: user.id,
|
|
301
|
+
slackUserId: "U_SENDER",
|
|
302
|
+
}),
|
|
303
|
+
).rejects.toBeInstanceOf(SteeringBlockedError);
|
|
304
|
+
expect(await lastEvent()).toMatchObject({
|
|
305
|
+
source: "slack",
|
|
306
|
+
sender: { kind: "user", userId: user.id },
|
|
307
|
+
messageTimestamps: ["1000.0002", "1000.0003"],
|
|
308
|
+
slack: { channelId: "C_STEER_POLICY", threadTs: "1000.0001", userId: "U_SENDER" },
|
|
309
|
+
});
|
|
310
|
+
await expectNoSteering(task.id);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("a blocked buffer flush is consumed and later senders still get delivered", async () => {
|
|
314
|
+
await installPolicy({ action: "block", slackUserId: "U_BLOCKED" });
|
|
315
|
+
const task = await runningTask();
|
|
316
|
+
const acked: string[] = [];
|
|
317
|
+
const posted: string[] = [];
|
|
318
|
+
const appSpy = spyOn(slackApp, "getSlackApp").mockReturnValue({
|
|
319
|
+
client: {
|
|
320
|
+
users: { info: async () => ({ ok: false }) },
|
|
321
|
+
reactions: {
|
|
322
|
+
add: async (args: { timestamp: string; name: string }) => {
|
|
323
|
+
acked.push(`${args.timestamp}:${args.name}`);
|
|
324
|
+
return { ok: true };
|
|
325
|
+
},
|
|
326
|
+
remove: async () => ({ ok: true }),
|
|
327
|
+
},
|
|
328
|
+
chat: {
|
|
329
|
+
postMessage: async (args: { text: string }) => {
|
|
330
|
+
posted.push(args.text);
|
|
331
|
+
return { ok: true };
|
|
332
|
+
},
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
} as unknown as NonNullable<ReturnType<typeof slackApp.getSlackApp>>);
|
|
336
|
+
try {
|
|
337
|
+
bufferThreadMessage("C_STEER_POLICY", "1000.0001", "Drop this", "U_BLOCKED", "1000.0002");
|
|
338
|
+
bufferThreadMessage("C_STEER_POLICY", "1000.0001", "Keep this", "U_ALLOWED", "1000.0003");
|
|
339
|
+
await instantFlush("C_STEER_POLICY:1000.0001");
|
|
340
|
+
expect((await getSteeringMessagesForTask(task.id)).map((row) => row.body)).toEqual([
|
|
341
|
+
"Keep this",
|
|
342
|
+
]);
|
|
343
|
+
expect(acked).toContain("1000.0002:no_entry");
|
|
344
|
+
expect(posted).toHaveLength(1);
|
|
345
|
+
expect(await getChildTasks(task.id)).toEqual([]);
|
|
346
|
+
expect(
|
|
347
|
+
(await getLogsByTaskId(task.id)).filter((log) => log.newValue === "slack_reaction"),
|
|
348
|
+
).toEqual([expect.objectContaining({ metadata: expect.stringContaining("1000.0003") })]);
|
|
349
|
+
} finally {
|
|
350
|
+
appSpy.mockRestore();
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
});
|
|
@@ -29,6 +29,11 @@ describe("extension bundle validation", () => {
|
|
|
29
29
|
expect(await validateBundle(bundle)).toEqual({ ok: true, manifest: bundle.manifest });
|
|
30
30
|
});
|
|
31
31
|
|
|
32
|
+
test("accepts the steering event and message rewrite types", async () => {
|
|
33
|
+
const bundle = await loadBundleFixture("steering-policy");
|
|
34
|
+
expect(await validateBundle(bundle)).toEqual({ ok: true, manifest: bundle.manifest });
|
|
35
|
+
});
|
|
36
|
+
|
|
32
37
|
test("accepts zod and stdlib imports", async () => {
|
|
33
38
|
const bundle = await loadBundleFixture("minimal");
|
|
34
39
|
bundle.files["hooks.ts"] = `import { table } from "stdlib";
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { block, modify, type SwarmExtension } from "swarm-extension";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
export const config = z.object({
|
|
5
|
+
action: z.enum(["continue", "modify", "block", "empty", "retarget", "throw"]),
|
|
6
|
+
prefix: z.string().default("rewritten: "),
|
|
7
|
+
slackUserId: z.string().optional(),
|
|
8
|
+
});
|
|
9
|
+
const manifest = {
|
|
10
|
+
name: "steering-policy",
|
|
11
|
+
description: "Records, rewrites, or blocks steering messages",
|
|
12
|
+
version: "1.0.0",
|
|
13
|
+
runtime: "api",
|
|
14
|
+
assets: { hooks: "hooks.ts" },
|
|
15
|
+
config,
|
|
16
|
+
} as const;
|
|
17
|
+
|
|
18
|
+
const extension: SwarmExtension<typeof manifest> = (api) => {
|
|
19
|
+
api.on("pre.task.steer", async (event, ctx) => {
|
|
20
|
+
await ctx.state.set("last", event);
|
|
21
|
+
if (ctx.config.slackUserId && event.slack?.userId !== ctx.config.slackUserId) return;
|
|
22
|
+
switch (ctx.config.action) {
|
|
23
|
+
case "continue":
|
|
24
|
+
return { action: "continue" };
|
|
25
|
+
case "modify":
|
|
26
|
+
return modify({ message: ctx.config.prefix + event.message });
|
|
27
|
+
case "block":
|
|
28
|
+
return block("Steering denied by policy");
|
|
29
|
+
case "empty":
|
|
30
|
+
return modify({ message: " \n " });
|
|
31
|
+
case "retarget":
|
|
32
|
+
return modify({ message: "Unauthorized rewrite", mode: "steer" });
|
|
33
|
+
case "throw":
|
|
34
|
+
throw new Error("Steering policy failed");
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export default extension;
|