Steps 5-6 of game detection. BREAKING wire change — bump everyone.
Wire (step 5):
- PeerState.game: Option<String> (display label only — never appid/source).
- SelfPresence.game + to_state carry it (single self-state builder).
- GOSSIP_PROTO 2->3, GOSSIP_SIG_DOMAIN v3, version comment bumped together;
Cargo MINOR 0.3.0 -> 0.4.0 per VERSIONING.md. v2/v3 isolate into
different topics + signature domains, so a coordinated redeploy is
required (same as the W4 avatar bump).
- Gossip ingest sanitizes incoming game via sanitize_game_label (bidi/
control strip, 64-char/256-byte cap); empty -> None.
- Bonus security fix (Codex find): reject inbound gossip frames over a
128KB cap BEFORE serde_json::from_slice — a legit Announce with a full
48KB avatar is ~49KB, so this bounds allocation abuse with headroom.
Core wiring:
- Spawns the detector at startup; consumes its watch channel in the main
select. Detection runs continuously (for the local background); the
broadcast is gated by game_presence_enabled (opt-in, default OFF).
New commands: SetGamePresenceEnabled (immediate publish/clear, D8),
SetGameOverride, SetGameProcessMap. New event: GameChanged.
- game_presence_label sanitizes the outgoing label too.
Background switch (step 6):
- GUI handles GameChanged: stores current_game, swaps background to the
per-game override (config.game_backgrounds[id]) or falls back to the
W16 default; reuses the existing cached-handle path (no redraw flicker).
397 lib tests (all green), clippy --all-targets clean, full binary builds.
Remaining: step 7 UI (opt-in toggle, roster 'Playing' text, manual
override control, Settings game-backgrounds + process-map editors).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rooms can now be named. A "Room name (optional)" field on the home Create card
mints a ticket carrying the label; PeerSpeakTicket gains a #[serde(default)]
`name` field (backward/forward compatible — serde ignores unknown fields and
defaults missing ones, so old/new builds still interoperate, just without
labels). restamp preserves the label so member-issued doors keep it; new
label_of helper reads it. Every member (creator or joiner) sets current_room.name
from the ticket, so presence reports a consistent "in <name>" to friends, and the
room-screen header shows the label under the wordmark. Labels are sanitized via
sanitize_name on both mint and display (untrusted peer-supplied ticket).
CoreCommand::Join gains room_name (used only when creating). +2 ticket tests
(label round-trip through restamp/label_of, pre-label backward-compat). clippy
--all-targets clean, 257 lib tests green.
Pure seam unit-tested + home field screenshot-verified; the in-room header label
and friend-side "in HangOut" presence display need a live/2-machine confirm.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds bundled preset avatars on top of the monogram foundation.
- New `Avatar` enum (Monogram | Preset(u8)) in the avatar module, serde-encoded;
6 preset PNGs embedded via include_bytes! (placeholder art in assets/avatars/
— swap for real designs later). +3 unit tests.
- `PeerState` gains `avatar` (rides the gossip presence plane like `sharing`);
`AppConfig` gains `avatar` (persisted). Avatar choice flows app → core (Join +
new SetAvatar command) → every self-state announce, so it reaches the room incl.
late joiners, and changing it mid-call re-announces live.
- Settings "Avatar" section: monogram + 6 preset tiles, applied live + persisted.
- Rendering: `avatar_view` draws the chosen preset image (iced `image` feature,
now enabled) else the monogram, in the self card, peer rows, and chat (chat
looks up the sender's avatar from presence by id).
⚠️ BREAKING gossip wire change: presence Announce is signed (S2) and the
signature is recomputed by re-serializing the parsed struct, so a new PeerState
field means old and new builds can't verify each other's presence — ALL peers
must run a build >= this one (same as the S2 change). Redeploy dopedart before
2-machine testing.
Build + clippy clean, 222 lib + integration tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The gossip bootstrap list was derived solely from the ticket: a client
dialed the host, but "we are the host" produced an EMPTY list. So when the
room CREATOR rejoined their own room (their ticket names themselves as host)
they dialed nobody and never re-entered the swarm — the remaining peer stayed
stuck until it too left and rejoined. (First 2-human field test, 2026-06-14;
user-confirmed call-breaking, P1.)
Fix: retain the peers seen in the current room across leave (core
`known_peers`, updated by the event task; reset only when the joined ticket
changes) and pass them to `RoomState::join` as extra bootstrap targets. The
new pure `compute_bootstrap` seam unions the ticket host + retained peers,
drops self, and de-dups; address resolution rides the persistent lookup.
- `RoomState::join` gains `extra_bootstrap: Vec<EndpointAddr>` (test_net
callers pass vec![]).
- +4 unit tests on `compute_bootstrap`, incl. the host-rejoin regression case.
Tests-green (208 lib + 6 + 4 integration), clippy clean. NOT yet 2-machine
field-verified — needs a live host leave→rejoin call.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The gossip `author` field was self-asserted, so an in-room member could forge
it to impersonate (chat), force-evict (Leave), or poison presence + the address
book (Announce). Now every GossipPayload is signed with the node's ed25519
secret key and verified on receipt; a forged author can't validate because the
attacker lacks the victim's key.
Done as the complete fix (forgery + replay):
- GossipPayload gains `ts` (sender-stamped) + `sig` (iroh::Signature, serde-64B);
Debug is hand-written since Signature has none.
- Signature covers domain tag + room topic_id + author + ts + msg
(`signable_bytes`): topic binding blocks cross-room replay, ts + a 2-min
freshness window block temporal replay (within-window replays are byte-
identical and de-duped by the swarm), author binding makes spoofing fail.
- New pure seams `sign_gossip` / `verify_gossip` (+ `GossipReject`); all four
outgoing broadcasts sign, the receive loop verifies-then-trusts (drops
unauthenticated/stale before any peer-map / event / address-book action).
- IrohGossipState now holds the node SecretKey + active topic bytes; callers
(core, test_net) updated.
NOTE: breaking gossip wire change — all peers must run this build (the staged
friend release + dopedart need rebuild). Node identity is ephemeral, so no
migration concern beyond rebuild.
+5 unit tests (genuine accept; forged author, tampered msg, cross-room, stale/
future all rejected). 163 lib tests (was 158), clippy --all-targets clean.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Surface pixelpass screen-sharing from inside a peerspeak room. peerspeak owns
voice, pixelpass owns pixels — they're never Cargo deps of each other; the
contract is pixelpass's CLI flags + its `--output json` stdout stream.
Modelled on Discord: multiple simultaneous sharers, a 🔴 Live badge + 👁 Watch
on each sharing peer's card, and in-progress shares visible to late joiners.
- New `src/screenshare` module: pure `parse_pixelpass_event` seam + `pixelpass_path`
discovery (13 unit tests), async `spawn_host` (→ ticket) and `spawn_viewer`
(→ parse connected{url} → open mpv, vlc fallback). No new deps.
- Sharing rides presence: `PeerState.sharing: Option<ticket>` (serde-defaulted),
so the existing gossip re-announce delivers the offer to late joiners for free
and a PeerUpdated fires on start/stop — no separate gossip message needed.
- core: Start/Stop/ViewShare commands; host + viewer children tracked in the
session, killed on stop/leave (kill_on_drop backstop). Viewer limit left to
pixelpass's bandwidth-measured cap.
- UI: Share/Stop button (graceful "needs pixelpass" disabled state), Live badge
+ Watch on peer cards, Sharing badge on the self card. Verified by screenshot.
- config: optional `pixelpass_path` override (hand-editable).
Tests-green; the 2-machine gossip/remote path is not yet field-verified.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Remove Gemini's committed update_*.py regex-surgery scripts
- Drop unused iroh-tickets dependency (hand-rolled ticket is used instead)
- Replace ToString antipattern with Display impl on PeerSpeakTicket
- Route debug log to XDG state/cache dir instead of hardcoded /home path
- Clear all compiler + clippy warnings (unused imports, collapsible ifs,
redundant pattern matching, missing Default)
Builds clean with zero warnings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Fix tokio runtime panic by spawning a dedicated Tokio runtime thread in CoreController.
- Add central log_msg utility in src/lib.rs for debugging.
- Add instrumentation/logs to join, leave, and gossip events in src/network/gossip.rs.
- Add test_net.rs bin for testing gossip loopback sync.
- Use std::sync::Mutex in IrohGossipState to resolve Tokio block-in-async panics.