Security audit finding EXP2-SEC-04. `validate_game_id` in the wire
protocol only enforced non-blank and a 255-byte maximum, so a request
such as `StreamInstall { game_id: "../../x" }` decoded successfully and
was passed on to the transfer layer. Every consumer resolves the ID
against the local catalog before touching the filesystem, so this was
not exploitable, but the protocol boundary is the right place to state
what a game ID is: the name of one catalog directory.
Requests and library snapshots now fail validation when the game ID
contains `/`, `\`, any Unicode control character (including NUL), or is
exactly `.` or `..`. A new `ControlValidationError::InvalidPathComponent`
variant reports the rejection. Embedded dots such as "game..v1" remain
valid because the catalog validators accept them.
Test plan: `just test` exercises the rejected forms plus an accepted ID
with embedded dots.
Claude-Session: https://claude.ai/code/session_017C3Nbgwpdm3YNwZhhFLHwg
Security audit finding NET-02. A discovered `_lanspread._udp` record
was accepted as a QUIC candidate as soon as it carried the current
protocol version and a peer_id TXT entry; the resolved socket address
itself was never inspected. Anyone on the LAN can publish mDNS records,
so a forged advertisement could point every peer's handshake attempt at
a multicast group (224.0.0.251), the broadcast address, 0.0.0.0/::, or
port 0. That wastes a discovery slot per record and sprays QUIC Initial
packets onto addresses no peer can ever answer from.
`validated_candidate_endpoint` now rejects unspecified, multicast and
IPv4 broadcast addresses as well as port 0 before the candidate enters
the negotiation set. Loopback and link-local addresses stay admissible
because single-host and DHCP-less LAN setups legitimately use them.
Test plan: `just test` (new unit tests cover the address filter and the
endpoint validator). Manually: start two peer-cli containers; discovery
still works with real interface addresses.
Claude-Session: https://claude.ai/code/session_017C3Nbgwpdm3YNwZhhFLHwg
Security audit finding SEC-IPC-03. The thumbnail IPC command resolved
`assets/{game_id}.jpg` from the resource directory without checking the
ID, unlike every other command that maps a game ID to a path. A crafted
ID containing path separators could therefore read any `.jpg` reachable
from the resource root. The handler also still carried a `dbg!` that
printed the resolved path to stderr in release builds.
The command now rejects anything that is not a single normal path
component with an `InvalidInput` I/O error, using the same
`is_single_component_game_id` gate as run_game and start_server. The
frontend already treats a failed thumbnail request as "no thumbnail".
Test plan: `just clippy`, `just test`. In the app, thumbnails for
catalog games still load; an invoke with game_id "../x" is rejected.
Claude-Session: https://claude.ai/code/session_017C3Nbgwpdm3YNwZhhFLHwg
Game-directory selection could reach update_game_directory and then fail before
peer startup, while the frontend only logged the rejected invoke. Preserve the
last accepted root until peer acknowledgement, surface backend rejection in
the settings and main-window UI, and avoid holding the published control lock
across runtime replies. Startup preflight remains fail-closed; synchronous
setup stays lexically owned through scoped_blocking so cancellation cannot
strand a partially published runtime.
Add peer-cli scenario S50 to verify invalid changes preserve the existing
library and valid changes acknowledge and refresh the library in both
directions. Modernize the fixed-size hex decoders to satisfy the current
workspace Clippy lint without changing their behavior.
Test Plan:
- `just fmt` -- passed
- `just clippy` -- passed
- `just test` -- passed
- `just frontend-test` -- passed
- `deno task build` -- passed
- `just peer-cli-tests S50` -- passed
- `just build` -- passed
- `git diff --cached --check` -- passed
Clarify that `just build` is the production no-bundle launcher build, while
fixture builds are explicit. Document package-directory generation, the
`LANSPREAD_GAMES_DIR` and `--set GAMES_DIR` default-run forms, the metadata
cache behavior, and the force-refresh override so the catalogue workflow is
usable without consulting the recipe implementation.
Test Plan:
- `prettier --check --prose-wrap always --print-width 80 README.md CLAUDE.md` -- passed
- `just --fmt --check` -- passed
- `git diff --cached --check` -- passed
The default GUI build previously selected the peer-CLI fixture catalog, which
made a successful no-bundle build unsuitable as a launcher built from real
packages. Route `build` through the production resource map and production
profile, retaining `build-fixture` for test-only GUI builds. Add explicit
production run/build recipes, generate the catalog before a package-dir build,
and let the default `run` route through that workflow when
`LANSPREAD_GAMES_DIR` or a Just variable override is supplied. Integrate the
metadata cache so unchanged `--all` generation is skipped, with
`LANSPREAD_CATALOG_FORCE=1` available for an explicit refresh.
The pre-existing release-mode change in the catalog recipe remains unstaged
and is intentionally not part of this commit.
Test Plan:
- `just --fmt --check` -- passed
- `just --dry-run build-production /srv/games` -- passed
- `just --dry-run catalog-generate-production /srv/games` -- passed
- `git diff --cached --check` -- passed
Complete production catalog generation hashes every package twice, which is
necessary for publication but wasteful when the same package tree has already
produced the current catalog. Add a metadata-only cache that records package
paths, sizes, nanosecond mtimes, catalog and unrar metadata, and output
identity. The cache is advisory: incomplete outputs never hit, and production
build validation remains the authority. Record files atomically under the
ignored local cache directory, with focused tests for hits, source changes,
and incomplete output.
Test Plan:
- `python3 -m unittest discover -s tools -p 'test_*.py'` -- passed
- `git diff --cached --check` -- passed
Catalog generation and peer install/download validation rejected any path component containing a tilde followed by digits, even when the component was a valid long filename such as Bosons TD Gold~1.w3m. Remove the heuristic from all three validators and retain the existing device-name and portable-alias checks. Add a regression test for the literal filename so catalog publication and later path validation agree.
Test Plan:
- just clippy (passed)
- just test (passed)
- git diff --check (passed)
Production catalog generation previously prepared every game serially and computed transfer-chunk hashes for extracted .eti files even though those hashes were discarded. Enable Rayon-backed BLAKE3 hashing, process selected games through the shared available-CPU pool, and hash large read blocks with update_rayon. Extracted outputs now use a whole-file-only hash path. Manifest collection remains deterministic and the independent second verification pass is preserved.
Test Plan:
- `just clippy` -- passed
- `just test` -- passed
Full catalog generation previously prepared each game sequentially, so a
version mismatch late in the catalog could surface only after earlier packages
had already been extracted and hashed. Validate every selected version.ini
against game.db before starting manifest preparation, while retaining the
per-package validation during preparation to detect later input changes.
Document the ordering and cover it with a regression where an earlier archive
would fail if archive processing began before a later version mismatch.
Test Plan:
- `just fmt` -- passed
- `just test` -- passed
- `just clippy` -- passed
- `git diff --cached --check` -- passed
Add small Justfile entry points for generating the complete production
catalog authority or safely regenerating one game. Keep fixture validation
separate and allow operators to select a trusted unrar executable.
Document the complete package layout, BLAKE3 and ContentId model,
full/incremental publication procedure, verification and bundle gates, and
peer-CLI workflows. Record the fresh final local acceptance run without
presenting Docker throughput as physical-LAN evidence.
Test Plan:
- just fixture-catalogs-check
- just test
- just clippy
- just frontend-test
- just build
- LANSPREAD_S37_MIN_MIB_PER_S=100 just peer-cli-tests
- just catalog-check-production (expected fail-closed: production manifests absent)
- deno fmt --check README.md organize/testing/PEER_CLI_SCENARIOS.md
- rumdl check --flavor commonmark README.md organize/testing/PEER_CLI_SCENARIOS.md
- just --fmt --check
- git diff --check
Keep the catalog authority safeguards while reducing task-runner duplication.
GUI run and build commands now validate only the default fixture profile they
actually package; the peer-CLI image and matrix retain the aggregate check for
default, solid, multi-archive, and unknown-game profiles.
Remove publisher-only format, lint, and test wrappers because the workspace
recipes already cover the same crate and targets.
Test Plan:
- `just --fmt --check` -- passed
- `just fixture-catalogs-check` -- passed
- `just test` -- passed
- `just clippy` -- passed
- `just frontend-test` -- passed, 91/91
- `just build` -- passed
- `git diff --cached --check` -- passed
Record the completed protocol-8 implementation, its security and lifecycle
decisions, and the final local acceptance evidence. Mark protocol-7 Call to
Play relay reviews as historical so they cannot be mistaken for current design.
Keep production acceptance honest by recording the unavailable canonical
186-game manifest corpus, real Windows/NTFS confinement and durability proof,
and representative physical-LAN evidence as external release prerequisites.
Test Plan:
- `just fmt` (passed)
- `just test` (passed; 708 workspace tests, including peer 480 and Tauri 56)
- `just clippy` (passed)
- `just frontend-test` (passed; 91/91)
- `just build` (passed; fixture-backed no-bundle build)
- `LANSPREAD_S37_MIN_MIB_PER_S=100 just peer-cli-tests` (passed; S1-S49)
- `git diff --cached --check` (passed)
Update user and developer documentation for the protocol-8 system: persistent
SPKI-derived identities, exact catalog ContentId authority, pinned responder
pulls, structured runtime ownership, direct-author Call to Play, and the global
local-network sharing switch.
Remove active descriptions of repository-wide certificates, pushed deltas,
relayed histories, and metadata consensus. Keep operational and UI boundaries
aligned with the implementation, including the fail-closed production catalog
gate.
Test Plan:
- `just fmt` (passed)
- `git diff --cached --check` (passed)
Replace legacy metadata and relay expectations with current protocol-8 JSONL
assertions. Scenarios now bind every source to authenticated PeerId and exact
ContentId, prove typed attempt lifecycle order, and fence cancellation,
quarantine, rollback, republishing, and peer-departure outcomes against vacuous
success.
Isolated topologies distinguish direct author pulls from relay and ambient mDNS
substitution. The run log records focused diagnostics and the final fresh-image
S1-S49 acceptance result without presenting Docker-host throughput as a
representative external-LAN measurement.
Test Plan:
- `LANSPREAD_S37_MIN_MIB_PER_S=100 just peer-cli-tests` -- passed S1-S49
- S37 -- passed 2,147,483,656 bytes in 17 chunks at 551.10 MiB/s
- `python3 -m py_compile crates/lanspread-peer-cli/scripts/run_extended_scenarios.py` -- passed
- `ruff check --select F,E9 crates/lanspread-peer-cli/scripts/run_extended_scenarios.py` -- passed
- `git diff --cached --check` -- passed
Add a durable, acknowledged Local network sharing switch with fail-closed
hydration, serialized mutation, and redacted ephemeral-identity diagnostics.
Keep local Call-to-Play state available while gating every network action on the
effective sharing generation.
Render revisioned verification, invalid-source retry, and sticky source
exhaustion states. Preserve opaque attempt IDs through progress delivery so
out-of-order webview events cannot attach stale bytes to a successor transfer,
and keep terminal exhaustion visible after the last source departs.
Own listeners, native invokes, persistence, dialogs, and companion-window
creation through webview close. Late creation is settled and cleaned before the
parent realm is destroyed.
Test Plan:
- `just frontend-test` -- passed (91/91)
- `just build` -- passed with TypeScript, Vite, and release Tauri compilation
- `just test` -- passed on the completed stack (708 workspace tests)
- `just clippy` -- passed on the completed stack
- `git diff --cached --check` -- passed
Replace address-only trust and pushed peer state with installation identities,
SPKI-pinned QUIC, candidate-only discovery, and bounded responder-owned
protocol-8 pulls. The runtime now owns each network generation and all admitted
work through shutdown.
Add exact bundled content identities, reproducible manifest publishing,
capability-confined downloads, streaming BLAKE3 verification, quarantine and
retry, and crash-recoverable download and install transactions. Ship generated
fixture catalogs and fail closed when production manifests are absent.
The Tauri backend exposes durable sharing policy, redacted identity state, and
attempt-keyed transfer snapshots. Frontend consumption follows in the next
commit. Repository-wide test certificates and protocol-7 paths are removed.
BREAKING CHANGE: peers must use protocol 8 and exact catalog content artifacts;
protocol-7 frames and shared-certificate identities are no longer accepted.
Test Plan:
- `just test` -- passed on the completed stack (708 workspace tests)
- `just clippy` -- passed on the completed stack
- `just build` -- passed with fixture catalogs on the completed stack
- `just catalog-check-production` -- failed closed because the external
production manifest corpus is absent
- `git diff --cached --check` -- passed
Treat directory/file shape as part of each peer manifest vote and reject
portable aliases before aggregating descriptors. This keeps majority selection
deterministic and avoids collapsing conflicting entries that share a path or
size.
This preserves the previously staged consensus hardening before the protocol-8
catalog-authority cutover layered in the working tree.
Test Plan:
- `git diff --cached --check` -- passed
Treat cancellation as part of the Stream Install transport contract. Frame
production and QUIC egress now run as structured futures, cancellation wins at
queued sends, blocked writes, and close, and exceptional exits reset the send
stream before producer cleanup completes.
Make the unrar listing subprocess cancellation-aware and explicitly kill and
reap it on cancellation or pipe-capture failure. This ensures outbound transfer
tracking is cleared only after provider work is quiescent, which is required by
game-root mutation and directory-switch draining.
Test Plan:
- `just clippy` -- passed
- `just test` -- passed (242 lanspread-peer tests)
- `just peer-cli-build` -- passed
- `git diff --cached --check` -- passed
Require catalog game IDs and every remotely described path component to use
Unicode NFC before any download transaction begins. This prevents canonically
equivalent spellings from bypassing portable alias checks on filesystems that
normalize names, while preserving the protocol's exact path spelling.
Cover accepted NFC names and both game-ID and nested-component rejection with
zero-mutation tree snapshots.
Test Plan:
- `just clippy` -- passed
- `just test` -- passed (242 lanspread-peer tests)
- `git diff --cached --check` -- passed
Keep initial peer transfers, retry attempts, and chunk receivers structurally owned until they quiesce. Cancellation now stops opening new streams, flushes accepted file writes, and drains active work before ownership rollback can begin.
Leave operation admission owned by the running download task when liveness detects that every source disappeared, and emit the peers-gone notification only once.
Test Plan:
- just fmt (Rust and configured formatters completed; 39 pre-existing rumdl findings remain)
- just clippy
- just test
- git diff --cached --check
Validate portable aliases across committed and pending ownership sets when loading persisted records. Malformed state can no longer make recovery delete the pending file through an older case-only spelling.
Test Plan:
- just clippy
- just test
- just fmt (Rust, TOML, and Prettier completed; rumdl still reports 39 pre-existing issues)
Reject exact manifest destinations that are not covered by the last committed ownership set before creating a baseline or parking version.ini. Align Windows device-name validation with the confined filesystem backend and keep cleanup capability-relative.
Replace recursive downloaded-game removal with an empty ownership generation. The operation now removes only proven-owned files and the sentinel, preserves unknown files and directories, and remains recoverable and idempotent across crashes.
Test Plan:
- just clippy
- just test
- just fmt (Rust, TOML, and Prettier completed; rumdl still reports 39 pre-existing issues)
Remote manifests were validated before mutation, but preparation, chunk writes,
sentinel transactions, and ownership recovery later reopened ambient paths. A
link or reparse-point swap between those steps could redirect a mutation outside
the validated game root.
Introduce a retained ConfinedGameRoot capability backed by cap-primitives. Carry
typed validated destinations into chunk plans, walk every component without
following links, and perform payload, sentinel, stale-file, abort, and recovery
mutations relative to the retained handle. File writes and verification use the
same opened handle, while final durability syncs payload files and unique parent
directories before committing version.ini.
Make ownership-record publication phase-aware as well. A directory-sync failure
after record rename now stops before payload mutation without performing an
unsafe old-sentinel rollback. Record the capability-root, bounded-handle,
hard-link, and unproven Windows durability tradeoffs in the decision log.
Test Plan:
- `just clippy` -- passed
- `just test` -- passed; 185 peer tests and the full workspace are green
- `just fmt` -- Rust, TOML, and Prettier completed; command remains nonzero on
39 pre-existing rumdl issues outside this change
- `git diff --cached --check` -- passed
Track the exact regular files owned by each completed and in-flight peer
download instead of sweeping every non-reserved path after cancellation. Bind
the record to the canonical games directory, publish pending ownership before
payload mutation, and use the final version.ini rename as the recovery commit
point.
Make replacement, cancellation, and startup recovery preserve unknown files
and install state while removing stale or partial downloader-owned bytes. Add a
new-format baseline so legacy discarded sentinels cannot make partially
modified payloads ready, sync payload and journal state in transaction order,
and serialize startup recovery against operation admission.
Document ambiguous legacy target adoption, portable alias transitions, and the
other ownership tradeoffs in the refactor decision log.
Test Plan:
- `just clippy` -- passed
- `just test` -- passed (182 peer-core tests plus the full workspace)
- `just fmt` -- Rust, TOML, and Prettier formatting completed; the command then
stopped on 40 pre-existing rumdl findings in unrelated Markdown content
- `git diff --cached --check` -- passed
Game scanning, manifest validation, install recovery, migration, and download
cleanup each carried their own spellings and case rules for reserved entries.
Those copies had already diverged, which made it possible for one subsystem to
accept or expose a path that another treated as application-owned state.
Introduce one game_paths module for the canonical names and conservative
portable comparison policy. Keep context-specific predicates for manifest and
scanner protection versus cancellation preservation: cancellation still sweeps
its own version transaction scratch files, while install and migration state
survive. Reuse the constants for all production path construction sites.
Test Plan:
- `just test` -- passed (175 lanspread-peer tests and full workspace)
- `just clippy` -- passed
- `just fmt` -- Rust, TOML, and Prettier completed; the recipe remains blocked
by 39 pre-existing rumdl issues in five unrelated Markdown files
- `git diff --cached --check` -- passed
Why:
- Remote and UI-echoed file descriptions could reach transaction and storage
code one entry at a time, so a hostile late path could mutate earlier files.
- Per-file consensus also accepted malformed peer lists and let duplicate rows
inflate a source's vote.
What:
- Add a complete protocol-7 manifest adapter with catalog-root confinement,
portable path and alias rules, reserved-path protection, shape and size caps,
symlink/reparse inspection, and zero-mutation tests.
- Keep download selection in the peer core, validate every peer manifest before
consensus, and pass only the validated manifest into storage/orchestration.
- Canonicalize locally advertised paths, cap exact chunk receives, and preserve
the local-only install fast path.
- Record the chosen safety limits and follow-up ownership/catalog decisions.
Test Plan:
- just clippy
- just test
- just frontend-test
- just build
- just fmt (Rust/TOML/Prettier completed; rumdl reports 39 pre-existing issues)
- git diff --cached --check
Replace the earlier peer-authentication proposal with the reviewed,
implementation-oriented design. The plan now records the identity-storage
state machine, pinned TLS and endpoint rules, signed Call-to-Play objects,
download-source authorization, resource limits, safe protocol phases, and
phase-owned acceptance gates.
Remove the standalone review after incorporating its findings and follow-up
adjudication into the authoritative plan, including an explicit closure matrix.
This avoids maintaining two documents with conflicting severity and guidance.
Test Plan:
- `git diff --cached --check` -- passed
- Code tests not run; documentation-only change
Treat an unavailable actor ID or an explicit not-ready result as normal peer
startup and tell the user that LAN connection is still in progress. Clear that
message when snapshot registration succeeds.
Map obsolete, missing-history, and active-history-limit store failures to
distinct guidance without marking a healthy transport unavailable. Preserve a
generic message for unexpected publish failures.
Test Plan:
- just frontend-test
- just build
- git diff --cached --check
Pass the effective nomination deadline into the Add time action and extend
from whichever is later: that deadline or the current time. This preserves
remaining time when a call becomes ready early while still giving an overdue
call a fresh five-minute window.
Test Plan:
- just fmt
- just frontend-test
- just build
- git diff --cached --check
Keep complete running and cancelled histories visible for fifteen minutes so
peers retain the roster, chat, and outcome long enough to understand what
happened. Compact them to terminal tombstones afterward without charging
settled calls against the active-history limit.
Model running and cancelled as durable read-only frontend states, exclude
them from active badges, prune retired raw events, and document the lifecycle.
Add peer scenario S49 to prove a late joiner reconstructs a terminal call with
its roster and chat intact.
Test Plan:
- just fmt
- just clippy
- just test
- just frontend-test
- just build
- just peer-cli-tests S48 S49
- python3 -m py_compile crates/lanspread-peer-cli/scripts/run_extended_scenarios.py
- git diff --cached --check
Raise the wire protocol to version 7 and add explicit Call to Play delivery
outcomes. Live requests now wait for an application acknowledgement, allowing
the sender to distinguish applied, duplicate, obsolete, incomplete, and
rejected updates instead of treating a successful write as acceptance.
Remove source-IP equality from actor verification. The receiver now requires
the envelope peer ID to be present in its known roster and requires every live
event actor to match that envelope. This matches the cooperative-LAN trust
model without misrepresenting the shared TLS identity as per-peer
authentication.
Transport failures, malformed responses, NeedHandshake, and NeedHistory each
trigger one asynchronous Hello/HelloAck resync. Rejections are logged without
retry, and local publication remains independent of remote availability.
Test Plan:
- `just fmt` -- passed
- `just clippy` -- passed
- `just test` -- passed
- `git diff --cached --check` -- passed
Replace per-event insertion with a transactional batch merge. The store now
validates and deduplicates an entire history before committing it, rejects
conflicting event IDs without partial mutation, evaluates retention after all
batch events are present, and reports applied, duplicate, obsolete, and
missing-root outcomes explicitly.
Derive event identity from retained history instead of preserving an unbounded
ID set. Expired histories can therefore be restored by a complete Create plus
AddTime batch, while orphan actions request history and terminal tombstones
continue to reject stale resurrection. Capacity applies only to unresolved
history, allowing Start and Cancel to settle a full call.
Only retained events reach the UI or live broadcast path. Handshake and live
merge callers log invalid or incomplete histories without publishing events
that compaction discarded.
Test Plan:
- `just fmt` -- passed
- `just clippy` -- passed
- `just test` -- passed
- `git diff --cached --check` -- passed
Expired call histories were removed on the next store insertion, so an otherwise
idle peer could continue carrying stale payload through handshakes after the
five-minute UI retention ended.
Run the same inactive-call compaction before local and handshake snapshots. This
makes the expiry boundary exact without trimming any event from an active call.
Test Plan:
- `just fmt` -- passed
- `just clippy` -- passed
- `just test` -- passed, 147 peer tests
- `git diff --cached --check` -- passed