1	# SPT
2
3	## What is SPT?
4
5	A communication ecosystem for all of your computers. Interact with _any resource_, on _any machine_, **from** any machine, using any interface. Empower your LLMs to do the same.
6
7	`spt-core` is the heart of a harness-independent framework. It offers: inter-agent messaging, live agent lifecycle, terminal hosting, seamless self-update, and zero-config cross-machine networking — shipped as both a Rust library workspace and a single canonical binary (`spt` / `spt.exe`).
8
9	SPT lets coding agents talk to each other — across sessions, across projects, and across machines — without a central server. An agent running under one harness can message an agent under another, keep a persistent identity and memory across context resets, and be reached (or revived) from any paired machine. Three pieces make that up:
10	- **(a)** the `spt` binary, the per-machine core that does the work — messaging, lifecycle, terminal hosting, P2P networking — and is what this repository distributes.
11	- **(b)** *harness adapters*, thin declarative manifests that teach spt-core how to drive a specific agent harness. An adapter for Claude Code is in development; harnesses like Codex and Pi — the [pi coding agent](https://github.com/badlogic/pi-mono) — are the kind of target the contract is designed for.
12	- **(c)** *shells*, bridge applications to any resource or service that an agent can drive and be driven through. The adapter contract is public, documented, and royalty-free to build against — today's surface for third parties is the binary plus that contract.
13
14	## Status
15
16	| Capability | State |
17	|---|---|
18	| Inter-agent messaging (send / ring / ready / poll), same-machine and cross-machine | ✅ works today |
19	| Live-agent lifecycle (persistent identity, context communes, dormancy/wake) | ✅ works today |
20	| Terminal hosting + remote attach (drive a session on another node) | ✅ works today |
21	| Zero-config P2P networking (QUIC) + TOTP-seeded machine pairing, private subnets | ✅ works today |
22	| Harness contract: manifest + `spt api` (build your own adapter) | ✅ works today |
23	| Shell adapters (e.g. desktop-notification shell) + `spt adapter add` | ✅ works today |
24	| Signed, peer-propagated self-update | ✅ works today |
25	| Claude Code harness adapter (`spt-claude-code`) | 🚧 in development |
26	| OS-service registration at install (always-on after reboot) | 🔜 next |
27	| macOS builds | 🔜 next |
28	| SDK crates on crates.io | 🔜 later |
29
30	Platforms today: **Windows + Linux**.
31
32	## Install
33
34	One line, non-interactive. Downloads the latest release, sha256-verifies it, places the binary, and registers it on your user PATH.
35
36	**Linux:**
37
38	```sh
39	curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh
40	```
41
42	**Windows (PowerShell):**
43
44	```powershell
45	irm https://sabermage.github.io/spt-releases/install.ps1 | iex
46	```
47
48	Then verify:
49
50	```
51	spt --version
52	```
53
54	> **Windows note:** the PATH registration takes effect in *new* terminals. The installer prints the absolute path to the binary so you can use it immediately in the current one.
55
56	## Getting started — developers
57
58	Two agents exchanging a message, end to end, in under 10 minutes:
59
60	- **[Human quickstart](https://sabermage.github.io/spt-releases/quickstart/messaging.html)** — install, `spt ready` two agents, send and poll a message, then see what actually happened.
61
62	## Getting started — adapter developers & dev-agents
63
64	Build a working harness or shell adapter from the published contract alone:
65
66	- **[Adapter quickstart](https://sabermage.github.io/spt-releases/quickstart/adapter.html)** — a minimal adapter that satisfies the manifest + `spt api` contract, walked through via the `mock-adapter` reference (source ships with every release).
67	- **[Manifest JSON Schema](https://sabermage.github.io/spt-releases/manifest.schema.json)** — validate your adapter manifest mechanically.
68
69	## Documentation
70
71	- **Docs site:** <https://sabermage.github.io/spt-releases> — mental model, quickstarts, the complete harness-contract reference, CLI reference, rustdoc.
72	- **For AI agents:** [`llms.txt`](https://sabermage.github.io/spt-releases/llms.txt) (curated index) and [`llms-full.txt`](https://sabermage.github.io/spt-releases/llms-full.txt) (full export). Append `.md` to any doc URL for raw markdown.
73
74	## License
75

# SPT

## What is SPT?

A communication ecosystem for all of your computers. Interact with _any resource_, on _any machine_, **from** any machine, using any interface. Empower your LLMs to do the same.

`spt-core` is the heart of a harness-independent framework. It offers: inter-agent messaging, live agent lifecycle, terminal hosting, seamless self-update, and zero-config cross-machine networking — shipped as both a Rust library workspace and a single canonical binary (`spt` / `spt.exe`).

SPT lets coding agents talk to each other — across sessions, across projects, and across machines — without a central server. An agent running under one harness can message an agent under another, keep a persistent identity and memory across context resets, and be reached (or revived) from any paired machine. Three pieces make that up:
- **(a)** the `spt` binary, the per-machine core that does the work — messaging, lifecycle, terminal hosting, P2P networking — and is what this repository distributes.
- **(b)** *harness adapters*, thin declarative manifests that teach spt-core how to drive a specific agent harness. An adapter for Claude Code is in development; harnesses like Codex and Pi — the [pi coding agent](https://github.com/badlogic/pi-mono) — are the kind of target the contract is designed for.
- **(c)** *shells*, bridge applications to any resource or service that an agent can drive and be driven through. The adapter contract is public, documented, and royalty-free to build against — today's surface for third parties is the binary plus that contract.

## Status

| Capability | State |
|---|---|
| Inter-agent messaging (send / ring / ready / poll), same-machine and cross-machine | ✅ works today |
| Live-agent lifecycle (persistent identity, context communes, dormancy/wake) | ✅ works today |
| Terminal hosting + remote attach (drive a session on another node) | ✅ works today |
| Zero-config P2P networking (QUIC) + TOTP-seeded machine pairing, private subnets | ✅ works today |
| Harness contract: manifest + `spt api` (build your own adapter) | ✅ works today |
| Shell adapters (e.g. desktop-notification shell) + `spt adapter add` | ✅ works today |
| Signed, peer-propagated self-update | ✅ works today |
| Claude Code harness adapter (`spt-claude-code`) | 🚧 in development |
| OS-service registration at install (always-on after reboot) | 🔜 next |
| macOS builds | 🔜 next |
| SDK crates on crates.io | 🔜 later |

Platforms today: **Windows + Linux**.

## Install

One line, non-interactive. Downloads the latest release, sha256-verifies it, places the binary, and registers it on your user PATH.

**Linux:**

```sh
curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh
```

**Windows (PowerShell):**

```powershell
irm https://sabermage.github.io/spt-releases/install.ps1 | iex
```

Then verify:

```
spt --version
```

> **Windows note:** the PATH registration takes effect in *new* terminals. The installer prints the absolute path to the binary so you can use it immediately in the current one.

## Getting started — developers

Two agents exchanging a message, end to end, in under 10 minutes:

- **[Human quickstart](https://sabermage.github.io/spt-releases/quickstart/messaging.html)** — install, `spt ready` two agents, send and poll a message, then see what actually happened.

## Getting started — adapter developers & dev-agents

Build a working harness or shell adapter from the published contract alone:

- **[Adapter quickstart](https://sabermage.github.io/spt-releases/quickstart/adapter.html)** — a minimal adapter that satisfies the manifest + `spt api` contract, walked through via the `mock-adapter` reference (source ships with every release).
- **[Manifest JSON Schema](https://sabermage.github.io/spt-releases/manifest.schema.json)** — validate your adapter manifest mechanically.

## Documentation

- **Docs site:** <https://sabermage.github.io/spt-releases> — mental model, quickstarts, the complete harness-contract reference, CLI reference, rustdoc.
- **For AI agents:** [`llms.txt`](https://sabermage.github.io/spt-releases/llms.txt) (curated index) and [`llms-full.txt`](https://sabermage.github.io/spt-releases/llms-full.txt) (full export). Append `.md` to any doc URL for raw markdown.

## License


Edit

| Inter-agent messaging (send / ring / ready / poll), same-machine and cross-machine | ✅ works today |

| Inter-agent messaging (send / ring / ready), same-machine and cross-machine | ✅ works today |


The file C:\Users\decid\Documents\projects\spt-core\releases-repo\README.md has been updated successfully. (file state is current in your context — no need to Read it back)

# SPT

## What is SPT?

A communication ecosystem for all of your computers. Interact with _any resource_, on _any machine_, **from** any machine, using any interface. Empower your LLMs to do the same.

`spt-core` is the heart of a harness-independent framework. It offers: inter-agent messaging, live agent lifecycle, terminal hosting, seamless self-update, and zero-config cross-machine networking — shipped as both a Rust library workspace and a single canonical binary (`spt` / `spt.exe`).

SPT lets coding agents talk to each other — across sessions, across projects, and across machines — without a central server. An agent running under one harness can message an agent under another, keep a persistent identity and memory across context resets, and be reached (or revived) from any paired machine. Three pieces make that up:
- **(a)** the `spt` binary, the per-machine core that does the work — messaging, lifecycle, terminal hosting, P2P networking — and is what this repository distributes.
- **(b)** *harness adapters*, thin declarative manifests that teach spt-core how to drive a specific agent harness. An adapter for Claude Code is in development; harnesses like Codex and Pi — the [pi coding agent](https://github.com/badlogic/pi-mono) — are the kind of target the contract is designed for.
- **(c)** *shells*, bridge applications to any resource or service that an agent can drive and be driven through. The adapter contract is public, documented, and royalty-free to build against — today's surface for third parties is the binary plus that contract.

## Status

| Capability | State |
|---|---|
| Inter-agent messaging (send / ring / ready / poll), same-machine and cross-machine | ✅ works today |
| Live-agent lifecycle (persistent identity, context communes, dormancy/wake) | ✅ works today |
| Terminal hosting + remote attach (drive a session on another node) | ✅ works today |
| Zero-config P2P networking (QUIC) + TOTP-seeded machine pairing, private subnets | ✅ works today |
| Harness contract: manifest + `spt api` (build your own adapter) | ✅ works today |
| Shell adapters (e.g. desktop-notification shell) + `spt adapter add` | ✅ works today |
| Signed, peer-propagated self-update | ✅ works today |
| Claude Code harness adapter (`spt-claude-code`) | 🚧 in development |
| OS-service registration at install (always-on after reboot) | 🔜 next |
| macOS builds | 🔜 next |
| SDK crates on crates.io | 🔜 later |

Platforms today: **Windows + Linux**.

## Install

One line, non-interactive. Downloads the latest release, sha256-verifies it, places the binary, and registers it on your user PATH.

**Linux:**

```sh
curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh
```

**Windows (PowerShell):**

```powershell
irm https://sabermage.github.io/spt-releases/install.ps1 | iex
```

Then verify:

```
spt --version
```

> **Windows note:** the PATH registration takes effect in *new* terminals. The installer prints the absolute path to the binary so you can use it immediately in the current one.

## Getting started — developers

Two agents exchanging a message, end to end, in under 10 minutes:

- **[Human quickstart](https://sabermage.github.io/spt-releases/quickstart/messaging.html)** — install, `spt ready` two agents, send and poll a message, then see what actually happened.

## Getting started — adapter developers & dev-agents

Build a working harness or shell adapter from the published contract alone:

- **[Adapter quickstart](https://sabermage.github.io/spt-releases/quickstart/adapter.html)** — a minimal adapter that satisfies the manifest + `spt api` contract, walked through via the `mock-adapter` reference (source ships with every release).
- **[Manifest JSON Schema](https://sabermage.github.io/spt-releases/manifest.schema.json)** — validate your adapter manifest mechanically.

## Documentation

- **Docs site:** <https://sabermage.github.io/spt-releases> — mental model, quickstarts, the complete harness-contract reference, CLI reference, rustdoc.
- **For AI agents:** [`llms.txt`](https://sabermage.github.io/spt-releases/llms.txt) (curated index) and [`llms-full.txt`](https://sabermage.github.io/spt-releases/llms-full.txt) (full export). Append `.md` to any doc URL for raw markdown.

## License

Split by artifact:

- **Install scripts, `mock-adapter`, and documentation content** — [MIT](./LICENSE-MIT). Copy them; that's what they're for.
- **The `spt` binary** — a short [proprietary freeware license](./LICENSE-BINARY): free to run and redistribute unmodified, no warranty. Building adapters, shells, or integrations against the public contract is explicitly **unrestricted and royalty-free**.

-| Inter-agent messaging (send / ring / ready / poll), same-machine and cross-machine | ✅ works today |

+| Inter-agent messaging (send / ring / ready), same-machine and cross-machine | ✅ works today |


- **[Human quickstart](https://sabermage.github.io/spt-releases/quickstart/messaging.html)** — install, `spt ready` two agents, send and poll a message, then see what actually happened.

- **[Human quickstart](https://sabermage.github.io/spt-releases/quickstart/messaging.html)** — install, optionally pair a second machine into a subnet (`spt subnet create` / `join`), then hand two agents their prompts: they bring themselves online with `spt ready`, exchange messages (live and spooled), and you see what actually happened.


# SPT

## What is SPT?

A communication ecosystem for all of your computers. Interact with _any resource_, on _any machine_, **from** any machine, using any interface. Empower your LLMs to do the same.

`spt-core` is the heart of a harness-independent framework. It offers: inter-agent messaging, live agent lifecycle, terminal hosting, seamless self-update, and zero-config cross-machine networking — shipped as both a Rust library workspace and a single canonical binary (`spt` / `spt.exe`).

SPT lets coding agents talk to each other — across sessions, across projects, and across machines — without a central server. An agent running under one harness can message an agent under another, keep a persistent identity and memory across context resets, and be reached (or revived) from any paired machine. Three pieces make that up:
- **(a)** the `spt` binary, the per-machine core that does the work — messaging, lifecycle, terminal hosting, P2P networking — and is what this repository distributes.
- **(b)** *harness adapters*, thin declarative manifests that teach spt-core how to drive a specific agent harness. An adapter for Claude Code is in development; harnesses like Codex and Pi — the [pi coding agent](https://github.com/badlogic/pi-mono) — are the kind of target the contract is designed for.
- **(c)** *shells*, bridge applications to any resource or service that an agent can drive and be driven through. The adapter contract is public, documented, and royalty-free to build against — today's surface for third parties is the binary plus that contract.

## Status

| Capability | State |
|---|---|
| Inter-agent messaging (send / ring / ready), same-machine and cross-machine | ✅ works today |
| Live-agent lifecycle (persistent identity, context communes, dormancy/wake) | ✅ works today |
| Terminal hosting + remote attach (drive a session on another node) | ✅ works today |
| Zero-config P2P networking (QUIC) + TOTP-seeded machine pairing, private subnets | ✅ works today |
| Harness contract: manifest + `spt api` (build your own adapter) | ✅ works today |
| Shell adapters (e.g. desktop-notification shell) + `spt adapter add` | ✅ works today |
| Signed, peer-propagated self-update | ✅ works today |
| Claude Code harness adapter (`spt-claude-code`) | 🚧 in development |
| OS-service registration at install (always-on after reboot) | 🔜 next |
| macOS builds | 🔜 next |
| SDK crates on crates.io | 🔜 later |

Platforms today: **Windows + Linux**.

## Install

One line, non-interactive. Downloads the latest release, sha256-verifies it, places the binary, and registers it on your user PATH.

**Linux:**

```sh
curl -fsSL https://sabermage.github.io/spt-releases/install.sh | sh
```

**Windows (PowerShell):**

```powershell
irm https://sabermage.github.io/spt-releases/install.ps1 | iex
```

Then verify:

```
spt --version
```

> **Windows note:** the PATH registration takes effect in *new* terminals. The installer prints the absolute path to the binary so you can use it immediately in the current one.

## Getting started — developers

Two agents exchanging a message, end to end, in under 10 minutes:

- **[Human quickstart](https://sabermage.github.io/spt-releases/quickstart/messaging.html)** — install, `spt ready` two agents, send and poll a message, then see what actually happened.

## Getting started — adapter developers & dev-agents

Build a working harness or shell adapter from the published contract alone:

- **[Adapter quickstart](https://sabermage.github.io/spt-releases/quickstart/adapter.html)** — a minimal adapter that satisfies the manifest + `spt api` contract, walked through via the `mock-adapter` reference (source ships with every release).
- **[Manifest JSON Schema](https://sabermage.github.io/spt-releases/manifest.schema.json)** — validate your adapter manifest mechanically.

## Documentation

- **Docs site:** <https://sabermage.github.io/spt-releases> — mental model, quickstarts, the complete harness-contract reference, CLI reference, rustdoc.
- **For AI agents:** [`llms.txt`](https://sabermage.github.io/spt-releases/llms.txt) (curated index) and [`llms-full.txt`](https://sabermage.github.io/spt-releases/llms-full.txt) (full export). Append `.md` to any doc URL for raw markdown.

## License

Split by artifact:

- **Install scripts, `mock-adapter`, and documentation content** — [MIT](./LICENSE-MIT). Copy them; that's what they're for.
- **The `spt` binary** — a short [proprietary freeware license](./LICENSE-BINARY): free to run and redistribute unmodified, no warranty. Building adapters, shells, or integrations against the public contract is explicitly **unrestricted and royalty-free**.

-- **[Human quickstart](https://sabermage.github.io/spt-releases/quickstart/messaging.html)** — install, `spt ready` two agents, send and poll a message, then see what actually happened.

+- **[Human quickstart](https://sabermage.github.io/spt-releases/quickstart/messaging.html)** — install, optionally pair a second machine into a subnet (`spt subnet create` / `join`), then hand two agents their prompts: they bring themselves online with `spt ready`, exchange messages (live and spooled), and you see what actually happened.


Bash

grep -rn "spt pair\|spt poll\|alice\|\bbob\b" docs-site/src docs-site/llms.txt releases-repo/*.md 2>/dev/null | grep -v Binary

Acceptance sweep: banned references


(Bash completed with no output)


cargo run -p xtask -- check 2>&1 | tail -2 && traceable-reqs check >/dev/null 2>&1; echo "reqs=$?"

Drift gate + traceability after docs


     Running `target\debug\xtask.exe check`
xtask check: OK
reqs=0


D4: docs rewrite (quickstart, networking, sweeps, README, llms)

quickstart rewritten (done), networking overview (done), messaging sweep (done); remaining: concepts/instances lea-sergey sweep, SUMMARY/llms.txt heading updates, releases-repo README, xtask check, commit.


git add -A && git commit -m "$(cat <<'EOF'
docs(site): M7 D4 — quickstart persona rewrite, networking around subnet verbs, lea/sergey sweep

The docs deliverable (M7-PLAN.md D4, decisions 13-15):

- Quickstart rewritten to honest personas: humans install + (optionally,
  collapsed <details>) pair machines via subnet create/join/status with the
  cross-machine payoff line; agents get copy-paste PROMPT BLOCKS pointing
  them at the in-binary `spt how-to ready`/`send` (anti-drift: the page
  never duplicates the how-to text); offline-delivery demo now rides
  `ready --once`; "what just happened" gains the how-to + subnet bullets.
- Networking overview rewritten around the subnet verbs (create/join/
  status/show-totp walkthrough replaces the "walkthrough coming" stub),
  adds node labels + addressable @node, the always-on join answering, and
  the elevation-gate postures. Page retitled "Networking & subnets"
  (SUMMARY, llms.txt, concepts cross-refs updated).
- Messaging overview poll sweep (`ready` + `--once`; how-to pointer).
- alice/bob -> lea/sergey across docs-site (quickstart, concepts diagram,
  instances, messaging); internal docs unchanged per decision 14.
- releases-repo README: status table loses poll; quickstart bullet now
  describes the persona flow + pairing mention (decision 15).
- Acceptance #4 sweep green: no doc page references spt pair, spt poll,
  alice, or bob. xtask check + traceable-reqs check green.

(quickstart_e2e.rs still drives the old names — its step-for-step rewrite
against this page, with how-to output asserts, is the D5 deliverable.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
EOF
)" && git log --oneline -1

Commit M7 D4


warning: in the working copy of 'docs-site/llms.txt', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs-site/src/SUMMARY.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs-site/src/concepts/overview.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs-site/src/instances/overview.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs-site/src/messaging/overview.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs-site/src/networking/overview.md', LF will be replaced by CRLF the next time Git touches it
warning: in the working copy of 'docs-site/src/quickstart/messaging.md', LF will be replaced by CRLF the next time Git touches it
[dev-freeform 21025c8] docs(site): M7 D4 — quickstart persona rewrite, networking around subnet verbs, lea/sergey sweep
 8 files changed, 172 insertions(+), 84 deletions(-)
21025c8 docs(site): M7 D4 — quickstart persona rewrite, networking around subnet verbs, lea/sergey sweep


Updated task #13 status


grep -n "run_initiator\|rig_broker\|fn rig_broker\|port_pair\|peer_broker_addr\|fn role_a\|fn role_b\|Rung 1" crates/spt-daemon/tests/twohost.rs | head -25

Map twohost rung 1 structure


76:use spt_net::net::pairing::wire::{run_initiator, run_responder, SPT_PAIR_ALPN};
108:    port_pair: u16,
139:            port_pair: env_or("SPT_TWO_HOST_PORT_PAIR", 7462),
157:    fn peer_broker_addr(&self, role: &str) -> serde_json::Value {
172:            .with_ip_addr(SocketAddr::new(self.peer_ip, self.port_pair))
208:fn rig_broker(name: &str, identity: Identity, port: u16, dir: &std::path::Path) -> Arc<Broker> {
505:    let _broker = rig_broker(&broker_name, rig.id_b.clone(), rig.port_b, scratch.path());
527:    // ── Rung 1 — pair (responder). The REAL TOTP-seeded SPAKE2 ceremony over
538:            BindScope::Port(rig.port_pair),
574:        start_loops(&broker_name, &rig.b_hex(), rig.a_hex(), rig.peer_broker_addr("b"), scratch.path());
711:    // ── Rung 1 — pair (initiator). A holds NO subnet record yet: the 6-digit
742:            match run_initiator(
800:    let _broker = rig_broker(&broker_name, rig.id_a.clone(), rig.port_a, scratch.path());
802:        start_loops(&broker_name, &rig.a_hex(), rig.b_hex(), rig.peer_broker_addr("a"), scratch.path());
813:    let conn = a.net_dial(rig.peer_broker_addr("a"), Some(op())).expect("dial B");


150	    }
151	    fn b_hex(&self) -> String {
152	        self.id_b.public_key().to_hex()
153	    }
154
155	    /// The peer's broker address as the brain-IPC JSON value: derived node id
156	    /// + the static ip:port (no relay, no discovery — one direct path).
157	    fn peer_broker_addr(&self, role: &str) -> serde_json::Value {
158	        let (hex, port) = match role {
159	            "a" => (self.b_hex(), self.port_b),
160	            _ => (self.a_hex(), self.port_a),
161	        };
162	        let addr = addr_for_node_hex(&hex)
163	            .expect("derived node hex is a valid key")
164	            .with_ip_addr(SocketAddr::new(self.peer_ip, port));
165	        serde_json::to_value(&addr).expect("EndpointAddr serializes")
166	    }
167
168	    /// B's pre-trust pairing endpoint address (role A dials this).
169	    fn pair_addr(&self) -> EndpointAddr {
170	        addr_for_node_hex(&self.b_hex())
171	            .expect("derived node hex is a valid key")
172	            .with_ip_addr(SocketAddr::new(self.peer_ip, self.port_pair))
173	    }
174	}
175
176	fn derive32(secret: &str, tag: &str) -> [u8; 32] {
177	    let mut h = Sha256::new();
178	    h.update(secret.as_bytes());
179	    h.update(b":");
180	    h.update(tag.as_bytes());
181	    h.finalize().into()
182	}
183
184	fn env_or<T: std::str::FromStr>(key: &str, default: T) -> T {
185	    std::env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
186	}
187
188	fn env_or_str(key: &str, default: &str) -> String {
189	    std::env::var(key).unwrap_or_else(|_| default.to_string())
190	}
191
192	fn now_secs() -> u64 {
193	    SystemTime::now().duration_since(UNIX_EPOCH).expect("clock").as_secs()
194	}
195
196	fn now_ms() -> u64 {
197	    SystemTime::now().duration_since(UNIX_EPOCH).expect("clock").as_millis() as u64
198	}
199
200	// High offset: the production pump journals its own ops on the SAME broker —
201	// test-driven ops must never collide with an already-applied pump op id.
202	static OP: AtomicU64 = AtomicU64::new(1_000_000_000);
203	fn op() -> u64 {
204	    OP.fetch_add(1, Ordering::Relaxed)
205	}
206
207	/// Bind this host's broker on its fixed production-scoped port.
208	fn rig_broker(name: &str, identity: Identity, port: u16, dir: &std::path::Path) -> Arc<Broker> {
209	    let host = NetHost::start(NetConfig {
210	        identity,
211	        relay: RelayPolicy::Disabled,
212	        discovery: LocalDiscovery::Off,
213	        scope: BindScope::Port(port),
214	    })
215	    .expect("net host start");
216	    let broker =
217	        Broker::bind_in_with_net(name, dir.join("effects.log"), Some(host)).expect("bind broker");
218	    let serve = Arc::clone(&broker);
219	    thread::spawn(move || {
220	        let _ = serve.serve();
221	    });
222	    broker
223	}
224
225	fn connect_retry(name: &str) -> Brain {
226	    for _ in 0..300 {
227	        if let Ok(b) = Brain::cold_start(name, 1) {
228	            return b;
229	        }
230	        thread::sleep(Duration::from_millis(10));
231	    }
232	    panic!("brain could not connect");
233	}
234
235	/// Poll `probe` until true; panic with `what` after the rig deadline. The
236	/// cross-host barrier primitive — every rung waits on observable store state.
237	fn rig_wait(what: &str, deadline: Duration, mut probe: impl FnMut() -> bool) {
238	    let end = Instant::now() + deadline;
239	    while Instant::now() < end {

    }
    fn b_hex(&self) -> String {
        self.id_b.public_key().to_hex()
    }

    /// The peer's broker address as the brain-IPC JSON value: derived node id
    /// + the static ip:port (no relay, no discovery — one direct path).
    fn peer_broker_addr(&self, role: &str) -> serde_json::Value {
        let (hex, port) = match role {
            "a" => (self.b_hex(), self.port_b),
            _ => (self.a_hex(), self.port_a),
        };
        let addr = addr_for_node_hex(&hex)
            .expect("derived node hex is a valid key")
            .with_ip_addr(SocketAddr::new(self.peer_ip, port));
        serde_json::to_value(&addr).expect("EndpointAddr serializes")
    }

    /// B's pre-trust pairing endpoint address (role A dials this).
    fn pair_addr(&self) -> EndpointAddr {
        addr_for_node_hex(&self.b_hex())
            .expect("derived node hex is a valid key")
            .with_ip_addr(SocketAddr::new(self.peer_ip, self.port_pair))
    }
}

fn derive32(secret: &str, tag: &str) -> [u8; 32] {
    let mut h = Sha256::new();
    h.update(secret.as_bytes());
    h.update(b":");
    h.update(tag.as_bytes());
    h.finalize().into()
}

fn env_or<T: std::str::FromStr>(key: &str, default: T) -> T {
    std::env::var(key).ok().and_then(|v| v.parse().ok()).unwrap_or(default)
}

fn env_or_str(key: &str, default: &str) -> String {
    std::env::var(key).unwrap_or_else(|_| default.to_string())
}

fn now_secs() -> u64 {
    SystemTime::now().duration_since(UNIX_EPOCH).expect("clock").as_secs()
}

fn now_ms() -> u64 {
    SystemTime::now().duration_since(UNIX_EPOCH).expect("clock").as_millis() as u64
}

// High offset: the production pump journals its own ops on the SAME broker —
// test-driven ops must never collide with an already-applied pump op id.
static OP: AtomicU64 = AtomicU64::new(1_000_000_000);
fn op() -> u64 {
    OP.fetch_add(1, Ordering::Relaxed)
}

/// Bind this host's broker on its fixed production-scoped port.
fn rig_broker(name: &str, identity: Identity, port: u16, dir: &std::path::Path) -> Arc<Broker> {
    let host = NetHost::start(NetConfig {
        identity,
        relay: RelayPolicy::Disabled,
        discovery: LocalDiscovery::Off,
        scope: BindScope::Port(port),
    })
    .expect("net host start");
    let broker =
        Broker::bind_in_with_net(name, dir.join("effects.log"), Some(host)).expect("bind broker");
    let serve = Arc::clone(&broker);
    thread::spawn(move || {
        let _ = serve.serve();
    });
    broker
}

fn connect_retry(name: &str) -> Brain {
    for _ in 0..300 {
        if let Ok(b) = Brain::cold_start(name, 1) {
            return b;
        }
        thread::sleep(Duration::from_millis(10));
    }
    panic!("brain could not connect");
}

/// Poll `probe` until true; panic with `what` after the rig deadline. The
/// cross-host barrier primitive — every rung waits on observable store state.
fn rig_wait(what: &str, deadline: Duration, mut probe: impl FnMut() -> bool) {
    let end = Instant::now() + deadline;
    while Instant::now() < end {


700
701	// Two-host int evidence (rig-run): the drive half of every rung. Tags sit on
702	// the rung asserts below.
703	#[test]
704	fn two_host_ladder_role_a() {
705	    let Some(rig) = Rig::from_env("a") else { return };
706	    let home = TempDir::new().expect("temp home");
707	    std::env::set_var("SPT_HOME", home.path());
708	    let scratch = TempDir::new().expect("scratch");
709	    println!("TWOHOST role A: node {}", rig.a_hex());
710
711	    // ── Rung 1 — pair (initiator). A holds NO subnet record yet: the 6-digit
712	    // code is read off the shared TOTP seed (the operator channel), the ceremony
713	    // is the real cross-host SPAKE2, and success transfers the subnet seed
714	    // (A becomes a full member / future seed-holder).
715	    // [int->REQ-PAIR-1] [int->REQ-PAIR-5]
716	    let rt = tokio::runtime::Runtime::new().expect("tokio rt");
717	    rt.block_on(async {
718	        let ep = NetEndpoint::bind_scoped(
719	            &rig.id_a,
720	            RelayPolicy::Disabled,
721	            LocalDiscovery::Off,
722	            vec![SPT_PAIR_ALPN.to_vec()],
723	            BindScope::All,
724	        )
725	        .await
726	        .expect("pairing dialer bind");
727	        let end = Instant::now() + rig.wait;
728	        loop {
729	            assert!(Instant::now() < end, "pairing never completed within the rig window");
730	            let conn = match ep.connect_with_alpn(rig.pair_addr(), SPT_PAIR_ALPN).await {
731	                Ok(c) => c,
732	                Err(e) => {
733	                    eprintln!("pairing dial not up yet ({e}); retrying");
734	                    // Blocking sleep is fine: this runtime runs nothing else.
735	                    thread::sleep(Duration::from_secs(3));
736	                    continue;
737	                }
738	            };
739	            let code = rig.totp.code_at(now_secs());
740	            let mut trust = TrustStore::load();
741	            let mut subnets = SubnetStore::load();
742	            match run_initiator(
743	                &conn,
744	                rig.id_a.public_key(),
745	                &rig.subnet,
746	                &code,
747	                &mut trust,
748	                &mut subnets,
749	                now_secs(),
750	            )
751	            .await
752	            {
753	                Ok(outcome) => {
754	                    trust.save().expect("save trust");
755	                    subnets.save().expect("save subnets");
756	                    println!("TWOHOST OK: pairing (initiator) — {outcome:?}");
757	                    break;
758	                }
759	                Err(e) => {
760	                    eprintln!("pairing ceremony failed ({e}); retrying");
761	                    thread::sleep(Duration::from_secs(5));
762	                }
763	            }
764	        }
765	        ep.close().await;
766	    });
767	    let subnets_now = SubnetStore::load();
768	    assert_eq!(
769	        subnets_now.find(&rig.subnet).and_then(|r| r.seed_bytes()),
770	        Some(*rig.totp.as_bytes()),
771	        "the ceremony transferred the subnet seed to the joiner (REQ-PAIR-5)"
772	    );
773	    assert!(
774	        TrustStore::load().is_trusted(&rig.subnet, &rig.b_hex()),
775	        "the ceremony pinned B in A's canonical trust store"
776	    );
777
778	    // ── A's local seeds: perch (registry-advertised), mind (B pulls it),
779	    // the marker notif (fires on B), the trusted release-keys file. ──
780	    seed_common_stores(&rig);
781	    let _perch_a = seed_perch(ID_A);
782	    seed_mind(ID_A, "rig-a", "the mind B pulls across the rig");
783	    let store_a = NotifStore::open().expect("A notif store");
784	    store_a
785	        .merge_row(&NotifRow {
786	            notif_id: format!("{}:{}", rig.a_hex(), now_ms()),
787	            subnet: rig.subnet.clone(),
788	            kind: "agent".to_string(),
789	            from_id: format!("{ID_A}@{}", rig.a_hex()),
790	            body: NOTIF_BODY.to_string(),
791	            created_ms: now_ms(),
792	            dismissed: false,
793	            seen: Default::default(),
794	            last_surfaced_ms: 0,
795	        })
796	        .expect("seed marker notif");
797
798	    // ── Broker + production loops. ──
799	    let broker_name = format!("spt-twohost-a-{}.sock", std::process::id());
800	    let _broker = rig_broker(&broker_name, rig.id_a.clone(), rig.port_a, scratch.path());
801	    let (registry, stop) =
802	        start_loops(&broker_name, &rig.a_hex(), rig.b_hex(), rig.peer_broker_addr("a"), scratch.path());
803
804	    // ── Rung 2 — register: B's perch row replicates into A's registry.

// Two-host int evidence (rig-run): the drive half of every rung. Tags sit on
// the rung asserts below.
#[test]
fn two_host_ladder_role_a() {
    let Some(rig) = Rig::from_env("a") else { return };
    let home = TempDir::new().expect("temp home");
    std::env::set_var("SPT_HOME", home.path());
    let scratch = TempDir::new().expect("scratch");
    println!("TWOHOST role A: node {}", rig.a_hex());

    // ── Rung 1 — pair (initiator). A holds NO subnet record yet: the 6-digit
    // code is read off the shared TOTP seed (the operator channel), the ceremony
    // is the real cross-host SPAKE2, and success transfers the subnet seed
    // (A becomes a full member / future seed-holder).
    // [int->REQ-PAIR-1] [int->REQ-PAIR-5]
    let rt = tokio::runtime::Runtime::new().expect("tokio rt");
    rt.block_on(async {
        let ep = NetEndpoint::bind_scoped(
            &rig.id_a,
            RelayPolicy::Disabled,
            LocalDiscovery::Off,
            vec![SPT_PAIR_ALPN.to_vec()],
            BindScope::All,
        )
        .await
        .expect("pairing dialer bind");
        let end = Instant::now() + rig.wait;
        loop {
            assert!(Instant::now() < end, "pairing never completed within the rig window");
            let conn = match ep.connect_with_alpn(rig.pair_addr(), SPT_PAIR_ALPN).await {
                Ok(c) => c,
                Err(e) => {
                    eprintln!("pairing dial not up yet ({e}); retrying");
                    // Blocking sleep is fine: this runtime runs nothing else.
                    thread::sleep(Duration::from_secs(3));
                    continue;
                }
            };
            let code = rig.totp.code_at(now_secs());
            let mut trust = TrustStore::load();
            let mut subnets = SubnetStore::load();
            match run_initiator(
                &conn,
                rig.id_a.public_key(),
                &rig.subnet,
                &code,
                &mut trust,
                &mut subnets,
                now_secs(),
            )
            .await
            {
                Ok(outcome) => {
                    trust.save().expect("save trust");
                    subnets.save().expect("save subnets");
                    println!("TWOHOST OK: pairing (initiator) — {outcome:?}");
                    break;
                }
                Err(e) => {
                    eprintln!("pairing ceremony failed ({e}); retrying");
                    thread::sleep(Duration::from_secs(5));
                }
            }
        }
        ep.close().await;
    });
    let subnets_now = SubnetStore::load();
    assert_eq!(
        subnets_now.find(&rig.subnet).and_then(|r| r.seed_bytes()),
        Some(*rig.totp.as_bytes()),
        "the ceremony transferred the subnet seed to the joiner (REQ-PAIR-5)"
    );
    assert!(
        TrustStore::load().is_trusted(&rig.subnet, &rig.b_hex()),
        "the ceremony pinned B in A's canonical trust store"
    );

    // ── A's local seeds: perch (registry-advertised), mind (B pulls it),
    // the marker notif (fires on B), the trusted release-keys file. ──
    seed_common_stores(&rig);
    let _perch_a = seed_perch(ID_A);
    seed_mind(ID_A, "rig-a", "the mind B pulls across the rig");
    let store_a = NotifStore::open().expect("A notif store");
    store_a
        .merge_row(&NotifRow {
            notif_id: format!("{}:{}", rig.a_hex(), now_ms()),
            subnet: rig.subnet.clone(),
            kind: "agent".to_string(),
            from_id: format!("{ID_A}@{}", rig.a_hex()),
            body: NOTIF_BODY.to_string(),
            created_ms: now_ms(),
            dismissed: false,
            seen: Default::default(),
            last_surfaced_ms: 0,
        })
        .expect("seed marker notif");

    // ── Broker + production loops. ──
    let broker_name = format!("spt-twohost-a-{}.sock", std::process::id());
    let _broker = rig_broker(&broker_name, rig.id_a.clone(), rig.port_a, scratch.path());
    let (registry, stop) =
        start_loops(&broker_name, &rig.a_hex(), rig.b_hex(), rig.peer_broker_addr("a"), scratch.path());

    // ── Rung 2 — register: B's perch row replicates into A's registry.


Edit

/// `pair-join` payload — the two user-entered ceremony inputs (M7 D3).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PairJoinReq {
    /// The subnet to join (names the rendezvous + the seed).
    pub subnet: String,
    /// The current 6-digit TOTP code (the SPAKE2 password — consumed by the
    /// ceremony, never logged or journaled).
    pub code: String,
}

/// `pair-join` payload — the two user-entered ceremony inputs (M7 D3).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PairJoinReq {
    /// The subnet to join (names the rendezvous + the seed).
    pub subnet: String,
    /// The current 6-digit TOTP code (the SPAKE2 password — consumed by the
    /// ceremony, never logged or journaled).
    pub code: String,
    /// Skip the rendezvous and dial THIS responder address directly (a
    /// serde-serialized `EndpointAddr`) — the static-`ip:port` rig seam,
    /// mirroring `pairing::meet::meet_at`'s explicit-address form. The
    /// guided CLI never sets it.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub responder_addr: Option<serde_json::Value>,
}


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\msg.rs has been updated successfully. (file state is current in your context — no need to Read it back)

/// `pair-join` payload — the two user-entered ceremony inputs (M7 D3).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PairJoinReq {
    /// The subnet to join (names the rendezvous + the seed).
    pub subnet: String,
    /// The current 6-digit TOTP code (the SPAKE2 password — consumed by the
    /// ceremony, never logged or journaled).
    pub code: String,
}

/// `pair-join` payload — the two user-entered ceremony inputs (M7 D3).
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PairJoinReq {
    /// The subnet to join (names the rendezvous + the seed).
    pub subnet: String,
    /// The current 6-digit TOTP code (the SPAKE2 password — consumed by the
    /// ceremony, never logged or journaled).
    pub code: String,
    /// Skip the rendezvous and dial THIS responder address directly (a
    /// serde-serialized `EndpointAddr`) — the static-`ip:port` rig seam,
    /// mirroring `pairing::meet::meet_at`'s explicit-address form. The
    /// guided CLI never sets it.
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub responder_addr: Option<serde_json::Value>,
}

+    /// Skip the rendezvous and dial THIS responder address directly (a

+    /// serde-serialized `EndpointAddr`) — the static-`ip:port` rig seam,

+    /// mirroring `pairing::meet::meet_at`'s explicit-address form. The

+    #[serde(default, skip_serializing_if = "Option::is_none")]

+    pub responder_addr: Option<serde_json::Value>,

/// `pair-joined` reply — the ceremony's verdict, classified for the CLI's


/// The joiner leg (M7 plan decision 10): find a seed-holder via the LAN+relay
/// rendezvous, run the unchanged initiator ceremony against its real
/// endpoint, and persist the results (peer trust-pinned; the transferred
/// seed makes this node a full member / future seed-holder). One attempt —
/// the CLI owns the re-prompt loop.
// [impl->REQ-SUBNET-2]
pub async fn join_ceremony(
    endpoint: &NetEndpoint,
    subnet_name: &str,
    code: &str,
) -> Result<String, JoinFail> {
    let conn = dial_via_rendezvous(endpoint, subnet_name, time_step(now_secs()))
        .await
        .map_err(|e| JoinFail::NoSeedHolder(e.to_string()))?;

/// The joiner leg (M7 plan decision 10): find a seed-holder via the LAN+relay
/// rendezvous (or dial `responder_addr` directly — the static-`ip:port` rig
/// seam, mirroring [`meet_at`](spt_net::net::pairing::meet::meet_at)'s
/// explicit-address form), run the unchanged initiator ceremony against its
/// real endpoint, and persist the results (peer trust-pinned; the
/// transferred seed makes this node a full member / future seed-holder).
/// One attempt — the CLI owns the re-prompt loop.
// [impl->REQ-SUBNET-2]
pub async fn join_ceremony(
    endpoint: &NetEndpoint,
    subnet_name: &str,
    code: &str,
    responder_addr: Option<spt_net::net::endpoint::EndpointAddr>,
) -> Result<String, JoinFail> {
    let conn = match responder_addr {
        Some(addr) => endpoint
            .connect_with_alpn(addr, spt_net::net::pairing::wire::SPT_PAIR_ALPN)
            .await
            .map_err(|e| JoinFail::NoSeedHolder(e.to_string()))?,
        None => dial_via_rendezvous(endpoint, subnet_name, time_step(now_secs()))
            .await
            .map_err(|e| JoinFail::NoSeedHolder(e.to_string()))?,
    };


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\pairhost.rs has been updated successfully. (file state is current in your context — no need to Read it back)

/// The joiner leg (M7 plan decision 10): find a seed-holder via the LAN+relay
/// rendezvous, run the unchanged initiator ceremony against its real
/// endpoint, and persist the results (peer trust-pinned; the transferred
/// seed makes this node a full member / future seed-holder). One attempt —
/// the CLI owns the re-prompt loop.
// [impl->REQ-SUBNET-2]
pub async fn join_ceremony(
    endpoint: &NetEndpoint,
    subnet_name: &str,
    code: &str,
) -> Result<String, JoinFail> {
    let conn = dial_via_rendezvous(endpoint, subnet_name, time_step(now_secs()))
        .await
        .map_err(|e| JoinFail::NoSeedHolder(e.to_string()))?;

/// The joiner leg (M7 plan decision 10): find a seed-holder via the LAN+relay
/// rendezvous (or dial `responder_addr` directly — the static-`ip:port` rig
/// seam, mirroring [`meet_at`](spt_net::net::pairing::meet::meet_at)'s
/// explicit-address form), run the unchanged initiator ceremony against its
/// real endpoint, and persist the results (peer trust-pinned; the
/// transferred seed makes this node a full member / future seed-holder).
/// One attempt — the CLI owns the re-prompt loop.
// [impl->REQ-SUBNET-2]
pub async fn join_ceremony(
    endpoint: &NetEndpoint,
    subnet_name: &str,
    code: &str,
    responder_addr: Option<spt_net::net::endpoint::EndpointAddr>,
) -> Result<String, JoinFail> {
    let conn = match responder_addr {
        Some(addr) => endpoint
            .connect_with_alpn(addr, spt_net::net::pairing::wire::SPT_PAIR_ALPN)
            .await
            .map_err(|e| JoinFail::NoSeedHolder(e.to_string()))?,
        None => dial_via_rendezvous(endpoint, subnet_name, time_step(now_secs()))
            .await
            .map_err(|e| JoinFail::NoSeedHolder(e.to_string()))?,
    };

-/// rendezvous, run the unchanged initiator ceremony against its real

-/// endpoint, and persist the results (peer trust-pinned; the transferred

-/// seed makes this node a full member / future seed-holder). One attempt —

+/// rendezvous (or dial `responder_addr` directly — the static-`ip:port` rig

+/// seam, mirroring [`meet_at`](spt_net::net::pairing::meet::meet_at)'s

+/// explicit-address form), run the unchanged initiator ceremony against its

+/// real endpoint, and persist the results (peer trust-pinned; the

+/// transferred seed makes this node a full member / future seed-holder).

+/// One attempt — the CLI owns the re-prompt loop.

+    responder_addr: Option<spt_net::net::endpoint::EndpointAddr>,

-    let conn = dial_via_rendezvous(endpoint, subnet_name, time_step(now_secs()))

-        .map_err(|e| JoinFail::NoSeedHolder(e.to_string()))?;

+            .connect_with_alpn(addr, spt_net::net::pairing::wire::SPT_PAIR_ALPN)

+            .map_err(|e| JoinFail::NoSeedHolder(e.to_string()))?,

+        None => dial_via_rendezvous(endpoint, subnet_name, time_step(now_secs()))


    pub fn pair_join(
        &self,
        subnet: &str,
        code: &str,
    ) -> Result<String, crate::pairhost::JoinFail> {
        let endpoint = Arc::clone(&self.endpoint);
        self.runtime
            .handle()
            .block_on(async move { crate::pairhost::join_ceremony(&endpoint, subnet, code).await })
    }

    pub fn pair_join(
        &self,
        subnet: &str,
        code: &str,
        responder_addr: Option<EndpointAddr>,
    ) -> Result<String, crate::pairhost::JoinFail> {
        let endpoint = Arc::clone(&self.endpoint);
        self.runtime.handle().block_on(async move {
            crate::pairhost::join_ceremony(&endpoint, subnet, code, responder_addr).await
        })
    }


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\nethost.rs has been updated successfully. (file state is current in your context — no need to Read it back)

    pub fn pair_join(
        &self,
        subnet: &str,
        code: &str,
    ) -> Result<String, crate::pairhost::JoinFail> {
        let endpoint = Arc::clone(&self.endpoint);
        self.runtime
            .handle()
            .block_on(async move { crate::pairhost::join_ceremony(&endpoint, subnet, code).await })
    }

    pub fn pair_join(
        &self,
        subnet: &str,
        code: &str,
        responder_addr: Option<EndpointAddr>,
    ) -> Result<String, crate::pairhost::JoinFail> {
        let endpoint = Arc::clone(&self.endpoint);
        self.runtime.handle().block_on(async move {
            crate::pairhost::join_ceremony(&endpoint, subnet, code, responder_addr).await
        })
    }

+        responder_addr: Option<EndpointAddr>,

-            .block_on(async move { crate::pairhost::join_ceremony(&endpoint, subnet, code).await })

+        self.runtime.handle().block_on(async move {

+            crate::pairhost::join_ceremony(&endpoint, subnet, code, responder_addr).await

/// The node's stable name (lowercase-hex pubkey == iroh EndpointId).


        let host = self
            .net
            .as_ref()
            .ok_or("net disabled: this broker was bound without a network host")?;
        let reply = match host.pair_join(&req.subnet, &req.code) {

        let host = self
            .net
            .as_ref()
            .ok_or("net disabled: this broker was bound without a network host")?;
        // The optional explicit responder address (the rig seam) decodes
        // strictly — a malformed address is a usage error, not a dial flop.
        let responder_addr = match req.responder_addr {
            Some(v) => Some(
                serde_json::from_value::<EndpointAddr>(v)
                    .map_err(|e| format!("bad pair-join responder_addr: {e}"))?,
            ),
            None => None,
        };
        let reply = match host.pair_join(&req.subnet, &req.code, responder_addr) {


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\broker.rs has been updated successfully. (file state is current in your context — no need to Read it back)

        let host = self
            .net
            .as_ref()
            .ok_or("net disabled: this broker was bound without a network host")?;
        let reply = match host.pair_join(&req.subnet, &req.code) {

        let host = self
            .net
            .as_ref()
            .ok_or("net disabled: this broker was bound without a network host")?;
        // The optional explicit responder address (the rig seam) decodes
        // strictly — a malformed address is a usage error, not a dial flop.
        let responder_addr = match req.responder_addr {
            Some(v) => Some(
                serde_json::from_value::<EndpointAddr>(v)
                    .map_err(|e| format!("bad pair-join responder_addr: {e}"))?,
            ),
            None => None,
        };
        let reply = match host.pair_join(&req.subnet, &req.code, responder_addr) {

-        let reply = match host.pair_join(&req.subnet, &req.code) {

+        // The optional explicit responder address (the rig seam) decodes

+        // strictly — a malformed address is a usage error, not a dial flop.

+        let responder_addr = match req.responder_addr {

+                serde_json::from_value::<EndpointAddr>(v)

+                    .map_err(|e| format!("bad pair-join responder_addr: {e}"))?,

+        let reply = match host.pair_join(&req.subnet, &req.code, responder_addr) {


    pub fn pair_join(&mut self, subnet: &str, code: &str) -> io::Result<PairJoinReply> {
        self.send(
            KIND_PAIR_JOIN,
            serde_json::to_value(PairJoinReq { subnet: subnet.to_string(), code: code.to_string() })
                .expect("PairJoinReq serializes"),
        )?;

    pub fn pair_join(
        &mut self,
        subnet: &str,
        code: &str,
        responder_addr: Option<serde_json::Value>,
    ) -> io::Result<PairJoinReply> {
        self.send(
            KIND_PAIR_JOIN,
            serde_json::to_value(PairJoinReq {
                subnet: subnet.to_string(),
                code: code.to_string(),
                responder_addr,
            })
            .expect("PairJoinReq serializes"),
        )?;


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\src\brain.rs has been updated successfully. (file state is current in your context — no need to Read it back)

    pub fn pair_join(&mut self, subnet: &str, code: &str) -> io::Result<PairJoinReply> {
        self.send(
            KIND_PAIR_JOIN,
            serde_json::to_value(PairJoinReq { subnet: subnet.to_string(), code: code.to_string() })
                .expect("PairJoinReq serializes"),
        )?;

    pub fn pair_join(
        &mut self,
        subnet: &str,
        code: &str,
        responder_addr: Option<serde_json::Value>,
    ) -> io::Result<PairJoinReply> {
        self.send(
            KIND_PAIR_JOIN,
            serde_json::to_value(PairJoinReq {
                subnet: subnet.to_string(),
                code: code.to_string(),
                responder_addr,
            })
            .expect("PairJoinReq serializes"),
        )?;

/// reply classifies failures for the CLI's guided UX. Not journaled —

/// the code must never persist, and a re-join is store-idempotent.

-    pub fn pair_join(&mut self, subnet: &str, code: &str) -> io::Result<PairJoinReply> {

+        responder_addr: Option<serde_json::Value>,

-            serde_json::to_value(PairJoinReq { subnet: subnet.to_string(), code: code.to_string() })

-                .expect("PairJoinReq serializes"),

+            serde_json::to_value(PairJoinReq {

+                subnet: subnet.to_string(),

+                code: code.to_string(),

+            .expect("PairJoinReq serializes"),


let reply = match brain.pair_join(&name, &code) {

let reply = match brain.pair_join(&name, &code, None) {


The file C:\Users\decid\Documents\projects\spt-core\crates\spt\src\cli.rs has been updated successfully. (file state is current in your context — no need to Read it back)

eprintln!("Searching for a \"{name}\" member (LAN + relay)…");

-        let reply = match brain.pair_join(&name, &code) {

+        let reply = match brain.pair_join(&name, &code, None) {


    // ── Rung 1 — pair (responder). The REAL TOTP-seeded SPAKE2 ceremony over
    // the pre-trust ALPN, cross-host. Trust lands in the canonical store the
    // dispatcher gates read.
    // [int->REQ-PAIR-1]
    let rt = tokio::runtime::Runtime::new().expect("tokio rt");
    rt.block_on(async {
        let ep = NetEndpoint::bind_scoped(
            &rig.id_b,
            RelayPolicy::Disabled,
            LocalDiscovery::Off,
            vec![SPT_PAIR_ALPN.to_vec()],
            BindScope::Port(rig.port_pair),
        )
        .await
        .expect("pairing endpoint bind");
        let end = Instant::now() + rig.wait;
        let mut rate = PairingRateLimiter::new();
        loop {
            assert!(Instant::now() < end, "no pairing ceremony arrived within the rig window");
            let Some(incoming) = ep.accept().await else {
                panic!("pairing endpoint closed before a ceremony arrived")
            };
            let Ok(connecting) = incoming.accept() else { continue };
            let Ok(conn) = connecting.await else { continue };
            let mut trust = TrustStore::load();
            match run_responder(&conn, rig.id_b.public_key(), &subnets, &mut rate, &mut trust, now_secs())
                .await
            {
                Ok(outcome) => {
                    trust.save().expect("save trust");
                    println!("TWOHOST OK: pairing (responder) — {outcome:?}");
                    break;
                }
                Err(e) => eprintln!("pairing attempt failed (joiner may retry): {e}"),
            }
        }
        ep.close().await;
    });
    let trust_now = TrustStore::load();
    assert!(
        trust_now.is_trusted(&rig.subnet, &rig.a_hex()),
        "the ceremony pinned A in B's canonical trust store"
    );

    // ── Rung 1 — pair (responder): the PRODUCT surface (M7 D5). The
    // daemon-hosted ALWAYS-ON responder — the broker NetHost speaking the
    // pre-trust pairing ALPN beside the trusted surface (pairhost) — answers
    // A's CLI-path ceremony. This test arms NOTHING: no dedicated endpoint,
    // no manual run_responder loop; the rung passing proves a stock member
    // daemon pairs a joiner. Trust lands in the canonical store the
    // dispatcher gates read.
    // [int->REQ-PAIR-1] [int->REQ-SUBNET-2]
    rig_wait("pairing: A pinned via the daemon-hosted responder", rig.wait, || {
        TrustStore::load().is_trusted(&rig.subnet, &rig.a_hex())
    });
    println!("TWOHOST OK: pairing (responder, daemon-hosted)");


The file C:\Users\decid\Documents\projects\spt-core\crates\spt-daemon\tests\twohost.rs has been updated successfully. (file state is current in your context — no need to Read it back)

    // ── Rung 1 — pair (responder). The REAL TOTP-seeded SPAKE2 ceremony over
    // the pre-trust ALPN, cross-host. Trust lands in the canonical store the
    // dispatcher gates read.
    // [int->REQ-PAIR-1]
    let rt = tokio::runtime::Runtime::new().expect("tokio rt");
    rt.block_on(async {
        let ep = NetEndpoint::bind_scoped(
            &rig.id_b,
            RelayPolicy::Disabled,
            LocalDiscovery::Off,
            vec![SPT_PAIR_ALPN.to_vec()],
            BindScope::Port(rig.port_pair),
        )
        .await
        .expect("pairing endpoint bind");
        let end = Instant::now() + rig.wait;
        let mut rate = PairingRateLimiter::new();
        loop {
            assert!(Instant::now() < end, "no pairing ceremony arrived within the rig window");
            let Some(incoming) = ep.accept().await else {
                panic!("pairing endpoint closed before a ceremony arrived")
            };
            let Ok(connecting) = incoming.accept() else { continue };
            let Ok(conn) = connecting.await else { continue };
            let mut trust = TrustStore::load();
            match run_responder(&conn, rig.id_b.public_key(), &subnets, &mut rate, &mut trust, now_secs())
                .await
            {
                Ok(outcome) => {
                    trust.save().expect("save trust");
                    println!("TWOHOST OK: pairing (responder) — {outcome:?}");
                    break;
                }
                Err(e) => eprintln!("pairing attempt failed (joiner may retry): {e}"),
            }
        }
        ep.close().await;
    });
    let trust_now = TrustStore::load();
    assert!(
        trust_now.is_trusted(&rig.subnet, &rig.a_hex()),
        "the ceremony pinned A in B's canonical trust store"
    );

    // ── Rung 1 — pair (responder): the PRODUCT surface (M7 D5). The
    // daemon-hosted ALWAYS-ON responder — the broker NetHost speaking the
    // pre-trust pairing ALPN beside the trusted surface (pairhost) — answers
    // A's CLI-path ceremony. This test arms NOTHING: no dedicated endpoint,
    // no manual run_responder loop; the rung passing proves a stock member
    // daemon pairs a joiner. Trust lands in the canonical store the
    // dispatcher gates read.
    // [int->REQ-PAIR-1] [int->REQ-SUBNET-2]
    rig_wait("pairing: A pinned via the daemon-hosted responder", rig.wait, || {
        TrustStore::load().is_trusted(&rig.subnet, &rig.a_hex())
    });
    println!("TWOHOST OK: pairing (responder, daemon-hosted)");

println!("TWOHOST role B: drive session {sid} ready");

-    // ── Rung 1 — pair (responder). The REAL TOTP-seeded SPAKE2 ceremony over

-    // the pre-trust ALPN, cross-host. Trust lands in the canonical store the

+    // ── Rung 1 — pair (responder): the PRODUCT surface (M7 D5). The

+    // daemon-hosted ALWAYS-ON responder — the broker NetHost speaking the

+    // pre-trust pairing ALPN beside the trusted surface (pairhost) — answers

+    // A's CLI-path ceremony. This test arms NOTHING: no dedicated endpoint,

+    // no manual run_responder loop; the rung passing proves a stock member

+    // daemon pairs a joiner. Trust lands in the canonical store the

-    let rt = tokio::runtime::Runtime::new().expect("tokio rt");

-        let ep = NetEndpoint::bind_scoped(

-            vec![SPT_PAIR_ALPN.to_vec()],

-            BindScope::Port(rig.port_pair),

-        .expect("pairing endpoint bind");

-        let end = Instant::now() + rig.wait;

-        let mut rate = PairingRateLimiter::new();

-            assert!(Instant::now() < end, "no pairing ceremony arrived within the rig window");

-            let Some(incoming) = ep.accept().await else {

-                panic!("pairing endpoint closed before a ceremony arrived")

-            let Ok(connecting) = incoming.accept() else { continue };

-            let Ok(conn) = connecting.await else { continue };

-            let mut trust = TrustStore::load();

-            match run_responder(&conn, rig.id_b.public_key(), &subnets, &mut rate, &mut trust, now_secs())

-                    trust.save().expect("save trust");

-                    println!("TWOHOST OK: pairing (responder) — {outcome:?}");

-                Err(e) => eprintln!("pairing attempt failed (joiner may retry): {e}"),

+    // [int->REQ-PAIR-1] [int->REQ-SUBNET-2]

+    rig_wait("pairing: A pinned via the daemon-hosted responder", rig.wait, || {

+        TrustStore::load().is_trusted(&rig.subnet, &rig.a_hex())

-    let trust_now = TrustStore::load();

-        trust_now.is_trusted(&rig.subnet, &rig.a_hex()),

-        "the ceremony pinned A in B's canonical trust store"

+    println!("TWOHOST OK: pairing (responder, daemon-hosted)");

// ── Production loops: dispatcher (serves A's pulls/feeds) + pump (pushes

// B's registry/notif/mind, bootstrap-pulls A's). ──


455	fn signed_release(rig: &Rig) -> SignedRelease {
456	    let meta = ReleaseMetadata {
457	        version: 6,
458	        channel: "stable".to_string(),
459	        expires_at_ms: u64::MAX,
460	        key_id: "twohost".to_string(),
461	        artifact_sha256: sha256_hex(RELEASE_BYTES),
462	        brain_ipc_version: spt_daemon::IPC_PROTOCOL_VERSION,
463	        broker_resource_abi: BROKER_RESOURCE_ABI,
464	    };
465	    let metadata_json = serde_json::to_string(&meta).expect("meta json");
466	    let sig = rig.release_sk.sign(metadata_json.as_bytes());
467	    SignedRelease {
468	        metadata_json,
469	        signature_hex: sig.to_bytes().iter().map(|b| format!("{b:02x}")).collect(),
470	    }
471	}
472
473	// ───────────────────────── role B: seed-holder / server ─────────────────────────
474
475	// Two-host int evidence (rig-run): the serve half of every rung, plus the
476	// notif dismiss origination. Tags sit on the rung asserts below.
477	#[test]
478	fn two_host_ladder_role_b() {
479	    let Some(rig) = Rig::from_env("b") else { return };
480	    let home = TempDir::new().expect("temp home");
481	    std::env::set_var("SPT_HOME", home.path());
482	    let scratch = TempDir::new().expect("scratch");
483	    println!("TWOHOST role B: node {}", rig.b_hex());
484
485	    // ── Stores the ceremony + the ladder serve from. B is the seed-holder:
486	    // the subnet record (derived TOTP seed, epoch 1) exists here FIRST.
487	    let mut subnets = SubnetStore::load();
488	    subnets.add_joined(&rig.subnet, *rig.totp.as_bytes(), 1).expect("seed subnet");
489	    subnets.save().expect("save subnets");
490	    seed_common_stores(&rig);
491	    let perch_b = seed_perch(ID_B);
492	    seed_mind(ID_B, "rig-b", "the mind A pulls across the rig");
493	    ReleaseCache::open(&perch::spt_home().join("releases"))
494	        .stage(&signed_release(&rig), RELEASE_BYTES)
495	        .expect("stage v6 at B");
496	    // M5 rungs 10/11 (D9a): the notify shell adapter + the owner's instance
497	    // exist before the loops serve anything.
498	    let (render_file, real_mode) = seed_notify_shell(scratch.path());
499	    // M5 rung 9 (presence): the user is most-recently-active HERE — the
500	    // fresh stamp rides B's advertisement rows (REQ-PRES-1).
501	    info::set_last_active(&perch_b, now_ms()).expect("stamp B's recency");
502
503	    // ── Broker on the fixed port + the remote-drive target session. ──
504	    let broker_name = format!("spt-twohost-b-{}.sock", std::process::id());
505	    let _broker = rig_broker(&broker_name, rig.id_b.clone(), rig.port_b, scratch.path());
506	    let mut host_brain = connect_retry(&broker_name);
507	    #[cfg(unix)]
508	    let (program, args) = ("cat".to_string(), Vec::<String>::new());
509	    #[cfg(windows)]
510	    let (program, args) = ("findstr".to_string(), vec![".".to_string()]);
511	    let sid = host_brain
512	        .spawn_session(spt_daemon::msg::SpawnReq {
513	            program,
514	            args,
515	            rows: 24,
516	            cols: 80,
517	            endpoint: ID_B.to_string(),
518	        })
519	        .expect("spawn drive target");
520	    // Publish the session id where A's file-fetch rung reads it (the fetch IS
521	    // the first remote-drive leg).
522	    let xroot = spt_store::xfer::transfers_dir();
523	    std::fs::create_dir_all(&xroot).expect("transfer root");
524	    std::fs::write(xroot.join("sid.txt"), sid.to_string()).expect("write sid");
525	    println!("TWOHOST role B: drive session {sid} ready");
526
527	    // ── Rung 1 — pair (responder): the PRODUCT surface (M7 D5). The
528	    // daemon-hosted ALWAYS-ON responder — the broker NetHost speaking the
529	    // pre-trust pairing ALPN beside the trusted surface (pairhost) — answers

fn signed_release(rig: &Rig) -> SignedRelease {
    let meta = ReleaseMetadata {
        version: 6,
        channel: "stable".to_string(),
        expires_at_ms: u64::MAX,
        key_id: "twohost".to_string(),
        artifact_sha256: sha256_hex(RELEASE_BYTES),
        brain_ipc_version: spt_daemon::IPC_PROTOCOL_VERSION,
        broker_resource_abi: BROKER_RESOURCE_ABI,
    };
    let metadata_json = serde_json::to_string(&meta).expect("meta json");
    let sig = rig.release_sk.sign(metadata_json.as_bytes());
    SignedRelease {
        metadata_json,
        signature_hex: sig.to_bytes().iter().map(|b| format!("{b:02x}")).collect(),
    }
}

// ───────────────────────── role B: seed-holder / server ─────────────────────────

// Two-host int evidence (rig-run): the serve half of every rung, plus the
// notif dismiss origination. Tags sit on the rung asserts below.
#[test]
fn two_host_ladder_role_b() {
    let Some(rig) = Rig::from_env("b") else { return };
    let home = TempDir::new().expect("temp home");
    std::env::set_var("SPT_HOME", home.path());
    let scratch = TempDir::new().expect("scratch");
    println!("TWOHOST role B: node {}", rig.b_hex());

    // ── Stores the ceremony + the ladder serve from. B is the seed-holder:
    // the subnet record (derived TOTP seed, epoch 1) exists here FIRST.
    let mut subnets = SubnetStore::load();
    subnets.add_joined(&rig.subnet, *rig.totp.as_bytes(), 1).expect("seed subnet");
    subnets.save().expect("save subnets");
    seed_common_stores(&rig);
    let perch_b = seed_perch(ID_B);
    seed_mind(ID_B, "rig-b", "the mind A pulls across the rig");
    ReleaseCache::open(&perch::spt_home().join("releases"))
        .stage(&signed_release(&rig), RELEASE_BYTES)
        .expect("stage v6 at B");
    // M5 rungs 10/11 (D9a): the notify shell adapter + the owner's instance
    // exist before the loops serve anything.
    let (render_file, real_mode) = seed_notify_shell(scratch.path());
    // M5 rung 9 (presence): the user is most-recently-active HERE — the
    // fresh stamp rides B's advertisement rows (REQ-PRES-1).
    info::set_last_active(&perch_b, now_ms()).expect("stamp B's recency");

    // ── Broker on the fixed port + the remote-drive target session. ──
    let broker_name = format!("spt-twohost-b-{}.sock", std::process::id());
    let _broker = rig_broker(&broker_name, rig.id_b.clone(), rig.port_b, scratch.path());
    let mut host_brain = connect_retry(&broker_name);
    #[cfg(unix)]
    let (program, args) = ("cat".to_string(), Vec::<String>::new());
    #[cfg(windows)]
    let (program, args) = ("findstr".to_string(), vec![".".to_string()]);
    let sid = host_brain
        .spawn_session(spt_daemon::msg::SpawnReq {
            program,
            args,
            rows: 24,
            cols: 80,
            endpoint: ID_B.to_string(),
        })
        .expect("spawn drive target");
    // Publish the session id where A's file-fetch rung reads it (the fetch IS
    // the first remote-drive leg).
    let xroot = spt_store::xfer::transfers_dir();
    std::fs::create_dir_all(&xroot).expect("transfer root");
    std::fs::write(xroot.join("sid.txt"), sid.to_string()).expect("write sid");
    println!("TWOHOST role B: drive session {sid} ready");

    // ── Rung 1 — pair (responder): the PRODUCT surface (M7 D5). The
    // daemon-hosted ALWAYS-ON responder — the broker NetHost speaking the
    // pre-trust pairing ALPN beside the trusted surface (pairhost) — answers


    // ── Rung 1 — pair (initiator). A holds NO subnet record yet: the 6-digit
    // code is read off the shared TOTP seed (the operator channel), the ceremony
    // is the real cross-host SPAKE2, and success transfers the subnet seed
    // (A becomes a full member / future seed-holder).
    // [int->REQ-PAIR-1] [int->REQ-PAIR-5]
    let rt = tokio::runtime::Runtime::new().expect("tokio rt");
    rt.block_on(async {
        let ep = NetEndpoint::bind_scoped(
            &rig.id_a,
            RelayPolicy::Disabled,
            LocalDiscovery::Off,
            vec![SPT_PAIR_ALPN.to_vec()],
            BindScope::All,
        )
        .await
        .expect("pairing dialer bind");
        let end = Instant::now() + rig.wait;
        loop {
            assert!(Instant::now() < end, "pairing never completed within the rig window");
            let conn = match ep.connect_with_alpn(rig.pair_addr(), SPT_PAIR_ALPN).await {
                Ok(c) => c,
                Err(e) => {
                    eprintln!("pairing dial not up yet ({e}); retrying");
                    // Blocking sleep is fine: this runtime runs nothing else.
                    thread::sleep(Duration::from_secs(3));
                    continue;
                }
            };
            let code = rig.totp.code_at(now_secs());
            let mut trust = TrustStore::load();
            let mut subnets = SubnetStore::load();
            match run_initiator(
                &conn,
                rig.id_a.public_key(),
                &rig.subnet,
                &code,
                &mut trust,
                &mut subnets,
                now_secs(),
            )
            .await
            {
                Ok(outcome) => {
                    trust.save().expect("save trust");
                    subnets.save().expect("save subnets");
                    println!("TWOHOST OK: pairing (initiator) — {outcome:?}");
                    break;
                }
                Err(e) => {
                    eprintln!("pairing ceremony failed ({e}); retrying");
                    thread::sleep(Duration::from_secs(5));
                }
            }
        }
        ep.close().await;
    });

    // ── Broker FIRST (M7 D5): the joiner leg runs on the daemon's own
    // endpoint — the ceremony binds and pins the node identity the broker
    // owns (pairhost), so pairing now needs the broker up before anything.
    let broker_name = format!("spt-twohost-a-{}.sock", std::process::id());
    let _broker = rig_broker(&broker_name, rig.id_a.clone(), rig.port_a, scratch.path());

    // ── Rung 1 — pair (initiator): the PRODUCT surface (M7 D5) — the exact
    // `spt subnet join` path (brain `pair-join` IPC → broker joiner leg →
    // the unchanged SPAKE2 ceremony), replacing the test-harness-driven
    // run_initiator as the pairing evidence. A holds NO subnet record yet:
    // the 6-digit code is read off the shared TOTP seed (the operator
    // channel) and success transfers the subnet seed (A becomes a full
    // member / future seed-holder). The explicit responder address is the
    // static-`ip:port` rig seam (no relay, no discovery — rendezvous can't
    // resolve here).
    // [int->REQ-PAIR-1] [int->REQ-PAIR-5] [int->REQ-SUBNET-2]
    {
        let mut pair_brain = connect_retry(&broker_name);
        let responder = serde_json::to_value(rig.pair_addr()).expect("EndpointAddr serializes");
        let end = Instant::now() + rig.wait;
        loop {
            assert!(Instant::now() < end, "pairing never completed within the rig window");
            let code = rig.totp.code_at(now_secs());
            match pair_brain.pair_join(&rig.subnet, &code, Some(responder.clone())) {
                Ok(reply) if reply.ok => {
                    println!("TWOHOST OK: pairing (initiator, product surface) — {}", reply.detail);
                    break;
                }
                Ok(reply) => {
                    // B's daemon may not be serving yet, or the window
                    // rolled — the guided-UX classes drive the retry.
                    eprintln!(
                        "pairing attempt refused ({}: {}); retrying",
                        reply.class.as_deref().unwrap_or("?"),
                        reply.detail
                    );
                    thread::sleep(Duration::from_secs(5));
                }
                Err(e) => {
                    eprintln!("pair-join IPC failed ({e}); retrying");
                    thread::sleep(Duration::from_secs(3));
                }
            }
        }
    }


    // ── Rung 1 — pair (initiator). A holds NO subnet record yet: the 6-digit
    // code is read off the shared TOTP seed (the operator channel), the ceremony
    // is the real cross-host SPAKE2, and success transfers the subnet seed
    // (A becomes a full member / future seed-holder).
    // [int->REQ-PAIR-1] [int->REQ-PAIR-5]
    let rt = tokio::runtime::Runtime::new().expect("tokio rt");
    rt.block_on(async {
        let ep = NetEndpoint::bind_scoped(
            &rig.id_a,
            RelayPolicy::Disabled,
            LocalDiscovery::Off,
            vec![SPT_PAIR_ALPN.to_vec()],
            BindScope::All,
        )
        .await
        .expect("pairing dialer bind");
        let end = Instant::now() + rig.wait;
        loop {
            assert!(Instant::now() < end, "pairing never completed within the rig window");
            let conn = match ep.connect_with_alpn(rig.pair_addr(), SPT_PAIR_ALPN).await {
                Ok(c) => c,
                Err(e) => {
                    eprintln!("pairing dial not up yet ({e}); retrying");
                    // Blocking sleep is fine: this runtime runs nothing else.
                    thread::sleep(Duration::from_secs(3));
                    continue;
                }
            };
            let code = rig.totp.code_at(now_secs());
            let mut trust = TrustStore::load();
            let mut subnets = SubnetStore::load();
            match run_initiator(
                &conn,
                rig.id_a.public_key(),
                &rig.subnet,
                &code,
                &mut trust,
                &mut subnets,
                now_secs(),
            )
            .await
            {
                Ok(outcome) => {
                    trust.save().expect("save trust");
                    subnets.save().expect("save subnets");
                    println!("TWOHOST OK: pairing (initiator) — {outcome:?}");
                    break;
                }
                Err(e) => {
                    eprintln!("pairing ceremony failed ({e}); retrying");
                    thread::sleep(Duration::from_secs(5));
                }
            }
        }
        ep.close().await;
    });

    // ── Broker FIRST (M7 D5): the joiner leg runs on the daemon's own
    // endpoint — the ceremony binds and pins the node identity the broker
    // owns (pairhost), so pairing now needs the broker up before anything.
    let broker_name = format!("spt-twohost-a-{}.sock", std::process::id());
    let _broker = rig_broker(&broker_name, rig.id_a.clone(), rig.port_a, scratch.path());

    // ── Rung 1 — pair (initiator): the PRODUCT surface (M7 D5) — the exact
    // `spt subnet join` path (brain `pair-join` IPC → broker joiner leg →
    // the unchanged SPAKE2 ceremony), replacing the test-harness-driven
    // run_initiator as the pairing evidence. A holds NO subnet record yet:
    // the 6-digit code is read off the shared TOTP seed (the operator
    // channel) and success transfers the subnet seed (A becomes a full
    // member / future seed-holder). The explicit responder address is the
    // static-`ip:port` rig seam (no relay, no discovery — rendezvous can't
    // resolve here).
    // [int->REQ-PAIR-1] [int->REQ-PAIR-5] [int->REQ-SUBNET-2]
    {
        let mut pair_brain = connect_retry(&broker_name);
        let responder = serde_json::to_value(rig.pair_addr()).expect("EndpointAddr serializes");
        let end = Instant::now() + rig.wait;
        loop {
            assert!(Instant::now() < end, "pairing never completed within the rig window");
            let code = rig.totp.code_at(now_secs());
            match pair_brain.pair_join(&rig.subnet, &code, Some(responder.clone())) {
                Ok(reply) if reply.ok => {
                    println!("TWOHOST OK: pairing (initiator, product surface) — {}", reply.detail);
                    break;
                }
                Ok(reply) => {
                    // B's daemon may not be serving yet, or the window
                    // rolled — the guided-UX classes drive the retry.
                    eprintln!(
                        "pairing attempt refused ({}: {}); retrying",
                        reply.class.as_deref().unwrap_or("?"),
                        reply.detail
                    );
                    thread::sleep(Duration::from_secs(5));
                }
                Err(e) => {
                    eprintln!("pair-join IPC failed ({e}); retrying");
                    thread::sleep(Duration::from_secs(3));
                }
            }
        }
    }

let scratch = TempDir::new().expect("scratch");

println!("TWOHOST role A: node {}", rig.a_hex());

-    // ── Rung 1 — pair (initiator). A holds NO subnet record yet: the 6-digit

-    // code is read off the shared TOTP seed (the operator channel), the ceremony

-    // is the real cross-host SPAKE2, and success transfers the subnet seed

-    // (A becomes a full member / future seed-holder).

-    // [int->REQ-PAIR-1] [int->REQ-PAIR-5]

-    let rt = tokio::runtime::Runtime::new().expect("tokio rt");

-        let ep = NetEndpoint::bind_scoped(

-            vec![SPT_PAIR_ALPN.to_vec()],

-        .expect("pairing dialer bind");

+    // ── Broker FIRST (M7 D5): the joiner leg runs on the daemon's own

+    // endpoint — the ceremony binds and pins the node identity the broker

+    // owns (pairhost), so pairing now needs the broker up before anything.

+    let broker_name = format!("spt-twohost-a-{}.sock", std::process::id());

+    let _broker = rig_broker(&broker_name, rig.id_a.clone(), rig.port_a, scratch.path());

+    // ── Rung 1 — pair (initiator): the PRODUCT surface (M7 D5) — the exact

+    // `spt subnet join` path (brain `pair-join` IPC → broker joiner leg →

+    // the unchanged SPAKE2 ceremony), replacing the test-harness-driven

+    // run_initiator as the pairing evidence. A holds NO subnet record yet:

+    // the 6-digit code is read off the shared TOTP seed (the operator

+    // channel) and success transfers the subnet seed (A becomes a full

+    // member / future seed-holder). The explicit responder address is the

+    // static-`ip:port` rig seam (no relay, no discovery — rendezvous can't

+    // [int->REQ-PAIR-1] [int->REQ-PAIR-5] [int->REQ-SUBNET-2]

+        let mut pair_brain = connect_retry(&broker_name);

+        let responder = serde_json::to_value(rig.pair_addr()).expect("EndpointAddr serializes");

-            let conn = match ep.connect_with_alpn(rig.pair_addr(), SPT_PAIR_ALPN).await {

-                    eprintln!("pairing dial not up yet ({e}); retrying");

-                    // Blocking sleep is fine: this runtime runs nothing else.

-                    thread::sleep(Duration::from_secs(3));

-            let mut trust = TrustStore::load();

-            let mut subnets = SubnetStore::load();

-                    trust.save().expect("save trust");

-                    subnets.save().expect("save subnets");

-                    println!("TWOHOST OK: pairing (initiator) — {outcome:?}");

+            match pair_brain.pair_join(&rig.subnet, &code, Some(responder.clone())) {

+                Ok(reply) if reply.ok => {

+                    println!("TWOHOST OK: pairing (initiator, product surface) — {}", reply.detail);

-                    eprintln!("pairing ceremony failed ({e}); retrying");

+                    // B's daemon may not be serving yet, or the window

+                    // rolled — the guided-UX classes drive the retry.

+                        "pairing attempt refused ({}: {}); retrying",

+                        reply.class.as_deref().unwrap_or("?"),

+                    eprintln!("pair-join IPC failed ({e}); retrying");

+                    thread::sleep(Duration::from_secs(3));


748	            body: NOTIF_BODY.to_string(),
749	            created_ms: now_ms(),
750	            dismissed: false,
751	            seen: Default::default(),
752	            last_surfaced_ms: 0,
753	        })
754	        .expect("seed marker notif");
755
756	    // ── Broker + production loops. ──
757	    let broker_name = format!("spt-twohost-a-{}.sock", std::process::id());
758	    let _broker = rig_broker(&broker_name, rig.id_a.clone(), rig.port_a, scratch.path());
759	    let (registry, stop) =
760	        start_loops(&broker_name, &rig.a_hex(), rig.b_hex(), rig.peer_broker_addr("a"), scratch.path());
761
762	    // ── Rung 2 — register: B's perch row replicates into A's registry.
763	    // [int->REQ-INST-7]
764	    rig_wait("register: B's perch row replicated to A", rig.wait, || {
765	        registry.rows(&rig.subnet, ID_B).iter().any(|i| i.node == rig.b_hex())
766	    });
767
768	    // ── Rung 3 — cross-node message: the WAN send (B-side spool asserts it).
769	    // [int->REQ-NET-1]
770	    let mut a = connect_retry(&broker_name);
771	    let conn = a.net_dial(rig.peer_broker_addr("a"), Some(op())).expect("dial B");
772	    let opened = a.net_open_stream(conn.conn_id, Some(op())).expect("open msg stream");
773	    let msg = WanMessage {
774	        target: ID_B.to_string(),
775	        from: ID_A.to_string(),
776	        body: WAN_BODY.to_string(),
777	        op_id: "twohost-wan-1".to_string(),
778	    };
779	    a.net_stream_send(opened.stream_id, &spt_net::net::ndjson::encode_line(&msg), None, true)
780	        .expect("send WAN message");
781	    println!("TWOHOST OK: message sent (B asserts the spool)");
782
783	    // ── Rung 4 — remote-drive: fetch the session id off B (file transfer),
784	    // then attach to B's live PTY, type, and read the echo back.
785	    // [int->REQ-REACH-1]
786	    let sid_dest = scratch.path().join("sid.txt");
787	    let end = Instant::now() + rig.wait;
788	    let sid: u64 = loop {
789	        assert!(Instant::now() < end, "sid.txt never became fetchable from B");
790	        match spt_daemon::xfer::fetch_file(
791	            &mut a,
792	            conn.conn_id,
793	            ID_B,
794	            "sid.txt",
795	            &sid_dest,
796	            &format!("twohost-sid-{}", op()),
797	            op(),
798	            &scratch.path().join("xfer-progress"),
799	        ) {
800	            Ok(_) => {
801	                let text = std::fs::read_to_string(&sid_dest).expect("read sid");
802	                break text.trim().parse().expect("sid parses");
803	            }
804	            Err(e) => {
805	                eprintln!("sid fetch not ready ({e}); retrying");
806	                thread::sleep(Duration::from_secs(3));
807	            }

            body: NOTIF_BODY.to_string(),
            created_ms: now_ms(),
            dismissed: false,
            seen: Default::default(),
            last_surfaced_ms: 0,
        })
        .expect("seed marker notif");

    // ── Broker + production loops. ──
    let broker_name = format!("spt-twohost-a-{}.sock", std::process::id());
    let _broker = rig_broker(&broker_name, rig.id_a.clone(), rig.port_a, scratch.path());
    let (registry, stop) =
        start_loops(&broker_name, &rig.a_hex(), rig.b_hex(), rig.peer_broker_addr("a"), scratch.path());

    // ── Rung 2 — register: B's perch row replicates into A's registry.
    // [int->REQ-INST-7]
    rig_wait("register: B's perch row replicated to A", rig.wait, || {
        registry.rows(&rig.subnet, ID_B).iter().any(|i| i.node == rig.b_hex())
    });

    // ── Rung 3 — cross-node message: the WAN send (B-side spool asserts it).
    // [int->REQ-NET-1]
    let mut a = connect_retry(&broker_name);
    let conn = a.net_dial(rig.peer_broker_addr("a"), Some(op())).expect("dial B");
    let opened = a.net_open_stream(conn.conn_id, Some(op())).expect("open msg stream");
    let msg = WanMessage {
        target: ID_B.to_string(),
        from: ID_A.to_string(),
        body: WAN_BODY.to_string(),
        op_id: "twohost-wan-1".to_string(),
    };
    a.net_stream_send(opened.stream_id, &spt_net::net::ndjson::encode_line(&msg), None, true)
        .expect("send WAN message");
    println!("TWOHOST OK: message sent (B asserts the spool)");

    // ── Rung 4 — remote-drive: fetch the session id off B (file transfer),
    // then attach to B's live PTY, type, and read the echo back.
    // [int->REQ-REACH-1]
    let sid_dest = scratch.path().join("sid.txt");
    let end = Instant::now() + rig.wait;
    let sid: u64 = loop {
        assert!(Instant::now() < end, "sid.txt never became fetchable from B");
        match spt_daemon::xfer::fetch_file(
            &mut a,
            conn.conn_id,
            ID_B,
            "sid.txt",
            &sid_dest,
            &format!("twohost-sid-{}", op()),
            op(),
            &scratch.path().join("xfer-progress"),
        ) {
            Ok(_) => {
                let text = std::fs::read_to_string(&sid_dest).expect("read sid");
                break text.trim().parse().expect("sid parses");
            }
            Err(e) => {
                eprintln!("sid fetch not ready ({e}); retrying");
                thread::sleep(Duration::from_secs(3));
            }


    // ── Broker + production loops. ──
    let broker_name = format!("spt-twohost-a-{}.sock", std::process::id());
    let _broker = rig_broker(&broker_name, rig.id_a.clone(), rig.port_a, scratch.path());
    let (registry, stop) =
        start_loops(&broker_name, &rig.a_hex(), rig.b_hex(), rig.peer_broker_addr("a"), scratch.path());

    // ── Production loops (the broker is already up — pairing needed it). ──
    let (registry, stop) =
        start_loops(&broker_name, &rig.a_hex(), rig.b_hex(), rig.peer_broker_addr("a"), scratch.path());


    // ── Broker + production loops. ──
    let broker_name = format!("spt-twohost-a-{}.sock", std::process::id());
    let _broker = rig_broker(&broker_name, rig.id_a.clone(), rig.port_a, scratch.path());
    let (registry, stop) =
        start_loops(&broker_name, &rig.a_hex(), rig.b_hex(), rig.peer_broker_addr("a"), scratch.path());

    // ── Production loops (the broker is already up — pairing needed it). ──
    let (registry, stop) =
        start_loops(&broker_name, &rig.a_hex(), rig.b_hex(), rig.peer_broker_addr("a"), scratch.path());

-    // ── Broker + production loops. ──

-    let broker_name = format!("spt-twohost-a-{}.sock", std::process::id());

-    let _broker = rig_broker(&broker_name, rig.id_a.clone(), rig.port_a, scratch.path());

+    // ── Production loops (the broker is already up — pairing needed it). ──


    /// B's pre-trust pairing endpoint address (role A dials this).
    fn pair_addr(&self) -> EndpointAddr {
        addr_for_node_hex(&self.b_hex())
            .expect("derived node hex is a valid key")
            .with_ip_addr(SocketAddr::new(self.peer_ip, self.port_pair))
    }

    /// B's pairing-responder address (role A's joiner dials this). M7 D3:
    /// the responder rides B's BROKER endpoint (pairing ALPN beside the
    /// trusted surface) — same node id, same fixed port; no dedicated
    /// pairing endpoint exists anymore.
    fn pair_addr(&self) -> EndpointAddr {
        addr_for_node_hex(&self.b_hex())
            .expect("derived node hex is a valid key")
            .with_ip_addr(SocketAddr::new(self.peer_ip, self.port_b))
    }


    /// B's pre-trust pairing endpoint address (role A dials this).
    fn pair_addr(&self) -> EndpointAddr {
        addr_for_node_hex(&self.b_hex())
            .expect("derived node hex is a valid key")
            .with_ip_addr(SocketAddr::new(self.peer_ip, self.port_pair))
    }

    /// B's pairing-responder address (role A's joiner dials this). M7 D3:
    /// the responder rides B's BROKER endpoint (pairing ALPN beside the
    /// trusted surface) — same node id, same fixed port; no dedicated
    /// pairing endpoint exists anymore.
    fn pair_addr(&self) -> EndpointAddr {
        addr_for_node_hex(&self.b_hex())
            .expect("derived node hex is a valid key")
            .with_ip_addr(SocketAddr::new(self.peer_ip, self.port_b))
    }

-    /// B's pre-trust pairing endpoint address (role A dials this).

+    /// B's pairing-responder address (role A's joiner dials this). M7 D3:

+    /// the responder rides B's BROKER endpoint (pairing ALPN beside the

+    /// trusted surface) — same node id, same fixed port; no dedicated

+    /// pairing endpoint exists anymore.

-            .with_ip_addr(SocketAddr::new(self.peer_ip, self.port_pair))

+            .with_ip_addr(SocketAddr::new(self.peer_ip, self.port_b))


Bash

grep -n "port_pair" crates/spt-daemon/tests/twohost.rs

Find remaining port_pair uses


108:    port_pair: u16,
139:            port_pair: env_or("SPT_TWO_HOST_PORT_PAIR", 7462),


100
101	/// The rig's static configuration, fully determined by env (no runtime
102	/// exchange between the hosts).
103	struct Rig {
104	    subnet: String,
105	    peer_ip: IpAddr,
106	    port_a: u16,
107	    port_b: u16,
108	    port_pair: u16,
109	    wait: Duration,
110	    id_a: Identity,
111	    id_b: Identity,
112	    totp: TotpSeed,
113	    release_sk: SigningKey,
114	}
115
116	impl Rig {
117	    /// Read the rig from env iff `SPT_TWO_HOST=1` and the role matches;
118	    /// `None` = silent skip (the CI path).
119	    fn from_env(role: &str) -> Option<Rig> {
120	        if std::env::var("SPT_TWO_HOST").as_deref() != Ok("1") {
121	            return None;
122	        }
123	        if std::env::var("SPT_TWO_HOST_ROLE").ok()?.to_ascii_lowercase() != role {
124	            return None;
125	        }
126	        let secret = std::env::var("SPT_TWO_HOST_SECRET")
127	            .expect("SPT_TWO_HOST_SECRET must be set (same value on both hosts)");
128	        let peer_ip: IpAddr = std::env::var("SPT_TWO_HOST_PEER_IP")
129	            .expect("SPT_TWO_HOST_PEER_IP must be set (the other host's IP)")
130	            .parse()
131	            .expect("SPT_TWO_HOST_PEER_IP parses as an IP address");
132	        let mut t20 = [0u8; 20];
133	        t20.copy_from_slice(&derive32(&secret, "subnet-totp")[..20]);
134	        Some(Rig {
135	            subnet: env_or_str("SPT_TWO_HOST_SUBNET", "twohost"),
136	            peer_ip,
137	            port_a: env_or("SPT_TWO_HOST_PORT_A", 7460),
138	            port_b: env_or("SPT_TWO_HOST_PORT_B", 7461),
139	            port_pair: env_or("SPT_TWO_HOST_PORT_PAIR", 7462),
140	            wait: Duration::from_secs(env_or("SPT_TWO_HOST_WAIT_SECS", 300)),
141	            id_a: Identity::from_seed(&derive32(&secret, "node-a")),
142	            id_b: Identity::from_seed(&derive32(&secret, "node-b")),
143	            totp: TotpSeed::from_bytes(t20),
144	            release_sk: SigningKey::from_bytes(&derive32(&secret, "release")),
145	        })
146	    }
147

/// The rig's static configuration, fully determined by env (no runtime
/// exchange between the hosts).
struct Rig {
    subnet: String,
    peer_ip: IpAddr,
    port_a: u16,
    port_b: u16,
    port_pair: u16,
    wait: Duration,
    id_a: Identity,
    id_b: Identity,
    totp: TotpSeed,
    release_sk: SigningKey,
}

impl Rig {
    /// Read the rig from env iff `SPT_TWO_HOST=1` and the role matches;
    /// `None` = silent skip (the CI path).
    fn from_env(role: &str) -> Option<Rig> {
        if std::env::var("SPT_TWO_HOST").as_deref() != Ok("1") {
            return None;
        }
        if std::env::var("SPT_TWO_HOST_ROLE").ok()?.to_ascii_lowercase() != role {
            return None;
        }
        let secret = std::env::var("SPT_TWO_HOST_SECRET")
            .expect("SPT_TWO_HOST_SECRET must be set (same value on both hosts)");
        let peer_ip: IpAddr = std::env::var("SPT_TWO_HOST_PEER_IP")
            .expect("SPT_TWO_HOST_PEER_IP must be set (the other host's IP)")
            .parse()
            .expect("SPT_TWO_HOST_PEER_IP parses as an IP address");
        let mut t20 = [0u8; 20];
        t20.copy_from_slice(&derive32(&secret, "subnet-totp")[..20]);
        Some(Rig {
            subnet: env_or_str("SPT_TWO_HOST_SUBNET", "twohost"),
            peer_ip,
            port_a: env_or("SPT_TWO_HOST_PORT_A", 7460),
            port_b: env_or("SPT_TWO_HOST_PORT_B", 7461),
            port_pair: env_or("SPT_TWO_HOST_PORT_PAIR", 7462),
            wait: Duration::from_secs(env_or("SPT_TWO_HOST_WAIT_SECS", 300)),
            id_a: Identity::from_seed(&derive32(&secret, "node-a")),
            id_b: Identity::from_seed(&derive32(&secret, "node-b")),
            totp: TotpSeed::from_bytes(t20),
            release_sk: SigningKey::from_bytes(&derive32(&secret, "release")),
        })
    }


Edit

    port_a: u16,
    port_b: u16,
    port_pair: u16,
    wait: Duration,

    port_a: u16,
    port_b: u16,
    wait: Duration,


    port_a: u16,
    port_b: u16,
    port_pair: u16,
    wait: Duration,

    port_a: u16,
    port_b: u16,
    wait: Duration,

peer_ip: IpAddr,
port_a: u16,
port_b: u16,
-    port_pair: u16,
wait: Duration,
id_a: Identity,
id_b: Identity,


            port_a: env_or("SPT_TWO_HOST_PORT_A", 7460),
            port_b: env_or("SPT_TWO_HOST_PORT_B", 7461),
            port_pair: env_or("SPT_TWO_HOST_PORT_PAIR", 7462),
            wait: Duration::from_secs(env_or("SPT_TWO_HOST_WAIT_SECS", 300)),

            port_a: env_or("SPT_TWO_HOST_PORT_A", 7460),
            port_b: env_or("SPT_TWO_HOST_PORT_B", 7461),
            // SPT_TWO_HOST_PORT_PAIR retired (M7 D3): the pairing responder
            // rides the broker endpoint — there is no dedicated pairing port.
            wait: Duration::from_secs(env_or("SPT_TWO_HOST_WAIT_SECS", 300)),


            port_a: env_or("SPT_TWO_HOST_PORT_A", 7460),
            port_b: env_or("SPT_TWO_HOST_PORT_B", 7461),
            port_pair: env_or("SPT_TWO_HOST_PORT_PAIR", 7462),
            wait: Duration::from_secs(env_or("SPT_TWO_HOST_WAIT_SECS", 300)),

            port_a: env_or("SPT_TWO_HOST_PORT_A", 7460),
            port_b: env_or("SPT_TWO_HOST_PORT_B", 7461),
            // SPT_TWO_HOST_PORT_PAIR retired (M7 D3): the pairing responder
            // rides the broker endpoint — there is no dedicated pairing port.
            wait: Duration::from_secs(env_or("SPT_TWO_HOST_WAIT_SECS", 300)),

-            port_pair: env_or("SPT_TWO_HOST_PORT_PAIR", 7462),

+            // SPT_TWO_HOST_PORT_PAIR retired (M7 D3): the pairing responder

+            // rides the broker endpoint — there is no dedicated pairing port.


Bash

cargo check -p spt-daemon --all-targets 2>&1 | grep -E "^error|^warning: unused" -A5 | head -40

Check twohost compiles + unused imports


warning: unused import: `NetEndpoint`
  --> crates\spt-daemon\tests\twohost.rs:72:65
   |
72 |     addr_for_node_hex, BindScope, EndpointAddr, LocalDiscovery, NetEndpoint, RelayPolicy,
   |                                                                 ^^^^^^^^^^^
   |
--
warning: unused import: `spt_net::net::pairing::ratelimit::PairingRateLimiter`
  --> crates\spt-daemon\tests\twohost.rs:74:5
   |
74 | use spt_net::net::pairing::ratelimit::PairingRateLimiter;
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

warning: unused imports: `SPT_PAIR_ALPN`, `run_initiator`, and `run_responder`
  --> crates\spt-daemon\tests\twohost.rs:76:35
   |
76 | use spt_net::net::pairing::wire::{run_initiator, run_responder, SPT_PAIR_ALPN};
   |                                   ^^^^^^^^^^^^^  ^^^^^^^^^^^^^  ^^^^^^^^^^^^^


66	use spt_daemon::peerloop::{run_peer_pump, PeerResolver, PumpCadence, PumpConfig, PumpHooks, PumpPaths};
67	use spt_daemon::registryhost::RegistryHost;
68	use spt_daemon::relcache::ReleaseCache;
69	use spt_daemon::release::{sha256_hex, ReleaseMetadata, SignedRelease};
70	use spt_daemon::update::BROKER_RESOURCE_ABI;
71	use spt_net::net::endpoint::{
72	    addr_for_node_hex, BindScope, EndpointAddr, LocalDiscovery, NetEndpoint, RelayPolicy,
73	};
74	use spt_net::net::pairing::ratelimit::PairingRateLimiter;
75	use spt_net::net::pairing::totp::TotpSeed;
76	use spt_net::net::pairing::wire::{run_initiator, run_responder, SPT_PAIR_ALPN};
77	use spt_net::net::wanmsg::WanMessage;
78	use spt_proto::identity::Identity;
79	use spt_store::contextstore::ContextStore;
80	use spt_store::epoch::EpochSource;
81	use spt_store::info::{self, InfoJson};

use spt_daemon::peerloop::{run_peer_pump, PeerResolver, PumpCadence, PumpConfig, PumpHooks, PumpPaths};
use spt_daemon::registryhost::RegistryHost;
use spt_daemon::relcache::ReleaseCache;
use spt_daemon::release::{sha256_hex, ReleaseMetadata, SignedRelease};
use spt_daemon::update::BROKER_RESOURCE_ABI;
use spt_net::net::endpoint::{
    addr_for_node_hex, BindScope, EndpointAddr, LocalDiscovery, NetEndpoint, RelayPolicy,
};
use spt_net::net::pairing::ratelimit::PairingRateLimiter;
use spt_net::net::pairing::totp::TotpSeed;
use spt_net::net::pairing::wire::{run_initiator, run_responder, SPT_PAIR_ALPN};
use spt_net::net::wanmsg::WanMessage;
use spt_proto::identity::Identity;
use spt_store::contextstore::ContextStore;
use spt_store::epoch::EpochSource;
use spt_store::info::{self, InfoJson};


Edit

use spt_net::net::endpoint::{
    addr_for_node_hex, BindScope, EndpointAddr, LocalDiscovery, NetEndpoint, RelayPolicy,
};
use spt_net::net::pairing::ratelimit::PairingRateLimiter;
use spt_net::net::pairing::totp::TotpSeed;
use spt_net::net::pairing::wire::{run_initiator, run_responder, SPT_PAIR_ALPN};

use spt_net::net::endpoint::{
    addr_for_node_hex, BindScope, EndpointAddr, LocalDiscovery, RelayPolicy,
};
use spt_net::net::pairing::totp::TotpSeed;