@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 +3 -0
- package/openapi.json +1 -1
- package/package.json +1 -1
- package/src/agentmail/types.ts +1 -0
- package/src/be/migrations/066_scripts_args_json_schema.sql +1 -0
- package/src/be/scripts/db.ts +34 -8
- package/src/be/scripts/embeddings.ts +2 -0
- package/src/be/scripts/extract-schema.ts +55 -0
- package/src/be/scripts/typecheck.ts +7 -1
- package/src/commands/runner.ts +81 -10
- package/src/http/scripts.ts +7 -0
- package/src/http/webhooks.ts +9 -0
- package/src/http/workflows.ts +2 -15
- package/src/providers/claude-adapter.ts +1 -0
- package/src/providers/types.ts +8 -0
- package/src/scripts-runtime/eval-harness.ts +25 -1
- package/src/scripts-runtime/executors/native.ts +3 -0
- package/src/scripts-runtime/extract-args-schema.ts +69 -0
- package/src/scripts-runtime/import-allowlist.ts +1 -1
- package/src/tests/error-tracker.test.ts +44 -0
- package/src/tests/http-api-integration.test.ts +8 -4
- package/src/tests/rate-limit-event.test.ts +292 -0
- package/src/tests/scripts-http.test.ts +53 -0
- package/src/tests/scripts-runtime.test.ts +55 -0
- package/src/tests/workflow-triggers-v2.test.ts +261 -20
- package/src/types.ts +1 -0
- package/src/utils/error-tracker.ts +58 -0
- package/src/workflows/input.ts +7 -2
- package/src/workflows/triggers.ts +89 -9
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.
|
|
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
package/src/agentmail/types.ts
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ALTER TABLE scripts ADD COLUMN argsJsonSchema TEXT;
|
package/src/be/scripts/db.ts
CHANGED
|
@@ -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<
|
|
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
|
-
[
|
|
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 = ?,
|
|
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
|
-
|
|
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
|
package/src/commands/runner.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
|
|
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
|
-
//
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
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]
|
|
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,
|
package/src/http/scripts.ts
CHANGED
|
@@ -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
|
});
|
package/src/http/webhooks.ts
CHANGED
|
@@ -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
|
}
|
package/src/http/workflows.ts
CHANGED
|
@@ -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 —
|
|
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);
|
package/src/providers/types.ts
CHANGED
|
@@ -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
|
-
|
|
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 =
|