Audience: anyone touching AgentKeys for the first time, regardless of role. Scope: environment bootstrap, then role-keyed setup so each contributor sets up only what they need.
The CDP demo path is the only supported path — earlier Gmail-backed variants are archived under archived/ and should not be used for new work.
Fresh machine? Run the bootstrap script — it installs every prerequisite below, builds the workspace, and runs the smoke tests:
bash scripts/utils/setup-dev-env.shThe script is idempotent (safe to re-run), detects macOS vs Linux (apt / dnf / pacman), and handles:
- Homebrew (macOS) or the system package manager (Linux)
rustup+ the repo-pinned Rust toolchain (rust-toolchain.toml— see "Toolchain pin + bump ritual" below)- Node 20+ (Homebrew
node@20, NodeSource on apt/dnf, distro package on Arch) jj(Homebrew / pacman, orcargo install jj-clias fallback) — also seeds the requiredHanwen Cheng <heawen.cheng@gmail.com>jj identity if unsetjq- AWS CLI v2 (Homebrew on macOS, official zip on Linux) — needed only by the operator role; harmless to have everywhere
cargo build --workspace --releasenpm install --prefix provisioner-scripts+playwright install chromiumcargo test --workspaceandnpm test --prefix provisioner-scriptsas a smoke gate
Two things the script intentionally does not do:
- Install Google Chrome. The CDP scrapers attach to real Chrome at
localhost:9222; install it from https://www.google.com/chrome/. - Touch AWS infra. That's the one-time operator setup in §5.2.
| Script | Audience | What it does |
|---|---|---|
scripts/utils/setup-dev-env.sh (operator-internal) |
Anyone — fresh dev machine | Installs every prerequisite above, builds workspace, runs smoke tests. (The one you just ran.) |
| the broker-host bring-up entry point (operator-internal) | Operator — fresh broker host | Provisions a Linux host into a running broker: builds binaries, creates the agentkeys system user, drops systemd units, optional nginx + Let's Encrypt. Idempotent. See stage7-wip.md "Remote deployment" for the manual long-form walk-through. |
| Tool | Why | Install |
|---|---|---|
Rust (pinned by rust-toolchain.toml) |
Workspace crates | rustup toolchain install from the repo root — rustup reads the pin (channel + clippy/rustfmt) |
| Node 20+ | provisioner-scripts/ (TypeScript scrapers, tsx) |
nvm / asdf / system install |
| Google Chrome | CDP scrapers connect to real Chrome to bypass Turnstile | Standard download |
| AWS CLI v2 | Operator-only — SES + S3 inbound email and sts:AssumeRole |
brew install awscli |
jj |
All VCS operations — never raw git |
brew install jj (see jj docs) |
jq |
Required by the helper scripts and by the runbook's JSON-generation pattern | brew install jq |
Optional but recommended:
- chrome-devtools-mcp — auto-wired via
.mcp.jsonwhen you open this repo in Claude Code / Cursor / Zed / Continue.dev. Gives the workflow-collection skill tool-level access to a live Chrome for diagnosing provider-side changes.
rust-toolchain.toml pins the exact Rust version (plus the clippy + rustfmt components) for every surface — local dev, all CI jobs, and the broker-host build. rustup resolves the file automatically for any cargo command run inside the repo; nothing floats on "latest stable". CI installs via plain rustup toolchain install (which reads the pin), and scripts/utils/check-toolchain-pin.sh (run by the rust-checks CI job) fails the build if the pin is loosened to a floating channel or a workflow installs around it (e.g. dtolnay/rust-toolchain@stable, which sets RUSTUP_TOOLCHAIN and bypasses the file).
Why pinned: CI runs cargo clippy --workspace --all-targets -- -D warnings. With a floating stable, a clippy lint introduced in a newer stable fails CI while passing on an older local toolchain (real case: PR #270's needless_lifetimes — local 1.94 green, CI latest-stable red). The pin makes local and CI identical.
Bump ritual (all steps in ONE change):
-
Edit
channel = "X.Y.Z"inrust-toolchain.tomlto the new version. -
From the repo root, run the exact CI gate — rustup auto-downloads the new toolchain on first use:
cargo fmt --all -- --check && cargo clippy --workspace --all-targets -- -D warnings -
Fix any lints the new version introduces; commit the pin bump + lint fixes together.
-
Nothing else to touch: CI,
setup-dev-env.sh, andsetup-broker-host.shall read the same file. The broker host downloads the new toolchain on its nextsetup-broker-host.sh --ref maindeploy (one-time ~1–2 min; old toolchains are reclaimed by--reclaim-toolchain).
setup-dev-env.sh points git at the committed .githooks/ dir (git config core.hooksPath .githooks). The pre-commit hook runs cargo fmt --all -- --check across BOTH cargo workspaces (the root + the standalone viz/server) — exactly the CI fmt step — so format drift is caught at commit time instead of as a red CI run. It only fires when a .rs file is staged (docs-only commits stay instant); fmt --check doesn't compile, so it's ~1s. A pre-push hook runs the same check for raw git push.
The fix when it fires: cargo fmt --all (run it in viz/server too if you touched that workspace), re-stage, recommit. Format-on-save in your editor (rust-analyzer: "editor.formatOnSave": true) avoids it entirely.
One caveat: jj git push does not run git hooks, so the pre-push hook never fires under jj — the pre-commit hook (which fires on the git commit step of the worktree PR flow) is the effective guard. Working purely in jj in the main repo (no git commit)? Rely on editor format-on-save, or run cargo fmt --all before jj describe. Existing clones: re-run setup-dev-env.sh or git config core.hooksPath .githooks once. Bypass a single commit with git commit --no-verify.
If you ran scripts/utils/setup-dev-env.sh in §1, the workspace is already built and tested — skip ahead to §3. Otherwise:
cd ~/Projects/agentkeys # or wherever your checkout lives
cargo build --workspace --release
npm install --prefix provisioner-scripts
npx --prefix provisioner-scripts playwright install chromium --with-depsSmoke-test the build:
cargo test --workspace
npm test --prefix provisioner-scriptsExpect a clean pass on both. If Rust fails, stop and fix before moving on — every role needs the workspace to build.
AgentKeys has three roles. Each runs a different set of processes and holds a different set of secrets. The broker server (Stage 7) is the boundary that lets these stay separated — the operator's AWS keys never leave the operator's machine.
| Role | What you run | What you hold | Read |
|---|---|---|---|
| App developer — building an agent against AgentKeys | agentkeys-daemon + an agent process |
A short-lived bearer token from the operator. Zero AWS credentials. | §4 |
| App owner / operator — running the broker for a team | agentkeys-broker-server (+ optionally the mock backend in dev) |
Long-lived agentkeys-daemon AWS access key (persisted in ~/.zshenv or supervisor-managed env). The broker's own master session. |
§5 |
| End user — using a credential-brokered agent | agentkeys CLI |
A 30-day master session token in OS keychain. | §6 |
Solo dev? You'll wear all three hats. Read §5 first to stand up your own broker, then §4 to point a daemon at it, then §6 for the user-facing CLI.
You're building an agent that needs OpenAI / OpenRouter / X / etc. credentials brokered through AgentKeys. You do not run AWS. You do not hold long-lived credentials. You run a daemon and point it at a broker your operator already provisioned.
AGENTKEYS_BROKER_URL— e.g.http://broker.local:8091orhttps://broker.litentry.org.AGENTKEYS_BEARER_TOKEN— short-lived; the operator hands these out per-developer.
That's it. No AWS keys, no aws sts assume-role, no per-developer env scripting.
export AGENTKEYS_BROKER_URL=http://broker.local:8091
export AGENTKEYS_BEARER_TOKEN=<token your operator gave you>
BIN=$(pwd)/target/release/agentkeys-daemon
$BIN --broker-url "$AGENTKEYS_BROKER_URL" --session "$AGENTKEYS_BEARER_TOKEN" --stdioWhen 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.
The provisioner scripts run unchanged from your machine. With --broker-url set, the daemon (or the agentkeys CLI directly) calls the broker's /v1/mint-oidc-jwt + AssumeRoleWithWebIdentity (issue #71 Option A) right before spawning the scraper subprocess, and injects 1-hour scoped AWS_* env vars into the child process. You don't need to set any AWS env vars yourself.
$BIN --broker-url "$AGENTKEYS_BROKER_URL" --session "$AGENTKEYS_BEARER_TOKEN" \
provision openrouter --identity bot-$(date +%s)@bots.example.devOr via the CLI:
agentkeys --broker-url "$AGENTKEYS_BROKER_URL" provision openrouterSuccess criteria:
- The scraper exits 0 with a key on stdout.
agentkeys read openrouterreturns that same key.
If the scraper fails, see §8 troubleshooting.
You operate the AgentKeys infrastructure for a team. You hold the long-lived agentkeys-daemon AWS key. You run the broker server. Other developers point their daemons at your broker.
Run through the internal cloud-bootstrap operator runbook (operator-docs/, not in the OSS mirror) §1–§3 once per AWS account. Afterwards you'll have:
- SES domain identity verified on
bots.litentry.org(or your substitute viaAGENTKEYS_EMAIL_DOMAIN) agentkeys-daemonIAM user withsts:AssumeRoleonlyagentkeys-data-rolerole with SES + S3 permissions- S3 bucket
agentkeys-mail-<ACCOUNT_ID>with receipt rule writing inbound toinbound/ - Route 53 records: three DKIM CNAMEs, MX, SPF, DMARC
Manage the daemon user's long-lived AWS keys via a named profile in ~/.aws/credentials (mode 0600). The broker uses the AWS SDK's default credential chain — AWS_PROFILE (set by awsp or your shell), the shared credentials file, or an EC2 instance profile via IMDS. No long-lived AWS keys live in env vars. See the broker-host bring-up entry point (operator-internal) for the bring-up + credential wiring; historical credential commentary archived at archived/operator-runbook-stage7-2026-04.md.
The broker holds your AWS daemon credentials (via the SDK default chain) and brokers scoped temp credentials to authenticated daemons. Same binary local + hosted; only the credential source differs.
Local development shape:
# Activate the daemon profile so the AWS SDK can resolve credentials.
awsp agentkeys-daemon # or: export AWS_PROFILE=agentkeys-daemon
# Non-secret config: BROKER_BACKEND_URL is required; the rest derive
# from ACCOUNT_ID + REGION already in your shell.
export BROKER_BACKEND_URL="http://127.0.0.1:8090" # mock backend for v0.1 dev loop
cargo run --release -p agentkeys-broker-server -- --port 8091
# → "AWS credentials: SDK default chain (AWS_PROFILE / ~/.aws / IMDS)"
# → "broker listening on 0.0.0.0:8091"The broker:
- Validates incoming bearer tokens against
BROKER_BACKEND_URL(the mock server in dev; the real chain backend in v0.2+). - Calls
sts:assume-roleonBROKER_DATA_ROLE_ARNusing whatever credentials the SDK default chain returned. - Returns 1-hour temp creds to the caller.
- Logs every mint to
BROKER_AUDIT_DB_PATH(SQLite, one row per mint).
For runbook detail (start / supervise / rotate / monitor / migrate to hosted), see the broker-host bring-up entry point (operator-internal; idempotent; the canonical entry point). For the automated remote-host bootstrap, see the broker-host bring-up entry point (operator-internal).
For v0.1 each developer gets a session token by running agentkeys init against your mock backend (or the real chain backend). The token they receive is what they paste into AGENTKEYS_BEARER_TOKEN per §4.1. Token TTL is 30 days per wiki/session-token.md.
If you're running everything on one box (typical solo dev), you'll want three terminals up:
# Terminal A — mock backend
cargo run --release -p agentkeys-mock-server -- --port 8090
# Terminal B — broker. AWS credentials come from the active profile.
awsp agentkeys-daemon
export BROKER_BACKEND_URL=http://127.0.0.1:8090
cargo run --release -p agentkeys-broker-server -- --port 8091
# Terminal C — real Chrome with CDP (only if you're running scrapers)
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome \
--remote-debugging-port=9222 \
--user-data-dir=/tmp/agentkeys-chrome-profileThen in a fourth terminal you wear the app-developer hat (§4): point a daemon at http://127.0.0.1:8091, with a bearer token minted via agentkeys init against 127.0.0.1:8090.
You're using an agent that's been provisioned via AgentKeys. Your only commitment is a 30-day session token that lives in your OS keychain. Your agent's daemon goes through someone else's broker — you don't run any AWS yourself.
BIN=$(pwd)/target/release/agentkeys
$BIN --backend "$AGENTKEYS_BACKEND_URL" init
# → mints a session, stores it in keychain
$BIN --backend "$AGENTKEYS_BACKEND_URL" read openrouter
# → returns the API keyThe user-facing CLI surface is unchanged from prior stages; the broker is invisible from this side.
The harness tracks stage completion in e2e/progress.json. Before opening a PR:
jj log --limit 10
cat e2e/progress.json
bash e2e/init.sh $(jq -r .current_stage e2e/progress.json)
bash e2e/stage-$(jq -r .current_stage e2e/progress.json)-done.sh
cargo test --workspace
npm test --prefix provisioner-scriptsThe stage-done script is the authoritative evaluator — never self-grade. If it exits 0, you're good.
| Symptom | Likely cause | Fix |
|---|---|---|
Cannot find package 'tsx' |
Running a scraper from repo root instead of provisioner-scripts/ |
cd provisioner-scripts && npm install first, or invoke via the daemon's provision subcommand which sets the cwd correctly |
ExpiredToken from broker |
Broker's daemon AWS key was rotated; broker process holds the old one | Restart the broker process — the SDK re-reads ~/.aws/credentials (or IMDS / env vars) on start |
401 Unauthorized from broker |
Bearer token expired (30-day TTL), or token issued against a different backend | Re-run agentkeys init against the broker's BROKER_BACKEND_URL |
Scraper hangs at waiting for Turnstile for >2 min |
Turnstile showing a visible checkbox | Click it in the Chrome window from §5.4 |
| Turnstile repeatedly fails even after checkbox | Chromium profile fingerprint flagged | rm -rf /tmp/agentkeys-chrome-profile and restart Chrome |
| Mock server won't bind port 8090 | Stale process | lsof -i :8090, kill, restart |
| Broker won't bind port 8091 | Stale process | lsof -i :8091, kill, restart |
agentkeys init double-prompts on macOS |
Known keyring-rs update path | Filed under Stage 9 "idempotent init" item |
bot-<ts>@bots.litentry.org email never arrives |
DNS / MX / SES receipt-rule misconfigured, or bucket missing write perm | aws s3 ls s3://$BUCKET/inbound/ --recursive — if empty >60s after signup, re-verify the domain/DNS steps (§1–§2) in the internal cloud-bootstrap operator runbook (operator-docs/, not in the OSS mirror) |
MalformedPolicyDocument: ... failed legacy parsing during operator setup |
Heredoc-generated JSON lost a $VAR:r / $VAR:h to a zsh modifier |
Use the jq -n --arg … '{…}' pattern — never heredoc JSON into AWS calls |
Providers add, remove, and reorder signup steps. When a deterministic scraper breaks, diagnose with the /agentkeys-workflow-collection skill — it drives a real Chrome session via chrome-devtools-mcp to produce a diff-ready transcript. That transcript is what feeds back into the scraper's pattern library.
The longer-term plan (Stage 5b → folded into M2 vendor wedge) is to detect drift automatically from telemetry and hand MCP-capable callers a fallback that their own LLM can drive — details in plan/milestones-roadmap.md (operator-internal) § M2.
plan/milestones-roadmap.md(operator-internal) — M1-M7 roadmap (replaces the archived v1/v2 stage plan)- the internal cloud-bootstrap operator runbook (
operator-docs/, not in the OSS mirror) — one-time AWS infra (DNS, SES, S3, IAM, OIDC federation) - the broker-host bring-up entry point (operator-internal) — idempotent broker bring-up + supervise + rotate
spec/credential-backend-interface.md— 15-method trait contractspec/ses-email-architecture.md— Stage 6 email pipeline deep-divespec/threat-model-key-custody.md— what the broker is defending againstdocs/wiki/email-system.md,docs/wiki/oidc-federation.md,docs/wiki/hosted-first.md— architecture wiki- PR #52 — merged Stage 5 + 6 completion (foundation for this guide)
archived/— prior-snapshot docs; read-only reference, not a setup path