From cb67be117aa8741e7927829294d63156052c6369 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Aug 2026 15:26:07 +0000 Subject: [PATCH] =?UTF-8?q?feat(privshield):=20add=20`veil`=20custom=20ter?= =?UTF-8?q?minal=20harness=20+=20TUI=20(ADR-288=20=C2=A7harness)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A dependency-free native binary (`src/bin/veil.rs`) — the in-repo counterpart to the npm metaharness — that drives the same crate API the tests use: - Interactive ANSI dashboard (TUI): live status, re-ID off/on vs chance, throughput, compliance (energy 1.000× / not jamming), a block-sparkline collapse curve, config, and PASS/OUT-OF-SPEC verdict. Command-driven redraw loop (std-only, no crossterm/ratatui): on/off, passes/bits/n/snr, metric euclid|cosine, preset scif|board|ward|hotel, optimize, proof. - Scriptable subcommands: report, sweep, optimize, adaptive , proof, doctor (exit 0 = healthy). Auto-picks TUI on a terminal, one-shot report when piped; honors NO_COLOR. Std-only, so it builds with no extra deps and runs in any pipe/CI. The wasm leaf story is unchanged (validated with `--lib`; the bin is native-only). All readouts are SYNTHETIC/L0 and never relabeled. Validated: 38 tests + doctest pass, clippy --all-targets -D warnings clean, rustfmt clean, wasm --lib builds; all subcommands + a scripted TUI session exercised. Documented in the crate README and ADR-288. Co-Authored-By: claude-flow Claude-Session: https://claude.ai/code/session_01WEXNqzs7UsfNFBcP5yW21p --- ...-veil-privacy-shield-compliant-waveform.md | 11 + .../wifi-densepose-privshield/Cargo.toml | 7 + v2/crates/wifi-densepose-privshield/README.md | 34 ++ .../wifi-densepose-privshield/src/bin/veil.rs | 542 ++++++++++++++++++ 4 files changed, 594 insertions(+) create mode 100644 v2/crates/wifi-densepose-privshield/src/bin/veil.rs diff --git a/docs/adr/ADR-288-veil-privacy-shield-compliant-waveform.md b/docs/adr/ADR-288-veil-privacy-shield-compliant-waveform.md index a2d09dad..d89b1c67 100644 --- a/docs/adr/ADR-288-veil-privacy-shield-compliant-waveform.md +++ b/docs/adr/ADR-288-veil-privacy-shield-compliant-waveform.md @@ -139,6 +139,17 @@ better on **both** privacy (re-ID 0.047 vs 0.078) and throughput (0.976 vs 0.974 and is now verified rather than assumed. See `docs/research/privacy-shield/08-optimization.md`. +### harness. Native terminal harness + TUI (`src/bin/veil.rs`) + +A custom, dependency-free binary (`veil`) ships with the crate — the in-repo, +native counterpart to the npm metaharness (ADR-289). It drives the same public +API the tests use, as an interactive ANSI dashboard plus scriptable subcommands +(`report`, `sweep`, `optimize`, `adaptive `, `proof`, `doctor`, `tui`). +Std-only (no `crossterm`/`ratatui`): the TUI is a command-driven redraw loop, so +it runs in any terminal, pipe, or CI and keeps the crate a pure leaf. It reports +only SYNTHETIC/L0 numbers and never relabels them. The wasm leaf story is +unchanged (validated with `--lib`; the bin is native-only). + ## 3. What this explicitly is NOT - **Not a radio driver.** No RF frontend, no transmit path, no diff --git a/v2/crates/wifi-densepose-privshield/Cargo.toml b/v2/crates/wifi-densepose-privshield/Cargo.toml index f1cf1491..ef854fa5 100644 --- a/v2/crates/wifi-densepose-privshield/Cargo.toml +++ b/v2/crates/wifi-densepose-privshield/Cargo.toml @@ -23,3 +23,10 @@ readme = "README.md" [lib] name = "wifi_densepose_privshield" path = "src/lib.rs" + +# `veil` — the custom, dependency-free terminal harness + TUI (ADR-288 §harness). +# Native counterpart to the npm metaharness. Std-only; builds without any extra +# deps. Excluded from the wasm leaf story (that stays `--lib`). +[[bin]] +name = "veil" +path = "src/bin/veil.rs" diff --git a/v2/crates/wifi-densepose-privshield/README.md b/v2/crates/wifi-densepose-privshield/README.md index ab0f1b62..6aba6db4 100644 --- a/v2/crates/wifi-densepose-privshield/README.md +++ b/v2/crates/wifi-densepose-privshield/README.md @@ -66,6 +66,40 @@ anything. cargo test -p wifi-densepose-privshield --no-default-features ``` +## `veil` — terminal harness & TUI + +A custom, **dependency-free** native harness ships with the crate (the in-repo +counterpart to the npm metaharness). It drives the same model the tests use — an +interactive ANSI dashboard plus scriptable subcommands, std-only (no +`crossterm`/`ratatui`), so it runs in any terminal, pipe, or CI. + +```bash +cargo run -p wifi-densepose-privshield --bin veil # interactive TUI (or a one-shot report when piped) +cargo run -p wifi-densepose-privshield --bin veil -- sweep # re-ID vs passes + throughput vs bits +cargo run -p wifi-densepose-privshield --bin veil -- optimize +cargo run -p wifi-densepose-privshield --bin veil -- doctor # self-check, exit 0 = healthy +``` + +```text +┌────────────────────────────────────────────────────────── +│ VEIL · wifi-sensing privacy shield ● PROTECTED +│ +│ re-ID off 100.0% re-ID on 4.7% (chance 6.25%) +│ throughput 97.6% emission 1.000× · not jamming +│ +│ collapse ████▇▆▅▂▂▂▁▂ passes 2→112 · op 96 +│ +│ config passes 96 · bits 5 · N 16 · snr 20dB · euclid +│ verdict ✓ PASS — re-ID at chance · throughput ≥95% · compliant +└────────────────────────────────────────────────────────── +``` + +In the TUI, type commands to steer the shield live: `on`/`off`, `passes `, +`bits `, `n `, `snr `, `metric euclid|cosine`, +`preset scif|board|ward|hotel`, `optimize`, `proof`, `quit`. All readouts are +**SYNTHETIC / L0**. (A graphical VEIL Console web dashboard mirrors this same +instrument; `veil` is the terminal-native version.) + ## Modules | Module | Purpose | diff --git a/v2/crates/wifi-densepose-privshield/src/bin/veil.rs b/v2/crates/wifi-densepose-privshield/src/bin/veil.rs new file mode 100644 index 00000000..b62b2f59 --- /dev/null +++ b/v2/crates/wifi-densepose-privshield/src/bin/veil.rs @@ -0,0 +1,542 @@ +//! `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/wifi-densepose-privshield/`, 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 # 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_densepose_privshield 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 only for a real terminal, and never when `NO_COLOR` is set. +fn color_enabled() -> bool { + io::stdout().is_terminal() && std::env::var_os("NO_COLOR").is_none() +} + +/// 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 { + 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 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 · bits · n · snr \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::() { + Ok(v) => cfg.shield.givens_passes = v.clamp(1, 512), + Err(_) => msg = "passes: need a number".into(), + }, + "bits" => match arg.parse::() { + Ok(v) => cfg.shield.feedback_bits = v.clamp(1, 12), + Err(_) => msg = "bits: need 1..12".into(), + }, + "n" => match arg.parse::() { + Ok(v) => cfg.scene.identities = v.clamp(2, 128), + Err(_) => msg = "n: need 2..128".into(), + }, + "snr" => match arg.parse::() { + 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 = 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); +}