Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions v2/crates/wifi-densepose-rufield/src/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,10 @@ fn event_id(snap: &SensingSnapshot) -> String {
/// 3. Stamps the §3.3 egress privacy class (information-content mapping with
/// the demotion floor) on both tensor and observation.
/// 4. Builds a real `ProvenanceRef` (sha256 raw hash over the tensor/feature
/// bytes, `synthetic = false`) and **signs** it with the supplied ed25519
/// [`Signer`] so `rufield_provenance::is_fusable` passes.
/// bytes, `synthetic` stamped from `snap.synthetic` — the caller's, not
/// this bridge's, judgment of where the cycle came from) and **signs** it
/// with the supplied ed25519 [`Signer`] so `rufield_provenance::is_fusable`
/// passes regardless of which way `synthetic` reads.
///
/// Determinism: with no RNG anywhere and a deterministic ed25519 signer, the
/// same `snap` + same signer seed yields a byte-identical event.
Expand Down Expand Up @@ -171,14 +173,14 @@ pub fn snapshot_to_field_event(snap: &SensingSnapshot, signer: &Signer) -> Field
firmware_hash: firmware_hash(),
model_id: MODEL_ID.to_string(),
calibration_id,
synthetic: false, // a real (non-synthetic) live/replay event
synthetic: snap.synthetic,
signature_hex: None,
signer_pubkey_hex: None,
};

let sensor = SensorDescriptor {
modality: "wifi_csi".to_string(),
vendor: "esp32".to_string(),
vendor: if snap.synthetic { "simulated" } else { "esp32" }.to_string(),
device_id: snap.node_id.clone(),
placement: "unknown".to_string(),
// Optional sensor pose, added upstream. Left unset on purpose: a CSI
Expand Down
1 change: 1 addition & 0 deletions v2/crates/wifi-densepose-rufield/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
//! demoted: false,
//! identity_bound: false,
//! node_id: "esp32_room_01".into(),
//! synthetic: false,
//! };
//!
//! let signer = Signer::from_seed(b"adr-262-bridge-seed-32-bytes-ok!");
Expand Down
7 changes: 7 additions & 0 deletions v2/crates/wifi-densepose-rufield/src/snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -149,4 +149,11 @@ pub struct SensingSnapshot {
pub identity_bound: bool,
/// Stable node id (e.g. `"esp32_room_01"`).
pub node_id: String,
/// Whether this cycle came from real hardware/replay or a synthetic demo
/// source (RuView's `--source simulated` / Docker demo mode). Stamped
/// verbatim onto the emitted event's `ProvenanceRef.synthetic` — this
/// bridge does not get to assume "real" by default; the caller who knows
/// where the cycle actually came from must say so.
#[serde(default)]
pub synthetic: bool,
}
25 changes: 25 additions & 0 deletions v2/crates/wifi-densepose-rufield/tests/p1_gates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ fn sample_snapshot() -> SensingSnapshot {
demoted: false,
identity_bound: false,
node_id: "esp32_room_01".into(),
synthetic: false,
}
}

Expand All @@ -68,6 +69,30 @@ fn gate_is_fusable_verified_receipt() {
assert!(is_fusable(&ev), "verified receipt ⇒ fusable (§11 invariant)");
}

/// Regression guard: `synthetic` must be a straight pass-through of the
/// caller's `SensingSnapshot.synthetic`, never hardcoded. Before this fix the
/// bridge always stamped `synthetic: false`, so a simulated/demo-source
/// snapshot produced an event indistinguishable from real hardware — a
/// dishonest claim this crate's own docs (§0 / §6) exist to prevent.
#[test]
fn gate_synthetic_flag_passes_through_honestly() {
let mut snap = sample_snapshot();

snap.synthetic = true;
let ev = snapshot_to_field_event(&snap, &signer());
assert!(
ev.provenance.synthetic,
"a snapshot marked synthetic must produce a synthetic-marked event"
);

snap.synthetic = false;
let ev = snapshot_to_field_event(&snap, &signer());
assert!(
!ev.provenance.synthetic,
"a snapshot marked non-synthetic must produce a non-synthetic-marked event"
);
}

#[test]
fn gate_fusion_ingest_accepts_and_infers() {
let ev = snapshot_to_field_event(&sample_snapshot(), &signer());
Expand Down
50 changes: 46 additions & 4 deletions v2/crates/wifi-densepose-sensing-server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4694,7 +4694,7 @@ fn derive_single_person_pose(
/// / AETHER concern, ADR-262 §8 Q4). This is conservative for egress — it only
/// ever *lowers* a Derived cycle from P5 to P4, both of which are already held
/// edge-local, so it cannot leak.
fn emit_rufield_event(s: &AppStateInner, update: &SensingUpdate, node_id: u8) {
fn emit_rufield_event(s: &AppStateInner, update: &SensingUpdate, node_id: u8, synthetic: bool) {
// No-presence ⇒ no phantom event.
if !update.classification.presence {
return;
Expand All @@ -4713,9 +4713,10 @@ fn emit_rufield_event(s: &AppStateInner, update: &SensingUpdate, node_id: u8) {
.unwrap_or(0)
};

let node_prefix = if synthetic { "simulated_node" } else { "esp32_node" };
let snap = rufield_surface::build_snapshot(
timestamp_ns,
format!("esp32_node_{node_id}"),
format!("{node_prefix}_{node_id}"),
rufield_surface::SensingFeatures {
mean_rssi: update.features.mean_rssi,
variance: update.features.variance,
Expand All @@ -4737,6 +4738,7 @@ fn emit_rufield_event(s: &AppStateInner, update: &SensingUpdate, node_id: u8) {
rufield_surface::ruview_class_from_bfld(effective_class),
s.engine_bridge.demoted(),
false, // identity_bound — see fn-doc (conservative, cannot leak).
synthetic,
);

// `field_surface` is its own Arc<RwLock<_>>; `try_write` is non-blocking and
Expand Down Expand Up @@ -7044,7 +7046,7 @@ async fn udp_receiver_task(
// whose mapped privacy class clears the §10 network egress
// gate are surfaced (P1/P2); a `Derived → P4/P5` cycle is
// held edge-local. `presence == false` ⇒ no phantom event.
emit_rufield_event(&s, &update, node_id);
emit_rufield_event(&s, &update, node_id, false);

observe_sensing_update(s.latest_update.as_ref(), &update);
s.latest_update = Some(update);
Expand Down Expand Up @@ -7148,6 +7150,10 @@ fn observe_sensing_update(prev: Option<&SensingUpdate>, update: &SensingUpdate)

// ── Simulated data task ──────────────────────────────────────────────────────

/// Node id the simulated demo source publishes under (`NodeInfo.node_id` and
/// the mirrored `node_states` entry both use this — see `simulated_data_task`).
const SIMULATED_NODE_ID: u8 = 1;

async fn simulated_data_task(state: SharedState, tick_ms: u64) {
let mut interval = tokio::time::interval(Duration::from_millis(tick_ms));
info!("Simulated data source active (tick={}ms)", tick_ms);
Expand Down Expand Up @@ -7179,6 +7185,27 @@ async fn simulated_data_task(state: SharedState, tick_ms: u64) {
s.frame_history.pop_front();
}

// Mirror this frame into `node_states` under a synthetic node id
// (matching the `NodeInfo.node_id` this task publishes below). Without
// this, `node_states` stays empty forever under `--source simulated`:
// it is otherwise populated only by the real ESP32 UDP ingestion path,
// so the governed trust engine (`engine_bridge.observe_cycle`, driven
// by `node_states`) never produces an `effective_class`, and
// `emit_rufield_event` — gated on exactly that — silently never fires.
// `/api/field` / `/ws/field` would report zero events forever under
// the project's own documented "no hardware, Docker demo" quick start.
{
let ns = s
.node_states
.entry(SIMULATED_NODE_ID)
.or_insert_with(NodeState::new);
ns.frame_history.push_back(frame.amplitudes.clone());
if ns.frame_history.len() > FRAME_HISTORY_CAPACITY {
ns.frame_history.pop_front();
}
ns.last_frame_time = Some(std::time::Instant::now());
}

let sample_rate_hz = 1000.0 / tick_ms as f64;
let (features, mut classification, breathing_rate_hz, sub_variances, raw_motion) =
extract_features_from_frame(&frame, &s.frame_history, sample_rate_hz);
Expand Down Expand Up @@ -7230,7 +7257,7 @@ async fn simulated_data_task(state: SharedState, tick_ms: u64) {
source: "simulated".to_string(),
tick,
nodes: vec![NodeInfo {
node_id: 1,
node_id: SIMULATED_NODE_ID,
rssi_dbm: features.mean_rssi,
position: [2.0, 0.0, 1.5],
amplitude: frame_amplitudes,
Expand Down Expand Up @@ -7295,6 +7322,21 @@ async fn simulated_data_task(state: SharedState, tick_ms: u64) {
if update.classification.presence {
s.total_detections += 1;
}

// Governed trust cycle + signed RuField emission (ADR-135..146 /
// ADR-262 P3), mirroring the real ESP32 UDP path so the simulated
// demo source can honestly exercise the same governance + `/api/field`
// surface instead of always reporting zero events.
{
let sref: &mut AppStateInner = &mut s;
let now_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as i64)
.unwrap_or(0);
sref.engine_bridge.observe_cycle(&sref.node_states, now_ms);
}
emit_rufield_event(&s, &update, SIMULATED_NODE_ID, true);

if let Ok(json) = serde_json::to_string(&update) {
let _ = s.tx.send(json);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@ pub fn build_snapshot(
trust_class: RuViewPrivacyClass,
demoted: bool,
identity_bound: bool,
synthetic: bool,
) -> SensingSnapshot {
SensingSnapshot {
timestamp_ns,
Expand All @@ -249,6 +250,7 @@ pub fn build_snapshot(
demoted,
identity_bound,
node_id,
synthetic,
}
}

Expand Down Expand Up @@ -387,6 +389,7 @@ mod tests {
RuViewPrivacyClass::Anonymous, // → P2, network-allowed
false,
false,
false,
);
let ev = surface.emit(&snap).expect("anonymous P2 cycle is surfaced");
assert_eq!(ev.observation.privacy_class, PrivacyClass::P2);
Expand All @@ -409,6 +412,7 @@ mod tests {
RuViewPrivacyClass::Derived,
false,
identity_bound,
false,
);
assert!(
surface.emit(&snap).is_none(),
Expand All @@ -431,6 +435,7 @@ mod tests {
RuViewPrivacyClass::Anonymous,
false,
false,
false,
);
surface.emit(&snap);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ async fn inject(state: &FieldState, trust: RuViewPrivacyClass, presence: bool, i
trust,
false, // demoted
identity_bound,
false, // synthetic
);
state.write().await.emit(&snap);
}
Expand Down