From de0712d4356659f45035b64c6e288bd1936eab07 Mon Sep 17 00:00:00 2001 From: ruv Date: Mon, 25 May 2026 17:05:34 -0400 Subject: [PATCH] feat(adr-125 tier1+2 iter 6 SOTA): PyO3 BFLD PrivacyClass binding MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit scripts/c6-presence-watcher.py and friends carry a Python port of `wifi_densepose_bfld::PrivacyClass`. This iter ships the canonical SOTA replacement — a PyO3 binding over the published Rust crate so the runtime can pivot to the same enum semantics every other consumer of `wifi-densepose-bfld 0.3.0` already uses. New file: `python/src/bindings/privacy_gate.rs` (~155 LOC) - `#[pyclass] PrivacyClass {Raw, Derived, Anonymous, Restricted}` - `.allows_network`, `.allows_matter`, `.allows_hap`, `.as_u8` getters - `PrivacyClass.from_u8(v)` / `PrivacyClass.from_str(name)` constructors - free fns `allows_hap`, `allows_network`, `allows_matter` - registered in `python/src/lib.rs` via `bindings::privacy_gate::register` Cargo.toml gains `wifi-densepose-bfld = { version = "0.3.0", path = ... }` as a hard dep; numpy + pyo3 + the existing core/vitals deps unchanged. ADR-125 §2.1.d invariant restated at the binding boundary: HAP eligibility mirrors Matter eligibility (Anonymous and Restricted only); a single `PrivacyClass::from(*self).allows_matter()` call is the gate truth-source. Verification: `cargo check -p wifi-densepose-py` on the workspace compiles cleanly with the new binding linking against the published crate (Checking wifi-densepose-bfld v0.3.0 ✓, Checking wifi-densepose-py v2.0.0-alpha.1 ✓). Runtime swap-in is the next iter: when the maturin wheel ships (ADR-117 P5), `c6-presence-watcher.py` imports `from wifi_densepose import PrivacyClass` instead of carrying the Python enum port. Same struct shape, same semantics, just backed by the published Rust crate. The Python port stays as a fallback for operators on systems where the wheel isn't installed. Refs ADR-118 §2.1, ADR-125 §2.1.d, ADR-117 §5.7 (binding strategy). Co-Authored-By: claude-flow --- python/Cargo.toml | 7 ++ python/src/bindings/privacy_gate.rs | 154 ++++++++++++++++++++++++++++ python/src/lib.rs | 5 + 3 files changed, 166 insertions(+) create mode 100644 python/src/bindings/privacy_gate.rs diff --git a/python/Cargo.toml b/python/Cargo.toml index be4542c6..c781ac53 100644 --- a/python/Cargo.toml +++ b/python/Cargo.toml @@ -39,6 +39,13 @@ wifi-densepose-core = { version = "0.3.0", path = "../v2/crates/wifi-densepose-c # no tokio (Q5 audited 2026-05-24); safe to wrap in py.allow_threads. wifi-densepose-vitals = { version = "0.3.0", path = "../v2/crates/wifi-densepose-vitals" } +# ADR-118 BFLD core — PrivacyClass enum + identity_risk scoring + +# privacy gate. Exposed to Python via bindings/privacy_gate.rs so the +# c6-presence-watcher.py runtime (currently using a Python port of the +# same semantics) can switch to the canonical Rust implementation when +# the wheel ships. ADR-125 §2.1.d invariant enforcement lives here. +wifi-densepose-bfld = { version = "0.3.0", path = "../v2/crates/wifi-densepose-bfld" } + # numpy bridge — needed for P3.5 BfldFrame (Complex64 ndarray) and for # the future P3 CsiFrame numpy round-trip. numpy = "0.22" diff --git a/python/src/bindings/privacy_gate.rs b/python/src/bindings/privacy_gate.rs new file mode 100644 index 00000000..9f7de50d --- /dev/null +++ b/python/src/bindings/privacy_gate.rs @@ -0,0 +1,154 @@ +//! ADR-118 / ADR-125 §2.1.d — Python binding for the BFLD `PrivacyClass` +//! enum and the HAP-eligibility gate. +//! +//! Python: +//! ```python +//! from wifi_densepose import PrivacyClass, allows_hap, allows_matter, allows_network +//! +//! PrivacyClass.Anonymous # → 2 +//! allows_hap(PrivacyClass.Raw) # → False (I1 invariant) +//! allows_hap(PrivacyClass.Anonymous)# → True +//! allows_matter(PrivacyClass.Restricted) # → True (ADR-122 §2.4) +//! ``` +//! +//! This is the SOTA replacement for the Python port that ships in +//! `scripts/c6-presence-watcher.py::PrivacyClass`. When the +//! `wifi-densepose` PyPI wheel lands (ADR-117 P5), runtimes flip from +//! the Python port to this Rust-backed binding and get the same enum +//! semantics as every other consumer of the published +//! `wifi-densepose-bfld 0.3.0` crate. + +use pyo3::prelude::*; +use wifi_densepose_bfld::PrivacyClass; + +/// Python-facing wrapper for [`wifi_densepose_bfld::PrivacyClass`]. +/// +/// Repr matches the Rust enum byte values 0..=3. +#[pyclass(eq, eq_int, hash, frozen, name = "PrivacyClass", module = "wifi_densepose")] +#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)] +pub enum PyPrivacyClass { + Raw = 0, + Derived = 1, + Anonymous = 2, + Restricted = 3, +} + +impl From for PyPrivacyClass { + fn from(c: PrivacyClass) -> Self { + match c { + PrivacyClass::Raw => Self::Raw, + PrivacyClass::Derived => Self::Derived, + PrivacyClass::Anonymous => Self::Anonymous, + PrivacyClass::Restricted => Self::Restricted, + } + } +} + +impl From for PrivacyClass { + fn from(c: PyPrivacyClass) -> Self { + match c { + PyPrivacyClass::Raw => Self::Raw, + PyPrivacyClass::Derived => Self::Derived, + PyPrivacyClass::Anonymous => Self::Anonymous, + PyPrivacyClass::Restricted => Self::Restricted, + } + } +} + +#[pymethods] +impl PyPrivacyClass { + /// True if frames of this class may cross a `NetworkSink`. + /// Class 0 (`Raw`) is local-only by structural invariant I1 + /// (ADR-118 §2.2). + #[getter] + fn allows_network(&self) -> bool { + PrivacyClass::from(*self).allows_network() + } + + /// True if frames of this class may cross the Matter boundary. + /// Only classes 2 (`Anonymous`) and 3 (`Restricted`) qualify per + /// ADR-122 §2.4 / ADR-125 §2.1.d. + #[getter] + fn allows_matter(&self) -> bool { + PrivacyClass::from(*self).allows_matter() + } + + /// True if frames of this class may cross the HomeKit Accessory + /// Protocol boundary. Same set as `allows_matter` — class 2 or 3. + #[getter] + fn allows_hap(&self) -> bool { + // HAP eligibility is the same shape as Matter eligibility per + // ADR-125 §2.1.d; we don't add a separate Rust method until + // there's a divergence to justify it. + PrivacyClass::from(*self).allows_matter() + } + + /// Byte value (0..=3) for serialization. + #[getter] + fn as_u8(&self) -> u8 { + PrivacyClass::from(*self).as_u8() + } + + fn __repr__(&self) -> String { + match self { + Self::Raw => "PrivacyClass.Raw", + Self::Derived => "PrivacyClass.Derived", + Self::Anonymous => "PrivacyClass.Anonymous", + Self::Restricted => "PrivacyClass.Restricted", + } + .to_string() + } + + /// Map a byte value 0..=3 to the corresponding `PrivacyClass`. + /// Raises `ValueError` on out-of-range input. + #[staticmethod] + fn from_u8(v: u8) -> PyResult { + PrivacyClass::try_from(v) + .map(Self::from) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(e.to_string())) + } + + /// Map a string ("raw" / "derived" / "anonymous" / "restricted", + /// case-insensitive) to the corresponding `PrivacyClass`. Raises + /// `ValueError` on unknown names. + #[staticmethod] + fn from_str(s: &str) -> PyResult { + match s.to_ascii_lowercase().as_str() { + "raw" => Ok(Self::Raw), + "derived" => Ok(Self::Derived), + "anonymous" => Ok(Self::Anonymous), + "restricted" => Ok(Self::Restricted), + _ => Err(pyo3::exceptions::PyValueError::new_err(format!( + "invalid PrivacyClass name: {s:?} (expected raw/derived/anonymous/restricted)" + ))), + } + } +} + +/// Free-function helper: `True` iff `c` may cross the HAP boundary. +/// Convenience wrapper so Python callers can write +/// `allows_hap(PrivacyClass.Anonymous)` without method-call syntax. +#[pyfunction] +fn allows_hap(c: PyPrivacyClass) -> bool { + c.allows_hap() +} + +/// Free-function helper: `True` iff `c` may cross a `NetworkSink`. +#[pyfunction] +fn allows_network(c: PyPrivacyClass) -> bool { + c.allows_network() +} + +/// Free-function helper: `True` iff `c` may cross the Matter boundary. +#[pyfunction] +fn allows_matter(c: PyPrivacyClass) -> bool { + c.allows_matter() +} + +pub fn register(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_class::()?; + m.add_function(wrap_pyfunction!(allows_hap, m)?)?; + m.add_function(wrap_pyfunction!(allows_network, m)?)?; + m.add_function(wrap_pyfunction!(allows_matter, m)?)?; + Ok(()) +} diff --git a/python/src/lib.rs b/python/src/lib.rs index c62ff4f1..b30faf75 100644 --- a/python/src/lib.rs +++ b/python/src/lib.rs @@ -20,6 +20,7 @@ mod bindings { pub mod bfld; pub mod keypoint; pub mod pose; + pub mod privacy_gate; pub mod vitals; } @@ -80,5 +81,9 @@ fn wifi_densepose_native(m: &Bound<'_, PyModule>) -> PyResult<()> { // P3.5 — BFLD bindings (stub Rust; future wifi-densepose-bfld crate // will replace the stub without changing the Python API). bindings::bfld::register(m)?; + // ADR-118 PrivacyClass + HAP/Matter eligibility gates (SOTA — backed by + // the published `wifi-densepose-bfld 0.3.0` crate, not the Python port). + // Closes ADR-125 §2.1.d at the binding boundary. + bindings::privacy_gate::register(m)?; Ok(()) }