From 5b9d485025978c0129f54e25a7914db97288f4e5 Mon Sep 17 00:00:00 2001 From: CyberSecurityUP Date: Fri, 10 Jul 2026 17:01:42 -0300 Subject: [PATCH] fix(repl): show recon/probe activity + don't let the idle guardrail kill recon MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Symptom: with a non-streaming subscription CLI (codex), a long/intense recon showed nothing in the feed ("phase starting") and the 5-min idle guardrail killed the run before any agent ran. - render_compact now SHOWS recon/probe/ai-recon/skills-audit/loaded/running lines (were dropped) so a long recon no longer looks frozen. - Idle guardrail reworked: resets on ANY streamed activity (not only new findings) and only ARMS after exploitation starts (agent launch / vote) โ€” recon can never trip it. Message: "no activity in N min". - RunLive.ingest sets phase=recon on recon/probe lines (was stuck at "starting"). --- neurosploit-rs/app/src/main.rs | 10 ++++++++-- neurosploit-rs/app/src/repl.rs | 24 ++++++++++++++++-------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/neurosploit-rs/app/src/main.rs b/neurosploit-rs/app/src/main.rs index 084581e..b362c76 100644 --- a/neurosploit-rs/app/src/main.rs +++ b/neurosploit-rs/app/src/main.rs @@ -1076,10 +1076,16 @@ pub(crate) fn render_compact(raw: &str) -> Option { "ai" => return None, // skip verbose model chatter in background feed _ => { let low = line.to_lowercase(); - if low.contains("recon complete") { "\x1b[36m ๐Ÿ” recon complete\x1b[0m".into() } + // Recon / probe activity โ€” SHOW it so a long recon (esp. via a + // non-streaming CLI like codex) doesn't look frozen. + if low.starts_with("probe:") { format!("\x1b[36m ๐Ÿ”Ž {}\x1b[0m", trunc1(line, 130)) } + else if low.contains("recon complete") { "\x1b[36m ๐Ÿ” recon complete\x1b[0m".into() } + else if low.starts_with("recon") || low.starts_with("ai-recon") || low.contains("recon round") || low.contains("intensity") { format!("\x1b[36m ๐Ÿ” {}\x1b[0m", trunc1(line, 130)) } + else if low.starts_with("skills audit") || low.starts_with("ai engagement") { format!("\x1b[36m ๐Ÿค– {}\x1b[0m", trunc1(line, 130)) } + else if low.starts_with("loaded ") || low.starts_with("running ") { format!("\x1b[36m ๐Ÿงญ {}\x1b[0m", trunc1(line, 130)) } else if low.contains("selected") && low.contains("agent") { format!("\x1b[36m ๐Ÿงญ {}\x1b[0m", trunc1(line, 110)) } else if low.starts_with("vote") && low.contains("confirmed") { format!("\x1b[1;32m โœ“ {}\x1b[0m", trunc1(line, 110)) } - else if low.starts_with("exploit") || low.starts_with("test ") || low.contains("launching agent") { format!("\x1b[35m ๐Ÿงช {}\x1b[0m", trunc1(line, 110)) } + else if low.starts_with("exploit") || low.starts_with("test ") || low.starts_with("ai ") || low.starts_with("skill ") || low.contains("launching agent") { format!("\x1b[35m ๐Ÿงช {}\x1b[0m", trunc1(line, 110)) } else if low.starts_with("vote") { format!("\x1b[2m ยท {}\x1b[0m", trunc1(line, 110)) } else if low.contains("fail") || low.contains("error") { format!("\x1b[31m โœ— {}\x1b[0m", trunc1(line, 110)) } else { return None; } diff --git a/neurosploit-rs/app/src/repl.rs b/neurosploit-rs/app/src/repl.rs index 112d771..acb5b4b 100644 --- a/neurosploit-rs/app/src/repl.rs +++ b/neurosploit-rs/app/src/repl.rs @@ -50,7 +50,7 @@ impl RunLive { let low = line.to_lowercase(); if low.contains("token/quota exhausted") || low.contains("run is paused") { self.phase = "paused (quota)".into(); } else if low.contains("resumed โ€” retrying") { self.phase = "exploiting".into(); } - else if low.contains("recon complete") { self.phase = "recon".into(); } + else if low.starts_with("recon") || low.starts_with("ai-recon") || low.contains("recon round") || low.contains("intensity") || low.starts_with("probe:") { self.phase = "recon".into(); } else if low.contains("selected") && low.contains("agent") { self.phase = "planning".into(); if let Some(n) = line.split_whitespace().find_map(|t| t.parse::().ok()) { self.agents = n; } @@ -1051,9 +1051,10 @@ async fn start_background(base: &Path, s: &Session, reader: &mut Reader, tokio::spawn(async move { let crate::Spawned { task, mut rx, workdir, .. } = sp; let mut last_saved = 0usize; - let mut last_find = Instant::now(); // time of the last NEW finding + let mut last_activity = Instant::now(); // last sign of PROGRESS (any activity) let mut idle_fired = false; let mut tool_events = 0usize; // exec/net/read/browser activity seen + let mut exploiting = false; // guardrail only arms once exploitation starts let mut ticker = tokio::time::interval(std::time::Duration::from_secs(15)); ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); loop { @@ -1061,14 +1062,20 @@ 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); + let low = line.to_lowercase(); if line.contains("exec:") || line.contains("net:") || line.contains("read:") || line.contains("browser") { tool_events += 1; } + // ANY streamed line is progress โ†’ reset the idle clock (a long + // recon or active tool use must NOT count as idle). + last_activity = Instant::now(); + // Exploitation has begun once agents launch / vote โ€” only then arm the guardrail. + if low.contains("launching agent") || low.starts_with("exploit ") || low.starts_with("test ") + || low.starts_with("ai ") || low.starts_with("skill ") || low.starts_with("vote") { exploiting = true; } if let Some(out) = crate::render_compact(&line) { let _ = printer.print(out); } - // Checkpoint on each new finding; also resets the idle clock. + // Checkpoint on each new finding. let snap = { let l = live2.lock().unwrap(); if l.full.len() != last_saved { last_saved = l.full.len(); - last_find = Instant::now(); Some(LiveCheckpoint { target: l.target.clone(), mode: l.mode.into(), phase: l.phase.clone(), workdir: workdir.display().to_string(), @@ -1079,15 +1086,16 @@ async fn start_background(base: &Path, s: &Session, reader: &mut Reader, if let Some(c) = snap { save_checkpoint(&c); } } _ = ticker.tick() => { - // Idle guardrail: no NEW finding within the window โ†’ soft-stop - // (stop launching exploit agents, validate what was found). - if idle_secs > 0 && !idle_fired && last_find.elapsed().as_secs() >= idle_secs + // Idle guardrail: only after exploitation started AND no activity + // (not just no finding) within the window โ†’ soft-stop & validate. + // Recon never trips it โ€” it streams progress lines that reset the clock. + if idle_secs > 0 && !idle_fired && exploiting && last_activity.elapsed().as_secs() >= idle_secs && !soft_task.load(Ordering::Relaxed) && !cancel_task.load(Ordering::Relaxed) { idle_fired = true; *choice2.lock().unwrap() = StopMode::Validate; soft_task.store(true, Ordering::Relaxed); let _ = printer.print(format!( - "\x1b[33mโน idle guardrail: no new finding in {} min โ€” stopping & validating what was found\x1b[0m", + "\x1b[33mโน idle guardrail: no activity in {} min โ€” stopping & validating what was found\x1b[0m", idle_secs / 60)); } }