feat(stage7): phase 2 — OIDC issuer in Rust broker + provisioner-scripts AWS-cred wiring#61
Open
hanwencheng wants to merge 15 commits intomainfrom
Open
feat(stage7): phase 2 — OIDC issuer in Rust broker + provisioner-scripts AWS-cred wiring#61hanwencheng wants to merge 15 commits intomainfrom
hanwencheng wants to merge 15 commits intomainfrom
Conversation
…pts AWS-cred wiring Phase 2 of Stage 7 (Generalized OIDC Provider). Two slices: OIDC issuer absorption (replaces TS services/oidc-stub): - crates/agentkeys-broker-server/src/oidc.rs — ES256 P-256 keypair generation, on-disk persistence (mode 0600 at ~/.agentkeys/broker/oidc-keypair.json), JWK serialization, JWT signing. - New broker routes: GET /.well-known/openid-configuration, GET /.well-known/jwks.json, POST /v1/mint-oidc-jwt (bearer-gated against the backend's /session/validate). JWT mints land in the same audit log as mint-aws-creds with requested_role=oidc_jwt. - New env vars: BROKER_OIDC_ISSUER, BROKER_OIDC_KEYPAIR_PATH, BROKER_OIDC_JWT_TTL_SECONDS (default 300, bounded [60, 3600]). - TS services/oidc-stub deleted — Rust broker now owns the surface. Provisioner-scripts AWS-cred wiring (replaces stage6-demo-env.sh sourcing): - crates/agentkeys-provisioner/src/aws_creds.rs — fetch_via_broker helper + AwsTempCreds.to_env() rendering AWS_ACCESS_KEY_ID/SECRET_ACCESS_KEY/ SESSION_TOKEN (+ AWS_REGION when set). - CLI: --broker-url / AGENTKEYS_BROKER_URL flag on agentkeys; cmd_provision fetches creds via the broker before spawning the scraper subprocess. - MCP: McpHandler::with_broker_url builder + run_stdio_with_broker entry point; daemon threads its existing --broker-url through automatically. - When --broker-url is unset the legacy stage6-demo-env.sh sourcing path still works — wiring is purely additive. Tests: broker integration (mint_flow + oidc_flow), MCP broker-env injection, provisioner aws_creds unit + stub-server tests, existing unit suite. cargo clippy --no-deps clean. Still deferred (Phase 2 federation step): public TLS hosting of \$BROKER_OIDC_ISSUER so AWS IAM accepts create-open-id-connect-provider; TEE-derived signer at oidc/issuer/v1 (heima-gaps §3). Recipe preserved in docs/stage7-wip.md. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…ability Phase 2 was being framed as "still blocked" on a TEE-derived signer (heima-gaps §3) and on chain-anchored audit. That framing conflated two concerns: (a) the OIDC issuer architecture, which is complete and shipping, and (b) the audit-destination *backend*, which is a pluggable layer. The audit destination is interchangeable behind a single "append a tamper-evident record" interface: - Federated public chain (Heima, other Substrate parachains) - General-purpose public chain (Ethereum, Solana, Sui, Cosmos) - Permissioned / consortium chain (Hyperledger Fabric, Quorum, Aliyun BaaS) — the relevant choice for jurisdictions like China - Plain backend server (append-only SQLite, Postgres + immutable WAL, S3-with-Object-Lock) — the broker ships in this row today - Sealed log services (CloudTrail with KMS, GCP Cloud Audit Logs) - TEE-attested append-only log (Heima TEE + sealed storage, AWS Nitro + KMS, Azure Confidential Ledger) The Stage 7 broker's ~/.agentkeys/broker/audit.sqlite is a complete v0.1 audit destination on the simple-server side of this table — append-only by construction, sha256-hashed bearer tokens, audit-write-before-credentials invariant. Migrating to a chain or sealed log is a deployment-time backend swap, not a Stage-7 redesign. Changes: - docs/spec/architecture.md — new §11 "Audit destination is pluggable" with backend-class table; renumbers License → §12, Cross-references → §13 (no inbound external refs to those sections). - docs/stage7-wip.md — reframe Phase 2 as architecturally complete; add audit-destination-pluggability subsection; rename "federation step (still blocked)" to "Cloud federation deployment" (operational runbook, not architectural prerequisite); restructure TODO pickups as operational follow-ups. - docs/spec/plans/development-stages.md — add Stage 7 phase 2 row to Shipped; collapse Stage 7 Active section to operational follow-ups only. - harness/stage-7-done.sh — new completion gate covering broker tests, provisioner aws_creds tests, MCP broker-env tests, daemon + CLI rebuild, clippy on all touched crates, retired-stub directory check, broken-link guard against the deleted services/oidc-stub path. - crates/agentkeys-daemon/tests/pair_tests.rs — drop unused Session + WalletAddress imports (uncovered by adding daemon to the gate's clippy invocation; pre-existing lint, fixed for cleanliness). Stage 7 phase 1 + phase 2 now pass `bash harness/stage-7-done.sh` end to end. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Two new sections in docs/stage7-wip.md, slotted between the Phase 2
issuer description and the Cloud federation runbook:
1. **Operator end-to-end test (Phase 2)** — a four-terminal walk-through
that exercises every Phase 2 surface offline (broker `--skip-startup-check`
so no AWS round-trip is required):
- mock backend on :8090
- broker on :8091 with BROKER_OIDC_ISSUER set
- healthz / discovery / JWKS smoke checks
- session-create → mint-oidc-jwt round-trip with claim decode
- mint-aws-creds round-trip (live-AWS path)
- CLI `agentkeys provision` with AGENTKEYS_BROKER_URL set
- audit log inspection via sqlite3
- acceptance criteria
- negative checks for the failure modes operators triage
2. **Remote deployment** — production deployment guide for putting both
the backend and the broker on real infrastructure:
- Topology diagram (developer laptop → reverse proxy → broker → backend)
- Caveats on the in-memory mock-server (state loss on restart, no HA,
no listener-side TLS); two pragmatic v0.1 options laid out.
- Step 1: provision a host (AWS / DO / Hetzner / Linode examples)
with DNS, public-CA TLS cert, firewall rules.
- Step 2: build + install binaries to /usr/local/bin.
- Step 3: persist operator config in /etc/agentkeys/broker.env mode 0600.
- Step 4: systemd units for both backend and broker, with hardening
directives (NoNewPrivileges, ProtectSystem, ReadWritePaths,
dedicated agentkeys user, broker bound to 127.0.0.1 only).
- Step 5: nginx + Let's Encrypt config terminating TLS on
broker.example.dev → 127.0.0.1:8091.
- Step 6: client-side smoke test from a laptop with no AWS env vars.
- Step 7: cross-link to the existing Cloud federation deployment recipe.
- Operations: rotate, observe, harden — pointers to operator-runbook.md
§5 and §6, and a hard rule against exposing :8091 directly.
Cross-links:
- Architecture pluggable-audit framing referenced from the audit log
subsection.
- BROKER_OIDC_ISSUER caveat (must equal proxy server_name) called out
twice — once in step 3, once in step 6.
- Direct pointer to warn_if_non_loopback_without_tls in the broker's
main.rs as the source of truth for why broker bind=0.0.0.0 is unsafe.
Stage 7 done-gate (`bash harness/stage-7-done.sh`) still passes.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…; add broker host bootstrap script
The broker no longer requires DAEMON_ACCESS_KEY_ID / DAEMON_SECRET_ACCESS_KEY
in the process environment. When both are set the broker still uses them
(legacy path for existing deployments); when either is unset the broker
delegates credential resolution to the AWS SDK's default provider chain —
named profiles in ~/.aws/credentials (AWS_PROFILE / awsp), EC2 instance
profile via IMDS, or any other link in the chain. Picked path is logged at
startup so misconfiguration is visible immediately.
Code:
- crates/agentkeys-broker-server/src/sts.rs:
- new AwsStsClient::with_default_chain(region) — no credentials_provider
override, SDK default chain handles it.
- existing from_keys(...) kept for the static-keys legacy path.
- crates/agentkeys-broker-server/src/config.rs:
- daemon_access_key_id / daemon_secret_access_key now Option<String>.
- rejects setting only one of the pair (XOR-safe at startup).
- crates/agentkeys-broker-server/src/main.rs:
- dispatches on (Some, Some) → from_keys, otherwise → with_default_chain.
- logs the chosen path; the startup-failure message lists all three credential
sources (AWS_PROFILE, instance profile, static keys).
- tests/mint_flow.rs + tests/oidc_flow.rs: pass Some(...) for the daemon
keys in BrokerConfig literals.
Docs:
- docs/operator-runbook.md §3.1: rewritten as "AWS credentials" — leads with
named profiles + awsp, then EC2 instance profile, then legacy static keys.
§3.2 lists the remaining (non-AWS-secret) env vars; §3.3 + §3.4 renumbered
for consistency. §5 rotation procedure split per credential path.
- docs/stage7-wip.md operator-E2E + remote-deploy:
- E2E walk-through: `awsp agentkeys-daemon` instead of DAEMON_* exports.
- Remote deploy §3 rewritten with three credential paths (instance
profile / named profile / legacy static), each with copy-paste
commands. systemd unit no longer needs EnvironmentFile by default;
AWS_PROFILE or instance-profile is preferred.
- docs/dev-setup.md §1: new "Other setup scripts at a glance" table
pointing at scripts/setup-dev-env.sh and scripts/setup-broker-host.sh.
§5.1/§5.2/§5.4: profile-based broker boot. §8 troubleshooting:
ExpiredToken note now references ~/.aws/credentials reload.
- agentkeys-secrets.env.example: stripped DAEMON_* (now profile-managed),
with a leading note explaining the move and a commented-out legacy
block at the bottom for operators who can't use profiles.
New automation:
- scripts/setup-broker-host.sh: idempotent bootstrap for a fresh Linux
broker host. Builds binaries, creates the agentkeys system user,
drops both systemd units, optional --with-nginx + --with-certbot.
Three credential modes via --cred-mode {instance-profile,profile,static};
default is instance-profile (zero secrets on disk). Prints
remaining manual steps (DNS A record, IAM role attach, certbot run,
client-side smoke test) on completion.
- scripts/setup-dev-env.sh: final-message pointer to setup-broker-host.sh
so operators can find it.
Stage 7 done-gate (`bash harness/stage-7-done.sh`) still passes.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Last commit accidentally checked in `.claude/scheduled_tasks.lock` via `git add -A`. That directory is Claude Code's per-workspace runtime (lock files, scheduled-task index, settings.local.json) — never repo content. Adding `.claude/` to .gitignore and untracking the lock file. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
The script previously required all decisions up front via CLI flags. Now, when run on a TTY with no flags (or with --interactive explicitly), it walks the operator through each decision with an explanation block before the prompt — what the choice does, why it matters, and when to skip it — plus a final summary + confirmation before any destructive work. CI / non-TTY paths still work via --non-interactive + the existing flags. Behavioral changes: - Auto-detects TTY via `[[ -t 0 ]]`; can be overridden with --interactive / --non-interactive. - New --without-nginx and --without-certbot flags so non-interactive callers can be explicit instead of relying on the implicit default. - New --yes/-y to skip the final "Proceed?" prompt. - Required flags (--issuer-url, --account-id) now prompt interactively if missing; non-interactive mode still dies with a helpful redirect to the interactive walk-through. - Cred-mode is now interactive too: a numbered menu with explanations of instance-profile / profile / static and when each is appropriate. - Profile-name only prompted when cred-mode=profile. - Certbot prompt defaults to "yes" when nginx was chosen, "no" when not (because certbot --nginx has nothing to talk to without nginx). Tested: - bash -n syntax check - --help renders the header block - --non-interactive with missing --issuer-url → dies with redirect - --non-interactive with --cred-mode bogus → dies with valid-values list - --non-interactive with valid inputs → reaches summary block, then proceeds to package-manager detection - harness/stage-7-done.sh still passes Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…role
The role's old name overloaded "agent" with the project name (AgentKeys),
the AI agent the credentials are minted *for*, and the IAM identity the
broker assumes *into*. Three different things sharing one word — confusing
during operator setup, and easy to mis-script. Renaming to
`agentkeys-data-role` makes the role's job (data-plane access: S3 + SES)
explicit and unambiguous.
Code:
- BrokerConfig.agent_role_arn → data_role_arn
- BROKER_DATA_ROLE_ARN env var (primary); BROKER_AGENT_ROLE_ARN still
accepted as a fallback for unmigrated deployments — startup error
message lists both names.
- ACCOUNT_ID-derived default ARN now points at agentkeys-data-role.
- handlers/mint.rs + tests/{mint_flow,oidc_flow}.rs updated to match.
Scripts:
- scripts/setup-broker-host.sh: prompts, hand-off text, and CLI flag
references all use the new name.
- scripts/stage6-demo-env.sh: --role-arn target updated.
Docs (every non-archived reference updated):
- docs/stage6-aws-setup.md (the canonical "create the role" runbook;
added a top-of-§3 note explaining the rename + back-compat env var).
- docs/stage7-wip.md (E2E walk-through, remote deploy, federation recipe)
- docs/operator-runbook.md (env-var table, audit-DB schema comment)
- docs/dev-setup.md
- docs/spec/ses-email-architecture.md (mermaid diagrams + bucket policies)
- docs/spec/plans/development-stages.md
- wiki/tag-based-access.md (cryptographic-isolation walk-through)
- wiki/email-system.md
Verified:
- cargo test -p agentkeys-broker-server → all green
- harness/stage-7-done.sh → STAGE 7 (phase 1 + phase 2) PASSED
Migration for existing deployments:
- Old AWS deployments with the legacy role name keep working unchanged
via the BROKER_AGENT_ROLE_ARN fallback.
- New deployments should follow the renamed instructions in stage6-aws-
setup.md §3b and use BROKER_DATA_ROLE_ARN.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
Aligns the docs with bots.litentry.org (already canonical for the SES data-plane domain) so a litentry-following operator doesn't have to mentally substitute placeholders. broker.litentry.org is a control-plane hostname — distinct from bots.litentry.org which is the data-plane (email recipient) domain. Files: docs/stage7-wip.md, docs/operator-runbook.md, docs/dev-setup.md, scripts/setup-broker-host.sh. ops@ contact also updated to [email protected]. Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…lout Step 1b walks through allocating an Elastic IP and upserting the broker.litentry.org A record in Route 53 — the prerequisite that lets certbot's HTTP-01 challenge resolve the host in Step 5. The "Automated path" callout above Step 1 points operators at scripts/setup-broker-host.sh as the bundled automation for Steps 2-5, making the manual walk-through the reference rather than the default.
setup-broker-host.sh wrote a full :80+:443 nginx site up front, but the
:443 block referenced LE cert files that don't exist yet. nginx then
refused to start, and `certbot --nginx` aborted at its preflight
`nginx -t` check — operators saw:
cannot load certificate "/etc/letsencrypt/live/<host>/fullchain.pem":
BIO_new_file() failed ... No such file or directory
Switch to a two-phase config:
• Phase A (no cert): :80-only with the ACME challenge location.
• Phase B (cert exists): adds the :443 ssl block with proxy_pass.
The script detects the cert file at /etc/letsencrypt/live/$ISSUER_HOST/
and writes the right config; re-running after `certbot certonly --webroot`
flips A → B automatically.
The post-run summary now points at `certbot certonly --webroot` (which
works while nginx is up on :80) instead of the broken `--nginx` flow.
A scheme-less issuer URL like `broker.litentry.org` was silently accepted and propagated into BROKER_OIDC_ISSUER. The broker then emitted JWTs with `iss: "broker.litentry.org"` (no `https://`), which AWS rejects at AssumeRoleWithWebIdentity time and which causes the documented smoke test `jq '.issuer == "https://broker.litentry.org"'` to print false. Validate up front: • require https:// (or http:// with a warning — AWS won't accept it, but local dev might). • strip a trailing slash so BROKER_OIDC_ISSUER matches the JWT iss claim byte-for-byte. Operators hitting the bad config in the wild: edit /etc/systemd/system/agentkeys-broker.service so Environment=BROKER_OIDC_ISSUER=https://<host>, daemon-reload, restart. If you've already registered the OIDC provider on AWS with the wrong URL, delete and recreate it.
…r register Two additions to the AWS federation recipe: 1. Strengthen the issuer prereq check to compare byte-for-byte (catches the scheme-less / trailing-slash bugs operators have hit), with the exact systemd-unit fix inline. 2. New "0. Check for stale provider state" subsection: list providers first, identify the three states (empty / matching / stale), and delete-and-recreate flow for the stale-URL case. 3. Step 1 now ends with `aws iam get-open-id-connect-provider` so operators can confirm AWS actually fetched the JWKS, plus a note on the LE intermediate-CA thumbprint persistence.
The Stage 6 AWS runbook and the AWS-side half of the Stage 7 doc
re-tangled themselves over time — every cross-link was "see also" rather
than "the source is here". Operators ended up reading both, then the
operator runbook, then both again to figure out which command to run.
Restructure into three focused docs, all referenced by stage:
• docs/cloud-setup.md (NEW, 548 lines) — every cloud-account resource
in one file, split internally by concern (identities → DNS →
inbound mail → IAM → OIDC federation → EC2 host → cleanup). Stage
6 vs Stage 7 vs federated-deployment is a *mode* of the same
machinery, not three separate runbooks. Tencent Cloud SimpleDM +
COS slots in at §2.2 with a 1:1 IAM→CAM mapping table — no new
file when we add it.
• docs/stage7-wip.md (-469 lines) — Phase 1 / Phase 2 bookkeeping
dropped; Stage 7 is just "the broker that issues OIDC JWTs and AWS
creds". AWS commands no longer embedded inline; the doc points at
cloud-setup.md for provisioning. Smoke test now shows how to mint
a session bearer end-to-end (the previous version left
SESSION=<bearer-from-the-backend> as a dangling placeholder).
• docs/operator-runbook.md (-86 lines) — concise. WIP/scratchpad
header gone; Phase 1/Phase 2 framing gone; threat-model section
points at the spec doc instead of duplicating it; rotation paths
fold into one §5 table.
• docs/stage6-aws-setup.md deleted; all referrers (dev-setup,
stage8-wip, ses-email-architecture, development-stages,
setup-dev-env.sh, setup-broker-host.sh) point at cloud-setup.md.
Net: 813 insertions, 1264 deletions across 10 files. Stage 7 gate
still passes (STAGE 7 phase 1 + phase 2 PASSED).
…ally
The role table and §4.1 / §5.3 had been framing the developer's bearer
as something the operator "hands out per-developer" while the end-user
bearer was minted via `agentkeys init`. That implied an architectural
split where there isn't one — the two roles use the same code path,
the same backend endpoint, and the same OS-keychain storage.
Reframe:
• §3 role table: dev's "What you hold" matches end user's. Adds an
inline callout: bearers are self-minted. Operator's job is making
the backend reachable, not pre-issuing tokens. v0.1 vs v0.2+ table
explains today's loopback friction vs tomorrow's chain RPC.
• §4.1 "What you need from the operator": replace
AGENTKEYS_BEARER_TOKEN-from-operator with AGENTKEYS_BACKEND_URL
+ a self-served `agentkeys init` snippet. Adds a "why isn't it
public" callout that names the bearer's role as the per-user
identity gate.
• §5.3 retitled "How developers get bearer tokens" (was "Hand off
bearer tokens to your developers"). Operator's job is *backend
reachability*, not token distribution.
• §6 end-user opener now explicitly notes the token is the same
kind the developer holds in §4.
No code changes; documentation-only.
The previous §4 framed the app developer as a passive consumer of
broker URLs the operator handed out. That misses the actual workflow:
devs are hacking on daemon code, MCP integrations, and provisioner
scrapers — they need to iterate on the binary AND test against real
SES/S3, neither of which the old text addressed clearly.
Rewrite around three contexts that match what a dev is actually doing
right now:
• Context A — pure-local code loop (mock-server + broker on the
laptop, --skip-startup-check, stub creds). Use for control-plane
iteration: session create, JWT mint, audit-row writes.
• Context B — local daemon, hosted email pipeline. Daemon runs
locally; broker, backend, and AWS account belong to the operator.
PrincipalTag scopes the dev's wallet to its own S3 prefix even
though the AWS account is shared. This is the realistic loop —
full SES → S3 → key-extract works because the operator's email
infra is real. The local-only attempt at the email pipeline is
explicitly called out as not-feasible (SES wants real DNS + MX +
receipt rule).
• Context C — operator runtime. Code-identical to B; differs only
in being unattended.
Also calls out the permanent vs transitional pieces: broker mints
creds, backend issues sessions — both forever. Mock-server is the
v0.1 stand-in for Heima chain RPC; goes away when chain lands.
Also includes the operator-runbook.md §1.1 callout from the prior
turn (session bearers — how callers get them) that didn't get
committed.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Stage 7 phase 2 — two slices that compose into the canonical "broker, not proxy" architecture, on top of #60's phase-1 broker.
Slice A — OIDC issuer absorption (replaces TS
services/oidc-stub/)The Rust broker now serves the conforming OIDC discovery + JWKS surface and a bearer-gated mint-jwt endpoint:
GET/.well-known/openid-configurationcreate-open-id-connect-providerstep readsGET/.well-known/jwks.jsonkidPOST/v1/mint-oidc-jwt/session/validate, then mints a short-lived ES256 JWT carryingsub=agentkeys:agent:<wallet>,aud=sts.amazonaws.com,agentkeys_user_wallet=<wallet>Implementation:
oidc.rs— ES256 P-256 keypair generation, on-disk persistence (mode 0600 at~/.agentkeys/broker/oidc-keypair.json), JWK serialization, JWT signingoidc.rs— three routes wired intolib.rsBROKER_OIDC_ISSUER,BROKER_OIDC_KEYPAIR_PATH,BROKER_OIDC_JWT_TTL_SECONDS(default 300, bounded[60, 3600])requested_role = "oidc_jwt"so operators see one ledger for both credential typesservices/oidc-stub/deleted — Rust broker owns the surface nowSlice B — Provisioner-scripts AWS-cred wiring (replaces
stage6-demo-env.shsourcing)When
--broker-urlis set,agentkeys provision <service>(CLI) andagentkeys.provision(MCP tool) automatically:POST /v1/mint-aws-credson the broker with the daemon's session bearerAWS_ACCESS_KEY_ID,AWS_SECRET_ACCESS_KEY,AWS_SESSION_TOKEN(+AWS_REGIONwhen set) into the scraper subprocess envImplementation:
aws_creds.rs—fetch_via_broker+AwsTempCreds::to_env()--broker-url/AGENTKEYS_BROKER_URLflag on theagentkeysbinary;cmd_provisionthreads throughMcpHandler::with_broker_urlbuilder +run_stdio_with_brokerentry point; daemon's existing--broker-urlis wired through automaticallystage6-demo-env.shsourcing still works when--broker-urlis unset — wiring is purely additiveStill deferred (Phase 2 federation step)
aws iam create-open-id-connect-provider --url \$BROKER_OIDC_ISSUER+sts:AssumeRoleWithWebIdentityexchange need:oidc/issuer/v1(heima-gaps §3)Recipe is preserved verbatim in docs/stage7-wip.md for when both prereqs land.
Test plan
cargo test --workspace— all green; broker tests = 13 lib + 9 mint_flow + 6 oidc_flow; provisioner tests coveraws_credsunit + stub-server; MCP tests cover broker-env injection + existing toolscargo clippy --no-deps -p agentkeys-broker-server -p agentkeys-provisioner -p agentkeys-mcp -p agentkeys-cli -p agentkeys-daemon --all-targets— cleankeypair_persists_across_broker_restartsin oidc_flow.rs)oidc.rs::sign_jwt_round_trips_via_public_key)broker_urlis unset, provision works as before (legacystage6-demo-env.shpath)--broker-urlset + a real OpenRouter signup — operator validation step before Phase 2 federation work begins🤖 Generated with Claude Code