@desplega.ai/agent-swarm 1.150.0 → 1.152.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -0
- package/dist/{acp-adapter-d5mzggyk.js → acp-adapter-d05sg0q4.js} +5 -5
- package/dist/{actions-wn6ew0p7.js → actions-4sc0v342.js} +38 -11
- package/dist/{anthropic-messages-ec35ktvx.js → anthropic-messages-2y8nktar.js} +16 -16
- package/dist/{app-dty7p9hg.js → app-2d0v6kt6.js} +7 -7
- package/dist/{assistant-q4vhgjtx.js → assistant-zs6wdnrf.js} +41 -16
- package/dist/{azure-openai-responses-g85jv7m8.js → azure-openai-responses-f38x2avq.js} +13 -13
- package/dist/{boot-reembed-n15q6s9x.js → boot-reembed-3dwdnnsr.js} +13 -15
- package/dist/{boot-reembed-92292pgf.js → boot-reembed-vhh33520.js} +8 -8
- package/dist/{boot-scrub-logs-44pc955m.js → boot-scrub-logs-e1fnabcg.js} +5 -5
- package/dist/{claude-adapter-cjegjbje.js → claude-adapter-h58xqzzd.js} +12 -13
- package/dist/{claude-managed-adapter-p3ts5m9m.js → claude-managed-adapter-dsmkcrjy.js} +3 -3
- package/dist/{claude-sdk-session-7ywbn12v.js → claude-sdk-session-pbaczbgt.js} +12 -13
- package/dist/{cli-9gqddmxs.js → cli-4fqn2vpg.js} +8 -8
- package/dist/{cli-mzb0n642.js → cli-4wppfkcm.js} +350 -20
- package/dist/{cli-h505yhex.js → cli-5bqt4gwa.js} +2 -2
- package/dist/{cli-hbagregr.js → cli-5p007m2q.js} +88 -13
- package/dist/{cli-3ckzxdt1.js → cli-6k2athpf.js} +94 -81
- package/dist/{cli-3mh119qv.js → cli-72vksk1n.js} +9855 -693
- package/dist/{cli-taym3dmg.js → cli-87hdfyde.js} +13 -13
- package/dist/{cli-zthatyrx.js → cli-8fy9dmpv.js} +5 -5
- package/dist/{cli-asvrh7kj.js → cli-9xvdm3bx.js} +1 -1
- package/dist/{cli-72xdszyx.js → cli-awb59f08.js} +2 -2
- package/dist/{cli-vr6qj104.js → cli-cs9z1hw3.js} +1 -1
- package/dist/{cli-kt7wevqh.js → cli-cwvey7n1.js} +164 -56
- package/dist/{cli-5my3bjsd.js → cli-d3p82erb.js} +7 -7
- package/dist/{cli-7kwmt3r5.js → cli-evgy5w3y.js} +9 -3
- package/dist/{cli-111ey3ra.js → cli-f1f9n77e.js} +5 -5
- package/dist/{cli-ttje5a4z.js → cli-fhdssm2v.js} +105 -56
- package/dist/{cli-a45d5v9b.js → cli-fm4ytsdk.js} +2 -2
- package/dist/{cli-qp69dd5d.js → cli-gacq5hg8.js} +116 -14
- package/dist/{cli-hmyynq9b.js → cli-ht5rs63n.js} +1 -1
- package/dist/{cli-dyx0237s.js → cli-j4nv2wqy.js} +2 -4
- package/dist/{cli-gm0fhz13.js → cli-jh9txj4n.js} +2 -1
- package/dist/{cli-8chp8xab.js → cli-nd5s3m4y.js} +2 -2
- package/dist/{cli-zd21jrrc.js → cli-p2hx1pxm.js} +9872 -424
- package/dist/cli-pf9q3696.js +172 -0
- package/dist/{cli-7fesa1ky.js → cli-pgj4a5fb.js} +1 -1
- package/dist/{cli-07jd68a1.js → cli-re8z5p8p.js} +1 -1
- package/dist/{cli-ryz8q8p1.js → cli-t1mykkng.js} +40 -40
- package/dist/{cli-ck1vqzdj.js → cli-t6axtdcg.js} +9 -4
- package/dist/{cli-1mx65de9.js → cli-wbyk465z.js} +1 -1
- package/dist/{cli-kyysm7m3.js → cli-wp7cejrg.js} +1 -1
- package/dist/{cli-n75jt793.js → cli-z61ykvam.js} +6 -5
- package/dist/{cli-ceb5y160.js → cli-zdcdh02m.js} +1 -1
- package/dist/{cli-cmp8g3sw.js → cli-zfzznn7c.js} +45 -9
- package/dist/cli.js +22 -21
- package/dist/{codex-adapter-rn3aq7jm.js → codex-adapter-v3cz462q.js} +8 -9
- package/dist/{codex-hook-gthcmm0g.js → codex-hook-8jexc34x.js} +2 -2
- package/dist/{codex-session-runner-bge0cbsw.js → codex-session-runner-de1a5g5z.js} +8 -9
- package/dist/{commands-26phw0pc.js → commands-wfqqpn95.js} +5 -5
- package/dist/{db-jxjx6s1b.js → db-9zzv5y05.js} +5 -5
- package/dist/dispatcher-hfm1jbgx.js +74 -0
- package/dist/{google-generative-ai-tsskx7te.js → google-generative-ai-ndjmj5fa.js} +7 -7
- package/dist/{google-vertex-pkhpkbq9.js → google-vertex-fr665tqs.js} +10 -10
- package/dist/handlers-wrrhsack.js +65 -0
- package/dist/{hook-jw0ztn96.js → hook-9skye8zd.js} +9 -10
- package/dist/{hook-t6fqsggw.js → hook-fgjv62k5.js} +9 -10
- package/dist/{http-hyc7s57s.js → http-7grttjfq.js} +1434 -579
- package/dist/{index-bcyx5djw.js → index-29gxx9ar.js} +25 -17
- package/dist/{index-wtwpthe1.js → index-30pz2h9w.js} +25 -19
- package/dist/{index-1d9r6a74.js → index-4ek1df87.js} +26 -20
- package/dist/{index-sfhaq59d.js → index-myfwqqyq.js} +120 -102
- package/dist/index-pfhzrh1d.js +25 -0
- package/dist/{keepalive-fbsak4ya.js → keepalive-j75m2m7k.js} +7 -7
- package/dist/{lead-rdzdm627.js → lead-a1hpjt0a.js} +41 -49
- package/dist/{maintenance-25z1c1vf.js → maintenance-tngjzqdq.js} +8 -8
- package/dist/{node-209sxmg3.js → node-x1g4v0pq.js} +9 -9
- package/dist/{oauth-refresh-sweep-j3d7gtse.js → oauth-refresh-sweep-em1fn044.js} +6 -6
- package/dist/{onboard-dkcwrc75.js → onboard-fv7ezvwx.js} +13 -4
- package/dist/{openai-codex-responses-pm5gnyfa.js → openai-codex-responses-wy6a43wd.js} +11 -11
- package/dist/{openai-responses-vwvk1taj.js → openai-responses-6xwf7bzg.js} +13 -13
- package/dist/{opencode-adapter-dce4wskd.js → opencode-adapter-28axqa09.js} +4 -4
- package/dist/{otel-impl-8rgyy3j8.js → otel-impl-6vbb568y.js} +1 -1
- package/dist/{pi-mono-adapter-4s2bbkgq.js → pi-mono-adapter-jcqt71cg.js} +14 -16
- package/dist/{pricing-refresh-9cek71nq.js → pricing-refresh-nmm949an.js} +7 -7
- package/dist/{rbac-roles-vk8mqp9v.js → rbac-roles-0fz5qryx.js} +7 -7
- package/dist/{rbac-roles-z98s2ypy.js → rbac-roles-xtp05syf.js} +13 -9
- package/dist/{render-v2-3j26g0qh.js → render-v2-fpb8whme.js} +7 -7
- package/dist/{seed-pricing-7rxhcnzm.js → seed-pricing-zwa8bs3t.js} +6 -6
- package/dist/{setup-e2t7vz0c.js → setup-3ybwbdpd.js} +2 -2
- package/dist/{work-objects-8d3nd3x2.js → work-objects-rmayxcx7.js} +41 -16
- package/dist/{worker-0x9fn7nq.js → worker-rng4jhnh.js} +41 -49
- package/openapi.json +1351 -265
- package/package.json +2 -1
- package/src/agentmail/handlers.ts +95 -71
- package/src/be/db-client.ts +5 -0
- package/src/be/db.ts +12 -4
- package/src/be/extensions/db.ts +461 -0
- package/src/be/extensions/validate.ts +107 -0
- package/src/be/memory/boot-reembed.ts +17 -12
- package/src/be/memory/index.ts +12 -0
- package/src/be/memory/providers/openai-embedding.ts +4 -0
- package/src/be/memory/types.ts +2 -0
- package/src/be/migrations/155_extensions.sql +66 -0
- package/src/be/migrations/156_inbox_item_state_read_at.sql +7 -0
- package/src/be/rbac-roles.ts +3 -2
- package/src/be/scripts/typecheck.ts +56 -38
- package/src/be/seed-skills/index.ts +7 -0
- package/src/be/swarm-config-guard.ts +10 -2
- package/src/commands/onboard/compose-generator.ts +14 -2
- package/src/commands/onboard/env-generator.ts +7 -0
- package/src/commands/runner.ts +1 -1
- package/src/extensions/apply-task-create.ts +135 -0
- package/src/extensions/bundle-path.ts +10 -0
- package/src/extensions/contract-runtime.ts +7 -0
- package/src/extensions/contract-types.generated.ts +4 -0
- package/src/extensions/contract.ts +270 -0
- package/src/extensions/ctx.ts +186 -0
- package/src/extensions/dispatcher.ts +479 -0
- package/src/extensions/identity.ts +44 -0
- package/src/extensions/lifecycle.ts +303 -0
- package/src/extensions/loader.ts +207 -0
- package/src/extensions/post-bridge.ts +161 -0
- package/src/github/handlers.ts +185 -149
- package/src/gitlab/handlers.ts +97 -81
- package/src/heartbeat/heartbeat.ts +152 -83
- package/src/http/agents.ts +1 -1
- package/src/http/all-routes.ts +1 -0
- package/src/http/apps.ts +14 -10
- package/src/http/core.ts +34 -0
- package/src/http/extensions.ts +552 -0
- package/src/http/inbox-state.ts +2 -0
- package/src/http/index.ts +25 -0
- package/src/http/mcp-bridge.ts +96 -62
- package/src/http/mcp.ts +1 -1
- package/src/http/status.ts +6 -4
- package/src/http/tasks.ts +51 -35
- package/src/integrations/kapso/inbound.ts +15 -11
- package/src/jira/sync.ts +26 -18
- package/src/linear/sync.ts +26 -18
- package/src/prompts/session-templates.ts +5 -4
- package/src/rbac/admission.ts +1 -1
- package/src/rbac/elevated-agents.ts +24 -0
- package/src/rbac/legacy-policy.ts +47 -6
- package/src/rbac/permissions.ts +8 -0
- package/src/scheduler/deferred-task-waits.ts +133 -51
- package/src/scheduler/schedule-task.ts +50 -24
- package/src/scheduler/scheduler.ts +4 -2
- package/src/scripts-runtime/executors/native.ts +5 -1
- package/src/scripts-runtime/executors/types.ts +5 -0
- package/src/scripts-runtime/import-allowlist.ts +79 -19
- package/src/scripts-runtime/sdk-allowlist.ts +8 -0
- package/src/scripts-runtime/swarm-config.ts +8 -0
- package/src/scripts-runtime/swarm-sdk.ts +3 -0
- package/src/scripts-runtime/types/stdlib.d.ts +13 -0
- package/src/scripts-runtime/types/swarm-extension.d.ts +562 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +13 -0
- package/src/server.ts +18 -0
- package/src/slack/HEURISTICS.md +1 -1
- package/src/slack/actions.ts +21 -17
- package/src/slack/blocks.ts +1 -1
- package/src/slack/handlers.ts +98 -5
- package/src/slack/inbound-files.ts +6 -2
- package/src/slack/render-v2.ts +181 -39
- package/src/slack/responses.ts +7 -4
- package/src/slack/router.ts +13 -3
- package/src/slack/steering.ts +3 -3
- package/src/slack/task-output.ts +21 -0
- package/src/slack/watcher.ts +3 -2
- package/src/slack/work-objects.ts +2 -1
- package/src/tasks/errors.ts +9 -0
- package/src/tasks/sibling-awareness.ts +35 -1
- package/src/tasks/worker-follow-up.ts +64 -12
- package/src/tests/base-prompt.test.ts +1 -0
- package/src/tests/deferred-task-wake.test.ts +161 -9
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/env-flag.test.ts +1 -0
- package/src/tests/extensions-db.test.ts +216 -0
- package/src/tests/extensions-dispatcher.test.ts +275 -0
- package/src/tests/extensions-example-heartbeat-remediate.test.ts +69 -0
- package/src/tests/extensions-example-mcp-install.test.ts +63 -0
- package/src/tests/extensions-example-no-exclamation-marks.test.ts +72 -0
- package/src/tests/extensions-example-rewrite-and-suppress.test.ts +70 -0
- package/src/tests/extensions-example-route-slack-channel.test.ts +217 -0
- package/src/tests/extensions-heartbeat.test.ts +291 -0
- package/src/tests/extensions-http.test.ts +512 -0
- package/src/tests/extensions-lead-equivalence.test.ts +70 -0
- package/src/tests/extensions-lifecycle.test.ts +209 -0
- package/src/tests/extensions-loader.test.ts +95 -0
- package/src/tests/extensions-mcp-tools.test.ts +353 -0
- package/src/tests/extensions-pre-task.test.ts +487 -0
- package/src/tests/extensions-run-context.test.ts +41 -0
- package/src/tests/extensions-tool-call.test.ts +288 -0
- package/src/tests/extensions-validate.test.ts +196 -0
- package/src/tests/fixtures/extensions/bad-import/hooks.ts +8 -0
- package/src/tests/fixtures/extensions/bad-import/manifest.json +9 -0
- package/src/tests/fixtures/extensions/bad-return-shape/hooks.ts +10 -0
- package/src/tests/fixtures/extensions/bad-return-shape/manifest.json +9 -0
- package/src/tests/fixtures/extensions/block-tasks-from-source/hooks.ts +22 -0
- package/src/tests/fixtures/extensions/block-tasks-from-source/manifest.json +9 -0
- package/src/tests/fixtures/extensions/ignore-channel/hooks.ts +22 -0
- package/src/tests/fixtures/extensions/ignore-channel/manifest.json +9 -0
- package/src/tests/fixtures/extensions/load.ts +10 -0
- package/src/tests/fixtures/extensions/minimal/hooks.ts +7 -0
- package/src/tests/fixtures/extensions/minimal/manifest.json +9 -0
- package/src/tests/fixtures/extensions/never-fail-long-tasks/hooks.ts +11 -0
- package/src/tests/fixtures/extensions/never-fail-long-tasks/manifest.json +9 -0
- package/src/tests/fixtures/extensions/no-exclamation-marks/hooks.ts +23 -0
- package/src/tests/fixtures/extensions/no-exclamation-marks/manifest.json +9 -0
- package/src/tests/fixtures/extensions/post-logger/hooks.ts +9 -0
- package/src/tests/fixtures/extensions/post-logger/manifest.json +9 -0
- package/src/tests/fixtures/extensions/priority-a/hooks.ts +7 -0
- package/src/tests/fixtures/extensions/priority-a/manifest.json +9 -0
- package/src/tests/fixtures/extensions/priority-b/hooks.ts +7 -0
- package/src/tests/fixtures/extensions/priority-b/manifest.json +9 -0
- package/src/tests/fixtures/extensions/record-only-on-tag/hooks.ts +16 -0
- package/src/tests/fixtures/extensions/record-only-on-tag/manifest.json +9 -0
- package/src/tests/fixtures/extensions/reserved-assets/hooks.ts +5 -0
- package/src/tests/fixtures/extensions/reserved-assets/manifest.json +10 -0
- package/src/tests/fixtures/extensions/rewrite-progress-prefix/hooks.ts +22 -0
- package/src/tests/fixtures/extensions/rewrite-progress-prefix/manifest.json +9 -0
- package/src/tests/fixtures/extensions/rewrite-task-priority/hooks.ts +7 -0
- package/src/tests/fixtures/extensions/rewrite-task-priority/manifest.json +9 -0
- package/src/tests/fixtures/extensions/route-channel-to-agent/hooks.ts +26 -0
- package/src/tests/fixtures/extensions/route-channel-to-agent/manifest.json +9 -0
- package/src/tests/fixtures/extensions/slow/hooks.ts +10 -0
- package/src/tests/fixtures/extensions/slow/manifest.json +9 -0
- package/src/tests/fixtures/extensions/suppress-lead-follow-up/hooks.ts +12 -0
- package/src/tests/fixtures/extensions/suppress-lead-follow-up/manifest.json +9 -0
- package/src/tests/fixtures/extensions/throws/hooks.ts +12 -0
- package/src/tests/fixtures/extensions/throws/manifest.json +9 -0
- package/src/tests/fixtures/extensions/worker-runtime/hooks.ts +5 -0
- package/src/tests/fixtures/extensions/worker-runtime/manifest.json +9 -0
- package/src/tests/fixtures/store-progress-malformed-output.json +4 -0
- package/src/tests/memory-edit-authorization.test.ts +32 -0
- package/src/tests/multi-runtime-enabled-stats.test.ts +2 -2
- package/src/tests/multi-runtime-registration.test.ts +14 -14
- package/src/tests/onboard-compose.test.ts +44 -0
- package/src/tests/preload.ts +5 -0
- package/src/tests/rbac-admission-e2e.test.ts +35 -4
- package/src/tests/rbac-admission.test.ts +18 -3
- package/src/tests/rbac-e2e-helpers.ts +18 -0
- package/src/tests/rbac-engine.test.ts +39 -0
- package/src/tests/rbac-mcp-admission-e2e.test.ts +22 -4
- package/src/tests/realtime-room-auth.test.ts +1 -1
- package/src/tests/reload-config.test.ts +61 -0
- package/src/tests/routing-affinity-inventory.test.ts +1 -1
- package/src/tests/script-connections.test.ts +2 -0
- package/src/tests/scripts-boot-reembed.test.ts +4 -0
- package/src/tests/scripts-embeddings.test.ts +4 -0
- package/src/tests/scripts-import-allowlist.test.ts +44 -1
- package/src/tests/scripts-mcp-e2e.test.ts +80 -0
- package/src/tests/scripts-only-gating.test.ts +6 -0
- package/src/tests/scripts-runtime-identity.test.ts +1 -1
- package/src/tests/slack-blocks.test.ts +12 -1
- package/src/tests/slack-render-v2.test.ts +100 -5
- package/src/tests/slack-router.test.ts +17 -2
- package/src/tests/slack-steering.test.ts +19 -0
- package/src/tests/slack-task-output.test.ts +36 -0
- package/src/tests/status.test.ts +12 -0
- package/src/tests/steering-enabled.test.ts +6 -8
- package/src/tests/store-progress-attachments-handler.test.ts +100 -1
- package/src/tests/swarm-config-reserved-keys.test.ts +6 -1
- package/src/tests/task-tool-preload-mcp.test.ts +4 -1
- package/src/tests/tool-annotations.test.ts +1 -1
- package/src/tests/tool-registrar-no-input.test.ts +42 -1
- package/src/tools/defer-task.ts +1 -1
- package/src/tools/extension-activate-version.ts +38 -0
- package/src/tools/extension-common.ts +85 -0
- package/src/tools/extension-delete.ts +29 -0
- package/src/tools/extension-disable.ts +36 -0
- package/src/tools/extension-enable.ts +36 -0
- package/src/tools/extension-install.ts +65 -0
- package/src/tools/extension-list.ts +66 -0
- package/src/tools/script-common.ts +43 -18
- package/src/tools/send-task.ts +69 -107
- package/src/tools/store-progress.ts +30 -0
- package/src/tools/task-action.ts +43 -22
- package/src/tools/task-tool-ctx.ts +2 -0
- package/src/tools/tool-config.ts +7 -1
- package/src/tools/utils.ts +140 -16
- package/src/types.ts +133 -0
- package/src/utils/multi-runtime.ts +2 -2
- package/src/utils/steering-enabled.ts +4 -4
- package/src/workflows/executors/agent-task.ts +13 -3
- package/templates/extensions/README.md +17 -0
- package/templates/extensions/notify-on-complete/README.md +11 -0
- package/templates/extensions/notify-on-complete/hooks.ts +61 -0
- package/templates/extensions/notify-on-complete/manifest.json +7 -0
- package/templates/extensions/require-ticket-ref/README.md +11 -0
- package/templates/extensions/require-ticket-ref/hooks.ts +30 -0
- package/templates/extensions/require-ticket-ref/manifest.json +7 -0
- package/templates/extensions/require-verification-note/README.md +11 -0
- package/templates/extensions/require-verification-note/hooks.ts +32 -0
- package/templates/extensions/require-verification-note/manifest.json +7 -0
- package/templates/skills/agent-browser/SKILL.md +84 -0
- package/templates/skills/agent-browser/content.md +21 -0
- package/templates/skills/scheduling/content.md +2 -0
- package/templates/skills/swarm-extensions/config.json +14 -0
- package/templates/skills/swarm-extensions/content.md +120 -0
- package/dist/cli-b8x19vbh.js +0 -823
- package/dist/cli-n1yj5fd5.js +0 -164
- package/dist/cli-q04q68be.js +0 -3066
- package/dist/cli-qgm2pfpq.js +0 -6377
- package/dist/cli-rnsr8asm.js +0 -4323
- package/dist/cli-rwp3ksm7.js +0 -545
- package/dist/cli-xjb9wghj.js +0 -668
- package/dist/cli-xvkmedgn.js +0 -332
- package/dist/cli-ydk0wjj8.js +0 -338
- package/dist/handlers-vr1djg4n.js +0 -41
- package/dist/{claude-managed-setup-f5pmsyrp.js → claude-managed-setup-48dwg9bv.js} +3 -3
- package/dist/{cli-81r683bv.js → cli-ch38q5z4.js} +0 -0
- package/dist/{cli-wk0gjrn3.js → cli-kj76p1ez.js} +6 -6
- package/dist/{cli-c71mg51n.js → cli-r2fpe7cx.js} +5 -5
- package/dist/{cli-8qx7jnrk.js → cli-y6x0gqtg.js} +16 -16
- package/dist/{index-aa3sa6s8.js → index-2wsdsnmm.js} +1 -1
- package/dist/{mistral-conversations-4ryk7h83.js → mistral-conversations-qhr84ph3.js} +9 -9
- package/dist/{openai-completions-qdcmkfnr.js → openai-completions-32rdwmze.js} +17 -17
- package/dist/{openrouter-images-qx18fyp8.js → openrouter-images-9f9ek4rw.js} +3 -3
- package/dist/{pi-messages-79jvzz1d.js → pi-messages-b1mbr6gp.js} +4 -4
|
@@ -314,3 +314,21 @@ export async function waitForAuditCount(
|
|
|
314
314
|
}
|
|
315
315
|
return n;
|
|
316
316
|
}
|
|
317
|
+
|
|
318
|
+
/** Poll until audit rows satisfy `predicate` (writer flushes every 2s). */
|
|
319
|
+
export async function waitForAuditRows(
|
|
320
|
+
dbPath: string,
|
|
321
|
+
predicate: (rows: AuditRow[]) => boolean,
|
|
322
|
+
deadlineMs = 8_000,
|
|
323
|
+
): Promise<AuditRow[]> {
|
|
324
|
+
const start = Date.now();
|
|
325
|
+
let rows = readAuditRows(dbPath);
|
|
326
|
+
while (!predicate(rows)) {
|
|
327
|
+
if (Date.now() - start >= deadlineMs) {
|
|
328
|
+
throw new Error(`audit rows did not satisfy predicate within ${deadlineMs}ms`);
|
|
329
|
+
}
|
|
330
|
+
await Bun.sleep(250);
|
|
331
|
+
rows = readAuditRows(dbPath);
|
|
332
|
+
}
|
|
333
|
+
return rows;
|
|
334
|
+
}
|
|
@@ -81,6 +81,9 @@ const LEAD_ONLY_VERBS: PermissionVerb[] = [
|
|
|
81
81
|
"script.api.delete",
|
|
82
82
|
];
|
|
83
83
|
|
|
84
|
+
const OPERATOR_ONLY_VERBS: PermissionVerb[] = [];
|
|
85
|
+
const LEAD_OR_OPERATOR_VERBS: PermissionVerb[] = ["extension.write", "extension.activate"];
|
|
86
|
+
|
|
84
87
|
const LEAD_OR_RESOURCE_OWNER_VERBS: PermissionVerb[] = [
|
|
85
88
|
"memory.edit.any",
|
|
86
89
|
"skill.update.any",
|
|
@@ -171,6 +174,8 @@ describe("verb-group partition", () => {
|
|
|
171
174
|
...ANY_AUTHENTICATED_VERBS,
|
|
172
175
|
...REQUESTER_OWNS_TASK_VERBS,
|
|
173
176
|
...COMPOSITE_VERBS,
|
|
177
|
+
...OPERATOR_ONLY_VERBS,
|
|
178
|
+
...LEAD_OR_OPERATOR_VERBS,
|
|
174
179
|
];
|
|
175
180
|
expect(new Set(grouped).size).toBe(grouped.length);
|
|
176
181
|
expect(grouped.sort()).toEqual([...PERMISSION_VERBS].sort());
|
|
@@ -216,6 +221,40 @@ describe("lead-only verbs", () => {
|
|
|
216
221
|
});
|
|
217
222
|
});
|
|
218
223
|
|
|
224
|
+
describe("operator-or-user verbs", () => {
|
|
225
|
+
const expected: Expected = {
|
|
226
|
+
lead: false,
|
|
227
|
+
worker: false,
|
|
228
|
+
ownerWorker: false,
|
|
229
|
+
creatorWorker: false,
|
|
230
|
+
userRequester: true,
|
|
231
|
+
foreignUser: true,
|
|
232
|
+
operator: true,
|
|
233
|
+
};
|
|
234
|
+
for (const verb of OPERATOR_ONLY_VERBS) {
|
|
235
|
+
test(`${verb}: operator or user allowed`, () => {
|
|
236
|
+
expectDecisions(verb, { kind: "none" }, expected);
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe("lead-or-operator-or-user verbs", () => {
|
|
242
|
+
const expected: Expected = {
|
|
243
|
+
lead: true,
|
|
244
|
+
worker: false,
|
|
245
|
+
ownerWorker: false,
|
|
246
|
+
creatorWorker: false,
|
|
247
|
+
userRequester: true,
|
|
248
|
+
foreignUser: true,
|
|
249
|
+
operator: true,
|
|
250
|
+
};
|
|
251
|
+
for (const verb of LEAD_OR_OPERATOR_VERBS) {
|
|
252
|
+
test(`${verb}: lead, operator, or user allowed`, () => {
|
|
253
|
+
expectDecisions(verb, { kind: "none" }, expected);
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
});
|
|
257
|
+
|
|
219
258
|
describe("lead-or-resource-owner verbs", () => {
|
|
220
259
|
const expected: Expected = {
|
|
221
260
|
lead: true,
|
|
@@ -12,12 +12,11 @@ import {
|
|
|
12
12
|
countAuditRows,
|
|
13
13
|
LEAD,
|
|
14
14
|
makeScratchDir,
|
|
15
|
-
readAuditRows,
|
|
16
15
|
registerAgent,
|
|
17
16
|
removeScratchDir,
|
|
18
17
|
type SwarmServer,
|
|
19
18
|
spawnSwarmServer,
|
|
20
|
-
|
|
19
|
+
waitForAuditRows,
|
|
21
20
|
} from "./rbac-e2e-helpers";
|
|
22
21
|
|
|
23
22
|
setDefaultTimeout(120_000);
|
|
@@ -312,8 +311,27 @@ describe("RBAC admission over /mcp-user", () => {
|
|
|
312
311
|
}),
|
|
313
312
|
);
|
|
314
313
|
|
|
315
|
-
|
|
316
|
-
|
|
314
|
+
const mcpRows = (
|
|
315
|
+
await waitForAuditRows(enabledDbPath, (rows) => {
|
|
316
|
+
const auditRows = rows.filter((row) => row.source === "mcp");
|
|
317
|
+
return (
|
|
318
|
+
auditRows.some(
|
|
319
|
+
(row) =>
|
|
320
|
+
row.principalId === requester.userId &&
|
|
321
|
+
row.resourceType === "mcp-tool" &&
|
|
322
|
+
row.resourceId === "send-task" &&
|
|
323
|
+
row.verb === "task.create.own" &&
|
|
324
|
+
row.decision === "allow",
|
|
325
|
+
) &&
|
|
326
|
+
["task.create.own", "task.cancel.own", "task.action.own"].every((verb) =>
|
|
327
|
+
auditRows.some(
|
|
328
|
+
(row) =>
|
|
329
|
+
row.principalId === empty.userId && row.decision === "deny" && row.verb === verb,
|
|
330
|
+
),
|
|
331
|
+
)
|
|
332
|
+
);
|
|
333
|
+
})
|
|
334
|
+
).filter((row) => row.source === "mcp");
|
|
317
335
|
expect(
|
|
318
336
|
mcpRows.some(
|
|
319
337
|
(row) =>
|
|
@@ -44,7 +44,7 @@ describe("realtime room namespace guards", () => {
|
|
|
44
44
|
|
|
45
45
|
test("allows page writes for user and operator contexts when RBAC is disabled", async () => {
|
|
46
46
|
const previous = process.env.RBAC_ENABLED;
|
|
47
|
-
|
|
47
|
+
process.env.RBAC_ENABLED = "false";
|
|
48
48
|
try {
|
|
49
49
|
await expect(
|
|
50
50
|
authorizeRoomNamespace(
|
|
@@ -3,6 +3,7 @@ import { unlink } from "node:fs/promises";
|
|
|
3
3
|
import { createServer as createHttpServer, type Server } from "node:http";
|
|
4
4
|
import { initAgentMail, resetAgentMail } from "../agentmail";
|
|
5
5
|
import { closeDb, deleteSwarmConfig, getDbClient, initDb, upsertSwarmConfig } from "../be/db";
|
|
6
|
+
import { getEmbeddingProvider, resetEmbeddingProvider } from "../be/memory";
|
|
6
7
|
import { initGitHub, resetGitHub } from "../github";
|
|
7
8
|
import {
|
|
8
9
|
__resetInjectedEnvTracking,
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
10
11
|
_resetAutoReloadForTests,
|
|
11
12
|
flushPendingIntegrationsReload,
|
|
12
13
|
loadGlobalConfigsIntoEnv,
|
|
14
|
+
reloadGlobalConfigsAndIntegrations,
|
|
13
15
|
scheduleIntegrationsReload,
|
|
14
16
|
} from "../http/core";
|
|
15
17
|
import { isOriginAllowedForCredentials } from "../http/utils";
|
|
@@ -225,6 +227,23 @@ describe("reload-config", () => {
|
|
|
225
227
|
}
|
|
226
228
|
});
|
|
227
229
|
|
|
230
|
+
test("stored lead activation gate cannot override the deployment at boot or reload", async () => {
|
|
231
|
+
const key = "EXTENSION_ALLOW_LEAD_ACTIVATION";
|
|
232
|
+
const original = process.env[key];
|
|
233
|
+
const rowId = await insertLegacyReservedRow(key, "true");
|
|
234
|
+
try {
|
|
235
|
+
process.env[key] = "false";
|
|
236
|
+
for (const override of [false, true]) {
|
|
237
|
+
expect(await loadGlobalConfigsIntoEnv(override)).not.toContain(key);
|
|
238
|
+
expect(process.env[key]).toBe("false");
|
|
239
|
+
}
|
|
240
|
+
} finally {
|
|
241
|
+
await deleteSwarmConfig(rowId);
|
|
242
|
+
if (original === undefined) delete process.env[key];
|
|
243
|
+
else process.env[key] = original;
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
228
247
|
test("loadGlobalConfigsIntoEnv skips unreadable reserved secret rows before decrypting them", async () => {
|
|
229
248
|
const id = await insertUnreadableReservedSecretRow("SECRETS_ENCRYPTION_KEY");
|
|
230
249
|
|
|
@@ -262,6 +281,48 @@ describe("reload-config", () => {
|
|
|
262
281
|
expect(res.status).toBe(404);
|
|
263
282
|
});
|
|
264
283
|
|
|
284
|
+
// Regression: EMBEDDING_API_KEY / OPENAI_API_KEY used to be read once at
|
|
285
|
+
// OpenAIEmbeddingProvider construction and never revisited, so a key set
|
|
286
|
+
// (or rotated) via the dashboard/API stayed inert until the API process
|
|
287
|
+
// restarted (customer-reported, Jan Carbonell 2026-09-16: valid key,
|
|
288
|
+
// reload reported it loaded, embeddings still stayed off). Assert the
|
|
289
|
+
// full path: no key -> provider unconfigured -> config write + reload ->
|
|
290
|
+
// provider live, without a restart.
|
|
291
|
+
test("reload turns embeddings on without a restart once a key lands via config", async () => {
|
|
292
|
+
const originalEmbeddingKey = process.env.EMBEDDING_API_KEY;
|
|
293
|
+
const originalOpenAiKey = process.env.OPENAI_API_KEY;
|
|
294
|
+
delete process.env.EMBEDDING_API_KEY;
|
|
295
|
+
delete process.env.OPENAI_API_KEY;
|
|
296
|
+
resetEmbeddingProvider();
|
|
297
|
+
expect(getEmbeddingProvider().isConfigured()).toBe(false);
|
|
298
|
+
|
|
299
|
+
const config = await upsertSwarmConfig({
|
|
300
|
+
scope: "global",
|
|
301
|
+
key: "EMBEDDING_API_KEY",
|
|
302
|
+
value: "sk-test-not-a-real-key",
|
|
303
|
+
isSecret: true,
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
try {
|
|
307
|
+
const result = await reloadGlobalConfigsAndIntegrations();
|
|
308
|
+
expect(result.keysUpdated).toContain("EMBEDDING_API_KEY");
|
|
309
|
+
expect(result.integrationsReinitialized).toContain("embeddings");
|
|
310
|
+
expect(getEmbeddingProvider().isConfigured()).toBe(true);
|
|
311
|
+
} finally {
|
|
312
|
+
await deleteSwarmConfig(config.id);
|
|
313
|
+
delete process.env.EMBEDDING_API_KEY;
|
|
314
|
+
if (originalOpenAiKey === undefined) {
|
|
315
|
+
delete process.env.OPENAI_API_KEY;
|
|
316
|
+
} else {
|
|
317
|
+
process.env.OPENAI_API_KEY = originalOpenAiKey;
|
|
318
|
+
}
|
|
319
|
+
resetEmbeddingProvider();
|
|
320
|
+
if (originalEmbeddingKey !== undefined) {
|
|
321
|
+
process.env.EMBEDDING_API_KEY = originalEmbeddingKey;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
});
|
|
325
|
+
|
|
265
326
|
// Regression: injection used to be one-way. Deleting a global row left the
|
|
266
327
|
// previously-injected value live in process.env until the process restarted,
|
|
267
328
|
// so "reset to default" in the dashboard silently did nothing.
|
|
@@ -101,7 +101,7 @@ const EXPECTED_INVENTORY: InventoryEntry[] = [
|
|
|
101
101
|
file: "src/tools/send-task.ts",
|
|
102
102
|
owner: "sendTaskHandler",
|
|
103
103
|
syntax: "PropertyAssignment",
|
|
104
|
-
count:
|
|
104
|
+
count: 1,
|
|
105
105
|
kind: "producer",
|
|
106
106
|
dispatchProof: {
|
|
107
107
|
file: "src/tests/task-tools-ctx.test.ts",
|
|
@@ -570,6 +570,8 @@ describe("script connections", () => {
|
|
|
570
570
|
markMigrationApplied(database, "151_repair_scheduled_task_required_params.sql");
|
|
571
571
|
// 152 indexes task routing context absent from this migration-112-only fixture.
|
|
572
572
|
markMigrationApplied(database, "152_routing_decisions.sql");
|
|
573
|
+
// 156 alters inbox_item_state, which this migration-112-only fixture does not create.
|
|
574
|
+
markMigrationApplied(database, "156_inbox_item_state_read_at.sql");
|
|
573
575
|
|
|
574
576
|
const id = crypto.randomUUID();
|
|
575
577
|
const now = new Date().toISOString();
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
-
import { validateScriptImports } from "../scripts-runtime/import-allowlist";
|
|
2
|
+
import { checkImportAllowlist, validateScriptImports } from "../scripts-runtime/import-allowlist";
|
|
3
3
|
|
|
4
4
|
describe("script import allowlist", () => {
|
|
5
5
|
test("allows relative imports and runtime barrels", () => {
|
|
@@ -61,6 +61,49 @@ describe("script import allowlist", () => {
|
|
|
61
61
|
if (!result.ok) expect(result.diagnostic).toContain("fs");
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
+
test("allows literal dynamic imports from the existing script allowlist", () => {
|
|
65
|
+
expect(validateScriptImports("export default async () => import('zod')")).toEqual({
|
|
66
|
+
ok: true,
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("allows computed dynamic imports by default", () => {
|
|
71
|
+
expect(validateScriptImports("const x = 'zod'; export default async () => import(x)")).toEqual({
|
|
72
|
+
ok: true,
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("strictDynamic rejects computed dynamic imports and requires", () => {
|
|
77
|
+
for (const source of [
|
|
78
|
+
"const name = 'zod'; export default async () => import(name)",
|
|
79
|
+
"const name = 'zod'; export default () => require(name)",
|
|
80
|
+
]) {
|
|
81
|
+
const result = checkImportAllowlist(source, {
|
|
82
|
+
allowedBare: ["swarm-extension", "stdlib", "zod"],
|
|
83
|
+
allowRelative: false,
|
|
84
|
+
strictDynamic: true,
|
|
85
|
+
});
|
|
86
|
+
expect(result.ok).toBe(false);
|
|
87
|
+
if (!result.ok) expect(result.diagnostic).toContain("Computed");
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("strictDynamic checks require and import-equals specifiers", () => {
|
|
92
|
+
for (const source of [
|
|
93
|
+
"export default () => require('node:fs')",
|
|
94
|
+
"import fs = require('node:fs'); export default () => fs",
|
|
95
|
+
]) {
|
|
96
|
+
expect(validateScriptImports(source)).toEqual({ ok: true });
|
|
97
|
+
const result = checkImportAllowlist(source, {
|
|
98
|
+
allowedBare: ["swarm-extension", "stdlib", "zod"],
|
|
99
|
+
allowRelative: false,
|
|
100
|
+
strictDynamic: true,
|
|
101
|
+
});
|
|
102
|
+
expect(result.ok).toBe(false);
|
|
103
|
+
if (!result.ok) expect(result.diagnostic).toContain("node:fs");
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
64
107
|
test("rejects Function constructor dynamic import bypasses", () => {
|
|
65
108
|
const result = validateScriptImports(
|
|
66
109
|
`export default async () => new Function("return import('node:fs')")()`,
|
|
@@ -10,6 +10,7 @@ import { handleCore } from "../http/core";
|
|
|
10
10
|
import { handleScriptRuns } from "../http/script-runs";
|
|
11
11
|
import { handleScripts } from "../http/scripts";
|
|
12
12
|
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
13
|
+
import { proxyScriptsApi } from "../tools/script-common";
|
|
13
14
|
import { registerScriptDeleteTool } from "../tools/script-delete";
|
|
14
15
|
import { registerScriptRunTool } from "../tools/script-run";
|
|
15
16
|
import { registerScriptRunsTools } from "../tools/script-runs";
|
|
@@ -221,6 +222,85 @@ beforeEach(async () => {
|
|
|
221
222
|
});
|
|
222
223
|
|
|
223
224
|
describe("script_ MCP HTTP proxy tools", () => {
|
|
225
|
+
test.each([200, 502])("rejects HTML responses with HTTP %i", async (status) => {
|
|
226
|
+
const tools = buildToolServer();
|
|
227
|
+
const html = "<!doctype html><html><body>Static site fallback</body></html>";
|
|
228
|
+
const previousFetch = globalThis.fetch;
|
|
229
|
+
globalThis.fetch = (async () =>
|
|
230
|
+
new Response(html, {
|
|
231
|
+
status,
|
|
232
|
+
headers: { "Content-Type": "text/html; charset=utf-8" },
|
|
233
|
+
})) as typeof globalThis.fetch;
|
|
234
|
+
try {
|
|
235
|
+
const result = (await tools.listScriptRuns.handler(
|
|
236
|
+
{ limit: 10, offset: 0 },
|
|
237
|
+
meta(workerId),
|
|
238
|
+
)) as StructuredResult<unknown>;
|
|
239
|
+
|
|
240
|
+
expect(result.isError).toBe(true);
|
|
241
|
+
expect(result.structuredContent.success).toBe(false);
|
|
242
|
+
expect(result.structuredContent.status).toBe(status);
|
|
243
|
+
expect(result.structuredContent.message).toContain("non-JSON");
|
|
244
|
+
expect(result.structuredContent.message).toContain(`HTTP ${status}`);
|
|
245
|
+
expect(result.structuredContent.message).toContain("text/html");
|
|
246
|
+
expect(result.content[0]?.text).toContain(html);
|
|
247
|
+
expect(result.content[0]?.text).not.toContain("Found 0 script run(s)");
|
|
248
|
+
} finally {
|
|
249
|
+
globalThis.fetch = previousFetch;
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
test("caps invalid JSON diagnostics without retaining the full body", async () => {
|
|
254
|
+
const body = `<html>${"x".repeat(20_000)}BODY_TAIL</html>`;
|
|
255
|
+
const previousFetch = globalThis.fetch;
|
|
256
|
+
globalThis.fetch = (async () =>
|
|
257
|
+
new Response(body, {
|
|
258
|
+
status: 200,
|
|
259
|
+
headers: { "Content-Type": "application/json" },
|
|
260
|
+
})) as typeof globalThis.fetch;
|
|
261
|
+
try {
|
|
262
|
+
const result = await proxyScriptsApi({
|
|
263
|
+
method: "GET",
|
|
264
|
+
path: "/api/script-runs",
|
|
265
|
+
requestInfo: {
|
|
266
|
+
sessionId: "s",
|
|
267
|
+
agentId: workerId,
|
|
268
|
+
runtimeInstanceId: undefined,
|
|
269
|
+
sourceTaskId: undefined,
|
|
270
|
+
contextKey: undefined,
|
|
271
|
+
callOrigin: "mcp",
|
|
272
|
+
},
|
|
273
|
+
successMessage: () => {
|
|
274
|
+
throw new Error("Invalid JSON must not reach the success callback");
|
|
275
|
+
},
|
|
276
|
+
uncappedDetails: true,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
expect(result.ok).toBe(false);
|
|
280
|
+
expect(result.message).toContain("application/json");
|
|
281
|
+
expect(result.details).toContain("<html>");
|
|
282
|
+
expect(result.details).toContain("[truncated");
|
|
283
|
+
expect(result.details?.length).toBeLessThan(16_100);
|
|
284
|
+
expect(JSON.stringify(result)).not.toContain("BODY_TAIL");
|
|
285
|
+
} finally {
|
|
286
|
+
globalThis.fetch = previousFetch;
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test("reports a legitimate JSON empty run list as success", async () => {
|
|
291
|
+
const tools = buildToolServer();
|
|
292
|
+
const result = (await tools.listScriptRuns.handler(
|
|
293
|
+
{ limit: 10, offset: 0 },
|
|
294
|
+
meta(workerId),
|
|
295
|
+
)) as StructuredResult<{ total: number; runs: unknown[] }>;
|
|
296
|
+
|
|
297
|
+
expect(result.isError).toBeFalsy();
|
|
298
|
+
expect(result.structuredContent.success).toBe(true);
|
|
299
|
+
expect(result.structuredContent.status).toBe(200);
|
|
300
|
+
expect(result.structuredContent.data?.runs).toEqual([]);
|
|
301
|
+
expect(result.structuredContent.message).toBe("Found 0 script run(s).");
|
|
302
|
+
});
|
|
303
|
+
|
|
224
304
|
test("upserts the canonical script authoring contract example verbatim", async () => {
|
|
225
305
|
const tools = buildToolServer();
|
|
226
306
|
// The authoring contract lives in the seeded `swarm-scripts` skill since
|
|
@@ -17,6 +17,12 @@ import { listenOnFreePort } from "./test-net";
|
|
|
17
17
|
|
|
18
18
|
const TEST_DB_PATH = "./test-scripts-only-gating.sqlite";
|
|
19
19
|
const SCRIPT_TOOL_NAMES = [
|
|
20
|
+
"extension-delete",
|
|
21
|
+
"extension-enable",
|
|
22
|
+
"extension-disable",
|
|
23
|
+
"extension-activate-version",
|
|
24
|
+
"extension-install",
|
|
25
|
+
"extension-list",
|
|
20
26
|
"get-script-run",
|
|
21
27
|
"launch-script-run",
|
|
22
28
|
"list-script-runs",
|
|
@@ -374,7 +374,7 @@ describe("script SDK dispatch hits the real runtime gates", () => {
|
|
|
374
374
|
});
|
|
375
375
|
|
|
376
376
|
test("with the flag off, the SDK dispatches without runtime identity as before", async () => {
|
|
377
|
-
|
|
377
|
+
process.env.MULTI_RUNTIME_ENABLED = "false";
|
|
378
378
|
const agentId = await makeAgent(1);
|
|
379
379
|
const task = await createTaskExtended("legacy-claim-work");
|
|
380
380
|
|
|
@@ -298,8 +298,10 @@ describe("buildAssignmentSummaryBlocks", () => {
|
|
|
298
298
|
failed: [{ agentName: "Delta", reason: "error" }],
|
|
299
299
|
});
|
|
300
300
|
|
|
301
|
+
expect(blocks).toHaveLength(1);
|
|
302
|
+
expect(blocks[0].text.text).not.toBe("");
|
|
301
303
|
expect(blocks[0].text.text).toContain("⚠️");
|
|
302
|
-
expect(blocks[0].text.text).toContain("Could not assign");
|
|
304
|
+
expect(blocks[0].text.text).toContain("Could not assign to:");
|
|
303
305
|
});
|
|
304
306
|
});
|
|
305
307
|
|
|
@@ -463,6 +465,15 @@ function makeTaskId(prefix: string): string {
|
|
|
463
465
|
}
|
|
464
466
|
|
|
465
467
|
describe("buildTreeBlocks", () => {
|
|
468
|
+
test("empty tree omits the section with empty text", () => {
|
|
469
|
+
const blocks = buildTreeBlocks([]);
|
|
470
|
+
|
|
471
|
+
expect(blocks).toHaveLength(0);
|
|
472
|
+
expect(blocks.every((block) => block.type !== "section" || block.text.text.length > 0)).toBe(
|
|
473
|
+
true,
|
|
474
|
+
);
|
|
475
|
+
});
|
|
476
|
+
|
|
466
477
|
test("single root, no children", () => {
|
|
467
478
|
const root: TreeNode = {
|
|
468
479
|
taskId: makeTaskId("aaaa0001"),
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
completeTask,
|
|
7
7
|
createAgent,
|
|
8
8
|
createLogEntry,
|
|
9
|
+
createScheduledTask,
|
|
9
10
|
createTaskExtended,
|
|
10
11
|
ensureSlackDelegationActivation,
|
|
11
12
|
ensureSlackRenderV2Activation,
|
|
@@ -26,6 +27,7 @@ import {
|
|
|
26
27
|
supersedeTask,
|
|
27
28
|
upsertSwarmConfig,
|
|
28
29
|
} from "../be/db";
|
|
30
|
+
import { createStandaloneScheduleTask } from "../scheduler/schedule-task";
|
|
29
31
|
import { getTaskLink, MAX_SECTION_LENGTH } from "../slack/blocks";
|
|
30
32
|
import {
|
|
31
33
|
_resetSlackRenderV2ForTests,
|
|
@@ -300,12 +302,83 @@ describe("Slack renderer v2", () => {
|
|
|
300
302
|
});
|
|
301
303
|
});
|
|
302
304
|
|
|
303
|
-
|
|
305
|
+
for (const delegationEnabled of [false, true]) {
|
|
306
|
+
test(`relays a deferred schedule continuation with delegation=${delegationEnabled}`, async () => {
|
|
307
|
+
process.env.SLACK_RENDER_V2_DELEGATION = String(delegationEnabled);
|
|
308
|
+
if (delegationEnabled) await ensureSlackDelegationActivation();
|
|
309
|
+
const lead = await createAgent({ name: "Continuation Lead", isLead: true, status: "idle" });
|
|
310
|
+
const { channelId, threadTs } = uniqueSlackAddress("C_CONTINUATION");
|
|
311
|
+
const ask = await createTaskExtended("answer when ready", {
|
|
312
|
+
agentId: lead.id,
|
|
313
|
+
source: "slack",
|
|
314
|
+
slackChannelId: channelId,
|
|
315
|
+
slackThreadTs: threadTs,
|
|
316
|
+
contextKey: slackContextKey({ channelId, threadTs }),
|
|
317
|
+
});
|
|
318
|
+
await startTask(ask.id);
|
|
319
|
+
await completeTask(ask.id, "Waiting for the result.", { addTags: ["deferred"] });
|
|
320
|
+
await backdateLastUpdated([ask.id], 20);
|
|
321
|
+
await processSlackRenderV2();
|
|
322
|
+
expect((await getSlackOutcomeMessage(ask.id))?.finalizedAt).toBeTruthy();
|
|
323
|
+
|
|
324
|
+
const schedule = await createScheduledTask({
|
|
325
|
+
name: "deferred-answer",
|
|
326
|
+
scheduleType: "one_time",
|
|
327
|
+
taskTemplate: "Deliver the result",
|
|
328
|
+
taskType: "deferred",
|
|
329
|
+
tags: ["deferred"],
|
|
330
|
+
parentTaskId: ask.id,
|
|
331
|
+
targetAgentId: lead.id,
|
|
332
|
+
createdByAgentId: lead.id,
|
|
333
|
+
nextRunAt: new Date().toISOString(),
|
|
334
|
+
});
|
|
335
|
+
const continuation = await createStandaloneScheduleTask(schedule);
|
|
336
|
+
expect(continuation.source).toBe("schedule");
|
|
337
|
+
expect(continuation.scheduleId).toBe(schedule.id);
|
|
338
|
+
expect(continuation.slackChannelId).toBe(channelId);
|
|
339
|
+
expect(continuation.slackThreadTs).toBe(threadTs);
|
|
340
|
+
await startTask(continuation.id);
|
|
341
|
+
await processSlackRenderV2();
|
|
342
|
+
expect(await getSlackOutcomeMessage(continuation.id)).toBeNull();
|
|
343
|
+
await completeTask(continuation.id, "Here is the complete answer.");
|
|
344
|
+
calls.length = 0;
|
|
345
|
+
|
|
346
|
+
await processSlackRenderV2();
|
|
347
|
+
await processSlackRenderV2();
|
|
348
|
+
|
|
349
|
+
expect((await getSlackOutcomeMessage(continuation.id))?.finalizedAt).toBeTruthy();
|
|
350
|
+
const streams = calls.filter((call) => call.method === "chat.startStream");
|
|
351
|
+
expect(streams).toHaveLength(1);
|
|
352
|
+
expect(streams[0]?.payload).toMatchObject({
|
|
353
|
+
channel: channelId,
|
|
354
|
+
thread_ts: threadTs,
|
|
355
|
+
markdown_text: "✅\n\nHere is the complete answer.",
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
test("does not relay an ordinary scheduled task without Slack context", async () => {
|
|
361
|
+
const schedule = await createScheduledTask({
|
|
362
|
+
name: "ordinary-maintenance",
|
|
363
|
+
scheduleType: "one_time",
|
|
364
|
+
taskTemplate: "Run maintenance",
|
|
365
|
+
taskType: "maintenance",
|
|
366
|
+
nextRunAt: new Date().toISOString(),
|
|
367
|
+
});
|
|
368
|
+
const task = await createStandaloneScheduleTask(schedule);
|
|
369
|
+
await startTask(task.id);
|
|
370
|
+
await completeTask(task.id, "Maintenance completed.");
|
|
371
|
+
await processSlackRenderV2();
|
|
372
|
+
expect(await getSlackOutcomeMessage(task.id)).toBeNull();
|
|
373
|
+
expect(calls).toHaveLength(0);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
test("defaults on and accepts an explicit opt-out", () => {
|
|
304
377
|
const previous = process.env.SLACK_RENDER_V2;
|
|
305
378
|
delete process.env.SLACK_RENDER_V2;
|
|
306
|
-
expect(isSlackRenderV2Enabled()).toBe(false);
|
|
307
|
-
process.env.SLACK_RENDER_V2 = "true";
|
|
308
379
|
expect(isSlackRenderV2Enabled()).toBe(true);
|
|
380
|
+
process.env.SLACK_RENDER_V2 = "false";
|
|
381
|
+
expect(isSlackRenderV2Enabled()).toBe(false);
|
|
309
382
|
if (previous === undefined) delete process.env.SLACK_RENDER_V2;
|
|
310
383
|
else process.env.SLACK_RENDER_V2 = previous;
|
|
311
384
|
});
|
|
@@ -1716,7 +1789,7 @@ describe("Slack renderer v2", () => {
|
|
|
1716
1789
|
expect(outcome?.finalizedAt).toBeDefined();
|
|
1717
1790
|
});
|
|
1718
1791
|
|
|
1719
|
-
test("
|
|
1792
|
+
test("omits the deferral ETA even when the agent already sent a slack-reply", async () => {
|
|
1720
1793
|
const lead = await createAgent({ name: "Deferring Lead", isLead: true, status: "idle" });
|
|
1721
1794
|
const { channelId, threadTs } = uniqueSlackAddress("C_RENDER_DEFER_AFTER_REPLY");
|
|
1722
1795
|
const ask = await createTaskExtended("defer after replying by hand", {
|
|
@@ -1741,10 +1814,32 @@ describe("Slack renderer v2", () => {
|
|
|
1741
1814
|
await processSlackRenderV2();
|
|
1742
1815
|
|
|
1743
1816
|
const started = calls.find((call) => call.method === "chat.startStream");
|
|
1744
|
-
expect(started?.payload.markdown_text).toBe(
|
|
1817
|
+
expect(started?.payload.markdown_text).toBe("✅\n\nPending: checking the new defer card");
|
|
1818
|
+
expect(JSON.stringify(calls)).not.toContain("17:30:19");
|
|
1819
|
+
expect(JSON.stringify(calls)).not.toContain("Deferred until");
|
|
1820
|
+
expect((await getTaskById(ask.id))?.output).toBe(deferralLine);
|
|
1745
1821
|
expect(started?.payload.markdown_text).not.toBe(`✅ ${lead.name} completed`);
|
|
1746
1822
|
});
|
|
1747
1823
|
|
|
1824
|
+
test("does not post an ETA-only deferral notice", async () => {
|
|
1825
|
+
const { channelId, threadTs } = uniqueSlackAddress("C_ETA_ONLY");
|
|
1826
|
+
const ask = await createTaskExtended("wait", {
|
|
1827
|
+
source: "slack",
|
|
1828
|
+
slackChannelId: channelId,
|
|
1829
|
+
slackThreadTs: threadTs,
|
|
1830
|
+
});
|
|
1831
|
+
await startTask(ask.id);
|
|
1832
|
+
await completeTask(
|
|
1833
|
+
ask.id,
|
|
1834
|
+
"Deferred until today 17:30 UTC ([12345678](https://example.com/schedules/12345678)) -> ",
|
|
1835
|
+
{ addTags: ["deferred"] },
|
|
1836
|
+
);
|
|
1837
|
+
calls.length = 0;
|
|
1838
|
+
await processSlackRenderV2();
|
|
1839
|
+
expect(calls.filter((call) => call.method === "chat.startStream")).toHaveLength(0);
|
|
1840
|
+
expect(JSON.stringify(calls)).not.toContain("17:30");
|
|
1841
|
+
});
|
|
1842
|
+
|
|
1748
1843
|
test("refreshes a stream started with stale content before finalizing it", async () => {
|
|
1749
1844
|
const lead = await createAgent({ name: "Refresh Lead", isLead: true, status: "idle" });
|
|
1750
1845
|
const { channelId, threadTs } = uniqueSlackAddress("C_RENDER_REFRESH_STALE");
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
2
|
import { unlinkSync } from "node:fs";
|
|
3
|
-
import { closeDb, createAgent, createTaskExtended, initDb } from "../be/db";
|
|
4
|
-
import { routeMessage } from "../slack/router";
|
|
3
|
+
import { closeDb, createAgent, createTaskExtended, getAllAgents, initDb } from "../be/db";
|
|
4
|
+
import { broadcastMatches, routeMessage } from "../slack/router";
|
|
5
5
|
import type { Agent } from "../types";
|
|
6
6
|
|
|
7
7
|
const TEST_DB_PATH = "./test-slack-router.sqlite";
|
|
@@ -169,6 +169,21 @@ describe("Slack router — thread follow-up routing", () => {
|
|
|
169
169
|
// Lead should not be in the matches
|
|
170
170
|
expect(matches.every((m) => !m.agent.isLead)).toBe(true);
|
|
171
171
|
});
|
|
172
|
+
|
|
173
|
+
test("swarm#all includes offline non-lead agents", async () => {
|
|
174
|
+
const offlineWorker = await createAgent({
|
|
175
|
+
name: "offline-broadcast-worker",
|
|
176
|
+
isLead: false,
|
|
177
|
+
status: "offline",
|
|
178
|
+
capabilities: [],
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const matches = broadcastMatches(await getAllAgents());
|
|
182
|
+
|
|
183
|
+
expect(matches.map((match) => match.agent.id)).toContain(offlineWorker.id);
|
|
184
|
+
expect(matches.every((match) => !match.agent.isLead)).toBe(true);
|
|
185
|
+
expect(matches.every((match) => match.matchedText === "swarm#all")).toBe(true);
|
|
186
|
+
});
|
|
172
187
|
});
|
|
173
188
|
|
|
174
189
|
describe("no thread context behaves normally", () => {
|
|
@@ -105,6 +105,25 @@ describe("Slack thread steering", () => {
|
|
|
105
105
|
expect(await getChildTasks(leadTask.id)).toHaveLength(1);
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
+
test("unset configuration steers the lead with queued delivery", async () => {
|
|
109
|
+
delete process.env.SLACK_THREAD_STEERING;
|
|
110
|
+
delete process.env.SLACK_THREAD_STEERING_MODE;
|
|
111
|
+
const channelId = "C_DEFAULT_STEER";
|
|
112
|
+
const threadTs = "1500.0001";
|
|
113
|
+
const task = await createRunningSlackTask(leadId, channelId, threadTs);
|
|
114
|
+
const result = await requestSlackThreadSteering({
|
|
115
|
+
channelId,
|
|
116
|
+
threadTs,
|
|
117
|
+
message: "new context",
|
|
118
|
+
});
|
|
119
|
+
expect(result).toMatchObject({
|
|
120
|
+
task: { id: task.id },
|
|
121
|
+
result: { outcome: "queued", effectiveMode: "queue" },
|
|
122
|
+
});
|
|
123
|
+
expect(await getSteeringMessagesForTask(task.id)).toHaveLength(1);
|
|
124
|
+
expect(await getChildTasks(task.id)).toEqual([]);
|
|
125
|
+
});
|
|
126
|
+
|
|
108
127
|
test("lead mode sends one steering message to an in-progress lead and creates no task", async () => {
|
|
109
128
|
process.env.SLACK_THREAD_STEERING = "lead";
|
|
110
129
|
process.env.SLACK_THREAD_STEERING_MODE = "steer";
|