Skip to content

feat(stage7): phase 2 — OIDC issuer in Rust broker + provisioner-scripts AWS-cred wiring#61

Open
hanwencheng wants to merge 15 commits intomainfrom
claude/dreamy-einstein-edd6c5
Open

feat(stage7): phase 2 — OIDC issuer in Rust broker + provisioner-scripts AWS-cred wiring#61
hanwencheng wants to merge 15 commits intomainfrom
claude/dreamy-einstein-edd6c5

Conversation

@hanwencheng
Copy link
Copy Markdown
Member

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:

Method Path Auth Purpose
GET /.well-known/openid-configuration none Discovery doc the AWS IAM create-open-id-connect-provider step reads
GET /.well-known/jwks.json none JWK Set with the broker's ES256 P-256 public key + kid
POST /v1/mint-oidc-jwt bearer Validates the bearer against the backend's /session/validate, then mints a short-lived ES256 JWT carrying sub=agentkeys:agent:<wallet>, aud=sts.amazonaws.com, agentkeys_user_wallet=<wallet>

Implementation:

  • New module oidc.rs — ES256 P-256 keypair generation, on-disk persistence (mode 0600 at ~/.agentkeys/broker/oidc-keypair.json), JWK serialization, JWT signing
  • New handler oidc.rs — three routes wired into lib.rs
  • New env vars: BROKER_OIDC_ISSUER, BROKER_OIDC_KEYPAIR_PATH, BROKER_OIDC_JWT_TTL_SECONDS (default 300, bounded [60, 3600])
  • JWT mints share the existing audit log with requested_role = "oidc_jwt" so operators see one ledger for both credential types
  • TS services/oidc-stub/ deleted — Rust broker owns the surface now

Slice B — Provisioner-scripts AWS-cred wiring (replaces stage6-demo-env.sh sourcing)

When --broker-url is set, agentkeys provision <service> (CLI) and agentkeys.provision (MCP tool) automatically:

  1. Call POST /v1/mint-aws-creds on the broker with the daemon's session bearer
  2. Inject AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_SESSION_TOKEN (+ AWS_REGION when set) into the scraper subprocess env
  3. Spawn the scraper unchanged — the existing SES → S3 email path works as-is

Implementation:

  • New helper aws_creds.rsfetch_via_broker + AwsTempCreds::to_env()
  • CLI: --broker-url / AGENTKEYS_BROKER_URL flag on the agentkeys binary; cmd_provision threads through
  • MCP: McpHandler::with_broker_url builder + run_stdio_with_broker entry point; daemon's existing --broker-url is wired through automatically
  • Legacy stage6-demo-env.sh sourcing still works when --broker-url is unset — wiring is purely additive

Still deferred (Phase 2 federation step)

aws iam create-open-id-connect-provider --url \$BROKER_OIDC_ISSUER + sts:AssumeRoleWithWebIdentity exchange need:

  • Public TLS hosting of the issuer URL (so AWS IAM can fetch the JWKS)
  • TEE-derived signer at 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 cover aws_creds unit + stub-server; MCP tests cover broker-env injection + existing tools
  • cargo clippy --no-deps -p agentkeys-broker-server -p agentkeys-provisioner -p agentkeys-mcp -p agentkeys-cli -p agentkeys-daemon --all-targets — clean
  • OIDC keypair persists across broker restarts (verified by keypair_persists_across_broker_restarts in oidc_flow.rs)
  • Minted JWT verifies against the JWKS we'd serve over the wire (round-trip test in oidc.rs::sign_jwt_round_trips_via_public_key)
  • When broker_url is unset, provision works as before (legacy stage6-demo-env.sh path)
  • Live three-terminal demo with --broker-url set + a real OpenRouter signup — operator validation step before Phase 2 federation work begins

🤖 Generated with Claude Code

WildmetaAgent and others added 15 commits April 27, 2026 13:18
…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) <noreply@anthropic.com>
…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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
…; 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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
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) <noreply@anthropic.com>
…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) <noreply@anthropic.com>
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 ops@litentry.org.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants