Conversation
Two issues were causing setup terminals to timeout during CLI registration: 1. **teamDir mismatch**: Dashboard-server used detectWorkspacePath() for spawner's projectRoot, but spawner calculated its own teamDir from that. In cloud workspaces, this could differ from the daemon's teamDir, causing spawner to poll wrong connected-agents.json file. Fix: Pass teamDir explicitly from dashboard-server to spawner. 2. **interactive flag ignored**: SpawnRequest.interactive was never used, so auto-accept flags (--dangerously-skip-permissions, --force, etc.) were always added even for setup terminals that need user interaction. Fix: Only add auto-accept flags when interactive=false. Root cause: Introduced in Phase 1 filesystem refactor (388cf85) on Jan 22 when detectWorkspacePath was added without ensuring path consistency. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add query operations: getStatus, getInbox, listAgents, getHealth, getMetrics - Add consensus support: createProposal, vote methods - Add logs utilities: getLogs, listLoggedAgents for file-based log reading - Add protocol types for all query/response messages - Create SWARM_PATTERNS.md with 8 detailed swarm orchestration patterns - Create SWARM_CAPABILITIES.md mapping primitives to swarm capabilities - Create docs/guides/swarm-primitives.mdx for Mintlify docs - Update README with swarm positioning and examples - Add comprehensive tests for new functionality (50 tests passing) The SDK now provides all primitives needed to build any swarm architecture: handoffs, continuity, consensus, shared memory, discovery, monitoring, and dynamic spawning. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Defines implementation specs for 8 new primitives to enhance competitive position: 1. Memory System - Short/long-term, entity, shared memory with semantic search 2. Guardrails - Input/output validation, PII detection, schema validation 3. Tracing & Observability - OpenTelemetry-compatible span-based tracing 4. Human-in-the-Loop - Approval requests, escalations, interventions 5. Backpressure & Flow Control - Bounded queues, priority lanes, rate limiting 6. Attachments - Chunked upload/download for large files 7. Roles & Permissions - RBAC with built-in roles (admin, lead, worker) 8. Task Queues - Persistent work distribution with claiming and dependencies Each spec includes: - Protocol message definitions - SDK API design - Database schemas - Configuration options - Usage examples Competitive analysis shows these primitives will make Agent Relay the only framework with all capabilities for production multi-agent systems. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Created 8 beads for new swarm primitives (Memory, Guardrails, Tracing, HITL, Backpressure, Attachments, Roles, Task Queues) - Added dependencies between primitives - Recorded trajectory for competitive analysis work 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
New package for manual interactive testing of CLI authentication flows: - Docker environment with all CLIs pre-installed - Shell scripts for testing individual CLIs with relay-pty - Credential verification and clearing utilities - TypeScript SDK for programmatic socket communication Usage: npm run cli-tester:start # Start test container npm run cli-tester:start:clean # Start with clean credentials Inside container: test-cli.sh claude # Test CLI with relay-pty verify-auth.sh claude # Check credentials inject-message.sh test-claude "Hello" # Send message 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
| */ | ||
| async spawn(request: SpawnRequest): Promise<SpawnResult> { | ||
| const { name, cli, task, team, spawnerName, userId, includeWorkflowConventions } = request; | ||
| const { name, cli, task, team, spawnerName, userId, includeWorkflowConventions, interactive } = request; |
Check failure
Code scanning / CodeQL
Insecure randomness High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 29 days ago
In general, the fix is to replace uses of Math.random() in packages/utils/src/name-generator.ts with a cryptographically secure random source. Since this code runs in Node.js (CLI and bridge), we should use crypto.randomInt (or crypto.randomBytes with proper conversion) from Node’s built-in crypto module, which is designed for secure randomness. This avoids predictability while keeping the existing behavior (uniform random choice from arrays and a numeric suffix) intact.
Concretely, in packages/utils/src/name-generator.ts:
- Add an import from Node’s
cryptomodule, e.g.import { randomInt } from 'node:crypto';. - Replace
Math.floor(Math.random() * ADJECTIVES.length)withrandomInt(ADJECTIVES.length)and similarly forNOUNS.length.randomInt(max)returns a uniform integer in[0, max), matching the original intent without bias. - In
generateUniqueAgentName, replace the fallback’sMath.floor(Math.random() * 1000)withrandomInt(1000)so the suffix comes from the same secure source.
No changes are needed in src/cli/index.ts, packages/config/src/shadow-config.ts, or packages/bridge/src/spawner.ts, because once the source of randomness is secure, all flows from those generated names become secure as well.
| @@ -3,6 +3,8 @@ | ||
| * Inspired by mcp_agent_mail's approach. | ||
| */ | ||
|
|
||
| import { randomInt } from 'node:crypto'; | ||
|
|
||
| const ADJECTIVES = [ | ||
| 'Blue', 'Green', 'Red', 'Purple', 'Golden', 'Silver', 'Crystal', 'Amber', | ||
| 'Coral', 'Jade', 'Ruby', 'Sapphire', 'Emerald', 'Onyx', 'Pearl', 'Copper', | ||
| @@ -29,8 +31,8 @@ | ||
| * Generate a random agent name (AdjectiveNoun format). | ||
| */ | ||
| export function generateAgentName(): string { | ||
| const adjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)]; | ||
| const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)]; | ||
| const adjective = ADJECTIVES[randomInt(ADJECTIVES.length)]; | ||
| const noun = NOUNS[randomInt(NOUNS.length)]; | ||
| return `${adjective}${noun}`; | ||
| } | ||
|
|
||
| @@ -45,7 +47,7 @@ | ||
| } | ||
| } | ||
| // Fallback: append random suffix | ||
| return `${generateAgentName()}${Math.floor(Math.random() * 1000)}`; | ||
| return `${generateAgentName()}${randomInt(1000)}`; | ||
| } | ||
|
|
||
| /** |
| */ | ||
| async spawn(request: SpawnRequest): Promise<SpawnResult> { | ||
| const { name, cli, task, team, spawnerName, userId, includeWorkflowConventions } = request; | ||
| const { name, cli, task, team, spawnerName, userId, includeWorkflowConventions, interactive } = request; |
Check failure
Code scanning / CodeQL
Insecure randomness High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 29 days ago
In general, to fix insecure randomness you must replace Math.random() with a cryptographically secure random generator wherever the value can influence security-sensitive behavior. In Node.js this means using the crypto module (for example, crypto.randomInt or crypto.webcrypto.getRandomValues) instead of Math.random.
For this codebase, all taint originates from generateAgentName and the fallback in generateUniqueAgentName in packages/utils/src/name-generator.ts. The best targeted fix is to change those two uses of Math.random() to use crypto.randomInt (for index selection and integer suffix) while keeping the public API and behavior identical from the caller’s perspective. This does not require modifications in src/cli/index.ts, packages/config/src/shadow-config.ts, or packages/bridge/src/spawner.ts, because once the randomness is secure at the source, the entire taint path becomes safe.
Concretely:
- In
packages/utils/src/name-generator.ts, import Node’scryptomodule. - Replace
Math.floor(Math.random() * ADJECTIVES.length)withcrypto.randomInt(ADJECTIVES.length). - Replace
Math.floor(Math.random() * NOUNS.length)withcrypto.randomInt(NOUNS.length). - Replace the fallback
Math.floor(Math.random() * 1000)withcrypto.randomInt(1000).
No other files need code changes for this particular issue.
| @@ -3,6 +3,8 @@ | ||
| * Inspired by mcp_agent_mail's approach. | ||
| */ | ||
|
|
||
| import crypto from 'node:crypto'; | ||
|
|
||
| const ADJECTIVES = [ | ||
| 'Blue', 'Green', 'Red', 'Purple', 'Golden', 'Silver', 'Crystal', 'Amber', | ||
| 'Coral', 'Jade', 'Ruby', 'Sapphire', 'Emerald', 'Onyx', 'Pearl', 'Copper', | ||
| @@ -29,8 +31,8 @@ | ||
| * Generate a random agent name (AdjectiveNoun format). | ||
| */ | ||
| export function generateAgentName(): string { | ||
| const adjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)]; | ||
| const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)]; | ||
| const adjective = ADJECTIVES[crypto.randomInt(ADJECTIVES.length)]; | ||
| const noun = NOUNS[crypto.randomInt(NOUNS.length)]; | ||
| return `${adjective}${noun}`; | ||
| } | ||
|
|
||
| @@ -45,7 +47,7 @@ | ||
| } | ||
| } | ||
| // Fallback: append random suffix | ||
| return `${generateAgentName()}${Math.floor(Math.random() * 1000)}`; | ||
| return `${generateAgentName()}${crypto.randomInt(1000)}`; | ||
| } | ||
|
|
||
| /** |
| */ | ||
| async spawn(request: SpawnRequest): Promise<SpawnResult> { | ||
| const { name, cli, task, team, spawnerName, userId, includeWorkflowConventions } = request; | ||
| const { name, cli, task, team, spawnerName, userId, includeWorkflowConventions, interactive } = request; |
Check failure
Code scanning / CodeQL
Insecure randomness High
Show autofix suggestion
Hide autofix suggestion
Copilot Autofix
AI 29 days ago
General fix: Replace all uses of Math.random() in the name‑generation utilities with a cryptographically secure random source. In Node.js, the standard approach is to use the crypto module (randomInt for indices and simple bounded integers, or randomBytes and then derive indices).
Best concrete fix here:
- In
packages/utils/src/name-generator.ts, add an import of Node’scryptomodule. - Change the random index selection for
ADJECTIVESandNOUNSfromMath.floor(Math.random() * array.length)tocrypto.randomInt(array.length), which yields a uniform integer in[0, length). - In the
generateUniqueAgentNamefallback, replaceMath.floor(Math.random() * 1000)withcrypto.randomInt(1000). - No other files (
src/cli/index.ts,packages/bridge/src/spawner.ts) need code changes; the taintedness will now originate from a CSPRNG instead ofMath.random, resolving all alert variants along this path. - This does not alter the public API or general behavior (still picks a random adjective, noun, and numeric suffix; just does so securely).
Concrete changes:
- File:
packages/utils/src/name-generator.ts- Add
import crypto from 'node:crypto';at the top (keeping existing style). - Update line 32 to
const adjective = ADJECTIVES[crypto.randomInt(ADJECTIVES.length)]; - Update line 33 to
const noun = NOUNS[crypto.randomInt(NOUNS.length)]; - Update line 48 to
return \${generateAgentName()}${crypto.randomInt(1000)}`;`
- Add
No changes are required in packages/bridge/src/spawner.ts or src/cli/index.ts per the constraint to only touch shown snippets and because fixing the source of randomness is sufficient.
| @@ -3,6 +3,8 @@ | ||
| * Inspired by mcp_agent_mail's approach. | ||
| */ | ||
|
|
||
| import crypto from 'node:crypto'; | ||
|
|
||
| const ADJECTIVES = [ | ||
| 'Blue', 'Green', 'Red', 'Purple', 'Golden', 'Silver', 'Crystal', 'Amber', | ||
| 'Coral', 'Jade', 'Ruby', 'Sapphire', 'Emerald', 'Onyx', 'Pearl', 'Copper', | ||
| @@ -29,8 +31,8 @@ | ||
| * Generate a random agent name (AdjectiveNoun format). | ||
| */ | ||
| export function generateAgentName(): string { | ||
| const adjective = ADJECTIVES[Math.floor(Math.random() * ADJECTIVES.length)]; | ||
| const noun = NOUNS[Math.floor(Math.random() * NOUNS.length)]; | ||
| const adjective = ADJECTIVES[crypto.randomInt(ADJECTIVES.length)]; | ||
| const noun = NOUNS[crypto.randomInt(NOUNS.length)]; | ||
| return `${adjective}${noun}`; | ||
| } | ||
|
|
||
| @@ -45,7 +47,7 @@ | ||
| } | ||
| } | ||
| // Fallback: append random suffix | ||
| return `${generateAgentName()}${Math.floor(Math.random() * 1000)}`; | ||
| return `${generateAgentName()}${crypto.randomInt(1000)}`; | ||
| } | ||
|
|
||
| /** |
There was a problem hiding this comment.
Devin Review found 1 potential issue.
🔴 1 issue in files not directly in the diff
🔴 BaseWrapper permanently dedupes relay commands even when send fails (channel path never checks success) (packages/wrapper/src/base-wrapper.ts:453-492)
BaseWrapper.sendRelayCommand() adds the message hash to sentMessageHashes before attempting to send, and the channel path doesn’t check the return value of sendChannelMessage().
Actual: if the client is READY but the underlying send fails (e.g., transient socket issue), the hash remains in sentMessageHashes, so subsequent retries of the same command are treated as duplicates and are skipped.
Expected: only mark as “sent” after the send succeeds, or remove the hash on failure.
Impact: message loss (especially for channel replies, since that branch never checks success), which is user-visible and can break workflows.
Click to expand
In BaseWrapper.sendRelayCommand():
-
Hash is recorded immediately:
const hash = `${cmd.to}:${cmd.body.substring(0, 100)}`; ... this.sentMessageHashes.add(hash);
packages/wrapper/src/base-wrapper.ts:453-465 -
Channel messages are sent without checking the return value:
this.client.sendChannelMessage(cmd.to, cmd.body, { ... });
packages/wrapper/src/base-wrapper.ts:484-492
So a failed send still “poisons” dedupe and prevents future sends.
Recommendation: Only add hash to sentMessageHashes after a successful send. For the channel branch, capture and validate the boolean return of sendChannelMessage() (and similarly handle the send result for sendAndWait()/sendMessage()), removing the hash on failure.
View issue and 9 additional flags in Devin Review.
There was a problem hiding this comment.
Devin Review found 2 new potential issues.
🔴 1 issue in files not directly in the diff
🔴 BaseWrapper permanently dedupes relay commands even when send fails (channel path never checks success) (packages/wrapper/src/base-wrapper.ts:453-492)
BaseWrapper.sendRelayCommand() adds the message hash to sentMessageHashes before attempting to send, and the channel path doesn’t check the return value of sendChannelMessage().
Actual: if the client is READY but the underlying send fails (e.g., transient socket issue), the hash remains in sentMessageHashes, so subsequent retries of the same command are treated as duplicates and are skipped.
Expected: only mark as “sent” after the send succeeds, or remove the hash on failure.
Impact: message loss (especially for channel replies, since that branch never checks success), which is user-visible and can break workflows.
Click to expand
In BaseWrapper.sendRelayCommand():
-
Hash is recorded immediately:
const hash = `${cmd.to}:${cmd.body.substring(0, 100)}`; ... this.sentMessageHashes.add(hash);
packages/wrapper/src/base-wrapper.ts:453-465 -
Channel messages are sent without checking the return value:
this.client.sendChannelMessage(cmd.to, cmd.body, { ... });
packages/wrapper/src/base-wrapper.ts:484-492
So a failed send still “poisons” dedupe and prevents future sends.
Recommendation: Only add hash to sentMessageHashes after a successful send. For the channel branch, capture and validate the boolean return of sendChannelMessage() (and similarly handle the send result for sendAndWait()/sendMessage()), removing the hash on failure.
View issues and 15 additional flags in Devin Review.
| this.parser.reset(); | ||
| this.socket = undefined; | ||
| this.rejectPendingSyncAcks(new Error('Disconnected while awaiting ACK')); | ||
| this.rejectPendingSpawns(new Error('Disconnected while awaiting spawn result')); | ||
| this.rejectPendingReleases(new Error('Disconnected while awaiting release result')); | ||
| this.rejectPendingQueries(new Error('Disconnected while awaiting query response')); |
There was a problem hiding this comment.
🟡 SDK query/spawn/release promises are not rejected when daemon responds with ERROR (request ID not correlated)
When the daemon rejects a request (e.g., spawnManager disabled, invalid query, etc.), it sends an ERROR envelope using a new id (not the request’s id) (packages/daemon/src/server.ts:1560-1576). The SDK’s RelayClient only resolves pending operations when it receives a typed *_RESPONSE / *_RESULT with an ID it recognizes (handleQueryResponse uses envelope.id; spawn/release use payload.replyTo) (packages/sdk/src/client.ts:1196-1228).
Actual: the SDK logs the error but leaves pendingQueries / pendingSpawns / pendingReleases unresolved until the timeout fires, surfacing a misleading timeout error.
Expected: the SDK should reject the corresponding pending promise immediately when the daemon indicates an error for that operation.
Click to expand
Example trigger
- Daemon configured without
spawnManager. - Client calls
await client.spawn(...). - Daemon executes
sendErrorEnvelope(connection, 'SpawnManager not enabled...')and returns (packages/daemon/src/server.ts:1259-1264). - SDK receives
ERRORand only logs it inhandleErrorFrame, without rejecting the pending spawn (packages/sdk/src/client.ts:1240-1256). client.spawn()rejects after 30s withSpawn timeout...(misleading).
Root cause
sendErrorEnvelopegenerates a fresh envelope ID (generateId()) and does not includereplyTo/correlation fields (packages/daemon/src/server.ts:1560-1576).RelayClientdoes not attempt to correlateERRORto pending operations (packages/sdk/src/client.ts:1240-1256).
(Refers to lines 859-1265)
Recommendation: Implement error correlation and fail-fast behavior:
- Preferably update daemon to send
ERRORwithid: originalRequest.id(or addreplyTo/correlationIdinErrorPayload). - In the SDK, on receiving
ERROR, reject the matching pending query/spawn/release when correlation is available; otherwise consider rejecting all pending queries (or expose the daemon error to callers).
Was this helpful? React with 👍 or 👎 to provide feedback.
Uh oh!
There was an error while loading. Please reload this page.