From f61fdaa59327f88a2802ea5ce9b55c1ae84f1e78 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 2 Aug 2026 16:15:27 +0000 Subject: [PATCH 1/6] docs: add security policy with private disclosure channel Relay publishes a CLI, a Rust broker, and 17 npm packages that broker credentials and tool invocations between agents, but the repo documented no way to report a vulnerability privately. The only channels on offer were public GitHub issues and Discord, both of which disclose a defect to every workspace running the affected version before a fix exists. Add SECURITY.md pointing at GitHub Security Advisories, with supported versions, scope, and reporting guidance. Scope and out-of-scope reflect what Relay actually owns: the message protocol's trust boundaries and credential handling are in, third-party harness defects are out. The report checklist asks for credentials to be redacted, since workspace keys, broker keys, and observer tokens routinely appear in Relay logs and terminal transcripts. Link it from the README references list. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Vfi46d3iWyTEm3NAWi31vy --- README.md | 1 + SECURITY.md | 103 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 104 insertions(+) create mode 100644 SECURITY.md diff --git a/README.md b/README.md index 36e1ec044..a24648c3a 100644 --- a/README.md +++ b/README.md @@ -348,6 +348,7 @@ References: - [TypeScript SDK README](./packages/sdk/README.md) - [CHANGELOG.md](./CHANGELOG.md) +- [SECURITY.md](./SECURITY.md) — reporting a vulnerability - [GitHub Issues](https://github.com/AgentWorkforce/relay/issues) ## License diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 000000000..00fc7f021 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,103 @@ +# Security Policy + +Agent Relay moves messages, credentials, and tool invocations between +autonomous agents. A defect here can expose a workspace to agents — or people — +that should never have reached it, so we treat security reports as a priority +over feature work. + +## Reporting a vulnerability + +**Report privately through GitHub Security Advisories:** + +> https://github.com/AgentWorkforce/relay/security/advisories/new + +That form is visible only to the maintainers until an advisory is published, +and it lets us open a private fix branch and credit you on release. + +**Please do not** report vulnerabilities through public GitHub issues, pull +requests, or the Discord server. Those are public the moment you post, which +puts every workspace running the affected version at risk before a fix exists. + +If GitHub Security Advisories is unavailable to you, open a public issue that +says only that you have a security report and asks for a private channel — +no details, no reproduction steps — and a maintainer will follow up. + +### What to include + +The more of this you can provide, the faster we can confirm and fix: + +- The affected component and version (`agent-relay --version`, the npm package + and version, or the broker build). +- The type of issue (authentication bypass, credential disclosure, injection, + privilege escalation across agents, denial of service, and so on). +- Step-by-step reproduction, ideally as a minimal workspace or script. +- The impact: what an attacker gains, and what access they need to start. +- Any proof-of-concept code, logs, or transcripts. + +**Redact credentials before you send them.** Workspace keys, broker keys, +observer tokens, API keys, and OAuth tokens frequently appear in Relay logs and +terminal transcripts. Replace them with placeholders. If a report requires a +live credential to demonstrate, say so and we will arrange a channel for it +rather than having it sit in an advisory thread. + +### What to expect + +- **Acknowledgement:** we aim to confirm receipt within 3 business days. +- **Assessment:** we aim to confirm or dispute the report, with a severity + assessment, within 10 business days. +- **Updates:** we will keep you posted on remediation progress, and will tell + you if a fix is going to take longer than expected. +- **Disclosure:** we publish an advisory once a fixed version is available. We + will credit you by name or handle unless you prefer otherwise. + +We ask that you give us a reasonable opportunity to ship a fix before +disclosing publicly. We do not run a paid bug bounty. + +## Supported versions + +Security fixes land on the latest released minor of the current major. We do +not backport to earlier majors. + +| Component | Version | Supported | +| ----------------------------------------------- | ------- | --------- | +| `agent-relay` CLI and `@agent-relay/*` packages | 11.x | Yes | +| `agent-relay` CLI and `@agent-relay/*` packages | < 11.0 | No | +| `agent-relay-broker` crate | 3.x | Yes | +| `agent-relay-broker` crate | < 3.0 | No | + +Upgrade before reporting against an older release — the issue may already be +fixed. + +## Scope + +**In scope:** + +- The `agent-relay` CLI and the published `@agent-relay/*` npm packages. +- The `agent-relay-broker` Rust crate and its prebuilt platform binaries. +- The message protocol itself: authentication, authorization between agents, + workspace and channel isolation, delivery integrity, action routing. +- Credential handling: how keys and tokens are stored on disk, passed to + spawned harnesses, and surfaced in output, logs, and error text. +- Agent-to-agent trust boundaries, including prompt or tool injection that + crosses from message content into another agent's privileged actions. + +**Out of scope:** + +- Vulnerabilities in third-party agent harnesses (Claude Code, Codex, Gemini + CLI, and others). Report those to their maintainers; tell us if Relay's + integration makes an existing harness issue materially worse. +- Findings that require an attacker to already have local filesystem or shell + access to the machine running the agent. +- Dependency advisories with no demonstrated exploit path through Relay. We + track these through automated scanning; a report is welcome if you can show + the path. +- Missing hardening headers, TLS configuration, or similar findings on the + marketing site, absent a concrete impact. +- Reports generated by automated scanners with no validation or reproduction. + +## Security tooling + +Every push and pull request runs CodeQL, `npm audit`, dependency review, +license compliance, and gitleaks secret scanning +(`.github/workflows/security.yml`), plus a weekly scheduled run. These catch +regressions; they are not a substitute for the reports we get from you. From ce6b49b6e3fe39bf4c91313248bdc0f80f53150e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 2 Aug 2026 16:24:18 +0000 Subject: [PATCH 2/6] docs(mcp): state what every MCP tool returns MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The gradable.dev report on the exposed MCP surface found 28 defects, almost all of them no_return_description: a tool description said what the call did but never what came back. An agent reading the tool list could not tell whether post_message hands back a message ID it can reply to, whether a successful spawn means the worker is running or merely queued, or whether an empty list_agents array means "no agents" or "the filter excluded them". Add a return clause to all 30 tool descriptions that lacked one, each grounded in what the handler actually returns — the confirmation string for textContent tools, the named array for list tools, the record shape for jsonContent tools. Where the return is asynchronous (spawn, add_agent, remove_agent, invoke_action) the description now says that acceptance is not completion. register_agent also carried a name_restates_behavior defect: "Register an agent identity..." restated the tool name without adding anything. It now leads with why an agent calls it — claiming an identity is a prerequisite for every messaging tool — and notes that the registered name can differ from the requested one when the session rebinds. invoke_action already documented its return and is unchanged. Tools registered dynamically from action descriptors carry descriptions supplied by whoever registered the action, so they are out of scope. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Vfi46d3iWyTEm3NAWi31vy --- CHANGELOG.md | 6 +- packages/cli/src/cli/agent-relay-mcp.ts | 36 +++++++--- packages/cli/src/cli/mcp/action-tools.ts | 4 +- packages/cli/src/cli/mcp/messaging-tools.ts | 80 +++++++++++++++------ 4 files changed, 95 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1c225fb0..c5c3a8d68 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,11 @@ All notable changes to Agent Relay will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [Unreleased - Patch] + +### Changed + +- Every `agent-relay` MCP tool description now states what the call returns, so an agent can tell from the tool list whether `post_message` hands back a message ID, whether `spawn` means the worker is running, and what an empty `list_agents` result implies. `register_agent` also explains that the registered name can differ from the requested one. ## [11.4.0] - 2026-08-02 diff --git a/packages/cli/src/cli/agent-relay-mcp.ts b/packages/cli/src/cli/agent-relay-mcp.ts index 0125de853..9b8d4f597 100644 --- a/packages/cli/src/cli/agent-relay-mcp.ts +++ b/packages/cli/src/cli/agent-relay-mcp.ts @@ -231,7 +231,8 @@ function registerAgentResultTool(server: McpServer, config: AgentResultCallbackC { title: 'Submit Result', description: - 'Submit the structured result for this spawned Agent Relay task. Call this when the requested work is complete and the result object is ready.' + + 'Submit the structured result for this spawned Agent Relay task. Call this when the requested work is complete and the result object is ready. ' + + 'Returns the acknowledgement payload from the spawning caller. Throws if the caller rejects the submission or the request times out, in which case the result was not recorded and the call can be retried.' + schemaText, inputSchema: { data: z.unknown().describe('The JSON result payload requested by the spawning SDK caller.'), @@ -406,7 +407,9 @@ function registerAgentRelayTools( 'create_workspace', { title: 'Create Workspace', - description: 'Explicitly start a new Agent Relay workspace session and persist it for this project.', + description: + 'Explicitly start a new Agent Relay workspace session and persist it for this project. ' + + 'Returns the new workspace key and its resolved name. A `warning` field is present only when the workspace was created but its session could not be saved to disk, meaning the key must be kept and re-supplied to reconnect.', inputSchema: { name: z.string().describe('Human-readable workspace name'), }, @@ -454,7 +457,9 @@ function registerAgentRelayTools( 'set_workspace_key', { title: 'Set Workspace Key', - description: 'Join this MCP session to an existing Agent Relay workspace using a shared workspace key.', + description: + 'Join this MCP session to an existing Agent Relay workspace using a shared workspace key. ' + + 'Returns a confirmation message stating whether the key was persisted for this project, and whether "register_agent" must be called to claim an identity in the newly joined workspace.', inputSchema: { workspace_key: z.string().optional().describe('Workspace key starting with "rk_live_"'), api_key: z.string().optional().describe('Deprecated alias for workspace_key'), @@ -513,7 +518,11 @@ function registerAgentRelayTools( 'register_agent', { title: 'Register Agent', - description: 'Register an agent identity in the current workspace and obtain an agent token.', + description: + 'Claim a named identity in the current workspace so this session can post messages, read channels, and be addressed by other agents. ' + + 'Required before any messaging tool will work. ' + + 'Returns the agent token and the registered name, which can differ from the requested `name` when that name is already taken and the session rebinds to an available one. ' + + 'The token is stored in this session, so later tool calls do not need to pass it.', inputSchema: { name: z.string().describe('Unique agent name within the workspace'), type: z.enum(['agent', 'human']).optional().describe('Whether this identity is an AI agent or human'), @@ -561,7 +570,9 @@ function registerAgentRelayTools( 'list_agents', { title: 'List Agents', - description: 'List agents registered in the current workspace.', + description: + 'List agents registered in the current workspace. ' + + 'Returns an `agents` array of registered identities, narrowed to only online or only offline agents when `status` is supplied. An empty array means the workspace has no agent matching the filter.', inputSchema: { status: z.enum(['online', 'offline']).optional().describe('Optional status filter'), }, @@ -581,7 +592,9 @@ function registerAgentRelayTools( 'query_nodes', { title: 'Query Fleet Nodes', - description: 'Query registered fleet nodes by capability or name.', + description: + 'Query registered fleet nodes by capability or name. ' + + 'Returns a `nodes` array of the fleet nodes matching every supplied filter; an empty array means no node matched. Use it to find a node name to pass as `target_node` when spawning.', inputSchema: { capability: z.string().optional().describe('Optional capability name filter'), name: z.string().optional().describe('Optional node name filter'), @@ -611,7 +624,8 @@ function registerAgentRelayTools( 'Examples: "spawn a codex agent" → cli:"codex"; ' + '"spawn an opus claude agent" → cli:"claude", model:"claude-opus-4-8"; ' + '"spawn a sonnet claude agent" → cli:"claude", model:"claude-sonnet-4-6". ' + - 'Do NOT use the built-in Agent/Task tool for relay workers.', + 'Do NOT use the built-in Agent/Task tool for relay workers. ' + + 'Returns the spawn record for the new worker, including the name it registered under. The worker boots asynchronously, so a successful return means the spawn was accepted, not that the worker is ready — watch for its messages or poll "list_agents" to confirm it came online.', inputSchema: { name: z.string().describe('Worker agent name'), cli: z @@ -669,7 +683,9 @@ function registerAgentRelayTools( 'spawn', { title: 'Spawn Agent', - description: 'Invoke the fleet spawn action. Optionally target a specific node.', + description: + 'Invoke the fleet spawn action, optionally targeting a specific node. ' + + 'Returns an `invocation` record acknowledging the request. The action runs asynchronously, so this confirms the spawn was queued, not that the worker is running.', inputSchema: { name: z.string().describe('Agent name'), cli: z @@ -713,7 +729,9 @@ function registerAgentRelayTools( 'remove_agent', { title: 'Remove Agent', - description: 'Release a worker agent from active duty.', + description: + 'Release a worker agent from active duty, optionally deleting it outright. ' + + 'Returns an `invocation` record acknowledging the request, which is processed asynchronously. Releasing keeps the agent registered and re-spawnable; passing `delete_agent` removes the identity permanently.', inputSchema: { name: z.string().describe('Agent name'), reason: z.string().optional().describe('Removal reason'), diff --git a/packages/cli/src/cli/mcp/action-tools.ts b/packages/cli/src/cli/mcp/action-tools.ts index 78d017507..500f8306e 100644 --- a/packages/cli/src/cli/mcp/action-tools.ts +++ b/packages/cli/src/cli/mcp/action-tools.ts @@ -88,7 +88,9 @@ export function registerAgentRelayActionTools( 'list_actions', { title: 'List Actions', - description: 'List Agent Relay actions available to this agent.', + description: + 'List Agent Relay actions available to this agent. ' + + 'Returns an `actions` array of action descriptors, each with its name, description, and input schema. Use it to discover what can be passed to "invoke_action".', inputSchema: {}, outputSchema: jsonResult, annotations: { diff --git a/packages/cli/src/cli/mcp/messaging-tools.ts b/packages/cli/src/cli/mcp/messaging-tools.ts index e6b9772c8..09591d3c7 100644 --- a/packages/cli/src/cli/mcp/messaging-tools.ts +++ b/packages/cli/src/cli/mcp/messaging-tools.ts @@ -34,7 +34,9 @@ export function registerMessagingTools( 'create_channel', { title: 'Create Channel', - description: 'Create a new workspace channel.', + description: + 'Create a new workspace channel. ' + + 'Returns the created channel record, including the name other agents use to join or post to it.', inputSchema: { name: z.string().describe('Unique channel name'), topic: z.string().optional().describe('Optional channel topic'), @@ -55,7 +57,9 @@ export function registerMessagingTools( 'list_channels', { title: 'List Channels', - description: 'List channels available in the workspace.', + description: + 'List channels available in the workspace. ' + + 'Returns a `channels` array. Archived channels are excluded unless `include_archived` is set.', inputSchema: { include_archived: z.boolean().optional().describe('Include archived channels'), ...identityOverrideInputShape, @@ -77,7 +81,9 @@ export function registerMessagingTools( 'join_channel', { title: 'Join Channel', - description: 'Join an existing channel.', + description: + 'Join an existing channel so its messages reach this agent. ' + + 'Returns a confirmation message naming the channel joined.', inputSchema: { channel: z.string().describe('Channel name'), ...identityOverrideInputShape, @@ -95,7 +101,9 @@ export function registerMessagingTools( 'leave_channel', { title: 'Leave Channel', - description: 'Leave a channel.', + description: + "Stop receiving a channel's messages without archiving it for anyone else. " + + 'Returns a confirmation message naming the channel left.', inputSchema: { channel: z.string().describe('Channel name'), ...identityOverrideInputShape, @@ -113,7 +121,9 @@ export function registerMessagingTools( 'invite_to_channel', { title: 'Invite to Channel', - description: 'Invite another agent to a channel.', + description: + 'Invite another agent to a channel. ' + + 'Returns a confirmation message naming the invited agent and the channel.', inputSchema: { channel: z.string().describe('Channel name'), agent: z.string().describe('Agent name to invite'), @@ -132,7 +142,9 @@ export function registerMessagingTools( 'set_channel_topic', { title: 'Set Channel Topic', - description: 'Update a channel topic.', + description: + "Replace a channel's topic with new text. " + + 'Returns the updated channel record carrying the new topic.', inputSchema: { channel: z.string().describe('Channel name'), topic: z.string().describe('New topic'), @@ -148,7 +160,9 @@ export function registerMessagingTools( 'archive_channel', { title: 'Archive Channel', - description: 'Archive a channel.', + description: + 'Archive a channel for the whole workspace, closing it to new messages. ' + + 'Returns a confirmation message naming the archived channel. Archived channels stay readable and are surfaced by "list_channels" only when `include_archived` is set.', inputSchema: { channel: z.string().describe('Channel name'), ...identityOverrideInputShape, @@ -166,7 +180,9 @@ export function registerMessagingTools( 'post_message', { title: 'Post Message', - description: 'Post a new message to a channel as the current agent.', + description: + 'Post a new message to a channel as the current agent. ' + + 'Returns the created message record, including the message ID needed to reply to it with "reply_to_thread" or react to it with "add_reaction".', inputSchema: { channel: z.string().describe('Channel name'), text: z.string().describe('Message text'), @@ -190,7 +206,9 @@ export function registerMessagingTools( 'list_messages', { title: 'Get Messages', - description: 'Retrieve message history from a channel.', + description: + 'Retrieve message history from a channel. ' + + 'Returns a `messages` array. Pass a message ID from a previous call as `before` or `after` to page through history beyond a single `limit`.', inputSchema: { channel: z.string().describe('Channel name'), limit: z.number().optional().describe('Maximum messages to return'), @@ -213,7 +231,9 @@ export function registerMessagingTools( 'reply_to_thread', { title: 'Reply to Thread', - description: 'Reply to an existing message thread.', + description: + 'Post a threaded reply under an existing message instead of to the channel at large. ' + + 'Returns the created reply record with its own message ID.', inputSchema: { message_id: z.string().describe('Parent message ID'), text: z.string().describe('Reply text'), @@ -234,7 +254,9 @@ export function registerMessagingTools( 'get_message_thread', { title: 'Get Thread', - description: 'Retrieve a message thread.', + description: + 'Retrieve a message thread. ' + + 'Returns the parent message together with its replies, capped by `limit` when supplied.', inputSchema: { message_id: z.string().describe('Parent message ID'), limit: z.number().optional().describe('Maximum replies to return'), @@ -251,7 +273,9 @@ export function registerMessagingTools( 'send_dm', { title: 'Send Direct Message', - description: 'Send a private direct message to another agent.', + description: + 'Send a private direct message visible only to the recipient and this agent. ' + + 'Returns the created message record, including its message ID.', inputSchema: { to: z.string().describe('Recipient agent name'), text: z.string().describe('DM text'), @@ -275,7 +299,9 @@ export function registerMessagingTools( 'list_dms', { title: 'List DM Conversations', - description: 'List direct message conversations for the current agent.', + description: + 'List direct message conversations for the current agent. ' + + 'Returns a `conversations` array covering both one-to-one and group DMs, with the conversation IDs used to send follow-up messages.', inputSchema: { ...identityOverrideInputShape, }, @@ -291,7 +317,9 @@ export function registerMessagingTools( 'send_group_dm', { title: 'Send Group DM', - description: 'Create a group DM and send the first message.', + description: + 'Open a private group conversation with several agents and post its first message. ' + + 'Returns the created `conversation` and the `message` posted to it.', inputSchema: { participants: z.array(z.string()).describe('Participant agent names'), name: z.string().optional().describe('Optional group name'), @@ -318,7 +346,9 @@ export function registerMessagingTools( 'add_reaction', { title: 'Add Reaction', - description: 'Add an emoji reaction to a message.', + description: + 'Add an emoji reaction to a message. ' + + 'Accepts a raw emoji or a shortcode such as `:+1:` or `rocket`. Returns a confirmation message showing the emoji the shortcode resolved to.', inputSchema: { message_id: z.string().describe('Message ID'), emoji: z.string().describe('Emoji character or shortcode'), @@ -338,7 +368,9 @@ export function registerMessagingTools( 'remove_reaction', { title: 'Remove Reaction', - description: 'Remove an emoji reaction from a message.', + description: + "Remove one of this agent's emoji reactions from a message. " + + 'Accepts the same raw emoji or shortcode forms as "add_reaction". Returns a confirmation message showing the resolved emoji.', inputSchema: { message_id: z.string().describe('Message ID'), emoji: z.string().describe('Emoji character or shortcode'), @@ -358,7 +390,9 @@ export function registerMessagingTools( 'search_messages', { title: 'Search Messages', - description: 'Search messages across the workspace.', + description: + 'Search messages across every channel this agent can see. ' + + 'Returns a `results` array of matching messages, narrowed by the optional `channel` and `from` filters. An empty array means nothing matched.', inputSchema: { query: z.string().describe('Text search query'), channel: z.string().optional().describe('Optional channel filter'), @@ -379,7 +413,9 @@ export function registerMessagingTools( 'check_inbox', { title: 'Check Inbox', - description: 'Check unread messages, mentions, DMs, and reactions for the current agent.', + description: + 'Check unread messages, mentions, DMs, and reactions for the current agent. ' + + 'Returns the inbox payload grouping unread items by kind. Reading the inbox does not clear it — call "mark_message_read" to do that.', inputSchema: { limit: z.number().optional().describe('Maximum inbox items'), ...identityOverrideInputShape, @@ -395,7 +431,9 @@ export function registerMessagingTools( 'mark_message_read', { title: 'Mark as Read', - description: 'Mark a message as read for the current agent.', + description: + "Clear a message from this agent's unread inbox. " + + 'Returns a confirmation message naming the message ID marked read.', inputSchema: { message_id: z.string().describe('Message ID'), ...identityOverrideInputShape, @@ -413,7 +451,9 @@ export function registerMessagingTools( 'get_message_readers', { title: 'Get Readers', - description: 'List agents who have read a message.', + description: + 'Check which agents have read a message, to confirm delivery before acting on silence. ' + + 'Returns a `readers` array of the agents that have read it; an empty array means nobody has.', inputSchema: { message_id: z.string().describe('Message ID'), ...identityOverrideInputShape, From 8c9e563859e51b0a82902c2c028149ffd627edd2 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 2 Aug 2026 22:36:33 +0000 Subject: [PATCH 3/6] docs: correct overstated claims found in review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three tool descriptions asserted more than the code guarantees: submit_result said a timeout meant the result was not recorded and the call could be retried. A timeout only means the client aborted before reading a response; the server may already have recorded the result, so a retry can duplicate it. The description now says the outcome is unknown. list_dms said conversation IDs are used to send follow-up messages. No tool accepts a conversation ID — send_dm addresses a recipient by name. The IDs do address the relay://dm/{conversation_id} resource, so the description now points there and says how to actually reply. get_message_thread said results are capped by limit "when supplied", but the handler tests limit for truthiness, so limit: 0 is dropped and the full thread comes back. Documented the actual behavior rather than changing the handler, which would alter runtime behavior in a description-only change. SECURITY.md carried three inaccuracies of its own: - The supported-versions table marked all of 11.x and 3.x supported while the text said only the latest minor gets fixes. Split into "reports investigated" and "fix delivered in" so each column means one thing. - The local-access exclusion was broad enough to reject reports about world-readable key files, since exploiting those needs another local user. Narrowed to same-user access, with cross-user weaknesses named as in scope. - The tooling section claimed every push runs the full suite. In fact CodeQL, npm audit, and license compliance are gated on Node-toolchain changes, dependency review is PR-only, CodeQL covers JS/TS only (not the in-scope Rust broker), and several jobs are advisory. Replaced with the real coverage. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Vfi46d3iWyTEm3NAWi31vy --- SECURITY.md | 50 ++++++++++++++------- packages/cli/src/cli/agent-relay-mcp.ts | 2 +- packages/cli/src/cli/mcp/messaging-tools.ts | 4 +- 3 files changed, 36 insertions(+), 20 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 00fc7f021..a3ca6d2c2 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -55,17 +55,19 @@ disclosing publicly. We do not run a paid bug bounty. ## Supported versions -Security fixes land on the latest released minor of the current major. We do -not backport to earlier majors. - -| Component | Version | Supported | -| ----------------------------------------------- | ------- | --------- | -| `agent-relay` CLI and `@agent-relay/*` packages | 11.x | Yes | -| `agent-relay` CLI and `@agent-relay/*` packages | < 11.0 | No | -| `agent-relay-broker` crate | 3.x | Yes | -| `agent-relay-broker` crate | < 3.0 | No | - -Upgrade before reporting against an older release — the issue may already be +We investigate reports against any release on the lines below. Fixes ship only +at the head of that line: we do not patch earlier minors in place, and we do +not backport across majors. If you are running an older 11.x, upgrading to the +current 11.x is how you receive the fix. + +| Component | Reports investigated | Fix delivered in | +| ----------------------------------------------- | -------------------- | ---------------- | +| `agent-relay` CLI and `@agent-relay/*` packages | any 11.x | latest 11.x | +| `agent-relay-broker` crate | any 3.x | latest 3.x | + +Releases before 11.0 (CLI and packages) and before 3.0 (broker) are +unsupported — we will not investigate a report that reproduces only there. +Upgrade before reporting against an older release; the issue may already be fixed. ## Scope @@ -86,8 +88,10 @@ fixed. - Vulnerabilities in third-party agent harnesses (Claude Code, Codex, Gemini CLI, and others). Report those to their maintainers; tell us if Relay's integration makes an existing harness issue materially worse. -- Findings that require an attacker to already have local filesystem or shell - access to the machine running the agent. +- Findings that require an attacker to already hold the same local user account + as the agent process. Weaknesses that let a _different_ local user or process + reach agent state — group- or world-readable key files, permissive directory + modes, predictable paths in shared temp directories — are in scope. - Dependency advisories with no demonstrated exploit path through Relay. We track these through automated scanning; a report is welcome if you can show the path. @@ -97,7 +101,19 @@ fixed. ## Security tooling -Every push and pull request runs CodeQL, `npm audit`, dependency review, -license compliance, and gitleaks secret scanning -(`.github/workflows/security.yml`), plus a weekly scheduled run. These catch -regressions; they are not a substitute for the reports we get from you. +`.github/workflows/security.yml` runs on pushes to `main`, on pull requests +targeting `main`, and weekly. Coverage is not uniform, so it is worth being +precise about what actually runs: + +- **Gitleaks secret scanning** runs on every trigger. +- **CodeQL, `npm audit`, and license compliance** run only when a change + touches the Node toolchain, so docs-only and Swift-only changes skip them. +- **Dependency review** runs on pull requests only. +- **CodeQL analyzes JavaScript and TypeScript only.** The `agent-relay-broker` + Rust crate is in scope for this policy but is not covered by CodeQL. +- Several of these jobs are advisory rather than blocking, so a green run does + not by itself mean no findings. + +Automated scanning catches regressions in the paths it covers. It is not a +substitute for the reports we get from you, and the gaps above are exactly +where your reports matter most. diff --git a/packages/cli/src/cli/agent-relay-mcp.ts b/packages/cli/src/cli/agent-relay-mcp.ts index 9b8d4f597..10b5ff401 100644 --- a/packages/cli/src/cli/agent-relay-mcp.ts +++ b/packages/cli/src/cli/agent-relay-mcp.ts @@ -232,7 +232,7 @@ function registerAgentResultTool(server: McpServer, config: AgentResultCallbackC title: 'Submit Result', description: 'Submit the structured result for this spawned Agent Relay task. Call this when the requested work is complete and the result object is ready. ' + - 'Returns the acknowledgement payload from the spawning caller. Throws if the caller rejects the submission or the request times out, in which case the result was not recorded and the call can be retried.' + + 'Returns the acknowledgement payload from the spawning caller. Throws if the caller rejects the submission, in which case the result was not recorded. A timeout leaves the outcome unknown — the request may have been recorded before the client stopped waiting — so treat a retry as a possible duplicate rather than a safe repeat.' + schemaText, inputSchema: { data: z.unknown().describe('The JSON result payload requested by the spawning SDK caller.'), diff --git a/packages/cli/src/cli/mcp/messaging-tools.ts b/packages/cli/src/cli/mcp/messaging-tools.ts index 09591d3c7..5eec8a7f5 100644 --- a/packages/cli/src/cli/mcp/messaging-tools.ts +++ b/packages/cli/src/cli/mcp/messaging-tools.ts @@ -256,7 +256,7 @@ export function registerMessagingTools( title: 'Get Thread', description: 'Retrieve a message thread. ' + - 'Returns the parent message together with its replies, capped by `limit` when supplied.', + 'Returns the parent message together with its replies, capped by `limit` when a positive value is supplied. A `limit` of 0 is ignored and returns the full thread.', inputSchema: { message_id: z.string().describe('Parent message ID'), limit: z.number().optional().describe('Maximum replies to return'), @@ -301,7 +301,7 @@ export function registerMessagingTools( title: 'List DM Conversations', description: 'List direct message conversations for the current agent. ' + - 'Returns a `conversations` array covering both one-to-one and group DMs, with the conversation IDs used to send follow-up messages.', + 'Returns a `conversations` array covering both one-to-one and group DMs. Each conversation ID addresses the `relay://dm/{conversation_id}` resource, which holds that conversation\'s messages; no tool sends a follow-up message by conversation ID, so reply with "send_dm" to the recipient by name.', inputSchema: { ...identityOverrideInputShape, }, From 4242b9c3663882a05ffb44e597267aff7999b49c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 2 Aug 2026 22:54:10 +0000 Subject: [PATCH 4/6] chore(trail): record PR #1420 review-response trajectory Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01Vfi46d3iWyTEm3NAWi31vy --- .../2026-08/traj_9835z9cvpl9q/summary.md | 53 +++++++++++++++++++ .../traj_9835z9cvpl9q/trajectory.json | 23 +++++++- 2 files changed, 74 insertions(+), 2 deletions(-) create mode 100644 .agentworkforce/trajectories/completed/2026-08/traj_9835z9cvpl9q/summary.md rename .agentworkforce/trajectories/{active => completed/2026-08}/traj_9835z9cvpl9q/trajectory.json (78%) diff --git a/.agentworkforce/trajectories/completed/2026-08/traj_9835z9cvpl9q/summary.md b/.agentworkforce/trajectories/completed/2026-08/traj_9835z9cvpl9q/summary.md new file mode 100644 index 000000000..61e07043c --- /dev/null +++ b/.agentworkforce/trajectories/completed/2026-08/traj_9835z9cvpl9q/summary.md @@ -0,0 +1,53 @@ +# Trajectory: Implement escape-aware paced injection (issue #801) + +> **Status:** ✅ Completed +> **Task:** 801 +> **Confidence:** 85% +> **Started:** July 15, 2026 at 07:48 PM +> **Completed:** August 2, 2026 at 10:53 PM + +--- + +## Summary + +Fixed 6 review findings (3 MCP description overclaims, 3 SECURITY.md inaccuracies); declined CodeRabbit's CHANGELOG heading change as contrary to CLAUDE.md convention. All 40 CI checks green. + +**Approach:** Standard approach + +--- + +## Key Decisions + +### Diagnosed view detach behavior +- **Chose:** Diagnosed view detach behavior +- **Reasoning:** PTY-backed view consumes Ctrl-C as raw stdin and resolves its session, but closes the WebSocket gracefully; if that close handshake leaves a live handle, the process stays alive until a second Ctrl-C is handled by Node's default signal behavior. + +### Use forced WebSocket teardown for local view detach +- **Chose:** Use forced WebSocket teardown for local view detach +- **Reasoning:** A view session has no outbound state to preserve. Calling ws.terminate after requesting a normal close releases the live socket handle immediately, ensuring the first Ctrl-C exits the viewer without terminating the agent. + +### Preserve the post-SIGKILL reaping wait in the node provider integration test +- **Chose:** Preserve the post-SIGKILL reaping wait in the node provider integration test +- **Reasoning:** The test's 50ms sleep shim shortened both the graceful shutdown timeout and the post-kill wait. Restricting it to the 5s graceful timeout prevents stop() from resolving before the OS reaps the real child. + +### Replace fixed standalone smoke delay with bounded readiness polling +- **Chose:** Replace fixed standalone smoke delay with bounded readiness polling +- **Reasoning:** The scripts/ci-standalone-smoke.sh test was tearing down `agent-relay node up` after eight seconds even when a retried Relaycast handshake was still in progress. Polling for its `Broker started.` readiness line for up to thirty seconds prevents cleanup from creating a false startup failure while retaining deterministic timeout diagnostics. + +### Documented get_message_thread limit:0 behavior instead of changing the handler +- **Chose:** Documented get_message_thread limit:0 behavior instead of changing the handler +- **Reasoning:** Both suggested fixes alter runtime behavior; limit:0 server semantics unverifiable from the client in a description-only PR + +--- + +## Chapters + +### 1. Work +*Agent: default* + +- Paced injection in drainer thread: Paced injection in drainer thread +- Diagnosed view detach behavior: Diagnosed view detach behavior +- Use forced WebSocket teardown for local view detach: Use forced WebSocket teardown for local view detach +- Preserve the post-SIGKILL reaping wait in the node provider integration test: Preserve the post-SIGKILL reaping wait in the node provider integration test +- Replace fixed standalone smoke delay with bounded readiness polling: Replace fixed standalone smoke delay with bounded readiness polling +- Documented get_message_thread limit:0 behavior instead of changing the handler: Documented get_message_thread limit:0 behavior instead of changing the handler diff --git a/.agentworkforce/trajectories/active/traj_9835z9cvpl9q/trajectory.json b/.agentworkforce/trajectories/completed/2026-08/traj_9835z9cvpl9q/trajectory.json similarity index 78% rename from .agentworkforce/trajectories/active/traj_9835z9cvpl9q/trajectory.json rename to .agentworkforce/trajectories/completed/2026-08/traj_9835z9cvpl9q/trajectory.json index bb48affe1..af4bc3455 100644 --- a/.agentworkforce/trajectories/active/traj_9835z9cvpl9q/trajectory.json +++ b/.agentworkforce/trajectories/completed/2026-08/traj_9835z9cvpl9q/trajectory.json @@ -8,8 +8,9 @@ "id": "801" } }, - "status": "active", + "status": "completed", "startedAt": "2026-07-15T19:48:09.268Z", + "completedAt": "2026-08-02T22:53:57.846Z", "agents": [ { "name": "default", @@ -23,6 +24,7 @@ "title": "Work", "agentName": "default", "startedAt": "2026-07-15T19:56:40.369Z", + "endedAt": "2026-08-02T22:53:57.846Z", "events": [ { "ts": 1784145400371, @@ -83,10 +85,27 @@ "reasoning": "The scripts/ci-standalone-smoke.sh test was tearing down `agent-relay node up` after eight seconds even when a retried Relaycast handshake was still in progress. Polling for its `Broker started.` readiness line for up to thirty seconds prevents cleanup from creating a false startup failure while retaining deterministic timeout diagnostics." }, "significance": "high" + }, + { + "ts": 1785711236093, + "type": "decision", + "content": "Documented get_message_thread limit:0 behavior instead of changing the handler: Documented get_message_thread limit:0 behavior instead of changing the handler", + "raw": { + "question": "Documented get_message_thread limit:0 behavior instead of changing the handler", + "chosen": "Documented get_message_thread limit:0 behavior instead of changing the handler", + "alternatives": [], + "reasoning": "Both suggested fixes alter runtime behavior; limit:0 server semantics unverifiable from the client in a description-only PR" + }, + "significance": "high" } ] } ], + "retrospective": { + "summary": "Fixed 6 review findings (3 MCP description overclaims, 3 SECURITY.md inaccuracies); declined CodeRabbit's CHANGELOG heading change as contrary to CLAUDE.md convention. All 40 CI checks green.", + "approach": "Standard approach", + "confidence": 0.85 + }, "commits": [], "filesChanged": [], "projectId": "AgentWorkforce/relay", @@ -95,4 +114,4 @@ "startRef": "765e42535063b9d61262a537b1fd364d3e965000", "endRef": "765e42535063b9d61262a537b1fd364d3e965000" } -} +} \ No newline at end of file From c09832829e012863d0b499f6b34de4e1c6b8d517 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 2 Aug 2026 22:55:18 +0000 Subject: [PATCH 5/6] style: auto-format with Prettier --- .../completed/2026-08/traj_9835z9cvpl9q/summary.md | 8 +++++++- .../completed/2026-08/traj_9835z9cvpl9q/trajectory.json | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/.agentworkforce/trajectories/completed/2026-08/traj_9835z9cvpl9q/summary.md b/.agentworkforce/trajectories/completed/2026-08/traj_9835z9cvpl9q/summary.md index 61e07043c..7e4912185 100644 --- a/.agentworkforce/trajectories/completed/2026-08/traj_9835z9cvpl9q/summary.md +++ b/.agentworkforce/trajectories/completed/2026-08/traj_9835z9cvpl9q/summary.md @@ -19,22 +19,27 @@ Fixed 6 review findings (3 MCP description overclaims, 3 SECURITY.md inaccuracie ## Key Decisions ### Diagnosed view detach behavior + - **Chose:** Diagnosed view detach behavior - **Reasoning:** PTY-backed view consumes Ctrl-C as raw stdin and resolves its session, but closes the WebSocket gracefully; if that close handshake leaves a live handle, the process stays alive until a second Ctrl-C is handled by Node's default signal behavior. ### Use forced WebSocket teardown for local view detach + - **Chose:** Use forced WebSocket teardown for local view detach - **Reasoning:** A view session has no outbound state to preserve. Calling ws.terminate after requesting a normal close releases the live socket handle immediately, ensuring the first Ctrl-C exits the viewer without terminating the agent. ### Preserve the post-SIGKILL reaping wait in the node provider integration test + - **Chose:** Preserve the post-SIGKILL reaping wait in the node provider integration test - **Reasoning:** The test's 50ms sleep shim shortened both the graceful shutdown timeout and the post-kill wait. Restricting it to the 5s graceful timeout prevents stop() from resolving before the OS reaps the real child. ### Replace fixed standalone smoke delay with bounded readiness polling + - **Chose:** Replace fixed standalone smoke delay with bounded readiness polling - **Reasoning:** The scripts/ci-standalone-smoke.sh test was tearing down `agent-relay node up` after eight seconds even when a retried Relaycast handshake was still in progress. Polling for its `Broker started.` readiness line for up to thirty seconds prevents cleanup from creating a false startup failure while retaining deterministic timeout diagnostics. ### Documented get_message_thread limit:0 behavior instead of changing the handler + - **Chose:** Documented get_message_thread limit:0 behavior instead of changing the handler - **Reasoning:** Both suggested fixes alter runtime behavior; limit:0 server semantics unverifiable from the client in a description-only PR @@ -43,7 +48,8 @@ Fixed 6 review findings (3 MCP description overclaims, 3 SECURITY.md inaccuracie ## Chapters ### 1. Work -*Agent: default* + +_Agent: default_ - Paced injection in drainer thread: Paced injection in drainer thread - Diagnosed view detach behavior: Diagnosed view detach behavior diff --git a/.agentworkforce/trajectories/completed/2026-08/traj_9835z9cvpl9q/trajectory.json b/.agentworkforce/trajectories/completed/2026-08/traj_9835z9cvpl9q/trajectory.json index af4bc3455..9cfcc6a96 100644 --- a/.agentworkforce/trajectories/completed/2026-08/traj_9835z9cvpl9q/trajectory.json +++ b/.agentworkforce/trajectories/completed/2026-08/traj_9835z9cvpl9q/trajectory.json @@ -114,4 +114,4 @@ "startRef": "765e42535063b9d61262a537b1fd364d3e965000", "endRef": "765e42535063b9d61262a537b1fd364d3e965000" } -} \ No newline at end of file +} From 1d9fe17edeed2d0efcd5a567c77700ec6433d85b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 3 Aug 2026 13:50:47 +0000 Subject: [PATCH 6/6] Revert "chore(trail): record PR #1420 review-response trajectory" This reverts commit 4242b9c3663882a05ffb44e597267aff7999b49c. --- .../traj_9835z9cvpl9q/trajectory.json | 23 +------- .../2026-08/traj_9835z9cvpl9q/summary.md | 53 ------------------- 2 files changed, 2 insertions(+), 74 deletions(-) rename .agentworkforce/trajectories/{completed/2026-08 => active}/traj_9835z9cvpl9q/trajectory.json (78%) delete mode 100644 .agentworkforce/trajectories/completed/2026-08/traj_9835z9cvpl9q/summary.md diff --git a/.agentworkforce/trajectories/completed/2026-08/traj_9835z9cvpl9q/trajectory.json b/.agentworkforce/trajectories/active/traj_9835z9cvpl9q/trajectory.json similarity index 78% rename from .agentworkforce/trajectories/completed/2026-08/traj_9835z9cvpl9q/trajectory.json rename to .agentworkforce/trajectories/active/traj_9835z9cvpl9q/trajectory.json index af4bc3455..bb48affe1 100644 --- a/.agentworkforce/trajectories/completed/2026-08/traj_9835z9cvpl9q/trajectory.json +++ b/.agentworkforce/trajectories/active/traj_9835z9cvpl9q/trajectory.json @@ -8,9 +8,8 @@ "id": "801" } }, - "status": "completed", + "status": "active", "startedAt": "2026-07-15T19:48:09.268Z", - "completedAt": "2026-08-02T22:53:57.846Z", "agents": [ { "name": "default", @@ -24,7 +23,6 @@ "title": "Work", "agentName": "default", "startedAt": "2026-07-15T19:56:40.369Z", - "endedAt": "2026-08-02T22:53:57.846Z", "events": [ { "ts": 1784145400371, @@ -85,27 +83,10 @@ "reasoning": "The scripts/ci-standalone-smoke.sh test was tearing down `agent-relay node up` after eight seconds even when a retried Relaycast handshake was still in progress. Polling for its `Broker started.` readiness line for up to thirty seconds prevents cleanup from creating a false startup failure while retaining deterministic timeout diagnostics." }, "significance": "high" - }, - { - "ts": 1785711236093, - "type": "decision", - "content": "Documented get_message_thread limit:0 behavior instead of changing the handler: Documented get_message_thread limit:0 behavior instead of changing the handler", - "raw": { - "question": "Documented get_message_thread limit:0 behavior instead of changing the handler", - "chosen": "Documented get_message_thread limit:0 behavior instead of changing the handler", - "alternatives": [], - "reasoning": "Both suggested fixes alter runtime behavior; limit:0 server semantics unverifiable from the client in a description-only PR" - }, - "significance": "high" } ] } ], - "retrospective": { - "summary": "Fixed 6 review findings (3 MCP description overclaims, 3 SECURITY.md inaccuracies); declined CodeRabbit's CHANGELOG heading change as contrary to CLAUDE.md convention. All 40 CI checks green.", - "approach": "Standard approach", - "confidence": 0.85 - }, "commits": [], "filesChanged": [], "projectId": "AgentWorkforce/relay", @@ -114,4 +95,4 @@ "startRef": "765e42535063b9d61262a537b1fd364d3e965000", "endRef": "765e42535063b9d61262a537b1fd364d3e965000" } -} \ No newline at end of file +} diff --git a/.agentworkforce/trajectories/completed/2026-08/traj_9835z9cvpl9q/summary.md b/.agentworkforce/trajectories/completed/2026-08/traj_9835z9cvpl9q/summary.md deleted file mode 100644 index 61e07043c..000000000 --- a/.agentworkforce/trajectories/completed/2026-08/traj_9835z9cvpl9q/summary.md +++ /dev/null @@ -1,53 +0,0 @@ -# Trajectory: Implement escape-aware paced injection (issue #801) - -> **Status:** ✅ Completed -> **Task:** 801 -> **Confidence:** 85% -> **Started:** July 15, 2026 at 07:48 PM -> **Completed:** August 2, 2026 at 10:53 PM - ---- - -## Summary - -Fixed 6 review findings (3 MCP description overclaims, 3 SECURITY.md inaccuracies); declined CodeRabbit's CHANGELOG heading change as contrary to CLAUDE.md convention. All 40 CI checks green. - -**Approach:** Standard approach - ---- - -## Key Decisions - -### Diagnosed view detach behavior -- **Chose:** Diagnosed view detach behavior -- **Reasoning:** PTY-backed view consumes Ctrl-C as raw stdin and resolves its session, but closes the WebSocket gracefully; if that close handshake leaves a live handle, the process stays alive until a second Ctrl-C is handled by Node's default signal behavior. - -### Use forced WebSocket teardown for local view detach -- **Chose:** Use forced WebSocket teardown for local view detach -- **Reasoning:** A view session has no outbound state to preserve. Calling ws.terminate after requesting a normal close releases the live socket handle immediately, ensuring the first Ctrl-C exits the viewer without terminating the agent. - -### Preserve the post-SIGKILL reaping wait in the node provider integration test -- **Chose:** Preserve the post-SIGKILL reaping wait in the node provider integration test -- **Reasoning:** The test's 50ms sleep shim shortened both the graceful shutdown timeout and the post-kill wait. Restricting it to the 5s graceful timeout prevents stop() from resolving before the OS reaps the real child. - -### Replace fixed standalone smoke delay with bounded readiness polling -- **Chose:** Replace fixed standalone smoke delay with bounded readiness polling -- **Reasoning:** The scripts/ci-standalone-smoke.sh test was tearing down `agent-relay node up` after eight seconds even when a retried Relaycast handshake was still in progress. Polling for its `Broker started.` readiness line for up to thirty seconds prevents cleanup from creating a false startup failure while retaining deterministic timeout diagnostics. - -### Documented get_message_thread limit:0 behavior instead of changing the handler -- **Chose:** Documented get_message_thread limit:0 behavior instead of changing the handler -- **Reasoning:** Both suggested fixes alter runtime behavior; limit:0 server semantics unverifiable from the client in a description-only PR - ---- - -## Chapters - -### 1. Work -*Agent: default* - -- Paced injection in drainer thread: Paced injection in drainer thread -- Diagnosed view detach behavior: Diagnosed view detach behavior -- Use forced WebSocket teardown for local view detach: Use forced WebSocket teardown for local view detach -- Preserve the post-SIGKILL reaping wait in the node provider integration test: Preserve the post-SIGKILL reaping wait in the node provider integration test -- Replace fixed standalone smoke delay with bounded readiness polling: Replace fixed standalone smoke delay with bounded readiness polling -- Documented get_message_thread limit:0 behavior instead of changing the handler: Documented get_message_thread limit:0 behavior instead of changing the handler