feat: #225 E7 — ERC-4337 accept-batch callData builders (atomic P.2+P.3)#227
Open
hanwencheng wants to merge 25 commits into
Open
feat: #225 E7 — ERC-4337 accept-batch callData builders (atomic P.2+P.3)#227hanwencheng wants to merge 25 commits into
hanwencheng wants to merge 25 commits into
Conversation
…st live infra) The agent-facing consumer of the #216 cred-fetch primitive, verified end-to-end against the LIVE broker + cred worker: - agentkeys-cli: `agentkeys cred fetch <service>` (cred_admin.rs) — mints a master-self/agent CredFetch cap → BackendClient.cred_fetch → STS → cred worker → decrypt → prints the plaintext. Adds the agentkeys-backend-client dep (the #204 one-owner path; no re-typed wire shapes). - harness/cred-fetch-demo.sh — the real e2e: a master VAULTS a probe cred via the daemon (web path), then the agent FETCHES it via the CLI (agent path), asserting the EXACT secret round-trips through cap-mint → STS → cred worker → S3 → decrypt. Idempotent (fixed `cred-e2e-probe`), --ci-tolerant, real-only. Contract-compliant (STEP_TOTAL=4, ok/skip/fail, EXIT-trap daemon cleanup). - keep-docs-in-sync: harness/CLAUDE.md orchestrator table + operator-runbook-harness.md. VERIFIED LIVE (this run): master vaulted via daemon (HTTP 200), agent `cred fetch` returned the EXACT key (len matched) — broker.litentry.org + cred.litentry.org. #216's cred half is proven, not just compiled. Remaining #216: the Hermes wire (phase1-wire Phase 4.0) — plant the fetched key into Hermes instead of $OPENROUTER_API_KEY (the full sandbox surprise).
… live, real LLM) Carries the #216 cred-fetch through the Hermes wire — the complete agent-side guarantee, proven end-to-end against the LIVE broker + cred worker + aiosandbox: master VAULTS the LLM key (daemon: cap-mint cred-store → STS → cred worker → S3) → agent CRED-FETCHES it (agentkeys cred fetch: cap-mint cred-fetch → STS → decrypt) → plant into Hermes (~/.hermes/.env + hermes config set model.*) IN THE SANDBOX → Hermes RUNS on the vault key (real LLM smoke) — NO OPENROUTER_API_KEY in the agent env harness/cred-wire-demo.sh (STEP_TOTAL=6, contract-compliant, headless): asserts the key Hermes uses == the master-vaulted key (sha), and that it arrived via the vault fetch, not an ambient env var (the sandbox shell has no OPENROUTER_API_KEY; the .env value is the cred-fetch result). The durable, no-Touch-ID complement to phase1-wire-demo.sh Phase 4.0b — same wire result without the interactive gates. Routes through the shared agentkeys-backend-client (#204). VERIFIED LIVE (this run, real OpenRouter key): step 4 ok agent fetched the vaulted key from the vault (len=73, sha fddff3ff…) — no env read step 5 ok planted the vault-fetched key into ~/.hermes/.env + hermes config step 6 ok 6.1 vault-sourced — the key Hermes will use == the master-vaulted key, NOT an env var step 6 ok 6.2 llm smoke — Hermes answered using the VAULT-FETCHED key: "OK" Exit 0. A REAL deepseek-v4-flash call via OpenRouter answered "OK" on the vault-fetched key — #216's acceptance ("the agent runs on MY authorized key, not the operator's env") proven with real data. Idempotent (FIXED openrouter service; the .env key-line is rewritten not appended); daemon killed on exit; --ci-tolerant. keep-docs-in-sync: harness/CLAUDE.md + docs/operator-runbook-harness.md.
…→ dev fallback) Replaces the operator-env-key write (#216's named target: phase1-wire-demo.sh:1072) with the vault path: Phase 4.0b now fetches the agent's LLM key from the master's VAULT via `agentkeys cred fetch cred:<service>` and plants THAT into the sandbox Hermes — the $OPENROUTER_API_KEY/$LLM_API_KEY env becomes a clearly-labelled DEV-ONLY fallback. - Phase 4.0b resolves WIRE_KEY VAULT-FIRST (the agent-identity cred-fetch: operator session authorizes, actor=agent device — mirrors the memory cap-mint identity model), env-fallback only when the vault is unavailable. Backward-compatible: with no vaulted key / no cred scope the fetch fails and it degrades to the env key exactly as before, so the change is fallback-safe. - SEED_SCOPE_SERVICES also grants the agent its cred scope (bare `$SERVICE` — the cred-fetch cap-mint hashes the bare service, unlike memory's `memory:<ns>`) so the P.3 pairing grant authorizes the vault fetch. - Honest labelling throughout: the 0.6 step, the header, and the top overview now state the env key is the dev fallback and the vault is primary; the 4.0 ok line prints which source the planted key came from. The full vault chain (master vaults → agent cred-fetches → plant → Hermes runs on it, real LLM smoke) is proven headless + live by harness/cred-wire-demo.sh (this PR). The interactive agent-identity path additionally needs the operator's Touch ID cred-scope grant (P.3) + a seeded vault — until then Phase 4.0b labels + uses the dev fallback.
…n fix (verified live) Completes the CLI cred surface with the store half of `cred fetch`, and folds the daemon's hand-rolled cred-store body into the crate (closing a #204 drift gap): - agentkeys-backend-client: `CredStoreBody`/`CredStoreResp`/`CredStoreInput`/ `CredStoreResult` (mirror the CredFetch types) + `BackendClient::cred_store` (cap-mint CredStore → per-actor STS under the VAULT role → cred worker `/v1/cred/store` → encrypt + S3 PUT). Exported from the crate. - agentkeys-daemon: `store_master_credential_inner` now builds the worker body from the crate-owned `CredStoreBody` instead of an inline `serde_json::json!({...})` (#204 — "broker/worker request shapes have ONE owner"; a drifted field is now a compile error, matching the memory-put path). - agentkeys-cli: `agentkeys cred store <service> --secret|--secret-env` (master-self by default). `--secret-env NAME` keeps the plaintext off argv / out of the shell history + process list. Prints the worker S3 key. VERIFIED LIVE (CLI-only store→fetch round-trip, master-self): stored `cred-store-probe` → bots/941…/credentials/cred-store-probe.enc ✅ CLI store→fetch ROUND-TRIP PASS — agentkeys cred store works end-to-end Scope note: this is the master-self vault primitive. The master provisioning a key INTO the agent's S3 prefix (so the agent fetches with actor=agent) needs dual bearers (operator session for cap-mint + agent session for the STS PrincipalTag) and is #214's authorization-side job — deliberately out of #216 scope. clippy -D warnings clean; cargo check green.
…web app + CLI, fresh start) Restructures the wire runbook from a CLI/sandbox + memory-only "run the demo" doc into the single fresh-start guide for testing the WHOLE wire — both the #216 vault-fetched LLM key and the permissioned memory — two ways: - New top: the two guarantees, a two-paths table (web app vs CLI, same agent side), the fastest test (`harness/cred-wire-demo.sh`), and a fresh-start checklist (3 setup scripts + sandbox + OpenRouter key + master identity). - Path A — Web app: `bash dev.sh` → onboard → vault the key (credentials page) → pair+authorize (pairing page, Touch ID). Honest "wired vs pending" note: the web vault + #214 pairing are real/on-chain today; the agent-identity vault-fetch needs #214's dual-bearer master-provisioning (not wired yet), so the master-self cred-wire-demo is the end-to-end proof. - Path B — CLI: the existing phase1-wire-demo walkthrough, reframed. - LLM-key gate now documents Phase 4.0b vault-first/env-fallback; "Verifying it worked" splits into the two deterministic checks; +3 web/cred troubleshooting rows; Appendix B gains the `cred store`/`cred fetch` primitives; cross-refs add the new demos + #216/#214 + dev.sh. keep-docs-in-sync: folds back the cred-wire-demo + cred-store + Phase 4.0b changes from this PR into the operator runbook.
Caught in review: Path A had the agent run in the sandbox (agentkeys-daemon --request-pairing → cred fetch → wire hermes) but never said how the compiled agentkeys / agentkeys-daemon / agentkeys-mcp-server binaries get INTO the sandbox. They can't run there unless cross-built for the sandbox's Linux arch and uploaded (the sandbox is aarch64/x86 Linux, not the operator's Mac) — which is what Path B / phase1-wire-demo.sh Phase 1 does (target/sandbox-linux cross-build → sbx_put). Rewrote Path A to be honest: - The web app is ONLY the master's console; it does not provision the agent device. - A. Vault the LLM key — fully standalone (no sandbox). - B. Pair — needs the agent binaries in the sandbox first; and phase1-wire's Phase 1 bundles the cross-build/upload WITH the CLI pairing (Phase P lives inside Phase 1), so there's no clean "binaries only" command and no one-command web-pairing flow yet (drive the web claim by hand: upload binaries, open a request, claim in the UI). - C. End-to-end is the headless cred-wire-demo.sh / Path B. Also corrected my own first attempt, which suggested `--skip-2..5` to "stage only the sandbox" — that still runs Phase 1 and therefore CLI-pairs the agent.
…t + add sandbox-build-push.sh Per review: the runbook treated Path A as leaning on Path B's harness for the agent side. Now each path is a self-contained quick-start. - NEW harness/sandbox-build-push.sh — Path A's standalone "compile agentkeys + push to the sandbox" command. Cross-builds the 3 binaries (agentkeys / -mcp-server / -daemon) for the sandbox's aarch64-Linux arch in the SAME cached arm64 builder image + cargo volumes phase1-wire-demo uses (warm tree re-pushes in seconds), uploads them to ~/.local/bin. Build + push ONLY — never pairs/wires. Re-run after any local change so the in-sandbox agent runs current source. VERIFIED live: pushed to the sandbox, and `agentkeys cred --help` there confirms the current #216 source. - operator-runbook-wire.md restructured: "Two independent paths — pick one" with BRIEF quick-starts for each (Path A = sandbox-build-push.sh + dev.sh + 3 UI actions; Path B = one phase1-wire-demo command) + a "neither path" headless check (cred-wire-demo). Path A details now use sandbox-build-push.sh (dropped the phase1-wire dependence + the now-moot "harness bundles pairing" caveat); kept the honest #214 wired-vs-pending note. - keep-docs-in-sync: harness/CLAUDE.md inventory + operator-runbook-harness.md.
…broker-url Operator hit `Error: --broker-url (or AGENTKEYS_BROKER_URL) required for --request-pairing` running the runbook command in the sandbox — my Path A command dropped the required flag. Verified the corrected invocation in the live sandbox (produces a pairing_code). Folded the complete, correct flow into Path A: 1. sandbox: agentkeys-daemon --request-pairing --broker-url https://broker.litentry.org → prints pairing_code + a state_file (the request_id lives in the file, not stdout) 2. web UI: claim the pairing_code (Touch ID) 3. sandbox: agentkeys-daemon --retrieve-pairing --request-id <from state file> --broker-url … Matches phase1-wire-demo.sh Phase P.0/P.1b exactly. Fixed both the quick-start and the Path A — details command.
…needed) `agentkeys-daemon --request-pairing` / `--retrieve-pairing` required --broker-url (or AGENTKEYS_BROKER_URL) and errored without it — friction for the Path-A operator running them in the sandbox. These commands ALWAYS need a broker, so default it: - main.rs: new `const DEFAULT_PAIRING_BROKER_URL = "https://broker.litentry.org"`; run_request_pairing + run_retrieve_pairing now `unwrap_or_else(default)` instead of erroring. `--broker-url` / `AGENTKEYS_BROKER_URL` still override (e.g. a test broker). Deliberately NOT a global arg default — `--ui-bridge`'s unset broker_url keeps its "fall back to pre-sourced AWS creds" meaning (the §191 pre-Stage-7 path). VERIFIED live: cross-built + pushed the daemon to the sandbox; `agentkeys-daemon --request-pairing` (no flag) now defaults to prod + opens a §10.2 request (code 9ZpC8nwu…) — the "--broker-url required" error is gone. Runbook (Path A quick-start + details) simplified to drop the flag; notes the prod default + the override. clippy -D warnings clean; daemon tests green.
…-create.sh
`accept pairing · Touch ID` POSTed /v1/agent/pairing/register and got 502. Root
cause: register_pairing derived the agent-register script as a SIBLING of
--register-master-script, but the two are NOT co-located — dev.sh's master register
is harness/scripts/heima-register-first-master.sh while heima-agent-create.sh lives
in <repo>/scripts/. The sibling path (harness/scripts/heima-agent-create.sh) doesn't
exist, so `bash <missing>` exited non-zero → register_agent_device errored → 502.
Fix: resolve heima-agent-create.sh from candidates — the sibling (co-located case)
AND <repo>/scripts/ derived from the master script path — picking the first that
exists; fail with a clear SERVICE_UNAVAILABLE message if neither is found.
Verified: scripts/heima-agent-create.sh accepts exactly the args register_agent_device
passes (--label/--agent-address/--actor-omni/--device-key-hash/--pop-sig, from-pubkey
mode auto-detected), and a dry-run with the live agent details returns
{"ok":true,"skipped":"already-registered"} → register_agent_device → Ok(None) → 200.
The "no Touch ID" is expected (browser passkey UserOp is the E7-pending frontend item;
the register goes through the daemon script shell-out today). clippy -D warnings clean;
daemon tests green.
…ude/216-agent-wire
…ull request_id (slice 1) The master pairing card showed a truncated "PAIR-CODE" that was actually the request_id (never the agent's one-time code), with no value the operator could cross-check against the agent — a confused-deputy surface (#224). Slice 1 surfaces the values that ARE on both sides today, with no broker change/deploy: - daemon (pending_binding_to_request): map the broker's device_key_hash → `deviceKeyHash` (+ short); keep `id` (the full request_id). The agent's `--request-pairing` already prints device_key_hash + D_pub, so these are the cross-verifiable identity. - agent (run_request_pairing): print device_key_hash on the human-facing line so the operator reads it off the agent to compare. - frontend (PairingRequest type + pairing card): replace the misleading "pair-code" with **device key hash · verify on agent** + **D_pub · verify on agent** (full) + **request id** (full handle). Operator confirms the device matches before accept · Touch ID. - test: pending_binding_maps_to_pairing_request asserts the full deviceKeyHash. Deferred to slice 2 (needs a broker change + deploy): created_at/expires_at timestamps on the card (the broker pending row has no timestamps today) and the `--force` supersede-prior-requests behavior. clippy/fmt clean; daemon tests + frontend typecheck green.
…ual reload acceptPairing did registerPairing + refreshPairing but never re-fetched the actor tree, so a freshly-registered agent only appeared in the device/permission views after the operator reloaded the page. Re-fetch listActors after a successful register (matches finishPairingCeremony), surfacing the paired device immediately.
The agent-accept gate (#225 / #164 E7) lands the device binding (registerAgentDevice, P.2) and the scope grant (setScope, P.3) in ONE P256Account.executeBatch UserOp — one block, one K11 signature, atomic. This adds the pure callData encoders that the batch needs (the genuinely new primitive); the sponsored-UserOp envelope is already owned by the broker's sponsor.rs (#200 Stage A). New crates/agentkeys-core/src/erc4337.rs: - register_agent_device_calldata — registerAgentDevice(bytes32,bytes32,bytes32,bytes,bytes) - set_scope_calldata — setScope(bytes32,bytes32,bytes32[],bool,uint128,uint128,uint128,uint32) - execute_batch_calldata — executeBatch(address[],uint256[],bytes[]) - accept_batch_calldata — the headline: executeBatch([register, setScope]); threads the agent's actor_omni into both inner calls so they can't disagree on which agent they bind. Hand-rolled ABI (no alloy/ethabi — matches sponsor.rs/audit::calldata style), reusing the public audit::calldata::selector so selectors never drift. Golden-tested byte-for-byte against foundry cast for all three: cast calldata "registerAgentDevice(bytes32,bytes32,bytes32,bytes,bytes)" ... cast calldata "setScope(bytes32,bytes32,bytes32[],bool,uint128,uint128,uint128,uint32)" ... cast calldata "executeBatch(address[],uint256[],bytes[])" "[reg,scope]" "[0,0]" "[reg_cd,scope_cd]" fixtures committed under src/testdata/. cargo test + clippy green. First slice of #225; the submission client (#200 Stage B), the daemon wiring, the browser ceremony, and the on-chain cutover remain (tracked in #225).
Ties the two existing halves into one ready-to-sign PackedUserOperation: - intent: agentkeys_core::erc4337::accept_batch_calldata (the atomic executeBatch([registerAgentDevice, setScope]), P.2+P.3) - sponsorship: the broker EIP-191 co-signs the VerifyingPaymaster getHash (J1-gated Sybil gate = gas-free), via crate::sponsor (#200 Stage A). New crates/agentkeys-broker-server/src/sponsored_accept.rs: - AcceptUserOpParams — every chain-derived value (nonce/gas/fees/validity/addrs) is an explicit input (nothing hardcoded; caller reads them on-chain). - assemble_accept_userop(params, broker_sk) -> AssembledAcceptUserOp { user_op, user_op_hash, paymaster_get_hash }. Sets paymasterAndData[20:52] (the gas word) provisionally so paymaster_get_hash commits the limits the broker approves, then rebuilds paymasterAndData with the real co-sign appended; computes the userOpHash the master K11 signs. Pure (broker key only, no chain I/O). Broker-side because the paymaster co-sign needs the broker key; the daemon will call this via an endpoint and just K11-sign the returned userOpHash (the #200 division of labour). 3 unit tests: callData==accept batch + sender==master + empty account sig + deterministic hash; paymasterAndData layout + broker co-sign recovers to the broker EOA; grant change => userOpHash change. cargo test + clippy green. Slice 2 of #225. Next: the broker HTTP endpoint wrapping this + the daemon call + the Stage-B handleOps submit (cast-based, mirrors the E8 proof). Refs #225.
Defines the daemon<->broker protocol for the on-chain K11-gated accept, in the ONE owner crate per the #204 rule (the daemon deps backend-client; the broker mirrors these shapes server-side, pinned by the frozen key-set tests): - BuildAcceptUserOpRequest — POST /v1/accept/build (J1_master): register fields (device_key_hash, agent_pop_sig, link_code_redemption) + the granted scope (services + u128 caps as wire-safe decimal strings + period_seconds). - WireUserOp — ERC-4337 v0.7 PackedUserOperation, hex per field; mirrors broker sponsor::PackedUserOp. The daemon fills with the master K11 assertion over user_op_hash. - BuildAcceptUserOpResponse — { user_op, user_op_hash, entry_point, chain_id }. - SubmitAcceptUserOpRequest / SubmitAcceptUserOpResponse — POST /v1/accept/submit → EntryPoint.handleOps (Stage B), returns { ok, tx_hash, block_number }. Fixtures regenerated via dump-protocol-fixtures + frozen key-set tests for the three request bodies (build_accept_userop_request, wire_user_op, submit_accept_userop_request). cargo test + clippy + fixture --check green. Slice 3 of #225. Next: the broker /v1/accept/{build,submit} handlers (mirror these shapes server-side, gate on J1, call assemble_accept_userop) + the daemon call + K11-sign. Refs #225.
The connective piece the broker accept handler returns: convert the internal
sponsor::PackedUserOp into the hex-encoded wire shape and shape the build body.
crates/agentkeys-broker-server/src/sponsored_accept.rs:
- WireUserOp — broker-side mirror of backend_client::protocol::WireUserOp (the
broker doesn't dep that crate; frozen key-set tests on both sides pin them).
- WireUserOp::from_packed — hex-0x each PackedUserOp field.
- BuildAcceptResponse + AssembledAcceptUserOp::into_build_response — the
/v1/accept/build body { user_op, user_op_hash, entry_point, chain_id }.
3 unit tests: every wire field round-trips back to the original bytes; the build
response carries the accept-batch callData + the userOpHash + entry_point/chain_id;
WireUserOp JSON keys match the backend-client frozen shape (server-side #204 pin).
cargo test + clippy green.
Slice 4 of #225. Next (the I/O layer, happy-path gated on a deployed P256Account
master): the axum /v1/accept/{build,submit} routes — J1_master auth (mirror
mint_cap) + eth_call operatorMasterWallet/getNonce + assemble_accept_userop +
into_build_response; submit relays EntryPoint.handleOps. Refs #225.
The precise, idempotent spec for the live-mainnet cutover that unblocks the #225 e2e (PR #227's /v1/accept flow needs the master to BE a deployed P256Account, not the current EOA). docs/plan/chain/account-auth-cutover.md specifies: - The gap: registry/scope sources are account-auth in code (E3) but the LIVE bytecode is pre-E3; heima-bring-up's cast-code idempotency check skips the redeploy, so account-auth never goes live. - The consequence (loud): DeployAgentKeysV1 redeploys to NEW addresses → all on-chain state (master, agents, scopes, epoch, audit) resets → full re-bootstrap; demo breaks until re-bootstrapped. Operator-gated, announced, NOT in the plain flow. - 6 phases (pre-flight → redeploy v2 set FORCE_DEPLOY → redeploy P256AccountFactory → onboarding-as-account → re-bootstrap actors → code/doc updates → broker redeploy), each idempotent with explicit skip checks. - Idempotency strategy for a REDEPLOY (cast-code alone is insufficient since the old contracts also have code): a CUTOVER_DONE_<profile> marker + a live setScope account-auth ABI capability probe. - The two scripts to implement (heima-cutover-account-auth.sh + heima-deploy-master-account.sh), the setup-heima.sh --cutover-account-auth wiring, the #201 env 3-file discipline, rollback (restore the .pre-cutover.bak env), and the arch.md §10/§12 + deployed-contracts.md sync owed at Phase 5. Refs #225. Scopes the cutover named in erc4337-master-account.md §3.1.
… master-as-account Diligence correction to the cutover spec after finding the onboarding-as-account step already exists: - Phase 3 (onboarding-as-account) reuses the existing `erc4337-register-master.sh` (build+submit) — it already does factory.createAccount + EntryPoint-deposit + register-first-master-as-account, idempotently. Dropped the proposed (redundant) `heima-deploy-master-account.sh`; only ONE new script remains (the cutover orchestrator `heima-cutover-account-auth.sh`). - Decoupling finding (from that script's header): master-as-account is VIABLE on the LIVE pre-cutover registry (no EOA-only guard), so operatorMasterWallet[omni] can be the P256Account TODAY — no disruptive redeploy needed for that half. The cutover is only required for the accept batch's setScope (P.3): the live scope has setScopeWithWebauthn, not the msg.sender-gated setScope. So work can stage: register master-as-account now + exercise /v1/accept/build against it; do the registry/scope redeploy only when account-auth setScope is needed e2e. Refs #225.
…er orchestrator) The one new script the cutover spec calls for. Forces a redeploy of the v2 set so the account-auth sources (E3) go live, making the #225 accept batch's setScope (P.3) real. Idempotent + safe + bash -n clean. scripts/heima-cutover-account-auth.sh: - Phase 0: pre-flight (assert local AgentKeysScope.sol is account-auth — setScope present, setScopeWithWebauthn gone) + back up the env addresses to operator-workstation.env.pre-cutover.bak (idempotent: skip if present). - Phase 1: redeploy via FORCE_DEPLOY=1 heima-bring-up.sh, then verify + set the CUTOVER_DONE_<profile> marker. DESTRUCTIVE → gated behind --yes; refuses otherwise. Idempotency ground truth is a read-only probe: the live scope bytecode carrying the setScope selector d8e9e3c6 (the marker is just the fast path). - Phase 2: factory CHECK only (E5 recover() isn't needed for accept; no reusable factory-deploy helper exists, so it doesn't blind-deploy). - Prints the follow-ups: re-register master-as-account (erc4337-register-master.sh), re-bootstrap agents/scopes, the repo edits (heima-scope-set.sh→setScope, arch.md), broker redeploy (setup-broker-host.sh --ref main). Classified as a directly-callable SURGICAL helper (the three-entry-points exemption for destructive heima-*-revoke/-rotate tools) — NOT wired into setup-heima.sh's plain flow, since a plain run must never reset on-chain state. Spec updated to match. Verified: bash -n clean; --help + unknown-arg guard work; setScope selector d8e9e3c6 confirmed against the earlier cast golden vectors. Cannot run e2e here (live mainnet redeploy). Refs #225.
…chain show The script died immediately with "no RPC" because it used a made-up resolution (AGENTKEYS_CHAIN_RPC_HTTP — a broker-runtime var — plus an invented RPC_HTTP_HEIMA fallback), neither of which operator-workstation.env carries. Diagnosis: both heima-bring-up.sh:122 and setup-heima.sh:195 resolve the chain RPC the same way — `agentkeys chain show "$CHAIN" | jq -r .rpc.http` (no RPC env key exists). Switched to that; added jq + agentkeys to the tool pre-check. Verified live: it now resolves https://rpc.heima-parachain.heima.network and runs to the destructive --yes gate. Also: back up the env to $HOME/.agentkeys/<name>.pre-cutover.bak instead of next to the git-tracked operator-workstation.env (a .bak there would surface as untracked). Verified the backup lands in ~/.agentkeys and leaves git status clean. Other assumptions re-checked against reality (correct): the SCOPE/REGISTRY/FACTORY address keys exist in operator-workstation.env; the profile suffix uses the sibling idiom tr 'a-z-' 'A-Z_'; the phase-0 guard holds (source AgentKeysScope.sol has setScope, no setScopeWithWebauthn). Refs #225.
…t-cutover re-bind path) Adds docs/operator-runbook-account-auth-cutover.md — the full 5-step operator procedure for the disruptive cutover, in the operator-runbook-*.md convention (H1, > warning blocks, ordered steps, rollback). Writing it surfaced a correctness bug in the earlier spec + the script's printed follow-ups: post-cutover, agent binding + scope grants go through ACCOUNT UserOps (the #225 accept flow), because account-auth gates registry/scope writes on msg.sender == operatorMasterWallet (the P256Account). The pre-cutover scripts do NOT work post-cutover — verified: - heima-agent-create.sh sends registerAgentDevice from the deployer EOA (≠ the account); - heima-scope-set.sh calls setScopeWithWebauthn (the assertion-in-calldata path account-auth removes; the new setScope is msg.sender-gated, no assertion param). So the runbook leads with two warnings: (1) SEQUENCING — run the cutover only AFTER the #225 accept flow is wired, else agents are stranded (you can re-register the master but not re-bind agents); (2) DESTRUCTIVE — state reset → full re-bootstrap. Corrected to match: - spec Phase 4 (re-bind = #225 accept flow, not heima-agent-create/heima-scope-set); - spec Phase 5 (drop the bogus heima-scope-set.sh setScopeWithWebauthn→setScope edit — it's a pre-cutover tool, retired post-cutover; just arch.md §10/§12 + deployed-contracts.md); - the script's printed follow-ups (point at the #225 accept flow + the new runbook). Verified: script bash -n clean; runbook H1/no-frontmatter/warnings present. Refs #225.
…re-onboard) + fix step-4 command
Per the "no user, only developer, register again" reality:
- Reframe: nothing to migrate. The cutover proper is redeploy + verify + broker redeploy
(steps 1-3); registering the master + pairing agents (4-5) is just normal onboarding on
the fresh contracts, not a special re-bootstrap. Dropped the "DESTRUCTIVE / announce +
schedule / state NOT migrated" alarm.
- Master register is still REQUIRED (the new registry is empty → registerAgentDevice would
revert OperatorNotRegistered), but it's one command, not the placeholder build/submit dance:
bash harness/scripts/erc4337-register-master.sh --operator-omni 0x<omni> # auto Touch ID
The old step-3 block was not executable (raw 0x<…>/<N> placeholders + a hand-wavy "K11 signs
the userop_hash"). The default `register` mode auto-runs k11 webauthn-keygen + webauthn-userop-sign;
the build/submit two-phase split is only for the browser web-flow.
- Synced the script's printed follow-ups + the spec Phase 3 to the one-command form.
Refs #225.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
#225 — on-chain K11-gated agent-accept (#164 plan E7 + the account-auth cutover): the accept becomes one
P256Account.executeBatch([registerAgentDevice, setScope])UserOp — P.2 + P.3 in one block, one K11 signature, atomic.This PR is the Rust foundation + the cutover tooling/spec. All Rust is pure + CI-verifiable; the cutover script is
bash -n-clean (its live run is operator-gated).What landed
Rust foundation (all unit-tested):
crates/agentkeys-core/src/erc4337.rs— callData builders (register_agent_device/set_scope/execute_batch/accept_batch_calldata), golden-tested byte-for-byte vs foundrycast.crates/agentkeys-broker-server/src/sponsored_accept.rs—assemble_accept_userop: composes the batch callData + the brokerVerifyingPaymasterco-sign (feat: #164 sponsored ERC-4337 register + v2-demo harness restructure #200 Stage A) into a completePackedUserOperation+ theuserOpHashto K11-sign.crates/agentkeys-backend-client/src/protocol.rs— the daemon↔broker/v1/accept/{build,submit}wire types (refactor: #203 agentkeys-backend-client — ONE owner for the broker/worker chain #204 one-owner), frozen-keyset tests + regenerated fixtures.sponsored_accept.rs—PackedUserOp→WireUserOpconversion + the/v1/accept/buildresponse builder (round-trip + server-side frozen-key tests).Cutover tooling + spec (the e2e unblock):
5.
docs/plan/chain/account-auth-cutover.md— the precise, idempotent, phased cutover spec, with the consequence (redeploy → state reset → full re-bootstrap) called out, and the decoupling finding: the master can be registered as aP256Accounton today's contracts (via the existingerc4337-register-master.sh), so only the accept batch'ssetScopehalf actually needs the disruptive redeploy.6.
scripts/heima-cutover-account-auth.sh— the one new script: forces the account-auth v2-set redeploy (FORCE_DEPLOY=1 heima-bring-up.sh), idempotent via aCUTOVER_DONE_<profile>marker + a read-onlysetScope-selector (d8e9e3c6) bytecode probe,--yes-gated (destructive), surgical-helper classified.bash -nclean.cargo test+clippygreen acrosscore,broker-server,backend-client.What did NOT land (remaining #225 work)
/v1/accept/{build,submit}axum routes — mirror the wire shapes server-side, J1_master auth (likemint_cap),eth_calloperatorMasterWallet/getNonce, callassemble_accept_userop+into_build_response; submit relayshandleOps. Needs the broker co-sign key + new env (entry_point/paymaster/gas) wired through the Config data class + lazy, config-driven memory list (Phases 1–5) #201 3-file env discipline./v1/accept/build→ K11-sign →/v1/accept/submit; replaces the deployer-cast sendinregister_pairing.ceremony.tsxTouch ID over theuserOpHash. Hardware; operator-verified.heima-cutover-account-auth.sh --yeson live mainnet + re-bootstrap (operator), OR the no-disruption partial path (register master-as-account today). Live-mainnet operator action.Refs #225. Continues #164 / #171 / #200.
🤖 Generated with Claude Code