@desplega.ai/agent-swarm 1.131.1 → 1.133.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/{actions-jgcy477k.js → actions-cskpayd7.js} +11 -9
- package/dist/{anthropic-messages-rjmd2e7n.js → anthropic-messages-tpxtbx7b.js} +28 -12
- package/dist/{app-5da78bd4.js → app-ang77nbb.js} +8 -6
- package/dist/{assistant-f2vkk1hj.js → assistant-b4exebdw.js} +16 -13
- package/dist/{azure-openai-responses-c0yjmbfd.js → azure-openai-responses-m3921t4y.js} +8 -8
- package/dist/{boot-reembed-q7mp8hcr.js → boot-reembed-ps2aftpb.js} +9 -7
- package/dist/{boot-reembed-xmw8wcx6.js → boot-reembed-znyk3nxb.js} +10 -8
- package/dist/{boot-scrub-logs-dsqrv79n.js → boot-scrub-logs-2ax3xgz1.js} +7 -5
- package/dist/{claude-adapter-q6x7yspx.js → claude-adapter-3ct8kgxw.js} +13 -9
- package/dist/{claude-managed-adapter-wsg37bt8.js → claude-managed-adapter-akx4ntf2.js} +2 -2
- package/dist/{cli-114fvtmj.js → cli-13w2hwkh.js} +1 -1
- package/dist/{cli-4rkfg1wx.js → cli-1532szb1.js} +8 -6
- package/dist/{cli-g74vpjp2.js → cli-23d3rseb.js} +33 -17
- package/dist/{cli-3ty0t082.js → cli-2a75z9c5.js} +196 -7
- package/dist/{cli-pxfxkx5m.js → cli-2t30rj0d.js} +1 -1
- package/dist/{cli-rgrfzg8z.js → cli-3c3tdgab.js} +10 -10
- package/dist/{cli-6dp3ydxa.js → cli-3zzxa9jz.js} +15 -11
- package/dist/{cli-2f07m0xr.js → cli-4k6233gc.js} +1 -1
- package/dist/{cli-t8d349cy.js → cli-5p2tz05r.js} +2 -2
- package/dist/{cli-w2wxd56v.js → cli-5z2yhzvd.js} +127 -49
- package/dist/{cli-kd1ck4rx.js → cli-6zfq31nj.js} +9 -3
- package/dist/{cli-er3a4bb0.js → cli-8m9de10a.js} +6 -1
- package/dist/{cli-gm9cbhbq.js → cli-8t14vg9w.js} +27 -8
- package/dist/{cli-ccf9vy7q.js → cli-9nfc2es8.js} +6 -6
- package/dist/{cli-ycrnynpw.js → cli-9ta3pwms.js} +11 -4
- package/dist/{cli-7dm2zqy2.js → cli-avr0ns7z.js} +2 -2
- package/dist/{cli-n6h8qt8n.js → cli-c897wyhw.js} +12 -7
- package/dist/{cli-gecy3z2s.js → cli-dgk20ft7.js} +4 -4
- package/dist/cli-et1bbwqf.js +13 -0
- package/dist/{cli-bqw0mt1n.js → cli-f5w27pj3.js} +3 -3
- package/dist/{cli-2wadx7p2.js → cli-f8dre4bz.js} +2 -2
- package/dist/{cli-6pcz9h15.js → cli-ftwpv451.js} +170 -387
- package/dist/{cli-nrkkknat.js → cli-g854s2dw.js} +1 -1
- package/dist/{cli-793jvbsh.js → cli-ggmdxeqa.js} +13 -8
- package/dist/cli-hkwfe3qj.js +530 -0
- package/dist/{cli-5dsbcjjw.js → cli-jqwnqx9m.js} +1 -1
- package/dist/{cli-9dr5qzc0.js → cli-ny122vh4.js} +21 -10
- package/dist/{cli-sy7jk3tx.js → cli-pvzqhbs9.js} +2 -2
- package/dist/cli-pzq5rjym.js +15 -0
- package/dist/{cli-xpxde7rm.js → cli-qes8ef67.js} +48 -30
- package/dist/{cli-sajgpkqg.js → cli-qs9hhsvb.js} +1723 -325
- package/dist/cli-qys3rhhz.js +27 -0
- package/dist/{cli-se2p6rkb.js → cli-r6tpm37h.js} +2 -2
- package/dist/{cli-g1re8r05.js → cli-rch80c76.js} +2 -2
- package/dist/{cli-0258203w.js → cli-rpp0x4zy.js} +137 -17
- package/dist/{cli-5n6e5yfn.js → cli-s1k0wxhe.js} +12 -7
- package/dist/{cli-aca5pz9f.js → cli-sfd9v7sj.js} +1 -1
- package/dist/{cli-2rk0cx7s.js → cli-sh3ytsyj.js} +49 -39
- package/dist/{cli-zsspvvm2.js → cli-w1mp9jy7.js} +1 -1
- package/dist/{cli-6dpytdr2.js → cli-xdb4mpnp.js} +2 -2
- package/dist/{cli-57khth04.js → cli-xmyqv00z.js} +120 -4
- package/dist/{cli-jbcva0xj.js → cli-xv8t8p3c.js} +56 -31
- package/dist/{cli-f1am9szv.js → cli-xxv5pn4f.js} +1 -1
- package/dist/{cli-jevgrb4w.js → cli-y08jvxzx.js} +601 -140
- package/dist/{cli-50vbt00w.js → cli-zfbf3srj.js} +650 -245
- package/dist/{cli-ygrbmjja.js → cli-zmq89k34.js} +6 -6
- package/dist/cli.js +22 -22
- package/dist/{codex-adapter-htjkn22b.js → codex-adapter-bef93pq4.js} +9 -7
- package/dist/{codex-hook-hq54t9ft.js → codex-hook-e8dba65p.js} +3 -2
- package/dist/{codex-session-runner-wjfc7smz.js → codex-session-runner-5pkxys7x.js} +9 -7
- package/dist/{commands-xhrfwqsk.js → commands-e23s1bff.js} +9 -7
- package/dist/{db-anvwb22f.js → db-m3nabatn.js} +17 -5
- package/dist/{google-generative-ai-hysrd8qx.js → google-generative-ai-8maw55xh.js} +11 -8
- package/dist/{google-vertex-p5arrga0.js → google-vertex-8vjqmx8g.js} +11 -8
- package/dist/{handlers-b9kbc621.js → handlers-akazedvr.js} +16 -13
- package/dist/{hook-xkvn6c5h.js → hook-ze4cpf0v.js} +55 -41
- package/dist/{hook-cgk4wxmx.js → hook-zfqyyenw.js} +10 -7
- package/dist/{http-9xh6ta4r.js → http-sja0n6kw.js} +608 -147
- package/dist/index-0gr8vd6k.js +39 -0
- package/dist/{index-eaxyqcmj.js → index-354vkhdv.js} +154 -74
- package/dist/{index-070v4eyb.js → index-5qthek5t.js} +2 -3
- package/dist/{index-25wjsdv2.js → index-95t3cqq5.js} +16 -14
- package/dist/{index-9znhar3x.js → index-sea66619.js} +3 -4
- package/dist/{index-ywds4812.js → index-vpn36mma.js} +15 -13
- package/dist/{keepalive-vc7s2aa6.js → keepalive-3sp3meqj.js} +16 -10
- package/dist/{lead-r2yqfmx7.js → lead-mefeewkf.js} +39 -36
- package/dist/{maintenance-mw70k468.js → maintenance-8tbrbjfg.js} +10 -8
- package/dist/mistral-conversations-00zrp7g4.js +717 -0
- package/dist/{oauth-refresh-sweep-yecgjpk4.js → oauth-refresh-sweep-srjwjz6m.js} +8 -6
- package/dist/{onboard-xj0f73f9.js → onboard-mt2tgyjg.js} +7 -7
- package/dist/{openai-codex-responses-5g27pd6f.js → openai-codex-responses-map32h2s.js} +18 -18
- package/dist/{openai-completions-3vkmfz2a.js → openai-completions-w871b1cq.js} +12 -11
- package/dist/{openai-responses-dg4t7j7a.js → openai-responses-b0j7xnw8.js} +15 -12
- package/dist/{opencode-adapter-1gm38aba.js → opencode-adapter-b39jefcj.js} +5 -3
- package/dist/{openrouter-images-g3gm8931.js → openrouter-images-srfkywy0.js} +4 -4
- package/dist/{otel-impl-nxg0n0b3.js → otel-impl-z6pxt3j9.js} +224 -9
- package/dist/{pi-mono-adapter-4g5kby31.js → pi-mono-adapter-btvqmj58.js} +1138 -408
- package/dist/{pricing-refresh-2cn6effr.js → pricing-refresh-kyq8cezh.js} +9 -7
- package/dist/{rbac-roles-1hcxdtg7.js → rbac-roles-3ryfrfna.js} +8 -6
- package/dist/{rbac-roles-vynday5g.js → rbac-roles-6qfvbee1.js} +9 -7
- package/dist/{render-v2-tc0bh8kk.js → render-v2-p61ppjey.js} +8 -6
- package/dist/{seed-pricing-8kceb43f.js → seed-pricing-j56bbj4y.js} +8 -6
- package/dist/{setup-46xtgq3c.js → setup-tpr6c1kn.js} +2 -2
- package/dist/{worker-qrbz6hmv.js → worker-9w1v0mbb.js} +39 -36
- package/openapi.json +542 -16
- package/package.json +6 -6
- package/src/be/budget-refusal-notify.ts +14 -7
- package/src/be/db-queries/task-artifacts.ts +182 -0
- package/src/be/db.ts +783 -169
- package/src/be/events.ts +5 -0
- package/src/be/migrations/131_session_logs_created_at_index.sql +2 -0
- package/src/be/migrations/132_multi_runtime_instances.sql +44 -0
- package/src/be/migrations/133_workflow_run_trigger_type.sql +12 -0
- package/src/be/migrations/135_backfill_task_pull_request_attachments.sql +228 -0
- package/src/be/migrations/136_task_requester_provenance.sql +37 -0
- package/src/be/migrations/137_memory_retrieval_composite_index.sql +1 -0
- package/src/be/multi-runtime.ts +430 -0
- package/src/be/scripts/extract-schema.ts +3 -1
- package/src/be/scripts/typecheck.ts +3 -2
- package/src/be/seed-skills/index.ts +7 -0
- package/src/be/steering.ts +1 -1
- package/src/be/swarm-config-guard.ts +18 -0
- package/src/commands/credential-wait.ts +54 -0
- package/src/commands/onboard/steps/core-credentials.tsx +1 -1
- package/src/commands/onboard/steps/custom-templates.tsx +1 -1
- package/src/commands/onboard/steps/harness-credentials.tsx +1 -1
- package/src/commands/onboard/steps/health-check.tsx +1 -1
- package/src/commands/onboard/steps/start.tsx +1 -1
- package/src/commands/profile-sync-audit.ts +56 -0
- package/src/commands/profile-sync.ts +287 -15
- package/src/commands/provider-credentials.ts +46 -19
- package/src/commands/runner.ts +196 -33
- package/src/commands/templates.ts +22 -0
- package/src/github/handlers.ts +55 -9
- package/src/heartbeat/heartbeat.ts +71 -12
- package/src/hooks/hook.ts +49 -33
- package/src/http/active-sessions.ts +11 -1
- package/src/http/agents.ts +261 -30
- package/src/http/all-routes.ts +1 -0
- package/src/http/config.ts +13 -2
- package/src/http/core.ts +102 -4
- package/src/http/db-query-bounded.ts +313 -0
- package/src/http/db-query-shared.ts +249 -0
- package/src/http/db-query.ts +54 -42
- package/src/http/events.ts +2 -0
- package/src/http/index.ts +22 -2
- package/src/http/mcp-bridge.ts +37 -3
- package/src/http/mcp.ts +12 -0
- package/src/http/memory.ts +13 -2
- package/src/http/openapi.ts +1 -0
- package/src/http/poll.ts +70 -27
- package/src/http/route-def.ts +43 -9
- package/src/http/schedules.ts +2 -2
- package/src/http/scripts.ts +10 -1
- package/src/http/session-data.ts +44 -0
- package/src/http/tasks.ts +2 -2
- package/src/http/users.ts +74 -52
- package/src/http/webhooks.ts +5 -2
- package/src/http/workflows.ts +1 -4
- package/src/integrations/kapso/inbound.ts +1 -1
- package/src/jira/outbound.ts +39 -8
- package/src/jira/webhook-lifecycle.ts +13 -2
- package/src/linear/outbound.ts +43 -10
- package/src/oauth/keepalive.ts +13 -2
- package/src/prompts/base-prompt.ts +1 -1
- package/src/prompts/session-templates.ts +45 -7
- package/src/providers/claude-adapter.ts +6 -0
- package/src/providers/codex-adapter.ts +6 -0
- package/src/providers/opencode-adapter.ts +6 -0
- package/src/providers/pi-mono-adapter.ts +8 -0
- package/src/providers/pricing-sources.md +24 -0
- package/src/queue-stall-alarm.ts +216 -0
- package/src/scheduler/scheduler.ts +7 -0
- package/src/script-workflows/executor.ts +2 -1
- package/src/scripts-runtime/executors/native.ts +2 -1
- package/src/scripts-runtime/executors/types.ts +5 -0
- package/src/scripts-runtime/loader.ts +5 -0
- package/src/scripts-runtime/swarm-config.ts +8 -0
- package/src/scripts-runtime/swarm-sdk.ts +7 -0
- package/src/scripts-runtime/types/stdlib.d.ts +1 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
- package/src/tests/budget-claim-gate.test.ts +20 -0
- package/src/tests/claude-adapter.test.ts +32 -0
- package/src/tests/codex-adapter.test.ts +30 -0
- package/src/tests/create-task-validation.test.ts +128 -0
- package/src/tests/credential-wait.test.ts +67 -1
- package/src/tests/db-query.test.ts +711 -2
- package/src/tests/http-api-integration.test.ts +81 -0
- package/src/tests/http-users.test.ts +166 -1
- package/src/tests/identity-field-budget.test.ts +176 -0
- package/src/tests/mcp-tools-user.test.ts +34 -0
- package/src/tests/mcp-user-route.test.ts +17 -0
- package/src/tests/metrics-http.test.ts +44 -0
- package/src/tests/multi-runtime-registration.test.ts +2703 -0
- package/src/tests/openapi-response-contract.test.ts +24 -0
- package/src/tests/openapi-route-headers.test.ts +83 -0
- package/src/tests/opencode-adapter.test.ts +40 -0
- package/src/tests/outbound-listener-rejection.test.ts +138 -0
- package/src/tests/pi-mono-adapter.test.ts +98 -0
- package/src/tests/profile-sync-warning.test.ts +277 -0
- package/src/tests/profile-sync.test.ts +232 -6
- package/src/tests/prompt-template-session.test.ts +32 -3
- package/src/tests/queue-stall-alarm.test.ts +226 -0
- package/src/tests/requester-comms.test.ts +32 -0
- package/src/tests/runner-requester-profile.test.ts +38 -0
- package/src/tests/schedule-target-type.test.ts +38 -0
- package/src/tests/script-connections.test.ts +13 -1
- package/src/tests/scripts-runtime-identity.test.ts +375 -0
- package/src/tests/scripts-runtime.test.ts +4 -3
- package/src/tests/sdk-allowlist.test.ts +47 -0
- package/src/tests/send-task-requested-by.test.ts +9 -0
- package/src/tests/session-costs.test.ts +506 -1
- package/src/tests/shutdown-error-scrubbing.test.ts +92 -0
- package/src/tests/steering-core.test.ts +28 -0
- package/src/tests/task-artifacts.test.ts +386 -0
- package/src/tests/task-attachments-schema.test.ts +17 -1
- package/src/tests/task-completion-idempotency.test.ts +57 -0
- package/src/tests/task-pull-request-backfill-migration.test.ts +321 -0
- package/src/tests/update-profile-api.test.ts +67 -39
- package/src/tests/update-profile-auth.test.ts +48 -4
- package/src/tests/user-attribution-migration.test.ts +54 -0
- package/src/tests/user-identity.test.ts +72 -0
- package/src/tests/workflow-async-v2.test.ts +39 -1
- package/src/tests/workflow-http-v2.test.ts +3 -2
- package/src/tests/workflow-mcp-trigger-schema.test.ts +52 -3
- package/src/tests/workflow-retry-v2.test.ts +45 -1
- package/src/tools/db-query.ts +18 -8
- package/src/tools/get-task-details.ts +24 -3
- package/src/tools/manage-user.ts +36 -3
- package/src/tools/poll-task.ts +93 -14
- package/src/tools/script-common.ts +5 -0
- package/src/tools/send-task.ts +1 -4
- package/src/tools/store-progress.ts +13 -2
- package/src/tools/swarm-config/delete-config.ts +9 -0
- package/src/tools/swarm-config/set-config.ts +3 -2
- package/src/tools/task-action.ts +30 -0
- package/src/tools/task-tool-ctx.ts +8 -1
- package/src/tools/update-profile.ts +16 -21
- package/src/tools/utils.ts +5 -0
- package/src/tools/workflows/trigger-workflow.ts +7 -1
- package/src/types.ts +165 -1
- package/src/utils/github-pull-request.ts +76 -0
- package/src/utils/identity-field-budget.ts +73 -0
- package/src/utils/multi-runtime.ts +20 -0
- package/src/utils/requester-comms.ts +24 -0
- package/src/workflows/engine.ts +22 -2
- package/src/workflows/resume.ts +19 -11
- package/src/workflows/retry-poller.ts +1 -0
- package/src/workflows/triggers.ts +4 -0
- package/templates/skills/db-query-guidance/config.json +14 -0
- package/templates/skills/db-query-guidance/content.md +57 -0
- package/dist/cli-3te4jdkm.js +0 -225
- package/dist/cli-bmze986d.js +0 -7
- package/dist/cli-kh3ba2zf.js +0 -607
- package/dist/index-vg4snay4.js +0 -36
- package/dist/mistral-conversations-81fwav0j.js +0 -30578
- package/dist/otel-nqa12wf3.js +0 -3149
- package/dist/{cli-wz9115x0.js → cli-zr9q011v.js} +6 -6
- package/dist/{pi-messages-tqj8q6b9.js → pi-messages-sssy32x4.js} +3 -3
|
@@ -3,15 +3,21 @@ import { unlinkSync } from "node:fs";
|
|
|
3
3
|
import {
|
|
4
4
|
closeDb,
|
|
5
5
|
createAgent,
|
|
6
|
+
createScheduledTask,
|
|
6
7
|
createTaskExtended,
|
|
7
8
|
createUser,
|
|
9
|
+
createWorkflow,
|
|
10
|
+
createWorkflowRun,
|
|
8
11
|
deleteUser,
|
|
9
12
|
getAllUsers,
|
|
10
13
|
getDb,
|
|
14
|
+
getScheduledTaskById,
|
|
11
15
|
getTaskById,
|
|
12
16
|
getUserById,
|
|
17
|
+
getWorkflowRun,
|
|
13
18
|
initDb,
|
|
14
19
|
updateUser,
|
|
20
|
+
updateWorkflowRun,
|
|
15
21
|
} from "../be/db";
|
|
16
22
|
import {
|
|
17
23
|
findOrCreateUserByEmail,
|
|
@@ -276,13 +282,69 @@ describe("deleteUser", () => {
|
|
|
276
282
|
source: "slack",
|
|
277
283
|
requestedByUserId: user.id,
|
|
278
284
|
});
|
|
285
|
+
getDb().prepare("UPDATE agent_tasks SET created_by = ? WHERE id = ?").run(user.id, task.id);
|
|
279
286
|
expect(getTaskById(task.id)!.requestedByUserId).toBe(user.id);
|
|
287
|
+
expect(
|
|
288
|
+
getDb()
|
|
289
|
+
.prepare<{ created_by: string | null }, [string]>(
|
|
290
|
+
"SELECT created_by FROM agent_tasks WHERE id = ?",
|
|
291
|
+
)
|
|
292
|
+
.get(task.id)?.created_by,
|
|
293
|
+
).toBe(user.id);
|
|
280
294
|
|
|
281
295
|
deleteUser(user.id);
|
|
282
296
|
expect(getTaskById(task.id)!.requestedByUserId).toBeUndefined();
|
|
297
|
+
expect(
|
|
298
|
+
getDb()
|
|
299
|
+
.prepare<{ created_by: string | null }, [string]>(
|
|
300
|
+
"SELECT created_by FROM agent_tasks WHERE id = ?",
|
|
301
|
+
)
|
|
302
|
+
.get(task.id)?.created_by,
|
|
303
|
+
).toBeNull();
|
|
283
304
|
// ON DELETE CASCADE on user_external_ids.userId should clear the mapping.
|
|
284
305
|
expect(findUserByExternalId("slack", "U_TASKOWNER")).toBeNull();
|
|
285
306
|
});
|
|
307
|
+
|
|
308
|
+
test("clears retained workflow run attribution before deleting the user", () => {
|
|
309
|
+
const user = createUser({ name: "Workflow Trigger" });
|
|
310
|
+
const workflow = createWorkflow({
|
|
311
|
+
name: `delete-user-workflow-${crypto.randomUUID()}`,
|
|
312
|
+
definition: { nodes: [] },
|
|
313
|
+
});
|
|
314
|
+
const run = createWorkflowRun({
|
|
315
|
+
id: crypto.randomUUID(),
|
|
316
|
+
workflowId: workflow.id,
|
|
317
|
+
createdBy: user.id,
|
|
318
|
+
});
|
|
319
|
+
updateWorkflowRun(run.id, {
|
|
320
|
+
context: {
|
|
321
|
+
swarm: { requestedByUserId: user.id, retained: "swarm-value" },
|
|
322
|
+
retained: "root-value",
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
expect(deleteUser(user.id)).toBe(true);
|
|
327
|
+
expect(getUserById(user.id)).toBeNull();
|
|
328
|
+
expect(getWorkflowRun(run.id)?.createdBy).toBeUndefined();
|
|
329
|
+
expect(getWorkflowRun(run.id)?.context).toEqual({
|
|
330
|
+
swarm: { retained: "swarm-value" },
|
|
331
|
+
retained: "root-value",
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
test("clears schedule attribution whose audit FKs were dropped by migration 103", () => {
|
|
336
|
+
const user = createUser({ name: "Schedule Trigger" });
|
|
337
|
+
const schedule = createScheduledTask({
|
|
338
|
+
name: `delete-user-schedule-${crypto.randomUUID()}`,
|
|
339
|
+
intervalMs: 60_000,
|
|
340
|
+
taskTemplate: "Run scheduled work",
|
|
341
|
+
createdBy: user.id,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
expect(deleteUser(user.id)).toBe(true);
|
|
345
|
+
expect(getScheduledTaskById(schedule.id)?.createdBy).toBeUndefined();
|
|
346
|
+
expect(getScheduledTaskById(schedule.id)?.updatedBy).toBeUndefined();
|
|
347
|
+
});
|
|
286
348
|
});
|
|
287
349
|
|
|
288
350
|
// ─── findUserByExternalId ─────────────────────────────────────────────────────
|
|
@@ -518,6 +580,13 @@ describe("recordIdentityEvent", () => {
|
|
|
518
580
|
// ─── requestedByUserId on tasks ───────────────────────────────────────────────
|
|
519
581
|
|
|
520
582
|
describe("requestedByUserId in tasks", () => {
|
|
583
|
+
const requesterWasInherited = (taskId: string): number | undefined =>
|
|
584
|
+
getDb()
|
|
585
|
+
.prepare<{ requestedByUserIdInherited: number }, [string]>(
|
|
586
|
+
"SELECT requestedByUserIdInherited FROM agent_tasks WHERE id = ?",
|
|
587
|
+
)
|
|
588
|
+
.get(taskId)?.requestedByUserIdInherited;
|
|
589
|
+
|
|
521
590
|
test("createTaskExtended stores requestedByUserId", () => {
|
|
522
591
|
const user = createUser({ name: "Requester" });
|
|
523
592
|
const task = createTaskExtended("task with requester", {
|
|
@@ -527,6 +596,7 @@ describe("requestedByUserId in tasks", () => {
|
|
|
527
596
|
});
|
|
528
597
|
const fetched = getTaskById(task.id);
|
|
529
598
|
expect(fetched!.requestedByUserId).toBe(user.id);
|
|
599
|
+
expect(requesterWasInherited(task.id)).toBe(0);
|
|
530
600
|
deleteUser(user.id);
|
|
531
601
|
});
|
|
532
602
|
|
|
@@ -544,6 +614,7 @@ describe("requestedByUserId in tasks", () => {
|
|
|
544
614
|
});
|
|
545
615
|
const fetchedChild = getTaskById(child.id);
|
|
546
616
|
expect(fetchedChild!.requestedByUserId).toBe(user.id);
|
|
617
|
+
expect(requesterWasInherited(child.id)).toBe(1);
|
|
547
618
|
deleteUser(user.id);
|
|
548
619
|
});
|
|
549
620
|
|
|
@@ -562,6 +633,7 @@ describe("requestedByUserId in tasks", () => {
|
|
|
562
633
|
requestedByUserId: user2.id,
|
|
563
634
|
});
|
|
564
635
|
expect(getTaskById(child.id)!.requestedByUserId).toBe(user2.id);
|
|
636
|
+
expect(requesterWasInherited(child.id)).toBe(0);
|
|
565
637
|
deleteUser(user1.id);
|
|
566
638
|
deleteUser(user2.id);
|
|
567
639
|
});
|
|
@@ -152,7 +152,7 @@ describe("Workflow Async v2 (Phase 4)", () => {
|
|
|
152
152
|
expect(task!.task).toBe("Do the thing");
|
|
153
153
|
});
|
|
154
154
|
|
|
155
|
-
test("
|
|
155
|
+
test("persists the requester on the run and attributes the root task", async () => {
|
|
156
156
|
const user = createUser({ name: "Workflow Requester" });
|
|
157
157
|
const workflow = makeWorkflow({
|
|
158
158
|
nodes: [
|
|
@@ -170,6 +170,7 @@ describe("Workflow Async v2 (Phase 4)", () => {
|
|
|
170
170
|
const steps = getWorkflowRunStepsByRunId(runId);
|
|
171
171
|
const task = getTaskByWorkflowRunStepId(steps[0]!.id);
|
|
172
172
|
|
|
173
|
+
expect(getWorkflowRun(runId)?.createdBy).toBe(user.id);
|
|
173
174
|
expect(task).toBeTruthy();
|
|
174
175
|
expect(task!.requestedByUserId).toBe(user.id);
|
|
175
176
|
});
|
|
@@ -251,6 +252,43 @@ describe("Workflow Async v2 (Phase 4)", () => {
|
|
|
251
252
|
expect(completedSteps).toHaveLength(2);
|
|
252
253
|
});
|
|
253
254
|
|
|
255
|
+
test("resume rehydrates requester attribution from the workflow run", async () => {
|
|
256
|
+
const user = createUser({ name: "Resumed Workflow Requester" });
|
|
257
|
+
const workflow = makeWorkflow({
|
|
258
|
+
nodes: [
|
|
259
|
+
{
|
|
260
|
+
id: "task1",
|
|
261
|
+
type: "agent-task",
|
|
262
|
+
config: { template: "First attributed task" },
|
|
263
|
+
next: "task2",
|
|
264
|
+
},
|
|
265
|
+
{
|
|
266
|
+
id: "task2",
|
|
267
|
+
type: "agent-task",
|
|
268
|
+
config: { template: "Second attributed task" },
|
|
269
|
+
},
|
|
270
|
+
],
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const runId = await startWorkflowExecution(workflow, {}, registry, {
|
|
274
|
+
requestedByUserId: user.id,
|
|
275
|
+
});
|
|
276
|
+
const firstStep = getWorkflowRunStepsByRunId(runId).find((step) => step.nodeId === "task1")!;
|
|
277
|
+
const firstTask = getTaskByWorkflowRunStepId(firstStep.id)!;
|
|
278
|
+
|
|
279
|
+
workflowEventBus.emit("task.completed", {
|
|
280
|
+
taskId: firstTask.id,
|
|
281
|
+
output: "done",
|
|
282
|
+
workflowRunId: runId,
|
|
283
|
+
workflowRunStepId: firstStep.id,
|
|
284
|
+
});
|
|
285
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
286
|
+
|
|
287
|
+
const secondStep = getWorkflowRunStepsByRunId(runId).find((step) => step.nodeId === "task2");
|
|
288
|
+
expect(secondStep).toBeDefined();
|
|
289
|
+
expect(getTaskByWorkflowRunStepId(secondStep!.id)?.requestedByUserId).toBe(user.id);
|
|
290
|
+
});
|
|
291
|
+
|
|
254
292
|
test("resume from task failure marks run as failed", async () => {
|
|
255
293
|
const workflow = makeWorkflow({
|
|
256
294
|
nodes: [
|
|
@@ -607,7 +607,7 @@ describe("Workflow HTTP API v2", () => {
|
|
|
607
607
|
expect(res.status).toBe(201);
|
|
608
608
|
});
|
|
609
609
|
|
|
610
|
-
test("
|
|
610
|
+
test("does not attribute a trigger with no user auth to the workflow author", async () => {
|
|
611
611
|
const author = createUser({
|
|
612
612
|
name: "Workflow Author",
|
|
613
613
|
email: `workflow-author-${crypto.randomUUID()}@example.com`,
|
|
@@ -622,7 +622,8 @@ describe("Workflow HTTP API v2", () => {
|
|
|
622
622
|
});
|
|
623
623
|
expect(res.status).toBe(201);
|
|
624
624
|
const { runId } = (await res.json()) as { runId: string };
|
|
625
|
-
expect(getWorkflowRun(runId)?.
|
|
625
|
+
expect(getWorkflowRun(runId)?.createdBy).toBeUndefined();
|
|
626
|
+
expect(getWorkflowRun(runId)?.context?.swarm).toBeUndefined();
|
|
626
627
|
});
|
|
627
628
|
|
|
628
629
|
test("returns 400 for disabled workflow", async () => {
|
|
@@ -8,7 +8,19 @@ import {
|
|
|
8
8
|
type ServerResponse,
|
|
9
9
|
} from "node:http";
|
|
10
10
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
closeDb,
|
|
13
|
+
createAgent,
|
|
14
|
+
createTaskExtended,
|
|
15
|
+
createUser,
|
|
16
|
+
createWorkflow,
|
|
17
|
+
deleteWorkflow,
|
|
18
|
+
getTaskByWorkflowRunStepId,
|
|
19
|
+
getWorkflow,
|
|
20
|
+
getWorkflowRun,
|
|
21
|
+
getWorkflowRunStepsByRunId,
|
|
22
|
+
initDb,
|
|
23
|
+
} from "../be/db";
|
|
12
24
|
import { getPathSegments, parseQueryParams } from "../http/utils";
|
|
13
25
|
import { handleWorkflows } from "../http/workflows";
|
|
14
26
|
import { SCRIPT_LONG_TIMEOUT_HINT_MS } from "../scripts-runtime/executors/types";
|
|
@@ -66,12 +78,17 @@ function buildServerWithTools() {
|
|
|
66
78
|
|
|
67
79
|
const callTool =
|
|
68
80
|
(name: string) =>
|
|
69
|
-
async (args: Record<string, unknown>, agentId = "agent-test") => {
|
|
81
|
+
async (args: Record<string, unknown>, agentId = "agent-test", sourceTaskId?: string) => {
|
|
70
82
|
const tool = registeredTools[name];
|
|
71
83
|
expect(tool).toBeDefined();
|
|
72
84
|
const extra = {
|
|
73
85
|
sessionId: "session-test",
|
|
74
|
-
requestInfo: {
|
|
86
|
+
requestInfo: {
|
|
87
|
+
headers: {
|
|
88
|
+
"x-agent-id": agentId,
|
|
89
|
+
...(sourceTaskId ? { "x-source-task-id": sourceTaskId } : {}),
|
|
90
|
+
},
|
|
91
|
+
},
|
|
75
92
|
};
|
|
76
93
|
return (await tool.handler(args, extra)) as ToolResult;
|
|
77
94
|
};
|
|
@@ -579,6 +596,38 @@ describe("MCP create-workflow / update-workflow / patch-workflow accept triggerS
|
|
|
579
596
|
|
|
580
597
|
// ─── Phase 3: trigger-workflow surfaces TriggerSchemaError ──
|
|
581
598
|
|
|
599
|
+
test("trigger-workflow attributes the run and its root task to the calling task's requester", async () => {
|
|
600
|
+
const requester = createUser({ name: "MCP Workflow Requester" });
|
|
601
|
+
const agent = createAgent({
|
|
602
|
+
name: uniqueName("mcp-trigger-agent"),
|
|
603
|
+
isLead: false,
|
|
604
|
+
status: "idle",
|
|
605
|
+
});
|
|
606
|
+
const sourceTask = createTaskExtended("Trigger a workflow", {
|
|
607
|
+
agentId: agent.id,
|
|
608
|
+
requestedByUserId: requester.id,
|
|
609
|
+
});
|
|
610
|
+
const workflow = createWorkflow({
|
|
611
|
+
name: uniqueName("mcp-trigger-attribution"),
|
|
612
|
+
definition: minimalDefinition,
|
|
613
|
+
});
|
|
614
|
+
createdWorkflowIds.push(workflow.id);
|
|
615
|
+
|
|
616
|
+
const triggered = await tools.callTrigger(
|
|
617
|
+
{ id: workflow.id, triggerData: { source: "test" } },
|
|
618
|
+
agent.id,
|
|
619
|
+
sourceTask.id,
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
expect(triggered.structuredContent?.success).toBe(true);
|
|
623
|
+
const runId = triggered.structuredContent?.runId as string;
|
|
624
|
+
expect(getWorkflowRun(runId)?.createdBy).toBe(requester.id);
|
|
625
|
+
|
|
626
|
+
const [step] = getWorkflowRunStepsByRunId(runId);
|
|
627
|
+
expect(step).toBeDefined();
|
|
628
|
+
expect(getTaskByWorkflowRunStepId(step!.id)?.requestedByUserId).toBe(requester.id);
|
|
629
|
+
});
|
|
630
|
+
|
|
582
631
|
test("trigger-workflow with missing required field → structured TriggerSchemaError", async () => {
|
|
583
632
|
const triggerSchema: Record<string, unknown> = {
|
|
584
633
|
type: "object",
|
|
@@ -3,6 +3,7 @@ import { unlink } from "node:fs/promises";
|
|
|
3
3
|
import { z } from "zod";
|
|
4
4
|
import {
|
|
5
5
|
closeDb,
|
|
6
|
+
createUser,
|
|
6
7
|
createWorkflow,
|
|
7
8
|
createWorkflowRun,
|
|
8
9
|
createWorkflowRunStep,
|
|
@@ -15,7 +16,7 @@ import {
|
|
|
15
16
|
updateWorkflowRun,
|
|
16
17
|
updateWorkflowRunStep,
|
|
17
18
|
} from "../be/db";
|
|
18
|
-
import type { RetryPolicy, Workflow, WorkflowDefinition } from "../types";
|
|
19
|
+
import type { ExecutorMeta, RetryPolicy, Workflow, WorkflowDefinition } from "../types";
|
|
19
20
|
import { startWorkflowExecution } from "../workflows/engine";
|
|
20
21
|
import { workflowEventBus } from "../workflows/event-bus";
|
|
21
22
|
import {
|
|
@@ -33,6 +34,7 @@ const TEST_DB_PATH = "./test-workflow-retry-v2.sqlite";
|
|
|
33
34
|
// ─── Test Executors ──────────────────────────────────────────
|
|
34
35
|
|
|
35
36
|
let failCounter = 0;
|
|
37
|
+
const failExecutorMeta: ExecutorMeta[] = [];
|
|
36
38
|
|
|
37
39
|
class FailOnceExecutor extends BaseExecutor<
|
|
38
40
|
typeof FailOnceExecutor.schema,
|
|
@@ -48,7 +50,10 @@ class FailOnceExecutor extends BaseExecutor<
|
|
|
48
50
|
|
|
49
51
|
protected async execute(
|
|
50
52
|
config: z.infer<typeof FailOnceExecutor.schema>,
|
|
53
|
+
_context: Readonly<Record<string, unknown>>,
|
|
54
|
+
meta: ExecutorMeta,
|
|
51
55
|
): Promise<ExecutorResult<z.infer<typeof FailOnceExecutor.outSchema>>> {
|
|
56
|
+
failExecutorMeta.push(meta);
|
|
52
57
|
failCounter++;
|
|
53
58
|
if (failCounter <= config.failUntilAttempt) {
|
|
54
59
|
return { status: "failed", error: `Intentional failure #${failCounter}` };
|
|
@@ -219,6 +224,45 @@ describe("Workflow Retry v2 (Phase 4)", () => {
|
|
|
219
224
|
});
|
|
220
225
|
|
|
221
226
|
describe("Retry Poller", () => {
|
|
227
|
+
test("rehydrates requester attribution before retrying an executor", async () => {
|
|
228
|
+
const user = createUser({ name: "Retry Requester" });
|
|
229
|
+
const workflow = makeWorkflow({
|
|
230
|
+
nodes: [
|
|
231
|
+
{
|
|
232
|
+
id: "attributed-retry",
|
|
233
|
+
type: "fail-once",
|
|
234
|
+
config: { failUntilAttempt: 1 },
|
|
235
|
+
retry: {
|
|
236
|
+
strategy: "static",
|
|
237
|
+
maxRetries: 2,
|
|
238
|
+
baseDelayMs: 10,
|
|
239
|
+
maxDelayMs: 10,
|
|
240
|
+
},
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
failCounter = 0;
|
|
246
|
+
failExecutorMeta.length = 0;
|
|
247
|
+
const runId = await startWorkflowExecution(workflow, {}, registry, {
|
|
248
|
+
requestedByUserId: user.id,
|
|
249
|
+
});
|
|
250
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
startRetryPoller(registry, 10);
|
|
254
|
+
for (let i = 0; i < 20 && getWorkflowRun(runId)?.status !== "completed"; i++) {
|
|
255
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
256
|
+
}
|
|
257
|
+
} finally {
|
|
258
|
+
stopRetryPoller();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
expect(getWorkflowRun(runId)?.status).toBe("completed");
|
|
262
|
+
expect(failExecutorMeta).toHaveLength(2);
|
|
263
|
+
expect(failExecutorMeta[1]?.requestedByUserId).toBe(user.id);
|
|
264
|
+
});
|
|
265
|
+
|
|
222
266
|
test("poller picks up failed steps past nextRetryAt", async () => {
|
|
223
267
|
// Reset fail counter so the executor succeeds on the "retry" attempt
|
|
224
268
|
failCounter = 0;
|
package/src/tools/db-query.ts
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import * as z from "zod";
|
|
3
|
-
import { DbQueryInputShape,
|
|
3
|
+
import { DbQueryInputShape, executeReadOnlyQueryGated, resolveDbQuerySql } from "@/http/db-query";
|
|
4
|
+
import { getDbQueryMcpBudgetMs, getDbQueryMcpMaxRows } from "@/http/db-query-shared";
|
|
4
5
|
import { createToolRegistrar, swarmToolOutputSchema, toolErr, toolOk } from "@/tools/utils";
|
|
5
6
|
|
|
6
|
-
const MCP_MAX_ROWS = 100;
|
|
7
|
-
|
|
8
7
|
const DbQueryToolInputSchema = z
|
|
9
8
|
.object({
|
|
10
9
|
...DbQueryInputShape,
|
|
11
|
-
sql: z
|
|
10
|
+
sql: z
|
|
11
|
+
.string()
|
|
12
|
+
.optional()
|
|
13
|
+
.describe(
|
|
14
|
+
"Read-only SQL query (writes are rejected). Runs with a wall-clock budget in a bounded child process by default, and results are capped at a default row count (operator-configurable via `DB_QUERY_MCP_MAX_ROWS`) — a query is safe to try even against a huge table. session_logs, agent_log, events, and task_context_snapshots are too large to read whole: filter on an indexed column (session_logs: taskId/sessionId; agent_log: agentId/taskId/eventType/createdAt) and add a LIMIT, don't COUNT(*)/SUM(...)/typeof() across the table, and don't split a large read into rowid chunks — each chunk still reads every row in its range. See the db-query-guidance skill for the operator config knobs (timeout, row cap, concurrency cap) if a query keeps timing out or getting rejected.",
|
|
15
|
+
),
|
|
12
16
|
query: z.string().optional().describe("Deprecated runtime alias for sql."),
|
|
13
17
|
params: z.array(z.any()).optional().default([]).describe("Query parameters"),
|
|
14
18
|
})
|
|
@@ -22,7 +26,7 @@ export const registerDbQueryTool = (server: McpServer) => {
|
|
|
22
26
|
{
|
|
23
27
|
title: "Execute database query",
|
|
24
28
|
description:
|
|
25
|
-
"Execute a read-only SQL query against the swarm database. Available to all authenticated agents — be aware results may include secrets (oauth_tokens, configs).
|
|
29
|
+
"Execute a read-only SQL query against the swarm database (SQLite). Available to all authenticated agents — be aware results may include secrets (oauth_tokens, configs). Runs in a short-lived child process with a wall-clock budget by default (fails gracefully with a timeout or a 429-style concurrency error rather than freezing); results capped at a default row count (operator-configurable via `DB_QUERY_MCP_MAX_ROWS`) regardless of how many the query matched. See the sql parameter's description for which tables are unsafe to read whole, and the db-query-guidance skill for config knobs.",
|
|
26
30
|
annotations: { readOnlyHint: true },
|
|
27
31
|
inputSchema: DbQueryToolInputSchema,
|
|
28
32
|
outputSchema: swarmToolOutputSchema({
|
|
@@ -37,15 +41,21 @@ export const registerDbQueryTool = (server: McpServer) => {
|
|
|
37
41
|
try {
|
|
38
42
|
const sql = resolveDbQuerySql(input);
|
|
39
43
|
const params = input.params ?? [];
|
|
40
|
-
const
|
|
41
|
-
const
|
|
44
|
+
const maxRows = getDbQueryMcpMaxRows();
|
|
45
|
+
const result = await executeReadOnlyQueryGated(
|
|
46
|
+
sql,
|
|
47
|
+
params,
|
|
48
|
+
getDbQueryMcpBudgetMs(),
|
|
49
|
+
maxRows,
|
|
50
|
+
);
|
|
51
|
+
const truncated = result.total > maxRows;
|
|
42
52
|
|
|
43
53
|
// Build a simple text table for Claude
|
|
44
54
|
const header = result.columns.join(" | ");
|
|
45
55
|
const separator = result.columns.map(() => "---").join(" | ");
|
|
46
56
|
const dataRows = result.rows.map((row) => row.map((v) => String(v ?? "NULL")).join(" | "));
|
|
47
57
|
const table = [header, separator, ...dataRows].join("\n");
|
|
48
|
-
const suffix = truncated ? `\n(Showing ${
|
|
58
|
+
const suffix = truncated ? `\n(Showing ${maxRows} of ${result.total} rows)` : "";
|
|
49
59
|
const details = `${table}${suffix}\n\n${result.total} rows in ${result.elapsed}ms`;
|
|
50
60
|
|
|
51
61
|
return toolOk(`${result.total} row(s) in ${result.elapsed}ms`, {
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
toolOk,
|
|
16
16
|
} from "@/tools/utils";
|
|
17
17
|
import { AgentTaskStatusSchema } from "@/types";
|
|
18
|
+
import { getUserCommsPrefs } from "@/utils/requester-comms";
|
|
18
19
|
|
|
19
20
|
export const getTaskDetailsInputSchema = z.object({
|
|
20
21
|
taskId: z.uuid().describe("The ID of the task to get details for."),
|
|
@@ -157,9 +158,23 @@ export const getTaskDetailsOutputSchema = swarmToolOutputSchema({
|
|
|
157
158
|
yourAgentId: z.string().optional(),
|
|
158
159
|
task: looseAgentTaskOutputSchema.optional(),
|
|
159
160
|
requestedBy: z
|
|
160
|
-
.looseObject({
|
|
161
|
+
.looseObject({
|
|
162
|
+
name: z.string().optional(),
|
|
163
|
+
email: z.string().optional(),
|
|
164
|
+
role: z.string().optional(),
|
|
165
|
+
notes: z.string().optional(),
|
|
166
|
+
comms: z
|
|
167
|
+
.looseObject({
|
|
168
|
+
tone: z.string().optional(),
|
|
169
|
+
language: z.string().optional(),
|
|
170
|
+
verbosity: z.string().optional(),
|
|
171
|
+
})
|
|
172
|
+
.optional(),
|
|
173
|
+
})
|
|
161
174
|
.optional()
|
|
162
|
-
.describe(
|
|
175
|
+
.describe(
|
|
176
|
+
"Resolved user who requested this task, with role/notes and structured communication preferences (users.metadata.comms) when set",
|
|
177
|
+
),
|
|
163
178
|
logs: z.array(looseAgentLogSchema).optional(),
|
|
164
179
|
attachments: z
|
|
165
180
|
.array(looseTaskAttachmentSchema)
|
|
@@ -191,7 +206,13 @@ export async function getTaskDetailsHandler(
|
|
|
191
206
|
// Resolve requesting user details if available
|
|
192
207
|
const requestedByUser = task.requestedByUserId ? getUserById(task.requestedByUserId) : undefined;
|
|
193
208
|
const requestedBy = requestedByUser
|
|
194
|
-
? {
|
|
209
|
+
? {
|
|
210
|
+
name: requestedByUser.name,
|
|
211
|
+
email: requestedByUser.email,
|
|
212
|
+
role: requestedByUser.role,
|
|
213
|
+
notes: requestedByUser.notes,
|
|
214
|
+
comms: getUserCommsPrefs(requestedByUser),
|
|
215
|
+
}
|
|
195
216
|
: undefined;
|
|
196
217
|
|
|
197
218
|
const data = {
|
package/src/tools/manage-user.ts
CHANGED
|
@@ -82,6 +82,17 @@ const InputSchema = z.object({
|
|
|
82
82
|
.nullable()
|
|
83
83
|
.optional()
|
|
84
84
|
.describe("Free-form JSON metadata (null clears the field)"),
|
|
85
|
+
comms: z
|
|
86
|
+
.object({
|
|
87
|
+
tone: z.string().optional(),
|
|
88
|
+
language: z.string().optional(),
|
|
89
|
+
verbosity: z.string().optional(),
|
|
90
|
+
})
|
|
91
|
+
.nullable()
|
|
92
|
+
.optional()
|
|
93
|
+
.describe(
|
|
94
|
+
"Merge-safe write to metadata.comms without touching sibling metadata keys; null removes only the comms key. When metadata is also provided, it is applied first and replaces the whole blob.",
|
|
95
|
+
),
|
|
85
96
|
});
|
|
86
97
|
|
|
87
98
|
function diffAliases(prev: string[], next: string[]): { added: string[]; removed: string[] } {
|
|
@@ -99,7 +110,7 @@ export const registerManageUserTool = (server: McpServer) => {
|
|
|
99
110
|
{
|
|
100
111
|
title: "Manage user profiles",
|
|
101
112
|
description:
|
|
102
|
-
"Create, update, delete, or list user profiles in the user registry. Identities are managed via an `identities: [{kind, externalId}]` array (declarative — update computes diff). Lead-only.",
|
|
113
|
+
"Create, update, delete, or list user profiles in the user registry. Identities are managed via an `identities: [{kind, externalId}]` array (declarative — update computes diff). comms is the merge-safe way to set communication preferences; metadata replaces the whole blob. Lead-only.",
|
|
103
114
|
annotations: { readOnlyHint: false },
|
|
104
115
|
inputSchema: InputSchema,
|
|
105
116
|
outputSchema: swarmToolOutputSchema({
|
|
@@ -157,6 +168,10 @@ export const registerManageUserTool = (server: McpServer) => {
|
|
|
157
168
|
return toolErr("name is required for create action.");
|
|
158
169
|
}
|
|
159
170
|
try {
|
|
171
|
+
const metadata =
|
|
172
|
+
input.comms != null
|
|
173
|
+
? { ...(input.metadata ?? {}), comms: input.comms }
|
|
174
|
+
: (input.metadata ?? undefined);
|
|
160
175
|
const user = createUser({
|
|
161
176
|
name: input.name,
|
|
162
177
|
email: input.email,
|
|
@@ -167,7 +182,7 @@ export const registerManageUserTool = (server: McpServer) => {
|
|
|
167
182
|
timezone: input.timezone,
|
|
168
183
|
dailyBudgetUsd: input.dailyBudgetUsd ?? undefined,
|
|
169
184
|
status: input.status,
|
|
170
|
-
metadata
|
|
185
|
+
metadata,
|
|
171
186
|
});
|
|
172
187
|
for (const ident of input.identities ?? []) {
|
|
173
188
|
linkIdentity(user.id, ident.kind, ident.externalId, operatorActor);
|
|
@@ -192,6 +207,24 @@ export const registerManageUserTool = (server: McpServer) => {
|
|
|
192
207
|
return toolErr(`User ${input.userId} not found.`);
|
|
193
208
|
}
|
|
194
209
|
|
|
210
|
+
// Merge-safe write: metadata (if provided) is applied first and
|
|
211
|
+
// replaces the whole blob, then comms is merged into it so
|
|
212
|
+
// sibling metadata keys survive.
|
|
213
|
+
let metadata = input.metadata;
|
|
214
|
+
if (input.comms !== undefined) {
|
|
215
|
+
const base: Record<string, unknown> = {
|
|
216
|
+
...(input.metadata !== undefined
|
|
217
|
+
? (input.metadata ?? {})
|
|
218
|
+
: (before.metadata ?? {})),
|
|
219
|
+
};
|
|
220
|
+
if (input.comms === null) {
|
|
221
|
+
delete base.comms;
|
|
222
|
+
} else {
|
|
223
|
+
base.comms = input.comms;
|
|
224
|
+
}
|
|
225
|
+
metadata = Object.keys(base).length > 0 ? base : null;
|
|
226
|
+
}
|
|
227
|
+
|
|
195
228
|
const user = updateUser(input.userId, {
|
|
196
229
|
name: input.name,
|
|
197
230
|
email: input.email,
|
|
@@ -202,7 +235,7 @@ export const registerManageUserTool = (server: McpServer) => {
|
|
|
202
235
|
timezone: input.timezone,
|
|
203
236
|
dailyBudgetUsd: input.dailyBudgetUsd,
|
|
204
237
|
status: input.status,
|
|
205
|
-
metadata
|
|
238
|
+
metadata,
|
|
206
239
|
});
|
|
207
240
|
if (!user) {
|
|
208
241
|
return toolErr(`User ${input.userId} not found.`);
|