v3.4.0: subscription backend (Claude Code / Codex / Grok logins)

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) <noreply@anthropic.com>
This commit is contained in:
CyberSecurityUP
2026-06-22 16:59:35 -03:00
co-authored by Claude Opus 4.8
parent 56d3f0c723
commit d59f28f36d
8 changed files with 111 additions and 9 deletions
+7 -2
View File
@@ -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<ModelRef> = 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::<String>(256);
let printer = tokio::spawn(async move {
+4 -1
View File
@@ -59,6 +59,7 @@ async fn info(State(st): State<Arc<AppState>>) -> Json<Value> {
"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<Arc<AppState>>, Json(body): Json<Value>) -> Json<V
let vote_n = body.get("vote_n").and_then(|v| v.as_u64()).unwrap_or(3) as usize;
let max_agents = body.get("max_agents").and_then(|v| v.as_u64()).unwrap_or(0) as usize;
let offline = body.get("offline").and_then(|v| v.as_bool()).unwrap_or(false);
let subscription = body.get("subscription").and_then(|v| v.as_bool()).unwrap_or(false);
let lib = agents::load(&base);
let refs: Vec<ModelRef> = if models.is_empty() {
@@ -133,7 +135,7 @@ async fn run(State(st): State<Arc<AppState>>, Json(body): Json<Value>) -> Json<V
} else {
models.iter().map(|s| ModelRef::parse(s)).collect()
};
let pool = ModelPool::new(refs, 8);
let pool = ModelPool::with_auth(refs, 8, subscription);
let (tx, mut rx) = tokio::sync::mpsc::channel::<String>(256);
let stf = st2.clone();
@@ -160,6 +162,7 @@ async fn run(State(st): State<Arc<AppState>>, Json(body): Json<Value>) -> Json<V
cfg.vote_n = vote_n;
cfg.max_agents = max_agents;
cfg.offline = offline;
cfg.subscription = subscription;
let _ = tx.send(format!("=== target: {url} ===")).await;
let out = harness::run(cfg, &lib, &pool, tx.clone()).await;
all_findings.extend(out.findings);
+6 -2
View File
@@ -86,7 +86,10 @@
<div class="field"><label>Validator votes (N)</label><input id="voten" type="number" value="3" min="1" max="9"/></div>
<div class="field"><label>Max agents (0 = all)</label><input id="maxa" type="number" value="8" min="0"/></div>
</div>
<div class="toggles"><label class="toggle on" id="t-off"><input type="checkbox" id="offline" checked/> Offline (no API calls — pipeline self-test)</label></div>
<div class="toggles">
<label class="toggle on" id="t-off"><input type="checkbox" id="offline" checked/> Offline (pipeline self-test)</label>
<label class="toggle" id="t-sub"><input type="checkbox" id="subscription"/> Use subscription (Claude/Codex login)</label>
</div>
<div class="btns"><button class="run" id="go">▶ Run harness</button></div>
<div class="term" id="term"></div>
<div class="sevrow" id="sevrow"></div>
@@ -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;}