## W3 — adapters travel between subnet peers (releases#322 + releases#264, milestone #331)

A node now hands the adapters it installed to its subnet peers, and a node updating an adapter asks those peers first.

- **Roster** (REQ-ADAPTER-PEER-ROSTER): the node-level registry record every node already sends each round carries an additive, serde-default `adapters` list (name, kind, version, retained `.spt` sha256, declared `signing_key`). Older peers ignore the field. It is snapshotted, so the CLI (and the W8 "available on subnet" list) read who holds what without dialing.
- **Serve** (REQ-ADAPTER-PEER-SERVE): every install/update keeps the archive as `releases/adapters/<name>.spt` plus a sidecar (version, kind, sha256, the verified detached signature, install source, trust anchor). `AdapterQuery` gets a FRESH `AdapterOffer` from the sidecar; the bytes go over W1's `FetchAsset adapter:<name>`. Same roster gate and one refusal shape as every update-family stream. `adapter_query` is classified into the update family in `dispatch.rs` (without that a real daemon drops it as a stranger stream). Serve-side reads are size-checked before they happen (`ASSET_SERVE_MAX_BYTES`): a peer now triggers the read. `adapter remove` stops the node serving the adapter.
- **Verify** (REQ-ADAPTER-PEER-VERIFY): see the trust argument below.
- **`spt adapter add <name> --via-subnet` / `-vs`** (REQ-ADAPTER-VIA-SUBNET): peers only, highest version first. It is recorded as `install_source = subnet`, so the adapter keeps updating over peers even if it declares no release avenue. `-vs` is rewritten to `--via-subnet` before clap sees it, and only on an `adapter add` line (clap short flags are one character).
- **Peers first** (REQ-ADAPTER-PEERS-FIRST): `adapter update`, `update adapters` and the composite adapters leg ask peers holding a NEWER copy before the declared channel. `--remote` goes straight to the channel.

### Trust argument (against docs/MANIFEST.md "Trust — optional signing, fail-closed")

A peer only relays the bytes. `request_asset` has no digest gate and ignores `Done{total}`, so `pull_peer_adapter` adds one. Its reassembly is capped at 256 MiB. Nothing is staged or extracted until all of these hold:
1. The offer names the adapter that was asked for.
2. The bytes hash to the offer's sha256. This catches torn, raced and substituted transfers.
3. The key anchor:
   - **Update:** the anchor is the key in the INSTALLED manifest, and the peer must offer that same key. A key change can never arrive through a peer; rotation still comes through the release channel.
   - **First install (`--via-subnet`):** the anchor is the key the peer offers (trust on first use). This is **loud**: `ADAPTER_INSTALL_TRUST:<name>: signing key <id> adopted on first install from peer <label>`. It is also recorded in the retained sidecar's `trust_anchor`, so it can be shown later. W8 renders it in `adapter list`.
   - When a key applies, the detached signature must be present and must verify.
4. After a scratch extract, the archive's own manifest must carry the offered name and version, and must declare the anchor key.
5. No key anywhere: the only trust is the handshake-authenticated roster peer. That is the trust the subnet already gives registry rows and messages, and it is weaker than HTTPS + GitHub. It is said on stderr at install (`ADAPTER_INSTALL_UNSIGNED`) and at update (`ADAPTER_UPDATE_UNSIGNED … trusting authenticated subnet peer <label>`), so a subnet install is never quieter than a repo install.

### Peer vs channel (condition d)

- A peer copy is taken only when its version is NEWER than the installed version. This is checked on the gossiped row and again on the fresh offer, so nothing ever downgrades (the W5 shape).
- Peers are tried highest version first.
- When any peer serves a newer copy that verifies, the channel is not asked on that run, even if the channel has something newer still. The next run asks the channel again once no peer is ahead. So a node can take an intermediate version from a peer and converge on the following run. That is the CONTEXT "update source" order: peers before the release channel.
- A peer miss, refusal, reject or dial failure falls through to the channel on the same run.

### W2 riders (why they are here)

- REQ-ADAPTER-UPDATE-PARALLEL, REQ-ADAPTER-UPDATE-PRUNES-STRINGS and REQ-ADAPTER-ENTRY-EXEC-BIT join their `[[groups]]`, as TRACEABILITY rule 7 requires. #252 was mid-CI when this was noticed, and re-pushing it would have cost a full lane for no product change (doyle's ruling).
- F2: the PARALLEL title now states the evidence the int actually gates on (state, not a stopwatch).
- F1: `entry_exec` containment now compares canonical paths. `{adapter_dir}/../x` passed the lexical `starts_with` and was chmod'ed; now it is not treated as an entry.

### Evidence
- unit: spt-net (wire round-trip, additive roster, lease), spt-daemon (relcache retain/roster/cap, verify refusals and anchors, dispatch classify), spt-runtime (containment), spt (route table, `-vs` rewrite, `--remote` parse, archive identity, trust words, holders).
- int: `crates/spt/tests/adapter_peer_e2e.rs` — ONE test function (`b_installs_and_updates_an_adapter_from_peer_a_before_the_channel`) on two real daemons driven through the CLI, running four SEQUENTIAL steps in order: (1) `add -vs` from A; (2) peers-first update with the channel rigged to claim a newer version from an unreachable repo; (3) `--remote` skips a peer that is ahead; (4) a tampered offer is rejected loud and falls through. The steps share one rig and are not independent evidence: an early step failing masks every later one. It is not in the thin lane (a `--test` binary, outside `kind(lib)+kind(bin)`), so it was run by hand: negative control first (step-1 assert broken to expect `rig-WRONG` → FAIL exit 100 at adapter_peer_e2e.rs:350 with real product stderr), then restored → PASS 9.27s; the gater re-ran it independently → PASS 8.61s.

Co-authored by: todlando

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_014j8UXzGXXsDsTJBYYUSCTC
