Context
An operator approving a §10.2 agent pairing in the parent-control web UI cannot cross-verify the pending card against the agent machine — a confused-deputy / approve-the-wrong-request surface. Surfaced live (Path A): two stale Pairing request · hermes cards showed PAIR-CODE values (bPe5Y8qNAd…, _rZYUUvRdU…) the operator never saw on the agent, which had displayed pairing codes e3iCqoc… / _QxjoWNP…. There's no way to confirm the request being approved is the one the agent actually opened.
Root cause (grounded)
- The card's "PAIR-CODE" is the truncated REQUEST_ID, not the agent's code —
pending_binding_to_request sets "pairCode": short(&request_id) (crates/agentkeys-daemon/src/ui_bridge.rs:2016); the comment notes the one-time pairing code is consumed at claim and not retained by the broker, so request_id is the only stable post-claim handle — but it's mislabeled + truncated.
- The card shows no full request_id, no start time, no expiry/countdown.
- The agent's
agentkeys-daemon --request-pairing displays the pairing code + device + expires_at but NOT the request_id (it's only in the 0600 ~/.agentkeys/pairing-request-*.json state file) — so the operator can't read the request_id off the agent to compare even if the master showed it.
--request-pairing --force opens a NEW broker request without canceling the device's prior open requests → they accumulate (real incident: 2 duplicate pending bindings for the same device 0xe98a6e82…, differing only by request_id, because the earlier register-502 bug never cleared them).
Scope
- Agent (
--request-pairing): prominently DISPLAY the request_id alongside the pairing code + device pubkey + expiry, so the operator can read request_id + device off the agent screen.
- Broker: the pending-binding row must carry
created_at + expires_at and expose the request_id; keep them for the pending list.
- Daemon (
/v1/agent/pairing/pending → pending_binding_to_request): map the full request_id + created_at + expires_at into the PairingRequest JSON; stop labeling request_id as pairCode.
- Frontend (
apps/parent-control pairing card, app/_components/pairing.tsx): show the full request_id, start time + live expiry countdown, and the full device pubkey — clearly labeled. The operator verifies request_id + device match the agent before accept · Touch ID. Remove/relabel the misleading "PAIR-CODE" field (it's the request_id, not the agent's one-time code).
--force supersede: a new --request-pairing (esp. --force) must cancel/supersede the device's prior OPEN broker requests (or the broker dedupes pending bindings per device) so repeated requests replace rather than accumulate.
Acceptance
- The master pairing card shows the full request_id + start + expiry + full device pubkey, all matching what the agent displays — an operator can cross-verify and refuse an unrecognized request.
agentkeys-daemon --request-pairing --force (or run N times) leaves exactly one open request for that device on the broker; the master pending list shows no duplicates.
Out of scope
- The browser passkey→UserOp wiring (E7 frontend item) · the on-chain
registerAgentDevice itself.
Effort
~L (cross-stack: agent output + broker pending-binding fields + daemon mapping + frontend card + --force supersede on the broker).
References
- arch.md §10.2 (agent-initiated pairing, Matter/HomeKit model)
crates/agentkeys-daemon/src/ui_bridge.rs (pending_binding_to_request:1966, list_pairing_requests, register_pairing) · crates/agentkeys-daemon/src/main.rs (run_request_pairing)
apps/parent-control/app/_components/pairing.tsx
- Surfaced while testing Path A in
docs/operator-runbook-wire.md.
Context
An operator approving a §10.2 agent pairing in the parent-control web UI cannot cross-verify the pending card against the agent machine — a confused-deputy / approve-the-wrong-request surface. Surfaced live (Path A): two stale
Pairing request · hermescards showed PAIR-CODE values (bPe5Y8qNAd…,_rZYUUvRdU…) the operator never saw on the agent, which had displayed pairing codese3iCqoc…/_QxjoWNP…. There's no way to confirm the request being approved is the one the agent actually opened.Root cause (grounded)
pending_binding_to_requestsets"pairCode": short(&request_id)(crates/agentkeys-daemon/src/ui_bridge.rs:2016); the comment notes the one-time pairing code is consumed at claim and not retained by the broker, so request_id is the only stable post-claim handle — but it's mislabeled + truncated.agentkeys-daemon --request-pairingdisplays the pairing code + device +expires_atbut NOT the request_id (it's only in the0600 ~/.agentkeys/pairing-request-*.jsonstate file) — so the operator can't read the request_id off the agent to compare even if the master showed it.--request-pairing --forceopens a NEW broker request without canceling the device's prior open requests → they accumulate (real incident: 2 duplicate pending bindings for the same device0xe98a6e82…, differing only by request_id, because the earlier register-502 bug never cleared them).Scope
--request-pairing): prominently DISPLAY the request_id alongside the pairing code + device pubkey + expiry, so the operator can read request_id + device off the agent screen.created_at+expires_atand expose therequest_id; keep them for the pending list./v1/agent/pairing/pending→pending_binding_to_request): map the full request_id + created_at + expires_at into thePairingRequestJSON; stop labeling request_id aspairCode.apps/parent-controlpairing card,app/_components/pairing.tsx): show the full request_id, start time + live expiry countdown, and the full device pubkey — clearly labeled. The operator verifies request_id + device match the agent beforeaccept · Touch ID. Remove/relabel the misleading "PAIR-CODE" field (it's the request_id, not the agent's one-time code).--forcesupersede: a new--request-pairing(esp.--force) must cancel/supersede the device's prior OPEN broker requests (or the broker dedupes pending bindings per device) so repeated requests replace rather than accumulate.Acceptance
agentkeys-daemon --request-pairing --force(or run N times) leaves exactly one open request for that device on the broker; the master pending list shows no duplicates.Out of scope
registerAgentDeviceitself.Effort
~L (cross-stack: agent output + broker pending-binding fields + daemon mapping + frontend card +
--forcesupersede on the broker).References
crates/agentkeys-daemon/src/ui_bridge.rs(pending_binding_to_request:1966,list_pairing_requests,register_pairing) ·crates/agentkeys-daemon/src/main.rs(run_request_pairing)apps/parent-control/app/_components/pairing.tsxdocs/operator-runbook-wire.md.