diff --git a/hive-priv/src/main.rs b/hive-priv/src/main.rs index 4511050a..dbfb4ee6 100644 --- a/hive-priv/src/main.rs +++ b/hive-priv/src/main.rs @@ -332,48 +332,27 @@ async fn write_line_event(writer: &mut OwnedWriteHalf, stream: PrivStream, data: /// For streaming ops (`CreateContainer`/`UpdateContainer` with `stream: true`) /// output lines are forwarded to `writer` as `PrivEvent::Line` messages and /// the returned strings are empty. -// One match arm per priv op — a flat 1:1 dispatch table. The length tracks -// the op count, not complexity; splitting it would just scatter the mapping. -#[allow(clippy::too_many_lines)] -/// Execute one validated request. /// /// `fd` is the descriptor that arrived with this request, already checked /// against the operation by [`check_fd_agreement`]: `Some` exactly for /// the variants that stream into a caller-supplied descriptor, `None` /// for every other operation. +// One match arm per priv op — a flat 1:1 dispatch table. Every arm either +// delegates directly or validates then delegates; an op whose handling is +// more than that gets its own named function instead, so the match's length +// tracks the op count, not complexity. +#[allow(clippy::too_many_lines)] async fn exec( req: PrivRequest, fd: Option, writer: &mut OwnedWriteHalf, ) -> Result<(String, String)> { match req { - PrivRequest::StartContainer { ref name } => { - validate_container_name(name)?; - let machine = container_system_name(name); - // Clear any start-limit lockout left by earlier failures so a - // now-correct start isn't blocked. nixos-container start does not - // do this itself. Best-effort: if the unit doesn't exist yet - // (first-time create) reset-failed is a no-op and we proceed. - let _ = Command::new("systemctl") - .args(["reset-failed", &format!("container@{machine}.service")]) - .status() - .await; - container_run(&["start", &machine]).await - } + PrivRequest::StartContainer { ref name } => start_container(name).await, - PrivRequest::StopContainer { ref name } => { - validate_container_name(name)?; - stop_and_release(&container_system_name(name)).await - } + PrivRequest::StopContainer { ref name } => stop_container(name).await, - PrivRequest::KillContainer { ref name } => { - validate_container_name(name)?; - // nixos-container has no kill verb. Use machinectl to send SIGKILL - // to all processes in the container — the right semantics for a - // forced shutdown after a graceful stop has already been attempted. - let machine = container_system_name(name); - machinectl_run(&["kill", &machine, "--signal=SIGKILL"]).await - } + PrivRequest::KillContainer { ref name } => kill_container(name).await, PrivRequest::UpdateContainer { ref name, stream } => { container_flake_action("update", name, stream, writer).await @@ -383,20 +362,14 @@ async fn exec( container_flake_action("create", name, stream, writer).await } - PrivRequest::DestroyContainer { ref name } => { - validate_container_name(name)?; - container_run(&["destroy", &container_system_name(name)]).await - } + PrivRequest::DestroyContainer { ref name } => destroy_container(name).await, PrivRequest::ListContainers => container_run(&["list"]).await, PrivRequest::ReadContainerJournal { ref container, ref query, - } => { - validate_container_system_name(container)?; - read_container_journal(container, query).await - } + } => exec_read_container_journal(container, query).await, PrivRequest::WriteNspawnFlags { ref container, @@ -419,105 +392,41 @@ async fn exec( PrivRequest::ReloadGatewayNginx => sync_gateway_nginx().await, - PrivRequest::RunForgeAdmin { ref args } => { - for arg in args { - validate_forge_admin_arg(arg)?; - } - run_forge_admin(args).await - } + PrivRequest::RunForgeAdmin { ref args } => exec_forge_admin(args).await, PrivRequest::SetAgentPaused { ref agent_name, paused, - } => { - validate_agent_name(agent_name)?; - set_agent_paused(agent_name, paused) - } + } => exec_set_agent_paused(agent_name, paused), PrivRequest::WriteAgentForgeToken { ref agent_name, ref token, - } => { - validate_agent_name(agent_name)?; - write_agent_state_file(agent_name, "forge-token", &format!("{token}\n")) - } + } => write_forge_token(agent_name, token), PrivRequest::WriteAgentMatrixToken { ref agent_name, ref token, ref account, ref homeserver, - } => { - validate_agent_name(agent_name)?; - // Build the token filename. `None` → the hive account's - // `matrix-token`; `Some(a)` → `matrix-token-`. The account - // suffix MUST be validated as a plain identifier (no `/`, `.`, - // `..`) before it goes into the filename, or a crafted account - // could traverse out of the state dir — `write_agent_state_file` - // trusts its `filename` argument. - let filename = match account { - None => "matrix-token".to_owned(), - Some(a) => { - validate_name_chars(a)?; - format!("matrix-token-{a}") - } - }; - let res = write_agent_state_file(agent_name, &filename, &format!("{token}\n"))?; - // For an extra account, persist its homeserver in a sidecar - // (`matrix-account-.json`) so the daemon can auto-discover the - // account without a static `matrixAccounts` config entry. Only - // when both `account` and `homeserver` are present; the account - // suffix is already validated above. - if let (Some(a), Some(hs)) = (account, homeserver) { - let meta = serde_json::to_string(&MatrixAccountSidecar { - homeserver: hs.as_str(), - }) - .context("serialize matrix account sidecar")?; - write_agent_state_file(agent_name, &format!("matrix-account-{a}.json"), &meta)?; - } - Ok(res) - } + } => write_matrix_token(agent_name, token, account.as_deref(), homeserver.as_deref()), PrivRequest::WriteAgentGithubToken { ref agent_name, ref token, - } => { - validate_agent_name(agent_name)?; - write_agent_state_file(agent_name, "github-token", &format!("{token}\n")) - } + } => write_github_token(agent_name, token), PrivRequest::WriteAgentExtraForgeAccount { ref agent_name, ref label, ref base_url, ref token, - } => { - validate_agent_name(agent_name)?; - validate_name_chars(label)?; - let res = write_agent_state_file( - agent_name, - &format!("forge-{label}-token"), - &format!("{token}\n"), - )?; - // Sidecar carries the base URL — there's no host-side nix config - // for extra forges, so this is the only place it's persisted. - let meta = serde_json::to_string(&ForgeSidecar { - base_url: base_url.as_str(), - }) - .context("serialize forge account sidecar")?; - write_agent_state_file(agent_name, &format!("forge-{label}.json"), &meta)?; - Ok(res) - } + } => write_extra_forge_account(agent_name, label, base_url, token), PrivRequest::DeleteAgentExtraForgeAccount { ref agent_name, ref label, - } => { - validate_agent_name(agent_name)?; - validate_name_chars(label)?; - delete_agent_state_file(agent_name, &format!("forge-{label}-token"))?; - delete_agent_state_file(agent_name, &format!("forge-{label}.json")) - } + } => delete_extra_forge_account(agent_name, label), PrivRequest::RestartMatrixDaemon { ref agent_name } => { restart_matrix_daemon(agent_name).await @@ -530,52 +439,37 @@ async fn exec( } PrivRequest::EnsureAgentSubvolume { ref agent_name } => { - validate_agent_name(agent_name)?; - ensure_agent_subvolume(agent_name).await + exec_ensure_agent_subvolume(agent_name).await } PrivRequest::DeleteAgentSubvolume { ref agent_name } => { - validate_agent_name(agent_name)?; - delete_agent_subvolume(agent_name).await + exec_delete_agent_subvolume(agent_name).await } PrivRequest::EnsureBtrfsQuota => ensure_btrfs_quota().await, PrivRequest::ReadSubvolumeUsage { ref agent_name } => { - validate_agent_name(agent_name)?; - read_subvolume_usage(agent_name).await + exec_read_subvolume_usage(agent_name).await } PrivRequest::SetSubvolumeQuota { ref agent_name, limit_bytes, - } => { - validate_agent_name(agent_name)?; - set_subvolume_quota(agent_name, limit_bytes).await - } + } => exec_set_subvolume_quota(agent_name, limit_bytes).await, PrivRequest::UpgradeAgentSubvolume { ref agent_name } => { - validate_agent_name(agent_name)?; - upgrade_agent_subvolume(agent_name).await + exec_upgrade_agent_subvolume(agent_name).await } PrivRequest::SnapshotAgentSubvolume { ref agent_name, ref snapshot_name, - } => { - validate_agent_name(agent_name)?; - validate_snapshot_name(snapshot_name)?; - snapshot_agent_subvolume(agent_name, snapshot_name).await - } + } => exec_snapshot_agent_subvolume(agent_name, snapshot_name).await, PrivRequest::DeleteAgentSnapshot { ref agent_name, ref snapshot_name, - } => { - validate_agent_name(agent_name)?; - validate_snapshot_name(snapshot_name)?; - delete_agent_snapshot(agent_name, snapshot_name).await - } + } => exec_delete_agent_snapshot(agent_name, snapshot_name).await, PrivRequest::SendAgentSnapshotToFile { ref agent_name, @@ -583,13 +477,7 @@ async fn exec( ref parent_snapshot_name, ref dest_file_name, } => { - validate_agent_name(agent_name)?; - validate_snapshot_name(snapshot_name)?; - if let Some(parent) = parent_snapshot_name { - validate_snapshot_name(parent)?; - } - validate_credential_name(dest_file_name)?; - send_agent_snapshot_to_file( + exec_send_agent_snapshot_to_file( agent_name, snapshot_name, parent_snapshot_name.as_deref(), @@ -603,17 +491,11 @@ async fn exec( ref snapshot_name, ref parent_snapshot_name, } => { - validate_agent_name(agent_name)?; - validate_snapshot_name(snapshot_name)?; - if let Some(parent) = parent_snapshot_name { - validate_snapshot_name(parent)?; - } - let dest = fd.context("no descriptor to stream into")?; - send_agent_snapshot_to_fd( + exec_send_agent_snapshot_to_fd( agent_name, snapshot_name, parent_snapshot_name.as_deref(), - dest, + fd, ) .await } @@ -622,6 +504,232 @@ async fn exec( } } +/// `StartContainer`: clear any start-limit lockout left by earlier failures +/// so a now-correct start isn't blocked (`nixos-container start` does not do +/// this itself). Best-effort — if the unit doesn't exist yet (first-time +/// create), `reset-failed` is a no-op and the start proceeds regardless. +async fn start_container(name: &str) -> Result<(String, String)> { + validate_container_name(name)?; + let machine = container_system_name(name); + let _ = Command::new("systemctl") + .args(["reset-failed", &format!("container@{machine}.service")]) + .status() + .await; + container_run(&["start", &machine]).await +} + +/// `RunForgeAdmin`: every arg must pass [`validate_forge_admin_arg`] before +/// the admin CLI ever sees it. +async fn exec_forge_admin(args: &[String]) -> Result<(String, String)> { + for arg in args { + validate_forge_admin_arg(arg)?; + } + run_forge_admin(args).await +} + +/// `WriteAgentMatrixToken`: `account = None` writes the hive account's +/// `matrix-token`; `Some(a)` writes `matrix-token-`. The account suffix +/// MUST be validated as a plain identifier (no `/`, `.`, `..`) before it goes +/// into the filename, or a crafted account could traverse out of the state +/// dir — `write_agent_state_file` trusts its `filename` argument. When both +/// `account` and `homeserver` are `Some`, also persists a +/// `matrix-account-.json` sidecar so the daemon can auto-discover the +/// extra account without a static `matrixAccounts` config entry. +fn write_matrix_token( + agent_name: &str, + token: &str, + account: Option<&str>, + homeserver: Option<&str>, +) -> Result<(String, String)> { + validate_agent_name(agent_name)?; + let filename = match account { + None => "matrix-token".to_owned(), + Some(a) => { + validate_name_chars(a)?; + format!("matrix-token-{a}") + } + }; + let res = write_agent_state_file(agent_name, &filename, &format!("{token}\n"))?; + if let (Some(a), Some(hs)) = (account, homeserver) { + let meta = serde_json::to_string(&MatrixAccountSidecar { homeserver: hs }) + .context("serialize matrix account sidecar")?; + write_agent_state_file(agent_name, &format!("matrix-account-{a}.json"), &meta)?; + } + Ok(res) +} + +/// `WriteAgentExtraForgeAccount`: writes the token, then a +/// `forge-