Skip to content

Plan: wire parent-control to real backends + phone-first host model#162

Merged
hanwencheng merged 5 commits into
mainfrom
claude/wire-real-data-plan
Jun 2, 2026
Merged

Plan: wire parent-control to real backends + phone-first host model#162
hanwencheng merged 5 commits into
mainfrom
claude/wire-real-data-plan

Conversation

@hanwencheng
Copy link
Copy Markdown
Member

@hanwencheng hanwencheng commented Jun 1, 2026

closes #163

Stacked on #136 (base = claude/pensive-poincare-1a631e) because the docs/plan/web-flow/ files it amends live on that branch, not yet on main. GitHub will auto-retarget to main once #136 merges.

This PR was split out of #136 (the wiring plan + docs were cherry-picked here so #136 stays a clean UI-only PR; #136 also dropped 1,658 lines of superseded dead frontend code in the same cleanup).

What's here

1. wire-real-paths.md — how to wire every narrated/stub path to the real backends the wire demo already uses

Daemon-as-orchestrator (desktop host): reuse the proxy.rs broker client + shell to scripts/heima-*.sh for chain writes + the real cap→STS→worker→S3 memory path. Per-flow tables (onboarding §9, pairing §10.2, memory) map UI surface → data-model.md endpoint → real call → arch ref. Sequenced W0–W6 + harness-parity test.

2. §0.5 — phone-first host model (consistency)

Most operators will have only a phone, so the master plane can't depend on a localhost daemon. Decision: one portable agentkeys-core hosted three ways behind the same AgentKeysClient contract — WASM (web) / native lib (mobile, via UniFFI) / daemon (desktop). Consistency is structural (one Rust crate, no TS reimplementation); the daemon is demoted to one host; the broker is the only always-on component; the master plane is event-driven + biometric-gated (push-woken).

3. §11 — gating decision (VERIFIED against the contracts)

Read SidecarRegistry.sol + AgentKeysScope.sol: every master write is msg.sender-bound to the operator's secp256k1 key (operatorMasterWallet[omni] = msg.sender at bootstrap; msg.sender != master → revert everywhere; K11 P-256 assertion is an additional gate, not a substitute). So no relayer/key-free path without a contract change. Phone holds the secp256k1 key in the Keychain (SE is P-256-only → it seals the K11 passkey, not the EVM key); browser/WASM can't custody it → delegates broadcast. Fork: (A) keep msg.sender-bound (phone holds key, no contract work — recommended for MVP) vs (B) assertion-only auth (enables key-free/web/relayer masters, needs contract redesign + security review).

4. §12 — WASM lift scope

agentkeys-core carve-out (X0) → wasm-bindgen exports (X1) → CoreBackend behind AgentKeysClient (X2) → WebAuthn interop (X3) → chain-write delegation (X4). Shared investment with the future mobile UniFFI shell.

Also: data-model.md flags the browser-direct prohibition as desktop-first (relaxed for the master plane); arch.md §22c.3 gets a master control-plane host-model paragraph deferring to this plan.

No code in this PR — the actual wiring is the W0–W6 / X0–X4 work, to be scheduled after the plan is approved.

🤖 Generated with Claude Code

@hanwencheng
Copy link
Copy Markdown
Member Author

Adversarial security review of §11 gating (codex) — committed as wire-real-paths-security-review.md

Verdict: fork (A) (keep msg.sender-bound) is the right direction — the current contracts are not assertion-only-safe — but (A) is NOT sound as written. The msg.sender-bound claim holds for post-bootstrap writes only.

Findings (full doc + required-changes checklist in docs/plan/web-flow/wire-real-paths-security-review.md, §11 callout updated):

  • CRITICALregisterFirstMasterDevice is unauthenticated first-call-wins → front-runnable operator lockout (SidecarRegistry.sol:100-123). Needs an on-chain first-master authorization proof.
  • HIGHregisterAgentDevice / revokeAgentDevice are msg.sender-only, no K11 (SidecarRegistry.sol:214-251) → a compromised master EVM key binds rogue agents with no biometric (contradicts arch.md:608-612).
  • HIGH — add-master K11 challenge omits newActorOmni + K11 cred/pubkey/attestation (SidecarRegistry.sol:167-193) → assertion reuse with substituted params.
  • HIGH — "the phone holds the key" = a software secp256k1 root (Keychain/biometric-ACL, not SE-sealed) — weaker than the K11 hardware promise.
  • HIGH — single global operatorMasterWallet (:66) ⇒ multi-device + recovery story is incomplete.
  • HIGH — browser→host delegation needs a native confirmation that re-derives the K11 challenge + renders calldata (confused-deputy otherwise).
  • MEDIUMAgentKeysScope doesn't update WebAuthn signCount; fork (B) is unsafe until full-intent K11 binding lands on every path.

Also confirms the relayer analysis: a non-custodial relayer under (A) is impossible without meta-tx / ERC-4337 — EIP-2771 sponsors gas but still needs the secp key.

…n from wire demo)

Add docs/plan/web-flow/wire-real-paths.md — an execution plan for turning every
narrated / in-memory-stub path in the parent-control UI + daemon ui-bridge into the
SAME real calls harness/phase1-wire-demo.sh makes (broker auth, broker cap-mint,
on-chain cast writes, S3-backed memory worker).

Key decisions captured:
- Daemon-as-orchestrator: browser → daemon ui-bridge only; daemon makes the real
  broker/chain/worker calls (the data-model.md seam).
- Reuse what exists: the daemon's proxy.rs broker client (reqwest + bearer +
  fail-closed) for broker calls; shell out to the existing agentkeys CLI +
  scripts/heima-*.sh (cast) for chain writes — no new Rust chain client.
- The load-bearing K11 bridge: browser does navigator.credentials WebAuthn → daemon
  injects the assertion into the chain tx via a new --assertion-file mode on
  heima-scope-set.sh (avoids a double Touch ID).
- Per-flow wiring tables (onboarding §9 stages 0–4, pairing §10.2, memory) mapping
  UI surface → data-model.md endpoint → real backend call → arch ref.
- Sequenced phases W0–W6, harness-parity test, and reconciliation of stale specs
  (superseded bootstrap endpoints, --upgrade no-op).

Cross-linked from issue-9step-flow.md as the execution detail for P2.1–P2.4.
…ASM lift scope

Amends the wiring plan for the phone-first reality (most operators have only a
phone, no desktop), keeping one consistent implementation across hosts:

- wire-real-paths.md §0.5 (host-model decision): factor the master-plane logic into
  one portable agentkeys-core hosted as WASM (web) / native lib (mobile, via UniFFI) /
  daemon (desktop), all behind the same lib/client AgentKeysClient contract. The daemon
  is demoted to one host, not a requirement; the broker is the only always-on component;
  the master plane is event-driven + biometric-gated (push-woken).
- wire-real-paths.md §11 (gating decision, VERIFIED): read SidecarRegistry.sol +
  AgentKeysScope.sol. Every master write is msg.sender-bound to the operator secp256k1
  key; the K11 P-256 assertion is an additional gate, not a substitute. => no relayer /
  key-free path without a contract change. Phone holds the secp256k1 key in the Keychain
  (SE is P-256-only, so it seals the K11 passkey, not the EVM key); browser/WASM cannot
  custody it and must delegate the broadcast. Fork (A) keep msg.sender-bound vs (B) move
  to assertion-only auth — recommend (A) for the phone MVP.
- wire-real-paths.md §12 (WASM lift scope): agentkeys-core carve-out, wasm-bindgen
  exports, CoreBackend, WebAuthn interop, chain-write delegation — shared with the future
  mobile UniFFI shell.
- data-model.md: flag the browser-direct prohibition as desktop-first, relaxed for the
  master plane (chain writes still constrained per §11).
- arch.md §22c.3: master control-plane host-model paragraph (defers to the plan).
…A) not sound as written

Codex adversarial review of the §11 gating decision, verified against the contracts.
Verdict: fork (A) is the right direction (contracts are NOT assertion-only-safe) but
NOT sound as written. Findings (full doc: wire-real-paths-security-review.md):
- CRITICAL: registerFirstMasterDevice unauthenticated first-call-wins → front-runnable
  operator lockout (SidecarRegistry.sol:100-123).
- HIGH: registerAgentDevice/revokeAgentDevice are msg.sender-only, no K11 (:214-251) —
  a compromised master EVM key binds rogue agents with no biometric.
- HIGH: add-master K11 challenge omits newActorOmni + K11 cred/pubkey/attestation (:167-193).
- HIGH: 'phone holds the key' = software secp256k1 root (not SE-sealed) — weaker than the
  K11 hardware promise; model it as a first-class key.
- HIGH: single global operatorMasterWallet (:66) ⇒ multi-device/recovery story incomplete.
- HIGH: browser→host delegation needs a native confirmation that re-derives the challenge.
- MEDIUM: AgentKeysScope doesn't update WebAuthn signCount; fork (B) unsafe until full-intent
  K11 binding lands on every path.
Confirms the relayer analysis: non-custodial relayer impossible under (A) without
meta-tx/ERC-4337 (EIP-2771 sponsors gas but still needs the secp key).
§11 recommendation updated to reflect 'not sound as written' + required-changes checklist.
…firmed decision

Supersedes the §11 fork-A-as-MVP framing. The master becomes an ERC-4337 smart account
whose validateUserOp verifies a P-256 (K11/passkey) signature; a bundler broadcasts, an
optional paymaster sponsors gas. Resolves the codex findings:
- removes the software-secp256k1 root (clients sign UserOps with the SE-sealed passkey only);
- key-free + relayer in one (no custodial relayer; bundler + paymaster);
- account address is the stable master → multiple passkeys + quorum recovery (multi-device gap);
- web + mobile become symmetric full masters → the browser→host delegation hop dissolves;
- reuses existing on-chain P-256 verify (K11Verifier.sol).
Residual (folded into the contract-hardening issue): authenticated first-master bootstrap
(CRITICAL), full-intent binding in validateUserOp, and Heima EntryPoint + Solidity-P-256
(London-level, no RIP-7212). Updated §0.5 table, §11 (decision block), §12 (X4/scope),
the security-review doc, and arch.md §22c.3.
…4337, #168 Cancun, #166 bootstrap)

The chain side this plan waited on has landed; reconcile the web-wiring plan with it
and correct two now-false facts.

- Top status banner: ERC-4337 P-256 master account is IMPLEMENTED (#171, Solution A) —
  EntryPoint v0.7 + P256Account + factory + VerifyingPaymaster live on Heima mainnet; #164
  closed. Heima is Cancun, not London (#168). Bootstrap front-run fixed (#166). Defers to
  docs/plan/chain/erc4337-master-account.md.
- §11 (gating): retitled '✅ RESOLVED by #171 (historical)' + a correction note; the
  'does-NOT-auto-fix' block rewritten as 'all addressed' (bootstrap #166; full-intent
  structural via userOpHash; agent-bind/multi-device #171 E3/E4/E5); fixed the false
  'London / no PUSH0' claim → Cancun + pure-Solidity P-256 (~707k gas, no RIP-7212).
- §4.3 + §12 X4: the K11→chain bridge is now an ERC-4337 UserOp (passkey-signs userOpHash
  → broker-gated bundler / handleOps → P256Account); reference erc4337-webauthn-sign.py +
  erc4337-master-e8.sh. Retired the cast/--assertion-file path.
- §5a/§5b: onboarding stage 2 derives the P256Account (CREATE2 from passkey); stage 4 =
  one initCode+registerFirstMasterDevice UserOp (= chain-plan E7, routed through #163);
  bind/grant via UserOp; setScopeWithWebauthn→setScope (E3). §6 W2 + §9 risk updated.
- security-review.md: header marks every finding addressed (#166/#171); Cancun.
- data-model.md phone-first note + arch.md §22c.3: 'decided/London/delegate' → 'implemented
  #171 / Cancun #168 / passkey-signed UserOp'.

Incorporates #163 comments (codex prerequisites + Kailai's Cancun/P-256 feasibility).
@hanwencheng hanwencheng force-pushed the claude/wire-real-data-plan branch from 207f481 to 1b46db4 Compare June 2, 2026 08:08
@hanwencheng hanwencheng merged commit b6e1a38 into main Jun 2, 2026
hanwencheng added a commit that referenced this pull request Jun 2, 2026
First implementation slice of docs/plan/web-flow/wire-real-paths.md (merged #162):
a typed, host-agnostic `agentkeys-core::broker::BrokerClient` that the daemon
ui-bridge, the future WASM CoreBackend (web), and the mobile UniFFI shell all
share — so the browser/phone never re-implement broker calls in TS/Swift
("consistency is structural").

Covers the master-plane endpoints the web wiring proxies:
- cap-mint: /v1/cap/{memory-put,memory-get,cred-store,cred-fetch} → CapToken
- pairing (§10.2 method A, master-side): /v1/agent/pairing/claim,
  /v1/agent/pending-bindings, /v1/agent/pending-bindings/ack

Design: builds on init_flow's conventions (BrokerError{Transport,Rejected,Decode};
trim trailing slash); stores no secret (bearer passed per call); host-agnostic
(no fs/clock/env) so it compiles for the wasm32 browser-fetch target — feature-
gating the crate's reqwest for wasm is the separate X1 build step. The
email/OAuth/SIWE auth flow already lives in init_flow and is unchanged.

Tests: axum-stub server per method (cap round-trip + bearer; pairing claim;
pending→ack; non-2xx → Rejected mapping). cargo test -p agentkeys-core: 145+3 ok;
clippy --all-targets -D warnings clean; fmt clean.

Next (separate PRs): X1 wasm-pack build + reqwest wasm feature-gate; daemon
ui-bridge consumes this client; X2 CoreBackend behind AgentKeysClient.
hanwencheng added a commit that referenced this pull request Jun 2, 2026
… build (W0/X0/X1/X2) (#172)

* feat(core): host-agnostic broker client (W0/X0 of the web-wiring plan)

First implementation slice of docs/plan/web-flow/wire-real-paths.md (merged #162):
a typed, host-agnostic `agentkeys-core::broker::BrokerClient` that the daemon
ui-bridge, the future WASM CoreBackend (web), and the mobile UniFFI shell all
share — so the browser/phone never re-implement broker calls in TS/Swift
("consistency is structural").

Covers the master-plane endpoints the web wiring proxies:
- cap-mint: /v1/cap/{memory-put,memory-get,cred-store,cred-fetch} → CapToken
- pairing (§10.2 method A, master-side): /v1/agent/pairing/claim,
  /v1/agent/pending-bindings, /v1/agent/pending-bindings/ack

Design: builds on init_flow's conventions (BrokerError{Transport,Rejected,Decode};
trim trailing slash); stores no secret (bearer passed per call); host-agnostic
(no fs/clock/env) so it compiles for the wasm32 browser-fetch target — feature-
gating the crate's reqwest for wasm is the separate X1 build step. The
email/OAuth/SIWE auth flow already lives in init_flow and is unchanged.

Tests: axum-stub server per method (cap round-trip + bearer; pairing claim;
pending→ack; non-2xx → Rejected mapping). cargo test -p agentkeys-core: 145+3 ok;
clippy --all-targets -D warnings clean; fmt clean.

Next (separate PRs): X1 wasm-pack build + reqwest wasm feature-gate; daemon
ui-bridge consumes this client; X2 CoreBackend behind AgentKeysClient.

* feat(web-core): WASM-ready broker core + browser CoreBackend + dev.sh build (X0/X1/X2)

Evolves the #172 broker client into the phone-first host model. agentkeys-core is
native-heavy (aws-sdk, keyring) → can't compile to wasm, so the host-agnostic
broker client moves to a new minimal crate that builds for BOTH native and wasm32.

- NEW crate `agentkeys-web-core`: the broker client (moved from agentkeys-core via
  git mv — cap-mint + pairing) + wasm-bindgen exports (`WebCore`, behind `--features
  wasm`). reqwest is default-features=false with the browser fetch backend on wasm32
  and rustls-tls on native (target-gated). crate-type = [cdylib, rlib].
- agentkeys-core: drops the broker module (no consumer yet; future native consumers
  depend on agentkeys-web-core directly). Workspace member + dep added.
- web app: `lib/client/core.ts` — `CoreBackend` lazy-loads the WASM pkg + talks to the
  broker directly (X1); exposes the cap/pairing calls for the onboarding/pairing
  slices. Registered as `NEXT_PUBLIC_AGENTKEYS_BACKEND=core` (default broker
  broker.litentry.org). The AgentKeysClient read endpoints inherit EmptyBackend's
  disconnected state until the later W-phases wire them (honest empty states).
- dev.sh: `build_wasm` step — wasm-pack build, cached by a src+Cargo.toml+wasm-pack
  version hash (skip when unchanged), copies the .wasm to public/wasm; graceful no-op
  if wasm-pack absent. Generated pkg + public/wasm are gitignored.

Verified: cargo test/clippy (-D warnings)/fmt on agentkeys-web-core + agentkeys-core;
wasm32 build + wasm-pack pkg generate; apps/parent-control tsc + next build clean;
bash -n dev.sh.

* fix(web-core): address codex adversarial-review findings (#172)

- core.ts: memoize the WASM core per broker URL (Map) + evict on reject, so a second CoreBackend with a different broker gets its own instance and a transient load/broker failure no longer poisons the cache forever (HIGH).
- broker.rs: native client gets a 20s request timeout (wasm keeps the fetch backend) so a stalled broker can't hang a worker thread (MED).
- broker.rs: bound the echoed broker error body to 512 chars so an oversized response can't bloat a JS rejection/log line; status + endpoint preserved. Adds a regression test (MED).
- dev.sh: stabilize the wasm src-hash -- sort the file list (filesystem order no longer flips the hash) + add workspace Cargo.toml/Cargo.lock + rustc version so a transitive-dep bump busts the cache (MED).

Deferred (tracked): broker CORS for the browser-direct path + typed TS DTOs land with the live web e2e slice. status() stays disconnected by design until the read endpoints are wired.

Verified: cargo test (5 passed) + clippy -D warnings + fmt + wasm-pack build + tsc --noEmit + next build all green.
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.

Complete real browser-side wiring of the master plane (parent-control web app)

1 participant