subscription login preflight + Playwright MCP fixes (browser install, codex wiring)

Why runs came back empty / "MCP didn't execute":
- Not logged in: a subscription CLI that isn't authenticated returns empty
  instantly (the Juice Shop symptom — every agent 0 candidates, no tool activity).
  Added models::cli_login_status + subscription_preflight(): before a run we check
  the primary provider's CLI is installed AND logged in and warn clearly if not
  (CLI run_mode + REPL start_background).
- Missing browser: ensure_playwright_mcp now also runs `npx playwright install
  chromium` (best-effort; NEUROSPLOIT_SKIP_BROWSER_INSTALL=1 to skip) so the first
  browser action doesn't fail/hang.
- Codex MCP was mis-wired (`--config mcp_config_file=` is not a codex key). Now
  injects our .mcp.json servers via `-c mcp_servers.<name>.command/.args` TOML
  overrides — MCP works on Codex, not only Claude. gemini/grok remain built-in-tools
  only (no MCP flag).
- REPL diagnostic: subscription+MCP run with zero tool/browser events warns the
  CLI likely isn't logged in / MCP didn't start.
This commit is contained in:
CyberSecurityUP
2026-07-05 16:09:15 -03:00
parent 3ca04498a9
commit 4ac4faec32
4 changed files with 144 additions and 4 deletions
+27
View File
@@ -552,6 +552,32 @@ pub(crate) struct Spawned {
pub workdir: PathBuf,
}
/// When running in subscription mode, verify the local CLI is installed AND
/// logged in before the engagement starts — otherwise every agent comes back
/// empty and it looks like "0 findings" when the real cause is auth. Checks the
/// primary model's provider; prints a clear warning (non-fatal).
pub(crate) async fn subscription_preflight(cfg: &RunConfig) {
if !cfg.subscription || cfg.offline { return; }
let Some(primary) = cfg.models.first() else { return };
let provider = ModelRef::parse(primary).provider;
if harness::models::cli_binary_for(&provider).is_none() { return; }
print!(" [*] checking {provider} subscription login… ");
use std::io::Write; let _ = std::io::stdout().flush();
match harness::models::cli_login_status(&provider).await {
harness::models::LoginStatus::LoggedIn => println!("\r [*] {provider} subscription: logged in ✓ "),
harness::models::LoginStatus::NotLoggedIn => {
let cli = harness::models::cli_binary_for(&provider).unwrap_or("the CLI");
println!("\r \x1b[1;33m[!] {provider} subscription NOT logged in\x1b[0m — run `{cli}` and log in (e.g. `claude` → /login), then retry.");
println!(" \x1b[2m(without login every agent returns empty — this is usually why a run finds 0.)\x1b[0m");
}
harness::models::LoginStatus::NotInstalled => {
let cli = harness::models::cli_binary_for(&provider).unwrap_or("?");
println!("\r \x1b[1;33m[!] subscription CLI `{cli}` for {provider} is not installed\x1b[0m — install it or use an API key (drop --subscription).");
}
harness::models::LoginStatus::Unknown => println!("\r [*] {provider} subscription: login state unknown (continuing) "),
}
}
/// Set up + start an engagement (synchronous setup; the work runs in the task).
pub(crate) fn spawn_engagement(base: &Path, mut cfg: RunConfig, mcp: bool, mode: Mode) -> Spawned {
let lib = agents::load(base);
@@ -666,6 +692,7 @@ pub(crate) fn finalize_run(mut out: RunOutput, workdir: &Path) -> RunOutput {
}
async fn run_mode(base: &Path, cfg: RunConfig, mcp: bool, mode: Mode) -> anyhow::Result<RunOutput> {
subscription_preflight(&cfg).await;
let Spawned { mut task, mut rx, cancel, workdir, .. } = spawn_engagement(base, cfg, mcp, mode);
let printer = tokio::spawn(async move {
while let Some(line) = rx.recv().await { render_line(&line); }
+11
View File
@@ -830,6 +830,7 @@ async fn start_background(base: &Path, s: &Session, reader: &mut Reader,
cfg.auth = s.auth.clone();
if matches!(mode_e, crate::Mode::Grey) { cfg.repo = s.repo.clone(); }
crate::apply_creds(&mut cfg, s.creds.as_deref()).await;
crate::subscription_preflight(&cfg).await; // warn early if the CLI isn't logged in
let mut printer = reader.external_printer()?; // None on piped stdin → blocking fallback
let sp = crate::spawn_engagement(base, cfg, mcp, mode_e);
@@ -848,6 +849,7 @@ async fn start_background(base: &Path, s: &Session, reader: &mut Reader,
let choice = Arc::new(Mutex::new(StopMode::Run));
let soft_task = soft.clone(); // idle guardrail triggers a soft-stop (validate)
let cancel_task = cancel.clone();
let sub_mcp = s.subscription && mcp; // for the "browser/tools never engaged" diagnostic
let (live2, done2, hist2, choice2) = (live.clone(), done.clone(), history, choice.clone());
tokio::spawn(async move {
@@ -855,6 +857,7 @@ async fn start_background(base: &Path, s: &Session, reader: &mut Reader,
let mut last_saved = 0usize;
let mut last_find = Instant::now(); // time of the last NEW finding
let mut idle_fired = false;
let mut tool_events = 0usize; // exec/net/read/browser activity seen
let mut ticker = tokio::time::interval(std::time::Duration::from_secs(15));
ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
loop {
@@ -862,6 +865,7 @@ async fn start_background(base: &Path, s: &Session, reader: &mut Reader,
maybe = rx.recv() => {
let Some(line) = maybe else { break };
live2.lock().unwrap().ingest(&line);
if line.contains("exec:") || line.contains("net:") || line.contains("read:") || line.contains("browser") { tool_events += 1; }
if let Some(out) = crate::render_compact(&line) { let _ = printer.print(out); }
// Checkpoint on each new finding; also resets the idle clock.
let snap = {
@@ -896,6 +900,13 @@ async fn start_background(base: &Path, s: &Session, reader: &mut Reader,
let task_out = task.await.unwrap_or_default();
let mode_choice = *choice2.lock().unwrap();
// Diagnostic: subscription + MCP but the agents never ran a single tool/
// browser action → the CLI almost certainly isn't logged in (or the MCP
// didn't engage), which is why nothing was found.
if sub_mcp && tool_events == 0 {
let _ = printer.print("\x1b[1;33m[!] no browser/tool activity was observed this run — the subscription CLI is likely NOT logged in (run `claude` → /login) or the Playwright MCP didn't start. That's usually why a run finds 0.\x1b[0m".to_string());
}
if mode_choice == StopMode::Discard {
std::fs::remove_dir_all(&workdir).ok();
clear_checkpoint();