Files
RuView/wifi-veil/src/bin/veil.rs
Claude 5114ed183f feat(wifi-veil): add self-contained standalone repository tree
Assemble `wifi-veil/` as an extraction-ready standalone repository for the WiFi
Veil privacy shield, decoupled from the RuView monorepo. The RuView copies under
v2/, harness/, firmware/, and docs/ are left untouched; this is an additive,
self-contained tree that can be split out to its own repo (e.g. ruvnet/wifi-veil).

Optimized for a standalone identity, with all monorepo coupling removed:

- Rust crate at the repo root: renamed `wifi-veil` (lib `wifi_veil`, bin `veil`),
  workspace-metadata inheritance inlined, own `[workspace]` root, release profile.
  Dependency-free and WASM-ready — it builds and tests OFFLINE, unlike the
  monorepo copy (which needs sibling submodules). Code is byte-identical, so the
  deterministic proof witness is unchanged.
- Portable C shield core + per-provider firmware scaffolds (openwifi/openwrt/
  nexmon/esp32) under firmware/; host C-core test passes.
- npm harness renamed `wifi-veil-harness`; its guidance paths/commands repointed
  to the standalone layout; manifest SHA-256 digests regenerated and verified.
- Docs: ADR-288/289/290 and the privacy-shield research bundle; research build
  commands/links normalized to the standalone crate.
- Root scaffolding: product README, dual LICENSE-MIT / LICENSE-APACHE,
  .gitignore, CHANGELOG, CONTRIBUTING, and a GitHub Actions CI workflow
  (Rust test/clippy/fmt + wasm build, C-core host test, harness smoke).

Validated locally: cargo fmt --check, cargo clippy --all-targets -D warnings,
cargo test (43 tests + witness), cargo build --lib --target wasm32-unknown-unknown,
make -C firmware/core test, and `node harness/bin/cli.js guidance` — all green.
No telemetry, build artifacts, or lockfile committed. All defense figures remain
SYNTHETIC / L0; compliant waveform controls only, never jamming.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01WEXNqzs7UsfNFBcP5yW21p
2026-08-09 17:06:48 +00:00

550 lines
17 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! `veil` — a custom, dependency-free terminal harness for the VEIL privacy
//! shield (ADR-288). It is the in-repo, native counterpart to the npm
//! metaharness (`harness/`, ADR-289): where that one
//! assists *development*, this one *drives the model* — an interactive TUI plus
//! scriptable subcommands over the same crate API the tests use.
//!
//! Std-only on purpose: no `crossterm`/`ratatui`, no external deps. The TUI is
//! a command-driven ANSI dashboard (line input, redraw on change), which keeps
//! the crate a pure leaf and lets the harness run in any pipe or CI.
//!
//! ```text
//! veil # TUI if attached to a terminal, else a one-shot report
//! veil tui # force the interactive dashboard
//! veil report # print the dashboard once (plain, pipe-friendly)
//! veil sweep # re-ID vs passes and throughput vs bits tables
//! veil optimize # run the hyper-optimizer, print the recommendation
//! veil adaptive <N> # derive the shield for a room of N candidate identities
//! veil proof # verify the deterministic witness
//! veil doctor # self-check (exit 0 = healthy)
//! ```
//!
//! All numbers are **SYNTHETIC / L0** — reproduced by `cargo test`, describing
//! the model, not real hardware.
use std::io::{self, BufRead, IsTerminal, Write};
use veil::optimize;
use veil::{run, ExperimentConfig, ExperimentReport, Metric, Proof};
use wifi_veil as veil;
// ---- ANSI palette (matches the VEIL Console: teal shield, amber threat) ----
const TEAL: &str = "\x1b[38;2;32;211;192m";
const AMBER: &str = "\x1b[38;2;245;158;75m";
const GOOD: &str = "\x1b[38;2;62;207;142m";
const CRIT: &str = "\x1b[38;2;242;107;111m";
const MUTE: &str = "\x1b[38;2;139;160;159m";
const BOLD: &str = "\x1b[1m";
const RST: &str = "\x1b[0m";
const BLOCKS: [char; 8] = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
/// Emit color for a real terminal; never when `NO_COLOR` is set; always when
/// `CLICOLOR_FORCE` is set (so piped captures keep their color).
fn color_enabled() -> bool {
if std::env::var_os("NO_COLOR").is_some() {
return false;
}
if std::env::var_os("CLICOLOR_FORCE").is_some() {
return true;
}
io::stdout().is_terminal()
}
/// Wrap `s` in `code` when color is on.
fn c(s: &str, code: &str, on: bool) -> String {
if on {
format!("{code}{s}{RST}")
} else {
s.to_string()
}
}
/// A raw color code, or "" when color is off — for inline `format!` colouring.
fn k(code: &'static str, on: bool) -> &'static str {
if on {
code
} else {
""
}
}
/// Shield-on re-ID at a given mixing budget, holding the rest of `cfg`.
fn reid_at(cfg: &ExperimentConfig, passes: usize) -> f32 {
let mut c = cfg.clone();
c.shield.givens_passes = passes;
run(&c).accuracy_shield_on
}
/// A block-sparkline character for a value in `[0, 1]`.
fn spark(v: f32) -> char {
let i = (v.clamp(0.0, 1.0) * 7.0).round() as usize;
BLOCKS[i.min(7)]
}
/// Render the full dashboard as colored lines (left-bar panel; no right border,
/// so ANSI escape width never has to be counted).
fn dashboard(cfg: &ExperimentConfig, on: bool) -> Vec<String> {
let rep: ExperimentReport = run(cfg);
let chance = rep.chance_level * 100.0;
let off = rep.accuracy_shield_off * 100.0;
let onp = rep.accuracy_shield_on * 100.0;
let tp = rep.throughput_ratio * 100.0;
let (state, scode) = if !cfg.shield.enabled {
("EXPOSED", CRIT)
} else if rep.passed() {
("PROTECTED", GOOD)
} else {
("AT RISK", AMBER)
};
let on_code = if onp <= rep.chance_band * 100.0 {
GOOD
} else {
AMBER
};
let tp_code = if tp >= 95.0 { GOOD } else { CRIT };
let bar = c("", MUTE, on);
let mut out = Vec::new();
out.push(c(
"┌──────────────────────────────────────────────────────────",
MUTE,
on,
));
out.push(format!(
"{} {}{}VEIL{} {}· wifi-sensing privacy shield{} {}{}{}",
bar,
k(BOLD, on),
k(TEAL, on),
k(RST, on),
k(MUTE, on),
k(RST, on),
k(scode, on),
state,
k(RST, on),
));
out.push(bar.clone());
out.push(format!(
"{} re-ID off {}{:>6.1}%{} re-ID on {}{:>5.1}%{} {}(chance {:.2}%){}",
bar,
k(AMBER, on),
off,
k(RST, on),
k(on_code, on),
onp,
k(RST, on),
k(MUTE, on),
chance,
k(RST, on),
));
out.push(format!(
"{} throughput {}{:>6.1}%{} emission {}{:.3}×{} {}· not jamming{}",
bar,
k(tp_code, on),
tp,
k(RST, on),
k(GOOD, on),
rep.compliance.energy_ratio,
k(RST, on),
k(MUTE, on),
k(RST, on),
));
out.push(bar.clone());
let cand = optimize::PASS_CANDIDATES;
let line: String = cand.iter().map(|&p| spark(reid_at(cfg, p))).collect();
out.push(format!(
"{} {}collapse{} {}{}{} {}passes {}{} · op {}{}",
bar,
k(MUTE, on),
k(RST, on),
k(TEAL, on),
line,
k(RST, on),
k(MUTE, on),
cand[0],
cand[cand.len() - 1],
cfg.shield.givens_passes,
k(RST, on),
));
out.push(bar.clone());
let metric = match cfg.attacker_metric {
Metric::Euclidean => "euclid",
Metric::Cosine => "cosine",
};
out.push(format!(
"{} {}config{} passes {} · bits {} · N {} · snr {:.0}dB · {}",
bar,
k(MUTE, on),
k(RST, on),
cfg.shield.givens_passes,
cfg.shield.feedback_bits,
cfg.scene.identities,
cfg.link.snr_db,
metric,
));
let (vlabel, vcode) = if !cfg.shield.enabled {
("SHIELD OFF — room exposed", CRIT)
} else if rep.passed() {
(
"✓ PASS — re-ID at chance · throughput ≥95% · compliant",
GOOD,
)
} else {
(
"△ OUT OF SPEC — raise passes/bits to re-enter the chance band",
AMBER,
)
};
out.push(format!(
"{} {}verdict{} {}{}{}",
bar,
k(MUTE, on),
k(RST, on),
k(vcode, on),
vlabel,
k(RST, on)
));
out.push(c(
"└──────────────────────────────────────────────────────────",
MUTE,
on,
));
out
}
/// Deployment presets (mirror `optimize::adaptive_shield` results per room).
fn preset(name: &str, cfg: &mut ExperimentConfig) -> bool {
let (n, passes, bits, snr) = match name {
"scif" => (64, 96, 5, 20.0),
"board" => (16, 96, 5, 25.0),
"ward" => (32, 96, 5, 15.0),
"hotel" => (48, 64, 5, 20.0),
_ => return false,
};
cfg.scene.identities = n;
cfg.shield.givens_passes = passes;
cfg.shield.feedback_bits = bits;
cfg.link.snr_db = snr;
true
}
fn print_dashboard(cfg: &ExperimentConfig, on: bool) {
for l in dashboard(cfg, on) {
println!("{l}");
}
}
fn cmd_sweep(cfg: &ExperimentConfig, on: bool) {
println!(
"{}re-ID (shield on) vs Givens passes — N={}{}",
k(MUTE, on),
cfg.scene.identities,
k(RST, on)
);
for &p in &optimize::PASS_CANDIDATES {
let robust =
optimize::passes_collapse_at_n(cfg, p, cfg.shield.feedback_bits, cfg.scene.identities);
println!(
" passes {:>3} re-ID {:>5.1}% {}",
p,
reid_at(cfg, p) * 100.0,
if robust {
c("collapses", GOOD, on)
} else {
c("above chance", AMBER, on)
}
);
}
println!(
"\n{}throughput vs feedback bits — snr={:.0}dB{}",
k(MUTE, on),
cfg.link.snr_db,
k(RST, on)
);
for bits in 1..=12u32 {
let mut s = cfg.shield.clone();
s.feedback_bits = bits;
let tp = cfg.link.throughput_ratio(&s) * 100.0;
let barlen = ((tp - 90.0).clamp(0.0, 10.0) / 10.0 * 24.0) as usize;
println!(
" {:>2} bit {:>6.3}% {}{}{}",
bits,
tp,
k(TEAL, on),
"".repeat(barlen),
k(RST, on)
);
}
let (sb, _) = optimize::spec_optimal_feedback_bits(cfg);
println!(" {}spec-optimal: {} bit{}", k(MUTE, on), sb, k(RST, on));
}
fn cmd_optimize(cfg: &ExperimentConfig, on: bool) {
let opt = veil::hyper_optimize(cfg);
let r = &opt.report;
println!("{}hyper-optimizer{}", k(BOLD, on), k(RST, on));
println!(" min robust passes : {}", opt.min_passes);
println!(
" shipped passes : {} {}(min × 2 margin, throughput-free){}",
opt.shipped_passes,
k(MUTE, on),
k(RST, on)
);
println!(
" spec-optimal bits : {} {}(model optimum {}){}",
opt.spec_optimal_bits,
k(MUTE, on),
opt.model_optimal_bits,
k(RST, on)
);
println!(
" result : re-ID {}{:.1}%{} · throughput {}{:.1}%{} · {}",
k(GOOD, on),
r.accuracy_shield_on * 100.0,
k(RST, on),
k(GOOD, on),
r.throughput_ratio * 100.0,
k(RST, on),
if r.passed() {
c("PASS", GOOD, on)
} else {
c("FAIL", CRIT, on)
}
);
println!(
" {}SNR → model-optimal bits: {:?}{}",
k(MUTE, on),
optimize::optimal_bits_across_snr(cfg),
k(RST, on)
);
}
fn cmd_adaptive(cfg: &ExperimentConfig, n: usize, on: bool) {
let sh = veil::adaptive_shield(cfg, n);
println!(
"adaptive shield for N={}: passes {} · bits {} {}(mixing budget is N-independent in this model){}",
n, sh.givens_passes, sh.feedback_bits, k(MUTE, on), k(RST, on)
);
}
fn cmd_proof(on: bool) -> i32 {
let w = Proof::witness(&Proof::run_reference());
let ok = w == Proof::EXPECTED_WITNESS;
println!(
"witness {:#018x} expected {:#018x} {}",
w,
Proof::EXPECTED_WITNESS,
if ok {
c("MATCH", GOOD, on)
} else {
c("DRIFT", CRIT, on)
}
);
i32::from(!ok)
}
fn cmd_doctor(on: bool) -> i32 {
let rep = run(&ExperimentConfig::default());
let checks = [
("reference experiment passes", rep.passed()),
(
"attack is real without shield",
rep.attack_is_effective_without_shield(),
),
("collapse drives to chance", rep.drives_to_chance()),
("throughput ≥ 95%", rep.preserves_throughput()),
("emission is compliant", rep.compliance.is_compliant()),
(
"deterministic witness matches",
Proof::witness(&Proof::run_reference()) == Proof::EXPECTED_WITNESS,
),
];
let mut ok = true;
for (label, pass) in checks {
ok &= pass;
println!(
"{} {label}",
if pass {
c("PASS", GOOD, on)
} else {
c("FAIL", CRIT, on)
}
);
}
println!(
"\nveil doctor: {}",
if ok {
c("all checks passed", GOOD, on)
} else {
c("problems found", CRIT, on)
}
);
i32::from(!ok)
}
fn help() {
println!(
"veil — VEIL privacy-shield harness (SYNTHETIC / L0)\n\n\
USAGE\n veil [command]\n\n\
COMMANDS\n\
\x20 tui interactive dashboard (default on a terminal)\n\
\x20 report print the dashboard once\n\
\x20 sweep re-ID vs passes + throughput vs bits\n\
\x20 optimize run the hyper-optimizer\n\
\x20 adaptive <N> derive the shield for N candidate identities\n\
\x20 proof verify the deterministic witness\n\
\x20 doctor self-check (exit 0 = healthy)\n\
\x20 help this text\n\n\
TUI COMMANDS (type + Enter)\n\
\x20 on | off toggle the shield\n\
\x20 passes <n> · bits <n> · n <k> · snr <db>\n\
\x20 metric euclid|cosine\n\
\x20 preset scif|board|ward|hotel\n\
\x20 run | optimize | proof | help | quit"
);
}
fn tui(mut cfg: ExperimentConfig, on: bool) {
let stdin = io::stdin();
let interactive = stdin.is_terminal();
let redraw = |cfg: &ExperimentConfig, msg: &str| {
if interactive {
print!("\x1b[2J\x1b[H");
}
print_dashboard(cfg, on);
if !msg.is_empty() {
println!(" {}{}{}", k(MUTE, on), msg, k(RST, on));
}
print!("{}veil{} ", k(TEAL, on), k(RST, on));
let _ = io::stdout().flush();
};
redraw(&cfg, "type `help` for commands");
for line in stdin.lock().lines() {
let line = match line {
Ok(l) => l,
Err(_) => break,
};
let mut it = line.split_whitespace();
let cmd = it.next().unwrap_or("");
let arg = it.next().unwrap_or("");
let mut msg = String::new();
match cmd {
"" => {}
"quit" | "q" | "exit" => break,
"help" | "h" => {
if interactive {
print!("\x1b[2J\x1b[H");
}
help();
continue;
}
"on" => cfg.shield.enabled = true,
"off" => cfg.shield.enabled = false,
"passes" => match arg.parse::<usize>() {
Ok(v) => cfg.shield.givens_passes = v.clamp(1, 512),
Err(_) => msg = "passes: need a number".into(),
},
"bits" => match arg.parse::<u32>() {
Ok(v) => cfg.shield.feedback_bits = v.clamp(1, 12),
Err(_) => msg = "bits: need 1..12".into(),
},
"n" => match arg.parse::<usize>() {
Ok(v) => cfg.scene.identities = v.clamp(2, 128),
Err(_) => msg = "n: need 2..128".into(),
},
"snr" => match arg.parse::<f64>() {
Ok(v) => cfg.link.snr_db = v.clamp(0.0, 60.0),
Err(_) => msg = "snr: need a number (dB)".into(),
},
"metric" => match arg {
"euclid" | "euclidean" => cfg.attacker_metric = Metric::Euclidean,
"cosine" | "cos" => cfg.attacker_metric = Metric::Cosine,
_ => msg = "metric: euclid | cosine".into(),
},
"preset" => {
if !preset(arg, &mut cfg) {
msg = "preset: scif | board | ward | hotel".into();
}
}
"run" => msg = "ran — numbers above reflect current settings".into(),
"optimize" | "opt" => {
cfg.shield = veil::hyper_optimize(&cfg).shield;
msg = format!(
"optimized → passes {} · bits {}",
cfg.shield.givens_passes, cfg.shield.feedback_bits
);
}
"proof" => {
let w = Proof::witness(&Proof::run_reference());
msg = format!(
"witness {:#018x} ({})",
w,
if w == Proof::EXPECTED_WITNESS {
"match"
} else {
"drift"
}
);
}
other => msg = format!("unknown: {other} (try `help`)"),
}
redraw(&cfg, &msg);
}
if interactive {
println!();
}
}
fn main() {
let on = color_enabled();
let args: Vec<String> = std::env::args().skip(1).collect();
let cfg = ExperimentConfig::default();
let code = match args.first().map(String::as_str).unwrap_or("") {
"" => {
if io::stdout().is_terminal() {
tui(cfg, on);
} else {
print_dashboard(&cfg, on);
}
0
}
"tui" => {
tui(cfg, on);
0
}
"report" => {
print_dashboard(&cfg, on);
0
}
"sweep" => {
cmd_sweep(&cfg, on);
0
}
"optimize" | "opt" => {
cmd_optimize(&cfg, on);
0
}
"adaptive" => {
let n = args
.get(1)
.and_then(|s| s.parse().ok())
.unwrap_or(cfg.scene.identities);
cmd_adaptive(&cfg, n, on);
0
}
"proof" => cmd_proof(on),
"doctor" => cmd_doctor(on),
"help" | "-h" | "--help" => {
help();
0
}
other => {
eprintln!("unknown command: {other}. Try `veil help`.");
2
}
};
std::process::exit(code);
}