@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.
- package/openapi.json +399 -14
- package/package.json +3 -1
- package/src/artifact-sdk/server.ts +2 -1
- package/src/be/db.ts +1 -1
- package/src/be/migrations/064_scripts.sql +39 -0
- package/src/be/migrations/065_script_embeddings.sql +7 -0
- package/src/be/scripts/db.ts +391 -0
- package/src/be/scripts/embeddings.ts +231 -0
- package/src/be/scripts/maintenance.ts +9 -0
- package/src/be/scripts/typecheck.ts +193 -0
- package/src/cli.tsx +22 -5
- package/src/commands/artifact.ts +3 -2
- package/src/commands/claude-managed-setup.ts +2 -1
- package/src/commands/codex-login.ts +5 -3
- package/src/commands/onboard.tsx +2 -1
- package/src/commands/runner.ts +72 -10
- package/src/commands/setup.tsx +5 -3
- package/src/hooks/hook.ts +4 -3
- package/src/http/index.ts +40 -29
- package/src/http/memory.ts +28 -0
- package/src/http/openapi.ts +1 -0
- package/src/http/page-proxy.ts +2 -1
- package/src/http/route-def.ts +1 -0
- package/src/http/schedules.ts +37 -0
- package/src/http/scripts.ts +381 -0
- package/src/linear/outbound.ts +9 -2
- package/src/otel.ts +5 -0
- package/src/providers/claude-adapter.ts +22 -1
- package/src/scripts-runtime/ctx.ts +23 -0
- package/src/scripts-runtime/eval-harness.ts +39 -0
- package/src/scripts-runtime/executors/native.ts +229 -0
- package/src/scripts-runtime/executors/registry.ts +16 -0
- package/src/scripts-runtime/executors/types.ts +63 -0
- package/src/scripts-runtime/extract-signature.ts +81 -0
- package/src/scripts-runtime/import-allowlist.ts +109 -0
- package/src/scripts-runtime/loader.ts +96 -0
- package/src/scripts-runtime/redacted.ts +48 -0
- package/src/scripts-runtime/sdk-allowlist.ts +29 -0
- package/src/scripts-runtime/stdlib/fetch.ts +46 -0
- package/src/scripts-runtime/stdlib/glob.ts +8 -0
- package/src/scripts-runtime/stdlib/grep.ts +34 -0
- package/src/scripts-runtime/stdlib/index.ts +16 -0
- package/src/scripts-runtime/stdlib/table.ts +17 -0
- package/src/scripts-runtime/swarm-config.ts +35 -0
- package/src/scripts-runtime/swarm-sdk.ts +197 -0
- package/src/scripts-runtime/types/stdlib.d.ts +104 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +86 -0
- package/src/server.ts +12 -0
- package/src/tests/api-key.test.ts +33 -0
- package/src/tests/codex-login.test.ts +1 -1
- package/src/tests/linear-outbound-sync.test.ts +109 -0
- package/src/tests/mcp-tools.test.ts +69 -0
- package/src/tests/redacted.test.ts +29 -0
- package/src/tests/runner-tool-spans.test.ts +268 -0
- package/src/tests/script-executor-conformance.test.ts +142 -0
- package/src/tests/script-executor-registry.test.ts +17 -0
- package/src/tests/scripts-db.test.ts +329 -0
- package/src/tests/scripts-embeddings.test.ts +291 -0
- package/src/tests/scripts-extract-signature.test.ts +47 -0
- package/src/tests/scripts-http.test.ts +350 -0
- package/src/tests/scripts-import-allowlist.test.ts +55 -0
- package/src/tests/scripts-mcp-e2e.test.ts +269 -0
- package/src/tests/scripts-runtime-secret-egress.test.ts +44 -0
- package/src/tests/scripts-runtime.test.ts +289 -0
- package/src/tests/sdk-allowlist.test.ts +59 -0
- package/src/tests/secret-scrubber.test.ts +35 -1
- package/src/tests/swarm-config.test.ts +38 -0
- package/src/tests/tool-annotations.test.ts +2 -2
- package/src/tests/tool-call-progress.test.ts +30 -0
- package/src/tests/workflow-e2e.test.ts +218 -0
- package/src/tests/workflow-executors.test.ts +32 -2
- package/src/tests/workflow-input-redaction.test.ts +232 -0
- package/src/tests/workflow-swarm-script.test.ts +273 -0
- package/src/tools/memory-rate.ts +2 -1
- package/src/tools/script-common.ts +88 -0
- package/src/tools/script-delete.ts +35 -0
- package/src/tools/script-query-types.ts +37 -0
- package/src/tools/script-run.ts +43 -0
- package/src/tools/script-search.ts +32 -0
- package/src/tools/script-upsert.ts +43 -0
- package/src/tools/tool-config.ts +7 -0
- package/src/types.ts +60 -1
- package/src/utils/api-key.ts +28 -0
- package/src/utils/page-session.ts +8 -6
- package/src/utils/secret-scrubber.ts +22 -1
- package/src/workflows/engine.ts +12 -4
- package/src/workflows/executors/index.ts +1 -0
- package/src/workflows/executors/registry.ts +2 -0
- package/src/workflows/executors/script.ts +12 -1
- package/src/workflows/executors/swarm-script.ts +170 -0
- package/src/workflows/input.ts +65 -0
- package/src/workflows/recovery.ts +31 -3
- 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 =
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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);
|
package/src/commands/artifact.ts
CHANGED
|
@@ -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 =
|
|
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 =
|
|
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
|
|
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
|
|
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 =
|
|
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;
|
package/src/commands/onboard.tsx
CHANGED
|
@@ -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 =
|
|
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) {
|
package/src/commands/runner.ts
CHANGED
|
@@ -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
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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 =
|
|
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 (
|
|
2802
|
-
configureHttpResolver(apiUrl,
|
|
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 =
|
|
2875
|
+
const telemetryApiKey = apiKey || undefined;
|
|
2814
2876
|
await initTelemetry(
|
|
2815
2877
|
"worker",
|
|
2816
2878
|
async (key) => {
|
package/src/commands/setup.tsx
CHANGED
|
@@ -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 ?
|
|
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 =
|
|
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:
|
|
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 =
|
|
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
|
|
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 =
|
|
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";
|