Skip to content

agentkeys: §10.2 agent-initiated pairing (method A) — flip from master-issued link codes#159

Merged
hanwencheng merged 6 commits into
mainfrom
claude/agent-initiated-pairing
Jun 1, 2026
Merged

agentkeys: §10.2 agent-initiated pairing (method A) — flip from master-issued link codes#159
hanwencheng merged 6 commits into
mainfrom
claude/agent-initiated-pairing

Conversation

@hanwencheng
Copy link
Copy Markdown
Member

What & why

Flips the §10.2 agent bootstrap from master-initiated (#149: the master mints a link code, the agent redeems it) to agent-initiated (method A: the agent shows a one-time pairing code, the master claims it). This is the Matter/HomeKit IoT model and the only structurally-correct one for a no-keyboard device — an AI companion in a box can show a QR/setup code but can't accept a master-minted code typed into it.

It stays Sybil-safe: the pairing request is unbound (names no master) and inert until a master deliberately claims the code — the master's claim is the sole binder, exactly as the master's mint was under #149. Reuses #149's on-chain bind + scope tail unchanged (the broker still never writes chain; the master submits registerAgentDevice).

Design doc: docs/spec/plans/agent-initiated-pairing-method-a.md. Supersedes the front-half of #149 (banner added to docs/spec/plans/issue-144-hdkd-agent-bootstrap.md).

What landed

Broker (crates/agentkeys-broker-server)

  • New storage/pairing_requests.rs — unbound, agent-created request pool. J1_agent is not stored at rest; it's minted fresh at poll time on a re-proved pop_sig, so no bearer secret sits in SQLite and the JWT TTL starts at retrieval.
  • New handlers/agent/{request,claim,poll}.rs; removed {create,redeem}.rs + storage/link_codes.rs.
  • Routes: POST /v1/agent/pairing/{request,claim,poll} replace create + link-code/redeem. pending-bindings + /ack kept (now keyed by request_id).
  • Renamed link_code_storepairing_request_store across state.rs/boot.rs/main.rs.

Daemon (crates/agentkeys-daemon/src/main.rs)

  • --init-link-code → two synchronous one-shots mirroring the two endpoints: --request-pairing (keygen in sandbox → open request → display code; writes a 0600 state file) and --retrieve-pairing (poll-until-claimed, bounded by --init-poll-timeout-seconds; mint + persist J1_agent 0600; emit binding artifact).

CLI (crates/agentkeys-cli)

  • agent createagent claim --pairing-code <code> --label … --services …. agent pending unchanged.

Harness (harness/phase1-wire-demo.sh)

  • Phase P inverted: P.0 agent --request-pairing (shows code) → P.1 master agent claim → P.1b agent --retrieve-pairing → P.1c pending → P.2 bind + ack-by-request_id → P.3 grant. P.depair unchanged.
  • Runbook-fix-fold-back in scripts/setup-broker-host.sh: updated both deploy smokes (nm symbol grep + the no-bearer-401 route probe → /v1/agent/pairing/claim).

Docsarch.md §10.2 rewritten (+ §6.2 routes, §10.4, §5/§10.6 tables, trust boundaries, CLI inventory); operator-runbook-wire.md Phase P walkthrough; v2-stage1-migration-and-demo.md §7 (also fixed pre-existing "broker submits register" drift). The Solidity link_code_redemption calldata param is kept (contract unchanged).

Out of scope (filed)

Verification

  • Broker: 14 store unit tests + 5 integration tests (agent_bootstrap_flow, incl. cross-device / bad-pop_sig poll rejection).
  • cargo fmt --all --check clean; cargo clippy --workspace --all-targets --all-features -- -D warnings clean.
  • bash -n on phase1-wire-demo.sh + setup-broker-host.sh.
  • Not done locally: a live phase1-wire-demo.sh --real --webauthn (can't drive sandbox+broker locally) — that's the behavioral gate after deploy.

Deploy note

The broker binary changed (new routes), so the broker host must be redeployed before the wire demo:
sudo bash scripts/setup-broker-host.sh --ref claude/agent-initiated-pairing (no AWS/setup-cloud.sh change — no infra touched).

🤖 Generated with Claude Code

Flip §10.2 from master-mints-link-code → agent-submits-request + master-claims-by-code (the IoT scan-the-device-QR model). Reuses #149's on-chain bind+scope tail. Unbind/factory-reset deferred → #156 (client) + #155 (on-chain self-revoke).
Flip the agent bootstrap from master-initiated (link code) to agent-
initiated (the agent shows a code, the master claims it — the Matter/
HomeKit IoT model). Replaces #149's master-mint front-half; reuses the
on-chain bind + scope tail unchanged.

Broker:
- NEW storage/pairing_requests.rs — unbound, agent-created request pool
  (issue/claim/poll/pending_bindings/mark_bound/purge). J1 is NOT stored
  at rest; minted fresh at poll time on a re-proved pop_sig.
- NEW handlers/agent/request.rs (agent, pop_sig-gated) — open an unbound
  request, return {request_id (secret), pairing_code (display)}.
- NEW handlers/agent/claim.rs (master, J1-gated) — claim by code, derive
  O_agent=HDKD(O_master,//label), record pending binding.
- NEW handlers/agent/poll.rs (agent, pop_sig-gated) — once claimed, mint
  + return J1_agent.
- REMOVE handlers/agent/{create,redeem}.rs + storage/link_codes.rs.
- Rename link_code_store -> pairing_request_store across state/boot/main.
- Rewire routes: /v1/agent/pairing/{request,claim,poll}; keep
  pending-bindings + /ack (now keyed by request_id).

Tests: 14 store unit tests + agent_bootstrap_flow rewritten for the
request->claim->poll flow (5 cases incl. cross-device/bad-pop_sig poll
rejection). clippy --all-features --all-targets -D warnings clean.

Unbind/factory-reset re-pair deferred -> #156; on-chain self-revoke -> #155.
…ost smoke

Flip the client + wire harness to agent-initiated pairing (issue #144,
method A), matching the broker request/claim/poll endpoints.

Daemon (--init-link-code → two one-shots mirroring the two endpoints):
- --request-pairing: in-sandbox K10 keygen → POST /v1/agent/pairing/request
  → print {request_id, pairing_code, …}; persist a 0600 state file so
  --retrieve-pairing can resolve request_id (--request-id overrides).
- --retrieve-pairing: poll /v1/agent/pairing/poll until claimed (bounded by
  --init-poll-timeout-seconds), mint+persist J1_agent (0600), emit artifact.

CLI: agent create → agent claim --pairing-code <code> --label … --services …
(POST /v1/agent/pairing/claim). agent pending unchanged (rows now keyed by
request_id).

Harness phase1-wire-demo.sh Phase P inverts: P.0 agent --request-pairing
(shows code) → P.1 master agent claim → P.1b agent --retrieve-pairing (J1) →
P.1c pending → P.2 bind + ack-by-request_id → P.3 grant. P.depair unchanged.
404 trap + route names updated.

setup-broker-host.sh (runbook-fix-fold-back): nm symbol grep →
pairing_{request,claim,poll}|pending_bindings; route smoke → no-bearer POST
/v1/agent/pairing/claim must be 401 not 404.

cli+daemon: clippy --all-targets -D warnings clean, fmt clean, tests pass
(38/38 single-threaded; the 1 parallel-suite failure is a pre-existing k11
enroll test race on a shared HOME path, unrelated). bash -n both scripts OK.
…ooks)

Reconcile every doc + code-comment surface with the agent-initiated
pairing flip (issue #144, method A).

arch.md (single source of truth):
- §10.2 ceremony fully rewritten for method A (agent requests → master
  claims → agent retrieves), incl. the IoT/Sybil-safety rationale + the
  deferred unbind notes (#155/#156).
- §6.2 route list: /v1/agent/pairing/{request,claim,poll} replace
  create + link-code/redeem; pending-bindings ack now by request_id.
- §10.4 re-bootstrap inverted; §5 agent_omni row, §10.6 threat row,
  trust-boundary + actor-role tables, CLI inventory → pairing terms.
  Solidity link_code_redemption calldata param kept (contract unchanged).

operator-runbook-wire.md: Phase P walkthrough (P.0 request → P.1 claim →
P.1b retrieve → P.1c pending → P.2 bind+ack → P.3 grant), 404 trap +
route checks, troubleshooting rows.

v2-stage1-migration-and-demo.md §7: rewritten for method A (also fixes
pre-existing drift — the master, not the broker, submits registerAgentDevice).

issue-144 plan: superseded-front-half banner → method-A doc. issue-74
ephemeral-rebootstrap paragraph corrected. Code doc-comments (mcp-server
config, core actor_omni, cli device_session) → pairing terms.

fmt + clippy --all-targets -D warnings clean on the 3 comment-edited crates.
…owing untracked files)

The --ref deploy path ran a plain `git checkout $PULL_REF`, which ABORTS
when an untracked working-tree file shadows a file the target ref tracks
("untracked working tree files would be overwritten by checkout" — hit on
the broker host when switching to a branch that tracks docs/wiki/*.md the
prior branch did not). The broker host is a deploy target, not a dev
checkout: -f overwrites the colliding files with the tracked version +
discards local edits to TRACKED files, while LEAVING unrelated untracked
files (env, keys, certs — all gitignored) intact. Folds the fix back into
the deploy runbook so the next operator does not hit the same abort.
@hanwencheng hanwencheng merged commit 4405edb into main Jun 1, 2026
7 checks passed
hanwencheng added a commit that referenced this pull request Jun 1, 2026
…al memory

After merging #159 (§10.2 agent-initiated pairing, method A — which resolves
pushback #1 upstream), wire the two genuinely-real pieces the user asked for.
Plan + status: docs/plan/web-flow/issue-9step-flow.md.

Daemon (ui_bridge.rs) — master memory, real + idempotent:
 - ApiMemoryEntry + master_memory store; content_hash = sha256(ns ‖ key ‖ body).
 - GET  /v1/master/memory          — list (sorted by ns/key).
 - POST /v1/master/memory/plant    — idempotent: dedup by content_hash →
                                     {planted, skipped, total}; emits a
                                     memory.write audit row when something lands.
 - dev_seed extended with master_memory (seeds the "already has memory" path).
 - 3 new unit tests (empty / plant→replant-dedup / changed-body-adds-entry);
   23 ui_bridge tests pass. Adds sha2 dep.

Client seam (lib/client) — new MasterMemoryEntry/PlantResult + listMasterMemory()
 + plantMemory(): DaemonBackend hits the real endpoints; EmptyBackend stays
 disconnected (offline → seed fallback in the UI).

UI:
 - OnboardingScreen (ceremony.tsx): real navigator.credentials.create() via the
   client (/v1/k11/enroll/{begin,finish}, PR-B) when a daemon is configured —
   shows a "K11 enrolled · real WebAuthn" chip; narrated fallback offline. No
   longer a pure setTimeout fake.
 - MemoryPage wiring (App.tsx): auto-detect existing memory on load
   (listMasterMemory → hides the plant button); plant calls plantMemory (server
   dedups) then re-lists; seed fallback when disconnected. The dedup guard is
   now enforced both client-side AND server-side.
 - pairing.tsx copy aligned to method A (agent shows a code → master claims it),
   matching #159. Functional claim-input + daemon pairing-proxy is the next step
   (needs the broker reachable).

Pushback status: #1 resolved by #159; #2 implemented here; #3 (audit decode)
remains a mock tracked in #153 (per the user's "just 2" scope).

Verified: cargo test -p agentkeys-daemon ui_bridge — 23/23 · npx tsc --noEmit clean ·
npm run build ok (21.2 kB route) · dev smoke renders onboarding, no runtime errors.
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