Skip to main content

omni_dev/daemon/
paths.rs

1//! Per-user runtime paths for the daemon (control socket, token file) and the
2//! filesystem-permission helpers that keep them owner-private.
3//!
4//! The runtime directory is resolved via [`dirs::data_dir`] —
5//! `~/Library/Application Support/omni-dev/` on macOS, `~/.local/share/omni-dev/`
6//! on Linux. This deliberately diverges from the `~/.omni-dev` config-file
7//! convention used by [`crate::claude::context`]: that governs user-editable
8//! configuration, whereas these are *runtime* artifacts of a long-lived
9//! menu-bar app, for which the platform data directory is the native home.
10//! See ADR-0039.
11
12use std::path::{Path, PathBuf};
13
14use anyhow::{bail, Context, Result};
15
16/// macOS caps a `sockaddr_un` path at 104 bytes (including the NUL
17/// terminator); Linux allows 108. Use the smaller, portable bound.
18pub const MAX_SOCKET_PATH_LEN: usize = 104;
19
20/// The per-user runtime directory holding the daemon's socket and token file.
21pub fn runtime_dir() -> Result<PathBuf> {
22    let base = dirs::data_dir().context("could not determine the user data directory")?;
23    Ok(base.join("omni-dev"))
24}
25
26/// Default control-socket path: `<runtime_dir>/daemon.sock`.
27pub fn socket_path() -> Result<PathBuf> {
28    Ok(runtime_dir()?.join("daemon.sock"))
29}
30
31/// Default bridge token-file path: `<runtime_dir>/bridge.token`. Thin clients
32/// (`request`/`harvest`) fall back to this when no `--token-file`/env is set.
33pub fn token_path() -> Result<PathBuf> {
34    Ok(runtime_dir()?.join("bridge.token"))
35}
36
37/// The bridge token file co-located with a control socket
38/// (`<socket dir>/bridge.token`), so a custom `--socket` keeps its token beside
39/// it. For the default socket this equals [`token_path`].
40pub fn token_path_for_socket(socket: &Path) -> PathBuf {
41    socket.parent().map_or_else(
42        || PathBuf::from("bridge.token"),
43        |dir| dir.join("bridge.token"),
44    )
45}
46
47/// Creates `dir` (and ancestors) if absent and tightens it to owner-only
48/// (`0700`) on Unix.
49pub fn ensure_dir_0700(dir: &Path) -> Result<()> {
50    std::fs::create_dir_all(dir)
51        .with_context(|| format!("failed to create directory {}", dir.display()))?;
52    #[cfg(unix)]
53    {
54        use std::os::unix::fs::PermissionsExt;
55        std::fs::set_permissions(dir, std::fs::Permissions::from_mode(0o700))
56            .with_context(|| format!("failed to set 0700 on {}", dir.display()))?;
57    }
58    Ok(())
59}
60
61/// Tightens an existing file to owner read/write only (`0600`) on Unix.
62pub fn set_file_0600(path: &Path) -> Result<()> {
63    #[cfg(unix)]
64    {
65        use std::os::unix::fs::PermissionsExt;
66        std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))
67            .with_context(|| format!("failed to set 0600 on {}", path.display()))?;
68    }
69    #[cfg(not(unix))]
70    {
71        let _ = path;
72    }
73    Ok(())
74}
75
76/// Fails closed if the socket path is too long for the platform's
77/// `sockaddr_un`, with an actionable message instead of an opaque bind error.
78pub fn check_socket_path_len(path: &Path) -> Result<()> {
79    let len = path.as_os_str().len();
80    if len >= MAX_SOCKET_PATH_LEN {
81        bail!(
82            "socket path is {len} bytes, exceeding the {MAX_SOCKET_PATH_LEN}-byte limit: {} — \
83             pass a shorter --socket path",
84            path.display()
85        );
86    }
87    Ok(())
88}
89
90#[cfg(test)]
91#[allow(clippy::unwrap_used, clippy::expect_used)]
92mod tests {
93    use super::*;
94
95    #[test]
96    fn default_paths_sit_under_the_runtime_dir() {
97        let rt = runtime_dir().unwrap();
98        assert!(socket_path().unwrap().starts_with(&rt));
99        assert!(token_path().unwrap().starts_with(&rt));
100    }
101
102    #[test]
103    fn token_sits_beside_its_socket() {
104        assert_eq!(
105            token_path_for_socket(Path::new("/tmp/x/daemon.sock")),
106            Path::new("/tmp/x/bridge.token")
107        );
108        // A bare filename (parent is the empty path) still yields a token name.
109        assert_eq!(
110            token_path_for_socket(Path::new("daemon.sock")),
111            Path::new("bridge.token")
112        );
113    }
114
115    #[test]
116    fn socket_path_length_guard() {
117        check_socket_path_len(Path::new("/tmp/short.sock")).unwrap();
118        let too_long = PathBuf::from(format!("/{}", "a".repeat(MAX_SOCKET_PATH_LEN)));
119        assert!(check_socket_path_len(&too_long).is_err());
120    }
121
122    #[test]
123    fn ensure_dir_and_file_perms() {
124        let dir = tempfile::tempdir().unwrap();
125        let sub = dir.path().join("run");
126        ensure_dir_0700(&sub).unwrap();
127        assert!(sub.is_dir());
128        let file = sub.join("k");
129        std::fs::write(&file, "x").unwrap();
130        set_file_0600(&file).unwrap();
131        #[cfg(unix)]
132        {
133            use std::os::unix::fs::PermissionsExt;
134            assert_eq!(
135                std::fs::metadata(&sub).unwrap().permissions().mode() & 0o777,
136                0o700
137            );
138            assert_eq!(
139                std::fs::metadata(&file).unwrap().permissions().mode() & 0o777,
140                0o600
141            );
142        }
143    }
144}