diff --git a/Cargo.toml b/Cargo.toml index b8ac38ca..327d3cbe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] resolver = "3" -members = ["hive-ag3nt", "hive-c0re", "hive-forge", "hive-matrix-mcp", "hive-priv", "hive-sh4re"] +members = ["hive-ag3nt", "hive-c0re", "hive-forge", "hive-matrix-mcp", "hive-sh4re"] [workspace.package] edition = "2024" diff --git a/hive-c0re/src/lib.rs b/hive-c0re/src/lib.rs index d9444f5c..44728f3a 100644 --- a/hive-c0re/src/lib.rs +++ b/hive-c0re/src/lib.rs @@ -39,7 +39,6 @@ pub mod matrix; pub mod meta; pub mod migrate; pub mod operator_questions; -pub mod priv_client; pub mod questions; pub mod rebuild_queue; pub mod reminder_scheduler; diff --git a/hive-c0re/src/priv_client.rs b/hive-c0re/src/priv_client.rs deleted file mode 100644 index 14abf071..00000000 --- a/hive-c0re/src/priv_client.rs +++ /dev/null @@ -1,123 +0,0 @@ -//! Async client for the `hive-priv` privileged-helper socket. -//! -//! Exposes a standalone async function per operation. Each call opens a -//! fresh connection to `/run/hive/priv.sock`, sends one JSON line, reads -//! the response, and closes. Connection-per-call is intentional: priv -//! calls are infrequent (once per rebuild step), so simplicity wins over -//! a persistent connection. - -use anyhow::{Context as _, Result, bail}; -use hive_sh4re::priv_proto::{PRIV_SOCK, BindMount, PrivRequest, PrivResponse}; -use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; -use tokio::net::UnixStream; - -/// Send a single request to `hive-priv` and return the response. -pub async fn call(req: &PrivRequest) -> Result { - let mut stream = UnixStream::connect(PRIV_SOCK) - .await - .context("connect to hive-priv socket")?; - let line = serde_json::to_string(req).context("serialise PrivRequest")? + "\n"; - stream - .write_all(line.as_bytes()) - .await - .context("send request to hive-priv")?; - stream.shutdown().await.context("shutdown write half")?; - let mut resp_line = String::new(); - BufReader::new(stream) - .read_line(&mut resp_line) - .await - .context("read response from hive-priv")?; - serde_json::from_str(&resp_line).context("parse PrivResponse") -} - -pub async fn start_container(name: &str) -> Result<()> { - ok(call(&PrivRequest::StartContainer { name: name.to_owned() }).await?) -} - -pub async fn stop_container(name: &str) -> Result<()> { - ok(call(&PrivRequest::StopContainer { name: name.to_owned() }).await?) -} - -pub async fn kill_container(name: &str) -> Result<()> { - ok(call(&PrivRequest::KillContainer { name: name.to_owned() }).await?) -} - -pub async fn update_container(name: &str) -> Result<(String, String)> { - check(call(&PrivRequest::UpdateContainer { name: name.to_owned() }).await?) -} - -pub async fn create_container(name: &str) -> Result<(String, String)> { - check(call(&PrivRequest::CreateContainer { name: name.to_owned() }).await?) -} - -pub async fn destroy_container(name: &str) -> Result<()> { - ok(call(&PrivRequest::DestroyContainer { name: name.to_owned() }).await?) -} - -pub async fn list_containers() -> Result { - let (stdout, _) = check(call(&PrivRequest::ListContainers).await?)?; - Ok(stdout) -} - -pub async fn write_nspawn_flags(container: &str, binds: &[BindMount]) -> Result<()> { - ok(call(&PrivRequest::WriteNspawnFlags { - container: container.to_owned(), - binds: binds.to_vec(), - }).await?) -} - -pub use hive_sh4re::priv_proto::BindMount; - -pub async fn write_resource_limits( - container: &str, - memory_max: &str, - cpu_quota: &str, -) -> Result<()> { - ok(call(&PrivRequest::WriteResourceLimits { - container: container.to_owned(), - memory_max: memory_max.to_owned(), - cpu_quota: cpu_quota.to_owned(), - }).await?) -} - -pub async fn remove_service_dropin(container: &str) -> Result<()> { - ok(call(&PrivRequest::RemoveServiceDropin { - container: container.to_owned(), - }).await?) -} - -pub async fn daemon_reload() -> Result<()> { - ok(call(&PrivRequest::DaemonReload).await?) -} - -pub async fn reload_gateway_nginx() -> Result<()> { - ok(call(&PrivRequest::ReloadGatewayNginx).await?) -} - -pub async fn chown_socket_dir(agent_name: &str, uid: u32, gid: u32) -> Result<()> { - ok(call(&PrivRequest::ChownSocketDir { - agent_name: agent_name.to_owned(), - uid, - gid, - }).await?) -} - -pub async fn chmod_socket_dir(agent_name: &str, mode: u32) -> Result<()> { - ok(call(&PrivRequest::ChmodSocketDir { - agent_name: agent_name.to_owned(), - mode, - }).await?) -} - -fn check(resp: PrivResponse) -> Result<(String, String)> { - if resp.ok { - Ok((resp.stdout, resp.stderr)) - } else { - bail!("{}", resp.error.as_deref().unwrap_or("hive-priv returned error")) - } -} - -fn ok(resp: PrivResponse) -> Result<()> { - check(resp)?; - Ok(()) -} diff --git a/hive-priv/Cargo.toml b/hive-priv/Cargo.toml deleted file mode 100644 index eccd4f69..00000000 --- a/hive-priv/Cargo.toml +++ /dev/null @@ -1,15 +0,0 @@ -[package] -name = "hive-priv" -edition.workspace = true -version.workspace = true - -[lints] -workspace = true - -[dependencies] -anyhow.workspace = true -hive-sh4re.workspace = true -serde_json.workspace = true -tokio.workspace = true -tracing.workspace = true -tracing-subscriber.workspace = true diff --git a/hive-priv/src/main.rs b/hive-priv/src/main.rs deleted file mode 100644 index 37753711..00000000 --- a/hive-priv/src/main.rs +++ /dev/null @@ -1,409 +0,0 @@ -//! Minimal privileged helper for hive-c0re. -//! -//! Runs as root. Exposes a narrow unix socket at `/run/hive/priv.sock` -//! that accepts `PrivRequest` JSON lines and executes only the -//! operations that genuinely require root. All coordination logic, -//! broker, HTTP, and scheduling stay in the unprivileged hive-c0re -//! process. -//! -//! **Security model**: every request is validated against a strict -//! container-name allowlist before any filesystem or process operation. -//! Only containers whose names match the hive convention (`h-*`, -//! the manager container, or known sibling service containers) are -//! accepted. Every variant maps to a single known operation — no -//! arbitrary command pass-through. -//! -//! **Socket activation**: when systemd passes the listener socket via -//! `LISTEN_FDS=1` + `LISTEN_PID=`, the inherited fd 3 is used -//! instead of binding a fresh socket. - -use std::path::{Path, PathBuf}; - -use anyhow::{Context as _, Result, bail}; -use hive_sh4re::priv_proto::{AGENT_PREFIX, MANAGER_NAME, META_DIR, PRIV_SOCK, SIBLING_CONTAINERS, BindMount, PrivRequest, PrivResponse}; -use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; -use tokio::net::{UnixListener, UnixStream}; -use tokio::process::Command; - -/// Root of the per-agent unix-socket dirs on the host. -const SOCKET_DIR_ROOT: &str = "/run/hive-agent"; - -#[tokio::main] -async fn main() -> Result<()> { - tracing_subscriber::fmt() - .with_env_filter( - tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| "info".into()), - ) - .init(); - - let listener = socket_listener()?; - tracing::info!("hive-priv listening"); - - loop { - match listener.accept().await { - Ok((stream, _)) => { - tokio::spawn(handle(stream)); - } - Err(e) => { - tracing::error!(error = %e, "accept failed"); - } - } - } -} - -fn socket_listener() -> Result { - // Socket activation: systemd passes the socket as fd 3 when - // LISTEN_FDS >= 1 and LISTEN_PID matches our pid. - let listen_fds: Option = std::env::var("LISTEN_FDS") - .ok() - .and_then(|s| s.parse().ok()); - let listen_pid: Option = std::env::var("LISTEN_PID") - .ok() - .and_then(|s| s.parse().ok()); - - if let (Some(n), Some(p)) = (listen_fds, listen_pid) { - if n >= 1 && p == std::process::id() { - // SAFETY: systemd has passed us a ready UnixListener on fd 3. - let std_listener = unsafe { - use std::os::unix::io::FromRawFd; - std::os::unix::net::UnixListener::from_raw_fd(3) - }; - std_listener - .set_nonblocking(true) - .context("set socket non-blocking")?; - let listener = - tokio::net::UnixListener::from_std(std_listener).context("wrap systemd socket")?; - tracing::info!("using systemd-activated socket"); - return Ok(listener); - } - } - - // Fallback: bind the socket ourselves. - let path = Path::new(PRIV_SOCK); - if let Some(parent) = path.parent() { - std::fs::create_dir_all(parent) - .with_context(|| format!("create {}", parent.display()))?; - } - let _ = std::fs::remove_file(path); - let listener = UnixListener::bind(path).with_context(|| format!("bind {PRIV_SOCK}"))?; - // Mode 0660: only the hive-core group can connect. - use std::os::unix::fs::PermissionsExt as _; - std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o660)) - .context("chmod priv.sock")?; - tracing::info!(path = PRIV_SOCK, "bound priv socket"); - Ok(listener) -} - -async fn handle(stream: UnixStream) { - let (reader, mut writer) = stream.into_split(); - let mut lines = BufReader::new(reader).lines(); - while let Ok(Some(line)) = lines.next_line().await { - let resp = dispatch(&line).await; - let mut json = serde_json::to_string(&resp).unwrap_or_else(|e| { - format!("{{\"ok\":false,\"stdout\":\"\",\"stderr\":\"\",\"error\":\"serialise failed: {e}\"}}") - }); - json.push('\n'); - if let Err(e) = writer.write_all(json.as_bytes()).await { - tracing::warn!(error = %e, "write response failed"); - break; - } - } -} - -async fn dispatch(line: &str) -> PrivResponse { - match serde_json::from_str::(line) { - Ok(req) => match exec(req).await { - Ok((stdout, stderr)) => PrivResponse { - ok: true, - stdout, - stderr, - error: None, - }, - Err(e) => PrivResponse { - ok: false, - stdout: String::new(), - stderr: String::new(), - error: Some(format!("{e:#}")), - }, - }, - Err(e) => PrivResponse { - ok: false, - stdout: String::new(), - stderr: String::new(), - error: Some(format!("parse request: {e}")), - }, - } -} - -/// Execute a validated `PrivRequest`. Returns `(stdout, stderr)` on success. -async fn exec(req: PrivRequest) -> Result<(String, String)> { - match req { - PrivRequest::StartContainer { ref name } => { - validate_container_name(name)?; - container_run(&["start", &container_system_name(name)]).await - } - - PrivRequest::StopContainer { ref name } => { - validate_container_name(name)?; - container_run(&["stop", &container_system_name(name)]).await - } - - PrivRequest::KillContainer { ref name } => { - validate_container_name(name)?; - container_run(&["kill", &container_system_name(name)]).await - } - - PrivRequest::UpdateContainer { ref name } => { - validate_container_name(name)?; - let flake_ref = agent_flake_ref(name); - container_run(&["update", &container_system_name(name), "--flake", &flake_ref]).await - } - - PrivRequest::CreateContainer { ref name } => { - validate_container_name(name)?; - let flake_ref = agent_flake_ref(name); - container_run(&["create", &container_system_name(name), "--flake", &flake_ref]).await - } - - PrivRequest::DestroyContainer { ref name } => { - validate_container_name(name)?; - container_run(&["destroy", &container_system_name(name)]).await - } - - PrivRequest::ListContainers => container_run(&["list"]).await, - - PrivRequest::WriteNspawnFlags { ref container, ref binds } => { - validate_container_system_name(container)?; - for bind in binds { - validate_bind_path(&bind.host_path)?; - validate_bind_path(&bind.container_path)?; - } - write_nspawn_flags(container, binds)?; - Ok((String::new(), String::new())) - } - - PrivRequest::WriteResourceLimits { - ref container, - ref memory_max, - ref cpu_quota, - } => { - validate_container_system_name(container)?; - let dir = format!("/run/systemd/system/container@{container}.service.d"); - std::fs::create_dir_all(&dir).with_context(|| format!("create {dir}"))?; - let path = format!("{dir}/hyperhive-limits.conf"); - let content = format!("[Service]\nMemoryMax={memory_max}\nCPUQuota={cpu_quota}\n"); - std::fs::write(&path, content).with_context(|| format!("write {path}"))?; - Ok((String::new(), String::new())) - } - - PrivRequest::RemoveServiceDropin { ref container } => { - validate_container_system_name(container)?; - let dir = format!("/run/systemd/system/container@{container}.service.d"); - if Path::new(&dir).exists() { - std::fs::remove_dir_all(&dir) - .with_context(|| format!("remove {dir}"))?; - } - Ok((String::new(), String::new())) - } - - PrivRequest::DaemonReload => { - let out = Command::new("systemctl") - .arg("daemon-reload") - .output() - .await - .context("invoke systemctl daemon-reload")?; - if !out.status.success() { - bail!( - "systemctl daemon-reload failed ({}): {}", - out.status, - String::from_utf8_lossy(&out.stderr).trim() - ); - } - Ok((String::new(), String::new())) - } - - PrivRequest::ReloadGatewayNginx => { - let out = Command::new("systemd-run") - .args(["--machine=hive-gateway", "--quiet", "--", "nginx", "-s", "reload"]) - .output() - .await - .context("invoke systemd-run for gateway nginx reload")?; - if !out.status.success() { - bail!( - "gateway nginx reload failed ({}): {}", - out.status, - String::from_utf8_lossy(&out.stderr).trim() - ); - } - Ok((String::new(), String::new())) - } - - PrivRequest::ChownSocketDir { ref agent_name, uid, gid } => { - validate_agent_name(agent_name)?; - let path = socket_dir_path(agent_name); - std::os::unix::fs::chown(&path, Some(uid), Some(gid)) - .with_context(|| format!("chown {} to {uid}:{gid}", path.display()))?; - Ok((String::new(), String::new())) - } - - PrivRequest::ChmodSocketDir { ref agent_name, mode } => { - validate_agent_name(agent_name)?; - let path = socket_dir_path(agent_name); - use std::os::unix::fs::PermissionsExt as _; - std::fs::set_permissions(&path, std::fs::Permissions::from_mode(mode)) - .with_context(|| format!("chmod {:o} {}", mode, path.display()))?; - Ok((String::new(), String::new())) - } - } -} - -/// Invoke `nixos-container` with the given args, log output to journald. -async fn container_run(args: &[&str]) -> Result<(String, String)> { - let out = Command::new("nixos-container") - .args(args) - .output() - .await - .context("invoke nixos-container")?; - let stdout = String::from_utf8_lossy(&out.stdout).into_owned(); - let stderr = String::from_utf8_lossy(&out.stderr).into_owned(); - for line in stdout.lines() { - tracing::info!(target: "nixos-container", "{line}"); - } - for line in stderr.lines() { - tracing::warn!(target: "nixos-container", "{line}"); - } - if !out.status.success() { - bail!( - "nixos-container {} failed ({}): {}", - args.join(" "), - out.status, - stderr.trim() - ); - } - Ok((stdout, stderr)) -} - -/// Return the system container name for a logical agent name. -/// Manager (`MANAGER_NAME`) passes through; sub-agents get `h-` prefix. -fn container_system_name(name: &str) -> String { - if name == MANAGER_NAME { - name.to_owned() - } else { - format!("{AGENT_PREFIX}{name}") - } -} - -/// Path of the per-agent unix-socket dir on the host. -fn socket_dir_path(agent_name: &str) -> PathBuf { - PathBuf::from(format!("{SOCKET_DIR_ROOT}/{agent_name}")) -} - -/// Validate a logical agent name (the name hive-c0re uses internally, -/// before the `h-` container prefix is applied). -fn validate_agent_name(name: &str) -> Result<()> { - if name == MANAGER_NAME { - return Ok(()); - } - validate_name_chars(name)?; - Ok(()) -} - -/// Validate a logical agent name and check it maps to a hive-managed container. -fn validate_container_name(name: &str) -> Result<()> { - if name == MANAGER_NAME { - return Ok(()); - } - if SIBLING_CONTAINERS.contains(&name) { - return Ok(()); - } - validate_name_chars(name)?; - Ok(()) -} - -/// Validate a system-level container name (already has `h-` prefix for -/// sub-agents, or is the manager name / sibling service name). -fn validate_container_system_name(name: &str) -> Result<()> { - if name == MANAGER_NAME { - return Ok(()); - } - if SIBLING_CONTAINERS.contains(&name) { - return Ok(()); - } - if let Some(suffix) = name.strip_prefix(AGENT_PREFIX) { - validate_name_chars(suffix)?; - return Ok(()); - } - bail!("container name {name:?} is not managed by hive"); -} - -fn validate_name_chars(name: &str) -> Result<()> { - if name.is_empty() - || !name - .chars() - .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') - { - bail!("invalid name {name:?}: must be non-empty lowercase ascii + digits + hyphens"); - } - Ok(()) -} - -/// Derive the meta-flake ref for an agent by name. -fn agent_flake_ref(name: &str) -> String { - format!("{META_DIR}#{name}") -} - -/// Validate a bind-mount path: must be absolute, non-empty, and contain -/// no newlines, null bytes, or double-quotes (which would break the -/// `EXTRA_NSPAWN_FLAGS="..."` conf line format). -fn validate_bind_path(path: &str) -> Result<()> { - if path.is_empty() - || !path.starts_with('/') - || path.bytes().any(|b| b == 0 || b == b'\n' || b == b'"' || b == b':') - { - bail!( - "invalid bind path {path:?}: must be an absolute path with no colons, newlines, null bytes, or double-quotes" - ); - } - Ok(()) -} - -/// Update `/etc/nixos-containers/.conf`: strips network-isolation -/// vars (`PRIVATE_NETWORK`, `HOST_ADDRESS*`, `LOCAL_ADDRESS*`, `HOST_BRIDGE`, -/// `EXTRA_NSPAWN_FLAGS`), forces `PRIVATE_NETWORK=0` and blank network vars, -/// then appends `EXTRA_NSPAWN_FLAGS=""`. -fn write_nspawn_flags(container: &str, binds: &[BindMount]) -> Result<()> { - let path = format!("/etc/nixos-containers/{container}.conf"); - let original = std::fs::read_to_string(&path) - .with_context(|| format!("read {path}"))?; - let mut lines: Vec<&str> = original - .lines() - .filter(|line| { - let t = line.trim_start(); - !t.starts_with("EXTRA_NSPAWN_FLAGS=") - && !t.starts_with("PRIVATE_NETWORK=") - && !t.starts_with("HOST_ADDRESS=") - && !t.starts_with("LOCAL_ADDRESS=") - && !t.starts_with("HOST_ADDRESS6=") - && !t.starts_with("LOCAL_ADDRESS6=") - && !t.starts_with("HOST_BRIDGE=") - }) - .collect(); - let mut out = lines.join("\n"); - if !out.is_empty() { - out.push('\n'); - } - out.push_str("PRIVATE_NETWORK=0\n"); - out.push_str("HOST_ADDRESS=\n"); - out.push_str("LOCAL_ADDRESS=\n"); - out.push_str("HOST_ADDRESS6=\n"); - out.push_str("LOCAL_ADDRESS6=\n"); - out.push_str("HOST_BRIDGE=\n"); - let flags: Vec = binds.iter().map(|b| { - let flag = if b.read_only { "--bind-ro" } else { "--bind" }; - format!("{flag}={}:{}", b.host_path, b.container_path) - }).collect(); - let flags_joined = flags.join(" "); - out.push_str(&format!("EXTRA_NSPAWN_FLAGS=\"{flags_joined}\"\n")); - std::fs::write(&path, out).with_context(|| format!("write {path}")) -} diff --git a/hive-sh4re/src/lib.rs b/hive-sh4re/src/lib.rs index 58c8dc8c..89c89140 100644 --- a/hive-sh4re/src/lib.rs +++ b/hive-sh4re/src/lib.rs @@ -3,7 +3,6 @@ use serde::{Deserialize, Serialize}; pub mod assets; -pub mod priv_proto; // ----------------------------------------------------------------------------- // Host admin socket — /run/hyperhive/host.sock diff --git a/hive-sh4re/src/priv_proto.rs b/hive-sh4re/src/priv_proto.rs deleted file mode 100644 index 37ebf686..00000000 --- a/hive-sh4re/src/priv_proto.rs +++ /dev/null @@ -1,119 +0,0 @@ -//! Wire types for the `hive-priv` privileged-helper socket. -//! -//! Both `hive-priv` (server) and `hive-c0re` (client via `priv_client`) -//! import these so the shapes stay in sync. - -use serde::{Deserialize, Serialize}; - -/// Default socket path for the privileged helper. -pub const PRIV_SOCK: &str = "/run/hive/priv.sock"; - -/// Manager container name. Used by `hive-priv` to skip the `h-` prefix -/// and by `hive-c0re` for identity checks. -pub const MANAGER_NAME: &str = "root"; - -/// Sub-agent container prefix. System container name = `h-`. -pub const AGENT_PREFIX: &str = "h-"; - -/// Sibling service containers managed by hive-c0re. -pub const SIBLING_CONTAINERS: &[&str] = &["hive-forge", "hive-matrix", "hive-gateway"]; - -/// Host path of the meta flake. The flake ref for agent `` is -/// `{META_DIR}#{name}`, derived by `hive-priv` — never passed over the wire. -pub const META_DIR: &str = "/var/lib/hyperhive/meta"; - -/// One bind-mount entry for `WriteNspawnFlags`. -/// hive-priv constructs `--bind=:` (or `--bind-ro=`) -/// and validates both paths before writing the conf file. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BindMount { - pub host_path: String, - pub container_path: String, - pub read_only: bool, -} - -/// A request to the privileged helper. -/// -/// Wire format: one JSON object per line over `/run/hive/priv.sock`. -/// Every variant is a specific known operation — no pass-through -/// shell commands or arbitrary paths. New privileged ops get new -/// variants. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "op", rename_all = "snake_case")] -pub enum PrivRequest { - // --- Container lifecycle --- - - /// `nixos-container start ` - StartContainer { name: String }, - - /// `nixos-container stop ` - StopContainer { name: String }, - - /// `nixos-container kill ` - KillContainer { name: String }, - - /// `nixos-container update --flake ` - /// The flake ref is derived from `name` by hive-priv. - UpdateContainer { name: String }, - - /// `nixos-container create --flake ` - /// The flake ref is derived from `name` by hive-priv. - CreateContainer { name: String }, - - /// `nixos-container destroy ` - DestroyContainer { name: String }, - - /// `nixos-container list` - ListContainers, - - // --- Config file writes --- - - /// Update `/etc/nixos-containers/.conf`: strip network-isolation - /// vars, force `PRIVATE_NETWORK=0`, and set `EXTRA_NSPAWN_FLAGS` from the - /// provided bind-mount list. Written by `lifecycle::set_nspawn_flags`. - WriteNspawnFlags { container: String, binds: Vec }, - - /// Write `/run/systemd/system/container@.service.d/hyperhive-limits.conf` - /// with `[Service]\nMemoryMax=\nCPUQuota=\n`. - /// Written by `lifecycle::set_resource_limits`. - WriteResourceLimits { - container: String, - memory_max: String, - cpu_quota: String, - }, - - /// Remove `/run/systemd/system/container@.service.d/` if present. - /// Called by `lifecycle::destroy` to clean up the resource-limits drop-in. - RemoveServiceDropin { container: String }, - - // --- System --- - - /// Run `systemctl daemon-reload`. - DaemonReload, - - /// Reload nginx inside the `hive-gateway` container via - /// `systemd-run --machine=hive-gateway nginx -s reload`. - ReloadGatewayNginx, - - // --- Socket dir ownership --- - - /// Set ownership of `/run/hive-agent//` to `uid:gid`. - /// Called by `lifecycle::set_nspawn_flags` after `create_dir_all`. - ChownSocketDir { agent_name: String, uid: u32, gid: u32 }, - - /// Set mode of `/run/hive-agent//`. - /// Fallback when uid lookup returns `None` on first spawn. - ChmodSocketDir { agent_name: String, mode: u32 }, -} - -/// Response from the privileged helper. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PrivResponse { - pub ok: bool, - #[serde(default)] - pub stdout: String, - #[serde(default)] - pub stderr: String, - #[serde(skip_serializing_if = "Option::is_none")] - pub error: Option, -}