refactor: #203 agentkeys-backend-client — ONE owner for the broker/worker chain#204
Merged
Merged
Conversation
…rker chain The broker/worker HTTP chain was hand-coded in three places (MCP HttpBackend, daemon ui_bridge, harness bash), the structural cause of the #200 drift bugs (evm_address vs {address,chain_id}, bare-vs-0x omni, per-namespace field shapes). Collapse it behind one crate so drift is a COMPILE error (Rust callers share the types) or a FIXTURE mismatch (the harness gate), not a runtime 4xx. New crate agentkeys-backend-client (the dual of broker-server / worker-*): - protocol.rs: every cap-mint / worker / audit wire shape, the memory:<ns> service builder, and the 0x-omni normalizer (the daemon's old inline bug site) - client.rs: BackendClient — cap-mint (4 data-class endpoints) -> STS relay -> worker put/get -> audit append (the reference impl lifted out of HttpBackend) - fixtures.rs + dump-protocol-fixtures bin: canonical fixtures serialized from the serde types + frozen key-set pins Collapse the duplicates (net -355 LOC in existing files): - MCP HttpBackend -> thin delegate over BackendClient; backend wire-shape submodules (broker/memory/audit) deleted, re-exported from the crate so the Backend trait + InMemoryBackend + tools keep their crate::backend::* paths - daemon memory_put_real / real_memory_ctx -> call the shared client (kills the duplicate cap-mint body + the inline 0x-normalize where the bugs lived) Enforce (fold-systemic-fixes-into-enforcement): - scripts/check-backend-fixture-drift.sh: diffs every # @backend-fixture- annotated bash body against the crate-emitted fixtures (catches add/rename/drop) - harness-ci.yml rust-checks runs the fixture --check + the bash gate on every PR touching crates/**, harness/**, scripts/** - root CLAUDE.md + harness/CLAUDE.md "broker/worker shapes have ONE owner" rule; arch.md component inventory updated
…parity ladder) #204 made the broker/worker chain tier-3 (compile-enforced). The adjacent blind spot the #206 ladder names is the daemon's web API: the route /v1/master/memory/plant + the ApiMemoryEntry body are hand-copied in 3 places — the daemon (Rust source of truth), the React frontend daemon.ts, and the harness web-parity-demo.sh — agreeing only by manual coincidence. A daemon.ts route/shape change left phase 6 green on the old path (false-green). Pin all three to one serde source of truth (rung 2 of the ladder): - daemon: MASTER_MEMORY_{,PLANT_}ROUTE consts (used by the router) + a ui_bridge unit test (master_memory_plant_contract_matches_fixture) pinning ApiMemoryEntry's keys + the route to harness/fixtures/web-api/master_memory_plant.json - gate scripts/check-web-api-drift.sh diffs the two NON-Rust consumers (daemon.ts + web-parity-demo.sh, both carrying a `@web-fixture: master_memory_plant` annotation) against that fixture — route + entry key-set. Wired into harness-ci rust-checks. - a daemon.ts route rename or entry field add/rename/drop is now CI-red, not a stale green (negative-tested both halves). Docs: update the #206 ladder section in harness/CLAUDE.md (false-green now CLOSED; plant contract is at rung 2; rung-3 endgame = agentkeys-web-core wasm so daemon.ts stops hand-building the body); add the web-api gate to the root CLAUDE.md #203 rule.
e8573d9 to
467dcd5
Compare
…in a UTF-8 locale
scripts/check-backend-fixture-drift.sh interpolated `$SCAN_DIR…` — the variable
immediately followed by a Unicode ellipsis (U+2026, E2 80 A6). Under `set -u` in a
UTF-8 locale (C.UTF-8 / en_US.UTF-8 — what GitHub ubuntu-latest uses), bash's
multibyte identifier scan absorbs the ellipsis into the name, reads `SCAN_DIR…` as
an unbound variable, and aborts before checking any fixture. The new harness-ci
`rust-checks` step (`bash scripts/check-backend-fixture-drift.sh`) would then fail
on EVERY PR regardless of protocol correctness, and the drift protection never ran.
Reproduced locally: `set -u; V=/tmp; echo "$V…"` exits 1 (`V: unbound variable`)
under LC_ALL=C.UTF-8/en_US.UTF-8 but exits 0 under LC_ALL=C; the braced form
`${V}...` exits 0 under all three. Both gates now pass under LC_ALL=C.UTF-8 +
en_US.UTF-8.
Fix: brace the var (`${SCAN_DIR}`, the CLAUDE.md interpolation-defense convention)
and use ASCII `...` so no following byte can extend the name. Also switched the one
other executable ellipsis log line in check-web-api-drift.sh to ASCII for the same
robustness. Repo-wide scan confirms no other `$VAR<multibyte>` adjacency in
scripts/ or harness/. (Codex adversarial-review finding.)
…nannotated-canonical guard
Two Codex adversarial-review findings, both a residual false-green:
1. Route check passed on stale literals (check-web-api-drift.sh). The web-api
gate `grep`ed the whole consumer file for the canonical route, so the route
appearing in a step label / comment satisfied it even if the actual POST URL
changed — the exact false-green the gate exists to close. Now assert the CALL
SITE: the route must appear immediately followed by a closing quote (it
terminates a URL/string literal) within a few lines of a `curl`/`-X POST`
(bash) or `postJson`/`fetch` (TS) call. A stale label (route followed by a
space/arrow) no longer satisfies it; a drifted prefix like `…/plantX"` is
rejected because the char after `plant` is `X`, not a quote. Verified: changing
the real curl URL while leaving the step label stale now fails.
2. Fixture gate missed an unannotated canonical body (memory-plant-demo.sh:154).
The `/v1/memory/get` read-back hand-rolled `{cap, namespace}` with no
`@backend-fixture` annotation, so pass 1 (annotated-only) never gated it.
Fix both ways: (a) annotate that body; (b) add pass 2 to
check-backend-fixture-drift.sh — scan EVERY single-quoted jq object literal
and fail any whose key-set EXACTLY matches a canonical fixture but lacks an
annotation. Exact-match is false-positive-free: the v2-stage3 cred bodies
(`{cap, plaintext_b64}`, `{cap}`) and the ttl-omitted 4-key cap variant
(broker `CapRequest.ttl_seconds` is `#[serde(default)]`) match no canonical set
and are left alone. Verified: removing the annotation now fails pass 2;
re-adding passes; no other unannotated canonical bodies exist in the harness.
Both gates pass under LC_ALL=C.UTF-8 + en_US.UTF-8; bash -n clean. (Codex
adversarial-review findings.)
… combine, not just resolve #205 (issue #201) landed a THIRD data class (Config): /v1/cap/config-{store,fetch} + an agentkeys-worker-config worker + a hand-rolled daemon config/per-ns-memory chain. #204 (#203) made agentkeys-backend-client the ONE owner of the broker/worker protocol. Rather than let the two coexist as parallel hand-rolled vs crate-owned chains, this merge folds #205's new surface INTO the #203 single-owner model. Conflicts resolved (2 files): - ui_bridge.rs: adopt #205's per-namespace storage model wholesale (memory_put_ns_real / memory_get_ns_real / RMW-under-plant-lock / real_config_ctx) — my per-entry memory_put_real + real_memory_client are SUPERSEDED, dropped. Kept my route consts (MASTER_MEMORY_{,PLANT_}ROUTE) + the plant-contract unit test, and #205's new /v1/master/memory/entry route. Swapped #205's inline 0x-normalize in the shared resolve_session_coords for the crate's normalize_omni_0x. - memory-plant-demo.sh: keep #205's per-ns JSON-array blob + my @backend-fixture annotation. Combine (#203 applied to #205's surface): - crate: CapMintOp gains ConfigStore/ConfigFetch (6 cap endpoints now); add ConfigPutBody/ConfigGetBody + fixtures (regenerated, now 6). - daemon mint_master_cap → BackendClient::cap_mint (the cap-mint body — the #200 drift locus — is now the crate's BrokerCapRequest for memory AND config; one function covers all 4 routes). Worker put/get bodies (memory + config) build from the crate's MemoryPutBody/MemoryGetBody/ConfigPutBody/ConfigGetBody types; the raw POST stays in the daemon to reuse the once-minted STS creds across namespaces. Re-added agentkeys-provisioner to the daemon (still used for that STS mint). - gate: config_put/config_get fixtures are pass-1-annotatable but EXCLUDED from pass-2 auto-detect (key-set-identical to cred bodies → would false-positive); documented in the gate + the fixtures README. #205's bash bodies (4-key ttl-omitted cap + ambiguous cred/config worker bodies) don't trip pass-2. - docs: arch.md tree gains agentkeys-worker-config + updated backend-client note; root CLAUDE.md #203 rule updated for the 6 endpoints + config body types. Verified: cargo build + clippy -D warnings + cargo test --workspace all clean (0 failures; plant-contract + config frozen tests pass); backend + web-api drift gates + fixture --check pass under LC_ALL=C.UTF-8; bash -n clean on all touched scripts.
…6 (web-parity)
harness-ci.yml ran v2-stage{1,2,3}-demo.sh in isolation — it predated the #200
v2-demo restructure and never picked up phase 4 (memory-plant) or phase 6
(web-parity). Phase 6 is the ONLY runtime proof of the daemon's web endpoint
(POST /v1/master/memory/plant → cap-mint → STS → worker → S3, the parent-control
app's path); stage 3 only exercises the CLI/curl path. The #203
check-web-api-drift.sh gate covers its SHAPE at compile/fixture time, but nothing
covered its runtime reachability in CI.
Switch the harness-e2e job to the whole orchestrator: `v2-demo.sh --ci` → phases
1-4 + 6. Phase 5/wire auto-skips — the §10.2 agent needs the aiosandbox, which CI
doesn't have, so --ci sets --wire none (the one phase CI genuinely can't run).
Running phases in sequence also means phase 1 registers the master that phase 6
reuses.
Enabler: v2-stage1-demo.sh now auto-skips deploy/email/provision under --ci/$CI
(CI runs against pre-provisioned infra — contracts pinned in TEST_*_HEIMA secrets,
identity via wallet_sig, vault/memory buckets+roles an operator one-shot the CI
role can't recreate). Mirrors stage-1's existing auto-WEBAUTHN-off + stage-2's
auto-stub under --ci, so `v2-demo.sh --ci` drives stage 1 without re-passing the
three skip flags. The build step now builds what v2-demo's preflight expects
(cli + daemon + mcp-server; mock-server is mock-mode-only and unused in real CI).
Docs: harness-ci.yml header + harness/CLAUDE.md CI-role note + the operator
runbook's On-CI semantics. (The runbook already documented `v2-demo.sh --ci` as
the CI front door — this makes the workflow match it.)
NOTE: the harness-e2e job is secret-gated (TEST_OIDC_AWS_ROLE_ARN) and can't run
locally — validated by YAML lint + bash -n + flag-threading review + the drift
gates; needs a CI run with the test secrets to confirm end-to-end.
…hase-1 fix) The harness-CI switch to `v2-demo.sh --ci` failed at phase 1: `v2-stage1-demo.sh: line 360: agentkeys: command not found`. The stages call a BARE `agentkeys` (resolved from PATH). In the old per-stage CI, each stage ran its own build step (no --skip-build), which installs agentkeys onto PATH via install-agentkeys-cli.sh. Under v2-demo the preflight builds target/release ONCE and tells the stages to --skip-build — so they skip the install, and the preflight never exposed the built binary on PATH. CI has no globally-installed agentkeys, so the bare call died, cascading: phase 1 died at step 5 (before its register), so the master was never registered → phase 2 register_first_master also couldn't find agentkeys-cli → heima-worker-smoke failed. Fix: the preflight now `export PATH="$PROJECT_ROOT/target/release:$PATH"` right after the build — so every phase subprocess resolves the just-built agentkeys / agentkeys-daemon (prepended, so it wins over any stale global install). This is the missing piece of the preflight's "build once, phases reuse" contract; it helps operators too (they get the build they just made, not a stale install). Verified locally: a bare `agentkeys chain show heima` resolves + runs under the exported PATH (the exact line-360 pattern). Also (cleanup completeness): the harness-e2e S3 cleanup now also wipes the CONFIG bucket's bots/<omni>/config/ (phase 6 writes the #201 memory-taxonomy there when config infra is present). Guarded by [ -n "$CONFIG_BUCKET" ], so it's a no-op until TEST_CONFIG_BUCKET is set. Memory + creds (vault/memory buckets) were already wiped; this closes the config-class gap.
hanwencheng
added a commit
that referenced
this pull request
Jun 6, 2026
…mo structure #204 (#203 backend-client refactor) merged after #205 and restructured harness-e2e from three v2-stage{1,2,3}-demo.sh steps into one v2-demo.sh --ci orchestrator — which already carried forward the config-worker-unreachable allow-skip. Re-apply the codex-review guard (PR #210) onto that new structure: a self-dissolving Guard step after the v2-demo run that warns while config-test is unprovisioned (#209) and FAILS once it becomes reachable, so the step-21 isolation skip can't silently persist. Resolves the #210<->main conflict; the --allow-skip invocation is unchanged from main.
hanwencheng
added a commit
that referenced
this pull request
Jun 6, 2026
…mo structure (#210) #204 (#203 backend-client refactor) merged after #205 and restructured harness-e2e from three v2-stage{1,2,3}-demo.sh steps into one v2-demo.sh --ci orchestrator — which already carried forward the config-worker-unreachable allow-skip. Re-apply the codex-review guard (PR #210) onto that new structure: a self-dissolving Guard step after the v2-demo run that warns while config-test is unprovisioned (#209) and FAILS once it becomes reachable, so the step-21 isolation skip can't silently persist. Resolves the #210<->main conflict; the --allow-skip invocation is unchanged from main.
hanwencheng
added a commit
that referenced
this pull request
Jun 6, 2026
Conflicts resolved: - ui_bridge.rs route block: kept #204's MASTER_MEMORY_PLANT_ROUTE constant + my new /v1/master/config/{presets,init} + /v1/master/classify/{tag,propose} routes. My inline classify/config cap-mint paths coexist with #204's agentkeys-backend-client (the #201 config/memory plant paths #204 deliberately kept inline — same pattern). - harness-ci.yml: kept #210's v2-demo.sh --ci structure + the #209 step-21 quarantine guard; added classify-not-configured,classify-worker-unavailable to --allow-skip. - Cargo.lock: took main's + cargo regenerated to include agentkeys-catalog + agentkeys-worker-classify alongside agentkeys-backend-client. Post-merge green: workspace build + clippy clean; daemon 100 + broker 36 + catalog/ worker tests; frontend tsc + next build; both #203/#204 drift checks pass.
hanwencheng
added a commit
that referenced
this pull request
Jun 6, 2026
…bstraction as memory) Credentials now mirror memory end-to-end: list-then-categorize over the master's own real vault, plus a vault-a-credential write. - cred worker: new POST /v1/cred/list (master-only — operator==actor, so a single-service cap can't enumerate the vault) lists the actor's stored service ids from S3. + service_from_key parsing test. - daemon: GET /v1/master/credentials (cred worker list → categorize each via the catalog, the parallel to GET /v1/master/memory's category list) + POST /v1/master/credentials/store (mint master-self cred-store cap → STS → cred worker, the parallel to the memory plant). real_cred_ctx reads AGENTKEYS_WORKER_CRED_URL + VAULT_ROLE_ARN. Unconfigured → empty (honest dev); configured-but-broken → 502 (real data or fail loud, no in-memory stand-in). Wired cred-store/cred-fetch into mint_master_cap's CapMintOp match (#204 owner). - frontend: a Credentials page (credentials.tsx) grouped by category with sensitivity chips + a 'vault a credential' form, a nav item, and client methods. Tests: daemon 102 + worker service_from_key + list-unconfigured-empty; frontend tsc + next build; clippy -D warnings + fmt clean. user-manual documents it.
hanwencheng
added a commit
that referenced
this pull request
Jun 6, 2026
…6/7/8) (#212) * feat: #207 onboarding + classifier auto-distribution (items 1A/2/3/5/6/7/8) Productionizes #207 onboarding + classifier-driven auto-distribution on the #205 Config substrate. All items except 1B (NL→COMPILE UI). - 1A: ~10 bundled presets + GET/POST /v1/master/config/{presets,init} author config/memory-taxonomy.enc (master-self read-modify-write MERGE — a later plant never clobbers it); onboarding gains a 'set up categories' step. - 2/3/6: agentkeys-worker-classify (COMPUTE gate, COMPILE+TAG, no S3), CapOp::Classify + /v1/cap/classify (data-class-bound), agentkeys-catalog (entity→category + per-category sensitivity floor + signed vendor overlays bounded by the floor). Deploy-wired: setup-broker-host.sh, dns-upsert-workers.sh, env files (prod/test/CI). - 5/7/8: daemon classify bridge (--classify-url → cap-gated worker TAG, local catalog tier-0 fallback), /v1/master/classify/{tag,propose}, /v1/actors/:id/scope/grant; AutoDistributePanel (propose→confirm, sensitivity-tiered: safe auto / sensitive K11). - stage-3 demo step 22: classifier-worker isolation negatives (op + data-class mismatch), skip-until-deployed. Docs synced (arch.md, CLAUDE.md, harness/CLAUDE.md, runbook). Determinism guardrail held — catalog hashmap lookups, no LLM on the gate hot path. Connect-time auto-distribution at agent pairing tracked in #211. Tests: daemon 99, catalog 7, worker-classify 6, broker 36; frontend tsc + next build green. * style: cargo fmt the #207 crates (catalog, worker-classify, ui_bridge) Pure rustfmt — whitespace only, no logic change. Satisfies the CI cargo-fmt gate. * fix: #207 CI step-22 skip + init graceful-degrade + general (non-memory-only) onboarding narrative Three fixes: 1. CI (harness step 22): curl '... || echo 000' DOUBLED %{http_code} to '000000' on the undeployed classify worker (TLS/connect failure), missing the 'case 000|502|503|504)' skip → spurious die → phase-3 FAIL. Mirror the config helper: send curl stderr to a side file, use '|| true' (curl already prints %{http_code}=000). Now skips cleanly via classify-worker-unavailable. 2. init 502 (user-reported): a degraded config worker (S3 GetObject 502) made reconcile_taxonomy's read-before-write abort init with a cryptic 502. Now init DEGRADES — authors into the in-memory mirror with a loud 'cached-degraded: <reason>' status (never silently 'ok') instead of hard-failing; the master-memory list shows the in-memory authored taxonomy on a durable error when local authored data exists (else still 502 — #201 finding 2 preserved for the empty case). 3. Narrative: the init page said 'memory categories', but the taxonomy is general (memory + credentials + future data classes). Reworded the onboarding setup step, the memory-page setup, and the user manual to 'category taxonomy — what an agent can access (memory, credentials, …)'; surfaced the degraded status in the UI. daemon 100 tests · frontend tsc+build · fmt+clippy clean · harness bash -n ok. * fix: #207 revert the init degrade — real durable data or fail loud (no in-memory compromise) The previous 'cached-degraded' fallback was wrong: it masked a broken Config store behind an in-memory stand-in, violating the all-real-data principle (#201 finding-2). Reverted to real-data-only: - init (configured path): a config worker failure (unreachable / S3 error) is now a HARD 502 with an actionable message — NO in-memory fallback. We author real durable data or fail loud so the operator fixes the Config data class. The ONLY in-memory path left is an explicit dev daemon started WITHOUT --config-url ('cached', labelled 'dev only — not durable'). - list (resolve_categories): a configured-but-broken Config 502s again (finding-2), never masked behind in-memory data. - config worker: s3_get/s3_put now surface the REAL S3 error (AccessDenied / NoSuchBucket / region) via ProvideErrorMetadata instead of a generic 'service error', so the broken store is diagnosable. - frontend + user manual: dropped the 'degraded/saved-locally' wording; a config-worker failure shows the real error, and the dev-only in-memory path is clearly labelled. daemon 100 + worker-config tests · frontend tsc+build · fmt+clippy clean. * feat: #207 onboarding init flow — progress ceremony, jump-to-app + sticky toast, idempotent re-onboard Three onboarding flow fixes (init is multi-second: cap-mint → STS → config worker → S3): 1. PROGRESS BAR — the setup step now runs a CeremonyRunner (Read profile → Compile taxonomy → Encrypt+store to Config → Index+audit), the real init fires as the slow step's awaited action, so the bar reflects the true duration (no more frozen 'authoring…' button). 2. JUMP TO APP — on success it goes straight to the main page (no dead 'Enter agentKeys' button). A STICKY toast (no auto-dismiss, with an × dismiss) carries the next step: 'N categories authored · Next: connect an agent (Pairing tab)'. 3. IDEMPOTENT RE-ONBOARD — on entering setup, probe listMemoryCategories: if a taxonomy already exists, skip straight in (never re-author / re-prompt). The daemon init is already data-idempotent (reconcile_taxonomy MERGES, never clobbers) and writes ONLY config/memory-taxonomy.enc — never the memory:<ns> plant blobs — so planted data is never deleted. New test init_preserves_a_pre_existing_planted_namespace proves it. daemon 101 tests · frontend tsc+build · fmt+clippy clean. * fix(harness): #207 no leaked demo memory + shrink phase-6 parity to a thin wiring smoke 1. (memory hygiene) Onboarding never plants memory (init authors the TAXONOMY only; dev_seed has no caller; --ui-bridge-seed-* seeds the session, not memory). The 'already planted' was the harness leaking durable S3 memory: - memory-plant-demo.sh: plant into DEDICATED demo-* namespaces (never the real travel/personal/family) + an EXIT-trap cleanup that deletes exactly those blobs on success OR failure (KEEP_DEMO_MEMORY=1 to keep). The real prepared archive is user-only (the web button) — never auto-planted. - web-parity-demo.sh: EXIT-trap now also deletes the dedicated webparity probe (was only cleaned on the success path via step 4). CI already materializes MEMORY_BUCKET + has a belt-and-braces prefix wipe. 2. (shrink #3) web-parity phase 6: 4 steps → 3. Dropped the redundant parity-artifact step (canonical-key HEAD + manual delete) — the body shape is compile/fixture-gated (check-web-api-drift.sh), the S3 key is deterministic + worker-unit-tested, and cleanup is the EXIT trap. The runtime check is now just the plant→200 wiring smoke (harness/CLAUDE.md 'parity checks evolve down a ladder'). Docs synced: harness/CLAUDE.md inventory + operator-runbook-harness.md. bash -n clean. * feat: #207 credentials as a first-class data class in the app (same abstraction as memory) Credentials now mirror memory end-to-end: list-then-categorize over the master's own real vault, plus a vault-a-credential write. - cred worker: new POST /v1/cred/list (master-only — operator==actor, so a single-service cap can't enumerate the vault) lists the actor's stored service ids from S3. + service_from_key parsing test. - daemon: GET /v1/master/credentials (cred worker list → categorize each via the catalog, the parallel to GET /v1/master/memory's category list) + POST /v1/master/credentials/store (mint master-self cred-store cap → STS → cred worker, the parallel to the memory plant). real_cred_ctx reads AGENTKEYS_WORKER_CRED_URL + VAULT_ROLE_ARN. Unconfigured → empty (honest dev); configured-but-broken → 502 (real data or fail loud, no in-memory stand-in). Wired cred-store/cred-fetch into mint_master_cap's CapMintOp match (#204 owner). - frontend: a Credentials page (credentials.tsx) grouped by category with sensitivity chips + a 'vault a credential' form, a nav item, and client methods. Tests: daemon 102 + worker service_from_key + list-unconfigured-empty; frontend tsc + next build; clippy -D warnings + fmt clean. user-manual documents it.
This was referenced Jun 6, 2026
Merged
hanwencheng
added a commit
that referenced
this pull request
Jun 6, 2026
…lted-key fetch) The #204-clean core of #216: the shared agentkeys-backend-client gains a cred_fetch capability so the agent can pull its AUTHORIZED LLM key from the vault (replacing the operator-env shortcut in phase1-wire Phase 4.0). - protocol.rs: CredFetchBody / CredFetchResp / CredFetchInput / CredFetchResult (mirrors agentkeys-worker-creds FetchRequest/Response — the service rides in the signed cap, not the body). - client.rs: cred_url field + cred() accessor + cred_fetch() — POST /v1/cred/fetch with per-actor STS under the VAULT role (mirrors memory_get), returns the b64 plaintext. The cap is minted separately via cap_mint(CredFetch). - The 2 BackendClient::new callers (daemon mint client, mcp-server) take the new cred_url (None for now — the MCP cred tool + the CLI `cred fetch` consumer are the next slice). Verified: cargo clippy -p agentkeys-backend-client -p agentkeys-daemon -p agentkeys-mcp-server -D warnings clean; #203 fixture gate green (no canonical shape drift); fmt clean. The live cred-fetch e2e is the next slice (CLI consumer + wire Phase-4.0 swap + sandbox run against the live cred worker).
hanwencheng
added a commit
that referenced
this pull request
Jun 6, 2026
… → register → grant) (#217) * feat: #214 web-app agent pairing (slice 1) — daemon poll for pending bindings First slice of the web-app agent-pairing build (#214). The parent-control pairing screen now shows REAL data instead of a local-state mock: - daemon GET /v1/agent/pairing/pending — reuses agentkeys_cli::agent_admin (new agent_pending_value) to pull the broker rendezvous via the master J1 session (agents the master claimed, awaiting on-chain register), mapped to the web UI's PairingRequest shape (pending_binding_to_request + unit test). - client: listPairingRequests() — types.ts interface + daemon.ts impl + empty.ts stub. - App.tsx: refreshPairing ("check for codes") now calls the real client. Verified: cargo clippy -p agentkeys-cli -p agentkeys-daemon -D warnings clean; daemon mapper unit test passes; frontend tsc --noEmit clean. Next slices (#214): claim-by-code, on-chain registerAgentDevice, scope grant (Touch ID), then the operator-runbook-wire.md split + web runbook. * feat: #214 web-app agent pairing (slice 2) — claim-by-code (master → broker) The master claims an agent's one-time pairing code from the web app (§10.2 P.1): - daemon POST /v1/agent/pairing/claim — reuses agentkeys_cli::agent_admin::agent_claim to bind the agent under a label + declare its requested scope via the broker, using the master J1 session. - client: claimPairing({code,label,scope?}) — types.ts + daemon.ts + empty.ts. - UI (pairing.tsx + App.tsx): a "claim a code" form — the master types the code the agent device shows (or scans its runtime QR); on success it re-polls so the claimed agent drops into the rendezvous awaiting on-chain register. Verified: cargo clippy -p agentkeys-daemon -D warnings clean; frontend tsc clean. Next: slice 3 (on-chain registerAgentDevice + ack), slice 4 (scope grant), #216. * feat: #214 web-app agent pairing (slice 3) — on-chain register + ack The master approves a claimed agent from the web app (§10.2 P.2): - agent_admin::agent_ack (new) — POST /v1/agent/pending-bindings/ack. - daemon POST /v1/agent/pairing/register — pulls the AUTHORITATIVE binding from the broker (device fields never come from the browser), shells out to heima-agent-create.sh --from-pubkey (sibling of the master register script — no new flag), submits registerAgentDevice on chain, then acks the broker. The binding's device_pubkey holds the agent EVM address; register_agent_device mirrors register_master_device. - client: registerPairing(requestId) — types.ts + daemon.ts + empty.ts. - App.tsx: the "accept" button now calls the real register → re-polls (the registered+acked binding clears from pending). The Touch-ID scope grant (P.3) is the next step. Verified: cargo clippy -p agentkeys-cli -p agentkeys-daemon -D warnings clean; frontend tsc clean. (On-chain tx is exercised by the harness e2e — needs a live chain + a claimed agent.) * test: #214 daemon pairing routes fail closed without a broker (503) Locks the no-broker guard on the new poll/claim/register routes — they fail closed before reaching the network. (Live broker + on-chain behavior is the harness e2e.) * test(harness): #214 web-parity phase 6 — pairing-poll wiring smoke (step 4) Adds a real harness step: boots the seeded daemon (reused from phase 6) and polls GET /v1/agent/pairing/pending, asserting a well-formed {requests:[...]} — the master-side web-pairing route reaches the real broker rendezvous with the master J1. Follows the orchestrator contract (STEP_TOTAL=4, three outcomes, idempotent, --ci tolerant). Keep-docs-in-sync: harness/CLAUDE.md + operator-runbook-harness.md. The full claim->register e2e needs a live §10.2 agent request (agent-side, #216). * feat: #214 web-app agent pairing (slice 4) — registered agent appears + grant-able After a successful on-chain register, insert the agent into state.actors (role agent, child omni, parent master, sandbox device) keyed by agent-<label>. It now shows in the devices view AND becomes targetable by the EXISTING scope-grant flow (P.3, /v1/actors/:id/scope/grant + the AutoDistributePanel) — so granting the agent its memory:<ns> + cred:<service> scopes (incl. the LLM key) reuses the #207 machinery, no new code. Mirrors the master actor's in-memory model. Remaining tail: the master-side DEFAULT-LLM-key designation (consumed agent-side in #216). Verified: cargo clippy -p agentkeys-daemon -D warnings clean; fmt clean. * feat: #216 cred-fetch primitive — BackendClient.cred_fetch (agent vaulted-key fetch) The #204-clean core of #216: the shared agentkeys-backend-client gains a cred_fetch capability so the agent can pull its AUTHORIZED LLM key from the vault (replacing the operator-env shortcut in phase1-wire Phase 4.0). - protocol.rs: CredFetchBody / CredFetchResp / CredFetchInput / CredFetchResult (mirrors agentkeys-worker-creds FetchRequest/Response — the service rides in the signed cap, not the body). - client.rs: cred_url field + cred() accessor + cred_fetch() — POST /v1/cred/fetch with per-actor STS under the VAULT role (mirrors memory_get), returns the b64 plaintext. The cap is minted separately via cap_mint(CredFetch). - The 2 BackendClient::new callers (daemon mint client, mcp-server) take the new cred_url (None for now — the MCP cred tool + the CLI `cred fetch` consumer are the next slice). Verified: cargo clippy -p agentkeys-backend-client -p agentkeys-daemon -p agentkeys-mcp-server -D warnings clean; #203 fixture gate green (no canonical shape drift); fmt clean. The live cred-fetch e2e is the next slice (CLI consumer + wire Phase-4.0 swap + sandbox run against the live cred worker).
hanwencheng
added a commit
that referenced
this pull request
Jun 6, 2026
… 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.
hanwencheng
added a commit
that referenced
this pull request
Jun 6, 2026
…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.
hanwencheng
added a commit
that referenced
this pull request
Jun 7, 2026
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.
hanwencheng
added a commit
that referenced
this pull request
Jun 7, 2026
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.
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.
Resolves #203. Builds on #206 (the parity-ladder doc) — this PR walks phase 6 down the ladder it describes.
Problem
The broker/worker HTTP chain was hand-coded in three places (MCP
HttpBackend, daemonui_bridge, harness bash). Only the STS relay was shared. That re-typing is the structural cause of the #200 drift bugs (evm_addressvs{address,chain_id}, bare-vs-0xomni, per-namespace field shapes).What this does
Collapses the chain behind one crate so drift becomes a compile error (Rust callers share the types) or a fixture mismatch (the harness gate). Then closes the adjacent false-green the #206 ladder names: the daemon's web-API plant contract shared by the React frontend + the harness.
What landed — part 1: the broker/worker chain (#203 core, tier-3)
agentkeys-backend-client— the dual ofbroker-server/worker-*:protocol.rs(all cap-mint/worker/audit wire shapes +service_memory()+normalize_omni_0x()— the daemon's old inline0xbug site),client.rs(BackendClient: cap-mint → STS → worker put/get → audit),fixtures.rs+ adump-protocol-fixturesbin.HttpBackend→ thin delegate; thebroker/memory/auditsubmodules deleted + re-exported from the crate (mod.rs).memory_put_real/real_memory_ctx→ shared client — kills the duplicate where the bugs lived. Net −355 LOC in existing files.check-backend-fixture-drift.shdiffs# @backend-fixture:-annotated bash bodies vs the crate fixtures; inharness-ci.ymlrust-checks.What landed — part 2: phase-6 frontend parity (the #206 ladder, rung 2)
The route
/v1/master/memory/plant+ theApiMemoryEntrybody were hand-copied in 3 places — the daemon (Rust), the React frontenddaemon.ts, andweb-parity-demo.sh— agreeing by manual coincidence. Adaemon.tschange left phase 6 green on the old path (the false-green #206 flags). Now pinned to one serde source of truth:5. Daemon:
MASTER_MEMORY_{,PLANT_}ROUTEconsts (used by the router) + aui_bridgeunit test (master_memory_plant_contract_matches_fixture) pinningApiMemoryEntry's keys + the route toharness/fixtures/web-api/master_memory_plant.json.6. Both non-Rust consumers carry a
@web-fixture: master_memory_plantannotation;check-web-api-drift.shdiffs their route + entry key-set against the fixture (in CI). Adaemon.tsroute rename or field add/rename/drop is now CI-red — negative-tested both halves.7. Docs: the #206 ladder section in
harness/CLAUDE.mdupdated (false-green CLOSED; plant contract at rung 2); rootCLAUDE.mdONE-owner rule covers both gates;arch.mdinventory updated.What did NOT land (genuine follow-ups)
agentkeysCLI instead of curl (issue step 4-full). The real-path bodies inmemory-plant-demo.sh/web-memory-bootstrap.share drift-safe via the annotation gate but not converted to CLI calls (needs an MCP server stood up in the harness). Unblocks: a follow-up that runsagentkeys mcp-serverin the harness.agentkeys-web-core(wasm) sodaemon.tsstops hand-building the body at all, and phase 6 step 3 collapses to a pure wiring smoke. This PR takes phase 6 to rung 2 (golden fixture); rung 3 is genuine future work tracked in theharness/CLAUDE.mdladder.Verification
cargo test --workspace— 0 failures (MCPthree_acts.rschain test; daemonreal_memory_ctx+ the newmaster_memory_plant_contracttest, which fails-closed on fixture drift — negative-tested).cargo fmt --check+cargo clippy— clean.dump-protocol-fixtures --checkcanonical;check-backend-fixture-drift.sh+check-web-api-drift.shboth pass (each negative-tested: catch added/renamed/dropped fields + route drift).Refs: #200, #206, #201,
docs/plan/web-flow/wire-real-paths.md§7 + W6.🤖 Generated with Claude Code