Skip to content
Merged
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [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.
Comment thread
willwashburn marked this conversation as resolved.

### Fixed

- CLI output no longer disappears when stdout or stderr is a pipe instead of a terminal. Node's stdio writes are asynchronous for pipes on macOS, so exiting in the same tick as the write discarded whatever was still buffered β€” `agent-relay cloud session --json | parser` and `$(agent-relay …)` could come back with empty stdout _and_ empty stderr, hiding the payload and the error that explained the failure. Every hard-exit path now drains stdio first.
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
119 changes: 119 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# 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

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

**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 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.
- 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

`.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.
36 changes: 27 additions & 9 deletions packages/cli/src/cli/agent-relay-mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,8 @@
{
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, 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.'),
Expand Down Expand Up @@ -274,7 +275,7 @@
});
} catch (err) {
if ((err as { name?: string }).name === 'AbortError') {
throw new Error(`Agent Relay result submission timed out after ${timeoutMs}ms`);

Check warning on line 278 in packages/cli/src/cli/agent-relay-mcp.ts

View workflow job for this annotation

GitHub Actions / lint

There is no `cause` attached to the symptom error being thrown
}
throw err;
} finally {
Expand Down Expand Up @@ -327,7 +328,7 @@
return { agentName, agentToken };
}

export async function registerAgentWithRebind({

Check warning on line 331 in packages/cli/src/cli/agent-relay-mcp.ts

View workflow job for this annotation

GitHub Actions / lint

Async function 'registerAgentWithRebind' has a complexity of 23. Maximum allowed is 15
session,
setSession,
getRelay,
Expand Down Expand Up @@ -406,7 +407,9 @@
'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'),
},
Expand Down Expand Up @@ -454,7 +457,9 @@
'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'),
Expand Down Expand Up @@ -513,7 +518,11 @@
'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'),
Expand Down Expand Up @@ -561,7 +570,9 @@
'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'),
},
Expand All @@ -581,7 +592,9 @@
'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'),
Expand Down Expand Up @@ -611,7 +624,8 @@
'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
Expand Down Expand Up @@ -669,7 +683,9 @@
'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
Expand Down Expand Up @@ -713,7 +729,9 @@
'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'),
Expand Down
4 changes: 3 additions & 1 deletion packages/cli/src/cli/mcp/action-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down
Loading
Loading