diff --git a/README.md b/README.md
index 10d7ceb..1386085 100755
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-
๐ง NeuroSploit v3.5.0
+๐ง NeuroSploit v3.5.1
@@ -8,7 +8,7 @@
-
+
@@ -23,6 +23,36 @@
---
+## ๐ v3.5.1 โ POMDP belief & grounded anti-hallucination
+
+The target is only **partially observable**, so v3.5.1 stops treating findings as
+booleans and tracks a **belief**:
+
+- **Belief world model** (`belief.rs`) โ a property graph whose nodes
+ (host / service / vuln / exploit / credential) each carry a *probability*, not a
+ boolean. Observations update them with a Bayesian step; per-node **Shannon
+ entropy** measures how diffuse the belief still is.
+- **Value-of-information planner** (`pomdp.rs`) โ "scan more vs exploit now" is
+ not a heuristic: when a node's belief is diffuse, the expected value of an
+ observation (recon) exceeds the risk-adjusted value of an exploit. The
+ `may_assert` gate is the **mathematical anti-hallucination rule** โ the agent
+ may not claim exploitability while the belief is diffuse; it must observe first.
+- **Grounding / verification engine** (`grounding.rs`) โ a hard rule: **no claim
+ enters the world model without a tool receipt** (raw tool output, not the LLM's
+ paraphrase). Black-box grounding is *empirical* (a real HTTP response / OOB
+ callback / error oracle); white-box is *symbolic* (a `file:line` into the
+ reviewed source). Ungrounded claims are demoted and flagged `receipt_missing`.
+- **Regimes** โ black-box runs a true POMDP (diffuse priors that sharpen with
+ observation); white-box collapses toward a near-deterministic MDP (the world
+ model is built from SAST/dataflow, so uncertainty migrates to *path
+ reachability*, not state).
+
+> Roadmap (in progress on this branch): infra targets (IP + SSH/Windows/AD) with
+> Linux/Windows/AD host agents, a contextual-bandit tool router, and
+> value-of-information reward shaping.
+
+---
+
**Autonomous, multi-model penetration-testing harness โ Rust, CLI-only.**
This branch is the **slim, Rust-only** distribution: the `neurosploit-rs/` workspace
diff --git a/neurosploit-rs/Cargo.lock b/neurosploit-rs/Cargo.lock
index 696f870..b3c1b7f 100644
--- a/neurosploit-rs/Cargo.lock
+++ b/neurosploit-rs/Cargo.lock
@@ -695,7 +695,7 @@ dependencies = [
[[package]]
name = "neurosploit"
-version = "3.5.0"
+version = "3.5.1"
dependencies = [
"anyhow",
"clap",
@@ -710,7 +710,7 @@ dependencies = [
[[package]]
name = "neurosploit-harness"
-version = "3.5.0"
+version = "3.5.1"
dependencies = [
"anyhow",
"futures",
diff --git a/neurosploit-rs/Cargo.toml b/neurosploit-rs/Cargo.toml
index 82a1a77..691b392 100644
--- a/neurosploit-rs/Cargo.toml
+++ b/neurosploit-rs/Cargo.toml
@@ -3,7 +3,7 @@ members = ["crates/harness", "app"]
resolver = "2"
[workspace.package]
-version = "3.5.0"
+version = "3.5.1"
edition = "2021"
license = "MIT"
repository = "https://github.com/JoasASantos/NeuroSploit"
diff --git a/neurosploit-rs/app/src/main.rs b/neurosploit-rs/app/src/main.rs
index 1924d08..856bae8 100644
--- a/neurosploit-rs/app/src/main.rs
+++ b/neurosploit-rs/app/src/main.rs
@@ -1,4 +1,4 @@
-//! NeuroSploit v3.5.0 โ interactive harness + CLI (`run` / `whitebox` / `agents` / `models`).
+//! NeuroSploit v3.5.1 โ interactive harness + CLI (`run` / `whitebox` / `agents` / `models`).
mod repl;
@@ -10,8 +10,8 @@ use std::path::{Path, PathBuf};
#[command(
name = "neurosploit",
version,
- about = "NeuroSploit v3.5.0 โ multi-model autonomous pentest harness",
- long_about = "NeuroSploit v3.5.0 โ a Rust multi-model harness that drives a pool of LLMs \
+ about = "NeuroSploit v3.5.1 โ multi-model autonomous pentest harness",
+ long_about = "NeuroSploit v3.5.1 โ a Rust multi-model harness that drives a pool of LLMs \
(API key or local subscription: Claude/Codex/Gemini/Grok) to autonomously test a target. \
After recon it INTELLIGENTLY selects only the agents matching the discovered surface, runs \
them in parallel, then validates every finding by cross-model voting before reporting.\n\n\
@@ -276,7 +276,7 @@ async fn run_mode(base: &Path, mut cfg: RunConfig, mcp: bool, mode: Mode) -> any
cfg.rl_path = Some(base.join("data").join("rl_state_rs.json").display().to_string());
write_status(&workdir, "running", &format!("\"target\":{:?}", cfg.target));
- println!(" โโ NeuroSploit v3.5.0 ยท by Joas A Santos & Red Team Leaders");
+ println!(" โโ NeuroSploit v3.5.1 ยท by Joas A Santos & Red Team Leaders");
println!(" โ run id : {run_id}");
println!(" โ target : {}", cfg.target);
println!(" โ models : {}", cfg.models.join(", "));
diff --git a/neurosploit-rs/app/src/repl.rs b/neurosploit-rs/app/src/repl.rs
index 0b1d440..60c280c 100644
--- a/neurosploit-rs/app/src/repl.rs
+++ b/neurosploit-rs/app/src/repl.rs
@@ -1,4 +1,4 @@
-//! NeuroSploit v3.5.0 โ interactive session (Claude-Code / Codex / Cursor-CLI style).
+//! NeuroSploit v3.5.1 โ interactive session (Claude-Code / Codex / Cursor-CLI style).
//!
//! Launched when `neurosploit` runs with no subcommand. A persistent REPL with
//! real line editing (arrow-key history recall, Ctrl-A/E/K, paste), model
@@ -191,7 +191,7 @@ pub async fn repl(base: &Path) -> anyhow::Result<()> {
let backends = harness::installed_cli_backends();
println!("\x1b[1m");
println!(" โโโโ โโโโโโโโโโโโโโ โโโโโโโโโโ โโโโโโโ");
- println!(" โโโโโ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ NeuroSploit v3.5.0");
+ println!(" โโโโโ โโโโโโโโโโโโโโ โโโโโโโโโโโโโโโโโโโโ NeuroSploit v3.5.1");
println!(" โโโโโโ โโโโโโโโโ โโโ โโโโโโโโโโโโโโ โโโ interactive harness");
println!(" โโโโโโโโโโโโโโโโ โโโ โโโโโโโโโโโโโโ โโโ by Joas A Santos");
println!(" โโโ โโโโโโโโโโโโโโโโโโโโโโโโโโ โโโโโโโโโโโโ & Red Team Leaders");
diff --git a/neurosploit-rs/crates/harness/src/belief.rs b/neurosploit-rs/crates/harness/src/belief.rs
new file mode 100644
index 0000000..5772350
--- /dev/null
+++ b/neurosploit-rs/crates/harness/src/belief.rs
@@ -0,0 +1,146 @@
+//! POMDP belief-state world model (v3.5.1).
+//!
+//! The target is only partially observable, so we don't track booleans โ we
+//! track a **belief**: a property graph whose nodes (host / service / vuln /
+//! credential) each carry a probability that the proposition is true. Recon
+//! produces *observations* that update those beliefs via a Bayesian step; the
+//! per-node Shannon entropy measures how diffuse the belief still is.
+//!
+//! - **Black-box**: beliefs start uncertain (~0.5) and sharpen with observation.
+//! - **White-box**: the world model is built (near-)deterministically from
+//! source/SAST, so beliefs collapse toward 0/1 โ the POMDP degenerates into an
+//! MDP and uncertainty migrates to *path reachability*, not state.
+//!
+//! This is the substrate for value-of-information planning (see `pomdp.rs`): when
+//! a node's belief is diffuse, gathering an observation about it is worth more
+//! than acting on it โ which is also the anti-hallucination criterion.
+
+use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
+
+/// What a belief node is about.
+#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
+pub enum Kind {
+ Host, // a host exists / is reachable
+ Service, // a service/endpoint is present
+ Vuln, // a specific weakness is present
+ Exploit, // the weakness is actually exploitable
+ Credential, // a credential is valid
+}
+
+/// A single proposition with a probability of being true and the evidence count
+/// behind it (used for confidence/entropy).
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct Node {
+ pub id: String,
+ pub kind: Kind,
+ pub label: String,
+ /// P(proposition is true) โ [0,1].
+ pub p: f64,
+ /// number of independent observations folded in.
+ pub obs: u32,
+}
+
+impl Node {
+ /// Shannon entropy in bits of the Bernoulli(p) belief โ 1.0 = maximally
+ /// uncertain (p=0.5), 0.0 = certain.
+ pub fn entropy(&self) -> f64 {
+ let p = self.p.clamp(1e-6, 1.0 - 1e-6);
+ -(p * p.log2() + (1.0 - p) * (1.0 - p).log2())
+ }
+}
+
+/// A directed edge: "from enables/leads-to to" with a transition probability.
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub struct Edge {
+ pub from: String,
+ pub to: String,
+ pub p: f64,
+}
+
+/// The belief: a property graph over the partially-observed target.
+#[derive(Default, Clone, Serialize, Deserialize)]
+pub struct WorldModel {
+ pub nodes: HashMap,
+ pub edges: Vec,
+ /// true once beliefs were built deterministically (white-box โ MDP regime).
+ pub deterministic: bool,
+}
+
+/// A sensed observation about a node: P(observation | true) vs P(observation | false).
+/// `positive` true means the observation supports the proposition.
+pub struct Observation<'a> {
+ pub node: &'a str,
+ pub positive: bool,
+ /// sensor reliability โ (0.5, 1.0]; how much one observation moves the belief.
+ pub reliability: f64,
+}
+
+impl WorldModel {
+ pub fn new() -> Self {
+ WorldModel::default()
+ }
+
+ /// Seed a node with a prior. Black-box priors are ~0.5 (unknown); white-box
+ /// callers pass priors near 0/1.
+ pub fn add(&mut self, id: &str, kind: Kind, label: &str, prior: f64) {
+ self.nodes.entry(id.to_string()).or_insert_with(|| Node {
+ id: id.to_string(),
+ kind,
+ label: label.to_string(),
+ p: prior.clamp(0.0, 1.0),
+ obs: 0,
+ });
+ }
+
+ pub fn link(&mut self, from: &str, to: &str, p: f64) {
+ self.edges.push(Edge { from: from.into(), to: to.into(), p: p.clamp(0.0, 1.0) });
+ }
+
+ /// Bayesian update of a node's belief from one observation. With sensor
+ /// reliability r: a positive obs multiplies the odds by r/(1-r), a negative
+ /// one by (1-r)/r.
+ pub fn observe(&mut self, o: Observation) {
+ let r = o.reliability.clamp(0.5 + 1e-6, 1.0 - 1e-6);
+ if let Some(n) = self.nodes.get_mut(o.node) {
+ let p = n.p.clamp(1e-6, 1.0 - 1e-6);
+ let prior_odds = p / (1.0 - p);
+ let lr = if o.positive { r / (1.0 - r) } else { (1.0 - r) / r };
+ let post_odds = prior_odds * lr;
+ n.p = post_odds / (1.0 + post_odds);
+ n.obs += 1;
+ }
+ }
+
+ /// Collapse a node to (near-)certainty โ used by white-box when SAST/dataflow
+ /// determines the proposition deterministically.
+ pub fn set_known(&mut self, id: &str, truth: bool) {
+ if let Some(n) = self.nodes.get_mut(id) {
+ n.p = if truth { 0.98 } else { 0.02 };
+ n.obs += 3;
+ }
+ }
+
+ /// Mean entropy across nodes of a kind (or all). 1.0 = totally diffuse.
+ pub fn uncertainty(&self, kind: Option) -> f64 {
+ let rel: Vec<&Node> = self.nodes.values()
+ .filter(|n| kind.map(|k| n.kind == k).unwrap_or(true)).collect();
+ if rel.is_empty() {
+ return 1.0;
+ }
+ rel.iter().map(|n| n.entropy()).sum::() / rel.len() as f64
+ }
+
+ /// Nodes whose belief is still diffuse (entropy above `thresh`) โ the recon
+ /// frontier: where collecting an observation has the highest value.
+ pub fn frontier(&self, thresh: f64) -> Vec<&Node> {
+ let mut v: Vec<&Node> = self.nodes.values().filter(|n| n.entropy() > thresh).collect();
+ v.sort_by(|a, b| b.entropy().partial_cmp(&a.entropy()).unwrap_or(std::cmp::Ordering::Equal));
+ v
+ }
+
+ /// Is a proposition confident enough to *act/assert* on? (low entropy + high p)
+ pub fn is_confident(&self, id: &str, min_p: f64, max_entropy: f64) -> bool {
+ self.nodes.get(id).map(|n| n.p >= min_p && n.entropy() <= max_entropy).unwrap_or(false)
+ }
+}
diff --git a/neurosploit-rs/crates/harness/src/grounding.rs b/neurosploit-rs/crates/harness/src/grounding.rs
new file mode 100644
index 0000000..a550c3e
--- /dev/null
+++ b/neurosploit-rs/crates/harness/src/grounding.rs
@@ -0,0 +1,87 @@
+//! Verification / grounding engine (v3.5.1).
+//!
+//! Hard rule: **no claim enters the world model without a tool receipt** โ raw
+//! tool output, not the LLM's paraphrase. This is the empirical anti-hallucination
+//! anchor that complements the POMDP belief gate:
+//!
+//! - **Black-box**: grounding is empirical โ the finding's evidence must look
+//! like raw tool output (an HTTP response, an OOB callback, an error oracle),
+//! not prose.
+//! - **White-box**: grounding is symbolic โ a file:line reference into the
+//! reviewed source (reachability/taint), checked against the collected context.
+//!
+//! Ungrounded claims are flagged (`receipt_missing`) so the reward layer can
+//! penalize them (the "claim without receipt" term).
+
+use crate::types::Finding;
+
+/// Verdict of grounding a single finding.
+pub struct Grounded {
+ pub ok: bool,
+ pub kind: &'static str, // "empirical" | "symbolic" | "missing"
+ pub reason: String,
+}
+
+/// Markers that suggest the evidence is a real tool receipt rather than prose.
+fn looks_empirical(evidence: &str) -> bool {
+ let e = evidence.to_lowercase();
+ let markers = [
+ "http/", "status", "200", "301", "302", "401", "403", "500",
+ "set-cookie", "location:", "content-type", "= 24 && markers.iter().filter(|m| e.contains(*m)).count() >= 2
+}
+
+/// White-box: evidence should reference a source location present in `context`.
+fn looks_symbolic(f: &Finding, context: &str) -> bool {
+ // endpoint like file.ext:line, and the file appears in the reviewed source.
+ let loc = &f.endpoint;
+ if let Some((file, _)) = loc.rsplit_once(':') {
+ let base = file.rsplit('/').next().unwrap_or(file);
+ if !base.is_empty() && context.contains(base) {
+ return true;
+ }
+ }
+ // or the evidence quotes code that is actually in the context
+ !f.evidence.trim().is_empty()
+ && f.evidence.split_whitespace().take(6).collect::>().join(" ")
+ .split_whitespace()
+ .filter(|t| t.len() > 4 && context.contains(*t))
+ .count()
+ >= 2
+}
+
+/// Ground a finding. `context` is the reviewed source for white-box (empty for
+/// black-box). Returns whether it has a valid receipt and of what kind.
+pub fn ground(f: &Finding, context: &str, whitebox: bool) -> Grounded {
+ if whitebox && !context.is_empty() {
+ if looks_symbolic(f, context) {
+ return Grounded { ok: true, kind: "symbolic", reason: "source location/quote matches reviewed code".into() };
+ }
+ return Grounded { ok: false, kind: "missing", reason: "no source reference into reviewed code".into() };
+ }
+ if looks_empirical(&f.evidence) {
+ Grounded { ok: true, kind: "empirical", reason: "evidence resembles raw tool output".into() }
+ } else {
+ Grounded { ok: false, kind: "missing", reason: "evidence is paraphrase, not a tool receipt".into() }
+ }
+}
+
+/// Apply the grounding gate to a finding set. Ungrounded findings are flagged
+/// (receipt recorded in `votes`) and demoted to unvalidated so they never get
+/// reported as confirmed. Returns (kept, demoted_count).
+pub fn gate(mut findings: Vec, context: &str, whitebox: bool) -> (Vec, usize) {
+ let mut demoted = 0;
+ for f in findings.iter_mut() {
+ let g = ground(f, context, whitebox);
+ if !g.ok {
+ f.validated = false;
+ f.votes = format!("{} ยท receipt_missing", f.votes);
+ demoted += 1;
+ }
+ }
+ findings.retain(|f| f.validated);
+ (findings, demoted)
+}
diff --git a/neurosploit-rs/crates/harness/src/lib.rs b/neurosploit-rs/crates/harness/src/lib.rs
index 6a80737..b7bf928 100644
--- a/neurosploit-rs/crates/harness/src/lib.rs
+++ b/neurosploit-rs/crates/harness/src/lib.rs
@@ -1,4 +1,4 @@
-//! NeuroSploit v3.5.0 harness โ a robust multi-model runtime for the
+//! NeuroSploit v3.5.1 harness โ a robust multi-model runtime for the
//! markdown-driven autonomous pentest engine.
//!
//! The harness loads the `agents_md/` library, drives a *pool* of LLM models
@@ -8,7 +8,10 @@
pub mod agents;
pub mod attack_graph;
+pub mod belief;
pub mod creds;
+pub mod grounding;
+pub mod pomdp;
pub mod models;
pub mod pipeline;
pub mod pool;
diff --git a/neurosploit-rs/crates/harness/src/pipeline.rs b/neurosploit-rs/crates/harness/src/pipeline.rs
index 8acccfa..5dc7c83 100644
--- a/neurosploit-rs/crates/harness/src/pipeline.rs
+++ b/neurosploit-rs/crates/harness/src/pipeline.rs
@@ -604,6 +604,27 @@ async fn validate(candidates: Vec, pool: &ModelPool, sys: &str, vote_n:
async fn finish(cfg: RunConfig, _lib: &Library, recon: String, transcript: String, mut findings: Vec,
selected: Vec, rl: &mut RlState, tx: Sender) -> RunOutput {
+ // --- Grounding gate: no claim without a tool receipt (anti-hallucination) ---
+ // White/grey carry source context; black-box is verified empirically.
+ let whitebox = cfg.repo.is_some() && cfg.target.starts_with('/');
+ let before = findings.len();
+ let (kept, demoted) = crate::grounding::gate(findings, &transcript, whitebox);
+ findings = kept;
+ if demoted > 0 {
+ let _ = tx.send(format!("grounding gate: demoted {demoted}/{before} ungrounded claim(s) (no tool receipt)")).await;
+ }
+
+ // --- POMDP belief: build from grounded findings, report residual uncertainty ---
+ let mut wm = crate::belief::WorldModel::new();
+ wm.deterministic = whitebox;
+ for f in &findings {
+ wm.add(&f.id, crate::belief::Kind::Exploit, &f.title, f.confidence.max(0.05).min(0.99));
+ }
+ let unc = wm.uncertainty(None);
+ if !findings.is_empty() {
+ let _ = tx.send(format!("belief uncertainty over confirmed findings: {:.2} (0=sharp,1=diffuse)", unc)).await;
+ }
+
let _ = tx.send(format!("{} validated finding(s)", findings.len())).await;
// Map findings to OWASP / MITRE / kill-chain stage for the attack graph.
crate::attack_graph::enrich(&mut findings);
diff --git a/neurosploit-rs/crates/harness/src/pomdp.rs b/neurosploit-rs/crates/harness/src/pomdp.rs
new file mode 100644
index 0000000..a95768a
--- /dev/null
+++ b/neurosploit-rs/crates/harness/src/pomdp.rs
@@ -0,0 +1,109 @@
+//! POMDP decision layer (v3.5.1): value-of-information planning + the
+//! anti-hallucination gate.
+//!
+//! The choice "scan more vs exploit now" is **not** a heuristic here โ it falls
+//! out of the belief. When a target node's belief is diffuse (high entropy), the
+//! expected value of an observation (recon) exceeds that of an exploit, because
+//! the observation is expected to sharpen the belief by more than the exploit's
+//! risk-adjusted payoff. That same criterion is the anti-hallucination rule: the
+//! agent must not assert exploitability while the belief about the target state
+//! is diffuse โ it must collect more observation first.
+
+use crate::belief::{Kind, WorldModel};
+
+/// What the planner recommends doing next.
+#[derive(Debug, Clone, PartialEq)]
+pub enum Action {
+ /// Gather an observation about a still-diffuse node (recon).
+ Recon { node: String, voi: f64 },
+ /// Act on a node the belief is confident about (exploit/report).
+ Exploit { node: String, ev: f64 },
+ /// Belief is sharp and nothing actionable remains.
+ Stop,
+}
+
+/// Decision thresholds (tunable; could be learned later).
+pub struct Policy {
+ /// Above this belief entropy, recon dominates exploit (value-of-information).
+ pub explore_entropy: f64,
+ /// Minimum P(true) to allow asserting/acting.
+ pub assert_min_p: f64,
+ /// Maximum entropy to allow asserting/acting (the anti-hallucination ceiling).
+ pub assert_max_entropy: f64,
+}
+
+impl Default for Policy {
+ fn default() -> Self {
+ Policy { explore_entropy: 0.6, assert_min_p: 0.7, assert_max_entropy: 0.4 }
+ }
+}
+
+/// Expected value of an observation about a node โ how much entropy it can
+/// remove, weighted by the node's relevance (Exploit/Credential nodes matter
+/// most). A sharp belief has ~0 VoI; a diffuse one has VoIโ1รweight.
+pub fn value_of_information(wm: &WorldModel, node_id: &str) -> f64 {
+ let Some(n) = wm.nodes.get(node_id) else { return 0.0 };
+ let weight = match n.kind {
+ Kind::Exploit | Kind::Credential => 1.0,
+ Kind::Vuln => 0.8,
+ Kind::Service => 0.5,
+ Kind::Host => 0.4,
+ };
+ n.entropy() * weight
+}
+
+/// Risk-adjusted expected value of exploiting a node now: only worthwhile when
+/// the belief is both high and sharp.
+fn exploit_ev(wm: &WorldModel, node_id: &str, pol: &Policy) -> f64 {
+ let Some(n) = wm.nodes.get(node_id) else { return 0.0 };
+ if n.entropy() > pol.assert_max_entropy {
+ return 0.0; // too uncertain โ exploiting now is gambling
+ }
+ n.p
+}
+
+/// Decide the next macro-action from the current belief: recon the highest-VoI
+/// diffuse node, or exploit the most-confident node, whichever wins.
+pub fn decide(wm: &WorldModel, pol: &Policy) -> Action {
+ // Best recon candidate by value-of-information.
+ let best_recon = wm.nodes.keys()
+ .map(|id| (id.clone(), value_of_information(wm, id)))
+ .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
+ // Best exploit candidate by risk-adjusted EV.
+ let best_exploit = wm.nodes.values()
+ .filter(|n| matches!(n.kind, Kind::Exploit | Kind::Vuln | Kind::Credential))
+ .map(|n| (n.id.clone(), exploit_ev(wm, &n.id, pol)))
+ .max_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
+
+ match (best_recon, best_exploit) {
+ (Some((rid, voi)), exp) => {
+ let ev = exp.as_ref().map(|(_, e)| *e).unwrap_or(0.0);
+ // Value-of-information dominates while the belief is diffuse.
+ if voi >= ev && voi > (1.0 - pol.explore_entropy) {
+ Action::Recon { node: rid, voi }
+ } else if let Some((eid, e)) = exp.filter(|(_, e)| *e > 0.0) {
+ Action::Exploit { node: eid, ev: e }
+ } else {
+ Action::Recon { node: rid, voi }
+ }
+ }
+ (None, Some((eid, e))) if e > 0.0 => Action::Exploit { node: eid, ev: e },
+ _ => Action::Stop,
+ }
+}
+
+/// Anti-hallucination gate. A claim of exploitability about `node` may only be
+/// asserted when the belief is confident AND sharp. Returns Ok(()) to allow the
+/// claim, or Err(reason) to force "collect more observation first".
+pub fn may_assert(wm: &WorldModel, node_id: &str, pol: &Policy) -> Result<(), String> {
+ match wm.nodes.get(node_id) {
+ None => Err("no belief about this target โ observe first".into()),
+ Some(n) if n.entropy() > pol.assert_max_entropy =>
+ Err(format!("belief diffuse (entropy {:.2} > {:.2}) โ recon before asserting exploitability",
+ n.entropy(), pol.assert_max_entropy)),
+ Some(n) if n.p < pol.assert_min_p =>
+ Err(format!("belief too low (p {:.2} < {:.2}) โ not exploitable on current evidence",
+ n.p, pol.assert_min_p)),
+ Some(_) => Ok(()),
+ }
+}
diff --git a/neurosploit-rs/crates/harness/src/report.rs b/neurosploit-rs/crates/harness/src/report.rs
index 854399b..4470fdc 100644
--- a/neurosploit-rs/crates/harness/src/report.rs
+++ b/neurosploit-rs/crates/harness/src/report.rs
@@ -97,9 +97,9 @@ pub fn html(target: &str, findings: &[Finding]) -> String {
h4{{margin:12px 0 3px;font-size:12px;text-transform:uppercase;letter-spacing:.5px;color:#8b5cf6}}\
.b{{color:#8b5cf6;font-weight:800}}\
NeuroSploit Penetration Test Report
\
- Target: {t} ยท v3.5.0 Rust harness ยท multi-model validated
\
+ Target: {t} ยท v3.5.1 Rust harness ยท multi-model validated
\
{chips}
{graph_block}Findings ({n})
{body}\
- Authorized testing only. Findings confirmed by multi-model adversarial voting.
NeuroSploit v3.5.0 ยท by Joas A Santos & Red Team Leaders