You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The open question was framed in issue #77 §"Open question — identity/audit endpoint placement" but stopped at "needs design." It is repeated implicitly in the user's "broker constructs → user signs locally → signer submits" flow proposal that motivated issue #76 (device-key auth). docs/spec/architecture.md §2 (trust boundaries) sketches the broker/signer split but does not nail the endpoint surface.
This issue commits to a concrete answer + migration plan so #77's cleanup can proceed and the post-#76 wire shapes are unblocked.
Architectural commitment
The two trust boundaries are non-overlapping by responsibility:
Concern
Owner
Why
Identity ⇄ omni mapping
Broker
Already mints session JWTs after the identity ceremony; identity-link writes are policy decisions and must be co-located with the policy authority.
Identity-link queries
Broker
Single source of truth; signer must remain stateless w.r.t. who-owns-what.
Audit of record
Broker
Already owns plugin_mint_log via the pluggable AuditAnchor trait (issue #64). The audit-anchor durability invariant ("no credential released unless every anchor wrote Ok") is broker-side and must stay there.
Audit emission for signer-side actions
Signer emits → Broker writes
Signer is the source of truth for what it did (derive-address, sign-message, future submit); broker is the durability boundary. Signer POSTs an audit record to a broker callback; broker decides anchors.
AWS / GCP / cloud submission
Daemon-direct (no signer hop)
OIDC JWT + AssumeRoleWithWebIdentity already works; routing through the signer adds latency + a SPOF without security benefit (per-PrincipalTag scoping already bounds blast radius). Don't break what works.
Chain (Heima / EVM) submission
Broker prepares payload → user signs locally with device key + JWT → signer signs with omni-derived key + submits to chain RPC + emits audit
This is the user's flow from #77. Fits because chain submission needs the omni-derived secp256k1 key (K4), which only the signer holds.
The cloud-vs-chain split is the load-bearing decision: cloud already has a working daemon-direct path that is more secure than routing through the signer (the daemon never sees long-lived AWS keys; STS issues per-session creds bound to the OIDC JWT's PrincipalTag). Forcing every submission through the signer would regress this. The signer's submission role is scoped to operations that require K4 — i.e. on-chain anchoring.
Concrete endpoint moves
DELETE (legacy duplicates on mock-server :8090)
Endpoint
Replacement
Callers to repoint
POST /identity/link (mock-server)
POST /v1/wallet/link (broker)
crates/agentkeys-cli/src/lib.rs:709 (cmd_link) — was using legacy bearer; needs session-JWT migration first.
GET /identity/resolve (mock-server)
GET /v1/wallet/links (broker) — returns the master's full list; daemon filters locally. Or add GET /v1/wallet/resolve?identity_type=...&identity_value=... if filtered lookup is needed.
crates/agentkeys-cli/src/lib.rs:877 (used by cmd_grant for alias/email lookup); crates/agentkeys-daemon/src/main.rs:409.
GET /audit/query (mock-server)
GET /v1/audit/query (broker — does not exist yet, see ADD below)
crates/agentkeys-cli/src/lib.rs:621 (cmd_audit_query) and crates/agentkeys-core/src/mock_client.rs:286,288.
ADD (broker)
Endpoint
Shape
Notes
GET /v1/audit/query
Query: ?owner=...&agent=...&service=...&since=...&limit=... ; Response: rows from plugin_mint_log filtered by session-JWT scope
Replaces mock-server's /audit/query. Auth: session JWT. Scope: claims.agentkeys.omni_account constrains visible rows (master sees own + child rows; agents see their own). Reads from plugin_mint_log directly via state.audit_query; no new storage.
Signer-only callback so signer-side actions land in the same plugin_mint_log. Auth: signer-to-broker mTLS or a dedicated signer service token (decision deferred to design step).
Repoint daemon's /identity/resolve call (daemon/src/main.rs:409) to broker /v1/wallet/links (filter locally) or to a new broker /v1/wallet/resolve endpoint.
Repoint CLI cmd_audit_query (lib.rs:621) and mock_client.rs:286,288 to broker.
Delete mock-server handlers::identity + handlers::audit modules and their routes.
Implement signer-event at broker — append-only into plugin_mint_log (or sibling chain_anchor_log if column shape diverges); auth via BROKER_SIGNER_AUTH_TOKEN env var (rotated per deploy via setup-broker-host.sh) — deliberately not device-key, since this is signer-→-broker not user-→-signer.
Phase 3 — wire to live chain (depends on Heima pallets per heima-gaps §5)
Replace stub canonical-payload builder with real Heima extrinsic encoding for pallet-credential-grants, pallet-email-grants (when they exist), etc.
Configure chain RPC endpoint via SIGNER_CHAIN_RPC_URL env var; signer's submit becomes a real submitAndWatchExtrinsic.
Backfill chain_anchor_log rows from pending to confirmed once tx receipts arrive (Phase C reconciliation per migrations/0001_v2_schema.sql:119).
Trust-shape verification
Compromise
Cloud flow (today)
Chain flow (post-Phase 2)
Operator workstation
Stolen JWT (replay until exp); stolen device key (post-#76, sign on operator's behalf until rotation)
Same — device key gates submit-anchored-tx exactly as it gates sign-message.
Broker
Mint session/OIDC JWTs; query AWS as scoped principal
Mint JWTs + craft prepare payloads — but cannot make signer accept them without operator's device-key signature (per #76).
Signer
Derive any wallet, sign any message, now also: submit any chain tx if it can also forge a device-key sig — which it cannot from K3 alone
Same caveat; broker compromise + signer compromise jointly = full takeover, but neither alone is sufficient for chain submission.
This matches and extends architecture.md §2's compromise table; that table needs a row for the chain-submit row added in Phase 2.
Where the discussion lived
The open question was framed in issue #77 §"Open question — identity/audit endpoint placement" but stopped at "needs design." It is repeated implicitly in the user's "broker constructs → user signs locally → signer submits" flow proposal that motivated issue #76 (device-key auth).
docs/spec/architecture.md§2 (trust boundaries) sketches the broker/signer split but does not nail the endpoint surface.This issue commits to a concrete answer + migration plan so #77's cleanup can proceed and the post-#76 wire shapes are unblocked.
Architectural commitment
The two trust boundaries are non-overlapping by responsibility:
plugin_mint_logvia the pluggableAuditAnchortrait (issue #64). The audit-anchor durability invariant ("no credential released unless every anchor wroteOk") is broker-side and must stay there.AssumeRoleWithWebIdentityalready works; routing through the signer adds latency + a SPOF without security benefit (per-PrincipalTag scoping already bounds blast radius). Don't break what works.The cloud-vs-chain split is the load-bearing decision: cloud already has a working daemon-direct path that is more secure than routing through the signer (the daemon never sees long-lived AWS keys; STS issues per-session creds bound to the OIDC JWT's PrincipalTag). Forcing every submission through the signer would regress this. The signer's submission role is scoped to operations that require K4 — i.e. on-chain anchoring.
Concrete endpoint moves
DELETE (legacy duplicates on mock-server :8090)
POST /identity/link(mock-server)POST /v1/wallet/link(broker)crates/agentkeys-cli/src/lib.rs:709(cmd_link) — was using legacy bearer; needs session-JWT migration first.GET /identity/resolve(mock-server)GET /v1/wallet/links(broker) — returns the master's full list; daemon filters locally. Or addGET /v1/wallet/resolve?identity_type=...&identity_value=...if filtered lookup is needed.crates/agentkeys-cli/src/lib.rs:877(used bycmd_grantfor alias/email lookup);crates/agentkeys-daemon/src/main.rs:409.GET /audit/query(mock-server)GET /v1/audit/query(broker — does not exist yet, see ADD below)crates/agentkeys-cli/src/lib.rs:621(cmd_audit_query) andcrates/agentkeys-core/src/mock_client.rs:286,288.ADD (broker)
GET /v1/audit/query?owner=...&agent=...&service=...&since=...&limit=...; Response: rows fromplugin_mint_logfiltered by session-JWT scope/audit/query. Auth: session JWT. Scope:claims.agentkeys.omni_accountconstrains visible rows (master sees own + child rows; agents see their own). Reads fromplugin_mint_logdirectly viastate.audit_query; no new storage.POST /v1/chain/anchor/prepare(post-#76){op_type, op_payload, chain_id, nonce_hint?}; Response:{canonical_payload (CBOR or JSON), nonce, gas_hint, expires_at, prepare_id}prepare_idrow so the signer can dedupe + bound replay.POST /v1/audit/signer-event(post-#76, internal){prepare_id?, signer_action, omni_account, payload_hash, outcome, outcome_detail?}; Response:{audit_record_id}plugin_mint_log. Auth: signer-to-broker mTLS or a dedicated signer service token (decision deferred to design step).ADD (signer)
POST /dev/submit-anchored-tx(post-#76){prepare_id, canonical_payload, omni_account, device_sig, session_jwt}; Response:{tx_hash, anchor_audit_id}canonical_payloadwith K4 (HKDF-derived secp256k1 from K3 + omni) ; submits to configured chain RPC ; POSTs audit row to broker/v1/audit/signer-event; returns tx hash. Wire shape pinned indocs/spec/signer-protocol.md.KEEP-AS-IS
POST /v1/wallet/link(broker) — already there; canonical.GET /v1/wallet/links(broker) — already there; canonical.POST /v1/wallet/recover/lookup(broker) — already there; intentionally unauth, read-only.POST /dev/derive-address+POST /dev/sign-message(signer) — wire shape unchanged; auth gains device-key per Retire the bearer-JWT-only daemon→signer /dev/* path (align to arch §14.2; rescoped from Step 1c) #76.POST /v1/mint-oidc-jwt+POST /v1/mint-aws-creds(broker) — daemon-direct cloud path stays./mock/inbox/*and/auth-request/*(mock-server) — pair-flow + email-link primitives; out of scope, see Cleanup: retire legacy mock-server endpoints + decide identity/audit placement (follow-up to #75 + #76) #77.Migration phases
Phase 1 — unblock #77 cleanup (no #76 dependency)
GET /v1/audit/queryat broker (readsplugin_mint_log, scope-filtered by session JWT).cmd_link(lib.rs:709) andcmd_grant's identity-resolve call (lib.rs:877) to broker/v1/wallet/*— requires session JWT, so depends onagentkeys inithaving shipped (already done in agentkeys: stage 7+ — issue #74 step 1 (dev_key_service signer + bootstrap chain) #75)./identity/resolvecall (daemon/src/main.rs:409) to broker/v1/wallet/links(filter locally) or to a new broker/v1/wallet/resolveendpoint.cmd_audit_query(lib.rs:621) andmock_client.rs:286,288to broker.handlers::identity+handlers::auditmodules and their routes.grep -rn '/identity/\|/audit/query' crates/ scripts/returns only doc/historical refs.Phase 2 — chain-submission surface (depends on #76 landing)
POST /v1/chain/anchor/prepare+POST /dev/submit-anchored-tx+POST /v1/audit/signer-eventwire shapes indocs/spec/signer-protocol.md.prepareat broker — stubcanonical_payloadbuilder for an EVMeth_sendRawTransactionshape; add SQLite tablechain_anchor_prepares (prepare_id, omni_account, payload_hash, expires_at, consumed_at).submit-anchored-txat signer — verify device sig (per Retire the bearer-JWT-only daemon→signer /dev/* path (align to arch §14.2; rescoped from Step 1c) #76) + session JWT +prepare_idnot consumed; sign with K4; POST to broker audit callback; today returnssubmitted: false, reason: "chain RPC not configured"with apendingaudit row.signer-eventat broker — append-only intoplugin_mint_log(or siblingchain_anchor_logif column shape diverges); auth viaBROKER_SIGNER_AUTH_TOKENenv var (rotated per deploy viasetup-broker-host.sh) — deliberately not device-key, since this is signer-→-broker not user-→-signer.Phase 3 — wire to live chain (depends on Heima pallets per
heima-gaps §5)pallet-credential-grants,pallet-email-grants(when they exist), etc.SIGNER_CHAIN_RPC_URLenv var; signer's submit becomes a realsubmitAndWatchExtrinsic.chain_anchor_logrows frompendingtoconfirmedonce tx receipts arrive (Phase C reconciliation permigrations/0001_v2_schema.sql:119).Trust-shape verification
submit-anchored-txexactly as it gatessign-message.preparepayloads — but cannot make signer accept them without operator's device-key signature (per #76).This matches and extends
architecture.md§2's compromise table; that table needs a row for the chain-submit row added in Phase 2.Out of scope (separate issues)
/auth-request/*) — see Cleanup: retire legacy mock-server endpoints + decide identity/audit placement (follow-up to #75 + #76) #77.pallet-email-grants,pallet-email-audit) — seeheima-gaps §5./dev/*(issue Replace dev_key_service with TEE worker for omni-anchored EVM keypair derivation #74 step 2) — orthogonal to endpoint placement.Dependencies
References
docs/spec/architecture.md§2 (trust boundaries), §7 (pluggable surfaces), §11 (deployment topology)docs/spec/signer-protocol.md— wire-contract registry to extend in Phase 2docs/spec/heima-gaps-vs-desired-architecture.md§5 (chain pallets), §10 (signer-edge contract), §11 (per-request crypto auth)