## What changed

High-risk actions now require a fresh passkey assertion bound to the exact operation. `wallet.send`, `agent_session.approve`, `passkey.register.additional`, and `passkey.remove` each have a `/stepup` preflight that issues an HMAC token committing to action type, target resource, and a canonical parameter hash; the WebAuthn challenge is set to `SHA-256(token)` and a persisted step-up session prevents replay. Each enforcement point has a `Disable*StepUp` config flag for operator rollback, and all four step-ups emit activity events that drive a new `GET /v1/security/stepup-log` audit endpoint (filterable by `all`, `agent_approve`, `wallet`, `passkey`).

Agent session caps are now USD-denominated. The `Assets` token allowlist is gone from session delegation — the agent never names a settlement token. Sessions carry a server-stamped `Currency` field (default `USD`), and the merchant chooses the settlement stablecoin at transaction time. Tool params and swagger have been reworded to USD/budget language; CLI and skill USD changes follow separately.

Account recovery for the lost-passkey case ships behind `POST /v1/recovery/{start,cancel,complete}` and `GET /v1/recovery/status`. `start` flips the account to `recovery_pending` (which `GetUserPasskeyReadiness` exposes, naturally blocking high-risk actions), `complete` enforces a configurable delay (72h prod default, 10m in nonprod) and atomically revokes credentials plus other auth sessions in a single transaction. The recovery-started email carries a one-shot HMAC cancel link bound to the user and `RecoveryStartedAt`; clicking it lands on a confirmation page and cancellation happens via explicit POST so link previews and prefetch can't silently cancel. All five lifecycle events (passkey added/disabled, cooldown started, recovery started/completed) now render through a shared branded HTML template with a plain-text fallback.

A `login_events` table now records every `/v1/login/verify` attempt with derived device, browser, and MaxMind-resolved city/country (IP is not stored). Users can read their own history via `GET /v1/security/login-log` with `since`/`until`/`limit`/`offset` and a 90-day default. On top of this, first-passkey enrollment now evaluates risk: when the registering session's device, browser, and country are all unseen versus the user's prior successful logins, enrollment is held (`403 ErrPasskeyEnrollmentRiskHold` at `POST /v1/passkey/register/options`, before WebAuthn options are returned) and a security email is sent. The hold lifts on a familiar re-login or after the cooldown window. `GET /v1/passkey` lists a user's credentials with a richer display name (`Platform passkey (synced, built-in) @ Mac · Chrome`), and `POST /v1/passkey/delete` soft-disables credentials so they stay in the audit trail but no longer count toward readiness. `GET /v1/me` now surfaces `passkey_readiness` so clients can drive gating from a single call. Both `login_events` and `first_passkey_risk` are gated by config flags and ship enabled in `base.yaml`; the GeoLite2-City database is bundled into the Docker image.

**Included changes:**

- refactor(agent): make session caps USD-denominated, drop token allowlist
- chore(config): shorten recovery & first-passkey-risk cooldowns to 10m in nonprod
- fix: address security review findings
- feat(passkey): richer display name with humanized transports
- docs(passkey): align login-history web surface with implemented columns
- docs(recovery): align cancel-email subject with confirm flow + document GET landing
- fix(recovery): require explicit POST to cancel recovery via email link
- docs(passkey): correct stale step-up challenge comment and risk-hold endpoint
- feat(passkey): derive and expose passkey display metadata
- fix(recovery): accept cancel links without started_at
- feat(security): brand security notification emails with shared HTML template
- fix(passkey): use singular login_event table name in partial index migration
- fix(passkey): review nits — empty cancel-URL fallback, doc comment, docs
- fix(passkey): run first-passkey risk gate before the step-up check
- fix(passkey): bind first-passkey risk to the exact session + gate at begin
- docs(passkey): align remaining design docs with implemented risk model
- feat(passkey): implement first-passkey enrollment risk hold + enable flags
- fix(recovery): claim recovery atomically before destructive revoke
- login_events: fix dev startup - default tag must quote the literal, make migration self-healing
- config: add stepup + recovery default stanzas to base.yaml
- login_events: review followups - close geoip on stop, partial email index, rune-safe truncate
- login_events: P1/P2 review fixes (defaults, EOF whitespace, evaluator ctx)
- go mod tidy: promote useragent + geoip2 to direct deps
- login_events: schema + ingestion + GET /v1/security/login-log + first-passkey-risk flag
- docs(passkey): add login-history + first-passkey-risk design + bundle GeoLite2
- passkey: soft-disable credentials on delete + load exclusions from DB
- swagger: fix /mcp empty-responses + enum-tag ActivityEvent fields
- address Round 12 review findings on passkey-improve
- address 8 review findings on passkey-improve
- release agent_session request claim on post-claim error
- fix two findings: premature approved status, swagger drift
- fix three race + state-persistence findings from 2026-05-26 review
- fix three verified bugs from review 2026-05-26
- add DisableAgentSessionApproveStepUp config flag
- send email notifications on passkey lifecycle + recovery, with cancel link
- backend fixes from passkey-improve review (H1-3, M1, M5, L2-3)
- bind passkey.register.additional and passkey.remove to WebAuthn
- bind wallet.send to a persisted WebAuthn step-up session
- enable wallet.send and additional-passkey step-up by default
- add GET /v1/security/stepup-log for the Security page audit panel
- add recovery flow for lost-passkey + email-in-hand case
- add POST /v1/passkey/delete with passkey.remove step-up
- persist passkey cooldown state on the user and consult it in readiness
- require step-up to add another passkey when one already exists
- add wallet.send step-up token flow
- bind agent_session.approve to action-bound step-up token
- add GET /v1/passkey to list user's registered passkeys
- add step-up activity kinds and recordStepUpEvent helper
- add pkg/stepup for action-bound HMAC tokens
- add passkey trust state and account readiness computation
- add prod design doc for passkey-improvement
- refine syntax
- add thread-model for current email-otp + passkey design
- refine Account States
- add passkey-based security evolution plan