Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c36d54a
docs(stage5): add concrete 'Run it' section for the live demo
WildmetaAgent Apr 20, 2026
0463d2e
stage5a: plus-addressing demo path + failure-mode logs
WildmetaAgent Apr 20, 2026
afd1349
stage5a: invoke main() when openrouter.ts is the entry point
WildmetaAgent Apr 20, 2026
fada5b6
stage5a: one-shot live-demo handoff script
WildmetaAgent Apr 20, 2026
7a37de5
stage5a: diag tooling + single-plus auto-mint in live-demo handoff
WildmetaAgent Apr 20, 2026
3c4f541
stage5a: deslop pass on ralph-session artifacts
WildmetaAgent Apr 20, 2026
3813dea
stage5b: CDP scraper + stage6 throwaway inbox scope
WildmetaAgent Apr 20, 2026
5430acd
stage5b: deslop openrouter-cdp.ts header comment + trim log line
WildmetaAgent Apr 20, 2026
d2fbcad
docs(stage6): operator runbook for real AWS infra setup
WildmetaAgent Apr 20, 2026
24744dc
docs(stage6): pivot runbook to litentry.org subdomain (bots.litentry.…
WildmetaAgent Apr 20, 2026
d1ca652
stage6: mock-server inbox endpoints + oidc-stub interim service
WildmetaAgent Apr 20, 2026
6e60e5f
docs(stage6): fix §3 bucket-policy ordering bug
WildmetaAgent Apr 21, 2026
afa233a
docs(stage6): finalize runbook structure + extract OIDC path to demo doc
WildmetaAgent Apr 21, 2026
e958869
docs(stage6): replace polished OIDC demo doc with a WIP scratchpad
WildmetaAgent Apr 21, 2026
eac83eb
chore(gitignore): ignore Stage 6 runbook-generated JSON artifacts
WildmetaAgent Apr 21, 2026
915eed9
docs: move OIDC-federation WIP from Stage 6 → Stage 7
WildmetaAgent Apr 21, 2026
c87e3cc
docs(stage6): guard §3 against empty env vars
WildmetaAgent Apr 21, 2026
4d43d91
docs(stage6): brace-wrap \${VAR}: in ARN templates (zsh modifier fix)
WildmetaAgent Apr 21, 2026
2f390de
docs(stage6): replace all heredoc+file patterns with jq --arg pipes
WildmetaAgent Apr 21, 2026
ef561c1
docs(stage6): self-substituting §6 test step + troubleshooting block
WildmetaAgent Apr 21, 2026
45c3fbf
docs(stage6): self-substituting §6 test step + troubleshooting block
WildmetaAgent Apr 21, 2026
e869936
docs(stage6): add §7 operational notes — inbound spam & lifecycle
WildmetaAgent Apr 21, 2026
1a5f26c
docs(stage6): add architecture topology section to email spec + wiki
WildmetaAgent Apr 21, 2026
f2155ce
docs(stage5): document AGENTKEYS_EMAIL_BACKEND selector
WildmetaAgent Apr 21, 2026
2bdc979
docs(stage6): live demo walkthrough for @bots.litentry.org
WildmetaAgent Apr 21, 2026
1e586de
docs(stage6): explicit two-identity AWS cred flow in §2
WildmetaAgent Apr 21, 2026
c444911
docs(stage6): simplify §2 to one block; drop admin-restore gymnastics
WildmetaAgent Apr 21, 2026
3912c2d
feat(workflow-recorder): chrome-devtools-mcp integration + multi-serv…
hanwencheng Apr 23, 2026
66ac92d
feat(scrapers): deterministic OpenRouter + OpenAI production scrapers
hanwencheng Apr 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,17 @@
.omc
.obsidian
/docs/test-screenshots/
.gstack/
AWSCLIV2.pkg

# Local developer secrets — template is checked in as .env.example.
agentkeys-secrets.env

# agentkeys-workflow-collection: per-run recordings (~50MB each, binary
# trace.zips don't delta-compress). Keep locally; commit only curated
# reference recordings via explicit negations below.
provisioner-scripts/recordings/*/
!provisioner-scripts/recordings/openrouter-signup-reference/
!provisioner-scripts/recordings/brave-search-signup-reference/
!provisioner-scripts/recordings/elevenlabs-signup-reference/
!provisioner-scripts/recordings/.gitkeep
13 changes: 13 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"mcpServers": {
"chrome-devtools": {
"type": "stdio",
"command": "npx",
"args": [
"chrome-devtools-mcp@latest",
"--browser-url=http://127.0.0.1:9222"
],
"env": {}
}
}
}
64 changes: 64 additions & 0 deletions agentkeys-secrets.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
# agentkeys-secrets.env.example
#
# Template for local developer secrets. DO NOT commit the real file — that's
# gitignored as `agentkeys-secrets.env`. Two ways to use:
#
# 1. Source it manually per shell:
# cp agentkeys-secrets.env.example agentkeys-secrets.env
# <fill in real values>
# source agentkeys-secrets.env
#
# 2. Source it from ~/.zshenv so non-interactive shells (Claude Code's Bash
# tool, cron jobs) pick it up too:
# echo "[ -f $PWD/agentkeys-secrets.env ] && source $PWD/agentkeys-secrets.env" >> ~/.zshenv
#
# After filling, run: `source scripts/stage6-demo-env.sh` to mint 1 h STS
# temp creds from DAEMON_* and export them as AWS_*.

# ─── Long-lived IAM users (rotate quarterly) ──────────────────────────────────

# Daemon user — only permission is `sts:AssumeRole` into agentkeys-agent.
# Compromise blast radius = can assume the role; rotate via `aws iam
# update-access-key --status Inactive` + create new key.
export DAEMON_ACCESS_KEY_ID=AKIA...REPLACE_ME
export DAEMON_SECRET_ACCESS_KEY=REPLACE_ME

# Admin user — used for infra changes (SES config, IAM policies). NOT used by
# the scraper/recorder runtime. If you don't do admin work, leave blank.
export ADMIN_AWS_ACCESS_KEY_ID=AKIA...REPLACE_ME_OR_BLANK
export ADMIN_AWS_ACCESS_KEY_SECRET=REPLACE_ME_OR_BLANK

# ─── Non-secret infrastructure knobs ──────────────────────────────────────────

export REGION=us-east-1
export DOMAIN=bots.litentry.org
export ACCOUNT_ID=429071895007
export BUCKET="agentkeys-mail-${ACCOUNT_ID}"
export ROLE_ARN="arn:aws:iam::${ACCOUNT_ID}:role/agentkeys-agent"
export DAEMON_USER_ARN="arn:aws:iam::${ACCOUNT_ID}:user/agentkeys-daemon"
export PARENT_ZONE_ID=Z09723983CFJOHAE3VC65 # litentry.org Route 53 zone

# Bucket where SES drops inbound mail for bots.litentry.org addresses.
export AGENTKEYS_SES_BUCKET="$BUCKET"
export AGENTKEYS_EMAIL_BACKEND=ses-s3

# Chrome CDP endpoint the recorder connects to.
export CDP_URL=http://localhost:9222

# ─── Signup / login test credentials ──────────────────────────────────────────

# Stable password for throwaway signup accounts. Fresh email per run is auto-
# generated by the recorder (bot-${Date.now()}@bots.litentry.org).
export AGENTKEYS_SIGNUP_PASSWORD=REPLACE_ME_WITH_STRONG_PASSWORD

# ─── CAPTCHA-solving service (optional) ───────────────────────────────────────
#
# CapSolver handles hCaptcha / reCAPTCHA / Cloudflare Turnstile on services
# that gate signup behind a challenge (ElevenLabs uses invisible hCaptcha).
# Without this key, the recorder escalates to human-in-loop on those
# services. Brave Search's custom PoW captcha is NOT a CapSolver task —
# it solves client-side on its own.
#
# Pricing: ~$1 per 1000 hCaptcha solves.
# Sign up: https://capsolver.com (paste the CAP-... token)
export CAPSOLVER_API_KEY=CAP-REPLACE_ME
36 changes: 36 additions & 0 deletions crates/agentkeys-cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -909,6 +909,42 @@ pub async fn cmd_provision(
}
}

pub async fn cmd_inbox_provision(ctx: &CommandContext, agent: Option<&str>) -> Result<String> {
let session = ctx.load_session().context("load session (run `agentkeys init` first)")?;
let backend = ctx.backend();
let agent_id = resolve_agent(&backend, &session, agent).await?;

if ctx.verbose {
eprintln!("[verbose] POST {}/mock/inbox/provision", ctx.backend_url);
eprintln!("[verbose] agent: {}", agent_id.0);
}

let address = backend
.provision_inbox(&session, &agent_id)
.await
.map_err(wrap_backend_error)?;

Ok(address.to_string())
}

pub async fn cmd_inbox_list(ctx: &CommandContext, agent: Option<&str>) -> Result<String> {
let session = ctx.load_session().context("load session (run `agentkeys init` first)")?;
let backend = ctx.backend();
let agent_id = resolve_agent(&backend, &session, agent).await?;

if ctx.verbose {
eprintln!("[verbose] GET {}/mock/inbox/list", ctx.backend_url);
eprintln!("[verbose] agent: {}", agent_id.0);
}

let addresses = backend
.list_inboxes(&session, &agent_id)
.await
.map_err(wrap_backend_error)?;

Ok(addresses.iter().map(|a| a.to_string()).collect::<Vec<_>>().join("\n"))
}

pub fn cmd_feedback() -> String {
let url = "https://github.com/agentkeys/agentkeys/discussions";
let opened = std::process::Command::new("open").arg(url).status().is_ok()
Expand Down
43 changes: 41 additions & 2 deletions crates/agentkeys-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use agentkeys_cli::{
cmd_approve, cmd_feedback, cmd_init, cmd_link, cmd_provision, cmd_read, cmd_recover,
cmd_revoke, cmd_run, cmd_scope, cmd_store, cmd_teardown, cmd_usage, CommandContext,
cmd_approve, cmd_feedback, cmd_inbox_list, cmd_inbox_provision, cmd_init, cmd_link,
cmd_provision, cmd_read, cmd_recover, cmd_revoke, cmd_run, cmd_scope, cmd_store, cmd_teardown,
cmd_usage, CommandContext,
};


Expand Down Expand Up @@ -172,6 +173,36 @@ enum Commands {
long_about = "Open https://github.com/agentkeys/agentkeys/discussions in the default browser.\n\nExamples:\n agentkeys feedback"
)]
Feedback,

#[command(
about = "Manage agent inbox addresses",
long_about = "Provision or list inbox addresses for an agent.\n\nOmit --agent to default to the session wallet.\n\nExamples:\n agentkeys inbox provision\n agentkeys inbox provision --agent 0xAGENT\n agentkeys inbox list\n agentkeys inbox list --agent 0xAGENT"
)]
Inbox {
#[command(subcommand)]
action: InboxAction,
},
}

#[derive(Subcommand)]
enum InboxAction {
#[command(
about = "Provision a new inbox address for an agent",
long_about = "Provision a new inbox email address for an agent and print the address.\n\nOmit --agent to default to the session wallet.\n\nExamples:\n agentkeys inbox provision\n agentkeys inbox provision --agent 0xAGENT"
)]
Provision {
#[arg(long, help = "Agent wallet address, alias, or email (defaults to session wallet)")]
agent: Option<String>,
},

#[command(
about = "List inbox addresses provisioned for an agent",
long_about = "List all inbox email addresses provisioned for an agent, one per line.\n\nOmit --agent to default to the session wallet.\n\nExamples:\n agentkeys inbox list\n agentkeys inbox list --agent 0xAGENT"
)]
List {
#[arg(long, help = "Agent wallet address, alias, or email (defaults to session wallet)")]
agent: Option<String>,
},
}

#[tokio::main]
Expand Down Expand Up @@ -208,6 +239,14 @@ async fn main() {
})
}
Commands::Feedback => Ok(cmd_feedback()),
Commands::Inbox { action } => match action {
InboxAction::Provision { agent } => {
cmd_inbox_provision(&ctx, agent.as_deref()).await
}
InboxAction::List { agent } => {
cmd_inbox_list(&ctx, agent.as_deref()).await
}
},
};

match result {
Expand Down
51 changes: 49 additions & 2 deletions crates/agentkeys-cli/tests/cli_tests.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
use std::sync::Arc;

use agentkeys_cli::{
cmd_init, cmd_link, cmd_provision, cmd_read, cmd_revoke, cmd_run, cmd_scope, cmd_store,
cmd_teardown, cmd_usage, CommandContext,
cmd_inbox_list, cmd_inbox_provision, cmd_init, cmd_link, cmd_provision, cmd_read, cmd_revoke,
cmd_run, cmd_scope, cmd_store, cmd_teardown, cmd_usage, CommandContext,
};
use agentkeys_core::backend::CredentialBackend;
use agentkeys_core::session_store::SessionStore;
Expand Down Expand Up @@ -1059,6 +1059,8 @@ impl CredentialBackend for ProvisionTestBackend {
async fn resolve_identity(&self, _: &Session, _: &str) -> Result<agentkeys_types::WalletAddress, agentkeys_core::backend::BackendError> { unimplemented!() }
async fn get_scope(&self, _: &Session, _: &agentkeys_types::WalletAddress) -> Result<Option<agentkeys_types::Scope>, agentkeys_core::backend::BackendError> { unimplemented!() }
async fn update_scope(&self, _: &Session, _: &agentkeys_types::WalletAddress, _: &agentkeys_types::Scope) -> Result<(), agentkeys_core::backend::BackendError> { unimplemented!() }
async fn provision_inbox(&self, _: &Session, _: &agentkeys_types::WalletAddress) -> Result<agentkeys_types::InboxAddress, agentkeys_core::backend::BackendError> { unimplemented!() }
async fn list_inboxes(&self, _: &Session, _: &agentkeys_types::WalletAddress) -> Result<Vec<agentkeys_types::InboxAddress>, agentkeys_core::backend::BackendError> { unimplemented!() }
}

// Test: provision masked output — subprocess emits a success key; stdout must be masked
Expand Down Expand Up @@ -1269,3 +1271,48 @@ async fn cmd_scope_add_remove_overlap_errors() {
"unexpected error: {err}"
);
}

#[tokio::test]
async fn inbox_provision_returns_address() {
let backend = create_test_backend();
let (store, _tmp) = test_store();
let (_wallet, session) = init_session_with_store(&backend, &store).await;
let ctx = ctx_with_session(backend, session, store);

let result = cmd_inbox_provision(&ctx, None).await.unwrap();
assert!(
result.starts_with("bot-") && result.contains('@'),
"expected bot-*@domain address, got: {result}"
);
}

#[tokio::test]
async fn inbox_list_after_provision_returns_one_entry() {
let backend = create_test_backend();
let (store, _tmp) = test_store();
let (_wallet, session) = init_session_with_store(&backend, &store).await;
let ctx = ctx_with_session(backend, session, store);

let provisioned = cmd_inbox_provision(&ctx, None).await.unwrap();
let listed = cmd_inbox_list(&ctx, None).await.unwrap();

let lines: Vec<&str> = listed.lines().collect();
assert_eq!(lines.len(), 1, "expected 1 inbox, got: {listed}");
assert_eq!(lines[0], provisioned.trim(), "listed address does not match provisioned");
}

#[tokio::test]
async fn inbox_list_accumulates_multiple_provisions() {
let backend = create_test_backend();
let (store, _tmp) = test_store();
let (_wallet, session) = init_session_with_store(&backend, &store).await;
let ctx = ctx_with_session(backend, session, store);

cmd_inbox_provision(&ctx, None).await.unwrap();
cmd_inbox_provision(&ctx, None).await.unwrap();
cmd_inbox_provision(&ctx, None).await.unwrap();

let listed = cmd_inbox_list(&ctx, None).await.unwrap();
let lines: Vec<&str> = listed.lines().collect();
assert_eq!(lines.len(), 3, "expected 3 inboxes, got: {listed}");
}
2 changes: 1 addition & 1 deletion crates/agentkeys-core/src/auth_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ fn cbor_key_bytes(key: &Value) -> Vec<u8> {
buf
}

fn sort_map(map: &mut Vec<(Value, Value)>) {
fn sort_map(map: &mut [(Value, Value)]) {
map.sort_by(|(a, _), (b, _)| {
let a_bytes = cbor_key_bytes(a);
let b_bytes = cbor_key_bytes(b);
Expand Down
32 changes: 30 additions & 2 deletions crates/agentkeys-core/src/backend.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use agentkeys_types::{
AuditEvent, AuditFilter, AuthRequest, AuthRequestId, AuthRequestType, CanonicalBytes,
EncryptedPairPayload, OpenedAuthRequest, PairCode, PairPayload, PublicKey, RegistrationToken,
Scope, ServiceName, Session, SignedAuthDecision, WalletAddress,
EncryptedPairPayload, InboxAddress, OpenedAuthRequest, PairCode, PairPayload, PublicKey,
RegistrationToken, Scope, ServiceName, Session, SignedAuthDecision, WalletAddress,
};
use async_trait::async_trait;
use thiserror::Error;
Expand Down Expand Up @@ -155,6 +155,18 @@ pub trait CredentialBackend: Send + Sync {
target_wallet: &WalletAddress,
new_scope: &Scope,
) -> Result<(), BackendError>;

async fn provision_inbox(
&self,
session: &Session,
agent_id: &WalletAddress,
) -> Result<InboxAddress, BackendError>;

async fn list_inboxes(
&self,
session: &Session,
agent_id: &WalletAddress,
) -> Result<Vec<InboxAddress>, BackendError>;
}

#[cfg(test)]
Expand Down Expand Up @@ -333,6 +345,22 @@ mod tests {
) -> Result<(), BackendError> {
unimplemented!()
}

async fn provision_inbox(
&self,
_session: &Session,
_agent_id: &WalletAddress,
) -> Result<InboxAddress, BackendError> {
unimplemented!()
}

async fn list_inboxes(
&self,
_session: &Session,
_agent_id: &WalletAddress,
) -> Result<Vec<InboxAddress>, BackendError> {
unimplemented!()
}
}

#[test]
Expand Down
Loading
Loading