Watch
0
0
Fork
You've already forked hyperhive
0

build an archetype agent from internal/meta only at the archetype it has now

on deploy, hive-c0re takes the internal/meta head for an archetype agent only
when the head lists that same archetype, and for a custom agent only when the
head lists it as custom, so a switch between the two, or to another archetype,
builds from the hive's own flake until the swarm commits the new flake.
the record location is described in the same docs paragraph.
This commit is contained in:
atlas 2026-10-24 19:20:37 +02:00
commit 4697740a7e
3 changed files with 138 additions and 55 deletions

View file

@ -10,6 +10,7 @@
use anyhow::{Context, Result};
use hive_priv_sock::{meta_flake_url, valid_meta_rev};
use swarm_queue_client::agent_archetype::ArchetypeRef;
/// What the forge says about `internal/meta` and an agent's config repo.
pub(crate) trait MetaSource {
@ -30,18 +31,18 @@ pub(crate) trait MetaSource {
///
/// `internal/meta` is private, so a hive whose nix cannot authenticate
/// ([`MetaSource::nix_can_fetch`]) builds from its own flake. The head is
/// taken only when its `flake.nix` builds the agent from the
/// config commit the forge's `main` is at now (`archetype`: when it lists the
/// agent at all). A head that is behind (the swarm's update has not run, or
/// failed) or lacks the agent would build an older config than the one being
/// deployed.
/// taken only when its `flake.nix` builds the agent the way it is wanted now:
/// from `archetype` when that is set, otherwise from the config commit the
/// forge's `main` is at. A head that is behind (the swarm's update has not
/// run, or failed), lacks the agent or lists it under another archetype would
/// build an older module than the one being deployed.
///
/// # Errors
/// A forge read that failed.
pub(crate) async fn choose(
source: &impl MetaSource,
agent: &str,
archetype: bool,
archetype: Option<&ArchetypeRef>,
) -> Result<Option<String>> {
if !source.nix_can_fetch() {
return Ok(None);
@ -53,7 +54,7 @@ pub(crate) async fn choose(
return Ok(None);
}
let flake = source.flake_at(&rev).await?;
let config_head = if archetype {
let config_head = if archetype.is_some() {
None
} else {
let Some(head) = source.config_head(agent).await? else {
@ -61,14 +62,17 @@ pub(crate) async fn choose(
};
Some(head)
};
Ok(hive_meta_flake::builds_agent_at(&flake, agent, config_head.as_deref()).then_some(rev))
Ok(
hive_meta_flake::builds_agent_at(&flake, agent, archetype, config_head.as_deref())
.then_some(rev),
)
}
/// [`choose`] against the forge this hive reads. `None`, logged, whenever the
/// hive cannot tell: no forge credential, a failed read, no usable commit.
pub(crate) async fn resolve(agent: &str) -> Option<String> {
let archetype = match crate::archetypes::of(agent) {
Ok(archetype) => archetype.is_some(),
Ok(archetype) => archetype,
Err(e) => {
tracing::warn!(%agent, error = ?e, "internal/meta: cannot read the agent's archetype; building from the hive's own flake");
return None;
@ -78,7 +82,7 @@ pub(crate) async fn resolve(agent: &str) -> Option<String> {
tracing::info!(%agent, "internal/meta: no forge read token; building from the hive's own flake");
return None;
};
match choose(&source, agent, archetype).await {
match choose(&source, agent, archetype.as_ref()).await {
Ok(Some(rev)) => {
tracing::info!(%agent, %rev, "internal/meta: building from the swarm's meta flake");
Some(rev)
@ -175,8 +179,7 @@ mod tests {
assert_eq!(Some(rev), self.head, "the flake is read at the head");
Ok(self.flake.clone())
}
async fn config_head(&self, agent: &str) -> Result<Option<String>> {
assert_eq!(agent, "argus");
async fn config_head(&self, _: &str) -> Result<Option<String>> {
Ok(self.config_head.map(str::to_owned))
}
}
@ -233,7 +236,7 @@ mod tests {
#[tokio::test]
async fn the_head_is_taken_when_it_pins_the_agents_current_config() {
assert_eq!(
choose(&forge(Some(ARGUS_HEAD)), "argus", false)
choose(&forge(Some(ARGUS_HEAD)), "argus", None)
.await
.unwrap(),
Some(META.to_owned())
@ -243,7 +246,7 @@ mod tests {
#[tokio::test]
async fn a_head_behind_the_agents_config_is_not_taken() {
assert_eq!(
choose(&forge(Some(STALE)), "argus", false).await.unwrap(),
choose(&forge(Some(STALE)), "argus", None).await.unwrap(),
None
);
}
@ -252,11 +255,18 @@ mod tests {
async fn a_head_without_the_agent_is_not_taken() {
let mut source = forge(Some(ARGUS_HEAD));
source.flake = source.flake.replace("argus", "someone");
assert_eq!(choose(&source, "argus", false).await.unwrap(), None);
assert_eq!(choose(&source, "argus", None).await.unwrap(), None);
}
fn reviewer() -> ArchetypeRef {
ArchetypeRef {
source: "hyperhive".to_owned(),
name: "reviewer".to_owned(),
}
}
#[tokio::test]
async fn an_archetype_agent_needs_only_to_be_listed() {
async fn an_archetype_agent_is_taken_when_the_head_lists_that_archetype() {
struct Archetypes(Forge);
impl MetaSource for Archetypes {
fn nix_can_fetch(&self) -> bool {
@ -274,10 +284,39 @@ mod tests {
}
let source = Archetypes(forge(None));
assert_eq!(
choose(&source, "rev", true).await.unwrap(),
choose(&source, "rev", Some(&reviewer())).await.unwrap(),
Some(META.to_owned())
);
assert_eq!(choose(&source, "ghost", true).await.unwrap(), None);
let other = ArchetypeRef {
name: "planner".to_owned(),
..reviewer()
};
assert_eq!(
choose(&source, "rev", Some(&other)).await.unwrap(),
None,
"the head lists another archetype"
);
assert_eq!(
choose(&source, "ghost", Some(&reviewer())).await.unwrap(),
None
);
}
/// The head lists argus as a custom agent and rev as an archetype agent;
/// an agent switched either way is not what the head builds.
#[tokio::test]
async fn an_agent_switched_between_custom_and_archetype_is_not_taken() {
let source = forge(Some(ARGUS_HEAD));
assert_eq!(
choose(&source, "argus", Some(&reviewer())).await.unwrap(),
None,
"custom in the head, archetype now"
);
assert_eq!(
choose(&source, "rev", None).await.unwrap(),
None,
"archetype in the head, custom now"
);
}
/// A hive without the credential nix's git reads builds from its own flake