Skip to content
Closed
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
35 changes: 35 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Line-ending policy.
#
# `* text=auto` lets git normalise text files to LF in the repository and convert
# to the platform's native line endings on checkout. That default is fine for
# .md / .rs / .toml / .py — broken for shell scripts and Dockerfiles, where
# CRLF on the shebang line causes Linux exec to look for an interpreter named
# `/bin/sh\r` (or similar) and fail with "no such file or directory".
#
# Force LF for anything that ends up executed inside a Linux container or a
# POSIX shell. This is what prevented the v0.8.0 image from booting at first
# build until the entrypoint was renormalised.
* text=auto
*.sh text eol=lf
*.bash text eol=lf
verify text eol=lf
Dockerfile* text eol=lf
docker/* text eol=lf
scripts/* text eol=lf

# Binary blobs that should never be touched by text-normalisation.
*.bin binary
*.png binary
*.jpg binary
*.jpeg binary
*.gif binary
*.ico binary
*.zip binary
*.tar binary
*.tgz binary
*.gz binary
*.wasm binary
*.rvf binary
*.task binary
*.csi.jsonl binary
*.pcap binary
12 changes: 10 additions & 2 deletions .github/workflows/verify-pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,15 +57,23 @@
"

- name: Run pipeline verification
working-directory: v1
working-directory: archive/v1
env:
# verify.py transitively imports src.app -> src.config.settings, which
# uses pydantic-settings with a required `secret_key` field. The proof
# only needs the import chain to resolve; the value is never used for
# any auth path in the proof pipeline.
SECRET_KEY: ci-proof-replay-only-not-a-real-secret

Check failure

Code scanning / KICS

Passwords And Secrets - Generic Secret Error

Hardcoded secret key appears in source

Check warning on line 66 in .github/workflows/verify-pipeline.yml

View workflow job for this annotation

GitHub Actions / Infrastructure Security Scan

[HIGH] Passwords And Secrets - Generic Secret

Query to find passwords and secrets in infrastructure code.
run: |
echo "=== Running pipeline verification ==="
python data/proof/verify.py
echo ""
echo "Pipeline verification PASSED."

- name: Run verification twice to confirm determinism
working-directory: v1
working-directory: archive/v1
env:
SECRET_KEY: ci-proof-replay-only-not-a-real-secret

Check failure

Code scanning / KICS

Passwords And Secrets - Generic Secret Error

Hardcoded secret key appears in source

Check warning on line 76 in .github/workflows/verify-pipeline.yml

View workflow job for this annotation

GitHub Actions / Infrastructure Security Scan

[HIGH] Passwords And Secrets - Generic Secret

Query to find passwords and secrets in infrastructure code.
run: |
echo "=== Second run for determinism confirmation ==="
python data/proof/verify.py
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
regime classification) and `temporal-compare` (DTW pattern matching) as a
**parallel tap** alongside RuView's existing event pipeline — no replacement,
no behaviour change to the existing `/ws/sensing` fan-out or `wifi-densepose-signal`
DSP. Two new endpoints (off by default, enabled via `--introspection`):
DSP. Two new endpoints (always mounted — the tap is cheap enough at 0.041 ms p99
per-frame `update()` to ship hot by default):
- `GET /ws/introspection` — newline-delimited JSON snapshots streamed at the CSI
frame rate. Each snapshot carries `frame_count`, `regime` (Idle / Periodic /
Transient / Chaotic / Unknown), `lyapunov_exponent`, `attractor_dim`,
Expand Down
52 changes: 52 additions & 0 deletions archive/v1/data/proof/REFERENCE_PLATFORMS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# Reference platforms for `expected_features.sha256`

The hash in `expected_features.sha256` was generated on a specific BLAS / FFT
backend. Numpy + scipy delegate FFT/linear-algebra to platform-native
libraries, and those libraries produce **bit-different output on identical
IEEE 754 inputs** depending on the backend. This is not a bug in the proof
pipeline — it is a property of the underlying numerical libraries. (See
issue #560.)

## Platforms where the hash is expected to MATCH

| Platform | BLAS backend | Status |
|---|---|---|
| `linux-x86_64-gnu` (Python 3.11.x, numpy 1.26.4 from PyPI wheels, scipy 1.14.1) | OpenBLAS | ✅ Reference |
| `windows-x86_64-msvc` (Python 3.11.x / 3.13.x, numpy 1.26.4 from PyPI wheels, scipy 1.14.1) | OpenBLAS | ✅ Reference |

## Platforms where the hash is **expected to MISMATCH**

| Platform | BLAS backend | Why |
|---|---|---|
| `darwin-arm64` (macOS arm64, Apple Silicon) | Accelerate.framework | FFT + matrix kernels differ in last-bit positions; the SHA-256 will differ even with pinned `numpy 1.26.4` / `scipy 1.14.1`. |
| Any environment with MKL installed | Intel MKL | Same root cause as Accelerate: different vectorized FFT path. |

## What to do if you get MISMATCH on a non-reference platform

The pipeline is still correct on your platform — the *output* is bit-different
because the *backend* is bit-different, not because the proof code has a bug.
Three workable responses:

1. **Run the proof on a reference platform** (Linux x86_64 or Windows x86_64
with the PyPI OpenBLAS wheels). This is what CI does.

2. **Generate a new local-reference hash** for your platform and check it
against the same hash on a teammate's machine with the *same* backend:

```bash
# Regenerate from your platform
python archive/v1/data/proof/verify.py --generate-hash

# Commit the new hash to a side file (do NOT overwrite expected_features.sha256
# unless you are publishing a new cross-platform reference)
```

3. **Compare numerical output, not the hash.** A relaxed-tolerance comparison
on the feature vectors (e.g. `np.allclose(features, reference, atol=1e-10)`)
will pass across backends. This is on the roadmap (see issue #560).

## The `verify.py` runtime environment block

Every run of `verify.py` now prints a `RUNTIME ENVIRONMENT` block before the
pipeline runs. Include that block in any issue report — it identifies the
platform + numpy version + BLAS backend in one place.
63 changes: 58 additions & 5 deletions archive/v1/data/proof/verify.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,48 @@ def print_source_provenance():
print()


def print_runtime_environment():
"""Print the platform + numpy/scipy BLAS backend.

The proof pipeline's SHA-256 is sensitive to the BLAS / FFT backend
behind numpy + scipy.fft. Different platforms ship different backends
(OpenBLAS on Linux/Windows wheels, Accelerate.framework on macOS arm64,
MKL when installed) and they produce bit-different output on identical
IEEE 754 inputs. Surfacing the backend up front turns an unexplained
MISMATCH into a one-line diagnosis -- see issue #560.
"""
import platform
print(" RUNTIME ENVIRONMENT:")
print(f" Platform : {platform.platform()}")
print(f" Machine : {platform.machine()}")
print(f" Python : {platform.python_version()} ({platform.python_implementation()})")

# numpy BLAS / LAPACK backend.
try:
blas_info = np.__config__.blas_ilp64_opt_info # type: ignore[attr-defined]
backend = getattr(blas_info, "get", lambda *_: None)("libraries", None) or "unknown"
except Exception:
# Newer numpy (>= 1.26) reports via show_config(); fall back to a stringified dump.
try:
import io
buf = io.StringIO()
np.show_config(mode="dicts") if hasattr(np, "show_config") else None
# `show_config(mode='dicts')` returns a dict in numpy >= 1.26.
cfg = np.show_config(mode="dicts") if hasattr(np, "show_config") else {}
if isinstance(cfg, dict):
blas = cfg.get("Build Dependencies", {}).get("blas", {})
backend = blas.get("name", "unknown")
else:
backend = "unknown"
except Exception:
backend = "unknown"
print(f" numpy BLAS : {backend}")
print(" (FFT/BLAS backend affects the hash -- see #560 if MISMATCH on")
print(" macOS arm64 / Accelerate. Reference platforms: linux-x86_64,")
print(" windows-x86_64 with OpenBLAS; see expected_features.sha256.)")
print()


def load_reference_signal(data_path):
"""Load the reference CSI signal from JSON.

Expand Down Expand Up @@ -417,6 +459,7 @@ def main():
# ---------------------------------------------------------------
print("[0/4] SOURCE PROVENANCE")
print_source_provenance()
print_runtime_environment()

# ---------------------------------------------------------------
# Step 1: Load and describe reference signal
Expand Down Expand Up @@ -518,13 +561,23 @@ def main():
print()
print(" The pipeline output does NOT match the expected hash.")
print()
print(" Possible causes:")
print(" - Numpy/scipy version mismatch (check requirements)")
print(" - Code change in CSI processor that alters numerical output")
print(" - Platform floating-point differences (unlikely for IEEE 754)")
print(" Likely causes, in order of probability:")
print(" 1. Platform BLAS/FFT backend differs from the reference.")
print(" The expected hash was generated on linux-x86_64 +")
print(" windows-x86_64 with OpenBLAS. macOS arm64 ships with")
print(" Accelerate.framework, which produces bit-different FFT")
print(" output on identical inputs (issue #560). Inspect the")
print(" RUNTIME ENVIRONMENT block printed at the top of this run.")
print(" 2. Numpy/scipy version mismatch.")
print(" Install pinned versions: pip install -r archive/v1/requirements-lock.txt")
print(" 3. Real code change in the CSI processor that alters output.")
print(" Investigate the diff against the reference commit.")
print()
print(" To update the expected hash after intentional changes:")
print(" To regenerate the expected hash on a NEW reference platform:")
print(" python verify.py --generate-hash")
print(" (Only do this if you intend to publish a new reference; the")
print(" single-platform contract of expected_features.sha256 is")
print(" documented at the top of that file.)")
print("=" * 72)
sys.exit(1)

Expand Down
6 changes: 5 additions & 1 deletion docker/Dockerfile.rust
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
# Multi-stage build for minimal final image

# Stage 1: Build
FROM rust:1.85-bookworm AS builder
# Rust 1.87+ is required: `hnsw_rs 0.3.4` (transitive via wifi-densepose-ruvector ->
# ruvector-attn-mincut) uses `u*::is_multiple_of`, stabilised in 1.87. Pinning to a
# recent stable (1.90) for reproducibility — bump cautiously since reproducible
# builds rely on this.
FROM rust:1.90-bookworm AS builder

WORKDIR /build

Expand Down
23 changes: 15 additions & 8 deletions firmware/esp32-csi-node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,21 @@ MSYS_NO_PATHCONV=1 docker run --rm \
```bash
python -m esptool --chip esp32s3 --port COM7 --baud 460800 \
write_flash --flash_mode dio --flash_size 8MB \
0x0 firmware/esp32-csi-node/build/bootloader/bootloader.bin \
0x8000 firmware/esp32-csi-node/build/partition_table/partition-table.bin \
0x10000 firmware/esp32-csi-node/build/esp32-csi-node.bin
0x0 firmware/esp32-csi-node/build/bootloader/bootloader.bin \
0x8000 firmware/esp32-csi-node/build/partition_table/partition-table.bin \
0xf000 firmware/esp32-csi-node/build/ota_data_initial.bin \
0x20000 firmware/esp32-csi-node/build/esp32-csi-node.bin
```

> The app slot (`ota_0`) starts at `0x20000` per `partitions_display.csv` /
> `partitions_4mb.csv`. `ota_data_initial.bin` at `0xf000` initialises the OTA
> slot pointer; without it the bootloader can refuse to boot the app after a
> factory wipe.

### 3. Provision WiFi credentials (no reflash needed)

```bash
python scripts/provision.py --port COM7 \
python firmware/esp32-csi-node/provision.py --port COM7 \
--ssid "YourSSID" --password "YourPass" --target-ip 192.168.1.20
```

Expand Down Expand Up @@ -254,9 +260,10 @@ Find your serial port: `COM7` on Windows, `/dev/ttyUSB0` on Linux, `/dev/cu.SLAB
```bash
python -m esptool --chip esp32s3 --port COM7 --baud 460800 \
write_flash --flash_mode dio --flash_size 8MB \
0x0 firmware/esp32-csi-node/build/bootloader/bootloader.bin \
0x8000 firmware/esp32-csi-node/build/partition_table/partition-table.bin \
0x10000 firmware/esp32-csi-node/build/esp32-csi-node.bin
0x0 firmware/esp32-csi-node/build/bootloader/bootloader.bin \
0x8000 firmware/esp32-csi-node/build/partition_table/partition-table.bin \
0xf000 firmware/esp32-csi-node/build/ota_data_initial.bin \
0x20000 firmware/esp32-csi-node/build/esp32-csi-node.bin
```

### Serial Monitor
Expand Down Expand Up @@ -285,7 +292,7 @@ All settings can be changed at runtime via Non-Volatile Storage (NVS) without re
The easiest way to write NVS settings:

```bash
python scripts/provision.py --port COM7 \
python firmware/esp32-csi-node/provision.py --port COM7 \
--ssid "MyWiFi" \
--password "MyPassword" \
--target-ip 192.168.1.20
Expand Down
8 changes: 4 additions & 4 deletions verify
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,9 @@
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROOF_DIR="${SCRIPT_DIR}/v1/data/proof"
PROOF_DIR="${SCRIPT_DIR}/archive/v1/data/proof"
VERIFY_PY="${PROOF_DIR}/verify.py"
V1_SRC="${SCRIPT_DIR}/v1/src"
V1_SRC="${SCRIPT_DIR}/archive/v1/src"

# Colors (disabled if not a terminal)
if [ -t 1 ]; then
Expand Down Expand Up @@ -136,7 +136,7 @@ echo ""
echo -e "${CYAN}[PHASE 3] PRODUCTION CODE INTEGRITY SCAN${RESET}"
echo ""
echo " Scanning ${V1_SRC} for np.random.rand / np.random.randn calls..."
echo " (Excluding v1/src/testing/ -- test helpers are allowed to use random.)"
echo " (Excluding archive/v1/src/testing/ -- test helpers are allowed to use random.)"
echo ""

MOCK_FINDINGS=0
Expand Down Expand Up @@ -204,7 +204,7 @@ elif [ $PIPELINE_EXIT -eq 2 ]; then
echo -e " ${YELLOW}${BOLD}RESULT: SKIP${RESET}"
echo ""
echo " No expected hash file to compare against."
echo " Run: python v1/data/proof/verify.py --generate-hash"
echo " Run: python archive/v1/data/proof/verify.py --generate-hash"
echo ""
echo -e "${BOLD}======================================================================${RESET}"
exit 2
Expand Down
Loading