build agents only from internal/meta; delete the hive's local meta flake
The hive no longer renders, locks or commits a meta flake. A rebuild's MetaSync node picks the internal/meta commit and records it with the hyperhive and config pins it holds; Prebuild, Swap and Create build from it, and a head that does not pin the agent's current config fails the build naming the agent. hive-priv's meta_rev is required. Removed: meta.rs sync/lock/render, the manual-rebuild relock, the boot sweep's hyperhive bump (MetaLock), the update-meta-inputs form and its panel, MetaWindow, the hive's hyperhive_rev setting. Kept: agent_set::refresh holds what sync_agents also did (topology.json, the gateway's agent-socket map and nginx include), called from spawn, destroy, rebuild and the migration. The boot reconcile and the stale-start upgrade compare the agent's marker with the hyperhive pin of internal/meta; the dashboard's deployed chip reads the config marker a successful build writes.
This commit is contained in:
parent
cea4827918
commit
4aa11b3a94
4 changed files with 1042 additions and 3793 deletions
|
|
@ -187,44 +187,6 @@ pub async fn git_rev_parse(dir: &Path, refname: &str) -> Result<String> {
|
|||
Ok(String::from_utf8_lossy(&out.stdout).trim().to_owned())
|
||||
}
|
||||
|
||||
/// The contents of `path` at commit `rev` in `dir`, `None` when `rev` has no
|
||||
/// such file. Reads the object database, not the working tree.
|
||||
pub async fn git_show_file(dir: &Path, rev: &str, path: &str) -> Result<Option<String>> {
|
||||
let spec = format!("{rev}:{path}");
|
||||
let listed = run_with_deadline(
|
||||
git_command()
|
||||
.current_dir(dir)
|
||||
.args(["ls-tree", "--name-only", rev, "--", path]),
|
||||
GIT_LOCAL_DEADLINE,
|
||||
&format!("git ls-tree {spec} in {}", dir.display()),
|
||||
)
|
||||
.await?;
|
||||
if !listed.status.success() {
|
||||
bail!(
|
||||
"git ls-tree {spec} failed ({}): {}",
|
||||
listed.status,
|
||||
String::from_utf8_lossy(&listed.stderr).trim()
|
||||
);
|
||||
}
|
||||
if listed.stdout.iter().all(u8::is_ascii_whitespace) {
|
||||
return Ok(None);
|
||||
}
|
||||
let shown = run_with_deadline(
|
||||
git_command().current_dir(dir).args(["show", &spec]),
|
||||
GIT_LOCAL_DEADLINE,
|
||||
&format!("git show {spec} in {}", dir.display()),
|
||||
)
|
||||
.await?;
|
||||
if !shown.status.success() {
|
||||
bail!(
|
||||
"git show {spec} failed ({}): {}",
|
||||
shown.status,
|
||||
String::from_utf8_lossy(&shown.stderr).trim()
|
||||
);
|
||||
}
|
||||
Ok(Some(String::from_utf8_lossy(&shown.stdout).into_owned()))
|
||||
}
|
||||
|
||||
/// Plant a lightweight tag at `target`. Errors if the tag already
|
||||
/// exists — we want loud failures on id reuse, not silent
|
||||
/// overwrites.
|
||||
|
|
@ -241,9 +203,10 @@ pub async fn git_read_tree_reset(dir: &Path, target: &str) -> Result<()> {
|
|||
git(dir, &["read-tree", "--reset", "-u", target]).await
|
||||
}
|
||||
|
||||
/// Hard-set a ref to `target`. Used to fast-forward `refs/heads/main`
|
||||
/// to the just-deployed proposal commit. Uses `update-ref`, not
|
||||
/// `branch -f`, so it works regardless of where HEAD currently sits.
|
||||
/// Hard-set a ref to `target` with `update-ref`, so it works regardless of
|
||||
/// where HEAD currently sits. Test fixtures only: production moves refs with
|
||||
/// [`git_update_ref_cas`].
|
||||
#[cfg(test)]
|
||||
pub async fn git_update_ref(dir: &Path, refname: &str, target: &str) -> Result<()> {
|
||||
git(dir, &["update-ref", refname, target]).await
|
||||
}
|
||||
|
|
@ -253,7 +216,7 @@ pub async fn git_update_ref(dir: &Path, refname: &str, target: &str) -> Result<(
|
|||
/// untouched — when the current value differs. That refusal is the whole
|
||||
/// difference between "advance this branch" and "overwrite whatever is there".
|
||||
///
|
||||
/// Prefer this over [`git_update_ref`] whenever the caller already knows the
|
||||
/// Prefer this over a plain `update-ref` whenever the caller already knows the
|
||||
/// value it believes the ref holds: a plain `update-ref` that raced another
|
||||
/// writer discards the other writer's commits without a word.
|
||||
///
|
||||
|
|
|
|||
|
|
@ -529,8 +529,8 @@ async fn set_nspawn_flags(
|
|||
}
|
||||
}
|
||||
// systemd-nspawn refuses to start a container whose bind
|
||||
// source doesn't exist. The meta repo is created by the
|
||||
// startup migration, but make sure the directory is there
|
||||
// source doesn't exist. The directory is created by the
|
||||
// startup migration, but make sure it is there
|
||||
// before the role holder comes up in case set_nspawn_flags
|
||||
// fires first (e.g. cold start with no agents).
|
||||
let meta_root = crate::paths::meta_root();
|
||||
|
|
|
|||
|
|
@ -7,10 +7,12 @@ mod setup;
|
|||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
#[cfg(test)]
|
||||
pub use git::git_update_ref;
|
||||
pub use git::{
|
||||
FETCH_DEADLINE, GIT_LOCAL_DEADLINE, git, git_authed, git_command, git_command_authed,
|
||||
git_is_ancestor, git_read_tree_reset, git_rev_parse, git_show_file, git_tag, git_update_ref,
|
||||
git_update_ref_cas, run_with_deadline,
|
||||
git_is_ancestor, git_read_tree_reset, git_rev_parse, git_tag, git_update_ref_cas,
|
||||
run_with_deadline,
|
||||
};
|
||||
pub use host_config::write_dropins;
|
||||
pub use setup::{
|
||||
|
|
@ -207,10 +209,10 @@ async fn port_collision(self_name: &str) -> Result<Option<String>> {
|
|||
/// First-spawn pre-create provisioning — the job queue's `Provision`
|
||||
/// node. Fail fast on a port collision, set up the proposed/applied
|
||||
/// repos + state subvolume + claude/notes dirs, then register the new
|
||||
/// agent in the meta flake (`sync_agents`) so the later
|
||||
/// `nixos-container create --flake meta#<name>` ref resolves. Does NOT
|
||||
/// agent in the host-side files that follow the agent set
|
||||
/// ([`crate::agent_set::refresh`]). Does NOT
|
||||
/// create the container — that's `create_only` / the `Create` node.
|
||||
pub async fn provision_container(name: &str, hive: &HiveEnv, paths: &AgentPaths) -> Result<()> {
|
||||
pub async fn provision_container(name: &str, paths: &AgentPaths) -> Result<()> {
|
||||
validate(name)?;
|
||||
if let Some(other) = port_collision(name).await? {
|
||||
bail!(
|
||||
|
|
@ -219,7 +221,7 @@ pub async fn provision_container(name: &str, hive: &HiveEnv, paths: &AgentPaths)
|
|||
);
|
||||
}
|
||||
// Read before anything is seeded: an archetype agent gets no config
|
||||
// repo, and the meta render below builds it from the archetype.
|
||||
// repo, and the build takes it from the archetype.
|
||||
crate::archetypes::refresh(name).await?;
|
||||
if crate::archetypes::of(name)?.is_none() {
|
||||
setup_proposed(&paths.proposed, name).await?;
|
||||
|
|
@ -228,19 +230,15 @@ pub async fn provision_container(name: &str, hive: &HiveEnv, paths: &AgentPaths)
|
|||
ensure_agent_state_subvolume(name).await?;
|
||||
ensure_claude_dir(&paths.claude)?;
|
||||
ensure_state_dir(&paths.notes)?;
|
||||
// Meta flake gets the new agent's input + nixosConfiguration
|
||||
// before `nixos-container create` so the `--flake meta#<name>`
|
||||
// ref resolves.
|
||||
let agents = agents_after_spawn(name).await?;
|
||||
crate::meta::sync_agents(hive, &agents).await
|
||||
crate::agent_set::refresh(&agents).await
|
||||
}
|
||||
|
||||
/// The `nixos-container create` proper — the job queue's `Create` node.
|
||||
/// Assumes `provision_container` already registered the agent in meta.
|
||||
/// `meta_rev` is the `internal/meta` commit to build from; none builds from the
|
||||
/// hive's own meta flake (`crate::meta_rev`).
|
||||
pub async fn create_only(name: &str, meta_rev: Option<&str>) -> Result<()> {
|
||||
priv_run_inner("create", name, meta_rev, None).await
|
||||
/// Assumes `provision_container` already registered the agent.
|
||||
/// `meta_rev` is the `internal/meta` commit to build from (`crate::meta_rev`).
|
||||
pub async fn create_only(name: &str, meta_rev: &str) -> Result<()> {
|
||||
priv_run_inner("create", name, Some(meta_rev), None).await
|
||||
}
|
||||
|
||||
/// Rebuild-path preamble, run by the job queue's `Prebuild` node: fail fast on
|
||||
|
|
@ -298,15 +296,15 @@ pub async fn swap_update(
|
|||
name: &str,
|
||||
hive: &HiveEnv,
|
||||
paths: &AgentPaths,
|
||||
meta_rev: Option<&str>,
|
||||
meta_rev: &str,
|
||||
node_id: Option<u64>,
|
||||
) -> Result<()> {
|
||||
write_dropins(name, hive, paths).await?;
|
||||
priv_run_inner("update", name, meta_rev, node_id).await
|
||||
priv_run_inner("update", name, Some(meta_rev), node_id).await
|
||||
}
|
||||
|
||||
/// Build the `AgentSpec` list for the meta flake from `nixos-container
|
||||
/// list` + a hypothetical extra name not yet in the list (for spawn
|
||||
/// Build the `AgentSpec` list for [`crate::agent_set::refresh`] from
|
||||
/// `nixos-container list` + a hypothetical extra name not yet in the list (for spawn
|
||||
/// where the new agent's container doesn't exist yet). Pass empty
|
||||
/// `name_to_add` from rebuild paths where the agent is already in the
|
||||
/// container list.
|
||||
|
|
@ -314,17 +312,17 @@ pub async fn swap_update(
|
|||
/// Propagates errors from `list()` rather than swallowing them.
|
||||
/// Using `.unwrap_or_default()` here would silently produce an empty
|
||||
/// agent list when `nixos-container list` fails (priv helper down, race),
|
||||
/// which `sync_agents` would then commit to meta — dropping every agent
|
||||
/// from `flake.nix`. Callers that can tolerate failures (e.g. migration)
|
||||
/// which would drop every agent from the topology and the gateway. Callers
|
||||
/// that can tolerate failures (e.g. migration)
|
||||
/// handle the `Err` themselves with `.unwrap_or_default()`.
|
||||
async fn agents_for_meta(name_to_add: Option<&str>) -> Result<Vec<crate::meta::AgentSpec>> {
|
||||
async fn agents_for_meta(name_to_add: Option<&str>) -> Result<Vec<hive_meta_flake::AgentSpec>> {
|
||||
let containers = list().await?;
|
||||
let archetypes = crate::archetypes::read_at(&crate::archetypes::path())?;
|
||||
let mut out: Vec<crate::meta::AgentSpec> = containers
|
||||
let mut out: Vec<hive_meta_flake::AgentSpec> = containers
|
||||
.into_iter()
|
||||
.filter_map(|c| {
|
||||
let name = c.strip_prefix(AGENT_PREFIX)?.to_owned();
|
||||
Some(crate::meta::AgentSpec {
|
||||
Some(hive_meta_flake::AgentSpec {
|
||||
port: agent_web_port(&name),
|
||||
archetype: archetypes.get(&name).cloned(),
|
||||
pin: None,
|
||||
|
|
@ -335,7 +333,7 @@ async fn agents_for_meta(name_to_add: Option<&str>) -> Result<Vec<crate::meta::A
|
|||
if let Some(extra) = name_to_add
|
||||
&& !out.iter().any(|a| a.name == extra)
|
||||
{
|
||||
out.push(crate::meta::AgentSpec {
|
||||
out.push(hive_meta_flake::AgentSpec {
|
||||
port: agent_web_port(extra),
|
||||
archetype: archetypes.get(extra).cloned(),
|
||||
pin: None,
|
||||
|
|
@ -346,7 +344,7 @@ async fn agents_for_meta(name_to_add: Option<&str>) -> Result<Vec<crate::meta::A
|
|||
Ok(out)
|
||||
}
|
||||
|
||||
async fn agents_after_spawn(name: &str) -> Result<Vec<crate::meta::AgentSpec>> {
|
||||
async fn agents_after_spawn(name: &str) -> Result<Vec<hive_meta_flake::AgentSpec>> {
|
||||
agents_for_meta(Some(name)).await
|
||||
}
|
||||
|
||||
|
|
@ -354,7 +352,7 @@ async fn agents_after_spawn(name: &str) -> Result<Vec<crate::meta::AgentSpec>> {
|
|||
/// `nixos-container list` says), sorted, no extras. For callers
|
||||
/// outside this module that need to reseed meta after lifecycle
|
||||
/// changes — destroy, startup reconciliation, etc.
|
||||
pub async fn agents_for_meta_listing() -> Result<Vec<crate::meta::AgentSpec>> {
|
||||
pub async fn agents_for_meta_listing() -> Result<Vec<hive_meta_flake::AgentSpec>> {
|
||||
agents_for_meta(None).await
|
||||
}
|
||||
|
||||
|
|
@ -970,6 +968,13 @@ fn make_log_callback(
|
|||
}
|
||||
}
|
||||
|
||||
/// A `create` or `update` asked for without an `internal/meta` commit. The
|
||||
/// public entry points take the commit, so this guards the string dispatch
|
||||
/// below only.
|
||||
fn missing_meta_rev(kind: &str, name: &str) -> anyhow::Error {
|
||||
anyhow::anyhow!("{name}: {kind} needs an internal/meta commit to build from")
|
||||
}
|
||||
|
||||
/// Execute a container operation via hive-priv and integrate with
|
||||
/// `build_logs.sqlite`. hive-priv runs as root and forwards output lines
|
||||
/// to hive-c0re in real time via the streaming priv protocol. Each line
|
||||
|
|
@ -1007,22 +1012,28 @@ async fn priv_run_inner(
|
|||
// For long-running ops use the streaming protocol so build_logs
|
||||
// receives lines in real time rather than as a batch at completion.
|
||||
let result: Result<()> = match kind {
|
||||
"create" => {
|
||||
crate::priv_client::create_container_streaming(
|
||||
name,
|
||||
meta_rev,
|
||||
make_log_callback(logs.clone(), log_id, cmdline.clone()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
"update" => {
|
||||
crate::priv_client::update_container_streaming(
|
||||
name,
|
||||
meta_rev,
|
||||
make_log_callback(logs.clone(), log_id, cmdline.clone()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
"create" => match meta_rev {
|
||||
Some(rev) => {
|
||||
crate::priv_client::create_container_streaming(
|
||||
name,
|
||||
rev,
|
||||
make_log_callback(logs.clone(), log_id, cmdline.clone()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
None => Err(missing_meta_rev(kind, name)),
|
||||
},
|
||||
"update" => match meta_rev {
|
||||
Some(rev) => {
|
||||
crate::priv_client::update_container_streaming(
|
||||
name,
|
||||
rev,
|
||||
make_log_callback(logs.clone(), log_id, cmdline.clone()),
|
||||
)
|
||||
.await
|
||||
}
|
||||
None => Err(missing_meta_rev(kind, name)),
|
||||
},
|
||||
"start" => crate::priv_client::start_container(name).await,
|
||||
"stop" => crate::priv_client::stop_container(name).await,
|
||||
"kill" => crate::priv_client::kill_container(name).await,
|
||||
|
|
|
|||
|
|
@ -260,8 +260,8 @@ fn initial_agent_nix(name: &str) -> String {
|
|||
}
|
||||
|
||||
/// Module-only flake exposed by every agent's repo. Consumed by the
|
||||
/// hive-c0re-owned meta flake at `/var/lib/hyperhive/meta/` as a flake
|
||||
/// input. The wrapper is intentionally permissive:
|
||||
/// swarm's `internal/meta` flake as a flake input. The wrapper is
|
||||
/// intentionally permissive:
|
||||
///
|
||||
/// - Manager edits `inputs.* = …` to add other flakes (e.g. an MCP
|
||||
/// server's own flake) — the lock for those lands in the agent's
|
||||
|
|
|
|||
Loading…
Reference in a new issue