You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Today the master's "accept pairing · Touch ID" button (parent-control → pairing) does a deployer-signed registerAgentDevice via scripts/heima-agent-create.sh — there is no real on-chain K11 gate. The button label promises Touch ID, but the dev master is an EOA (scripts/heima-register-first-master.sh, deployer-signs), so the agent-device binding is authorized by the deployer key, not by the operator's Secure-Enclave passkey.
Most of the ERC-4337 P-256 smart-account infra to fix this already landed in #171/#200 (closes #164): contracts E0–E6 + E8 + live mainnet infra (E7 + the cutover remain — see the plan-mapping section below). What's missing is making the master be a passkey-account and routing the browser accept through it, so registerAgentDevice is authorized by an on-chain-verified K11 (WebAuthn P-256 / Touch ID) signature instead of the deployer EOA.
This was split out of the #216 wire work after the interim "server-verified K11 gate" approach was rejected in favor of doing the gate on-chain (the real authority boundary per arch.md §11).
Where this sits in the #164 ERC-4337 plan — this is E7 + the cutover, NOT a new design
P256Accountis the ERC-4337 plan (docs/plan/chain/erc4337-master-account.md, "Solution A"); this epic continues that plan, it does not re-invent it. Plan status: E0–E6 + E8 ✅ — EntryPoint v0.7 + CREATE2 account factory live (E1), production P256Account (E2), SidecarRegistry/AgentKeysScope master-writes thinned to msg.sender == masterAccount with setScopeWithWebauthn→setScopein code (E3/E4), guardian M-of-N recovery (E5), broker-co-signed VerifyingPaymaster sponsorship (E6), and an E8 mechanism proof that ran green on Heima mainnet (a passkey-only UserOp landed addSigner via handleOps). What remains, per the plan, is E7 (ceremony binding) — "CLI hardware register WIRED; browser frontend + coordinated cutover remain" — and the cutover (redeploy factory+registry+scope to account-auth; flip heima-scope-set.shsetScopeWithWebauthn→setScope).
This epic = E7's browser item + the cutover, applied to the agent-accept mutation, with register+scope batched.
It reuses #200's sponsored-UserOp machinery — same envelope, batched callData.#200 ("#164 sponsored ERC-4337 register") landed Stage A: the broker crates/agentkeys-broker-server/src/sponsor.rs co-sign core — a VerifyingPaymaster-sponsored UserOp, broker-EIP-191-co-signed (the J1-gated Sybil gate) = gas-free. The accept batch UserOp is the same sponsored envelope as #200's register; only the inner callData changes from a single registerAgentDevice to executeBatch([register, setScope]). #200 explicitly deferred Stage B (EntryPoint.handleOps submission — needs an EVM client); E8 proved direct handleOps works and scripts/erc4337-bundler.sh is the always-on layer (not stood up live). This epic depends on Stage B for submission.
Today's gap (why this isn't done): the daemon accept (register_pairing → heima-agent-create.sh --from-pubkey, crates/agentkeys-daemon/src/ui_bridge.rs) is a deployer-signed cast send registerAgentDevice that bypasses the entire 4337 stack — no UserOp, no passkey, no sponsor. E7 replaces that with the sponsored, batched, K11-signed UserOp.
Browser accept triggers a real WebAuthn assertion (Touch ID) over the accept UserOp hash — navigator.credentials.get() with the enrolled K11 credential. No assertion ⇒ no register (the gate the button label already promises).
Accept submits ONE atomic batch UserOp — P.2 (register) + P.3 (scope) together (see the design note below). The daemon builds P256Account.executeBatch([registerAgentDevice, setScope]), attaches the single P-256 WebAuthn signature, and submits via EntryPoint.handleOps. This replaces both the deployer-signed heima-agent-create.sh shell-out in register_pairing AND the separate heima-scope-set.sh scope step (crates/agentkeys-daemon/src/ui_bridge.rs).
Design note — atomic P.2 + P.3 in a single block (the "one block" win)
All building blocks already exist on-chain; batching is daemon-side UserOp assembly with zero contract changes:
P256Account.executeBatch(address[] dest, uint256[] value, bytes[] func) (src/P256Account.sol:191) runs N inner calls from one UserOp. Its _call (src/P256Account.sol:203) re-throws on any inner revert, so the batch is all-or-nothing.
Both inner targets are already msg.sender == master-gated — SidecarRegistry.registerAgentDevice (src/SidecarRegistry.sol:271) and AgentKeysScope.setScope (src/AgentKeysScope.sol:82). When the master account fans out via executeBatch→_call, msg.sender is the master account for both, so both guards pass unchanged. (Use setScope, the msg.sender-gated variant — NOT setScopeWithWebauthn, which is the EOA/cast path that carries the assertion in calldata.)
One K11 signature gates the whole batch. The assertion validates the UserOp at P256Account validation, not per inner call → a single Touch ID covers register + scope.
Result: 2 txs → 1, 2 blocks → 1, 2 Touch IDs → 1, and atomicity — the partial-pairing failure (device registered, scope grant fails, agent stuck at 0 namespaces) becomes structurally impossible. Batching is NOT possible on the EOA path (an EOA tx has a single to), which is the additional reason this rides the passkey-account migration rather than shipping standalone.
Acceptance
Clicking accept in the web UI raises a real Touch ID prompt; cancelling it aborts the whole batch (observable: no registerAgentDevice tx, pending request unchanged, scope unchanged).
A single on-chain tx (one EntryPoint.handleOps, one block) performs BOTH registerAgentDevice and setScope, sent from the passkey-account master, its UserOp carrying a P-256 signature that EntryPoint + the account's P-256 verifier accept — verifiable on-chain (the signer is the K11 pubkey, not the deployer EOA).
Atomicity proven: a forced setScope revert (e.g. malformed scope) rolls back the registerAgentDevice in the same tx — no half-paired agent (harness negative step).
scripts/verify-heima-contracts.sh + a harness step prove the end-to-end: enroll K11 → deploy passkey-account master → accept (Touch ID) → one batched UserOp lands register+scope, authorized by K11; the agent immediately shows its granted memory:<ns> namespaces.
The interim server-verified gate is NOT built (explicitly rejected — gate lives on-chain).
~XL — spans onboarding (passkey-account deploy), browser WebAuthn assertion over a UserOp hash, daemon executeBatch UserOp assembly + EntryPoint.handleOps submission, and a harness proof (incl. the atomic-rollback negative). Best done as its own epic after the #216/#214 wire stabilizes. The batch itself adds ~no contract work — executeBatch + both msg.sender == master guards already exist.
Context
Today the master's "accept pairing · Touch ID" button (parent-control → pairing) does a deployer-signed
registerAgentDeviceviascripts/heima-agent-create.sh— there is no real on-chain K11 gate. The button label promises Touch ID, but the dev master is an EOA (scripts/heima-register-first-master.sh, deployer-signs), so the agent-device binding is authorized by the deployer key, not by the operator's Secure-Enclave passkey.Most of the ERC-4337 P-256 smart-account infra to fix this already landed in #171/#200 (closes #164): contracts E0–E6 + E8 + live mainnet infra (E7 + the cutover remain — see the plan-mapping section below). What's missing is making the master be a passkey-account and routing the browser accept through it, so
registerAgentDeviceis authorized by an on-chain-verified K11 (WebAuthn P-256 / Touch ID) signature instead of the deployer EOA.This was split out of the #216 wire work after the interim "server-verified K11 gate" approach was rejected in favor of doing the gate on-chain (the real authority boundary per arch.md §11).
Where this sits in the #164 ERC-4337 plan — this is E7 + the cutover, NOT a new design
P256Accountis the ERC-4337 plan (docs/plan/chain/erc4337-master-account.md, "Solution A"); this epic continues that plan, it does not re-invent it. Plan status: E0–E6 + E8 ✅ — EntryPoint v0.7 + CREATE2 account factory live (E1), productionP256Account(E2),SidecarRegistry/AgentKeysScopemaster-writes thinned tomsg.sender == masterAccountwithsetScopeWithWebauthn→setScopein code (E3/E4), guardian M-of-N recovery (E5), broker-co-signedVerifyingPaymastersponsorship (E6), and an E8 mechanism proof that ran green on Heima mainnet (a passkey-only UserOp landedaddSignerviahandleOps). What remains, per the plan, is E7 (ceremony binding) — "CLI hardware register WIRED; browser frontend + coordinated cutover remain" — and the cutover (redeploy factory+registry+scope to account-auth; flipheima-scope-set.shsetScopeWithWebauthn→setScope).This epic = E7's browser item + the cutover, applied to the agent-accept mutation, with register+scope batched.
It reuses #200's sponsored-UserOp machinery — same envelope, batched callData. #200 ("#164 sponsored ERC-4337 register") landed Stage A: the broker
crates/agentkeys-broker-server/src/sponsor.rsco-sign core — aVerifyingPaymaster-sponsored UserOp, broker-EIP-191-co-signed (the J1-gated Sybil gate) = gas-free. The accept batch UserOp is the same sponsored envelope as #200's register; only the innercallDatachanges from a singleregisterAgentDevicetoexecuteBatch([register, setScope]). #200 explicitly deferred Stage B (EntryPoint.handleOpssubmission — needs an EVM client); E8 proved directhandleOpsworks andscripts/erc4337-bundler.shis the always-on layer (not stood up live). This epic depends on Stage B for submission.Today's gap (why this isn't done): the daemon accept (
register_pairing→heima-agent-create.sh --from-pubkey,crates/agentkeys-daemon/src/ui_bridge.rs) is a deployer-signedcast send registerAgentDevicethat bypasses the entire 4337 stack — no UserOp, no passkey, no sponsor. E7 replaces that with the sponsored, batched, K11-signed UserOp.Scope
/v1/k11/enroll/{begin,finish}) becomes the smart-account's authorized signer. Deploy the ERC-4337 P-256 smart-account (ERC-4337 P-256 smart-account master (#164): plan + contracts E0–E8 + live mainnet infra & demo #171 contracts) for the master at onboarding; persist its address in the master session (rides Persist + rehydrate the master session across daemon restart #220's persist/rehydrate).navigator.credentials.get()with the enrolled K11 credential. No assertion ⇒ no register (the gate the button label already promises).P256Account.executeBatch([registerAgentDevice, setScope]), attaches the single P-256 WebAuthn signature, and submits viaEntryPoint.handleOps. This replaces both the deployer-signedheima-agent-create.shshell-out inregister_pairingAND the separateheima-scope-set.shscope step (crates/agentkeys-daemon/src/ui_bridge.rs).memory:<ns>set the operator approves is thesetScopeargument in the batch. (Depends on Wire web-app agent pairing to the real claim → register → scope flow #214 wiring the requested scope through the claim form, today empty.)Design note — atomic P.2 + P.3 in a single block (the "one block" win)
All building blocks already exist on-chain; batching is daemon-side UserOp assembly with zero contract changes:
P256Account.executeBatch(address[] dest, uint256[] value, bytes[] func)(src/P256Account.sol:191) runs N inner calls from one UserOp. Its_call(src/P256Account.sol:203) re-throws on any inner revert, so the batch is all-or-nothing.msg.sender == master-gated —SidecarRegistry.registerAgentDevice(src/SidecarRegistry.sol:271) andAgentKeysScope.setScope(src/AgentKeysScope.sol:82). When the master account fans out viaexecuteBatch→_call,msg.senderis the master account for both, so both guards pass unchanged. (UsesetScope, themsg.sender-gated variant — NOTsetScopeWithWebauthn, which is the EOA/cast path that carries the assertion in calldata.)P256Accountvalidation, not per inner call → a single Touch ID covers register + scope.Result: 2 txs → 1, 2 blocks → 1, 2 Touch IDs → 1, and atomicity — the partial-pairing failure (device registered, scope grant fails, agent stuck at 0 namespaces) becomes structurally impossible. Batching is NOT possible on the EOA path (an EOA tx has a single
to), which is the additional reason this rides the passkey-account migration rather than shipping standalone.Acceptance
registerAgentDevicetx, pending request unchanged, scope unchanged).EntryPoint.handleOps, one block) performs BOTHregisterAgentDeviceandsetScope, sent from the passkey-account master, its UserOp carrying a P-256 signature thatEntryPoint+ the account's P-256 verifier accept — verifiable on-chain (the signer is the K11 pubkey, not the deployer EOA).setScoperevert (e.g. malformed scope) rolls back theregisterAgentDevicein the same tx — no half-paired agent (harness negative step).scripts/verify-heima-contracts.sh+ a harness step prove the end-to-end: enroll K11 → deploy passkey-account master → accept (Touch ID) → one batched UserOp lands register+scope, authorized by K11; the agent immediately shows its grantedmemory:<ns>namespaces.Dependencies / relations
docs/plan/chain/erc4337-master-account.md) — E7 (browser ceremony) + the coordinated cutover, applied to agent-accept. Not a parallel design.sponsor.rsco-sign core (Stage A, landed) supplies the gas-free sponsored envelope; depends on feat: #164 sponsored ERC-4337 register + v2-demo harness restructure #200's Stage B (EntryPoint.handleOpssubmission EVM client, still a follow-up) for actual submission.heima-scope-set.shsetScopeWithWebauthn→setScope. The batch'ssetScopecall needs the post-cutover account-auth scope contract live.chain_tx_hash) — the master-side analog of this binding.707k→3.4k gas; not a hard blocker (the ERC-4337 P-256 smart-account master (#164): plan + contracts E0–E8 + live mainnet infra & demo #171 Solidity verifier works today) but the natural optimization once it lands.register+scopesteps from deployer-EOA + two-tx into a single on-chain-K11 batch UserOp; Wire web-app agent pairing to the real claim → register → scope flow #214 also supplies the requested-scope plumbing the batch'ssetScopeconsumes.Effort
~XL — spans onboarding (passkey-account deploy), browser WebAuthn assertion over a UserOp hash, daemon
executeBatchUserOp assembly +EntryPoint.handleOpssubmission, and a harness proof (incl. the atomic-rollback negative). Best done as its own epic after the #216/#214 wire stabilizes. The batch itself adds ~no contract work —executeBatch+ bothmsg.sender == masterguards already exist.