From d59f28f36d1d12f3c0add55f9dc090a44755b261 Mon Sep 17 00:00:00 2001 From: CyberSecurityUP Date: Mon, 22 Jun 2026 16:59:35 -0300 Subject: [PATCH] v3.4.0: subscription backend (Claude Code / Codex / Grok logins) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Rust harness can now use models two ways: - API: provider API key (OpenAI-compatible HTTP) — existing path - Subscription: drive the locally-installed agentic CLI login directly, no API key (anthropic→claude, openai→codex, xai→grok) - models.rs: ChatClient::chat_cli spawns the CLI (stdin prompt), cli_binary_for + installed_cli_backends + binary_in_path PATH detection - pool.rs: ModelPool::with_auth(subscription); one() routes per model - types/CLI: RunConfig.subscription + `run --subscription` flag - web: /api/run honors "subscription"; /api/info reports detected cli_backends; SPA gets a "Use subscription" toggle Verified live: `run --subscription --model anthropic:claude-haiku-4-5` drove the Claude subscription end-to-end (recon + agent + vote) with no API key set. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 3 + neurosploit-rs/app/src/main.rs | 9 ++- neurosploit-rs/app/src/web.rs | 5 +- neurosploit-rs/app/web/index.html | 8 ++- neurosploit-rs/crates/harness/src/lib.rs | 4 +- neurosploit-rs/crates/harness/src/models.rs | 63 +++++++++++++++++++++ neurosploit-rs/crates/harness/src/pool.rs | 23 +++++++- neurosploit-rs/crates/harness/src/types.rs | 5 ++ 8 files changed, 111 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 5a70bc7..e483ff4 100755 --- a/README.md +++ b/README.md @@ -29,7 +29,10 @@ and a **reinforcement-learning** loop that gets smarter every run. > ./target/release/neurosploit serve # web dashboard → :8788 > ./target/release/neurosploit run https://target.example --model anthropic:claude-opus-4-8 --model openai:gpt-5.1 > ./target/release/neurosploit run https://t.example --offline # pipeline self-test, no API keys +> ./target/release/neurosploit run https://t.example --subscription --model anthropic:claude-opus-4-8 # uses Claude Code login, no API key > ``` +> Two auth paths: **model APIs** (provider key) or **subscription** — drive your +> local **Claude Code** / **Codex** / **Grok** logins directly (no API key). > 11 OpenAI-compatible providers / 31 models (Claude, GPT, Grok, NVIDIA NIM, > DeepSeek, Mistral, Qwen, Groq, Together, OpenRouter, Ollama). Reads the same > `agents_md/` library (213 agents). diff --git a/neurosploit-rs/app/src/main.rs b/neurosploit-rs/app/src/main.rs index fa86e1f..d764bd2 100644 --- a/neurosploit-rs/app/src/main.rs +++ b/neurosploit-rs/app/src/main.rs @@ -33,6 +33,10 @@ enum Cmd { /// Exercise the pipeline without calling any model API. #[arg(long)] offline: bool, + /// Use local agentic CLI subscriptions (Claude Code / Codex / Grok) + /// instead of HTTP API keys. + #[arg(long)] + subscription: bool, }, /// Show agent library counts. Agents, @@ -84,18 +88,19 @@ async fn main() -> anyhow::Result<()> { } } } - Cmd::Run { url, models, max_agents, vote_n, offline } => { + Cmd::Run { url, models, max_agents, vote_n, offline, subscription } => { let url = if url.starts_with("http") { url } else { format!("https://{url}") }; let mut cfg = RunConfig::new(&url); cfg.max_agents = max_agents; cfg.vote_n = vote_n; cfg.offline = offline; + cfg.subscription = subscription; if !models.is_empty() { cfg.models = models; } let lib = agents::load(&base); let refs: Vec = cfg.models.iter().map(|s| ModelRef::parse(s)).collect(); - let pool = ModelPool::new(refs, cfg.concurrency); + let pool = ModelPool::with_auth(refs, cfg.concurrency, cfg.subscription); let (tx, mut rx) = tokio::sync::mpsc::channel::(256); let printer = tokio::spawn(async move { diff --git a/neurosploit-rs/app/src/web.rs b/neurosploit-rs/app/src/web.rs index 488fa73..374f010 100644 --- a/neurosploit-rs/app/src/web.rs +++ b/neurosploit-rs/app/src/web.rs @@ -59,6 +59,7 @@ async fn info(State(st): State>) -> Json { "version": "3.4.0", "agents": {"vulns": lib.vulns.len(), "meta": lib.meta.len(), "total": lib.total()}, "providers": provs, + "cli_backends": harness::installed_cli_backends(), })) } @@ -126,6 +127,7 @@ async fn run(State(st): State>, Json(body): Json) -> Json = if models.is_empty() { @@ -133,7 +135,7 @@ async fn run(State(st): State>, Json(body): Json) -> Json(256); let stf = st2.clone(); @@ -160,6 +162,7 @@ async fn run(State(st): State>, Json(body): Json) -> Json
-
+
+ + +
@@ -112,6 +115,7 @@ let INFO=null,AGENTS=[],lastRun=null; $$('.nav').forEach(n=>n.onclick=()=>{$$('.nav').forEach(x=>x.classList.remove('on'));n.classList.add('on'); $$('.view').forEach(v=>v.classList.remove('on'));$('#v-'+n.dataset.v).classList.add('on');}); $('#t-off').querySelector('input').onchange=e=>$('#t-off').classList.toggle('on',e.target.checked); +$('#t-sub').querySelector('input').onchange=e=>$('#t-sub').classList.toggle('on',e.target.checked); async function init(){ INFO=await (await fetch('/api/info')).json(); @@ -136,7 +140,7 @@ async function run(){ const targets=$('#targets').value.split('\n').map(s=>s.trim()).filter(Boolean); if(!targets.length){$('#targets').focus();$('#targets').style.borderColor='var(--crit)';return;} $('#go').disabled=true;$('#term').innerHTML='';$('#sevrow').style.display='none';$('#findings').innerHTML=''; - const body={targets,models:selectedModels(),vote_n:+$('#voten').value,max_agents:+$('#maxa').value,offline:$('#offline').checked}; + const body={targets,models:selectedModels(),vote_n:+$('#voten').value,max_agents:+$('#maxa').value,offline:$('#offline').checked,subscription:$('#subscription').checked}; logLine('Queued '+targets.length+' target(s) · panel: '+(body.models.join(', ')||'default')); const r=await (await fetch('/api/run',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)})).json(); if(r.error){logLine('ERROR: '+r.error);$('#go').disabled=false;return;} diff --git a/neurosploit-rs/crates/harness/src/lib.rs b/neurosploit-rs/crates/harness/src/lib.rs index d77586d..372425f 100644 --- a/neurosploit-rs/crates/harness/src/lib.rs +++ b/neurosploit-rs/crates/harness/src/lib.rs @@ -14,7 +14,9 @@ pub mod report; pub mod types; pub use agents::{Agent, Library}; -pub use models::{provider_for, providers, ChatClient, ModelRef, Provider}; +pub use models::{ + cli_binary_for, installed_cli_backends, provider_for, providers, ChatClient, ModelRef, Provider, +}; pub use pipeline::run; pub use pool::ModelPool; pub use types::{Finding, RunConfig}; diff --git a/neurosploit-rs/crates/harness/src/models.rs b/neurosploit-rs/crates/harness/src/models.rs index d6420a8..8659dfe 100644 --- a/neurosploit-rs/crates/harness/src/models.rs +++ b/neurosploit-rs/crates/harness/src/models.rs @@ -1,6 +1,9 @@ use anyhow::{anyhow, Result}; use serde::Serialize; +use std::process::Stdio; use std::time::Duration; +use tokio::io::AsyncWriteExt; +use tokio::process::Command; /// A model provider exposing an OpenAI-compatible `/chat/completions` endpoint. #[derive(Clone, Debug, Serialize)] @@ -119,6 +122,66 @@ impl ChatClient { } } +impl ChatClient { + /// Complete via a locally-installed **agentic CLI subscription** (Claude + /// Code / Codex / Grok) instead of an API key. This uses the user's logged-in + /// subscription, so no provider key is required. + pub async fn chat_cli(&self, provider: &str, model: &str, system: &str, user: &str) -> Result { + let bin = cli_binary_for(provider) + .ok_or_else(|| anyhow!("no CLI/subscription backend for provider '{}'", provider))?; + let prompt = format!("{system}\n\n{user}"); + let mut cmd = Command::new(bin); + match bin { + // Claude Code headless print mode (uses the Claude subscription login). + "claude" => { + cmd.arg("-p").arg("--model").arg(model); + } + // Codex non-interactive exec (uses the ChatGPT/Codex login), prompt on stdin. + "codex" => { + cmd.arg("exec").arg("--model").arg(model).arg("-"); + } + // Grok CLI, prompt on stdin (best-effort flags). + "grok" => { + cmd.arg("--model").arg(model); + } + _ => {} + } + cmd.stdin(Stdio::piped()).stdout(Stdio::piped()).stderr(Stdio::piped()); + let mut child = cmd.spawn().map_err(|e| anyhow!("spawn {} failed: {}", bin, e))?; + if let Some(mut stdin) = child.stdin.take() { + stdin.write_all(prompt.as_bytes()).await?; + // Drop closes stdin so the CLI processes the prompt and exits. + } + let out = child.wait_with_output().await?; + if !out.status.success() { + return Err(anyhow!("{} subscription CLI failed: {}", bin, truncate(&String::from_utf8_lossy(&out.stderr), 200))); + } + Ok(String::from_utf8_lossy(&out.stdout).trim().to_string()) + } +} + +/// Map a provider to its local agentic CLI binary (subscription backend). +pub fn cli_binary_for(provider: &str) -> Option<&'static str> { + match provider { + "anthropic" => Some("claude"), + "openai" => Some("codex"), + "xai" => Some("grok"), + _ => None, + } +} + +/// Is `name` an executable found on PATH? +pub fn binary_in_path(name: &str) -> bool { + std::env::var_os("PATH") + .map(|path| std::env::split_paths(&path).any(|dir| dir.join(name).is_file())) + .unwrap_or(false) +} + +/// Which subscription CLI backends are installed locally. +pub fn installed_cli_backends() -> Vec<&'static str> { + ["claude", "codex", "grok"].into_iter().filter(|b| binary_in_path(b)).collect() +} + impl Default for ChatClient { fn default() -> Self { Self::new() diff --git a/neurosploit-rs/crates/harness/src/pool.rs b/neurosploit-rs/crates/harness/src/pool.rs index d47f7b6..4403705 100644 --- a/neurosploit-rs/crates/harness/src/pool.rs +++ b/neurosploit-rs/crates/harness/src/pool.rs @@ -1,18 +1,26 @@ -use crate::models::{ChatClient, ModelRef}; +use crate::models::{cli_binary_for, ChatClient, ModelRef}; use anyhow::{anyhow, Result}; use std::sync::Arc; use tokio::sync::Semaphore; /// A pool of candidate models with a global concurrency cap and provider /// failover. The same panel of models is reused for validator voting. +/// +/// `subscription = true` routes each model through its local agentic CLI +/// (Claude Code / Codex / Grok login) instead of an HTTP API key. pub struct ModelPool { client: ChatClient, sem: Arc, pub candidates: Vec, + pub subscription: bool, } impl ModelPool { pub fn new(models: Vec, concurrency: usize) -> Self { + Self::with_auth(models, concurrency, false) + } + + pub fn with_auth(models: Vec, concurrency: usize, subscription: bool) -> Self { let concurrency = concurrency.max(1); ModelPool { client: ChatClient::new(), @@ -22,16 +30,25 @@ impl ModelPool { } else { models }, + subscription, } } + /// One completion for a model, via subscription CLI or HTTP API. + async fn one(&self, m: &ModelRef, system: &str, user: &str) -> Result { + if self.subscription && cli_binary_for(&m.provider).is_some() { + return self.client.chat_cli(&m.provider, &m.model, system, user).await; + } + self.client.chat(m, system, user).await + } + /// Complete a prompt, trying each candidate model until one succeeds. /// Returns the model that answered and its text. pub async fn complete(&self, system: &str, user: &str) -> Result<(ModelRef, String)> { let _permit = self.sem.acquire().await.expect("semaphore closed"); let mut last = anyhow!("no candidate models"); for m in &self.candidates { - match self.client.chat(m, system, user).await { + match self.one(m, system, user).await { Ok(text) => return Ok((m.clone(), text)), Err(e) => last = e, } @@ -51,7 +68,7 @@ impl ModelPool { Ok(p) => p, Err(_) => break, }; - if let Ok(text) = self.client.chat(m, system, user).await { + if let Ok(text) = self.one(m, system, user).await { total += 1; let t = text.to_lowercase(); if t.contains("\"verdict\": \"confirmed\"") diff --git a/neurosploit-rs/crates/harness/src/types.rs b/neurosploit-rs/crates/harness/src/types.rs index ed49fcf..591cda9 100644 --- a/neurosploit-rs/crates/harness/src/types.rs +++ b/neurosploit-rs/crates/harness/src/types.rs @@ -70,6 +70,10 @@ pub struct RunConfig { /// Offline mode: exercise the full pipeline without calling any model API. #[serde(default)] pub offline: bool, + /// Use local agentic CLI subscriptions (Claude Code / Codex / Grok) instead + /// of HTTP API keys. + #[serde(default)] + pub subscription: bool, } fn default_vote() -> usize { @@ -88,6 +92,7 @@ impl RunConfig { concurrency: 8, max_agents: 0, offline: false, + subscription: false, } } }