1	//! M11-W2 drive-channel E2E (REQ-SHELL-3) — the six impl-gate criteria proven
2	//! through the REAL drive socket + the real `spt` surfaces, no in-process
3	//! shortcut on the driven path:
4	//!
5	//! spawn (offline) → `api bind-shell` onlines the perch → `spt shell drive`
6	//! writes the daemon's single live slot → `api drive-poll --link` takes-and-clears
7	//! it. The slot lives in the daemon (here an in-process drive-control serve on the
8	//! home's drive socket — the broker-home anchor in production), so this exercises
9	//! the true CLI→socket→hub→socket→CLI wire, not a hub call.
10	//!
11	//! Proves: (1) drop-at-write-offline, (2) latest-wins overwrite, (3)
12	//! take-and-clear, (4) NO-SPOOL (the durable shell spool stays empty), (5)
13	//! clear-on-link-break (write → break → relink → poll EMPTY), (6) slot has no
14	//! file under the perch. The cross-node leg is the [twohost] test; this is the
15	//! same-node gate.
16	//
17	// [int->REQ-SHELL-3]
18
19	use std::path::PathBuf;
20	use std::process::{Command, Output};
21	use std::time::Duration;
22
23	mod common;
24	use common::CommandNoWindowExt;
25
26	use spt_store::perch::{self, ParentHint};
27
28	/// Stand up the in-process daemon surfaces this E2E needs on the current
29	/// `SPT_HOME`'s sockets: the **seed** control (so subprocess `ensure_running`
30	/// pings succeed without spawning a real detached daemon) and the **drive**
31	/// control (the broker-home slot the drive surfaces read/write). Both serve
32	/// threads are leaked; they die with the test process.
33	fn start_inproc_daemon() {
34	    let reg = std::sync::Arc::new(spt_daemon::SeedRegistry::new());
35	    let seed = spt_daemon::seed_socket_name();
36	    std::thread::spawn(move || {
37	        let _ = spt_daemon::seedmap::serve_seed_control(&seed, reg);
38	    });
39	    let hub = std::sync::Arc::new(spt_daemon::DriveHub::new());
40	    let drive = spt_daemon::drive_socket_name();
41	    std::thread::spawn(move || {
42	        let _ = spt_daemon::serve_drive_control(&drive, hub);
43	    });
44	    for _ in 0..400 {
45	        if spt_daemon::is_running() {
46	            return;
47	        }
48	        std::thread::sleep(Duration::from_millis(5));
49	    }
50	    panic!("in-process seed daemon did not come up");
51	}
52
53	/// `Command::output()` with a deadline, run off-thread (the shell_e2e pattern —
54	/// a detached child inheriting the capture pipe would otherwise hang).
55	fn output_bounded(mut cmd: Command, deadline: Duration) -> Output {
56	    let (tx, rx) = std::sync::mpsc::channel();
57	    std::thread::spawn(move || {
58	        let _ = tx.send(cmd.output());
59	    });
60	    rx.recv_timeout(deadline)
61	        .expect("captured spt call must complete")
62	        .expect("run spt")
63	}
64
65	fn register_adapter(name: &str, manifest: &str) {
66	    let src = perch::spt_home().join("srcs").join(name);
67	    std::fs::create_dir_all(&src).unwrap();
68	    std::fs::write(src.join("manifest.toml"), manifest).unwrap();
69	    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();
70	}
71
72	#[test]
73	fn drive_channel_slot_through_the_real_socket() {
74	    // One test per file: a plain set_var is safe and every child inherits it.
75	    let home = tempfile::tempdir().unwrap();
76	    std::env::set_var("SPT_HOME", home.path());
77	    start_inproc_daemon();
78
79	    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
80	    let spt = |args: &[&str]| {
81	        let mut cmd = Command::new(&spt_bin);
82	        cmd.no_window().args(args).env("SPT_HOME", home.path());
83	        output_bounded(cmd, Duration::from_secs(60))
84	    };
85
86	    // A shell adapter with a [shell.drive] vocabulary. The spawn template is a
87	    // noop that exits immediately (never binds) — the perch stays offline until
88	    // we online it by token, so we control the online/offline edges precisely.
89	    #[cfg(windows)]
90	    let noop = "cmd /c exit 0";
91	    #[cfg(unix)]
92	    let noop = "true";
93	    register_adapter(
94	        "mock-shell",
95	        &format!(
96	            "[adapter]\nname = \"mock-shell\"\nkind = \"shell\"\nversion = \"1\"\n\
97	             min_spt_core_version = \"0\"\n\n[shell]\nspawn = '{noop} {{link_token}} {{id}}'\n\n\
98	             [shell.drive]\ntypes = [\"stick\"]\n"
99	        ),
100	    );
101
102	    // The owner: a bound Self perch (the bind-by-token scan starts at Self
103	    // perches) + node identity.
104	    let owner_perch = perch::resolve_perch_path("doyle", ParentHint::Infer);
105	    std::fs::create_dir_all(&owner_perch).unwrap();
106	    let rec = spt_store::info::InfoJson::new("doyle", "0", std::process::id(), "sid", "live_agent");
107	    spt_store::info::write_info(&owner_perch, &rec).unwrap();
108	    spt_store::nodeid::load_or_create().expect("node identity");
109
110	    let owlery = perch::owlery_dir();
111	    let shell_perch = perch::resolve_shell_perch_path_in(&owlery, "doyle", "mock-shell-0");
112	    let token_of = || {
113	        spt_daemon::shellhost::read_link_token(&shell_perch).expect("a link token is parked")
114	    };
115	    let drive = |drive_type: &str, payload: &str| {
116	        spt(&[
117	            "shell", "drive", "Scout", "--type", drive_type, payload, "--owner", "doyle",
118	        ])
119	    };
120	    let drive_poll = |token: &str| {
121	        spt(&[
122	            "api",
123	            "--adapter",
124	            "mock-shell",
125	            "drive-poll",
126	            "mock-shell-0",
127	            "--link",
128	            token,
129	        ])
130	    };
131	    let online_by_token = |token: &str| {
132	        let out = spt(&[
133	            "api",
134	            "--adapter",
135	            "mock-shell",
136	            "bind-shell",
137	            "--link",
138	            token,
139	        ]);
140	        assert!(
141	            out.status.success(),
142	            "bind-shell: {}",
143	            String::from_utf8_lossy(&out.stderr)
144	        );
145	    };
146	    let is_online = || {
147	        spt_store::shellinfo::read_shell_info(&shell_perch).map(|i| i.status)
148	            == Some(spt_store::shellinfo::SHELL_STATUS_ONLINE.to_string())
149	    };
150
151	    // ── spawn: the noop binary exits, the perch stays offline; a token is parked.
152	    let out = spt(&[
153	        "shell", "spawn", "mock-shell", "--alias", "Scout", "--owner", "doyle",
154	    ]);
155	    assert!(
156	        out.status.success(),
157	        "spawn: {}",
158	        String::from_utf8_lossy(&out.stderr)
159	    );
160	    assert!(!is_online(), "the noop binary never binds — perch starts offline");
161	    let token_a = token_of();
162
163	    // ── (1) DROP-AT-WRITE: driving an offline shell drops (exit 0, diagnostic),
164	    // writes no slot — the poll comes back empty.
165	    let out = drive("stick", "x=0.1");
166	    assert!(out.status.success(), "an offline drive is a defined drop (exit 0)");
167	    assert!(
168	        String::from_utf8_lossy(&out.stderr).contains("DRIVE_DROPPED"),
169	        "offline drive diagnoses a drop: {}",
170	        String::from_utf8_lossy(&out.stderr)
171	    );
172	    let out = drive_poll(&token_a);
173	    assert!(
174	        out.stdout.is_empty(),
175	        "an offline drive populated no slot: {}",
176	        String::from_utf8_lossy(&out.stdout)
177	    );
178
179	    // ── online the shell by its parked token.
180	    online_by_token(&token_a);
181	    assert!(is_online());
182
183	    // ── (2) LATEST-WINS: a second write before the poll clobbers the first.
184	    assert!(drive("stick", "x=0.1").status.success());
185	    assert!(drive("stick", "x=0.9").status.success());
186	    let out = drive_poll(&token_a);
187	    let frame = String::from_utf8_lossy(&out.stdout);
188	    assert!(frame.contains("type=\"drive\""), "served the drive frame: {frame}");
189	    assert!(
190	        frame.contains("x=0.9") && !frame.contains("x=0.1"),
191	        "the latest write superseded the earlier one: {frame}"
192	    );
193
194	    // ── (3) TAKE-AND-CLEAR: a second poll after a single (already taken) write
195	    // is empty — exactly-once drain.
196	    assert!(
197	        drive_poll(&token_a).stdout.is_empty(),
198	        "the frame was drained exactly once"
199	    );
200
201	    // ── (4) NO-SPOOL: none of the drive writes touched the durable shell spool.
202	    assert_eq!(
203	        spt_store::spool::pending_count_at(&shell_perch).unwrap(),
204	        0,
205	        "the drive write path makes zero spool call"
206	    );
207
208	    // ── (6) the slot is in daemon memory, NOT a file: a freshly-written slot
209	    // leaves no `drive` artifact under the perch (the real guarantee is
210	    // by-construction — DriveHub is an in-memory map, no fs/serde).
211	    assert!(drive("stick", "x=0.4").status.success());
212	    let no_drive_file = std::fs::read_dir(&shell_perch)
213	        .unwrap()
214	        .flatten()
215	        .all(|e| !e.file_name().to_string_lossy().to_lowercase().contains("drive"));
216	    assert!(no_drive_file, "the drive slot is held in memory, not a perch file");
217
218	    // ── (5) CLEAR-ON-LINK-BREAK: a frame written while online must NEVER be
219	    // served to the relinked instance. Break the link (close_shell flips offline,
220	    // retires the token, clears the slot), relink (mints a FRESH token), online,
221	    // and poll — EMPTY. The crash-proof token-stamp leg (a frame stamped under
222	    // the dead generation is inert even if the clear never ran) is the drivehub
223	    // unit `dead_generation_frame_is_never_served`; here we prove the live break.
224	    assert!(drive("stick", "x=0.7").status.success()); // a pending frame under token_a
225	    let shell = spt_runtime::registry::resolve_option(&perch::adapters_dir(), "mock-shell")
226	        .unwrap()
227	        .1
228	        .shell
229	        .unwrap();
230	    spt_daemon::shellhost::close_shell(&owlery, "doyle", "mock-shell-0", Some(&shell))
231	        .expect("link break flips offline + clears the drive slot");
232	    assert!(!is_online(), "the link break flipped the perch offline");
233
234	    let out = spt(&["shell", "relink", "Scout", "--owner", "doyle"]);
235	    assert!(
236	        out.status.success(),
237	        "relink mints a fresh token: {}",
238	        String::from_utf8_lossy(&out.stderr)
239	    );
240	    let token_b = token_of();
241	    assert_ne!(token_a, token_b, "relink rotates the link token");
242	    online_by_token(&token_b);
243	    assert!(
244	        drive_poll(&token_b).stdout.is_empty(),
245	        "no stale frame survives the link break + relink (no-replay-across-relink)"
246	    );
247
248	    // Reap any real daemon a `ensure_running` ping race may have spawned against
249	    // this throwaway home (best-effort hygiene — workspace-scoped, never a
250	    // machine-wide reap).
251	    if let Ok(pid) = std::fs::read_to_string(home.path().join("daemon.pid")) {
252	        if let Ok(pid) = pid.trim().parse::<u32>() {
253	            #[cfg(windows)]
254	            let _ = Command::new("taskkill")
255	                .no_window()
256	                .args(["/PID", &pid.to_string(), "/F"])
257	                .output();
258	            #[cfg(unix)]
259	            let _ = Command::new("kill").args(["-9", &pid.to_string()]).output();
260	        }
261	    }
262	}
263

//! M11-W2 drive-channel E2E (REQ-SHELL-3) — the six impl-gate criteria proven
//! through the REAL drive socket + the real `spt` surfaces, no in-process
//! shortcut on the driven path:
//!
//! spawn (offline) → `api bind-shell` onlines the perch → `spt shell drive`
//! writes the daemon's single live slot → `api drive-poll --link` takes-and-clears
//! it. The slot lives in the daemon (here an in-process drive-control serve on the
//! home's drive socket — the broker-home anchor in production), so this exercises
//! the true CLI→socket→hub→socket→CLI wire, not a hub call.
//!
//! Proves: (1) drop-at-write-offline, (2) latest-wins overwrite, (3)
//! take-and-clear, (4) NO-SPOOL (the durable shell spool stays empty), (5)
//! clear-on-link-break (write → break → relink → poll EMPTY), (6) slot has no
//! file under the perch. The cross-node leg is the [twohost] test; this is the
//! same-node gate.
//
// [int->REQ-SHELL-3]

use std::path::PathBuf;
use std::process::{Command, Output};
use std::time::Duration;

mod common;
use common::CommandNoWindowExt;

use spt_store::perch::{self, ParentHint};

/// Stand up the in-process daemon surfaces this E2E needs on the current
/// `SPT_HOME`'s sockets: the **seed** control (so subprocess `ensure_running`
/// pings succeed without spawning a real detached daemon) and the **drive**
/// control (the broker-home slot the drive surfaces read/write). Both serve
/// threads are leaked; they die with the test process.
fn start_inproc_daemon() {
    let reg = std::sync::Arc::new(spt_daemon::SeedRegistry::new());
    let seed = spt_daemon::seed_socket_name();
    std::thread::spawn(move || {
        let _ = spt_daemon::seedmap::serve_seed_control(&seed, reg);
    });
    let hub = std::sync::Arc::new(spt_daemon::DriveHub::new());
    let drive = spt_daemon::drive_socket_name();
    std::thread::spawn(move || {
        let _ = spt_daemon::serve_drive_control(&drive, hub);
    });
    for _ in 0..400 {
        if spt_daemon::is_running() {
            return;
        }
        std::thread::sleep(Duration::from_millis(5));
    }
    panic!("in-process seed daemon did not come up");
}

/// `Command::output()` with a deadline, run off-thread (the shell_e2e pattern —
/// a detached child inheriting the capture pipe would otherwise hang).
fn output_bounded(mut cmd: Command, deadline: Duration) -> Output {
    let (tx, rx) = std::sync::mpsc::channel();
    std::thread::spawn(move || {
        let _ = tx.send(cmd.output());
    });
    rx.recv_timeout(deadline)
        .expect("captured spt call must complete")
        .expect("run spt")
}

fn register_adapter(name: &str, manifest: &str) {
    let src = perch::spt_home().join("srcs").join(name);
    std::fs::create_dir_all(&src).unwrap();
    std::fs::write(src.join("manifest.toml"), manifest).unwrap();
    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();
}

#[test]
fn drive_channel_slot_through_the_real_socket() {
    // One test per file: a plain set_var is safe and every child inherits it.
    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());
    start_inproc_daemon();

    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let spt = |args: &[&str]| {
        let mut cmd = Command::new(&spt_bin);
        cmd.no_window().args(args).env("SPT_HOME", home.path());
        output_bounded(cmd, Duration::from_secs(60))
    };

    // A shell adapter with a [shell.drive] vocabulary. The spawn template is a
    // noop that exits immediately (never binds) — the perch stays offline until
    // we online it by token, so we control the online/offline edges precisely.
    #[cfg(windows)]
    let noop = "cmd /c exit 0";
    #[cfg(unix)]
    let noop = "true";
    register_adapter(
        "mock-shell",
        &format!(
            "[adapter]\nname = \"mock-shell\"\nkind = \"shell\"\nversion = \"1\"\n\
             min_spt_core_version = \"0\"\n\n[shell]\nspawn = '{noop} {{link_token}} {{id}}'\n\n\
             [shell.drive]\ntypes = [\"stick\"]\n"
        ),
    );

    // The owner: a bound Self perch (the bind-by-token scan starts at Self
    // perches) + node identity.
    let owner_perch = perch::resolve_perch_path("doyle", ParentHint::Infer);
    std::fs::create_dir_all(&owner_perch).unwrap();
    let rec = spt_store::info::InfoJson::new("doyle", "0", std::process::id(), "sid", "live_agent");
    spt_store::info::write_info(&owner_perch, &rec).unwrap();
    spt_store::nodeid::load_or_create().expect("node identity");

    let owlery = perch::owlery_dir();
    let shell_perch = perch::resolve_shell_perch_path_in(&owlery, "doyle", "mock-shell-0");
    let token_of = || {
        spt_daemon::shellhost::read_link_token(&shell_perch).expect("a link token is parked")
    };
    let drive = |drive_type: &str, payload: &str| {
        spt(&[
            "shell", "drive", "Scout", "--type", drive_type, payload, "--owner", "doyle",
        ])
    };
    let drive_poll = |token: &str| {
        spt(&[
            "api",
            "--adapter",
            "mock-shell",
            "drive-poll",
            "mock-shell-0",
            "--link",
            token,
        ])
    };
    let online_by_token = |token: &str| {
        let out = spt(&[
            "api",
            "--adapter",
            "mock-shell",
            "bind-shell",
            "--link",
            token,
        ]);
        assert!(
            out.status.success(),
            "bind-shell: {}",
            String::from_utf8_lossy(&out.stderr)
        );
    };
    let is_online = || {
        spt_store::shellinfo::read_shell_info(&shell_perch).map(|i| i.status)
            == Some(spt_store::shellinfo::SHELL_STATUS_ONLINE.to_string())
    };

    // ── spawn: the noop binary exits, the perch stays offline; a token is parked.
    let out = spt(&[
        "shell", "spawn", "mock-shell", "--alias", "Scout", "--owner", "doyle",
    ]);
    assert!(
        out.status.success(),
        "spawn: {}",
        String::from_utf8_lossy(&out.stderr)
    );
    assert!(!is_online(), "the noop binary never binds — perch starts offline");
    let token_a = token_of();

    // ── (1) DROP-AT-WRITE: driving an offline shell drops (exit 0, diagnostic),
    // writes no slot — the poll comes back empty.
    let out = drive("stick", "x=0.1");
    assert!(out.status.success(), "an offline drive is a defined drop (exit 0)");
    assert!(
        String::from_utf8_lossy(&out.stderr).contains("DRIVE_DROPPED"),
        "offline drive diagnoses a drop: {}",
        String::from_utf8_lossy(&out.stderr)
    );
    let out = drive_poll(&token_a);
    assert!(
        out.stdout.is_empty(),
        "an offline drive populated no slot: {}",
        String::from_utf8_lossy(&out.stdout)
    );

    // ── online the shell by its parked token.
    online_by_token(&token_a);
    assert!(is_online());

    // ── (2) LATEST-WINS: a second write before the poll clobbers the first.
    assert!(drive("stick", "x=0.1").status.success());
    assert!(drive("stick", "x=0.9").status.success());
    let out = drive_poll(&token_a);
    let frame = String::from_utf8_lossy(&out.stdout);
    assert!(frame.contains("type=\"drive\""), "served the drive frame: {frame}");
    assert!(
        frame.contains("x=0.9") && !frame.contains("x=0.1"),
        "the latest write superseded the earlier one: {frame}"
    );

    // ── (3) TAKE-AND-CLEAR: a second poll after a single (already taken) write
    // is empty — exactly-once drain.
    assert!(
        drive_poll(&token_a).stdout.is_empty(),
        "the frame was drained exactly once"
    );

    // ── (4) NO-SPOOL: none of the drive writes touched the durable shell spool.
    assert_eq!(
        spt_store::spool::pending_count_at(&shell_perch).unwrap(),
        0,
        "the drive write path makes zero spool call"
    );

    // ── (6) the slot is in daemon memory, NOT a file: a freshly-written slot
    // leaves no `drive` artifact under the perch (the real guarantee is
    // by-construction — DriveHub is an in-memory map, no fs/serde).
    assert!(drive("stick", "x=0.4").status.success());
    let no_drive_file = std::fs::read_dir(&shell_perch)
        .unwrap()
        .flatten()
        .all(|e| !e.file_name().to_string_lossy().to_lowercase().contains("drive"));
    assert!(no_drive_file, "the drive slot is held in memory, not a perch file");

    // ── (5) CLEAR-ON-LINK-BREAK: a frame written while online must NEVER be
    // served to the relinked instance. Break the link (close_shell flips offline,
    // retires the token, clears the slot), relink (mints a FRESH token), online,
    // and poll — EMPTY. The crash-proof token-stamp leg (a frame stamped under
    // the dead generation is inert even if the clear never ran) is the drivehub
    // unit `dead_generation_frame_is_never_served`; here we prove the live break.
    assert!(drive("stick", "x=0.7").status.success()); // a pending frame under token_a
    let shell = spt_runtime::registry::resolve_option(&perch::adapters_dir(), "mock-shell")
        .unwrap()
        .1
        .shell
        .unwrap();
    spt_daemon::shellhost::close_shell(&owlery, "doyle", "mock-shell-0", Some(&shell))
        .expect("link break flips offline + clears the drive slot");
    assert!(!is_online(), "the link break flipped the perch offline");

    let out = spt(&["shell", "relink", "Scout", "--owner", "doyle"]);
    assert!(
        out.status.success(),
        "relink mints a fresh token: {}",
        String::from_utf8_lossy(&out.stderr)
    );
    let token_b = token_of();
    assert_ne!(token_a, token_b, "relink rotates the link token");
    online_by_token(&token_b);
    assert!(
        drive_poll(&token_b).stdout.is_empty(),
        "no stale frame survives the link break + relink (no-replay-across-relink)"
    );

    // Reap any real daemon a `ensure_running` ping race may have spawned against
    // this throwaway home (best-effort hygiene — workspace-scoped, never a
    // machine-wide reap).
    if let Ok(pid) = std::fs::read_to_string(home.path().join("daemon.pid")) {
        if let Ok(pid) = pid.trim().parse::<u32>() {
            #[cfg(windows)]
            let _ = Command::new("taskkill")
                .no_window()
                .args(["/PID", &pid.to_string(), "/F"])
                .output();
            #[cfg(unix)]
            let _ = Command::new("kill").args(["-9", &pid.to_string()]).output();
        }
    }
}


Read actgate E2E + check self-perch listing + cmd CLI surface.


1	//! M11-W1 act-gate E2E (REQ-CONSENT-3) — closes the per-capability
2	//! class-keyed act-gate through the **real** `spt` binary + the `mock-shell`
3	//! adapter binary, no in-process shortcuts on the driven path. The interactive
4	//! loopback mirror of `shell_e2e.rs`'s spawn-gate floor, one rung lower: the
5	//! gate is now on the *command*, keyed by the manifest `class_key`, and the
6	//! durable channel stays clean until the right-class grant lands:
7	//!
8	//! spawn (ungated) → online → drive a `remembered` gated `attach` UNGRANTED:
9	//! the CLI exits `CONSENT_PENDING` and the frame NEVER spools (evidence stays
10	//! empty) → a grant for the WRONG class (`storage`) still refuses (class scope
11	//! is real, not a blanket op grant) → the RIGHT-class grant (`hid`, the
12	//! allow-always write) flips the same `attach` through to the binary's drain →
13	//! evidence lands. The class scoping is end-to-end: the grant row authorizes
14	//! `(owner × shell-act:attach × hid × node)` and nothing wider.
15	//
16	// [int->REQ-CONSENT-3]
17
18	use std::path::PathBuf;
19	use std::process::{Command, Output};
20	use std::time::{Duration, Instant};
21
22	mod common;
23	use common::CommandNoWindowExt;
24
25	use spt_store::perch::{self, ParentHint};
26
27	/// Absolute path to a sibling binary in the same target dir as the `spt` test bin.
28	fn sibling_bin(name: &str) -> PathBuf {
29	    let spt = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
30	    spt.with_file_name(format!("{name}{}", std::env::consts::EXE_SUFFIX))
31	}
32
33	/// In-process seed daemon so the children's `ensure_running` pings succeed —
34	/// no REAL detached `spt daemon` ever spawns (shell_e2e's pattern).
35	fn start_inproc_daemon() {
36	    let reg = std::sync::Arc::new(spt_daemon::SeedRegistry::new());
37	    let name = spt_daemon::seed_socket_name();
38	    std::thread::spawn(move || {
39	        let _ = spt_daemon::seedmap::serve_seed_control(&name, reg);
40	    });
41	    for _ in 0..400 {
42	        if spt_daemon::is_running() {
43	            return;
44	        }
45	        std::thread::sleep(Duration::from_millis(5));
46	    }
47	    panic!("in-process seed daemon did not come up");
48	}
49
50	/// `Command::output()` with a deadline, run off-thread — bounds the KH 5.6
51	/// detached-pipe-inherit regression into a clean failure instead of a hang.
52	// [unit->REQ-HAZARD-DETACHED-PIPE-INHERIT]
53	fn output_bounded(mut cmd: Command, deadline: Duration) -> Output {
54	    let (tx, rx) = std::sync::mpsc::channel();
55	    std::thread::spawn(move || {
56	        let _ = tx.send(cmd.output());
57	    });
58	    rx.recv_timeout(deadline)
59	        .expect(
60	            "captured spt call must complete — a hang here means a detached child \
61	             inherited the capture pipe (KNOWN-HAZARDS 5.6 regression)",
62	        )
63	        .expect("run spt")
64	}
65
66	/// Author + register a shell adapter manifest under the current home.
67	fn register_adapter(name: &str, manifest: &str) {
68	    let src = perch::spt_home().join("srcs").join(name);
69	    std::fs::create_dir_all(&src).unwrap();
70	    std::fs::write(src.join("manifest.toml"), manifest).unwrap();
71	    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();
72	}
73
74	#[test]
75	fn act_gate_blocks_command_until_class_scoped_grant() {
76	    // One test in this file: a plain set_var is safe, every child inherits the
77	    // isolated home.
78	    let home = tempfile::tempdir().unwrap();
79	    std::env::set_var("SPT_HOME", home.path());
80	    start_inproc_daemon();
81
82	    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
83	    let shell_bin = sibling_bin("mock-shell");
84	    assert!(
85	        shell_bin.exists(),
86	        "mock-shell must be built: {}",
87	        shell_bin.display()
88	    );
89	    let spt = |args: &[&str]| {
90	        let mut cmd = Command::new(&spt_bin);
91	        cmd.no_window().args(args).env("SPT_HOME", home.path());
92	        output_bounded(cmd, Duration::from_secs(60))
93	    };
94
95	    // The adapter under the binary's hardcoded `--adapter mock-shell` bind name.
96	    // Spawn is ungated (the instance mints freely); the per-capability gate
97	    // rides the COMMAND: `attach` is class-keyed `hid` + remembered; `wipe` is
98	    // class-keyed `storage` + remembered (a second class, to prove the grant
99	    // never crosses class). Sensory left out — this test drives the command
100	    // leg, not the sensory leg.
101	    let evidence = home.path().join("evidence.txt");
102	    let spawn_tmpl = format!(
103	        "\"{}\" --link {{link_token}} --id {{id}} --spt \"{}\" --evidence \"{}\"",
104	        shell_bin.display(),
105	        spt_bin.display(),
106	        evidence.display(),
107	    );
108	    register_adapter(
109	        "mock-shell",
110	        &format!(
111	            "[adapter]\nname = \"mock-shell\"\nkind = \"shell\"\nversion = \"1\"\n\
112	             min_spt_core_version = \"0\"\n\n[shell]\nspawn = '{spawn_tmpl}'\n\
113	             broadcast = \"same-node\"\n\n\
114	             [shell.capabilities.attach]\nargs = [\"busid\"]\n\
115	             require_approval = \"remembered\"\nclass_key = \"hid\"\n\n\
116	             [shell.capabilities.wipe]\nargs = [\"target\"]\n\
117	             require_approval = \"remembered\"\nclass_key = \"storage\"\n"
118	        ),
119	    );
120
121	    // The owner: a bound, LIVE Self perch + node identity (the act-gate is
122	    // node-scoped and non-minting; the grant store is target-node-local).
123	    let owner_perch = perch::resolve_perch_path("doyle", ParentHint::Infer);
124	    std::fs::create_dir_all(&owner_perch).unwrap();
125	    let rec = spt_store::info::InfoJson::new("doyle", "0", std::process::id(), "sid", "live_agent");
126	    spt_store::info::write_info(&owner_perch, &rec).unwrap();
127	    std::fs::write(perch::resolve_ready_file("doyle", ParentHint::Infer), "").unwrap();
128	    let _listener = spt_msg::listener::PollListener::bind("doyle").expect("owner listener");
129	    spt_store::nodeid::load_or_create().expect("node identity");
130
131	    // ── spawn (ungated) → online.
132	    let out = spt(&[
133	        "shell", "spawn", "mock-shell", "--alias", "Stick", "--owner", "doyle",
134	    ]);
135	    assert!(
136	        out.status.success(),
137	        "spawn: {}",
138	        String::from_utf8_lossy(&out.stderr)
139	    );
140	    let owlery = perch::owlery_dir();
141	    let shell_perch = perch::resolve_shell_perch_path_in(&owlery, "doyle", "mock-shell-0");
142	    let deadline = Instant::now() + Duration::from_secs(30);
143	    loop {
144	        let status = spt_store::shellinfo::read_shell_info(&shell_perch)
145	            .map(|i| i.status)
146	            .unwrap_or_default();
147	        if status == spt_store::shellinfo::SHELL_STATUS_ONLINE {
148	            break;
149	        }
150	        assert!(
151	            Instant::now() < deadline,
152	            "bind-shell never onlined the perch ({status})"
153	        );
154	        std::thread::sleep(Duration::from_millis(50));
155	    }
156
157	    // ── the gate holds on the command: an ungranted `remembered attach`
158	    // refuses (non-TTY ⇒ CONSENT_PENDING) and the frame NEVER spools — the
159	    // durable channel stays clean (evidence absent, the binary drained nothing).
160	    let out = spt(&[
161	        "shell", "cmd", "--owner", "doyle", "Stick", "attach", "busid-001",
162	    ]);
163	    assert!(
164	        !out.status.success(),
165	        "ungranted gated attach must refuse"
166	    );
167	    let pending = String::from_utf8_lossy(&out.stderr).to_string();
168	    assert!(
169	        pending.contains("CONSENT_PENDING") && pending.contains("shell-act:attach"),
170	        "refused as a pending act-gate, naming the namespaced capability: {pending}"
171	    );
172	    assert!(
173	        std::fs::read_to_string(&evidence).map(|s| s.trim().is_empty()).unwrap_or(true),
174	        "the gated command must NOT spool before approval (channel stays clean)"
175	    );
176
177	    // ── class scope is real: a grant for the WRONG class (`storage`) does not
178	    // authorize the `hid`-class `attach` — it still refuses.
179	    let out = spt(&[
180	        "grant", "add", "shell-act:attach", "doyle", "--qualifier", "storage",
181	    ]);
182	    assert!(out.status.success(), "grant add (storage): {}", String::from_utf8_lossy(&out.stderr));
183	    let out = spt(&[
184	        "shell", "cmd", "--owner", "doyle", "Stick", "attach", "busid-001",
185	    ]);
186	    assert!(
187	        !out.status.success(),
188	        "a storage-class grant must not authorize a hid-class attach"
189	    );
190	    assert!(
191	        std::fs::read_to_string(&evidence).map(|s| s.trim().is_empty()).unwrap_or(true),
192	        "still no spool under the wrong-class grant"
193	    );
194
195	    // ── the RIGHT-class grant (`hid`, the allow-always write) flips the same
196	    // `attach` through: it spools, the resident binary drains it, evidence lands.
197	    let out = spt(&[
198	        "grant", "add", "shell-act:attach", "doyle", "--qualifier", "hid",
199	    ]);
200	    assert!(out.status.success(), "grant add (hid): {}", String::from_utf8_lossy(&out.stderr));
201	    let out = spt(&[
202	        "shell", "cmd", "--owner", "doyle", "Stick", "attach", "busid-001",
203	    ]);
204	    assert!(
205	        out.status.success(),
206	        "the hid-class grant passes the act-gate: {}",
207	        String::from_utf8_lossy(&out.stderr)
208	    );
209	    let deadline = Instant::now() + Duration::from_secs(60);
210	    let drained = loop {
211	        if let Ok(s) = std::fs::read_to_string(&evidence) {
212	            if !s.trim().is_empty() {
213	                break s;
214	            }
215	        }
216	        assert!(
217	            Instant::now() < deadline,
218	            "the binary never drained the approved command frame"
219	        );
220	        std::thread::sleep(Duration::from_millis(50));
221	    };
222	    assert!(
223	        drained.contains("attach") && drained.contains("busid-001"),
224	        "the drained frame is the approved command: {drained}"
225	    );
226
227	    // ── a DIFFERENT gated op (`wipe`, class `storage`) is unaffected by the
228	    // attach grant — per-capability scope, not per-shell — and still refuses.
229	    let out = spt(&[
230	        "shell", "cmd", "--owner", "doyle", "Stick", "wipe", "everything",
231	    ]);
232	    assert!(
233	        !out.status.success(),
234	        "the attach grant must not authorize a different gated op"
235	    );
236	    let pending = String::from_utf8_lossy(&out.stderr).to_string();
237	    assert!(
238	        pending.contains("CONSENT_PENDING") && pending.contains("shell-act:wipe"),
239	        "wipe refuses on its own ungranted capability: {pending}"
240	    );
241
242	    // ── teardown frees the slot.
243	    let out = spt(&["shell", "teardown", "Stick", "--owner", "doyle"]);
244	    assert!(
245	        out.status.success(),
246	        "teardown: {}",
247	        String::from_utf8_lossy(&out.stderr)
248	    );
249
250	    // Reap any REAL daemon that auto-started against this throwaway home (KH 5.6
251	    // no-inherit spawn means it can't wedge; this is CI hygiene). Best-effort.
252	    if let Ok(pid) = std::fs::read_to_string(home.path().join("daemon.pid")) {
253	        if let Ok(pid) = pid.trim().parse::<u32>() {
254	            #[cfg(windows)]
255	            let _ = Command::new("taskkill")
256	                .no_window()
257	                .args(["/PID", &pid.to_string(), "/F"])
258	                .output();
259	            #[cfg(unix)]
260	            let _ = Command::new("kill").args(["-9", &pid.to_string()]).output();
261	        }
262	    }
263	}
264

//! M11-W1 act-gate E2E (REQ-CONSENT-3) — closes the per-capability
//! class-keyed act-gate through the **real** `spt` binary + the `mock-shell`
//! adapter binary, no in-process shortcuts on the driven path. The interactive
//! loopback mirror of `shell_e2e.rs`'s spawn-gate floor, one rung lower: the
//! gate is now on the *command*, keyed by the manifest `class_key`, and the
//! durable channel stays clean until the right-class grant lands:
//!
//! spawn (ungated) → online → drive a `remembered` gated `attach` UNGRANTED:
//! the CLI exits `CONSENT_PENDING` and the frame NEVER spools (evidence stays
//! empty) → a grant for the WRONG class (`storage`) still refuses (class scope
//! is real, not a blanket op grant) → the RIGHT-class grant (`hid`, the
//! allow-always write) flips the same `attach` through to the binary's drain →
//! evidence lands. The class scoping is end-to-end: the grant row authorizes
//! `(owner × shell-act:attach × hid × node)` and nothing wider.
//
// [int->REQ-CONSENT-3]

use std::path::PathBuf;
use std::process::{Command, Output};
use std::time::{Duration, Instant};

mod common;
use common::CommandNoWindowExt;

use spt_store::perch::{self, ParentHint};

/// Absolute path to a sibling binary in the same target dir as the `spt` test bin.
fn sibling_bin(name: &str) -> PathBuf {
    let spt = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    spt.with_file_name(format!("{name}{}", std::env::consts::EXE_SUFFIX))
}

/// In-process seed daemon so the children's `ensure_running` pings succeed —
/// no REAL detached `spt daemon` ever spawns (shell_e2e's pattern).
fn start_inproc_daemon() {
    let reg = std::sync::Arc::new(spt_daemon::SeedRegistry::new());
    let name = spt_daemon::seed_socket_name();
    std::thread::spawn(move || {
        let _ = spt_daemon::seedmap::serve_seed_control(&name, reg);
    });
    for _ in 0..400 {
        if spt_daemon::is_running() {
            return;
        }
        std::thread::sleep(Duration::from_millis(5));
    }
    panic!("in-process seed daemon did not come up");
}

/// `Command::output()` with a deadline, run off-thread — bounds the KH 5.6
/// detached-pipe-inherit regression into a clean failure instead of a hang.
// [unit->REQ-HAZARD-DETACHED-PIPE-INHERIT]
fn output_bounded(mut cmd: Command, deadline: Duration) -> Output {
    let (tx, rx) = std::sync::mpsc::channel();
    std::thread::spawn(move || {
        let _ = tx.send(cmd.output());
    });
    rx.recv_timeout(deadline)
        .expect(
            "captured spt call must complete — a hang here means a detached child \
             inherited the capture pipe (KNOWN-HAZARDS 5.6 regression)",
        )
        .expect("run spt")
}

/// Author + register a shell adapter manifest under the current home.
fn register_adapter(name: &str, manifest: &str) {
    let src = perch::spt_home().join("srcs").join(name);
    std::fs::create_dir_all(&src).unwrap();
    std::fs::write(src.join("manifest.toml"), manifest).unwrap();
    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();
}

#[test]
fn act_gate_blocks_command_until_class_scoped_grant() {
    // One test in this file: a plain set_var is safe, every child inherits the
    // isolated home.
    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());
    start_inproc_daemon();

    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let shell_bin = sibling_bin("mock-shell");
    assert!(
        shell_bin.exists(),
        "mock-shell must be built: {}",
        shell_bin.display()
    );
    let spt = |args: &[&str]| {
        let mut cmd = Command::new(&spt_bin);
        cmd.no_window().args(args).env("SPT_HOME", home.path());
        output_bounded(cmd, Duration::from_secs(60))
    };

    // The adapter under the binary's hardcoded `--adapter mock-shell` bind name.
    // Spawn is ungated (the instance mints freely); the per-capability gate
    // rides the COMMAND: `attach` is class-keyed `hid` + remembered; `wipe` is
    // class-keyed `storage` + remembered (a second class, to prove the grant
    // never crosses class). Sensory left out — this test drives the command
    // leg, not the sensory leg.
    let evidence = home.path().join("evidence.txt");
    let spawn_tmpl = format!(
        "\"{}\" --link {{link_token}} --id {{id}} --spt \"{}\" --evidence \"{}\"",
        shell_bin.display(),
        spt_bin.display(),
        evidence.display(),
    );
    register_adapter(
        "mock-shell",
        &format!(
            "[adapter]\nname = \"mock-shell\"\nkind = \"shell\"\nversion = \"1\"\n\
             min_spt_core_version = \"0\"\n\n[shell]\nspawn = '{spawn_tmpl}'\n\
             broadcast = \"same-node\"\n\n\
             [shell.capabilities.attach]\nargs = [\"busid\"]\n\
             require_approval = \"remembered\"\nclass_key = \"hid\"\n\n\
             [shell.capabilities.wipe]\nargs = [\"target\"]\n\
             require_approval = \"remembered\"\nclass_key = \"storage\"\n"
        ),
    );

    // The owner: a bound, LIVE Self perch + node identity (the act-gate is
    // node-scoped and non-minting; the grant store is target-node-local).
    let owner_perch = perch::resolve_perch_path("doyle", ParentHint::Infer);
    std::fs::create_dir_all(&owner_perch).unwrap();
    let rec = spt_store::info::InfoJson::new("doyle", "0", std::process::id(), "sid", "live_agent");
    spt_store::info::write_info(&owner_perch, &rec).unwrap();
    std::fs::write(perch::resolve_ready_file("doyle", ParentHint::Infer), "").unwrap();
    let _listener = spt_msg::listener::PollListener::bind("doyle").expect("owner listener");
    spt_store::nodeid::load_or_create().expect("node identity");

    // ── spawn (ungated) → online.
    let out = spt(&[
        "shell", "spawn", "mock-shell", "--alias", "Stick", "--owner", "doyle",
    ]);
    assert!(
        out.status.success(),
        "spawn: {}",
        String::from_utf8_lossy(&out.stderr)
    );
    let owlery = perch::owlery_dir();
    let shell_perch = perch::resolve_shell_perch_path_in(&owlery, "doyle", "mock-shell-0");
    let deadline = Instant::now() + Duration::from_secs(30);
    loop {
        let status = spt_store::shellinfo::read_shell_info(&shell_perch)
            .map(|i| i.status)
            .unwrap_or_default();
        if status == spt_store::shellinfo::SHELL_STATUS_ONLINE {
            break;
        }
        assert!(
            Instant::now() < deadline,
            "bind-shell never onlined the perch ({status})"
        );
        std::thread::sleep(Duration::from_millis(50));
    }

    // ── the gate holds on the command: an ungranted `remembered attach`
    // refuses (non-TTY ⇒ CONSENT_PENDING) and the frame NEVER spools — the
    // durable channel stays clean (evidence absent, the binary drained nothing).
    let out = spt(&[
        "shell", "cmd", "--owner", "doyle", "Stick", "attach", "busid-001",
    ]);
    assert!(
        !out.status.success(),
        "ungranted gated attach must refuse"
    );
    let pending = String::from_utf8_lossy(&out.stderr).to_string();
    assert!(
        pending.contains("CONSENT_PENDING") && pending.contains("shell-act:attach"),
        "refused as a pending act-gate, naming the namespaced capability: {pending}"
    );
    assert!(
        std::fs::read_to_string(&evidence).map(|s| s.trim().is_empty()).unwrap_or(true),
        "the gated command must NOT spool before approval (channel stays clean)"
    );

    // ── class scope is real: a grant for the WRONG class (`storage`) does not
    // authorize the `hid`-class `attach` — it still refuses.
    let out = spt(&[
        "grant", "add", "shell-act:attach", "doyle", "--qualifier", "storage",
    ]);
    assert!(out.status.success(), "grant add (storage): {}", String::from_utf8_lossy(&out.stderr));
    let out = spt(&[
        "shell", "cmd", "--owner", "doyle", "Stick", "attach", "busid-001",
    ]);
    assert!(
        !out.status.success(),
        "a storage-class grant must not authorize a hid-class attach"
    );
    assert!(
        std::fs::read_to_string(&evidence).map(|s| s.trim().is_empty()).unwrap_or(true),
        "still no spool under the wrong-class grant"
    );

    // ── the RIGHT-class grant (`hid`, the allow-always write) flips the same
    // `attach` through: it spools, the resident binary drains it, evidence lands.
    let out = spt(&[
        "grant", "add", "shell-act:attach", "doyle", "--qualifier", "hid",
    ]);
    assert!(out.status.success(), "grant add (hid): {}", String::from_utf8_lossy(&out.stderr));
    let out = spt(&[
        "shell", "cmd", "--owner", "doyle", "Stick", "attach", "busid-001",
    ]);
    assert!(
        out.status.success(),
        "the hid-class grant passes the act-gate: {}",
        String::from_utf8_lossy(&out.stderr)
    );
    let deadline = Instant::now() + Duration::from_secs(60);
    let drained = loop {
        if let Ok(s) = std::fs::read_to_string(&evidence) {
            if !s.trim().is_empty() {
                break s;
            }
        }
        assert!(
            Instant::now() < deadline,
            "the binary never drained the approved command frame"
        );
        std::thread::sleep(Duration::from_millis(50));
    };
    assert!(
        drained.contains("attach") && drained.contains("busid-001"),
        "the drained frame is the approved command: {drained}"
    );

    // ── a DIFFERENT gated op (`wipe`, class `storage`) is unaffected by the
    // attach grant — per-capability scope, not per-shell — and still refuses.
    let out = spt(&[
        "shell", "cmd", "--owner", "doyle", "Stick", "wipe", "everything",
    ]);
    assert!(
        !out.status.success(),
        "the attach grant must not authorize a different gated op"
    );
    let pending = String::from_utf8_lossy(&out.stderr).to_string();
    assert!(
        pending.contains("CONSENT_PENDING") && pending.contains("shell-act:wipe"),
        "wipe refuses on its own ungranted capability: {pending}"
    );

    // ── teardown frees the slot.
    let out = spt(&["shell", "teardown", "Stick", "--owner", "doyle"]);
    assert!(
        out.status.success(),
        "teardown: {}",
        String::from_utf8_lossy(&out.stderr)
    );

    // Reap any REAL daemon that auto-started against this throwaway home (KH 5.6
    // no-inherit spawn means it can't wedge; this is CI hygiene). Best-effort.
    if let Ok(pid) = std::fs::read_to_string(home.path().join("daemon.pid")) {
        if let Ok(pid) = pid.trim().parse::<u32>() {
            #[cfg(windows)]
            let _ = Command::new("taskkill")
                .no_window()
                .args(["/PID", &pid.to_string(), "/F"])
                .output();
            #[cfg(unix)]
            let _ = Command::new("kill").args(["-9", &pid.to_string()]).output();
        }
    }
}


Got the full pattern. Check unit-test location + NoShell display + self-perch listing (type-agnostic?).


Bash

echo "===unit tags shellhost/linkhost===" && grep -rn "unit->REQ-SHELL" crates/spt-daemon/src/ | head -30
echo "===list_self_perch_ids===" && grep -rn "pub fn list_self_perch_ids" crates/spt-store/src/
echo "===NoShell display===" && grep -rn "NoShell\|NO_SHELL\|ShellLinkError" crates/spt-daemon/src/linkhost.rs | head -20
echo "===InfoJson::new sig===" && grep -rn "pub fn new" crates/spt-store/src/info.rs

Locate unit tests, perch listing, error display, InfoJson sig


===unit tags shellhost/linkhost===
crates/spt-daemon/src/drivehub.rs:311:    // [unit->REQ-SHELL-3] latest-wins: a second write before the poll clobbers
crates/spt-daemon/src/drivehub.rs:325:    // [unit->REQ-SHELL-3] take-and-clear is exactly-once: a second poll after a
crates/spt-daemon/src/drivehub.rs:343:    // [unit->REQ-SHELL-3] no-replay-across-relink: a frame written under one link
crates/spt-daemon/src/drivehub.rs:364:    // [unit->REQ-SHELL-3] clear evicts the slot unconditionally (the link-break
crates/spt-daemon/src/drivehub.rs:376:    // [unit->REQ-SHELL-3] slots are per-owner: the same shell id under two owners
crates/spt-daemon/src/linkhost.rs:744:    // [unit->REQ-SHELL-2] the serve's action core: relink relaunches an
crates/spt-daemon/src/linkhost.rs:825:    // [unit->REQ-SHELL-3] the pure drive prep is SPOOL-FREE by construction:
crates/spt-daemon/src/linkhost.rs:885:    // [unit->REQ-SHELL-3] the cross-node drive serve DROPS an offline shell —
crates/spt-daemon/src/nethost.rs:1309:    // [unit->REQ-SHELL-4] the ring policy split (M11-W3): a RETENTIVE log (a shell
crates/spt-daemon/src/nethost.rs:1338:    // [unit->REQ-SHELL-4] the loopback tunnel pair under BACKPRESSURE (M11-W3,
crates/spt-daemon/src/resting.rs:796:    // [unit->REQ-SHELL-2] the owner-edge shell cascade (CONTEXT §Shell
crates/spt-daemon/src/shellchan.rs:246:    // [unit->REQ-SHELL-1] the vocabulary bounds what an agent may ask: an
crates/spt-daemon/src/shellchan.rs:279:    // [unit->REQ-SHELL-1] frames compose as typed EVENT envelopes and
crates/spt-daemon/src/shellchan.rs:319:    // [unit->REQ-SHELL-1] the sensory frame carries the manifest-declared
crates/spt-daemon/src/shellchan.rs:338:    // [unit->REQ-SHELL-3] the drive frame is the owner→shell mirror of sensory:
crates/spt-daemon/src/shellhost.rs:594:    // [unit->REQ-SHELL-1] the handshake end to end on disk: launch parks a
crates/spt-daemon/src/shellhost.rs:667:    // [unit->REQ-SHELL-1] launch is template-driven and fails closed: a
crates/spt-daemon/src/shellhost.rs:700:    // [unit->REQ-SHELL-2] the link-break close, running-binary case: the
crates/spt-daemon/src/shellhost.rs:749:    // [unit->REQ-SHELL-2] the divergence, ephemeral side: close IS teardown —
crates/spt-daemon/src/shellhost.rs:773:    // [unit->REQ-SHELL-2] close is bounded and tolerant: a child already gone
crates/spt-daemon/src/shellhost.rs:811:    // [unit->REQ-SHELL-1] the channel-key derivation + per-frame MAC: both
crates/spt-daemon/src/shellwake.rs:602:    // [unit->REQ-SHELL-2] the backoff curve: base × 2^(n-1), saturating at
crates/spt-daemon/src/shellwake.rs:623:    // [unit->REQ-SHELL-2] exit-opcode supervision: exit(86) fires the wake
crates/spt-daemon/src/shellwake.rs:659:    // [unit->REQ-SHELL-2] crash-exit supervision: a crashing waker respawns
crates/spt-daemon/src/shellwake.rs:713:    // [unit->REQ-SHELL-2] wake resolution, the no-reachable branch: no local
crates/spt-daemon/src/shellwake.rs:731:    // [unit->REQ-SHELL-2] wake resolution, the dormant branch: the owner is
crates/spt-daemon/src/shellwake.rs:765:    // [unit->REQ-SHELL-2] wake resolution, the suspended branch: revive the
crates/spt-daemon/src/shellwake.rs:840:    // [unit->REQ-SHELL-2] the reconciler holds the mutual exclusivity: an
crates/spt-daemon/src/tunnelhub.rs:504:    // [unit->REQ-SHELL-4] a resolve serves the held stream id for the requested
crates/spt-daemon/src/tunnelhub.rs:514:    // [unit->REQ-SHELL-4] no-stale-stream-across-relink (R1): a stream id opened
===list_self_perch_ids===
crates/spt-store/src/perch.rs:406:pub fn list_self_perch_ids(owlery: &Path) -> Vec<String> {
===NoShell display===
18://! inside that owner's own shell tree — a foreign owner's ref is NO_SHELL,
61:pub enum ShellLinkError {
64:    NoShell,
87:impl std::fmt::Display for ShellLinkError {
90:            ShellLinkError::NoShell => write!(f, "no such instance for that owner"),
91:            ShellLinkError::AdapterUnregistered(a) => {
94:            ShellLinkError::ReceiptUnsupported => {
97:            ShellLinkError::NoLink => write!(f, "no link token parked (never launched)"),
98:            ShellLinkError::Vocab(e) => write!(f, "{e}"),
99:            ShellLinkError::Failed(e) => write!(f, "{e}"),
100:            ShellLinkError::AlreadyOnline => {
103:            ShellLinkError::NeedsApproval(ask, _) => {
194:) -> Result<(String, ShellInfo, Shell), ShellLinkError> {
196:        return Err(ShellLinkError::NoShell);
206:        return Err(ShellLinkError::AdapterUnregistered(info.adapter_name));
275:) -> Result<(String, Shell), ShellLinkError> {
280:        return Err(ShellLinkError::ReceiptUnsupported);
284:        return Err(ShellLinkError::NoLink);
286:    let args_obj = shellchan::vocab_check(&shell, op, args).map_err(ShellLinkError::Vocab)?;
293:        return Err(ShellLinkError::NeedsApproval(ask, persist));
===InfoJson::new sig===
137:    pub fn new(id: &str, started: &str, pid: u32, session_id: &str, state: &str) -> InfoJson {


Check self-perch listing type-agnosticism + shellhost unit test module + CLI NoShell surface.


406	pub fn list_self_perch_ids(owlery: &Path) -> Vec<String> {
407	    let Ok(entries) = std::fs::read_dir(owlery) else {
408	        return Vec::new();
409	    };
410	    let mut ids = Vec::new();
411	    for entry in entries.flatten() {
412	        if !entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
413	            continue;
414	        }
415	        if !info_file_at(&entry.path()).is_file() {
416	            continue; // a dir without info.json is not a bound perch
417	        }
418	        if let Some(name) = entry.file_name().to_str() {
419	            ids.push(name.to_string());
420	        }
421	    }
422	    ids
423	}
424
425	#[cfg(test)]
426	mod tests {
427	    use super::*;
428	    use std::sync::Mutex;
429
430	    static ENV_LOCK: Mutex<()> = Mutex::new(());
431
432	    struct EnvGuard {
433	        _lock: std::sync::MutexGuard<'static, ()>,
434	        prev: Option<String>,
435	    }
436	    impl EnvGuard {
437	        fn set(tmp: &Path) -> Self {
438	            let lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
439	            let prev = std::env::var("SPT_HOME").ok();
440	            std::env::set_var("SPT_HOME", tmp);
441	            EnvGuard { _lock: lock, prev }
442	        }
443	    }
444	    impl Drop for EnvGuard {
445	        fn drop(&mut self) {

pub fn list_self_perch_ids(owlery: &Path) -> Vec<String> {
    let Ok(entries) = std::fs::read_dir(owlery) else {
        return Vec::new();
    };
    let mut ids = Vec::new();
    for entry in entries.flatten() {
        if !entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
            continue;
        }
        if !info_file_at(&entry.path()).is_file() {
            continue; // a dir without info.json is not a bound perch
        }
        if let Some(name) = entry.file_name().to_str() {
            ids.push(name.to_string());
        }
    }
    ids
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::sync::Mutex;

    static ENV_LOCK: Mutex<()> = Mutex::new(());

    struct EnvGuard {
        _lock: std::sync::MutexGuard<'static, ()>,
        prev: Option<String>,
    }
    impl EnvGuard {
        fn set(tmp: &Path) -> Self {
            let lock = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());
            let prev = std::env::var("SPT_HOME").ok();
            std::env::set_var("SPT_HOME", tmp);
            EnvGuard { _lock: lock, prev }
        }
    }
    impl Drop for EnvGuard {
        fn drop(&mut self) {


Check CLI shell cmd/drive error surfacing for negative assertions.


Grep

no such instance|NO_SHELL|ShellLinkError|NO_TUNNEL|fn cmd_shell|shell.*cmd


Found 4 files
crates\spt\src\api\reporting.rs
crates\spt\src\cli.rs
crates\spt\src\wansend.rs
crates\spt\src\elevation.rs


Bash

grep -n "no such instance\|NO_SHELL\|NO_TUNNEL\|ShellLinkError\|shell_cmd\|Shell::Cmd\|Shell::Drive\|Shell::Tunnel\|fn .*tunnel\|fn .*drive\|fn .*cmd" crates/spt/src/cli.rs | head -50

Find shell cmd/drive/tunnel CLI handlers + error strings


917:fn grouped_help_template(cmd: &mut clap::Command) -> String {
1118:fn cmd_digest(id: &str, follow: bool, json: bool) -> i32 {
1191:pub(crate) fn cmd_endpoint_run(
1296:fn cmd_fork(src: &str, new_id: &str, subnet: &str, delete_source: bool) -> i32 {
1404:fn cmd_description(action: DescriptionCmd) -> i32 {
1472:fn cmd_role(id: Option<String>, overwrite: Option<PathBuf>) -> i32 {
1544:fn cmd_endpoint_list(local: bool, subnet: Option<String>, detail: bool) -> i32 {
1630:fn cmd_rest(id: &str, event: spt_daemon::RestEvent, verb: &str) -> i32 {
1702:fn cmd_shutdown(id: Option<String>) -> i32 {
1739:fn cmd_daemon_run() -> i32 {
1777:fn cmd_daemon_brain(generation: u64, start_reason: &str) -> i32 {
1791:fn cmd_daemon_start() -> i32 {
1818:fn cmd_daemon_stop() -> i32 {
1879:fn cmd_daemon_status() -> i32 {
2038:fn cmd_notify(body: Option<String>, target: Option<String>, from: Option<String>) -> i32 {
2117:fn cmd_notif_list(subnet: Option<String>) -> i32 {
2156:fn cmd_notif_dismiss(notif_id: &str) -> i32 {
2206:fn cmd_update_apply() -> i32 {
2311:fn cmd_update_fetch(channel: Option<String>, tag: Option<String>) -> i32 {
2504:fn cmd_send(
2638:fn cmd_ring(target: String, from: Option<String>, timeout: u64) -> i32 {
2751:fn cmd_list_local() -> i32 {
2770:fn cmd_stop(id: &str) -> i32 {
2806:fn cmd_whoami() -> i32 {
3297:fn cmd_subnet_show_code(name: Option<String>) -> i32 {
3316:fn cmd_subnet_create(name: String) -> i32 {
3906:fn cmd_subnet_status(name: Option<String>, nodes: bool, hints: bool) -> i32 {
4073:fn cmd_subnet_leave(name: &str) -> i32 {
4149:fn cmd_subnet_prune(node: &str) -> i32 {
4246:fn cmd_subnet_revoke(nodes: &[String], force_rotate_seed: bool) -> i32 {
4398:fn cmd_subnet_attachment(name: &str, detach: bool, save: bool) -> i32 {
4473:fn cmd_subnet_join(name: Option<String>, code: Option<String>) -> i32 {
4725:fn cmd_how_to(topic: Option<&str>) -> i32 {
4768:fn cmd_access(action: AccessCmd) -> i32 {
4869:fn cmd_adapter(action: AdapterCmd) -> i32 {
5136:fn cmd_adapter_digest_proof(
5230:fn cmd_shell(action: ShellCmd) -> i32 {
5244:    fn resolve_driveable(
5251:            eprintln!("NO_SHELL:{shell_ref}: no such instance for {owner}");
5659:                Err(spt_daemon::ShellLinkError::NeedsApproval(ask, persist)) => {
5712:                Err(spt_daemon::ShellLinkError::NoShell) => {
5713:                    eprintln!("NO_SHELL:{shell_ref}: no such instance for {owner}");
5747:                    eprintln!("NO_TUNNEL:{shell_ref}: no live tunnel for {owner} (offline, or the shell declares no [shell.tunnel])");
5921:fn render_link_err(verb: &str, shell_ref: &str, owner: &str, e: spt_daemon::ShellLinkError) -> i32 {
5922:    use spt_daemon::ShellLinkError as E;
5924:        E::NoShell => eprintln!("NO_SHELL:{shell_ref}: no such instance for {owner}"),
5968:            eprintln!("NO_SHELL:{shell_ref}: no such instance for {owner} at {node}");
6210:fn cmd_grant(action: GrantCmd) -> i32 {
6289:fn cmd_rename(old_id: &str, new_id: &str) -> i32 {
8490:    fn subnet_cmd(args: &[&str]) -> Option<SubnetCmd> {


Read shellhost test module structure.


560	        // A Gateway-typed owner perch (state="gateway"), bound like any Self perch.
561	        let gw = "playdate-gw";
562	        let p = owlery.join(gw);
563	        std::fs::create_dir_all(&p).unwrap();
564	        spt_store::info::write_info(
565	            &p,
566	            &spt_store::info::InfoJson::new(gw, "now", std::process::id(), "sid-gw", "gateway"),
567	        )
568	        .unwrap();
569
570	        let id = spawn_record(owlery, gw, "mock-shell", None).unwrap();
571	        let shell = shell_section(&format!("{NOOP} --link {{link_token}}"));
572	        launch_shell(owlery, gw, &id, "mock-shell", &shell).unwrap();
573
574	        // The shell perch lives under the GATEWAY owner's namespace, pid recorded.
575	        let perch = spt_store::perch::resolve_shell_perch_path_in(owlery, gw, &id);
576	        assert!(
577	            perch.join(SHELL_PID_FILE).exists(),
578	            "gateway-owned shell pid recorded"
579	        );
580
581	        // "Owner from the link" resolves the gateway owner verbatim — ownership
582	        // resolution never inspects the owner's endpoint type.
583	        let token = read_link_token(&perch).expect("token parked");
584	        assert_eq!(
585	            find_shell_by_token(owlery, &token),
586	            Some((gw.to_string(), id.clone()))
587	        );
588	        assert_eq!(
589	            bind_shell_by_token(owlery, &token),
590	            Some((gw.to_string(), id))
591	        );
592	    }
593
594	    // [unit->REQ-SHELL-1] the handshake end to end on disk: launch parks a
595	    // fresh token + records a pid, the perch stays offline until
596	    // bind-by-token flips exactly that instance online; a wrong/empty token
597	    // binds nothing; per-instance tokens are distinct.
598	    #[test]
599	    fn launch_parks_token_and_bind_by_token_onlines() {
600	        let tmp = tempfile::tempdir().unwrap();
601	        let owlery = tmp.path();
602	        // Owners must be bound Self perches for the bind scan to see them.
603	        for owner in ["doyle", "ling"] {
604	            let p = owlery.join(owner);
605	            std::fs::create_dir_all(&p).unwrap();
606	            std::fs::write(p.join("info.json"), "{}").unwrap();
607	        }
608	        let a = spawn_record(owlery, "doyle", "mock-shell", None).unwrap();
609	        let b = spawn_record(owlery, "ling", "mock-shell", None).unwrap();
610	        let shell = shell_section(&format!("{NOOP} --link {{link_token}}"));
611	        launch_shell(owlery, "doyle", &a, "mock-shell", &shell).unwrap();
612	        launch_shell(owlery, "ling", &b, "mock-shell", &shell).unwrap();
613
614	        let pa = spt_store::perch::resolve_shell_perch_path_in(owlery, "doyle", &a);
615	        let pb = spt_store::perch::resolve_shell_perch_path_in(owlery, "ling", &b);
616	        let ta = read_link_token(&pa).expect("token parked");
617	        let tb = read_link_token(&pb).expect("token parked");
618	        assert_eq!(ta.len(), 32);
619	        assert_ne!(ta, tb, "per-link tokens are distinct");
620	        assert!(pa.join(SHELL_PID_FILE).exists(), "pid recorded");
621
622	        // Launch alone is not the online switch.
623	        let before = spt_store::shellinfo::read_shell_info(&pa).unwrap();
624	        assert_eq!(before.status, SHELL_STATUS_OFFLINE);
625
626	        // The pure lookup resolves "owner from the link" WITHOUT onlining —
627	        // the machinery calls (emit/poll) share it; only bind flips status.
628	        assert_eq!(
629	            find_shell_by_token(owlery, &ta),
630	            Some(("doyle".into(), a.clone()))
631	        );
632	        assert_eq!(
633	            spt_store::shellinfo::read_shell_info(&pa).unwrap().status,
634	            SHELL_STATUS_OFFLINE,
635	            "find is a read, not the online switch"
636	        );
637
638	        // Wrong / empty tokens bind nothing.
639	        assert_eq!(
640	            bind_shell_by_token(owlery, "00000000000000000000000000000000"),
641	            None
642	        );
643	        assert_eq!(bind_shell_by_token(owlery, ""), None);
644
645	        // The right token binds exactly its instance.
646	        assert_eq!(
647	            bind_shell_by_token(owlery, &ta),
648	            Some(("doyle".into(), a.clone()))
649	        );
650	        assert_eq!(
651	            spt_store::shellinfo::read_shell_info(&pa).unwrap().status,
652	            SHELL_STATUS_ONLINE
653	        );
654	        assert_eq!(
655	            spt_store::shellinfo::read_shell_info(&pb).unwrap().status,
656	            SHELL_STATUS_OFFLINE,
657	            "the other instance is untouched"
658	        );
659
660	        // The per-message credential gate matches the exact tuple only.
661	        assert!(verify_link(owlery, "doyle", &a, &ta));
662	        assert!(!verify_link(owlery, "doyle", &a, &tb));
663	        assert!(!verify_link(owlery, "doyle", &a, ""));
664	        assert!(!verify_link(owlery, "ling", &b, &ta));
665	    }
666
667	    // [unit->REQ-SHELL-1] launch is template-driven and fails closed: a
668	    // template missing a value errs before any spawn; an unspawnable program
669	    // errs after parking the token (the caller rolls the perch back).
670	    #[test]
671	    fn launch_fails_closed_on_bad_templates() {
672	        let tmp = tempfile::tempdir().unwrap();
673	        let owlery = tmp.path();
674	        let id = spawn_record(owlery, "doyle", "mock-shell", None).unwrap();
675
676	        let unknown_key = shell_section("prog --x {not_a_key}");
677	        assert!(
678	            launch_shell(owlery, "doyle", &id, "mock-shell", &unknown_key)
679	                .unwrap_err()

        // A Gateway-typed owner perch (state="gateway"), bound like any Self perch.
        let gw = "playdate-gw";
        let p = owlery.join(gw);
        std::fs::create_dir_all(&p).unwrap();
        spt_store::info::write_info(
            &p,
            &spt_store::info::InfoJson::new(gw, "now", std::process::id(), "sid-gw", "gateway"),
        )
        .unwrap();

        let id = spawn_record(owlery, gw, "mock-shell", None).unwrap();
        let shell = shell_section(&format!("{NOOP} --link {{link_token}}"));
        launch_shell(owlery, gw, &id, "mock-shell", &shell).unwrap();

        // The shell perch lives under the GATEWAY owner's namespace, pid recorded.
        let perch = spt_store::perch::resolve_shell_perch_path_in(owlery, gw, &id);
        assert!(
            perch.join(SHELL_PID_FILE).exists(),
            "gateway-owned shell pid recorded"
        );

        // "Owner from the link" resolves the gateway owner verbatim — ownership
        // resolution never inspects the owner's endpoint type.
        let token = read_link_token(&perch).expect("token parked");
        assert_eq!(
            find_shell_by_token(owlery, &token),
            Some((gw.to_string(), id.clone()))
        );
        assert_eq!(
            bind_shell_by_token(owlery, &token),
            Some((gw.to_string(), id))
        );
    }

    // [unit->REQ-SHELL-1] the handshake end to end on disk: launch parks a
    // fresh token + records a pid, the perch stays offline until
    // bind-by-token flips exactly that instance online; a wrong/empty token
    // binds nothing; per-instance tokens are distinct.
    #[test]
    fn launch_parks_token_and_bind_by_token_onlines() {
        let tmp = tempfile::tempdir().unwrap();
        let owlery = tmp.path();
        // Owners must be bound Self perches for the bind scan to see them.
        for owner in ["doyle", "ling"] {
            let p = owlery.join(owner);
            std::fs::create_dir_all(&p).unwrap();
            std::fs::write(p.join("info.json"), "{}").unwrap();
        }
        let a = spawn_record(owlery, "doyle", "mock-shell", None).unwrap();
        let b = spawn_record(owlery, "ling", "mock-shell", None).unwrap();
        let shell = shell_section(&format!("{NOOP} --link {{link_token}}"));
        launch_shell(owlery, "doyle", &a, "mock-shell", &shell).unwrap();
        launch_shell(owlery, "ling", &b, "mock-shell", &shell).unwrap();

        let pa = spt_store::perch::resolve_shell_perch_path_in(owlery, "doyle", &a);
        let pb = spt_store::perch::resolve_shell_perch_path_in(owlery, "ling", &b);
        let ta = read_link_token(&pa).expect("token parked");
        let tb = read_link_token(&pb).expect("token parked");
        assert_eq!(ta.len(), 32);
        assert_ne!(ta, tb, "per-link tokens are distinct");
        assert!(pa.join(SHELL_PID_FILE).exists(), "pid recorded");

        // Launch alone is not the online switch.
        let before = spt_store::shellinfo::read_shell_info(&pa).unwrap();
        assert_eq!(before.status, SHELL_STATUS_OFFLINE);

        // The pure lookup resolves "owner from the link" WITHOUT onlining —
        // the machinery calls (emit/poll) share it; only bind flips status.
        assert_eq!(
            find_shell_by_token(owlery, &ta),
            Some(("doyle".into(), a.clone()))
        );
        assert_eq!(
            spt_store::shellinfo::read_shell_info(&pa).unwrap().status,
            SHELL_STATUS_OFFLINE,
            "find is a read, not the online switch"
        );

        // Wrong / empty tokens bind nothing.
        assert_eq!(
            bind_shell_by_token(owlery, "00000000000000000000000000000000"),
            None
        );
        assert_eq!(bind_shell_by_token(owlery, ""), None);

        // The right token binds exactly its instance.
        assert_eq!(
            bind_shell_by_token(owlery, &ta),
            Some(("doyle".into(), a.clone()))
        );
        assert_eq!(
            spt_store::shellinfo::read_shell_info(&pa).unwrap().status,
            SHELL_STATUS_ONLINE
        );
        assert_eq!(
            spt_store::shellinfo::read_shell_info(&pb).unwrap().status,
            SHELL_STATUS_OFFLINE,
            "the other instance is untouched"
        );

        // The per-message credential gate matches the exact tuple only.
        assert!(verify_link(owlery, "doyle", &a, &ta));
        assert!(!verify_link(owlery, "doyle", &a, &tb));
        assert!(!verify_link(owlery, "doyle", &a, ""));
        assert!(!verify_link(owlery, "ling", &b, &ta));
    }

    // [unit->REQ-SHELL-1] launch is template-driven and fails closed: a
    // template missing a value errs before any spawn; an unspawnable program
    // errs after parking the token (the caller rolls the perch back).
    #[test]
    fn launch_fails_closed_on_bad_templates() {
        let tmp = tempfile::tempdir().unwrap();
        let owlery = tmp.path();
        let id = spawn_record(owlery, "doyle", "mock-shell", None).unwrap();

        let unknown_key = shell_section("prog --x {not_a_key}");
        assert!(
            launch_shell(owlery, "doyle", &id, "mock-shell", &unknown_key)
                .unwrap_err()


A gateway-owner unit test already exists. Read its header/tag.


540	            .unwrap()
541	            .shell
542	            .unwrap()
543	    }
544
545	    /// A cross-platform spawnable no-op command.
546	    #[cfg(windows)]
547	    const NOOP: &str = "cmd /c exit 0";
548	    #[cfg(unix)]
549	    const NOOP: &str = "true";
550
551	    // [unit->REQ-EP-6] a GATEWAY-typed owner spawns + owns a shell identically
552	    // to an agent owner — shell ownership is NOT agent-family-gated (CONTEXT
553	    // §Shell model: "any non-Shell endpoint type may own shells"; ratified at the
554	    // Gateway grill). The owner is an opaque namespace string through mint,
555	    // launch, and the "owner from the link" lookup — no path inspects its type.
556	    #[test]
557	    fn gateway_typed_owner_spawns_and_owns_a_shell() {
558	        let tmp = tempfile::tempdir().unwrap();
559	        let owlery = tmp.path();
560	        // A Gateway-typed owner perch (state="gateway"), bound like any Self perch.
561	        let gw = "playdate-gw";

            .unwrap()
            .shell
            .unwrap()
    }

    /// A cross-platform spawnable no-op command.
    #[cfg(windows)]
    const NOOP: &str = "cmd /c exit 0";
    #[cfg(unix)]
    const NOOP: &str = "true";

    // [unit->REQ-EP-6] a GATEWAY-typed owner spawns + owns a shell identically
    // to an agent owner — shell ownership is NOT agent-family-gated (CONTEXT
    // §Shell model: "any non-Shell endpoint type may own shells"; ratified at the
    // Gateway grill). The owner is an opaque namespace string through mint,
    // launch, and the "owner from the link" lookup — no path inspects its type.
    #[test]
    fn gateway_typed_owner_spawns_and_owns_a_shell() {
        let tmp = tempfile::tempdir().unwrap();
        let owlery = tmp.path();
        // A Gateway-typed owner perch (state="gateway"), bound like any Self perch.
        let gw = "playdate-gw";


resolve_link_target is the cmd/drive/tunnel front-half — needs its own [unit->REQ-SHELL-5]. Read linkhost test module.


700	                }
701	            }
702	            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == opened.stream_id => {
703	                return Ok(ShellLinkRequestOutcome::NoReply);
704	            }
705	            BrokerEvent::Error { message } => return Err(io::Error::other(message)),
706	            _ => {}
707	        }
708	    }
709	}
710
711	#[cfg(test)]
712	mod tests {
713	    use super::*;
714	    use crate::test_home::with_home;
715	    use spt_store::perch;
716
717	    /// Register a shell adapter whose spawn template is a trivial real
718	    /// program (the launch must succeed; the binary's bind never happens in a
719	    /// unit test — the perch stays offline, which is the D3b contract).
720	    fn register_trivial_shell(name: &str, persistent: bool) {
721	        #[cfg(windows)]
722	        let spawn = "cmd /C exit 0";
723	        #[cfg(unix)]
724	        let spawn = "true";
725	        let p = if persistent {
726	            "persistent = true\n"
727	        } else {
728	            ""
729	        };
730	        let src = perch::spt_home().join("srcs").join(name);
731	        std::fs::create_dir_all(&src).unwrap();
732	        std::fs::write(
733	            src.join("manifest.toml"),
734	            format!(
735	                "[adapter]\nname = \"{name}\"\nkind = \"shell\"\nversion = \"1\"\n\
736	                 min_spt_core_version = \"0\"\n\n[shell]\nspawn = '{spawn} {{link_token}} {{id}}'\n{p}\n\
737	                 [shell.capabilities.note]\nargs = [\"text\"]\n"
738	            ),
739	        )
740	        .unwrap();
741	        spt_runtime::registry::register(&perch::adapters_dir(), &src, 1).unwrap();
742	    }
743
744	    // [unit->REQ-SHELL-2] the serve's action core: relink relaunches an
745	    // offline instance (fresh token parked, pid reported) and is idempotent
746	    // on already-online; a cmd vocab-checks, stamps under the CURRENT parked
747	    // token, spools, and wakes an offline persistent instance; a foreign
748	    // owner / unknown ref is NO_SHELL — owner exclusivity as resolution
749	    // scope; an unknown action refuses loud.
750	    #[test]
751	    fn run_action_relinks_drives_and_refuses() {
752	        with_home(|_| {
753	            let owlery = perch::owlery_dir();
754	            register_trivial_shell("Trivial", true);
755	            let id =
756	                shellinfo::spawn_record(&owlery, "doyle", "Trivial", Some("Scout")).expect("mint");
757	            assert_eq!(id, "Trivial-0");
758
759	            // Relink the offline instance: launches, parks a fresh token.
760	            let (out, reply) = run_action(&owlery, "doyle", "Scout", SHELL_LINK_RELINK, &[], "op1");
761	            assert!(matches!(out, ShellLinkServeOutcome::Ok(_)), "{out:?}");
762	            assert!(matches!(reply, ShellLinkRecord::Reply { outcome, .. } if outcome == "ok"));
763	            let perch_path = perch::resolve_shell_perch_path_in(&owlery, "doyle", &id);
764	            let token = shellhost::read_link_token(&perch_path).expect("token parked");
765
766	            // Drive a command (alias addressing): spooled + MAC-valid under
767	            // the parked token; offline+persistent ⇒ the wake leg fires too.
768	            let args = vec!["note".to_string(), "hello".to_string()];
769	            let (out, _) = run_action(&owlery, "doyle", "Scout", SHELL_LINK_CMD, &args, "op2");
770	            let ShellLinkServeOutcome::Ok(detail) = &out else {
771	                panic!("{out:?}")
772	            };
773	            assert!(detail.contains("spooled op=note"), "{detail}");
774	            let rows = spt_store::spool::peek_all_at(&perch_path).unwrap();
775	            assert_eq!(rows.len(), 1);
776	            let key = shellhost::link_key(&token);
777	            let frame = shellchan::verify_stamped_frame(&key, &rows[0].2)
778	                .expect("MAC-valid under the parked token");
779	            assert!(frame.contains("op=\"note\""), "{frame}");
780
781	            // Owner exclusivity: mallory resolves nothing.
782	            let (out, reply) =
783	                run_action(&owlery, "mallory", "Scout", SHELL_LINK_CMD, &args, "op3");
784	            assert!(matches!(out, ShellLinkServeOutcome::Failed(_)), "{out:?}");
785	            assert!(
786	                matches!(reply, ShellLinkRecord::Reply { outcome, .. } if outcome == "no_shell")
787	            );
788
789	            // Unknown action: loud refusal.
790	            let (out, _) = run_action(&owlery, "doyle", "Scout", "spawn", &[], "op4");
791	            assert!(
792	                matches!(out, ShellLinkServeOutcome::BadRequest(_)),
793	                "{out:?}"
794	            );
795
796	            // Vocab gate holds over the wire path too.
797	            let bad = vec!["selfdestruct".to_string()];
798	            let (out, _) = run_action(&owlery, "doyle", "Scout", SHELL_LINK_CMD, &bad, "op5");
799	            assert!(matches!(out, ShellLinkServeOutcome::Failed(_)), "{out:?}");
800	        });
801	    }
802
803	    /// Register a shell adapter with a `[shell.drive]` vocabulary (one type,
804	    /// `stick`) and no capabilities — the drive-channel exerciser.
805	    fn register_drivable_shell(name: &str, persistent: bool) {
806	        #[cfg(windows)]
807	        let spawn = "cmd /C exit 0";
808	        #[cfg(unix)]
809	        let spawn = "true";
810	        let p = if persistent { "persistent = true\n" } else { "" };
811	        let src = perch::spt_home().join("srcs").join(name);
812	        std::fs::create_dir_all(&src).unwrap();
813	        std::fs::write(
814	            src.join("manifest.toml"),
815	            format!(
816	                "[adapter]\nname = \"{name}\"\nkind = \"shell\"\nversion = \"1\"\n\
817	                 min_spt_core_version = \"0\"\n\n[shell]\nspawn = '{spawn} {{link_token}} {{id}}'\n{p}\n\
818	                 [shell.drive]\ntypes = [\"stick\"]\n"
819	            ),
820	        )
821	        .unwrap();
822	        spt_runtime::registry::register(&perch::adapters_dir(), &src, 1).unwrap();
823	    }
824
825	    // [unit->REQ-SHELL-3] the pure drive prep is SPOOL-FREE by construction:
826	    // an online shell yields a Deliver (frame + the parked token stamp) and an
827	    // offline shell yields a DroppedOffline — in BOTH cases the durable shell
828	    // spool stays empty (drive never spools). An undeclared drive-type refuses
829	    // against the manifest vocabulary; a foreign owner is NO_SHELL.

                }
            }
            BrokerEvent::NetStreamEof { stream_id: sid, .. } if sid == opened.stream_id => {
                return Ok(ShellLinkRequestOutcome::NoReply);
            }
            BrokerEvent::Error { message } => return Err(io::Error::other(message)),
            _ => {}
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::test_home::with_home;
    use spt_store::perch;

    /// Register a shell adapter whose spawn template is a trivial real
    /// program (the launch must succeed; the binary's bind never happens in a
    /// unit test — the perch stays offline, which is the D3b contract).
    fn register_trivial_shell(name: &str, persistent: bool) {
        #[cfg(windows)]
        let spawn = "cmd /C exit 0";
        #[cfg(unix)]
        let spawn = "true";
        let p = if persistent {
            "persistent = true\n"
        } else {
            ""
        };
        let src = perch::spt_home().join("srcs").join(name);
        std::fs::create_dir_all(&src).unwrap();
        std::fs::write(
            src.join("manifest.toml"),
            format!(
                "[adapter]\nname = \"{name}\"\nkind = \"shell\"\nversion = \"1\"\n\
                 min_spt_core_version = \"0\"\n\n[shell]\nspawn = '{spawn} {{link_token}} {{id}}'\n{p}\n\
                 [shell.capabilities.note]\nargs = [\"text\"]\n"
            ),
        )
        .unwrap();
        spt_runtime::registry::register(&perch::adapters_dir(), &src, 1).unwrap();
    }

    // [unit->REQ-SHELL-2] the serve's action core: relink relaunches an
    // offline instance (fresh token parked, pid reported) and is idempotent
    // on already-online; a cmd vocab-checks, stamps under the CURRENT parked
    // token, spools, and wakes an offline persistent instance; a foreign
    // owner / unknown ref is NO_SHELL — owner exclusivity as resolution
    // scope; an unknown action refuses loud.
    #[test]
    fn run_action_relinks_drives_and_refuses() {
        with_home(|_| {
            let owlery = perch::owlery_dir();
            register_trivial_shell("Trivial", true);
            let id =
                shellinfo::spawn_record(&owlery, "doyle", "Trivial", Some("Scout")).expect("mint");
            assert_eq!(id, "Trivial-0");

            // Relink the offline instance: launches, parks a fresh token.
            let (out, reply) = run_action(&owlery, "doyle", "Scout", SHELL_LINK_RELINK, &[], "op1");
            assert!(matches!(out, ShellLinkServeOutcome::Ok(_)), "{out:?}");
            assert!(matches!(reply, ShellLinkRecord::Reply { outcome, .. } if outcome == "ok"));
            let perch_path = perch::resolve_shell_perch_path_in(&owlery, "doyle", &id);
            let token = shellhost::read_link_token(&perch_path).expect("token parked");

            // Drive a command (alias addressing): spooled + MAC-valid under
            // the parked token; offline+persistent ⇒ the wake leg fires too.
            let args = vec!["note".to_string(), "hello".to_string()];
            let (out, _) = run_action(&owlery, "doyle", "Scout", SHELL_LINK_CMD, &args, "op2");
            let ShellLinkServeOutcome::Ok(detail) = &out else {
                panic!("{out:?}")
            };
            assert!(detail.contains("spooled op=note"), "{detail}");
            let rows = spt_store::spool::peek_all_at(&perch_path).unwrap();
            assert_eq!(rows.len(), 1);
            let key = shellhost::link_key(&token);
            let frame = shellchan::verify_stamped_frame(&key, &rows[0].2)
                .expect("MAC-valid under the parked token");
            assert!(frame.contains("op=\"note\""), "{frame}");

            // Owner exclusivity: mallory resolves nothing.
            let (out, reply) =
                run_action(&owlery, "mallory", "Scout", SHELL_LINK_CMD, &args, "op3");
            assert!(matches!(out, ShellLinkServeOutcome::Failed(_)), "{out:?}");
            assert!(
                matches!(reply, ShellLinkRecord::Reply { outcome, .. } if outcome == "no_shell")
            );

            // Unknown action: loud refusal.
            let (out, _) = run_action(&owlery, "doyle", "Scout", "spawn", &[], "op4");
            assert!(
                matches!(out, ShellLinkServeOutcome::BadRequest(_)),
                "{out:?}"
            );

            // Vocab gate holds over the wire path too.
            let bad = vec!["selfdestruct".to_string()];
            let (out, _) = run_action(&owlery, "doyle", "Scout", SHELL_LINK_CMD, &bad, "op5");
            assert!(matches!(out, ShellLinkServeOutcome::Failed(_)), "{out:?}");
        });
    }

    /// Register a shell adapter with a `[shell.drive]` vocabulary (one type,
    /// `stick`) and no capabilities — the drive-channel exerciser.
    fn register_drivable_shell(name: &str, persistent: bool) {
        #[cfg(windows)]
        let spawn = "cmd /C exit 0";
        #[cfg(unix)]
        let spawn = "true";
        let p = if persistent { "persistent = true\n" } else { "" };
        let src = perch::spt_home().join("srcs").join(name);
        std::fs::create_dir_all(&src).unwrap();
        std::fs::write(
            src.join("manifest.toml"),
            format!(
                "[adapter]\nname = \"{name}\"\nkind = \"shell\"\nversion = \"1\"\n\
                 min_spt_core_version = \"0\"\n\n[shell]\nspawn = '{spawn} {{link_token}} {{id}}'\n{p}\n\
                 [shell.drive]\ntypes = [\"stick\"]\n"
            ),
        )
        .unwrap();
        spt_runtime::registry::register(&perch::adapters_dir(), &src, 1).unwrap();
    }

    // [unit->REQ-SHELL-3] the pure drive prep is SPOOL-FREE by construction:
    // an online shell yields a Deliver (frame + the parked token stamp) and an
    // offline shell yields a DroppedOffline — in BOTH cases the durable shell
    // spool stays empty (drive never spools). An undeclared drive-type refuses
    // against the manifest vocabulary; a foreign owner is NO_SHELL.


Complete picture. Now build. Three pieces:
1. Unit (linkhost): gateway-typed owner resolves via `resolve_link_target` identically; same-type-different-id → NoShell — tagged `[unit->REQ-SHELL-5]`.
2. Int E2E: new `gateway_owner_shell_e2e.rs`.
3. Activate REQ-SHELL-5 = [doc,impl,unit,int].

Add unit test in linkhost.


Edit

    /// Register a shell adapter with a `[shell.drive]` vocabulary (one type,
    /// `stick`) and no capabilities — the drive-channel exerciser.
    fn register_drivable_shell(name: &str, persistent: bool) {

    // [unit->REQ-SHELL-5] shell ownership is owner-type-agnostic at the
    // resolution layer that fronts cmd/drive/tunnel/relink: a GATEWAY-typed
    // owner resolves + drives its shell IDENTICALLY to an agent owner
    // (resolve_link_target / run_action inspect the owner only as an opaque
    // namespace string — never its endpoint type), and control-exclusivity keys
    // on the owner ENDPOINT-ID, not the type: a SECOND gateway-typed owner with
    // a DIFFERENT id resolves NOTHING for the same alias (NoShell). Same type,
    // different id ⇒ refused — proves the key is the id, not the type
    // (CONTEXT §Shell model "Owner-linked, exclusive"; Gateway grill).
    #[test]
    fn gateway_typed_owner_resolves_and_drives_keyed_on_id_not_type() {
        with_home(|_| {
            let owlery = perch::owlery_dir();
            register_trivial_shell("Trivial", true);

            // Two GATEWAY-typed owners (SAME type, DIFFERENT id) — bound Self
            // perches tagged state="gateway", so the type is genuinely non-agent.
            for gw in ["playdate-gw-a", "playdate-gw-b"] {
                let p = owlery.join(gw);
                std::fs::create_dir_all(&p).unwrap();
                spt_store::info::write_info(
                    &p,
                    &spt_store::info::InfoJson::new(
                        gw,
                        "now",
                        std::process::id(),
                        "sid",
                        "gateway",
                    ),
                )
                .unwrap();
            }

            // gateway-A spawns + owns the shell.
            let id = shellinfo::spawn_record(&owlery, "playdate-gw-a", "Trivial", Some("Scout"))
                .expect("mint");
            let (out, _) =
                run_action(&owlery, "playdate-gw-a", "Scout", SHELL_LINK_RELINK, &[], "op1");
            assert!(matches!(out, ShellLinkServeOutcome::Ok(_)), "relink: {out:?}");

            // resolve_link_target — the shared front half of cmd/drive/tunnel/
            // relink — resolves the gateway owner's shell opaquely (no type gate).
            let (rid, _info, _shell) = resolve_link_target(&owlery, "playdate-gw-a", "Scout")
                .expect("a gateway-typed owner resolves its shell with no type inspection");
            assert_eq!(rid, id);

            // ...and drives a command through identically to an agent owner.
            let args = vec!["note".to_string(), "hi".to_string()];
            let (out, _) =
                run_action(&owlery, "playdate-gw-a", "Scout", SHELL_LINK_CMD, &args, "op2");
            assert!(matches!(out, ShellLinkServeOutcome::Ok(_)), "gateway cmd: {out:?}");

            // Exclusivity keys on the owner ENDPOINT-ID, not the type: gateway-B
            // (same type="gateway", different id) resolves NOTHING for the same
            // alias — NOT a type rejection, an id-scope miss.
            assert!(
                matches!(
                    resolve_link_target(&owlery, "playdate-gw-b", "Scout"),
                    Err(ShellLinkError::NoShell)
                ),
                "a same-type different-id owner resolves no shell (id-scoped, not type-scoped)"
            );
            let (out, reply) =
                run_action(&owlery, "playdate-gw-b", "Scout", SHELL_LINK_CMD, &args, "op3");
            assert!(matches!(out, ShellLinkServeOutcome::Failed(_)), "{out:?}");
            assert!(
                matches!(reply, ShellLinkRecord::Reply { outcome, .. } if outcome == "no_shell"),
                "gateway-B is refused control over gateway-A's shell"
            );
        });
    }

    /// Register a shell adapter with a `[shell.drive]` vocabulary (one type,
    /// `stick`) and no capabilities — the drive-channel exerciser.
    fn register_drivable_shell(name: &str, persistent: bool) {


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

    // [unit->REQ-SHELL-5] shell ownership is owner-type-agnostic at the
    // resolution layer that fronts cmd/drive/tunnel/relink: a GATEWAY-typed
    // owner resolves + drives its shell IDENTICALLY to an agent owner
    // (resolve_link_target / run_action inspect the owner only as an opaque
    // namespace string — never its endpoint type), and control-exclusivity keys
    // on the owner ENDPOINT-ID, not the type: a SECOND gateway-typed owner with
    // a DIFFERENT id resolves NOTHING for the same alias (NoShell). Same type,
    // different id ⇒ refused — proves the key is the id, not the type
    // (CONTEXT §Shell model "Owner-linked, exclusive"; Gateway grill).
    #[test]
    fn gateway_typed_owner_resolves_and_drives_keyed_on_id_not_type() {
        with_home(|_| {
            let owlery = perch::owlery_dir();
            register_trivial_shell("Trivial", true);

            // Two GATEWAY-typed owners (SAME type, DIFFERENT id) — bound Self
            // perches tagged state="gateway", so the type is genuinely non-agent.
            for gw in ["playdate-gw-a", "playdate-gw-b"] {
                let p = owlery.join(gw);
                std::fs::create_dir_all(&p).unwrap();
                spt_store::info::write_info(
                    &p,
                    &spt_store::info::InfoJson::new(
                        gw,
                        "now",
                        std::process::id(),
                        "sid",
                        "gateway",
                    ),
                )
                .unwrap();
            }

            // gateway-A spawns + owns the shell.
            let id = shellinfo::spawn_record(&owlery, "playdate-gw-a", "Trivial", Some("Scout"))
                .expect("mint");
            let (out, _) =
                run_action(&owlery, "playdate-gw-a", "Scout", SHELL_LINK_RELINK, &[], "op1");
            assert!(matches!(out, ShellLinkServeOutcome::Ok(_)), "relink: {out:?}");

            // resolve_link_target — the shared front half of cmd/drive/tunnel/
            // relink — resolves the gateway owner's shell opaquely (no type gate).
            let (rid, _info, _shell) = resolve_link_target(&owlery, "playdate-gw-a", "Scout")
                .expect("a gateway-typed owner resolves its shell with no type inspection");
            assert_eq!(rid, id);

            // ...and drives a command through identically to an agent owner.
            let args = vec!["note".to_string(), "hi".to_string()];
            let (out, _) =
                run_action(&owlery, "playdate-gw-a", "Scout", SHELL_LINK_CMD, &args, "op2");
            assert!(matches!(out, ShellLinkServeOutcome::Ok(_)), "gateway cmd: {out:?}");

            // Exclusivity keys on the owner ENDPOINT-ID, not the type: gateway-B
            // (same type="gateway", different id) resolves NOTHING for the same
            // alias — NOT a type rejection, an id-scope miss.
            assert!(
                matches!(
                    resolve_link_target(&owlery, "playdate-gw-b", "Scout"),
                    Err(ShellLinkError::NoShell)
                ),
                "a same-type different-id owner resolves no shell (id-scoped, not type-scoped)"
            );
            let (out, reply) =
                run_action(&owlery, "playdate-gw-b", "Scout", SHELL_LINK_CMD, &args, "op3");
            assert!(matches!(out, ShellLinkServeOutcome::Failed(_)), "{out:?}");
            assert!(
                matches!(reply, ShellLinkRecord::Reply { outcome, .. } if outcome == "no_shell"),
                "gateway-B is refused control over gateway-A's shell"
            );
        });
    }

    /// Register a shell adapter with a `[shell.drive]` vocabulary (one type,
    /// `stick`) and no capabilities — the drive-channel exerciser.
    fn register_drivable_shell(name: &str, persistent: bool) {

+    // [unit->REQ-SHELL-5] shell ownership is owner-type-agnostic at the

+    // resolution layer that fronts cmd/drive/tunnel/relink: a GATEWAY-typed

+    // owner resolves + drives its shell IDENTICALLY to an agent owner

+    // (resolve_link_target / run_action inspect the owner only as an opaque

+    // namespace string — never its endpoint type), and control-exclusivity keys

+    // on the owner ENDPOINT-ID, not the type: a SECOND gateway-typed owner with

+    // a DIFFERENT id resolves NOTHING for the same alias (NoShell). Same type,

+    // different id ⇒ refused — proves the key is the id, not the type

+    // (CONTEXT §Shell model "Owner-linked, exclusive"; Gateway grill).

+    fn gateway_typed_owner_resolves_and_drives_keyed_on_id_not_type() {

+            let owlery = perch::owlery_dir();

+            register_trivial_shell("Trivial", true);

+            // Two GATEWAY-typed owners (SAME type, DIFFERENT id) — bound Self

+            // perches tagged state="gateway", so the type is genuinely non-agent.

+            for gw in ["playdate-gw-a", "playdate-gw-b"] {

+                let p = owlery.join(gw);

+                std::fs::create_dir_all(&p).unwrap();

+                spt_store::info::write_info(

+                    &spt_store::info::InfoJson::new(

+                        std::process::id(),

+            // gateway-A spawns + owns the shell.

+            let id = shellinfo::spawn_record(&owlery, "playdate-gw-a", "Trivial", Some("Scout"))

+                run_action(&owlery, "playdate-gw-a", "Scout", SHELL_LINK_RELINK, &[], "op1");

+            assert!(matches!(out, ShellLinkServeOutcome::Ok(_)), "relink: {out:?}");

+            // resolve_link_target — the shared front half of cmd/drive/tunnel/

+            // relink — resolves the gateway owner's shell opaquely (no type gate).

+            let (rid, _info, _shell) = resolve_link_target(&owlery, "playdate-gw-a", "Scout")

+                .expect("a gateway-typed owner resolves its shell with no type inspection");

+            // ...and drives a command through identically to an agent owner.

+            let args = vec!["note".to_string(), "hi".to_string()];

+                run_action(&owlery, "playdate-gw-a", "Scout", SHELL_LINK_CMD, &args, "op2");

+            assert!(matches!(out, ShellLinkServeOutcome::Ok(_)), "gateway cmd: {out:?}");

+            // Exclusivity keys on the owner ENDPOINT-ID, not the type: gateway-B

+            // (same type="gateway", different id) resolves NOTHING for the same

+            // alias — NOT a type rejection, an id-scope miss.

+                    resolve_link_target(&owlery, "playdate-gw-b", "Scout"),

+                    Err(ShellLinkError::NoShell)

+                "a same-type different-id owner resolves no shell (id-scoped, not type-scoped)"

+                run_action(&owlery, "playdate-gw-b", "Scout", SHELL_LINK_CMD, &args, "op3");

+            assert!(matches!(out, ShellLinkServeOutcome::Failed(_)), "{out:?}");

+                matches!(reply, ShellLinkRecord::Reply { outcome, .. } if outcome == "no_shell"),

+                "gateway-B is refused control over gateway-A's shell"


Now the int E2E. Combined harness (seed + broker-with-net + tunnel + drive controls). Write the file.