fix(#999): resolve all clippy warnings across the workspace

All crates now pass `cargo clippy --workspace -- -D warnings` cleanly.

Fixes span six crates (hive-sh4re, hive-ag3nt, hive-c0re, hive-forge,
hive-priv, hive-matrix-mcp was already clean):

- doc_markdown: wrap snake_case, type names, constants in backticks
- collapsible_if / collapsible_match: fold nested ifs into let-chains
- duration_suboptimal_units: Duration::from_secs(N) → from_mins/from_hours
- implicit_hasher: allow on HashMap-param fns where generalization is risky
- items_after_statements: hoist use to function tops
- map(f).unwrap_or(x) → map_or(x, f); map(f).unwrap_or_else(g) → map_or_else
- is_ok_and / is_none_or in place of map().unwrap_or(bool)
- needless_continue: {} instead of continue in loop match arms
- match_same_arms: Ok(None) | Err(_) merged
- format_push_str: write!() instead of push_str(&format!())
- while let replaces loop { let Some(..) = x else { break } }
- struct_excessive_bools / dead_code: allow on purpose-built structs
- too_many_lines / too_many_arguments: allow where refactor not worth it
- unused_async: remove async from poll_once in bash_runner
- needless_borrow: fix &repo deref in hive-forge comments verb
- cast_possible_truncation: allow u64→usize in fetch_tail

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
atlas 2026-06-01 22:02:21 +02:00 committed by mara
commit 5c5ca38fe8
32 changed files with 121 additions and 127 deletions

View file

@ -229,7 +229,7 @@ async fn run_loop(socket: PathBuf) {
let claimed: Arc<Mutex<HashSet<String>>> = Arc::new(Mutex::new(HashSet::new())); let claimed: Arc<Mutex<HashSet<String>>> = Arc::new(Mutex::new(HashSet::new()));
loop { loop {
poll_once(&socket, &claimed).await; poll_once(&socket, &claimed);
tokio::time::sleep(POLL_INTERVAL).await; tokio::time::sleep(POLL_INTERVAL).await;
} }
} }
@ -258,7 +258,7 @@ async fn mark_interrupted(socket: &Path) {
} }
} }
async fn poll_once(socket: &Path, claimed: &Arc<Mutex<HashSet<String>>>) { fn poll_once(socket: &Path, claimed: &Arc<Mutex<HashSet<String>>>) {
let Ok(rd) = std::fs::read_dir(tasks_dir()) else { return }; let Ok(rd) = std::fs::read_dir(tasks_dir()) else { return };
for entry in rd.flatten() { for entry in rd.flatten() {
let path = entry.path(); let path = entry.path();

View file

@ -117,7 +117,7 @@ async fn main() -> Result<()> {
/// Surface a `SYSTEM_SENDER` message in the live event bus + tracing /// Surface a `SYSTEM_SENDER` message in the live event bus + tracing
/// log. Both agents and the manager receive `QuestionAnswered`, /// log. Both agents and the manager receive `QuestionAnswered`,
/// `ContainerCrash`, reparent notifications, and friends; the parse /// `ContainerCrash`, reparent notifications, and friends; the parse
/// + log path is identical. Quiet no-op when `from` isn't /// and log path is identical. Quiet no-op when `from` isn't
/// `SYSTEM_SENDER`. /// `SYSTEM_SENDER`.
fn log_system_event(bus: &Bus, from: &str, body: &str) { fn log_system_event(bus: &Bus, from: &str, body: &str) {
if from != SYSTEM_SENDER { if from != SYSTEM_SENDER {
@ -165,7 +165,7 @@ fn consume_continue_sentinel() -> bool {
/// What a `Recv` long-poll returned. Decoupled from the per-role /// What a `Recv` long-poll returned. Decoupled from the per-role
/// Response enum so `serve_loop` can pattern-match without seeing /// Response enum so `serve_loop` can pattern-match without seeing
/// either AgentResponse or ManagerResponse directly. /// either `AgentResponse` or `ManagerResponse` directly.
enum RecvOutcome { enum RecvOutcome {
/// Long-poll returned at least one message; first one is detached. /// Long-poll returned at least one message; first one is detached.
Message(hive_sh4re::DeliveredMessage), Message(hive_sh4re::DeliveredMessage),
@ -212,7 +212,7 @@ trait Surface {
fn send_to_parent(socket: &Path, body: String) -> impl Future<Output = ()>; fn send_to_parent(socket: &Path, body: String) -> impl Future<Output = ()>;
/// Fire a `Wake { from: "self", body: "continue" }` at our own /// Fire a `Wake { from: "self", body: "continue" }` at our own
/// inbox — the request_next_turn sentinel pickup. /// inbox — the `request_next_turn` sentinel pickup.
fn self_wake(socket: &Path) -> impl Future<Output = ()>; fn self_wake(socket: &Path) -> impl Future<Output = ()>;
/// Long-poll the broker for the next message. Wraps the /// Long-poll the broker for the next message. Wraps the

View file

@ -80,12 +80,12 @@ fn harness_json_path() -> PathBuf {
fn read_harness_state() -> (bool, bool) { fn read_harness_state() -> (bool, bool) {
// Try the new consolidated file first. // Try the new consolidated file first.
if let Ok(raw) = std::fs::read_to_string(harness_json_path()) { if let Ok(raw) = std::fs::read_to_string(harness_json_path())
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&raw) { && let Ok(v) = serde_json::from_str::<serde_json::Value>(&raw)
let rate_limited = v.get("rate_limited").and_then(|x| x.as_bool()).unwrap_or(false); {
let needs_login = v.get("needs_login").and_then(|x| x.as_bool()).unwrap_or(false); let rate_limited = v.get("rate_limited").and_then(serde_json::Value::as_bool).unwrap_or(false);
return (rate_limited, needs_login); let needs_login = v.get("needs_login").and_then(serde_json::Value::as_bool).unwrap_or(false);
} return (rate_limited, needs_login);
} }
// Fall back to legacy sentinel files written by older harness builds. // Fall back to legacy sentinel files written by older harness builds.
let state_dir = crate::paths::state_dir(); let state_dir = crate::paths::state_dir();

View file

@ -88,8 +88,7 @@ pub async fn run(socket: PathBuf) {
// HIVE_FORGE_KEEP_SUBSCRIPTIONS=1 disables auto-unsubscribe for agents // HIVE_FORGE_KEEP_SUBSCRIPTIONS=1 disables auto-unsubscribe for agents
// that intentionally consume the full repo notification firehose (e.g. triage). // that intentionally consume the full repo notification firehose (e.g. triage).
let keep_subscriptions = std::env::var("HIVE_FORGE_KEEP_SUBSCRIPTIONS") let keep_subscriptions = std::env::var("HIVE_FORGE_KEEP_SUBSCRIPTIONS")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true")) .is_ok_and(|v| v == "1" || v.eq_ignore_ascii_case("true"));
.unwrap_or(false);
// Optional reason drop-list — comma-separated Forgejo `reason` // Optional reason drop-list — comma-separated Forgejo `reason`
// values to silently mark-read instead of deliver. See // values to silently mark-read instead of deliver. See
@ -159,7 +158,7 @@ fn notif_type_label(t: &str) -> &str {
/// inside the forge-notify wrapper, so a leading `## title` line /// inside the forge-notify wrapper, so a leading `## title` line
/// doesn't blow into an h2 in the dashboard render. See /// doesn't blow into an h2 in the dashboard render. See
/// `docs/forge.md::Body excerpt + truncation + heading escape` for /// `docs/forge.md::Body excerpt + truncation + heading escape` for
/// the strict-ATX-vs-`#tag` rationale and the split_inclusive /// the strict-ATX-vs-`#tag` rationale and the `split_inclusive`
/// trailing-newline contract. /// trailing-newline contract.
fn escape_md_headings(body: &str) -> String { fn escape_md_headings(body: &str) -> String {
let mut out = String::with_capacity(body.len()); let mut out = String::with_capacity(body.len());

View file

@ -448,20 +448,20 @@ fn format_bash_status(id: &str) -> String {
let age = crate::serve_common::now_unix() - t; let age = crate::serve_common::now_unix() - t;
let _ = write!(out, ", running for {age}s"); let _ = write!(out, ", running for {age}s");
} }
if let Some(t) = task.completed_at { if let Some(t) = task.completed_at
if let Some(s) = task.started_at { && let Some(s) = task.started_at
let _ = write!(out, ", took {}s", t - s); {
} let _ = write!(out, ", took {}s", t - s);
} }
if let Some(ref stdout) = task.stdout_tail { if let Some(ref stdout) = task.stdout_tail
if !stdout.trim().is_empty() { && !stdout.trim().is_empty()
let _ = write!(out, "\n\nstdout:\n```\n{}\n```", stdout.trim()); {
} let _ = write!(out, "\n\nstdout:\n```\n{}\n```", stdout.trim());
} }
if let Some(ref stderr) = task.stderr_tail { if let Some(ref stderr) = task.stderr_tail
if !stderr.trim().is_empty() { && !stderr.trim().is_empty()
let _ = write!(out, "\n\nstderr:\n```\n{}\n```", stderr.trim()); {
} let _ = write!(out, "\n\nstderr:\n```\n{}\n```", stderr.trim());
} }
out out
} }
@ -1913,14 +1913,14 @@ pub enum Flavor {
} }
/// Env var written by the meta renderer with a comma-separated list of /// Env var written by the meta renderer with a comma-separated list of
/// `hive_sh4re::ToolGroup` snake_case names (e.g. `"messaging,inbox,meta"`). /// `hive_sh4re::ToolGroup` `snake_case` names (e.g. `"messaging,inbox,meta"`).
/// When present, the harness expands the groups into per-tool allow entries /// When present, the harness expands the groups into per-tool allow entries
/// instead of using the hardcoded flavor default. See `docs/conventions.md::Tool groups`. /// instead of using the hardcoded flavor default. See `docs/conventions.md::Tool groups`.
const TOOL_GROUPS_ENV: &str = "HIVE_TOOL_GROUPS"; const TOOL_GROUPS_ENV: &str = "HIVE_TOOL_GROUPS";
/// `HIVE_CAPABILITIES` env var injected by `meta::render_flake` when the /// `HIVE_CAPABILITIES` env var injected by `meta::render_flake` when the
/// operator grants capabilities to this agent. Comma-separated /// operator grants capabilities to this agent. Comma-separated
/// `hive_sh4re::Capability` snake_case names. Absent = no extra capabilities. /// `hive_sh4re::Capability` `snake_case` names. Absent = no extra capabilities.
const CAPABILITIES_ENV: &str = "HIVE_CAPABILITIES"; const CAPABILITIES_ENV: &str = "HIVE_CAPABILITIES";
/// Returns the MCP tool names (without `mcp__hyperhive__` prefix) that are /// Returns the MCP tool names (without `mcp__hyperhive__` prefix) that are
@ -1976,14 +1976,12 @@ fn effective_tool_groups(flavor: Flavor) -> Vec<hive_sh4re::ToolGroup> {
for token in raw.split(',') { for token in raw.split(',') {
let t = token.trim().to_ascii_lowercase(); let t = token.trim().to_ascii_lowercase();
// Parse via serde_json (the canonical deserialization path). // Parse via serde_json (the canonical deserialization path).
match serde_json::from_value::<hive_sh4re::ToolGroup>( if let Ok(g) = serde_json::from_value::<hive_sh4re::ToolGroup>(
serde_json::Value::String(t.clone()), serde_json::Value::String(t.clone()),
) { ) {
Ok(g) => groups.push(g), groups.push(g);
Err(_) => tracing::warn!( } else {
token = %t, tracing::warn!(token = %t, "{TOOL_GROUPS_ENV}: unknown tool group, skipping");
"{TOOL_GROUPS_ENV}: unknown tool group, skipping"
),
} }
} }
if groups.is_empty() { if groups.is_empty() {

View file

@ -423,8 +423,7 @@ fn maybe_auto_reset(bus: &Bus) {
// Compute idle seconds using the same clock as now_unix (unix epoch, i64). // Compute idle seconds using the same clock as now_unix (unix epoch, i64).
let now = std::time::SystemTime::now() let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH) .duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs()) .map_or(0, |d| d.as_secs());
.unwrap_or(0);
let idle_secs = now.saturating_sub(u64::try_from(last_ended).unwrap_or(0)); let idle_secs = now.saturating_sub(u64::try_from(last_ended).unwrap_or(0));
let ttl = cache_ttl_secs(); let ttl = cache_ttl_secs();
if idle_secs < ttl { if idle_secs < ttl {
@ -482,7 +481,7 @@ pub fn emit_turn_end(bus: &Bus, outcome: &TurnOutcome) {
/// snapshot advances (mtime OR file-count change), avoiding the /// snapshot advances (mtime OR file-count change), avoiding the
/// infinite-401 loop a bare-existence check would produce when stale /// infinite-401 loop a bare-existence check would produce when stale
/// credentials are already on disk. Mtime-snapshot resumption rationale /// credentials are already on disk. Mtime-snapshot resumption rationale
/// + DirSnapshot two-axis design: see /// and `DirSnapshot` two-axis design: see
/// [`docs/turn-loop.md::The loop`](../../docs/turn-loop.md). /// [`docs/turn-loop.md::The loop`](../../docs/turn-loop.md).
/// ///
/// # Panics /// # Panics

View file

@ -173,6 +173,7 @@ pub async fn serve(
/// Marker-gating + the gateway-side consumer: see /// Marker-gating + the gateway-side consumer: see
/// [`docs/gateway.md::Per-agent unix-socket upstream`](../../../docs/gateway.md). /// [`docs/gateway.md::Per-agent unix-socket upstream`](../../../docs/gateway.md).
fn bind_unix(path: &Path) -> Result<tokio::net::UnixListener> { fn bind_unix(path: &Path) -> Result<tokio::net::UnixListener> {
use std::os::unix::fs::PermissionsExt;
if let Some(parent) = path.parent() { if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent) std::fs::create_dir_all(parent)
.with_context(|| format!("create socket parent dir {}", parent.display()))?; .with_context(|| format!("create socket parent dir {}", parent.display()))?;
@ -183,7 +184,6 @@ fn bind_unix(path: &Path) -> Result<tokio::net::UnixListener> {
let _ = std::fs::remove_file(path); let _ = std::fs::remove_file(path);
let listener = tokio::net::UnixListener::bind(path) let listener = tokio::net::UnixListener::bind(path)
.with_context(|| format!("bind unix socket at {}", path.display()))?; .with_context(|| format!("bind unix socket at {}", path.display()))?;
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o666)) std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o666))
.with_context(|| format!("set perms on {}", path.display()))?; .with_context(|| format!("set perms on {}", path.display()))?;
// Best-effort ready marker: failed write isn't fatal (the harness // Best-effort ready marker: failed write isn't fatal (the harness
@ -319,11 +319,10 @@ async fn relay_ws_vnc(socket: axum::extract::ws::WebSocket, vnc_port: u16) {
let ws_to_tcp = tokio::spawn(async move { let ws_to_tcp = tokio::spawn(async move {
while let Some(Ok(msg)) = futures_util::StreamExt::next(&mut ws_rx).await { while let Some(Ok(msg)) = futures_util::StreamExt::next(&mut ws_rx).await {
match msg { match msg {
Message::Binary(data) => { Message::Binary(data)
if tcp_tx.write_all(&data).await.is_err() { if tcp_tx.write_all(&data).await.is_err() => {
break; break;
} }
}
Message::Close(_) => break, Message::Close(_) => break,
_ => {} // ping/pong/text: ignore _ => {} // ping/pong/text: ignore
} }

View file

@ -19,6 +19,7 @@ pub struct AgentSocket {
} }
pub fn start(agent: &str, socket_path: &Path, coord: Arc<Coordinator>) -> Result<AgentSocket> { pub fn start(agent: &str, socket_path: &Path, coord: Arc<Coordinator>) -> Result<AgentSocket> {
use std::os::unix::fs::PermissionsExt as _;
let agent = agent.to_owned(); let agent = agent.to_owned();
if let Some(parent) = socket_path.parent() { if let Some(parent) = socket_path.parent() {
std::fs::create_dir_all(parent) std::fs::create_dir_all(parent)
@ -36,7 +37,6 @@ pub fn start(agent: &str, socket_path: &Path, coord: Arc<Coordinator>) -> Result
// perms (0755) lock it out. 0666 lets the agent user connect; // perms (0755) lock it out. 0666 lets the agent user connect;
// the bind source dir is per-agent on host so blast radius is // the bind source dir is per-agent on host so blast radius is
// unchanged. // unchanged.
use std::os::unix::fs::PermissionsExt as _;
std::fs::set_permissions(socket_path, std::fs::Permissions::from_mode(0o666)) std::fs::set_permissions(socket_path, std::fs::Permissions::from_mode(0o666))
.with_context(|| format!("chmod agent socket {}", socket_path.display()))?; .with_context(|| format!("chmod agent socket {}", socket_path.display()))?;
tracing::info!(%agent, socket = %socket_path.display(), "agent socket listening"); tracing::info!(%agent, socket = %socket_path.display(), "agent socket listening");
@ -95,7 +95,7 @@ async fn serve(stream: UnixStream, agent: String, coord: Arc<Coordinator>) -> Re
/// cheap "is there anything pending?" check without blocking the /// cheap "is there anything pending?" check without blocking the
/// turn for 30 seconds. To actually park, the caller passes a /// turn for 30 seconds. To actually park, the caller passes a
/// positive `wait_seconds`. /// positive `wait_seconds`.
pub(crate) const RECV_LONG_POLL_MAX: std::time::Duration = std::time::Duration::from_secs(180); pub(crate) const RECV_LONG_POLL_MAX: std::time::Duration = std::time::Duration::from_mins(3);
/// Server-side hard cap on `Recv.max`. Bounds the size of a single /// Server-side hard cap on `Recv.max`. Bounds the size of a single
/// round-trip so a confused caller can't drain the entire inbox in /// round-trip so a confused caller can't drain the entire inbox in
@ -357,6 +357,7 @@ async fn dispatch(req: &AgentRequest, agent: &str, coord: &Arc<Coordinator>) ->
/// ///
/// The manager is not exempt - grant `read_host_journal` in /// The manager is not exempt - grant `read_host_journal` in
/// `meta/capabilities.json` to enable it for any agent including the manager. /// `meta/capabilities.json` to enable it for any agent including the manager.
#[allow(clippy::too_many_arguments)]
pub async fn dispatch_host_journal( pub async fn dispatch_host_journal(
agent: &str, agent: &str,
unit: &Option<String>, unit: &Option<String>,
@ -619,8 +620,7 @@ fn prepare_remind_storage(
fn auto_reminder_path(agent: &str) -> String { fn auto_reminder_path(agent: &str) -> String {
let ts_ns = std::time::SystemTime::now() let ts_ns = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH) .duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos()) .map_or(0, |d| d.as_nanos());
.unwrap_or(0);
format!("/agents/{agent}/state/reminders/auto-{ts_ns}.md") format!("/agents/{agent}/state/reminders/auto-{ts_ns}.md")
} }

View file

@ -209,16 +209,16 @@ pub async fn ensure_manager(coord: &Arc<Coordinator>) -> Result<()> {
/// Sort `names` in-place so parents precede their children in the topology. /// Sort `names` in-place so parents precede their children in the topology.
/// Uses BFS from root agents (depth 0). Agents absent from `topo` sort last, /// Uses BFS from root agents (depth 0). Agents absent from `topo` sort last,
/// alphabetically within their tier. Stable within each depth tier. /// alphabetically within their tier. Stable within each depth tier.
pub fn topology_sort(names: &mut Vec<String>, topo: &std::collections::BTreeMap<String, Option<String>>) { pub fn topology_sort(names: &mut [String], topo: &std::collections::BTreeMap<String, Option<String>>) {
use std::collections::{HashMap, VecDeque}; use std::collections::{HashMap, VecDeque};
// Build depth map using owned clones so the borrow on `names` is released // Build depth map using owned clones so the borrow on `names` is released
// before the sort_by mutable borrow. // before the sort_by mutable borrow.
let name_set: Vec<String> = names.clone(); let name_set: Vec<String> = names.to_vec();
let mut depth: HashMap<String, usize> = HashMap::new(); let mut depth: HashMap<String, usize> = HashMap::new();
let mut queue: VecDeque<String> = VecDeque::new(); let mut queue: VecDeque<String> = VecDeque::new();
// Seed roots: entries with no parent, or names not present in topo at all. // Seed roots: entries with no parent, or names not present in topo at all.
for name in &name_set { for name in &name_set {
if topo.get(name).map_or(true, |p| p.is_none()) { if topo.get(name).is_none_or(Option::is_none) {
depth.insert(name.clone(), 0); depth.insert(name.clone(), 0);
queue.push_back(name.clone()); queue.push_back(name.clone());
} }

View file

@ -23,7 +23,7 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH};
use crate::coordinator::Coordinator; use crate::coordinator::Coordinator;
const VACUUM_INTERVAL: Duration = Duration::from_secs(3600); const VACUUM_INTERVAL: Duration = Duration::from_hours(1);
/// Keep completed task files for 48 hours before sweeping them. /// Keep completed task files for 48 hours before sweeping them.
const KEEP_SECS: i64 = 48 * 3600; const KEEP_SECS: i64 = 48 * 3600;
@ -97,7 +97,7 @@ fn should_delete(json_path: &Path, cutoff: i64) -> bool {
if !TERMINAL_STATUSES.contains(&status) { if !TERMINAL_STATUSES.contains(&status) {
return false; return false;
} }
let completed_at = v.get("completed_at").and_then(|t| t.as_i64()).unwrap_or(i64::MAX); let completed_at = v.get("completed_at").and_then(serde_json::Value::as_i64).unwrap_or(i64::MAX);
completed_at < cutoff completed_at < cutoff
} }
@ -106,10 +106,10 @@ fn should_delete(json_path: &Path, cutoff: i64) -> bool {
fn delete_trio(dir: &Path, stem: &str) { fn delete_trio(dir: &Path, stem: &str) {
for ext in ["json", "out", "err"] { for ext in ["json", "out", "err"] {
let path = dir.join(format!("{stem}.{ext}")); let path = dir.join(format!("{stem}.{ext}"));
if path.exists() { if path.exists()
if let Err(e) = std::fs::remove_file(&path) { && let Err(e) = std::fs::remove_file(&path)
tracing::warn!(path = %path.display(), error = ?e, "bash-tasks vacuum: remove failed"); {
} tracing::warn!(path = %path.display(), error = ?e, "bash-tasks vacuum: remove failed");
} }
} }
} }

View file

@ -410,7 +410,7 @@ pub fn spawn_vacuum(coord: &Arc<crate::coordinator::Coordinator>) {
use std::time::Duration; use std::time::Duration;
let logs = coord.build_logs.clone(); let logs = coord.build_logs.clone();
let mut shutdown = coord.shutdown_rx(); let mut shutdown = coord.shutdown_rx();
let interval = Duration::from_secs(3_600); let interval = Duration::from_hours(1);
tokio::spawn(async move { tokio::spawn(async move {
loop { loop {
match logs.vacuum() { match logs.vacuum() {

View file

@ -3,7 +3,7 @@
//! and `tool-groups.json`. //! and `tool-groups.json`.
//! //!
//! Format: a JSON object mapping agent name to an array of //! Format: a JSON object mapping agent name to an array of
//! `hive_sh4re::Capability` snake_case strings: //! `hive_sh4re::Capability` `snake_case` strings:
//! //!
//! ```json //! ```json
//! { //! {

View file

@ -217,12 +217,12 @@ fn read_dashboard_links(name: &str) -> Vec<DashboardLink> {
/// don't lose state during the transition window. /// don't lose state during the transition window.
fn read_harness_flags(name: &str) -> (bool, bool) { fn read_harness_flags(name: &str) -> (bool, bool) {
let dir = Coordinator::agent_notes_dir(name); let dir = Coordinator::agent_notes_dir(name);
if let Ok(raw) = std::fs::read_to_string(dir.join("hyperhive-harness.json")) { if let Ok(raw) = std::fs::read_to_string(dir.join("hyperhive-harness.json"))
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&raw) { && let Ok(v) = serde_json::from_str::<serde_json::Value>(&raw)
let rl = v.get("rate_limited").and_then(|x| x.as_bool()).unwrap_or(false); {
let nl = v.get("needs_login").and_then(|x| x.as_bool()).unwrap_or(false); let rl = v.get("rate_limited").and_then(serde_json::Value::as_bool).unwrap_or(false);
return (rl, nl); let nl = v.get("needs_login").and_then(serde_json::Value::as_bool).unwrap_or(false);
} return (rl, nl);
} }
// Legacy fallback: presence of individual sentinel files. // Legacy fallback: presence of individual sentinel files.
let rate_limited = dir.join("hyperhive-rate-limited").exists(); let rate_limited = dir.join("hyperhive-rate-limited").exists();

View file

@ -159,6 +159,7 @@ fn try_bind(addr: SocketAddr) -> std::io::Result<tokio::net::TcpListener> {
sock.listen(1024) sock.listen(1024)
} }
#[allow(clippy::struct_excessive_bools)]
#[derive(Serialize)] #[derive(Serialize)]
struct StateSnapshot { struct StateSnapshot {
/// Broker seq at the moment this snapshot was assembled. Clients /// Broker seq at the moment this snapshot was assembled. Clients
@ -1833,7 +1834,7 @@ async fn get_build_log_stream(
match notify_rx.recv().await { match notify_rx.recv().await {
// Notification for a different build — ignore and wait // Notification for a different build — ignore and wait
// for the next one. // for the next one.
Ok(notif_id) if notif_id != id => continue, Ok(notif_id) if notif_id != id => {}
Ok(_) => { Ok(_) => {
match logs.get_progress(id, stdout_cursor, stderr_cursor) { match logs.get_progress(id, stdout_cursor, stderr_cursor) {
Ok(Some(prog)) => { Ok(Some(prog)) => {
@ -1845,20 +1846,17 @@ async fn get_build_log_stream(
stderr_append: prog.stderr_append, stderr_append: prog.stderr_append,
status: prog.status, status: prog.status,
done, done,
}) { }) && tx.send(Ok(Event::default().data(json))).await.is_err() {
if tx.send(Ok(Event::default().data(json))).await.is_err() { return; // browser disconnected
return; // browser disconnected
}
} }
if done { if done {
return; return;
} }
} }
Ok(None) => return, // vacuum reaped the row Ok(None) | Err(_) => return, // vacuum reaped row / channel closed
Err(_) => return,
} }
} }
Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => continue, Err(tokio::sync::broadcast::error::RecvError::Lagged(_)) => {}
Err(tokio::sync::broadcast::error::RecvError::Closed) => return, Err(tokio::sync::broadcast::error::RecvError::Closed) => return,
} }
} }

View file

@ -18,7 +18,7 @@ use rusqlite::{Connection, Result, params};
use crate::coordinator::Coordinator; use crate::coordinator::Coordinator;
const VACUUM_INTERVAL: Duration = Duration::from_secs(3600); const VACUUM_INTERVAL: Duration = Duration::from_hours(1);
const KEEP_SECS: i64 = 7 * 24 * 3600; const KEEP_SECS: i64 = 7 * 24 * 3600;
/// Background loop: sweep every existing agent state dir hourly, run /// Background loop: sweep every existing agent state dir hourly, run

View file

@ -161,8 +161,7 @@ pub fn duplicate_groups(raw: &str) -> Result<Vec<DuplicateGroup>> {
pub async fn check_lock_in_sync(repo: &Path, tag: &str, approval_id: i64) -> Result<()> { pub async fn check_lock_in_sync(repo: &Path, tag: &str, approval_id: i64) -> Result<()> {
let suffix = std::time::SystemTime::now() let suffix = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH) .duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos()) .map_or(0, |d| d.as_nanos());
.unwrap_or(0);
let tmp_dir = std::env::temp_dir().join(format!("hive-flake-check-{approval_id}-{suffix}")); let tmp_dir = std::env::temp_dir().join(format!("hive-flake-check-{approval_id}-{suffix}"));
// Detached worktree at the proposal tag — gives us a clean, mutable // Detached worktree at the proposal tag — gives us a clean, mutable

View file

@ -276,8 +276,7 @@ async fn mint_token(name: &str, scopes: &str) -> Result<String> {
"{TOKEN_NAME_PREFIX}-{}", "{TOKEN_NAME_PREFIX}-{}",
std::time::SystemTime::now() std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH) .duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs()) .map_or(0, |d| d.as_secs())
.unwrap_or(0)
); );
let stdout = forge_admin(&[ let stdout = forge_admin(&[
"user", "user",

View file

@ -274,7 +274,7 @@ fn gateway_systemctl(args: &[&str]) -> bool {
/// Synchronise the gateway nginx unit with the current agents.conf: /// Synchronise the gateway nginx unit with the current agents.conf:
/// ///
/// - **active**: send `nginx -s reload` (SIGHUP to master, zero-downtime /// - **active**: send `nginx -s reload` (SIGHUP to master, zero-downtime
/// worker replacement). Keeps RELOAD_PENDING set on failure so the /// worker replacement). Keeps `RELOAD_PENDING` set on failure so the
/// next poll tick retries. /// next poll tick retries.
/// - **failed / start-limit-hit**: run `systemctl reset-failed nginx` /// - **failed / start-limit-hit**: run `systemctl reset-failed nginx`
/// then `systemctl start nginx`. This is the self-healing path: a /// then `systemctl start nginx`. This is the self-healing path: a

View file

@ -181,7 +181,7 @@ async fn port_collision(self_name: &str) -> Option<String> {
None None
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments, clippy::implicit_hasher)]
pub async fn spawn( pub async fn spawn(
name: &str, name: &str,
hyperhive_flake: &str, hyperhive_flake: &str,
@ -324,8 +324,7 @@ pub async fn is_running(name: &str) -> bool {
.args(["is-active", "--quiet", &unit]) .args(["is-active", "--quiet", &unit])
.status() .status()
.await .await
.map(|s| s.success()) .is_ok_and(|s| s.success())
.unwrap_or(false)
} }
/// Fully tear down a sub-agent's container: stop + remove via `nixos-container /// Fully tear down a sub-agent's container: stop + remove via `nixos-container
@ -346,7 +345,7 @@ pub async fn destroy(name: &str) -> Result<()> {
Ok(()) Ok(())
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments, clippy::implicit_hasher)]
pub async fn rebuild( pub async fn rebuild(
name: &str, name: &str,
hyperhive_flake: &str, hyperhive_flake: &str,
@ -1004,7 +1003,7 @@ fn set_resource_limits(container: &str) -> Result<()> {
std::fs::create_dir_all(&dir).with_context(|| format!("create {dir}"))?; std::fs::create_dir_all(&dir).with_context(|| format!("create {dir}"))?;
let path = format!("{dir}/hyperhive-limits.conf"); let path = format!("{dir}/hyperhive-limits.conf");
let content = let content =
format!("[Service]\nMemoryMax={DEFAULT_MEMORY_MAX}\nCPUQuota={DEFAULT_CPU_QUOTA}\n",); format!("[Service]\nMemoryMax={DEFAULT_MEMORY_MAX}\nCPUQuota={DEFAULT_CPU_QUOTA}\n");
std::fs::write(&path, content).with_context(|| format!("write {path}"))?; std::fs::write(&path, content).with_context(|| format!("write {path}"))?;
tracing::info!( tracing::info!(
%path, %path,
@ -1081,6 +1080,7 @@ fn bind_child_agent_dirs(child: &str, binds: &mut String) {
let _ = write!(binds, " --bind={config_dir}:/agents/{child}/config"); let _ = write!(binds, " --bind={config_dir}:/agents/{child}/config");
} }
#[allow(clippy::too_many_lines)]
fn set_nspawn_flags( fn set_nspawn_flags(
container: &str, container: &str,
runtime_dir: &Path, runtime_dir: &Path,
@ -1265,8 +1265,7 @@ async fn run(args: &[&str]) -> Result<()> {
let agent = args let agent = args
.get(1) .get(1)
.copied() .copied()
.map(|c| c.strip_prefix(AGENT_PREFIX).unwrap_or(c).to_string()) .map_or_else(|| "<unknown>".to_string(), |c| c.strip_prefix(AGENT_PREFIX).unwrap_or(c).to_string());
.unwrap_or_else(|| "<unknown>".to_string());
let logs = crate::build_logs::global(); let logs = crate::build_logs::global();
let log_id = logs.as_ref().and_then(|h| { let log_id = logs.as_ref().and_then(|h| {

View file

@ -227,7 +227,7 @@ async fn cmd_serve(
let vacuum_coord = coord.clone(); let vacuum_coord = coord.clone();
let mut vacuum_shutdown = coord.shutdown_rx(); let mut vacuum_shutdown = coord.shutdown_rx();
tokio::spawn(async move { tokio::spawn(async move {
let interval = std::time::Duration::from_secs(3600); let interval = std::time::Duration::from_hours(1);
let keep_secs: i64 = 30 * 24 * 3600; let keep_secs: i64 = 30 * 24 * 3600;
loop { loop {
match vacuum_coord.broker.vacuum_delivered(keep_secs) { match vacuum_coord.broker.vacuum_delivered(keep_secs) {

View file

@ -14,6 +14,7 @@ use crate::coordinator::Coordinator;
use crate::lifecycle; use crate::lifecycle;
pub fn start(coord: Arc<Coordinator>) -> Result<()> { pub fn start(coord: Arc<Coordinator>) -> Result<()> {
use std::os::unix::fs::PermissionsExt as _;
let dir = Coordinator::manager_dir(); let dir = Coordinator::manager_dir();
std::fs::create_dir_all(&dir) std::fs::create_dir_all(&dir)
.with_context(|| format!("create manager dir {}", dir.display()))?; .with_context(|| format!("create manager dir {}", dir.display()))?;
@ -25,7 +26,6 @@ pub fn start(coord: Arc<Coordinator>) -> Result<()> {
.with_context(|| format!("bind manager socket {}", socket.display()))?; .with_context(|| format!("bind manager socket {}", socket.display()))?;
// 0666 so the in-container root user (non-root) can connect; // 0666 so the in-container root user (non-root) can connect;
// the bind source dir is manager-only on host. See agent_server.rs. // the bind source dir is manager-only on host. See agent_server.rs.
use std::os::unix::fs::PermissionsExt as _;
std::fs::set_permissions(&socket, std::fs::Permissions::from_mode(0o666)) std::fs::set_permissions(&socket, std::fs::Permissions::from_mode(0o666))
.with_context(|| format!("chmod manager socket {}", socket.display()))?; .with_context(|| format!("chmod manager socket {}", socket.display()))?;
tracing::info!(socket = %socket.display(), "manager socket listening"); tracing::info!(socket = %socket.display(), "manager socket listening");

View file

@ -49,7 +49,7 @@ pub fn meta_dir() -> PathBuf {
/// seed commit. Subsequent calls only touch `flake.nix` when the /// seed commit. Subsequent calls only touch `flake.nix` when the
/// rendered contents differ from disk; an unchanged `flake.nix` is a /// rendered contents differ from disk; an unchanged `flake.nix` is a
/// no-op. /// no-op.
#[allow(dead_code)] // first caller lands in a later commit #[allow(dead_code, clippy::implicit_hasher)] // first caller lands in a later commit
pub async fn sync_agents( pub async fn sync_agents(
hyperhive_flake: &str, hyperhive_flake: &str,
dashboard_port: u16, dashboard_port: u16,

View file

@ -211,10 +211,10 @@ async fn rename_manager_container(coord: &Arc<Coordinator>) {
// Move rootfs if it exists (may be absent for ephemeral containers). // Move rootfs if it exists (may be absent for ephemeral containers).
let old_rootfs = std::path::PathBuf::from("/var/lib/nixos-containers/root"); let old_rootfs = std::path::PathBuf::from("/var/lib/nixos-containers/root");
let new_rootfs = std::path::PathBuf::from("/var/lib/nixos-containers/h-root"); let new_rootfs = std::path::PathBuf::from("/var/lib/nixos-containers/h-root");
if old_rootfs.exists() && !new_rootfs.exists() { if old_rootfs.exists() && !new_rootfs.exists()
if let Err(e) = std::fs::rename(&old_rootfs, &new_rootfs) { && let Err(e) = std::fs::rename(&old_rootfs, &new_rootfs)
tracing::warn!(error = ?e, "migration phase 5: rename rootfs failed (non-fatal)"); {
} tracing::warn!(error = ?e, "migration phase 5: rename rootfs failed (non-fatal)");
} }
// Daemon reload so systemd picks up the new container@h-root unit. // Daemon reload so systemd picks up the new container@h-root unit.

View file

@ -27,7 +27,7 @@ const POLL_INTERVAL: Duration = Duration::from_secs(5);
/// the dashboard list view doesn't accrue tombstones forever. /// the dashboard list view doesn't accrue tombstones forever.
/// Cancelled rows live long enough that the operator can still /// Cancelled rows live long enough that the operator can still
/// see what they cancelled in the recent past. /// see what they cancelled in the recent past.
const CANCELLED_REAP_AGE: Duration = Duration::from_secs(3600); const CANCELLED_REAP_AGE: Duration = Duration::from_hours(1);
pub fn spawn(coord: Arc<Coordinator>) { pub fn spawn(coord: Arc<Coordinator>) {
let mut shutdown = coord.shutdown_rx(); let mut shutdown = coord.shutdown_rx();

View file

@ -15,7 +15,7 @@ use rusqlite::{Connection, Result, params};
use crate::coordinator::Coordinator; use crate::coordinator::Coordinator;
const VACUUM_INTERVAL: Duration = Duration::from_secs(3600); const VACUUM_INTERVAL: Duration = Duration::from_hours(1);
const KEEP_SECS: i64 = 90 * 24 * 3600; const KEEP_SECS: i64 = 90 * 24 * 3600;
/// Background loop: sweep every existing agent state dir hourly, run /// Background loop: sweep every existing agent state dir hourly, run

View file

@ -3,7 +3,7 @@
//! and the meta `flake.nix`. //! and the meta `flake.nix`.
//! //!
//! Format: a JSON object mapping agent name to an array of //! Format: a JSON object mapping agent name to an array of
//! `hive_sh4re::ToolGroup` snake_case strings: //! `hive_sh4re::ToolGroup` `snake_case` strings:
//! //!
//! ```json //! ```json
//! { //! {

View file

@ -48,8 +48,8 @@ pub struct Args {
pub fn run(client: &Client, args: Args) -> Result<()> { pub fn run(client: &Client, args: Args) -> Result<()> {
let repo = client.repo(); let repo = client.repo();
let comments = match args.tail { let comments = match args.tail {
Some(n) => fetch_tail(client, &repo, args.number, n)?, Some(n) => fetch_tail(client, repo, args.number, n)?,
None => fetch_head(client, &repo, args.number, args.limit)?, None => fetch_head(client, repo, args.number, args.limit)?,
}; };
if client.json_mode() { if client.json_mode() {
let trimmed: Vec<Value> = comments let trimmed: Vec<Value> = comments
@ -99,6 +99,7 @@ fn fetch_head(client: &Client, repo: &str, number: u64, limit: u64) -> Result<Ve
/// first to know how many exist, then start paginating from the /// first to know how many exist, then start paginating from the
/// page that contains item `total - n`. Work is bounded by /// page that contains item `total - n`. Work is bounded by
/// `ceil(n/50) + 1` page fetches, regardless of thread length. /// `ceil(n/50) + 1` page fetches, regardless of thread length.
#[allow(clippy::cast_possible_truncation)]
fn fetch_tail(client: &Client, repo: &str, number: u64, n: usize) -> Result<Vec<Value>> { fn fetch_tail(client: &Client, repo: &str, number: u64, n: usize) -> Result<Vec<Value>> {
if n == 0 { if n == 0 {
return Ok(Vec::new()); return Ok(Vec::new());

View file

@ -144,8 +144,7 @@ fn parse_diff_git_path(rest: &str) -> Option<String> {
// containing `"` doesn't terminate early). // containing `"` doesn't terminate early).
let mut iter = after_open.char_indices(); let mut iter = after_open.char_indices();
let mut a_close = None; let mut a_close = None;
loop { while let Some((i, c)) = iter.next() {
let Some((i, c)) = iter.next() else { break };
if c == '\\' { if c == '\\' {
// Skip the next char — it's part of the escape. // Skip the next char — it's part of the escape.
iter.next(); iter.next();
@ -167,8 +166,7 @@ fn parse_diff_git_path(rest: &str) -> Option<String> {
// Find b-side's closing quote with the same escape rule. // Find b-side's closing quote with the same escape rule.
let mut iter = b_inside.char_indices(); let mut iter = b_inside.char_indices();
let mut b_close = None; let mut b_close = None;
loop { while let Some((i, c)) = iter.next() {
let Some((i, c)) = iter.next() else { break };
if c == '\\' { if c == '\\' {
iter.next(); iter.next();
continue; continue;

View file

@ -8,6 +8,8 @@
//! second of the four #694 gaps (read-side; no boundary concerns — //! second of the four #694 gaps (read-side; no boundary concerns —
//! every agent + the operator queries the issue tracker constantly). //! every agent + the operator queries the issue tracker constantly).
use std::fmt::Write as _;
use anyhow::Result; use anyhow::Result;
use clap::{Args as ClapArgs, ValueEnum}; use clap::{Args as ClapArgs, ValueEnum};
use serde_json::Value; use serde_json::Value;
@ -97,24 +99,24 @@ pub fn run(client: &Client, args: Args) -> Result<()> {
if let Some(u) = args.assignee.as_deref() if let Some(u) = args.assignee.as_deref()
&& !u.is_empty() && !u.is_empty()
{ {
path.push_str(&format!("&assigned_by={}", pct_encode(u))); write!(path, "&assigned_by={}", pct_encode(u)).unwrap();
} }
if let Some(u) = args.author.as_deref() if let Some(u) = args.author.as_deref()
&& !u.is_empty() && !u.is_empty()
{ {
path.push_str(&format!("&created_by={}", pct_encode(u))); write!(path, "&created_by={}", pct_encode(u)).unwrap();
} }
if let Some(u) = args.mention.as_deref() if let Some(u) = args.mention.as_deref()
&& !u.is_empty() && !u.is_empty()
{ {
path.push_str(&format!("&mentioned_by={}", pct_encode(u))); write!(path, "&mentioned_by={}", pct_encode(u)).unwrap();
} }
if !args.labels.is_empty() { if !args.labels.is_empty() {
// Encode each label individually so a comma INSIDE a label // Encode each label individually so a comma INSIDE a label
// (rare but legal) gets escaped while the field separator // (rare but legal) gets escaped while the field separator
// stays a literal comma the forge will parse as N labels. // stays a literal comma the forge will parse as N labels.
let encoded: Vec<String> = args.labels.iter().map(|l| pct_encode(l)).collect(); let encoded: Vec<String> = args.labels.iter().map(|l| pct_encode(l)).collect();
path.push_str(&format!("&labels={}", encoded.join(","))); write!(path, "&labels={}", encoded.join(",")).unwrap();
} }
let resp = client.get_json(&path)?; let resp = client.get_json(&path)?;
if client.json_mode() { if client.json_mode() {
@ -143,7 +145,7 @@ fn pct_encode(s: &str) -> String {
if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'~') { if b.is_ascii_alphanumeric() || matches!(b, b'-' | b'_' | b'.' | b'~') {
out.push(b as char); out.push(b as char);
} else { } else {
out.push_str(&format!("%{b:02X}")); write!(out, "%{b:02X}").unwrap();
} }
} }
out out

View file

@ -6,7 +6,7 @@
//! //!
//! Forgejo's `/issues/{n}/timeline` endpoint returns BOTH the actual //! Forgejo's `/issues/{n}/timeline` endpoint returns BOTH the actual
//! comments AND the event entries (label, assignee, close, reopen, //! comments AND the event entries (label, assignee, close, reopen,
//! pull_push, etc.) in chronological order. We render each row in //! `pull_push`, etc.) in chronological order. We render each row in
//! a human-readable form by default; pass the global `--json` flag //! a human-readable form by default; pass the global `--json` flag
//! for the raw API shape. //! for the raw API shape.
//! //!
@ -62,6 +62,7 @@ pub fn run(client: &Client, args: Args) -> Result<()> {
/// output for every supported event type without re-implementing the /// output for every supported event type without re-implementing the
/// per-arm dispatch. `print_event` is the only caller that adds the /// per-arm dispatch. `print_event` is the only caller that adds the
/// terminating newline. /// terminating newline.
#[allow(clippy::too_many_lines)]
fn format_event(ev: &Value) -> String { fn format_event(ev: &Value) -> String {
let event_type = ev.get("type").and_then(Value::as_str).unwrap_or("?"); let event_type = ev.get("type").and_then(Value::as_str).unwrap_or("?");
let user = ev let user = ev

View file

@ -17,6 +17,7 @@
//! `LISTEN_FDS=1` + `LISTEN_PID=<self>`, the inherited fd 3 is used //! `LISTEN_FDS=1` + `LISTEN_PID=<self>`, the inherited fd 3 is used
//! instead of binding a fresh socket. //! instead of binding a fresh socket.
use std::fmt::Write as _;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use anyhow::{Context as _, Result, bail}; use anyhow::{Context as _, Result, bail};
@ -53,6 +54,7 @@ async fn main() -> Result<()> {
} }
fn socket_listener() -> Result<UnixListener> { fn socket_listener() -> Result<UnixListener> {
use std::os::unix::fs::PermissionsExt as _;
// Socket activation: systemd passes the socket as fd 3 when // Socket activation: systemd passes the socket as fd 3 when
// LISTEN_FDS >= 1 and LISTEN_PID matches our pid. // LISTEN_FDS >= 1 and LISTEN_PID matches our pid.
let listen_fds: Option<i32> = std::env::var("LISTEN_FDS") let listen_fds: Option<i32> = std::env::var("LISTEN_FDS")
@ -62,21 +64,21 @@ fn socket_listener() -> Result<UnixListener> {
.ok() .ok()
.and_then(|s| s.parse().ok()); .and_then(|s| s.parse().ok());
if let (Some(n), Some(p)) = (listen_fds, listen_pid) { if let (Some(n), Some(p)) = (listen_fds, listen_pid)
if n >= 1 && p == std::process::id() { && n >= 1 && p == std::process::id()
// SAFETY: systemd has passed us a ready UnixListener on fd 3. {
let std_listener = unsafe { // SAFETY: systemd has passed us a ready UnixListener on fd 3.
use std::os::unix::io::FromRawFd; let std_listener = unsafe {
std::os::unix::net::UnixListener::from_raw_fd(3) use std::os::unix::io::FromRawFd;
}; std::os::unix::net::UnixListener::from_raw_fd(3)
std_listener };
.set_nonblocking(true) std_listener
.context("set socket non-blocking")?; .set_nonblocking(true)
let listener = .context("set socket non-blocking")?;
tokio::net::UnixListener::from_std(std_listener).context("wrap systemd socket")?; let listener =
tracing::info!("using systemd-activated socket"); tokio::net::UnixListener::from_std(std_listener).context("wrap systemd socket")?;
return Ok(listener); tracing::info!("using systemd-activated socket");
} return Ok(listener);
} }
// Fallback: bind the socket ourselves. // Fallback: bind the socket ourselves.
@ -88,7 +90,6 @@ fn socket_listener() -> Result<UnixListener> {
let _ = std::fs::remove_file(path); let _ = std::fs::remove_file(path);
let listener = UnixListener::bind(path).with_context(|| format!("bind {PRIV_SOCK}"))?; let listener = UnixListener::bind(path).with_context(|| format!("bind {PRIV_SOCK}"))?;
// Mode 0660: only the hive-core group can connect. // 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)) std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o660))
.context("chmod priv.sock")?; .context("chmod priv.sock")?;
tracing::info!(path = PRIV_SOCK, "bound priv socket"); tracing::info!(path = PRIV_SOCK, "bound priv socket");
@ -137,6 +138,7 @@ async fn dispatch(line: &str) -> PrivResponse {
} }
/// Execute a validated `PrivRequest`. Returns `(stdout, stderr)` on success. /// Execute a validated `PrivRequest`. Returns `(stdout, stderr)` on success.
#[allow(clippy::too_many_lines)]
async fn exec(req: PrivRequest) -> Result<(String, String)> { async fn exec(req: PrivRequest) -> Result<(String, String)> {
match req { match req {
PrivRequest::StartContainer { ref name } => { PrivRequest::StartContainer { ref name } => {
@ -248,9 +250,9 @@ async fn exec(req: PrivRequest) -> Result<(String, String)> {
} }
PrivRequest::ChmodSocketDir { ref agent_name, mode } => { PrivRequest::ChmodSocketDir { ref agent_name, mode } => {
use std::os::unix::fs::PermissionsExt as _;
validate_agent_name(agent_name)?; validate_agent_name(agent_name)?;
let path = socket_dir_path(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)) std::fs::set_permissions(&path, std::fs::Permissions::from_mode(mode))
.with_context(|| format!("chmod {:o} {}", mode, path.display()))?; .with_context(|| format!("chmod {:o} {}", mode, path.display()))?;
Ok((String::new(), String::new())) Ok((String::new(), String::new()))
@ -404,6 +406,6 @@ fn write_nspawn_flags(container: &str, binds: &[BindMount]) -> Result<()> {
format!("{flag}={}:{}", b.host_path, b.container_path) format!("{flag}={}:{}", b.host_path, b.container_path)
}).collect(); }).collect();
let flags_joined = flags.join(" "); let flags_joined = flags.join(" ");
out.push_str(&format!("EXTRA_NSPAWN_FLAGS=\"{flags_joined}\"\n")); writeln!(out, "EXTRA_NSPAWN_FLAGS=\"{flags_joined}\"").unwrap();
std::fs::write(&path, out).with_context(|| format!("write {path}")) std::fs::write(&path, out).with_context(|| format!("write {path}"))
} }

View file

@ -751,7 +751,7 @@ pub struct SchedulePromptPayload {
/// Named group of MCP tools an agent may be granted. The harness reads /// Named group of MCP tools an agent may be granted. The harness reads
/// `HIVE_TOOL_GROUPS` from the environment (a comma-separated list of /// `HIVE_TOOL_GROUPS` from the environment (a comma-separated list of
/// snake_case group names written by the meta renderer from per-agent /// `snake_case` group names written by the meta renderer from per-agent
/// config) and expands it to the matching tool names for `--allowedTools`. /// config) and expands it to the matching tool names for `--allowedTools`.
/// When the env var is absent the harness falls back to the flavor default /// When the env var is absent the harness falls back to the flavor default
/// (`AGENT_DEFAULT` or `MANAGER_DEFAULT`). See `docs/conventions.md::Tool groups`. /// (`AGENT_DEFAULT` or `MANAGER_DEFAULT`). See `docs/conventions.md::Tool groups`.
@ -841,7 +841,7 @@ impl ToolGroup {
Self::Execution, Self::Execution,
]; ];
/// The snake_case wire name for this group (matches `serde(rename_all = /// The `snake_case` wire name for this group (matches `serde(rename_all =
/// "snake_case")` serialisation). /// "snake_case")` serialisation).
#[must_use] #[must_use]
pub fn as_str(self) -> &'static str { pub fn as_str(self) -> &'static str {
@ -897,7 +897,7 @@ impl JournalPriority {
/// which MCP tools the harness exposes to claude). /// which MCP tools the harness exposes to claude).
/// ///
/// Injected into containers as `HIVE_CAPABILITIES` (comma-separated /// Injected into containers as `HIVE_CAPABILITIES` (comma-separated
/// snake_case) via `meta::render_flake`. The harness reads this to /// `snake_case`) via `meta::render_flake`. The harness reads this to
/// conditionally register capability-gated MCP tools so claude only /// conditionally register capability-gated MCP tools so claude only
/// sees tools it can actually invoke. See `docs/conventions.md::Capabilities`. /// sees tools it can actually invoke. See `docs/conventions.md::Capabilities`.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
@ -925,7 +925,7 @@ pub enum Capability {
} }
impl Capability { impl Capability {
/// Canonical snake_case name for this capability (matches serde). /// Canonical `snake_case` name for this capability (matches serde).
#[must_use] #[must_use]
pub fn as_str(self) -> &'static str { pub fn as_str(self) -> &'static str {
match self { match self {