This is a parent issue, not a fix. It proposes no implementation and adds no new evidence. It exists to state a claim that none of the child issues can state on their own. Do not implement this issue; implement its children.
The claim
Presenting an agent name you do not own can yield a working credential for that agent's existing record. Three independent code paths reach that outcome today, and each was found separately.
Each child issue reads as "this one call is unsafe". Together they say something different: there is no invariant at the identity layer, and each path is enforcing — or failing to enforce — its own local rule.
The three paths
| # |
path |
mechanism |
| #310 |
PATCH /v1/agents/:name |
workspace-key-only, shallow-merges caller metadata onto any agent, so metadata.identity_key can be planted and the admission gate then satisfied with the caller's own proof. POST /agents/:name/rotate-token reaches the same outcome in one step. |
| #311 |
SDK registerOrRotate |
catches the 409 and calls agents.get + agents.rotateToken. The name string alone is sufficient. |
| #306 |
engine registerAgentViaNode |
node-control agent.register reclaims via onConflictDoUpdate with a token_hash overwrite. Demonstrated: the reply returns the incumbent's existing row id with a fresh at_live_ token and evicts the incumbent's token. |
Different entry points, different auth surfaces, different owners — same outcome on the same column.
Why this needs to exist as a parent
Fixing three call sites is not the same as establishing an invariant. If each is closed independently:
The question they are all answering is: what must a caller prove before an existing agent's identity is transferred to it? Today the answer is path-dependent, and on at least one path it is "nothing but the name".
What would close this
Not code. An agreed statement of the invariant, and a check that each path enforces it. Something a future registration surface can be held to, and against which the three child fixes can be reviewed for agreement rather than merely for individual correctness.
Children
Related: AgentWorkforce/relay#1438 gates one of the three on the Rust broker's own registration path.
Not in scope
Presence and status reporting (#312, #313). Those made this area hard to reason about but are not identity decisions.
The claim
Presenting an agent name you do not own can yield a working credential for that agent's existing record. Three independent code paths reach that outcome today, and each was found separately.
Each child issue reads as "this one call is unsafe". Together they say something different: there is no invariant at the identity layer, and each path is enforcing — or failing to enforce — its own local rule.
The three paths
PATCH /v1/agents/:namemetadata.identity_keycan be planted and the admission gate then satisfied with the caller's own proof.POST /agents/:name/rotate-tokenreaches the same outcome in one step.registerOrRotateagents.get+agents.rotateToken. The name string alone is sufficient.registerAgentViaNodeagent.registerreclaims viaonConflictDoUpdatewith atoken_hashoverwrite. Demonstrated: the reply returns the incumbent's existing row id with a freshat_live_token and evicts the incumbent's token.Different entry points, different auth surfaces, different owners — same outcome on the same column.
Why this needs to exist as a parent
Fixing three call sites is not the same as establishing an invariant. If each is closed independently:
The question they are all answering is: what must a caller prove before an existing agent's identity is transferred to it? Today the answer is path-dependent, and on at least one path it is "nothing but the name".
What would close this
Not code. An agreed statement of the invariant, and a check that each path enforces it. Something a future registration surface can be held to, and against which the three child fixes can be reviewed for agreement rather than merely for individual correctness.
Children
identity_keyvia unrestricted metadata write; alsorotate-tokenregisterOrRotateunverified takeover; also carries the standinglocation_node_id = 'node_direct_' || idhole (14,074 rows fleet-wide reclaimable today)statusontolast_seen; removes an accidental trigger, adds no proof of identityRelated: AgentWorkforce/relay#1438 gates one of the three on the Rust broker's own registration path.
Not in scope
Presence and status reporting (#312, #313). Those made this area hard to reason about but are not identity decisions.