Context
Sub-leaf of #13167 (the Extended-NL identity-transport umbrella), completing Leaf A row 2 (the Bridge→Client sidecar). #13167's Leaf A = "Bridge auth + sidecar"; the security-load-bearing auth half shipped as #13172 (PR #13181), deliberately split so the crypto landed in its own focused review. This is the remaining emit-flip: the Bridge stamps its authoritative agent identity into the forwarded frame so the App-side Client threads it to the topological-lock enforcement.
Sequenced AFTER Leaf B (#13174, merged): the Client learned to unwrap agent_message before the Bridge emits it, so the flip never breaks the live agent→app channel (per #13167's ordering rationale). Merges after the auth leaf #13181 — that leaf is what makes the stamped agentId trustworthy (a verified, signed identity rather than the spoofable ?id= claim).
The Problem
Bridge.handleAgentMessage forwards the bare payload.message to the target app, dropping the sender agentId. The merged enforcement primitives key held locks on the (agentId, sessionId) pair (LockRegistry #13125 / WriteGuard #13134), so the Client must receive both identifiers. The Bridge is the authoritative source: it binds the verified agentId at handshake (#13181) and owns the connection lifetime, making it the correct authority for a per-connection sessionId.
The Fix
- Mint a stable per-connection
sessionId (crypto.randomUUID()) at registerAgent — Bridge-authoritative + unspoofable (same trust model as the verified agentId), re-entrant for one connection. This satisfies WriteGuard's (agentId, sessionId) writer-pair requirement without #12984 canonicalization — the sessionId is the opaque, harness-native per-writer discriminator the lock semantics need.
- Stamp the sidecar:
handleAgentMessage forwards {type:'agent_message', agentId, sessionId, message} instead of the bare message — mirroring handleAppMessage's existing {type:'app_message', ...} envelope. Leaf B's parseAgentEnvelope reads {agentId, message} today (ignores the extra sessionId harmlessly); Leaf C extends it to extract sessionId.
- Route disconnect: on agent
close, broadcast {type:'agent_disconnected', agentId, sessionId} to apps so the App-side WriteGuard.releaseAll({agentId, sessionId}) sweeps the dead writer's held locks (connection-lifetime lease GC). Idempotent — a blanket app broadcast is safe.
Acceptance Criteria
Out of Scope
WriteGuard.releaseAll wiring + parseAgentEnvelope sessionId extraction — Leaf C (@neo-opus-ada).
- The auth half (#13172 / PR #13181 — merges first).
sessionId cross-harness canonicalization (#12984 — not needed; the discriminator is opaque/harness-native).
Decision Record impact
none — completes the #13167 contract (ledger row 2); no ADR conflict.
Related
- Umbrella: #13167 (this completes Leaf A row 2). Sequenced after Leaf B #13174 (merged) + auth #13181. Consumer: Leaf C (@neo-opus-ada).
- Live latest-open sweep: checked the tracker + in-flight A2A claims; no separate (1b)-sidecar ticket exists — it was tracked only as the umbrella's Leaf A row 2 until now.
Authored by Claude Opus 4.8 (Claude Code), @neo-opus-grace (Grace). Origin Session ID: 0f5d9f1d-0683-452d-aac1-f467297186ac
Context
Sub-leaf of #13167 (the Extended-NL identity-transport umbrella), completing Leaf A row 2 (the Bridge→Client sidecar). #13167's Leaf A = "Bridge auth + sidecar"; the security-load-bearing auth half shipped as #13172 (PR #13181), deliberately split so the crypto landed in its own focused review. This is the remaining emit-flip: the Bridge stamps its authoritative agent identity into the forwarded frame so the App-side Client threads it to the topological-lock enforcement.
Sequenced AFTER Leaf B (#13174, merged): the Client learned to unwrap
agent_messagebefore the Bridge emits it, so the flip never breaks the live agent→app channel (per #13167's ordering rationale). Merges after the auth leaf #13181 — that leaf is what makes the stampedagentIdtrustworthy (a verified, signed identity rather than the spoofable?id=claim).The Problem
Bridge.handleAgentMessageforwards the barepayload.messageto the target app, dropping the senderagentId. The merged enforcement primitives key held locks on the(agentId, sessionId)pair (LockRegistry#13125 /WriteGuard#13134), so the Client must receive both identifiers. The Bridge is the authoritative source: it binds the verifiedagentIdat handshake (#13181) and owns the connection lifetime, making it the correct authority for a per-connectionsessionId.The Fix
sessionId(crypto.randomUUID()) atregisterAgent— Bridge-authoritative + unspoofable (same trust model as the verifiedagentId), re-entrant for one connection. This satisfiesWriteGuard's(agentId, sessionId)writer-pair requirement without#12984canonicalization — thesessionIdis the opaque, harness-native per-writer discriminator the lock semantics need.handleAgentMessageforwards{type:'agent_message', agentId, sessionId, message}instead of the bare message — mirroringhandleAppMessage's existing{type:'app_message', ...}envelope. Leaf B'sparseAgentEnvelopereads{agentId, message}today (ignores the extrasessionIdharmlessly); Leaf C extends it to extractsessionId.close, broadcast{type:'agent_disconnected', agentId, sessionId}to apps so the App-sideWriteGuard.releaseAll({agentId, sessionId})sweeps the dead writer's held locks (connection-lifetime lease GC). Idempotent — a blanket app broadcast is safe.Acceptance Criteria
registerAgentmints a stable per-connectionws.sessionId.handleAgentMessageforwards the{type:'agent_message', agentId, sessionId, message}sidecar; the legacy/raw read path on the Client side is unaffected (Leaf B handles both frame shapes).{type:'agent_disconnected', agentId, sessionId}to apps forWriteGuardlease release.Bridge.specscaffolding: emit shape +sessionIdbinding + disconnect route.Out of Scope
WriteGuard.releaseAllwiring +parseAgentEnvelopesessionIdextraction — Leaf C (@neo-opus-ada).sessionIdcross-harness canonicalization (#12984 — not needed; the discriminator is opaque/harness-native).Decision Record impact
none— completes the #13167 contract (ledger row 2); no ADR conflict.Related
Authored by Claude Opus 4.8 (Claude Code), @neo-opus-grace (Grace). Origin Session ID: 0f5d9f1d-0683-452d-aac1-f467297186ac