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

@ -41,7 +41,7 @@ pub async fn approve(coord: Arc<Coordinator>, id: i64) -> Result<()> {
// Sub-second git seed + forge-remote wire. Routing through
// the queue would surface a queue card that's gone before
// the operator's eyes refocus. Run inline.
let agent = hive_host_sock::Ident::parse(&approval.agent).map_err(|e| {
let agent = hive_types::Ident::parse(&approval.agent).map_err(|e| {
anyhow::anyhow!("approval {} has invalid agent name: {e}", approval.id)
})?;
let proposed_dir = Coordinator::agent_proposed_dir(&agent);
@ -804,7 +804,7 @@ pub async fn destroy(coord: &Arc<Coordinator>, name: &str, purge: bool) -> Resul
// A malformed name can't have a persistent state tree (the state dir
// is only ever created under a validated Ident), so its removal is a
// no-op — skip the state-dir sweep and just clear the applied dir.
let state_dir = hive_host_sock::Ident::parse(name)
let state_dir = hive_types::Ident::parse(name)
.ok()
.map(|id| crate::paths::agent_state_dir(&id));
for dir in state_dir

View file

@ -70,7 +70,7 @@ pub async fn build_all(coord: &Coordinator) -> Vec<ContainerView> {
// Parse the nspawn machine suffix into an Ident once at this
// enumeration origin; a suffix that isn't a valid ident isn't one
// of our agents, so skip it.
let Ok(logical) = hive_host_sock::Ident::parse(logical) else {
let Ok(logical) = hive_types::Ident::parse(logical) else {
continue;
};
let deployed_full = locked
@ -133,7 +133,7 @@ pub fn claude_has_session(dir: &Path) -> bool {
/// the consolidated `hyperhive-harness.json`. Falls back to the legacy
/// individual sentinel files written by older harness builds so in-place
/// upgrades don't lose state during the transition window.
fn read_harness_flags(name: &hive_host_sock::Ident) -> (bool, bool) {
fn read_harness_flags(name: &hive_types::Ident) -> (bool, bool) {
let dir = Coordinator::agent_notes_dir(name);
if let Ok(raw) = std::fs::read_to_string(dir.join("hyperhive-harness.json"))
&& let Ok(v) = serde_json::from_str::<serde_json::Value>(&raw)
@ -154,7 +154,7 @@ fn read_harness_flags(name: &hive_host_sock::Ident) -> (bool, bool) {
(rate_limited, needs_login)
}
fn auth_failed_sentinel(name: &hive_host_sock::Ident) -> bool {
fn auth_failed_sentinel(name: &hive_types::Ident) -> bool {
read_harness_flags(name).1
}
@ -165,7 +165,7 @@ fn auth_failed_sentinel(name: &hive_host_sock::Ident) -> bool {
/// NB: callers building `AgentMeta` for a *stopped* container should
/// clear the result — the on-disk status is a stale snapshot from
/// before the stop. Use `read_agent_status_live` for that.
pub fn read_agent_status(name: &hive_host_sock::Ident) -> (Option<String>, Option<i64>) {
pub fn read_agent_status(name: &hive_types::Ident) -> (Option<String>, Option<i64>) {
let path = Coordinator::agent_notes_dir(name).join("hyperhive-status");
let meta = std::fs::metadata(&path).ok();
// Read at most STATUS_MAX_CHARS * 4 + 2 bytes: 4 is the max UTF-8 byte
@ -206,7 +206,7 @@ pub fn read_agent_status(name: &hive_host_sock::Ident) -> (Option<String>, Optio
/// Returned tuple is `(status_text, status_set_at, running)`.
/// `name` is the logical agent name (same as the broker recipient).
pub async fn read_agent_status_live(
name: &hive_host_sock::Ident,
name: &hive_types::Ident,
) -> (Option<String>, Option<i64>, bool) {
if !lifecycle::is_running(name.as_str()).await {
return (None, None, false);
@ -221,7 +221,7 @@ pub async fn read_agent_status_live(
/// so it always reflects the resolved priority (nix config > runtime
/// override > default). Returns `None` when the field is absent or the
/// harness has not yet started a turn.
fn read_active_model(name: &hive_host_sock::Ident) -> Option<String> {
fn read_active_model(name: &hive_types::Ident) -> Option<String> {
let path = Coordinator::agent_notes_dir(name).join("hyperhive-harness.json");
let raw = std::fs::read_to_string(path).ok()?;
let v: serde_json::Value = serde_json::from_str(&raw).ok()?;

View file

@ -553,7 +553,7 @@ impl Coordinator {
// MANAGER_NAME const), so an invalid ident here is a construction
// bug. This is the step-3 boundary between the Ident-threaded path
// builders and the job_queue layer (threaded post hive-jobq cutover).
let name = hive_host_sock::Ident::parse(name)
let name = hive_types::Ident::parse(name)
.expect("agent_paths: name must be a valid ident (validated at spawn/enqueue)");
AgentPaths {
agent: agent_dir,
@ -1448,7 +1448,7 @@ impl Coordinator {
/// Manager-editable proposed config repo. Bind-mounted into the manager
/// container as `/agents/<name>/config/`.
pub fn agent_proposed_dir(name: &hive_host_sock::Ident) -> PathBuf {
pub fn agent_proposed_dir(name: &hive_types::Ident) -> PathBuf {
crate::paths::agent_state_dir(name).join("config")
}
@ -1456,14 +1456,14 @@ impl Coordinator {
/// container at `/root/.claude` so OAuth state survives container
/// destroy/recreate. Each agent owns its own token lineage — sharing
/// would break on the first refresh-token rotation.
pub fn agent_claude_dir(name: &hive_host_sock::Ident) -> PathBuf {
pub fn agent_claude_dir(name: &hive_types::Ident) -> PathBuf {
crate::paths::agent_state_dir(name).join("claude")
}
/// Per-agent durable knowledge dir. Bind-mounted RW into the agent
/// container at `/agents/{name}/state`. Survives destroy/recreate.
/// Agent-visible — claude is told to write long-lived notes here.
pub fn agent_notes_dir(name: &hive_host_sock::Ident) -> PathBuf {
pub fn agent_notes_dir(name: &hive_types::Ident) -> PathBuf {
crate::paths::agent_state_dir(name).join("state")
}
@ -1473,7 +1473,7 @@ impl Coordinator {
/// `hyperhive-turn-stats.sqlite`, `hyperhive-model`) — kept separate
/// from the agent-visible `state/` so claude's "my notes" view is
/// uncluttered and the host vacuum has a clean sweep root.
pub fn agent_harness_dir(name: &hive_host_sock::Ident) -> PathBuf {
pub fn agent_harness_dir(name: &hive_types::Ident) -> PathBuf {
crate::paths::agent_state_dir(name).join("harness")
}
@ -1483,14 +1483,14 @@ impl Coordinator {
/// destroyed-but-kept tombstones; callers filter the latter by
/// subtracting `lifecycle::list()`.
#[must_use]
pub fn kept_state_names() -> Vec<hive_host_sock::Ident> {
pub fn kept_state_names() -> Vec<hive_types::Ident> {
let Ok(rd) = std::fs::read_dir(crate::paths::agents_root()) else {
return Vec::new();
};
let mut out: Vec<hive_host_sock::Ident> = rd
let mut out: Vec<hive_types::Ident> = rd
.flatten()
.filter(|e| e.file_type().is_ok_and(|t| t.is_dir()))
.filter_map(|e| hive_host_sock::Ident::parse(&e.file_name().into_string().ok()?).ok())
.filter_map(|e| hive_types::Ident::parse(&e.file_name().into_string().ok()?).ok())
.collect();
out.sort();
out
@ -1503,7 +1503,7 @@ impl Coordinator {
/// apply-commit spawns the container. Distinct from tombstones,
/// which have an applied repo from a prior deploy.
#[must_use]
pub fn pending_init_names() -> Vec<hive_host_sock::Ident> {
pub fn pending_init_names() -> Vec<hive_types::Ident> {
Self::kept_state_names()
.into_iter()
.filter(|n| {

View file

@ -66,7 +66,7 @@ pub(super) fn gc_orphans(coord: &Coordinator, approvals: Vec<Approval>) -> Vec<A
) {
return true;
}
let Ok(agent) = hive_host_sock::Ident::parse(&a.agent) else {
let Ok(agent) = hive_types::Ident::parse(&a.agent) else {
let _ = coord.approvals.mark_failed(a.id, "invalid agent name");
tracing::warn!(id = a.id, agent = %a.agent, "auto-failed approval with invalid agent name");
return false;

View file

@ -23,7 +23,7 @@ mod extra_forges;
// owning agent-path facts) so every dashboard path-param validates through the
// same type used to build agent paths. Re-exported so submodules + the socket
// server reach it as `crate::dashboard::Ident`.
pub(crate) use hive_host_sock::Ident;
pub(crate) use hive_types::Ident;
mod infra_containers;
mod journal;
mod lifecycle_ops;

View file

@ -344,7 +344,7 @@ pub(super) async fn get_stale_permissions(
let kept: std::collections::HashSet<String> =
crate::coordinator::Coordinator::kept_state_names()
.into_iter()
.map(hive_host_sock::Ident::into_string)
.map(hive_types::Ident::into_string)
.collect();
// Known = live roster kept-state names.
let known: std::collections::HashSet<&String> = live.iter().chain(kept.iter()).collect();

View file

@ -422,7 +422,7 @@ pub async fn ensure_meta_remote(name: &str) -> Result<()> {
}
// A malformed name has no proposed config repo (repos are only created
// under a validated Ident), so there's nothing to wire — no-op.
let Ok(agent) = hive_host_sock::Ident::parse(name) else {
let Ok(agent) = hive_types::Ident::parse(name) else {
return Ok(());
};
let proposed_dir = Coordinator::agent_proposed_dir(&agent);

View file

@ -51,7 +51,7 @@ pub const CONTAINER_MANAGER_APPLIED_MOUNT: &str = "/applied";
/// state") for the rationale. Creates missing host-side directories so
/// nspawn doesn't refuse to start; missing dirs are non-fatal.
fn bind_child_agent_dirs(child: &str, binds: &mut Vec<BindMount>) {
let Ok(child) = hive_host_sock::Ident::parse(child) else {
let Ok(child) = hive_types::Ident::parse(child) else {
tracing::warn!(%child, "skipping child bind: invalid agent name");
return;
};
@ -201,7 +201,7 @@ async fn set_nspawn_flags(
read_only: false,
});
}
let agent_id = hive_host_sock::Ident::parse(agent_name)
let agent_id = hive_types::Ident::parse(agent_name)
.map_err(|e| anyhow::anyhow!("invalid agent name {agent_name:?}: {e}"))?;
let own_config = crate::paths::agent_state_dir(&agent_id).join("config");
std::fs::create_dir_all(&own_config)

View file

@ -212,7 +212,7 @@ pub fn ensure_state_dir(notes_dir: &Path) -> Result<()> {
/// brand-new agent on a btrfs host gets a real subvolume. Subvolume creation
/// is privileged, so it's delegated to hive-priv.
pub async fn ensure_agent_state_subvolume(name: &str) -> Result<()> {
let agent = hive_host_sock::Ident::parse(name)
let agent = hive_types::Ident::parse(name)
.map_err(|e| anyhow::anyhow!("invalid agent name {name:?}: {e}"))?;
let root = crate::paths::agent_state_dir(&agent);
if root.exists() {

View file

@ -65,7 +65,7 @@ pub fn admin_token_path() -> PathBuf {
/// Token file inside the agent's bind-mounted state dir (visible as
/// `/state/matrix-token` from inside the container).
fn token_path(name: &hive_host_sock::Ident) -> PathBuf {
fn token_path(name: &hive_types::Ident) -> PathBuf {
Coordinator::agent_notes_dir(name).join("matrix-token")
}
@ -89,7 +89,7 @@ fn password_path(name: &str) -> PathBuf {
/// move credentials from old deployments to the new location. Safe to
/// call after `destroy --purge` — the path will simply not exist and
/// the migration is a no-op.
fn legacy_password_path(name: &hive_host_sock::Ident) -> PathBuf {
fn legacy_password_path(name: &hive_types::Ident) -> PathBuf {
Coordinator::agent_notes_dir(name).join("matrix-password")
}
@ -611,7 +611,7 @@ pub async fn ensure_user_for(
register_token: &str,
) -> Result<()> {
use std::os::unix::fs::PermissionsExt;
let agent = hive_host_sock::Ident::parse(name)
let agent = hive_types::Ident::parse(name)
.map_err(|e| anyhow::anyhow!("invalid agent name {name:?}: {e}"))?;
let path = token_path(&agent);
if path.exists()

View file

@ -124,7 +124,7 @@ pub async fn sync_agents(hive: &HiveEnv, agents: &[AgentSpec]) -> Result<()> {
let agent_names: Vec<String> = agents.iter().map(|a| a.name.clone()).collect();
let pending: Vec<String> = crate::coordinator::Coordinator::pending_init_names()
.into_iter()
.map(hive_host_sock::Ident::into_string)
.map(hive_types::Ident::into_string)
.collect();
crate::topology::reconcile(&agent_names, &pending)
.with_context(|| format!("reconcile {}", crate::topology::topology_path().display()))?;

View file

@ -141,7 +141,7 @@ pub async fn run(coord: &Arc<Coordinator>) -> Result<()> {
/// and into the sibling harness dir. Best-effort: logs warnings but never
/// fails. Idempotent — each file is only moved if present at the old path
/// and absent at the new path.
fn migrate_harness_files(name: &hive_host_sock::Ident) {
fn migrate_harness_files(name: &hive_types::Ident) {
const HARNESS_FILES: &[&str] = &[
"hyperhive-events.sqlite",
"hyperhive-turn-stats.sqlite",
@ -267,7 +267,7 @@ async fn rename_manager_container(coord: &Arc<Coordinator>) {
}
}
async fn enumerate_agents() -> Vec<hive_host_sock::Ident> {
async fn enumerate_agents() -> Vec<hive_types::Ident> {
let containers = lifecycle::list().await.unwrap_or_default();
containers
.into_iter()
@ -277,7 +277,7 @@ async fn enumerate_agents() -> Vec<hive_host_sock::Ident> {
} else {
c.strip_prefix(AGENT_PREFIX)?
};
hive_host_sock::Ident::parse(name).ok()
hive_types::Ident::parse(name).ok()
})
.collect()
}
@ -353,7 +353,7 @@ async fn repoint_container(name: &str) -> Result<()> {
/// Idempotent — skips when entry already present. Prevents a silent tool
/// downgrade when upgrading from a build that relied on the manager-flavor
/// fallback in `effective_tool_groups()`.
fn backfill_manager_tool_groups(names: &[hive_host_sock::Ident]) {
fn backfill_manager_tool_groups(names: &[hive_types::Ident]) {
if !names.iter().any(|n| n.as_str() == MANAGER_NAME) {
return; // ruth not deployed — nothing to backfill
}

View file

@ -83,11 +83,11 @@ async fn handle(stream: UnixStream, coord: Arc<Coordinator>) -> Result<()> {
async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
let result: anyhow::Result<HostResponse> = async {
Ok(match req {
HostRequest::Spawn { name } => handle_spawn(&coord, name).await?,
HostRequest::Spawn { name } => handle_spawn(&coord, name.as_str()).await?,
HostRequest::RequestSpawn { name } => {
tracing::info!(%name, "request_spawn");
let id = coord.approvals.submit_kind(
name,
name.as_str(),
hive_sh4re::ApprovalKind::Spawn,
"",
None,
@ -97,8 +97,10 @@ async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
tracing::info!(%id, %name, "spawn approval queued");
HostResponse::success()
}
HostRequest::Kill { name } => submit_single(&coord, name, Verb::Kill).await,
HostRequest::Restart { name } => submit_single(&coord, name, Verb::Restart).await,
HostRequest::Kill { name } => submit_single(&coord, name.as_str(), Verb::Kill).await,
HostRequest::Restart { name } => {
submit_single(&coord, name.as_str(), Verb::Restart).await
}
HostRequest::RestartAll => handle_restart_all(&coord).await?,
HostRequest::RestartScoped { scope, graceful } => {
handle_restart_scoped(&coord, scope, *graceful).await?
@ -140,10 +142,12 @@ async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
handle_start(&coord, &agents, &infra).await?
}
HostRequest::Destroy { name, purge } => {
actions::destroy(&coord, name, *purge).await?;
actions::destroy(&coord, name.as_str(), *purge).await?;
HostResponse::success()
}
HostRequest::Rebuild { name } => submit_single(&coord, name, Verb::Rebuild).await,
HostRequest::Rebuild { name } => {
submit_single(&coord, name.as_str(), Verb::Rebuild).await
}
HostRequest::QueueDag { id } => {
// A multi-step op is one DAG now (no fan-out children to gather).
let dags = coord
@ -179,7 +183,10 @@ async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
// skip both the messages and the disk write per the
// topology fast-path.
coord
.reparent_with_notify(child, new_parent.as_deref())
.reparent_with_notify(
child.as_str(),
new_parent.as_ref().map(hive_types::Ident::as_str),
)
.await
.map_err(anyhow::Error::msg)?;
HostResponse::success()
@ -188,8 +195,12 @@ async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
handle_matrix_create_user(name, password.as_deref()).await?
}
HostRequest::MatrixSyncAdmin => handle_matrix_sync_admin().await?,
HostRequest::MatrixPromoteUser { name } => handle_matrix_promote_user(name).await?,
HostRequest::MatrixResetPassword { name } => handle_matrix_reset_password(name).await?,
HostRequest::MatrixPromoteUser { name } => {
handle_matrix_promote_user(name.as_str()).await?
}
HostRequest::MatrixResetPassword { name } => {
handle_matrix_reset_password(name.as_str()).await?
}
HostRequest::MatrixInvite { user, room } => {
handle_matrix_invite(user, room.as_deref()).await?
}
@ -197,10 +208,10 @@ async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
handle_forge_create_user(name, password.as_deref()).await?
}
HostRequest::ReconcileConfigStatus { agent, verbose } => {
crate::forge::reconcile_config_status(agent, *verbose).await?
crate::forge::reconcile_config_status(agent.as_str(), *verbose).await?
}
HostRequest::ReconcileConfigApply { agent, direction } => {
crate::forge::reconcile_config_apply(agent, *direction).await?
crate::forge::reconcile_config_apply(agent.as_str(), *direction).await?
}
HostRequest::GatewayCreateUser { username, password } => {
HostResponse::messages(vec![crate::gateway_nginx::create_user(username, password)?])
@ -212,24 +223,30 @@ async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
HostResponse::messages(crate::gateway_nginx::list_users()?)
}
HostRequest::SetAgentGithubToken { agent, token } => {
handle_set_agent_github_token(agent, token).await?
handle_set_agent_github_token(agent.as_str(), token).await?
}
HostRequest::QuotaEnable => handle_quota_enable().await?,
HostRequest::QuotaLimit { name, limit } => handle_quota_limit(name, *limit).await?,
HostRequest::QuotaShow { name } => handle_quota_show(name.as_deref()).await?,
HostRequest::UpgradeSubvolume { name } => handle_upgrade_subvolume(name).await?,
HostRequest::QuotaLimit { name, limit } => {
handle_quota_limit(name.as_str(), *limit).await?
}
HostRequest::QuotaShow { name } => {
handle_quota_show(name.as_ref().map(hive_types::Ident::as_str)).await?
}
HostRequest::UpgradeSubvolume { name } => {
handle_upgrade_subvolume(name.as_str()).await?
}
HostRequest::SnapshotSubvolume { name, label } => {
handle_snapshot_subvolume(name, label).await?
handle_snapshot_subvolume(name.as_str(), label).await?
}
HostRequest::DeleteSnapshot { name, label } => {
handle_delete_snapshot(name, label).await?
handle_delete_snapshot(name.as_str(), label).await?
}
HostRequest::SendSnapshot {
name,
label,
parent,
dest,
} => handle_send_snapshot(name, label, parent.as_deref(), dest).await?,
} => handle_send_snapshot(name.as_str(), label, parent.as_deref(), dest).await?,
})
}
.await;
@ -320,10 +337,8 @@ fn matrix_http_client() -> Result<reqwest::Client> {
/// True when `name` has a state dir under the agents root, i.e. it's a
/// managed agent rather than a bare (operator/human) matrix account.
fn agent_exists(name: &str) -> Result<bool> {
let name = hive_host_sock::Ident::parse(name)
.map_err(|e| anyhow::anyhow!("invalid agent name {name:?}: {e}"))?;
crate::paths::agent_state_dir(&name)
fn agent_exists(name: &hive_types::Ident) -> Result<bool> {
crate::paths::agent_state_dir(name)
.try_exists()
.with_context(|| format!("check agent state dir for {name}"))
}
@ -338,7 +353,10 @@ async fn require_matrix_present() -> Result<()> {
)
}
async fn handle_matrix_create_user(name: &str, password: Option<&str>) -> Result<HostResponse> {
async fn handle_matrix_create_user(
name: &hive_types::Ident,
password: Option<&str>,
) -> Result<HostResponse> {
require_matrix_present().await?;
let register_token =
crate::matrix::ensure_register_token().context("read matrix register token")?;
@ -353,12 +371,10 @@ async fn handle_matrix_create_user(name: &str, password: Option<&str>) -> Result
"matrix create-user: a password is for non-agent (operator) accounts only; '{name}' is an agent which authenticates via access_token"
);
}
crate::matrix::ensure_user_for(&client, name, &register_token)
crate::matrix::ensure_user_for(&client, name.as_str(), &register_token)
.await
.with_context(|| format!("matrix create-user {name}"))?;
let agent = hive_host_sock::Ident::parse(name)
.map_err(|e| anyhow::anyhow!("invalid agent name {name:?}: {e}"))?;
let path = Coordinator::agent_notes_dir(&agent).join("matrix-token");
let path = Coordinator::agent_notes_dir(name).join("matrix-token");
out.push(format!("matrix: provisioned agent user '{name}'"));
out.push(format!("token persisted at: {}", path.display()));
} else {
@ -368,7 +384,7 @@ async fn handle_matrix_create_user(name: &str, password: Option<&str>) -> Result
};
let token = crate::matrix::provision_user_token(
&client,
name,
name.as_str(),
&register_token,
&effective_password,
)
@ -391,7 +407,10 @@ async fn handle_matrix_create_user(name: &str, password: Option<&str>) -> Result
Ok(HostResponse::messages(out))
}
async fn handle_forge_create_user(name: &str, password: Option<&str>) -> Result<HostResponse> {
async fn handle_forge_create_user(
name: &hive_types::Ident,
password: Option<&str>,
) -> Result<HostResponse> {
if !crate::forge::is_present().await {
anyhow::bail!(
"hive-forge container not running — wait for hive-c0re to start it before provisioning forge users"
@ -406,16 +425,14 @@ async fn handle_forge_create_user(name: &str, password: Option<&str>) -> Result<
"forge create-user: a password is for non-agent (operator) accounts only; '{name}' is an agent which authenticates via API token"
);
}
crate::forge::ensure_user_for(name)
crate::forge::ensure_user_for(name.as_str())
.await
.with_context(|| format!("forge create-user {name}"))?;
let agent = hive_host_sock::Ident::parse(name)
.map_err(|e| anyhow::anyhow!("invalid agent name {name:?}: {e}"))?;
let path = Coordinator::agent_notes_dir(&agent).join("forge-token");
let path = Coordinator::agent_notes_dir(name).join("forge-token");
out.push(format!("forge: provisioned agent user '{name}'"));
out.push(format!("token persisted at: {}", path.display()));
} else {
let token = crate::forge::provision_user_token(name, password)
let token = crate::forge::provision_user_token(name.as_str(), password)
.await
.with_context(|| format!("forge create-user {name}"))?;
out.push(format!(
@ -467,7 +484,7 @@ async fn handle_quota_show(name: Option<&str>) -> Result<HostResponse> {
Some(n) => vec![n.to_owned()],
None => Coordinator::kept_state_names()
.into_iter()
.map(hive_host_sock::Ident::into_string)
.map(hive_types::Ident::into_string)
.collect(),
};
let mut rows = Vec::with_capacity(agents.len());

View file

@ -195,7 +195,7 @@ pub(crate) fn submit_init_config(
parent: Option<&str>,
description: Option<String>,
) -> anyhow::Result<i64> {
let agent = hive_host_sock::Ident::parse(name)
let agent = hive_types::Ident::parse(name)
.map_err(|e| anyhow::anyhow!("invalid agent name {name:?}: {e}"))?;
let proposed_dir = crate::coordinator::Coordinator::agent_proposed_dir(&agent);
if proposed_dir.join(".git").exists() {

View file

@ -425,7 +425,7 @@ async fn handle_get_agent_meta(
// the OS level. Validate it before any path is built. The `None` default
// (`target == agent`) is the caller's own authenticated name, already
// valid — but validating unconditionally is simplest and harmless.
let target_id = match hive_host_sock::Ident::parse(target) {
let target_id = match hive_types::Ident::parse(target) {
Ok(id) => id,
Err(reason) => {
return hive_agent_sock::Response::Err {
@ -461,7 +461,7 @@ async fn handle_get_agent_meta(
/// or the daemon not up yet) yields an empty list. The `MatrixIdentity`
/// serde shape matches the snapshot entries; the snapshot's `live` field is
/// ignored (only live accounts are written).
fn read_agent_matrix_identities(agent: &hive_host_sock::Ident) -> Vec<hive_sh4re::MatrixIdentity> {
fn read_agent_matrix_identities(agent: &hive_types::Ident) -> Vec<hive_sh4re::MatrixIdentity> {
let path = Coordinator::agent_notes_dir(agent).join("matrix-accounts.json");
std::fs::read_to_string(&path)
.ok()
@ -751,7 +751,7 @@ fn require_group(agent: &str, group: &str, action: &str) -> Option<Response> {
/// `submit_init_config`, which builds filesystem paths from it, so validate
/// before that.
fn require_new_child(agent: &str, target: &str, action: &str) -> Option<Response> {
if let Err(reason) = hive_host_sock::Ident::parse(target) {
if let Err(reason) = hive_types::Ident::parse(target) {
return Some(Response::Err {
message: format!("agent `{agent}` cannot {action} `{target}`: {reason}"),
});
@ -1118,7 +1118,7 @@ pub(crate) fn handle_send(
// A name that doesn't parse as an Ident can't be a local agent, so
// it collapses into the same "unknown recipient" error as a valid
// name with no state dir.
let exists = hive_host_sock::Ident::parse(&resolved)
let exists = hive_types::Ident::parse(&resolved)
.is_ok_and(|id| crate::paths::agent_state_dir(&id).exists());
if !exists {
return Response::Err {

View file

@ -115,7 +115,7 @@ async fn du_bytes(path: &std::path::Path) -> Option<u64> {
/// agent's state-dir contribution was 0; with the writable rootfs nearly empty
/// (almost everything is bind-mounted), that surfaced as all agents reporting
/// 0 disk.
async fn measure_agent_disk(name: &hive_host_sock::Ident) -> u64 {
async fn measure_agent_disk(name: &hive_types::Ident) -> u64 {
let state_dir = Coordinator::agent_notes_dir(name);
let rootfs = PathBuf::from(format!("{NIXOS_CONTAINERS_ROOT}/h-{name}"));
let mut total = 0u64;

View file

@ -41,7 +41,7 @@ pub fn spawn(coord: Arc<Coordinator>) {
if lifecycle::is_running(&logical).await {
current_running.insert(logical.clone());
}
if hive_host_sock::Ident::parse(&logical)
if hive_types::Ident::parse(&logical)
.is_ok_and(|id| claude_has_session(&Coordinator::agent_claude_dir(&id)))
{
current_logged_in.insert(logical.clone());

View file

@ -133,7 +133,7 @@ fn inline_fallback(req_path: &str, reason: &str, message: &str) -> String {
/// inline-falls-back). `pub` because `socket_server::handle_remind`
/// reuses it for the at-remind-time auto-file path.
pub fn write_payload(agent: &str, host_path: &Path, message: &str) -> Result<(), String> {
let agent = hive_host_sock::Ident::parse(agent)
let agent = hive_types::Ident::parse(agent)
.map_err(|e| format!("invalid agent name {agent:?}: {e}"))?;
let Some(parent) = host_path.parent() else {
return Err("internal: host path has no parent".to_owned());
@ -191,7 +191,7 @@ pub fn container_state_prefix(agent: &str) -> String {
/// reason string on rejection. `pub` so `socket_server::handle_remind`
/// can reuse it for the at-remind-time auto-file path.
pub fn resolve_host_path(agent: &str, req_path: &str) -> Result<PathBuf, String> {
let agent = hive_host_sock::Ident::parse(agent)
let agent = hive_types::Ident::parse(agent)
.map_err(|e| format!("invalid agent name {agent:?}: {e}"))?;
let prefix = container_state_prefix(agent.as_str());
let Some(rel) = req_path.strip_prefix(&prefix) else {