Skip to main content

agent_engine/events/
registry.rs

1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use std::path::PathBuf;
4
5use crate::core::config::base_dir;
6
7/// Discriminator written into every registration so the registry can tell
8/// its own files from other `*.json` sharing the run dir (`daemon.json`,
9/// tooling drops). Files missing it (older binaries) are treated as sessions.
10pub const REGISTRATION_KIND: &str = "session";
11
12fn default_kind() -> String {
13    REGISTRATION_KIND.to_string()
14}
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct SessionRegistration {
18    /// Always `"session"`; see [`REGISTRATION_KIND`].
19    #[serde(default = "default_kind")]
20    pub kind: String,
21    pub session_id: String,
22    pub name: Option<String>,
23    pub socket_path: String,
24    pub pid: u32,
25    pub started_at: DateTime<Utc>,
26}
27
28/// Returns the session runtime directory, creating it mode 0700 if needed.
29///
30/// `SYNAPS_RUNTIME_DIR` deliberately controls only ephemeral Unix sockets and
31/// registration files. Persistent state and plugin discovery remain rooted at
32/// `SYNAPS_BASE_DIR`; this avoids the 108-byte `sun_path` limit when the base
33/// directory is an EFS session path.
34pub fn registry_dir() -> PathBuf {
35    let dir = std::env::var_os("SYNAPS_RUNTIME_DIR")
36        .map(PathBuf::from)
37        .unwrap_or_else(|| base_dir().join("run"));
38    if let Err(e) = std::fs::create_dir_all(&dir) {
39        tracing::warn!("registry: failed to create run dir {:?}: {}", dir, e);
40    }
41    #[cfg(unix)]
42    {
43        use std::os::unix::fs::PermissionsExt;
44        let _ = std::fs::set_permissions(&dir, std::fs::Permissions::from_mode(0o700));
45    }
46    std::fs::canonicalize(&dir).unwrap_or(dir)
47}
48
49/// Sanitize a session ID for safe use in filenames and socket paths.
50/// Rejects path separators, `..`, and non-printable characters.
51/// Returns the sanitized string (replaces unsafe chars with `_`).
52pub fn sanitize_session_id(raw: &str) -> String {
53    // Only allow alphanumeric, hyphens, and underscores. Dots are not needed
54    // in session IDs (format is {name}-{timestamp}-{pid}) and allowing them
55    // complicates path traversal prevention (single-pass ".." replace is
56    // incomplete for "..." inputs).
57    raw.chars()
58        .map(|c| {
59            if c.is_alphanumeric() || c == '-' || c == '_' {
60                c
61            } else {
62                '_'
63            }
64        })
65        .collect::<String>()
66}
67
68/// Returns the legacy Unix socket path for a session.
69/// Active endpoint clients must read `SessionRegistration::socket_path`: new
70/// endpoints use bounded generation names, not this pre-publication helper.
71/// Sockets live in the registry dir — `$SYNAPS_RUNTIME_DIR` when set (e.g.
72/// `/run/user/<uid>/synaps`), otherwise `$SYNAPS_BASE_DIR/run` (default
73/// `~/.synaps-cli/run/`) — which is user-owned and mode 0700, avoiding /tmp
74/// symlink squatting and TOCTOU races.
75pub fn socket_path_for_session(session_id: &str) -> String {
76    socket_path_in_dir(&registry_dir(), session_id)
77}
78
79fn socket_path_in_dir(dir: &std::path::Path, session_id: &str) -> String {
80    let safe_id = sanitize_session_id(session_id);
81    dir.join(format!("{}.sock", safe_id))
82        .to_string_lossy()
83        .into_owned()
84}
85
86/// A permanent lock inode: never unlink this file, including during cleanup.
87/// Bounded acquisition keeps Drop and synchronous boot fail-soft under contention.
88pub(crate) fn lock_dir(dir: &std::path::Path) -> Result<std::fs::File, String> {
89    let mut options = std::fs::OpenOptions::new();
90    options.read(true).write(true).create(true);
91    #[cfg(unix)]
92    {
93        use std::os::unix::fs::OpenOptionsExt;
94        options
95            .mode(0o600)
96            .custom_flags(libc::O_NOFOLLOW | libc::O_NONBLOCK);
97    }
98    let file = options
99        .open(dir.join(".session-registry.lock"))
100        .map_err(|e| e.to_string())?;
101    if !file.metadata().map_err(|e| e.to_string())?.is_file() {
102        return Err("registry lock is not a regular file".into());
103    }
104    #[cfg(unix)]
105    {
106        use std::os::unix::fs::MetadataExt;
107        let meta = file.metadata().map_err(|e| e.to_string())?;
108        // SAFETY: geteuid has no preconditions.
109        if meta.uid() != unsafe { libc::geteuid() }
110            || meta.nlink() != 1
111            || meta.mode() & 0o777 != 0o600
112        {
113            return Err("unsafe registry lock owner, links or permissions".into());
114        }
115    }
116    let deadline = std::time::Instant::now() + std::time::Duration::from_millis(100);
117    loop {
118        if fs4::fs_std::FileExt::try_lock_exclusive(&file).map_err(|e| e.to_string())? {
119            return Ok(file);
120        }
121        if std::time::Instant::now() >= deadline {
122            return Err("registry lock busy".into());
123        }
124        std::thread::sleep(std::time::Duration::from_millis(2));
125    }
126}
127
128fn same_owner(a: &SessionRegistration, b: &SessionRegistration) -> bool {
129    a.session_id == b.session_id
130        && a.pid == b.pid
131        && a.started_at == b.started_at
132        && a.socket_path == b.socket_path
133}
134
135fn generated_endpoint_missing(dir: &std::path::Path, reg: &SessionRegistration) -> bool {
136    #[cfg(unix)]
137    {
138        let path = dir.join(generated_basename(reg));
139        std::path::Path::new(&reg.socket_path) == path
140            && std::fs::symlink_metadata(path)
141                .is_err_and(|e| e.kind() == std::io::ErrorKind::NotFound)
142    }
143    #[cfg(not(unix))]
144    {
145        let _ = (dir, reg);
146        false
147    }
148}
149
150fn generated_basename(reg: &SessionRegistration) -> String {
151    use sha2::{Digest, Sha256};
152    let identity = format!("{}\0{}\0{}", reg.session_id, reg.pid, reg.started_at);
153    let digest = Sha256::digest(identity.as_bytes());
154    let hex: String = digest[..16].iter().map(|b| format!("{b:02x}")).collect();
155    format!("s-{hex}.sock")
156}
157
158fn regular_card(path: &std::path::Path) -> bool {
159    !is_reserved_json(path)
160        && std::fs::symlink_metadata(path).is_ok_and(|m| m.file_type().is_file())
161}
162
163fn valid_card(path: &std::path::Path, reg: &SessionRegistration) -> bool {
164    reg.kind == REGISTRATION_KIND
165        && !is_reserved_json(path)
166        && path.file_name()
167            == Some(std::ffi::OsStr::new(&format!(
168                "{}.json",
169                sanitize_session_id(&reg.session_id)
170            )))
171        && std::fs::symlink_metadata(path).is_ok_and(|m| m.file_type().is_file())
172}
173
174fn remove_socket(dir: &std::path::Path, reg: &SessionRegistration) {
175    let sock = std::path::Path::new(&reg.socket_path);
176    // Exact immediate child only; never follow a claimed subdirectory/symlink.
177    let legacy = format!("{}.sock", sanitize_session_id(&reg.session_id));
178    if (sock == dir.join(generated_basename(reg)) || sock == dir.join(legacy))
179        && sock.parent() == Some(dir)
180    {
181        super::socket::cleanup_socket(&reg.socket_path);
182    }
183}
184
185/// Owns a bound, published generation and its captured runtime directory.
186/// A timestamp/hash collision fails binding without replacing the prior socket.
187/// Unix lock-contention leftovers are repaired on the next successful scan;
188/// Windows card cleanup under contention requires retry or own-PID reload sweep.
189/// No asynchronous task can bind after this guard is dropped.
190pub struct SessionEndpoint {
191    pub registration: SessionRegistration,
192    dir: PathBuf,
193    shutdown: std::sync::Arc<std::sync::atomic::AtomicBool>,
194    task: tokio::task::JoinHandle<()>,
195    socket: super::socket::SocketIdentity,
196}
197
198impl SessionEndpoint {
199    pub fn start(
200        id: &str,
201        name: Option<String>,
202        queue: std::sync::Arc<super::queue::EventQueue>,
203    ) -> Result<Self, String> {
204        let dir = std::fs::canonicalize(registry_dir()).map_err(|e| e.to_string())?;
205        Self::start_in(&dir, id, name, queue)
206    }
207
208    fn start_in(
209        dir: &std::path::Path,
210        id: &str,
211        name: Option<String>,
212        queue: std::sync::Arc<super::queue::EventQueue>,
213    ) -> Result<Self, String> {
214        let _lock = lock_dir(dir)?;
215        scan_registrations_locked(dir, None);
216        let mut registration = SessionRegistration {
217            kind: REGISTRATION_KIND.into(),
218            session_id: id.into(),
219            name,
220            socket_path: String::new(),
221            pid: std::process::id(),
222            started_at: Utc::now(),
223        };
224        registration.socket_path = dir
225            .join(generated_basename(&registration))
226            .to_string_lossy()
227            .into_owned();
228        let (listener, socket) = super::socket::bind_session_socket(&registration.socket_path)
229            .map_err(|e| e.to_string())?;
230        if let Err(e) = write_registration(&registration, dir) {
231            socket.remove();
232            return Err(e);
233        }
234        let shutdown = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
235        let task = super::socket::serve_session_socket(
236            listener,
237            registration.socket_path.clone(),
238            queue,
239            shutdown.clone(),
240        );
241        Ok(Self {
242            registration,
243            dir: dir.to_path_buf(),
244            shutdown,
245            task,
246            socket,
247        })
248    }
249
250    /// Rename only this published generation in the captured directory.
251    pub fn update_name(&self, name: Option<&str>) -> Result<(), String> {
252        let _lock = lock_dir(&self.dir)?;
253        let path = self.dir.join(format!(
254            "{}.json",
255            sanitize_session_id(&self.registration.session_id)
256        ));
257        if !regular_card(&path) {
258            return Err("session card missing".into());
259        }
260        let mut disk: SessionRegistration =
261            serde_json::from_str(&std::fs::read_to_string(&path).map_err(|e| e.to_string())?)
262                .map_err(|e| e.to_string())?;
263        if !valid_card(&path, &disk) || !same_owner(&disk, &self.registration) {
264            return Err("session generation replaced".into());
265        }
266        disk.name = name.map(str::to_owned);
267        write_registration(&disk, &self.dir)
268    }
269
270    pub fn shutdown(&self) {
271        self.shutdown
272            .store(true, std::sync::atomic::Ordering::Release);
273        self.task.abort();
274        // This unique generation cannot be reused by another successful bind.
275        // Remove only the captured inode even if card cleanup cannot get the lock.
276        self.socket.remove();
277        let Ok(_lock) = lock_dir(&self.dir) else {
278            // Unix scans repair missing generated endpoints, including live-PID
279            // cards. Windows has no filesystem endpoint: card cleanup remains
280            // best effort until explicit shutdown retry / own-PID reload sweep.
281            return;
282        };
283        unregister_owned_locked(&self.registration, &self.dir, false);
284    }
285}
286
287impl Drop for SessionEndpoint {
288    fn drop(&mut self) {
289        self.shutdown();
290    }
291}
292
293/// Write `{session_id}.json` atomically (tmp + rename). Chmod 0600 on Unix.
294pub fn register_session(reg: &SessionRegistration) -> Result<(), String> {
295    register_session_in(reg, &registry_dir())
296}
297
298fn register_session_in(reg: &SessionRegistration, dir: &std::path::Path) -> Result<(), String> {
299    let _lock = lock_dir(dir)?;
300    write_registration(reg, dir)
301}
302
303fn write_registration(reg: &SessionRegistration, dir: &std::path::Path) -> Result<(), String> {
304    use std::io::Write;
305    let safe_id = sanitize_session_id(&reg.session_id);
306    let path = dir.join(format!("{}.json", safe_id));
307    if is_reserved_json(&path) {
308        return Err("reserved session id".into());
309    }
310    // Do not overwrite foreign JSON, symlinks or special files sharing run/.
311    match std::fs::symlink_metadata(&path) {
312        Ok(meta) => {
313            if !meta.file_type().is_file() {
314                return Err("not a regular session card".into());
315            }
316            let old: SessionRegistration =
317                serde_json::from_str(&std::fs::read_to_string(&path).map_err(|e| e.to_string())?)
318                    .map_err(|e| e.to_string())?;
319            if !valid_card(&path, &old) || old.session_id != reg.session_id {
320                return Err("refusing to replace a foreign session card".into());
321            }
322        }
323        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
324        Err(e) => return Err(e.to_string()),
325    }
326    let tmp = dir.join(format!(".session-{}.tmp", uuid::Uuid::new_v4().simple()));
327
328    let json = serde_json::to_string(reg).map_err(|e| format!("serialize error: {}", e))?;
329
330    let mut options = std::fs::OpenOptions::new();
331    options.write(true).create_new(true);
332    #[cfg(unix)]
333    {
334        use std::os::unix::fs::OpenOptionsExt;
335        options.mode(0o600);
336    }
337    let result = (|| {
338        let mut file = options.open(&tmp)?;
339        file.write_all(json.as_bytes())?;
340        drop(file);
341        std::fs::rename(&tmp, &path)
342    })();
343    if result.is_err() {
344        let _ = std::fs::remove_file(&tmp);
345    }
346    result.map_err(|e: std::io::Error| e.to_string())?;
347
348    Ok(())
349}
350
351/// Rewrite `{session_id}.json` with a new `name` (rename / `saveas`). The
352/// rest of the record (socket, pid, started_at) is kept. Errors when there
353/// is no registration to update.
354pub fn update_session_name(session_id: &str, name: Option<&str>) -> Result<(), String> {
355    update_session_name_in(session_id, name, &registry_dir())
356}
357
358fn update_session_name_in(
359    session_id: &str,
360    name: Option<&str>,
361    dir: &std::path::Path,
362) -> Result<(), String> {
363    let _lock = lock_dir(dir)?;
364    let safe_id = sanitize_session_id(session_id);
365    let path = dir.join(format!("{}.json", safe_id));
366    if !regular_card(&path) {
367        return Err("not a regular session card".into());
368    }
369    let content =
370        std::fs::read_to_string(&path).map_err(|e| format!("read {}: {}", path.display(), e))?;
371    let mut reg: SessionRegistration =
372        serde_json::from_str(&content).map_err(|e| format!("parse {}: {}", path.display(), e))?;
373    if !valid_card(&path, &reg) {
374        return Err("not a session card".into());
375    }
376    reg.name = name.map(str::to_string);
377    write_registration(&reg, dir)
378}
379
380/// Operator removal by ID (not owner-scoped). No runtime teardown uses this.
381/// Remove only a valid matching registration. Best-effort — never panics.
382/// Also removes the socket file at `socket_path` if it exists.
383pub fn unregister_session(session_id: &str) {
384    unregister_session_in(session_id, &registry_dir());
385}
386
387fn unregister_session_in(session_id: &str, dir: &std::path::Path) {
388    let Ok(_lock) = lock_dir(dir) else {
389        return;
390    };
391    let path = dir.join(format!("{}.json", sanitize_session_id(session_id)));
392    if !regular_card(&path) {
393        return;
394    }
395    if let Ok(content) = std::fs::read_to_string(&path) {
396        if let Ok(reg) = serde_json::from_str::<SessionRegistration>(&content) {
397            if valid_card(&path, &reg) {
398                remove_registration_files(dir, &path, &reg);
399            }
400        }
401    }
402}
403
404/// Remove `reg`'s registration and socket, but only while the file on disk
405/// is still this exact registration (same owner pid, start time and socket).
406///
407/// This is what a session calls when it ends. The check matters because the
408/// same id can be registered again while an older owner is still winding
409/// down (a second actor on one id, a rehydrated session after a reload): the
410/// old owner must not delete the newer registration. Best-effort and
411/// idempotent: a missing or foreign file is left alone.
412pub fn unregister_owned(reg: &SessionRegistration) {
413    unregister_owned_in(reg, &registry_dir());
414}
415
416fn unregister_owned_in(reg: &SessionRegistration, dir: &std::path::Path) {
417    let Ok(_lock) = lock_dir(dir) else {
418        return;
419    };
420    unregister_owned_locked(reg, dir, true);
421}
422
423fn unregister_owned_locked(
424    reg: &SessionRegistration,
425    dir: &std::path::Path,
426    remove_claimed_socket: bool,
427) {
428    let safe_id = sanitize_session_id(&reg.session_id);
429    let path = dir.join(format!("{}.json", safe_id));
430    if !regular_card(&path) {
431        return;
432    }
433    let Ok(content) = std::fs::read_to_string(&path) else {
434        return;
435    };
436    let Ok(on_disk) = serde_json::from_str::<SessionRegistration>(&content) else {
437        return;
438    };
439    if !valid_card(&path, &on_disk)
440        || on_disk.session_id != reg.session_id
441        || on_disk.pid != reg.pid
442        || on_disk.started_at != reg.started_at
443        || on_disk.socket_path != reg.socket_path
444    {
445        tracing::debug!(session = %reg.session_id, "registry: registration was replaced; leaving the newer one");
446        return;
447    }
448    if remove_claimed_socket {
449        remove_socket(dir, &on_disk);
450    }
451    let _ = std::fs::remove_file(path);
452}
453
454/// Unlink a registration file and its socket. The socket is only removed
455/// when it lies inside the registry dir, so a crafted file can never delete
456/// anything elsewhere.
457fn remove_registration_files(
458    dir: &std::path::Path,
459    path: &std::path::Path,
460    reg: &SessionRegistration,
461) {
462    if !valid_card(path, reg) {
463        return;
464    }
465    remove_socket(dir, reg);
466    let _ = std::fs::remove_file(path);
467}
468
469/// Remove registrations that no running session can own and return how
470/// many were removed. Always: an owner pid that no longer exists. With
471/// `own_pid` (the daemon, at startup): also registrations naming that pid,
472/// which can only be leftovers. A reload re-execs the same pid without
473/// running any session's shutdown, and the new image has not registered
474/// anything yet when it sweeps.
475pub fn sweep_stale_registrations(own_pid: Option<u32>) -> usize {
476    sweep_stale_registrations_in(&registry_dir(), own_pid)
477}
478
479fn sweep_stale_registrations_in(dir: &std::path::Path, own_pid: Option<u32>) -> usize {
480    scan_registrations(dir, own_pid).1
481}
482
483/// Returns true if a process with `pid` is alive (Unix: `kill(pid, 0)`).
484fn pid_is_alive(pid: u32) -> bool {
485    if pid == 0 || pid > i32::MAX as u32 {
486        return false;
487    }
488    #[cfg(unix)]
489    {
490        // SAFETY: kill with signal 0 never sends a signal; it only checks
491        // whether the process exists and we have permission to signal it.
492        let result = unsafe { libc::kill(pid as libc::pid_t, 0) };
493        result == 0 || std::io::Error::last_os_error().raw_os_error() == Some(libc::EPERM)
494    }
495    #[cfg(not(unix))]
496    {
497        let _ = pid;
498        true
499    }
500}
501
502/// Files in the run dir that are never session registrations, whatever
503/// their contents: the daemon's own `daemon.json` / `daemon-<profile>.json`.
504fn is_reserved_json(path: &std::path::Path) -> bool {
505    path.file_stem()
506        .and_then(|s| s.to_str())
507        .is_some_and(|stem| stem == "daemon" || stem.starts_with("daemon-"))
508}
509
510/// Read all registration files, prune stale ones (dead PID), return live set.
511///
512/// Only files this registry wrote are ever unlinked: a `*.json` that is not
513/// a `SessionRegistration` (parse failure, or `kind != "session"`) is logged
514/// and skipped — `daemon.json` lives in the same dir and `synaps send` must
515/// never destroy it.
516pub fn list_active_sessions() -> Vec<SessionRegistration> {
517    list_active_sessions_in(&registry_dir())
518}
519
520fn list_active_sessions_in(dir: &std::path::Path) -> Vec<SessionRegistration> {
521    scan_registrations(dir, None).0
522}
523
524/// Read every session registration in `dir`, unlink the stale ones (dead
525/// owner pid, or `own_pid` when given) and return `(live, removed)`.
526fn scan_registrations(
527    dir: &std::path::Path,
528    own_pid: Option<u32>,
529) -> (Vec<SessionRegistration>, usize) {
530    let Ok(_lock) = lock_dir(dir) else {
531        return (Vec::new(), 0);
532    };
533    scan_registrations_locked(dir, own_pid)
534}
535
536fn scan_registrations_locked(
537    dir: &std::path::Path,
538    own_pid: Option<u32>,
539) -> (Vec<SessionRegistration>, usize) {
540    let Ok(entries) = std::fs::read_dir(dir) else {
541        return (Vec::new(), 0);
542    };
543
544    let mut live = Vec::new();
545    let mut removed = 0;
546
547    for entry in entries.flatten() {
548        let path = entry.path();
549        if !path.extension().is_some_and(|e| e == "json") || is_reserved_json(&path) {
550            continue;
551        }
552        if !regular_card(&path) {
553            continue;
554        }
555        let Ok(content) = std::fs::read_to_string(&path) else {
556            continue;
557        };
558        let reg = match serde_json::from_str::<SessionRegistration>(&content) {
559            Ok(reg) if reg.kind == REGISTRATION_KIND => reg,
560            Ok(reg) => {
561                tracing::debug!(path = %path.display(), kind = %reg.kind, "registry: not a session registration — skipped");
562                continue;
563            }
564            Err(e) => {
565                tracing::debug!(path = %path.display(), error = %e, "registry: unparseable json — skipped, not ours");
566                continue;
567            }
568        };
569
570        if !valid_card(&path, &reg) {
571            continue;
572        }
573        if own_pid != Some(reg.pid)
574            && pid_is_alive(reg.pid)
575            && !generated_endpoint_missing(dir, &reg)
576        {
577            live.push(reg);
578        } else {
579            remove_registration_files(dir, &path, &reg);
580            removed += 1;
581        }
582    }
583
584    (live, removed)
585}
586
587/// Resolve a query to a registration. Resolution order:
588/// 1. Exact session_id
589/// 2. Name match
590/// 3. Partial session_id prefix (unambiguous)
591pub fn find_session_registration(query: &str) -> Option<SessionRegistration> {
592    find_session_registration_in(query, &registry_dir())
593}
594
595fn find_session_registration_in(query: &str, dir: &std::path::Path) -> Option<SessionRegistration> {
596    let sessions = list_active_sessions_in(dir);
597
598    // 1. Exact ID
599    if let Some(reg) = sessions.iter().find(|r| r.session_id == query) {
600        return Some(reg.clone());
601    }
602
603    // 2. Name match
604    if let Some(reg) = sessions.iter().find(|r| r.name.as_deref() == Some(query)) {
605        return Some(reg.clone());
606    }
607
608    // 3. Partial prefix — only if unambiguous
609    let matches: Vec<_> = sessions
610        .iter()
611        .filter(|r| r.session_id.starts_with(query))
612        .collect();
613
614    if matches.len() == 1 {
615        Some(matches[0].clone())
616    } else {
617        None
618    }
619}
620
621#[cfg(test)]
622mod tests {
623    use super::*;
624    use serial_test::serial;
625    use tempfile::TempDir;
626
627    #[cfg(unix)]
628    #[tokio::test]
629    async fn locked_drop_removes_endpoint_and_next_scan_repairs_live_pid_card() {
630        let tmp = tmp_registry();
631        let endpoint = SessionEndpoint::start_in(
632            tmp.path(),
633            "locked",
634            None,
635            std::sync::Arc::new(super::super::queue::EventQueue::new(10)),
636        )
637        .unwrap();
638        let path = endpoint.registration.socket_path.clone();
639        let lock = lock_dir(tmp.path()).unwrap();
640        drop(endpoint);
641        assert!(!std::path::Path::new(&path).exists());
642        assert!(tmp.path().join("locked.json").exists());
643        drop(lock);
644        assert_eq!(sweep_stale_registrations_in(tmp.path(), None), 1);
645        assert!(!tmp.path().join("locked.json").exists());
646    }
647
648    #[tokio::test]
649    async fn owned_rename_refuses_replacement_and_uses_captured_root() {
650        let tmp = tmp_registry();
651        let other = tmp_registry();
652        let queue = std::sync::Arc::new(super::super::queue::EventQueue::new(10));
653        let old = SessionEndpoint::start_in(tmp.path(), "same", None, queue.clone()).unwrap();
654        let new = SessionEndpoint::start_in(tmp.path(), "same", None, queue.clone()).unwrap();
655        let unrelated = SessionEndpoint::start_in(other.path(), "same", None, queue).unwrap();
656        assert!(old.update_name(Some("wrong")).is_err());
657        new.update_name(Some("right")).unwrap();
658        assert_eq!(
659            list_active_sessions_in(tmp.path())[0].name.as_deref(),
660            Some("right")
661        );
662        assert!(list_active_sessions_in(other.path())[0].name.is_none());
663        drop(new);
664        assert!(other.path().join("same.json").exists());
665        drop(unrelated);
666    }
667
668    #[tokio::test]
669    async fn publication_failure_rolls_back_bound_endpoint() {
670        let tmp = tmp_registry();
671        assert!(SessionEndpoint::start_in(
672            tmp.path(),
673            "daemon",
674            None,
675            std::sync::Arc::new(super::super::queue::EventQueue::new(10))
676        )
677        .is_err());
678        let names: Vec<_> = std::fs::read_dir(tmp.path())
679            .unwrap()
680            .map(|e| e.unwrap().file_name())
681            .collect();
682        assert_eq!(
683            names,
684            vec![std::ffi::OsString::from(".session-registry.lock")]
685        );
686    }
687
688    #[cfg(unix)]
689    #[tokio::test]
690    async fn replacement_survives_old_task_ending_and_immediate_drop() {
691        let tmp = tmp_registry();
692        let queue = std::sync::Arc::new(super::super::queue::EventQueue::new(10));
693        let old = SessionEndpoint::start_in(tmp.path(), "same", None, queue.clone()).unwrap();
694        let old_path = old.registration.socket_path.clone();
695        let new = SessionEndpoint::start_in(tmp.path(), "same", None, queue).unwrap();
696        assert_ne!(old_path, new.registration.socket_path);
697        update_session_name_in("same", Some("renamed"), tmp.path()).unwrap();
698        // No await since either start: neither listener task has been polled.
699        drop(old);
700        assert!(!std::path::Path::new(&old_path).exists());
701        tokio::task::yield_now().await;
702        let _client = tokio::net::UnixStream::connect(&new.registration.socket_path)
703            .await
704            .unwrap();
705        let new_path = new.registration.socket_path.clone();
706        assert_eq!(
707            list_active_sessions_in(tmp.path())[0].name.as_deref(),
708            Some("renamed")
709        );
710        drop(new);
711        assert!(!std::path::Path::new(&new_path).exists());
712        assert!(!tmp.path().join("same.json").exists());
713    }
714
715    #[test]
716    fn lock_is_permanent_and_contention_is_bounded() {
717        let tmp = tmp_registry();
718        let lock = lock_dir(tmp.path()).unwrap();
719        let barrier = std::sync::Arc::new(std::sync::Barrier::new(2));
720        let ready = barrier.clone();
721        let dir = tmp.path().to_path_buf();
722        let contender = std::thread::spawn(move || {
723            ready.wait();
724            assert!(lock_dir(&dir).is_err());
725        });
726        barrier.wait();
727        contender.join().unwrap();
728        drop(lock);
729        assert!(tmp.path().join(".session-registry.lock").exists());
730        assert!(lock_dir(tmp.path()).is_ok());
731    }
732
733    #[cfg(unix)]
734    #[tokio::test]
735    async fn stale_sweep_and_registration_are_serialized() {
736        let tmp = tmp_registry();
737        reg_with_socket(tmp.path(), "same", 0);
738        let lock = lock_dir(tmp.path()).unwrap();
739        let barrier = std::sync::Arc::new(std::sync::Barrier::new(2));
740        let ready = barrier.clone();
741        let dir = tmp.path().to_path_buf();
742        let sweep = std::thread::spawn(move || {
743            ready.wait();
744            // The other actor holds the publication lock: no stale deletion.
745            assert_eq!(
746                sweep_stale_registrations_in(&dir, Some(std::process::id())),
747                0
748            );
749        });
750        barrier.wait();
751        sweep.join().unwrap();
752        drop(lock);
753        let endpoint = SessionEndpoint::start_in(
754            tmp.path(),
755            "same",
756            None,
757            std::sync::Arc::new(super::super::queue::EventQueue::new(10)),
758        )
759        .unwrap();
760        assert_eq!(sweep_stale_registrations_in(tmp.path(), None), 0);
761        let _client = tokio::net::UnixStream::connect(&endpoint.registration.socket_path)
762            .await
763            .unwrap();
764    }
765
766    #[cfg(unix)]
767    #[test]
768    fn malicious_socket_claims_leave_user_files_and_other_sockets() {
769        let tmp = tmp_registry();
770        let outside = tmp_registry();
771        let outside_path = outside.path().join("victim.sock");
772        let _outside = std::os::unix::net::UnixListener::bind(&outside_path).unwrap();
773        let unrelated = tmp.path().join("other.sock");
774        let _other = std::os::unix::net::UnixListener::bind(&unrelated).unwrap();
775        let regular = tmp.path().join("victim.sock");
776        std::fs::write(&regular, b"user data").unwrap();
777        std::os::unix::fs::symlink(outside.path(), tmp.path().join("link")).unwrap();
778        for path in [
779            outside_path.clone(),
780            unrelated.clone(),
781            regular.clone(),
782            tmp.path().join("link/victim.sock"),
783            tmp.path().join("../victim.sock"),
784        ] {
785            let mut reg = make_reg("victim", None, 0);
786            reg.socket_path = path.to_string_lossy().into_owned();
787            register_session_in(&reg, tmp.path()).unwrap();
788            sweep_stale_registrations_in(tmp.path(), None);
789        }
790        assert!(outside_path.exists());
791        assert!(unrelated.exists());
792        assert_eq!(std::fs::read(regular).unwrap(), b"user data");
793    }
794
795    #[cfg(windows)]
796    #[tokio::test]
797    async fn replacement_pipe_is_bound_before_publication() {
798        let tmp = tmp_registry();
799        let queue = std::sync::Arc::new(super::super::queue::EventQueue::new(10));
800        let old = SessionEndpoint::start_in(tmp.path(), "same", None, queue.clone()).unwrap();
801        let new = SessionEndpoint::start_in(tmp.path(), "same", None, queue).unwrap();
802        drop(old);
803        let pipe = super::super::socket::pipe_name_for(&new.registration.socket_path);
804        let _client = tokio::net::windows::named_pipe::ClientOptions::new()
805            .open(pipe)
806            .unwrap();
807    }
808
809    fn tmp_registry() -> TempDir {
810        let dir = tempfile::tempdir().unwrap();
811        std::fs::create_dir_all(dir.path()).unwrap();
812        dir
813    }
814
815    fn make_reg(id: &str, name: Option<&str>, pid: u32) -> SessionRegistration {
816        SessionRegistration {
817            kind: REGISTRATION_KIND.to_string(),
818            session_id: id.to_string(),
819            name: name.map(|s| s.to_string()),
820            socket_path: format!("/unused/{id}.sock"),
821            pid,
822            started_at: Utc::now(),
823        }
824    }
825
826    fn dir_buf(tmp: &TempDir) -> PathBuf {
827        tmp.path().to_path_buf()
828    }
829
830    #[test]
831    fn register_creates_file() {
832        let tmp = tmp_registry();
833        let dir = dir_buf(&tmp);
834        let reg = make_reg("abc-1234", None, std::process::id());
835        register_session_in(&reg, &dir).unwrap();
836        assert!(dir.join("abc-1234.json").exists());
837    }
838
839    #[test]
840    fn list_returns_live_sessions() {
841        let tmp = tmp_registry();
842        let dir = dir_buf(&tmp);
843        let pid = std::process::id();
844        let reg = make_reg("live-0001", Some("my-agent"), pid);
845        register_session_in(&reg, &dir).unwrap();
846
847        let sessions = list_active_sessions_in(&dir);
848        assert!(sessions.iter().any(|r| r.session_id == "live-0001"));
849    }
850
851    #[test]
852    fn find_by_exact_id() {
853        let tmp = tmp_registry();
854        let dir = dir_buf(&tmp);
855        let reg = make_reg("find-exact-01", None, std::process::id());
856        register_session_in(&reg, &dir).unwrap();
857
858        let found = find_session_registration_in("find-exact-01", &dir);
859        assert!(found.is_some());
860        assert_eq!(found.unwrap().session_id, "find-exact-01");
861    }
862
863    #[test]
864    fn find_by_name() {
865        let tmp = tmp_registry();
866        let dir = dir_buf(&tmp);
867        let reg = make_reg("named-session-01", Some("prod-agent"), std::process::id());
868        register_session_in(&reg, &dir).unwrap();
869
870        let found = find_session_registration_in("prod-agent", &dir);
871        assert!(found.is_some());
872        assert_eq!(found.unwrap().session_id, "named-session-01");
873    }
874
875    #[test]
876    fn find_by_partial_prefix() {
877        let tmp = tmp_registry();
878        let dir = dir_buf(&tmp);
879        let reg = make_reg("prefix-abcdef-01", None, std::process::id());
880        register_session_in(&reg, &dir).unwrap();
881
882        let found = find_session_registration_in("prefix-abc", &dir);
883        assert!(found.is_some());
884        assert_eq!(found.unwrap().session_id, "prefix-abcdef-01");
885    }
886
887    #[test]
888    fn ambiguous_prefix_returns_none() {
889        let tmp = tmp_registry();
890        let dir = dir_buf(&tmp);
891        let pid = std::process::id();
892        register_session_in(&make_reg("dup-aaaa-01", None, pid), &dir).unwrap();
893        register_session_in(&make_reg("dup-aaaa-02", None, pid), &dir).unwrap();
894
895        let found = find_session_registration_in("dup-aaaa", &dir);
896        assert!(found.is_none(), "ambiguous prefix should return None");
897    }
898
899    /// Bug: `synaps send` used to unlink every `run/*.json` it could not
900    /// parse as a registration — `daemon.json` included.
901    #[test]
902    fn foreign_json_is_never_unlinked() {
903        let tmp = tmp_registry();
904        let dir = dir_buf(&tmp);
905        let daemon = dir.join("daemon.json");
906        std::fs::write(
907            &daemon,
908            r#"{"pid":1,"version":"0.9.0","exe":"/usr/bin/synaps","socket":"/x/daemon.sock","protocol":2}"#,
909        )
910        .unwrap();
911        let garbage = dir.join("garbage.json");
912        std::fs::write(&garbage, "this is not json {").unwrap();
913        // Parses as a registration structurally but declares another kind.
914        let other = dir.join("other-kind.json");
915        std::fs::write(
916            &other,
917            r#"{"kind":"widget","session_id":"x","name":null,"socket_path":"/x.sock","pid":1,"started_at":"2024-01-01T00:00:00Z"}"#,
918        )
919        .unwrap();
920        // A legacy registration without `kind` still counts as a session.
921        let legacy = dir.join("legacy-0001.json");
922        std::fs::write(
923            &legacy,
924            format!(
925                r#"{{"session_id":"legacy-0001","name":"old","socket_path":"{}","pid":{},"started_at":"2024-01-01T00:00:00Z"}}"#,
926                dir.join("legacy-0001.sock").display(),
927                std::process::id()
928            ),
929        )
930        .unwrap();
931        let reg = make_reg("live-0002", Some("ambient"), std::process::id());
932        register_session_in(&reg, &dir).unwrap();
933
934        for _ in 0..2 {
935            let found = find_session_registration_in("ambient", &dir).expect("session resolved");
936            assert_eq!(found.session_id, "live-0002");
937            assert!(
938                find_session_registration_in("old", &dir).is_some(),
939                "legacy reg counts"
940            );
941            let ids: Vec<_> = list_active_sessions_in(&dir)
942                .into_iter()
943                .map(|r| r.session_id)
944                .collect();
945            assert!(!ids.contains(&"x".to_string()), "foreign kind not listed");
946            assert!(daemon.exists(), "daemon.json must survive");
947            assert!(garbage.exists(), "garbage.json must survive");
948            assert!(other.exists(), "other-kind.json must survive");
949            assert!(legacy.exists());
950        }
951        assert!(std::fs::read_to_string(&daemon).unwrap().contains("0.9.0"));
952    }
953
954    /// Bug: the name was frozen at create — `saveas` never reached the
955    /// registry, so `synaps send --session <name>` missed until a
956    /// `--continue`.
957    #[test]
958    fn update_name_rewrites_registration() {
959        let tmp = tmp_registry();
960        let dir = dir_buf(&tmp);
961        let reg = make_reg("rename-0001", None, std::process::id());
962        register_session_in(&reg, &dir).unwrap();
963        assert!(find_session_registration_in("ambient", &dir).is_none());
964
965        update_session_name_in("rename-0001", Some("ambient"), &dir).unwrap();
966        let found = find_session_registration_in("ambient", &dir).expect("resolves by new name");
967        assert_eq!(found.session_id, "rename-0001");
968        assert_eq!(
969            found.socket_path, reg.socket_path,
970            "rest of the record kept"
971        );
972
973        update_session_name_in("rename-0001", None, &dir).unwrap();
974        assert!(
975            find_session_registration_in("ambient", &dir).is_none(),
976            "cleared"
977        );
978        assert!(
979            update_session_name_in("ghost", Some("x"), &dir).is_err(),
980            "no reg → error"
981        );
982    }
983
984    #[test]
985    fn unregister_removes_file() {
986        let tmp = tmp_registry();
987        let dir = dir_buf(&tmp);
988        let reg = make_reg("unreg-0001", None, std::process::id());
989        register_session_in(&reg, &dir).unwrap();
990
991        let path = dir.join("unreg-0001.json");
992        assert!(path.exists());
993
994        unregister_session_in("unreg-0001", &dir);
995        assert!(!path.exists());
996    }
997
998    #[test]
999    fn unregister_is_idempotent() {
1000        let tmp = tmp_registry();
1001        let dir = dir_buf(&tmp);
1002        // Should not panic even if the file was never registered
1003        unregister_session_in("ghost-session-99", &dir);
1004    }
1005
1006    #[cfg(unix)]
1007    #[test]
1008    fn stale_pid_pruned() {
1009        let tmp = tmp_registry();
1010        let dir = dir_buf(&tmp);
1011        // PID 999999 is effectively guaranteed to not exist
1012        let reg = make_reg("stale-dead-pid", None, 999999);
1013        register_session_in(&reg, &dir).unwrap();
1014
1015        let sessions = list_active_sessions_in(&dir);
1016        assert!(
1017            !sessions.iter().any(|r| r.session_id == "stale-dead-pid"),
1018            "stale registration should have been pruned"
1019        );
1020
1021        // File should also be gone
1022        assert!(!dir.join("stale-dead-pid.json").exists());
1023    }
1024
1025    /// A registration whose socket lives in `dir`, with a real socket file,
1026    /// so removal of both can be observed.
1027    #[cfg(unix)]
1028    fn reg_with_socket(dir: &std::path::Path, id: &str, pid: u32) -> SessionRegistration {
1029        let reg = SessionRegistration {
1030            kind: REGISTRATION_KIND.to_string(),
1031            session_id: id.to_string(),
1032            name: None,
1033            socket_path: socket_path_in_dir(dir, id),
1034            pid,
1035            started_at: Utc::now(),
1036        };
1037        let _ = std::fs::remove_file(&reg.socket_path);
1038        let _socket = std::os::unix::net::UnixListener::bind(&reg.socket_path).unwrap();
1039        register_session_in(&reg, dir).unwrap();
1040        reg
1041    }
1042
1043    #[cfg(unix)]
1044    #[test]
1045    fn unregister_owned_removes_its_own_registration_and_socket() {
1046        let tmp = tmp_registry();
1047        let dir = dir_buf(&tmp);
1048        let reg = reg_with_socket(&dir, "owned", std::process::id());
1049        unregister_owned_in(&reg, &dir);
1050        assert!(!dir.join("owned.json").exists());
1051        assert!(!dir.join("owned.sock").exists());
1052        // Idempotent: shutdown() and Drop both call it.
1053        unregister_owned_in(&reg, &dir);
1054    }
1055
1056    #[cfg(unix)]
1057    #[test]
1058    fn unregister_owned_leaves_a_newer_registration_of_the_same_id() {
1059        let tmp = tmp_registry();
1060        let dir = dir_buf(&tmp);
1061        let old = reg_with_socket(&dir, "same-id", std::process::id());
1062        std::thread::sleep(std::time::Duration::from_millis(2));
1063        let newer = reg_with_socket(&dir, "same-id", std::process::id());
1064        assert_ne!(old.started_at, newer.started_at);
1065
1066        unregister_owned_in(&old, &dir);
1067        assert!(dir.join("same-id.json").exists(), "newer registration kept");
1068        assert!(dir.join("same-id.sock").exists(), "newer socket kept");
1069        let live = list_active_sessions_in(&dir);
1070        assert_eq!(live.len(), 1);
1071        assert_eq!(live[0].started_at, newer.started_at);
1072    }
1073
1074    #[cfg(unix)]
1075    #[test]
1076    fn a_renamed_registration_is_still_owned_and_removed() {
1077        let tmp = tmp_registry();
1078        let dir = dir_buf(&tmp);
1079        let reg = reg_with_socket(&dir, "renamed", std::process::id());
1080        update_session_name_in("renamed", Some("ambient"), &dir).unwrap();
1081        unregister_owned_in(&reg, &dir);
1082        assert!(
1083            !dir.join("renamed.json").exists(),
1084            "a rename keeps ownership"
1085        );
1086    }
1087
1088    #[cfg(unix)]
1089    #[test]
1090    fn daemon_start_sweep_removes_dead_and_own_pid_and_keeps_other_live_owners() {
1091        let tmp = tmp_registry();
1092        let dir = dir_buf(&tmp);
1093        let own = std::process::id();
1094        // SAFETY: getppid has no preconditions; the parent (the test runner's
1095        // parent shell) is alive and owned by the same user.
1096        let other_live = unsafe { libc::getppid() } as u32;
1097        reg_with_socket(&dir, "dead-owner", 999_999);
1098        reg_with_socket(&dir, "pre-reload", own);
1099        reg_with_socket(&dir, "other-process", other_live);
1100        std::fs::write(dir.join("daemon.json"), r#"{"pid":999999}"#).unwrap();
1101
1102        assert_eq!(sweep_stale_registrations_in(&dir, Some(own)), 2);
1103        for gone in ["dead-owner", "pre-reload"] {
1104            assert!(
1105                !dir.join(format!("{gone}.json")).exists(),
1106                "{gone} card removed"
1107            );
1108            assert!(
1109                !dir.join(format!("{gone}.sock")).exists(),
1110                "{gone} socket removed"
1111            );
1112        }
1113        assert!(dir.join("other-process.json").exists());
1114        assert!(dir.join("other-process.sock").exists());
1115        assert!(
1116            dir.join("daemon.json").exists(),
1117            "daemon.json is never a session"
1118        );
1119    }
1120
1121    #[cfg(unix)]
1122    #[test]
1123    fn sweep_without_own_pid_keeps_registrations_of_this_process() {
1124        let tmp = tmp_registry();
1125        let dir = dir_buf(&tmp);
1126        reg_with_socket(&dir, "mine", std::process::id());
1127        reg_with_socket(&dir, "dead", 999_999);
1128        assert_eq!(sweep_stale_registrations_in(&dir, None), 1);
1129        assert!(dir.join("mine.json").exists());
1130        assert!(!dir.join("dead.json").exists());
1131    }
1132
1133    #[test]
1134    // Keyed to the same lock as every HOME/SYNAPS_BASE_DIR mutator: unkeyed
1135    // `#[serial]` and `#[serial(synaps_base_dir)]` do NOT exclude each other,
1136    // so this base-dir *reader* raced mutator tests that point
1137    // SYNAPS_BASE_DIR at a tempdir under /tmp.
1138    #[serial(synaps_base_dir)]
1139    fn socket_path_format() {
1140        let path = socket_path_for_session("20240101-120000-ab12");
1141        // Sockets now live in the registry dir, not /tmp
1142        assert!(
1143            path.ends_with("/run/20240101-120000-ab12.sock"),
1144            "got: {}",
1145            path
1146        );
1147        assert!(!path.contains("/tmp/"), "socket should not be in /tmp");
1148    }
1149
1150    // Regression: an EFS-backed SYNAPS_BASE_DIR can exceed Linux's 108-byte
1151    // sockaddr_un.sun_path limit. Runtime sockets must be independently rooted.
1152    #[cfg(unix)]
1153    #[test]
1154    fn short_runtime_dirs_keep_long_session_socket_bindable_and_isolated() {
1155        use std::os::unix::net::UnixListener;
1156        let long_session = "s".repeat(80);
1157        // These model deterministic per-UID runtime roots created by guest-agent.
1158        let path_a = socket_path_in_dir(std::path::Path::new("/tmp/a"), &long_session);
1159        let path_b = socket_path_in_dir(std::path::Path::new("/tmp/b"), &long_session);
1160        assert!(path_a.len() < 108, "Unix socket path too long: {path_a}");
1161        assert_ne!(
1162            path_a, path_b,
1163            "per-user runtime roots must isolate same session ids"
1164        );
1165
1166        // Bind under SHORT, /tmp-rooted dirs. The whole point is short socket
1167        // paths; some platforms' tempdir() (e.g. macOS /var/folders/...) is long
1168        // enough to blow the sun_path limit and flake this exact assertion, so we
1169        // control the root length explicitly. Two sibling dirs model two per-uid
1170        // runtime roots.
1171        let pid = std::process::id();
1172        let a = std::path::PathBuf::from(format!("/tmp/sa{pid}"));
1173        let b = std::path::PathBuf::from(format!("/tmp/sb{pid}"));
1174        std::fs::create_dir_all(&a).unwrap();
1175        std::fs::create_dir_all(&b).unwrap();
1176        let _a = UnixListener::bind(socket_path_in_dir(&a, &long_session)).unwrap();
1177        let _b = UnixListener::bind(socket_path_in_dir(&b, &long_session)).unwrap();
1178        let _ = std::fs::remove_dir_all(&a);
1179        let _ = std::fs::remove_dir_all(&b);
1180    }
1181
1182    #[cfg(unix)]
1183    #[test]
1184    fn registration_file_is_0600() {
1185        use std::os::unix::fs::PermissionsExt;
1186        let tmp = tmp_registry();
1187        let dir = dir_buf(&tmp);
1188        let reg = make_reg("perms-check-01", None, std::process::id());
1189        register_session_in(&reg, &dir).unwrap();
1190
1191        let path = dir.join("perms-check-01.json");
1192        let perms = std::fs::metadata(&path).unwrap().permissions();
1193        assert_eq!(perms.mode() & 0o777, 0o600, "registry file should be 0600");
1194    }
1195}
1196
1197// ── daemon paths (Phase 2 B) ──────────────────────────────────────────────────
1198
1199/// Files the `synaps daemon` keeps under `registry_dir()` (0700). One daemon
1200/// per profile: `daemon.{sock,lock,json,pid}` or `daemon-<P>.{…}`.
1201#[derive(Debug, Clone, PartialEq, Eq)]
1202pub struct DaemonPaths {
1203    pub dir: PathBuf,
1204    pub sock: PathBuf,
1205    pub lock: PathBuf,
1206    pub json: PathBuf,
1207    pub pid: PathBuf,
1208}
1209
1210/// Daemon file paths for a profile, under `registry_dir()`.
1211pub fn daemon_paths(profile: Option<&str>) -> DaemonPaths {
1212    daemon_paths_in(&registry_dir(), profile)
1213}
1214
1215/// Same, rooted at an explicit dir (tests; `--socket` overrides only `.sock`).
1216pub fn daemon_paths_in(dir: &std::path::Path, profile: Option<&str>) -> DaemonPaths {
1217    let stem = match profile.map(sanitize_session_id).filter(|p| !p.is_empty()) {
1218        Some(p) => format!("daemon-{p}"),
1219        None => "daemon".to_string(),
1220    };
1221    DaemonPaths {
1222        dir: dir.to_path_buf(),
1223        sock: dir.join(format!("{stem}.sock")),
1224        lock: dir.join(format!("{stem}.lock")),
1225        json: dir.join(format!("{stem}.json")),
1226        pid: dir.join(format!("{stem}.pid")),
1227    }
1228}
1229
1230#[cfg(test)]
1231mod daemon_paths_tests {
1232    use super::*;
1233
1234    #[test]
1235    fn daemon_paths_are_profile_scoped_and_sanitised() {
1236        let d = std::path::Path::new("/run/x");
1237        let p = daemon_paths_in(d, None);
1238        assert_eq!(p.sock, d.join("daemon.sock"));
1239        assert_eq!(p.lock, d.join("daemon.lock"));
1240        assert_eq!(p.json, d.join("daemon.json"));
1241        assert_eq!(p.pid, d.join("daemon.pid"));
1242        let p = daemon_paths_in(d, Some("work/../evil"));
1243        assert_eq!(p.sock, d.join("daemon-work____evil.sock"));
1244    }
1245}