From e68f2efc03fbabb57efcf89715f9aca9c35adad4 Mon Sep 17 00:00:00 2001 From: Hanwen Cheng Date: Sun, 24 May 2026 00:02:02 +0800 Subject: [PATCH 1/2] docs+comments: fold back /v1/mint-aws-creds retirement (closes #72) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The route + handler + tests were deleted in PR #96, but four downstream spots still described it as a live endpoint with current behavior. Land the doc + comment fixes so the next operator running the runbooks does not curl a 404. - docs/dev-setup.md:110 — describe the actual mint flow (OIDC JWT + client-side STS) instead of the deleted server-side aggregator. - crates/agentkeys-broker-server/src/env.rs — drop the stale "(broker-internal, used by /v1/mint-aws-creds)" parenthetical on the SessionJwt env group; name the current consumers (email-link / OAuth2 mint paths + /v1/mint-oidc-jwt). - crates/agentkeys-broker-server/src/main.rs — drop the stale comment about mint_v2 mirroring rows into the audit log (mint_v2 was deleted in PR #96); name /v1/mint-oidc-jwt as the current writer. - docs/operator-runbook-stage7.md — collapse the "two endpoints" § framing into the single surviving path. The old request label in the ASCII trust-relationship diagram now reads /v1/mint-oidc-jwt; the whole "POST /v1/mint-aws-creds — server-side gated" subsection is replaced with a one-paragraph retirement callout so operators searching for the old path find a clear "this is gone" note. - docs/stage7-demo-and-verification.md — drop "two paths" framing in §5, delete §5.2 (the server-side aggregator deep-dive that pointed into a deleted handlers/mint.rs and a deleted tests/mint_v2_flow.rs), rewrite §12.2 Idempotency-Key to explain the dedup layer is gone with the route (JWT TTL + daemon JWT cache cover the same use case), update the future-work bullet from "Retire /v1/mint-aws-creds entirely" to "✅ Done in PR #96", and rewrite §16's audit-trail note to say the broker-side row of the actual mint no longer exists (AWS CloudTrail is the STS-side trail). Per CLAUDE.md Runbook-fix-fold-back + Land-the-fix policies: PR #96 shipped the code work but did not touch these descriptive doc sections, so the operator-facing runbooks would still send the next person reading them to a 404. This patch closes that gap and explicitly closes #72 (GitHub did not auto-close from PR #96's body). cargo build -p agentkeys-broker-server clean (34s, exit 0). --- crates/agentkeys-broker-server/src/env.rs | 3 +- crates/agentkeys-broker-server/src/main.rs | 4 +- docs/dev-setup.md | 2 +- docs/operator-runbook-stage7.md | 42 +++---- docs/stage7-demo-and-verification.md | 136 +++++++-------------- 5 files changed, 65 insertions(+), 122 deletions(-) diff --git a/crates/agentkeys-broker-server/src/env.rs b/crates/agentkeys-broker-server/src/env.rs index c10d66c..731585b 100644 --- a/crates/agentkeys-broker-server/src/env.rs +++ b/crates/agentkeys-broker-server/src/env.rs @@ -21,7 +21,8 @@ pub enum Group { Core, /// OIDC issuer keypair + JWT TTL (used by AWS STS AssumeRoleWithWebIdentity). Oidc, - /// Session JWT keypair + TTL (broker-internal, used by /v1/mint-aws-creds). + /// Session JWT keypair + TTL (broker-internal; minted by the + /// email-link / OAuth2 auth flows, consumed by /v1/mint-oidc-jwt). SessionJwt, /// Audit storage policy (anchor selection, multi-anchor strategy). Audit, diff --git a/crates/agentkeys-broker-server/src/main.rs b/crates/agentkeys-broker-server/src/main.rs index 6ce0c0a..fc2e2fd 100644 --- a/crates/agentkeys-broker-server/src/main.rs +++ b/crates/agentkeys-broker-server/src/main.rs @@ -125,8 +125,8 @@ async fn main() -> anyhow::Result<()> { "Tier-1 boot complete; Tier-2 reachability checks deferred until after listener bind" ); - // Legacy mint-log table opened alongside the plugin-trait audit anchors; - // mint_v2 mirrors success/failure rows here for monitoring continuity. + // Mint-log table opened alongside the plugin-trait audit anchors; + // /v1/mint-oidc-jwt writes success/failure rows here via record_mint. let audit = AuditLog::open(&config.audit_db_path)?; // Issue #71 OIDC-only migration: the broker mint flow uses diff --git a/docs/dev-setup.md b/docs/dev-setup.md index e9800f0..b908e29 100644 --- a/docs/dev-setup.md +++ b/docs/dev-setup.md @@ -107,7 +107,7 @@ BIN=$(pwd)/target/release/agentkeys-daemon $BIN --broker-url "$AGENTKEYS_BROKER_URL" --session "$AGENTKEYS_BEARER_TOKEN" --stdio ``` -When the daemon needs to access the operator's S3 vault (to read or store a credential), it calls the broker's `POST /v1/mint-aws-creds` with the bearer token. The broker exchanges it for a 1-hour scoped AWS session and hands it back — you never touch the long-lived daemon AWS key. +When the daemon needs to access the operator's S3 vault (to read or store a credential), it calls the broker's `POST /v1/mint-oidc-jwt` with the bearer token, then exchanges the JWT for a 1-hour scoped AWS session via client-side `sts:AssumeRoleWithWebIdentity` (issue #71 / PR #96). The broker no longer holds any AWS principal — you never touch the long-lived daemon AWS key, and the broker can't either. ### 4.3 Provision a new service diff --git a/docs/operator-runbook-stage7.md b/docs/operator-runbook-stage7.md index 655def1..d040348 100644 --- a/docs/operator-runbook-stage7.md +++ b/docs/operator-runbook-stage7.md @@ -137,7 +137,7 @@ A concrete request flow makes the split obvious: │ (PUBLIC — AWS reaches this) ┌──────────────────┐ legacy bearer ┌───────────▼───────────┐ │ agentkeys-cli ├──────────────────────▶│ agentkeys-broker- │ -│ / agentkeys- │ /v1/mint-aws-creds │ server │ +│ / agentkeys- │ /v1/mint-oidc-jwt │ server │ │ daemon │ │ │ └──────────────────┘ │ ┌───────────────────┐ │ │ │ POST /session/ │ │ @@ -367,20 +367,18 @@ by `aud=sts.amazonaws.com` and a `sub` prefix. The broker's `BROKER_DATA_ROLE_ARN` must point at this role. -### Mint-time STS paths (issue #71) +### Mint-time STS path (issue #71 / issue #72) -There are two endpoints that result in AWS credentials, with **different -trust models** and **identical end-state security** (both go through -`AssumeRoleWithWebIdentity`, both emit creds tagged with the user's -`agentkeys_user_wallet` PrincipalTag): +One endpoint produces AWS credentials, via `AssumeRoleWithWebIdentity`, +with creds tagged with the user's `agentkeys_user_wallet` PrincipalTag. -#### `POST /v1/mint-oidc-jwt` — daemon-side STS (recommended) +#### `POST /v1/mint-oidc-jwt` — daemon-side STS The broker signs a short-lived OIDC JWT with the user's wallet claim and returns it. The daemon exchanges that JWT for AWS creds **on its own machine** by calling `sts:AssumeRoleWithWebIdentity` directly. This is the path the provisioner / MCP / `agentkeys-daemon` use after the -issue #71 Option A migration. +issue #71 Option A + issue #72 retirement of the server-side mint. - **Broker work**: validate bearer → sign JWT → return. - **Daemon work**: receive JWT → `AssumeRoleWithWebIdentity` → inject @@ -388,28 +386,18 @@ issue #71 Option A migration. - **AWS principal on broker**: none required. - **AWS principal on daemon**: none required (the JWT authenticates). -#### `POST /v1/mint-aws-creds` — server-side gated (kept for callers needing audit/grants/idempotency) - -Broker handles the full mint pipeline: - -1. Verifies the session JWT against the broker's session keypair. -2. Verifies a per-call EIP-191 signature on the request body. -3. Resolves any Phase B grant (consume → 403 if revoked/expired/exhausted). -4. Mints an internal user-scoped OIDC JWT (same claim shape as - `/v1/mint-oidc-jwt`). -5. Calls `sts:AssumeRoleWithWebIdentity` with that JWT (broker-side). -6. Writes the audit anchor row(s) per `BROKER_AUDIT_POLICY` (single - `sqlite` or `dual_strict` for multi-anchor durability). -7. Returns the temporary credentials. - -Use this endpoint when: -- You want the broker to be the policy point (mandatory audit log, - Phase B grants, Idempotency-Key dedup, multi-anchor coordination). -- You can't trust callers to self-audit. +> **Retired in PR #96 (issue #72).** The previous server-side +> aggregator `POST /v1/mint-aws-creds` no longer exists — the route +> returns 404. Its in-process gates (Phase B grant `try_consume`, +> Idempotency-Key dedup, multi-anchor audit coordination) were dropped +> with the route; isolation now relies on `/v1/mint-oidc-jwt`'s audit +> row + AWS CloudTrail's `AssumeRoleWithWebIdentity` events + AWS +> PrincipalTag/bucket policy per `arch.md §17.2`. Daemons must not +> retry against the old route. ### Broker creds-free posture (post-migration) -Both paths above use `AssumeRoleWithWebIdentity`, which is JWT-authenticated. The broker **does not need** an IAM principal at +The path above uses `AssumeRoleWithWebIdentity`, which is JWT-authenticated. The broker **does not need** an IAM principal at runtime for credential minting. After cutover you can: - Drop `AWS_PROFILE` from `agentkeys-broker.service`. diff --git a/docs/stage7-demo-and-verification.md b/docs/stage7-demo-and-verification.md index d046435..dc37028 100644 --- a/docs/stage7-demo-and-verification.md +++ b/docs/stage7-demo-and-verification.md @@ -1438,18 +1438,21 @@ contains only `ses:SendRawEmail`. --- -## 5. Mint AWS creds — two paths, post-issue-#71 +## 5. Mint AWS creds — single client-side path, post-issue-#71 / #72 -After issue #71 Option A landed, the auto-provision pipeline mints AWS -creds **client-side** by combining `/v1/mint-oidc-jwt` (broker call) + -`AssumeRoleWithWebIdentity` (daemon-side STS call). The broker no longer -needs an IAM principal at runtime. +After issue #71 Option A landed (caller-side migration) and PR #96 / issue +#72 deleted the legacy `/v1/mint-aws-creds` server-side aggregator, the +auto-provision pipeline mints AWS creds **client-side** by combining +`/v1/mint-oidc-jwt` (broker call) + `AssumeRoleWithWebIdentity` +(daemon-side STS call). The broker no longer needs an IAM principal at +runtime, and no longer holds the mint pipeline at all — it's a pure JWT +signer. -`/v1/mint-aws-creds` (server-side aggregator) **still works** for callers -who want server-side enforcement of audit + grants + idempotency — but -the production auto-provision path no longer hits it. +The old `POST /v1/mint-aws-creds` route now returns 404. Daemons that +still try to call it will see a hard failure; re-deploy with a binary +that uses `fetch_via_broker_default_ttl()` (the OIDC-first helper). -### 5.1 The new daemon-side flow (auto-provision uses this) +### 5.1 The daemon-side flow (auto-provision uses this) ```bash # === ON OPERATOR WORKSTATION === (or anywhere with the JWT) @@ -1514,44 +1517,7 @@ Inside `agentkeys-provisioner`, the `fetch_via_broker_default_ttl()` helper does the same two-step internally and returns an `AwsTempCreds` struct ready for env-var injection into the scraper subprocess. -### 5.2 The server-side aggregator (parallel architectural endpoint — not curl-able) - -`/v1/mint-aws-creds` is NOT a legacy / backward-compat shim — it's the -broker-as-policy-point endpoint upgraded in issue-64 (US-027: grant -resolution + atomic counter). It does §5.1's steps 1+2 internally -plus the audit-anchor write, and returns temp creds in the same shape. - -**Why no curl example.** The endpoint requires `auth.address` + -`auth.signature` — an EIP-191 signature by the wallet bound in the -session JWT over the canonical body (sans `auth.signature`). The -broker enforces three checks ([handlers/mint.rs:125–145](../crates/agentkeys-broker-server/src/handlers/mint.rs#L125)): - -1. `ecrecover(canonical, auth.signature) == auth.address` -2. `auth.address == claims.agentkeys.wallet_address` -3. Atomic grant-store consume for `(actor_omni, daemon_address, service)` - -For an auto-init operator: `wallet_address = master_wallet`, but the -signer's strict JWT-omni check ([dev_keys.rs:98](../crates/agentkeys-mock-server/src/handlers/dev_keys.rs#L98)) -only signs with `JWT.omni_account = actor_omni` — which recovers to -`derived_address(actor_omni)`, not `master_wallet`. Check 2 fails. - -For a §2 manual SIWE operator: `wallet_address = derived_address(actor_omni)`, -the signer signs with `actor_omni`, ecrecover matches, and the endpoint -returns creds. But that's already what §5.1 does without the audit-write -overhead, so the curl is operator-unfriendly. - -**Realistic callers.** Test fixtures with in-memory signing keys (see -[`crates/agentkeys-broker-server/tests/mint_v2_flow.rs:201–237`](../crates/agentkeys-broker-server/tests/mint_v2_flow.rs#L201) -for the working canonical-body + EIP-191 pattern), and the future TEE -worker (issue #74 step 2) which will hold the master_wallet key inside -the enclave. - -**For end-to-end demos, use §5.1 (client-side flow) or §5.3 (CLI -provision).** They both exercise the same STS path; §5.2's audit -record is a server-side bonus that operators rarely need to invoke -directly. - -### 5.3 Auto-provision pipeline against live broker.litentry.org +### 5.2 Auto-provision pipeline against live broker.litentry.org The end-to-end auto-provision trigger is the CLI's `provision` subcommand. `agentkeys provision ` loads the saved session @@ -1976,40 +1942,16 @@ When `BROKER_METRICS_ENABLED` is unset or `false`, `/metrics` returns 404 — operators not running a Prometheus scraper should leave it disabled to avoid leaking counter shapes to unauthenticated probers. -### 12.2 Idempotency-Key +### 12.2 Idempotency-Key (retired with `/v1/mint-aws-creds` in PR #96) -```bash -KEY=$(uuidgen | tr '[:upper:]' '[:lower:]') +Server-side idempotency dedup lived in the now-deleted +`/v1/mint-aws-creds` handler. With the route gone (issue #72), there is +no server-side dedup layer — `/v1/mint-oidc-jwt` is short-lived (5 min +default TTL) and the daemon caches the JWT in-process, so a re-mint +within the TTL window is a no-op without any server help. -# First call — mints + caches. -curl -i -X POST $OIDC_ISSUER/v1/mint-aws-creds \ - -H "Authorization: Bearer $SESSION_JWT_A" \ - -H "Idempotency-Key: $KEY" \ - -H 'content-type: application/json' \ - -d '{...}' # full mint body -# HTTP/2 200 -# x-idempotency: miss - -# Same key + same body within 5 min — returns cached response. -curl -i -X POST $OIDC_ISSUER/v1/mint-aws-creds \ - -H "Authorization: Bearer $SESSION_JWT_A" \ - -H "Idempotency-Key: $KEY" \ - -H 'content-type: application/json' \ - -d '{...}' -# HTTP/2 200 -# x-idempotency: hit ← no re-mint, no STS quota burn - -# Same key + DIFFERENT body — 422. -curl -i -X POST $OIDC_ISSUER/v1/mint-aws-creds \ - -H "Authorization: Bearer $SESSION_JWT_A" \ - -H "Idempotency-Key: $KEY" \ - -H 'content-type: application/json' \ - -d '{...different...}' -# HTTP/2 422 -``` - -`BROKER_REQUEST_BODY_LIMIT_BYTES` (default 1 MiB) caps body size at -the router level. +`BROKER_REQUEST_BODY_LIMIT_BYTES` (default 1 MiB) still caps body size +at the router level for every endpoint. --- @@ -2216,12 +2158,16 @@ structural plumbing is in place but the live integration isn't wired: every daemon has been issued a grant. - **Histogram metrics + per-handler counter bumps.** Counter shapes ship; latency histograms land in V0.1-FOLLOWUPS. -- **Retire `/v1/mint-aws-creds` entirely.** The provisioner / MCP / - daemon use `/v1/mint-oidc-jwt` + client-side - `AssumeRoleWithWebIdentity` (issue #71 Option A). The route stays - for callers who want server-side gates; once every operator's - pipeline confirms the new path works in production, the route can - be dropped. +- **Retire `/v1/mint-aws-creds` entirely.** ✅ Done in PR #96 (issue + #72). The provisioner / MCP / daemon use `/v1/mint-oidc-jwt` + + client-side `AssumeRoleWithWebIdentity` (issue #71 Option A); the + legacy server-side aggregator route was deleted along with its + handler (`handlers/mint.rs`) and tests (`tests/mint_v2_flow.rs`). + The route now returns 404. Server-side gates dropped with the + route: Phase B `try_consume` grants, Idempotency-Key dedup, and + multi-anchor audit coordination. Isolation now rides on + `/v1/mint-oidc-jwt`'s audit row + AWS CloudTrail + PrincipalTag/bucket + policy per `arch.md §17.2`. - **Retire `/v1/auth/exchange` and backend `/session/validate`.** Issue #74 step 1's CLI/daemon rewrite (this PR) removed every in-tree caller of the legacy `/session/create` → bearer → @@ -2476,12 +2422,20 @@ sudo sqlite3 /var/lib/agentkeys/.agentkeys/broker/audit.sqlite \ -header -column ``` -After the OIDC-only migration, the daemon-side path is invisible to -the broker's audit log (the broker only sees `/v1/mint-oidc-jwt` -calls). Use AWS CloudTrail's `AssumeRoleWithWebIdentity` events for -the STS-side audit trail. If you need server-side audit row coverage -of the actual mint, hit `/v1/mint-aws-creds` instead — it audits before -returning creds. +After the OIDC-only migration (issue #71) + `/v1/mint-aws-creds` +retirement (issue #72 / PR #96), the daemon-side STS call is invisible +to the broker's audit log — the broker only sees `/v1/mint-oidc-jwt` +calls. The full audit chain is: + +- `/v1/mint-oidc-jwt` writes the JWT-mint row to + `~/.agentkeys/broker/audit.sqlite` (`mint_log` table) via + `state.audit.record_mint(...)`. +- AWS CloudTrail's `AssumeRoleWithWebIdentity` events capture the + actual STS exchange, with the role + session name as named in §5.1. + +There is no longer a "server-side audit row of the actual mint" — the +mint IS the daemon's STS call, and that's audited by AWS, not the +broker. --- From 969386acc8781c5dfe092958e0b20d2ff6c30146 Mon Sep 17 00:00:00 2001 From: Hanwen Cheng Date: Sun, 24 May 2026 00:22:40 +0800 Subject: [PATCH 2/2] docs+comments: address codex challenge findings on PR #104 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex adversarial review (via /codex challenge) caught 5 categories of real defects the first commit on this branch missed. All P1s + P2s addressed here: [P1 #1] operator-runbook-stage7.md still described deleted endpoint behavior as live in two sections the first audit missed: - §L540-557 "Migration window — implicit-grant fallback" pointed operators at src/handlers/mint.rs::mint_v2 (deleted) and described a Phase E flip (BROKER_REQUIRE_EXPLICIT_GRANT=true) that no longer has a consumption point. Replaced with a "Grant enforcement retired" callout noting grant CRUD endpoints remain (so masters can still manage grants for audit / future re-introduction) but the mint-time try_consume path is gone. - §L698-707 "Idempotency-Key" claimed the mint endpoint accepts the header and dedups bodies within a 5min window. /v1/mint-oidc-jwt does not honor Idempotency-Key — replaced with a retirement note pointing at BROKER_OIDC_JWT_TTL_SECONDS=300 as the only re-mint cost knob. [P1 #2] My §12.2 rewrite in stage7-demo-and-verification.md invented a "daemon caches the JWT in-process" claim that does not match crates/agentkeys-provisioner/src/aws_creds.rs::fetch_via_broker, which fetches a fresh OIDC JWT and assumes a fresh role every call (no cache layer). Replaced with the truth: clients must implement batching / dedup / rate-limiting themselves, with a code reference for verification. [P1 #3] docs/spec/plans/issue-64/PLAN.md (and the prd.json next to it) still describe /v1/mint-aws-creds + Phase B grant try_consume + Idempotency-Key as live with passing acceptance criteria. Added a retirement preamble at the top of PLAN.md flagging that the route + gates were deleted in PR #96 and pointing readers at arch.md §17.2 for the current isolation contract. The prd.json acceptance entries are left as-is to preserve the audit record — the preamble + this commit message are the durable "this no longer matches reality" signal. [P2 #4] Code-comment cleanup: - state.rs:42-46 (grant_store) — re-cast from "the mint endpoint consults this" to "backs the /v1/grant/* CRUD endpoints; mint-time try_consume gone with mint_v2" - state.rs:52-55 (idempotency_store) — note that the only consumer is gone and the field is slated for removal (follow-up task spawned via mcp__ccd_session__spawn_task — see "Remove dead IdempotencyStore code"). - aws_creds.rs:32 — clarify the AwsTempCreds field shape matched the legacy /v1/mint-aws-creds response, which is now deleted. - aws_creds.rs::build_session_name doc comment + matches_broker_format test comment — drop references to handlers/mint.rs::build_session_name (deleted); reframe as daemon-side-only. - tests/grant_flow.rs module doc — drop the "covered in mint_v2_flow separately" claim (mint_v2_flow.rs is deleted); note CRUD-only surface today. - tests/oidc_flow.rs:181 — drop the "(parity with /v1/mint-aws-creds)" parenthetical. [P2 #5] PrincipalTag terminology drift between operator runbook and arch.md §17.2. Runbook said creds are tagged with `agentkeys_user_wallet`, but §17.2's per-actor isolation invariant is `agentkeys_actor_omni`. Code (oidc.rs:181) emits both for v0.1 bucket-policy back-compat. Rewrote runbook to lead with the §17.2-canonical tag and note the legacy tag stays for back-compat — per the "Terminology-source-of-truth rule" in CLAUDE.md. Also: a second pass of repo-wide grep found 3 more stale references outside the first commit's blast radius: - aws_creds.rs:32 field-shape comment (fixed) - tests/grant_flow.rs + tests/oidc_flow.rs comments (fixed) - docs/spec/plans/issue-74-dev-key-service-plan.md ASCII diagram showing /v1/mint-aws-creds as a live arrow (fixed inline + small retirement note) What stays (intentionally): - docs/spec/plans/development-stages.md:23 — historical "Stage 7 phase 1 (2026-04)" table entry. Date-anchored historical record, accurate at that stage. Not rewritten. - docs/archived/**, docs/research/**, docs/spec/plans/issue-64/*.md (other than PLAN.md preamble), progress.txt — pre-existing historical / scratch content, not operator-facing. Build still clean (cargo build -p agentkeys-broker-server -p agentkeys-provisioner, exit 0). Test suite unchanged in behavior — all edits to test files are comments only. --- crates/agentkeys-broker-server/src/state.rs | 16 +++--- .../tests/grant_flow.rs | 6 +-- .../tests/oidc_flow.rs | 2 +- crates/agentkeys-provisioner/src/aws_creds.rs | 20 +++---- docs/operator-runbook-stage7.md | 52 ++++++++++--------- docs/spec/plans/issue-64/PLAN.md | 18 +++++++ .../plans/issue-74-dev-key-service-plan.md | 6 ++- docs/stage7-demo-and-verification.md | 14 +++-- 8 files changed, 83 insertions(+), 51 deletions(-) diff --git a/crates/agentkeys-broker-server/src/state.rs b/crates/agentkeys-broker-server/src/state.rs index 56e4fd3..66931aa 100644 --- a/crates/agentkeys-broker-server/src/state.rs +++ b/crates/agentkeys-broker-server/src/state.rs @@ -39,19 +39,21 @@ pub struct AppState { pub audit_policy: AuditPolicy, pub wallet_store: Arc, pub nonce_store: Arc, - /// Capability grants (Phase B, US-025/026/027). Always compiled in; - /// the mint endpoint consults this even if no grant has yet been - /// issued (Phase 0 grant-less mints continue to work via the - /// implicit-grant fallback documented in mint.rs). + /// Capability grants (Phase B, US-025/026/027). Backs the + /// `/v1/grant/{create,list,revoke}` CRUD endpoints. The mint-time + /// `try_consume` enforcement point disappeared with mint_v2 in PR #96 + /// (issue #72); grants are kept in-tree for master-managed audit and + /// potential future re-introduction at the JWT-mint site. pub grant_store: Arc, /// Identity links (Phase B, US-028). Maps verified identities /// (email, oauth2 sub, secondary EVM wallet) to their owning master /// OmniAccount. Recovery flow consults this to find which master /// should sign the recovery grant. pub identity_link_store: Arc, - /// Idempotency-Key dedup (Phase D-rest, US-037). Mint endpoint - /// consults this on every request that carries an Idempotency-Key - /// header. + /// Idempotency-Key dedup (Phase D-rest, US-037). Originally consumed + /// by mint_v2; after PR #96 (issue #72) the only consumer is gone, + /// so this field is currently unread by any live handler. Slated for + /// removal — see follow-up task "Remove dead IdempotencyStore code". pub idempotency_store: Arc, /// Atomic counters surfaced via /metrics (Phase D-rest, US-036). pub metrics: Arc, diff --git a/crates/agentkeys-broker-server/tests/grant_flow.rs b/crates/agentkeys-broker-server/tests/grant_flow.rs index b3cb6cb..5e84952 100644 --- a/crates/agentkeys-broker-server/tests/grant_flow.rs +++ b/crates/agentkeys-broker-server/tests/grant_flow.rs @@ -4,9 +4,9 @@ //! - `POST /v1/grant/create` (master JWT) → 200, returns grant_id + //! audit_proof (compact JWS). //! - `GET /v1/grant/list` → 200, returns the just-created grant. -//! - `POST /v1/grant/revoke` → 200, instant revoke. Mint after revoke -//! would 403 (covered in `mint_v2_flow` separately when grant store is -//! wired into the mint endpoint — Phase B US-027). +//! - `POST /v1/grant/revoke` → 200, instant revoke. Mint-time enforcement +//! of revoked grants was retired with mint_v2 in PR #96 (issue #72); +//! today /v1/grant/* is CRUD-only (no consume point). //! - Re-revoke is idempotent at storage level (caller sees 400 because //! revoke() returns false). //! - Cross-master revoke (different OmniAccount tries to revoke a grant diff --git a/crates/agentkeys-broker-server/tests/oidc_flow.rs b/crates/agentkeys-broker-server/tests/oidc_flow.rs index d78d9f4..bedd946 100644 --- a/crates/agentkeys-broker-server/tests/oidc_flow.rs +++ b/crates/agentkeys-broker-server/tests/oidc_flow.rs @@ -180,7 +180,7 @@ async fn mint_oidc_jwt_signs_claims_for_session_wallet() { // same path the SIWE wallet/email/oauth2 verify handlers take. Replaces // the legacy `mint_session_against_backend` flow now that // /v1/mint-oidc-jwt verifies session JWTs locally instead of round- - // tripping to /session/validate (parity with /v1/mint-aws-creds). + // tripping to /session/validate. let wallet = "0xabcdef0123456789abcdef0123456789abcdef01".to_string(); let omni = derive_omni_account("evm", &wallet); let session_token = mint_session_jwt( diff --git a/crates/agentkeys-provisioner/src/aws_creds.rs b/crates/agentkeys-provisioner/src/aws_creds.rs index ff0682f..ee6bab1 100644 --- a/crates/agentkeys-provisioner/src/aws_creds.rs +++ b/crates/agentkeys-provisioner/src/aws_creds.rs @@ -29,8 +29,10 @@ pub struct OidcJwtResponse { } /// Final temp-cred shape passed to the scraper subprocess. The struct fields -/// match the broker's pre-issue-#71 `/v1/mint-aws-creds` response so callers -/// who already consume `AwsTempCreds.to_env(...)` need no changes. +/// match the response shape of the legacy `/v1/mint-aws-creds` route (deleted +/// in PR #96 / issue #72) so callers that already consume +/// `AwsTempCreds.to_env(...)` need no changes during the migration to the +/// daemon-side mint path. #[derive(Debug, Clone)] pub struct AwsTempCreds { pub access_key_id: String, @@ -212,12 +214,11 @@ async fn assume_role_with_jwt( } /// Wallet → STS session name (max 64 chars; alphanumeric + `=,.@-_`). -/// **Mirrors `crates/agentkeys-broker-server/src/handlers/mint.rs::build_session_name` -/// byte-for-byte** so audit rows + CloudTrail events line up across broker -/// mints (`/v1/mint-aws-creds` -> `mint_v2`) and daemon-side mints (this -/// function). The trailing micro-second timestamp gives every call a unique -/// session name even when the same wallet mints in rapid succession; without -/// it AWS returns the same temp creds for repeated calls within the +/// Daemon-side STS calls only — the server-side mint path was deleted in +/// PR #96 (issue #72), so this is the sole producer of STS session names +/// in the system. The trailing micro-second timestamp gives every call a +/// unique session name even when the same wallet mints in rapid succession; +/// without it AWS returns the same temp creds for repeated calls within the /// `DurationSeconds` window (subtle caching footgun called out in critic M1). fn build_session_name(wallet: &str) -> String { let now = SystemTime::now() @@ -294,7 +295,8 @@ mod tests { #[test] fn build_session_name_matches_broker_format() { - // Mirrors broker handlers/mint.rs build_session_name (critic M1). + // STS session-name format invariant — daemon-side only since PR #96 + // deleted the broker's handlers/mint.rs (issue #72) (critic M1). let name = build_session_name("0xAbCdEf0123456789ABCDEF0123456789AbCdEf0123456789"); assert!(name.starts_with("agentkeys-")); assert!(name.len() <= 64, "STS rejects session names >64 chars"); diff --git a/docs/operator-runbook-stage7.md b/docs/operator-runbook-stage7.md index d040348..9862854 100644 --- a/docs/operator-runbook-stage7.md +++ b/docs/operator-runbook-stage7.md @@ -370,7 +370,11 @@ The broker's `BROKER_DATA_ROLE_ARN` must point at this role. ### Mint-time STS path (issue #71 / issue #72) One endpoint produces AWS credentials, via `AssumeRoleWithWebIdentity`, -with creds tagged with the user's `agentkeys_user_wallet` PrincipalTag. +with creds carrying the per-actor `agentkeys_actor_omni` PrincipalTag +that drives bucket-policy isolation per [`arch.md §17.2`](../docs/arch.md#172-pri-actor-isolation). +The legacy `agentkeys_user_wallet` tag is still emitted alongside it for +backward compatibility with v0.1 bucket policies; new policies should +key off `agentkeys_actor_omni`. #### `POST /v1/mint-oidc-jwt` — daemon-side STS @@ -537,24 +541,20 @@ curl -X POST https://broker.litentry.org/v1/grant/revoke \ -d '{"grant_id":"grn-..."}' ``` -### Migration window — implicit-grant fallback +### Grant enforcement — retired with `/v1/mint-aws-creds` in PR #96 (issue #72) -The mint endpoint currently allows mints WITHOUT an explicit grant for -backward-compatibility with Phase 0 daemons (legacy `NoGrant` path -documented inline in `src/handlers/mint.rs::mint_v2`). The audit log -records these mints with an empty `grant_id` column. +The grant `try_consume` enforcement point lived inside the deleted +`src/handlers/mint.rs::mint_v2`. With that route gone (issue #72), the +broker no longer consults `grant_store` at mint time at all — the +`NoGrant` fallback, the planned Phase E fail-closed flip +(`BROKER_REQUIRE_EXPLICIT_GRANT=true`), and the empty-`grant_id` audit +rows are all moot. The grant CRUD endpoints (`/v1/grant/create`, +`/v1/grant/list`, `/v1/grant/revoke`) still exist so master devices can +manage grants for audit / future re-introduction, but no broker path +consumes them today. -**This is an intentional Phase 0→Phase B migration window.** Phase E -US-039 will flip the default to fail-closed (`NoGrant` → 403). Operators -should: - -1. Roll out the broker with grants enabled (this build). -2. Call `/v1/grant/create` for every existing daemon address. -3. Verify mints continue to succeed (now with non-empty `grant_id` in - audit rows). -4. Set `BROKER_REQUIRE_EXPLICIT_GRANT=true` (Phase E env var) to flip - the default to fail-closed. -5. Audit any 403s for daemons that didn't get a grant. +Per-actor isolation now rides on `/v1/mint-oidc-jwt`'s audit row + AWS +CloudTrail + AWS PrincipalTag/bucket policy (see `arch.md §17.2`). ### Recovery flow @@ -695,16 +695,18 @@ disabled to avoid leaking counter shapes to unauthenticated probers. Histograms (mint_latency, audit_write_latency) + per-handler counter bumps land in V0.1-FOLLOWUPS Phase E hardening. -### Idempotency-Key +### Idempotency-Key — retired with `/v1/mint-aws-creds` in PR #96 (issue #72) -The mint endpoint accepts an `Idempotency-Key: ` header. Bodies -that hash to the same fingerprint within the 5-minute window return -the cached response (no re-mint, no STS quota burn). Same key + a -different body returns 422. +The `Idempotency-Key` header was consumed by the deleted +`mint_v2` handler. No surviving broker route honors the header today — +`/v1/mint-oidc-jwt` always re-signs (the OIDC JWT TTL of 5 min, default +`BROKER_OIDC_JWT_TTL_SECONDS=300`, is the only knob bounding re-mint +cost). Callers that need rate-limiting / dedup must implement it +client-side. -`BROKER_REQUEST_BODY_LIMIT_BYTES` enforces the request body size limit -(default 1 MiB) at router level (DefaultBodyLimit middleware) — closes -Codex R2-F18 (declared-but-unenforced). +`BROKER_REQUEST_BODY_LIMIT_BYTES` (default 1 MiB) still enforces the +request body size limit at router level via the `DefaultBodyLimit` +middleware for every endpoint. --- diff --git a/docs/spec/plans/issue-64/PLAN.md b/docs/spec/plans/issue-64/PLAN.md index f8c2e9f..94b250f 100644 --- a/docs/spec/plans/issue-64/PLAN.md +++ b/docs/spec/plans/issue-64/PLAN.md @@ -8,6 +8,24 @@ --- +> **RETIREMENT NOTICE (2026-05-24, issue #72 / PR #96).** Substantial portions of this +> plan describe the `POST /v1/mint-aws-creds` server-side aggregator, its +> session-JWT + per-call EIP-191 signature wire format, Phase B grant `try_consume` +> enforcement, and Idempotency-Key dedup. **All of those surfaces were deleted** +> in PR #96 (closes issue #72) — the route, [`crates/agentkeys-broker-server/src/handlers/mint.rs`](../../../crates/agentkeys-broker-server/src/handlers/mint.rs) +> (which no longer exists), and [`crates/agentkeys-broker-server/tests/mint_v2_flow.rs`](../../../crates/agentkeys-broker-server/tests/mint_v2_flow.rs) +> (also gone). The current mint flow is `/v1/mint-oidc-jwt` (JWT signer only) + +> client-side `sts:AssumeRoleWithWebIdentity`; isolation rides on AWS +> CloudTrail + PrincipalTag/bucket policy per [`docs/arch.md`](../../arch.md) §17.2. +> +> Read the rest of this doc as **historical record of the pre-#96 design**, not as +> a description of the current system. The `prd.json` in this directory has +> matching stale acceptance criteria — same caveat applies. For the current +> mint + isolation contract see [`docs/arch.md`](../../arch.md) and the surviving +> tests under [`crates/agentkeys-broker-server/tests/`](../../../crates/agentkeys-broker-server/tests/). + +--- + ## 0. Context — why this plan exists PR #61 (broker phase 2 — OIDC issuer + AWS-cred wiring) merged to main. The broker today exposes 6 routes: `/healthz`, `/readyz`, `/v1/mint-aws-creds`, `/.well-known/openid-configuration`, `/.well-known/jwks.json`, `/v1/mint-oidc-jwt`. Auth is a bearer token validated by an HTTP call to `BROKER_BACKEND_URL/session/validate`. Audit is local SQLite. Wallet provisioning, user-identity verification, and chain anchoring are all implicit / external today. diff --git a/docs/spec/plans/issue-74-dev-key-service-plan.md b/docs/spec/plans/issue-74-dev-key-service-plan.md index 52191ad..4aa96c8 100644 --- a/docs/spec/plans/issue-74-dev-key-service-plan.md +++ b/docs/spec/plans/issue-74-dev-key-service-plan.md @@ -78,9 +78,11 @@ Move the daemon off the legacy `agentkeys init --mock-token` → backend `/sessi └────┬───────────────────┬─────┘ └────────────────────┘ │ ① email/OAuth2 │ ③ /v1/wallet/link │ auth flows │ ④ /v1/auth/wallet/{start,verify} - │ ④ /v1/mint-oidc-jwt ④ /v1/mint-aws-creds - ▼ ▼ + │ ④ /v1/mint-oidc-jwt + ▼ Broker (stateless minter, no key material from this flow) + (/v1/mint-aws-creds retired in PR #96 / issue #72 — + daemons now do client-side AssumeRoleWithWebIdentity) ``` The backend → broker path doesn't change. The dev_key_service is a **new** edge: daemon → backend (signer), parallel to the existing daemon → backend (credential vault). When TEE lands, this edge re-routes to the TEE worker; daemon code doesn't change. diff --git a/docs/stage7-demo-and-verification.md b/docs/stage7-demo-and-verification.md index dc37028..c9fe3b8 100644 --- a/docs/stage7-demo-and-verification.md +++ b/docs/stage7-demo-and-verification.md @@ -1945,10 +1945,16 @@ disabled to avoid leaking counter shapes to unauthenticated probers. ### 12.2 Idempotency-Key (retired with `/v1/mint-aws-creds` in PR #96) Server-side idempotency dedup lived in the now-deleted -`/v1/mint-aws-creds` handler. With the route gone (issue #72), there is -no server-side dedup layer — `/v1/mint-oidc-jwt` is short-lived (5 min -default TTL) and the daemon caches the JWT in-process, so a re-mint -within the TTL window is a no-op without any server help. +`/v1/mint-aws-creds` handler. With the route gone (issue #72), no +broker route honors the `Idempotency-Key` header. The only cost-bounding +knob is `BROKER_OIDC_JWT_TTL_SECONDS` (default 300s) — every call to +`/v1/mint-oidc-jwt` re-signs and writes a fresh `mint_log` row, and +every call to `sts:AssumeRoleWithWebIdentity` is a fresh AWS API call +(no caching in the provisioner — see +[`crates/agentkeys-provisioner/src/aws_creds.rs::fetch_via_broker`](../crates/agentkeys-provisioner/src/aws_creds.rs#L128) +which fetches a fresh JWT and assumes a fresh role every invocation). +Callers that need batching, dedup, or rate-limiting must implement it +client-side. `BROKER_REQUEST_BODY_LIMIT_BYTES` (default 1 MiB) still caps body size at the router level for every endpoint.