Skip to content

feat: #214 web-app agent pairing — master side complete (poll → claim → register → grant)#217

Merged
hanwencheng merged 8 commits into
mainfrom
claude/214-web-pairing
Jun 6, 2026
Merged

feat: #214 web-app agent pairing — master side complete (poll → claim → register → grant)#217
hanwencheng merged 8 commits into
mainfrom
claude/214-web-pairing

Conversation

@hanwencheng
Copy link
Copy Markdown
Member

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):

Slice Daemon route Reuses What it does
1 GET /v1/agent/pairing/pending agent_pending_value poll the broker rendezvous → map PendingBinding → the UI PairingRequest shape
2 POST /v1/agent/pairing/claim agent_claim the master claims the agent's one-time code → broker binds the HDKD child omni
3 POST /v1/agent/pairing/register agent_ack (new) + a heima-agent-create.sh --from-pubkey shell-out (mirrors register_master_device; sibling of the master register script — no new flag) submit registerAgentDevice on chain for the sandbox device key, then ack the broker

Frontend (apps/parent-control): listPairingRequests / claimPairing / registerPairing across 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_hash from the broker's pending binding, never from the browser.

Verified

  • cargo clippy -p agentkeys-cli -p agentkeys-daemon --all-targets -D warnings clean · cargo fmt --check clean · frontend tsc --noEmit clean.
  • Daemon unit tests: pending_binding_maps_to_pairing_request (the broker→UI mapper) + pairing_routes_fail_closed_without_a_broker (the no-broker 503 guard).
  • Merged latest 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)

🤖 Generated with Claude Code

…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.
@hanwencheng hanwencheng changed the title feat: #214 web-app agent pairing — master-side claim → register (slices 1-3) feat: #214 web-app agent pairing — master side complete (poll → claim → register → grant) Jun 6, 2026
@hanwencheng
Copy link
Copy Markdown
Member Author

Slice 4 landed (88cb80b) — #214 master side is now complete end-to-end.

After register, the agent is inserted into state.actors, so it appears in the devices view and becomes targetable by the existing P.3 scope-grant flow (/v1/actors/:id/scope/grant + the #207 AutoDistributePanel) — granting its memory:<ns> + cred:<service> scopes (incl. the LLM key) reuses the #207 machinery, no new code.

Full master-side route chain: GET /pendingPOST /claimPOST /register (+ ack) → existing scope/grant. Daemon tests (mapper + no-broker guard) + the web-parity phase-6 pairing-poll smoke (step 4) cover the wiring; the on-chain register + a real paired agent are the live harness e2e.

Remaining: the master-side default-LLM-key designation (small) + the agent-side vaulted-key wire (#216 — pairing → cred-fetch → wire hermes plants the authorized key).

…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).
@hanwencheng hanwencheng marked this pull request as ready for review June 6, 2026 18:19
@hanwencheng hanwencheng merged commit 3e66adf into main Jun 6, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant