diff --git a/.github/workflows/readme-visuals.yml b/.github/workflows/readme-visuals.yml new file mode 100644 index 0000000000..f3abb83251 --- /dev/null +++ b/.github/workflows/readme-visuals.yml @@ -0,0 +1,44 @@ +name: README visual asset validation + +on: + pull_request: + paths: + - 'README.md' + - 'assets/readme/**' + - 'docs/readme-visual-system.md' + - 'scripts/*readme*' + - 'scripts/tests/test_readme_visuals_xml.py' + - '.github/workflows/readme-visuals.yml' + push: + branches: [main] + paths: + - 'README.md' + - 'assets/readme/**' + - 'docs/readme-visual-system.md' + - 'scripts/*readme*' + - 'scripts/tests/test_readme_visuals_xml.py' + - '.github/workflows/readme-visuals.yml' + workflow_dispatch: + +permissions: + contents: read + +jobs: + verify: + name: Safe and reproducible README visuals + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 + with: + python-version: '3.11' + - name: Install visual tooling dependency + run: python -m pip install -r scripts/requirements-readme-visuals.txt + - name: Test XML input boundaries + run: python -m unittest discover -s scripts/tests -p test_readme_visuals_xml.py + - name: Verify reproducible and accessible assets + run: | + python scripts/render-readme-visuals.py + python scripts/verify-readme-visuals.py + git diff --exit-code -- assets/readme + test -z "$(git ls-files --others --exclude-standard -- assets/readme)" diff --git a/README.md b/README.md index 08faa63a2a..49cd48214d 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,31 @@ # π RuView +[![RuView: spaces become signals, with an animated conceptual RF field](assets/readme/hero.svg)](docs/user-guide.md) + + + + + + + + + + + + +
Start here
Start here
Hardware
Hardware
Models
Models
AI toolkit
AI toolkit
Smart home
Smart home
Cognitum Seed
Cognitum Seed
+ +
+Watch the original RuView sensing demo +

RuView - WiFi DensePose — animated visualization of real-time pose estimation, breathing, and heart-rate sensing through WiFi

+
+

RuView — WiFi becomes spatial awareness, with Ruflo coordination and a ruOS sensing workspace @@ -27,6 +47,8 @@ Works natively with the four major smart-home ecosystems: **[Home Assistant](doc Every WiFi router already fills your space with radio waves. When people move, breathe, or even sit still, they disturb those waves in measurable ways. RuView captures these disturbances using Channel State Information (CSI) from low-cost ESP32 sensors and turns them into actionable data: who's there, what they're doing, and whether they're okay. +[![Explore RuView sensing, models and automation](assets/readme/header-features.svg)](docs/user-guide.md) + **What it senses:** - **Presence and occupancy** — detect people through walls, count them, track entries and exits - **Vital signs** — breathing rate and heart rate, contactless, while sleeping or sitting @@ -43,6 +65,22 @@ Every WiFi router already fills your space with radio waves. When people move, b - **Governed evidence** — attach privacy policy, uncertainty, provenance, and witness records to sensing events - **RuView MetaHarness** — use an AI operator to onboard, calibrate, train, verify, and check sensing claims + + + + + + + + + + + + + + +
Presence and motion: open documentation and current limitations
Presence and motion
Breathing and vital signals: open documentation and current limitations
Breathing and vital signals
Pose research and limitations: open documentation and current limitations
Pose research and limitations
Edge sensor mesh: open documentation and current limitations
Edge sensor mesh
Local automation: open documentation and current limitations
Local automation
Guided AI operator: open documentation and current limitations
Guided AI operator
+

RuView MetaHarness — guided operation for humans and AI agents @@ -263,6 +301,8 @@ The separate **17-keypoint pose-estimation model** is now published at [`ruvnet/ ### Results & proof +[![RuView visual guide: header-evidence](assets/readme/header-evidence.svg)](docs/benchmarks/pose-estimation-cog.md) + See the measured benchmarks, witness records, and one-command reproducibility check.
@@ -317,6 +357,8 @@ number. The path to a first *reproducible* on-device baseline (PCK@20 ≥ 35%) i ## 🧩 Edge Module Catalog +[![RuView visual guide: header-edge](assets/readme/header-edge.svg)](firmware/esp32-csi-node/README.md) + Add signed modules for health, security, buildings, industry, research, AI, and more.
@@ -491,6 +533,8 @@ Browse and install modules at [seed.cognitum.one/store](https://seed.cognitum.on ## 🔬 How It Works +[![RuView visual guide: overview](assets/readme/overview.svg)](docs/user-guide.md) + WiFi routers flood every room with radio waves. When a person moves — or even breathes — those waves scatter differently. WiFi DensePose reads that scattering pattern and reconstructs what happened: ``` @@ -615,6 +659,8 @@ These scenarios exploit WiFi's ability to penetrate solid materials — concrete ## 🧠 Self-Learning WiFi AI +[![RuView visual guide: header-learning](assets/readme/header-learning.svg)](docs/user-guide.md) + Learn compact room fingerprints from raw CSI and adapt the model to each environment.
@@ -744,6 +790,8 @@ For the portable RuView MetaHarness, use `npx @ruvnet/ruview@0.9.1`. The quick c ## 📖 Documentation +[![RuView visual guide: header-docs](assets/readme/header-docs.svg)](docs/user-guide.md) + Start with the user, build, and calibration guides; expand for the full reference map.
@@ -785,6 +833,8 @@ Start with the user, build, and calibration guides; expand for the full referenc > > Contributions and bug reports welcome at [Issues](https://github.com/ruvnet/RuView/issues). +[![Cognitum One: Ambient Intelligence at the edge of the physical world](assets/readme/cognitum.svg)](https://cognitum.one) + ## 📄 License MIT License — see [LICENSE](LICENSE) for details. diff --git a/assets/readme/cognitum.svg b/assets/readme/cognitum.svg new file mode 100644 index 0000000000..c5a7615ae5 --- /dev/null +++ b/assets/readme/cognitum.svg @@ -0,0 +1 @@ +Explore Cognitum OneAmbient Intelligence at the edge of the physical world. Open cognitum.one.COGNITUM ONEAmbient Intelligence.At the edge of the physical world.Explore the platform behind the ecosystem.Explore Cognitum diff --git a/assets/readme/feature-agents.svg b/assets/readme/feature-agents.svg new file mode 100644 index 0000000000..7cd6197bc9 --- /dev/null +++ b/assets/readme/feature-agents.svg @@ -0,0 +1 @@ +A guided AI operatorSource-cited setup and verification. CLI, MCP and agent workflows. Conceptual animation; follow the linked source for capability limits.RUVIEW / 06A guided AI operatorSource-cited setup and verification.CLI, MCP and agent workflows.TOOLKIT GUIDE diff --git a/assets/readme/feature-automation.svg b/assets/readme/feature-automation.svg new file mode 100644 index 0000000000..8104d37abe --- /dev/null +++ b/assets/readme/feature-automation.svg @@ -0,0 +1 @@ +Local automationConnect rooms to your smart home. Home Assistant, MQTT and Matter. Conceptual animation; follow the linked source for capability limits.RUVIEW / 05Local automationConnect rooms to your smart home.Home Assistant, MQTT and Matter.INTEGRATION GUIDE diff --git a/assets/readme/feature-edge.svg b/assets/readme/feature-edge.svg new file mode 100644 index 0000000000..d3c589cc1e --- /dev/null +++ b/assets/readme/feature-edge.svg @@ -0,0 +1 @@ +Edge sensor meshProvision, capture and calibrate. Explore ESP32 firmware and runbooks. Conceptual animation; follow the linked source for capability limits.RUVIEW / 04Edge sensor meshProvision, capture and calibrate.Explore ESP32 firmware and runbooks.HARDWARE GUIDE diff --git a/assets/readme/feature-pose.svg b/assets/readme/feature-pose.svg new file mode 100644 index 0000000000..eeaa9cb209 --- /dev/null +++ b/assets/readme/feature-pose.svg @@ -0,0 +1 @@ +Pose researchTrain and compare body keypoints. Live accuracy differs from benchmarks. Conceptual animation; follow the linked source for capability limits.RUVIEW / 03Pose researchTrain and compare body keypoints.Live accuracy differs from benchmarks.READ THE EVIDENCE diff --git a/assets/readme/feature-presence.svg b/assets/readme/feature-presence.svg new file mode 100644 index 0000000000..910d9d0d8e --- /dev/null +++ b/assets/readme/feature-presence.svg @@ -0,0 +1 @@ +Presence & motionExplore changes in room occupancy. Inspect motion features from CSI. Conceptual animation; follow the linked source for capability limits.RUVIEW / 01Presence & motionExplore changes in room occupancy.Inspect motion features from CSI.SENSING GUIDE diff --git a/assets/readme/feature-vitals.svg b/assets/readme/feature-vitals.svg new file mode 100644 index 0000000000..c2cb41c035 --- /dev/null +++ b/assets/readme/feature-vitals.svg @@ -0,0 +1 @@ +Breathing & vital signalsInspect phase-derived signal trends. Validate estimates on your hardware. Conceptual animation; follow the linked source for capability limits.RUVIEW / 02Breathing & vital signalsInspect phase-derived signal trends.Validate estimates on your hardware.SIGNAL PIPELINE diff --git a/assets/readme/header-docs.svg b/assets/readme/header-docs.svg new file mode 100644 index 0000000000..50e9885e72 --- /dev/null +++ b/assets/readme/header-docs.svg @@ -0,0 +1 @@ +Choose your next stepAnimated section heading. GUIDES / HARDWARE / INTEGRATIONSChoose your next stepGUIDES / HARDWARE / INTEGRATIONS diff --git a/assets/readme/header-edge.svg b/assets/readme/header-edge.svg new file mode 100644 index 0000000000..1c989c9f9a --- /dev/null +++ b/assets/readme/header-edge.svg @@ -0,0 +1 @@ +Intelligence at the edgeAnimated section heading. SENSORS / FIRMWARE / MODULESIntelligence at the edgeSENSORS / FIRMWARE / MODULES diff --git a/assets/readme/header-evidence.svg b/assets/readme/header-evidence.svg new file mode 100644 index 0000000000..2b8a102b07 --- /dev/null +++ b/assets/readme/header-evidence.svg @@ -0,0 +1 @@ +Evidence before accuracy claimsAnimated section heading. BENCHMARKS / REPRODUCERS / LIMITSEvidence before accuracy claimsBENCHMARKS / REPRODUCERS / LIMITS diff --git a/assets/readme/header-features.svg b/assets/readme/header-features.svg new file mode 100644 index 0000000000..8eb839430e --- /dev/null +++ b/assets/readme/header-features.svg @@ -0,0 +1 @@ +Explore the capabilitiesAnimated section heading. SENSING / MODELS / AUTOMATIONExplore the capabilitiesSENSING / MODELS / AUTOMATION diff --git a/assets/readme/header-learning.svg b/assets/readme/header-learning.svg new file mode 100644 index 0000000000..ee40ae04e9 --- /dev/null +++ b/assets/readme/header-learning.svg @@ -0,0 +1 @@ +From recordings to modelsAnimated section heading. CAPTURE / TRAIN / EVALUATEFrom recordings to modelsCAPTURE / TRAIN / EVALUATE diff --git a/assets/readme/hero.svg b/assets/readme/hero.svg new file mode 100644 index 0000000000..51e0d1d44a --- /dev/null +++ b/assets/readme/hero.svg @@ -0,0 +1 @@ +RuView: spaces become signalsAnimated conceptual RF field and room geometry. Explore the RuView user guide. This illustration is not a sensor measurement.RUVIEW / COGNITUM ECOSYSTEMRuViewSpaces become signals.Signals become understanding.Explore RF sensing, edge inference and local automation.Explore the guideRF SENSING / EDGE MODELS / LOCAL ACTIONCONCEPTUAL RF FIELD diff --git a/assets/readme/manifest.json b/assets/readme/manifest.json new file mode 100644 index 0000000000..c7a3e4c2f9 --- /dev/null +++ b/assets/readme/manifest.json @@ -0,0 +1,147 @@ +{ + "schema_version": 1, + "style": "Cognitum cyan on midnight", + "motion": "Decorative CSS; prefers-reduced-motion supported; no script or remote dependencies", + "assets": [ + { + "file": "assets/readme/hero.svg", + "title": "RuView: spaces become signals", + "target": "docs/user-guide.md", + "width": 1200, + "height": 450 + }, + { + "file": "assets/readme/nav-start.svg", + "title": "Start here", + "target": "docs/user-guide.md", + "width": 180, + "height": 116 + }, + { + "file": "assets/readme/nav-hardware.svg", + "title": "Hardware", + "target": "firmware/esp32-csi-node/README.md", + "width": 180, + "height": 116 + }, + { + "file": "assets/readme/nav-models.svg", + "title": "Models", + "target": "https://huggingface.co/ruvnet/wifi-densepose-pretrained", + "width": 180, + "height": 116 + }, + { + "file": "assets/readme/nav-agents.svg", + "title": "AI toolkit", + "target": "harness/ruview/README.md", + "width": 180, + "height": 116 + }, + { + "file": "assets/readme/nav-home.svg", + "title": "Smart home", + "target": "docs/integrations/home-assistant.md", + "width": 180, + "height": 116 + }, + { + "file": "assets/readme/nav-seed.svg", + "title": "Cognitum", + "target": "https://cognitum.one/seed", + "width": 180, + "height": 116 + }, + { + "file": "assets/readme/feature-presence.svg", + "title": "Presence & motion", + "target": "docs/user-guide.md", + "width": 580, + "height": 276 + }, + { + "file": "assets/readme/feature-vitals.svg", + "title": "Breathing & vital signals", + "target": "v2/crates/wifi-densepose-signal/README.md", + "width": 580, + "height": 276 + }, + { + "file": "assets/readme/feature-pose.svg", + "title": "Pose research", + "target": "docs/benchmarks/pose-estimation-cog.md", + "width": 580, + "height": 276 + }, + { + "file": "assets/readme/feature-edge.svg", + "title": "Edge sensor mesh", + "target": "firmware/esp32-csi-node/README.md", + "width": 580, + "height": 276 + }, + { + "file": "assets/readme/feature-automation.svg", + "title": "Local automation", + "target": "docs/integrations/home-assistant.md", + "width": 580, + "height": 276 + }, + { + "file": "assets/readme/feature-agents.svg", + "title": "A guided AI operator", + "target": "harness/ruview/README.md", + "width": 580, + "height": 276 + }, + { + "file": "assets/readme/overview.svg", + "title": "RuView sensing overview", + "target": "docs/user-guide.md", + "width": 1200, + "height": 396 + }, + { + "file": "assets/readme/header-features.svg", + "title": "Explore the capabilities", + "target": "docs/user-guide.md", + "width": 1200, + "height": 148 + }, + { + "file": "assets/readme/header-edge.svg", + "title": "Intelligence at the edge", + "target": "firmware/esp32-csi-node/README.md", + "width": 1200, + "height": 148 + }, + { + "file": "assets/readme/header-learning.svg", + "title": "From recordings to models", + "target": "docs/user-guide.md", + "width": 1200, + "height": 148 + }, + { + "file": "assets/readme/header-evidence.svg", + "title": "Evidence before accuracy claims", + "target": "docs/benchmarks/pose-estimation-cog.md", + "width": 1200, + "height": 148 + }, + { + "file": "assets/readme/header-docs.svg", + "title": "Choose your next step", + "target": "docs/user-guide.md", + "width": 1200, + "height": 148 + }, + { + "file": "assets/readme/cognitum.svg", + "title": "Explore Cognitum One", + "target": "https://cognitum.one", + "width": 1200, + "height": 304 + } + ] +} diff --git a/assets/readme/nav-agents.svg b/assets/readme/nav-agents.svg new file mode 100644 index 0000000000..5b427af96c --- /dev/null +++ b/assets/readme/nav-agents.svg @@ -0,0 +1 @@ +AI toolkitOpen AI toolkit. Animated navigation icon.AI toolkit diff --git a/assets/readme/nav-hardware.svg b/assets/readme/nav-hardware.svg new file mode 100644 index 0000000000..ab83aa8ad7 --- /dev/null +++ b/assets/readme/nav-hardware.svg @@ -0,0 +1 @@ +HardwareOpen Hardware. Animated navigation icon.Hardware diff --git a/assets/readme/nav-home.svg b/assets/readme/nav-home.svg new file mode 100644 index 0000000000..1ef9806728 --- /dev/null +++ b/assets/readme/nav-home.svg @@ -0,0 +1 @@ +Smart homeOpen Smart home. Animated navigation icon.Smart home diff --git a/assets/readme/nav-models.svg b/assets/readme/nav-models.svg new file mode 100644 index 0000000000..41f538a745 --- /dev/null +++ b/assets/readme/nav-models.svg @@ -0,0 +1 @@ +ModelsOpen Models. Animated navigation icon.Models diff --git a/assets/readme/nav-seed.svg b/assets/readme/nav-seed.svg new file mode 100644 index 0000000000..0788f43814 --- /dev/null +++ b/assets/readme/nav-seed.svg @@ -0,0 +1 @@ +CognitumOpen Cognitum. Animated navigation icon.Cognitum diff --git a/assets/readme/nav-start.svg b/assets/readme/nav-start.svg new file mode 100644 index 0000000000..2e911f1153 --- /dev/null +++ b/assets/readme/nav-start.svg @@ -0,0 +1 @@ +Start hereOpen Start here. Animated navigation icon.Start here diff --git a/assets/readme/overview.svg b/assets/readme/overview.svg new file mode 100644 index 0000000000..27d98d2b8c --- /dev/null +++ b/assets/readme/overview.svg @@ -0,0 +1 @@ +RuView sensing overviewConceptual sequence: capture RF, extract context, act locally. Links to architecture and user documentation.SIGNAL TO CONTEXTA sensing loop you can inspect.01Capture RFCSI from supported hardwareProvision and calibrate02Extract contextSignal processing and modelsCompare against real data03Act locallyEvents, APIs and integrationsApply rules and review outputsCONCEPTUAL PIPELINE / HARDWARE AND ENVIRONMENT VALIDATION MATTER diff --git a/docs/readme-visual-system.md b/docs/readme-visual-system.md new file mode 100644 index 0000000000..ad6fcd7406 --- /dev/null +++ b/docs/readme-visual-system.md @@ -0,0 +1,36 @@ +# RuView README visual system + +The README uses a Cognitum-inspired palette: midnight backgrounds, cyan gradients, wireframe geometry and restrained signal motion. It includes a hero, six feature cards, six navigation icons, an overview, five chapter headings and a Cognitum destination banner. + +## Rebuild and check + +```bash +python3 -m pip install -r scripts/requirements-readme-visuals.txt +python3 scripts/render-readme-visuals.py +python3 scripts/verify-readme-visuals.py +python3 -m unittest discover -s scripts/tests -p test_readme_visuals_xml.py +``` + +Install the tooling dependency in a virtual environment. The generator and verifier +use `defusedxml` to reject DTDs, entities and external XML references. SVG inputs +must be smaller than 20,000 bytes; the verifier reads at most that limit and +rejects oversized inputs before parsing. Both tools use UTF-8, and generated +files use LF line endings for reproducibility across platforms. They make no +network calls. This dependency is only for visual maintenance, not the runtime. +Manifest paths must be relative: SVG files stay inside `assets/readme`, and +local link targets stay inside the repository, including after symlink resolution. + +The committed SVGs are the deployment artifacts. The manifest in +`assets/readme/manifest.json` lists every destination. + +## Links and rendering + +GitHub renders SVGs as images, so image-internal links are not used. Each SVG has an outer Markdown or HTML link in the README. Each card and navigation icon also has a plain text link for narrow screens and assistive technology. Source headings remain intact so existing README anchors still work. + +Animation uses CSS only. All copy and essential geometry are visible without animation, JavaScript, remote fonts or remote images. `prefers-reduced-motion` disables decorative movement. The original GIF remains available in an expandable demo section; the existing ruOS promotion remains linked. + +## Content boundaries + +The overview and RF room are conceptual illustrations, not live measurements or benchmark evidence. Feature cards link to current source documentation. Pose research is identified as such, and the signal card directs readers to hardware validation. No new speed, accuracy, medical or device-support claims are introduced. + +Sources: `docs/user-guide.md`, `docs/benchmarks/pose-estimation-cog.md`, `v2/crates/wifi-densepose-signal/README.md`, `firmware/esp32-csi-node/README.md`, `docs/integrations/home-assistant.md`, and `harness/ruview/README.md`. diff --git a/scripts/readme_visuals_xml.py b/scripts/readme_visuals_xml.py new file mode 100644 index 0000000000..334d02d32f --- /dev/null +++ b/scripts/readme_visuals_xml.py @@ -0,0 +1,50 @@ +"""Bounded, hardened XML parsing for the README visual maintenance tools.""" + +from pathlib import Path, PurePosixPath, PureWindowsPath + +from defusedxml import ElementTree + + +# Keep the visual verifier's existing exclusive per-asset size limit. +MAX_SVG_BYTES = 20_000 + + +def resolve_repo_file(root: Path, relative: str, *, asset: bool = False) -> Path: + """Resolve a portable manifest path inside its trusted repository boundary.""" + if ( + not isinstance(relative, str) + or not relative + or "\\" in relative + or ":" in relative + or "\x00" in relative + or PurePosixPath(relative).is_absolute() + or PureWindowsPath(relative).drive + or ".." in relative.split("/") + ): + raise ValueError("Expected a repository-relative path without traversal") + repository = root.resolve() + boundary = repository / "assets" / "readme" if asset else repository + resolved = (repository / relative).resolve() + if not resolved.is_relative_to(boundary): + raise ValueError("Manifest path escapes its allowed repository directory") + if not resolved.is_file(): + raise ValueError("Manifest path must name an existing file") + return resolved + + +def parse_svg(raw: bytes): + """Reject oversized input and DTD/entity declarations before building a tree.""" + if not isinstance(raw, bytes): + raise TypeError("SVG input must be bytes") + if len(raw) >= MAX_SVG_BYTES: + raise ValueError(f"SVG must be smaller than {MAX_SVG_BYTES} bytes") + return ElementTree.fromstring( + raw, forbid_dtd=True, forbid_entities=True, forbid_external=True + ) + + +def read_svg(path: Path): + """Read at most the rejection boundary, including when a file grows mid-read.""" + with path.open("rb") as stream: + raw = stream.read(MAX_SVG_BYTES) + return raw, parse_svg(raw) diff --git a/scripts/render-readme-visuals.py b/scripts/render-readme-visuals.py new file mode 100644 index 0000000000..e37a9c58b9 --- /dev/null +++ b/scripts/render-readme-visuals.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python3 +"""Generate the RuView README visual system. Deterministic, no network.""" +from pathlib import Path +from html import escape +import math,json +from readme_visuals_xml import parse_svg +ROOT=Path(__file__).resolve().parents[1];OUT=ROOT/'assets/readme';OUT.mkdir(parents=True,exist_ok=True) +C='#72eee0';BLUE='#70b6ff';INK='#041117';WHITE='#ecfafb';MUTED='#9abac5' +def text(x,y,value,size=16,color=MUTED,anchor='start',extra=''): + return f'{escape(value)}' +def path(d,color=C,width=1.5,extra=''): + return f'' +def circle(x,y,r,color=C,extra=''): + fill = '' if 'fill=' in extra else 'fill="none"' + return f'' + +def icon(kind,x,y,scale=1): + s='' + if kind in ['signal','start']: + for r in [13,25,38]:s+=path(f'M{-r*.8} {-r*.4}Q0 {-r*1.4} {r*.8} {-r*.4}',extra=f'class="breathe" style="animation-delay:-{r/20}s"') + s+=circle(0,20,4,extra='fill="#72eee0"')+path('M0 12V-1') + elif kind in ['presence','pose']: + s+=circle(0,-28,8)+path('M0 -20V12M-24 -6L0 -14L24 -6M0 12L-18 38M0 12L18 38',width=2) + if kind=='pose': + for px,py in [(0,-14),(0,12),(-24,-6),(24,-6),(-18,38),(18,38)]:s+=circle(px,py,3,BLUE,extra='fill="#041117" class="breathe"') + else:s+=f'' + elif kind in ['vitals','models']: + s+=path('M-42 6H-27L-18 -10L-9 25L3 -25L14 10L24 -3L31 6H43',width=2,extra='class="trace"')+path('M-42 31H43','#214451',1) + elif kind in ['edge','hardware']: + s+='' + s+='' + for v in [-16,0,16]:s+=path(f'M{v} -36V-25M{v} 25V36M-36 {v}H-25M25 {v}H36') + s+=circle(0,0,4,extra='fill="#72eee0" class="breathe"') + elif kind in ['home','automation']: + s+=path('M-37 -3L0 -34L37 -3M-28 -10V32H28V-10M-9 32V8H9V32',width=2) + s+=circle(0,-9,5,BLUE,extra='class="breathe"') + elif kind in ['agent','agents']: + for px,py in [(0,-33),(-35,23),(35,23)]:s+=path(f'M0 0L{px} {py}',BLUE,1,extra='class="flow"')+circle(px,py,7,extra='class="breathe"') + s+=circle(0,0,15)+path('M-6 0L-1 5L8 -6') + elif kind in ['seed','evidence']: + s+=path('M0 -38L31 -20V18L0 38L-31 18V-20ZM-31 -20L0 -1L31 -20M0 -1V38',width=2) + s+=path('M-15 1L-5 11L17 -12',BLUE,2,extra='class="trace"') + else: + for j in range(3):s+=path(f'M-38 {j*14-20}L0 {j*14-38}L38 {j*14-20}L0 {j*14-2}Z',extra='class="breathe"') + return f'{s}' +def frame(w,h,title,desc,body): + return f'''{escape(title)}{escape(desc)}{body}''' +manifest=[] +def save(name,w,h,title,desc,body,target): + s=frame(w,h,title,desc,body);raw=(s+'\n').encode('utf-8');parse_svg(raw);(OUT/(name+'.svg')).write_bytes(raw);manifest.append({'file':'assets/readme/'+name+'.svg','title':title,'target':target,'width':w,'height':h}) +# Main title panel: a radio field passes through an illustrated room. +s=text(48,44,'RUVIEW / COGNITUM ECOSYSTEM',13,C,extra='class="mono"')+text(48,121,'RuView',76,WHITE)+text(48,184,'Spaces become signals.',38,WHITE)+text(48,234,'Signals become understanding.',32,C)+text(50,279,'Explore RF sensing, edge inference and local automation.',18) +s+=''+text(72,359,'Explore the guide',20,INK)+path('M272 352H292M285 345L292 352L285 359',INK,2) +s+=text(49,419,'RF SENSING / EDGE MODELS / LOCAL ACTION',11,MUTED,extra='class="mono"') +s+='' +for i in range(9):s+=path(f'M{750+i*40} 282L{610+i*80} 443','#12343d',1) +for y in [306,336,372,418]:s+=path(f'M680 {y}H1168','#12343d',1) +s+=path('M803 157L1008 106L1137 177L930 236ZM803 157V314L930 391L1137 324V177M930 236V391M1008 106V268L803 314M1008 268L1137 324','#4b9ba4',1.5) +s+=icon('presence',976,265,1.35)+icon('edge',780,349,.7) +for i in range(4):s+=path(f'M{794+i*12} {333-i*9}Q{830+i*25} {245-i*24} {896+i*29} {207-i*3}',C,1.4,extra=f'class="flow" style="animation-delay:-{i}s" opacity="{.8-i*.12}"') +for x,y in [(803,157),(1008,106),(1137,177),(930,236),(930,391),(803,314),(1137,324)]:s+=circle(x,y,3,extra='fill="#72eee0" class="breathe"') +s+=text(1148,419,'CONCEPTUAL RF FIELD',10,MUTED,'end',extra='class="mono"') +save('hero',1200,450,'RuView: spaces become signals','Animated conceptual RF field and room geometry. Explore the RuView user guide. This illustration is not a sensor measurement.',s,'docs/user-guide.md') +# Visual navigation icons, each receives a real outer link in Markdown. +nav=[('start','Start here','docs/user-guide.md'),('hardware','Hardware','firmware/esp32-csi-node/README.md'),('models','Models','https://huggingface.co/ruvnet/wifi-densepose-pretrained'),('agents','AI toolkit','harness/ruview/README.md'),('home','Smart home','docs/integrations/home-assistant.md'),('seed','Cognitum','https://cognitum.one/seed')] +for k,label,target in nav:save('nav-'+k,180,116,label,'Open '+label+'. Animated navigation icon.',icon(k,90,45,.65)+text(90,98,label,16,WHITE,'middle'),target) +# Capability cards. No unverified performance claims. +cards=[('presence','01','Presence & motion','Explore changes in room occupancy.','Inspect motion features from CSI.','SENSING GUIDE','docs/user-guide.md'),('vitals','02','Breathing & vital signals','Inspect phase-derived signal trends.','Validate estimates on your hardware.','SIGNAL PIPELINE','v2/crates/wifi-densepose-signal/README.md'),('pose','03','Pose research','Train and compare body keypoints.','Live accuracy differs from benchmarks.','READ THE EVIDENCE','docs/benchmarks/pose-estimation-cog.md'),('edge','04','Edge sensor mesh','Provision, capture and calibrate.','Explore ESP32 firmware and runbooks.','HARDWARE GUIDE','firmware/esp32-csi-node/README.md'),('automation','05','Local automation','Connect rooms to your smart home.','Home Assistant, MQTT and Matter.','INTEGRATION GUIDE','docs/integrations/home-assistant.md'),('agents','06','A guided AI operator','Source-cited setup and verification.','CLI, MCP and agent workflows.','TOOLKIT GUIDE','harness/ruview/README.md')] +for k,num,title,l1,l2,cta,target in cards: + s=text(30,39,'RUVIEW / '+num,11,C,extra='class="mono"')+text(30,94,title,29,WHITE)+text(30,136,l1,16)+text(30,164,l2,16) + s+=''+icon(k,474,168,.83)+path('M30 204H550','#245360',1)+path('M30 204H550',C,1,extra='class="flow" opacity=".5"') + s+=text(30,245,cta,12,C,extra='class="mono"')+path('M510 239H540M532 231L540 239L532 247',C,1.5) + save('feature-'+k,580,276,title,l1+' '+l2+' Conceptual animation; follow the linked source for capability limits.',s,target) +# Overview uses three clearly labelled stages rather than fabricated telemetry. +s=text(36,44,'SIGNAL TO CONTEXT',12,C,extra='class="mono"')+text(36,89,'A sensing loop you can inspect.',34,WHITE) +for i,(label,sub,kind,tail) in enumerate([('Capture RF','CSI from supported hardware','signal','Provision and calibrate'),('Extract context','Signal processing and models','models','Compare against real data'),('Act locally','Events, APIs and integrations','automation','Apply rules and review outputs')]): + x=36+i*396;s+=f'' + s+=text(x+24,153,f'0{i+1}',12,C)+text(x+24,188,label,26,WHITE)+text(x+24,216,sub,15)+icon(kind,x+269,281,.62)+text(x+24,311,tail,13) + if i<2:s+=path(f'M{x+344} 229H{x+384}M{x+376} 221L{x+384} 229L{x+376} 237',C,2,extra='class="flow"') +s+=text(36,373,'CONCEPTUAL PIPELINE / HARDWARE AND ENVIRONMENT VALIDATION MATTER',11,MUTED,extra='class="mono"') +save('overview',1200,396,'RuView sensing overview','Conceptual sequence: capture RF, extract context, act locally. Links to architecture and user documentation.',s,'docs/user-guide.md') +# Chapter headings act as visual landmarks, all remain links. +headers=[('features','Explore the capabilities','SENSING / MODELS / AUTOMATION','presence','docs/user-guide.md'),('edge','Intelligence at the edge','SENSORS / FIRMWARE / MODULES','edge','firmware/esp32-csi-node/README.md'),('learning','From recordings to models','CAPTURE / TRAIN / EVALUATE','models','docs/user-guide.md'),('evidence','Evidence before accuracy claims','BENCHMARKS / REPRODUCERS / LIMITS','evidence','docs/benchmarks/pose-estimation-cog.md'),('docs','Choose your next step','GUIDES / HARDWARE / INTEGRATIONS','agents','docs/user-guide.md')] +for k,title,sub,kind,target in headers: + s=icon(kind,76,75,.75)+text(140,63,title,34,WHITE)+text(142,99,sub,12,C,extra='class="mono"') + for i in range(4):s+=path(f'M865 {44+i*17}Q960 {10+i*28} 1124 {52+i*17}',C,1,extra=f'class="flow" opacity="{.65-i*.1}"') + s+=path('M1140 70H1160M1152 62L1160 70L1152 78',C,2) + save('header-'+k,1200,148,title,'Animated section heading. '+sub,s,target) +# Brand destination; original ruOS promotion remains in the README. +s=text(44,42,'COGNITUM ONE',12,C,extra='class="mono"')+text(44,99,'Ambient Intelligence.',42,WHITE)+text(44,147,'At the edge of the physical world.',25,C)+text(44,190,'Explore the platform behind the ecosystem.',16) +s+=''+text(65,254,'Explore Cognitum',18,INK)+path('M263 247H284M277 240L284 247L277 254',INK,2) +s+=''+icon('seed',945,150,2.4) +for j in range(3):s+=f'' +save('cognitum',1200,304,'Explore Cognitum One','Ambient Intelligence at the edge of the physical world. Open cognitum.one.',s,'https://cognitum.one') +(OUT/'manifest.json').write_bytes((json.dumps({'schema_version':1,'style':'Cognitum cyan on midnight','motion':'Decorative CSS; prefers-reduced-motion supported; no script or remote dependencies','assets':manifest},indent=2)+'\n').encode('utf-8')) +print(f'Generated {len(manifest)} SVGs, {sum((ROOT/x["file"]).stat().st_size for x in manifest):,} bytes') diff --git a/scripts/requirements-readme-visuals.txt b/scripts/requirements-readme-visuals.txt new file mode 100644 index 0000000000..d17db13ae5 --- /dev/null +++ b/scripts/requirements-readme-visuals.txt @@ -0,0 +1,2 @@ +# Development-only dependency for README SVG generation and verification. +defusedxml==0.7.1 diff --git a/scripts/tests/test_readme_visuals_xml.py b/scripts/tests/test_readme_visuals_xml.py new file mode 100644 index 0000000000..d9786fcda4 --- /dev/null +++ b/scripts/tests/test_readme_visuals_xml.py @@ -0,0 +1,152 @@ +"""Security regressions for the README visual tools' shared XML boundary.""" + +import io +from pathlib import Path +import sys +import tempfile +import unittest +from unittest.mock import patch + +from defusedxml.common import DTDForbidden + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import readme_visuals_xml as svg_xml + + +class ReadmeVisualXmlTests(unittest.TestCase): + def test_manifest_paths_reject_absolute_and_traversal_before_resolution(self): + for relative in [ + '', '/tmp/outside.svg', '//server/share/outside.svg', + 'C:/outside.svg', 'C:outside.svg', r'\\server\share\outside.svg', + r'\outside.svg', r'\\?\C:\outside.svg', 'file.svg:stream', + '../outside.svg', 'assets/readme/../../../outside.svg', + r'assets\readme\..\outside.svg', 'assets/readme/\x00.svg', + ]: + with self.subTest(relative=relative): + with patch.object(Path, 'resolve') as resolve: + with self.assertRaises(ValueError): + svg_xml.resolve_repo_file(Path('repository'), relative) + resolve.assert_not_called() + + def test_manifest_paths_keep_assets_and_targets_in_separate_boundaries(self): + with tempfile.TemporaryDirectory() as directory: + repository = Path(directory).resolve() + asset = repository / 'assets' / 'readme' / 'ordinary.svg' + asset.parent.mkdir(parents=True) + asset.write_bytes(b'') + target = repository / 'README.md' + target.write_bytes(b'# Readme\n') + self.assertEqual(svg_xml.resolve_repo_file(repository, 'assets/readme/ordinary.svg', asset=True), asset) + self.assertEqual(svg_xml.resolve_repo_file(repository, 'README.md'), target) + with patch.object(Path, 'is_file') as is_file: + with self.assertRaises(ValueError): + svg_xml.resolve_repo_file(repository, 'README.md', asset=True) + is_file.assert_not_called() + + def test_manifest_paths_reject_symlink_escape_before_file_inspection(self): + with tempfile.TemporaryDirectory() as directory: + base = Path(directory) + repository = base / 'repository' + assets = repository / 'assets' / 'readme' + assets.mkdir(parents=True) + outside = base / 'outside.svg' + outside.write_bytes(b'') + try: + (assets / 'escape.svg').symlink_to(outside) + (repository / 'escaped-directory').symlink_to(base, target_is_directory=True) + except (OSError, NotImplementedError) as error: + if sys.platform == 'win32' and (isinstance(error, NotImplementedError) or getattr(error, 'winerror', None) == 1314): + self.skipTest('Windows symlink privilege unavailable') + raise + for relative, asset in [('assets/readme/escape.svg', True), ('escaped-directory/outside.svg', False)]: + with self.subTest(relative=relative): + with patch.object(Path, 'is_file') as is_file: + with self.assertRaises(ValueError): + svg_xml.resolve_repo_file(repository, relative, asset=asset) + is_file.assert_not_called() + + def test_asset_directory_symlink_cannot_redefine_trusted_boundary(self): + with tempfile.TemporaryDirectory() as directory: + base = Path(directory) + repository = base / 'repository' + (repository / 'assets').mkdir(parents=True) + outside = base / 'outside' + outside.mkdir() + (outside / 'escape.svg').write_bytes(b'') + try: + (repository / 'assets' / 'readme').symlink_to(outside, target_is_directory=True) + except (OSError, NotImplementedError) as error: + if sys.platform == 'win32' and (isinstance(error, NotImplementedError) or getattr(error, 'winerror', None) == 1314): + self.skipTest('Windows symlink privilege unavailable') + raise + with patch.object(Path, 'is_file') as is_file: + with self.assertRaises(ValueError): + svg_xml.resolve_repo_file(repository, 'assets/readme/escape.svg', asset=True) + is_file.assert_not_called() + + def test_plain_utf8_svg_parses(self): + root = svg_xml.parse_svg('Présence'.encode('utf-8')) + self.assertEqual(root.tag, 'svg') + self.assertEqual(root.find('title').text, 'Présence') + + def test_dtd_and_entity_payloads_are_rejected(self): + for raw in [ + b'', + b']>&payload;', + b']>&payload;', + b'', + ]: + with self.subTest(raw=raw): + with self.assertRaises(DTDForbidden): + svg_xml.parse_svg(raw) + + def test_size_limit_is_enforced_before_parser_runs(self): + for size in [svg_xml.MAX_SVG_BYTES, svg_xml.MAX_SVG_BYTES + 1]: + with self.subTest(size=size): + with patch.object(svg_xml.ElementTree, 'fromstring') as parser: + with self.assertRaisesRegex(ValueError, 'smaller than 20000 bytes'): + svg_xml.parse_svg(b' ' * size) + parser.assert_not_called() + + def test_limit_counts_utf8_bytes_and_allows_last_valid_size(self): + valid = b'' + b' ' * (svg_xml.MAX_SVG_BYTES - 7) + self.assertEqual(len(valid), svg_xml.MAX_SVG_BYTES - 1) + self.assertEqual(svg_xml.parse_svg(valid).tag, 'svg') + text = '' + 'é' * 10000 + '' + self.assertLess(len(text), svg_xml.MAX_SVG_BYTES) + with self.assertRaises(ValueError): + svg_xml.parse_svg(text.encode('utf-8')) + + def test_reader_never_requests_unbounded_input(self): + class BoundedReader(io.BytesIO): + requested = None + + def read(self, size=-1): + self.requested = size + if size < 0 or size > svg_xml.MAX_SVG_BYTES: + raise AssertionError('unbounded read') + return super().read(size) + + stream = BoundedReader(b' ' * (svg_xml.MAX_SVG_BYTES * 2)) + with patch.object(Path, 'open', return_value=stream): + with patch.object(svg_xml.ElementTree, 'fromstring') as parser: + with self.assertRaises(ValueError): + svg_xml.read_svg(Path('oversized.svg')) + parser.assert_not_called() + self.assertEqual(stream.requested, svg_xml.MAX_SVG_BYTES) + + def test_file_reader_uses_same_hardened_parser(self): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / 'fixture.svg' + raw = b'ordinary' + path.write_bytes(raw) + read, root = svg_xml.read_svg(path) + self.assertEqual(read, raw) + self.assertEqual(root.find('title').text, 'ordinary') + path.write_bytes(b'') + with self.assertRaises(DTDForbidden): + svg_xml.read_svg(path) + + +if __name__ == '__main__': + unittest.main() diff --git a/scripts/verify-readme-visuals.py b/scripts/verify-readme-visuals.py new file mode 100644 index 0000000000..7431903f8e --- /dev/null +++ b/scripts/verify-readme-visuals.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +"""Verify the generated SVGs and GitHub-compatible image link wrappers.""" +from pathlib import Path +import json,re +from urllib.parse import urlparse +from readme_visuals_xml import read_svg,resolve_repo_file +ROOT=Path(__file__).resolve().parents[1];NS='{http://www.w3.org/2000/svg}' +manifest=json.loads((ROOT/'assets/readme/manifest.json').read_text(encoding='utf-8'));readme=(ROOT/'README.md').read_text(encoding='utf-8');total=0 +for asset in manifest['assets']: + file=resolve_repo_file(ROOT,asset['file'],asset=True);data,root=read_svg(file);raw=data.decode('utf-8');total+=len(data) + assert root.tag==NS+'svg' and root.get('viewBox'),asset['file'] + assert root.find(NS+'title') is not None and root.find(NS+'desc') is not None + ids=[e.get('id') for e in root.iter() if e.get('id')];assert len(ids)==len(set(ids)) + assert 'prefers-reduced-motion:reduce' in raw and '@keyframes' in raw + for e in root.iter(): + assert e.tag not in [NS+'script',NS+'foreignObject',NS+'image'],asset['file'] + assert all(not k.lower().startswith('on') for k in e.attrib) + assert not any(k.endswith('href') for k in e.attrib),'Links belong to README wrappers' + assert not re.search(r'url\((?!#)',raw),'Remote dependency' + target=asset['target'];parsed=urlparse(target) + if parsed.scheme: + assert parsed.scheme=='https' and parsed.hostname in ['cognitum.one','huggingface.co'] + else:resolve_repo_file(ROOT,target.split('#')[0]) + escaped=re.escape(asset['file']);destination=re.escape(target) + linked=bool(re.search(r'\[!\[[^\]]*\]\('+escaped+r'\)\]\('+destination+r'\)',readme)) + linked=linked or bool(re.search(r'\s*