Context
Leaf B of #13167 (Extended-NL identity-transport — the contract umbrella). The first landable + backward-compatible half of the Body-side enforcement wiring: the Client learns to unwrap the Bridge's {type:'agent_message', agentId, message} sidecar (per @neo-opus-grace's locked contract) and thread the verified agentId as a request context to the service dispatch — before the Bridge starts emitting the sidecar. That ordering is the cross-leaf finding @neo-opus-grace surfaced: a Client that already understands agent_message never breaks the live agent→app channel (Leaf A #13172 = the Bridge auth; the (1b) emit is split off as the final flip, sequenced last).
The Problem
Client.onSocketMessage accepts bare JSON-RPC and calls handleRequest(method, params) with no request context (src/ai/Client.mjs:253 / :186). The enforcement (Leaf C) needs the verified agentId threaded to the write services. This leaf adds the unwrap + the context thread, backward-compatibly: a bare frame is dispatched exactly as today (legacy / non-agent, unenforced); an agent_message frame yields a {agentId} context (the enforcement keys on it; a malformed / absent agentId yields a fail-closed marker).
The Fix
src/ai/parseAgentEnvelope.mjs — a pure parseAgentEnvelope(frame) → {jsonrpc, context}: agent_message → {jsonrpc: frame.message, context: {agentId}} (null agentId if absent/invalid → fail-closed marker); bare → {jsonrpc: frame, context: null} (legacy, unenforced); non-object → {jsonrpc: null, context: null}.
Client.onSocketMessage consumes it: dispatch handleRequest(jsonrpc.method, jsonrpc.params, context) when jsonrpc?.method.
Client.handleRequest(method, params, context) — forwards context to the service call (fn.call(service, params, context)); existing service fns ignore the extra arg (backward-compat).
Contract Ledger
Inherits #13167's ledger. This leaf delivers row 3 (Client request dispatch → handleRequest(method, params, context) with {agentId} when present; reads unaffected) + the consume side of row 2 (the Client unwraps the {type:'agent_message', agentId, message} sidecar; raw read path unchanged). The write-service WriteGuard enforcement (rows 4–6) is Leaf C.
Acceptance Criteria
Out of Scope
- The write-service WriteGuard enforcement (Leaf C — ledger rows 4–6; needs the live parent-chain + an e2e proof).
- The Bridge auth (Leaf A / #13172) + the
(1b) emit-flip (@neo-opus-grace).
Decision Record impact
aligned-with #13167 — implements its ledger row 3 + the consume side of row 2.
Related
- Parent #13167 (umbrella + Contract Ledger); grandparent #13056. Sequenced before the
(1b) emit (the ordering @neo-opus-grace found). Leaf A = #13172.
Live latest-open sweep: this leaf was just split off #13167 with @neo-opus-grace (2026-06-14T01:43Z); no equivalent open ticket.
Release classification: post-release (Agent Harness product line).
Origin Session ID: 4c598c8f-d8a7-4288-9420-e825a45d310e
Context
Leaf B of #13167 (Extended-NL identity-transport — the contract umbrella). The first landable + backward-compatible half of the Body-side enforcement wiring: the Client learns to unwrap the Bridge's
{type:'agent_message', agentId, message}sidecar (per @neo-opus-grace's locked contract) and thread the verifiedagentIdas a request context to the service dispatch — before the Bridge starts emitting the sidecar. That ordering is the cross-leaf finding @neo-opus-grace surfaced: a Client that already understandsagent_messagenever breaks the live agent→app channel (Leaf A #13172 = the Bridge auth; the(1b)emit is split off as the final flip, sequenced last).The Problem
Client.onSocketMessageaccepts bare JSON-RPC and callshandleRequest(method, params)with no request context (src/ai/Client.mjs:253/:186). The enforcement (Leaf C) needs the verifiedagentIdthreaded to the write services. This leaf adds the unwrap + the context thread, backward-compatibly: a bare frame is dispatched exactly as today (legacy / non-agent, unenforced); anagent_messageframe yields a{agentId}context (the enforcement keys on it; a malformed / absent agentId yields a fail-closed marker).The Fix
src/ai/parseAgentEnvelope.mjs— a pureparseAgentEnvelope(frame)→{jsonrpc, context}:agent_message→{jsonrpc: frame.message, context: {agentId}}(null agentId if absent/invalid → fail-closed marker); bare →{jsonrpc: frame, context: null}(legacy, unenforced); non-object →{jsonrpc: null, context: null}.Client.onSocketMessageconsumes it: dispatchhandleRequest(jsonrpc.method, jsonrpc.params, context)whenjsonrpc?.method.Client.handleRequest(method, params, context)— forwardscontextto the service call (fn.call(service, params, context)); existing service fns ignore the extra arg (backward-compat).Contract Ledger
Inherits #13167's ledger. This leaf delivers row 3 (Client request dispatch →
handleRequest(method, params, context)with{agentId}when present; reads unaffected) + the consume side of row 2 (the Client unwraps the{type:'agent_message', agentId, message}sidecar; raw read path unchanged). The write-service WriteGuard enforcement (rows 4–6) is Leaf C.Acceptance Criteria
parseAgentEnvelope(frame)(pure):agent_message→{jsonrpc: frame.message, context: {agentId}}; null / invalid agentId →{..., context: {agentId: null}}(fail-closed marker); bare →{jsonrpc: frame, context: null}; non-object →{jsonrpc: null, context: null}.Client.onSocketMessagedispatches the unwrapped JSON-RPC + context; a bare frame is handled exactly as today (backward-compat).Client.handleRequestaccepts + forwardscontextto the service; existing (read) services are unaffected by the extra arg.parseAgentEnvelope(pure); the Client wiring is a thin backward-compat diff (node --check; live dispatch is whitebox-e2e under Leaf C).Out of Scope
(1b)emit-flip (@neo-opus-grace).Decision Record impact
aligned-with#13167 — implements its ledger row 3 + the consume side of row 2.Related
(1b)emit (the ordering @neo-opus-grace found). Leaf A = #13172.Live latest-open sweep: this leaf was just split off #13167 with @neo-opus-grace (2026-06-14T01:43Z); no equivalent open ticket.
Release classification: post-release (Agent Harness product line).
Origin Session ID: 4c598c8f-d8a7-4288-9420-e825a45d310e