# WEBSERVE W2 — attachments, `spt fetch`, message short-IDs, entry audience Closes releases#246 and releases#147, and carries the releases#17 rider. Base: `ff4b405d` (rebased onto the landed #198). Four requirements minted for the wave plus one minted in the lane (`REQ-WEB-ENTRY-AUDIENCE` — the per-entry narrowing needed its own requirement because ENFORCEMENT lives on the WEB gate, not inside a now-signal category). =====DIFF LINES===== 1,358c1,358 < # WEBSERVE W2 — attachments, `spt fetch`, message short-IDs, entry audience < < Closes releases#246 and releases#147, and carries the releases#17 rider. < Base: `bfb5d58a`. Four requirements minted for the wave plus one minted in the < lane (`REQ-WEB-ENTRY-AUDIENCE` — the per-entry narrowing needed its own < requirement because ENFORCEMENT lives on the WEB gate, not inside a now-signal < category). < < ## What lands < < **Attachments are pull-model** (ADR-0058). `spt send --attachment` snapshots the < bytes at send time into the node's own store, registers an entry, and the < message carries the URL and the size — never the bytes. The snapshot is the one < place the serving registry does not resolve at request time, and that is what < makes a message's attachment as immutable as the message: an edit after the send < does not change what the receiver pulls, and deleting the source does not turn a < delivered link into a dead one. Lifetime is a TTL, 30 days by default, and an < expired entry stops serving the moment it expires rather than when the reaper < next runs. < < **`spt fetch`** is the one-command pull, and its exit codes separate the two < things a caller must not confuse: `3` is the owner's ACCESS DECISION (retrying < is wrong), `1` is a state that may change. The body streams to a temp file and < is renamed into place, so an interrupted fetch never leaves a truncated file < where a later reader would trust it. < < **Message short-IDs** (ADR-0061) are minted at commit, not at render, so ONE < token names a message in the delivery envelope, in both io-event rows, and at < `//m/`. `spt msg show` and the `m/` facet resolve through the same < function, so a link and a command cannot disagree. < < **Per-entry `audience`** narrows an entry to one endpoint, enforced where the < fetch origin is proven, with the limit stated rather than implied: the handshake < proves a NODE and an audience names an ENDPOINT, so the check is whether the < proven node hosts that endpoint — it stops another machine, not another agent on < the audience's own machine. < < **The FILE_ACCESS_HELPER** hands an agent the exact `spt fetch` line for a file < it was given, from either trigger: an attachment on a delivered message, or a < filepath the user quoted. The remote arm — a user on another node — registers on < the agent's behalf over the same stream family the proxy uses. < < ## The four refusals on `serve_for`, in the order they run < < A register-on-my-behalf request asks a node to EXPOSE a file. It is refused < unless all four hold, cheapest first: < < 1. **The `WEB` access gate at node scope.** There is no entry yet to take a < subject from, and asking a node to expose a file is at least as consequential < as asking it to serve one. < 2. **The requester may only ask on behalf of an endpoint IT HOSTS.** The < handshake proves the requesting node; `audience` names an endpoint; if this < node cannot place that endpoint on the proven node, the request is refused. < Without this, a third node could ask us to expose a user's file to somebody < else entirely. < 3. **The user's authority** (gate finding F1). This node's own `MSG_OUT` row for < the origin short-ID must exist, be a `user-msg`, be addressed to exactly this < audience, and contain this exact path in the user's own words. **1 and 2 < alone are not enough**: `WEB` is default-on inside a subnet, so 1 admits every < member and 2 only proves the audience lives where the asker says — together < they would let any subnet node have this one expose any absolute path on it. < Every fact in 3 is read from the owner's own record, so the requester can < forge none of it. < The conjuncts themselves are `spt_store::msgid::user_authorized_serve_for` < — pure, one unit cell each; `serve_for` looks the row up and calls it. < 4. **The path rules `serve add` already enforces**, unchanged. < < 1, 2 and 3 all answer with `deny_message()` — the same 403 body a surface deny < produces — so a narrowing cannot be told from a denial by probing. < < ## Two design decisions worth reading < < **The helper's URLs are NOT written into the delivered envelope.** The obvious < carrier was the message's own `attachments` attribute, so the existing < attachment trigger would read them with no second arm. That is a forgery: < `attachments`, `msg-id` and `reply-to` are the sender-authored class this PR < pins in a proto cell, so a receiving node writing one would hand its agent a < link the sender never minted, wearing the sender's voice. The carrier is a < receiver-side record (`spt_store::helperline`) instead, and three cells hold the < line. < < **The int arm for the remote helper was rewritten, not extended.** Its previous < positive asked with an origin the owner had never sent, and it PASSED — because < the F1 binding did not exist yet. That old green was the hole. < < The rewrite went further than the binding, because closing the hole exposed a < second one. The replacement arm drove `request_serve_for` **from the test < process**, and it did not run: it ate the brain IPC read deadline at the first < call. That client lives INSIDE the receiving daemon's WAN ingress in < production — the same place the fetch path's client lives — so the arm was < driving a client that does not exist. *A deadline I would have to widen to make < my own test shape work is the test being wrong about where the client lives.* < < So the arm is now driven the only way production drives it: one real < `spt send --user-msg` from a Gateway-typed endpoint on B to an endpoint hosted < on A, quoting an absolute path that exists on B, and then **waited for on A's < helperline store**. That wait is the observable — it can only be satisfied by < the ingress caller firing after delivery and completing a daemon-to-daemon < round trip. The Gateway type is not decoration: WAN ingress re-stamps an < arriving `user-msg` down to a plain `msg` unless the origin endpoint is < advertised as a Gateway (REQ-MSG-6), and the arm asserts the send was not < degraded before it asserts anything that depends on it. < < **The five F1 conjuncts had NO evidence outside the arm being reshaped.** That < is a measurement, not an impression: `msgid`'s existing cells cover < `local_msg_out_in`'s LOOKUP — found, not found, nested perch — and a grep across < every crate finds the conjuncts themselves named in exactly one place, the int < arm. They sat inline in the daemon's `serve_for`, reachable only through a QUIC < round trip. So reshaping that arm would have taken the gate finding's own fix < down to zero evidence, which is why the conjuncts moved rather than shrank: < they are now `spt_store::msgid::user_authorized_serve_for`, PURE, taking nothing < the requester sent except the `audience` and `path` the request is about, with < one unit cell per conjunct asserting both the refusal and the admit side of the < same inputs. `serve_for` looks the row up and calls it where conjunct 3 always < sat. The extraction is a fix, not a scope cut. < < Four of the five cannot be produced end to end AT ALL — the receiving daemon < only ever asks for a path the user's own words carried, addressed to that < message's own target, under that message's own id — so the int arm keeps the < one that can: an agent-authored message quoting the same path, which is a < DIFFERENT gate (the receiver's own "only a `user-msg` triggers this") and has < no other evidence. Its zero is judged after the positive raises the same < counters through the same pipe, so it cannot be the zero of a dead emitter. < < ## Two defects the battery found, closed in-lane < < Both are product defects this PR introduced and this PR fixes; neither is a test < accommodation. < < **The quoted-path extractor trimmed once each in a fixed order.** A path written < the way people write them — `see "C:\x\notes.md".` — carries a closing quote < INSIDE the sentence dot, so one pass of quote-then-dot left the quote attached < and minted a URL naming a file that does not exist. It now trims to a FIXED < POINT. Measured before the fix: the token came back as `C:\x\notes.md"`. < < **A torn helperline tail fused with the next record.** A crash mid-append leaves < a fragment with no newline; appending straight onto it produced one unparseable < line out of two records, so a single interruption cost TWO records rather than < one. `append_at` now closes a torn tail with a newline before writing. The cell < asserting that a torn line costs only itself was red until this existed — and < the module's own doc had already claimed the property the code lacked. < < ## Where the helper's evidence lives — read this before the gate table < < Three witnesses. **None of them proves what another proves**, and saying so is < the point of this section: the coverage is narrower than a reader would assume < from the requirement's wording, and that is a fact about the rigs, not a < shortfall to be papered over. < < 1. **Gossip converges** — the preserved patch < `.spt/preserved/pump-on-one-box-rig.patch`. Node A learned node B's < Gateway-typed endpoint in **751 ms, identical across two invocations**, with < the reachable set measured at one peer inside the pair. This is the estate's < first datum that a pump-on `spt daemon run` rig works at all; every other rig < pins `peer_pump: false` and calls it the hermetic kill switch. It is NOT on < the lane, for the reason in the next paragraph. < 2. **The daemon-to-daemon round trip** — `twohost_web`'s two new cells, run on < the local pair and climbed cross-box by the gate. B's user quotes a path, B < registers it on its own user's authority, A's ingress fires after delivery, < and A pulls the file byte-equal. The Gateway ROW is seeded in-process here, < through `apply_feed_flips` under the real gate policy — this rig runs no pump < either. < 3. **Both together** — the gate's field leg on the operator-paired rig, two real < hostnames and real gossip. Field evidence, not an `int` tag. < < **The coverage sentence**, checked against the lanes rather than asserted: < < - **Thin CI** carries the five predicate cells and **nothing cross-node**. This < changed under this lane's feet and the sentence is written for the sha this PR < carries, not the one it was drafted on: #198 added `webserve_cross_node_e2e` < to BOTH `HEAVY` strings, so arm (iii) is **golden-only** now too. A PR that is < green has exercised the five predicate cells and no cross-node arm at all. < - **Golden only, and only two steps of it**, carries the twohost positive: < `twohost-web-b` (Linux, role B) and `twohost-web-a` (Windows, role A), each an < explicit `cargo test -p spt-daemon --test twohost_web`. Those cells are < env-gated on `SPT_TWO_HOST=1` plus a role, so everywhere else they are a < silent no-op — including in the `HEAVY` group that names the binary. < - **Golden also carries arm (iii)**, via `HEAVY`'s serialized phase. < - **No single run carries gossip and the round trip together.** That pairing < exists only in the field leg. < < `webserve_attachment_e2e` is in both `HEAVY` strings by **hand**, and that hand < is load-bearing rather than belt-and-braces. #198's < `check_heavy_integration_classification` derives the qualifying set from a source < grep, and measured against its own predicate this binary scores **zero** on all < three literals: it autostarts its daemon through ordinary verbs (`send`, < `serve list`, `fetch`). The only daemon literal it carries is `["daemon", < "stop"]` — its reap. So the checker is blind to it, nothing classes it < automatically, and dropping the name silently returns a daemon-spawning binary < to the full-parallel pool. < < So a PR that is green has not exercised the cross-node helper's positive at all. < That is the sentence I would want to read before trusting this milestone's < `int` tag, which is why it is here rather than in a commit message. < < **Why not one rig.** Gossip-on and node-label routing are **mutually exclusive < on one box**. `webserve::known_subnet_node` prefers the registry-ADVERTISED < label over the roster's; `RegistryHost::new` sets that label from < `os_hostname()`; one box gives both daemons one hostname; and the prefix < dispatcher takes the LOCAL arm whenever the first segment equals the local < node's label. With the pump on, every peer-prefixed URL is answered locally and < arm 1 returns `404 NO_DOCS_LANDED`. There is no label override to escape through < (no `SPT_NODE_LABEL`, no `daemon.json` field; `COMPUTERNAME` is Windows-only and < `gethostname` has no hook), and label matching has no node-hex leg. Two hosts < carry two hostnames and the whole problem evaporates. < < **A meter I built wrong, on the record.** The first version of the hermeticism < assertion read node A alone, 751 ms in, saw an empty set, and would have < reported the rig hermetic. A had learned from B's push and had not yet reached < its own cadence tick — so that zero was the zero of a meter that had not ticked, < not the zero of a quiet network. A guard against a false green produced a false < green. It now reads both daemons and refuses to judge the set until it is < non-empty, so the meter proves its own liveness before it is allowed to prove < anything else. < < ## Rig isolation, and one product seam found underneath it < < The attachment rig was being answered by **this box's resident fleet daemon**. < It mints a URL, the URL named port 5474, and the 404 that came back was the live < daemon truthfully reporting an entry it had never heard of. < < `SPT_TEST_EPHEMERAL_ADVISORY_PORTS=1` does not fix that, and the reason is a < product seam: `serveverb::node_and_port` mints the port from `DaemonConfig` / < `SPT_DOCS_PORT` and never asks what the listener actually **bound**, so under the < switch the daemon moves and the URL does not — **releases#282**, ruled a defect, < fixed outside this lane. < < Pinning a fixed port then exposed the sharper half: the rig autostarts a daemon < and never stopped it, so the next run was answered by the **previous run's** < daemon — same port, different `SPT_HOME`, registry right and surface wrong. < `netstat` named the holder. The rig now picks its port per run and reaps its own < daemon on every exit path, panic included, because leaving a daemon behind is < what makes a fixed port unsafe. < < ## The `MSG_NOT_FOUND` defect, and why one string was the second defect < < `spt msg show ` answered `MSG_NOT_FOUND` for an id read out of the funnel's < own `MSG_OUT` row. The cause is measured, not inferred, and it is bigger than < the arm that found it: **no spool row has ever carried a `short_id`.** < < Three greps, each falsifiable: < < - `spool_message_identified_at` and `IdentifiedInsert` — the only API that < accepted an identity — have **zero callers** anywhere in the tree. The < definitions are the only hits. < - The one live insert path, `insert_message`, passed `identity: None` as a < literal. < - No `UPDATE` anywhere sets `short_id`; the five `UPDATE messages` sites all < touch `delivered` / `taken_leg` / `body`. < < So `resolve_in`'s spool leg was **unreachable in production by construction**. < This also refutes the shape I first reported it in: I had it as "the spool path < drops the id when the target is OFFLINE." It drops it always. Offline was where < I happened to be standing. < < **The fix is one insert path, not two.** The identity is now derived at the one < `INSERT` from the envelope's own `msg-id` attribute, which is already in the < bytes being inserted — the id is minted before the envelope is composed < (ADR-0061). That is sound because `msg-id` and `reply-to` are sender-authored < and survive the receiver's ingress strip, which `spt-proto` already pins in < `the_w2_message_attrs_are_sender_authored_and_survive_the_strip`. A plain < non-envelope body parses to nothing and stores NULL, which is the honest answer < for a row that never had an id. The dead `spool_message_identified_at` / < `IdentifiedInsert` / `MessageIdentity` trio is **deleted** rather than left < beside the live path, because one of the two was always going to be the one < somebody read. < < **The single string was a defect in its own right.** `MSG_NOT_FOUND` covered < three different misses with three different fixes — no index row, a spool row < with a NULL id, no `MSG_OUT` in the log the resolver read — and it sent me at < the wrong half twice. `resolve_detail_in` now answers a miss with `ResolveMiss`, < and every count is carried **beside the total it came from**: `0 of 7` names a < mechanism, a bare `0` cannot be told apart from "there is no spool here". The < public code is unchanged — `MSG_NOT_FOUND`, same exit code — and the three < `MSG_MISS_*` lines are stderr breadcrumbs of the `F1_ARM` class, said so in the < enum's own doc comment so nobody promotes them into a contract later. The `m/` < facet's 404 body is likewise unchanged; a fetcher is a stranger and the counts < describe this node's stores, so they go to the daemon's stderr. < < ## The two-host pair run, and the three rig defects under it < < The `twohost_web` helper cell had never been compiled, let alone run. Five < attempts on the local pair, each red closed at a **named mechanism**: < < 1. **`RegistryUpdate` in the wrong module** — the type lives in < `spt_net::net::replicate`, not `::registry`. One line. < 2. **The cell reached for AMBIENT stores.** `RegistryGatePolicy::load()`, < `perch::epoch_file()` and `presence::registry_snapshot_dir()` all resolve < through the process-global `SPT_HOME`, which the runner unsets — so a role A < cell was answered by **this box's real fleet home**, the policy carried none < of the rig's subnets, and the merge returned `NotMember`. Role B can use < those calls because it sets `SPT_HOME` at its own head; role A never had. < The cell now claims its own `SPT_HOME` and calls the **product's** < constructors unmodified — no rig copy of how a daemon builds its registry < host. The proof is in the log rather than in this paragraph: the cell used to < print `PAIR_MEET_UP` for `SPT_DEV`, `BIGNET` and `SPT_MANTLE` — the fleet's < subnets — and now prints `PAIR_MEET_UP:twohost` and nothing else. < 3. **A false green one line before the real failure.** B's send loop broke on < any `Ok`, so a dialled-and-refused `NoPerch` printed "the user's message < reached A (NoPerch)" and walked into a 240 s wait for a registration that < could not come. The break condition is now the product's own < `took_custody()`. That predicate choice was load-bearing in the other < direction too: the honest success here is `Spooled`, not `Delivered` — the < audience endpoint is recorded but not listening — so a hand-written < `Delivered`-only list would have hung on the correct outcome. < < Under those three sat the real one. With the message ADMITTED, A's ingress < **did** fire the helper and died at the last hop: < < ``` < TWOHOST-WEB role B: the user's message was ADMITTED by A (Spooled) < HELPER_SERVE_FOR: path=…\quoted-by-the-user.md outcome=unanswered < reason=no daemon to proxy through: The system cannot find the file specified. (os error 2) < ``` < < That log pair is also the evidence for checklist **(b)**: an absent proxy < neither delayed nor failed the delivery. The cause is a rig gap, not a product < defect — `request_quoted_paths` dials `endpoint::seed_socket_name()` < (`spt-daemon-seed-{home_tag}.sock`, the node's canonical daemon socket) while < the cell answered on an invented rig name. Since the name derives from the home < and the cell now owns its home, the helper cell takes its broker name from the < product's own function; the other three A cells keep rig names, because nothing < dials into them. < < **Isolation is measured, not asserted.** Setting `SPT_HOME` inside a cell is < only safe if nextest gives each `#[test]` its own process, so every A cell's < listener breadcrumb now prints its pid, and the run reports **four distinct < pids** for four cells. < < **One measured limit, recorded for the field run.** A nextest `TIMEOUT` does not < always kill the cell. Attempt 4's role A sat **14 minutes** past its last < verdict at **0.61 CPU-seconds** — idle, not working — because a timed-out cell < left one live `twohost_web` child holding its broker threads and nextest blocked < on it; `a.exit` was never written while that child lived, so a finished run < looked hung and every verdict was already sitting in `a.raw`. Diagnose a hung < pair lane by **CPU time and an orphan census**, before reasoning about the < network. The rig-wide Drop-guard fix for thread-holding cells belongs to the < `IR-79` reaper lane, not here. < < ## Order of work < < The remote arm (#17's cross-node registration) was built BEFORE the battery, on < the gater's order, rather than deferred: it was the one piece written after the < compiler was available, precisely because its only honest proof is the two-host < rig. < < ## Gates < < - `cargo check --workspace --all-targets` — 0 < - `cargo clippy --workspace --all-targets -- -D warnings` — 0, and zero warnings < - `cargo run -p xtask -- check` (docs drift) — 0, with `reference.md` regenerated < - `traceable-reqs check` — 0 findings; every W2 requirement carries doc, impl, < unit and int < - Targeted battery: see the run recorded on the PR. < < Clippy findings were fixed at the mechanism, never suppressed: two 8-argument < spool functions became a value, a wrapper that went dead when the extras axis < landed was collapsed into one gate, and the lints in my own cells were fixed. --- > # WEBSERVE W2 — attachments, `spt fetch`, message short-IDs, entry audience > > Closes releases#246 and releases#147, and carries the releases#17 rider. > Base: `ff4b405d` (rebased onto the landed #198). Four requirements minted for the wave plus one minted in the > lane (`REQ-WEB-ENTRY-AUDIENCE` — the per-entry narrowing needed its own > requirement because ENFORCEMENT lives on the WEB gate, not inside a now-signal > category). > > ## What lands > > **Attachments are pull-model** (ADR-0058). `spt send --attachment` snapshots the > bytes at send time into the node's own store, registers an entry, and the > message carries the URL and the size — never the bytes. The snapshot is the one > place the serving registry does not resolve at request time, and that is what > makes a message's attachment as immutable as the message: an edit after the send > does not change what the receiver pulls, and deleting the source does not turn a > delivered link into a dead one. Lifetime is a TTL, 30 days by default, and an > expired entry stops serving the moment it expires rather than when the reaper > next runs. > > **`spt fetch`** is the one-command pull, and its exit codes separate the two > things a caller must not confuse: `3` is the owner's ACCESS DECISION (retrying > is wrong), `1` is a state that may change. The body streams to a temp file and > is renamed into place, so an interrupted fetch never leaves a truncated file > where a later reader would trust it. > > **Message short-IDs** (ADR-0061) are minted at commit, not at render, so ONE > token names a message in the delivery envelope, in both io-event rows, and at > `//m/`. `spt msg show` and the `m/` facet resolve through the same > function, so a link and a command cannot disagree. > > **Per-entry `audience`** narrows an entry to one endpoint, enforced where the > fetch origin is proven, with the limit stated rather than implied: the handshake > proves a NODE and an audience names an ENDPOINT, so the check is whether the > proven node hosts that endpoint — it stops another machine, not another agent on > the audience's own machine. > > **The FILE_ACCESS_HELPER** hands an agent the exact `spt fetch` line for a file > it was given, from either trigger: an attachment on a delivered message, or a > filepath the user quoted. The remote arm — a user on another node — registers on > the agent's behalf over the same stream family the proxy uses. > > ## The four refusals on `serve_for`, in the order they run > > A register-on-my-behalf request asks a node to EXPOSE a file. It is refused > unless all four hold, cheapest first: > > 1. **The `WEB` access gate at node scope.** There is no entry yet to take a > subject from, and asking a node to expose a file is at least as consequential > as asking it to serve one. > 2. **The requester may only ask on behalf of an endpoint IT HOSTS.** The > handshake proves the requesting node; `audience` names an endpoint; if this > node cannot place that endpoint on the proven node, the request is refused. > Without this, a third node could ask us to expose a user's file to somebody > else entirely. > 3. **The user's authority** (gate finding F1). This node's own `MSG_OUT` row for > the origin short-ID must exist, be a `user-msg`, be addressed to exactly this > audience, and contain this exact path in the user's own words. **1 and 2 > alone are not enough**: `WEB` is default-on inside a subnet, so 1 admits every > member and 2 only proves the audience lives where the asker says — together > they would let any subnet node have this one expose any absolute path on it. > Every fact in 3 is read from the owner's own record, so the requester can > forge none of it. > The conjuncts themselves are `spt_store::msgid::user_authorized_serve_for` > — pure, one unit cell each; `serve_for` looks the row up and calls it. > 4. **The path rules `serve add` already enforces**, unchanged. > > 1, 2 and 3 all answer with `deny_message()` — the same 403 body a surface deny > produces — so a narrowing cannot be told from a denial by probing. > > ## Two design decisions worth reading > > **The helper's URLs are NOT written into the delivered envelope.** The obvious > carrier was the message's own `attachments` attribute, so the existing > attachment trigger would read them with no second arm. That is a forgery: > `attachments`, `msg-id` and `reply-to` are the sender-authored class this PR > pins in a proto cell, so a receiving node writing one would hand its agent a > link the sender never minted, wearing the sender's voice. The carrier is a > receiver-side record (`spt_store::helperline`) instead, and three cells hold the > line. > > **The int arm for the remote helper was rewritten, not extended.** Its previous > positive asked with an origin the owner had never sent, and it PASSED — because > the F1 binding did not exist yet. That old green was the hole. > > The rewrite went further than the binding, because closing the hole exposed a > second one. The replacement arm drove `request_serve_for` **from the test > process**, and it did not run: it ate the brain IPC read deadline at the first > call. That client lives INSIDE the receiving daemon's WAN ingress in > production — the same place the fetch path's client lives — so the arm was > driving a client that does not exist. *A deadline I would have to widen to make > my own test shape work is the test being wrong about where the client lives.* > > So the arm is now driven the only way production drives it: one real > `spt send --user-msg` from a Gateway-typed endpoint on B to an endpoint hosted > on A, quoting an absolute path that exists on B, and then **waited for on A's > helperline store**. That wait is the observable — it can only be satisfied by > the ingress caller firing after delivery and completing a daemon-to-daemon > round trip. The Gateway type is not decoration: WAN ingress re-stamps an > arriving `user-msg` down to a plain `msg` unless the origin endpoint is > advertised as a Gateway (REQ-MSG-6), and the arm asserts the send was not > degraded before it asserts anything that depends on it. > > **The five F1 conjuncts had NO evidence outside the arm being reshaped.** That > is a measurement, not an impression: `msgid`'s existing cells cover > `local_msg_out_in`'s LOOKUP — found, not found, nested perch — and a grep across > every crate finds the conjuncts themselves named in exactly one place, the int > arm. They sat inline in the daemon's `serve_for`, reachable only through a QUIC > round trip. So reshaping that arm would have taken the gate finding's own fix > down to zero evidence, which is why the conjuncts moved rather than shrank: > they are now `spt_store::msgid::user_authorized_serve_for`, PURE, taking nothing > the requester sent except the `audience` and `path` the request is about, with > one unit cell per conjunct asserting both the refusal and the admit side of the > same inputs. `serve_for` looks the row up and calls it where conjunct 3 always > sat. The extraction is a fix, not a scope cut. > > Four of the five cannot be produced end to end AT ALL — the receiving daemon > only ever asks for a path the user's own words carried, addressed to that > message's own target, under that message's own id — so the int arm keeps the > one that can: an agent-authored message quoting the same path, which is a > DIFFERENT gate (the receiver's own "only a `user-msg` triggers this") and has > no other evidence. Its zero is judged after the positive raises the same > counters through the same pipe, so it cannot be the zero of a dead emitter. > > ## Two defects the battery found, closed in-lane > > Both are product defects this PR introduced and this PR fixes; neither is a test > accommodation. > > **The quoted-path extractor trimmed once each in a fixed order.** A path written > the way people write them — `see "C:\x\notes.md".` — carries a closing quote > INSIDE the sentence dot, so one pass of quote-then-dot left the quote attached > and minted a URL naming a file that does not exist. It now trims to a FIXED > POINT. Measured before the fix: the token came back as `C:\x\notes.md"`. > > **A torn helperline tail fused with the next record.** A crash mid-append leaves > a fragment with no newline; appending straight onto it produced one unparseable > line out of two records, so a single interruption cost TWO records rather than > one. `append_at` now closes a torn tail with a newline before writing. The cell > asserting that a torn line costs only itself was red until this existed — and > the module's own doc had already claimed the property the code lacked. > > ## Where the helper's evidence lives — read this before the gate table > > Three witnesses. **None of them proves what another proves**, and saying so is > the point of this section: the coverage is narrower than a reader would assume > from the requirement's wording, and that is a fact about the rigs, not a > shortfall to be papered over. > > 1. **Gossip converges** — the preserved patch > `.spt/preserved/pump-on-one-box-rig.patch`. Node A learned node B's > Gateway-typed endpoint in **751 ms, identical across two invocations**, with > the reachable set measured at one peer inside the pair. This is the estate's > first datum that a pump-on `spt daemon run` rig works at all; every other rig > pins `peer_pump: false` and calls it the hermetic kill switch. It is NOT on > the lane, for the reason in the next paragraph. > 2. **The daemon-to-daemon round trip** — `twohost_web`'s two new cells, run on > the local pair and climbed cross-box by the gate. B's user quotes a path, B > registers it on its own user's authority, A's ingress fires after delivery, > and A pulls the file byte-equal. The Gateway ROW is seeded in-process here, > through `apply_feed_flips` under the real gate policy — this rig runs no pump > either. > 3. **Both together** — the gate's field leg on the operator-paired rig, two real > hostnames and real gossip. Field evidence, not an `int` tag. > > **The coverage sentence**, checked against the lanes rather than asserted: > > - **Thin CI** carries the five predicate cells and **nothing cross-node**. This > changed under this lane's feet and the sentence is written for the sha this PR > carries, not the one it was drafted on: #198 added `webserve_cross_node_e2e` > to BOTH `HEAVY` strings, so arm (iii) is **golden-only** now too. A PR that is > green has exercised the five predicate cells and no cross-node arm at all. > - **Golden only, and only two steps of it**, carries the twohost positive: > `twohost-web-b` (Linux, role B) and `twohost-web-a` (Windows, role A), each an > explicit `cargo test -p spt-daemon --test twohost_web`. Those cells are > env-gated on `SPT_TWO_HOST=1` plus a role, so everywhere else they are a > silent no-op — including in the `HEAVY` group that names the binary. > - **Golden also carries arm (iii)**, via `HEAVY`'s serialized phase. > - **No single run carries gossip and the round trip together.** That pairing > exists only in the field leg. > > `webserve_attachment_e2e` is in both `HEAVY` strings by **hand**, and that hand > is load-bearing rather than belt-and-braces. #198's > `check_heavy_integration_classification` derives the qualifying set from a source > grep, and measured against its own predicate this binary scores **zero** on all > three literals: it autostarts its daemon through ordinary verbs (`send`, > `serve list`, `fetch`). The only daemon literal it carries is `["daemon", > "stop"]` — its reap. So the checker is blind to it, nothing classes it > automatically, and dropping the name silently returns a daemon-spawning binary > to the full-parallel pool. > > So a PR that is green has not exercised the cross-node helper's positive at all. > That is the sentence I would want to read before trusting this milestone's > `int` tag, which is why it is here rather than in a commit message. > > **Why not one rig.** Gossip-on and node-label routing are **mutually exclusive > on one box**. `webserve::known_subnet_node` prefers the registry-ADVERTISED > label over the roster's; `RegistryHost::new` sets that label from > `os_hostname()`; one box gives both daemons one hostname; and the prefix > dispatcher takes the LOCAL arm whenever the first segment equals the local > node's label. With the pump on, every peer-prefixed URL is answered locally and > arm 1 returns `404 NO_DOCS_LANDED`. There is no label override to escape through > (no `SPT_NODE_LABEL`, no `daemon.json` field; `COMPUTERNAME` is Windows-only and > `gethostname` has no hook), and label matching has no node-hex leg. Two hosts > carry two hostnames and the whole problem evaporates. > > **A meter I built wrong, on the record.** The first version of the hermeticism > assertion read node A alone, 751 ms in, saw an empty set, and would have > reported the rig hermetic. A had learned from B's push and had not yet reached > its own cadence tick — so that zero was the zero of a meter that had not ticked, > not the zero of a quiet network. A guard against a false green produced a false > green. It now reads both daemons and refuses to judge the set until it is > non-empty, so the meter proves its own liveness before it is allowed to prove > anything else. > > ## Rig isolation, and one product seam found underneath it > > The attachment rig was being answered by **this box's resident fleet daemon**. > It mints a URL, the URL named port 5474, and the 404 that came back was the live > daemon truthfully reporting an entry it had never heard of. > > `SPT_TEST_EPHEMERAL_ADVISORY_PORTS=1` does not fix that, and the reason is a > product seam: `serveverb::node_and_port` mints the port from `DaemonConfig` / > `SPT_DOCS_PORT` and never asks what the listener actually **bound**, so under the > switch the daemon moves and the URL does not — **releases#282**, ruled a defect, > fixed outside this lane. > > Pinning a fixed port then exposed the sharper half: the rig autostarts a daemon > and never stopped it, so the next run was answered by the **previous run's** > daemon — same port, different `SPT_HOME`, registry right and surface wrong. > `netstat` named the holder. The rig now picks its port per run and reaps its own > daemon on every exit path, panic included, because leaving a daemon behind is > what makes a fixed port unsafe. > > ## The `MSG_NOT_FOUND` defect, and why one string was the second defect > > `spt msg show ` answered `MSG_NOT_FOUND` for an id read out of the funnel's > own `MSG_OUT` row. The cause is measured, not inferred, and it is bigger than > the arm that found it: **no spool row has ever carried a `short_id`.** > > Three greps, each falsifiable: > > - `spool_message_identified_at` and `IdentifiedInsert` — the only API that > accepted an identity — have **zero callers** anywhere in the tree. The > definitions are the only hits. > - The one live insert path, `insert_message`, passed `identity: None` as a > literal. > - No `UPDATE` anywhere sets `short_id`; the five `UPDATE messages` sites all > touch `delivered` / `taken_leg` / `body`. > > So `resolve_in`'s spool leg was **unreachable in production by construction**. > This also refutes the shape I first reported it in: I had it as "the spool path > drops the id when the target is OFFLINE." It drops it always. Offline was where > I happened to be standing. > > **The fix is one insert path, not two.** The identity is now derived at the one > `INSERT` from the envelope's own `msg-id` attribute, which is already in the > bytes being inserted — the id is minted before the envelope is composed > (ADR-0061). That is sound because `msg-id` and `reply-to` are sender-authored > and survive the receiver's ingress strip, which `spt-proto` already pins in > `the_w2_message_attrs_are_sender_authored_and_survive_the_strip`. A plain > non-envelope body parses to nothing and stores NULL, which is the honest answer > for a row that never had an id. The dead `spool_message_identified_at` / > `IdentifiedInsert` / `MessageIdentity` trio is **deleted** rather than left > beside the live path, because one of the two was always going to be the one > somebody read. > > **The single string was a defect in its own right.** `MSG_NOT_FOUND` covered > three different misses with three different fixes — no index row, a spool row > with a NULL id, no `MSG_OUT` in the log the resolver read — and it sent me at > the wrong half twice. `resolve_detail_in` now answers a miss with `ResolveMiss`, > and every count is carried **beside the total it came from**: `0 of 7` names a > mechanism, a bare `0` cannot be told apart from "there is no spool here". The > public code is unchanged — `MSG_NOT_FOUND`, same exit code — and the three > `MSG_MISS_*` lines are stderr breadcrumbs of the `F1_ARM` class, said so in the > enum's own doc comment so nobody promotes them into a contract later. The `m/` > facet's 404 body is likewise unchanged; a fetcher is a stranger and the counts > describe this node's stores, so they go to the daemon's stderr. > > ## The two-host pair run, and the three rig defects under it > > The `twohost_web` helper cell had never been compiled, let alone run. Five > attempts on the local pair, each red closed at a **named mechanism**: > > 1. **`RegistryUpdate` in the wrong module** — the type lives in > `spt_net::net::replicate`, not `::registry`. One line. > 2. **The cell reached for AMBIENT stores.** `RegistryGatePolicy::load()`, > `perch::epoch_file()` and `presence::registry_snapshot_dir()` all resolve > through the process-global `SPT_HOME`, which the runner unsets — so a role A > cell was answered by **this box's real fleet home**, the policy carried none > of the rig's subnets, and the merge returned `NotMember`. Role B can use > those calls because it sets `SPT_HOME` at its own head; role A never had. > The cell now claims its own `SPT_HOME` and calls the **product's** > constructors unmodified — no rig copy of how a daemon builds its registry > host. The proof is in the log rather than in this paragraph: the cell used to > print `PAIR_MEET_UP` for `SPT_DEV`, `BIGNET` and `SPT_MANTLE` — the fleet's > subnets — and now prints `PAIR_MEET_UP:twohost` and nothing else. > 3. **A false green one line before the real failure.** B's send loop broke on > any `Ok`, so a dialled-and-refused `NoPerch` printed "the user's message > reached A (NoPerch)" and walked into a 240 s wait for a registration that > could not come. The break condition is now the product's own > `took_custody()`. That predicate choice was load-bearing in the other > direction too: the honest success here is `Spooled`, not `Delivered` — the > audience endpoint is recorded but not listening — so a hand-written > `Delivered`-only list would have hung on the correct outcome. > > Under those three sat the real one. With the message ADMITTED, A's ingress > **did** fire the helper and died at the last hop: > > ``` > TWOHOST-WEB role B: the user's message was ADMITTED by A (Spooled) > HELPER_SERVE_FOR: path=…\quoted-by-the-user.md outcome=unanswered > reason=no daemon to proxy through: The system cannot find the file specified. (os error 2) > ``` > > That log pair is also the evidence for checklist **(b)**: an absent proxy > neither delayed nor failed the delivery. The cause is a rig gap, not a product > defect — `request_quoted_paths` dials `endpoint::seed_socket_name()` > (`spt-daemon-seed-{home_tag}.sock`, the node's canonical daemon socket) while > the cell answered on an invented rig name. Since the name derives from the home > and the cell now owns its home, the helper cell takes its broker name from the > product's own function; the other three A cells keep rig names, because nothing > dials into them. > > **Isolation is measured, not asserted.** Setting `SPT_HOME` inside a cell is > only safe if nextest gives each `#[test]` its own process, so every A cell's > listener breadcrumb now prints its pid, and the run reports **four distinct > pids** for four cells. > > **One measured limit, recorded for the field run.** A nextest `TIMEOUT` does not > always kill the cell. Attempt 4's role A sat **14 minutes** past its last > verdict at **0.61 CPU-seconds** — idle, not working — because a timed-out cell > left one live `twohost_web` child holding its broker threads and nextest blocked > on it; `a.exit` was never written while that child lived, so a finished run > looked hung and every verdict was already sitting in `a.raw`. Diagnose a hung > pair lane by **CPU time and an orphan census**, before reasoning about the > network. The rig-wide Drop-guard fix for thread-holding cells belongs to the > `IR-79` reaper lane, not here. > > ## Order of work > > The remote arm (#17's cross-node registration) was built BEFORE the battery, on > the gater's order, rather than deferred: it was the one piece written after the > compiler was available, precisely because its only honest proof is the two-host > rig. > > ## Gates > > - `cargo check --workspace --all-targets` — 0 > - `cargo clippy --workspace --all-targets -- -D warnings` — 0, and zero warnings > - `cargo run -p xtask -- check` (docs drift) — 0, with `reference.md` regenerated > - `traceable-reqs check` — 0 findings; every W2 requirement carries doc, impl, > unit and int > - Targeted battery: see the run recorded on the PR. > > Clippy findings were fixed at the mechanism, never suppressed: two 8-argument > spool functions became a value, a wrapper that went dead when the extras axis > landed was collapsed into one gate, and the lints in my own cells were fixed.