agentkeys-core: AuditEnvelope v1 cross-language CBOR vector exporter#137
Merged
Conversation
3ff5dad to
476cd49
Compare
Merged
5 tasks
hanwencheng
added a commit
that referenced
this pull request
May 31, 2026
Merged origin/main (#140 IAM strategy reset + #141 agentkeys wire/hook + #137 audit-vector exporter + #138 CI hardening). The merge brought in the Authority-Host / Task-Host model, which obsoletes the prior agent-onboarding design (paste-a-pair-code into a remote sandbox). Redesigned the web-flow plan to match. What changed in the plan: - stage3-agent-usage.md — full rewrite. Agent onboarding is now pair (agentkeys agent device-session — key born in the runtime, never on the master) → wire (agentkeys wire installs 3 IAM-guarantee hooks the LLM can't bypass: pre_tool_call→check, post_tool_call→audit, pre_llm_call→memory-inject) → the three acts (permissioned memory / deterministic denial / audit) + the memory-aware surprise (deterministically backed by `hermes hooks test pre_llm_call`, not a chat reply). Adds the hook-aware live dashboard + guarantee-health panel. Preserves the 16-step isolation health check. - overview.md — new "two-host model" section up top (Authority Host vs Task Host; IAM tool vs IAM guarantee). Act-3 TODO reframed as "Phase 2 — the wire flow." Master onboarding (Phase 1) unchanged. - data-model.md — replaced the bootstrap/* endpoints with the pair/wire/observe surface: /v1/agents/pair/{init,bind,approve-scope}, /v1/agents/:id/{wire, unwire,verify/memory-inject,guarantee-health}, hook-tagged /v1/audit/stream. Notes the per-actor STS relay config + MCP port 18088. - input-discipline.md — §2.5: runtime choice + wire namespaces/payment-scope are Real inputs; the agent device key is born in the runtime (master never holds it); the runtime list reflects real adapter support, never faked. - README.md — redesign banner + updated source-of-truth + file-map row. - dev.sh — MCP_PORT default 8088 → 18088 (8088 collides with the sandbox gem-server, per #141); header comments aligned. Plan only — no implementation. Master-onboarding docs (stage1/stage2) untouched.
4 tasks
hanwencheng
added a commit
that referenced
this pull request
Jun 1, 2026
* m1: parent-control web UI (closes #110) Next.js 14 app under apps/parent-control/ implementing the Phase 1 mobile-responsive parent dashboard for the M1 demo. Six pages, iii.dev-styled (IBM Plex Mono + Serif, cream/ink palette, hairline rules, per-section accent hues): - actors — HDKD tree + devices/agents table + stats strip - actor detail — per-namespace scope toggles (deny/read/read+write), payment-cap inputs, live cap-tokens with per-cap revoke - audit feed — SSE-simulated stream filterable by worker - anchor status — countdown to next tier-2 batch + recent Merkle roots - workers — five worker cards (memory/credentials/audit/email/payment) with per-actor usage share + trust profile - logo — six Bedlington Terrier variants for brand exploration Demo Act 3 path is wired end-to-end: revoke device → K11 WebAuthn modal with intent context (per arch.md §10.1) and mock Touch ID scan → on confirm, actor flips to revoked status and a device.revoked event appears at the top of the audit feed within ~200ms. Stack matches issue #110: Next.js + thin client (no backend in this project). Mock data is inlined for M1; M2 wires to the broker session JWT + audit-service SSE feed (per #109). Port 3113 aligns with arch.md §22c.1 (canonical web-UI surface). When this UI is later folded into agentkeys daemon's `web` subcommand, the URL stays identical. Source: design handoff from claude.ai/design — port preserves visuals 1:1 while splitting the single-file React+Babel prototype into typed TSX modules (types/data/shared/pages/workers/logos/App). * parent-control: extract mocks, empty states, coverage scaffold (PR-A) Foundation for issue #110 follow-up. Removes all inline mock data from the parent-control UI and introduces a single AgentKeysClient interface that every read + write call now flows through. Adds cargo-llvm-cov to CI as a non-blocking artifact (threshold gating arrives in PR-C). # What changed apps/parent-control/lib/client/types.ts AgentKeysClient interface: listActors, getActor, listCapTokens, listRecentAuditEvents, streamAudit, listWorkers, getWorker, getAnchorStatus, updateScope, updatePaymentCap, revokeDevice, revokeCap, enrollK11Begin, enrollK11Finish. Discriminated Result<T> forces every consumer to handle the disconnected variant explicitly. apps/parent-control/lib/client/empty.ts EmptyBackend — default implementation. Every method returns { ok: false, status: { kind: 'disconnected', reason: 'no-backend-configured' } }. No mock data. Operator sees explicit empty states. apps/parent-control/lib/client/index.ts selectBackend() factory. Reads NEXT_PUBLIC_AGENTKEYS_BACKEND; defaults to 'empty'. 'daemon' falls back with a console warning until DaemonBackend lands in PR-C. apps/parent-control/lib/ClientProvider.tsx React context + useClient() / useConnectionStatus() hooks. Wraps the whole app in app/layout.tsx. apps/parent-control/lib/constants.ts NAMESPACES, CHIP_STYLES (config, not mock data). apps/parent-control/app/_components/data.ts DELETED. Was the home of INITIAL_ACTORS, INITIAL_EVENTS, SIM_EVENTS. apps/parent-control/app/_components/App.tsx Rewritten to fetch via useClient() on mount. Subscribes to client.streamAudit. Revoke flows now call client.revokeDevice + client.revokeCap; scope/payment updates call client.updateScope + client.updatePaymentCap with optimistic rollback on rejection. New sidebar section 'onboarding' with two stub pages (full wizard + WebAuthn ceremony land in PR-B). apps/parent-control/app/_components/pages.tsx apps/parent-control/app/_components/workers.tsx Empty-state rendering everywhere a list was previously inlined. ActorsPage, AuditPage take ConnectionStatus prop; WorkersPage owns its own fetch via useClient(). Every empty state explains what daemon endpoint will populate it. apps/parent-control/app/_components/shared.tsx Adds <EmptyState status={...}> component used by every list page. .github/workflows/coverage.yml cargo-llvm-cov via taiki-e/install-action. Runs on every PR that touches crates/**, generates lcov + html, attaches both as artifacts, prints summary to job summary. Non-blocking. Threshold gating lands in PR-C. # Verified - npm run typecheck — clean - npm run build — 4 static pages, 16.5 kB route, 104 kB First Load JS - npm run dev — HTTP 200, empty state renders 'no actors enrolled' + 'No daemon backend configured.' + harness hint; no 'Sara' / 'FoloToy' / mock data in the SSR HTML. # What did NOT land (intentional, per PR-A scope) - DaemonBackend implementation (PR-C) - Real WebAuthn ceremony (PR-B) - Coverage threshold gate (PR-C) - Harness v2-stage1 onboarding wizard (PR-B) - Daemon HTTP endpoints for actors/audit/anchor/workers (PR-C) * parent-control: real WebAuthn onboarding wizard (PR-B) Issue #110 follow-up. Replaces the simulated 'Touch ID scan' modal with a real browser-driven K11 WebAuthn ceremony backed by a new daemon mode and HTTP surface. # Daemon — new ui-bridge mode crates/agentkeys-daemon/src/ui_bridge.rs (new) Dedicated HTTP surface for the parent-control web UI. Binds 127.0.0.1:3114 by default, CORS-allows http://localhost:3113. Routes: GET /healthz POST /v1/k11/enroll/begin → returns PublicKeyCredentialCreationOptions POST /v1/k11/enroll/finish → verifies attestation with webauthn-rs, returns credentialId + chain stub State is in-memory (pending HashMap keyed by user_id). On-chain SidecarRegistry.register_master_device() submission stubbed for M1 (chain_tx_hash returns null); lands in PR-C. crates/agentkeys-daemon/src/main.rs New --ui-bridge mode + 4 args (--ui-bridge-bind / --ui-bridge-origin / --ui-bridge-rp-id / --ui-bridge-rp-name). Independent of --proxy and --master-companion. crates/agentkeys-daemon/Cargo.toml Adds webauthn-rs 0.5, tower-http 0.5 (cors feature), url 2. # Daemon — unit tests (cargo llvm-cov visible) crates/agentkeys-daemon/src/ui_bridge.rs::tests (6 tests, all green) - begin_returns_user_id_and_creation_options - begin_rejects_empty_username - finish_with_unknown_user_id_returns_no_pending - finish_with_malformed_credential_returns_malformed - replay_after_consume_returns_no_pending (verifies pending entry is only consumed once the credential parses; parse-stage failure leaves pending intact so the user can retry) - healthz_returns_ok Run: cargo test -p agentkeys-daemon --bin agentkeys-daemon ui_bridge # UI — DaemonBackend apps/parent-control/lib/client/daemon.ts (new) DaemonBackend implements AgentKeysClient. status() pings /healthz. enrollK11Begin / enrollK11Finish wire to the new daemon endpoints. All other methods return a 'not yet wired' disconnected variant until PR-C lands the read endpoints (actors, audit-SSE, anchor, workers). apps/parent-control/lib/client/index.ts selectBackend() now actually constructs DaemonBackend when NEXT_PUBLIC_AGENTKEYS_BACKEND=daemon. # UI — real browser WebAuthn apps/parent-control/lib/webauthn.ts (new) Helpers: base64url encode/decode, jsonToCreationOptions (server options → navigator.credentials.create() args), credentialToFinishPayload (PublicKeyCredential → daemon /finish JSON), webauthnAvailable + platformAuthenticatorAvailable feature detection. apps/parent-control/app/_components/onboarding.tsx (new) Onboarding wizard mirroring harness/v2-stage1-demo.sh as 8 numbered steps. Step 3 (K11 WebAuthn) is LIVE — clicks 'run' invoke real navigator.credentials.create() via daemon /v1/k11/enroll/begin and ship the attestation to /v1/k11/enroll/finish. Other 7 steps are honestly labeled 'stubbed; lands in PR-C'. apps/parent-control/app/_components/App.tsx Routes /onboarding to the live OnboardingPage (replaces the PR-A stub list). # To exercise the real ceremony $ cargo run -p agentkeys-daemon -- --ui-bridge & $ cd apps/parent-control $ echo 'NEXT_PUBLIC_AGENTKEYS_BACKEND=daemon' > .env.local $ npm run dev # open http://localhost:3113 → 'add device' → step 3 'run' # browser triggers Touch ID / Windows Hello / passkey UI for real # Verified - cargo build -p agentkeys-daemon — clean - cargo test -p agentkeys-daemon --bin agentkeys-daemon ui_bridge — 6/6 green - npx tsc --noEmit — clean - npm run build — 4 static pages, 19.4 kB route, 107 kB First Load # What did NOT land (intentional, per PR-B scope) - Daemon read endpoints (/v1/actors, /v1/audit/stream, etc.) → PR-C - Identity ceremony, K10 gen, SIWE, STS, provision, chain bring-up, on-chain register-master-device wiring → PR-C - Coverage threshold gate (blocking) → PR-C * parent-control: full daemon read/write surface + harness mirror (PR-C) Issue #110 follow-up. Wires the parent-control UI to a real daemon backend end-to-end: read endpoints for actors / audit-SSE / anchor / workers, write endpoints for scope / payment-cap / device-revoke / cap-revoke, and a harness page mirroring the v2-stage2 + v2-stage3 shell scripts. # Daemon — ui-bridge expansion crates/agentkeys-daemon/src/ui_bridge.rs New ApiActor / ApiAuditEvent / ApiCapToken / ApiWorker / ApiAnchorStatus serializable types. New state: actors HashMap, caps HashMap, audit VecDeque (ring buffer, AUDIT_BUFFER_CAP=200), audit_tx broadcast::Sender for SSE, workers HashMap, anchor RwLock. New routes: GET /v1/actors list_actors (sorted master-first) GET /v1/actors/:id get_actor GET /v1/actors/:id/caps list_caps POST /v1/actors/:id/scope update_scope + audit emit POST /v1/actors/:id/payment-cap update_payment_cap + audit emit POST /v1/actors/:id/revoke revoke_device + audit emit + cap clear POST /v1/actors/:id/caps/revoke revoke_cap + audit emit GET /v1/audit/recent?actor_id&limit list_recent_audit (filterable) GET /v1/audit/stream audit_stream (SSE via tokio broadcast) GET /v1/anchor/status anchor_status (dynamic next_anchor_in) GET /v1/workers list_workers GET /v1/workers/:id get_worker POST /v1/dev/seed dev_seed (operator-only data injection) POST /v1/dev/event dev_emit_event (manual audit emit) push_audit() helper ring-buffers + broadcasts in one place. crates/agentkeys-daemon/Cargo.toml Adds futures-util 0.3 + tokio-stream 0.1 (sync feature) for SSE stream wrapping of the broadcast receiver. # Daemon — tests (20 total, all green; previous 6 plus 14 new) list_actors_returns_empty_when_nothing_registered list_actors_returns_master_first get_actor_unknown_returns_404 get_actor_known_returns_payload update_scope_writes_and_emits_audit update_scope_unknown_actor_404 update_payment_cap_writes_and_emits_audit revoke_device_flips_status_and_clears_caps revoke_cap_removes_only_matching_cap_and_emits_audit dev_seed_populates_all_collections list_workers_empty_by_default get_worker_unknown_returns_404 audit_buffer_caps_at_buffer_cap audit_stream_subscribes_before_emit_and_receives Run: cargo test -p agentkeys-daemon --bin agentkeys-daemon ui_bridge # UI — DaemonBackend full wiring apps/parent-control/lib/client/daemon.ts Every AgentKeysClient method now hits a real daemon endpoint: listActors, getActor (404 → null), listCapTokens, listRecentAuditEvents, streamAudit (EventSource on /v1/audit/stream listening for 'audit' events), listWorkers, getWorker, getAnchorStatus, updateScope, updatePaymentCap, revokeDevice, revokeCap, enrollK11Begin, enrollK11Finish. Wire-type translation (snake_case daemon JSON ↔ camelCase UI types) lives in apiToActor / apiToAuditEvent / apiToWorker helpers. normalizeStatus + normalizeChip clamp daemon strings to the UI's StatusKind + ChipKind unions. # UI — harness mirror apps/parent-control/app/_components/harness.tsx (new) New /harness route. Lists every step of v2-stage2-demo.sh (8 steps) and v2-stage3-demo.sh (15 steps) with file:line source pointers and the invariant each step protects (when applicable). Includes the operator runbook (`AGENTKEYS_CHAIN=heima bash harness/v2-stage{1,2,3}-demo.sh`). apps/parent-control/app/_components/App.tsx Sidebar gains 'stage 2 + 3' under 'onboarding'. Routes /harness to HarnessPage. Adds 'harness' to the data-section accent set. # CI — coverage gate now blocking .github/workflows/coverage.yml Removes continue-on-error: true. Adds `cargo llvm-cov report --workspace --fail-under-lines 60`. 60% is a conservative floor — the new ui_bridge.rs module is well above it (20 unit tests covering every handler) so it carries the workspace. Bump in follow-up PRs as other crates' coverage catches up. # Verified - cargo build -p agentkeys-daemon — clean - cargo test -p agentkeys-daemon ui_bridge — 20/20 green - npx tsc --noEmit (apps/parent-control) — clean - npm run build — 4 static pages, 19.6 kB route, 110 kB First Load # To exercise end-to-end $ cargo run -p agentkeys-daemon -- --ui-bridge & $ curl -X POST http://localhost:3114/v1/dev/seed \ -d @docs/dev-fixtures/parent-control-seed.json # (operator can author) $ cd apps/parent-control $ echo 'NEXT_PUBLIC_AGENTKEYS_BACKEND=daemon' > .env.local $ npm run dev # browse http://localhost:3113 — actors, audit-stream, revoke flows # are all live; no mock data anywhere in the codebase # What did NOT land (called out explicitly per CLAUDE.md plan-completion-policy) - Daemon-side wiring of stage-2 + stage-3 harness steps into a live status feed (clickable 'run' per step) — the harness page is a read-only mirror today. Live execution from the UI is a follow-up. - On-chain SidecarRegistry.register_master_device() submission from the K11 enroll/finish handler — still stubbed (chain_tx_hash=null). - Mobile-device cross-device WebAuthn (M5). - Coverage threshold above 60% — bump once non-daemon crates add tests. * plan: docs/plan/web-flow — parent-control operator user flow (Phase 1) Plan-only commit. No implementation. Maps every harness v2-stage{1,2,3} step into a natural operator user flow with real inputs, and locks the Phase 1 scope to overview-Act-1 steps 1–7 (identity → cloud → chain master register). Everything past step 7 is in an explicit TODO list. # What's here docs/plan/web-flow/README.md Index + how to read. docs/plan/web-flow/overview.md End-to-end narrative · 4-screen Phase 1 state machine sketch · Phase 1 endpoint inventory (12 new + 3 shipped) · TODO list for deferred work. docs/plan/web-flow/stage1-first-run.md Harness v2-stage1 steps 6–11 → 4 UI screens A–D. Includes "Part B" on screen C: master vault + memory listings (per user feedback — the operator's own slice of the cloud is visible immediately after provisioning succeeds, separate from any agent inbox). Screens E, F (first agent, done) explicitly deferred to Phase 2. docs/plan/web-flow/stage2-second-master.md Harness v2-stage2 → 6 screens G–L (pair, companion enroll, confirm, quorum, recovery drill, done). Entire stage marked deferred (Phase 3). docs/plan/web-flow/stage3-agent-usage.md Agent bootstrap paths · live ops dashboard · on-demand isolation health check (16-step v2-stage3 against operator's real cloud). Entire stage marked deferred. docs/plan/web-flow/input-discipline.md Real / Derived / Auto-generated triage. §1 resolves the operator-login-email vs agent-inbox-sub-address distinction explicitly (operator types sara@example.com; agent inbox is derived agent-folotoy@bots.litentry.org, system-derived, never operator-typed; email-service worker per arch.md §15.4 routes the agent's mail without touching the operator's inbox). docs/plan/web-flow/data-model.md Daemon HTTP contract. Every endpoint tagged shipped / Phase 1 / deferred. Phase 1 surface is exactly 12 new endpoints + 3 shipped; everything else is called out as deferred to Phase 2 or Phase 3. docs/plan/web-flow/deferred-and-followups.md What stays shell-only · operator-power-user escape hatches · 6 open questions for review (Q3 cross-browser passkey is the only one that blocks Phase 1) · 7-phase implementation sequencing (~9 days estimated). docs/plan/README.md Adds an "Active plans" section pointing at agentkeys-memory-design and web-flow/. # Phase 1 endpoint inventory (the only new endpoints to build) GET /v1/onboarding/state — umbrella state machine POST /v1/auth/email/start — broker-proxy: email magic link POST /v1/auth/email/verify — broker-proxy: magic-token verify GET /v1/auth/email/status — polled by the original tab POST /v1/onboarding/cloud/provision — dispatches 6 existing scripts GET /v1/onboarding/cloud/stream (SSE) — per-script progress POST /v1/onboarding/cloud/smoke — envelope round-trip GET /v1/master/credentials — metadata listing (no plaintext) GET /v1/master/memory — metadata listing (no plaintext) POST /v1/onboarding/chain/deploy — 4 contracts: deploy or detect POST /v1/onboarding/chain/register-master — register_master_device POST /v1/k11/assert/begin — uniform K11 mutation pattern POST /v1/k11/assert/finish Three shipped endpoints (PR-B) used by Phase 1 without changes: GET /healthz POST /v1/k11/enroll/{begin,finish} # Ready for review Verify: - stage docs match the harness scripts (spot-check any step against harness/v2-stage1-demo.sh's `# ─── Step N` headers). - the email distinction in input-discipline.md §1 is correct (arch.md §15.4 email worker routes the agent's mail). - the data-model.md daemon contract doesn't require rewriting any PR-C endpoint — only net-new endpoints + tagging existing ones. * parent-control: scripts/dev.sh — single-terminal dev stack Runs agentkeys-daemon --ui-bridge + Next.js dev server in one terminal with color-prefixed multiplexed logs. Replaces the manual two-terminal setup ("start the daemon in tab A, npm run dev in tab B, env-var the backend kind by hand") with one command. # What it does apps/parent-control/scripts/dev.sh - Bash 3.2 compatible (macOS default /bin/bash). - Kills stale processes on UI_PORT (3113) + DAEMON_PORT (3114). - Auto-rebuilds agentkeys-daemon iff any .rs source is newer than the debug binary (cargo build -p agentkeys-daemon). - Starts the daemon in --ui-bridge mode, streams its stdout/stderr through a magenta [daemon] prefix. - Waits up to 5s for GET /healthz before launching the UI; fails fast with a clear error if the daemon dies during startup. - Pre-sets NEXT_PUBLIC_AGENTKEYS_BACKEND=daemon + NEXT_PUBLIC_AGENTKEYS_DAEMON_URL=http://127.0.0.1:3114 for the Next.js child so the UI talks to the real daemon out of the box. - Starts npx next dev, streams its output through a cyan [ui] prefix. - Polls both PIDs; when either exits, sends SIGTERM to the other. Ctrl-C cleanly tears down both via a single trap. - All script-side status lines wear a bold-yellow [dev] prefix. apps/parent-control/package.json Adds `npm run dev:stack` → `bash scripts/dev.sh`. The plain `npm run dev` remains the UI-only EmptyBackend path. apps/parent-control/README.md New "dev:stack" subsection documenting the color scheme, what the script does, and the env overrides (UI_PORT, DAEMON_PORT, etc.). # Verified $ UI_PORT=3115 DAEMON_PORT=3116 bash apps/parent-control/scripts/dev.sh [dev] starting daemon on http://127.0.0.1:3116 (rp_id=localhost) [daemon] ui-bridge serving bind=127.0.0.1:3116 origin=http://localhost:3115 [dev] daemon ready. [dev] starting Next.js dev server on http://localhost:3115 [ui] ▲ Next.js 14.2.34 [ui] ✓ Ready in 1498ms [ui] GET / 200 in 1118ms ← HTML title: "agentKeys · parent control" curl http://127.0.0.1:3116/healthz → {"ok":true,"surface":"ui-bridge"} Ctrl-C tears both down. Re-run on stale port detects + kills the previous run's leftovers automatically. # Compatibility note Initial draft used `wait -n` which requires bash 4.3+; macOS ships bash 3.2 by default. Replaced with a `kill -0` polling loop so the script runs on `/bin/bash` everywhere. * dev.sh: move single-terminal dev stack to repo root Previously at apps/parent-control/scripts/dev.sh; now at the repo root so the entry point is one path away on a fresh clone. Invocation: bash dev.sh # from the repo root ./dev.sh # from the repo root, same cd apps/parent-control && npm run dev:stack # via npm wrapper apps/parent-control/package.json dev:stack now calls `bash ../../dev.sh`. apps/parent-control/README.md Updated to reference the new root location with all three invocation forms documented. dev.sh (moved + path fixes) REPO_ROOT now resolves from the script's own dirname (which is the repo root). APP_DIR = "$REPO_ROOT/apps/parent-control". Added a preflight check that fails fast with a clear error if the script is copied somewhere that isn't the agentkeys repo root. Verified: $ UI_PORT=3115 DAEMON_PORT=3116 bash dev.sh [dev] starting daemon on http://127.0.0.1:3116 [daemon] ui-bridge serving bind=127.0.0.1:3116 [dev] daemon ready. [ui] ✓ Ready in 1526ms curl http://127.0.0.1:3116/healthz → {"ok":true,"surface":"ui-bridge"} curl http://localhost:3115/ → 200, <title>agentKeys · parent control</title> * dev.sh: harden free_port, add agentkeys-mcp-server to the stack Two changes addressing direct operator feedback. # 1. Harden free_port — fix "Address already in use" after kill Before: free_port sent a single SIGTERM and slept 0.4 s — fast enough to race the kernel's port release, especially when a previous dev.sh run's children were still in TIME_WAIT or hadn't actually shut down. A second dev.sh would crash on: [daemon] Error: ui-bridge: bind TCP 127.0.0.1:3114 [daemon] Caused by: Address already in use (os error 48) After: free_port now does graceful → forceful → verify: 1. Send SIGTERM. 2. Poll up to 3 s waiting for the pid to exit (`kill -0`). 3. If still alive, send SIGKILL. 4. Re-check the port with lsof; abort the script with a clear error if it's still occupied (so the operator can investigate manually rather than hit the same cryptic bind error). # 2. Bring up agentkeys-mcp-server as part of the stack A third process joins the dev stack, with its own line-prefix color: [daemon] magenta agentkeys-daemon --ui-bridge (port 3114) [mcp] green agentkeys-mcp-server (port 8088) [ui] cyan npx next dev (port 3113) [dev] yellow this script's own status lines Defaults: `--backend in-memory` (zero external dependencies — the MCP server auto-seeds the three-act demo fixtures per crates/agentkeys-mcp-server/README.md). `--listen 127.0.0.1:8088`. Overridable via MCP_PORT + MCP_BACKEND. The Next.js child also receives NEXT_PUBLIC_AGENTKEYS_MCP_URL so the UI can call the MCP server once the stage-3 §1 agent-bootstrap flow lands (Phase 2 — see docs/plan/web-flow/stage3-agent-usage.md). # Refactored - build_daemon_if_needed → generic build_if_needed taking a binary path + cargo package name + watched crate dirs. Reused for both the daemon and the mcp-server. - cleanup() now iterates over all three pids. - the "wait for either to exit" poll loop now watches all three. # Verified $ UI_PORT=3115 DAEMON_PORT=3116 MCP_PORT=8089 bash dev.sh [dev] building agentkeys-mcp-server (debug)... [dev] starting daemon on http://127.0.0.1:3116 [daemon] ui-bridge serving bind=127.0.0.1:3116 [dev] daemon ready. [dev] starting mcp-server on http://127.0.0.1:8089 (backend=in-memory) [mcp] agentkeys-mcp-server listening (HTTP) [dev] mcp-server ready. [dev] starting Next.js dev server on http://localhost:3115 [ui] Ready in 1.5 s curl http://127.0.0.1:3116/healthz → {"ok":true,"surface":"ui-bridge"} curl http://127.0.0.1:8089/ → 404 (server listening; bare / unmapped) curl http://localhost:3115/ → 200 * dev.sh: fix free_port multi-pid handling — make it truly idempotent Operator hit: [dev] port :3114 held by pid 57667 90638 — sending SIGTERM [dev] port :3114 is still occupied after SIGKILL — investigate manually [dev] lsof -i tcp:3114 The first line reveals the bug: `lsof -ti tcp:3114` returns ONE pid per line, but a process listening on both IPv4 and IPv6 (or with a shared child) shows up as TWO pids. The previous code captured the multiline string into one variable and then did: kill "$pid" # $pid == "57667\n90638" which is malformed. `kill` errors out silently (the `|| true` suppresses it), so nothing dies. The verification re-checks lsof, sees the pids still there, and aborts the script. Fix: free_port now iterates over each pid individually for both SIGTERM and SIGKILL. Added a second cleanup pass — if any new pid grabbed the port between the kill and the check (rare but possible during daemon restarts), the second pass kills it too. Only after the second pass fails does free_port abort. Verified: $ ./target/debug/agentkeys-daemon --ui-bridge ... & # plant squatter $ lsof -ti tcp:3114 57667 90638 # two pids — reproduces bug $ UI_PORT=3115 DAEMON_PORT=3114 bash dev.sh [dev] port :3114 held by pid 57667 — sending SIGTERM (pass 1) [dev] port :3114 held by pid 90638 — sending SIGTERM (pass 1) [daemon] ui-bridge serving bind=127.0.0.1:3114 [dev] all three processes running. Ctrl-C to stop. $ curl http://127.0.0.1:3114/healthz {"ok":true,"surface":"ui-bridge"} # new daemon, not the squatter The script is now idempotent against any number of stale processes holding the dev ports (3113, 3114, 8088 — or whatever the operator overrides via env). Re-running after a hard kill / lost terminal cleans up the prior run and starts fresh. * dev.sh: silent + clean shutdown — fix Ctrl-C hang, suppress noise Operator hit: ^C [dev] shutting down… ← trap fired, but then script hung (no further output, no prompt, ports still bound) Plus a cosmetic glitch: [dev] \033[2magentkeys-daemon binary is current — skipping build # Root causes + fixes ## 1. Literal "\033[2m" leaked into the build-skip line The printf format used %s for $C_DIM. %s prints the literal string; %b is what interprets backslash escapes. Single-quoted bash strings don't process \033, so $C_DIM stays as the 6-char literal until %b unfolds it. Fixed by reordering the format specifier. ## 2. Ctrl-C hung the script — process substitution kept fds open Previous attempt used `> >(prefix ...)` so $! would resolve to the real binary pid. That fixed pid tracking but introduced a subtler bug: process substitution opens an fd in the parent shell pointed at the reader's stdin. Even after the daemon binary exits, the script still holds that fd open, so the prefix reader never sees EOF, and `wait` blocks forever in cleanup. Fix: switch to named FIFOs in a per-run temp dir ($TMPDIR/agentkeys-dev-stack-$$/). For each process: prefix "$C_X" "x" < "$FIFO_X" & # reader, blocks on FIFO read PREFIX_X_PID=$! disown "$PREFIX_X_PID" "$X_BIN" ... > "$FIFO_X" 2>&1 & # writer; $! = real binary pid X_PID=$! disown "$X_PID" The script itself never opens the FIFO, so kill -> binary exit -> writer fd closes -> reader sees EOF -> reader exits. Clean. ## 3. "Terminated: 15" job-control noise When `wait` reaped the SIGTERM'd children, bash printed termination notices ("dev.sh: line 198: 34855 Terminated: 15 $DAEMON_BIN ..."). These appeared between [dev] shutting down… and [dev] stopped., muddying the operator's view of what happened. Fix: `disown` each backgrounded pid right after capture. Bash drops the job from its job table, so SIGCHLD reaping is silent. Replaced the cleanup's `wait` with a polling loop (`kill -0` in a tight loop) since `wait` doesn't accept disowned pids. ## 4. False "one of the children exited" warning after clean shutdown After cleanup() returned, control fell back to the polling-loop's post-condition where `warn` printed about an unexpected child exit — misleading after an operator-initiated shutdown. Fix: `exit 0` at the end of cleanup() so the script terminates immediately without re-entering the polling loop. ## 5. set +m Added `set +m` at the top to disable job-control monitor mode. With disown this is belt-and-braces, but it removes the last possible source of "[N]+ Done" / "[N]+ Terminated" announcements. # Verified $ bash dev.sh # then SIGTERM the script pid 1 s later ... [dev] all three processes running. Ctrl-C to stop. [ui] ✓ Starting... ^TERM [dev] shutting down… [dev] stopped. 'Terminated' hits in log: 0 'dev.sh: line' hits in log: 0 'one of the children' hits in log: 0 pids on :3113, :3114, :8088: empty after shutdown total shutdown duration: 1 second * plan(web-flow): redesign agent flow around #141 wire/hook model Merged origin/main (#140 IAM strategy reset + #141 agentkeys wire/hook + #137 audit-vector exporter + #138 CI hardening). The merge brought in the Authority-Host / Task-Host model, which obsoletes the prior agent-onboarding design (paste-a-pair-code into a remote sandbox). Redesigned the web-flow plan to match. What changed in the plan: - stage3-agent-usage.md — full rewrite. Agent onboarding is now pair (agentkeys agent device-session — key born in the runtime, never on the master) → wire (agentkeys wire installs 3 IAM-guarantee hooks the LLM can't bypass: pre_tool_call→check, post_tool_call→audit, pre_llm_call→memory-inject) → the three acts (permissioned memory / deterministic denial / audit) + the memory-aware surprise (deterministically backed by `hermes hooks test pre_llm_call`, not a chat reply). Adds the hook-aware live dashboard + guarantee-health panel. Preserves the 16-step isolation health check. - overview.md — new "two-host model" section up top (Authority Host vs Task Host; IAM tool vs IAM guarantee). Act-3 TODO reframed as "Phase 2 — the wire flow." Master onboarding (Phase 1) unchanged. - data-model.md — replaced the bootstrap/* endpoints with the pair/wire/observe surface: /v1/agents/pair/{init,bind,approve-scope}, /v1/agents/:id/{wire, unwire,verify/memory-inject,guarantee-health}, hook-tagged /v1/audit/stream. Notes the per-actor STS relay config + MCP port 18088. - input-discipline.md — §2.5: runtime choice + wire namespaces/payment-scope are Real inputs; the agent device key is born in the runtime (master never holds it); the runtime list reflects real adapter support, never faked. - README.md — redesign banner + updated source-of-truth + file-map row. - dev.sh — MCP_PORT default 8088 → 18088 (8088 collides with the sandbox gem-server, per #141); header comments aligned. Plan only — no implementation. Master-onboarding docs (stage1/stage2) untouched. * parent-control: implement the 9-step operator flow (Claude-design port) Ports the Claude Design "agentkeyweb" handoff into apps/parent-control as the primary, demoable operator experience. Maps 1:1 to the 9 user workflows. Plan + verification + pushback: docs/plan/web-flow/issue-9step-flow.md. Flow (workflow → component): 1. WebAuthn login + onboarding ceremony → ceremony.tsx (OnboardingScreen + CeremonyRunner) 2. Memory panel: plant preserved memory → memory.tsx (empty-state + plant ceremony, auto-detect existing, dedup guard — plant hidden + blocked once planted) 3-4. Agent connects + master notified → App bell + pairing request (post-#149: a pending binding) 5. Request detail (agent + permissions) → pairing.tsx request card 6-7. Accept + Touch ID + ceremony → WebAuthnModal → CeremonyRunner (PAIRING_STEPS) 8. Device view + permission view → pairing.tsx (device-grid) + permissions.tsx (mobile-style scoped PermissionList — replaces tables, the "won't scale" ask) 9. Audit + decodable Heima TXs → dashboard.tsx AuditFeed → EventDecodeModal (decodeCalldata mock; real decode tracked in #153) New files: - lib/demoData.ts seed actors/events + ONBOARDING_STEPS, PAIRING_STEPS, PRESERVED_MEMORY, INCOMING_PAIRING, CHAIN_PROFILE, VAULT_ITEMS, txHash, decodeCalldata (mock), ONCHAIN_KINDS - _components/ceremony.tsx CeremonyRunner (progress bar + live step log + tx hashes) + OnboardingScreen - _components/memory.tsx MemoryPage (plant / dedup / per-namespace listing) - _components/pairing.tsx PairingPage (request → accept → device/permission view toggle) - _components/permissions.tsx PermSeg/PermSwitch/PermissionList/PermissionView (mobile scoped) - _components/dashboard.tsx ActorsList, ActorDetail (editable PermissionList), AuditFeed Changed: - _components/App.tsx rewritten as the self-contained flow orchestrator: onboarding gate (localStorage), header bell + badge, memory/pairing/audit/chain routes, WebAuthn + pairing-ceremony + tx-decode + memory-view modals - _components/types.ts + CeremonyStep/PreservedMemory/PairingRequest/ChainProfile/ ContractInfo/RequestedPerm; Actor.justPaired; ChipKind +scope/device/k11; Route +memory/pairing/chain - lib/constants.ts CHIP_STYLES covers the new chip kinds - app/globals.css ceremony/onboard/empty-memory/pair-req/view-toggle/device-grid/ bell/tx-decode/mem-body/perm-* blocks (no rounded corners, hairline rules) Scope note: M1 visible flow is seed-data + local ceremony state (the prototype's model). Real-daemon wiring stays behind the lib/client seam and is Phase 2 — #149 endpoints (agent create / pending-bindings / bind / grant), onboarding + master-memory endpoints, and the #153 audit decoder. The old client-based pages (pages.tsx/workers.tsx/onboarding.tsx) remain on disk for that wiring; App no longer imports them. Verified: npx tsc --noEmit clean · npm run build ok (4 static pages, 20.1 kB route) · dev smoke serves the onboarding screen (HTTP 200, all markers present). * parent-control: implement pushback #2 — real onboarding WebAuthn + real 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. * parent-control: remove all mock/seed data — client-drive every page + email-first §9 onboarding - demoData.ts: strip INITIAL_ACTORS / INITIAL_EVENTS / SIM_EVENTS / PRESERVED_MEMORY / INCOMING_PAIRING / VAULT_ITEMS / ONBOARDING_STEPS / MASTER_DEVICES. Keep ONLY the audit tx-decode mock (txHash / decodeCalldata / ONCHAIN_KINDS / contractFor, GH #153), PAIRING_STEPS narration, and CHAIN_PROFILE config (contract addresses now placeholder). - App.tsx: actors / events / memory load from the lib/client seam (listActors / listRecentAuditEvents / streamAudit / listMasterMemory). Drop the synthetic SSE tick, the 9s INCOMING_PAIRING timer, pushEvent echoes, and the hardcoded Hermes actor. Revoke routes through client.revokeDevice; the pairing ceremony re-fetches the actor tree. Header identity + connection status derive from real data (no Sara / iPhone / fake block / ttl). - dashboard / memory / permissions: render EmptyState when disconnected (the default EmptyBackend) and neutral empty copy when connected-but-empty. Vault items come via a prop (default []); the memory page is read-only (no fixture-plant button). - ceremony.tsx + types.ts: first-run is the arch.md §9 master-bootstrap ceremony — real email FIRST, then K10 keygen -> email verify -> K11 Touch ID bound MID-ceremony (CeremonyStep.action) -> wallet + SIWE -> register_master_device. There is no separate 'register' step. Verified: tsc --noEmit clean; next build clean (4/4 static pages). * parent-control: plant the PREPARED real memory archive (rework, not fixture) The memory plant button is reworked to import a PREPARED canonical archive through the real client seam (plantMemory -> daemon POST /v1/master/memory/plant, content-hash dedup) instead of the removed kevin.zhao display fixture. - lib/preparedMemory.ts (NEW): PREPARED_MEMORY = the documented demo dataset the rest of the system already uses — the 'Chengdu trip' the wire demo seeds (SEED_MEMORY_CONTENT) plus the per-namespace composition from docs/agent-iam-strategy.md §3.5 (travel / personal / family). Bytes computed from the body; sent without contentHash (the daemon computes + dedups server-side). - memory.tsx: plant button + ceremony return, gated to connected + empty. Disconnected -> EmptyState (no daemon to plant into). Has-memory -> read-only per-namespace list. - App.tsx: planting state + plantMemory/plantDone restored; plantDone calls client.plantMemory(PREPARED_MEMORY) -> re-lists from the daemon. On disconnected it toasts 'Connect a daemon to plant prepared memory' (no client-side faking). Verified: tsc --noEmit clean; next build clean (4/4 static pages). * parent-control: add log out button (header + sidebar) with full session reset - logout() clears the ak_onboarded localStorage flag and resets all in-memory view state (actors / events / memory / planting / pairing / modals / nav) so the next login starts clean, returning to the §9 email/onboarding screen. - Surfaced as a header 'log out' button next to the master identity, and the sidebar 'account' nav item (was the buried 'replay onboarding' partial reset) now calls the same handler. Verified: tsc --noEmit clean; next build clean (4/4 static pages). * parent-control: remove superseded/unused frontend code Drop dead component files left over from the 9-step-flow port (replaced by dashboard.tsx + ceremony.tsx, never imported by App.tsx — the sole entry is page.tsx → App): - app/_components/pages.tsx (→ dashboard.tsx) - app/_components/onboarding.tsx (→ ceremony.tsx OnboardingScreen) - app/_components/harness.tsx (unreferenced) - app/_components/workers.tsx (unreferenced) Cascade cleanup of symbols they were the only consumers of: - shared.tsx: remove TripleToggle (only used by pages.tsx) + AsciiRule (unused); drop now-unused ScopeBits import. - types.ts: remove SimEvent (only fed the deleted SIM_EVENTS) + Route (unused). Verified: tsc --noEmit clean; next build clean (4/4 static pages). * agentkeys-daemon: rustfmt ui_bridge.rs + main.rs (fix cargo fmt --check CI) The master-memory + ui-bridge test code (added earlier in this PR) was committed without rustfmt, failing the 'cargo fmt + clippy + test' required check on #136. cargo fmt --all is behavior-preserving (test bodies only); the companion 'test + clippy' job already passed on this commit. * ci(coverage): fix cargo-llvm-cov report invocation (drop --workspace/test-args) The non-blocking coverage job failed at the report step: error: --workspace is specific to [test,nextest,...] and not supported for subcommand 'report' A newer cargo-llvm-cov rejects --workspace (and trailing -- <test-args>) on the 'report' subcommand. Collect once with 'cargo llvm-cov --no-report --workspace', then emit lcov/html/summary + apply --fail-under-lines 60 via plain 'report' calls. Same artifacts + gate; one test pass instead of four. * agentkeys-daemon: fix 2 clippy lints in ui_bridge (clippy --all-targets -D warnings) The 'cargo fmt + clippy + test' job runs 'cargo clippy --workspace --all-targets -- -D warnings', which (unlike the other clippy job) lints test code and denies warnings. Two pre-existing lints only surfaced once the fmt fix let clippy run: - plant_master_memory: contains_key-then-insert → use the HashMap entry API (clippy::map_entry). Behavior-identical (vacant → insert+plant; occupied → skip). - a test: assert_eq!(..., true) → assert!(...) (clippy::bool_assert_comparison). Verified: cargo fmt --all --check clean; cargo clippy -p agentkeys-daemon --all-targets -- -D warnings clean; cargo test -p agentkeys-daemon passes.
4 tasks
hanwencheng
added a commit
that referenced
this pull request
Jun 4, 2026
…153) Replaces the parent-control web UI's decodeCalldata mock + placeholder contract addresses with real backend decode, end to end. agentkeys-core: - audit/calldata.rs: keccak selectors + minimal head/tail ABI decode/encode for the four stage-1 contracts. Selectors pinned to cast ground truth. - audit/mod.rs: AuditEnvelope::to_json() + decode_envelope_hex(); #137 CBOR vectors are the decode fixtures. - chain_profile.rs + chain-profiles/heima.json: embedded deployed-contract registry + real explorer.heima.network /address/ + /contract/ links. agentkeys-daemon: - audit_decode.rs: decode an audit event → CBOR envelope + on-chain calldata (encode→decode round-trip of real-shaped bytes). - ui_bridge.rs: GET /v1/chain/info + GET /v1/audit/:id/decode. apps/parent-control: client seam getChainInfo()/decodeAuditEvent(); ChainPage renders real addresses, EventDecodeModal renders decoded envelope + typed args. Post-review fixes (codex adversarial review): - calldata.rs: bounds-check bytes32[] length before alloc (OOM/DoS guard). - calldata.rs: register both deployed setScopeWithWebauthn(...,tuple) and current-source setScope/revokeScope(bytes32,bytes32) — src↔deploy divergence. - audit_decode.rs + UI: mark the decode synthesized:true (preview banner) so derived hashes are never mistaken for stored on-chain evidence. - UI: drop the fabricated tx explorer link; link only to the real contract page. - ui_bridge.rs: warn (not silent) on bad-chain profile fallback. Tests: core 157, daemon 66/15/15, tsc clean, clippy -D warnings clean.
hanwencheng
added a commit
that referenced
this pull request
Jun 4, 2026
…153) Replaces the parent-control web UI's decodeCalldata mock + placeholder contract addresses with real backend decode, end to end. Core decode (agentkeys-core): - audit/calldata.rs: keccak selectors + minimal head/tail ABI decode/encode for the four stage-1 contracts; bounds-checked (no OOM); selectors pinned to cast. - audit/mod.rs: AuditEnvelope::to_json() + decode_envelope_hex(); #137 vectors are the decode fixtures. - chain_profile.rs + heima.json: embedded contract registry + real explorer.heima.network /address/ + /contract/ links. Daemon: ui_bridge.rs GET /v1/chain/info + /v1/audit/:id/decode; audit_decode.rs decodes envelope + calldata (encode→decode round-trip), labelled synthesized:true. Web UI: client seam getChainInfo()/decodeAuditEvent(); ChainPage real addresses; EventDecodeModal real decode + preview banner; no fabricated tx links. Contract src↔deploy divergence (codex review + operator follow-up): - The DEPLOYED mainnet AgentKeysScope (0xd44b375…) is the stage-1 setScopeWithWebauthn(...,K11Assertion) design (sel 0x864ae93c / 0x6f37dd80, bytecode-verified). src/AgentKeysScope.sol is the #164 ERC-4337 rewrite (setScope, no inline K11) — deliberately NOT deployed until the master-account cutover. Preserved the live source at crates/agentkeys-chain/archived/AgentKeysScope.deployed-stage1.sol and documented the gap in deployed-contracts.md. src/ unchanged (no #164 revert). - calldata.rs: scope selectors corrected to the real deployed expanded-struct forms; decoder treats `(...)` struct types as opaque tuples. CI deploy fix (real, not papered-over): - setup-broker-host.sh: self-healing rust toolchain health gate — source rustup env, force-install stable, and verify rustc runs (repair once if not) before building. Fixes the test-EC2 'could not execute process rustc … No such file' deploy failure from a partial ~/.rustup. Idempotent; no-op on healthy hosts. Tests: core 157, daemon 66/15/15, tsc clean, clippy -D warnings clean, forge build clean, bash -n clean.
hanwencheng
added a commit
that referenced
this pull request
Jun 4, 2026
…153) Replaces the parent-control web UI's decodeCalldata mock + placeholder contract addresses with real backend decode, end to end. Core decode (agentkeys-core): - audit/calldata.rs: keccak selectors + minimal head/tail ABI decode/encode for the four stage-1 contracts; bounds-checked (no OOM); selectors pinned to cast. - audit/mod.rs: AuditEnvelope::to_json() + decode_envelope_hex(); #137 vectors are the decode fixtures. - chain_profile.rs + heima.json: embedded contract registry + real explorer.heima.network /address/ + /contract/ links. Daemon: ui_bridge.rs GET /v1/chain/info + /v1/audit/:id/decode; audit_decode.rs decodes envelope + calldata (encode→decode round-trip), labelled synthesized:true. Web UI: client seam getChainInfo()/decodeAuditEvent(); ChainPage real addresses; the audit decode is a dedicated page (audit feed → click event → `decode` view with a back button + panels + preview banner), not a modal; no fabricated tx links. Contract src↔deploy divergence (verified on-chain): - DEPLOYED AgentKeysScope (prod 0xd44b375… + test 0x338d68…, both 4572B) is the stage-1 setScopeWithWebauthn(...,K11Assertion) design (sel 0x864ae93c / 0x6f37dd80). src/AgentKeysScope.sol is the #164 ERC-4337 rewrite (setScope) — built by the deploy script but NOT yet deployed (idempotent skip; gated on the master-account cutover). #164's ERC-4337 infra (EntryPoint, factory) IS live. Live source preserved at archived/AgentKeysScope.deployed-stage1.sol; gap documented in deployed-contracts.md. calldata.rs decodes both forms with the real deployed selectors. CI deploy fix: setup-broker-host.sh self-healing rust toolchain health gate — verify rustc runs (repair once if not) before building. Fixes the test-EC2 'could not execute process rustc … No such file' failure. Idempotent. Tests: core 157, daemon 66/15/15, clippy -D warnings clean, forge build clean, bash -n clean. App tsc: changed files clean (the only 2 errors are the gitignored, dev.sh-generated lib/wasm bindings absent in CI checkouts).
hanwencheng
added a commit
that referenced
this pull request
Jun 6, 2026
…153) Replaces the parent-control web UI's decodeCalldata mock + placeholder contract addresses with real backend decode, end to end. Core decode (agentkeys-core): - audit/calldata.rs: keccak selectors + minimal head/tail ABI decode/encode for the four stage-1 contracts; bounds-checked (no OOM); selectors pinned to cast. - audit/mod.rs: AuditEnvelope::to_json() + decode_envelope_hex(); #137 vectors are the decode fixtures. - chain_profile.rs + heima.json: embedded contract registry + real explorer.heima.network /address/ + /contract/ links. Daemon: ui_bridge.rs GET /v1/chain/info + /v1/audit/:id/decode; audit_decode.rs decodes envelope + calldata (encode→decode round-trip), labelled synthesized:true. Web UI: client seam getChainInfo()/decodeAuditEvent(); ChainPage real addresses; the audit decode is a dedicated page (audit feed → click event → `decode` view with a back button + panels + preview banner), not a modal; no fabricated tx links. Contract src↔deploy divergence (verified on-chain): - DEPLOYED AgentKeysScope (prod 0xd44b375… + test 0x338d68…, both 4572B) is the stage-1 setScopeWithWebauthn(...,K11Assertion) design (sel 0x864ae93c / 0x6f37dd80). src/AgentKeysScope.sol is the #164 ERC-4337 rewrite (setScope) — built by the deploy script but NOT yet deployed (idempotent skip; gated on the master-account cutover). #164's ERC-4337 infra (EntryPoint, factory) IS live. Live source preserved at archived/AgentKeysScope.deployed-stage1.sol; gap documented in deployed-contracts.md. calldata.rs decodes both forms with the real deployed selectors. CI deploy fix: setup-broker-host.sh self-healing rust toolchain health gate — verify rustc runs (repair once if not) before building. Fixes the test-EC2 'could not execute process rustc … No such file' failure. Idempotent. Tests: core 157, daemon 66/15/15, clippy -D warnings clean, forge build clean, bash -n clean. App tsc: changed files clean (the only 2 errors are the gitignored, dev.sh-generated lib/wasm bindings absent in CI checkouts).
hanwencheng
added a commit
that referenced
this pull request
Jun 6, 2026
…153) Replaces the parent-control web UI's decodeCalldata mock + placeholder contract addresses with real backend decode, end to end. Core decode (agentkeys-core): - audit/calldata.rs: keccak selectors + minimal head/tail ABI decode/encode for the four stage-1 contracts; bounds-checked (no OOM); selectors pinned to cast. - audit/mod.rs: AuditEnvelope::to_json() + decode_envelope_hex(); #137 vectors are the decode fixtures. - chain_profile.rs + heima.json: embedded contract registry + real explorer.heima.network /address/ + /contract/ links. Daemon: ui_bridge.rs GET /v1/chain/info + /v1/audit/:id/decode; audit_decode.rs decodes envelope + calldata (encode→decode round-trip), labelled synthesized:true. Web UI: client seam getChainInfo()/decodeAuditEvent(); ChainPage real addresses; the audit decode is a dedicated page (audit feed → click event → `decode` view with a back button + panels + preview banner), not a modal; no fabricated tx links. Contract src↔deploy divergence (verified on-chain): - DEPLOYED AgentKeysScope (prod 0xd44b375… + test 0x338d68…, both 4572B) is the stage-1 setScopeWithWebauthn(...,K11Assertion) design (sel 0x864ae93c / 0x6f37dd80). src/AgentKeysScope.sol is the #164 ERC-4337 rewrite (setScope) — built by the deploy script but NOT yet deployed (idempotent skip; gated on the master-account cutover). #164's ERC-4337 infra (EntryPoint, factory) IS live. Live source preserved at archived/AgentKeysScope.deployed-stage1.sol; gap documented in deployed-contracts.md. calldata.rs decodes both forms with the real deployed selectors. CI deploy fix: setup-broker-host.sh self-healing rust toolchain health gate — verify rustc runs (repair once if not) before building. Fixes the test-EC2 'could not execute process rustc … No such file' failure. Idempotent. Tests: core 157, daemon 66/15/15, clippy -D warnings clean, forge build clean, bash -n clean. App tsc: changed files clean (the only 2 errors are the gitignored, dev.sh-generated lib/wasm bindings absent in CI checkouts).
hanwencheng
added a commit
that referenced
this pull request
Jun 6, 2026
…153) (#194) Replaces the parent-control web UI's decodeCalldata mock + placeholder contract addresses with real backend decode, end to end. Core decode (agentkeys-core): - audit/calldata.rs: keccak selectors + minimal head/tail ABI decode/encode for the four stage-1 contracts; bounds-checked (no OOM); selectors pinned to cast. - audit/mod.rs: AuditEnvelope::to_json() + decode_envelope_hex(); #137 vectors are the decode fixtures. - chain_profile.rs + heima.json: embedded contract registry + real explorer.heima.network /address/ + /contract/ links. Daemon: ui_bridge.rs GET /v1/chain/info + /v1/audit/:id/decode; audit_decode.rs decodes envelope + calldata (encode→decode round-trip), labelled synthesized:true. Web UI: client seam getChainInfo()/decodeAuditEvent(); ChainPage real addresses; the audit decode is a dedicated page (audit feed → click event → `decode` view with a back button + panels + preview banner), not a modal; no fabricated tx links. Contract src↔deploy divergence (verified on-chain): - DEPLOYED AgentKeysScope (prod 0xd44b375… + test 0x338d68…, both 4572B) is the stage-1 setScopeWithWebauthn(...,K11Assertion) design (sel 0x864ae93c / 0x6f37dd80). src/AgentKeysScope.sol is the #164 ERC-4337 rewrite (setScope) — built by the deploy script but NOT yet deployed (idempotent skip; gated on the master-account cutover). #164's ERC-4337 infra (EntryPoint, factory) IS live. Live source preserved at archived/AgentKeysScope.deployed-stage1.sol; gap documented in deployed-contracts.md. calldata.rs decodes both forms with the real deployed selectors. CI deploy fix: setup-broker-host.sh self-healing rust toolchain health gate — verify rustc runs (repair once if not) before building. Fixes the test-EC2 'could not execute process rustc … No such file' failure. Idempotent. Tests: core 157, daemon 66/15/15, clippy -D warnings clean, forge build clean, bash -n clean. App tsc: changed files clean (the only 2 errors are the gitignored, dev.sh-generated lib/wasm bindings absent in CI checkouts).
hanwencheng
added a commit
that referenced
this pull request
Jun 11, 2026
…pe 40/41 + device 50/51 (broker submit-relay decode) (#270) * feat: #97 phase F — control-plane audit emits: sign 20/21 (CLI) + scope 40/41 + device 50/51 (broker decodes the confirmed executeBatch) Six control-plane op_kinds of the AuditEnvelope v1 rollout, on the #261 data-plane substrate: - core: erc4337::decode_execute_batch (inverse of the composer, bounds-checked before allocation per the codex #153 lesson) + ScopeGrantBody/ScopeRevokeBody aligned to the #164/#225 set-replace setScope (pre-first-emit schema fix — bytes 40/41 never emitted under the per-service draft; invariant #7 freezes numbers, not unreleased drafts). §15.3b roundtrip tests for the scope + device families. - broker: handlers/audit_emit.rs — after a confirmed /v1/{accept,scope,revoke}/submit receipt, decode what LANDED and emit DeviceAdd (ROLE_CAP_MINT, zero attestation) / ScopeGrant (full replacement set) / ScopeRevoke (empty set) / DeviceRevoke (actor = operator). Omnis from calldata (on-chain truth). Best-effort + WARN by design: the tx is already final, unlike the data-plane REQUIRE_AUDIT case where the response releases data. Responses gain audit_envelope_hashes. - cli: agentkeys signer sign / sign-typed-data emit SignEip191/712 (712 carries the PR #95 ERC-7730 intent_text + intent_commitment when --preview-7730 rendered one); output gains audit_envelope_hash. EIP-191 digest golden-pinned vs cast hash-message. - daemon: representative scope.grant/scope.revoke bodies follow the new shape; #137 vector exporter updated in lockstep. - setup-broker-host.sh: explicit AGENTKEYS_AUDIT_WORKER_URL loopback line on the broker unit (parity with the #261 worker env blocks). - arch.md §15.3a: table rows + the control-plane LIVE section + the schema-alignment note. * style: cargo fmt — format the #97 control-plane audit emit code (CI fmt gate) * fix: elide the needless lifetime in audit_emit::arg (CI clippy -D warnings on latest stable) * fix: adapt #97 audit-emit tests to the #260 fleet-revoke signature (device_key_hashes slice; N envelopes per fleet batch) * feat: #97 — surface the audit receipts in the web app (display + submit paths) The control-plane submits now produce AuditEnvelope receipts; this wires them through the daemon to the operator UI: - daemon: ApiAuditEvent gains optional tx_hash + audit_envelope_hashes; the accept/scope submit proxies parse the broker response and push feed events carrying the real receipts (device.paired / scope.grant — these flows previously pushed NO event); the verified unpair threads receipts through RevokeDeviceRequest into its device.revoked event. /v1/audit/:id/decode now FETCHES the real envelope(s) by receipt hash from the audit worker (--audit-worker-url / AGENTKEYS_AUDIT_WORKER_URL, default the public worker; injected via state per the no-env-in-tests rule) and overlays synthesized:false + envelopes[]; preview fallback when no receipt / worker unreachable. 3 new unit tests incl. a hermetic closed-port fallback test. - web: typed SubmitResult (tx_hash, audit_envelope_hashes, pending) for accept/scope/revoke submits; receipts shown in the success toasts + threaded into revokeDevice; the audit decode page renders EVERY fetched envelope (accept = DeviceAdd + ScopeGrant), a green real-vs- amber preview provenance banner, and tx + receipt rows on the event panel. - docs: user-manual §'Audit receipts' (operator-facing behavior incl. the independent curl verify) + arch.md §15.3a receipt-surfacing note.
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.
Summary
Adds
crates/agentkeys-core/examples/export_audit_vectors.rs— an exporter that emits canonical-CBOR test vectors forAuditEnvelope v1, one per op_kind, as{ envelope_json, canonical_cbor_hex, envelope_hash_hex }.This is the reproducible source for the cross-language determinism contract the Heima explorer (
litentry/subscan-essentials) needs. The explorer hand-ports our canonical CBOR encoder into Go (internal/agentkeys) and TS; re-ports drift (two encoder bugs were already caught this way — see subscan #12). These vectors let the explorer's CI assert byte-for-byte parity against the reference encoder instead of reconstructing the spec from issue prose.Coverage: 10 typed op_kinds — including the three subscan #12 requires end-to-end (
SignEip712=21,ScopeGrant=40,DeviceAdd=50) — plus anUnknown(250)canary for non-break invariants #1/#4.Delivered to the explorer team
The generated artifacts were posted directly to the explorer repo issues (chosen delivery channel — a public gist was deliberately avoided):
CredentialAuditABI + event topic0 table → AgentKeys live ABI acceptance gaps: op_kind filter, root path, and envelope proof subscan-essentials#42 (comment)payloadHash-vs-V2-envelopeHashworker-404) → AgentKeys live ABI acceptance gaps: op_kind filter, root path, and envelope proof subscan-essentials#42 (comment)Why this lives on
main(not a feature branch)AuditEnvelope v1(crates/agentkeys-core/src/audit/) is onmain; the exporter is general tooling co-located with the code it exercises. Verified the example builds and produces byte-identical output againstmain(45f04262).Test plan
cargo run -p agentkeys-core --example export_audit_vectors→ 11 vectors, exit 0cast keccak <canonical_cbor_hex>==envelope_hash_hex(confirmed for vector 0:0x68753389…d3d0d4)