diff --git a/frontend/packages/dashboard/src/logs.js b/frontend/packages/dashboard/src/logs.js index c6807244..38608f75 100644 --- a/frontend/packages/dashboard/src/logs.js +++ b/frontend/packages/dashboard/src/logs.js @@ -45,15 +45,8 @@ import { createTabStrip } from '@hive/shared/tabs.js'; return 'fetched ' + (ageSecs < 5 ? 'just now' : fmtAgeSecs(ageSecs) + ' ago'); } - // Names from `state.infra_containers` (hive-ci / hive-forge / hive-gateway - // / hive-matrix) — kept in sync with the fetched list so fetchAgent() can - // tell an infra container apart from an agent and skip the (inapplicable) - // unit filter for it. - let infraContainerNames = new Set(); - - // Populate agent selector from /api/state (agents, then the four infra - // containers in their own optgroup), then honour any `?agent=` / `?unit=` - // URL params (used by the per-agent ⋮ menu's deep-link). + // Populate agent selector from /api/state, then honour any `?agent=` / + // `?unit=` URL params (used by the per-agent ⋮ menu's deep-link). async function loadAgentList() { try { const resp = await fetch('/api/state'); @@ -65,14 +58,6 @@ import { createTabStrip } from '@hive/shared/tabs.js'; for (const c of (state.containers || [])) { agentSelect.append(el('option', { value: c.name }, c.name)); } - infraContainerNames = new Set((state.infra_containers || []).map((c) => c.name)); - if (infraContainerNames.size) { - const infraGroup = el('optgroup', { label: 'infra' }); - for (const name of infraContainerNames) { - infraGroup.append(el('option', { value: name }, name)); - } - agentSelect.append(infraGroup); - } // Deep-link: honour ?agent= and ?unit= URL params. const urlAgent = new URLSearchParams(location.search).get('agent'); const urlUnit = new URLSearchParams(location.search).get('unit'); @@ -85,24 +70,12 @@ import { createTabStrip } from '@hive/shared/tabs.js'; .some((o) => o.value === urlUnit); if (unitFound) agentUnitSelect.value = urlUnit; } - syncUnitSelectForSelection(); fetchAgent(); } } } catch { /**/ } } - // Infra containers don't run the per-agent hive daemons, so the unit - // filter is meaningless for them — disable the selector and always fetch - // the full machine journal to avoid a picked unit silently doing nothing. - function syncUnitSelectForSelection() { - if (!agentUnitSelect) return; - const isInfra = infraContainerNames.has(agentSelect ? agentSelect.value : ''); - agentUnitSelect.disabled = isInfra; - if (isInfra) agentUnitSelect.value = ''; - } - if (agentSelect) agentSelect.addEventListener('change', syncUnitSelectForSelection); - let agentFetching = false; let agentLastFetch = 0; async function fetchAgent() { @@ -113,8 +86,7 @@ import { createTabStrip } from '@hive/shared/tabs.js'; agentFetching = true; agentOutput.textContent = 'fetching…'; if (agentFetchTs) agentFetchTs.hidden = true; - const isInfra = infraContainerNames.has(name); - const unit = (!isInfra && agentUnitSelect) ? agentUnitSelect.value : ''; + const unit = agentUnitSelect ? agentUnitSelect.value : ''; const params = new URLSearchParams({ lines: '500' }); if (unit) params.set('unit', unit); try { diff --git a/hive-c0re/src/dashboard/journal.rs b/hive-c0re/src/dashboard/journal.rs index 5eb739f3..f579b19c 100644 --- a/hive-c0re/src/dashboard/journal.rs +++ b/hive-c0re/src/dashboard/journal.rs @@ -1,13 +1,10 @@ //! Journal-read endpoints for the dashboard. //! -//! `GET /api/journal/{name}` reads a managed agent container's journal, OR -//! one of the four hive infra containers (`hive-ci`, `hive-forge`, -//! `hive-gateway`, `hive-matrix` — [`hive_priv_sock::InfraContainer`] is the -//! allowlist), via the root helper (`journalctl -M`, delegated to hive-priv -//! since hive-c0re is unprivileged). `GET /api/journal-host` reads -//! host-side journald, both gated by an allow-list of known units so -//! arbitrary unit names can't be probed. Operator-only by virtue of the -//! dashboard binding host-only. +//! `GET /api/journal/{name}` reads a managed container's journal via the +//! root helper (`journalctl -M`, delegated to hive-priv since hive-c0re is +//! unprivileged). `GET /api/journal-host` reads host-side journald, both +//! gated by an allow-list of known units so arbitrary unit names can't be +//! probed. Operator-only by virtue of the dashboard binding host-only. use axum::{ extract::Path as AxumPath, @@ -36,23 +33,10 @@ pub(super) struct JournalQuery { /// Operator-only by virtue of the dashboard being host-bound. hive-c0re /// runs unprivileged (privsep), so the `-M` read — which enters the /// container namespace and needs root — is delegated to hive-priv. -/// -/// `name` is either a managed agent name (`iris`, optionally already -/// carrying the `h-` prefix) or one of the four infra container names -/// (`hive-ci` / `hive-forge` / `hive-gateway` / `hive-matrix` — see -/// [`hive_priv_sock::InfraContainer`]). Infra containers don't run the -/// per-agent hive daemons, so `unit` is ignored for them — always the -/// full machine journal. pub(super) async fn get_journal( AxumPath(name): AxumPath, axum::extract::Query(q): axum::extract::Query, ) -> Result { - let lines = q.lines.unwrap_or(500).min(5000); - - if let Ok(infra) = name.parse::() { - return read_journal_response(infra.unit_name(), None, lines).await; - } - // Defense-in-depth format check so weird chars never reach the // shellout below — the `lifecycle::list()` existence check would // catch them anyway, but rejecting at the boundary keeps the @@ -70,6 +54,7 @@ pub(super) async fn get_journal( return Err(ProblemDetails::from_status_code(StatusCode::NOT_FOUND) .with_detail(format!("journal: no managed container {prefixed:?}"))); } + let lines = q.lines.unwrap_or(500).min(5000); let unit = match q.unit.as_deref().filter(|s| !s.is_empty()) { Some(u) => { // accept any of the per-container hive daemons [.service] — @@ -93,21 +78,8 @@ pub(super) async fn get_journal( } None => None, }; - read_journal_response(&prefixed, unit, lines).await -} - -/// Shared `journalctl -M [-u ]` shellout + response -/// formatting for [`get_journal`], factored out so the infra-container -/// branch (no `unit` filtering) and the agent-container branch (allow-listed -/// `unit` filtering) don't duplicate the priv-client call + stdout/stderr -/// combining. -async fn read_journal_response( - machine: &str, - unit: Option, - lines: u32, -) -> Result { match crate::priv_client::read_container_journal( - machine, + &prefixed, hive_priv_sock::JournalQuery { lines, boot: true, diff --git a/hive-c0re/src/forge/mod.rs b/hive-c0re/src/forge/mod.rs index 7fdfc35f..f9be3d02 100644 --- a/hive-c0re/src/forge/mod.rs +++ b/hive-c0re/src/forge/mod.rs @@ -169,15 +169,9 @@ pub(crate) fn api(token: &str) -> Result { /// /// Called by both `ensure_all()` (startup sweep) and `rebuild_agent` /// (per-rebuild) so the two paths stay equivalent. -/// Returns `true` if all steps succeeded, `false` if any step failed. The -/// caller can use the return value to aggregate per-agent failures into a -/// dashboard warning (see [`ensure_all`]); the rebuild path ignores it and -/// relies on the journal `warn!` lines alone (a rebuild is its own retry). -pub async fn sync_agent(name: &str, core_token: Option<&str>) -> bool { - let mut ok = true; +pub async fn sync_agent(name: &str, core_token: Option<&str>) { if let Err(e) = ensure_user_for(name).await { tracing::warn!(%name, error = ?e, "forge: ensure_user failed"); - ok = false; } // Align email to match the git user.email set by meta::render_flake // so commits link to the agent's Forgejo profile. Best-effort; @@ -194,11 +188,9 @@ pub async fn sync_agent(name: &str, core_token: Option<&str>) -> bool { // was down. if let Err(e) = ensure_config_repo(name).await { tracing::warn!(%name, error = ?e, "forge: ensure_config_repo failed"); - ok = false; } if let Err(e) = push_config(name).await { tracing::warn!(%name, error = ?e, "forge: push_config failed"); - ok = false; } // Grant read-only access to core/meta and wire the `meta` remote // into the proposed repo so agents can fetch their deployment context. @@ -206,11 +198,9 @@ pub async fn sync_agent(name: &str, core_token: Option<&str>) -> bool { && let Err(e) = meta_read_access(name, token).await { tracing::warn!(%name, error = ?e, "forge: ensure_meta_read_access failed"); - ok = false; } if let Err(e) = ensure_meta_remote(name).await { tracing::warn!(%name, error = ?e, "forge: ensure_meta_remote failed"); - ok = false; } // Grant read-only access to internal/docs so the agent can clone // the operator-curated shared skills/runbook repo. Best-effort. @@ -218,10 +208,8 @@ pub async fn sync_agent(name: &str, core_token: Option<&str>) -> bool { && let Err(e) = shared_docs_access(name, token).await { tracing::warn!(%name, error = ?e, "forge: shared_docs_access failed"); - ok = false; } // internal/knowledge is public — no per-agent collaborator grant needed. - ok } /// The `core_token.is_some()` half of [`ensure_all`]: orgs, teams, the meta @@ -358,27 +346,11 @@ pub async fn ensure_all() { ); return; }; - let mut sync_failed: Vec = Vec::new(); for c in containers { let Some(name) = c.strip_prefix(crate::lifecycle::AGENT_PREFIX) else { continue; }; - if !sync_agent(name, core_token.as_deref()).await { - sync_failed.push(name.to_owned()); - } - } - if !sync_failed.is_empty() { - // Use static_kind to mint a `&'static str` key from the failed-agent - // list (bounded leak: one per hive-c0re boot, not per request). - let key = static_kind(format!("forge_sync_agent_{}", sync_failed.join("_"))); - crate::warnings::set_boot_warning( - key, - "warn", - format!( - "forge: per-agent sync failed for: {} (see journal for per-step detail)", - sync_failed.join(", ") - ), - ); + sync_agent(name, core_token.as_deref()).await; } }