Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Serving resources

The node’s serving registry is the single list of what it exposes. Register a file or directory deliberately, then copy its URL from spt serve list. Removing an entry stops serving it; it does not delete its source bytes.

If node-prefixed docs return 404 or serve controls are unavailable after an in-place update, check spt node status before restarting anything. A resident network layer older than 0.68.0 cannot supply these routes even when the installed release can. See the update diagnosis for the full-restart requirement and its cost to hosted sessions.

Node-prefixed URLs

Every served resource uses the owning node’s name:

http://localhost:5474/hfenduleam/f/report.md
http://localhost:5474/hfenduleam/docs/
http://localhost:5474/hfenduleam/a/my-harness/

The server listens on loopback only. The default port is 5474; SPT_DOCS_PORT overrides daemon.json’s docs_port. A request to bare / redirects with HTTP 302 to /<local-node>/. Existing bare docs paths, including /llms-full.txt, /manifest.schema.json, and raw .md pages, remain compatibility aliases.

Generated serving, attachment, and docs URLs use the running daemon’s actual bound listener port. Changing a client’s environment or config does not change that port. Configuration is used to predict a URL only when no daemon runs; an unavailable or unreported listener on a running daemon is an error, not a guessed link. spt fetch resolves the receiving node’s local listener after startup, including when a pasted URL names another node or another port.

Node prefixes take precedence over compatibility aliases. If a known subnet peer is named cli, /cli/... addresses that peer, not the local docs section. The alias is shadowed only while that peer is known. Use the canonical twin, /<local-node>/docs/cli/..., for a local docs link that cannot change meaning. A single-leaf URL naming a docs root file (llms.txt, llms-full.txt, manifest.schema.json, or a root .md page) serves that file before peer lookup. Dotted hostnames remain valid: a peer named llms.txt loses only the leaf /llms.txt, not its node-prefixed /llms.txt/... paths. A bare known node prefix redirects to its trailing-slash form. The local node still wins over the root-file exception: if the local hostname is llms.txt, /llms.txt redirects to /llms.txt/; its docs file remains at /llms.txt/docs/llms.txt.

The node root /<node>/ redirects with HTTP 302 to /<node>/docs/, for both local and remote nodes. Query parameters are preserved. There is no browser registry index or ?json index: use spt serve list or spt serve list --json to audit exposed paths and URLs.

The path beneath the node prefix identifies the facet:

PathResource
f/<served-name>A registered file or directory
docs/The installed docs bundle
a/<adapter>/The adapter’s core-owned served root
<short-alias>/An adapter’s optional short path, pointing at the same entry
m/<short-id>One message, rendered; ?json for its machine twin
bin/, installReserved facets; currently answer 404 naming the facet

A request naming a known subnet peer is answered by that peer through the local daemon; see Cross-node serving for what comes back and what each status means. Other first segments retain the docs compatibility behavior, including the docs server’s own 404 for nonexistent pages. An unknown local facet returns 404 naming that facet.

Use a trailing slash for directory roots: /<node>/docs/, /<node>/f/<directory>/, /<node>/a/<adapter>/, and adapter aliases. Requests without that slash redirect before serving the root index.html, so browser relative links resolve inside the correct subtree. Redirects preserve query parameters. This does not add directory listings or implicit nested indexes; the docs compatibility surface retains its existing bytes and status behavior.

The facet tokens docs, f, a, m, bin, and install are reserved in the URL grammar. A node with one of those OS hostnames is reported loudly at daemon startup and by a peer learning it at join, but may still boot and join. The router never treats such a token as a top-level node, so that node cannot be addressed by prefix until renamed at the OS. W0 introduces no hostname admission policy or SPT node-renaming verb.

Register, inspect, remove

spt serve add report.md
spt serve add ./reports --as review
spt serve list
spt serve list --json
spt serve rm report.md

add records an absolute reference to the file or directory, not a copy. Later edits are visible at the same URL; deletion of the source returns 404. A directory exposes only its own subtree: traversal and symlinks escaping the root are refused. rm accepts the served name or the entry id printed by the listing. It removes only the registry entry.

Served names remain stable for an entry’s lifetime:

  • The first report.md keeps that name.
  • Another path requesting report.md receives report~1.md, then report~2.md, with the suffix before the final extension.
  • Removing the first entry reserves report.md for its prior absolute path and kind. Re-adding that same file reclaims report.md; a different third file still receives report~2.md. A stale URL never opens a different resource.
  • Re-registering the same absolute path returns its existing entry, even if the caller supplies a different --as name.
  • Explicit --as names use the same allocator. A name assigned explicitly or by suffixing can be reclaimed only by its prior path with the same kind.
  • Allocation history is persisted per node. Another node has its own names; the node prefix keeps their URLs distinct.

The broker and brain are separate processes that write $SPT_HOME/serve/registry; CLI clients do not write it directly. The store includes entry ids, kinds (file, dir, attachment), paths, assigned names, registration timestamps, optional origin endpoint ids, and allocation history. Optional lifetime (ttl_ms) and receiving-endpoint audience metadata apply to every entry kind, including live file and directory references. W0’s explicit registrations leave both unset; timed, audience-scoped helper registration and its enforcement belong to W2. Do not edit or remove this file to reclaim a name: that would destroy the history which prevents stale URLs changing meaning.

Writer coordination requires both processes to hold the same serve/registry.lock guard across reading and updating the registry. Atomic snapshot replacement alone does not prevent one writer from losing another’s update. Both running writer implementations must be updated: a new brain with an old broker does not fully activate protection. HTTP readers remain lock-free; attachment expiry and source protection are unchanged.

Adapter served roots

Core creates $SPT_HOME/adapters/<adapter>/web/ at activation and registers it as one directory entry. The adapter writes only the output it wants exposed into this directory. Its install tree, manifest, and other auxiliary files are never served on its behalf.

The entry answers at /<node>/a/<adapter>/. An optional [adapter].web_short_path manifest field requests an additional short alias for the same entry and directory. A requested alias such as cspt produces /<node>/cspt/; if that name has already been assigned, the ordinary stable suffix rule applies. Use the listing to discover the assigned alias instead of guessing it.

The names f, docs, a, m, bin, and install are reserved. Requesting one as web_short_path refuses activation with a field-naming diagnostic. Without a short alias the adapter facet still serves normally.

Deactivation removes the registry entry but retains the directory and its files. Reactivation with the same declaration reclaims the prior alias, not a new suffix. Reactivation or an adapter update must not erase those output bytes.

Adapter docs

An adapter that ships its own documentation points at it with an optional [adapter].docs_dir manifest field, and it is served at the reserved docs segment of the adapter facet:

http://<node>:5474/<node>/a/<adapter>/docs/

docs_dir is adapter-relative — a path inside the adapter’s own installed directory, never an absolute path and never a parent-directory escape. Both refusals are named MANIFEST_DOCS_DIR_OUTSIDE:<adapter>, and the check runs twice: once when the manifest is validated, and again on every request, so a directory replaced by a link pointing elsewhere after installation is refused at the fetch rather than served from wherever the link now leads. The adapter’s own directory root is refused for the same reason its install tree is never served: it holds the adapter’s record and state.

Without the field, /<node>/a/<adapter>/docs/ answers 404 naming the facet. It never falls through to a docs/ folder inside the core-owned web/ root — the segment is reserved, so the URL means the same thing on a node whose adapter declares docs_dir and on one whose adapter does not. Reads also refuse replacement of the core-owned root with a symlink to an install tree or another directory. The same containment rules apply through the adapter facet and its alias.

Entry fields: lifetime, audience, origin

Beside its path and served name, an entry may carry three fields. They are per-entry and independent of kind, and spt serve list renders each one it has — the registry’s job is answering what this node exposes and to whom, and a narrowing that did not render would make that answer a half-truth.

FieldMeaning
ttlA lifetime from registration. Absent means the entry lives until removed. Attachments carry 30 days by default; entries the FILE_ACCESS_HELPER registers carry 24 hours.
audienceThe one endpoint allowed to fetch it. Absent means anyone the WEB surface admits.
originWho or what registered it — for a message helper, the message short-ID; for an input-report helper, user-input:<receipt-id>.

An audience is a cross-node narrowing. The owner serves the entry only to the named endpoint and answers everyone else 403 naming the surface — deliberately the same refusal a WEB deny rule produces, so a narrower audience cannot be distinguished from a denial by probing and the registry does not become an oracle for what exists.

It is enforced where the fetch origin is proven: the cross-node stream’s handshake identity. That handshake proves a node, while an audience names an endpoint, so the check the owner can honestly make is whether the proven node hosts that endpoint — it stops another machine, not another agent on the audience’s own machine. An audience this node cannot place is refused rather than admitted.

Loopback remains the trusted machine. A local browser presents no endpoint identity, so a request arriving on the loopback listener is served regardless of audience. An operator who needs secrecy from other users of the same machine has file permissions, not this field.

An entry whose lifetime has elapsed stops serving immediately — it answers the ordinary not-found — whether or not the daemon’s reaper has run yet.

Access control

WEB is a rule-addressable control surface, on by default within a subnet because registration is already deliberate exposure. A blanket closed posture does not close it; a rule or per-surface mode naming WEB can. It is currently non-attributable: no sender-endpoint stamp is proven for web reads.

The owning node runs the WEB check for every request that arrives from another subnet member, under that member’s handshake-proven identity; a refusal answers 403 with a body naming WEB (Cross-node serving). Local serving remains loopback-only.

XFER is retired

WEB did not rename XFER — it replaced what XFER was for, and XFER itself is retired. The file-transfer machinery it gated has left the tree with it: the pull-model attachment supersedes it, and cross-node reads of served content are governed by WEB. Retiring rather than repurposing the id is deliberate — reusing XFER for web reads would silently change the meaning of every stored rule that already names it.

The id is gone from the vocabulary, so nothing new can be granted on it, and spt endpoint access no longer offers it. A rule you already stored that names XFER stays exactly where you put it. It governs nothing — the same standing any unminted id has always had under this open vocabulary — and every load of the store that contains it prints one line naming it:

ACCESS_SURFACE_RETIRED XFER — a stored rule or mode entry names this surface,
which no longer exists: ... The entry is KEPT and governs nothing (a retired id
is legal exactly as an unminted one is). Remove it when you are ready
(`spt endpoint access remove ...`).

One line per retired id, however many rows name it. Nothing is rewritten or dropped on your behalf: a vocabulary that shrank is not licence to edit a decision you made, and the report is the only record that the surface was ever used at all. Remove the row when you are ready to.

Bootstrap LAN firewall admission

spt serve lan --bootstrap retains the signed-binary admission gate and reports the listener’s actual port and binder executable. Firewall repair is separate: a denied elevation prompt, unavailable manager, or failed repair leaves a valid listener running and reports LAN_FIREWALL_UNVERIFIED. This is not proof that the listener is unreachable. Repeat bootstrap to retry; status only inspects.

LAN_FIREWALL_REPAIR_REQUESTED means elevation was requested, not that a rule was installed. The elevated helper queries the original broker’s current state; it never replays bootstrap or starts a stopped listener. Its observed result is reported separately. LAN_FIREWALL_RECONCILED means the owned rule’s scope was observed, not that another machine successfully reached the listener.

On both Windows and Linux, admission is port-scoped: binder identity is ownership metadata, not executable enforcement. Another process using that port may be admitted. Windows writes two rules, not one – a tailnet half and a LAN half – because a single rule cannot cover both without also admitting the local subnet of a public network. Linux selects UFW before firewalld before nft; an opaque manager does not authorize falling through to a different backend.

Supported Linux layouts are active dual-stack UFW, an already-provisioned owned firewalld policy, or one unrestricted existing inet/filter nft input chain. IPv6-disabled UFW, multiple or family-specific nft input chains, and unknown rule scope remain unverified rather than being rewritten.

Fresh firewalld hosts need an operator-provisioned runtime policy named spt-bootstrap-tcp, with description spt-core bootstrap TCP admission v1, target CONTINUE, priority -32768, ingress ANY, egress HOST, and no unrelated allowances. Its permanent identity must also exist. Bootstrap does not create a silent permanent-only policy or reload firewalld: a reload can discard unrelated runtime configuration. Missing runtime policy leaves the listener up, reports the provisioning prerequisite, and remains unverified.

spt serve lan --stop stops the listener before attempting owned-rule cleanup. Cleanup never removes an unowned rule or the installation’s UDP rule. LAN_FIREWALL_CLEANUP_UNVERIFIED prints residual-resource guidance; inactive managers may require operator inspection. Firewalld runtime policy definitions cannot be deleted without a separately authorized reload, so an inert policy can remain after its TCP allowances and permanent definition are removed. After this cleanup, a later firewalld bootstrap needs operator provisioning again: retaining an inert runtime policy does not retain its permanent identity. Concurrent SPT repair helpers are refused using a host-wide lock. This does not serialize independent administrator commands; avoid editing firewall policy while repair runs, especially UFW rules addressed by mutable rule numbers.

SPT_INSTALL_NO_FIREWALL suppresses firewall mutations and elevation, not listener startup or read-only reporting. Elevated CLI fixtures must set it unless they explicitly own an isolated firewall environment.