@desplega.ai/agent-swarm 1.90.0 → 1.92.0
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 +2 -1
- package/openapi.json +803 -150
- package/package.json +5 -5
- package/src/artifact-sdk/server.ts +2 -1
- package/src/be/db.ts +337 -1
- package/src/be/memory/providers/sqlite-store.ts +6 -1
- package/src/be/memory/types.ts +1 -0
- package/src/be/migrations/083_script_workflows.sql +51 -0
- package/src/be/modelsdev-cache.json +42352 -38595
- package/src/be/scripts/typecheck.ts +181 -1
- package/src/be/seed-scripts/catalog/compound-insights.ts +398 -0
- package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +911 -0
- package/src/be/seed-scripts/catalog/schedule-health.ts +73 -0
- package/src/be/seed-scripts/catalog/smart-recall.ts +65 -0
- package/src/be/seed-scripts/catalog/task-context-gathering.ts +92 -0
- package/src/be/seed-scripts/catalog/tool-usage.ts +59 -0
- package/src/be/seed-scripts/index.ts +54 -0
- package/src/be/seed-skills/index.ts +7 -0
- package/src/be/swarm-config-guard.ts +17 -0
- package/src/commands/artifact.ts +3 -2
- package/src/commands/profile-sync.ts +310 -0
- package/src/commands/runner.ts +134 -3
- package/src/hooks/hook.ts +32 -9
- package/src/http/db-query.ts +20 -5
- package/src/http/index.ts +57 -0
- package/src/http/integrations.ts +6 -1
- package/src/http/mcp-bridge.ts +117 -0
- package/src/http/mcp-oauth.ts +97 -39
- package/src/http/memory.ts +5 -2
- package/src/http/openapi.ts +2 -2
- package/src/http/pages-public.ts +10 -11
- package/src/http/pages.ts +7 -11
- package/src/http/script-runs.ts +555 -0
- package/src/http/scripts.ts +24 -1
- package/src/http/utils.ts +11 -4
- package/src/jira/app.ts +2 -3
- package/src/jira/webhook-lifecycle.ts +2 -1
- package/src/linear/app.ts +2 -3
- package/src/prompts/session-templates.ts +24 -4
- package/src/providers/claude-adapter.ts +86 -13
- package/src/script-workflows/executor.ts +110 -0
- package/src/script-workflows/harness.ts +73 -0
- package/src/script-workflows/label-lint.ts +51 -0
- package/src/script-workflows/limits.ts +22 -0
- package/src/script-workflows/supervisor.ts +139 -0
- package/src/script-workflows/workflow-ctx.ts +205 -0
- package/src/scripts-runtime/executors/native.ts +1 -0
- package/src/scripts-runtime/sdk-allowlist.ts +124 -0
- package/src/scripts-runtime/swarm-sdk.ts +198 -3
- package/src/scripts-runtime/types/stdlib.d.ts +287 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +287 -0
- package/src/server.ts +2 -0
- package/src/slack/handlers.ts +11 -4
- package/src/slack/message-text.ts +98 -0
- package/src/slack/thread-buffer.ts +5 -3
- package/src/tests/claude-adapter-binary.test.ts +147 -4
- package/src/tests/claude-adapter-otel.test.ts +85 -1
- package/src/tests/db-query.test.ts +28 -0
- package/src/tests/error-tracker.test.ts +121 -0
- package/src/tests/harness-provider-resolution.test.ts +33 -0
- package/src/tests/hook-registration-nudge.test.ts +69 -0
- package/src/tests/mcp-oauth-manual-client.test.ts +213 -0
- package/src/tests/mcp-tools.test.ts +6 -0
- package/src/tests/pages-public-html.test.ts +41 -0
- package/src/tests/pages-public-json-redirect.test.ts +37 -2
- package/src/tests/profile-sync.test.ts +282 -0
- package/src/tests/prompt-template-session.test.ts +34 -5
- package/src/tests/script-runs-http.test.ts +278 -0
- package/src/tests/script-workflows-label-lint.test.ts +43 -0
- package/src/tests/script-workflows-runtime-e2e.test.ts +170 -0
- package/src/tests/scripts-mcp-e2e.test.ts +49 -2
- package/src/tests/scripts-runtime.test.ts +33 -0
- package/src/tests/seed-scripts.test.ts +347 -2
- package/src/tests/slack-message-text.test.ts +250 -0
- package/src/tests/system-default-skills.test.ts +40 -0
- package/src/tools/create-metric.ts +2 -3
- package/src/tools/create-page.ts +3 -6
- package/src/tools/db-query.ts +16 -6
- package/src/tools/memory-rate.ts +2 -1
- package/src/tools/memory-search.ts +1 -0
- package/src/tools/register-kapso-number.ts +2 -4
- package/src/tools/request-human-input.ts +2 -1
- package/src/tools/script-common.ts +2 -4
- package/src/tools/script-run.ts +7 -0
- package/src/tools/script-runs.ts +123 -0
- package/src/tools/slack-read.ts +12 -3
- package/src/tools/tool-config.ts +4 -1
- package/src/types.ts +52 -0
- package/src/utils/constants.ts +58 -8
- package/src/utils/error-tracker.ts +40 -1
- package/src/utils/internal-ai/complete-structured.ts +10 -4
- package/src/workflows/executors/raw-llm.ts +76 -59
- package/templates/skills/pages/content.md +205 -55
- package/templates/skills/script-workflows/config.json +14 -0
- package/templates/skills/script-workflows/content.md +68 -0
- package/templates/skills/swarm-scripts/content.md +45 -7
package/src/slack/handlers.ts
CHANGED
|
@@ -18,6 +18,7 @@ import type { SlackFile } from "./files";
|
|
|
18
18
|
import { extractTaskFromMessage, hasOtherUserMention, routeMessage } from "./router";
|
|
19
19
|
// Side-effect import: registers all Slack event templates in the in-memory registry
|
|
20
20
|
import "./templates";
|
|
21
|
+
import { extractSlackMessageText } from "./message-text";
|
|
21
22
|
import { bufferThreadMessage, getBufferMessageCount, instantFlush } from "./thread-buffer";
|
|
22
23
|
import { registerTreeMessage } from "./watcher";
|
|
23
24
|
|
|
@@ -173,6 +174,8 @@ interface ThreadMessage {
|
|
|
173
174
|
subtype?: string;
|
|
174
175
|
text?: string;
|
|
175
176
|
ts: string;
|
|
177
|
+
attachments?: Array<{ fallback?: string; text?: string; title?: string; pretext?: string }>;
|
|
178
|
+
blocks?: unknown[];
|
|
176
179
|
}
|
|
177
180
|
|
|
178
181
|
// Cache for bot's own user ID (avoids redundant auth.test calls)
|
|
@@ -286,8 +289,11 @@ async function getThreadContext(
|
|
|
286
289
|
});
|
|
287
290
|
|
|
288
291
|
const messages = (result.messages || []) as ThreadMessage[];
|
|
289
|
-
// Filter out the current message
|
|
290
|
-
const previousMessages = messages.filter((m) =>
|
|
292
|
+
// Filter out the current message; include any message with extractable text
|
|
293
|
+
const previousMessages = messages.filter((m) => {
|
|
294
|
+
if (m.ts === currentTs) return false;
|
|
295
|
+
return extractSlackMessageText(m) !== "";
|
|
296
|
+
});
|
|
291
297
|
|
|
292
298
|
if (previousMessages.length === 0) return "";
|
|
293
299
|
|
|
@@ -298,13 +304,14 @@ async function getThreadContext(
|
|
|
298
304
|
const isBotMessage =
|
|
299
305
|
m.user === botUserId || m.bot_id !== undefined || m.subtype === "bot_message";
|
|
300
306
|
|
|
307
|
+
const text = extractSlackMessageText(m);
|
|
301
308
|
if (isBotMessage) {
|
|
302
309
|
// Bot/agent message - truncate if too long
|
|
303
|
-
const truncatedText =
|
|
310
|
+
const truncatedText = text.length > 500 ? `${text.slice(0, 500)}...` : text;
|
|
304
311
|
formattedMessages.push(`[Agent]: ${truncatedText}`);
|
|
305
312
|
} else {
|
|
306
313
|
const userName = m.user ? await getUserDisplayName(client, m.user) : "Unknown";
|
|
307
|
-
formattedMessages.push(`${userName}: ${
|
|
314
|
+
formattedMessages.push(`${userName}: ${text}`);
|
|
308
315
|
}
|
|
309
316
|
}
|
|
310
317
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utility for extracting displayable text from a Slack message.
|
|
3
|
+
*
|
|
4
|
+
* Slack integration/alert apps (Datadog, PagerDuty, GitHub) often post with
|
|
5
|
+
* an empty top-level `text` field and put all content in legacy `attachments`
|
|
6
|
+
* or Block Kit `blocks`. This helper falls back through those layers so callers
|
|
7
|
+
* never silently drop those messages.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
interface SlackAttachment {
|
|
11
|
+
fallback?: string;
|
|
12
|
+
text?: string;
|
|
13
|
+
title?: string;
|
|
14
|
+
pretext?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Internal shape used only inside the helper; callers pass `blocks?: unknown[]`
|
|
18
|
+
interface SlackBlockInternal {
|
|
19
|
+
type?: string;
|
|
20
|
+
text?: { type?: string; text?: string };
|
|
21
|
+
/** section blocks may use fields[] instead of (or alongside) a top-level text object */
|
|
22
|
+
fields?: Array<{ type?: string; text?: string }>;
|
|
23
|
+
elements?: unknown[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SlackMessageLike {
|
|
27
|
+
text?: string;
|
|
28
|
+
attachments?: SlackAttachment[];
|
|
29
|
+
/** Typed as unknown[] so any Slack SDK block variant is accepted without casting. */
|
|
30
|
+
blocks?: unknown[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Recursively collect plain text from a Slack rich_text node tree.
|
|
35
|
+
*
|
|
36
|
+
* Handles text leaf nodes plus all container types that carry child elements:
|
|
37
|
+
* rich_text_section, rich_text_list, rich_text_quote, rich_text_preformatted.
|
|
38
|
+
*/
|
|
39
|
+
function collectRichTextParts(node: unknown, parts: string[]): void {
|
|
40
|
+
if (node == null || typeof node !== "object") return;
|
|
41
|
+
const n = node as { type?: string; text?: string; elements?: unknown[] };
|
|
42
|
+
if (n.type === "text" && n.text) {
|
|
43
|
+
parts.push(n.text);
|
|
44
|
+
}
|
|
45
|
+
if (Array.isArray(n.elements)) {
|
|
46
|
+
for (const child of n.elements) {
|
|
47
|
+
collectRichTextParts(child, parts);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Return the best displayable text for a Slack message.
|
|
54
|
+
*
|
|
55
|
+
* Priority:
|
|
56
|
+
* 1. `msg.text` (non-empty)
|
|
57
|
+
* 2. `msg.attachments[]` — joins `fallback || text || title || pretext` for each
|
|
58
|
+
* 3. `msg.blocks[]` — extracts text from section (text + fields) and rich_text blocks
|
|
59
|
+
* 4. `""` if nothing found
|
|
60
|
+
*/
|
|
61
|
+
export function extractSlackMessageText(msg: SlackMessageLike): string {
|
|
62
|
+
if (msg.text?.trim()) return msg.text;
|
|
63
|
+
|
|
64
|
+
// Legacy attachments (Datadog, PagerDuty, GitHub alert apps)
|
|
65
|
+
if (Array.isArray(msg.attachments) && msg.attachments.length > 0) {
|
|
66
|
+
const parts = msg.attachments
|
|
67
|
+
.filter((a): a is SlackAttachment => a != null && typeof a === "object")
|
|
68
|
+
.map((a) => a.fallback || a.text || a.title || a.pretext || "")
|
|
69
|
+
.filter(Boolean);
|
|
70
|
+
if (parts.length > 0) return parts.join("\n");
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Block Kit blocks
|
|
74
|
+
if (Array.isArray(msg.blocks) && msg.blocks.length > 0) {
|
|
75
|
+
const parts: string[] = [];
|
|
76
|
+
for (const rawBlock of msg.blocks) {
|
|
77
|
+
if (rawBlock == null || typeof rawBlock !== "object") continue;
|
|
78
|
+
const block = rawBlock as SlackBlockInternal;
|
|
79
|
+
if (block.type === "section") {
|
|
80
|
+
if (block.text?.text) parts.push(block.text.text);
|
|
81
|
+
if (Array.isArray(block.fields)) {
|
|
82
|
+
for (const field of block.fields) {
|
|
83
|
+
if (field != null && typeof field === "object" && field.text) {
|
|
84
|
+
parts.push(field.text);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} else if (block.type === "rich_text" && Array.isArray(block.elements)) {
|
|
89
|
+
for (const el of block.elements) {
|
|
90
|
+
collectRichTextParts(el, parts);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (parts.length > 0) return parts.join("\n");
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return "";
|
|
98
|
+
}
|
|
@@ -4,6 +4,7 @@ import { slackContextKey } from "../tasks/context-key";
|
|
|
4
4
|
import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
|
|
5
5
|
import { getSlackApp } from "./app";
|
|
6
6
|
import { buildBufferFlushBlocks } from "./blocks";
|
|
7
|
+
import { extractSlackMessageText } from "./message-text";
|
|
7
8
|
import { registerTreeMessage } from "./watcher";
|
|
8
9
|
|
|
9
10
|
interface BufferedMessage {
|
|
@@ -93,15 +94,16 @@ async function getThreadContextForBuffer(channelId: string, threadTs: string): P
|
|
|
93
94
|
if (messages.length === 0) return "";
|
|
94
95
|
|
|
95
96
|
const formatted = messages
|
|
96
|
-
.filter((m) => m
|
|
97
|
+
.filter((m) => extractSlackMessageText(m) !== "")
|
|
97
98
|
.map((m) => {
|
|
98
99
|
const msg = m as Record<string, unknown>;
|
|
99
100
|
const isBotMessage = msg.bot_id !== undefined || msg.subtype === "bot_message";
|
|
101
|
+
const text = extractSlackMessageText(m);
|
|
100
102
|
if (isBotMessage) {
|
|
101
|
-
const truncated =
|
|
103
|
+
const truncated = text.length > 500 ? `${text.slice(0, 500)}...` : text;
|
|
102
104
|
return `[Agent]: ${truncated}`;
|
|
103
105
|
}
|
|
104
|
-
return `<@${m.user}>: ${
|
|
106
|
+
return `<@${m.user}>: ${text}`;
|
|
105
107
|
})
|
|
106
108
|
.join("\n");
|
|
107
109
|
|
|
@@ -6,10 +6,12 @@
|
|
|
6
6
|
* 1. Binary resolution — argv[0..n] tracks `parseClaudeBinary(process.env.CLAUDE_BINARY)`,
|
|
7
7
|
* with `["claude"]` as the default. Same flags follow. Supports
|
|
8
8
|
* whitespace-separated command strings (e.g. `"bunx @dexh/shannon"`).
|
|
9
|
-
* 2.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* 3.
|
|
9
|
+
* 2. Claude Bridge routing — SWARM_USE_CLAUDE_BRIDGE=true/1 forces the
|
|
10
|
+
* installed `claude-bridge` argv prefix and wins over
|
|
11
|
+
* `CLAUDE_BINARY`.
|
|
12
|
+
* 3. Tmux fail-fast — when the resolved binary string contains "shannon"
|
|
13
|
+
* or claude-bridge is enabled, createSession throws if `tmux` is not on PATH.
|
|
14
|
+
* 4. Trust pre-seed — when the resolved binary contains "shannon", the
|
|
13
15
|
* adapter writes `projects[cwd].hasTrustDialogAccepted: true` to
|
|
14
16
|
* `$HOME/.claude.json` before spawning. Idempotent. No-op for "claude".
|
|
15
17
|
*
|
|
@@ -27,8 +29,10 @@ import { join } from "node:path";
|
|
|
27
29
|
import {
|
|
28
30
|
ClaudeAdapter,
|
|
29
31
|
parseClaudeBinary,
|
|
32
|
+
parseClaudeBridgeEnabled,
|
|
30
33
|
preseedClaudeTrustDialog,
|
|
31
34
|
resolveClaudeBinary,
|
|
35
|
+
resolveClaudeBridgeEnabled,
|
|
32
36
|
} from "../providers/claude-adapter";
|
|
33
37
|
import type { ProviderSessionConfig } from "../providers/types";
|
|
34
38
|
|
|
@@ -150,6 +154,45 @@ describe("resolveClaudeBinary precedence", () => {
|
|
|
150
154
|
});
|
|
151
155
|
});
|
|
152
156
|
|
|
157
|
+
describe("SWARM_USE_CLAUDE_BRIDGE boolean parsing", () => {
|
|
158
|
+
test("true/1 enable claude-bridge", () => {
|
|
159
|
+
expect(parseClaudeBridgeEnabled("true")).toBe(true);
|
|
160
|
+
expect(parseClaudeBridgeEnabled("TRUE")).toBe(true);
|
|
161
|
+
expect(parseClaudeBridgeEnabled(" 1 ")).toBe(true);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test("false/0/unset and invalid values are disabled", () => {
|
|
165
|
+
expect(parseClaudeBridgeEnabled("false")).toBe(false);
|
|
166
|
+
expect(parseClaudeBridgeEnabled("0")).toBe(false);
|
|
167
|
+
expect(parseClaudeBridgeEnabled(undefined)).toBe(false);
|
|
168
|
+
expect(parseClaudeBridgeEnabled("yes")).toBe(false);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("resolvedEnv wins over fallbackEnv", () => {
|
|
172
|
+
expect(
|
|
173
|
+
resolveClaudeBridgeEnabled(
|
|
174
|
+
{ SWARM_USE_CLAUDE_BRIDGE: "false" },
|
|
175
|
+
{ SWARM_USE_CLAUDE_BRIDGE: "true" },
|
|
176
|
+
),
|
|
177
|
+
).toBe(false);
|
|
178
|
+
expect(
|
|
179
|
+
resolveClaudeBridgeEnabled(
|
|
180
|
+
{ SWARM_USE_CLAUDE_BRIDGE: "1" },
|
|
181
|
+
{ SWARM_USE_CLAUDE_BRIDGE: "0" },
|
|
182
|
+
),
|
|
183
|
+
).toBe(true);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("empty resolvedEnv value falls through to fallbackEnv", () => {
|
|
187
|
+
expect(
|
|
188
|
+
resolveClaudeBridgeEnabled(
|
|
189
|
+
{ SWARM_USE_CLAUDE_BRIDGE: " " },
|
|
190
|
+
{ SWARM_USE_CLAUDE_BRIDGE: "true" },
|
|
191
|
+
),
|
|
192
|
+
).toBe(true);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
153
196
|
describe("preseedClaudeTrustDialog", () => {
|
|
154
197
|
let homeDir: string;
|
|
155
198
|
|
|
@@ -241,6 +284,7 @@ describe("preseedClaudeTrustDialog", () => {
|
|
|
241
284
|
describe("CLAUDE_BINARY env override", () => {
|
|
242
285
|
// Cache the originals and restore after each test so the suite stays clean.
|
|
243
286
|
let originalClaudeBinary: string | undefined;
|
|
287
|
+
let originalUseClaudeBridge: string | undefined;
|
|
244
288
|
let originalOauthToken: string | undefined;
|
|
245
289
|
let originalHome: string | undefined;
|
|
246
290
|
let homeDir: string;
|
|
@@ -250,11 +294,13 @@ describe("CLAUDE_BINARY env override", () => {
|
|
|
250
294
|
|
|
251
295
|
beforeEach(async () => {
|
|
252
296
|
originalClaudeBinary = process.env.CLAUDE_BINARY;
|
|
297
|
+
originalUseClaudeBridge = process.env.SWARM_USE_CLAUDE_BRIDGE;
|
|
253
298
|
originalOauthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
254
299
|
originalHome = process.env.HOME;
|
|
255
300
|
homeDir = await mkdtemp(join(tmpdir(), "claude-adapter-test-home-"));
|
|
256
301
|
process.env.HOME = homeDir;
|
|
257
302
|
delete process.env.CLAUDE_BINARY;
|
|
303
|
+
delete process.env.SWARM_USE_CLAUDE_BRIDGE;
|
|
258
304
|
// Credential check runs before binary resolution; satisfy it.
|
|
259
305
|
process.env.CLAUDE_CODE_OAUTH_TOKEN = "test-token";
|
|
260
306
|
|
|
@@ -285,6 +331,11 @@ describe("CLAUDE_BINARY env override", () => {
|
|
|
285
331
|
} else {
|
|
286
332
|
process.env.CLAUDE_BINARY = originalClaudeBinary;
|
|
287
333
|
}
|
|
334
|
+
if (originalUseClaudeBridge === undefined) {
|
|
335
|
+
delete process.env.SWARM_USE_CLAUDE_BRIDGE;
|
|
336
|
+
} else {
|
|
337
|
+
process.env.SWARM_USE_CLAUDE_BRIDGE = originalUseClaudeBridge;
|
|
338
|
+
}
|
|
288
339
|
if (originalOauthToken === undefined) {
|
|
289
340
|
delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
290
341
|
} else {
|
|
@@ -422,10 +473,65 @@ describe("CLAUDE_BINARY env override", () => {
|
|
|
422
473
|
|
|
423
474
|
expect(spawnedArgs[0][0]).toBe("shannon");
|
|
424
475
|
});
|
|
476
|
+
|
|
477
|
+
test("SWARM_USE_CLAUDE_BRIDGE=true routes through installed claude-bridge", async () => {
|
|
478
|
+
process.env.SWARM_USE_CLAUDE_BRIDGE = "true";
|
|
479
|
+
|
|
480
|
+
const adapter = new ClaudeAdapter();
|
|
481
|
+
await adapter.createSession(makeConfig());
|
|
482
|
+
|
|
483
|
+
const argv = spawnedArgs[0];
|
|
484
|
+
expect(argv[0]).toBe("claude-bridge");
|
|
485
|
+
expect(argv).toContain("--model");
|
|
486
|
+
expect(argv).toContain("-p");
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
test("SWARM_USE_CLAUDE_BRIDGE=1 wins over CLAUDE_BINARY=shannon", async () => {
|
|
490
|
+
process.env.SWARM_USE_CLAUDE_BRIDGE = "1";
|
|
491
|
+
process.env.CLAUDE_BINARY = "shannon";
|
|
492
|
+
|
|
493
|
+
const adapter = new ClaudeAdapter();
|
|
494
|
+
await adapter.createSession(makeConfig());
|
|
495
|
+
|
|
496
|
+
expect(spawnedArgs[0][0]).toBe("claude-bridge");
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
test("config.env SWARM_USE_CLAUDE_BRIDGE=true is reloadable and wins over process.env false", async () => {
|
|
500
|
+
process.env.SWARM_USE_CLAUDE_BRIDGE = "false";
|
|
501
|
+
|
|
502
|
+
const adapter = new ClaudeAdapter();
|
|
503
|
+
await adapter.createSession(
|
|
504
|
+
makeConfig({
|
|
505
|
+
env: {
|
|
506
|
+
SWARM_USE_CLAUDE_BRIDGE: "true",
|
|
507
|
+
CLAUDE_CODE_OAUTH_TOKEN: "test-token",
|
|
508
|
+
} as Record<string, string>,
|
|
509
|
+
}),
|
|
510
|
+
);
|
|
511
|
+
|
|
512
|
+
expect(spawnedArgs[0][0]).toBe("claude-bridge");
|
|
513
|
+
});
|
|
514
|
+
|
|
515
|
+
test("config.env SWARM_USE_CLAUDE_BRIDGE=false disables process.env true", async () => {
|
|
516
|
+
process.env.SWARM_USE_CLAUDE_BRIDGE = "true";
|
|
517
|
+
|
|
518
|
+
const adapter = new ClaudeAdapter();
|
|
519
|
+
await adapter.createSession(
|
|
520
|
+
makeConfig({
|
|
521
|
+
env: {
|
|
522
|
+
SWARM_USE_CLAUDE_BRIDGE: "false",
|
|
523
|
+
CLAUDE_CODE_OAUTH_TOKEN: "test-token",
|
|
524
|
+
} as Record<string, string>,
|
|
525
|
+
}),
|
|
526
|
+
);
|
|
527
|
+
|
|
528
|
+
expect(spawnedArgs[0][0]).toBe("claude");
|
|
529
|
+
});
|
|
425
530
|
});
|
|
426
531
|
|
|
427
532
|
describe("Shannon tmux fail-fast gate", () => {
|
|
428
533
|
let originalClaudeBinary: string | undefined;
|
|
534
|
+
let originalUseClaudeBridge: string | undefined;
|
|
429
535
|
let originalOauthToken: string | undefined;
|
|
430
536
|
let originalHome: string | undefined;
|
|
431
537
|
let homeDir: string;
|
|
@@ -434,11 +540,13 @@ describe("Shannon tmux fail-fast gate", () => {
|
|
|
434
540
|
|
|
435
541
|
beforeEach(async () => {
|
|
436
542
|
originalClaudeBinary = process.env.CLAUDE_BINARY;
|
|
543
|
+
originalUseClaudeBridge = process.env.SWARM_USE_CLAUDE_BRIDGE;
|
|
437
544
|
originalOauthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
438
545
|
originalHome = process.env.HOME;
|
|
439
546
|
homeDir = await mkdtemp(join(tmpdir(), "claude-adapter-test-home-"));
|
|
440
547
|
process.env.HOME = homeDir;
|
|
441
548
|
delete process.env.CLAUDE_BINARY;
|
|
549
|
+
delete process.env.SWARM_USE_CLAUDE_BRIDGE;
|
|
442
550
|
process.env.CLAUDE_CODE_OAUTH_TOKEN = "test-token";
|
|
443
551
|
spawnSpy = spyOn(Bun, "spawn").mockImplementation((() => makeFakeProc()) as typeof Bun.spawn);
|
|
444
552
|
whichSpy = spyOn(Bun, "which");
|
|
@@ -458,6 +566,11 @@ describe("Shannon tmux fail-fast gate", () => {
|
|
|
458
566
|
} else {
|
|
459
567
|
process.env.CLAUDE_BINARY = originalClaudeBinary;
|
|
460
568
|
}
|
|
569
|
+
if (originalUseClaudeBridge === undefined) {
|
|
570
|
+
delete process.env.SWARM_USE_CLAUDE_BRIDGE;
|
|
571
|
+
} else {
|
|
572
|
+
process.env.SWARM_USE_CLAUDE_BRIDGE = originalUseClaudeBridge;
|
|
573
|
+
}
|
|
461
574
|
if (originalOauthToken === undefined) {
|
|
462
575
|
delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
463
576
|
} else {
|
|
@@ -520,10 +633,22 @@ describe("Shannon tmux fail-fast gate", () => {
|
|
|
520
633
|
const adapter = new ClaudeAdapter();
|
|
521
634
|
await expect(adapter.createSession(makeConfig())).rejects.toThrow(/tmux/i);
|
|
522
635
|
});
|
|
636
|
+
|
|
637
|
+
test("SWARM_USE_CLAUDE_BRIDGE=true triggers the tmux check", async () => {
|
|
638
|
+
process.env.SWARM_USE_CLAUDE_BRIDGE = "true";
|
|
639
|
+
whichSpy.mockImplementation((name: string) => {
|
|
640
|
+
if (name === "tmux") return null;
|
|
641
|
+
return null;
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
const adapter = new ClaudeAdapter();
|
|
645
|
+
await expect(adapter.createSession(makeConfig())).rejects.toThrow(/SWARM_USE_CLAUDE_BRIDGE/);
|
|
646
|
+
});
|
|
523
647
|
});
|
|
524
648
|
|
|
525
649
|
describe("Trust pre-seed via ClaudeAdapter.createSession", () => {
|
|
526
650
|
let originalClaudeBinary: string | undefined;
|
|
651
|
+
let originalUseClaudeBridge: string | undefined;
|
|
527
652
|
let originalOauthToken: string | undefined;
|
|
528
653
|
let originalHome: string | undefined;
|
|
529
654
|
let homeDir: string;
|
|
@@ -532,11 +657,13 @@ describe("Trust pre-seed via ClaudeAdapter.createSession", () => {
|
|
|
532
657
|
|
|
533
658
|
beforeEach(async () => {
|
|
534
659
|
originalClaudeBinary = process.env.CLAUDE_BINARY;
|
|
660
|
+
originalUseClaudeBridge = process.env.SWARM_USE_CLAUDE_BRIDGE;
|
|
535
661
|
originalOauthToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
536
662
|
originalHome = process.env.HOME;
|
|
537
663
|
homeDir = await mkdtemp(join(tmpdir(), "claude-adapter-trust-test-"));
|
|
538
664
|
process.env.HOME = homeDir;
|
|
539
665
|
delete process.env.CLAUDE_BINARY;
|
|
666
|
+
delete process.env.SWARM_USE_CLAUDE_BRIDGE;
|
|
540
667
|
process.env.CLAUDE_CODE_OAUTH_TOKEN = "test-token";
|
|
541
668
|
spawnSpy = spyOn(Bun, "spawn").mockImplementation((() => makeFakeProc()) as typeof Bun.spawn);
|
|
542
669
|
whichSpy = spyOn(Bun, "which").mockImplementation((name: string) => {
|
|
@@ -559,6 +686,11 @@ describe("Trust pre-seed via ClaudeAdapter.createSession", () => {
|
|
|
559
686
|
} else {
|
|
560
687
|
process.env.CLAUDE_BINARY = originalClaudeBinary;
|
|
561
688
|
}
|
|
689
|
+
if (originalUseClaudeBridge === undefined) {
|
|
690
|
+
delete process.env.SWARM_USE_CLAUDE_BRIDGE;
|
|
691
|
+
} else {
|
|
692
|
+
process.env.SWARM_USE_CLAUDE_BRIDGE = originalUseClaudeBridge;
|
|
693
|
+
}
|
|
562
694
|
if (originalOauthToken === undefined) {
|
|
563
695
|
delete process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
564
696
|
} else {
|
|
@@ -625,4 +757,15 @@ describe("Trust pre-seed via ClaudeAdapter.createSession", () => {
|
|
|
625
757
|
const exists = await Bun.file(join(homeDir, ".claude.json")).exists();
|
|
626
758
|
expect(exists).toBe(false);
|
|
627
759
|
});
|
|
760
|
+
|
|
761
|
+
test("SWARM_USE_CLAUDE_BRIDGE=true does NOT use the legacy shannon trust pre-seed", async () => {
|
|
762
|
+
process.env.SWARM_USE_CLAUDE_BRIDGE = "true";
|
|
763
|
+
const adapter = new ClaudeAdapter();
|
|
764
|
+
await adapter.createSession(makeConfig({ cwd: "/some/abs/cwd" }));
|
|
765
|
+
|
|
766
|
+
// claude-bridge owns its own pre-clear flow; the adapter should not write
|
|
767
|
+
// the legacy shannon pre-seed file before spawning.
|
|
768
|
+
const exists = await Bun.file(join(homeDir, ".claude.json")).exists();
|
|
769
|
+
expect(exists).toBe(false);
|
|
770
|
+
});
|
|
628
771
|
});
|
|
@@ -15,7 +15,11 @@
|
|
|
15
15
|
|
|
16
16
|
import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test";
|
|
17
17
|
import { type Span, trace } from "@opentelemetry/api";
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
buildClaudeCodeOtelEnv,
|
|
20
|
+
buildClaudeCodeRuntimeEnv,
|
|
21
|
+
ClaudeAdapter,
|
|
22
|
+
} from "../providers/claude-adapter";
|
|
19
23
|
import type { ProviderSessionConfig } from "../providers/types";
|
|
20
24
|
|
|
21
25
|
// Example IDs from the task — trace af2c8371…, span adff4f24… (the orphaned
|
|
@@ -136,6 +140,51 @@ describe("buildClaudeCodeOtelEnv — gate on", () => {
|
|
|
136
140
|
});
|
|
137
141
|
});
|
|
138
142
|
|
|
143
|
+
describe("buildClaudeCodeRuntimeEnv", () => {
|
|
144
|
+
test("sets memory/privacy defaults for ephemeral Claude Code sessions", () => {
|
|
145
|
+
const env = buildClaudeCodeRuntimeEnv({});
|
|
146
|
+
|
|
147
|
+
expect(env.ENABLE_TOOL_SEARCH).toBe("true");
|
|
148
|
+
expect(env.CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING).toBe("1");
|
|
149
|
+
expect(env.CLAUDE_CODE_SKIP_PROMPT_HISTORY).toBe("1");
|
|
150
|
+
expect(env.CLAUDE_CODE_DISABLE_ATTACHMENTS).toBe("1");
|
|
151
|
+
expect(env.DISABLE_FEEDBACK_COMMAND).toBe("1");
|
|
152
|
+
expect(env.DISABLE_BUG_COMMAND).toBe("1");
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("always disables Claude Code Statsig/DNT telemetry", () => {
|
|
156
|
+
const env = buildClaudeCodeRuntimeEnv({});
|
|
157
|
+
|
|
158
|
+
expect(env.DISABLE_TELEMETRY).toBe("1");
|
|
159
|
+
expect(env.DO_NOT_TRACK).toBe("1");
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("keeps Claude Code Statsig/DNT opt-out separate from OTel config", () => {
|
|
163
|
+
for (const sourceEnv of [
|
|
164
|
+
{ CLAUDE_CODE_ENABLE_TELEMETRY: "1" },
|
|
165
|
+
{ CLAUDE_CODE_ENABLE_TELEMETRY: "true" },
|
|
166
|
+
{ OTEL_EXPORTER_OTLP_ENDPOINT: "https://otel.example.test" },
|
|
167
|
+
{ OTEL_TRACES_EXPORTER: "otlp" },
|
|
168
|
+
{ OTEL_METRICS_EXPORTER: "otlp" },
|
|
169
|
+
{ OTEL_LOGS_EXPORTER: "otlp" },
|
|
170
|
+
]) {
|
|
171
|
+
const env = buildClaudeCodeRuntimeEnv(sourceEnv);
|
|
172
|
+
expect(env.DISABLE_TELEMETRY).toBe("1");
|
|
173
|
+
expect(env.DO_NOT_TRACK).toBe("1");
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("ignores empty OTel env values for runtime defaults", () => {
|
|
178
|
+
const env = buildClaudeCodeRuntimeEnv({
|
|
179
|
+
OTEL_EXPORTER_OTLP_ENDPOINT: "",
|
|
180
|
+
CLAUDE_CODE_ENABLE_TELEMETRY: "0",
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
expect(env.DISABLE_TELEMETRY).toBe("1");
|
|
184
|
+
expect(env.DO_NOT_TRACK).toBe("1");
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
|
|
139
188
|
// ─── Spawn integration through ClaudeAdapter.createSession ────────────────────
|
|
140
189
|
|
|
141
190
|
/** Fake Bun.Subprocess — exits cleanly with no output. */
|
|
@@ -222,4 +271,39 @@ describe("ClaudeSession spawn env — SWARM_ENABLE_CLAUDE_CODE_OTEL", () => {
|
|
|
222
271
|
expect(env?.ENABLE_PROMPT_CACHING_1H).toBe("1");
|
|
223
272
|
expect(env?.CLAUDE_CODE_OAUTH_TOKEN).toBe("test-token");
|
|
224
273
|
});
|
|
274
|
+
|
|
275
|
+
test("spawn env carries Claude Code runtime guardrails", async () => {
|
|
276
|
+
const adapter = new ClaudeAdapter();
|
|
277
|
+
await adapter.createSession(makeConfig({ env: { CLAUDE_CODE_OAUTH_TOKEN: "test-token" } }));
|
|
278
|
+
|
|
279
|
+
expect(spawnedEnvs).toHaveLength(1);
|
|
280
|
+
const env = spawnedEnvs[0];
|
|
281
|
+
expect(env?.ENABLE_TOOL_SEARCH).toBe("true");
|
|
282
|
+
expect(env?.CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING).toBe("1");
|
|
283
|
+
expect(env?.CLAUDE_CODE_SKIP_PROMPT_HISTORY).toBe("1");
|
|
284
|
+
expect(env?.CLAUDE_CODE_DISABLE_ATTACHMENTS).toBe("1");
|
|
285
|
+
expect(env?.DISABLE_FEEDBACK_COMMAND).toBe("1");
|
|
286
|
+
expect(env?.DISABLE_BUG_COMMAND).toBe("1");
|
|
287
|
+
expect(env?.DISABLE_TELEMETRY).toBe("1");
|
|
288
|
+
expect(env?.DO_NOT_TRACK).toBe("1");
|
|
289
|
+
expect(env?.CLAUDE_CODE_DISABLE_THINKING).toBeUndefined();
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
test("spawn env keeps Statsig/DNT opt-out even when OTel is configured", async () => {
|
|
293
|
+
const adapter = new ClaudeAdapter();
|
|
294
|
+
await adapter.createSession(
|
|
295
|
+
makeConfig({
|
|
296
|
+
env: {
|
|
297
|
+
CLAUDE_CODE_OAUTH_TOKEN: "test-token",
|
|
298
|
+
CLAUDE_CODE_ENABLE_TELEMETRY: "1",
|
|
299
|
+
OTEL_EXPORTER_OTLP_ENDPOINT: "https://otel.example.test",
|
|
300
|
+
},
|
|
301
|
+
}),
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
expect(spawnedEnvs).toHaveLength(1);
|
|
305
|
+
const env = spawnedEnvs[0];
|
|
306
|
+
expect(env?.DISABLE_TELEMETRY).toBe("1");
|
|
307
|
+
expect(env?.DO_NOT_TRACK).toBe("1");
|
|
308
|
+
});
|
|
225
309
|
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import { DbQueryInputSchema, resolveDbQuerySql } from "../http/db-query";
|
|
3
|
+
|
|
4
|
+
describe("db-query input compatibility", () => {
|
|
5
|
+
test("canonical sql input resolves to sql", () => {
|
|
6
|
+
const parsed = DbQueryInputSchema.parse({ sql: "SELECT 1", params: [] });
|
|
7
|
+
|
|
8
|
+
expect(resolveDbQuerySql(parsed)).toBe("SELECT 1");
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("legacy query input remains a runtime alias", () => {
|
|
12
|
+
const parsed = DbQueryInputSchema.parse({ query: "SELECT 2" });
|
|
13
|
+
|
|
14
|
+
expect(resolveDbQuerySql(parsed)).toBe("SELECT 2");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("sql takes precedence when both sql and query are present", () => {
|
|
18
|
+
const parsed = DbQueryInputSchema.parse({ sql: "SELECT 3", query: "SELECT 4" });
|
|
19
|
+
|
|
20
|
+
expect(resolveDbQuerySql(parsed)).toBe("SELECT 3");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("rejects input without sql or query", () => {
|
|
24
|
+
const parsed = DbQueryInputSchema.safeParse({});
|
|
25
|
+
|
|
26
|
+
expect(parsed.success).toBe(false);
|
|
27
|
+
});
|
|
28
|
+
});
|