@desplega.ai/agent-swarm 1.164.1 → 1.166.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 +1 -1
- package/dist/acp-adapter-yzfxn8vj.js +23 -0
- package/dist/{actions-hp2h32dx.js → actions-qz3m692w.js} +24 -28
- package/dist/{amp-adapter-zfd7djyx.js → amp-adapter-j1xbmsqz.js} +7 -7
- package/dist/{app-w2d26h4f.js → app-43vrdmv8.js} +9 -10
- package/dist/{assistant-45pc4en7.js → assistant-ertffmka.js} +25 -29
- package/dist/{boot-reembed-k1q2s4fc.js → boot-reembed-998jtpdg.js} +12 -15
- package/dist/{boot-reembed-6rhcpsjf.js → boot-reembed-s7k7c6v2.js} +13 -15
- package/dist/boot-scrub-sweep-nepsk4t1.js +211 -0
- package/dist/{boot-seeds-ex93edx7.js → boot-seeds-vvzs1zhk.js} +12 -12
- package/dist/{claude-adapter-hcxd0szm.js → claude-adapter-2zvmekrp.js} +15 -17
- package/dist/{claude-managed-adapter-fz7xps2r.js → claude-managed-adapter-s49wbk8t.js} +5 -5
- package/dist/{claude-managed-setup-xx6f15h9.js → claude-managed-setup-cvhqq8qn.js} +4 -4
- package/dist/{claude-sdk-session-6nb9raem.js → claude-sdk-session-kkbxtw5q.js} +2624 -2606
- package/dist/{cli-etbrt8x1.js → cli-0nhdwxd5.js} +4 -4
- package/dist/{cli-jeefgppq.js → cli-0qjcsxgz.js} +7 -7
- package/dist/{cli-6behs2bt.js → cli-32bca7nr.js} +1 -1
- package/dist/{cli-gt8v96qx.js → cli-3ddybbbx.js} +4 -4
- package/dist/{acp-adapter-rxvek4ym.js → cli-3qn4r334.js} +317 -35
- package/dist/{cli-1d3w4xm3.js → cli-4r1sjfc4.js} +14 -6
- package/dist/{cli-691f2rxm.js → cli-66zh26sp.js} +68 -16
- package/dist/{cli-et8wdyqh.js → cli-68r1j4sg.js} +9 -9
- package/dist/{cli-xb7kyanp.js → cli-8kgvmkak.js} +3 -3
- package/dist/{cli-fdpkw8vk.js → cli-8t229cmr.js} +8 -8
- package/dist/{cli-gc54qc0p.js → cli-aqcey741.js} +1 -1
- package/dist/{cli-4q4cmr4t.js → cli-cpfmqz3n.js} +8 -8
- package/dist/{cli-a9bw0xxt.js → cli-df56tqdw.js} +997 -408
- package/dist/{cli-n7h1kqpf.js → cli-dmtrxqhg.js} +6 -1
- package/dist/{cli-xfb7333f.js → cli-dw2q9f7g.js} +5 -5
- package/dist/{cli-4drstbb9.js → cli-ecknp7p1.js} +81 -23
- package/dist/{cli-v0pvvhfq.js → cli-efnd2kce.js} +36 -1
- package/dist/{cli-1wetf34q.js → cli-f2n17zhp.js} +1 -1
- package/dist/{cli-2c8eky5y.js → cli-feh3dkah.js} +1 -1
- package/dist/cli-fn697bnt.js +126 -0
- package/dist/{cli-v5pr2fgt.js → cli-gt8azyet.js} +3 -3
- package/dist/{cli-dbszf3rw.js → cli-h3x613ze.js} +4 -4
- package/dist/{cli-nwhb8y2e.js → cli-hf5s9pgf.js} +1 -1
- package/dist/cli-j2jbr8af.js +516 -0
- package/dist/{cli-agjzafns.js → cli-jhm6hzjn.js} +593 -10
- package/dist/{cli-j0tmc908.js → cli-jvj6a0cv.js} +484 -477
- package/dist/{cli-xxz89c2n.js → cli-mb75wpxf.js} +1 -1
- package/dist/{cli-07s39g1n.js → cli-mv2520nm.js} +1 -1
- package/dist/{cli-v90vfwyb.js → cli-mwegs4af.js} +5 -5
- package/dist/{cli-bz1gbac9.js → cli-p4ztb80h.js} +2 -2
- package/dist/{cli-1wd1n2ya.js → cli-rfphkzh5.js} +1 -1
- package/dist/{cli-976r1zqx.js → cli-rggdq8wq.js} +5 -5
- package/dist/{cli-3ajw5v5v.js → cli-rsy933yz.js} +6 -6
- package/dist/{cli-yj790e19.js → cli-tadfgd0t.js} +8 -1
- package/dist/{cli-cn53e7vc.js → cli-ttbsse3f.js} +6 -1
- package/dist/{cli-dyqsrqcf.js → cli-v2a8x45d.js} +4 -4
- package/dist/{cli-wbvnn6tk.js → cli-v6vv7dy8.js} +91 -9
- package/dist/{cli-z2ap2c0c.js → cli-vzc73wet.js} +90 -55
- package/dist/cli-wt7ccy4r.js +2654 -0
- package/dist/{cli-4cn19jvn.js → cli-x7kpr72a.js} +3 -3
- package/dist/{cli-003785v9.js → cli-xvc3q9br.js} +1 -1
- package/dist/{cli-v0e7jpr5.js → cli-xvdgasgy.js} +13 -4
- package/dist/{cli-asqeg4ve.js → cli-y59h2pk9.js} +2 -2
- package/dist/{cli-0wn3ypqv.js → cli-yshaazs1.js} +14121 -3478
- package/dist/{cli-wg8yjqtw.js → cli-ysnyhws9.js} +16 -3
- package/dist/cli-zp67qg5x.js +92 -0
- package/dist/{cli-g3qgrzck.js → cli-zsh093hk.js} +71 -3
- package/dist/{cli-ma3zvh39.js → cli-zy7vwsne.js} +6066 -5829
- package/dist/cli.js +79 -26
- package/dist/{codex-adapter-pcxdn10v.js → codex-adapter-dw1wxqt8.js} +10 -10
- package/dist/{codex-hook-b1fb7c2k.js → codex-hook-d2rkvf39.js} +1 -1
- package/dist/{codex-login-059nm145.js → codex-login-terbdsqq.js} +2 -2
- package/dist/{codex-session-runner-xzwwddp8.js → codex-session-runner-3cgb8y1g.js} +10 -10
- package/dist/{commands-98at8mkn.js → commands-mhxam6z2.js} +9 -9
- package/dist/{cursor-adapter-dkg6qnkp.js → cursor-adapter-2qda3st7.js} +6 -6
- package/dist/{db-01g8wtvw.js → db-sjks418k.js} +11 -9
- package/dist/{devin-adapter-qmttt2fz.js → devin-adapter-bffjj5n4.js} +2 -2
- package/dist/{dispatcher-nn2k1wxd.js → dispatcher-1es759rn.js} +22 -26
- package/dist/{dsh-adapter-w4d4vwdq.js → dsh-adapter-bek4cyw9.js} +6 -6
- package/dist/{e2b-9e4g2hvt.js → e2b-nd50eccg.js} +1 -1
- package/dist/{event-bus-z7v2t26e.js → event-bus-vnfthafg.js} +2 -2
- package/dist/grok-adapter-ar9cw9jg.js +29 -0
- package/dist/{handlers-b0kqsbg8.js → handlers-1gkbhktr.js} +25 -29
- package/dist/{hook-sap7v1aj.js → hook-ds2zxq9v.js} +16 -18
- package/dist/{hook-pz50gqep.js → hook-vxa3tcsy.js} +10 -12
- package/dist/{http-ncr8rmd0.js → http-3h21z9md.js} +679 -227
- package/dist/{index-c4aqcyz1.js → index-9b65j1m5.js} +11 -11
- package/dist/{index-r46s01nq.js → index-bn6cy8ps.js} +22 -26
- package/dist/{index-38kqdt3e.js → index-dkfxpkmm.js} +23 -27
- package/dist/{index-630kj5vd.js → index-f8xnqrye.js} +55 -43
- package/dist/{index-fdk1t4d6.js → index-n0rxa4ca.js} +11962 -9900
- package/dist/{index-9nrj7797.js → index-n99fycq7.js} +22 -26
- package/dist/{keepalive-31613r2h.js → keepalive-3xspa9jw.js} +14 -11
- package/dist/{lead-mx59tr5s.js → lead-6qphc124.js} +43 -43
- package/dist/{maintenance-55ff39ca.js → maintenance-0teem1bb.js} +10 -12
- package/dist/{node-f0ykebbv.js → node-9sg7ajby.js} +25 -7
- package/dist/{oauth-refresh-sweep-anma954w.js → oauth-refresh-sweep-sh9q25xp.js} +13 -10
- package/dist/{onboard-b2fy8ftb.js → onboard-54qmnqxe.js} +4 -3
- package/dist/{opencode-adapter-p1s92tmy.js → opencode-adapter-wjfa2wvd.js} +6 -6
- package/dist/{otel-impl-9brvt7xv.js → otel-impl-ycqzp2a1.js} +2 -2
- package/dist/{pi-mono-adapter-kz4rrwvd.js → pi-mono-adapter-e77e9zst.js} +6 -6
- package/dist/{pricing-refresh-7w8mbmva.js → pricing-refresh-atnkngqs.js} +10 -10
- package/dist/{rbac-roles-ejp7qwvb.js → rbac-roles-9pjphh9q.js} +10 -10
- package/dist/{rbac-roles-nhk4r7bc.js → rbac-roles-j78wdksm.js} +10 -10
- package/dist/{render-v2-2gzt8skq.js → render-v2-pxjs49fx.js} +9 -10
- package/dist/{setup-5et8t412.js → setup-ymej0xtx.js} +2 -2
- package/dist/{work-objects-tb0er7qa.js → work-objects-3nh4ymzb.js} +26 -30
- package/dist/{worker-62cv075d.js → worker-qa9fkz5s.js} +43 -43
- package/dist/{x-vbxb31jr.js → x-ma4gnfgy.js} +2 -2
- package/openapi.json +457 -31
- package/package.json +14 -6
- package/plugin/commands/create-pr.md +7 -1
- package/plugin/pi-skills/create-pr/SKILL.md +7 -1
- package/src/agentmail/app.ts +5 -4
- package/src/be/automation-preflight.ts +11 -9
- package/src/be/boot-scrub-sweep.ts +302 -0
- package/src/be/connector-codes.ts +81 -0
- package/src/be/crypto/key-bootstrap.ts +1 -1
- package/src/be/db/approvals.ts +15 -1
- package/src/be/db/codex-oauth-device-flows.ts +40 -0
- package/src/be/db/config-secrets.ts +31 -0
- package/src/be/db/runtime.ts +4 -0
- package/src/be/db/script-runs.ts +15 -0
- package/src/be/db/tasks/write.ts +1 -1
- package/src/be/db-queries/mcp-oauth.ts +5 -0
- package/src/be/db-queries/oauth.ts +77 -0
- package/src/be/db.ts +162 -59
- package/src/be/events.ts +17 -1
- package/src/be/memory/index-content.ts +2 -2
- package/src/be/memory/providers/sqlite-store.ts +63 -11
- package/src/be/memory/types.ts +14 -0
- package/src/be/migrations/197_connector_codes.sql +23 -0
- package/src/be/migrations/198_permission_audit_guest.sql +33 -0
- package/src/be/migrations/200_backfill_auto_memory_expiry.sql +20 -0
- package/src/be/migrations/201_workflow_replay_columns.sql +10 -0
- package/src/be/migrations/202_codex_oauth_device_flows.sql +24 -0
- package/src/be/model-tier-resolution.ts +2 -1
- package/src/be/model-validation.ts +1 -1
- package/src/be/pricing-normalize.ts +3 -0
- package/src/be/rbac-audit.ts +2 -1
- package/src/be/scripts/db.ts +28 -4
- package/src/be/scripts/source-secrets.ts +37 -0
- package/src/be/scripts/typecheck.ts +23 -2
- package/src/be/scrub-json.ts +32 -0
- package/src/be/sealed-json.ts +59 -0
- package/src/be/secret-registry.ts +131 -0
- package/src/be/seed-pricing.ts +14 -0
- package/src/be/users.ts +7 -1
- package/src/be/workflow-replay.ts +77 -0
- package/src/cli.tsx +7 -0
- package/src/commands/provider-credentials.ts +46 -8
- package/src/commands/runner.ts +2 -10
- package/src/extensions/contract-types.generated.ts +1 -1
- package/src/github/app.ts +4 -4
- package/src/heartbeat/heartbeat.ts +20 -8
- package/src/hooks/pr-body-guard.ts +1 -1
- package/src/http/agents.ts +1 -0
- package/src/http/approval-requests.ts +1 -0
- package/src/http/apps.ts +3 -0
- package/src/http/auth.ts +4 -1
- package/src/http/codex-oauth-device.ts +8 -10
- package/src/http/codex-oauth-keep-warm.ts +2 -2
- package/src/http/config-values.ts +59 -0
- package/src/http/config.ts +5 -0
- package/src/http/db-query-bounded.ts +3 -0
- package/src/http/db-query-shared.ts +107 -15
- package/src/http/db-query.ts +10 -2
- package/src/http/extensions.ts +1 -0
- package/src/http/index.ts +23 -6
- package/src/http/integrations.ts +1 -38
- package/src/http/ip-rate-limit.ts +82 -0
- package/src/http/kv.ts +38 -4
- package/src/http/mcp-servers.ts +1 -0
- package/src/http/memory.ts +9 -0
- package/src/http/models-catalog.ts +2 -0
- package/src/http/operator-actor.ts +4 -0
- package/src/http/page-proxy.ts +22 -0
- package/src/http/pages.ts +4 -2
- package/src/http/realtime.ts +1 -1
- package/src/http/request-principal.ts +14 -1
- package/src/http/script-connections.ts +8 -0
- package/src/http/script-runs.ts +12 -4
- package/src/http/scripts.ts +15 -0
- package/src/http/session-cost-recompute.ts +3 -2
- package/src/http/tasks.ts +9 -0
- package/src/http/users.ts +226 -0
- package/src/http/workflows.ts +3 -2
- package/src/http.ts +1 -0
- package/src/integrations/kapso/client.ts +3 -1
- package/src/jira/app.ts +2 -2
- package/src/kv-view.ts +324 -0
- package/src/linear/app.ts +2 -2
- package/src/oauth/mcp-wrapper.ts +1 -1
- package/src/prompts/memories.ts +42 -1
- package/src/providers/acp-adapter.ts +85 -24
- package/src/providers/acp-target-catalog.ts +54 -1
- package/src/providers/acp-targets.ts +358 -4
- package/src/providers/amp-adapter.ts +1 -1
- package/src/providers/cursor-adapter.ts +1 -1
- package/src/providers/grok-adapter.ts +186 -0
- package/src/providers/index.ts +5 -1
- package/src/providers/pricing-sources.md +6 -0
- package/src/providers/reasoning-effort.ts +17 -1
- package/src/providers/types.ts +2 -1
- package/src/rbac/can.ts +4 -3
- package/src/rbac/legacy-policy.ts +3 -3
- package/src/rbac/types.ts +6 -0
- package/src/script-workflows/supervisor.ts +11 -2
- package/src/scripts-runtime/eval-harness.ts +2 -12
- package/src/scripts-runtime/executors/quickjs-bundle.ts +152 -0
- package/src/scripts-runtime/executors/quickjs-prelude.ts +285 -0
- package/src/scripts-runtime/executors/quickjs-runner.ts +580 -0
- package/src/scripts-runtime/executors/quickjs-worker.ts +81 -0
- package/src/scripts-runtime/executors/quickjs.ts +196 -0
- package/src/scripts-runtime/executors/registry.ts +2 -0
- package/src/scripts-runtime/executors/sourcemap.ts +79 -0
- package/src/scripts-runtime/script-error-hints.ts +14 -0
- package/src/scripts-runtime/swarm-sdk.ts +9 -2
- package/src/scripts-runtime/types/stdlib.d.ts +27 -8
- package/src/scripts-runtime/types/swarm-extension.d.ts +3 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +27 -8
- package/src/server-user.ts +2 -20
- package/src/server.ts +3 -1
- package/src/slack/app.ts +4 -0
- package/src/slack/config.ts +3 -1
- package/src/slack/egress-scrub.ts +87 -0
- package/src/tests/acp-adapter.test.ts +191 -2
- package/src/tests/boot-scrub-sweep.test.ts +390 -0
- package/src/tests/budget-user-scope.test.ts +7 -21
- package/src/tests/check-pr-body.test.ts +149 -6
- package/src/tests/check-text-columns.test.ts +145 -0
- package/src/tests/claude-adapter-binary.test.ts +1 -1
- package/src/tests/claude-sdk-process.fixture.ts +13 -2
- package/src/tests/codex-oauth-device.test.ts +23 -37
- package/src/tests/codex-oauth-keep-warm.test.ts +29 -0
- package/src/tests/console-scrub.test.ts +141 -0
- package/src/tests/credential-check.test.ts +90 -0
- package/src/tests/db-query-denylist.test.ts +202 -0
- package/src/tests/db-task-read-contract.test.ts +1 -0
- package/src/tests/db-task-write-contract.test.ts +1 -0
- package/src/tests/docs-index-footprint.test.js +3 -3
- package/src/tests/entrypoint-acp-target-binary.test.ts +44 -1
- package/src/tests/events-scrub.test.ts +88 -0
- package/src/tests/explicit-model-validation.test.ts +81 -0
- package/src/tests/extensions-pre-task.test.ts +33 -1
- package/src/tests/fixtures/grok/acp-updates-recorded.jsonl +53 -0
- package/src/tests/fixtures/grok/prompt-response-meta.json +36 -0
- package/src/tests/fixtures/secret-corpus.ts +320 -0
- package/src/tests/grok-adapter.test.ts +647 -0
- package/src/tests/heartbeat-checklist.test.ts +44 -0
- package/src/tests/integration-disable-flags.test.ts +156 -0
- package/src/tests/kv-http.test.ts +34 -0
- package/src/tests/kv-tool.test.ts +274 -1
- package/src/tests/kv-view.test.ts +147 -0
- package/src/tests/mcp-user-route.test.ts +257 -15
- package/src/tests/memory-injection-outcome.test.ts +143 -0
- package/src/tests/model-tier-resolution.test.ts +76 -3
- package/src/tests/page-guest-session.test.ts +424 -0
- package/src/tests/page-proxy.test.ts +1 -1
- package/src/tests/page-viewer-auth.test.ts +22 -1
- package/src/tests/pr-body-leaks.test.ts +157 -2
- package/src/tests/preload.ts +10 -0
- package/src/tests/provider-registration.test.ts +3 -1
- package/src/tests/rbac-audit.test.ts +32 -0
- package/src/tests/rbac-mcp-admission-e2e.test.ts +4 -62
- package/src/tests/script-connections.test.ts +9 -0
- package/src/tests/script-executor-conformance.test.ts +3 -0
- package/src/tests/script-executor-quickjs.test.ts +442 -0
- package/src/tests/script-executor-registry.test.ts +6 -1
- package/src/tests/script-runs-http.test.ts +126 -1
- package/src/tests/script-runs-scrub.test.ts +275 -0
- package/src/tests/scripts-http.test.ts +82 -1
- package/src/tests/scripts-mcp-e2e.test.ts +42 -6
- package/src/tests/scripts-runtime.test.ts +37 -0
- package/src/tests/scrubbed-text-12e.test.ts +289 -0
- package/src/tests/sealed-key-guard.test.ts +166 -0
- package/src/tests/secret-canary-sweep.test.ts +958 -0
- package/src/tests/secret-registry.test.ts +355 -0
- package/src/tests/secret-rules.test.ts +230 -0
- package/src/tests/secret-scrubber.test.ts +317 -1
- package/src/tests/session-costs-golden.test.ts +17 -0
- package/src/tests/slack-egress-scrub.test.ts +216 -0
- package/src/tests/swarm-tool-result-gate.test.ts +143 -30
- package/src/tests/task-tools-ctx.test.ts +3 -0
- package/src/tests/user-token-routes.test.ts +342 -3
- package/src/tests/workflow-replay.test.ts +518 -0
- package/src/tests/workflow-scrub.test.ts +242 -0
- package/src/tests/workflow-system-one-decision-providers.test.ts +737 -0
- package/src/tools/db-query.ts +1 -1
- package/src/tools/inject-learning.ts +1 -1
- package/src/tools/kv/kv-get.ts +136 -6
- package/src/tools/send-task.ts +61 -17
- package/src/tools/utils.ts +186 -43
- package/src/tools/workflows/get-workflow-run.ts +3 -2
- package/src/types.ts +16 -1
- package/src/utils/console-scrub-install.ts +5 -0
- package/src/utils/console-scrub.ts +57 -0
- package/src/utils/credentials.ts +8 -0
- package/src/utils/page-session.ts +6 -2
- package/src/utils/pr-body-leaks.ts +149 -10
- package/src/utils/request-auth-context.ts +2 -0
- package/src/utils/secret-rules.generated.ts +2055 -0
- package/src/utils/secret-scrubber.ts +662 -36
- package/src/workflows/engine.ts +39 -4
- package/src/workflows/executors/notify.ts +6 -1
- package/src/workflows/executors/system-one-decision.ts +31 -12
- package/src/workflows/executors/system-one-openai-wire.ts +168 -0
- package/src/workflows/executors/system-one-providers.ts +112 -11
- package/src/workflows/executors/system-one-wire.ts +79 -0
- package/src/workflows/recovery.ts +10 -1
- package/src/workflows/resume.ts +62 -3
- package/src/workflows/retry-poller.ts +3 -0
- package/templates/ai-toolbox.manifest.json +2 -2
- package/templates/skills/agent-browser/SKILL.md +5 -5
- package/templates/skills/agent-browser/config.json +1 -1
- package/templates/skills/agent-browser/content.md +4 -4
- package/templates/skills/artifacts/content.md +17 -7
- package/templates/skills/jira-interaction/config.json +1 -1
- package/templates/skills/jira-interaction/content.md +56 -205
- package/templates/skills/linear-interaction/config.json +1 -1
- package/templates/skills/linear-interaction/content.md +31 -50
- package/templates/skills/pages/content.md +8 -4
- package/templates/skills/qa/content.md +2 -2
- package/templates/skills/workflow-iterate/content.md +4 -4
- package/tsconfig.json +3 -3
- package/dist/boot-scrub-logs-t8xg7wd7.js +0 -99
- package/dist/cli-0a7r5331.js +0 -168
- package/dist/cli-93rxw9yd.js +0 -951
- package/dist/cli-cvk6yzw5.js +0 -40
- package/dist/cli-dgnj0zcc.js +0 -40
- package/dist/cli-qt5wzq6v.js +0 -256
- package/dist/cli-v7fqehqd.js +0 -9281
- package/dist/cli-vbr859rz.js +0 -572
- package/src/be/boot-scrub-logs.ts +0 -132
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { resolveKvView, sliceKvTarget, summarizeKvShape } from "../kv-view";
|
|
3
|
+
|
|
4
|
+
describe("resolveKvView", () => {
|
|
5
|
+
const stored = JSON.stringify({ a: { rows: [{ id: 0 }, { id: 1 }, { id: 2 }] }, s: "abcdef" });
|
|
6
|
+
|
|
7
|
+
test("a JSON string resolves dot paths, numeric segments index arrays", () => {
|
|
8
|
+
expect(resolveKvView(stored, { path: "a.rows.1.id" })).toEqual({
|
|
9
|
+
ok: true,
|
|
10
|
+
value: 1,
|
|
11
|
+
view: { path: "a.rows.1.id", type: "number" },
|
|
12
|
+
});
|
|
13
|
+
expect(resolveKvView(stored, { path: "a.rows", offset: 1, limit: 1 })).toMatchObject({
|
|
14
|
+
ok: true,
|
|
15
|
+
value: [{ id: 1 }],
|
|
16
|
+
view: { total: 3, offset: 1, returned: 1, nextOffset: 2 },
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test("objects page by key, strings by character", () => {
|
|
21
|
+
expect(resolveKvView({ x: 1, y: 2, z: 3 }, { offset: 1, limit: 1 })).toMatchObject({
|
|
22
|
+
value: { y: 2 },
|
|
23
|
+
view: { type: "object", total: 3, nextOffset: 2 },
|
|
24
|
+
});
|
|
25
|
+
expect(resolveKvView(stored, { path: "s", offset: 2, limit: 3 })).toMatchObject({
|
|
26
|
+
value: "cde",
|
|
27
|
+
view: { type: "string", returned: 3, nextOffset: 5 },
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("out-of-range offsets return an empty page with the real total", () => {
|
|
32
|
+
expect(resolveKvView(stored, { path: "a.rows", offset: 99 })).toMatchObject({
|
|
33
|
+
ok: true,
|
|
34
|
+
value: [],
|
|
35
|
+
view: { total: 3, offset: 99, returned: 0, nextOffset: null },
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("missing keys, bad indices, empty segments and scalar paging are errors", () => {
|
|
40
|
+
expect(resolveKvView(stored, { path: "a.nope" })).toEqual({
|
|
41
|
+
ok: false,
|
|
42
|
+
error: 'path "a.nope" not found: no key "nope" under "a"',
|
|
43
|
+
});
|
|
44
|
+
expect(resolveKvView(stored, { path: "a.rows.3" })).toMatchObject({ ok: false });
|
|
45
|
+
expect(resolveKvView(stored, { path: "a.rows.-1" })).toMatchObject({ ok: false });
|
|
46
|
+
expect(resolveKvView(stored, { path: "a..rows" })).toMatchObject({ ok: false });
|
|
47
|
+
expect(resolveKvView(stored, { path: "a.rows.0.id.x" })).toMatchObject({ ok: false });
|
|
48
|
+
expect(resolveKvView(42, { offset: 1 })).toMatchObject({ ok: false });
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("a non-JSON string rejects a path but pages its characters", () => {
|
|
52
|
+
expect(resolveKvView("plain {not json", { path: "a" })).toMatchObject({ ok: false });
|
|
53
|
+
expect(resolveKvView("plain {not json", { offset: 6, limit: 5 })).toMatchObject({
|
|
54
|
+
ok: true,
|
|
55
|
+
value: "{not ",
|
|
56
|
+
});
|
|
57
|
+
// A string holding a JSON scalar stays a plain string.
|
|
58
|
+
expect(resolveKvView('"quoted"', { offset: 1, limit: 6 })).toMatchObject({ value: "quoted" });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("a string page never ends on a lone high surrogate", () => {
|
|
62
|
+
const sliced = sliceKvTarget("a🙂b", 0, 2);
|
|
63
|
+
expect(sliced).toMatchObject({ value: "a", returned: 1, nextOffset: 1 });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("limit 1 on an astral character returns the whole pair and advances", () => {
|
|
67
|
+
const text = "🙂🙃x";
|
|
68
|
+
const seen: string[] = [];
|
|
69
|
+
let offset: number | null = 0;
|
|
70
|
+
while (offset !== null && seen.length < 10) {
|
|
71
|
+
const page = sliceKvTarget(text, offset, 1)!;
|
|
72
|
+
expect(page.returned).toBeGreaterThan(0);
|
|
73
|
+
seen.push(page.value as string);
|
|
74
|
+
offset = page.nextOffset;
|
|
75
|
+
}
|
|
76
|
+
expect(seen).toEqual(["🙂", "🙃", "x"]);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe("summarizeKvShape", () => {
|
|
81
|
+
test("expands the dominant object down to where the bulk lives", () => {
|
|
82
|
+
const rows = Array.from({ length: 300 }, (_, id) => ({ id, note: "x".repeat(50) }));
|
|
83
|
+
const shape = summarizeKvShape({
|
|
84
|
+
version: 1,
|
|
85
|
+
toolName: "script-run",
|
|
86
|
+
outcome: { ok: true, message: "done", data: { status: 200, data: { result: { rows } } } },
|
|
87
|
+
});
|
|
88
|
+
expect(shape[0]).toMatchObject({
|
|
89
|
+
path: "outcome.data.data.result.rows",
|
|
90
|
+
type: "array",
|
|
91
|
+
items: 300,
|
|
92
|
+
});
|
|
93
|
+
expect(shape[0]!.bytes).toBe(Buffer.byteLength(JSON.stringify(rows)));
|
|
94
|
+
// Crumbs under 1% of the value are left out.
|
|
95
|
+
expect(shape.map((entry) => entry.path)).not.toContain("version");
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("escapes keys with dots so every shape path resolves", () => {
|
|
99
|
+
const rows = Array.from({ length: 50 }, (_, id) => ({ id, note: "x".repeat(50) }));
|
|
100
|
+
const value = { "rows.v2": rows, "a\\b": { "c.d": "y".repeat(2_000) } };
|
|
101
|
+
const shape = summarizeKvShape(value);
|
|
102
|
+
expect(shape.map((entry) => entry.path)).toEqual(["rows\\.v2", "a\\\\b.c\\.d"]);
|
|
103
|
+
for (const entry of shape) {
|
|
104
|
+
expect(resolveKvView(value, { path: entry.path, offset: 0 })).toMatchObject({
|
|
105
|
+
ok: true,
|
|
106
|
+
view: { path: entry.path, type: entry.type, total: entry.items },
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("keeps long paths exact and replaces unaddressable ones with their nearest ancestor", () => {
|
|
112
|
+
const rows = Array.from({ length: 80 }, (_, id) => ({ id, note: "x".repeat(60) }));
|
|
113
|
+
const longKey = "L".repeat(200);
|
|
114
|
+
expect(summarizeKvShape({ [longKey]: rows })[0]).toMatchObject({ path: longKey, items: 80 });
|
|
115
|
+
// An empty key has no path syntax: its parent object stands in.
|
|
116
|
+
expect(summarizeKvShape({ "": rows, small: 1 })[0]).toMatchObject({
|
|
117
|
+
path: "",
|
|
118
|
+
type: "object",
|
|
119
|
+
items: 2,
|
|
120
|
+
});
|
|
121
|
+
expect(summarizeKvShape({ outer: { "": rows, side: [1] } })[0]).toMatchObject({
|
|
122
|
+
path: "outer",
|
|
123
|
+
type: "object",
|
|
124
|
+
});
|
|
125
|
+
// Longer than kv-get's path limit: the root stands in.
|
|
126
|
+
expect(summarizeKvShape({ ["X".repeat(1_100)]: rows })[0]).toMatchObject({ path: "" });
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("is bounded by total path bytes", () => {
|
|
130
|
+
const value = Object.fromEntries(
|
|
131
|
+
Array.from({ length: 8 }, (_, i) => [`${i}`.repeat(600), "v".repeat(1_000)]),
|
|
132
|
+
);
|
|
133
|
+
const shape = summarizeKvShape(value);
|
|
134
|
+
const pathBytes = shape.reduce((sum, entry) => sum + JSON.stringify(entry.path).length, 0);
|
|
135
|
+
expect(pathBytes).toBeLessThanOrEqual(2_048);
|
|
136
|
+
expect(shape.filter((entry) => entry.path.length === 600)).toHaveLength(3);
|
|
137
|
+
expect(shape.at(-1)).toMatchObject({ path: "", type: "object", items: 8 });
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test("is bounded to maxEntries", () => {
|
|
141
|
+
const wide = Object.fromEntries(
|
|
142
|
+
Array.from({ length: 50 }, (_, i) => [`k${i}`, "v".repeat(100)]),
|
|
143
|
+
);
|
|
144
|
+
expect(summarizeKvShape(wide)).toHaveLength(8);
|
|
145
|
+
expect(summarizeKvShape(wide, 3)).toHaveLength(3);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
@@ -9,17 +9,20 @@ import {
|
|
|
9
9
|
import type { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
10
10
|
import {
|
|
11
11
|
closeDb,
|
|
12
|
+
completeTask,
|
|
12
13
|
createAgent,
|
|
13
14
|
createTaskExtended,
|
|
14
15
|
createUser,
|
|
15
16
|
getDbClient,
|
|
16
17
|
getTaskById,
|
|
17
18
|
initDb,
|
|
19
|
+
startTask,
|
|
18
20
|
} from "../be/db";
|
|
19
21
|
import { type IdentityActor, mintToken, revokeToken } from "../be/users";
|
|
20
22
|
import { handleCore } from "../http/core";
|
|
21
23
|
import { handleMcp } from "../http/mcp";
|
|
22
24
|
import { handleMcpUser } from "../http/mcp-user";
|
|
25
|
+
import { handlePoll } from "../http/poll";
|
|
23
26
|
import { listenOnFreePort } from "./test-net";
|
|
24
27
|
|
|
25
28
|
const TEST_DB_PATH = "./test-mcp-user-route.sqlite";
|
|
@@ -80,6 +83,7 @@ beforeEach(async () => {
|
|
|
80
83
|
await client.run("DELETE FROM user_tokens");
|
|
81
84
|
await client.run("DELETE FROM agent_tasks");
|
|
82
85
|
await client.run("DELETE FROM users");
|
|
86
|
+
await client.run("DELETE FROM agents");
|
|
83
87
|
});
|
|
84
88
|
|
|
85
89
|
function endpoint(path = "/mcp-user"): string {
|
|
@@ -125,6 +129,32 @@ async function mcpPost(
|
|
|
125
129
|
return { response, payload, text };
|
|
126
130
|
}
|
|
127
131
|
|
|
132
|
+
async function callPoll(agentId: string): Promise<{
|
|
133
|
+
status: number;
|
|
134
|
+
body: { trigger: { type: string; [key: string]: unknown } | null } | { error: string };
|
|
135
|
+
}> {
|
|
136
|
+
let status = 200;
|
|
137
|
+
let bodyStr = "";
|
|
138
|
+
const req = {
|
|
139
|
+
method: "GET",
|
|
140
|
+
url: "/api/poll",
|
|
141
|
+
headers: { "x-agent-id": agentId },
|
|
142
|
+
} as unknown as Parameters<typeof handlePoll>[0];
|
|
143
|
+
const res = {
|
|
144
|
+
setHeader() {},
|
|
145
|
+
writeHead(code: number) {
|
|
146
|
+
status = code;
|
|
147
|
+
},
|
|
148
|
+
end(body?: string) {
|
|
149
|
+
bodyStr = body ?? "";
|
|
150
|
+
},
|
|
151
|
+
} as unknown as Parameters<typeof handlePoll>[1];
|
|
152
|
+
|
|
153
|
+
const handled = await handlePoll(req, res, ["api", "poll"], new URLSearchParams(), agentId);
|
|
154
|
+
if (!handled) throw new Error("handlePoll did not handle the request");
|
|
155
|
+
return { status, body: bodyStr ? JSON.parse(bodyStr) : { trigger: null } };
|
|
156
|
+
}
|
|
157
|
+
|
|
128
158
|
async function initialize(
|
|
129
159
|
token: string,
|
|
130
160
|
path = "/mcp-user",
|
|
@@ -237,7 +267,7 @@ describe("/mcp-user auth and tool surface", () => {
|
|
|
237
267
|
expect(response.status).toBe(401);
|
|
238
268
|
});
|
|
239
269
|
|
|
240
|
-
test("valid active-user token initializes and
|
|
270
|
+
test("valid active-user token initializes and exposes the narrow send-task schema", async () => {
|
|
241
271
|
const user = await createUser({ name: "Active User" });
|
|
242
272
|
const token = (await mintToken(user.id, "active", ACTOR)).plaintext;
|
|
243
273
|
const sessionId = await initialize(token);
|
|
@@ -250,23 +280,106 @@ describe("/mcp-user auth and tool surface", () => {
|
|
|
250
280
|
);
|
|
251
281
|
|
|
252
282
|
expect(response.status).toBe(200);
|
|
253
|
-
const result = payload as {
|
|
283
|
+
const result = payload as {
|
|
284
|
+
result: {
|
|
285
|
+
tools: Array<{ name: string; inputSchema: { properties?: Record<string, unknown> } }>;
|
|
286
|
+
};
|
|
287
|
+
};
|
|
254
288
|
const names = result.result.tools.map((tool) => tool.name).sort();
|
|
255
289
|
expect(names).toEqual(
|
|
256
|
-
[
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
"steer-task",
|
|
262
|
-
"task-action",
|
|
263
|
-
].sort(),
|
|
290
|
+
["cancel-task", "get-task-details", "get-tasks", "send-task", "steer-task"].sort(),
|
|
291
|
+
);
|
|
292
|
+
const sendTask = result.result.tools.find((tool) => tool.name === "send-task");
|
|
293
|
+
expect(Object.keys(sendTask?.inputSchema.properties ?? {}).sort()).toEqual(
|
|
294
|
+
["model", "modelTier", "outputSchema", "priority", "tags", "task", "taskType"].sort(),
|
|
264
295
|
);
|
|
265
296
|
});
|
|
266
297
|
|
|
267
|
-
test("send-task
|
|
298
|
+
test("send-task assigns user work to the Lead with internal routing proof", async () => {
|
|
299
|
+
const lead = await createAgent({
|
|
300
|
+
id: "10000000-0000-4000-8000-000000000001",
|
|
301
|
+
name: "User MCP Lead",
|
|
302
|
+
isLead: true,
|
|
303
|
+
status: "idle",
|
|
304
|
+
});
|
|
305
|
+
const user = await createUser({ name: "Lead Requester" });
|
|
306
|
+
const token = (await mintToken(user.id, "lead-route", ACTOR)).plaintext;
|
|
307
|
+
const sessionId = await initialize(token);
|
|
308
|
+
await notifyInitialized(token, sessionId);
|
|
309
|
+
|
|
310
|
+
const { response, payload } = await mcpPost(
|
|
311
|
+
token,
|
|
312
|
+
{
|
|
313
|
+
jsonrpc: "2.0",
|
|
314
|
+
id: 3,
|
|
315
|
+
method: "tools/call",
|
|
316
|
+
params: { name: "send-task", arguments: { task: "route this through the lead" } },
|
|
317
|
+
},
|
|
318
|
+
sessionId,
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
expect(response.status).toBe(200);
|
|
322
|
+
const result = payload as { result: { structuredContent: { task: { id: string } } } };
|
|
323
|
+
const task = await getTaskById(result.result.structuredContent.task.id);
|
|
324
|
+
expect(task).toMatchObject({
|
|
325
|
+
status: "pending",
|
|
326
|
+
agentId: lead.id,
|
|
327
|
+
requestedByUserId: user.id,
|
|
328
|
+
routingReason: "skill",
|
|
329
|
+
routingSource: "engine_default",
|
|
330
|
+
routingNote: "User MCP ingress assigns new work to the Lead for delegation.",
|
|
331
|
+
});
|
|
332
|
+
expect(task?.offeredTo).toBeUndefined();
|
|
333
|
+
expect(task?.routingAffinity).toBeUndefined();
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test("send-task queues for a busy Lead and poll starts it after capacity returns", async () => {
|
|
337
|
+
const lead = await createAgent({
|
|
338
|
+
id: "10000000-0000-4000-8000-000000000002",
|
|
339
|
+
name: "Busy User MCP Lead",
|
|
340
|
+
isLead: true,
|
|
341
|
+
status: "idle",
|
|
342
|
+
maxTasks: 1,
|
|
343
|
+
});
|
|
344
|
+
const activeTask = await createTaskExtended("existing lead work", { agentId: lead.id });
|
|
345
|
+
expect((await startTask(activeTask.id))?.status).toBe("in_progress");
|
|
346
|
+
const user = await createUser({ name: "Busy Lead Requester" });
|
|
347
|
+
const token = (await mintToken(user.id, "busy-lead-route", ACTOR)).plaintext;
|
|
348
|
+
const sessionId = await initialize(token);
|
|
349
|
+
await notifyInitialized(token, sessionId);
|
|
350
|
+
|
|
351
|
+
const { payload } = await mcpPost(
|
|
352
|
+
token,
|
|
353
|
+
{
|
|
354
|
+
jsonrpc: "2.0",
|
|
355
|
+
id: 3,
|
|
356
|
+
method: "tools/call",
|
|
357
|
+
params: { name: "send-task", arguments: { task: "wait for the busy lead" } },
|
|
358
|
+
},
|
|
359
|
+
sessionId,
|
|
360
|
+
);
|
|
361
|
+
|
|
362
|
+
const result = payload as { result: { structuredContent: { task: { id: string } } } };
|
|
363
|
+
const task = await getTaskById(result.result.structuredContent.task.id);
|
|
364
|
+
expect(task).toMatchObject({ status: "pending", agentId: lead.id });
|
|
365
|
+
|
|
366
|
+
const blockedPoll = await callPoll(lead.id);
|
|
367
|
+
expect(blockedPoll.status).toBe(200);
|
|
368
|
+
if ("error" in blockedPoll.body) throw new Error("unexpected poll error");
|
|
369
|
+
expect(blockedPoll.body.trigger).toBeNull();
|
|
370
|
+
expect((await getTaskById(task!.id))?.status).toBe("pending");
|
|
371
|
+
|
|
372
|
+
await completeTask(activeTask.id, "capacity returned");
|
|
373
|
+
const readyPoll = await callPoll(lead.id);
|
|
374
|
+
expect(readyPoll.status).toBe(200);
|
|
375
|
+
if ("error" in readyPoll.body) throw new Error("unexpected poll error");
|
|
376
|
+
expect(readyPoll.body.trigger?.type).toBe("task_assigned");
|
|
377
|
+
expect((readyPoll.body.trigger as { taskId: string }).taskId).toBe(task?.id);
|
|
378
|
+
expect((await getTaskById(task!.id))?.status).toBe("in_progress");
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test("send-task refuses with no Lead and creates no task", async () => {
|
|
268
382
|
const user = await createUser({ name: "Task Requester" });
|
|
269
|
-
const otherUser = await createUser({ name: "Other Task Requester" });
|
|
270
383
|
const token = (await mintToken(user.id, "task", ACTOR)).plaintext;
|
|
271
384
|
const sessionId = await initialize(token);
|
|
272
385
|
await notifyInitialized(token, sessionId);
|
|
@@ -283,9 +396,81 @@ describe("/mcp-user auth and tool surface", () => {
|
|
|
283
396
|
);
|
|
284
397
|
|
|
285
398
|
expect(response.status).toBe(200);
|
|
286
|
-
const result = payload as {
|
|
287
|
-
|
|
288
|
-
|
|
399
|
+
const result = payload as {
|
|
400
|
+
result: { isError?: boolean; structuredContent: { success: boolean; message: string } };
|
|
401
|
+
};
|
|
402
|
+
expect(result.result.isError).toBe(true);
|
|
403
|
+
expect(result.result.structuredContent).toMatchObject({
|
|
404
|
+
success: false,
|
|
405
|
+
message: "No online Lead is available. Start or register a Lead before sending a task.",
|
|
406
|
+
});
|
|
407
|
+
expect(
|
|
408
|
+
(await getDbClient().get<{ count: number }>("SELECT COUNT(*) AS count FROM agent_tasks"))
|
|
409
|
+
?.count,
|
|
410
|
+
).toBe(0);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test("send-task refuses with only an offline Lead and creates no task", async () => {
|
|
414
|
+
await createAgent({
|
|
415
|
+
id: "10000000-0000-4000-8000-000000000004",
|
|
416
|
+
name: "Offline User MCP Lead",
|
|
417
|
+
isLead: true,
|
|
418
|
+
status: "offline",
|
|
419
|
+
});
|
|
420
|
+
const user = await createUser({ name: "Offline Lead Requester" });
|
|
421
|
+
const token = (await mintToken(user.id, "offline-lead", ACTOR)).plaintext;
|
|
422
|
+
const sessionId = await initialize(token);
|
|
423
|
+
await notifyInitialized(token, sessionId);
|
|
424
|
+
|
|
425
|
+
const { payload } = await mcpPost(
|
|
426
|
+
token,
|
|
427
|
+
{
|
|
428
|
+
jsonrpc: "2.0",
|
|
429
|
+
id: 3,
|
|
430
|
+
method: "tools/call",
|
|
431
|
+
params: { name: "send-task", arguments: { task: "do not queue without a Lead" } },
|
|
432
|
+
},
|
|
433
|
+
sessionId,
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
const result = payload as {
|
|
437
|
+
result: { isError?: boolean; structuredContent: { success: boolean; message: string } };
|
|
438
|
+
};
|
|
439
|
+
expect(result.result.isError).toBe(true);
|
|
440
|
+
expect(result.result.structuredContent.success).toBe(false);
|
|
441
|
+
expect(
|
|
442
|
+
(await getDbClient().get<{ count: number }>("SELECT COUNT(*) AS count FROM agent_tasks"))
|
|
443
|
+
?.count,
|
|
444
|
+
).toBe(0);
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test("send-task preserves requester ownership and get-tasks filters foreign work", async () => {
|
|
448
|
+
const lead = await createAgent({ name: "Ownership Lead", isLead: true, status: "idle" });
|
|
449
|
+
const user = await createUser({ name: "Task Requester" });
|
|
450
|
+
const otherUser = await createUser({ name: "Other Task Requester" });
|
|
451
|
+
const token = (await mintToken(user.id, "task-ownership", ACTOR)).plaintext;
|
|
452
|
+
const sessionId = await initialize(token);
|
|
453
|
+
await notifyInitialized(token, sessionId);
|
|
454
|
+
|
|
455
|
+
const sent = await mcpPost(
|
|
456
|
+
token,
|
|
457
|
+
{
|
|
458
|
+
jsonrpc: "2.0",
|
|
459
|
+
id: 3,
|
|
460
|
+
method: "tools/call",
|
|
461
|
+
params: { name: "send-task", arguments: { task: "owned user mcp task" } },
|
|
462
|
+
},
|
|
463
|
+
sessionId,
|
|
464
|
+
);
|
|
465
|
+
const sentResult = sent.payload as {
|
|
466
|
+
result: { structuredContent: { task: { id: string } } };
|
|
467
|
+
};
|
|
468
|
+
const taskId = sentResult.result.structuredContent.task.id;
|
|
469
|
+
expect(await getTaskById(taskId)).toMatchObject({
|
|
470
|
+
status: "pending",
|
|
471
|
+
agentId: lead.id,
|
|
472
|
+
requestedByUserId: user.id,
|
|
473
|
+
});
|
|
289
474
|
const foreignTask = await createTaskExtended("foreign user mcp task", {
|
|
290
475
|
requestedByUserId: otherUser.id,
|
|
291
476
|
});
|
|
@@ -312,6 +497,63 @@ describe("/mcp-user auth and tool surface", () => {
|
|
|
312
497
|
expect(listResult.result.structuredContent.tasks).toHaveLength(1);
|
|
313
498
|
});
|
|
314
499
|
|
|
500
|
+
test("send-task ignores injected routing fields and keeps internal routing authoritative", async () => {
|
|
501
|
+
const lead = await createAgent({
|
|
502
|
+
id: "10000000-0000-4000-8000-000000000003",
|
|
503
|
+
name: "Authoritative User MCP Lead",
|
|
504
|
+
isLead: true,
|
|
505
|
+
status: "idle",
|
|
506
|
+
});
|
|
507
|
+
const attacker = await createAgent({
|
|
508
|
+
id: "20000000-0000-4000-8000-000000000001",
|
|
509
|
+
name: "Injected Target",
|
|
510
|
+
isLead: false,
|
|
511
|
+
status: "idle",
|
|
512
|
+
});
|
|
513
|
+
const user = await createUser({ name: "Injection Requester" });
|
|
514
|
+
const foreignUser = await createUser({ name: "Foreign Requester" });
|
|
515
|
+
const token = (await mintToken(user.id, "routing-injection", ACTOR)).plaintext;
|
|
516
|
+
const sessionId = await initialize(token);
|
|
517
|
+
await notifyInitialized(token, sessionId);
|
|
518
|
+
|
|
519
|
+
const { response, payload } = await mcpPost(
|
|
520
|
+
token,
|
|
521
|
+
{
|
|
522
|
+
jsonrpc: "2.0",
|
|
523
|
+
id: 3,
|
|
524
|
+
method: "tools/call",
|
|
525
|
+
params: {
|
|
526
|
+
name: "send-task",
|
|
527
|
+
arguments: {
|
|
528
|
+
task: "keep server routing authoritative",
|
|
529
|
+
agentId: attacker.id,
|
|
530
|
+
routingReason: "human_pinned",
|
|
531
|
+
routingNote: "Injected routing note must not survive.",
|
|
532
|
+
offerMode: false,
|
|
533
|
+
leadOnly: true,
|
|
534
|
+
allowDuplicate: true,
|
|
535
|
+
requestedByUserId: foreignUser.id,
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
},
|
|
539
|
+
sessionId,
|
|
540
|
+
);
|
|
541
|
+
|
|
542
|
+
expect(response.status).toBe(200);
|
|
543
|
+
const result = payload as { result: { structuredContent: { task: { id: string } } } };
|
|
544
|
+
const task = await getTaskById(result.result.structuredContent.task.id);
|
|
545
|
+
expect(task).toMatchObject({
|
|
546
|
+
status: "pending",
|
|
547
|
+
agentId: lead.id,
|
|
548
|
+
requestedByUserId: user.id,
|
|
549
|
+
routingReason: "skill",
|
|
550
|
+
routingSource: "engine_default",
|
|
551
|
+
routingNote: "User MCP ingress assigns new work to the Lead for delegation.",
|
|
552
|
+
});
|
|
553
|
+
expect(task?.offeredTo).toBeUndefined();
|
|
554
|
+
expect(task?.routingAffinity?.leadOnly).not.toBe(true);
|
|
555
|
+
});
|
|
556
|
+
|
|
315
557
|
test("owner /mcp initialize requires a known X-Agent-ID", async () => {
|
|
316
558
|
const missing = await mcpPost(
|
|
317
559
|
API_KEY,
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
2
|
+
import { unlink } from "node:fs/promises";
|
|
3
|
+
import { closeDb, createAgent, getDbClient, initDb } from "../be/db";
|
|
4
|
+
import { SqliteMemoryStore } from "../be/memory/providers/sqlite-store";
|
|
5
|
+
import { memorySnippet, renderMemoriesPrompt } from "../prompts/memories";
|
|
6
|
+
|
|
7
|
+
const longPrompt = `Implement the thing. ${"Background detail that is not the outcome. ".repeat(12)}`;
|
|
8
|
+
|
|
9
|
+
describe("renderMemoriesPrompt — outcome, date and source", () => {
|
|
10
|
+
test("a task_completion line shows the output, not the start of a >300-char prompt", () => {
|
|
11
|
+
expect(longPrompt.length).toBeGreaterThan(300);
|
|
12
|
+
const prompt = renderMemoriesPrompt([
|
|
13
|
+
{
|
|
14
|
+
id: "m-1",
|
|
15
|
+
name: "Task: Implement the thing",
|
|
16
|
+
content: `Task: ${longPrompt}\n\nOutput:\nShipped PR #42; tests green.`,
|
|
17
|
+
similarity: 0.9,
|
|
18
|
+
source: "task_completion",
|
|
19
|
+
createdAt: "2026-09-29T07:20:00.000Z",
|
|
20
|
+
},
|
|
21
|
+
]);
|
|
22
|
+
expect(prompt).toContain(
|
|
23
|
+
"- [2026-09-29, task_completion] **Task: Implement the thing** (id: m-1): Shipped PR #42; tests green.",
|
|
24
|
+
);
|
|
25
|
+
expect(prompt).not.toContain("Background detail");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("a failed task_completion shows the failure reason without the boilerplate tail", () => {
|
|
29
|
+
const snippet = memorySnippet({
|
|
30
|
+
id: "m-2",
|
|
31
|
+
name: "x",
|
|
32
|
+
content: `Task: ${longPrompt}\n\nFailure reason:\nCI red on main.\n\nThis task failed. Learn from this to avoid repeating the mistake.`,
|
|
33
|
+
similarity: 0.9,
|
|
34
|
+
source: "task_completion",
|
|
35
|
+
});
|
|
36
|
+
expect(snippet).toBe("FAILED: CI red on main.");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("the last Output marker wins when the task prompt embeds an earlier one", () => {
|
|
40
|
+
const snippet = memorySnippet({
|
|
41
|
+
id: "m-3",
|
|
42
|
+
name: "x",
|
|
43
|
+
content:
|
|
44
|
+
'Task: Worker task completed.\nTask: "y"\n\nOutput:\nold worker output\n\nOutput:\nreview done',
|
|
45
|
+
similarity: 0.9,
|
|
46
|
+
source: "task_completion",
|
|
47
|
+
});
|
|
48
|
+
expect(snippet).toBe("review done");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test("summary wins when set; other sources keep their content; missing metadata adds no prefix", () => {
|
|
52
|
+
expect(
|
|
53
|
+
memorySnippet({
|
|
54
|
+
id: "m-4",
|
|
55
|
+
name: "x",
|
|
56
|
+
content: `Task: ${longPrompt}\n\nOutput:\nlong output`,
|
|
57
|
+
similarity: 0.9,
|
|
58
|
+
source: "task_completion",
|
|
59
|
+
summary: " short summary ",
|
|
60
|
+
}),
|
|
61
|
+
).toBe("short summary");
|
|
62
|
+
const prompt = renderMemoriesPrompt([
|
|
63
|
+
{ id: "m-5", name: "manual note", content: "Output:\nkeep me", similarity: 0.9 },
|
|
64
|
+
]);
|
|
65
|
+
expect(prompt).toContain("- **manual note** (id: m-5): Output:\nkeep me");
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
describe("200_backfill_auto_memory_expiry", () => {
|
|
70
|
+
const TEST_DB_PATH = "./test-memory-injection-outcome.sqlite";
|
|
71
|
+
const agentId = crypto.randomUUID();
|
|
72
|
+
let store: SqliteMemoryStore;
|
|
73
|
+
|
|
74
|
+
beforeAll(async () => {
|
|
75
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
76
|
+
try {
|
|
77
|
+
await unlink(TEST_DB_PATH + suffix);
|
|
78
|
+
} catch {}
|
|
79
|
+
}
|
|
80
|
+
initDb(TEST_DB_PATH);
|
|
81
|
+
await createAgent({ id: agentId, name: "Backfill Agent", isLead: false, status: "idle" });
|
|
82
|
+
store = new SqliteMemoryStore();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
afterAll(async () => {
|
|
86
|
+
closeDb();
|
|
87
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
88
|
+
try {
|
|
89
|
+
await unlink(TEST_DB_PATH + suffix);
|
|
90
|
+
} catch {}
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("sets a 7-day expiry on legacy agent-scope auto memories only", async () => {
|
|
95
|
+
const base = { agentId, name: "m", content: "Task: x\n\nOutput:\ny" };
|
|
96
|
+
const completion = await store.store({ ...base, scope: "agent", source: "task_completion" });
|
|
97
|
+
const session = await store.store({ ...base, scope: "agent", source: "session_summary" });
|
|
98
|
+
const swarm = await store.store({ ...base, scope: "swarm", source: "task_completion" });
|
|
99
|
+
const manual = await store.store({ ...base, scope: "agent", source: "manual" });
|
|
100
|
+
const longterm = await store.store({
|
|
101
|
+
...base,
|
|
102
|
+
scope: "agent",
|
|
103
|
+
source: "task_completion",
|
|
104
|
+
key: "/longterm/facts/backfill/keep",
|
|
105
|
+
});
|
|
106
|
+
const alreadySet = await store.store({ ...base, scope: "agent", source: "task_completion" });
|
|
107
|
+
|
|
108
|
+
const db = getDbClient();
|
|
109
|
+
const legacy = [completion, session, swarm, manual, longterm];
|
|
110
|
+
for (const row of legacy) {
|
|
111
|
+
await db.run("UPDATE agent_memory SET expiresAt = NULL WHERE id = ?", [row.id]);
|
|
112
|
+
}
|
|
113
|
+
const presetExpiry = "2099-01-01T00:00:00.000Z";
|
|
114
|
+
await db.run("UPDATE agent_memory SET expiresAt = ? WHERE id = ?", [
|
|
115
|
+
presetExpiry,
|
|
116
|
+
alreadySet.id,
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
const sql = await Bun.file(
|
|
120
|
+
new URL("../be/migrations/200_backfill_auto_memory_expiry.sql", import.meta.url),
|
|
121
|
+
).text();
|
|
122
|
+
const before = Date.now();
|
|
123
|
+
await db.run(sql);
|
|
124
|
+
|
|
125
|
+
const expiry = async (id: string) =>
|
|
126
|
+
(
|
|
127
|
+
await db.get<{ expiresAt: string | null }>(
|
|
128
|
+
"SELECT expiresAt FROM agent_memory WHERE id = ?",
|
|
129
|
+
[id],
|
|
130
|
+
)
|
|
131
|
+
)?.expiresAt ?? null;
|
|
132
|
+
|
|
133
|
+
for (const row of [completion, session]) {
|
|
134
|
+
const value = await expiry(row.id);
|
|
135
|
+
expect(value).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
|
|
136
|
+
const days = (Date.parse(value!) - before) / 86_400_000;
|
|
137
|
+
expect(days).toBeGreaterThan(6.99);
|
|
138
|
+
expect(days).toBeLessThan(7.01);
|
|
139
|
+
}
|
|
140
|
+
for (const row of [swarm, manual, longterm]) expect(await expiry(row.id)).toBeNull();
|
|
141
|
+
expect(await expiry(alreadySet.id)).toBe(presetExpiry);
|
|
142
|
+
});
|
|
143
|
+
});
|