diff --git a/docs/agent-lifecycle/persistence.md b/docs/agent-lifecycle/persistence.md index 142b7d05..112b3e77 100644 --- a/docs/agent-lifecycle/persistence.md +++ b/docs/agent-lifecycle/persistence.md @@ -339,8 +339,7 @@ Under `/var/lib/hyperhive/agents//`: `work/` doesn't walk up into `state/AGENTS.md` and load the agent's own identity. Survives `destroy`/recreate, gone on `--purge`. Counted in the per-agent disk metric (`measure_agent_disk`) together with - `state/` and the container rootfs. The dashboard doesn't serve it: - the state-file endpoint reads `state/` only. + `state/` and the container rootfs. The dashboard doesn't serve it. ### Cross-agent access to state diff --git a/docs/trust-boundary/security.md b/docs/trust-boundary/security.md index a0a57a9b..0b69a089 100644 --- a/docs/trust-boundary/security.md +++ b/docs/trust-boundary/security.md @@ -2,8 +2,8 @@ ## Agent trust model -The sections below document specific mechanisms (the state-file endpoint, -nixbld isolation, privilege separation). This section frames the model they +The sections below document specific mechanisms (nixbld isolation, +privilege separation). This section frames the model they serve: **what hyperhive defends, what it deliberately doesn't, and where the operator is accepting risk.** It's the reference for "is it safe to give an agent capability X?." @@ -20,9 +20,8 @@ default**. Isolating credentials _from the agent itself_ is therefore **not a goal**: an agent can read its own tokens, its own `/home//.claude`, and run arbitrary commands as root inside its container. (The narrow exception is _cross-tenant_ leakage — for example the unsandboxed-nix-build `0600` token policy -below stops a build's nixbld user reading the agent's own forge token, and the -state-file endpoint stops one agent proxying another's files. Those harden the -boundary; they don't sandbox the agent from itself.) +below stops a build's nixbld user reading the agent's own forge token. That hardens +the boundary; it doesn't sandbox the agent from itself.) @@ -162,45 +161,10 @@ into an agent's config, that agent has exactly that reach, with no hyperhive layer narrowing it. Scope external tokens tightly at the source (the external provider) before handing them over. -## State-file endpoint security model +## Agent-supplied names in paths -`GET /api/state-file?path=

` serves files from agent state dirs and -the shared space to authenticated dashboard users (browser, operator). -It accepts two allow-listed root prefixes and rejects all other paths -before touching the filesystem: - -- `/var/lib/hyperhive/agents//state/` — per-agent durable notes - (canonical host form or the in-container view `/agents//state/`) -- `/var/lib/hyperhive/shared/` — shared docs (`/shared/` in-container) - -`/state/...` without an agent prefix is explicitly _not_ accepted — it's -ambiguous from the host's perspective. - -Defense-in-depth layers (in order): - -1. **Allow-list prefix check** — rejects without touching the filesystem - if the path doesn't match either root. -2. **No symlinks below the matched root** — each path component is - checked with `symlink_metadata` before canonicalize. A sub-agent - that plants `ln -s /other/secret /agents/me/state/peek` can't proxy - another agent's file through this endpoint (canonicalize would - happily resolve the symlink to a still-within-allow-list path). -3. **Canonicalize as belt-and-braces** — resolves `..`/`.` traversal - and rejects if the result escapes the roots. -4. **`state/` subdir constraint** — under `AGENTS_ROOT`, the second - path component must be `state/`. Applied, proposed git repos and - config dirs are off-limits. -5. **World-readable check** — file must have `mode & 0o004` set. - A `0600` file inside `state/` would otherwise be accessible to any - operator with dashboard access. - -`scan_validated_paths` (broker-message ingest, linkifier) uses the same -`resolve_state_path` helper so security rules stay in sync — the -dashboard renders anchors only for tokens that passed the same checks the -read endpoint enforces. - -The same invariant holds wherever an agent-supplied name reaches a filesystem -path: the agent socket's `GetAgentMeta` takes `name` as a serde-validated +Wherever an agent-supplied name reaches a filesystem path, hyperhive validates +it first. The agent socket's `GetAgentMeta` takes `name` as a serde-validated `hive_types::Ident` (or falls back to `Ident::parse` for the "self" case) before building `agent_notes_dir(name)`, so a `..` component can't traverse. diff --git a/docs/web-ui/dashboard.md b/docs/web-ui/dashboard.md index 4e712c54..a305e2b3 100644 --- a/docs/web-ui/dashboard.md +++ b/docs/web-ui/dashboard.md @@ -555,23 +555,6 @@ below — some endpoints aren't in it yet. stdout+stderr as `text/plain`. Used by the `↓ raw` / `↓ download raw` links in the build tabs. Works on both finished and in-progress build logs. -- `GET /static/marked.js` serves the vendored `marked` bundle used - for markdown previews. -- `GET /api/state-file?path=` — bounded - text read of a file under the per-agent `state/` subtree or - the shared `/var/lib/hyperhive/shared/`. Accepts the - container-view forms (`/agents//state/...`, `/shared/...`) - and the host form. Canonicalises + verifies the path stays - inside the allow-list, refuses anything but a regular file, - refuses `/agents//claude` / `config` subtrees, truncates - bodies at 1 MiB. No dashboard page calls it since the flow page - stopped existing. - - The broker forwarder still detects path tokens **server-side at - broker-message ingest**: it calls `scan_validated_paths(body)` — - same allow-list helper the read endpoint uses — and attaches - the verified file tokens to the event as `file_refs: Vec`. - No dashboard page reads that list either. - `GET /api/reminders` — list pending reminders for the dashboard's queued-reminders panel. - `GET /api/stats-hive?window=<1h|4h|24h|3d|7d|30d|all>` — hive-wide diff --git a/frontend/npm-deps-hash b/frontend/npm-deps-hash index 2616ec20..22918726 100644 --- a/frontend/npm-deps-hash +++ b/frontend/npm-deps-hash @@ -1 +1 @@ -sha256-cX/fNFPKFmN88KQiyau2yYp3xydx5wrv5ZFdMud7UcQ= +sha256-CkojHOVdQJGFhqZV8A8dBtR2hb04cRSgd1PbV9LYkzQ= diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 0f272639..aa9a88a4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1001,8 +1001,6 @@ "version": "0.0.0", "dependencies": { "@hive/shared": "*", - "dompurify": "^3.2.4", - "marked": "18.0.6", "preact": "10.29.8" } }, diff --git a/frontend/packages/dashboard/package.json b/frontend/packages/dashboard/package.json index 5a000b14..0bc57fed 100644 --- a/frontend/packages/dashboard/package.json +++ b/frontend/packages/dashboard/package.json @@ -9,8 +9,6 @@ }, "dependencies": { "@hive/shared": "*", - "dompurify": "^3.2.4", - "marked": "18.0.6", "preact": "10.29.8" } } diff --git a/hive-c0re/src/dashboard/mod.rs b/hive-c0re/src/dashboard/mod.rs index d2e90c07..5747569b 100644 --- a/hive-c0re/src/dashboard/mod.rs +++ b/hive-c0re/src/dashboard/mod.rs @@ -40,7 +40,6 @@ use crate::lifecycle; (name = "meta_inputs", description = "bulk flake-input update for the meta flake"), (name = "misc_api", description = "mark-all-read, compose, spawn-request, hive stats"), (name = "rebuild_queue", description = "rebuild-queue cancel"), - (name = "state_files", description = "proxied reads of allow-listed per-agent state files"), (name = "state_snapshot", description = "cold-load dashboard snapshot"), (name = "tombstones", description = "purge of retained state for destroyed agents"), ) @@ -58,7 +57,6 @@ mod lifecycle_ops; mod meta_inputs; mod misc_api; mod rebuild_queue; -mod state_files; mod state_snapshot; mod tombstones; @@ -67,10 +65,6 @@ mod tombstones; // Re-exported to preserve the `crate::dashboard::*` paths across the split. pub use meta_inputs::MetaInputView; pub(crate) use meta_inputs::emit_meta_inputs_snapshot; -// Run at broker-message ingest by the coordinator + the operator-msg path -// (`main.rs`); re-exported to preserve the `crate::dashboard::scan_validated_paths` -// path across the split. -pub use state_files::scan_validated_paths; // Called after destroy/purge/spawn finalisation (`actions.rs`); the view // type feeds `DashboardEvent::TombstonesChanged` (`dashboard_events.rs`). // Re-exported to preserve the `crate::dashboard::*` paths across the split. @@ -114,7 +108,6 @@ fn app(coord: Arc) -> axum::Router { .routes(routes!(health::get_health_live)) .routes(routes!(health::get_health_ready)) .routes(routes!(state_snapshot::api_state)) - .routes(routes!(state_files::get_state_file)) .routes(routes!(misc_api::api_stats_hive)) .routes(routes!(misc_api::post_mark_all_read)) .routes(routes!(build_logs::get_build_logs_all)) @@ -366,7 +359,6 @@ mod router_build_probe { .routes(routes!(health::get_health_live)) .routes(routes!(health::get_health_ready)) .routes(routes!(state_snapshot::api_state)) - .routes(routes!(state_files::get_state_file)) .routes(routes!(misc_api::api_stats_hive)) .routes(routes!(misc_api::post_mark_all_read)) .routes(routes!(build_logs::get_build_logs_all)) diff --git a/hive-c0re/src/dashboard/state_files.rs b/hive-c0re/src/dashboard/state_files.rs deleted file mode 100644 index 114eacf4..00000000 --- a/hive-c0re/src/dashboard/state_files.rs +++ /dev/null @@ -1,322 +0,0 @@ -//! State-file proxy + path-validation for the dashboard. -//! -//! `GET /api/state-file?path=…` serves an allow-listed file (per-agent -//! `state/` or `shared/`) with defense-in-depth symlink + traversal checks -//! (see `docs/trust-boundary/security.md::State-file endpoint`); raster images are served -//! with their real content-type, everything else as truncated text. -//! `scan_validated_paths` runs the same allow-list at broker-message ingest -//! so dashboard events carry a pre-verified file-ref set. - -use std::path::Path; - -use axum::response::{IntoResponse, Response}; -use serde::Deserialize; -use utoipa::IntoParams; - -use super::error_response; -use crate::paths::{AGENTS_ROOT, SHARED_ROOT}; - -#[derive(Deserialize, IntoParams)] -pub(super) struct StateFileQuery { - /// Absolute path under an agent's `state/` dir or under `shared/`; - /// checked against the allow-list (`docs/trust-boundary/security.md::State-file - /// endpoint`). - path: String, -} - -/// Resolve a caller-supplied path against the allow-listed roots -/// (`agents//state/` and `shared/`). Applies defense-in-depth -/// symlink + traversal checks before serving. Security model and -/// all five layers: `docs/trust-boundary/security.md::State-file endpoint`. -fn resolve_state_path( - raw: &str, -) -> std::result::Result<(std::path::PathBuf, std::fs::Metadata), String> { - use std::os::unix::fs::PermissionsExt as _; - let raw = raw.trim(); - let (mapped, root): (std::path::PathBuf, &str) = - if let Some(rest) = raw.strip_prefix("/agents/") { - ( - std::path::PathBuf::from(format!("{AGENTS_ROOT}/{rest}")), - AGENTS_ROOT, - ) - } else if let Some(rest) = raw.strip_prefix("/shared/") { - ( - std::path::PathBuf::from(format!("{SHARED_ROOT}/{rest}")), - SHARED_ROOT, - ) - } else if let Some(rest) = raw.strip_prefix(&format!("{AGENTS_ROOT}/")) { - ( - std::path::PathBuf::from(format!("{AGENTS_ROOT}/{rest}")), - AGENTS_ROOT, - ) - } else if let Some(rest) = raw.strip_prefix(&format!("{SHARED_ROOT}/")) { - ( - std::path::PathBuf::from(format!("{SHARED_ROOT}/{rest}")), - SHARED_ROOT, - ) - } else { - return Err(format!("path not in allow-list: {raw}")); - }; - reject_symlinks_below(std::path::Path::new(root), &mapped)?; - let canonical = - std::fs::canonicalize(&mapped).map_err(|e| format!("{}: {e}", mapped.display()))?; - if !(canonical.starts_with(AGENTS_ROOT) || canonical.starts_with(SHARED_ROOT)) { - return Err(format!( - "resolved path escapes allow-list: {}", - canonical.display() - )); - } - if let Ok(rel) = canonical.strip_prefix(AGENTS_ROOT) { - let mut components = rel.components(); - let _agent = components.next(); - let dir = components.next().and_then(|c| c.as_os_str().to_str()); - if dir != Some("state") { - return Err(format!( - "only per-agent state/ is readable here ({} dir not allowed)", - dir.unwrap_or("(root)") - )); - } - } - let meta = - std::fs::metadata(&canonical).map_err(|e| format!("stat {}: {e}", canonical.display()))?; - if meta.is_file() { - let mode = meta.permissions().mode(); - if mode & 0o004 == 0 { - return Err(format!( - "{} not world-readable (mode 0{:o}); refusing to proxy non-public file", - canonical.display(), - mode & 0o777, - )); - } - } - Ok((canonical, meta)) -} - -/// Walk every path component under `root` and refuse if any of -/// them is a symlink. The roots themselves (`AGENTS_ROOT`, -/// `SHARED_ROOT`) are hive-c0re-owned and assumed trusted; only -/// the parts the agent / operator can plant matter. Components -/// that don't exist yet are skipped — `canonicalize` reports -/// non-existence separately, and missing-component checks would -/// just race the filesystem. -fn reject_symlinks_below( - root: &std::path::Path, - mapped: &std::path::Path, -) -> std::result::Result<(), String> { - let Ok(rel) = mapped.strip_prefix(root) else { - return Ok(()); - }; - let mut cumulative = root.to_path_buf(); - for component in rel.components() { - match component { - std::path::Component::Normal(name) => { - cumulative.push(name); - match std::fs::symlink_metadata(&cumulative) { - Ok(m) if m.file_type().is_symlink() => { - return Err(format!( - "symlink at {} not allowed (canonicalize would resolve it past the \ - allow-list check; refuse outright)", - cumulative.display() - )); - } - Ok(_) | Err(_) => {} - } - } - std::path::Component::ParentDir => { - return Err(format!( - "path contains `..` traversal below {}; refuse outright", - root.display() - )); - } - _ => {} - } - } - Ok(()) -} - -/// Scan `body` for path-shaped tokens and return those that pass the -/// allow-list + `is_file` check via `resolve_state_path`. Called at -/// broker-message ingest so the dashboard event already carries the -/// verified set; security rules stay in sync with the read endpoint. -pub fn scan_validated_paths(body: &str) -> Vec { - let agents_slash = format!("{AGENTS_ROOT}/"); - let shared_slash = format!("{SHARED_ROOT}/"); - let prefixes: [&str; 4] = ["/agents/", "/shared/", &agents_slash, &shared_slash]; - let mut out = Vec::::new(); - for raw in body.split(|c: char| c.is_whitespace()) { - // Trim trailing natural-language punctuation that wouldn't - // be part of any real path. Inline rather than via a regex - // dep — the set is small and the call is hot. - let token = raw.trim_end_matches([',', ';', ':', ')', ']', '}', '.', '\'', '"']); - if token.is_empty() { - continue; - } - if !prefixes.iter().any(|p| token.starts_with(p)) { - continue; - } - // Cheap dedupe — typical message has 0-3 refs. - if out.iter().any(|s| s == token) { - continue; - } - if let Ok((_canonical, meta)) = resolve_state_path(token) - && meta.is_file() - { - out.push(token.to_owned()); - } - } - out -} - -/// Serve an allow-listed file. -/// -/// Raster images get their real content-type; everything else is -/// served as (possibly truncated) text. -#[utoipa::path( - get, - path = "/api/state-file", - params(StateFileQuery), - responses( - (status = 200, description = "file contents (text, truncated at 1 MiB) or image bytes"), - (status = 500, description = "path outside the allow-list, not a regular file, or read failed"), - ), - tag = "state_files" -)] -pub(super) async fn get_state_file( - axum::extract::Query(q): axum::extract::Query, -) -> Response { - const MAX_BYTES: usize = 1 << 20; // 1 MiB - let (canonical, meta) = match resolve_state_path(&q.path) { - Ok(pair) => pair, - Err(e) => return error_response(&format!("state-file: {e}")), - }; - if !meta.is_file() { - return error_response(&format!( - "state-file: {} is not a regular file", - canonical.display() - )); - } - let size = meta.len(); - let bytes = match std::fs::read(&canonical) { - Ok(b) => b, - Err(e) => return error_response(&format!("state-file: read {}: {e}", canonical.display())), - }; - // Raster images: serve the raw bytes with their real content-type - // so the dashboard can render them in an . Not truncated — - // a clipped binary is corrupt, so over-cap images are rejected - // instead. (SVG stays on the text path: it's text, and the client - // renders it via a data: URI.) - if let Some(ct) = image_content_type(&canonical) { - if bytes.len() > MAX_BYTES { - return error_response(&format!( - "state-file: image {} is {size} bytes, over the {MAX_BYTES}-byte preview cap", - canonical.display() - )); - } - return ([("content-type", ct)], bytes).into_response(); - } - let truncated = bytes.len() > MAX_BYTES; - let body_bytes = if truncated { - &bytes[..MAX_BYTES] - } else { - &bytes[..] - }; - let mut body = String::from_utf8_lossy(body_bytes).into_owned(); - if truncated { - use std::fmt::Write as _; - let _ = write!( - body, - "\n\n--- truncated at {MAX_BYTES} of {size} bytes ---\n" - ); - } - ([("content-type", "text/plain; charset=utf-8")], body).into_response() -} - -/// Content-type for a raster image the dashboard can preview in an -/// ``, keyed off the file extension. `None` for non-image, SVG, -/// and text files (SVG is served on the text path and rendered -/// client-side via a `data:` URI). -fn image_content_type(path: &Path) -> Option<&'static str> { - let ext = path.extension()?.to_str()?.to_ascii_lowercase(); - Some(match ext.as_str() { - "png" => "image/png", - "jpg" | "jpeg" => "image/jpeg", - "gif" => "image/gif", - "webp" => "image/webp", - "bmp" => "image/bmp", - "ico" => "image/x-icon", - "avif" => "image/avif", - _ => return None, - }) -} - -#[cfg(test)] -mod tests { - use super::*; - use std::os::unix::fs::symlink; - - /// Make a unique tmp subdir for the calling test. Caller is responsible - /// for cleanup (we leak on panic, fine for ephemeral CI runs). - fn tmproot(tag: &str) -> std::path::PathBuf { - let ts = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map_or(0, |d| d.as_nanos()); - let p = std::env::temp_dir().join(format!("hyperhive-test-{tag}-{ts}")); - std::fs::create_dir_all(&p).unwrap(); - p - } - - #[test] - fn reject_symlinks_below_accepts_plain_dirs_and_files() { - let root = tmproot("symlink-ok"); - std::fs::create_dir_all(root.join("alice/state")).unwrap(); - std::fs::write(root.join("alice/state/notes.md"), b"hi").unwrap(); - assert!(reject_symlinks_below(&root, &root.join("alice/state/notes.md")).is_ok()); - } - - #[test] - fn reject_symlinks_below_rejects_leaf_symlink() { - let root = tmproot("symlink-leaf"); - std::fs::create_dir_all(root.join("alice/state")).unwrap(); - // Plant a symlink that points anywhere; resolve_state_path's - // canonicalize would happily resolve it past the allow-list - // check, so we have to refuse at the un-canonical layer. - symlink("/etc/shadow", root.join("alice/state/peek")).unwrap(); - let err = reject_symlinks_below(&root, &root.join("alice/state/peek")).unwrap_err(); - assert!(err.contains("symlink at"), "msg = {err}"); - assert!(err.contains("peek"), "msg = {err}"); - } - - #[test] - fn reject_symlinks_below_rejects_directory_symlink_in_middle() { - let root = tmproot("symlink-mid"); - std::fs::create_dir_all(root.join("real/state")).unwrap(); - std::fs::write(root.join("real/state/secret.md"), b"hi").unwrap(); - // alice's "state" dir is actually a symlink to real/state — a - // sub-agent shouldn't be able to plant this and proxy real's - // private files via the dashboard. - std::fs::create_dir_all(root.join("alice")).unwrap(); - symlink(root.join("real/state"), root.join("alice/state")).unwrap(); - let err = reject_symlinks_below(&root, &root.join("alice/state/secret.md")).unwrap_err(); - assert!(err.contains("symlink at"), "msg = {err}"); - } - - #[test] - fn reject_symlinks_below_rejects_parent_dir_traversal() { - let root = tmproot("symlink-dotdot"); - // `..` doesn't survive canonicalize anyway, but we want a - // friendlier error than "path escapes allow-list" — refusing - // upfront also avoids walking ancestors with `symlink_metadata`. - let p = root.join("alice/state/../escape"); - let err = reject_symlinks_below(&root, &p).unwrap_err(); - assert!(err.contains("`..`"), "msg = {err}"); - } - - #[test] - fn reject_symlinks_below_passes_through_when_path_not_under_root() { - // resolve_state_path's earlier allow-list check would reject - // this; reject_symlinks_below stays a no-op so the caller - // surfaces the better-fit error. - let root = std::path::Path::new("/var/lib/hyperhive/agents"); - assert!(reject_symlinks_below(root, std::path::Path::new("/etc/shadow")).is_ok()); - } -} diff --git a/hive-c0re/src/dashboard_events.rs b/hive-c0re/src/dashboard_events.rs index 8327adc2..d640dc80 100644 --- a/hive-c0re/src/dashboard_events.rs +++ b/hive-c0re/src/dashboard_events.rs @@ -14,12 +14,6 @@ use chrono::{DateTime, Utc}; #[serde(rename_all = "snake_case", tag = "kind")] pub enum DashboardEvent { /// Broker `Sent` event mirrored onto the dashboard channel. - /// `file_refs` carries every path-shaped token in `body` that - /// hive-c0re verified is a regular file under the allow-listed - /// roots (per-agent `state/` + `shared/`). The forwarder - /// pre-validates so the dashboard doesn't need a probe - /// endpoint — the client renders anchors only for tokens that - /// appear in this list, everything else stays plain text. Sent { seq: u64, /// Broker row id. Allows the dashboard to track reply threads. @@ -30,11 +24,8 @@ pub enum DashboardEvent { at: DateTime, #[serde(default, skip_serializing_if = "Option::is_none")] in_reply_to: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - file_refs: Vec, }, /// Broker `Delivered` event mirrored onto the dashboard channel. - /// `file_refs` is the same shape as `Sent`. Delivered { seq: u64, /// Broker row id. Allows the dashboard to track reply threads. @@ -45,8 +36,6 @@ pub enum DashboardEvent { at: DateTime, #[serde(default, skip_serializing_if = "Option::is_none")] in_reply_to: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - file_refs: Vec, }, /// A lifecycle action started for an agent (spawn / start / stop /// / restart / rebuild / destroy). Clients render a spinner next @@ -201,7 +190,6 @@ mod tests { body: String::new(), at: hive_sh4re::wire_time::from_secs(0), in_reply_to: None, - file_refs: Vec::new(), }, DashboardEvent::Delivered { seq: 1, @@ -211,7 +199,6 @@ mod tests { body: String::new(), at: hive_sh4re::wire_time::from_secs(0), in_reply_to: None, - file_refs: Vec::new(), }, DashboardEvent::TransientSet { seq: 1, diff --git a/hive-c0re/src/main.rs b/hive-c0re/src/main.rs index 31e3fb6c..1c91613a 100644 --- a/hive-c0re/src/main.rs +++ b/hive-c0re/src/main.rs @@ -469,7 +469,6 @@ fn spawn_broker_to_dashboard_forwarder(coord: Arc) { at, in_reply_to, }) => { - let file_refs = dashboard::scan_validated_paths(&body); coord.emit_dashboard_event(DashboardEvent::Sent { seq: coord.next_seq(), id, @@ -478,7 +477,6 @@ fn spawn_broker_to_dashboard_forwarder(coord: Arc) { body, at: hive_sh4re::wire_time::from_secs(at), in_reply_to, - file_refs, }); } Ok(MessageEvent::Delivered { @@ -489,7 +487,6 @@ fn spawn_broker_to_dashboard_forwarder(coord: Arc) { at, in_reply_to, }) => { - let file_refs = dashboard::scan_validated_paths(&body); coord.emit_dashboard_event(DashboardEvent::Delivered { seq: coord.next_seq(), id, @@ -498,7 +495,6 @@ fn spawn_broker_to_dashboard_forwarder(coord: Arc) { body, at: hive_sh4re::wire_time::from_secs(at), in_reply_to, - file_refs, }); } Err(tokio::sync::broadcast::error::RecvError::Lagged(n)) => { diff --git a/hive-c0re/src/paths.rs b/hive-c0re/src/paths.rs index e35e8efb..d7cbddff 100644 --- a/hive-c0re/src/paths.rs +++ b/hive-c0re/src/paths.rs @@ -195,8 +195,7 @@ pub fn meta_git_index_lock() -> PathBuf { meta_root().join(".git/index.lock") } -/// `shared/` — the cross-agent `/shared` scratch space. A `&str` (the -/// dashboard state-file allow-list uses it for prefix checks), so it +/// `shared/` — the cross-agent `/shared` scratch space. A `&str`, so it /// stays a const; [`shared_root`] wraps it. // nix: bind-mounted into every agent container as `/shared` (the harness nix modules) — must match. pub const SHARED_ROOT: &str = "/var/lib/hyperhive/shared"; diff --git a/hive-forge/tests/credential_helper.rs b/hive-forge/tests/credential_helper.rs index 6c79de09..a4436a86 100644 --- a/hive-forge/tests/credential_helper.rs +++ b/hive-forge/tests/credential_helper.rs @@ -12,9 +12,8 @@ use std::process::{Command, Stdio}; const TOKEN: &str = "test-token-value"; /// A unique scratch `HYPERHIVE_STATE_DIR`, seeded with a `forge-token` -/// file. Matches the manual-tmpdir pattern `hive-c0re`'s -/// `dashboard::state_files` tests use — this crate carries no `tempfile` -/// dependency to reach for instead. +/// file. Built by hand under the system temp dir, since this crate carries no +/// `tempfile` dependency to reach for instead. fn scratch_state_dir(tag: &str) -> PathBuf { let ts = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH)