reportPath used to print 'you A:B ↔ peer C:D' in path-status and the log, exposing both participants' real IP addresses to anyone glancing at the screen. Show DIRECT vs RELAYED and the candidate types (host/srflx/relay) only; addresses are gone. CLAUDE.md gains a 'Web UI privacy — never display peer IPs' rule so this does not regress.
6.9 KiB
Agent Blackops
This repo is operated by agent blackops — ml agent for fox/timehexon on the unsandbox/unturf/permacomputer platform.
Identity
Full shard: ~/git/unsandbox.com/blackops/BLACKOPS.md
Rules
- I propose, fox decides. Unsure = ask. Can't ask = stop.
- No autonomous ops decisions. No destructive commands without explicit instruction.
- Fail-closed. Cleanup crew, not demolition.
- Check the time every session. Gaps are information.
- DRY in context — single source of truth, no sprawl.
- Never say "AI" — always say "machine learning."
- Prefer "defect" over "bug."
Orientation
date -u
pwd
git log --oneline -5
git status
Then ask fox what the mission is.
Zebra Report System
Concept: covert bidirectional communication channel using browser tab volume as the modulation medium — dial-up modem principles, userland only, no kernel involvement, no network stack.
Collaborators & Stakeholders
| Handle | Role |
|---|---|
| foxhop | fox — handler, operator, TimeHexOn |
| brackishbert | collaborator |
| SEW | collaborator |
| russell@unturf | Russell Ballestrini — unturf founder, permacomputer manifesto, ago library |
| TimeHexOn | oracle platform — primary deployment target |
| groupr | related project |
How it works
PulseAudio exposes each browser tab as a separate sink input, visible and controllable in pavucontrol. Volume is settable per-tab in userland with no kernel involvement. Each tab has a range of 0–100 (101 discrete levels — 101 dalmatians).
By modulating volume at a consistent rate (bauds), two sides can exchange data:
- transmitter: steps volume through values at a fixed clock rate
- receiver: reads volume at the same clock rate, decodes the steps back to data
- bidirectional: two tabs (or two processes watching different tabs) run opposite directions simultaneously
Signal space
- 101 levels = ~6.66 bits per symbol
- practical: use power-of-2 subsets — 2 levels (1 bit), 4 levels (2 bits), 64 levels (6 bits)
- higher symbol depth trades noise margin for throughput
- low baud rate = high reliability, low throughput (like 300 baud dialup)
- high baud rate = races PulseAudio update latency
- measured ceiling on neoblanka: ~1000–1200 baud (PA IPC ~350–400µs avg)
Binaries
| Binary | Description |
|---|---|
tx |
transmitter — reads stdin, modulates tab volume |
rx |
receiver — reads tab volume, writes decoded bytes to stdout |
chat |
bidirectional chat — two tabs, two threads |
bt |
Battle Toads — stereo dual-channel, 2x bandwidth |
Project Battle Toads
One stereo browser tab carries two independent UART streams simultaneously — L channel and R channel. PulseAudio's pa_cvolume is per-channel; a single get_sink_input_info call returns both L and R volumes.
- TX sets L and R to independent bit values each symbol
- RX decodes L and R from a single PA poll — no extra IPC cost
- Net: 2x throughput at same baud rate, same PA polling budget
- Web carrier upgraded to stereo: two oscillators (440Hz L, 441Hz R) merged into a stereo stream → PA sees
channels=2
# After opening web/index.html and clicking 'start audio' (stereo tab):
./bt -T MY_SINK -R THEIR_SINK -b 500
Auto-negotiate (handshake protocol)
RX benchmarks its own PA polling speed and signals the max safe baud to TX. No manual baud matching needed.
./rx -s RX_SINK -t TX_SINK # RX benchmarks, sends offer at 50 baud
./tx -s TX_SINK -r RX_SINK # TX listens for offer, locks to RX's rate
Handshake frame: [0x5A 0x42 0x01 baud_lo baud_hi xor_cksum] — 6 bytes at 50 baud (~1.2s).
Known defect: 3-way handshake not yet implemented. TX can fire before RX enters receive loop at high baud rates. Fix: RX-ready signal back to TX before data phase.
Tools
pactl set-sink-input-volume— set volume by sink-input indexpactl list sink-inputs— enumerate tabs, read current volumepavucontrol— visual verification of modulation./tx -l— list all PA sink inputs with index, volume, channels- sink-input index maps to tab; stable within a session
Use cases
- agent-to-agent signaling without touching the filesystem or network stack
- side-channel between sandboxed browser tab and host process
- low-bandwidth status heartbeat (alive/dead/mode) at ~1–10 baud
- covert channel for oracle↔host communication on TimeHexOn
Constraints
- sink-input index resets when tab navigates or crashes — handshake needed on reconnect
- PA polling latency sets the baud ceiling — benchmark with
./rx -s SINK -t SINK2before sending - stereo (channels=2) required for Battle Toads — open web/index.html, click 'start audio'
- userland only — survives without root
- Operation Voyeur: all terminal output is public — never pass secrets through these channels unencrypted. The web page does ECDH key exchange + AES-256-GCM before TX.
Web UI privacy — never display peer IPs
chat.html and zebra-audio.html must never print peer IP addresses or ports
in the page UI or in any visible log. Our users do not run Wireshark — if it
is not on the screen, peers cannot dox each other. Candidate types
(host/srflx/relay) from pc.getStats() are abstract and fine to show
(they tell you direct vs relayed); loc.address / loc.port /
rem.address / rem.port are not. The WebRTC stack already obfuscates host
candidates via mDNS by default — do not undo that work in the UI.
Web page integrity stamping
Each deployed page (web/chat.html, web/zebra-audio.html, web/how-it-works.html,
web/host-your-own.html) carries a footer with the build date + its own MD5 + SHA-256.
Run make stamp before deploying any page change — it sets today's date and
recomputes the hashes (web/stamp.js).
- A file can't hold its own hash, so the hashes are computed with the two hash
fields zeroed, then written back (same length). Self-consistent and
idempotent: re-running
make stampgives identical hashes unless the content changed. - The stamp is static HTML written at build time — no JavaScript computes or
injects it in the browser.
stamp.jsis build tooling, never loaded by a page. - Verify a served page: blank the md5 field to 32 zeros and the sha256 field
to 64 zeros, then re-hash with
sha256sum/md5sum— must match the footer. - Deploy = push to BOTH repos. A page change is not live until it lands in
both. Pushing only the source changes nothing served; pushing only the deploy
repo orphans the source of truth. Both, every time:
- source — this repo (
zebra-report): editweb/*.html→make stamp→ commit → push to origin. - served —
www.unturf.com: copy the page(s) into~/git/www.unturf.com/zebra-report/(chat.html→index.html;zebra-audio.html,how-it-works.html,host-your-own.htmlkeep their names) → commit → push to origin.
- source — this repo (