diff --git a/hive-priv/src/main.rs b/hive-priv/src/main.rs index 4511050a..43b4fc8e 100644 --- a/hive-priv/src/main.rs +++ b/hive-priv/src/main.rs @@ -332,34 +332,23 @@ 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)?; @@ -419,12 +408,7 @@ 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, @@ -447,36 +431,7 @@ async fn exec( 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, @@ -491,23 +446,7 @@ async fn exec( 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, @@ -622,6 +561,83 @@ 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-