358	## Instances
359
360	The multi-instance identity model. An agent is no longer "a perch on one machine"; identity and materialization split into two layers:
361
362	**endpoint ID**:
363	The logical identity (`ling`). Shared subnet-wide. The thing you address. **Adapter-agnostic**: the same ID can run under different adapters over its life (start on `claude-spt`, later revive on `spt-codex` or `spt-pi`) — adapters expose the same SPT-relevant capability subset, so the ID is not bound to an adapter. The currently-running `adapter_name` is a property of the live instance, not part of the identity. **At most one instance of an ID per node** is allowed; a second attempt on the same node (any adapter) is rejected. **Identity is node-global, advertised per-subnet** (see *subnet membership*): one `ling` exists on a node (one mind, one `tracked/` context) and is advertised into each subnet the node belongs to, subject to per-subnet visibility (see *endpoint visibility*). The same bare name may exist as *distinct* endpoints in different subnets; the resolution policy forces qualification when both are visible. Within a single subnet a bare id is unique — enforced by a **join-time collision check**: when a node carrying endpoint `X` joins/advertises into a subnet that already has a different `X`, the clash is surfaced and resolved (rename one — below) before advertisement. An endpoint keeps an **ordered adapter history** (in `tracked/agents/<id>/meta.json`, synced) driving the resume UX: latest adapter is the default resume offer, then next-latest, … oldest, then "choose a different adapter". (Adapter-agnosticism is an *agent-endpoint* property; Shells are adapter/platform-bound — see Shells.)
364
365	**adapter selection (creation & change)**:
366	At **creation**, the adapter is chosen from the node's **registered `kind="harness"` adapters whose `hostable_types` includes the endpoint's type** — **auto if exactly one qualifies, else chosen** (`--adapter` / picker): the same *auto-if-one-ask-if-many* rule as *home subnet*. The choice seeds the **head of the ordered adapter history** and the live `adapter_name`. **Changing** an endpoint's adapter is **not** a standalone operation — adapter is a *live-instance* property, so you change it by **launching/resuming the endpoint under a different registered adapter** (the resume UX surfaces the adapter history: head = default, then prior adapters, then "choose a different adapter"). For a currently-active instance this is a **resume-under-the-new-adapter** carrying context via the *fresh-with-preload* psyche-download (a session cycle, not a hot swap — the mind is adapter-agnostic). Same gate both times: the target adapter must be **registered on the node** and its `hostable_types` must include the endpoint's type.
367
368	**instance**:
369	The *same* endpoint (same harness + adapter) running **natively on a machine** — local files, local compute. `ling@desktop` and `ling@laptop` are both real local `ling`s, not remote views of one. What is per-node vs synced:
370	- **Per-node (anchored, cannot teleport):** the project working directory / files, and the harness session history. A git repo can't teleport; files are obtained locally (e.g. `git pull`).
371	- **Synced across instances:** the **Psyche context** (the agent's mind). See the two-tier sync below.
372
373	This is the core meaning of "the same agent on multiple machines": same identity, native local materialization on each, with the mind kept in sync. The ID is the shared identity for addressing; files are local; the mind follows.
374
375	**dormant / suspended instance:**
376	At most one instance of an endpoint is typically the *actively-driven* one; the others rest. Two resting states:
377	- **dormant** (default, *warm*): a live, state-preserved seat whose harness session stays running → instant re-activation. The default resting state.
378	- **suspended** (opt-in, *cold*): the harness session is closed and resumed-on-wake → frees RAM/compute (and, for billable harnesses, footprint), at the cost of a resume on wake. Reached via per-endpoint opt-in *auto-suspend*, or on demand: a subcommand can **suspend an endpoint from anywhere** (any node).
379
380	**State transitions (no idle timer):**
381	- **active** = the instance is the **most-recently-interacted instance for that ID** *and* **has a driver attached**. Both conditions. There is **no idle-grace timer** — a session stays active as long as it holds those two properties. A **linked Shell counts as a driver** (an agent with a live shell stays active even when the user isn't typing).
382	- **active → dormant** — the driver detaches, *or* another instance of the same ID becomes the most-recently-interacted one (attention shifts). No explicit stop needed to switch; driving `ling@laptop` makes `ling@desktop` dormant.
383	- **dormant → suspended** — manual (`spt suspend` / `spt shutdown` / shell `api owner-shutdown`), *or* opt-in auto-suspend after an `auto-suspend-after` threshold **counted from the moment the instance went dormant**.
384	- **wake (→ active)** — `spt wake`, a driver attaches, or a shell wake-watcher fires.
385
386	A resting instance retains its files + last context and is re-activatable in place — a lightweight **wake** (state's already there), distinct from instantiate-anywhere's fresh spawn. Resting instances remain addressable by node (`ling@desktop`). Registry status: **active / dormant / suspended / offline** (offline = node unreachable).
387
388	**Default policy:** warm (dormant) is the default when undriven. **Auto-suspend is opt-in, default OFF globally**, but **node-overridable** (a handheld / Pi / resource-constrained node may default it ON) and **per-endpoint overridable** (e.g. a billable harness whose warm session holds a cost footprint). Thresholds (`auto-suspend-after`) are config knobs: global default → node override → endpoint override. *Confirmed by measurement (M4-D9-3, `docs/DORMANCY-BUDGET.md`): an idle warm seat burns zero CPU — the cost is RSS only (~8 MiB shell-class, ~300 MiB LLM-harness-class); suspended residual is just the on-disk record.*
389
390	**Commands** (user/agent-initiated lifecycle, unprefixed): `spt suspend <id[@node]>` (suspend any of your instances from any node), `spt wake <id[@node]>` (explicitly wake a dormant/suspended instance), `spt shutdown` (an agent suspends its *own* endpoint — graceful, fires the suspend boundary signoff, cascades shells offline), `spt refresh` (spt-hosted only — an agent clears + resumes itself without stalling: `/clear` → commune capture → guaranteed post-clear resume signal so its turn restarts from immediate next-steps; see `docs/CONTEXT-MEMORY.md`).
391
392	**`spt rename <id> <new_id>`** (endpoint rename, **rippled**): change an endpoint's logical ID and propagate the change to **all its instances** subnet-wide — the registry entries, every node's perch, and the synced context branches (`a-<id>` → `a-<new_id>`, and project-view references). The rename is one logical operation against the eventually-consistent registry; it must collision-check `new_id` against every subnet the endpoint is advertised into (the join-time check above), and reconcile concurrent renames by the same precedence marker the context merge uses (node + newest-wins, KNOWN-HAZARDS 6.5). The adapter-agnostic, node-anchored identity (ADR-0003) is what makes a clean rename possible; without it the ID would be entangled with per-adapter or per-session state.
393
394	**`spt fork <id> [--into <subnet>] [--as <new_id>] [--delete-source]`** (cross-subnet clone):
395	Clone an endpoint into a **new, identity-distinct endpoint** on another subnet — the sanctioned way to place an agent's mind in a different subnet (home subnet being immutable). The fork's **home = the target subnet**; it is **seeded with a one-time copy** of the source's mind (the live + project *Cross-node Psyche sync* tiers), after which the two **diverge** — there is **no ongoing sync**. A fork is therefore **not an instance**: an *instance* is the **same** identity on another node with a **synced** mind; a *fork* is a **new** identity with a **copied-then-independent** mind. The new id defaults to the source's bare name if free in the target subnet (the join-time collision check; else prompt/rename); the adapter follows the normal creation rule (*adapter selection*). The **source is untouched**, optionally deleted afterward (`--delete-source`, or a later delete). Distinct from **rename** (same identity, new label, rippled) and **instantiate-anywhere** (same identity, new *node*, consent-gated).
396
397	**transition echo commune:** any active → (dormant | suspended) transition fires an **echo commune** capturing the outgoing instance's final context delta; that commune syncs to whichever instance becomes active next. This is the finer mechanism behind catch-up-on-activation (and mirrors the sister project's "echo commune before signoff" pattern — KNOWN-HAZARDS 3.3).
398
399	**deferred-message gate:** deferred (spool-only, hook-consumer) messages are **not** delivered to a dormant or suspended instance — they hold until it is active again. (Extends the deferred-row semantics, KNOWN-HAZARDS 1.4/4.4, with an instance-state gate.)
400
401	**Remote-control vs local operation (two distinct modes — not the same as instances):**
402	- **Operate locally:** drive the native instance on *your* machine (its local files, its synced mind). The normal case.
403	- **Remote-control (Shell-like):** attach a control/view surface to an instance *running on another node* — compute + files stay remote; you are a viewport (the byte-stream terminal attach, daemon-to-daemon over Iroh). Used when you specifically want *that machine's* environment. This is effectively a Shell (a driven surface, user→agent direction), separate from the instance concept itself.
404	- **Smooth handoff (remote-control → local):** no teardown required. `git pull` brings the files; the **fresh-with-preload resume seam** preloads the other instance's latest synced context (psyche-download); the remote instance simply goes dormant. The mind follows; the files are local.
405
406	### Pieces the Instances model requires
407
408	**subnet registry**:
409	Distributed, eventually-consistent map `endpoint_id → [instances: (node, perch, type, status)]`, **per subnet**. Every `spt-daemon` participates in the registry of each subnet it belongs to. This is the "agent name resolution" the sister project deferred — now first-class and mandatory.
410
411	**endpoint visibility (per-(endpoint, subnet))**:
412	Whether an endpoint is exposed to a given subnet. **Hidden means *excluded*** — not advertised in that subnet's registry **and not routable from it** (addressing it from that subnet fails until explicitly revealed), not merely unlisted. This makes hiding a real boundary, ready for the cross-user (b) seam, not a silent non-boundary. Computed as: endpoint `E` is **hidden in subnet `S` iff** `S.hide_new_endpoints` (a per-subnet policy captured at the node's **join time**) **OR** `E.default_hide_from_new_subnets` (a per-endpoint setting) — **unless** an explicit per-`(E,S)` override says otherwise (the override always wins). Both defaults ship **OFF** (visible); hiding is opt-in. **Visibility gates sync:** `hidden ⟹ not synced`, and replicating a mind into `S` (the sync-membership list under *Cross-node Psyche sync*) requires `E` be visible in `S` — visibility is the outer gate, sync-membership the inner opt-in within visible subnets.
413
414	**home subnet (per endpoint)**:
415	The single subnet that anchors an endpoint's *defaults*: the default **sync** scope (subnet-exclusive mind replication — *Cross-node Psyche sync*) and the subnet that qualifies its **bare name**. **Assigned at creation:** the node's **sole** subnet automatically; on a **multi-subnet** node it **must be specified** at creation (no silent guess — mirrors the *resolution policy*'s refuse-and-qualify); on an **unpaired** node the endpoint is local-only until first join, when home is set. Distinct from advertisement: identity is **node-global and advertised into *every* subnet the node belongs to** per the visibility defaults — home subnet is only the default-scope *anchor*, not a limit on where the endpoint is visible or addressable. **Home is immutable** — there is **no re-home** (ADR-0010). To place an agent's mind in a different subnet, **fork** it (see *`spt fork`*) and optionally delete the original; this avoids any scope-migration / stale-mind ambiguity (the source is untouched until explicitly deleted).
416
417	<!-- [doc->REQ-INST-14] -->
418	**resource advertisement (subnet resource registry)**:
419	A per-endpoint **free-text blurb** describing the services/functions the endpoint can serve — an agent **yellow-pages** for service discovery, distinct from *capability declaration* (machine-readable, which endpoint *types* a node hosts) and from *endpoint visibility* (whether it's addressable at all). **Both-authored + mutable:** config seeds a default; the agent refines its own at runtime (`spt resources set …`). It is **not a separate registry** — it is a field on the endpoint record and a **projection** of the subnet registry (`(id, node, resources-blurb)`), surfaced as the "subnet resource registry" view. **Gated by the access layers:** an endpoint excluded from subnet S (visibility) — or, later, one whose access whitelist (*endpoint access whitelist*) excludes the viewer's node — never appears in that view; discovery leaks nothing a viewer couldn't reach. Synced as **registry data, not context** (directory metadata, not the agent's mind). M4 (needs the distributed registry).
420
421	**resolution policy**:
422	How bare `ling` resolves when multiple instances exist. Rule: local-node instance if present → else most-recently-active → explicit override always available. **Two qualifier syntaxes**, combinable: **subnet-qualified** `home:ling` and **node-qualified** `ling@hfenduleam` (→ `home:ling@hfenduleam`). **Multi-subnet ambiguity rule:** under node-global identity (endpoint ID below), a node on several subnets may see two *distinct* endpoints that share a bare name (a `home:ling` and an unrelated `work:ling`). When a bare `id` is ambiguous across the visible subnets, resolution **refuses and forces qualification** (by subnet or node) rather than guessing. Within a single subnet, bare ids stay unique (join-time collision check, below).
423
424	**instantiate-anywhere** (seam day-one, behavior deferred):
425	From node Y, spin up an instance of an endpoint on node X by routing a launch command to X's daemon, which runs the endpoint's manifest locally. "Agents can spin up instances (including of themselves) on any node, possibly requiring user consent" — so a **consent gate** governs remote instantiation. The consent-UX design is deferred; the model is built to accommodate it.
426
427	**cross-instance context freshness** (two-tier):
428	When a dormant instance is activated again, it is unobtrusively fed the latest Psyche context — newest-among-peers *and* newer than its own. Scoped by the existing Psyche-context split:
429	- **live context** (per-agent, project-independent — the agent's general mind): synced to **all** instances of the endpoint, regardless of project.
430	- **project context** (per-agent-per-project — project-specific detail): synced **only** to instances sharing the **same project**.
431
432	So all `ling` instances catch up on live context; only same-project `ling` instances catch up on project context. This resolves the "divergent simultaneous work" concern (two instances on different projects don't clobber each other's project detail) without losing the "mind follows everywhere" property.
433
434	The precedence/freshness guard (KNOWN-HAZARDS 6.5) — with node identity in the marker — keeps a dormant instance's stale context from clobbering the active authoritative one.
435
436	**Authoring directives + memformat** (full design: `docs/CONTEXT-MEMORY.md`): the live/project split is made stable by (1) a per-*topic* cross-project discriminator with default-to-project bias, (2) **commune-source asymmetry** — echo communes are project-primary and live-conservative (the leak fix), and (3) **deep memformat integration** — memformat becomes the load-bearing, self-evolving, **tier-tagged** schema (each topic tagged `live`/`project`, decided once at creation, re-tiering migrates content), whose topics are containers of **structured blocks** (id + Concepts/Aliases/Applies-to + Source + timestamps + Hash + Pinned). A generated **Retrieval Map (INDEX)** per project + per agent realizes cross-agent synthesis + query-routing. **Start-injection** is dense on the working set (live + current-project hot/`Pinned` blocks → immediate productivity) and sparse on the tail (other projects, cross-agent synthesis, cold blocks → pointer + INDEX, on-demand), tunable via a density knob. Several patterns adapted from `BigscreenVR/pi-agent-memory`.
437
438	### Off-node reach-back
439
440	When the user is driving an instance from off-node, the agent should be able to:
441	- **detect** that it is being driven remotely (and from which node) — observable from the `spt-daemon`, which knows whether the driving session-surface stream is local or network-attached; (v1)
442	- **transfer files** to/from the user's (driving) node — file payloads over the messaging substrate; (v1)
443	- **run commands** on the user's node — remote command execution, **deferred + security-gated** (shares the consent model with instantiate-anywhere; arbitrary cross-node exec is the highest-risk capability in the system). Deferred partly because workarounds exist: the remote-node agent can message a local-node agent to run the command, or the user can interact with a local-node agent directly. (deferred)
444
445	### v1 scope line (V1-mid)
446
447	- **In v1:** the endpoint-ID / instance data-model split; the subnet registry; resolution policy; remote-drive of *already-running* instances (network session-surface); cross-node Psyche sync (full — replaces gh-repo-sync); cross-instance context-freshness auto-feed; off-node file transfer; off-node remote-drive detection.
448	- **Seam-compatible but deferred:** instantiate-anywhere + its consent gate; remote command execution + its security gate (workarounds: remote agent messages a local agent, or user interacts with a local agent directly).
449	- **Rationale:** the data-model split and registry are foundations — deferring them forces a later rewrite, so they are in regardless. Remote-drive of a running instance is the highest-value slice and exercises the whole networking+daemon stack end-to-end. Instantiate-anywhere and remote-exec carry consent/security design that should not gate v1.
450
451	## Workspace & versioning
452
453	The crate-boundary design is the rebuild's main value (ADR-0001) — honest decoupling that prevents the sister project's tangle from re-forming. Principle: **many small, acyclically-layered crates** (not a few fat ones).
454
455	Layering (bottom → top): `spt-proto` (wire envelope grammar, endpoint types, message framing, Ed25519 identity types, typed+binary payload schema — pure types, no I/O) → `spt-store` (spool, perch layout, trust store, registry persistence) → `spt-msg` (delivery TCP+spool-fallback, routing, send/ring) → `spt-net` (Iroh, mDNS, SPAKE2/TOTP pairing, subnet-registry distribution; `net` feature-flagged) and `spt-term` (session-surface trait, PTY impl, broker) and `spt-runtime` (`AgentRuntime` trait, `ManifestRuntime`, manifest schema) → `spt-live` (Psyche/pulse/commune/signoff) → `spt-daemon` (the brain: ties all, hosts broker, update orchestration, instance registry) → `spt` (the `spt.exe` binary; thin CLI).
456
457	**public SDK surface** (semver-committed): `spt-proto` (the wire contract — anyone speaking SPT depends on it), `spt-runtime` (the trait third-party harnesses implement), `spt-msg` (the deeper messaging-integration path). Everything else (`spt-store`, `spt-net`, `spt-term`, `spt-live`, `spt-daemon`) is internal/unstable pre-1.0 — reachable, but no stability promise. The *integration* crates are stable; the *machinery* churns.
458
459	**wire-protocol version vs crate semver** (orthogonal, never conflated): `spt-proto` carries an explicit **wire-protocol version** independent of crate semver. Two nodes — or a node and a linked library consumer — interoperate iff their proto versions are compatible, with a documented **N-1 compat window**. Crate semver governs *API* stability; proto version governs *wire* stability.
460
461	## Pairing & trust
462
463	How a single user's own nodes come to trust each other and form a subnet. Cross-*user* subnet (two *different* users' subnets interoperating) is explicitly deferred (the sister project's "cross-subnet contract" — very-early future concept), but the model is **built with the seam for it** (see *subnet membership*).
464
465	**subnet membership (multi-subnet)**:
466	A node may be a member of **multiple subnets at once** — but, for v1, only multiple subnets *of the same user* (e.g. a `home` fleet and a `work` fleet, each cryptographically its own TOTP seed + trust context). A node simply holds *N* seeds / trust-contexts rather than one. The per-subnet trust boundary is unchanged: within each subnet, all nodes are the one user's own mutually-trusted nodes (no stranger-auth). **Cross-*user* membership (joining another person's subnet) stays deferred**, but the membership model is multiplicity-ready so it can drop in later without re-architecture. An **external subnet** (relative to a given subnet) means *another subnet the node also belongs to* — in v1, always another of the user's own.
467
468	**Cross-user seam (forward design, not v1):** the seed key generalizes from *per-subnet* to **per-(subnet, user)** — a seed then doubles as an identifier+authenticator for *whose* it is. The pairing/code prompt becomes **2-stage** (select user / create-new → then select subnet / create-new); in v1 the single implicit user collapses it back to the 1-stage prompt. This makes node↔user attribution fall out for free ("node `enlyzeam` is using Brandon's `work` seed → that machine's active endpoint is Brandon's") and makes **force-unlink of an entire user's node-group** a single operation (drop that user's seeds). v1 stores seeds in a shape that already accommodates the (subnet, user) key.
469
470	**TOTP-seeded SPAKE2 pairing**:
471	The day-one pairing model. A durable **TOTP seed** is the subnet secret: generated on the first node, shown as a QR → stored in the user's authenticator app *and* held by every already-trusted node. To pair a new node, the rotating 6-digit TOTP code is used as the **password for a SPAKE2 (PAKE) handshake** — *not* as a bearer token verified by a seed-holder. A trusted node (online, no human needed — it holds the seed) computes the current code as its PAKE password; the user reads the code off their phone and types it into the new node. Matching codes → PAKE succeeds → pubkeys exchanged and bound, MITM-resistant encrypted channel established.
472
473	Why this construction (not plain TOTP-verification, not plain Magic Wormhole):
474	- **Not TOTP-as-bearer-token** — a 6-digit code sent over the wire for verification is ~20 bits, replayable within its 30s window, and doesn't bind to the key being exchanged (real-time MITM/replay seam). PAKE makes the low-entropy code MITM-resistant and limits attackers to one online guess per attempt (no offline brute force).
475	- **Better UX than plain Magic Wormhole** — Wormhole needs a fresh code on one node typed into the other, requiring a human/relay at *both* ends each pairing. With the persistent TOTP seed replicated to online trusted nodes, the trusted side is automatic; **the user only ever interacts with the new node** (read phone, type into new machine).
476
477	**trust store**:
478	Each node keeps a local store of authorized peer pubkeys. TOFU after pairing: accept on first connect post-ceremony, warn on key change. Lives near `node.key`.
479
480	**link discovery (TOTP + name)**:
481	The TOTP code is **also the link-discovery selector**, not only the PAKE password — it routes the two not-yet-trusted nodes to the same pairing rendezvous over the relay's pairing ALPN. Because a 6-digit code is a weak global selector, discovery+link takes **two inputs: the TOTP code + the subnet-name** (the R-PAIR-4 human label), which namespaces the rendezvous. **The subnet-name is the second input in all cases** (the unified rule):
482	- **join-existing** → enter the target subnet's name + its current code. Any online seed-holder of that subnet listens on that rendezvous and completes the PAKE; the joiner never enumerates the subnet's nodes.
483	- **create-new** → the subnet is **named at creation**: one machine generates the new seed *and names the subnet* up front (becoming the sole seed-holder), then the joiner uses that subnet-name + the new seed's code. No node-name mode — naming simply happens at link start rather than after.
484
485	The name only namespaces the rendezvous; a collision (two unrelated pairings sharing a code + name) merely fails the PAKE (wrong password) — no security break, just a retry. **Rendezvous-token hashing (under the hood):** the relay routes the *pre-trust* pairing by a rendezvous token; the payload is already E2E-encrypted (R-NET-2), but the token itself is relay-visible, so spt-core derives it as `H(subnet-name ‖ TOTP-epoch)` rather than the plaintext label — the user/agent still enters the **raw name + code**, the hash is internal.
486
487	**fetch-code-from-any-node (per-subnet, QR-optional)**:
488	Every node in a subnet holds that subnet's seed, so the user can fetch the *current* code for **any subnet the node belongs to** from any node in it — no phone required if a trusted node is handy. Because a node may be in several subnets (*subnet membership*), the fetch is **per-subnet**: CLI prompts *"Show TOTP for which subnet? [subnet-1 … subnet-n | create new subnet]"*, with **`--subnet <name>` / `--create-new`** flags to bypass the prompt (the agent-driven path). The code is offered optionally as a **QR / `otpauth://` URI** so the seed can be stored directly in an authenticator app (Google Authenticator etc.).
489
490	**Node-bound code fetch is gated behind OS privilege elevation** (Windows UAC / Linux root-or-equivalent): retrieving a subnet's code *from the node* requires either hardware/elevated access OR an **elevated endpoint** (an agent whose process is elevated can surface it). This means the node-bound path proves real possession of the machine; everyone else falls back to **their own authenticator-app TOTP store** (where the seed was stored at pairing). The gate narrows the multi-subnet exposure — without elevation, mere CLI/agent presence on a node no longer yields *any* subnet's join-code; with it, node compromise still implies full trust loss for that node's subnets (unchanged baseline).
491
492	**subnet naming**:
493	The first time two nodes pair, the user is prompted to **name the subnet**. The name is a human label on the subnet identity (which is cryptographically the shared seed). On every subsequent pairing the same name is shown ("adding node to subnet `<name>`"), giving the user confidence they're extending the right subnet as the fleet grows.
494
495	**subnet icon** (GUI metadata): an optional small image (square PNG/WebP, ≤256 KB) representing the subnet in future GUIs. Stored **inline** in subnet metadata (so it syncs for free over the same subnet-material channel as the name; the cap keeps inline-sync trivial — it's an icon, not a media library), **editable by any node in the subnet, any time** (seeded at create/name). **GUI-only consumer** (frontend milestone); data distribution rides M4.
496
497	Design caveats (carried forward, none disqualifying): a seed-holder must be online + reachable by the untrusted new node at pairing time (relay allows pre-trust contact on a pairing ALPN); ±1 TOTP window tolerance; rate-limit handshake attempts; **recovery** = re-provision the QR from any still-trusted node (all hold the seed) — losing all nodes *and* the auth app makes the subnet unrecoverable (documented); **revocation** of a paired node is a separate trust-store delete (TOTP gates joining only).
498
499	**local-link authentication**:
500	Cross-node traffic already rides Iroh (E2E-encrypted by node keypairs). The exposed surface is *same-node* channels where potentially-untrusted code touches SPT — a shell binary's HTTP/stdin/relay link, HTTP-to-harness-binary delivery. These require a **per-link handshake at `api bind` that establishes a link token + local-channel encryption**, required on every subsequent message — a local-link auth capability so other local processes can't inject into or read a link. Shell↔broker links specifically are encrypted + handshaked.
501
502	**binary-trust disclosure (accepted risk)**:
503	A shell binary is the least-trusted code in the system (3rd-party, runs on the user's node, agent-controllable) — but the same is true of harness binaries. The capability toolset bounds what an *agent* may ask; it does not sandbox what the *binary* may do with its OS permissions. Optional **shell-binary sandboxing** is deferred (gated like instantiate-anywhere/remote-exec). The baseline stance: **running any adapter or shell binary means trusting it — a disclosed, accepted risk for all spt-core users.**
504
505	## Consent & security gates
506
507	Gates the high-risk cross-node capabilities. **Trust boundary:** everything is within *one user's own subnet* (their own TOTP-SPAKE2-paired, mutually-trusted nodes; cross-subnet is deferred). So consent is **not** stranger-authentication — it is guardrails against (1) an agent doing something surprising/destructive the user didn't want, (2) **runaway** (spawning instances / burning compute/billing across nodes), (3) **compromise containment** (limiting a compromised node/agent's cross-node blast radius).
508
509	**Consent model — hybrid (grants + interactive escalation):**
510	- **Grant store** — records `capability × subject (agent) × target (node)` (this granularity is sufficient; finer per-command-pattern scoping is a later refinement). **Enforced at the target node** (the node receiving a remote action checks its local grants), **settable subnet-wide** (a grant authored from any node propagates to the target), and **revocable**. Lives with subnet security material (near the trust store), not in the context git repo.
511	- **Interactive escalation** — an ungranted high-risk action → the target node routes a **consent prompt to the user's most-recently-active session** (the registry-resolution precursor the update-prompt uses; PresenceChannel later generalizes it). Options: **allow-once / allow-always (writes a grant) / deny** — plus the harness's native free-text field (e.g. CC's AskUserQuestion "Other") for refinement.
512	- **Pre-consent flags** — the shell `can_shutdown` and endpoint `shell_wake_spawn_anywhere` flags are simply grants authored ahead of time via manifest/endpoint settings (same model, different authoring path).
513
514	**What is gated vs not:**
515	- **Gated (default-deny, grant or prompt):** remote command execution (highest risk), instantiate-anywhere (spawn an endpoint on a remote node).
516	- **Pre-consented by flag:** shell owner-shutdown, shell wake-spawn-anywhere.
517	- **Ungated:** remote-drive of your *own running* instance (low risk — your own instance; a light "node X is driving `ling@desktop`" notification suffices), cross-node context/Psyche sync (your own data).
518
519	**endpoint access whitelist** (distinct from the grant store — the outer reach gate):
520	A per-endpoint allow-list controlling **who may remotely reach** an endpoint, **by origin node** (and, later, by user). The load-bearing semantic: it checks the **origin node of the inbound interaction** (where the operator is acting), **not** the sender endpoint's identity or home — *"the operator must be on a whitelisted machine; the sender endpoint's location is irrelevant."* It is a **stateful-firewall** model, not a blanket block:
521	- **Outbound** from the endpoint → any **visible** node (the whitelist never restricts who it talks *to*).
522	- **Inbound** → a **reply** correlated to the endpoint's own prior outbound (`__REPLY_TO__`) is allowed from any visible node ("established/related"); an **unsolicited/direct** control or message ("new inbound") is allowed only from a **whitelisted** node.
523	- **Same-node operation is always allowed** (you're at the hardware; the home node never whitelists itself). The whitelist gates **remote (cross-node)** reach only.
524
525	**Default empty = open** (any subnet-visible node, current behavior); setting a whitelist *restricts*. **Node-tier ships now (M4)** — whitelist Ed25519 node pubkeys. **User-tier is deferred** (the per-(subnet,user) model, ADR-0006 cross-user seam) — the schema reserves an inert `users` field until then. Enforced **at the target endpoint's node**, synced as **security material near the trust store** (not the context repo), subnet-settable, revocable — same plumbing as the grant store, **different table + polarity** (origin-node/default-open vs agent-subject/default-deny). **Composition — three orthogonal gates, nested:** subnet *visibility* (is it routable here at all?) → *access whitelist* (may your node reach it?) → *capability grants* (may this agent do this high-risk thing?). Discovery (*resource advertisement*) is gated by the first two.
526
527	**Scope:** the gated *capabilities* (remote-exec, instantiate-anywhere) are deferred, so the *full* consent UX lands with them. **v1 ships the framework seam** — the grant-store shape + the deliver-consent-to-user mechanism — so the deferred capabilities drop into a coherent gate without a rewrite.
528
529	### Messaging substrate
530
531	**file-transfer progress**:
532	Every file transfer over the substrate is addressable and **progress-queryable mid-flight** by both the agent and the SPT binary (for the GUI) — at any point during the transfer. A substrate-wide requirement, not shell-specific (the Shell text+file channel inherits it).
533
534	## Installation
535
536	spt-core is per-machine and harness-independent, so it installs *before* and *independent of* any adapter.
537

## Instances

The multi-instance identity model. An agent is no longer "a perch on one machine"; identity and materialization split into two layers:

**endpoint ID**:
The logical identity (`ling`). Shared subnet-wide. The thing you address. **Adapter-agnostic**: the same ID can run under different adapters over its life (start on `claude-spt`, later revive on `spt-codex` or `spt-pi`) — adapters expose the same SPT-relevant capability subset, so the ID is not bound to an adapter. The currently-running `adapter_name` is a property of the live instance, not part of the identity. **At most one instance of an ID per node** is allowed; a second attempt on the same node (any adapter) is rejected. **Identity is node-global, advertised per-subnet** (see *subnet membership*): one `ling` exists on a node (one mind, one `tracked/` context) and is advertised into each subnet the node belongs to, subject to per-subnet visibility (see *endpoint visibility*). The same bare name may exist as *distinct* endpoints in different subnets; the resolution policy forces qualification when both are visible. Within a single subnet a bare id is unique — enforced by a **join-time collision check**: when a node carrying endpoint `X` joins/advertises into a subnet that already has a different `X`, the clash is surfaced and resolved (rename one — below) before advertisement. An endpoint keeps an **ordered adapter history** (in `tracked/agents/<id>/meta.json`, synced) driving the resume UX: latest adapter is the default resume offer, then next-latest, … oldest, then "choose a different adapter". (Adapter-agnosticism is an *agent-endpoint* property; Shells are adapter/platform-bound — see Shells.)

**adapter selection (creation & change)**:
At **creation**, the adapter is chosen from the node's **registered `kind="harness"` adapters whose `hostable_types` includes the endpoint's type** — **auto if exactly one qualifies, else chosen** (`--adapter` / picker): the same *auto-if-one-ask-if-many* rule as *home subnet*. The choice seeds the **head of the ordered adapter history** and the live `adapter_name`. **Changing** an endpoint's adapter is **not** a standalone operation — adapter is a *live-instance* property, so you change it by **launching/resuming the endpoint under a different registered adapter** (the resume UX surfaces the adapter history: head = default, then prior adapters, then "choose a different adapter"). For a currently-active instance this is a **resume-under-the-new-adapter** carrying context via the *fresh-with-preload* psyche-download (a session cycle, not a hot swap — the mind is adapter-agnostic). Same gate both times: the target adapter must be **registered on the node** and its `hostable_types` must include the endpoint's type.

**instance**:
The *same* endpoint (same harness + adapter) running **natively on a machine** — local files, local compute. `ling@desktop` and `ling@laptop` are both real local `ling`s, not remote views of one. What is per-node vs synced:
- **Per-node (anchored, cannot teleport):** the project working directory / files, and the harness session history. A git repo can't teleport; files are obtained locally (e.g. `git pull`).
- **Synced across instances:** the **Psyche context** (the agent's mind). See the two-tier sync below.

This is the core meaning of "the same agent on multiple machines": same identity, native local materialization on each, with the mind kept in sync. The ID is the shared identity for addressing; files are local; the mind follows.

**dormant / suspended instance:**
At most one instance of an endpoint is typically the *actively-driven* one; the others rest. Two resting states:
- **dormant** (default, *warm*): a live, state-preserved seat whose harness session stays running → instant re-activation. The default resting state.
- **suspended** (opt-in, *cold*): the harness session is closed and resumed-on-wake → frees RAM/compute (and, for billable harnesses, footprint), at the cost of a resume on wake. Reached via per-endpoint opt-in *auto-suspend*, or on demand: a subcommand can **suspend an endpoint from anywhere** (any node).

**State transitions (no idle timer):**
- **active** = the instance is the **most-recently-interacted instance for that ID** *and* **has a driver attached**. Both conditions. There is **no idle-grace timer** — a session stays active as long as it holds those two properties. A **linked Shell counts as a driver** (an agent with a live shell stays active even when the user isn't typing).
- **active → dormant** — the driver detaches, *or* another instance of the same ID becomes the most-recently-interacted one (attention shifts). No explicit stop needed to switch; driving `ling@laptop` makes `ling@desktop` dormant.
- **dormant → suspended** — manual (`spt suspend` / `spt shutdown` / shell `api owner-shutdown`), *or* opt-in auto-suspend after an `auto-suspend-after` threshold **counted from the moment the instance went dormant**.
- **wake (→ active)** — `spt wake`, a driver attaches, or a shell wake-watcher fires.

A resting instance retains its files + last context and is re-activatable in place — a lightweight **wake** (state's already there), distinct from instantiate-anywhere's fresh spawn. Resting instances remain addressable by node (`ling@desktop`). Registry status: **active / dormant / suspended / offline** (offline = node unreachable).

**Default policy:** warm (dormant) is the default when undriven. **Auto-suspend is opt-in, default OFF globally**, but **node-overridable** (a handheld / Pi / resource-constrained node may default it ON) and **per-endpoint overridable** (e.g. a billable harness whose warm session holds a cost footprint). Thresholds (`auto-suspend-after`) are config knobs: global default → node override → endpoint override. *Confirmed by measurement (M4-D9-3, `docs/DORMANCY-BUDGET.md`): an idle warm seat burns zero CPU — the cost is RSS only (~8 MiB shell-class, ~300 MiB LLM-harness-class); suspended residual is just the on-disk record.*

**Commands** (user/agent-initiated lifecycle, unprefixed): `spt suspend <id[@node]>` (suspend any of your instances from any node), `spt wake <id[@node]>` (explicitly wake a dormant/suspended instance), `spt shutdown` (an agent suspends its *own* endpoint — graceful, fires the suspend boundary signoff, cascades shells offline), `spt refresh` (spt-hosted only — an agent clears + resumes itself without stalling: `/clear` → commune capture → guaranteed post-clear resume signal so its turn restarts from immediate next-steps; see `docs/CONTEXT-MEMORY.md`).

**`spt rename <id> <new_id>`** (endpoint rename, **rippled**): change an endpoint's logical ID and propagate the change to **all its instances** subnet-wide — the registry entries, every node's perch, and the synced context branches (`a-<id>` → `a-<new_id>`, and project-view references). The rename is one logical operation against the eventually-consistent registry; it must collision-check `new_id` against every subnet the endpoint is advertised into (the join-time check above), and reconcile concurrent renames by the same precedence marker the context merge uses (node + newest-wins, KNOWN-HAZARDS 6.5). The adapter-agnostic, node-anchored identity (ADR-0003) is what makes a clean rename possible; without it the ID would be entangled with per-adapter or per-session state.

**`spt fork <id> [--into <subnet>] [--as <new_id>] [--delete-source]`** (cross-subnet clone):
Clone an endpoint into a **new, identity-distinct endpoint** on another subnet — the sanctioned way to place an agent's mind in a different subnet (home subnet being immutable). The fork's **home = the target subnet**; it is **seeded with a one-time copy** of the source's mind (the live + project *Cross-node Psyche sync* tiers), after which the two **diverge** — there is **no ongoing sync**. A fork is therefore **not an instance**: an *instance* is the **same** identity on another node with a **synced** mind; a *fork* is a **new** identity with a **copied-then-independent** mind. The new id defaults to the source's bare name if free in the target subnet (the join-time collision check; else prompt/rename); the adapter follows the normal creation rule (*adapter selection*). The **source is untouched**, optionally deleted afterward (`--delete-source`, or a later delete). Distinct from **rename** (same identity, new label, rippled) and **instantiate-anywhere** (same identity, new *node*, consent-gated).

**transition echo commune:** any active → (dormant | suspended) transition fires an **echo commune** capturing the outgoing instance's final context delta; that commune syncs to whichever instance becomes active next. This is the finer mechanism behind catch-up-on-activation (and mirrors the sister project's "echo commune before signoff" pattern — KNOWN-HAZARDS 3.3).

**deferred-message gate:** deferred (spool-only, hook-consumer) messages are **not** delivered to a dormant or suspended instance — they hold until it is active again. (Extends the deferred-row semantics, KNOWN-HAZARDS 1.4/4.4, with an instance-state gate.)

**Remote-control vs local operation (two distinct modes — not the same as instances):**
- **Operate locally:** drive the native instance on *your* machine (its local files, its synced mind). The normal case.
- **Remote-control (Shell-like):** attach a control/view surface to an instance *running on another node* — compute + files stay remote; you are a viewport (the byte-stream terminal attach, daemon-to-daemon over Iroh). Used when you specifically want *that machine's* environment. This is effectively a Shell (a driven surface, user→agent direction), separate from the instance concept itself.
- **Smooth handoff (remote-control → local):** no teardown required. `git pull` brings the files; the **fresh-with-preload resume seam** preloads the other instance's latest synced context (psyche-download); the remote instance simply goes dormant. The mind follows; the files are local.

### Pieces the Instances model requires

**subnet registry**:
Distributed, eventually-consistent map `endpoint_id → [instances: (node, perch, type, status)]`, **per subnet**. Every `spt-daemon` participates in the registry of each subnet it belongs to. This is the "agent name resolution" the sister project deferred — now first-class and mandatory.

**endpoint visibility (per-(endpoint, subnet))**:
Whether an endpoint is exposed to a given subnet. **Hidden means *excluded*** — not advertised in that subnet's registry **and not routable from it** (addressing it from that subnet fails until explicitly revealed), not merely unlisted. This makes hiding a real boundary, ready for the cross-user (b) seam, not a silent non-boundary. Computed as: endpoint `E` is **hidden in subnet `S` iff** `S.hide_new_endpoints` (a per-subnet policy captured at the node's **join time**) **OR** `E.default_hide_from_new_subnets` (a per-endpoint setting) — **unless** an explicit per-`(E,S)` override says otherwise (the override always wins). Both defaults ship **OFF** (visible); hiding is opt-in. **Visibility gates sync:** `hidden ⟹ not synced`, and replicating a mind into `S` (the sync-membership list under *Cross-node Psyche sync*) requires `E` be visible in `S` — visibility is the outer gate, sync-membership the inner opt-in within visible subnets.

**home subnet (per endpoint)**:
The single subnet that anchors an endpoint's *defaults*: the default **sync** scope (subnet-exclusive mind replication — *Cross-node Psyche sync*) and the subnet that qualifies its **bare name**. **Assigned at creation:** the node's **sole** subnet automatically; on a **multi-subnet** node it **must be specified** at creation (no silent guess — mirrors the *resolution policy*'s refuse-and-qualify); on an **unpaired** node the endpoint is local-only until first join, when home is set. Distinct from advertisement: identity is **node-global and advertised into *every* subnet the node belongs to** per the visibility defaults — home subnet is only the default-scope *anchor*, not a limit on where the endpoint is visible or addressable. **Home is immutable** — there is **no re-home** (ADR-0010). To place an agent's mind in a different subnet, **fork** it (see *`spt fork`*) and optionally delete the original; this avoids any scope-migration / stale-mind ambiguity (the source is untouched until explicitly deleted).

<!-- [doc->REQ-INST-14] -->
**resource advertisement (subnet resource registry)**:
A per-endpoint **free-text blurb** describing the services/functions the endpoint can serve — an agent **yellow-pages** for service discovery, distinct from *capability declaration* (machine-readable, which endpoint *types* a node hosts) and from *endpoint visibility* (whether it's addressable at all). **Both-authored + mutable:** config seeds a default; the agent refines its own at runtime (`spt resources set …`). It is **not a separate registry** — it is a field on the endpoint record and a **projection** of the subnet registry (`(id, node, resources-blurb)`), surfaced as the "subnet resource registry" view. **Gated by the access layers:** an endpoint excluded from subnet S (visibility) — or, later, one whose access whitelist (*endpoint access whitelist*) excludes the viewer's node — never appears in that view; discovery leaks nothing a viewer couldn't reach. Synced as **registry data, not context** (directory metadata, not the agent's mind). M4 (needs the distributed registry).

**resolution policy**:
How bare `ling` resolves when multiple instances exist. Rule: local-node instance if present → else most-recently-active → explicit override always available. **Two qualifier syntaxes**, combinable: **subnet-qualified** `home:ling` and **node-qualified** `ling@hfenduleam` (→ `home:ling@hfenduleam`). **Multi-subnet ambiguity rule:** under node-global identity (endpoint ID below), a node on several subnets may see two *distinct* endpoints that share a bare name (a `home:ling` and an unrelated `work:ling`). When a bare `id` is ambiguous across the visible subnets, resolution **refuses and forces qualification** (by subnet or node) rather than guessing. Within a single subnet, bare ids stay unique (join-time collision check, below).

**instantiate-anywhere** (seam day-one, behavior deferred):
From node Y, spin up an instance of an endpoint on node X by routing a launch command to X's daemon, which runs the endpoint's manifest locally. "Agents can spin up instances (including of themselves) on any node, possibly requiring user consent" — so a **consent gate** governs remote instantiation. The consent-UX design is deferred; the model is built to accommodate it.

**cross-instance context freshness** (two-tier):
When a dormant instance is activated again, it is unobtrusively fed the latest Psyche context — newest-among-peers *and* newer than its own. Scoped by the existing Psyche-context split:
- **live context** (per-agent, project-independent — the agent's general mind): synced to **all** instances of the endpoint, regardless of project.
- **project context** (per-agent-per-project — project-specific detail): synced **only** to instances sharing the **same project**.

So all `ling` instances catch up on live context; only same-project `ling` instances catch up on project context. This resolves the "divergent simultaneous work" concern (two instances on different projects don't clobber each other's project detail) without losing the "mind follows everywhere" property.

The precedence/freshness guard (KNOWN-HAZARDS 6.5) — with node identity in the marker — keeps a dormant instance's stale context from clobbering the active authoritative one.

**Authoring directives + memformat** (full design: `docs/CONTEXT-MEMORY.md`): the live/project split is made stable by (1) a per-*topic* cross-project discriminator with default-to-project bias, (2) **commune-source asymmetry** — echo communes are project-primary and live-conservative (the leak fix), and (3) **deep memformat integration** — memformat becomes the load-bearing, self-evolving, **tier-tagged** schema (each topic tagged `live`/`project`, decided once at creation, re-tiering migrates content), whose topics are containers of **structured blocks** (id + Concepts/Aliases/Applies-to + Source + timestamps + Hash + Pinned). A generated **Retrieval Map (INDEX)** per project + per agent realizes cross-agent synthesis + query-routing. **Start-injection** is dense on the working set (live + current-project hot/`Pinned` blocks → immediate productivity) and sparse on the tail (other projects, cross-agent synthesis, cold blocks → pointer + INDEX, on-demand), tunable via a density knob. Several patterns adapted from `BigscreenVR/pi-agent-memory`.

### Off-node reach-back

When the user is driving an instance from off-node, the agent should be able to:
- **detect** that it is being driven remotely (and from which node) — observable from the `spt-daemon`, which knows whether the driving session-surface stream is local or network-attached; (v1)
- **transfer files** to/from the user's (driving) node — file payloads over the messaging substrate; (v1)
- **run commands** on the user's node — remote command execution, **deferred + security-gated** (shares the consent model with instantiate-anywhere; arbitrary cross-node exec is the highest-risk capability in the system). Deferred partly because workarounds exist: the remote-node agent can message a local-node agent to run the command, or the user can interact with a local-node agent directly. (deferred)

### v1 scope line (V1-mid)

- **In v1:** the endpoint-ID / instance data-model split; the subnet registry; resolution policy; remote-drive of *already-running* instances (network session-surface); cross-node Psyche sync (full — replaces gh-repo-sync); cross-instance context-freshness auto-feed; off-node file transfer; off-node remote-drive detection.
- **Seam-compatible but deferred:** instantiate-anywhere + its consent gate; remote command execution + its security gate (workarounds: remote agent messages a local agent, or user interacts with a local agent directly).
- **Rationale:** the data-model split and registry are foundations — deferring them forces a later rewrite, so they are in regardless. Remote-drive of a running instance is the highest-value slice and exercises the whole networking+daemon stack end-to-end. Instantiate-anywhere and remote-exec carry consent/security design that should not gate v1.

## Workspace & versioning

The crate-boundary design is the rebuild's main value (ADR-0001) — honest decoupling that prevents the sister project's tangle from re-forming. Principle: **many small, acyclically-layered crates** (not a few fat ones).

Layering (bottom → top): `spt-proto` (wire envelope grammar, endpoint types, message framing, Ed25519 identity types, typed+binary payload schema — pure types, no I/O) → `spt-store` (spool, perch layout, trust store, registry persistence) → `spt-msg` (delivery TCP+spool-fallback, routing, send/ring) → `spt-net` (Iroh, mDNS, SPAKE2/TOTP pairing, subnet-registry distribution; `net` feature-flagged) and `spt-term` (session-surface trait, PTY impl, broker) and `spt-runtime` (`AgentRuntime` trait, `ManifestRuntime`, manifest schema) → `spt-live` (Psyche/pulse/commune/signoff) → `spt-daemon` (the brain: ties all, hosts broker, update orchestration, instance registry) → `spt` (the `spt.exe` binary; thin CLI).

**public SDK surface** (semver-committed): `spt-proto` (the wire contract — anyone speaking SPT depends on it), `spt-runtime` (the trait third-party harnesses implement), `spt-msg` (the deeper messaging-integration path). Everything else (`spt-store`, `spt-net`, `spt-term`, `spt-live`, `spt-daemon`) is internal/unstable pre-1.0 — reachable, but no stability promise. The *integration* crates are stable; the *machinery* churns.

**wire-protocol version vs crate semver** (orthogonal, never conflated): `spt-proto` carries an explicit **wire-protocol version** independent of crate semver. Two nodes — or a node and a linked library consumer — interoperate iff their proto versions are compatible, with a documented **N-1 compat window**. Crate semver governs *API* stability; proto version governs *wire* stability.

## Pairing & trust

How a single user's own nodes come to trust each other and form a subnet. Cross-*user* subnet (two *different* users' subnets interoperating) is explicitly deferred (the sister project's "cross-subnet contract" — very-early future concept), but the model is **built with the seam for it** (see *subnet membership*).

**subnet membership (multi-subnet)**:
A node may be a member of **multiple subnets at once** — but, for v1, only multiple subnets *of the same user* (e.g. a `home` fleet and a `work` fleet, each cryptographically its own TOTP seed + trust context). A node simply holds *N* seeds / trust-contexts rather than one. The per-subnet trust boundary is unchanged: within each subnet, all nodes are the one user's own mutually-trusted nodes (no stranger-auth). **Cross-*user* membership (joining another person's subnet) stays deferred**, but the membership model is multiplicity-ready so it can drop in later without re-architecture. An **external subnet** (relative to a given subnet) means *another subnet the node also belongs to* — in v1, always another of the user's own.

**Cross-user seam (forward design, not v1):** the seed key generalizes from *per-subnet* to **per-(subnet, user)** — a seed then doubles as an identifier+authenticator for *whose* it is. The pairing/code prompt becomes **2-stage** (select user / create-new → then select subnet / create-new); in v1 the single implicit user collapses it back to the 1-stage prompt. This makes node↔user attribution fall out for free ("node `enlyzeam` is using Brandon's `work` seed → that machine's active endpoint is Brandon's") and makes **force-unlink of an entire user's node-group** a single operation (drop that user's seeds). v1 stores seeds in a shape that already accommodates the (subnet, user) key.

**TOTP-seeded SPAKE2 pairing**:
The day-one pairing model. A durable **TOTP seed** is the subnet secret: generated on the first node, shown as a QR → stored in the user's authenticator app *and* held by every already-trusted node. To pair a new node, the rotating 6-digit TOTP code is used as the **password for a SPAKE2 (PAKE) handshake** — *not* as a bearer token verified by a seed-holder. A trusted node (online, no human needed — it holds the seed) computes the current code as its PAKE password; the user reads the code off their phone and types it into the new node. Matching codes → PAKE succeeds → pubkeys exchanged and bound, MITM-resistant encrypted channel established.

Why this construction (not plain TOTP-verification, not plain Magic Wormhole):
- **Not TOTP-as-bearer-token** — a 6-digit code sent over the wire for verification is ~20 bits, replayable within its 30s window, and doesn't bind to the key being exchanged (real-time MITM/replay seam). PAKE makes the low-entropy code MITM-resistant and limits attackers to one online guess per attempt (no offline brute force).
- **Better UX than plain Magic Wormhole** — Wormhole needs a fresh code on one node typed into the other, requiring a human/relay at *both* ends each pairing. With the persistent TOTP seed replicated to online trusted nodes, the trusted side is automatic; **the user only ever interacts with the new node** (read phone, type into new machine).

**trust store**:
Each node keeps a local store of authorized peer pubkeys. TOFU after pairing: accept on first connect post-ceremony, warn on key change. Lives near `node.key`.

**link discovery (TOTP + name)**:
The TOTP code is **also the link-discovery selector**, not only the PAKE password — it routes the two not-yet-trusted nodes to the same pairing rendezvous over the relay's pairing ALPN. Because a 6-digit code is a weak global selector, discovery+link takes **two inputs: the TOTP code + the subnet-name** (the R-PAIR-4 human label), which namespaces the rendezvous. **The subnet-name is the second input in all cases** (the unified rule):
- **join-existing** → enter the target subnet's name + its current code. Any online seed-holder of that subnet listens on that rendezvous and completes the PAKE; the joiner never enumerates the subnet's nodes.
- **create-new** → the subnet is **named at creation**: one machine generates the new seed *and names the subnet* up front (becoming the sole seed-holder), then the joiner uses that subnet-name + the new seed's code. No node-name mode — naming simply happens at link start rather than after.

The name only namespaces the rendezvous; a collision (two unrelated pairings sharing a code + name) merely fails the PAKE (wrong password) — no security break, just a retry. **Rendezvous-token hashing (under the hood):** the relay routes the *pre-trust* pairing by a rendezvous token; the payload is already E2E-encrypted (R-NET-2), but the token itself is relay-visible, so spt-core derives it as `H(subnet-name ‖ TOTP-epoch)` rather than the plaintext label — the user/agent still enters the **raw name + code**, the hash is internal.

**fetch-code-from-any-node (per-subnet, QR-optional)**:
Every node in a subnet holds that subnet's seed, so the user can fetch the *current* code for **any subnet the node belongs to** from any node in it — no phone required if a trusted node is handy. Because a node may be in several subnets (*subnet membership*), the fetch is **per-subnet**: CLI prompts *"Show TOTP for which subnet? [subnet-1 … subnet-n | create new subnet]"*, with **`--subnet <name>` / `--create-new`** flags to bypass the prompt (the agent-driven path). The code is offered optionally as a **QR / `otpauth://` URI** so the seed can be stored directly in an authenticator app (Google Authenticator etc.).

**Node-bound code fetch is gated behind OS privilege elevation** (Windows UAC / Linux root-or-equivalent): retrieving a subnet's code *from the node* requires either hardware/elevated access OR an **elevated endpoint** (an agent whose process is elevated can surface it). This means the node-bound path proves real possession of the machine; everyone else falls back to **their own authenticator-app TOTP store** (where the seed was stored at pairing). The gate narrows the multi-subnet exposure — without elevation, mere CLI/agent presence on a node no longer yields *any* subnet's join-code; with it, node compromise still implies full trust loss for that node's subnets (unchanged baseline).

**subnet naming**:
The first time two nodes pair, the user is prompted to **name the subnet**. The name is a human label on the subnet identity (which is cryptographically the shared seed). On every subsequent pairing the same name is shown ("adding node to subnet `<name>`"), giving the user confidence they're extending the right subnet as the fleet grows.

**subnet icon** (GUI metadata): an optional small image (square PNG/WebP, ≤256 KB) representing the subnet in future GUIs. Stored **inline** in subnet metadata (so it syncs for free over the same subnet-material channel as the name; the cap keeps inline-sync trivial — it's an icon, not a media library), **editable by any node in the subnet, any time** (seeded at create/name). **GUI-only consumer** (frontend milestone); data distribution rides M4.

Design caveats (carried forward, none disqualifying): a seed-holder must be online + reachable by the untrusted new node at pairing time (relay allows pre-trust contact on a pairing ALPN); ±1 TOTP window tolerance; rate-limit handshake attempts; **recovery** = re-provision the QR from any still-trusted node (all hold the seed) — losing all nodes *and* the auth app makes the subnet unrecoverable (documented); **revocation** of a paired node is a separate trust-store delete (TOTP gates joining only).

**local-link authentication**:
Cross-node traffic already rides Iroh (E2E-encrypted by node keypairs). The exposed surface is *same-node* channels where potentially-untrusted code touches SPT — a shell binary's HTTP/stdin/relay link, HTTP-to-harness-binary delivery. These require a **per-link handshake at `api bind` that establishes a link token + local-channel encryption**, required on every subsequent message — a local-link auth capability so other local processes can't inject into or read a link. Shell↔broker links specifically are encrypted + handshaked.

**binary-trust disclosure (accepted risk)**:
A shell binary is the least-trusted code in the system (3rd-party, runs on the user's node, agent-controllable) — but the same is true of harness binaries. The capability toolset bounds what an *agent* may ask; it does not sandbox what the *binary* may do with its OS permissions. Optional **shell-binary sandboxing** is deferred (gated like instantiate-anywhere/remote-exec). The baseline stance: **running any adapter or shell binary means trusting it — a disclosed, accepted risk for all spt-core users.**

## Consent & security gates

Gates the high-risk cross-node capabilities. **Trust boundary:** everything is within *one user's own subnet* (their own TOTP-SPAKE2-paired, mutually-trusted nodes; cross-subnet is deferred). So consent is **not** stranger-authentication — it is guardrails against (1) an agent doing something surprising/destructive the user didn't want, (2) **runaway** (spawning instances / burning compute/billing across nodes), (3) **compromise containment** (limiting a compromised node/agent's cross-node blast radius).

**Consent model — hybrid (grants + interactive escalation):**
- **Grant store** — records `capability × subject (agent) × target (node)` (this granularity is sufficient; finer per-command-pattern scoping is a later refinement). **Enforced at the target node** (the node receiving a remote action checks its local grants), **settable subnet-wide** (a grant authored from any node propagates to the target), and **revocable**. Lives with subnet security material (near the trust store), not in the context git repo.
- **Interactive escalation** — an ungranted high-risk action → the target node routes a **consent prompt to the user's most-recently-active session** (the registry-resolution precursor the update-prompt uses; PresenceChannel later generalizes it). Options: **allow-once / allow-always (writes a grant) / deny** — plus the harness's native free-text field (e.g. CC's AskUserQuestion "Other") for refinement.
- **Pre-consent flags** — the shell `can_shutdown` and endpoint `shell_wake_spawn_anywhere` flags are simply grants authored ahead of time via manifest/endpoint settings (same model, different authoring path).

**What is gated vs not:**
- **Gated (default-deny, grant or prompt):** remote command execution (highest risk), instantiate-anywhere (spawn an endpoint on a remote node).
- **Pre-consented by flag:** shell owner-shutdown, shell wake-spawn-anywhere.
- **Ungated:** remote-drive of your *own running* instance (low risk — your own instance; a light "node X is driving `ling@desktop`" notification suffices), cross-node context/Psyche sync (your own data).

**endpoint access whitelist** (distinct from the grant store — the outer reach gate):
A per-endpoint allow-list controlling **who may remotely reach** an endpoint, **by origin node** (and, later, by user). The load-bearing semantic: it checks the **origin node of the inbound interaction** (where the operator is acting), **not** the sender endpoint's identity or home — *"the operator must be on a whitelisted machine; the sender endpoint's location is irrelevant."* It is a **stateful-firewall** model, not a blanket block:
- **Outbound** from the endpoint → any **visible** node (the whitelist never restricts who it talks *to*).
- **Inbound** → a **reply** correlated to the endpoint's own prior outbound (`__REPLY_TO__`) is allowed from any visible node ("established/related"); an **unsolicited/direct** control or message ("new inbound") is allowed only from a **whitelisted** node.
- **Same-node operation is always allowed** (you're at the hardware; the home node never whitelists itself). The whitelist gates **remote (cross-node)** reach only.

**Default empty = open** (any subnet-visible node, current behavior); setting a whitelist *restricts*. **Node-tier ships now (M4)** — whitelist Ed25519 node pubkeys. **User-tier is deferred** (the per-(subnet,user) model, ADR-0006 cross-user seam) — the schema reserves an inert `users` field until then. Enforced **at the target endpoint's node**, synced as **security material near the trust store** (not the context repo), subnet-settable, revocable — same plumbing as the grant store, **different table + polarity** (origin-node/default-open vs agent-subject/default-deny). **Composition — three orthogonal gates, nested:** subnet *visibility* (is it routable here at all?) → *access whitelist* (may your node reach it?) → *capability grants* (may this agent do this high-risk thing?). Discovery (*resource advertisement*) is gated by the first two.

**Scope:** the gated *capabilities* (remote-exec, instantiate-anywhere) are deferred, so the *full* consent UX lands with them. **v1 ships the framework seam** — the grant-store shape + the deliver-consent-to-user mechanism — so the deferred capabilities drop into a coherent gate without a rewrite.

### Messaging substrate

**file-transfer progress**:
Every file transfer over the substrate is addressable and **progress-queryable mid-flight** by both the agent and the SPT binary (for the GUI) — at any point during the transfer. A substrate-wide requirement, not shell-specific (the Shell text+file channel inherits it).

## Installation

spt-core is per-machine and harness-independent, so it installs *before* and *independent of* any adapter.