@desplega.ai/agent-swarm 1.80.0 → 1.80.2

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 (100) hide show
  1. package/openapi.json +399 -14
  2. package/package.json +3 -1
  3. package/src/artifact-sdk/server.ts +2 -1
  4. package/src/be/db.ts +1 -1
  5. package/src/be/migrations/064_scripts.sql +39 -0
  6. package/src/be/migrations/065_script_embeddings.sql +7 -0
  7. package/src/be/migrations/066_scripts_args_json_schema.sql +1 -0
  8. package/src/be/scripts/db.ts +417 -0
  9. package/src/be/scripts/embeddings.ts +233 -0
  10. package/src/be/scripts/extract-schema.ts +55 -0
  11. package/src/be/scripts/maintenance.ts +9 -0
  12. package/src/be/scripts/typecheck.ts +199 -0
  13. package/src/cli.tsx +22 -5
  14. package/src/commands/artifact.ts +3 -2
  15. package/src/commands/claude-managed-setup.ts +2 -1
  16. package/src/commands/codex-login.ts +5 -3
  17. package/src/commands/onboard.tsx +2 -1
  18. package/src/commands/runner.ts +153 -20
  19. package/src/commands/setup.tsx +5 -3
  20. package/src/hooks/hook.ts +4 -3
  21. package/src/http/index.ts +40 -29
  22. package/src/http/memory.ts +28 -0
  23. package/src/http/openapi.ts +1 -0
  24. package/src/http/page-proxy.ts +2 -1
  25. package/src/http/route-def.ts +1 -0
  26. package/src/http/schedules.ts +37 -0
  27. package/src/http/scripts.ts +388 -0
  28. package/src/linear/outbound.ts +9 -2
  29. package/src/otel.ts +5 -0
  30. package/src/providers/claude-adapter.ts +23 -1
  31. package/src/providers/types.ts +8 -0
  32. package/src/scripts-runtime/ctx.ts +23 -0
  33. package/src/scripts-runtime/eval-harness.ts +63 -0
  34. package/src/scripts-runtime/executors/native.ts +232 -0
  35. package/src/scripts-runtime/executors/registry.ts +16 -0
  36. package/src/scripts-runtime/executors/types.ts +63 -0
  37. package/src/scripts-runtime/extract-args-schema.ts +69 -0
  38. package/src/scripts-runtime/extract-signature.ts +81 -0
  39. package/src/scripts-runtime/import-allowlist.ts +109 -0
  40. package/src/scripts-runtime/loader.ts +96 -0
  41. package/src/scripts-runtime/redacted.ts +48 -0
  42. package/src/scripts-runtime/sdk-allowlist.ts +29 -0
  43. package/src/scripts-runtime/stdlib/fetch.ts +46 -0
  44. package/src/scripts-runtime/stdlib/glob.ts +8 -0
  45. package/src/scripts-runtime/stdlib/grep.ts +34 -0
  46. package/src/scripts-runtime/stdlib/index.ts +16 -0
  47. package/src/scripts-runtime/stdlib/table.ts +17 -0
  48. package/src/scripts-runtime/swarm-config.ts +35 -0
  49. package/src/scripts-runtime/swarm-sdk.ts +197 -0
  50. package/src/scripts-runtime/types/stdlib.d.ts +104 -0
  51. package/src/scripts-runtime/types/swarm-sdk.d.ts +86 -0
  52. package/src/server.ts +12 -0
  53. package/src/tests/api-key.test.ts +33 -0
  54. package/src/tests/codex-login.test.ts +1 -1
  55. package/src/tests/error-tracker.test.ts +44 -0
  56. package/src/tests/linear-outbound-sync.test.ts +109 -0
  57. package/src/tests/mcp-tools.test.ts +69 -0
  58. package/src/tests/rate-limit-event.test.ts +292 -0
  59. package/src/tests/redacted.test.ts +29 -0
  60. package/src/tests/runner-tool-spans.test.ts +268 -0
  61. package/src/tests/script-executor-conformance.test.ts +142 -0
  62. package/src/tests/script-executor-registry.test.ts +17 -0
  63. package/src/tests/scripts-db.test.ts +329 -0
  64. package/src/tests/scripts-embeddings.test.ts +291 -0
  65. package/src/tests/scripts-extract-signature.test.ts +47 -0
  66. package/src/tests/scripts-http.test.ts +403 -0
  67. package/src/tests/scripts-import-allowlist.test.ts +55 -0
  68. package/src/tests/scripts-mcp-e2e.test.ts +269 -0
  69. package/src/tests/scripts-runtime-secret-egress.test.ts +44 -0
  70. package/src/tests/scripts-runtime.test.ts +344 -0
  71. package/src/tests/sdk-allowlist.test.ts +59 -0
  72. package/src/tests/secret-scrubber.test.ts +35 -1
  73. package/src/tests/swarm-config.test.ts +38 -0
  74. package/src/tests/tool-annotations.test.ts +2 -2
  75. package/src/tests/tool-call-progress.test.ts +30 -0
  76. package/src/tests/workflow-e2e.test.ts +218 -0
  77. package/src/tests/workflow-executors.test.ts +32 -2
  78. package/src/tests/workflow-input-redaction.test.ts +232 -0
  79. package/src/tests/workflow-swarm-script.test.ts +273 -0
  80. package/src/tools/memory-rate.ts +2 -1
  81. package/src/tools/script-common.ts +88 -0
  82. package/src/tools/script-delete.ts +35 -0
  83. package/src/tools/script-query-types.ts +37 -0
  84. package/src/tools/script-run.ts +43 -0
  85. package/src/tools/script-search.ts +32 -0
  86. package/src/tools/script-upsert.ts +43 -0
  87. package/src/tools/tool-config.ts +7 -0
  88. package/src/types.ts +61 -1
  89. package/src/utils/api-key.ts +28 -0
  90. package/src/utils/error-tracker.ts +58 -0
  91. package/src/utils/page-session.ts +8 -6
  92. package/src/utils/secret-scrubber.ts +22 -1
  93. package/src/workflows/engine.ts +12 -4
  94. package/src/workflows/executors/index.ts +1 -0
  95. package/src/workflows/executors/registry.ts +2 -0
  96. package/src/workflows/executors/script.ts +12 -1
  97. package/src/workflows/executors/swarm-script.ts +170 -0
  98. package/src/workflows/input.ts +65 -0
  99. package/src/workflows/recovery.ts +31 -3
  100. package/src/workflows/resume.ts +43 -5
@@ -0,0 +1,170 @@
1
+ import { z } from "zod";
2
+ import { getScript, getScriptVersion } from "../../be/scripts/db";
3
+ import { runScript } from "../../scripts-runtime/loader";
4
+ import type { ExecutorMeta } from "../../types";
5
+ import { BaseExecutor, type ExecutorResult } from "./base";
6
+
7
+ export const SwarmScriptConfigSchema = z.object({
8
+ scriptName: z.string().min(1),
9
+ scope: z.enum(["global", "agent"]).optional(),
10
+ pinHash: z.string().min(1).optional(),
11
+ args: z.record(z.string(), z.unknown()).default({}),
12
+ fsMode: z.enum(["none", "workspace-rw"]).default("none"),
13
+ });
14
+
15
+ export const SwarmScriptOutputSchema = z.object({
16
+ result: z.unknown().optional(),
17
+ stdout: z.string(),
18
+ stderr: z.string(),
19
+ truncated: z.object({ stdout: z.boolean(), stderr: z.boolean() }),
20
+ durationMs: z.number(),
21
+ exitCode: z.number(),
22
+ scriptName: z.string(),
23
+ contentHash: z.string(),
24
+ version: z.number(),
25
+ });
26
+
27
+ type SwarmScriptConfig = z.infer<typeof SwarmScriptConfigSchema>;
28
+ type SwarmScriptOutput = z.infer<typeof SwarmScriptOutputSchema>;
29
+
30
+ export class SwarmScriptExecutor extends BaseExecutor<
31
+ typeof SwarmScriptConfigSchema,
32
+ typeof SwarmScriptOutputSchema
33
+ > {
34
+ readonly type = "swarm-script";
35
+ readonly mode = "instant" as const;
36
+ readonly configSchema = SwarmScriptConfigSchema;
37
+ readonly outputSchema = SwarmScriptOutputSchema;
38
+
39
+ protected async execute(
40
+ config: SwarmScriptConfig,
41
+ context: Readonly<Record<string, unknown>>,
42
+ meta: ExecutorMeta,
43
+ ): Promise<ExecutorResult<SwarmScriptOutput>> {
44
+ if (config.fsMode === "workspace-rw") {
45
+ return {
46
+ status: "failed",
47
+ error: "swarm-script: fsMode 'workspace-rw' is v2 only; use 'none' or omit",
48
+ };
49
+ }
50
+
51
+ const workflow = this.deps.db.getWorkflow(meta.workflowId);
52
+ const agentId = workflow?.createdByAgentId ?? agentIdFromContext(context);
53
+ const resolved = resolveScriptSource(config, agentId);
54
+
55
+ if (!resolved.ok) {
56
+ return { status: "failed", error: resolved.error };
57
+ }
58
+
59
+ const output = await runScript({
60
+ source: resolved.source,
61
+ args: config.args,
62
+ fsMode: "none",
63
+ agentId: agentId ?? "workflow",
64
+ });
65
+
66
+ const workflowOutput = {
67
+ result: output.result,
68
+ stdout: output.stdout,
69
+ stderr: output.stderr,
70
+ truncated: output.truncated,
71
+ durationMs: output.durationMs,
72
+ exitCode: output.exitCode,
73
+ scriptName: resolved.script.name,
74
+ contentHash: resolved.contentHash,
75
+ version: resolved.version,
76
+ };
77
+
78
+ if (output.exitCode !== 0 || output.error) {
79
+ return {
80
+ status: "failed",
81
+ error:
82
+ output.stderr ||
83
+ `swarm-script: script exited with code ${output.exitCode}${
84
+ output.error ? ` (${output.error})` : ""
85
+ }`,
86
+ output: workflowOutput,
87
+ };
88
+ }
89
+
90
+ return {
91
+ status: "success",
92
+ output: workflowOutput,
93
+ nextPort: "success",
94
+ };
95
+ }
96
+ }
97
+
98
+ function agentIdFromContext(context: Readonly<Record<string, unknown>>): string | undefined {
99
+ const trigger = context.trigger;
100
+ if (trigger && typeof trigger === "object") {
101
+ const value = (trigger as Record<string, unknown>).agentId;
102
+ if (typeof value === "string" && value.length > 0) return value;
103
+ }
104
+ return undefined;
105
+ }
106
+
107
+ function resolveScriptSource(
108
+ config: SwarmScriptConfig,
109
+ agentId: string | undefined,
110
+ ):
111
+ | {
112
+ ok: true;
113
+ script: NonNullable<ReturnType<typeof getScript>>;
114
+ source: string;
115
+ contentHash: string;
116
+ version: number;
117
+ }
118
+ | { ok: false; error: string } {
119
+ if (config.scope === "agent" && !agentId) {
120
+ return {
121
+ ok: false,
122
+ error:
123
+ "swarm-script: agent-scoped scripts require the workflow to have createdByAgentId or trigger.agentId",
124
+ };
125
+ }
126
+
127
+ const script =
128
+ config.scope === "global"
129
+ ? getScript({ name: config.scriptName, scope: "global" })
130
+ : config.scope === "agent"
131
+ ? getScript({ name: config.scriptName, scope: "agent", scopeId: agentId })
132
+ : agentId
133
+ ? (getScript({ name: config.scriptName, scope: "agent", scopeId: agentId }) ??
134
+ getScript({ name: config.scriptName, scope: "global" }))
135
+ : getScript({ name: config.scriptName, scope: "global" });
136
+
137
+ if (!script) {
138
+ const scopeHint = config.scope ? ` in ${config.scope} scope` : "";
139
+ return {
140
+ ok: false,
141
+ error: `swarm-script: script '${config.scriptName}' not found${scopeHint}`,
142
+ };
143
+ }
144
+
145
+ if (!config.pinHash) {
146
+ return {
147
+ ok: true,
148
+ script,
149
+ source: script.source,
150
+ contentHash: script.contentHash,
151
+ version: script.version,
152
+ };
153
+ }
154
+
155
+ const version = getScriptVersion({ scriptId: script.id, contentHash: config.pinHash });
156
+ if (!version) {
157
+ return {
158
+ ok: false,
159
+ error: `swarm-script: pinHash '${config.pinHash}' not found for script '${config.scriptName}'`,
160
+ };
161
+ }
162
+
163
+ return {
164
+ ok: true,
165
+ script,
166
+ source: version.source,
167
+ contentHash: version.contentHash,
168
+ version: version.version,
169
+ };
170
+ }
@@ -1,4 +1,5 @@
1
1
  import { getSwarmConfigs } from "../be/db";
2
+ import { isSensitiveKey } from "../utils/secret-scrubber";
2
3
 
3
4
  /**
4
5
  * Resolve workflow input values.
@@ -44,3 +45,67 @@ function resolveValue(value: string): string {
44
45
  // Literal
45
46
  return value;
46
47
  }
48
+
49
+ /**
50
+ * Marker placed in persisted step inputs in place of a secret value.
51
+ * Kept short and stable so future readers (logs, debug tools) can grep for it.
52
+ */
53
+ export const REDACTED_SECRET_VALUE = "***REDACTED***";
54
+
55
+ /**
56
+ * Determine which keys of a workflow's `input` map carry a secret value once
57
+ * resolved. A key is treated as a secret iff it references either:
58
+ * - `secret.NAME` — always sensitive (DB-stored swarm secrets).
59
+ * - `${ENV_VAR}` where ENV_VAR's name matches the secret-scrubber sensitive
60
+ * heuristic (`*_TOKEN`, `*_KEY`, `*_SECRET`, etc., or an explicit
61
+ * `SENSITIVE_KEY_EXACT` entry).
62
+ *
63
+ * Pure function — does NOT resolve values. Safe to call during recovery
64
+ * without DB lookups.
65
+ */
66
+ export function getSecretInputKeys(input: Record<string, string> | undefined): Set<string> {
67
+ const keys = new Set<string>();
68
+ if (!input) return keys;
69
+ for (const [key, value] of Object.entries(input)) {
70
+ if (typeof value !== "string") continue;
71
+ if (value.startsWith("secret.")) {
72
+ keys.add(key);
73
+ continue;
74
+ }
75
+ const envMatch = /^\$\{(.+)\}$/.exec(value);
76
+ if (envMatch?.[1] && isSensitiveKey(envMatch[1])) {
77
+ keys.add(key);
78
+ }
79
+ }
80
+ return keys;
81
+ }
82
+
83
+ /**
84
+ * Return a shallow clone of `ctx` suitable for persistence to
85
+ * `workflow_run_steps.input`, with `ctx.input[k]` replaced by
86
+ * `REDACTED_SECRET_VALUE` for every k in `secretKeys`.
87
+ *
88
+ * The live `ctx` is not mutated — executors continue to see real values.
89
+ * Only the persisted record is redacted, eliminating the leak surface that
90
+ * `get-workflow-run` and any other reader of `workflow_run_steps` exposes.
91
+ *
92
+ * Empty secretKeys → returns `ctx` unchanged (no allocation).
93
+ */
94
+ export function redactSecretsForStorage(
95
+ ctx: Record<string, unknown>,
96
+ secretKeys: Set<string>,
97
+ ): Record<string, unknown> {
98
+ if (secretKeys.size === 0) return ctx;
99
+ const inputBlock = ctx.input;
100
+ if (!inputBlock || typeof inputBlock !== "object") return ctx;
101
+ const redactedInput: Record<string, unknown> = { ...(inputBlock as Record<string, unknown>) };
102
+ let touched = false;
103
+ for (const key of secretKeys) {
104
+ if (key in redactedInput) {
105
+ redactedInput[key] = REDACTED_SECRET_VALUE;
106
+ touched = true;
107
+ }
108
+ }
109
+ if (!touched) return ctx;
110
+ return { ...ctx, input: redactedInput };
111
+ }
@@ -14,6 +14,7 @@ import { checkpointStep } from "./checkpoint";
14
14
  import { getSuccessors } from "./definition";
15
15
  import { findReadyNodes, walkGraph } from "./engine";
16
16
  import type { ExecutorRegistry } from "./executors/registry";
17
+ import { getSecretInputKeys } from "./input";
17
18
  import { finalizeOrWait, resumeWaitState } from "./resume";
18
19
 
19
20
  /**
@@ -78,7 +79,16 @@ async function recoverRunningRuns(registry: ExecutorRegistry): Promise<number> {
78
79
  finishedAt: new Date().toISOString(),
79
80
  });
80
81
  } else {
81
- await walkGraph(workflow.definition, runId, ctx, readyNodes, registry, workflow.id);
82
+ const secretKeys = getSecretInputKeys(workflow.input);
83
+ await walkGraph(
84
+ workflow.definition,
85
+ runId,
86
+ ctx,
87
+ readyNodes,
88
+ registry,
89
+ workflow.id,
90
+ secretKeys,
91
+ );
82
92
  }
83
93
  recovered++;
84
94
  } catch (err) {
@@ -112,7 +122,16 @@ async function recoverWaitingRuns(registry: ExecutorRegistry): Promise<number> {
112
122
  updateWorkflowRun(stuck.runId, { status: "running" });
113
123
 
114
124
  const successors = getSuccessors(workflow.definition, stuck.nodeId, "default");
115
- await walkGraph(workflow.definition, stuck.runId, ctx, successors, registry, workflow.id);
125
+ const secretKeys = getSecretInputKeys(workflow.input);
126
+ await walkGraph(
127
+ workflow.definition,
128
+ stuck.runId,
129
+ ctx,
130
+ successors,
131
+ registry,
132
+ workflow.id,
133
+ secretKeys,
134
+ );
116
135
  } else {
117
136
  // Task failed or cancelled — mark run failed
118
137
  const reason =
@@ -191,7 +210,16 @@ async function recoverApprovalWaitingRuns(registry: ExecutorRegistry): Promise<n
191
210
  const successors = getSuccessors(workflow.definition, stuck.nodeId, nextPort);
192
211
 
193
212
  if (successors.length > 0) {
194
- await walkGraph(workflow.definition, stuck.runId, ctx, successors, registry, workflow.id);
213
+ const secretKeys = getSecretInputKeys(workflow.input);
214
+ await walkGraph(
215
+ workflow.definition,
216
+ stuck.runId,
217
+ ctx,
218
+ successors,
219
+ registry,
220
+ workflow.id,
221
+ secretKeys,
222
+ );
195
223
  } else {
196
224
  finalizeOrWait(stuck.runId);
197
225
  }
@@ -20,6 +20,7 @@ import type { WorkflowEventBus } from "./event-bus";
20
20
  import { workflowEventBus } from "./event-bus";
21
21
  import type { ExecutorRegistry } from "./executors/registry";
22
22
  import { computeNextPort } from "./executors/wait";
23
+ import { getSecretInputKeys } from "./input";
23
24
  import { matchesFilter } from "./wait-filter";
24
25
 
25
26
  interface TaskEvent {
@@ -137,7 +138,16 @@ async function resumeFromTaskCompletion(
137
138
  const successors = getSuccessors(workflow.definition, step.nodeId);
138
139
 
139
140
  if (successors.length > 0) {
140
- await walkGraph(workflow.definition, run.id, ctx, successors, registry, workflow.id);
141
+ const secretKeys = getSecretInputKeys(workflow.input);
142
+ await walkGraph(
143
+ workflow.definition,
144
+ run.id,
145
+ ctx,
146
+ successors,
147
+ registry,
148
+ workflow.id,
149
+ secretKeys,
150
+ );
141
151
  } else {
142
152
  finalizeOrWait(run.id);
143
153
  }
@@ -201,7 +211,16 @@ async function handleTaskFailure(
201
211
  const successors = getSuccessors(workflow.definition, step.nodeId);
202
212
 
203
213
  if (successors.length > 0) {
204
- await walkGraph(workflow.definition, run.id, ctx, successors, registry, workflow.id);
214
+ const secretKeys = getSecretInputKeys(workflow.input);
215
+ await walkGraph(
216
+ workflow.definition,
217
+ run.id,
218
+ ctx,
219
+ successors,
220
+ registry,
221
+ workflow.id,
222
+ secretKeys,
223
+ );
205
224
  } else {
206
225
  finalizeOrWait(run.id);
207
226
  }
@@ -254,7 +273,8 @@ export async function retryFailedRun(runId: string, registry: ExecutorRegistry):
254
273
  const nodesToRun = readyNodes.some((n) => n.id === failedNode.id)
255
274
  ? readyNodes
256
275
  : [failedNode, ...readyNodes];
257
- await walkGraph(workflow.definition, runId, ctx, nodesToRun, registry, workflow.id);
276
+ const secretKeys = getSecretInputKeys(workflow.input);
277
+ await walkGraph(workflow.definition, runId, ctx, nodesToRun, registry, workflow.id, secretKeys);
258
278
  }
259
279
 
260
280
  /**
@@ -343,7 +363,16 @@ async function resumeFromApprovalResolution(
343
363
  const successors = getSuccessors(workflow.definition, step.nodeId, nextPort);
344
364
 
345
365
  if (successors.length > 0) {
346
- await walkGraph(workflow.definition, run.id, ctx, successors, registry, workflow.id);
366
+ const secretKeys = getSecretInputKeys(workflow.input);
367
+ await walkGraph(
368
+ workflow.definition,
369
+ run.id,
370
+ ctx,
371
+ successors,
372
+ registry,
373
+ workflow.id,
374
+ secretKeys,
375
+ );
347
376
  } else {
348
377
  finalizeOrWait(run.id);
349
378
  }
@@ -423,7 +452,16 @@ export async function resumeWaitState(
423
452
 
424
453
  const successors = getSuccessors(workflow.definition, step.nodeId, nextPort);
425
454
  if (successors.length > 0) {
426
- await walkGraph(workflow.definition, run.id, ctx, successors, registry, workflow.id);
455
+ const secretKeys = getSecretInputKeys(workflow.input);
456
+ await walkGraph(
457
+ workflow.definition,
458
+ run.id,
459
+ ctx,
460
+ successors,
461
+ registry,
462
+ workflow.id,
463
+ secretKeys,
464
+ );
427
465
  } else {
428
466
  finalizeOrWait(run.id);
429
467
  }