{
  "summary": "Node labels originate from OS hostname, not an operator node-renaming verb. Pairing, roster gossip, and registry label feeds currently accept labels without reserved-word or dot restrictions. Node-only peer lookup is available at explicit home through SubnetStore + RosterStore, including endpoint-less/offline members and tombstone filtering. No edits or validation performed.",
  "files": [
    {
      "path": "crates/spt-store/src/hostlabel.rs",
      "description": "os_hostname (24–28), node_fill_label (40–46), platform detect (49–70): actual hostname source and template consumer. Shared is_reserved_web_facet now present at 15–19, case-insensitive."
    },
    {
      "path": "crates/spt-store/src/roster.rs",
      "description": "RosterEntry.label is node metadata; RosterStore::load_from, members_in, is_member, upsert_self, merge_entry provide explicit-path membership and authoritative/self versus remote label mutation."
    },
    {
      "path": "crates/spt-store/src/subnet.rs",
      "description": "SubnetStore::load_from (~257), create_subnet (~299), add_joined: local subnet membership/seed state, not node-label minting."
    },
    {
      "path": "crates/spt-store/src/perch.rs",
      "description": "identity_dir 177–179, subnet_file 195–197, roster_file 206–208 establish home/identity/{subnet,roster}.json; ambient helpers resolve process SPT_HOME."
    },
    {
      "path": "crates/spt-net/src/net/pairing/wire.rs",
      "description": "NodeIntro 138; run_responder 188–261 accepts Hello intro before SPAKE/seed transfer; run_initiator Seed arm 550–589 adopts seed and remote roster; adopt_roster 898–910 merges raw labels."
    },
    {
      "path": "crates/spt-daemon/src/pairhost.rs",
      "description": "respond 61–155 commits remote joiner after wire success; join_ceremony 296 onward creates local NodeIntro and runs initiator; refresh_self_roster 835 and record_roster_peer 850 author local and remote roster labels."
    },
    {
      "path": "crates/spt-daemon/src/seedproofx.rs",
      "description": "production_roster_exchange 1086–1165: provider freshly reads hostname/upserts self; sink merges gossiped RosterEntry labels and reconciles addresses."
    },
    {
      "path": "crates/spt-daemon/src/registryhost.rs",
      "description": "RegistryHost::new 137–144 caches OS label; with_node_label 174 assigns it; merge_labels_locked 410–438 admits incoming node-level labels; advertisement 735–751 publishes labels independently of endpoints."
    },
    {
      "path": "crates/spt-daemon/src/pump/mod.rs",
      "description": "Self-address refresh reads hostname at 1391 and supplies refresh_self_addr (1424), which upserts self rows at 1446 when address changes."
    },
    {
      "path": "crates/spt-net/src/net/registry.rs",
      "description": "SubnetRegistry::merge_node_label 446, node_labels 467 are unrestricted node-tier label storage; node_qualifier_names 887 matches labels case-insensitively; node_label_display 576 is rendering only."
    },
    {
      "path": "crates/spt/src/cli.rs",
      "description": "SubnetCmd 1826, dispatch 2311–2329; cmd_subnet_create 13295, cmd_subnet_join 14998 and pair_submit_code/pair_join 15139–15140 are operator admission edges. resolve 12397 and save_minted 12937 mint/save subnet seeds, not node labels. Rename commands target endpoints/shell aliases, not nodes."
    },
    {
      "path": "crates/spt/src/wansend.rs",
      "description": "load_snapshots 1351–1373 takes explicit directory; label_of 1333 is endpoint-only and unsuitable; node-tier snapshots can instead use SubnetRegistry::node_labels."
    }
  ],
  "architecture": "OS hostname → daemon startup registry label and fresh per-ceremony/per-exchange roster labels. CLI join → broker IPC → pairhost::join_ceremony → wire initiator; daemon responder → wire ceremony → roster commit. Durable node membership lives in roster.json, scoped by local subnet.json; endpoint-independent label leases also live in registry snapshots.",
  "report": "All paths relative to C:/Users/decid/Documents/projects/spt-core/.worktrees/ws272-w0. Read-only source investigation; BOX HOLD honored: no cargo, rustc, analyzer, builds, formatting, lint, tests, or other validation.\n\n1. Actual label production versus display\n- hostlabel::os_hostname trims leading/trailing whitespace and returns nonempty text; Windows detect reads COMPUTERNAME, Unix detect calls libc::gethostname. Neither strips a domain suffix, rejects dots, checks reserved facets, nor validates URL-path characters.\n- RegistryHost::new reads OS hostname once and with_node_label assigns node_label. Advertisement stamps that cached value onto endpoint instances and separately emits NodeLabelUpdate for every served subnet even with zero endpoints (registryhost.rs 735–751).\n- Roster label production is NOT exclusively startup-cached: pairhost::refresh_self_roster, seedproofx::production_roster_exchange provider, and pump self-address refresh re-read the hostname. Thus changing the OS hostname can update roster state during daemon life while registry labels retain the startup value until restart. A startup-only reservation guard does not cover all producers.\n- No operator node-label rename/set verb found: CLI Rename variants are endpoint logical ID and shell instance alias; subnet create's name is the SUBNET name. hostlabel::node_fill_label, CLI own-node status, node_label_display, picker labels, cli@node sender attribution, rc identity banners, and serve URL construction consume/render labels; they do not persistently name nodes.\n\n2. Join admission seams and ordering\nLOCAL JOINER:\n- cmd_subnet_join is the user-facing preflight; after elevation gate, before daemon startup/discovery/prompt is the cheap loud-refusal location. Guard the OS NODE label, not the operator-supplied subnet name.\n- Authoritative broker-side gate is pairhost::join_ceremony before NodeIntro construction and connect_seed_holder (currently lines 313–322). Check the raw hostname independently of machine_id: current intro is None if either hostname or machine_id is unavailable, so merely checking the resulting optional intro misses a reserved hostname on a machine lacking a machine ID.\n- Wire run_initiator accepts a caller-supplied optional NodeIntro; checking that supplied label before opening/sending closes direct library callers without reaching into ambient OS state at the protocol layer.\nLOCAL SEED HOLDER:\n- pairhost::respond loads serving subnet state then refreshes its self roster BEFORE calling run_responder. Refuse its reserved OS node label before refresh/ceremony so it cannot transfer a reserved self-label. CLI cmd_subnet_create should likewise refuse before mode/admin capture/seed mint if reservation is a node-membership admission invariant; SubnetStore::create_subnet itself does not accept a node label and is not the right place to confuse node and subnet namespaces.\nREMOTE JOINER:\n- wire::run_responder extracts Hello { subnet_name, step, intro } at 204–211, then subnet lookup/rate admission, then responder_admitted. The correct remote-label rejection point is after Hello parsing and BEFORE responder_admitted can confirm and send Done/Seed.\n- pairhost::respond's outcome arm is TOO LATE for admission refusal: the wire has already transferred subnet secret material. That arm records address, possibly evicts a superseded key using intro label+machine_id, clears tombstone, merges peer row, and saves. Never implement reservation solely in record_roster_peer or after success.\nREMOTE SEED HOLDER / TRANSFERRED MEMBERS:\n- wire::run_initiator receives Seed containing all roster members (550–589). Check transferred labels before subnets.add_joined and adopt_roster; adopt_roster otherwise blindly forces subnet and merge_entry. This is the receiving-side refusal seam for seed-holder and third-party roster labels. Existing absent-intro/empty-label compatibility is separate from reserved names and should remain unchanged unless explicitly ruled otherwise.\n- Loud protocol refusal should identify offending node label; do not silently erase it or return success with an omitted member. Existing abort reasons are fixed codes, so a peer-visible named failure may require a deliberate wire-error encoding decision; returning a local Protocol error alone will not necessarily give the opposite endpoint that same named diagnostic.\n\n3. Continued admission after join\n- seedproofx::production_roster_exchange provider reauthors self labels; sink merges every incoming RosterEntry before saving and feeding address reconciliation. A join-only guard cannot prevent a later OS rename or an existing peer gossiping a reserved label. Guard self publication and remote ingress here, before membership/addresses consume invalid rows, with explicit diagnostics.\n- RegistryHost::merge_labels_locked checks local subnet and origin roster membership, then blindly calls merge_node_label. Apply the shared reservation predicate at this admitted label-feed seam. Endpoint instance feeds also carry node_label: cover them if those fields remain a naming source, rather than protecting only dedicated NodeLabel records.\n- RosterStore::upsert_self and merge_entry are raw mutations with no validation; SubnetRegistry::merge_node_label likewise only enforces epoch order. Their exported signatures currently cannot express label rejection. Changing them entails public-API/caller analysis and mandatory LSP references, deferred under HOLD. Avoid pretending a stale merge outcome means invalid-name refusal. New helpers/private ingress guards allow a smaller cut without changing existing exported signatures.\n\n4. Explicit-home HTTP peer lookup (no endpoint approximation)\n- Load SubnetStore::load_from(&home.join(\"identity/subnet.json\")) and RosterStore::load_from(&home.join(\"identity/roster.json\")). For each locally held subnet, inspect roster.members_in(&subnet.name), which excludes tombstoned members. Compare each member.label case-insensitively to the requested first segment, excluding reserved facets before any node match.\n- This is actual node-level membership: no endpoint IDs, liveness state, visibility, presence, or addresses are required. Offline and zero-endpoint members remain known and should receive W0 502 when their name is requested. Scope by held subnet to avoid obsolete roster rows from a left subnet falsely claiming node prefixes.\n- If fresher/dedicated registry labels are required, read snapshots at home/identity/registry using explicit directory reading, iterate SubnetRegistry::node_labels, and intersect candidate node keys with non-tombstoned roster membership in the same held subnet. Do not use resource_projection, endpoint_ids/instances, node_routability, or wansend::label_of: these are endpoint-derived and omit zero-endpoint nodes. Snapshot labels alone are not membership authority.\n- Existing CLI wansend::load_snapshots has an explicit-directory interface, but a daemon/router cannot acquire a dependency on the CLI binary just to reuse it. The roster route is already available from spt-store without introducing that dependency.\n- Never call SubnetStore::load, RosterStore::load, perch::identity_dir, or nodeid::load_or_create merely to implement supplied-home HTTP lookup: the first three consult process home and the last may mint identity. Keep handle_path signature and use its supplied home.\n- Registry label and roster label can temporarily differ after an OS rename because of the different sampling points above. Do not claim a cross-source union is a proven canonical rename-resolution policy; choose the roster's current membership label for the minimal node-only lookup, or explicitly specify precedence if using registry labels.\n\n5. Dotted-name ruling is not an existing invariant\n- Direct evidence: os_hostname only trim/nonempty; NodeIntro decoding checks UTF-8 only; roster merges accept arbitrary String labels; merge_node_label accepts arbitrary String labels; node_qualifier_names compares the full label case-insensitively. Consequently a hostname such as box.example or index.html survives production and is addressable by the present label machinery. This is a source-supported capability, not a claim that any particular live node currently has such a name.\n- Therefore 'extension paths cannot be node names' is not enforced today. The reservation helper for docs/f/a/m/bin/install alone cannot make it true. A router shortcut that classifies every dotted first segment as docs BEFORE local/known-peer resolution silently violates local-first/known-peer-second for presently legal dotted labels.\n- Safest W0 recommendation: use exact local/known-peer matching before docs fallback, reserved facets excluded unconditionally; do not add extension heuristics. If the ruling intends a new no-dots node-label grammar, it needs an explicit admission/migration decision covering OS labels, both pairing directions, roster gossip, registry feeds, and preexisting stored labels. Do not quietly expand the reserved-facet patch into that incompatible rename policy.\n\nCompressed implementation recommendation: shared case-insensitive reserved-facet predicate; loud pre-mutation guards at operator preflight plus authoritative pairing/advertisement/gossip ingress; node-only explicit-home roster lookup scoped to held subnets and tombstones; reserved facet exclusion before HTTP node matches; no extension shortcut; retain exported signatures until references can be obtained after HOLD."
}
