refactor(#2302): type socket wire fields as ident, validated by serde on deserialize

This commit is contained in:
damocles 2026-07-20 21:21:03 +02:00 committed by mara
commit 84b750fba5
33 changed files with 333 additions and 263 deletions

View file

@ -17,6 +17,7 @@ clap_complete.workspace = true
clap-markdown = "0.1"
hive-host-sock.workspace = true
hive-sh4re.workspace = true
hive-types.workspace = true
indicatif.workspace = true
serde_json.workspace = true
tokio.workspace = true

View file

@ -14,7 +14,7 @@ async fn agents_restart(socket: &Path, name: &str, no_wait: bool) -> Result<()>
let resp = crate::client::request(
socket,
hive_host_sock::HostRequest::Restart {
name: name.to_owned(),
name: crate::util::parse_ident(name)?,
},
)
.await
@ -135,18 +135,23 @@ pub(crate) async fn run_agents(socket: &Path, cmd: AgentsCmd) -> Result<()> {
AgentsCmd::Restart { name, no_wait } => agents_restart(socket, &name, no_wait).await,
AgentsCmd::RestartAll { no_wait } => agents_restart_all(socket, no_wait).await,
AgentsCmd::Spawn { name } => {
let name = crate::util::parse_ident(&name)?;
render(crate::client::request(socket, HostRequest::Spawn { name }).await?)
}
AgentsCmd::RequestSpawn { name } => {
let name = crate::util::parse_ident(&name)?;
render(crate::client::request(socket, HostRequest::RequestSpawn { name }).await?)
}
AgentsCmd::Kill { name } => {
let name = crate::util::parse_ident(&name)?;
render(crate::client::request(socket, HostRequest::Kill { name }).await?)
}
AgentsCmd::Destroy { name, purge } => {
let name = crate::util::parse_ident(&name)?;
render(crate::client::request(socket, HostRequest::Destroy { name, purge }).await?)
}
AgentsCmd::Rebuild { name } => {
let name = crate::util::parse_ident(&name)?;
render(crate::client::request(socket, HostRequest::Rebuild { name }).await?)
}
AgentsCmd::SetParent {
@ -154,7 +159,12 @@ pub(crate) async fn run_agents(socket: &Path, cmd: AgentsCmd) -> Result<()> {
parent,
root,
} => {
let new_parent = if root { None } else { parent };
let child = crate::util::parse_ident(&child)?;
let new_parent = if root {
None
} else {
parent.map(|p| crate::util::parse_ident(&p)).transpose()?
};
render(
crate::client::request(socket, HostRequest::SetParent { child, new_parent })
.await?,

View file

@ -25,7 +25,7 @@ pub(crate) async fn forge_create_user(
daemon_request(
socket,
hive_host_sock::HostRequest::ForgeCreateUser {
name: name.to_owned(),
name: crate::util::parse_ident(name)?,
password,
},
"forge",
@ -45,7 +45,7 @@ pub(crate) async fn forge_reconcile_config(
daemon_request(
socket,
HostRequest::ReconcileConfigStatus {
agent: agent.to_owned(),
agent: crate::util::parse_ident(agent)?,
verbose,
},
"forge",
@ -62,7 +62,7 @@ pub(crate) async fn forge_reconcile_config(
daemon_request(
socket,
HostRequest::ReconcileConfigApply {
agent: agent.to_owned(),
agent: crate::util::parse_ident(agent)?,
direction,
},
"forge",

View file

@ -39,7 +39,7 @@ pub(crate) async fn github_set_token(
daemon_request(
socket,
hive_host_sock::HostRequest::SetAgentGithubToken {
agent: agent.to_owned(),
agent: crate::util::parse_ident(agent)?,
token,
},
"github",

View file

@ -59,7 +59,7 @@ async fn matrix_create_user(
matrix_request(
socket,
hive_host_sock::HostRequest::MatrixCreateUser {
name: name.to_owned(),
name: crate::util::parse_ident(name)?,
password,
},
)
@ -74,7 +74,7 @@ async fn matrix_promote_user(socket: &Path, name: &str) -> Result<()> {
matrix_request(
socket,
hive_host_sock::HostRequest::MatrixPromoteUser {
name: name.to_owned(),
name: crate::util::parse_ident(name)?,
},
)
.await
@ -95,7 +95,7 @@ async fn matrix_reset_password(socket: &Path, name: &str) -> Result<()> {
matrix_request(
socket,
hive_host_sock::HostRequest::MatrixResetPassword {
name: name.to_owned(),
name: crate::util::parse_ident(name)?,
},
)
.await

View file

@ -20,7 +20,7 @@ pub(crate) async fn quota_show(socket: &Path, name: Option<&str>) -> Result<()>
let resp = crate::client::request(
socket,
hive_host_sock::HostRequest::QuotaShow {
name: name.map(str::to_owned),
name: name.map(crate::util::parse_ident).transpose()?,
},
)
.await
@ -60,7 +60,7 @@ pub(crate) async fn quota_limit(socket: &Path, name: &str, size: &str) -> Result
daemon_request(
socket,
hive_host_sock::HostRequest::QuotaLimit {
name: name.to_owned(),
name: crate::util::parse_ident(name)?,
limit,
},
"quota",

View file

@ -89,7 +89,7 @@ async fn subvol_upgrade(socket: &Path, name: &str, yes: bool) -> Result<()> {
let upgrade = daemon_request(
socket,
hive_host_sock::HostRequest::UpgradeSubvolume {
name: name.to_owned(),
name: crate::util::parse_ident(name)?,
},
"upgrade",
)
@ -170,7 +170,7 @@ async fn subvol_snapshot_create(socket: &Path, name: &str, label: String) -> Res
daemon_request(
socket,
hive_host_sock::HostRequest::SnapshotSubvolume {
name: name.to_owned(),
name: crate::util::parse_ident(name)?,
label,
},
"snapshot",
@ -184,7 +184,7 @@ async fn subvol_snapshot_delete(socket: &Path, name: &str, label: &str) -> Resul
daemon_request(
socket,
hive_host_sock::HostRequest::DeleteSnapshot {
name: name.to_owned(),
name: crate::util::parse_ident(name)?,
label: label.to_owned(),
},
"snapshot delete",
@ -211,7 +211,7 @@ async fn subvol_snapshot_send(
daemon_request(
socket,
hive_host_sock::HostRequest::SendSnapshot {
name: name.to_owned(),
name: crate::util::parse_ident(name)?,
label: label.to_owned(),
parent: parent.map(str::to_owned),
dest: dest.to_owned(),

View file

@ -6,6 +6,16 @@ use std::path::Path;
use anyhow::{Context as _, Result, bail};
/// Parse a CLI-supplied agent/account name into a validated
/// [`hive_types::Ident`], mapping the parse error to an `anyhow` error that
/// names the offending input. Used at hivectl's `HostRequest` construction
/// sites so the wire `Ident` fields are built from validated names (the daemon
/// re-validates on deserialize; parsing here gives the operator an immediate,
/// local error instead of a round-trip rejection).
pub(crate) fn parse_ident(name: &str) -> Result<hive_types::Ident> {
hive_types::Ident::parse(name).map_err(|e| anyhow::anyhow!("invalid agent name {name:?}: {e}"))
}
/// Send a provisioning request to the daemon and print its result lines.
/// The daemon owns the provisioning logic; hivectl just relays the outcome,
/// prefixing any error with `label` (e.g. `forge` / `github`).
@ -115,7 +125,7 @@ pub(crate) async fn query_hive_urls(socket: &Path) -> Option<hive_host_sock::Hiv
/// "needs root" error fixes that first-run footgun, where running a
/// privileged verb without sudo reported as a missing agent.
pub(crate) fn agent_exists(name: &str) -> Result<bool> {
let Ok(name) = hive_host_sock::Ident::parse(name) else {
let Ok(name) = hive_types::Ident::parse(name) else {
bail!("invalid agent name {name:?}");
};
let root = hive_host_sock::agent_state_dir(&name);