Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

61 changes: 61 additions & 0 deletions TODOS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# TODOs

## Deferred to v0.2 / v0.1+

### Twitter (X) scripted signup

Scripted account creation on X violates their developer terms and practically
requires phone verification + CAPTCHA solving. Not viable in the provisioner's
current form. Re-evaluate in v0.2 if any of the following land:
- An official developer API for account creation
- A different product angle where users bring existing X accounts and
AgentKeys only stores API credentials (not signup)
- A residential-proxy-browser setup (Browserbase etc.) with explicit legal
sign-off

Until then, `/agentkeys-record-scraper` will stop on CAPTCHA and flag this
service as unsupported.

### Instagram (Meta) scripted signup

Same story as Twitter, worse. Meta actively pursues bot accounts with device
fingerprinting + phone verification. Dropped entirely from the Phase C target
list per 2026-04-16 decision. No v0.2 plan to revisit unless the product
pivots in a way that makes Instagram credentials load-bearing.

### OpenRouter ToS compliance check

Per 2026-04-16 CEO review, confirm scripted account creation does not violate
OpenRouter's ToS before the first live Stage 5a provision. Repeat this check
for every new service added to Tier 2. Flag noted in Stage 5a "open item to
resolve before first live provision" section.

## Phase C — new scrapers to add after Stage 5a ships

Once Stage 5a ships (infrastructure + OpenRouter reference scraper), add the
following services via `/agentkeys-record-scraper` in sequence:

1. **Brave Search** — dev API, reasonable signup, verifiable key
2. **Jina Search** — dev API, minimal friction
3. **Anthropic** — replaces Twitter in the target list; dev-focused, standard OAuth + email
4. **Groq** — dev-focused API, fast signup
5. **Gemini (Google AI Studio)** — replaces Instagram in the target list; Google OAuth

Each session produces: a `scrapers/<slug>.ts` composing patterns, a HAR fixture,
a unit test, and possibly a new pattern extraction if the signup shape is not
yet in the library. See `~/.claude/skills/agentkeys-record-scraper/SKILL.md` for
the full workflow.

**Expected pattern extractions during Phase C:**
- `oauth_google.ts` — likely from Gemini (Google AI Studio uses Google OAuth)
- Additional archetypes if Anthropic or Groq surface non-email-OTP flows

## v0.1 milestone deliverables (named, to prevent drift)

Per 2026-04-16 CEO review, the following must ship as part of a named v0.1
milestone. Filing as TODOs to prevent "post-MVP" from becoming "never":

- Stage 5b: agentic fallback + audit trail + fallback→PR + `/agentkeys-record-scraper` skill usage
- Stage 6: npm package + install.sh + README polish + DX docs
- Stage 8: production hardening (daemon memory hygiene + CLI defensive features)
- Pattern 4 (Heima) audit submission infrastructure — see docs/spec/plans/development-stages.md Stage 9
3 changes: 3 additions & 0 deletions crates/agentkeys-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ path = "src/lib.rs"
[dependencies]
agentkeys-types = { workspace = true }
agentkeys-core = { workspace = true }
agentkeys-provisioner = { path = "../agentkeys-provisioner" }
clap = { version = "4", features = ["derive"] }
tokio = { workspace = true }
serde_json = { workspace = true }
Expand All @@ -25,7 +26,9 @@ reqwest = { version = "0.12", features = ["json"] }
assert_cmd = "2"
predicates = "3"
agentkeys-mock-server = { path = "../agentkeys-mock-server" }
agentkeys-provisioner = { path = "../agentkeys-provisioner" }
agentkeys-types = { workspace = true }
async-trait = { workspace = true }
tokio = { workspace = true }
reqwest = { version = "0.12", features = ["json"] }
axum = { version = "0.7", features = ["json"] }
Expand Down
104 changes: 103 additions & 1 deletion crates/agentkeys-cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
use std::collections::HashMap;
use std::sync::Arc;

use agentkeys_core::backend::{BackendError, CredentialBackend};
use agentkeys_core::mock_client::MockHttpClient;
pub use agentkeys_core::session_store;
use agentkeys_core::session_store::SessionStore;
use agentkeys_provisioner::{run_provision, ProvisionError, Provisioner};
use agentkeys_types::{
AuditEvent, AuditFilter, AuthToken, Scope, ServiceName, Session, WalletAddress,
};
Expand Down Expand Up @@ -769,7 +771,7 @@ pub async fn cmd_scope(
));
}

let mut new_scope = if let Some(set_val) = set {
let new_scope = if let Some(set_val) = set {
let mut services: Vec<ServiceName> = set_val
.split(',')
.map(|s| s.trim())
Expand Down Expand Up @@ -807,6 +809,106 @@ pub async fn cmd_scope(
))
}

fn format_provision_error(err: &ProvisionError) -> String {
match err {
ProvisionError::InProgress { active_service } => format!(
"Problem: Another provision is running for {}.\nCause: Provisioner serializes calls per daemon.\nFix: Wait and retry.\nDocs: https://github.com/litentry/agentKeys/blob/main/docs/spec/plans/development-stages.md",
active_service
),
ProvisionError::Tripwire { kind, step, .. } => format!(
"Problem: A script step timed out at '{}'.\nCause: The target site's DOM may have changed (tripwire: {:?}).\nFix: Open an issue at https://github.com/litentry/agentKeys/issues with the logs.\nDocs: https://github.com/litentry/agentKeys/blob/main/docs/spec/plans/development-stages.md",
step, kind
),
ProvisionError::StoreFailed { obtained_key_masked, .. } => format!(
"Problem: Credential provisioned but storage failed.\nCause: Backend store_credential returned an error.\nFix: Manually store the key with `agentkeys store <service> <key>`. Masked key for reference: {}.\nDocs: https://github.com/litentry/agentKeys/blob/main/docs/spec/plans/development-stages.md",
obtained_key_masked
),
ProvisionError::VerificationFailed { service, reason } => format!(
"Problem: Key verification failed for {}.\nCause: {}.\nFix: Re-run with --force to attempt a fresh provision.\nDocs: https://github.com/litentry/agentKeys/blob/main/docs/spec/plans/development-stages.md",
service, reason
),
other => format!(
"Problem: Provision failed.\nCause: {}.\nFix: Check logs and retry.\nDocs: https://github.com/litentry/agentKeys/blob/main/docs/spec/plans/development-stages.md",
other
),
}
}

pub struct ProvisionOutput {
pub stdout_line: String,
pub stderr_lines: Vec<String>,
}

pub async fn cmd_provision(
ctx: &CommandContext,
service: &str,
force: bool,
provisioner: Option<Arc<Provisioner>>,
) -> Result<ProvisionOutput> {
let session = ctx.load_session().context("load session (run `agentkeys init` first)")?;
let backend = ctx.backend();
let agent_id = session.wallet.clone();

if force {
eprintln!("existing key present — re-provisioning (--force)");
}

let provisioner = provisioner.unwrap_or_else(|| Arc::new(Provisioner::new()));

let script_command: Vec<String> = match service {
"openrouter" => vec![
"npx".to_string(),
"tsx".to_string(),
"provisioner-scripts/src/scrapers/openrouter.ts".to_string(),
],
other => {
return Err(anyhow!(
"Problem: Service '{}' not supported.\nCause: Only 'openrouter' is supported in Stage 5a.\nFix: Use a supported service name.\nDocs: https://github.com/litentry/agentKeys/blob/main/docs/spec/plans/development-stages.md",
other
));
}
};

let cmd_refs: Vec<&str> = script_command.iter().map(|s| s.as_str()).collect();
let repo_root = std::env::var("AGENTKEYS_REPO_ROOT")
.map(std::path::PathBuf::from)
.unwrap_or_else(|_| std::env::current_dir().unwrap_or_default());

let mut stderr_lines: Vec<String> = Vec::new();

let result = run_provision(
&provisioner,
service,
&cmd_refs,
HashMap::new(),
Some(&repo_root),
backend,
&session,
&agent_id,
force,
)
.await;

match result {
Ok(success) => {
if !success.stored {
let msg = format!(
"{} already provisioned, key valid (re-verify returned true)",
service
);
stderr_lines.push(msg);
}
Ok(ProvisionOutput {
stdout_line: success.obtained_key_masked,
stderr_lines,
})
}
Err(e) => {
Err(anyhow!("{}", format_provision_error(&e)))
}
}
}

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
23 changes: 21 additions & 2 deletions crates/agentkeys-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use agentkeys_cli::{
cmd_approve, cmd_feedback, cmd_init, cmd_link, cmd_read, cmd_recover, cmd_revoke, cmd_run,
cmd_scope, cmd_store, cmd_teardown, cmd_usage, CommandContext,
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,
};


Expand Down Expand Up @@ -156,6 +156,17 @@ enum Commands {
list: bool,
},

#[command(
about = "Provision (sign up and store) an API key for a service",
long_about = "Run the provisioner script to sign up for a service and store the credential.\n\nExamples:\n agentkeys provision openrouter\n agentkeys provision openrouter --force"
)]
Provision {
#[arg(help = "Service name to provision (e.g. openrouter)")]
service: String,
#[arg(long, help = "Re-provision even if a credential already exists")]
force: bool,
},

#[command(
about = "Open the feedback forum in your browser",
long_about = "Open https://github.com/agentkeys/agentkeys/discussions in the default browser.\n\nExamples:\n agentkeys feedback"
Expand Down Expand Up @@ -188,6 +199,14 @@ async fn main() {
Commands::Scope { agent, add, remove, set, list } => {
cmd_scope(&ctx, agent, add, remove, set.as_deref(), *list).await
}
Commands::Provision { service, force } => {
cmd_provision(&ctx, service, *force, None).await.map(|out| {
for line in &out.stderr_lines {
eprintln!("{}", line);
}
out.stdout_line
})
}
Commands::Feedback => Ok(cmd_feedback()),
};

Expand Down
Loading
Loading