@desplega.ai/agent-swarm 1.80.0 → 1.80.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) 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/scripts/db.ts +391 -0
  8. package/src/be/scripts/embeddings.ts +231 -0
  9. package/src/be/scripts/maintenance.ts +9 -0
  10. package/src/be/scripts/typecheck.ts +193 -0
  11. package/src/cli.tsx +22 -5
  12. package/src/commands/artifact.ts +3 -2
  13. package/src/commands/claude-managed-setup.ts +2 -1
  14. package/src/commands/codex-login.ts +5 -3
  15. package/src/commands/onboard.tsx +2 -1
  16. package/src/commands/runner.ts +72 -10
  17. package/src/commands/setup.tsx +5 -3
  18. package/src/hooks/hook.ts +4 -3
  19. package/src/http/index.ts +40 -29
  20. package/src/http/memory.ts +28 -0
  21. package/src/http/openapi.ts +1 -0
  22. package/src/http/page-proxy.ts +2 -1
  23. package/src/http/route-def.ts +1 -0
  24. package/src/http/schedules.ts +37 -0
  25. package/src/http/scripts.ts +381 -0
  26. package/src/linear/outbound.ts +9 -2
  27. package/src/otel.ts +5 -0
  28. package/src/providers/claude-adapter.ts +22 -1
  29. package/src/scripts-runtime/ctx.ts +23 -0
  30. package/src/scripts-runtime/eval-harness.ts +39 -0
  31. package/src/scripts-runtime/executors/native.ts +229 -0
  32. package/src/scripts-runtime/executors/registry.ts +16 -0
  33. package/src/scripts-runtime/executors/types.ts +63 -0
  34. package/src/scripts-runtime/extract-signature.ts +81 -0
  35. package/src/scripts-runtime/import-allowlist.ts +109 -0
  36. package/src/scripts-runtime/loader.ts +96 -0
  37. package/src/scripts-runtime/redacted.ts +48 -0
  38. package/src/scripts-runtime/sdk-allowlist.ts +29 -0
  39. package/src/scripts-runtime/stdlib/fetch.ts +46 -0
  40. package/src/scripts-runtime/stdlib/glob.ts +8 -0
  41. package/src/scripts-runtime/stdlib/grep.ts +34 -0
  42. package/src/scripts-runtime/stdlib/index.ts +16 -0
  43. package/src/scripts-runtime/stdlib/table.ts +17 -0
  44. package/src/scripts-runtime/swarm-config.ts +35 -0
  45. package/src/scripts-runtime/swarm-sdk.ts +197 -0
  46. package/src/scripts-runtime/types/stdlib.d.ts +104 -0
  47. package/src/scripts-runtime/types/swarm-sdk.d.ts +86 -0
  48. package/src/server.ts +12 -0
  49. package/src/tests/api-key.test.ts +33 -0
  50. package/src/tests/codex-login.test.ts +1 -1
  51. package/src/tests/linear-outbound-sync.test.ts +109 -0
  52. package/src/tests/mcp-tools.test.ts +69 -0
  53. package/src/tests/redacted.test.ts +29 -0
  54. package/src/tests/runner-tool-spans.test.ts +268 -0
  55. package/src/tests/script-executor-conformance.test.ts +142 -0
  56. package/src/tests/script-executor-registry.test.ts +17 -0
  57. package/src/tests/scripts-db.test.ts +329 -0
  58. package/src/tests/scripts-embeddings.test.ts +291 -0
  59. package/src/tests/scripts-extract-signature.test.ts +47 -0
  60. package/src/tests/scripts-http.test.ts +350 -0
  61. package/src/tests/scripts-import-allowlist.test.ts +55 -0
  62. package/src/tests/scripts-mcp-e2e.test.ts +269 -0
  63. package/src/tests/scripts-runtime-secret-egress.test.ts +44 -0
  64. package/src/tests/scripts-runtime.test.ts +289 -0
  65. package/src/tests/sdk-allowlist.test.ts +59 -0
  66. package/src/tests/secret-scrubber.test.ts +35 -1
  67. package/src/tests/swarm-config.test.ts +38 -0
  68. package/src/tests/tool-annotations.test.ts +2 -2
  69. package/src/tests/tool-call-progress.test.ts +30 -0
  70. package/src/tests/workflow-e2e.test.ts +218 -0
  71. package/src/tests/workflow-executors.test.ts +32 -2
  72. package/src/tests/workflow-input-redaction.test.ts +232 -0
  73. package/src/tests/workflow-swarm-script.test.ts +273 -0
  74. package/src/tools/memory-rate.ts +2 -1
  75. package/src/tools/script-common.ts +88 -0
  76. package/src/tools/script-delete.ts +35 -0
  77. package/src/tools/script-query-types.ts +37 -0
  78. package/src/tools/script-run.ts +43 -0
  79. package/src/tools/script-search.ts +32 -0
  80. package/src/tools/script-upsert.ts +43 -0
  81. package/src/tools/tool-config.ts +7 -0
  82. package/src/types.ts +60 -1
  83. package/src/utils/api-key.ts +28 -0
  84. package/src/utils/page-session.ts +8 -6
  85. package/src/utils/secret-scrubber.ts +22 -1
  86. package/src/workflows/engine.ts +12 -4
  87. package/src/workflows/executors/index.ts +1 -0
  88. package/src/workflows/executors/registry.ts +2 -0
  89. package/src/workflows/executors/script.ts +12 -1
  90. package/src/workflows/executors/swarm-script.ts +170 -0
  91. package/src/workflows/input.ts +65 -0
  92. package/src/workflows/recovery.ts +31 -3
  93. package/src/workflows/resume.ts +43 -5
@@ -0,0 +1,193 @@
1
+ import ts from "typescript";
2
+
3
+ export type ScriptTypecheckResult = { ok: true } | { ok: false; diagnostics: string[] };
4
+
5
+ export const SCRIPT_SDK_TYPES = `
6
+ export type JsonValue = null | boolean | number | string | JsonValue[] | { [key: string]: JsonValue };
7
+ export type ScriptScope = "agent" | "global";
8
+ export type ScriptFsMode = "none" | "workspace-rw";
9
+
10
+ export interface Redacted<T> {
11
+ readonly __redactedBrand?: T;
12
+ toString(): "<redacted>";
13
+ toJSON(): "<redacted>";
14
+ }
15
+
16
+ export interface RedactedStatic {
17
+ value<T>(self: Redacted<T>): T;
18
+ meta<T>(self: Redacted<T>): { type: "system" | "user"; isSecret: boolean };
19
+ isSecret<T>(self: Redacted<T>): boolean;
20
+ }
21
+
22
+ export interface SwarmConfig {
23
+ apiKey: Redacted<string>;
24
+ agentId: Redacted<string>;
25
+ mcpBaseUrl: Redacted<string>;
26
+ get<T = string>(key: string): Redacted<T> | undefined;
27
+ }
28
+
29
+ export interface SwarmSdk {
30
+ memory_search(args: { query: string; scope?: "all" | "agent" | "swarm"; limit?: number; source?: string }): Promise<unknown>;
31
+ memory_get(args: { memoryId: string }): Promise<unknown>;
32
+ memory_rate(args: { id: string; useful: boolean; note?: string }): Promise<unknown>;
33
+ task_list(args?: Record<string, unknown>): Promise<unknown>;
34
+ task_get(args: { taskId: string }): Promise<unknown>;
35
+ task_storeProgress(args: Record<string, unknown>): Promise<unknown>;
36
+ kv_get(args: { key: string; namespace?: string }): Promise<unknown>;
37
+ kv_set(args: { key: string; value: unknown; namespace?: string; ttlSeconds?: number; valueType?: "string" | "json" | "integer" }): Promise<unknown>;
38
+ kv_del(args: { key: string; namespace?: string }): Promise<unknown>;
39
+ kv_incr(args: { key: string; by?: number; namespace?: string }): Promise<unknown>;
40
+ kv_list(args?: { prefix?: string; namespace?: string; limit?: number }): Promise<unknown>;
41
+ repo_list(args?: Record<string, unknown>): Promise<unknown>;
42
+ schedule_list(args?: Record<string, unknown>): Promise<unknown>;
43
+ script_search(args: { query?: string; scope?: ScriptScope; limit?: number }): Promise<unknown>;
44
+ script_run(args: { name?: string; source?: string; args?: unknown; intent?: string; scope?: ScriptScope; fsMode?: ScriptFsMode }): Promise<unknown>;
45
+ }
46
+
47
+ export interface ScriptStdlib {
48
+ fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
49
+ fetchJson(input: string | URL | Request, init?: RequestInit): Promise<unknown>;
50
+ grep(pattern: string, files?: string | string[]): Promise<string>;
51
+ glob(pattern: string): Promise<string[]>;
52
+ table(rows: Array<Record<string, unknown>>): string;
53
+ Redacted: RedactedStatic;
54
+ }
55
+
56
+ export interface ScriptLogger extends Console {}
57
+
58
+ export interface ScriptContext {
59
+ swarm: SwarmSdk & { config: SwarmConfig };
60
+ stdlib: ScriptStdlib;
61
+ logger: ScriptLogger;
62
+ }
63
+
64
+ // biome-ignore lint/suspicious/noExplicitAny: scripts may narrow their args type at the entrypoint.
65
+ export type ScriptMain = (args: any, ctx: ScriptContext) => unknown | Promise<unknown>;
66
+ `;
67
+
68
+ export const SCRIPT_STDLIB_TYPES = `
69
+ declare module "stdlib" {
70
+ export interface Redacted<T> {
71
+ readonly __redactedBrand?: T;
72
+ toString(): "<redacted>";
73
+ toJSON(): "<redacted>";
74
+ }
75
+ export const Redacted: {
76
+ value<T>(self: Redacted<T>): T;
77
+ meta<T>(self: Redacted<T>): { type: "system" | "user"; isSecret: boolean };
78
+ isSecret<T>(self: Redacted<T>): boolean;
79
+ };
80
+ export function fetch(input: string | URL | Request, init?: RequestInit): Promise<Response>;
81
+ export function fetchJson(input: string | URL | Request, init?: RequestInit): Promise<unknown>;
82
+ export function grep(pattern: string, files?: string | string[]): Promise<string>;
83
+ export function glob(pattern: string): Promise<string[]>;
84
+ export function table(rows: Array<Record<string, unknown>>): string;
85
+ }
86
+
87
+ declare module "swarm-sdk" {
88
+ ${SCRIPT_SDK_TYPES.replace(/^/gm, " ")}
89
+ }
90
+ `;
91
+
92
+ const USER_FILE = "/virtual/user-script.ts";
93
+ const CHECK_FILE = "/virtual/check.ts";
94
+ const SDK_FILE = "/virtual/swarm-sdk.d.ts";
95
+ const STDLIB_FILE = "/virtual/stdlib.d.ts";
96
+
97
+ function createCompilerHost(
98
+ files: Map<string, string>,
99
+ options: ts.CompilerOptions,
100
+ ): ts.CompilerHost {
101
+ const host = ts.createCompilerHost(options, true);
102
+ const originalGetSourceFile = host.getSourceFile.bind(host);
103
+
104
+ host.getSourceFile = (fileName, languageVersion, onError, shouldCreateNewSourceFile) => {
105
+ const normalized = fileName.replace(/\\/g, "/");
106
+ const source = files.get(normalized);
107
+ if (source !== undefined) {
108
+ return ts.createSourceFile(normalized, source, languageVersion, true, ts.ScriptKind.TS);
109
+ }
110
+ return originalGetSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile);
111
+ };
112
+
113
+ host.fileExists = (fileName) => {
114
+ const normalized = fileName.replace(/\\/g, "/");
115
+ return files.has(normalized) || ts.sys.fileExists(fileName);
116
+ };
117
+
118
+ host.readFile = (fileName) => {
119
+ const normalized = fileName.replace(/\\/g, "/");
120
+ return files.get(normalized) ?? ts.sys.readFile(fileName);
121
+ };
122
+
123
+ host.resolveModuleNames = (moduleNames, containingFile) =>
124
+ moduleNames.map((moduleName) => {
125
+ if (moduleName === "./user-script") {
126
+ return { resolvedFileName: USER_FILE, extension: ts.Extension.Ts };
127
+ }
128
+ if (moduleName === "swarm-sdk") {
129
+ return { resolvedFileName: SDK_FILE, extension: ts.Extension.Dts };
130
+ }
131
+ if (moduleName === "stdlib") {
132
+ return { resolvedFileName: STDLIB_FILE, extension: ts.Extension.Dts };
133
+ }
134
+ return ts.resolveModuleName(moduleName, containingFile, options, host).resolvedModule;
135
+ });
136
+
137
+ // In compiled binary mode, TypeScript's lib .d.ts files live alongside
138
+ // typescript.js in /$bunfs/ — but .d.ts files are not embedded in the binary.
139
+ // Redirect lib lookups to TS_LIB_DIR where the Dockerfile copies real copies.
140
+ const tsLibDir = process.env.TS_LIB_DIR;
141
+ if (tsLibDir) {
142
+ host.getDefaultLibLocation = () => tsLibDir;
143
+ }
144
+
145
+ return host;
146
+ }
147
+
148
+ export function typecheckScript(source: string): ScriptTypecheckResult {
149
+ const options: ts.CompilerOptions = {
150
+ allowImportingTsExtensions: true,
151
+ module: ts.ModuleKind.ESNext,
152
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
153
+ noEmit: true,
154
+ skipLibCheck: true,
155
+ strict: true,
156
+ target: ts.ScriptTarget.ES2022,
157
+ types: [],
158
+ };
159
+
160
+ const files = new Map<string, string>([
161
+ [USER_FILE, source],
162
+ [SDK_FILE, SCRIPT_SDK_TYPES],
163
+ [STDLIB_FILE, SCRIPT_STDLIB_TYPES],
164
+ [
165
+ CHECK_FILE,
166
+ `import run from "./user-script";
167
+ import type { ScriptMain } from "swarm-sdk";
168
+ const _scriptMain: ScriptMain = run;
169
+ void _scriptMain;
170
+ `,
171
+ ],
172
+ ]);
173
+
174
+ const host = createCompilerHost(files, options);
175
+ const program = ts.createProgram([USER_FILE, CHECK_FILE, SDK_FILE, STDLIB_FILE], options, host);
176
+ const diagnostics = [
177
+ ...program.getSyntacticDiagnostics(),
178
+ ...program.getSemanticDiagnostics(),
179
+ ].filter((diagnostic) => {
180
+ const fileName = diagnostic.file?.fileName.replace(/\\/g, "/");
181
+ return fileName === USER_FILE || fileName === CHECK_FILE;
182
+ });
183
+
184
+ if (diagnostics.length === 0) return { ok: true };
185
+
186
+ const formatted = ts.formatDiagnosticsWithColorAndContext(diagnostics, {
187
+ getCanonicalFileName: (fileName) => fileName,
188
+ getCurrentDirectory: () => "/virtual",
189
+ getNewLine: () => "\n",
190
+ });
191
+
192
+ return { ok: false, diagnostics: formatted.split("\n\n").filter(Boolean) };
193
+ }
package/src/cli.tsx CHANGED
@@ -9,6 +9,7 @@ import { runLead } from "./commands/lead.ts";
9
9
  import { Onboard } from "./commands/onboard.tsx";
10
10
  import { Setup as Connect } from "./commands/setup.tsx";
11
11
  import { runWorker } from "./commands/worker.ts";
12
+ import { getApiKey, setApiKey } from "./utils/api-key.ts";
12
13
 
13
14
  // Get CLI name from bin field (assumes single key)
14
15
  const binName = Object.keys(pkg.bin)[0];
@@ -43,7 +44,7 @@ interface ParsedArgs {
43
44
  function parseArgs(args: string[]): ParsedArgs {
44
45
  const command = args[0] && !args[0].startsWith("-") ? args[0] : undefined;
45
46
  let port = process.env.PORT || "3013";
46
- let key = process.env.API_KEY || "";
47
+ let key = getApiKey();
47
48
  let msg = "";
48
49
  let headless = false;
49
50
  let dryRun = false;
@@ -151,7 +152,7 @@ const COMMAND_HELP: Record<
151
152
  connect: {
152
153
  usage: `${binName} connect [options]`,
153
154
  description:
154
- "Connect this project to an existing swarm.\nCreates .mcp.json and .claude/settings.local.json with server URL and API key.\nAuto-reads API_KEY from .env if present.",
155
+ "Connect this project to an existing swarm.\nCreates .mcp.json and .claude/settings.local.json with server URL and API key.\nAuto-reads AGENT_SWARM_API_KEY (or legacy API_KEY) from .env if present.",
155
156
  options: [
156
157
  " --dry-run Show what would be changed without writing",
157
158
  " --restore Restore files from .bak backups",
@@ -248,13 +249,19 @@ const COMMAND_HELP: Record<
248
249
  options: " -h, --help Show this help",
249
250
  examples: [` ${binName} artifact serve`, ` ${binName} artifact help`].join("\n"),
250
251
  },
252
+ scripts: {
253
+ usage: `${binName} scripts reembed`,
254
+ description: "Maintenance commands for reusable swarm scripts.",
255
+ options: " -h, --help Show this help",
256
+ examples: ` ${binName} scripts reembed`,
257
+ },
251
258
  "codex-login": {
252
259
  usage: `${binName} codex-login [options]`,
253
260
  description:
254
261
  "Authenticate Codex via ChatGPT OAuth (browser or manual paste).\nPrompts interactively for the target API URL and a best-effort masked API key, then stores credentials in the swarm API config store for deployed workers.",
255
262
  options: [
256
263
  " --api-url <url> Swarm API URL (default: MCP_BASE_URL or http://localhost:3013)",
257
- " --api-key <key> Swarm API key (default: API_KEY or 123123)",
264
+ " --api-key <key> Swarm API key (default: AGENT_SWARM_API_KEY or API_KEY, falling back to 123123)",
258
265
  " -h, --help Show this help",
259
266
  ].join("\n"),
260
267
  examples: [
@@ -269,7 +276,7 @@ const COMMAND_HELP: Record<
269
276
  "Bootstrap Anthropic Managed Agents for the swarm: create the cloud environment, upload plugin/commands/*.md skills, create the managed agent, and persist the resulting IDs to swarm_config so deployed workers restore them at boot. Prompts interactively for ANTHROPIC_API_KEY when not set in env. Idempotent — re-run with --force to recreate.",
270
277
  options: [
271
278
  " --api-url <url> Swarm API URL (default: MCP_BASE_URL or http://localhost:3013)",
272
- " --api-key <key> Swarm API key (default: API_KEY or 123123)",
279
+ " --api-key <key> Swarm API key (default: AGENT_SWARM_API_KEY or API_KEY, falling back to 123123)",
273
280
  " --force Recreate Anthropic-side resources even if already configured",
274
281
  " -h, --help Show this help",
275
282
  ].join("\n"),
@@ -306,6 +313,7 @@ function printHelp(command?: string) {
306
313
  ["claude", "Run Claude CLI"],
307
314
  ["hook", "Handle Claude Code hook events (stdin)"],
308
315
  ["artifact", "Manage agent artifacts"],
316
+ ["scripts", "Reusable scripts maintenance"],
309
317
  ["docs", "Open documentation (--open to launch in browser)"],
310
318
  ["codex-login", "Authenticate Codex via ChatGPT OAuth"],
311
319
  ["claude-managed-setup", "Bootstrap Anthropic Managed Agents (agent + env + skills)"],
@@ -324,7 +332,7 @@ function McpServer({ port, apiKey, dbPath }: { port: string; apiKey: string; dbP
324
332
 
325
333
  useEffect(() => {
326
334
  process.env.PORT = port;
327
- process.env.API_KEY = apiKey;
335
+ setApiKey(apiKey);
328
336
  if (dbPath) {
329
337
  process.env.DATABASE_PATH = dbPath;
330
338
  }
@@ -547,6 +555,15 @@ if (args.showHelp || args.command === "help" || args.command === undefined) {
547
555
  port: args.port,
548
556
  key: args.key,
549
557
  });
558
+ } else if (args.command === "scripts") {
559
+ const scriptsArgs = process.argv.slice(process.argv.indexOf("scripts") + 1);
560
+ if (args.showHelp || scriptsArgs[0] !== "reembed") {
561
+ printHelp("scripts");
562
+ process.exit(scriptsArgs[0] === "reembed" || args.showHelp ? 0 : 1);
563
+ }
564
+ const { runScriptsMaintenanceCommand } = await import("./be/scripts/maintenance");
565
+ await runScriptsMaintenanceCommand(scriptsArgs);
566
+ console.log("Scripts re-embedded.");
550
567
  } else if (args.command === "codex-login") {
551
568
  const { runCodexLogin } = await import("./commands/codex-login");
552
569
  const codexLoginArgs = process.argv.slice(process.argv.indexOf("codex-login") + 1);
@@ -1,5 +1,6 @@
1
1
  import { Hono } from "hono";
2
2
  import { createArtifactServer } from "../artifact-sdk";
3
+ import { getApiKey } from "../utils/api-key";
3
4
 
4
5
  interface ArtifactArgs {
5
6
  additionalArgs: string[];
@@ -137,7 +138,7 @@ async function artifactServe(args: ArtifactArgs) {
137
138
  }
138
139
 
139
140
  async function artifactList() {
140
- const apiKey = process.env.API_KEY || "";
141
+ const apiKey = getApiKey();
141
142
  const mcpBaseUrl = process.env.MCP_BASE_URL || `http://localhost:${process.env.PORT || "3013"}`;
142
143
  const agentId = process.env.AGENT_ID || "";
143
144
 
@@ -195,7 +196,7 @@ async function artifactStop(args: ArtifactArgs) {
195
196
  process.exit(1);
196
197
  }
197
198
 
198
- const apiKey = process.env.API_KEY || "";
199
+ const apiKey = getApiKey();
199
200
  const mcpBaseUrl = process.env.MCP_BASE_URL || `http://localhost:${process.env.PORT || "3013"}`;
200
201
  const agentId = process.env.AGENT_ID || "";
201
202
 
@@ -33,6 +33,7 @@ import type { BetaEnvironment } from "@anthropic-ai/sdk/resources/beta/environme
33
33
  import type { SkillCreateResponse } from "@anthropic-ai/sdk/resources/beta/skills";
34
34
  import { toFile } from "@anthropic-ai/sdk/uploads";
35
35
 
36
+ import { getApiKey } from "../utils/api-key";
36
37
  import { promptHiddenInput } from "./codex-login.js";
37
38
 
38
39
  // ─── Types ───────────────────────────────────────────────────────────────────
@@ -397,7 +398,7 @@ export async function resolveClaudeManagedSetupConfig(
397
398
  const isInteractive = deps.isInteractive ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
398
399
 
399
400
  const apiUrl = parsed.apiUrl ?? env.MCP_BASE_URL ?? "http://localhost:3013";
400
- const apiKey = parsed.apiKey ?? env.API_KEY ?? "123123";
401
+ const apiKey = parsed.apiKey ?? (getApiKey(env) || "123123");
401
402
 
402
403
  let anthropicApiKey = env.ANTHROPIC_API_KEY ?? "";
403
404
  if (!anthropicApiKey && isInteractive) {
@@ -14,6 +14,7 @@ import { emitKeypressEvents } from "node:readline";
14
14
 
15
15
  import { loginCodexOAuth } from "../providers/codex-oauth/flow.js";
16
16
  import { storeCodexOAuth } from "../providers/codex-oauth/storage.js";
17
+ import { getApiKey } from "../utils/api-key";
17
18
 
18
19
  type PromptTextFn = (label: string, defaultValue: string) => Promise<string>;
19
20
  type PromptSecretFn = (label: string, defaultValue: string, helpText?: string) => Promise<string>;
@@ -146,7 +147,8 @@ export async function resolveCodexLoginConfig(
146
147
  const promptSecret = deps.promptSecret ?? promptHiddenInput;
147
148
  const isInteractive = deps.isInteractive ?? Boolean(process.stdin.isTTY && process.stdout.isTTY);
148
149
  const defaultApiUrl = env.MCP_BASE_URL || "http://localhost:3013";
149
- const defaultApiKey = env.API_KEY || "123123";
150
+ const envApiKey = getApiKey(env);
151
+ const defaultApiKey = envApiKey || "123123";
150
152
 
151
153
  let apiUrl = parsed.apiUrl ?? defaultApiUrl;
152
154
  let apiKey = parsed.apiKey ?? defaultApiKey;
@@ -156,8 +158,8 @@ export async function resolveCodexLoginConfig(
156
158
  }
157
159
 
158
160
  if (!parsed.apiKey && isInteractive) {
159
- const apiKeyHelp = env.API_KEY
160
- ? "Press Enter to use API_KEY from the environment"
161
+ const apiKeyHelp = envApiKey
162
+ ? "Press Enter to use AGENT_SWARM_API_KEY/API_KEY from the environment"
161
163
  : "Press Enter to use the default local API key";
162
164
  apiKey =
163
165
  (await promptSecret("Swarm API key", defaultApiKey, apiKeyHelp)).trim() || defaultApiKey;
@@ -3,6 +3,7 @@ import { Select } from "@inkjs/ui";
3
3
  import { Box, Text, useApp, useInput } from "ink";
4
4
  import { useCallback, useEffect, useRef, useState } from "react";
5
5
  import pkg from "../../package.json";
6
+ import { getApiKey } from "../utils/api-key.ts";
6
7
  import { getAgentSummary, getPresetById, PRESETS } from "./onboard/presets.ts";
7
8
  import { CoreCredentialsStep } from "./onboard/steps/core-credentials.tsx";
8
9
  import { CustomTemplatesStep } from "./onboard/steps/custom-templates.tsx";
@@ -140,7 +141,7 @@ export function Onboard({ dryRun = false, yes = false, preset }: OnboardProps) {
140
141
  }
141
142
 
142
143
  const credentialType = anthropicKey ? "api_key" : "oauth";
143
- const apiKey = process.env.API_KEY || crypto.randomBytes(16).toString("hex");
144
+ const apiKey = getApiKey() || crypto.randomBytes(16).toString("hex");
144
145
 
145
146
  const agentIds: Record<string, string> = {};
146
147
  for (const svc of selectedPreset.services) {
@@ -6,6 +6,7 @@ import {
6
6
  type Attributes,
7
7
  initOtel,
8
8
  injectTraceContext,
9
+ isPollTracingEnabled,
9
10
  type SwarmSpan,
10
11
  startSpan,
11
12
  withSpan,
@@ -31,6 +32,7 @@ import {
31
32
  } from "../providers/index.ts";
32
33
  import { initTelemetry, telemetry } from "../telemetry.ts";
33
34
  import type { ProviderName, RepoGuidelines } from "../types.ts";
35
+ import { getApiKey } from "../utils/api-key.ts";
34
36
  import { computeBudgetBackoffMs } from "../utils/budget-backoff.ts";
35
37
  import { getContextWindowSize } from "../utils/context-window.ts";
36
38
  import { type CredentialSelection, resolveCredentialPools } from "../utils/credentials.ts";
@@ -412,6 +414,12 @@ const SWARM_TOOL_LABELS: Record<string, string | null> = {
412
414
  "list-services": "🔌 Listing services",
413
415
  "unregister-service": "🔌 Unregistering service",
414
416
  "update-service-status": "🔌 Updating service status",
417
+ // Reusable scripts
418
+ "script-search": "📜 Searching scripts",
419
+ "script-run": "📜 Running script",
420
+ "script-upsert": "📜 Saving script",
421
+ "script-delete": "📜 Deleting script",
422
+ "script-query-types": "📜 Reading script types",
415
423
  };
416
424
 
417
425
  /** Convert kebab-case to sentence case: "get-task-details" → "Get task details" */
@@ -487,13 +495,11 @@ export function toolCallToProgress(toolName: string, args: unknown): string | nu
487
495
  }
488
496
 
489
497
  // Pi-mono exposes tools from the built-in swarm MCP endpoint as bare
490
- // names ("store-progress", "send-task", ...), not as mcp__ names.
498
+ // names ("store-progress", "send-task", "script-run", ...), not as mcp__ names.
491
499
  // Treat those names as agent-swarm tools so activity stays readable.
492
- if (toolName.includes("-")) {
493
- const label = SWARM_TOOL_LABELS[toolName];
494
- if (label === null) return null;
495
- if (label) return label;
496
- }
500
+ const label = SWARM_TOOL_LABELS[toolName];
501
+ if (label === null) return null;
502
+ if (label) return label;
497
503
 
498
504
  return `🔧 ${toolName}`;
499
505
  }
@@ -1513,6 +1519,9 @@ async function registerAgent(opts: {
1513
1519
 
1514
1520
  /** Poll for triggers via HTTP API */
1515
1521
  async function pollForTrigger(opts: PollOptions): Promise<Trigger | null> {
1522
+ if (!isPollTracingEnabled()) {
1523
+ return pollForTriggerOnce(opts);
1524
+ }
1516
1525
  return withSpan(
1517
1526
  "worker.poll",
1518
1527
  async (span) => {
@@ -1917,6 +1926,47 @@ function providerEventAttributes(event: ProviderEvent): Attributes {
1917
1926
  }
1918
1927
  }
1919
1928
 
1929
+ /**
1930
+ * Entry shape for the `activeToolSpans` map maintained by `runWithSession`.
1931
+ * Exported for unit tests that exercise `implicitCloseActiveToolSpans`.
1932
+ */
1933
+ export type ActiveToolSpanEntry = {
1934
+ span: SwarmSpan;
1935
+ startedAt: number;
1936
+ };
1937
+
1938
+ /**
1939
+ * Closes any still-open tool spans (both `worker.tool` and `worker.mcp.tool`)
1940
+ * in `activeToolSpans` at the assistant-message boundary, tagging them with
1941
+ * `agentswarm.tool.implicit_close=true` and removing them from the map.
1942
+ *
1943
+ * The Claude SDK adapter doesn't emit per-tool completion events for any
1944
+ * tool kind, MCP or harness-side — so the assistant-message boundary serves
1945
+ * as an implicit `tool_end` for everything. Spans closed here are still
1946
+ * successful (code 1, OK); the boundary is just a signal that the prior
1947
+ * tool turn is done.
1948
+ *
1949
+ * Returns the number of spans closed (handy for tests / metrics).
1950
+ */
1951
+ export function implicitCloseActiveToolSpans(
1952
+ activeToolSpans: Map<string, ActiveToolSpanEntry>,
1953
+ now: number = Date.now(),
1954
+ ): number {
1955
+ let closed = 0;
1956
+ for (const [toolCallId, active] of activeToolSpans) {
1957
+ active.span.setAttributes({
1958
+ "agentswarm.tool.duration_ms": now - active.startedAt,
1959
+ "agentswarm.tool.implicit_close": true,
1960
+ "agentswarm.tool.call_id": toolCallId,
1961
+ });
1962
+ active.span.setStatus({ code: 1 });
1963
+ active.span.end();
1964
+ activeToolSpans.delete(toolCallId);
1965
+ closed++;
1966
+ }
1967
+ return closed;
1968
+ }
1969
+
1920
1970
  async function spawnProviderProcess(
1921
1971
  adapter: ReturnType<typeof createProviderAdapter>,
1922
1972
  opts: {
@@ -2155,6 +2205,18 @@ async function spawnProviderProcess(
2155
2205
  sessionId: event.sessionId,
2156
2206
  });
2157
2207
  break;
2208
+ case "message": {
2209
+ // Assistant-message boundary acts as an implicit tool_end for ALL
2210
+ // still-open tool spans (both `worker.tool` and `worker.mcp.tool`).
2211
+ // The Claude SDK adapter doesn't emit per-tool completion events for
2212
+ // any tool kind, so without this their spans would only close at
2213
+ // session shutdown via `closeActiveToolSpans` and report wall-clock
2214
+ // duration from tool_start to session end.
2215
+ if (event.role === "assistant") {
2216
+ implicitCloseActiveToolSpans(activeToolSpans);
2217
+ }
2218
+ break;
2219
+ }
2158
2220
  case "tool_start": {
2159
2221
  const tool = classifyTool(event.toolName, event.args);
2160
2222
  const toolSpan = startSpan(tool.kind === "mcp" ? "worker.mcp.tool" : "worker.tool", {
@@ -2772,7 +2834,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
2772
2834
 
2773
2835
  const apiUrl = process.env.MCP_BASE_URL || `http://localhost:${process.env.PORT || "3013"}`;
2774
2836
  const swarmUrl = process.env.SWARM_URL || "localhost";
2775
- const apiKey = process.env.API_KEY || "";
2837
+ const apiKey = getApiKey();
2776
2838
 
2777
2839
  // Resolve the boot harness provider from swarm_config (repo > agent > global,
2778
2840
  // overlaid on top of `process.env`). This is what selects the adapter for
@@ -2798,8 +2860,8 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
2798
2860
  let adapter = createProviderAdapter(bootProvider);
2799
2861
 
2800
2862
  // Configure HTTP-based template resolution (workers resolve via API, not local DB)
2801
- if (process.env.API_KEY) {
2802
- configureHttpResolver(apiUrl, process.env.API_KEY);
2863
+ if (apiKey) {
2864
+ configureHttpResolver(apiUrl, apiKey);
2803
2865
  }
2804
2866
 
2805
2867
  // Initialize anonymized telemetry (opt-out via ANONYMIZED_TELEMETRY=false).
@@ -2810,7 +2872,7 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
2810
2872
  // skips telemetry instead of minting a fresh `install_<hex>` ID per
2811
2873
  // restart, which floods prod metrics with phantom installs.
2812
2874
  {
2813
- const telemetryApiKey = process.env.API_KEY;
2875
+ const telemetryApiKey = apiKey || undefined;
2814
2876
  await initTelemetry(
2815
2877
  "worker",
2816
2878
  async (key) => {
@@ -2,6 +2,7 @@
2
2
  import { Spinner, TextInput } from "@inkjs/ui";
3
3
  import { Box, Text, useApp } from "ink";
4
4
  import { useCallback, useEffect, useRef, useState } from "react";
5
+ import { getApiKey } from "../utils/api-key.ts";
5
6
  import {
6
7
  createDefaultMcpJson,
7
8
  createDefaultSettingsLocal,
@@ -47,7 +48,7 @@ export function Setup({ dryRun = false, restore = false, yes = false }: SetupPro
47
48
  const { exit } = useApp();
48
49
  const [state, setState] = useState<SetupState>({
49
50
  step: restore ? "restoring" : "check_dirs",
50
- token: yes ? process.env.API_KEY || "" : "",
51
+ token: yes ? getApiKey() : "",
51
52
  agentId: yes ? process.env.AGENT_ID || "" : "",
52
53
  existingToken: "",
53
54
  existingAgentId: "",
@@ -258,14 +259,15 @@ export function Setup({ dryRun = false, restore = false, yes = false }: SetupPro
258
259
 
259
260
  // In non-interactive mode (yes=true), skip prompts and go directly to updating
260
261
  if (yes) {
261
- const token = process.env.API_KEY;
262
+ const token = getApiKey();
262
263
  const agentId = process.env.AGENT_ID;
263
264
 
264
265
  if (!token) {
265
266
  setState((s) => ({
266
267
  ...s,
267
268
  step: "error",
268
- error: "API_KEY environment variable is required in non-interactive mode (-y/--yes)",
269
+ error:
270
+ "AGENT_SWARM_API_KEY (or legacy API_KEY) environment variable is required in non-interactive mode (-y/--yes)",
269
271
  }));
270
272
  return;
271
273
  }
package/src/hooks/hook.ts CHANGED
@@ -10,6 +10,7 @@ import {
10
10
  type RetrievalRow,
11
11
  } from "../be/memory/raters/llm";
12
12
  import type { Agent } from "../types";
13
+ import { getApiKey } from "../utils/api-key";
13
14
  import { summarizeSession as runSummarize } from "../utils/internal-ai";
14
15
  import { checkToolLoop, clearToolHistory } from "./tool-loop-detection";
15
16
 
@@ -150,7 +151,7 @@ async function fetchTaskDetails(
150
151
  taskId: string,
151
152
  ): Promise<{ id: string; task: string; progress?: string } | null> {
152
153
  const apiUrl = process.env.MCP_BASE_URL || `http://localhost:${process.env.PORT || "3013"}`;
153
- const apiKey = process.env.API_KEY || "";
154
+ const apiKey = getApiKey();
154
155
  const headers: Record<string, string> = {};
155
156
  if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
156
157
 
@@ -301,7 +302,7 @@ export async function runStopHookSessionSummary(
301
302
  const { taskContext, taskId } = await resolveStopHookTaskContext(env);
302
303
 
303
304
  const apiUrl = env.MCP_BASE_URL || `http://localhost:${env.PORT || "3013"}`;
304
- const apiKey = env.API_KEY || "";
305
+ const apiKey = getApiKey(env);
305
306
 
306
307
  // Memory-rater v1.5 step-4: piggyback per-memory ratings on the
307
308
  // existing summary call when MEMORY_RATERS includes `llm`.
@@ -1152,7 +1153,7 @@ ${hasAgentIdHeader() ? `You have a pre-defined agent ID via header: ${mcpConfig?
1152
1153
  try {
1153
1154
  const apiUrl =
1154
1155
  process.env.MCP_BASE_URL || `http://localhost:${process.env.PORT || "3013"}`;
1155
- const apiKey = process.env.API_KEY || "";
1156
+ const apiKey = getApiKey();
1156
1157
  const fileContent = await Bun.file(editedPath).text();
1157
1158
  const isShared = editedPath.startsWith("/workspace/shared/");
1158
1159
  const fileName = editedPath.split("/").pop() ?? "unnamed";