Watch
0
0
Fork
You've already forked hyperhive
0

add a per-agent nix builds mode, host or none, carried like privateUsers

This commit is contained in:
atlas 2026-11-03 00:30:55 +01:00 • committed by mara
commit 5485151ba6
5 changed files with 618 additions and 58 deletions

View file

@ -174,6 +174,7 @@ mod tests {
built_unix: 1,
cached,
private_users: false,
nix_builds: hive_types::NixBuilds::Host,
}
}

View file

@ -246,6 +246,7 @@ mod tests {
built_unix: 1,
cached: true,
private_users: false,
nix_builds: hive_types::NixBuilds::Host,
}
}

View file

@ -275,6 +275,7 @@ fn closure(cached: bool) -> AgentClosure {
built_unix: 1,
cached,
private_users: false,
nix_builds: hive_types::NixBuilds::Host,
}
}

View file

@ -7,7 +7,7 @@ use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use hive_priv_sock::BindMount;
use hive_types::Permission;
use hive_types::{NixBuilds, Permission};
use crate::coordinator::{AgentPaths, HiveEnv};
@ -451,9 +451,11 @@ async fn set_nspawn_flags(
});
}
// Whether the agent's config sets `privateUsers`, as the swarm builder
// recorded it. Read from the record, never from the agent's nix.
let private_users = private_users_of(&crate::attic::closure::record(agent_name).await);
// What the agent's config sets for `privateUsers` and `nixBuilds`, as the
// swarm builder recorded it. Read from the record, never from the agent's nix.
let record = crate::attic::closure::record(agent_name).await;
let private_users = private_users_of(&record);
let nix_builds = nix_builds_of(&record);
// Web-socket subdir: bind-mount `/run/hive-agent/<name>/` into the
// container so the harness can bind `web.sock` there and the host-side
@ -487,6 +489,7 @@ async fn set_nspawn_flags(
&load_creds,
super::hive_facts(hive),
private_users,
nix_builds,
)
.await
}
@ -529,6 +532,21 @@ fn private_users_of(
}
}
/// What hive-priv is told about the agent's nix builds: the record's mode, or
/// `None` (leave the agent as the last write set it) when there is no record or
/// it cannot be read, so a swarm outage never turns an agent's host builds on.
fn nix_builds_of(
record: &Result<Option<swarm_wire::agent_closures::AgentClosure>>,
) -> Option<NixBuilds> {
match record {
Ok(record) => record.as_ref().map(|r| r.nix_builds),
Err(e) => {
tracing::warn!(error = %format!("{e:#}"), "closure record unreadable; the agent's nix builds are left as they are");
None
}
}
}
/// The binds every agent container gets, before its own state, config and
/// role-specific dirs.
fn base_binds(runtime_dir: &Path, claude_dir: &Path, claude_mount: String) -> Vec<BindMount> {
@ -559,8 +577,16 @@ mod tests {
stage_limits,
};
use crate::coordinator::HiveEnv;
use hive_types::NixBuilds;
fn record_with(private_users: bool) -> swarm_wire::agent_closures::AgentClosure {
record_with_builds(private_users, NixBuilds::Host)
}
fn record_with_builds(
private_users: bool,
nix_builds: NixBuilds,
) -> swarm_wire::agent_closures::AgentClosure {
swarm_wire::agent_closures::AgentClosure {
meta_rev: "a".repeat(40),
store_path: "/nix/store/x".to_owned(),
@ -568,6 +594,7 @@ mod tests {
built_unix: 1,
cached: true,
private_users,
nix_builds,
}
}
@ -609,6 +636,38 @@ mod tests {
assert_eq!(super::private_users_of(&Ok(Some(old))), Some(false));
}
/// The record's mode is what hive-priv is told, both ways. No record and an
/// unreadable one say nothing, so a swarm outage changes no agent; a record
/// from before the field reads as host builds.
#[test]
fn the_records_nix_builds_is_what_the_start_path_passes() {
for mode in [NixBuilds::Host, NixBuilds::Disabled] {
assert_eq!(
super::nix_builds_of(&Ok(Some(record_with_builds(false, mode)))),
Some(mode)
);
}
assert_eq!(super::nix_builds_of(&Ok(None)), None);
assert_eq!(super::nix_builds_of(&Err(anyhow::anyhow!("down"))), None);
let old = serde_json::json!({
"meta_rev": "a", "store_path": "/nix/store/x",
"system": "s", "built_unix": 1,
});
let old = serde_json::from_value(old).expect("decodes");
assert_eq!(super::nix_builds_of(&Ok(Some(old))), Some(NixBuilds::Host));
// A mode a newer swarm wrote, which this build does not know, decodes
// and builds nothing.
let newer = serde_json::json!({
"meta_rev": "a", "store_path": "/nix/store/x",
"system": "s", "built_unix": 1, "nix_builds": "container",
});
let newer = serde_json::from_value(newer).expect("decodes");
assert_eq!(
super::nix_builds_of(&Ok(Some(newer))),
Some(NixBuilds::Disabled)
);
}
/// No agent container gets a cross-agent scratch dir; `/knowledge` is the
/// control that the lookup finds a bind that is there.
#[test]

View file

@ -11,6 +11,7 @@ use hive_priv_sock::{
BindMount, CredentialMount, HiveFacts, InfraAction, InfraContainer, MEASURE_AGENT_DISK_TIMEOUT,
NetworkIsolation, PRIV_SOCK, PrivEvent, PrivRequest, PrivResponse, PrivStream,
};
use hive_types::NixBuilds;
use std::os::fd::{AsRawFd as _, OwnedFd, RawFd};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader, Interest};
@ -320,6 +321,7 @@ pub async fn write_nspawn_flags(
load_credentials: &[CredentialMount],
hive_facts: HiveFacts,
private_users: Option<bool>,
nix_builds: Option<NixBuilds>,
) -> Result<()> {
ok(call(&PrivRequest::WriteNspawnFlags {
container: container.to_owned(),
@ -328,6 +330,7 @@ pub async fn write_nspawn_flags(
load_credentials: load_credentials.to_vec(),
hive_facts: Box::new(hive_facts),
private_users,
nix_builds,
})
.await?)
}