@desplega.ai/agent-swarm 1.80.1 → 1.80.3

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 CHANGED
@@ -34,6 +34,9 @@
34
34
  <a href="https://x.com/desplegalabs">
35
35
  <img src="https://img.shields.io/badge/𝕏-@desplegalabs-000?style=for-the-badge&logo=x&logoColor=white" alt="Follow on X">
36
36
  </a>
37
+ <a href="https://www.linkedin.com/company/desplega-labs/">
38
+ <img src="https://img.shields.io/badge/LinkedIn-Desplega%20Labs-0A66C2?style=for-the-badge&logo=linkedin&logoColor=white" alt="Desplega Labs on LinkedIn">
39
+ </a>
37
40
  </p>
38
41
 
39
42
  > **What if your AI agents remembered everything, learned from every mistake, and got better with every task?**
package/openapi.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Agent Swarm API",
5
- "version": "1.80.1",
5
+ "version": "1.80.3",
6
6
  "description": "Multi-agent orchestration API for Claude Code, Codex, and Gemini CLI. Enables task distribution, agent communication, and service discovery.\n\nMCP tools are documented separately in [MCP.md](./MCP.md)."
7
7
  },
8
8
  "servers": [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "1.80.1",
3
+ "version": "1.80.3",
4
4
  "description": "Multi-agent orchestration for Claude Code, Codex, Gemini CLI, and other AI coding assistants",
5
5
  "license": "MIT",
6
6
  "author": "desplega.sh <contact@desplega.sh>",
@@ -43,6 +43,7 @@ export interface AgentMailWebhookPayload {
43
43
 
44
44
  export type AgentMailEventType =
45
45
  | "message.received"
46
+ | "message.received.unauthenticated"
46
47
  | "message.sent"
47
48
  | "message.delivered"
48
49
  | "message.bounced"
@@ -0,0 +1 @@
1
+ ALTER TABLE scripts ADD COLUMN argsJsonSchema TEXT;
@@ -20,6 +20,7 @@ type ScriptWriteArgs = ScriptIdentity & {
20
20
  description: string;
21
21
  intent: string;
22
22
  signatureJson: string;
23
+ argsJsonSchema?: string | null;
23
24
  isScratch?: boolean;
24
25
  typeChecked?: boolean;
25
26
  fsMode?: ScriptFsMode;
@@ -115,6 +116,7 @@ export function insertScript(args: ScriptWriteArgs): ScriptRecord {
115
116
  string,
116
117
  string,
117
118
  string,
119
+ string | null,
118
120
  string,
119
121
  number,
120
122
  number,
@@ -126,9 +128,9 @@ export function insertScript(args: ScriptWriteArgs): ScriptRecord {
126
128
  >(
127
129
  `INSERT INTO scripts (
128
130
  id, name, scope, scopeId, source, description, intent, signatureJson,
129
- contentHash, isScratch, typeChecked, fsMode, createdByAgentId, createdAt, updatedAt
131
+ argsJsonSchema, contentHash, isScratch, typeChecked, fsMode, createdByAgentId, createdAt, updatedAt
130
132
  )
131
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
133
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
132
134
  RETURNING *`,
133
135
  )
134
136
  .get(
@@ -140,6 +142,7 @@ export function insertScript(args: ScriptWriteArgs): ScriptRecord {
140
142
  args.description,
141
143
  args.intent,
142
144
  args.signatureJson,
145
+ args.argsJsonSchema ?? null,
143
146
  contentHash,
144
147
  isScratch,
145
148
  typeChecked,
@@ -193,10 +196,13 @@ export async function upsertScriptByName(args: ScriptWriteArgs): Promise<UpsertS
193
196
  const fsMode = args.fsMode ?? existing.fsMode;
194
197
  const isScratch = args.isScratch ?? existing.isScratch;
195
198
  const typeChecked = args.typeChecked ?? existing.typeChecked;
199
+ const argsJsonSchema =
200
+ args.argsJsonSchema !== undefined ? args.argsJsonSchema : existing.argsJsonSchema;
196
201
  const trackedMetadataChanged =
197
202
  args.description !== existing.description ||
198
203
  args.intent !== existing.intent ||
199
- args.signatureJson !== existing.signatureJson;
204
+ args.signatureJson !== existing.signatureJson ||
205
+ argsJsonSchema !== existing.argsJsonSchema;
200
206
  const promotedFromScratch = existing.isScratch && !isScratch;
201
207
  if (
202
208
  fsMode !== existing.fsMode ||
@@ -205,9 +211,12 @@ export async function upsertScriptByName(args: ScriptWriteArgs): Promise<UpsertS
205
211
  trackedMetadataChanged
206
212
  ) {
207
213
  const row = getDb()
208
- .prepare<ScriptRow, [string, string, string, number, number, string, string, string]>(
214
+ .prepare<
215
+ ScriptRow,
216
+ [string, string, string, string | null, number, number, string, string, string]
217
+ >(
209
218
  `UPDATE scripts
210
- SET description = ?, intent = ?, signatureJson = ?,
219
+ SET description = ?, intent = ?, signatureJson = ?, argsJsonSchema = ?,
211
220
  isScratch = ?, typeChecked = ?, fsMode = ?, updatedAt = ?
212
221
  WHERE id = ?
213
222
  RETURNING *`,
@@ -216,6 +225,7 @@ export async function upsertScriptByName(args: ScriptWriteArgs): Promise<UpsertS
216
225
  args.description,
217
226
  args.intent,
218
227
  args.signatureJson,
228
+ argsJsonSchema ?? null,
219
229
  isScratch ? 1 : 0,
220
230
  typeChecked ? 1 : 0,
221
231
  fsMode,
@@ -247,16 +257,31 @@ export async function upsertScriptByName(args: ScriptWriteArgs): Promise<UpsertS
247
257
  const fsMode = args.fsMode ?? existing.fsMode;
248
258
  const isScratch = args.isScratch ?? existing.isScratch;
249
259
  const typeChecked = args.typeChecked ?? existing.typeChecked;
260
+ const argsJsonSchema =
261
+ args.argsJsonSchema !== undefined ? args.argsJsonSchema : existing.argsJsonSchema;
250
262
 
251
263
  const txn = getDb().transaction(() => {
252
264
  const row = getDb()
253
265
  .prepare<
254
266
  ScriptRow,
255
- [string, string, string, string, string, number, number, number, string, string, string]
267
+ [
268
+ string,
269
+ string,
270
+ string,
271
+ string,
272
+ string | null,
273
+ string,
274
+ number,
275
+ number,
276
+ number,
277
+ string,
278
+ string,
279
+ string,
280
+ ]
256
281
  >(
257
282
  `UPDATE scripts
258
- SET source = ?, description = ?, intent = ?, signatureJson = ?, contentHash = ?,
259
- version = ?, isScratch = ?, typeChecked = ?, fsMode = ?, updatedAt = ?
283
+ SET source = ?, description = ?, intent = ?, signatureJson = ?, argsJsonSchema = ?,
284
+ contentHash = ?, version = ?, isScratch = ?, typeChecked = ?, fsMode = ?, updatedAt = ?
260
285
  WHERE id = ?
261
286
  RETURNING *`,
262
287
  )
@@ -265,6 +290,7 @@ export async function upsertScriptByName(args: ScriptWriteArgs): Promise<UpsertS
265
290
  args.description,
266
291
  args.intent,
267
292
  args.signatureJson,
293
+ argsJsonSchema ?? null,
268
294
  contentHash,
269
295
  newVersion,
270
296
  isScratch ? 1 : 0,
@@ -22,6 +22,7 @@ type ScriptEmbeddingCandidateRow = ScriptEmbeddingRow & {
22
22
  description: string;
23
23
  intent: string;
24
24
  signatureJson: string;
25
+ argsJsonSchema: string | null;
25
26
  contentHash: string;
26
27
  version: number;
27
28
  isScratch: number;
@@ -63,6 +64,7 @@ function rowToScript(row: ScriptEmbeddingCandidateRow): ScriptRecord {
63
64
  description: row.description,
64
65
  intent: row.intent,
65
66
  signatureJson: row.signatureJson,
67
+ argsJsonSchema: row.argsJsonSchema ?? null,
66
68
  contentHash: row.contentHash,
67
69
  version: row.version,
68
70
  isScratch: row.isScratch === 1,
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Server-side helper: spawns a subprocess to extract the argsJsonSchema
3
+ * from a user script's `argsSchema` Zod export. Returns the JSON Schema
4
+ * serialized as a string, or null if the script has no argsSchema or
5
+ * if extraction fails.
6
+ *
7
+ * Extraction is best-effort and non-blocking — failures return null.
8
+ */
9
+
10
+ const TIMEOUT_MS = 5_000;
11
+
12
+ function extractorPath(): string {
13
+ return new URL("../../scripts-runtime/extract-args-schema.ts", import.meta.url).pathname;
14
+ }
15
+
16
+ export async function extractArgsJsonSchema(source: string): Promise<string | null> {
17
+ const tmpdir = `${process.env.TMPDIR ?? "/tmp"}/schema-extract-${crypto.randomUUID()}`;
18
+
19
+ try {
20
+ await Bun.$`mkdir -p ${tmpdir}`.quiet();
21
+
22
+ const sourceFile = `${tmpdir}/source.ts`;
23
+ const resultFile = `${tmpdir}/result.json`;
24
+ await Bun.write(sourceFile, source);
25
+
26
+ const proc = Bun.spawn(["bun", "run", extractorPath()], {
27
+ env: {
28
+ PATH: process.env.PATH ?? "/usr/bin:/bin",
29
+ HOME: process.env.HOME ?? "/tmp",
30
+ TMPDIR: tmpdir,
31
+ SWARM_SCHEMA_SOURCE_FILE: sourceFile,
32
+ SWARM_SCHEMA_RESULT_FILE: resultFile,
33
+ SWARM_SCHEMA_TMPDIR: tmpdir,
34
+ },
35
+ cwd: tmpdir,
36
+ stdout: "ignore",
37
+ stderr: "ignore",
38
+ });
39
+
40
+ const timeout = setTimeout(() => proc.kill(), TIMEOUT_MS);
41
+ const exitCode = await proc.exited.catch(() => 1);
42
+ clearTimeout(timeout);
43
+
44
+ if (exitCode !== 0) return null;
45
+
46
+ const result = await Bun.file(resultFile).text();
47
+ const parsed: unknown = JSON.parse(result);
48
+ if (parsed === null) return null;
49
+ return JSON.stringify(parsed);
50
+ } catch {
51
+ return null;
52
+ } finally {
53
+ await Bun.$`rm -rf ${tmpdir}`.quiet().nothrow();
54
+ }
55
+ }
@@ -120,6 +120,10 @@ function createCompilerHost(
120
120
  return files.get(normalized) ?? ts.sys.readFile(fileName);
121
121
  };
122
122
 
123
+ // Resolve external packages (e.g. "zod") from the project root rather than
124
+ // the virtual path "/virtual/..." so TypeScript can find real node_modules.
125
+ const projectBase = new URL("../../index.ts", import.meta.url).pathname;
126
+
123
127
  host.resolveModuleNames = (moduleNames, containingFile) =>
124
128
  moduleNames.map((moduleName) => {
125
129
  if (moduleName === "./user-script") {
@@ -131,7 +135,9 @@ function createCompilerHost(
131
135
  if (moduleName === "stdlib") {
132
136
  return { resolvedFileName: STDLIB_FILE, extension: ts.Extension.Dts };
133
137
  }
134
- return ts.resolveModuleName(moduleName, containingFile, options, host).resolvedModule;
138
+ // For external packages, resolve from project root so node_modules is found
139
+ const base = containingFile.startsWith("/virtual/") ? projectBase : containingFile;
140
+ return ts.resolveModuleName(moduleName, base, options, host).resolvedModule;
135
141
  });
136
142
 
137
143
  // In compiled binary mode, TypeScript's lib .d.ts files live alongside
@@ -368,7 +368,17 @@ const SWARM_TOOL_LABELS: Record<string, string | null> = {
368
368
  "inject-learning": "🧠 Storing learning",
369
369
  "memory-search": "🧠 Searching memory",
370
370
  "memory-get": "🧠 Retrieving memory",
371
+ "memory-delete": "🧠 Deleting memory",
371
372
  "update-profile": "🪪 Updating profile",
373
+ // Users
374
+ "manage-user": "👤 Managing user",
375
+ "resolve-user": "👤 Resolving user",
376
+ // Key-value store
377
+ "kv-get": "🔑 Reading KV value",
378
+ "kv-set": "🔑 Setting KV value",
379
+ "kv-list": "🔑 Listing KV keys",
380
+ "kv-delete": "🔑 Deleting KV value",
381
+ "kv-incr": "🔑 Incrementing KV value",
372
382
  // Slack
373
383
  "slack-post": "💬 Posting to Slack",
374
384
  "slack-start-thread": "💬 Starting Slack thread",
@@ -388,20 +398,37 @@ const SWARM_TOOL_LABELS: Record<string, string | null> = {
388
398
  "get-workflow": "⚙️ Checking workflow",
389
399
  "list-workflows": "⚙️ Listing workflows",
390
400
  "create-workflow": "⚙️ Creating workflow",
401
+ "update-workflow": "⚙️ Updating workflow",
402
+ "delete-workflow": "⚙️ Deleting workflow",
403
+ "patch-workflow": "⚙️ Patching workflow",
404
+ "patch-workflow-node": "⚙️ Patching workflow node",
405
+ "get-workflow-run": "⚙️ Checking workflow run",
406
+ "list-workflow-runs": "⚙️ Listing workflow runs",
407
+ "cancel-workflow-run": "⚙️ Cancelling workflow run",
408
+ "retry-workflow-run": "⚙️ Retrying workflow run",
391
409
  // Skills
392
410
  "skill-search": "🔎 Searching skills",
393
411
  "skill-install": "📦 Installing skill",
394
412
  "skill-install-remote": "📦 Installing remote skill",
395
413
  "skill-get": "📦 Getting skill details",
396
414
  "skill-list": "📦 Listing skills",
415
+ "skill-create": "📦 Creating skill",
416
+ "skill-update": "📦 Updating skill",
417
+ "skill-delete": "📦 Deleting skill",
418
+ "skill-publish": "📦 Publishing skill",
419
+ "skill-uninstall": "📦 Uninstalling skill",
420
+ "skill-sync-remote": "📦 Syncing remote skills",
397
421
  // Config
398
422
  "get-config": "⚙️ Reading config",
399
423
  "set-config": "⚙️ Setting config",
400
424
  "list-config": "⚙️ Listing config",
425
+ "delete-config": "⚙️ Deleting config",
401
426
  // Schedules
402
427
  "create-schedule": "📅 Creating schedule",
403
428
  "list-schedules": "📅 Listing schedules",
404
429
  "run-schedule-now": "📅 Running schedule",
430
+ "update-schedule": "📅 Updating schedule",
431
+ "delete-schedule": "📅 Deleting schedule",
405
432
  // Context
406
433
  "context-diff": "📜 Viewing context diff",
407
434
  "context-history": "📜 Viewing context history",
@@ -422,10 +449,34 @@ const SWARM_TOOL_LABELS: Record<string, string | null> = {
422
449
  "script-query-types": "📜 Reading script types",
423
450
  };
424
451
 
425
- /** Convert kebab-case to sentence case: "get-task-details" "Get task details" */
452
+ /** Words that keep specific casing when humanizing tool names. */
453
+ const TOOL_NAME_ACRONYMS: Record<string, string> = {
454
+ mcp: "MCP",
455
+ kv: "KV",
456
+ api: "API",
457
+ url: "URL",
458
+ id: "ID",
459
+ };
460
+
461
+ /**
462
+ * Convert kebab/snake-case to sentence case, preserving known acronyms.
463
+ * "get-task-details" → "Get task details"; "mcp-server-create" → "MCP server create".
464
+ */
426
465
  export function humanizeToolName(name: string): string {
427
466
  if (!name) return name;
428
- return name.charAt(0).toUpperCase() + name.slice(1).replaceAll("-", " ");
467
+ const words = name
468
+ .replaceAll("_", " ")
469
+ .replaceAll("-", " ")
470
+ .trim()
471
+ .split(/\s+/)
472
+ .filter(Boolean)
473
+ .map((w) => TOOL_NAME_ACRONYMS[w.toLowerCase()] ?? w);
474
+ const first = words[0];
475
+ if (!first) return name;
476
+ const head = TOOL_NAME_ACRONYMS[first.toLowerCase()]
477
+ ? first
478
+ : first.charAt(0).toUpperCase() + first.slice(1);
479
+ return [head, ...words.slice(1)].join(" ");
429
480
  }
430
481
 
431
482
  /**
@@ -501,7 +552,7 @@ export function toolCallToProgress(toolName: string, args: unknown): string | nu
501
552
  if (label === null) return null;
502
553
  if (label) return label;
503
554
 
504
- return `🔧 ${toolName}`;
555
+ return `🔧 ${humanizeToolName(toolName)}`;
505
556
  }
506
557
  }
507
558
  }
@@ -2681,15 +2732,35 @@ async function checkCompletedProcesses(
2681
2732
  credentialInfo &&
2682
2733
  /rate.?limit|hit your limit|usage[ _-]?limit|too many requests/i.test(failureReason)
2683
2734
  ) {
2684
- // Try to extract reset time from the error message (e.g. "resets 3pm (UTC)")
2685
- const parsedResetTime = parseRateLimitResetTime(failureReason);
2686
- const defaultCooldownMs = 5 * 60 * 1000;
2687
- const rateLimitedUntil =
2688
- parsedResetTime ?? new Date(Date.now() + defaultCooldownMs).toISOString();
2689
- if (parsedResetTime) {
2735
+ // Three-tier reset-time resolver (most to least precise):
2736
+ // Tier 1: structured rate_limit_event from Claude CLI (resetsAt epoch sec)
2737
+ // Tier 2: regex on the error message (e.g. "resets 3pm (UTC)")
2738
+ // Tier 3: 5-min hard fallback — only when both structured and regex fail
2739
+ // Tiers 1 & 2 are clamped to [now+60s, now+6h] at their source.
2740
+ const clampResetTime = (isoString: string): string => {
2741
+ const nowMs = Date.now();
2742
+ const minMs = nowMs + 60_000;
2743
+ const maxMs = nowMs + 6 * 60 * 60 * 1000;
2744
+ const candidateMs = new Date(isoString).getTime();
2745
+ return new Date(Math.min(Math.max(candidateMs, minMs), maxMs)).toISOString();
2746
+ };
2747
+
2748
+ let rateLimitedUntil: string;
2749
+ if (result.rateLimitResetAt) {
2750
+ rateLimitedUntil = clampResetTime(result.rateLimitResetAt);
2690
2751
  console.log(
2691
- `[credentials] Parsed rate limit reset time from error: ${parsedResetTime}`,
2752
+ `[credentials] Rate limit reset from rate_limit_event: ${rateLimitedUntil}`,
2692
2753
  );
2754
+ } else {
2755
+ const parsedResetTime = parseRateLimitResetTime(failureReason);
2756
+ if (parsedResetTime) {
2757
+ rateLimitedUntil = clampResetTime(parsedResetTime);
2758
+ console.log(
2759
+ `[credentials] Parsed rate limit reset time from error: ${rateLimitedUntil}`,
2760
+ );
2761
+ } else {
2762
+ rateLimitedUntil = new Date(Date.now() + 5 * 60 * 1000).toISOString();
2763
+ }
2693
2764
  }
2694
2765
  reportKeyRateLimit(
2695
2766
  apiConfig.apiUrl,
@@ -4,6 +4,7 @@ import { getAgentById } from "../be/db";
4
4
  import { createEvent } from "../be/events";
5
5
  import { deleteScript, getScript, upsertScriptByName } from "../be/scripts/db";
6
6
  import { searchScripts } from "../be/scripts/embeddings";
7
+ import { extractArgsJsonSchema } from "../be/scripts/extract-schema";
7
8
  import { SCRIPT_SDK_TYPES, SCRIPT_STDLIB_TYPES, typecheckScript } from "../be/scripts/typecheck";
8
9
  import { extractScriptSignature } from "../scripts-runtime/extract-signature";
9
10
  import { runScript } from "../scripts-runtime/loader";
@@ -217,6 +218,7 @@ export async function handleScripts(
217
218
  parsed.body.scope === "global"
218
219
  ? getScript({ name: parsed.body.name, scope: "agent", scopeId: agent.id })
219
220
  : null;
221
+ const argsJsonSchema = await extractArgsJsonSchema(parsed.body.source);
220
222
  const result = await upsertScriptByName({
221
223
  name: parsed.body.name,
222
224
  scope: parsed.body.scope,
@@ -225,6 +227,7 @@ export async function handleScripts(
225
227
  description: parsed.body.description,
226
228
  intent: parsed.body.intent,
227
229
  signatureJson: signatureJsonFor(parsed.body.source),
230
+ argsJsonSchema,
228
231
  fsMode: parsed.body.fsMode,
229
232
  agentId: agent.id,
230
233
  isScratch: false,
@@ -331,6 +334,9 @@ export async function handleScripts(
331
334
  results: matches.map(({ script, score }) => ({
332
335
  name: script.name,
333
336
  signature: JSON.parse(script.signatureJson),
337
+ argsJsonSchema: script.argsJsonSchema
338
+ ? (JSON.parse(script.argsJsonSchema) as unknown)
339
+ : null,
334
340
  description: script.description,
335
341
  score,
336
342
  })),
@@ -351,6 +357,7 @@ export async function handleScripts(
351
357
  }
352
358
  json(res, {
353
359
  signature: JSON.parse(script.signatureJson),
360
+ argsJsonSchema: script.argsJsonSchema ? (JSON.parse(script.argsJsonSchema) as unknown) : null,
354
361
  sdkTypes: SCRIPT_SDK_TYPES,
355
362
  stdlibTypes: SCRIPT_STDLIB_TYPES,
356
363
  });
@@ -412,9 +412,18 @@ export async function handleWebhooks(
412
412
 
413
413
  try {
414
414
  switch (payload.event_type) {
415
+ case "message.received.unauthenticated":
416
+ console.warn(
417
+ `[AgentMail] Received unauthenticated message - treating as received event for inbox ${payload.message?.inbox_id ?? "unknown"}`,
418
+ );
419
+
420
+ await handleMessageReceived(payload);
421
+ break;
422
+
415
423
  case "message.received":
416
424
  await handleMessageReceived(payload);
417
425
  break;
426
+
418
427
  default:
419
428
  console.log(`[AgentMail] Ignoring event type: ${payload.event_type}`);
420
429
  }
@@ -341,24 +341,11 @@ export async function handleWorkflows(
341
341
  }
342
342
  const rawBody = Buffer.concat(chunks).toString();
343
343
 
344
- // Validate JSON before processing (but pass raw string for HMAC)
345
- try {
346
- if (rawBody) JSON.parse(rawBody);
347
- } catch {
348
- jsonError(res, "Invalid JSON body", 400);
349
- return true;
350
- }
351
-
352
- const signature =
353
- (req.headers["x-hub-signature-256"] as string | undefined) ??
354
- (req.headers["x-signature"] as string | undefined);
355
-
356
344
  try {
357
345
  const result = await handleWebhookTrigger(
358
346
  workflowId,
359
- rawBody, // Raw body string — used for HMAC verification + passed as triggerData
360
- signature,
361
- signature,
347
+ rawBody, // Raw body string — HMAC is verified against raw bytes; JSON parsing happens inside
348
+ req.headers, // Full header bag — signature header resolved per trigger config
362
349
  getExecutorRegistry(),
363
350
  );
364
351
  json(res, result, 201);
@@ -458,6 +458,7 @@ class ClaudeSession implements ProviderSession {
458
458
  cost: lastCost,
459
459
  isError: (exitCode ?? 1) !== 0,
460
460
  failureReason,
461
+ rateLimitResetAt: this.errorTracker.getRateLimitResetAt(),
461
462
  };
462
463
  }
463
464
 
@@ -116,6 +116,14 @@ export interface ProviderResult {
116
116
  errorCategory?: string;
117
117
  /** Human-readable failure reason built from error tracking. */
118
118
  failureReason?: string;
119
+ /**
120
+ * ISO timestamp of the rate limit reset time, parsed from a structured
121
+ * `rate_limit_event` line in the Claude CLI stream. Only set by the Claude
122
+ * adapter when a `status: "rejected"` event is present. Already clamped to
123
+ * [now+60s, now+6h] at the source. The runner uses this as tier-1 of the
124
+ * three-tier cooldown resolver.
125
+ */
126
+ rateLimitResetAt?: string;
119
127
  }
120
128
 
121
129
  /** Behavioral traits that govern prompt assembly and feature gating. */
@@ -31,7 +31,31 @@ try {
31
31
  throw new Error("Swarm script must export a default function");
32
32
  }
33
33
 
34
- const result = await mod.default(parsedArgs, ctx);
34
+ let validatedArgs = parsedArgs;
35
+ if (mod.argsSchema && typeof mod.argsSchema === "object" && "parse" in mod.argsSchema) {
36
+ try {
37
+ // biome-ignore lint/suspicious/noExplicitAny: argsSchema is a Zod schema at runtime
38
+ validatedArgs = (mod.argsSchema as any).parse(parsedArgs);
39
+ } catch (err) {
40
+ // Format ZodError issues into a readable message
41
+ if (
42
+ err &&
43
+ typeof err === "object" &&
44
+ "issues" in err &&
45
+ Array.isArray((err as { issues: unknown[] }).issues)
46
+ ) {
47
+ const issues = (
48
+ err as { issues: Array<{ path: (string | number)[]; message: string }> }
49
+ ).issues
50
+ .map((i) => ` ${i.path.length ? i.path.join(".") : "(root)"}: ${i.message}`)
51
+ .join("\n");
52
+ throw new Error(`argsSchema validation failed:\n${issues}`);
53
+ }
54
+ throw err;
55
+ }
56
+ }
57
+
58
+ const result = await mod.default(validatedArgs, ctx);
35
59
  await Bun.write(requiredEnv("SWARM_SCRIPT_RESULT_FILE"), JSON.stringify(result ?? null));
36
60
  } catch (error) {
37
61
  console.error(error instanceof Error ? error.stack || error.message : String(error));
@@ -90,6 +90,9 @@ async function writeBareImportShims(tmpdir: string): Promise<void> {
90
90
  }
91
91
  await writeBareImportShim(tmpdir, "stdlib", new URL("../stdlib/index.ts", import.meta.url));
92
92
  await writeBareImportShim(tmpdir, "swarm-sdk", new URL("../swarm-sdk.ts", import.meta.url));
93
+ // Allow `import { z } from "zod"` in user scripts (for argsSchema definitions).
94
+ const zodEntry = Bun.resolveSync("zod", import.meta.dir);
95
+ await writeBareImportShim(tmpdir, "zod", new URL(`file://${zodEntry}`));
93
96
  }
94
97
 
95
98
  function harnessCommand(harnessPath: string, input: ExecutorInput): string[] {
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Subprocess script: extracts the argsJsonSchema from a user script module.
3
+ * Spawned by src/be/scripts/extract-schema.ts during script_upsert.
4
+ *
5
+ * Env vars (all required):
6
+ * SWARM_SCHEMA_SOURCE_FILE path to the script source file
7
+ * SWARM_SCHEMA_RESULT_FILE path where JSON Schema (or "null") is written
8
+ * SWARM_SCHEMA_TMPDIR tmpdir used for shims + the user module
9
+ */
10
+ import { toJSONSchema } from "zod";
11
+
12
+ async function createShims(tmpdir: string): Promise<void> {
13
+ const zodEntry = Bun.resolveSync("zod", import.meta.dir);
14
+ const shims: [string, URL][] = [
15
+ ["stdlib", new URL("./stdlib/index.ts", import.meta.url)],
16
+ ["swarm-sdk", new URL("./swarm-sdk.ts", import.meta.url)],
17
+ ["zod", new URL(`file://${zodEntry}`)],
18
+ ];
19
+ for (const [name, url] of shims) {
20
+ const dir = `${tmpdir}/node_modules/${name}`;
21
+ await Bun.$`mkdir -p ${dir}`.quiet();
22
+ await Bun.write(`${dir}/package.json`, JSON.stringify({ type: "module", main: "index.ts" }));
23
+ await Bun.write(`${dir}/index.ts`, `export * from ${JSON.stringify(url.href)};\n`);
24
+ }
25
+ }
26
+
27
+ const sourceFile = process.env.SWARM_SCHEMA_SOURCE_FILE;
28
+ const resultFile = process.env.SWARM_SCHEMA_RESULT_FILE;
29
+ const tmpdir = process.env.SWARM_SCHEMA_TMPDIR;
30
+
31
+ if (!sourceFile || !resultFile || !tmpdir) {
32
+ process.stderr.write("extract-args-schema: missing required env vars\n");
33
+ process.exit(1);
34
+ }
35
+
36
+ try {
37
+ await createShims(tmpdir);
38
+
39
+ const source = await Bun.file(sourceFile).text();
40
+ const userModulePath = `${tmpdir}/user-script.ts`;
41
+ await Bun.write(userModulePath, source);
42
+
43
+ let mod: Record<string, unknown>;
44
+ try {
45
+ mod = (await import(userModulePath)) as Record<string, unknown>;
46
+ } catch {
47
+ // Import failed (e.g. unresolvable imports) — not an error, just no schema
48
+ await Bun.write(resultFile, "null");
49
+ process.exit(0);
50
+ }
51
+
52
+ if (!mod.argsSchema || typeof mod.argsSchema !== "object") {
53
+ await Bun.write(resultFile, "null");
54
+ process.exit(0);
55
+ }
56
+
57
+ // biome-ignore lint/suspicious/noExplicitAny: argsSchema is a Zod schema at runtime
58
+ const schema = toJSONSchema(mod.argsSchema as any);
59
+ await Bun.write(resultFile, JSON.stringify(schema));
60
+ process.exit(0);
61
+ } catch (err) {
62
+ process.stderr.write(
63
+ `extract-args-schema: ${err instanceof Error ? err.message : String(err)}\n`,
64
+ );
65
+ try {
66
+ await Bun.write(resultFile, "null");
67
+ } catch {}
68
+ process.exit(0);
69
+ }
@@ -1,6 +1,6 @@
1
1
  import ts from "typescript";
2
2
 
3
- const ALLOWED_BARE_IMPORTS = new Set(["swarm-sdk", "stdlib"]);
3
+ const ALLOWED_BARE_IMPORTS = new Set(["swarm-sdk", "stdlib", "zod"]);
4
4
  const FORBIDDEN_HINTS = new Set(["node:", "bun:", "fs", "child_process", "crypto", "bun:sqlite"]);
5
5
 
6
6
  export type ImportAllowlistResult =