From 0fbf589dc59cc266bf7311ee4b26001b436aa86b Mon Sep 17 00:00:00 2001 From: ddidderr Date: Mon, 10 Aug 2026 14:05:26 +0200 Subject: [PATCH] docs(plan): record peer-auth decisions and evidence 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) --- organize/decision-tracking/IMPL_DECISIONS.md | 10 +- .../PEER-AUTH-REFACTOR-DECISIONS.md | 978 +++++++++++++++++- organize/planning/PEER_AUTH_PLAN.md | 36 +- organize/unsorted/CALL_TO_PLAY_FIXES_PLAN.md | 7 + .../CALL_TO_PLAY_REVIEW_FABLE_5_XHIGH.md | 17 +- .../CALL_TO_PLAY_REVIEW_GEMINI_3.6_FLASH.md | 9 + .../unsorted/CALL_TO_PLAY_REVIEW_KIMI_K3.md | 6 + organize/unsorted/FABLE_5_FINDINGS.md | 6 + organize/unsorted/FINDINGS.md | 8 +- 9 files changed, 1048 insertions(+), 29 deletions(-) diff --git a/organize/decision-tracking/IMPL_DECISIONS.md b/organize/decision-tracking/IMPL_DECISIONS.md index c18be82..9f80a9b 100644 --- a/organize/decision-tracking/IMPL_DECISIONS.md +++ b/organize/decision-tracking/IMPL_DECISIONS.md @@ -20,11 +20,13 @@ gates; the token map is only cancellation plumbing for in-flight downloads. - Treated a downloaded-but-not-installed game as immediately installable from Tauri by sending `PeerCommand::InstallGame` directly. A not-downloaded game - still uses `GetGame`, and the peer auto-installs after the sentinel commit. + uses `PeerCommand::DownloadGameFiles`; the peer derives every path, size, and + hash from its local catalog manifest and auto-installs after the sentinel + commit. - Removed the dead internal `PeerCommand::UpdateGame` path. The UI update button - now sends `FetchLatestFromPeers`, which skips local manifest serving and asks - latest-version peers for fresh file metadata before the normal download and - update transaction runs. + now sends the same exact-content download command. The local catalog remains + descriptor authority, and only authenticated peers advertising its exact + `ContentId` are eligible sources before the normal update transaction runs. - Removed the unreachable `Availability::Downloading` protocol value. Active operations are reported separately, and local summaries emit only settled availability. diff --git a/organize/decision-tracking/PEER-AUTH-REFACTOR-DECISIONS.md b/organize/decision-tracking/PEER-AUTH-REFACTOR-DECISIONS.md index f88fb0b..775503c 100644 --- a/organize/decision-tracking/PEER-AUTH-REFACTOR-DECISIONS.md +++ b/organize/decision-tracking/PEER-AUTH-REFACTOR-DECISIONS.md @@ -4,8 +4,16 @@ This file records implementation choices that are not fully determined by `PEER_AUTH_PLAN.md`, especially choices where every available option has a meaningful downside. +Entries describe the design at the time they were made. A supersession note is +part of the record: the older rationale remains for traceability, but must not +be read as the current protocol contract. + ## 2026-08-09 — Bound protocol-7 download descriptions generously +> **Superseded by the Phase 4 protocol-8 cutover:** remote download descriptions +> were deleted. Catalog manifests and exact `ContentId` requests now bound +> transfer authority, so these wire-description limits are historical. + **TL;DR:** Until catalog manifests replace remote descriptions, accept at most 100,000 entries, 1 TiB per ordinary file, 64 KiB for the in-memory `version.ini` sentinel, 255 bytes per path component, 900 bytes per relative path, and 16 TiB @@ -72,6 +80,10 @@ Alternatives: ## 2026-08-09 — Bind ownership records to one canonical games directory +> Superseded on 2026-08-10 by root-namespaced ownership records. Binding the +> record was necessary but not sufficient because a second root could replace +> the single per-game slot. + **TL;DR:** A per-game ownership record includes an opaque, exact identity for the canonical configured games directory. A record from another directory is ignored rather than authorizing deletion in the current one. @@ -86,8 +98,9 @@ Alternatives: - Clear every ownership record whenever the setting changes. This is safe, but loses useful cleanup history when the user switches back to an earlier tree. -- Nest ownership records under a games-directory key. This makes the separation - structural, but complicates all existing per-game state layout and migration. +- Nest ownership records under a games-directory key. This was later selected in + game-first form after the singleton replacement risk was proven; see + "Namespace download ownership by canonical games root" below. - Store the canonical path as JSON text. This is easy to inspect, but cannot represent every valid native path without a lossy conversion. @@ -383,3 +396,964 @@ Alternatives: - Track inode identities in the ownership ledger. This can detect later replacement, but makes persistent state platform-specific and still cannot prevent a local actor from changing links concurrently. + +## 2026-08-09 — Keep a durable quarantine marker across download commit + +**TL;DR:** From the moment pending ownership becomes durable until the settled +ownership record is durable, keep a small per-game recovery marker. A visible +`version.ini` is not advertised, served, or installed while either that marker +or a pending/invalid current ownership record says recovery is required. + +This durable transaction-recovery marker is distinct from Phase 2's +runtime-local source integrity quarantine. The latter records only +`(PeerId, content_id)` failures and deliberately disappears with the peer +runtime. + +The ownership record is published by rename. If finalization's rename succeeds +but syncing its parent directory fails, the visible record already says +`pending_files: null`; inspecting that JSON alone would incorrectly classify the +game as settled. The separate marker spans that ambiguity. Recovery re-publishes +the settled record durably before removing the marker, and local library +fingerprints include recovery readiness so a journal-only transition invalidates +cached availability. + +Once the settled record is durable, a successful marker unlink is treated as +settled even if syncing that unlink's parent fails. A power loss may resurrect +the marker and cause conservative recovery on the next run, but the live process +never reports both a failed marker removal and a marker that is already visibly +absent. + +Alternatives: + +- Infer readiness only from `pending_files`. This uses one file, but cannot + represent the post-finalization-rename durability window safely. +- Keep transaction recovery quarantine only in memory. This is smaller on disk, + but loses the safety boundary on restart and makes exceptional task + termination able to expose a cached ready game. +- Add a larger tagged write-ahead-log schema with every transaction phase. This + can encode the same state in one artifact, but adds migration and recovery + complexity without improving the current decision table. + +## 2026-08-09 — Retain admission after an unexpected mutation-task exit + +**TL;DR:** If a download, install, update, uninstall, or downloaded-file removal +task disappears before its normal recovery, rescan, and explicit cleanup path, +keep that game marked active until the process restarts. Startup recovery is the +escape hatch. + +Dropping an async guard cannot safely run filesystem recovery or publish a new +library snapshot. Clearing the operation entry anyway would permit another +writer to enter while install intent, staging, backup, ownership, or sentinel +state may still be unresolved. Retaining the entry is deliberately fail-closed: +other games keep working, but the affected game and a game-directory change are +blocked until restart recovery settles the state. + +Alternatives: + +- Spawn recovery from `Drop`. Destructors cannot await it, task/runtime shutdown + is exactly when this path is likely, and clearing the gate would still need a + proven recovery-and-publication completion point. +- Clear the entry and rely on the next command to recover. This keeps the UI + available, but admits a second mutation before the recovery boundary. +- Parse persistent transaction state on every operation and serving request. + This can self-heal without restart, but puts large journals and filesystem I/O + on hot paths and still needs coordination with cached library publication. + +## 2026-08-09 — Quarantine startup recovery failures per configured root + +**TL;DR:** While a games directory is being recovered, block every mutation and +serve request. After its recovery scan is safely published, block only game IDs +whose recovery failed. A same-path settings refresh retries recovery; restart +naturally retries it before networking starts. + +The download ownership journal already has its own durable recovery marker. +Install recovery failures and root-enumeration errors need a runtime gate too, +but adding another persistent transaction format would duplicate state that is +reconstructed before services start on every launch. Binding the gate to the +configured root prevents a failure in one library directory from poisoning a +different directory, while the failed-ID projection keeps healthy LAN-party +games usable. + +Alternatives: + +- Hide the entire library after one game fails recovery. This is simpler and + safe, but one broken game unnecessarily disables every healthy game. +- Add a second durable per-game recovery marker for all install states. This + survives without rerunning recovery, but creates another crash-consistent + schema beside install intent and download ownership. +- Log recovery errors and scan normally. This maximizes availability, but can + advertise, serve, launch, or overwrite a root whose transaction state never + settled. + +## 2026-08-09 — Drain outbound readers before changing or mutating a game root + +**TL;DR:** File, chunk, and streamed-install senders register under the same +admission barrier as filesystem mutations and game-directory changes. A root +transition first cancels and drains every registered sender; if draining times +out, the requested transition makes no local state change. + +An outbound sender may still have a package file or extractor process open. +Switching or recovering a root while that work is live can expose inconsistent +bytes and can make rename/delete behavior platform-dependent. Admission closes +the registration race, while the explicit drain makes an empty registry the +mutation boundary. A timed-out command leaves the prior root, cache, database, +and recovery quarantine unchanged so it can be retried without reconstructing an +abandoned half-transition. + +If an outbound handler disappears unexpectedly, its token is cancelled but its +registry entry is deliberately retained. The affected process must restart +before a root transition is attempted again; the registry is owned outside one +peer-runtime instance, so a peer-runtime restart alone is not claimed to clear +this fail-closed state. + +Alternatives: + +- Let a directory switch cancel senders without waiting. This is responsive, but + cancellation is a signal rather than proof that file and extractor work has + stopped. +- Mark recovery before draining and leave the old root quarantined on timeout. + This is safe, but needlessly discards a known-good published state when no + recovery mutation ever began. +- Force-remove unexpected transfer entries after a timeout. This restores + availability, but invents quiescence and can race detached OS or provider + work. +- Clear the shared transfer registry on every peer-runtime start. This could + shorten recovery, but is safe only after proving every task from the previous + runtime has fully stopped. + +## 2026-08-09 — Retain a no-follow game-root capability around install mutations + +**TL;DR:** After operation admission and root-dependent revalidation, open the +configured games directory and its one direct game-ID child without following +their final components, reject non-direct/non-directory/link/reparse shapes, and +retain both handles until install, update, uninstall, recovery, or streamed +install commit/rollback finishes. The existing unpacker still receives an +ambient staging path because concurrent local root replacement is outside the +threat model. + +This closes the concrete preflight hole where `/` could be a +symlink or Windows reparse point before an install mutation began. Each blocking +open is awaited directly, and the capability is owned by the same structured +transaction as the mutation; no child task or filesystem authority detaches past +operation completion. Local scanning and readiness use no-follow metadata for +the direct game root so an unsafe shape cannot be cached as ready while startup +recovery reports and quarantines it. + +Alternatives: + +- Change `Unpacker` and every install primitive to accept only handle-relative + capabilities. This is the strongest design, but external `unrar` requires a + pathname and the cross-platform directory API rewrite is much larger than the + direct-root vulnerability being closed. +- Canonicalize the game path once and continue without retained handles. This is + smaller, but follows the very link/reparse component that must be rejected and + loses object identity before the transaction ends. +- Reject unsafe roots only during library scanning. This prevents publication, + but a stale command can still reach a mutation path unless the check is + repeated after admission immediately before mutation. + +## 2026-08-09 — Poll local game roots with a structured one-second snapshot + +**TL;DR:** Replace the operating-system filesystem watcher with a one-second +lexical metadata snapshot of the games directory and each direct game root. +Snapshot reads run inline as finite scoped blocking work, while changed game IDs +enter a lexically owned rescan set that is drained before the monitor returns. + +The previous `notify` backend owned an internal thread whose drop only requested +shutdown; dropping the watcher did not prove that thread had exited. The polling +snapshot has no callback channel or hidden worker. It compares direct entry +names, kinds, sizes, and modification times, ignores install/download-protected +children, and retains the five-minute full scan as a slower reconciliation path. +A monitor-loop unwind is caught and reported only after the same natural rescan +drain used by ordinary shutdown, so dropping the rescan set never invents child +quiescence. A one-second interval keeps sideloaded games and manual package +changes prompt enough for the desktop UI while limiting idle directory reads to +once per second. + +This trades event-driven responsiveness and near-zero idle I/O for a bounded +shutdown lifetime. Metadata polling can miss an in-place rewrite that preserves +both file size and reported modification time. The periodic full scan recovers +from transient snapshot failures and reconciles broader library state, but its +archive cache uses the same size-and-time identity and does not close a +deliberately metadata-preserving rewrite edge case. + +Alternatives: + +- Keep `notify` and accept its backend lifetime. This gives immediate events and + avoids periodic reads, but the peer-runtime shutdown boundary cannot prove the + backend thread has stopped. +- Run a full library scan every second. This detects more state directly, but + repeatedly parses and fingerprints complete game packages when nothing has + changed. +- Poll less frequently. This reduces idle filesystem work, but makes a manual + sideload or package edit visibly laggy during a LAN party. + +## 2026-08-09 — Own shared QUIC endpoints up to the provider boundary + +> **Partly superseded by the Phase 4 protocol-8 cutover:** shared endpoint +> ownership and lexical connection closure remain current; the `Goodbye` +> delivery step was deleted and liveness removal is authoritative. + +**TL;DR:** Use one runtime-owned outgoing QUIC client endpoint and one +server-owned incoming endpoint. A local IO-provider wrapper retains each s2n +endpoint task, every request closes its connection lexically, and shutdown +always awaits the endpoint task after application connection scopes drain. + +Creating a new client endpoint for every control request repeatedly bound UDP +sockets and spawned transport work whose handle the stock s2n Tokio adapter +discarded. The shared client makes connection creation cheap and gives the peer +runtime one final network child to close after services, user operations, and +Goodbye delivery finish. Short control exchanges use one absolute deadline from +connect through response EOF; downloads and streamed installs use their +operation cancellation token plus QUIC handshake and idle bounds, avoiding a +wall-clock limit on a large LAN transfer. A connection guard initiates close on +every success, error, cancellation, deadline, or unwind path. + +The wrapper depends on the public provider trait from an exactly matched +`s2n-quic-core`, so `s2n-quic` and `s2n-quic-core` are pinned together at 1.85.0 +and 0.85.0. s2n starts IO last, so a successful internal endpoint spawn has no +later fallible builder step before the Client or Server is returned. + +This is an application-level structured boundary, not a claim that every task +inside s2n is joinable. `s2n-quic-platform` privately spawns RX and TX workers +and discards their handles. When the owned endpoint finishes, it drops their +ring counterparts and removes their ability to reach application state; they +then wake and exit after observing the closed ring. A final private socket/ring +iteration may still occur after the endpoint join, and the public API offers no +handle with which to await it. That narrow dependency-internal exception is +accepted here because this refactor explicitly forbids vendoring or forking the +provider. + +Alternatives: + +- Keep the stock provider. This is less local code and looser version coupling, + but loses the endpoint handle and cannot make runtime completion a real QUIC + lifecycle boundary. +- Create one fully owned endpoint per request. This makes each request naturally + lexical, but adds socket/task churn, local port pressure, and repeated setup + for frequent control traffic. +- Fork or patch `s2n-quic-platform` to retain and join endpoint, RX, and TX + handles. This gives literal transport-worker quiescence, but adds an ongoing + security/update maintenance burden for a large networking dependency. +- Give each endpoint a dedicated Tokio runtime and join that runtime. This can + contain dependency tasks more strongly, but is operationally heavy and + complicates every async handoff for little LAN-party benefit. + +## 2026-08-09 — Run finite blocking work lexically, not as detached pool work + +**TL;DR:** A finite filesystem or validation operation runs inside the owning +future through `scoped_blocking`. On a multi-thread Tokio runtime this uses +`block_in_place`; on a current-thread runtime or outside Tokio it calls the +closure directly. Once entered, the operation is deliberately non-cancellable, +so its parent cannot finish or release filesystem authority until the closure +has returned. + +Tokio filesystem APIs and `spawn_blocking` both submit work that may keep +running after the async future is dropped. That is incompatible with rollback, +operation-admission, and outbound-transfer guards whose destruction is supposed +to mean that mutation or reads have stopped. Whole bounded batches therefore use +synchronous file handles and one lexical blocking scope, with cancellation +checks at safe boundaries before and after the batch. Large bounded loops check +their cancellation token between entries where doing so cannot expose a partial +publication. + +This favors lifetime proof over prompt forced cancellation. A stuck kernel or +filesystem call can delay shutdown, and current-thread tests execute the closure +on their only runtime thread. Production uses a multi-thread runtime, while +controlled tests release deliberately blocked closures from an operating-system +thread rather than another task on the same executor. + +Alternatives: + +- Drop a `spawn_blocking` handle on cancellation. Tokio cannot stop an already + running closure, so mutation could outlive its operation guard. +- Spawn a detached reaper that waits for blocking work. This eventually cleans + up, but the parent still returns before its child and violates the same + ownership boundary. +- Put every file operation on a dedicated executor. Its request still needs a + synchronous completion acknowledgement on drop; it adds a queue without + strengthening the finite-call guarantee. + +## 2026-08-09 — Treat bundled extractors as trusted leaf helpers + +**TL;DR:** Setup and archive-extraction helpers are owned through direct-child +termination, reap, pipe drainage, and supervisor join. They must be trusted leaf +executables that do not fork descendants. User-launched games and game servers +are explicitly transferred to the user and are not runtime children. + +The bundled `unrar` is invoked with stdin closed and password prompting +disabled. Its lexical owner kills and waits for the child on cancellation, +error, or parent drop, and joins bounded stdout/stderr readers before returning. +The CLI `--unrar` override is local-authority configuration and has the same +trusted-leaf contract. A helper that forks is outside this process scope: a +descendant can otherwise retain pipes or continue mutation after the direct +child exits. + +This is narrower than arbitrary process-tree structured concurrency. Rust's +portable process API cannot create and await a whole Unix process tree or a +Windows Job Object without platform-specific support, and the evaluated +`process-wrap` API does not expose a reliable all-descendants join boundary. +Supporting arbitrary helper trees would require a separate Unix +process-group/subreaper and Windows Job Object implementation plus real Windows +tests. + +Alternatives: + +- Assume killing the direct PID kills its descendants. That is false on both + supported process models and would overclaim rollback safety. +- Add a generic process-tree crate and rely on its kill call. Killing is not + joining; the evaluated implementation can return before every descendant has + exited. +- Treat game and server launches as peer-runtime children. That would make + normal app shutdown terminate the games the user intentionally launched. + +## 2026-08-09 — Isolate the peer runtime behind a joinable supervisor thread + +> **Partly superseded by the Phase 4 protocol-8 cutover:** the joinable +> supervisor and ordered child drainage remain current; references to a +> `Goodbye` child describe the removed protocol-7 shutdown path. + +**TL;DR:** A peer runtime is not stopped when cancellation is requested. A +dedicated supervisor thread creates and owns its Tokio runtime, root future, +services, operations, and QUIC endpoint. Normal owners request shutdown and +await `wait_stopped`; active `PeerRuntimeHandle::Drop` is a loud misuse fallback +that cancels and synchronously joins the same supervisor without depending on +the caller's Tokio executor. + +Startup uses a synchronous result handshake after runtime-local construction, so +an endpoint or root startup error/panic is joined before it is returned to the +caller. The async wait retains the sole thread handle in the owner across every +await, making cancellation retryable; completion is published only after the +isolated Tokio runtime has been destroyed. Root, Goodbye, and transport cleanup +panics are contained in order. Even an endpoint task aborted by an exceptional +destructor is bounded by runtime teardown before either `wait_stopped` or active +Drop returns. Production CLI and Tauri lifecycles still use explicit +shutdown-and-wait as their ordinary, non-blocking ownership path. As with any +strict synchronous join, active Drop must not form an ownership cycle: providers +and peer callbacks may not obtain/drop their own runtime handle, and a caller +may not hold a shared lock or resource required by shutdown while dropping it. +Such a cycle has no quiescent synchronous completion. + +Alternatives: + +- Keep the root task on the caller's Tokio runtime. Its destructor cannot + synchronously join a current-thread executor without deadlocking, so active + Drop can only detach or overclaim quiescence. +- Use `block_in_place` or drive the caller runtime from Drop. This is + unavailable on current-thread runtimes and makes peer progress depend on the + executor that is currently blocked. +- Spawn a cleanup task or reaper from Drop. That merely detaches the lifetime + under a different name and lets the public owner return first. +- Abort the root task and report it stopped. Abort is a cancellation request; it + skips ordered child drainage, Goodbye, and provider cleanup. + +## 2026-08-09 — Await fanout and directory transitions through publication + +> **Partly superseded by the Phase 4 protocol-8 cutover:** `SetGameDir` +> admission and acknowledged publication remain current. Library deltas and +> Call-to-Play event fanout were replaced by bounded revision hints and pinned +> full pulls. + +**TL;DR:** Concurrent peer deliveries remain lexically owned and are all awaited +before a publication returns. `SetGameDir` holds operation admission through +outbound drain, recovery, scan, cache/database publication, and its acknowledged +result; callers persist or display only the canonical path returned on success. + +Returning after merely starting delta or Call-to-Play sends lets operation +guards and directory admission clear while child QUIC work still references the +old state. Awaited fanout preserves concurrency without losing the parent-child +boundary. A directory transition is intentionally slower because its command +completion now means the new root has recovered and its projected library state +has been published. Rejection or drain timeout leaves both backend and UI on the +previous root. + +Alternatives: + +- Queue delivery on the global runtime tracker. Shutdown eventually drains it, + but the operation or publication that created it can finish first. +- Acknowledge a directory change immediately and recover in the background. This + makes settings feel faster, but permits commands and UI state to race a root + whose recovery status is not known. +- Persist the requested path before core acknowledgement. A rejected core + transition then leaves durable UI configuration disagreeing with the active + peer root. + +## 2026-08-09 — Fail closed on interrupted catalog publication + +**TL;DR:** Generate and independently verify every selected manifest before the +first destination write. During publication, retain a durable corpus marker; +`check` and later generation reject that marker until an operator reconciles the +output and deliberately removes it. + +Atomic replacement protects one manifest, but a multi-game generation can still +stop between files and leave a mixed authority corpus. The marker makes that +interrupted state visible and prevents a release check from blessing it. Manual +recovery is intentional: automatically deleting the marker cannot know whether a +power loss made every directory entry durable on the current platform. The +production build remains blocked until the complete canonical package corpus is +available and all catalog rows have independently generated artifacts. + +Alternatives: + +- Publish games one at a time with no corpus marker. Each JSON file is valid, + but a late failure silently leaves a mixed old/new catalog. +- Delete the marker automatically on the next run. This improves convenience, + but hides the exact interrupted state an operator must inspect. +- Write the entire corpus into one file. Replacement becomes atomic at the file + level, but on-demand per-game loading and bounded validation are lost. + +## 2026-08-09 — Use the mDNS daemon acknowledgement as its public completion boundary + +**TL;DR:** Explicitly unregister advertisements and wait for `mdns-sd`'s +`DaemonStatus::Shutdown` before the owning discovery/advertisement scope +returns. Describe this as the dependency's shutdown acknowledgement, not as a +literal thread join. + +The dependency discards its internal daemon-thread handle. Its shutdown status +is sent from that thread at the end of daemon processing, immediately before the +thread closure returns, so the acknowledgement proves that daemon-owned mDNS +state and callbacks have settled. The public API provides no handle for joining +the final thread return itself. The application additionally owns and joins its +outer discovery worker thread; no mDNS callback retains application filesystem +or transaction authority after the daemon acknowledgement. + +Alternatives: + +- Claim that the status receiver joins the thread. It does not, and that wording + would overstate what the dependency exposes. +- Drop the daemon after merely sending shutdown. This can leave callbacks and + daemon state active beyond the discovery scope. +- Fork `mdns-sd` to retain its thread handle. That yields a literal join, but + adds dependency maintenance for a final return boundary that carries no + application authority after the acknowledged daemon shutdown. + +## 2026-08-10 — Persist only active, root-bound install intents + +**TL;DR:** An install intent exists only while an install, update, or uninstall +transaction is active or awaiting recovery. Schema 2 binds that intent to the +canonical configured games directory. Startup and `SetGameDir` scan every +persisted intent before mutation and fail closed on foreign, invalid, aliased, +or settled-on-disk state; successful settlement durably removes the intent. + +Install recovery can rename or delete `local`, `.local.installing`, and +`.local.backup`, so a per-game record without a root identity could be replayed +against a different configured directory. Treating a corrupt record as absent +would likewise let ordinary cleanup overwrite the only recovery evidence. Every +operation therefore preflights reserved slots, durably publishes a root-bound +active intent before its first filesystem transition, and retains that intent +until the strict recovery table has settled and synced the game root. There is +no schema-1 compatibility path. + +The state layout retains one intent path per game ID. This is safe only because +an unresolved intent for another root blocks peer startup, a root change, and a +new same-ID mutation before any cache, quarantine, transfer, or payload state is +changed. Recovery also unions persisted intent IDs with visible game-root IDs, +so an absent current game directory cannot hide an active transaction. + +Alternatives: + +- Store intents in a namespace keyed by the canonical games root. This would + preserve unresolved transactions for several roots simultaneously, but adds + state discovery, migration, and operator-recovery complexity not needed while + root changes are required to settle the old transaction first. +- Ignore a foreign-root intent when switching directories. A later same-ID + operation could overwrite the sole journal and permanently lose recovery + authority for the old root. +- Persist a settled `None` record. That creates foreign-root ambiguity during a + normal directory switch; absence is the canonical settled representation. +- Treat malformed or legacy intents as missing. That restores availability by + guessing, but can destructively apply cleanup to markerless or partially + transitioned install state. + +## 2026-08-10 — Namespace download ownership by canonical games root + +**TL;DR:** Keep each game's download ledger and recovery marker under +`games//download_ownership/v1-/`, where the digest is a +domain-separated BLAKE3 hash of the lossless canonical games-directory key. +Retain the full key inside `record.json`, and migrate the former singleton +record into its derived namespace with a copy-first, fsync-before-delete +protocol. + +A root binding inside one per-game record prevented cross-root deletion, but it +did not preserve authority: downloading the same ID in root B replaced root A's +settled or pending record and could clear A's recovery marker. Independent +namespaces preserve exact recovery and downloaded-file removal history for both +roots. The digest is only a fixed-length lookup component; the embedded full key +remains authoritative, so a digest/key mismatch is invalid and never eligible +for baseline replacement. + +Startup and same-root recovery inspect only the exact selected namespace, while +unrelated root namespaces remain inert. The bounded state inventory still +discovers ownership-only IDs, including an interrupted download whose physical +game root is absent. A current pending absent-root generation settles to empty; +malformed, marker-only, misplaced, or aliased selected state remains +quarantined. + +Existing singleton records are migrated as local durable state, not accepted as +a second runtime format. Migration publishes and syncs the namespaced record and +marker before unlinking the singleton. A crash may therefore leave two copies; +the next run completes cleanup only when they are byte-for-byte and marker-state +equivalent. Conflicting or split evidence fails closed without overwriting +either copy. A legacy temporary file without a record or marker is safe scratch +and is swept after no-follow validation. + +Alternatives: + +- Keep replacing one root-bound slot. This prevents deletion in the wrong tree, + but loses recovery and removal authority for every previously selected root + with the same game ID. +- Reject every foreign settled record before using a new root. This preserves + authority but makes normal multi-root use unavailable indefinitely because a + settled ownership ledger is intentionally retained for later removal. +- Use a root-first global state tree. It gives the same separation, but makes + per-game state inventory, install intent coordination, and migration broader + than the game-first namespace needed here. +- Trust the digest without checking the embedded key. A collision or misplaced + directory could then bind state to the wrong root; the full lossless key is + always revalidated instead. + +## 2026-08-10 — Bound peer-CLI stdin cancellation by descriptor or process lifetime + +**TL;DR:** On Unix, duplicate stdin before peer startup, preload ordinary files, +and read pipes and terminals through an owned nonblocking Tokio `AsyncFd`. +SIGINT and SIGTERM participate in the same CLI scope as commands and events: +they cancel and settle the command future, await peer shutdown, and drain the +event loop before returning. On non-Unix, Tokio's stdin remains the normal +EOF/command reader, but cancelling its hidden blocking read cannot be made +quiescent under `unsafe_code = "forbid"`; after exceptional event termination or +a shutdown signal, the CLI therefore exits the process only after command-task +bookkeeping, peer shutdown, and event settlement have completed. + +The non-Unix process boundary is deliberately narrow. EOF and the JSONL +`shutdown` command finish normally and retain their existing output semantics; +neither path calls `process::exit`. The exceptional event path exits nonzero, +while a handled shutdown signal exits successfully after cleanup. This avoids +returning into Tokio runtime destruction with a blocking stdin worker that can +wait forever for another byte. + +Alternatives: + +- Abort the Tokio stdin task and return normally. Tokio documents that the + underlying blocking read is uncancellable, so dropping the runtime can hang + while stdin remains open. +- Put blocking stdin on a detached or uninterruptible application thread. That + moves the same lifetime leak outside Tokio and still cannot be joined on every + exit path. +- Add a stdin-proxy child process. It provides a portable kill-and-wait handle, + but adds a helper process and protocol surface to every CLI invocation when + Unix already has a direct safe descriptor solution. +- Call Windows cancellation APIs directly. That requires a separately audited + unsafe boundary and still depends on best-effort device cancellation; the CLI + crate forbids unsafe code. + +## 2026-08-10 — Require extracted regular files to be emitted exactly once + +**TL;DR:** Catalog publication rejects a regular extracted path repeated within +one RAR or across several root `.eti` archives. Repeated directory entries are +allowed because they carry no bytes and archives commonly restate parents. + +The receiver verifies one final path, size, and BLAKE3 value per extracted +entry. Allowing several archive members to write the same regular path would +make the actual extraction order an extra, implicit authority and could hash a +different occurrence from the one promoted at install time. Emit-once regular +files keep the manifest and extraction transaction one-to-one. + +Alternatives: + +- Let the last archive win. This matches some extractors, but binds authority to + sorting and overwrite behavior that is not represented in the manifest. +- Permit duplicates only when bytes are equal. The publisher would still need to + prove which occurrence the installer emits, adding work with no fixture or + production requirement. +- Reject repeated directories too. This is simpler but excludes ordinary + multi-archive layouts that redundantly list shared parent directories without + creating overwrite ambiguity. + +## 2026-08-10 — Keep Phase 2 catalog authority behind one protocol-7 adapter + +> **Superseded by the Phase 4 protocol-8 cutover:** the temporary version-only +> adapter was deleted. Peer availability and both transfer requests now carry +> the exact catalog `ContentId`. + +**TL;DR:** Build every ordinary download from the receiver's immutable catalog +manifest. Until the coordinated Phase 4 cutover, protocol 7 selects sources only +by exact catalog game version; it does not pretend to advertise `content_id`. +Every received chunk is still checked against the local catalog, and exact wire +content eligibility arrives in the one planned protocol bump. + +Protocol 7 has no content-ID field. A partial wire bump would either violate the +one-cutover policy or require a compatibility path that this project explicitly +rejects. The temporary adapter therefore carries the locally expected +`content_id` through planning, ownership, verification, and retry while using +only version-matched protocol-7 peers as candidates. Remote paths, sizes, +chunks, hashes, and majority descriptions have no storage authority. + +An integrity mismatch quarantines the runtime-local `(PeerId, content_id)` pair. +The address is deliberately excluded so endpoint rotation cannot clear the +failure, while another content ID remains eligible. Transport failures retry but +do not quarantine, and no result creates durable peer trust. Each failed chunk +may try every distinct eligible peer identity once; terminal failure means that +set is exhausted rather than that a numeric retry counter expired. One absolute +ten-minute deadline covers the complete open/request/receive/check lifetime of +each ordinary chunk and is classified as transport failure. + +Ownership schema 2 records the exact committed and pending catalog content IDs. +The no-transfer local shortcut requires settled ownership for the expected +content ID in addition to the catalog-version sentinel. Missing, pending, +legacy, pre-content-ID, recovery-marked, or differently bound state cannot make +local bytes authoritative. + +Alternatives: + +- Add `content_id` to protocol 7 or ship an intermediate protocol version. This + would improve pre-transfer selection now, but splits the deliberately + coordinated identity/catalog/synchronization cutover and creates an + unsupported intermediate wire design. +- Continue selecting a remote description or file-size majority. That lets the + same peers supplying bytes choose the paths and expected shape, defeating the + bundled catalog authority. +- Persist integrity quarantine. This would survive restarts, but turns one byte + failure into durable trust state without an operator recovery model. +- Quarantine by address or stop after a fixed retry count. Addresses are not + peer identity, while a fixed count can skip a distinct eligible source that + remains available. +- Treat a matching `version.ini` as verified local content. That proves only a + version string and would let unknown or pre-catalog bytes bypass streaming + verification. + +## 2026-08-10 — Make Stream Install exact at admission and promotion + +**TL;DR:** Offer and accept Stream Install only for catalog games with a +verified extracted-file manifest. The sender must have exactly the catalog's +direct root `.eti` archive set, and the receiver must materialize exactly the +catalog's archive and extracted-output sets before verified staging can be +promoted. + +Sender admission loads the same catalog manifest used for ordinary serving, +requires Stream Install support, and compares the direct regular root `.eti` +names with the manifest before starting the extraction provider. This prevents a +raw requester from learning unsupported or extra archive contents merely because +the receiver would later reject them. + +The receiver treats sender framing and RAR CRC32 as bounded transport metadata, +not authority. It verifies the complete archive set and every extracted +path/kind/size/BLAKE3 value while writing isolated staging. Missing, extra, +shape-mismatched, repeated, or hash-mismatched regular files fail integrity and +roll back before another source starts. The global emit-once regular-file rule +recorded above removes archive-order overwrite authority; repeated directory +entries remain allowed. + +Only after catalog verification does the receiver sanitize and apply account, +language, and persona settings to staging. Promotion to `local/` follows that +rewrite. The one-shot launch-settings marker is written after a successful +commit, so a marker failure cannot bless an unpromoted tree and first play can +safely retry the rewrite. + +Alternatives: + +- Rely only on receiver rejection. This preserves receiver integrity but lets a + hostile raw requester trigger extraction of unsupported or extra sender + archives. +- Trust RAR CRC32 and sender-declared sizes. The source controls both metadata + and bytes, so this detects accidents without establishing catalog authority. +- Apply launcher settings after promotion. A rewrite failure would leave a live + installed tree that never passed the complete pre-promotion transaction. +- Write the one-shot marker before promotion. A later commit failure could make + first play skip the rewrite for a tree that was never installed. + +## 2026-08-10 — Separate fixture authority from the production catalog gate + +**TL;DR:** The fixture generator creates reduced catalogs only for development +and acceptance tests. Every non-opted-in build defaults to production resources, +and production packaging cannot proceed until every row in the production +`game.db` has an independently generated and checked manifest. + +The test-only `lanspread-fixture-catalog` derives a filtered `game.db` and +companion manifests from explicitly selected fixture packages, using the Rust +manifest implementation rather than scenario-owned hashes. Tauri accepts that +authority only when both the exact checked-in development resource map and +`LANSPREAD_USE_FIXTURE_CATALOG=1` are present. The custom production profile +cannot be downgraded by that opt-in. + +All other builds require exactly `game.db`, `manifests/*`, and `assets/*` as +production resources and run the publisher's complete-catalog check before +packaging. The checkout currently contains 186 production catalog rows but not +their canonical packages or generated manifest corpus, so this gate +intentionally fails. Passing fixture checks proves the implementation path, not +production catalog completion. + +Alternatives: + +- Let ordinary development configuration silently supply fixtures to release + builds. This makes local packaging convenient but can ship test hashes as + production authority. +- Generate production hashes from whatever bytes a runtime peer happens to have. + That makes untrusted local inventory, rather than the canonical package + publisher, the authority. +- Permit a partial production manifest directory. This would package games that + cannot be verified and make catalog availability dependent on missing runtime + artifacts. +- Treat the fixture corpus as production proof. Its reduced rows and synthetic + packages do not cover the real 186-game catalog. + +## 2026-08-10 — Make one Ed25519 TLS key the installation identity + +**TL;DR:** One installation-local Ed25519 key signs one self-issued certificate. +`PeerId` is lowercase unpadded RFC 4648 base32 of BLAKE3 over the complete +canonical DER SubjectPublicKeyInfo. There is no UUID, address-derived identity, +shared repository key, or alternate identity fallback. + +The desktop app owns a strict version-1 `peer-identity-v1.json` record in its +Tauri app-data directory. The JSON stores only the exact version, `ed25519` +algorithm, canonical no-pad base64 certificate DER, and canonical no-pad base64 +PKCS#8 private-key DER. Load derives `PeerId` from the certificate SPKI and +requires the key, SPKI, certificate signature algorithm, and exact derived SAN +to agree within fixed record, certificate, and key size bounds. Private key +material is redacted from `Debug` and errors. + +File and parent opens reject links and Windows reparse points. Publication uses +a mode-0600, synced temporary file and a no-clobber hard-link winner; a +pre-existing canonical record is never overwritten. Corrupt bytes are preserved +byte-for-byte in a quarantine sidecar before replacement, and retained handles +are used for permission changes and object checks. Read, permission, quarantine, +or publication failure returns a typed `Ephemeral` outcome and a fresh identity +for that runtime. An explicitly selected peer-CLI `--identity-file` is instead +strict and nonmutating: missing or invalid input fails startup without repair, +quarantine, replacement, or generation. + +This state has one supported normal app-data owner and deliberately has no +cross-process identity lease. Under that model, no-follow capabilities contain +traversal and avoid following or changing an outside symlink target, while +no-clobber publication preserves an existing winner. A concurrent same-user +writer in the final quarantine check-to-unlink window can still cause +availability or identity-continuity loss; that writer is outside the supported +ownership model. + +Alternatives: + +- Keep the old UUID beside an unrelated TLS key. That leaves the displayed and + routed identity unauthenticated and permits address or payload substitution. +- Distribute one application certificate and key. Every installation would then + possess the same signing authority, so CertificateVerify could not identify a + peer. +- Add a mandatory OS lock or lease. That expands the portability and recovery + model without a multi-process app-data-owner requirement. + +## 2026-08-10 — Pin outbound responders without changing protocol 7 + +> **Partly superseded by the Phase 4 protocol-8 cutover:** typed endpoint/SPKI +> pinning and TLS hardening remain current. The protocol-7 ALPN, serialized +> identity assertions, and `Goodbye` discussion below are historical; the only +> current ALPN is `lanspread/8`. + +**TL;DR:** Every outbound dial consumes `PeerEndpoint { peer_id, addr }`. The +shared s2n client endpoint carries the expected ID through exact SNI, and a +custom rustls verifier checks the full certificate SPKI before delegating the +real TLS 1.3 CertificateVerify signature to rustls. + +The rustls provider is the only compiled s2n TLS provider. Both endpoints permit +TLS 1.3 only and bind ALPN to `lanspread/7`. The client disables resumption and +0-RTT. The server has no session store, emits no TLS 1.3 tickets, accepts no +early data, and sends no half-RTT data. Ed25519 is the only advertised signature +scheme. All network, handshake, liveness, download, retry, and Stream Install +callers retain the typed endpoint to the final `QuicConnector::connect` call; +there is no address-only dial seam. + +The load-bearing in-process QUIC proof uses these production providers and +connector: the correct key succeeds, certificate A signed by private key B +passes the SPKI check but fails CertificateVerify, a different valid peer at an +exactly reused address fails the expected-ID pin, and two reconnects repeat both +certificate and signature verification. The hostile A/B resolver is test-only. + +This is responder authentication, not mutual TLS. Protocol 7 still serializes +its existing string identity fields, and an inbound request's actor or `Goodbye` +assertion is not yet bound to a client certificate. Catalog `content_id`, +inbound authority, and metadata generations change together in the Phase 4 wire +cutover; Phase 3 introduces no intermediate wire mode. + +Alternatives: + +- Assert CertificateVerify success in the custom verifier. That would accept a + certificate whose public key does not match the handshake signature. +- Configure one verifier per connection. s2n's shared client endpoint does not + expose that seam; exact SNI is the stateless, concurrency-safe carrier. +- Leave resumption enabled. A resumed connection could bypass the per-connect + certificate and signature proof this identity model requires. + +## 2026-08-10 — Treat bounded mDNS records as candidates + +**TL;DR:** mDNS can propose a canonical `PeerEndpoint`, but it cannot add a +peer, publish UI topology, merge a library, or merge Call to Play history. Only +a current TLS-pinned outbound handshake can commit authenticated state. + +The native mDNS browser runs in one joinable worker thread. It uses a bounded +64-entry nonblocking ingress channel; repeated observations are hints and are +coalesced by dropping overflow instead of blocking shutdown or allocating +without bound. The async discovery service also caps active endpoint +negotiations at 64 and lexically drains every started negotiation before the +worker joins. + +A candidate reserves an RAII negotiation lease before it is queued or awaited. +The lease owns current claims for both `PeerId` and listener address. Failure, +cancellation, or future drop clears current claims synchronously without an +async database lock. A stale handshake result performs zero endpoint, library, +feature, UI, or Call to Play mutation. Direct CLI connections use the same typed +candidate reservation before their structured child starts. + +Alternatives: + +- Insert mDNS records directly into `PeerGameDB`. A spoofed advertisement would + become roster and download-source authority before TLS proof. +- Use an unbounded callback channel. A discovery burst could retain arbitrary + memory and delay shutdown indefinitely. +- Reserve inside the handshake future. Queueing or scheduling delay would leave + an older future able to claim authority after a newer observation committed. + +## 2026-08-10 — Separate candidate leases from endpoint generations + +**TL;DR:** Candidate transitions and refreshes have different precedence, and +every successful authentication receives a fresh endpoint generation. Work that +observed an older generation cannot refresh, remove, or roll state back. + +A candidate and a refresh both refuse to reserve while any in-flight negotiation +physically overlaps their peer ID or listener address; neither silently +supersedes the current lease owner. A refresh must also present the exact +authenticated `PeerLivenessSnapshot { endpoint, generation, last_seen }`. +Observing a new runtime session fences the older ticket's commit authority, but +the fenced lease retains physical peer/address occupancy until its owner drops +or releases it. Conditional commit validates both claims before any remote state +mutation and reports same-ID moves and different-ID address eviction. + +Ping success updates `last_seen` only for its captured endpoint and generation. +Ping failure and stale pruning remove only that same generation. Removal queues +`PeerLost`, the exact count, and the catalog `ListGames` snapshot while +retaining the database write guard, so a reauthentication cannot publish its +newer `PeerDiscovered` transition between the removal and its UI snapshot. +Handshake commit uses the same topology-critical-section rule. + +Alternatives: + +- Let every refresh become the latest negotiation. An old endpoint refresh can + then supersede an already observed move or address takeover. +- Key liveness only by peer ID or address. A delayed probe can update or remove + a different authenticated generation after reconnect or address reuse. +- Emit topology after releasing the database lock. Concurrent reauthentication + can invert `PeerLost` and `PeerDiscovered` and pair them with inconsistent + count or library snapshots. + +## 2026-08-10 — Cut protocol 8 to responder-owned bounded state pulls + +**TL;DR:** Protocol 8 (`lanspread/8`) is the one current wire contract. mDNS and +revision hints are untrusted candidates; all library and Call-to-Play state is +accepted only from a full pull against an exact TLS-pinned `PeerEndpoint` and +committed against its current generation and runtime session. Exact catalog +`ContentId` is required for availability and transfer. + +The cut deletes protocol-7 `HelloAck`, library deltas, `Goodbye`, remote file +descriptions, version-only source selection, pushed/acknowledged Call-to-Play +events, union-history healing, and every compatibility shim. `Hello` is an empty +request. Its pinned responder returns only its own `PeerStateSnapshot`, while +`Pong` returns the runtime-session ID and the library and Call-to-Play +revisions. `LibraryChanged` and `CallToPlayChanged` carry the same small +untrusted `ChangeHint`; an unknown claim is ignored and a known claim can only +coalesce a pinned full pull. A dropped hint converges through the next pinned +revision check. + +The library domain is a sorted, unique, bounded full slice of +`GameAvailability { game_id, content_id }`. It contains no remote-chosen path, +size, version, file list, or manifest digest. Ordinary chunk requests name the +exact `ContentId`, a `CanonicalCatalogPath`, and one catalog range; Stream +Install requests name the exact `ContentId`, and all path-bearing Stream Install +frames, including archive names, are canonical typed paths. The receiver's +immutable catalog remains the only authority for expected bytes and layout. + +Call to Play is a collection of direct per-author slices, never a relayed +ledger. The responder serves only its local author snapshot; the receiver +attributes it to the pinned peer. A typed `CallId` binds creator identity to a +fresh nonce, while the core generates event IDs and timestamps and the snapshot +owns the display name. A higher same-session revision replaces the whole slice; +new sessions clear the old domain, invalid same-session candidates preserve the +last valid slice, and invalid new-session candidates leave it absent. +Participant events remain hidden until a direct creator slice is present. +Unresolved and terminal histories expire as whole calls after five and 15 +minutes respectively, without tombstones. + +Wire and scheduler limits are part of the authority boundary. Strict fallible +control decoding rejects unknown/noncanonical shapes, v7 frames, extra frames, +oversized frames, and duplicate or unsorted library IDs. The state-sync queue +and tracked-peer table are capped at 64, with at most eight pulls and eight hint +sends in flight. Discovery caps both queued records and negotiations at 64. The +server caps unauthenticated handshakes and established connections at 64, caps +each connection at 32 control streams, accepts exactly one bounded request frame +plus EOF, and drains all started work on shutdown. + +This decision fully supersedes “Bound protocol-7 download descriptions +generously” and “Keep Phase 2 catalog authority behind one protocol-7 adapter.” +It supersedes only the protocol/ALPN and removed-message clauses in “Pin +outbound responders without changing protocol 7,” “Own shared QUIC endpoints up +to the provider boundary,” “Isolate the peer runtime behind a joinable +supervisor thread,” and “Await fanout and directory transitions through +publication”; their endpoint-authentication, structured-lifetime, and directory +admission decisions remain current. + +Alternatives: + +- Retain protocol 7 as a fallback. This would create a second authority model + and contradict the current-only protocol policy. +- Put revisions or state in mDNS. The advertiser is static after registration, + and an unauthenticated record must never become roster or state authority. +- Push deltas or Call-to-Play events directly. This multiplies ordering, + acknowledgement, relay, and healing paths that a coalesced full replacement + avoids. +- Dial by address or accept author IDs from payloads. Either would separate the + data authority from the identity proven by the responder certificate. +- Reuse a repository certificate and private key. Every installation would + possess the same signing authority and could not authenticate a peer identity. + +## 2026-08-10 — Index catalog content identity without loading manifest bodies + +**TL;DR:** Every packaged catalog has one mandatory compact, exact-coverage +content index. Remote availability can be joined to local `ContentId` and Stream +Install policy without disk access, while every lazily loaded manifest must +reproduce the indexed values. + +`game.db` remains the catalog ID/version authority. Its sibling +`manifests/catalog-content-index-v1.jsonl` contains exactly one entry for every +database row: exact game ID, exact version, catalog `ContentId`, and whether the +manifest has catalog-owned Stream Install output. The index is bounded, +canonical, portable-alias safe, and eagerly read once when `CatalogBundle` is +constructed. Missing, extra, version-skewed, malformed, noncanonical, linked, or +oversized index artifacts fail construction. + +Full manifest bodies remain on demand. Loading any body revalidates its complete +structure and recomputed content ID, then requires both that ID and its derived +Stream Install capability to equal the immutable index entry. There is no +fallback to body parsing when the index lacks an ID. Tauri and peer-CLI remote +availability joins use only this non-I/O identity lookup; body consumers such as +sender admission, download planning, and Stream Install verification retain the +full manifest path. Disk-backed bundle loads and full-corpus validation check +the durable publication marker both before and after filesystem work; the +explicit cache-only lookup remains an immutable, non-I/O snapshot. + +The publisher prepares and independently reproduces every selected package +before taking the durable publication marker. Once it owns that create-new +writer exclusion, it freshly loads the current index and validates every +unselected body against it before deriving a mixed index. It then writes the +selected bodies, atomically writes the complete index, validates the resulting +corpus, and removes the marker last. Full generation derives every entry from +the newly prepared bodies. Incremental generation requires an existing exact +indexed corpus and cannot bootstrap a partial catalog. Production build +validation still loads every body and proves that the database, index, and body +corpus agree exactly. + +Alternatives: + +- Parse a manifest body for every remote availability. A bounded authenticated + peer set could repeatedly force synchronous parsing and monotonic cache growth + in UI and CLI adapter paths. +- Preload every catalog manifest at peer startup. The accepted manifest bounds + make total decoded memory far larger than the compact identity data actually + needed for availability joins, and it would reverse the deliberate lazy-body + contract. +- Treat a missing index entry as permission to load the body. That creates a + second authority path and makes exact coverage unenforceable. diff --git a/organize/planning/PEER_AUTH_PLAN.md b/organize/planning/PEER_AUTH_PLAN.md index 03f7308..9737bcf 100644 --- a/organize/planning/PEER_AUTH_PLAN.md +++ b/organize/planning/PEER_AUTH_PLAN.md @@ -2,7 +2,18 @@ ## Status -Revised implementation plan; not yet implemented. +The code and local test surface described by Phases 1-5 are implemented in this +checkout. The standard Rust, Tauri, and frontend gates pass, and the current +fresh-image S1-S49 Docker matrix is recorded in +`organize/testing/PEER_CLI_SCENARIOS.md`. This is not yet full +production-release acceptance because three external evidence gates remain: + +- the canonical 186-game package corpus and production manifests are absent, so + the production catalog and bundle gates remain fail-closed; +- no run on a supported Windows/NTFS system yet proves reparse-point confinement + and file/directory durability; and +- no representative physical-LAN three-peer and throughput run has been + recorded. Docker-host throughput is local acceptance evidence only. This plan deliberately treats Lanspread as what it is: a desktop utility for friends and other attendees at a LAN party to discover each other, share a known @@ -48,7 +59,8 @@ bundled content manifest -> validated local download plan -> version.ini commit only after complete success local Call to Play change -> cheap invalidation hint to known peers - -> each peer pulls the author's current state over pinned TLS + -> each peer pulls the author's current state + over pinned TLS pinned liveness ping -> responder's own current revisions -> pull that one responder only on mismatch @@ -99,8 +111,8 @@ The following are explicit non-goals: ### 3.1 Confine download preparation first -This remains the first implementation task because it fixes a live local -data-loss path without depending on authentication or a wire change. +This was implemented first because it fixed a live local data-loss path without +depending on authentication or a wire change. The peer core constructs a `ValidatedDownloadManifest` before `begin_version_ini_transaction`, `prepare_game_storage`, directory creation, @@ -136,13 +148,10 @@ the authoritative manifest before committing `version.ini`. Preserve `local/`, install staging/backup state, and user-owned files in all success, failure, cancellation, and recovery paths. -For the current protocol, this validator safely contains the existing remote -descriptions. A narrow protocol-7 adapter requires and removes exactly one -matching leading `game_id/` component (and discards only the current exact -redundant game-root directory entry) before constructing root-relative paths; it -rejects a missing/different/doubled prefix. After the protocol cutover, the same -validated type is constructed directly from the bundled content manifest and -remote descriptions cease to define local paths at all. +Historically, this validator contained protocol-7 remote descriptions through a +narrow adapter that required and removed exactly one matching leading `game_id/` +component. The current protocol constructs the same validated type directly from +the bundled content manifest; remote descriptions no longer define local paths. Required proof includes hostile descriptors placed after valid descriptors, cross-game paths, both requested and other-game `local/` sentinels, reserved @@ -459,9 +468,8 @@ Do not ask the user to solve a cryptographic implementation problem. ## 4. One protocol cutover -Develop the pieces behind internal APIs, then replace protocol 7 with one new -current protocol (protocol 8 if the version has not moved). Do not ship -intermediate protocol 8/9/10 designs and do not add compatibility decoding. +The coordinated cutover replaced protocol 7 with current protocol 8. There is no +intermediate 8/9/10 design and no compatibility decoding. The cutover includes: diff --git a/organize/unsorted/CALL_TO_PLAY_FIXES_PLAN.md b/organize/unsorted/CALL_TO_PLAY_FIXES_PLAN.md index 79438ba..105bc45 100644 --- a/organize/unsorted/CALL_TO_PLAY_FIXES_PLAN.md +++ b/organize/unsorted/CALL_TO_PLAY_FIXES_PLAN.md @@ -1,5 +1,12 @@ # Finish Call to Play’s replication seam +> **Historical / superseded:** This plan describes the removed protocol-v7 +> client-authored, relayed event and acknowledgement model. The current protocol +> accepts local intents, generates identity and time fields in the core, pulls +> author-owned snapshots directly, and publishes full replacement views. Retain +> this document only as design history, not current architecture or acceptance +> guidance. + ## Summary Keep the existing architecture: immutable events, deterministic reduction, live diff --git a/organize/unsorted/CALL_TO_PLAY_REVIEW_FABLE_5_XHIGH.md b/organize/unsorted/CALL_TO_PLAY_REVIEW_FABLE_5_XHIGH.md index 58d27cb..952e6ab 100644 --- a/organize/unsorted/CALL_TO_PLAY_REVIEW_FABLE_5_XHIGH.md +++ b/organize/unsorted/CALL_TO_PLAY_REVIEW_FABLE_5_XHIGH.md @@ -1,5 +1,11 @@ # Call to Play review — Fable 5 (xhigh) +> **Historical / superseded:** This review assesses the removed protocol-v7 +> client-authored, relayed event and acknowledgement model. The current protocol +> accepts local intents, generates identity and time fields in the core, pulls +> author-owned snapshots directly, and publishes full replacement views. Its +> conclusions and test counts are not current acceptance evidence. + ## Verdict The plan is faithfully implemented — all five commits match the planned @@ -119,8 +125,9 @@ installed) would remove the most awkward step in the happy path. Worth a follow-up commit if you agree. Two nits: `design/launcher/SPEC.md` still describes the ticker sort as "ready → -starting-soon → the rest (TICKER_RANK = ready 0, soon 1…)" while the code ranks -`expired` first — that mismatch predates this branch, but since the spec section -was touched anyway it could have been corrected. And the ticker's "waiting to -start" line for Ready calls doesn't say _who_ everyone is waiting for, while the -card note does name the creator — a tiny inconsistency, fine as is. +starting-soon → the rest (`TICKER_RANK` = ready 0, soon 1…)" while the code +ranks `expired` first — that mismatch predates this branch, but since the spec +section was touched anyway it could have been corrected. And the ticker's +"waiting to start" line for Ready calls doesn't say _who_ everyone is waiting +for, while the card note does name the creator — a tiny inconsistency, fine as +is. diff --git a/organize/unsorted/CALL_TO_PLAY_REVIEW_GEMINI_3.6_FLASH.md b/organize/unsorted/CALL_TO_PLAY_REVIEW_GEMINI_3.6_FLASH.md index d6fab02..f245bbe 100644 --- a/organize/unsorted/CALL_TO_PLAY_REVIEW_GEMINI_3.6_FLASH.md +++ b/organize/unsorted/CALL_TO_PLAY_REVIEW_GEMINI_3.6_FLASH.md @@ -1,5 +1,14 @@ # Call to Play Code & Architecture Review Report + + + +> **Historical / superseded:** This review assesses the removed protocol-v7 +> client-authored, relayed event and acknowledgement model. The current protocol +> accepts local intents, generates identity and time fields in the core, pulls +> author-owned snapshots directly, and publishes full replacement views. Its +> conclusions are not current architecture or acceptance evidence. + I have conducted a thorough review of the commits (`e141229` through `2c204ac`) on branch `calltoplay`, referencing [`FABLE_5_FINDINGS.md`](file:///pantheon/pfs/git/rust/pfs/lanspread/FABLE_5_FINDINGS.md) diff --git a/organize/unsorted/CALL_TO_PLAY_REVIEW_KIMI_K3.md b/organize/unsorted/CALL_TO_PLAY_REVIEW_KIMI_K3.md index eab2bdf..c66a888 100644 --- a/organize/unsorted/CALL_TO_PLAY_REVIEW_KIMI_K3.md +++ b/organize/unsorted/CALL_TO_PLAY_REVIEW_KIMI_K3.md @@ -1,5 +1,11 @@ # Review: Call to Play fix series (`872692e..2c204ac`) +> **Historical / superseded:** This review assesses the removed protocol-v7 +> client-authored, relayed event and acknowledgement model. The current protocol +> accepts local intents, generates identity and time fields in the core, pulls +> author-owned snapshots directly, and publishes full replacement views. Its +> conclusions are not current architecture or acceptance evidence. + ## a) Faithfulness to the plan — high, with only minor test-plan gaps **Commit mapping is 1:1 with the planned sequence**, same titles, and every diff --git a/organize/unsorted/FABLE_5_FINDINGS.md b/organize/unsorted/FABLE_5_FINDINGS.md index 5ae2423..242adab 100644 --- a/organize/unsorted/FABLE_5_FINDINGS.md +++ b/organize/unsorted/FABLE_5_FINDINGS.md @@ -1,5 +1,11 @@ # Fable 5 findings +> **Historical / superseded:** These findings assess the removed protocol-v7 +> shared-certificate, client-authored, relayed event model. The current protocol +> authenticates typed peer identities, accepts local intents, generates event +> authority in the core, and pulls author-owned snapshots directly. Retain this +> document only as historical review context, not a current issue list. + Review of the issues Fable found after the Call to Play follow-up fixes. This document records the assessment only; it is not an implementation plan. diff --git a/organize/unsorted/FINDINGS.md b/organize/unsorted/FINDINGS.md index 93f1073..117c613 100644 --- a/organize/unsorted/FINDINGS.md +++ b/organize/unsorted/FINDINGS.md @@ -28,10 +28,10 @@ The previous four findings have landed in code and tests: startup recovery remove only exact downloader-owned paths and preserve unknown root files, instead of leaving crashed partial archives or broadly deleting the game root. -- `update_game` now uses `PeerCommand::FetchLatestFromPeers` to skip local - manifest serving and fetch fresh peer metadata. Covered by - `update_fetch_emits_fresh_manifest_from_latest_peer` and - `update_request_skips_local_manifest_even_when_download_exists`. +- Historical, superseded by the protocol-v8 cutover: `update_game` once used + `PeerCommand::FetchLatestFromPeers` and fresh peer metadata. That command and + its cited tests no longer exist; updates now use the local catalog manifest + and authenticated peers advertising its exact `ContentId`. - Download-to-install handoff no longer relies on `OperationGuard::Drop` for ordered state transitions. Covered by `download_handoff_waits_for_readers_and_auto_installs` and the liveness