@desplega.ai/agent-swarm 1.102.0 → 1.104.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.
Files changed (174) hide show
  1. package/README.md +2 -0
  2. package/dist/{actions-j9399ktm.js → actions-2expzt47.js} +6 -6
  3. package/dist/{app-tnq1pz27.js → app-dr8hwx7z.js} +3 -3
  4. package/dist/{assistant-c7y92gkw.js → assistant-9rhgsw7v.js} +9 -10
  5. package/dist/{boot-reembed-b97r699t.js → boot-reembed-3q1aywnz.js} +5 -4
  6. package/dist/{boot-reembed-k7e6fc1p.js → boot-reembed-9kt97g1z.js} +4 -3
  7. package/dist/{boot-scrub-logs-14kbc61y.js → boot-scrub-logs-vskq108e.js} +2 -2
  8. package/dist/{claude-managed-adapter-g07dbz3b.js → claude-managed-adapter-z29hjgzz.js} +3 -3
  9. package/dist/{cli-d0t4854c.js → cli-0jw6j755.js} +220 -478
  10. package/dist/{cli-vgck9mqq.js → cli-23zk7xqy.js} +171 -49
  11. package/dist/{cli-g5px3d7x.js → cli-2957pqpj.js} +24 -1
  12. package/dist/{cli-zmf7refz.js → cli-2g9jf9ek.js} +1 -1
  13. package/dist/{cli-mhj07xbq.js → cli-2hap19dv.js} +3 -3
  14. package/dist/{cli-wjyyv518.js → cli-3j01d33e.js} +9 -14
  15. package/dist/{cli-zk4gn40w.js → cli-54h5daha.js} +41 -31
  16. package/dist/{cli-2aaa9mqr.js → cli-5pf0b84v.js} +2 -2
  17. package/dist/{cli-stqqf5kh.js → cli-7v4y3hyd.js} +1 -1
  18. package/dist/{cli-7xdq3jg3.js → cli-92fqyjcv.js} +1 -1
  19. package/dist/{cli-g861fwjp.js → cli-d4z1csms.js} +30 -5
  20. package/dist/{cli-r68m8arr.js → cli-e7bwjntj.js} +1 -1
  21. package/dist/{cli-q6jzg6wj.js → cli-etvgz5x8.js} +5 -7
  22. package/dist/{cli-9qascwj7.js → cli-gj2n2ec1.js} +1 -1
  23. package/dist/{cli-axtg1zaa.js → cli-khe4x17a.js} +1 -1
  24. package/dist/{cli-zctkpdmd.js → cli-nzc0h54v.js} +121 -2
  25. package/dist/{cli-k81y1cjb.js → cli-p4cj4vhc.js} +351 -24
  26. package/dist/{cli-fjx4x7nn.js → cli-q5tshxd6.js} +1 -1
  27. package/dist/{http-eq6wxve6.js → cli-qvzgncd8.js} +85356 -77793
  28. package/dist/{cli-k1ghyj8b.js → cli-s42f54ja.js} +1 -1
  29. package/dist/{cli-dgab5x2h.js → cli-t9v14cnd.js} +23 -4
  30. package/dist/{cli-qbc6mncp.js → cli-v1syqkcb.js} +900 -352
  31. package/dist/{cli-xde8vhhp.js → cli-w1nfskkg.js} +2 -2
  32. package/dist/{cli-ap4md0gc.js → cli-y9p3e94h.js} +30 -5
  33. package/dist/{cli-hc4rd6sv.js → cli-yjt8a36k.js} +3 -3
  34. package/dist/{cli-s8jw5bmg.js → cli-yp2krx5r.js} +2 -2
  35. package/dist/{cli-5bqa41fd.js → cli-yvektv79.js} +1 -1
  36. package/dist/{cli-sygq3fhn.js → cli-zaxksbgj.js} +5 -5
  37. package/dist/{cli-t2jk33ta.js → cli-zghef0em.js} +18 -6
  38. package/dist/cli.js +11 -11
  39. package/dist/{codex-adapter-wf119mnw.js → codex-adapter-w0xharsg.js} +3 -3
  40. package/dist/{codex-session-runner-1gknw7mh.js → codex-session-runner-kzbjcm2k.js} +3 -3
  41. package/dist/{commands-2wyn3swb.js → commands-wfrkef57.js} +2 -2
  42. package/dist/{db-567ntpxv.js → db-c42pxt7h.js} +2 -2
  43. package/dist/{handlers-wvwpk2kj.js → handlers-n36zkfvx.js} +10 -12
  44. package/dist/{hook-fatkywc0.js → hook-gw3vhv6s.js} +8 -6
  45. package/dist/http-59ygpn2h.js +17263 -0
  46. package/dist/{index-d1rm4ea3.js → index-881c1baf.js} +9 -8
  47. package/dist/{index-400x6p3j.js → index-jca7jdpt.js} +23 -10
  48. package/dist/{index-6aj8966p.js → index-xf6fp4y1.js} +5 -5
  49. package/dist/{index-9wqh5352.js → index-xz8vaa5c.js} +14 -11
  50. package/dist/{keepalive-6x9nx92g.js → keepalive-mj4302vw.js} +4 -4
  51. package/dist/{lead-5fgqwe13.js → lead-qm0xjvkv.js} +23 -23
  52. package/dist/{maintenance-eajx5k1k.js → maintenance-xppxbe4t.js} +5 -4
  53. package/dist/{onboard-jc1w1edg.js → onboard-ejx6yxh8.js} +2 -2
  54. package/dist/{otel-impl-8k375amf.js → otel-impl-zs5exkpk.js} +1 -1
  55. package/dist/{pi-mono-adapter-hf8sw7hs.js → pi-mono-adapter-zf7dt1wg.js} +1 -1
  56. package/dist/{pricing-refresh-zed6916t.js → pricing-refresh-nqyf8bkb.js} +4 -4
  57. package/dist/{seed-pricing-5805qzkz.js → seed-pricing-w3x550cj.js} +3 -3
  58. package/dist/{setup-wz05vxh0.js → setup-7729ktfj.js} +2 -2
  59. package/dist/{worker-j359graf.js → worker-4ttemwhw.js} +23 -23
  60. package/openapi.json +159 -4
  61. package/package.json +3 -3
  62. package/src/be/db.ts +83 -13
  63. package/src/be/memory/constants.ts +6 -0
  64. package/src/be/memory/providers/sqlite-store.ts +468 -71
  65. package/src/be/memory/raters/retrieval.ts +4 -2
  66. package/src/be/memory/reranker.ts +5 -2
  67. package/src/be/memory/retrieval-store.ts +2 -0
  68. package/src/be/memory/types.ts +46 -0
  69. package/src/be/migrations/098_repo_hooks_config.sql +3 -0
  70. package/src/be/migrations/099_memory_structured_key_versioning.sql +41 -0
  71. package/src/be/migrations/100_memory_retrieval_source.sql +8 -0
  72. package/src/be/migrations/101_script_connections.sql +67 -0
  73. package/src/be/script-connections.ts +744 -0
  74. package/src/be/script-credential-broker.ts +38 -0
  75. package/src/be/scripts/typecheck.ts +23 -2
  76. package/src/be/seed-scripts/catalog/boot-triage.inline.ts +16 -4
  77. package/src/be/seed-scripts/catalog/boot-triage.ts +16 -4
  78. package/src/be/task-lifecycle-events.ts +42 -0
  79. package/src/commands/runner.ts +182 -31
  80. package/src/github/task-reactions.ts +21 -0
  81. package/src/heartbeat/heartbeat.ts +42 -1
  82. package/src/heartbeat/index.ts +7 -1
  83. package/src/hooks/tool-loop-detection.test.ts +40 -0
  84. package/src/hooks/tool-loop-detection.ts +37 -1
  85. package/src/http/agents.ts +7 -0
  86. package/src/http/index.ts +16 -2
  87. package/src/http/memory.ts +127 -14
  88. package/src/http/prompt-templates.ts +1 -1
  89. package/src/http/repos.ts +5 -1
  90. package/src/http/schedules.ts +1 -1
  91. package/src/http/scripts.ts +15 -14
  92. package/src/http/session-data.ts +2 -0
  93. package/src/http/tasks.ts +2 -1
  94. package/src/http/workflows.ts +1 -0
  95. package/src/prompts/resolver.ts +1 -1
  96. package/src/scheduler/scheduler.ts +25 -2
  97. package/src/scripts-runtime/api-client.ts +100 -0
  98. package/src/scripts-runtime/api-types.ts +45 -0
  99. package/src/scripts-runtime/credential-broker/README.md +40 -0
  100. package/src/scripts-runtime/credential-broker/broker.ts +51 -0
  101. package/src/scripts-runtime/credential-broker/default-bindings.ts +19 -0
  102. package/src/scripts-runtime/credential-broker/fetch-patch.ts +102 -0
  103. package/src/scripts-runtime/credential-broker/index.ts +17 -0
  104. package/src/scripts-runtime/credential-broker/store.ts +72 -0
  105. package/src/scripts-runtime/credential-broker/types.ts +54 -0
  106. package/src/scripts-runtime/ctx.ts +11 -1
  107. package/src/scripts-runtime/egress-secrets.ts +14 -76
  108. package/src/scripts-runtime/eval-harness.ts +1 -1
  109. package/src/scripts-runtime/executors/types.ts +5 -5
  110. package/src/scripts-runtime/loader.ts +6 -1
  111. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  112. package/src/scripts-runtime/types/stdlib.d.ts +11 -0
  113. package/src/scripts-runtime/types/swarm-sdk.d.ts +11 -0
  114. package/src/server-runtime-counters.ts +12 -0
  115. package/src/server-user.ts +1 -2
  116. package/src/server.ts +11 -0
  117. package/src/telemetry.ts +18 -1
  118. package/src/tests/credential-broker.test.ts +281 -0
  119. package/src/tests/heartbeat.test.ts +239 -0
  120. package/src/tests/http-api-integration.test.ts +4 -0
  121. package/src/tests/memory-edit.test.ts +177 -0
  122. package/src/tests/memory-hybrid.test.ts +212 -0
  123. package/src/tests/model-control.test.ts +6 -6
  124. package/src/tests/runner-repo-autostash.test.ts +39 -8
  125. package/src/tests/script-connections.test.ts +360 -0
  126. package/src/tests/scripts-http.test.ts +16 -10
  127. package/src/tests/scripts-runtime-secret-egress.test.ts +26 -5
  128. package/src/tests/swarm-config-reserved-keys.test.ts +23 -0
  129. package/src/tests/swarm-repos.test.ts +25 -2
  130. package/src/tests/task-lifecycle-events.test.ts +53 -0
  131. package/src/tests/task-lifecycle-telemetry.test.ts +37 -1
  132. package/src/tests/telemetry-init.test.ts +32 -0
  133. package/src/tests/update-schedule-mcp-tool.test.ts +83 -4
  134. package/src/tools/credential-bindings/index.ts +1 -0
  135. package/src/tools/credential-bindings/tool.ts +238 -0
  136. package/src/tools/memory-edit.ts +148 -0
  137. package/src/tools/memory-search.ts +17 -10
  138. package/src/tools/prompt-templates/preview.ts +1 -1
  139. package/src/tools/repos/update-repo.ts +6 -1
  140. package/src/tools/schedules/create-schedule.ts +1 -1
  141. package/src/tools/schedules/delete-schedule.ts +18 -8
  142. package/src/tools/schedules/update-schedule.ts +5 -10
  143. package/src/tools/script-connections/index.ts +1 -0
  144. package/src/tools/script-connections/tool.ts +167 -0
  145. package/src/tools/send-task.ts +7 -2
  146. package/src/tools/swarm-config/delete-config.ts +5 -0
  147. package/src/tools/swarm-config/set-config.ts +23 -1
  148. package/src/tools/task-action.ts +6 -2
  149. package/src/tools/tool-config.ts +6 -1
  150. package/src/types.ts +161 -1
  151. package/src/utils/template.ts +127 -0
  152. package/src/workflows/engine.ts +7 -0
  153. package/src/workflows/executors/agent-task.ts +1 -2
  154. package/src/workflows/executors/swarm-script.ts +2 -0
  155. package/src/workflows/template.ts +12 -121
  156. package/src/workflows/triggers.ts +6 -2
  157. package/templates/community/code-health-reports/PLAYBOOK.md +355 -0
  158. package/templates/community/code-health-reports/README.md +38 -0
  159. package/templates/community/code-health-reports/lead-prompt.md +52 -0
  160. package/templates/community/code-health-reports/report.mjs +378 -0
  161. package/templates/community/code-health-reports/run.sh +143 -0
  162. package/templates/community/dora-metrics/PLAYBOOK.md +294 -0
  163. package/templates/community/dora-metrics/README.md +39 -0
  164. package/templates/community/dora-metrics/lead-prompt.md +56 -0
  165. package/templates/community/dora-metrics/report.mjs +393 -0
  166. package/templates/community/dora-metrics/run.sh +113 -0
  167. package/templates/schedules/weekly-code-health-reports/config.json +13 -0
  168. package/templates/schedules/weekly-code-health-reports/content.md +60 -0
  169. package/templates/schedules/weekly-dora-metrics/config.json +13 -0
  170. package/templates/schedules/weekly-dora-metrics/content.md +61 -0
  171. package/dist/cli-2deg0cen.js +0 -120
  172. package/dist/cli-tsz1q7gk.js +0 -23871
  173. package/src/model-tiers.ts +0 -140
  174. package/templates/community/.gitkeep +0 -0
@@ -0,0 +1,53 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import { emitTaskStarted, onTaskStarted } from "../be/task-lifecycle-events";
3
+ import type { AgentTask } from "../types";
4
+
5
+ function makeTask(overrides: Partial<AgentTask> = {}): AgentTask {
6
+ return {
7
+ id: "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
8
+ agentId: "11111111-2222-3333-4444-555555555555",
9
+ task: "Test task",
10
+ status: "in_progress",
11
+ source: "github",
12
+ priority: 50,
13
+ tags: [],
14
+ dependsOn: [],
15
+ wasPaused: false,
16
+ createdAt: new Date().toISOString(),
17
+ lastUpdatedAt: new Date().toISOString(),
18
+ ...overrides,
19
+ } as AgentTask;
20
+ }
21
+
22
+ describe("task-lifecycle-events", () => {
23
+ test("emitTaskStarted invokes a registered onTaskStarted handler with the task", () => {
24
+ const received: AgentTask[] = [];
25
+ onTaskStarted((task) => {
26
+ received.push(task);
27
+ });
28
+
29
+ const task = makeTask();
30
+ emitTaskStarted(task);
31
+
32
+ expect(received).toContain(task);
33
+ });
34
+
35
+ test("a throwing handler does not break emit and later handlers still run", () => {
36
+ let laterRan = false;
37
+ onTaskStarted(() => {
38
+ throw new Error("boom");
39
+ });
40
+ onTaskStarted(() => {
41
+ laterRan = true;
42
+ });
43
+
44
+ expect(() => emitTaskStarted(makeTask())).not.toThrow();
45
+ expect(laterRan).toBe(true);
46
+ });
47
+
48
+ test("a promise-rejecting handler is fire-and-forget and does not throw", () => {
49
+ onTaskStarted(() => Promise.reject(new Error("async boom")));
50
+
51
+ expect(() => emitTaskStarted(makeTask())).not.toThrow();
52
+ });
53
+ });
@@ -9,6 +9,7 @@ import {
9
9
  failTask,
10
10
  getDb,
11
11
  initDb,
12
+ updateTaskClaudeSessionId,
12
13
  } from "../be/db";
13
14
  import { telemetry } from "../telemetry";
14
15
 
@@ -69,7 +70,6 @@ describe("task lifecycle telemetry", () => {
69
70
  props: {
70
71
  taskId: task.id,
71
72
  source: "mcp",
72
- tags: ["telemetry"],
73
73
  hasParent: false,
74
74
  priority: 60,
75
75
  },
@@ -150,4 +150,40 @@ describe("task lifecycle telemetry", () => {
150
150
  });
151
151
  expect(typeof calls[0]?.props.durationMs).toBe("number");
152
152
  });
153
+
154
+ test("emits structured task provider and harness context instead of tags", async () => {
155
+ const task = createTaskExtended("complete telemetry with harness context", {
156
+ agentId: WORKER_ID,
157
+ source: "mcp",
158
+ tags: ["telemetry"],
159
+ });
160
+ updateTaskClaudeSessionId(
161
+ task.id,
162
+ "provider-session-1",
163
+ "codex",
164
+ undefined,
165
+ "gpt-5.5",
166
+ "stock",
167
+ { version: "0.40.1" },
168
+ );
169
+ await flushMicrotasks();
170
+ calls = [];
171
+
172
+ completeTask(task.id, "done");
173
+ await flushMicrotasks();
174
+
175
+ expect(calls).toHaveLength(1);
176
+ expect(calls[0]).toMatchObject({
177
+ event: "completed",
178
+ props: {
179
+ taskId: task.id,
180
+ source: "mcp",
181
+ agentId: WORKER_ID,
182
+ provider: "codex",
183
+ harnessVariant: "stock",
184
+ harnessVersion: "0.40.1",
185
+ },
186
+ });
187
+ expect(calls[0]?.props.tags).toBeUndefined();
188
+ });
153
189
  });
@@ -1,4 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, expect, test } from "bun:test";
2
+ import pkg from "../../package.json";
2
3
  import {
3
4
  _getInstallationIdForTests,
4
5
  _isE2bSandbox,
@@ -478,6 +479,37 @@ describe("initTelemetry", () => {
478
479
  });
479
480
  });
480
481
 
482
+ describe("track() ships swarmVersion in properties", () => {
483
+ const originalFetch = globalThis.fetch;
484
+ let captured: Record<string, unknown> | null = null;
485
+
486
+ beforeEach(() => {
487
+ captured = null;
488
+ globalThis.fetch = (async (_url: string, init?: { body?: string }) => {
489
+ captured = init?.body ? JSON.parse(init.body) : null;
490
+ return new Response(null, { status: 204 });
491
+ }) as typeof fetch;
492
+ });
493
+
494
+ afterEach(() => {
495
+ globalThis.fetch = originalFetch;
496
+ });
497
+
498
+ test("includes the package version on every event and ignores caller overrides", async () => {
499
+ await initTelemetry(
500
+ "worker",
501
+ async () => "install_version_test",
502
+ async () => {},
503
+ );
504
+
505
+ track({ event: "test.event", properties: { swarmVersion: "spoofed" } });
506
+ await new Promise((r) => setTimeout(r, 0));
507
+
508
+ const properties = (captured as { properties: Record<string, unknown> }).properties;
509
+ expect(properties.swarmVersion).toBe(pkg.version);
510
+ });
511
+ });
512
+
481
513
  describe("track() metadata.environment", () => {
482
514
  const originalFetch = globalThis.fetch;
483
515
  const originalNodeEnv = process.env.NODE_ENV;
@@ -11,7 +11,15 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
11
11
  import { unlink } from "node:fs/promises";
12
12
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
13
13
  import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
14
- import { closeDb, createAgent, createScheduledTask, getScheduledTaskById, initDb } from "../be/db";
14
+ import {
15
+ closeDb,
16
+ createAgent,
17
+ createScheduledTask,
18
+ getDb,
19
+ getScheduledTaskById,
20
+ initDb,
21
+ } from "../be/db";
22
+ import { registerDeleteScheduleTool } from "../tools/schedules/delete-schedule";
15
23
  import { registerUpdateScheduleTool } from "../tools/schedules/update-schedule";
16
24
 
17
25
  const TEST_DB_PATH = "./test-update-schedule-mcp-tool.sqlite";
@@ -23,24 +31,34 @@ type RegisteredTool = {
23
31
  function buildServer(): McpServer {
24
32
  const server = new McpServer({ name: "update-schedule-mcp-test", version: "1.0.0" });
25
33
  registerUpdateScheduleTool(server);
34
+ registerDeleteScheduleTool(server);
26
35
  return server;
27
36
  }
28
37
 
29
- function callUpdateSchedule(
38
+ function callTool(
30
39
  server: McpServer,
40
+ toolName: string,
31
41
  args: Record<string, unknown>,
32
42
  callerAgentId: string,
33
43
  ): Promise<CallToolResult> {
34
44
  const tools = (server as unknown as { _registeredTools: Record<string, RegisteredTool> })
35
45
  ._registeredTools;
36
- const tool = tools["update-schedule"];
37
- if (!tool) throw new Error("update-schedule not registered");
46
+ const tool = tools[toolName];
47
+ if (!tool) throw new Error(`${toolName} not registered`);
38
48
  return tool.handler(args, {
39
49
  sessionId: "test-session",
40
50
  requestInfo: { headers: { "x-agent-id": callerAgentId } },
41
51
  });
42
52
  }
43
53
 
54
+ function callUpdateSchedule(
55
+ server: McpServer,
56
+ args: Record<string, unknown>,
57
+ callerAgentId: string,
58
+ ): Promise<CallToolResult> {
59
+ return callTool(server, "update-schedule", args, callerAgentId);
60
+ }
61
+
44
62
  type ScheduleOutput = {
45
63
  success: boolean;
46
64
  message: string;
@@ -57,6 +75,7 @@ function structured(result: CallToolResult): ScheduleOutput {
57
75
  }
58
76
 
59
77
  let creatorId: string;
78
+ let otherAgentId: string;
60
79
 
61
80
  beforeAll(async () => {
62
81
  for (const suffix of ["", "-wal", "-shm"]) {
@@ -70,7 +89,13 @@ beforeAll(async () => {
70
89
  isLead: false,
71
90
  status: "idle",
72
91
  });
92
+ const otherAgent = createAgent({
93
+ name: "update-schedule-mcp-other",
94
+ isLead: false,
95
+ status: "idle",
96
+ });
73
97
  creatorId = creator.id;
98
+ otherAgentId = otherAgent.id;
74
99
  });
75
100
 
76
101
  afterAll(async () => {
@@ -83,6 +108,60 @@ afterAll(async () => {
83
108
  });
84
109
 
85
110
  describe("update-schedule MCP tool", () => {
111
+ test("non-lead, non-creator agent can update a schedule", async () => {
112
+ const server = buildServer();
113
+ const schedule = createScheduledTask({
114
+ name: `mcp-noncreator-update-${Date.now()}`,
115
+ intervalMs: 60000,
116
+ taskTemplate: "owned by creator",
117
+ createdByAgentId: creatorId,
118
+ timezone: "UTC",
119
+ });
120
+
121
+ const result = await callUpdateSchedule(
122
+ server,
123
+ { scheduleId: schedule.id, intervalMs: 120000 },
124
+ otherAgentId,
125
+ );
126
+ const sc = structured(result);
127
+
128
+ expect(sc.success).toBe(true);
129
+ expect(sc.schedule?.intervalMs).toBe(120000);
130
+ expect(getScheduledTaskById(schedule.id)?.intervalMs).toBe(120000);
131
+ });
132
+
133
+ test("non-lead, non-creator agent can delete a schedule and writes an audit event", async () => {
134
+ const server = buildServer();
135
+ const schedule = createScheduledTask({
136
+ name: `mcp-noncreator-delete-${Date.now()}`,
137
+ intervalMs: 60000,
138
+ taskTemplate: "owned by creator",
139
+ createdByAgentId: creatorId,
140
+ timezone: "UTC",
141
+ });
142
+
143
+ const result = await callTool(
144
+ server,
145
+ "delete-schedule",
146
+ { scheduleId: schedule.id },
147
+ otherAgentId,
148
+ );
149
+ const sc = result.structuredContent as { success: boolean };
150
+
151
+ expect(sc.success).toBe(true);
152
+ expect(getScheduledTaskById(schedule.id)).toBeNull();
153
+ const event = getDb()
154
+ .prepare<{ data: string }, []>(
155
+ "SELECT data FROM events WHERE event = 'schedule.deleted' ORDER BY createdAt DESC LIMIT 1",
156
+ )
157
+ .get();
158
+ expect(JSON.parse(event!.data)).toMatchObject({
159
+ scheduleId: schedule.id,
160
+ deletedByAgentId: otherAgentId,
161
+ createdByAgentId: creatorId,
162
+ });
163
+ });
164
+
86
165
  test("regression: { cronExpression: null, intervalMs: null, enabled: false } returns success:false and leaves DB row unchanged", async () => {
87
166
  const server = buildServer();
88
167
  const schedule = createScheduledTask({
@@ -0,0 +1 @@
1
+ export { registerCredentialBindingsTool } from "./tool";
@@ -0,0 +1,238 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { getAgentById } from "@/be/db";
4
+ import {
5
+ disableCredentialBinding,
6
+ importLegacyCredentialBindings,
7
+ listRelationalCredentialBindings,
8
+ upsertCredentialBinding,
9
+ } from "@/be/script-connections";
10
+ import {
11
+ CredentialBindingSchema,
12
+ placeholderForConfigKey,
13
+ } from "@/scripts-runtime/credential-broker";
14
+ import { createToolRegistrar } from "@/tools/utils";
15
+
16
+ const credentialBindingsOutputSchema = z.object({
17
+ yourAgentId: z.string().uuid().optional(),
18
+ success: z.boolean(),
19
+ message: z.string(),
20
+ bindings: z.array(CredentialBindingSchema),
21
+ });
22
+
23
+ const credentialBindingsInputSchema = z.object({
24
+ action: z
25
+ .enum(["list", "upsert", "disable", "import-legacy"])
26
+ .describe("List, add/update, disable, or import legacy JSON bindings."),
27
+ id: z.string().uuid().optional(),
28
+ configKey: z.string().min(1).max(255).optional(),
29
+ allowedHosts: z.array(z.string().min(1)).min(1).optional(),
30
+ headerTemplate: z.string().min(1).optional(),
31
+ queryTemplate: z.string().min(1).optional(),
32
+ scope: z.enum(["global", "agent", "repo"]).default("global").optional(),
33
+ scopeId: z.string().uuid().nullable().optional(),
34
+ });
35
+
36
+ export const registerCredentialBindingsTool = (server: McpServer) => {
37
+ createToolRegistrar(server)(
38
+ "credential-bindings",
39
+ {
40
+ title: "Credential Bindings",
41
+ description:
42
+ "Lead-only management for scripts-runtime credential broker bindings. Bindings map config keys to allowed egress hosts; scripts consume them only through fetch-layer placeholder substitution.",
43
+ annotations: { idempotentHint: true },
44
+ inputSchema: credentialBindingsInputSchema,
45
+ outputSchema: credentialBindingsOutputSchema,
46
+ },
47
+ async (args, requestInfo) => {
48
+ if (!requestInfo.agentId) {
49
+ return {
50
+ content: [{ type: "text", text: 'Agent ID not found. Set the "X-Agent-ID" header.' }],
51
+ structuredContent: {
52
+ success: false,
53
+ message: 'Agent ID not found. Set the "X-Agent-ID" header.',
54
+ bindings: [],
55
+ },
56
+ };
57
+ }
58
+
59
+ const agent = getAgentById(requestInfo.agentId);
60
+ if (!agent?.isLead) {
61
+ return {
62
+ content: [{ type: "text", text: "Only the lead can manage credential bindings." }],
63
+ structuredContent: {
64
+ yourAgentId: requestInfo.agentId,
65
+ success: false,
66
+ message: "Only the lead can manage credential bindings.",
67
+ bindings: [],
68
+ },
69
+ };
70
+ }
71
+
72
+ const bindings = listRelationalCredentialBindings({ includeInactive: true });
73
+
74
+ if (args.action === "list") {
75
+ return {
76
+ content: [
77
+ {
78
+ type: "text",
79
+ text:
80
+ bindings.length === 0
81
+ ? "No configured credential bindings."
82
+ : `Found ${bindings.length} credential binding(s).`,
83
+ },
84
+ ],
85
+ structuredContent: {
86
+ yourAgentId: requestInfo.agentId,
87
+ success: true,
88
+ message:
89
+ bindings.length === 0
90
+ ? "No configured credential bindings."
91
+ : `Found ${bindings.length} credential binding(s).`,
92
+ bindings,
93
+ },
94
+ };
95
+ }
96
+
97
+ if (args.action === "import-legacy") {
98
+ const imported = importLegacyCredentialBindings();
99
+ const nextBindings = listRelationalCredentialBindings({ includeInactive: true });
100
+ return {
101
+ content: [{ type: "text", text: `Imported ${imported} legacy credential binding(s).` }],
102
+ structuredContent: {
103
+ yourAgentId: requestInfo.agentId,
104
+ success: true,
105
+ message: `Imported ${imported} legacy credential binding(s).`,
106
+ bindings: nextBindings,
107
+ },
108
+ };
109
+ }
110
+
111
+ if (args.action === "disable") {
112
+ const disabled = args.id ? disableCredentialBinding(args.id) : null;
113
+ if (!disabled) {
114
+ return {
115
+ content: [{ type: "text", text: "Credential binding id not found." }],
116
+ structuredContent: {
117
+ yourAgentId: requestInfo.agentId,
118
+ success: false,
119
+ message: "Credential binding id not found.",
120
+ bindings,
121
+ },
122
+ };
123
+ }
124
+
125
+ const nextBindings = listRelationalCredentialBindings({ includeInactive: true });
126
+ return {
127
+ content: [{ type: "text", text: `Credential binding ${disabled.configKey} disabled.` }],
128
+ structuredContent: {
129
+ yourAgentId: requestInfo.agentId,
130
+ success: true,
131
+ message: `Credential binding ${disabled.configKey} disabled.`,
132
+ bindings: nextBindings,
133
+ },
134
+ };
135
+ }
136
+
137
+ if (!args.configKey) {
138
+ return {
139
+ content: [{ type: "text", text: "configKey is required for upsert." }],
140
+ structuredContent: {
141
+ yourAgentId: requestInfo.agentId,
142
+ success: false,
143
+ message: "configKey is required for upsert.",
144
+ bindings,
145
+ },
146
+ };
147
+ }
148
+
149
+ const scope = args.scope ?? "global";
150
+ const scopeId = scope === "global" ? null : (args.scopeId ?? null);
151
+ if (scope !== "global" && !scopeId) {
152
+ return {
153
+ content: [{ type: "text", text: `scopeId is required for ${scope} bindings.` }],
154
+ structuredContent: {
155
+ yourAgentId: requestInfo.agentId,
156
+ success: false,
157
+ message: `scopeId is required for ${scope} bindings.`,
158
+ bindings,
159
+ },
160
+ };
161
+ }
162
+
163
+ if (!args.allowedHosts || (!args.headerTemplate && !args.queryTemplate)) {
164
+ return {
165
+ content: [
166
+ {
167
+ type: "text",
168
+ text: "allowedHosts and at least one of headerTemplate or queryTemplate are required for upsert.",
169
+ },
170
+ ],
171
+ structuredContent: {
172
+ yourAgentId: requestInfo.agentId,
173
+ success: false,
174
+ message:
175
+ "allowedHosts and at least one of headerTemplate or queryTemplate are required for upsert.",
176
+ bindings,
177
+ },
178
+ };
179
+ }
180
+
181
+ const placeholder = placeholderForConfigKey(args.configKey);
182
+ if (args.headerTemplate && !args.headerTemplate.includes(placeholder)) {
183
+ return {
184
+ content: [{ type: "text", text: `headerTemplate must include ${placeholder}.` }],
185
+ structuredContent: {
186
+ yourAgentId: requestInfo.agentId,
187
+ success: false,
188
+ message: `headerTemplate must include ${placeholder}.`,
189
+ bindings,
190
+ },
191
+ };
192
+ }
193
+ if (args.queryTemplate && !args.queryTemplate.includes(placeholder)) {
194
+ return {
195
+ content: [{ type: "text", text: `queryTemplate must include ${placeholder}.` }],
196
+ structuredContent: {
197
+ yourAgentId: requestInfo.agentId,
198
+ success: false,
199
+ message: `queryTemplate must include ${placeholder}.`,
200
+ bindings,
201
+ },
202
+ };
203
+ }
204
+
205
+ const nextBinding = CredentialBindingSchema.parse({
206
+ configKey: args.configKey,
207
+ allowedHosts: args.allowedHosts,
208
+ headerTemplate: args.headerTemplate,
209
+ queryTemplate: args.queryTemplate,
210
+ scope,
211
+ scopeId,
212
+ active: true,
213
+ });
214
+
215
+ upsertCredentialBinding({
216
+ id: args.id,
217
+ configKey: nextBinding.configKey,
218
+ allowedHosts: nextBinding.allowedHosts,
219
+ headerTemplate: nextBinding.headerTemplate,
220
+ queryTemplate: nextBinding.queryTemplate,
221
+ scope: nextBinding.scope,
222
+ scopeId: nextBinding.scopeId ?? null,
223
+ active: true,
224
+ });
225
+ const nextBindings = listRelationalCredentialBindings({ includeInactive: true });
226
+
227
+ return {
228
+ content: [{ type: "text", text: `Credential binding ${args.configKey} saved.` }],
229
+ structuredContent: {
230
+ yourAgentId: requestInfo.agentId,
231
+ success: true,
232
+ message: `Credential binding ${args.configKey} saved.`,
233
+ bindings: nextBindings,
234
+ },
235
+ };
236
+ },
237
+ );
238
+ };
@@ -0,0 +1,148 @@
1
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import * as z from "zod";
3
+ import { getEmbeddingProvider, getMemoryStore } from "@/be/memory";
4
+ import { storeLinks } from "@/be/memory/link-resolver";
5
+ import { createToolRegistrar } from "@/tools/utils";
6
+ import { AgentMemorySchema, AgentMemoryScopeSchema } from "@/types";
7
+
8
+ export const registerMemoryEditTool = (server: McpServer) => {
9
+ createToolRegistrar(server)(
10
+ "memory-edit",
11
+ {
12
+ title: "Edit a memory",
13
+ description:
14
+ "Edit a single memory in place while preserving its ID, usefulness posterior, and audit history. Two modes: 'replace' overwrites the entire content (requires `content`); 'exact' performs a surgical find-and-replace of `oldString` with `newString` within the existing content (fails if `oldString` is missing or ambiguous). Use 'replace' for full rewrites, 'exact' for targeted edits.",
15
+ annotations: { destructiveHint: true },
16
+
17
+ inputSchema: z.object({
18
+ memoryId: z.uuid().optional().describe("The memory ID to edit."),
19
+ key: z.string().min(1).optional().describe("Structured key alternative to memoryId."),
20
+ scope: AgentMemoryScopeSchema.optional().describe("Required when editing by key."),
21
+ mode: z
22
+ .enum(["replace", "exact"])
23
+ .default("replace")
24
+ .describe(
25
+ "'replace' overwrites the entire memory content; 'exact' finds a unique substring (oldString) and replaces it with newString.",
26
+ ),
27
+ content: z
28
+ .string()
29
+ .min(1)
30
+ .optional()
31
+ .describe("Full replacement content. Required for 'replace' mode, ignored in 'exact'."),
32
+ oldString: z
33
+ .string()
34
+ .min(1)
35
+ .optional()
36
+ .describe(
37
+ "Substring to find in existing content. Required for 'exact' mode. Must appear exactly once.",
38
+ ),
39
+ newString: z
40
+ .string()
41
+ .optional()
42
+ .describe(
43
+ "Replacement for oldString. Required for 'exact' mode. Can be empty to delete.",
44
+ ),
45
+ intent: z.string().min(1).describe("Why you are editing this memory."),
46
+ expectedVersion: z.number().int().min(1).optional(),
47
+ }),
48
+ outputSchema: z.object({
49
+ yourAgentId: z.string().uuid().optional(),
50
+ success: z.boolean(),
51
+ message: z.string(),
52
+ memory: AgentMemorySchema.optional(),
53
+ changed: z.boolean().optional(),
54
+ previousVersion: z.number().int().optional(),
55
+ version: z.number().int().optional(),
56
+ }),
57
+ },
58
+ async (
59
+ { memoryId, key, scope, mode, content, oldString, newString, intent, expectedVersion },
60
+ requestInfo,
61
+ _meta,
62
+ ) => {
63
+ if (!requestInfo.agentId) {
64
+ return {
65
+ content: [{ type: "text", text: "Agent ID required to edit memories." }],
66
+ structuredContent: {
67
+ yourAgentId: undefined,
68
+ success: false,
69
+ message: "Agent ID required. Are you registered in the swarm?",
70
+ },
71
+ };
72
+ }
73
+
74
+ if (!memoryId && !(key && scope)) {
75
+ return {
76
+ content: [{ type: "text", text: "memoryId or key+scope required." }],
77
+ structuredContent: {
78
+ yourAgentId: requestInfo.agentId,
79
+ success: false,
80
+ message: "memoryId or key+scope required.",
81
+ },
82
+ };
83
+ }
84
+
85
+ try {
86
+ const store = getMemoryStore();
87
+ const result = store.edit({
88
+ id: memoryId,
89
+ key,
90
+ scope,
91
+ agentId: requestInfo.agentId,
92
+ mode,
93
+ content,
94
+ oldString,
95
+ newString,
96
+ intent,
97
+ expectedVersion,
98
+ changedByAgentId: requestInfo.agentId,
99
+ });
100
+
101
+ if (result.changed) {
102
+ const provider = getEmbeddingProvider();
103
+ const embedding = await provider.embed(result.memory.content);
104
+ if (embedding) store.updateEmbedding(result.memory.id, embedding, provider.name);
105
+ try {
106
+ storeLinks(result.memory.id, requestInfo.agentId, result.memory.content);
107
+ } catch (err) {
108
+ console.error(
109
+ `[memory-edit] Link resolution failed for ${result.memory.id}:`,
110
+ (err as Error).message,
111
+ );
112
+ }
113
+ }
114
+
115
+ return {
116
+ content: [
117
+ {
118
+ type: "text",
119
+ text: result.changed
120
+ ? `Memory "${result.memory.id}" edited to version ${result.version}.`
121
+ : `Memory "${result.memory.id}" unchanged.`,
122
+ },
123
+ ],
124
+ structuredContent: {
125
+ yourAgentId: requestInfo.agentId,
126
+ success: true,
127
+ message: result.changed
128
+ ? `Memory edited to version ${result.version}.`
129
+ : "Memory unchanged.",
130
+ memory: result.memory,
131
+ changed: result.changed,
132
+ previousVersion: result.previousVersion,
133
+ version: result.version,
134
+ },
135
+ };
136
+ } catch (err) {
137
+ return {
138
+ content: [{ type: "text", text: `Memory edit failed: ${(err as Error).message}` }],
139
+ structuredContent: {
140
+ yourAgentId: requestInfo.agentId,
141
+ success: false,
142
+ message: `Memory edit failed: ${(err as Error).message}`,
143
+ },
144
+ };
145
+ }
146
+ },
147
+ );
148
+ };