drop the proposed config repo: applied seeds from forge main, and the dead pending-init topology list goes with it
This commit is contained in:
parent
08b8eb3f73
commit
25295fd33f
1 changed files with 225 additions and 520 deletions
|
|
@ -1,159 +1,53 @@
|
|||
//! First-spawn provisioning: seed the agent's proposed config repo and
|
||||
//! the hive-c0re-owned applied repo, and ensure the per-agent state /
|
||||
//! claude-credentials dirs (btrfs subvolume when available) exist.
|
||||
//! First-spawn provisioning: seed the hive-c0re-owned applied repo, and
|
||||
//! ensure the per-agent state / claude-credentials dirs (btrfs subvolume when
|
||||
//! available) exist.
|
||||
//!
|
||||
//! Seeding is the only write either repo gets from here. Later config
|
||||
//! changes arrive as forge PRs authored in a clone — nothing edits a
|
||||
//! working tree in place.
|
||||
//! Seeding is the only write the repo gets from here. Later config changes
|
||||
//! arrive as forge PRs authored in a clone - nothing edits a working tree in
|
||||
//! place.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{Context, Result, bail};
|
||||
|
||||
use super::git::{
|
||||
git, git_command, git_commit, git_read_tree_reset, git_rev_parse, git_root_commit, git_tag,
|
||||
};
|
||||
use super::git::{git, git_commit, git_rev_parse, git_tag};
|
||||
|
||||
/// Initialize an agent's config repo, preferring the one the forge
|
||||
/// already has. `agent-configs/<name>` is authored by whoever created
|
||||
/// the agent — at swarm level that is the controller, which writes the
|
||||
/// config before any hive is told to deploy it — so this **clones**
|
||||
/// when there is something to clone and seeds a template only when
|
||||
/// there is not. Two authors for one file is the failure mode being
|
||||
/// avoided: whichever wrote second would win, and neither knows about
|
||||
/// the other.
|
||||
/// Seed an agent's applied repo on first spawn, preferring the config the
|
||||
/// forge already has. `agent-configs/<name>` is authored by whoever created
|
||||
/// the agent (at swarm level the controller, which writes the config before
|
||||
/// any hive is told to deploy it), so this takes the forge's `main` when
|
||||
/// there is one and writes a template only when there is not. Two authors for
|
||||
/// one file is the failure mode being avoided: whichever wrote second would
|
||||
/// win, and neither knows about the other.
|
||||
///
|
||||
/// The template ([`seed_template`]) is what this still falls back to when
|
||||
/// there's genuinely nothing to clone yet — `swarm-controller` creates
|
||||
/// `agent-configs/<name>` before telling a hive to deploy, but hasn't
|
||||
/// necessarily pushed `main` to it yet, or the forge is unreachable, or
|
||||
/// this hive has no forge read token.
|
||||
/// The template ([`seed_template`]) is the fallback when there is genuinely
|
||||
/// nothing to take yet: `swarm-controller` creates `agent-configs/<name>`
|
||||
/// before telling a hive to deploy, but hasn't necessarily pushed `main` to
|
||||
/// it, or the forge is unreachable, or this hive has no forge read token.
|
||||
///
|
||||
/// **Seeding is the whole of hive-c0re's write.** Changes to the repo
|
||||
/// arrive as PRs from a clone, via the forge, like any other code
|
||||
/// change — nothing edits a working tree in place, here or in the
|
||||
/// agent's bind-mounted `config/` (which is a read-only copy).
|
||||
/// Seeding is the only write hive-c0re makes here. Later config changes
|
||||
/// arrive as forge PRs and are fetched from the forge's `main` (see
|
||||
/// `actions::advance_applied_to_rev`). `flake.nix` is tracked in the seed
|
||||
/// and rides along on every fetch.
|
||||
///
|
||||
/// Touched by hive-c0re only on first spawn — never again — so a
|
||||
/// hive-c0re commit or working-tree reset can't surprise anyone
|
||||
/// holding a clone.
|
||||
pub async fn setup_proposed(proposed_dir: &Path, name: &str) -> Result<()> {
|
||||
let fresh = !proposed_dir.join(".git").exists();
|
||||
if fresh {
|
||||
std::fs::create_dir_all(proposed_dir)
|
||||
.with_context(|| format!("create {}", proposed_dir.display()))?;
|
||||
if !crate::forge::clone_config_into_proposed(proposed_dir, name).await {
|
||||
seed_template(proposed_dir, name).await?;
|
||||
}
|
||||
}
|
||||
// Idempotently wire the `applied` remote — purely for the
|
||||
// manager's ergonomics. The URL is the path inside the manager
|
||||
// container (`/applied/<n>/.git`), where the RO bind in
|
||||
// `set_nspawn_flags` makes it real. hive-c0re itself never
|
||||
// dereferences this remote.
|
||||
ensure_applied_remote(proposed_dir, name).await
|
||||
}
|
||||
|
||||
/// Author the initial config in place: `agent.nix` (the agent's own module)
|
||||
/// and `flake.nix` (the boilerplate that lets the meta flake import this repo
|
||||
/// as an input — meta locks at a specific sha and reads
|
||||
/// `nixosModules.default`, so `flake.nix` must be in the commit). `flake.nix`
|
||||
/// isn't meant to be edited, but it's tracked so it can be read.
|
||||
///
|
||||
/// The fallback half of [`setup_proposed`]: reached only when the forge has
|
||||
/// no config for this agent to take.
|
||||
async fn seed_template(proposed_dir: &Path, name: &str) -> Result<()> {
|
||||
let agent_path = proposed_dir.join("agent.nix");
|
||||
if !agent_path.exists() {
|
||||
std::fs::write(&agent_path, initial_agent_nix(name))
|
||||
.with_context(|| format!("write {}", agent_path.display()))?;
|
||||
}
|
||||
let flake_path = proposed_dir.join("flake.nix");
|
||||
if !flake_path.exists() {
|
||||
std::fs::write(&flake_path, initial_flake_nix())
|
||||
.with_context(|| format!("write {}", flake_path.display()))?;
|
||||
}
|
||||
git(proposed_dir, &["init", "--initial-branch=main"]).await?;
|
||||
git(proposed_dir, &["add", "agent.nix", "flake.nix"]).await?;
|
||||
git_commit(proposed_dir, "hive-c0re init").await
|
||||
}
|
||||
|
||||
async fn ensure_applied_remote(proposed_dir: &Path, name: &str) -> Result<()> {
|
||||
let want = format!("/applied/{name}/.git");
|
||||
let existing = git_command()
|
||||
.current_dir(proposed_dir)
|
||||
.args(["remote", "get-url", "applied"])
|
||||
.output()
|
||||
.await
|
||||
.with_context(|| format!("git remote get-url applied in {}", proposed_dir.display()))?;
|
||||
if existing.status.success() {
|
||||
let current = String::from_utf8_lossy(&existing.stdout).trim().to_owned();
|
||||
if current == want {
|
||||
return Ok(());
|
||||
}
|
||||
// URL drifted (path scheme changed, etc.) — re-point it.
|
||||
return git(proposed_dir, &["remote", "set-url", "applied", &want]).await;
|
||||
}
|
||||
git(proposed_dir, &["remote", "add", "applied", &want]).await
|
||||
}
|
||||
|
||||
/// Set up the applied repo. First-spawn only: init the repo, pull
|
||||
/// proposed's initial commit in via `git fetch`, tag it `deployed/0`.
|
||||
/// This is the *only* time hive-c0re reads from `proposed` for an
|
||||
/// agent — subsequent config changes are fetched from the forge's `main`
|
||||
/// (see `actions::advance_applied_to_rev`).
|
||||
///
|
||||
/// `proposed_dir` is `None` on rebuild paths where the repo already
|
||||
/// exists — we just verify it's the right shape and bail otherwise.
|
||||
/// Unlike the pre-overhaul code path, `flake.nix` is no longer
|
||||
/// regenerated at the host level: it's tracked in proposed (seeded by
|
||||
/// `setup_proposed`) and rides along on every fetch.
|
||||
pub async fn setup_applied(
|
||||
applied_dir: &Path,
|
||||
proposed_dir: Option<&Path>,
|
||||
name: &str,
|
||||
) -> Result<()> {
|
||||
/// A repo that is already there is only checked for its shape (a rebuild
|
||||
/// finds it already seeded): a missing `deployed/0` tag means it predates the
|
||||
/// meta-flake layout and is an error.
|
||||
pub async fn setup_applied(applied_dir: &Path, name: &str) -> Result<()> {
|
||||
std::fs::create_dir_all(applied_dir)
|
||||
.with_context(|| format!("create {}", applied_dir.display()))?;
|
||||
|
||||
if !applied_dir.join(".git").exists() {
|
||||
let Some(proposed) = proposed_dir else {
|
||||
bail!(
|
||||
"applied repo at {} is missing its .git directory; \
|
||||
cannot rebuild without a proposed source to seed from. \
|
||||
destroy --purge and re-spawn this agent.",
|
||||
applied_dir.display()
|
||||
);
|
||||
};
|
||||
git(applied_dir, &["init", "--initial-branch=main"]).await?;
|
||||
let proposed_str = proposed.display().to_string();
|
||||
// Seed the applied repo at the root (template) commit of proposed,
|
||||
// not at `main`, so `deployed/0` is the template baseline rather
|
||||
// than whatever the manager may have already committed on top.
|
||||
let root_sha = git_root_commit(proposed).await?;
|
||||
git(
|
||||
applied_dir,
|
||||
// --update-head-ok lets us fetch into refs/heads/main while
|
||||
// HEAD still points there. git's default safeguard refuses
|
||||
// to avoid index/working-tree desync, but the working tree
|
||||
// is empty (we just `init`'d) and we read-tree-reset right
|
||||
// after, so the safeguard is moot here.
|
||||
&[
|
||||
"fetch",
|
||||
"--no-tags",
|
||||
"--update-head-ok",
|
||||
&proposed_str,
|
||||
&format!("{root_sha}:refs/heads/main"),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
git_read_tree_reset(applied_dir, "refs/heads/main").await?;
|
||||
if crate::forge::fetch_config_main_into_applied(name).await {
|
||||
return Ok(());
|
||||
}
|
||||
seed_template(applied_dir, name).await?;
|
||||
git_tag(applied_dir, "deployed/0", "refs/heads/main").await?;
|
||||
} else if git_rev_parse(applied_dir, "refs/tags/deployed/0")
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
// Pre-overhaul applied repo — no deployed/* tag scheme,
|
||||
// Pre-overhaul applied repo - no deployed/* tag scheme,
|
||||
// flake.nix may be untracked, agent.nix possibly authored by
|
||||
// hive-c0re directly. The startup auto-migration fixes this
|
||||
// in place; if it didn't run (or got skipped), surface a
|
||||
|
|
@ -168,6 +62,30 @@ pub async fn setup_applied(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Author the initial config in place: `agent.nix` (the agent's own module)
|
||||
/// and `flake.nix` (the boilerplate that lets the meta flake import this repo
|
||||
/// as an input: meta locks at a specific sha and reads
|
||||
/// `nixosModules.default`, so `flake.nix` must be in the commit). `flake.nix`
|
||||
/// isn't meant to be edited, but it's tracked so it can be read.
|
||||
///
|
||||
/// The fallback half of [`setup_applied`]: reached only when the forge has
|
||||
/// no config for this agent to take.
|
||||
async fn seed_template(dir: &Path, name: &str) -> Result<()> {
|
||||
let agent_path = dir.join("agent.nix");
|
||||
if !agent_path.exists() {
|
||||
std::fs::write(&agent_path, initial_agent_nix(name))
|
||||
.with_context(|| format!("write {}", agent_path.display()))?;
|
||||
}
|
||||
let flake_path = dir.join("flake.nix");
|
||||
if !flake_path.exists() {
|
||||
std::fs::write(&flake_path, initial_flake_nix())
|
||||
.with_context(|| format!("write {}", flake_path.display()))?;
|
||||
}
|
||||
git(dir, &["init", "--initial-branch=main"]).await?;
|
||||
git(dir, &["add", "agent.nix", "flake.nix"]).await?;
|
||||
git_commit(dir, "hive-c0re init").await
|
||||
}
|
||||
|
||||
/// Create the per-agent Claude credentials dir if missing. Mode 0755 — hive-core
|
||||
/// needs read+execute to list the directory so `claude_has_session` can detect a
|
||||
/// valid session; credential files inside (`.credentials.json` etc.) are 0600 so
|
||||
|
|
|
|||
Loading…
Reference in a new issue