You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The SDK offers four ways to register an agent. On a name collision they do four different things, ranging from a clean refusal to silently handing the caller the incumbent's identity. Callers pick between them by method name, with nothing in the naming to signal that the choice is a security decision.
All four in packages/sdk-typescript/src/relay.ts:
call
on collision
outcome
agents.register
throws agent_already_exists (409)
incumbent untouched — correct
registerAgent({ strict: true }) (:473)
delegates to agents.register
same as above
registerAgent({ strict: false }) (:482)
registerWithLegacySuffix (:448) — retries under name-<suffix>, up to 5 attempts
a second agent under a near-identical name
registerOrRotate (:485)
agents.get(name) then agents.rotateToken(name)
returns the incumbent's id and a fresh token; the incumbent's token is invalidated
registerOrRotate verifies nothing. The name string alone is sufficient to take over any agent record in the workspace and evict whoever was using it.
Both of these are the defect relay#1438 was written to remove
relay's v11.4.2 spawn-admission gate closed exactly these two behaviours on the Rust broker's own registration path. Its doc comment (relay crates/broker/src/relaycast/auth.rs:934-947) names them:
formerly split between a strict branch that always reclaimed a name-collision via register_or_get_agent — handing the caller the incumbent's id, name, AND bearer token — and a non-strict branch that silently minted a -{uuid8} sibling name once before failing. Both were the same defect: a spawn-admission gate that doesn't verify who is asking… A silent suffix would have produced a second agent doing duplicate work under a near-identical name — exactly the AR-448 duplicate-agent class this gate exists to stop.
That reasoning was applied to the Rust path. Both behaviours are still live here, unchanged, and the gate does not cover this code.
Reachable surface
registerOrRotate is what relay's MCP register_agent tool calls (relay packages/cli/src/cli/agent-relay-mcp.ts:381, via registerAgentWithRebind). The caller-supplied name is pinned to the session identity only when strict worker identity is on:
With strict worker identity off, or with no configured name, name passes through as given — so an agent calling the MCP tool can register under another agent's name and receive that agent's credentials.
It is also the Rust broker's documented HTTP fallback for worker pre-registration when node-control agent.register is unavailable (relay crates/broker/src/runtime/relaycast_events.rs:493-510, via ws.rs::register_agent_token). That call site already carries a warning in its own doc comment (ws.rs:163-170) that passing an unvalidated name "risks silently disconnecting an unrelated, already-registered agent that happens to share the name" — the hazard is known at the call site but unfixed at the source.
The CLI refuses, and that inconsistency is the point
A probe on 2026-08-07 ran agent-relay agent register <throwaway> twice. The second call returned Agent "…" already exists in this workspace, exited with no token, and left the incumbent untouched.
That is consistent with everything above, and it is worth stating why: the CLI reaches a different method. Both agent register and agent add call plain agents.register:
// relay packages/cli/src/cli/commands/agent.ts:38 (register) and :85 (add)constregistration=awaitrelay.agents.register({ name, type, persona });
So the surface an operator would naturally use to probe this behaviour is the one surface that is safe, while the programmatic surfaces are not. Anyone testing the takeover by hand will conclude the system is fine. That makes the inconsistency worse than a uniformly permissive API would be — it hides itself from exactly the check most likely to be run.
Suggested direction
The specific fix is a judgement call for the owners, but the shape:
registerWithLegacySuffix should be considered for removal outright. It was judged the wrong behaviour on the Rust side for reasons that apply identically here, and a caller that silently gets a differently-named agent than it asked for has no way to notice.
Whatever is decided, the four methods should not silently differ. If the collision policy is genuinely a caller choice, the method names should say which one is being made.
Verification
SDK, engine and CLI line references read from relaycastmain at 08ddec7; relay references from origin/main. The CLI double-register probe was run against a live workspace with a throwaway name. The registerOrRotate takeover was not executed against a live workspace — it would evict a running agent — and is derived from the source above.
Summary
The SDK offers four ways to register an agent. On a name collision they do four different things, ranging from a clean refusal to silently handing the caller the incumbent's identity. Callers pick between them by method name, with nothing in the naming to signal that the choice is a security decision.
All four in
packages/sdk-typescript/src/relay.ts:agents.registeragent_already_exists(409)registerAgent({ strict: true })(:473)agents.registerregisterAgent({ strict: false })(:482)registerWithLegacySuffix(:448) — retries undername-<suffix>, up to 5 attemptsregisterOrRotate(:485)agents.get(name)thenagents.rotateToken(name)registerOrRotateverifies nothing. The name string alone is sufficient to take over any agent record in the workspace and evict whoever was using it.Both of these are the defect relay#1438 was written to remove
relay's v11.4.2 spawn-admission gate closed exactly these two behaviours on the Rust broker's own registration path. Its doc comment (
relay crates/broker/src/relaycast/auth.rs:934-947) names them:That reasoning was applied to the Rust path. Both behaviours are still live here, unchanged, and the gate does not cover this code.
Reachable surface
registerOrRotateis what relay's MCPregister_agenttool calls (relay packages/cli/src/cli/agent-relay-mcp.ts:381, viaregisterAgentWithRebind). The caller-suppliednameis pinned to the session identity only when strict worker identity is on:With strict worker identity off, or with no configured name,
namepasses through as given — so an agent calling the MCP tool can register under another agent's name and receive that agent's credentials.It is also the Rust broker's documented HTTP fallback for worker pre-registration when node-control
agent.registeris unavailable (relay crates/broker/src/runtime/relaycast_events.rs:493-510, viaws.rs::register_agent_token). That call site already carries a warning in its own doc comment (ws.rs:163-170) that passing an unvalidated name "risks silently disconnecting an unrelated, already-registered agent that happens to share the name" — the hazard is known at the call site but unfixed at the source.The CLI refuses, and that inconsistency is the point
A probe on 2026-08-07 ran
agent-relay agent register <throwaway>twice. The second call returnedAgent "…" already exists in this workspace, exited with no token, and left the incumbent untouched.That is consistent with everything above, and it is worth stating why: the CLI reaches a different method. Both
agent registerandagent addcall plainagents.register:So the surface an operator would naturally use to probe this behaviour is the one surface that is safe, while the programmatic surfaces are not. Anyone testing the takeover by hand will conclude the system is fine. That makes the inconsistency worse than a uniformly permissive API would be — it hides itself from exactly the check most likely to be run.
Suggested direction
The specific fix is a judgement call for the owners, but the shape:
registerOrRotate's reclaim needs the same thing relay#1438 requires: proof the caller is the same work unit, not just the same name.registerAgentViaNode(packages/engine/src/engine/node.ts:1118) already demonstrates a server-enforced version keyed on node identity — that proof cannot be forged by a caller, unlike anything held in caller-writable metadata (see PATCH /v1/agents/:name lets any workspace-key holder write identity_key onto any agent, satisfying relay's admission gate with a planted proof #310).registerWithLegacySuffixshould be considered for removal outright. It was judged the wrong behaviour on the Rust side for reasons that apply identically here, and a caller that silently gets a differently-named agent than it asked for has no way to notice.Verification
SDK, engine and CLI line references read from
relaycastmainat08ddec7; relay references fromorigin/main. The CLI double-register probe was run against a live workspace with a throwaway name. TheregisterOrRotatetakeover was not executed against a live workspace — it would evict a running agent — and is derived from the source above.Related
PATCH /v1/agents/:namelets a workspace-key holder plant the proof relay#1438 reads. More urgent than this one.Context
Filed from an incident investigation on 2026-08-07. Khaliq owns the merge gate — no agent merges.