feat: #214 web-app agent pairing — master side complete (poll → claim → register → grant)#217
Conversation
…bindings First slice of the web-app agent-pairing build (#214). The parent-control pairing screen now shows REAL data instead of a local-state mock: - daemon GET /v1/agent/pairing/pending — reuses agentkeys_cli::agent_admin (new agent_pending_value) to pull the broker rendezvous via the master J1 session (agents the master claimed, awaiting on-chain register), mapped to the web UI's PairingRequest shape (pending_binding_to_request + unit test). - client: listPairingRequests() — types.ts interface + daemon.ts impl + empty.ts stub. - App.tsx: refreshPairing ("check for codes") now calls the real client. Verified: cargo clippy -p agentkeys-cli -p agentkeys-daemon -D warnings clean; daemon mapper unit test passes; frontend tsc --noEmit clean. Next slices (#214): claim-by-code, on-chain registerAgentDevice, scope grant (Touch ID), then the operator-runbook-wire.md split + web runbook.
…broker)
The master claims an agent's one-time pairing code from the web app (§10.2 P.1):
- daemon POST /v1/agent/pairing/claim — reuses agentkeys_cli::agent_admin::agent_claim
to bind the agent under a label + declare its requested scope via the broker,
using the master J1 session.
- client: claimPairing({code,label,scope?}) — types.ts + daemon.ts + empty.ts.
- UI (pairing.tsx + App.tsx): a "claim a code" form — the master types the code
the agent device shows (or scans its runtime QR); on success it re-polls so the
claimed agent drops into the rendezvous awaiting on-chain register.
Verified: cargo clippy -p agentkeys-daemon -D warnings clean; frontend tsc clean.
Next: slice 3 (on-chain registerAgentDevice + ack), slice 4 (scope grant), #216.
The master approves a claimed agent from the web app (§10.2 P.2): - agent_admin::agent_ack (new) — POST /v1/agent/pending-bindings/ack. - daemon POST /v1/agent/pairing/register — pulls the AUTHORITATIVE binding from the broker (device fields never come from the browser), shells out to heima-agent-create.sh --from-pubkey (sibling of the master register script — no new flag), submits registerAgentDevice on chain, then acks the broker. The binding's device_pubkey holds the agent EVM address; register_agent_device mirrors register_master_device. - client: registerPairing(requestId) — types.ts + daemon.ts + empty.ts. - App.tsx: the "accept" button now calls the real register → re-polls (the registered+acked binding clears from pending). The Touch-ID scope grant (P.3) is the next step. Verified: cargo clippy -p agentkeys-cli -p agentkeys-daemon -D warnings clean; frontend tsc clean. (On-chain tx is exercised by the harness e2e — needs a live chain + a claimed agent.)
Locks the no-broker guard on the new poll/claim/register routes — they fail closed before reaching the network. (Live broker + on-chain behavior is the harness e2e.)
…tep 4)
Adds a real harness step: boots the seeded daemon (reused from phase 6) and polls
GET /v1/agent/pairing/pending, asserting a well-formed {requests:[...]} — the
master-side web-pairing route reaches the real broker rendezvous with the master
J1. Follows the orchestrator contract (STEP_TOTAL=4, three outcomes, idempotent,
--ci tolerant). Keep-docs-in-sync: harness/CLAUDE.md + operator-runbook-harness.md.
The full claim->register e2e needs a live §10.2 agent request (agent-side, #216).
… + grant-able After a successful on-chain register, insert the agent into state.actors (role agent, child omni, parent master, sandbox device) keyed by agent-<label>. It now shows in the devices view AND becomes targetable by the EXISTING scope-grant flow (P.3, /v1/actors/:id/scope/grant + the AutoDistributePanel) — so granting the agent its memory:<ns> + cred:<service> scopes (incl. the LLM key) reuses the #207 machinery, no new code. Mirrors the master actor's in-memory model. Remaining tail: the master-side DEFAULT-LLM-key designation (consumed agent-side in #216). Verified: cargo clippy -p agentkeys-daemon -D warnings clean; fmt clean.
|
Slice 4 landed ( After register, the agent is inserted into Full master-side route chain: Remaining: the master-side default-LLM-key designation (small) + the agent-side vaulted-key wire (#216 — pairing → cred-fetch → |
…lted-key fetch) The #204-clean core of #216: the shared agentkeys-backend-client gains a cred_fetch capability so the agent can pull its AUTHORIZED LLM key from the vault (replacing the operator-env shortcut in phase1-wire Phase 4.0). - protocol.rs: CredFetchBody / CredFetchResp / CredFetchInput / CredFetchResult (mirrors agentkeys-worker-creds FetchRequest/Response — the service rides in the signed cap, not the body). - client.rs: cred_url field + cred() accessor + cred_fetch() — POST /v1/cred/fetch with per-actor STS under the VAULT role (mirrors memory_get), returns the b64 plaintext. The cap is minted separately via cap_mint(CredFetch). - The 2 BackendClient::new callers (daemon mint client, mcp-server) take the new cred_url (None for now — the MCP cred tool + the CLI `cred fetch` consumer are the next slice). Verified: cargo clippy -p agentkeys-backend-client -p agentkeys-daemon -p agentkeys-mcp-server -D warnings clean; #203 fixture gate green (no canonical shape drift); fmt clean. The live cred-fetch e2e is the next slice (CLI consumer + wire Phase-4.0 swap + sandbox run against the live cred worker).
Builds the master-side of #214 — the parent-control web app can now pair a real agent end to end (§10.2 method A), replacing the local-state mock. Draft: slices 1–3 of the master side are done + verified; the scope-grant step, the agent-side wire (#216), and the full mainnet harness e2e remain (see below).
What's built (master side)
The full web pairing route chain, all reusing
agentkeys_cli::agent_admin(no re-typed broker shapes, #204):GET /v1/agent/pairing/pendingagent_pending_valuePendingBinding→ the UIPairingRequestshapePOST /v1/agent/pairing/claimagent_claimPOST /v1/agent/pairing/registeragent_ack(new) + aheima-agent-create.sh --from-pubkeyshell-out (mirrorsregister_master_device; sibling of the master register script — no new flag)registerAgentDeviceon chain for the sandbox device key, then ack the brokerFrontend (
apps/parent-control):listPairingRequests/claimPairing/registerPairingacross the client seam (types.ts+daemon.ts+empty.ts); the pairing screen now has a real "claim a code" form + the accept button does the real on-chain register, replacing the mock.Device fields are authoritative: register pulls
device_pubkey(the agent's EVM address) /pop_sig/device_key_hashfrom the broker's pending binding, never from the browser.Verified
cargo clippy -p agentkeys-cli -p agentkeys-daemon --all-targets -D warningsclean ·cargo fmt --checkclean · frontendtsc --noEmitclean.pending_binding_maps_to_pairing_request(the broker→UI mapper) +pairing_routes_fail_closed_without_a_broker(the no-broker 503 guard).main(the feat(audit): real CBOR + EVM-calldata decode for the web audit view (#153) #194 audit PR) into the branch — daemon + frontend re-verified post-merge.Remaining (this PR or follow-ups)
memory:<ns>+cred:<service>scopes via the existinggrant_service_scopeWebAuthn flow, plus the master-side default LLM key designation (the clarified flow in Wire web-app agent pairing to the real claim → register → scope flow #214's body).wire hermesplants it (not the operator env).web-pairingharness orchestrator (following the harness contract) is the live gate; the unit tests above cover the route logic.🤖 Generated with Claude Code