Compare commits

..
4 changed files with 6 additions and 173 deletions

View file

@ -213,31 +213,13 @@ transient.
The hive-c0re-owned repo at `/var/lib/hyperhive/meta/`
declares one flake input per agent (`agent-<n>.url =
"git+http://<forge>/agent-configs/<n>.git"`) and one
"git+file:///var/lib/hyperhive/applied/<n>"`) and one
`nixosConfigurations.<n>` output per agent. Each output wraps
`inputs.agent-<n>.nixosModules.default` with the identity +
`HIVE_PORT` / `HIVE_LABEL` / `HIVE_DASHBOARD_PORT` injection
module that `setup_applied` used to generate inline.
Containers run against `--flake /var/lib/hyperhive/meta#<n>`.
The declared input url is the agent's **forge config repo** (the
same `agent-configs/<n>` the config-PR flow lands approved changes
on), so the meta flake references a reviewable, reproducible source
rather than a local checkout. hive-c0re authenticates that
`git+http` fetch via a git credential helper that reads the live
forge-core token — no token in the url or the lock. The deploy and
manual-rebuild paths, however, do **not** re-lock from the forge:
they `--override-input agent-<n>
git+file:///var/lib/hyperhive/applied/<n>`, locking the exact config
that `verify_commit` gated and `applied/<n>/main` was
fast-forwarded to. That keeps a deploy/rebuild reproducible and
independent of forge reachability — rebuilds fire on crash-restart
and meta bumps, not just config PRs — while the declared url stays
the forge. `sync_agents` re-renders + re-locks the persistent input;
a plain `nix flake lock` leaves an existing applied override in
place (it only re-locks when the declared url itself changes), so
the forge-declared / applied-deployed split is stable.
Per-deploy lock flow (two-phase, owned by
`actions::run_merge_config_pr``deploy_applied_target`
`meta::{prepare,finalize,abort}_deploy`):

View file

@ -64,11 +64,6 @@ Two things live in the `agent-configs` Forgejo organization:
approved history, the `failed/<id>` tag records the divergence).
Repos stay private, so an agent can't read another
agent's config. (Agents remain read-only collaborators on `core/meta`.)
hive-c0re also references this repo as the agent's **persistent meta
flake input** (`agent-<n>.url = git+http://<forge>/agent-configs/<n>.git`;
see [approvals.md § Meta flake](approvals.md)), fetching it as the `core`
user via a git credential helper that reads the live forge-core token —
so the config lives on the forge, not a hand-synced local checkout.
- The dashboard links each container's "config" anchor to this
config repo, so operators can click straight from the SW4RM tab into
the rendered repo without an extra `git` step.

View file

@ -74,7 +74,6 @@ pub async fn sync_agents(hive: &HiveEnv, agents: &[AgentSpec]) -> Result<()> {
&hive.operator_pronouns,
&hive.context_window_tokens,
agents,
crate::forge::forge_http_base(),
);
let flake_path = dir.join("flake.nix");
let on_disk = std::fs::read_to_string(&flake_path).unwrap_or_default();
@ -239,29 +238,7 @@ pub async fn prepare_deploy(name: &str) -> Result<()> {
let _guard = META_LOCK.lock().await;
let dir = crate::paths::meta_root();
let input = format!("agent-{name}");
// Re-lock the agent input against the LOCAL applied mirror, not the
// persistent forge URL declared in the meta flake (see the `## Meta flake`
// note in docs/approvals.md): the deploy must build the exact reviewed
// config that `verify_commit` gated and `applied/<n>/main` was
// fast-forwarded to, and it must keep working when the forge is
// unreachable (rebuilds fire on crash-restart / meta bumps too, not just
// config PRs). `--override-input` writes the applied rev into the lock;
// the forge URL stays the declared, reviewable source of truth.
let applied = applied_override_url(&crate::paths::applied_dir(name));
nix_logged(
&dir,
&[
"flake",
"update",
&input,
"--override-input",
&input,
&applied,
],
name,
"prepare-deploy",
)
.await?;
nix_logged(&dir, &["flake", "update", &input], name, "prepare-deploy").await?;
// Stage the new lock — git+file://'s dirty-tree fetcher reads
// index entries, so the upcoming nixos-container update sees the
// bumped rev without a commit yet.
@ -304,22 +281,7 @@ pub async fn lock_update_for_rebuild(name: &str) -> Result<()> {
let _guard = META_LOCK.lock().await;
let dir = crate::paths::meta_root();
let input = format!("agent-{name}");
// Re-lock from the local applied mirror, not the persistent forge URL —
// same rationale as `prepare_deploy`: build exactly `applied/<n>/main` and
// stay reproducible when the forge is unreachable.
let applied = applied_override_url(&crate::paths::applied_dir(name));
nix(
&dir,
&[
"flake",
"update",
&input,
"--override-input",
&input,
&applied,
],
)
.await?;
nix(&dir, &["flake", "update", &input]).await?;
if !paths_dirty(&dir, &["flake.lock"]).await? {
return Ok(());
}
@ -339,17 +301,6 @@ fn agent_input_override(applied_dir: &Path, sha: &str) -> String {
format!("git+file://{}?rev={sha}", applied_dir.display())
}
/// `--override-input` URL re-locking an agent's config input against its
/// LOCAL applied mirror (`git+file://<applied_dir>`, current `main` head)
/// instead of the persistent forge URL declared in the meta flake. The
/// deploy + rebuild paths re-lock from here: the applied tree was already
/// fast-forwarded to the reviewed head, so this builds exactly that config
/// and stays reproducible when the forge is unreachable. No `?rev` — `main`
/// head is the reviewed head at deploy time. Pure so it's unit-testable.
fn applied_override_url(applied_dir: &Path) -> String {
format!("git+file://{}", applied_dir.display())
}
/// Non-mutating "would this commit apply?" verify for the PR-based config
/// flow. Evaluates the agent's nixos configuration with its meta
/// input overridden to the exact `sha`, WITHOUT moving `applied/main` or
@ -635,11 +586,6 @@ pub async fn bulk_commit_topology(
Ok(changed)
}
#[allow(
clippy::too_many_arguments,
reason = "many genuine flake inputs (source flakes, port, pronouns, tokens, \
agents, forge base); a params struct would just move the same fields"
)]
fn render_flake(
hyperhive_flake: &str,
docs_flake: &str,
@ -648,7 +594,6 @@ fn render_flake(
operator_pronouns: &str,
context_window_tokens: &std::collections::HashMap<String, u64>,
agents: &[AgentSpec],
forge_base: &str,
) -> String {
render_flake_with_lookup(
hyperhive_flake,
@ -658,7 +603,6 @@ fn render_flake(
operator_pronouns,
context_window_tokens,
agents,
forge_base,
agent_canonical_inputs,
)
}
@ -935,7 +879,6 @@ fn render_flake_with_lookup<F>(
operator_pronouns: &str,
context_window_tokens: &std::collections::HashMap<String, u64>,
agents: &[AgentSpec],
forge_base: &str,
lookup: F,
) -> String
where
@ -980,19 +923,12 @@ where
let _ = writeln!(out, " hyperhive-docs.url = \"{docs_flake}\";");
out.push_str(" hyperhive-docs.flake = false;\n");
}
// Each agent's *persistent* config input is its canonical repo on the
// forge (`git+{forge_base}/agent-configs/<name>.git`, `forge_base` supplied
// by the caller from `HIVE_FORGE_URL`), authenticated by hive-core's git
// credential helper (which reads the live `forge-core-token` — no token in
// the URL or lock). The deploy re-lock + `verify_commit` eval keep pinning
// the local `applied/<name>` override (`agent_input_override`), so a deploy
// never does a network fetch — only the persistent input tracks the forge.
for spec in agents {
let _ = writeln!(
out,
" agent-{name}.url = \"git+{forge_base}/{org}/{name}.git\";",
name = spec.name,
org = crate::forge::CONFIG_ORG,
" agent-{}.url = \"git+file://{}\";",
spec.name,
crate::paths::applied_dir(&spec.name).display(),
);
// For each canonical input the agent declares in its own
// `flake.nix` (detected by reading its applied `flake.lock`),
@ -1514,20 +1450,6 @@ mod tests {
);
}
#[test]
fn applied_override_url_targets_local_mirror_main_head() {
// Deploy + rebuild re-lock against the local applied mirror's `main`
// head (no `?rev`), never the persistent forge URL — so a rebuild
// survives forge unreachability and builds the fast-forwarded config.
let p = Path::new("/var/lib/hyperhive/agents/iris/applied");
let url = applied_override_url(p);
assert_eq!(url, "git+file:///var/lib/hyperhive/agents/iris/applied");
assert!(
!url.contains("?rev="),
"must lock main head, not a pinned rev: {url}"
);
}
#[test]
fn render_flake_uses_explicit_nixpkgs_url_when_provided() {
let out = render_flake(
@ -1538,7 +1460,6 @@ mod tests {
"she/her",
&std::collections::HashMap::new(),
&[sample_spec("alice", false, 9001)],
"http://forge.test",
);
// nixpkgs is a top-level input with an explicit URL; hyperhive
// follows it.
@ -1582,7 +1503,6 @@ mod tests {
"she/her",
&std::collections::HashMap::new(),
&[sample_spec("alice", false, 9001)],
"http://forge.test",
);
assert!(
!out.contains("hyperhive-docs"),
@ -1602,7 +1522,6 @@ mod tests {
"she/her",
&std::collections::HashMap::new(),
&[sample_spec("alice", false, 9001)],
"http://forge.test",
);
assert!(
out.contains("nixpkgs.follows = \"hyperhive/nixpkgs\""),
@ -1636,7 +1555,6 @@ mod tests {
sample_spec("bitburner", false, 9002),
sample_spec("dmatrix", false, 9003),
],
"http://forge.test",
lookup,
);
// bitburner declares nixpkgs → follows emitted.
@ -1665,7 +1583,6 @@ mod tests {
"she/her",
&std::collections::HashMap::new(),
&[sample_spec("alice", false, 9001)],
"http://forge.test",
|_| Vec::new(),
);
// No agent-side follows when the lookup reports nothing
@ -1700,7 +1617,6 @@ mod tests {
"she/her",
&std::collections::HashMap::new(),
&[sample_spec("alice", false, 9001)],
"http://forge.test",
);
unsafe {
std::env::remove_var("HIVE_FORGE_URL");
@ -1723,36 +1639,6 @@ mod tests {
);
}
#[test]
fn render_flake_agent_input_points_at_forge_config_repo() {
// The persistent agent config input must reference the canonical
// repo on the forge (git+http, org `agent-configs`), NOT the local
// `applied/<n>` checkout — that's what lets the config live on the
// forge instead of a hand-synced local copy. Auth is out-of-band via
// hive-core's git credential helper, so no creds appear in the URL.
// `forge_base` is an explicit param now, so no env mutation is needed.
let out = render_flake(
"github:example/hyperhive",
"path:/nix/store/bbbb-hyperhive-docs-source",
"path:/nix/store/aaaa-nixpkgs-source",
8000,
"she/her",
&std::collections::HashMap::new(),
&[sample_spec("alice", false, 9001)],
"http://forge.example.test",
);
assert!(
out.contains(
"agent-alice.url = \"git+http://forge.example.test/agent-configs/alice.git\""
),
"expected the agent input to point at the forge config repo:\n{out}"
);
assert!(
!out.contains("agent-alice.url = \"git+file://"),
"the local applied/<n> path must no longer be the persistent input:\n{out}"
);
}
#[test]
fn render_flake_embeds_hive_ca_when_signalled() {
// When hive-tls.nix signals a self-signed hive CA via
@ -1779,7 +1665,6 @@ mod tests {
"she/her",
&std::collections::HashMap::new(),
&[sample_spec("alice", false, 9001)],
"http://forge.test",
)
};
@ -1862,7 +1747,6 @@ mod tests {
"she/her",
&std::collections::HashMap::new(),
&[sample_spec("alice", false, 9001)],
"http://forge.test",
)
};
unsafe {
@ -1947,7 +1831,6 @@ mod tests {
"she/her",
&std::collections::HashMap::new(),
&[sample_spec("alice", false, 9001)],
"http://forge.test",
)
};
unsafe {

View file

@ -21,33 +21,9 @@ let
# only satisfies the ownership guard. libgit2 honours the literal `*`
# (mid-path globs aren't supported, so per-agent repos can't be listed);
# in practice these processes only ever touch hyperhive's own repos.
#
# The agent config inputs now live on the forge
# (`git+http://${forge.domain}/agent-configs/<n>.git`, see meta.rs render),
# so hive-core's `nix flake update` of those inputs is an authenticated
# `git+http` fetch. The `[credential]` stanza points git at the `hive-forge`
# helper below (scoped to the forge host) so the fetch authenticates as the
# forge `core` user with no token in any URL or lock.
safeDirGitconfig = pkgs.writeText "hyperhive-safe-gitconfig" ''
[safe]
directory = *
[credential "http://${config.services.hyperhive.forge.domain}"]
helper = hive-forge
username = core
'';
# git credential helper for hive-core's authenticated fetches of the
# agent-config repos on the forge. Reads the live forge-core admin token
# (`/var/lib/hyperhive/forge-core-token` — paths.rs `FORGE_CORE_TOKEN`) on
# every invocation, so it never holds a stale copy and survives token
# rotation. Scoped to the forge host by the `[credential]` stanza above;
# implements the git credential-helper protocol (only `get` answers).
forgeCredHelper = pkgs.writeShellScriptBin "git-credential-hive-forge" ''
[ "''${1:-}" = "get" ] || exit 0
if [ -r /var/lib/hyperhive/forge-core-token ]; then
printf 'username=core\n'
printf 'password=%s\n' "$(cat /var/lib/hyperhive/forge-core-token)"
fi
'';
# The `hive-c0re serve` config JSON. Keys are snake_case to match the
@ -140,9 +116,6 @@ in
after = [ "hive-c0re.socket" ];
path = [
pkgs.git
# `git-credential-hive-forge` on PATH so git finds it when nix fetches
# the forge-hosted agent-config inputs (helper = hive-forge).
forgeCredHelper
"/run/current-system/sw"
];
environment = import ./environment.nix { inherit lib config pkgs; };