hives read the forge through their own swarm-minted read account
swarm-controller now gives every hive in its directory a forge account, `hive-<hive>`, in a new `hives` team of `agent-configs` (read on `repo.code` + `repo.pulls`, every repo including later ones), and mints one `read:repository` token named `swarm-hive` into the store at `swarm/hives/<hive>/forge/token`. A five-minute pass queues a `MintHiveForgeToken` node for a hive whose token is missing or no longer matches the forge, whose account is missing, or whose team membership or team settings drifted. A hive whose store or forge read failed is never queued, so an outage cannot rotate every hive's token. The token decision reuses `agent_token`'s classifier and replace step, now parameterised by name and scopes. An agent may be called `hive-<x>`. The hive account is created with `hive-<hive>@hives.hyperhive.local` and the job refuses an account by that name with any other email before it locks it down, adds it to the team or mints for it; agent creation already refuses the hive's account by the same email check. The controller's bao policy gains the `swarm/hives/+/forge/token` leaf (create/read/update), pinned by the bao-grants module-eval assertion. The hive's own policy already reads `swarm/hives/<hive>/*`. hive-c0re reads that token through `forge::reader_token()`, falling back to the core token file when the store holds none, and uses it for: fetching a merged config rev (swarm deploy, `hivectl forge reconcile-config`), reseeding a purged `applied`, the meta-update cascade's fast-forward, seeding `proposed` on first deploy, and finding the PR that merged a refused rev. Those reads no longer require a local `hive-forge` container. The `/knowledge` clone moves from the forge host's boot sweep into the knowledge pull, so every hive clones it. Posting the deploy-failure comment on that PR stays on the core token. Refs #4851
This commit is contained in:
parent
c8472966e9
commit
21e3f626f4
17 changed files with 1222 additions and 138 deletions
|
|
@ -17,7 +17,7 @@ use crate::coordinator::Coordinator;
|
|||
|
||||
use super::{
|
||||
CONFIG_ORG, SHARED_DOCS_REPO, SHARED_ORG, api, core_auth_header, core_token, forge_git_url,
|
||||
forge_http_base, is_present,
|
||||
forge_http_base, is_present, reader_token,
|
||||
};
|
||||
|
||||
/// Creation options for an empty repo defaulting to `main`.
|
||||
|
|
@ -223,17 +223,14 @@ pub async fn ensure_meta_remote(name: &str) -> Result<()> {
|
|||
/// snapshot. A config deployed through hive-c0re without a forge merge never
|
||||
/// reaches it.
|
||||
///
|
||||
/// Best-effort: returns `false` (not an error) rather than bailing when the
|
||||
/// forge is absent, the core token isn't minted yet, `applied` already has a
|
||||
/// `.git` (nothing to do), or any git step fails — callers fall back to a
|
||||
/// less-current source or the original hard error. Only `true` once `applied` is
|
||||
/// Best-effort: returns `false` (not an error) rather than bailing when there
|
||||
/// is no [`reader_token`], `applied` already has a `.git` (nothing to do), or
|
||||
/// any git step fails — callers fall back to a less-current source or the
|
||||
/// original hard error. Only `true` once `applied` is
|
||||
/// actually seeded and tagged `deployed/0`, mirroring first-spawn's own
|
||||
/// `setup_applied` seed shape exactly.
|
||||
pub async fn fetch_config_main_into_applied(name: &str) -> bool {
|
||||
if !is_present().await {
|
||||
return false;
|
||||
}
|
||||
let Some(token) = core_token() else {
|
||||
let Some(reader) = reader_token().await else {
|
||||
return false;
|
||||
};
|
||||
let applied = crate::paths::applied_dir(name);
|
||||
|
|
@ -249,7 +246,7 @@ pub async fn fetch_config_main_into_applied(name: &str) -> bool {
|
|||
return false;
|
||||
}
|
||||
let url = forge_git_url(&format!("{CONFIG_ORG}/{name}"));
|
||||
let auth = core_auth_header(&token);
|
||||
let auth = reader.auth_header();
|
||||
let out = crate::lifecycle::git_command_authed(&auth)
|
||||
.current_dir(&applied)
|
||||
.args([
|
||||
|
|
@ -298,14 +295,11 @@ pub async fn fetch_config_main_into_applied(name: &str) -> bool {
|
|||
/// ancestry-gated advance for a reviewed PR merge, which this is not.
|
||||
///
|
||||
/// Best-effort, same shape as [`fetch_config_main_into_applied`]: returns
|
||||
/// `false` (not an error) when the forge is absent, the core token isn't
|
||||
/// minted, `applied` has no `.git` yet (nothing to fast-forward — reseed
|
||||
/// instead), or any git step fails.
|
||||
/// `false` (not an error) when there is no [`reader_token`], `applied` has
|
||||
/// no `.git` yet (nothing to fast-forward — reseed instead), or any git step
|
||||
/// fails.
|
||||
pub async fn fast_forward_applied_main(name: &str) -> bool {
|
||||
if !is_present().await {
|
||||
return false;
|
||||
}
|
||||
let Some(token) = core_token() else {
|
||||
let Some(reader) = reader_token().await else {
|
||||
return false;
|
||||
};
|
||||
let applied = crate::paths::applied_dir(name);
|
||||
|
|
@ -313,7 +307,7 @@ pub async fn fast_forward_applied_main(name: &str) -> bool {
|
|||
return false;
|
||||
}
|
||||
let url = forge_git_url(&format!("{CONFIG_ORG}/{name}"));
|
||||
let auth = core_auth_header(&token);
|
||||
let auth = reader.auth_header();
|
||||
let out = crate::lifecycle::git_command_authed(&auth)
|
||||
.current_dir(&applied)
|
||||
.args(["fetch", "--no-tags", &url, "refs/heads/main"])
|
||||
|
|
@ -356,8 +350,8 @@ pub async fn fast_forward_applied_main(name: &str) -> bool {
|
|||
/// is the fallback, not the default.
|
||||
///
|
||||
/// Best-effort like [`fetch_config_main_into_applied`], and `false` is an
|
||||
/// ordinary answer rather than an error: the forge is absent, the core token
|
||||
/// is not minted yet, or `agent-configs/<name>` has no `main` — the last of
|
||||
/// ordinary answer rather than an error: there is no [`reader_token`], the
|
||||
/// forge is unreachable, or `agent-configs/<name>` has no `main` — the last of
|
||||
/// which is the *normal* case right after `swarm-controller` creates the
|
||||
/// agent, before it has pushed anything into the repo it just made.
|
||||
///
|
||||
|
|
@ -366,17 +360,14 @@ pub async fn fast_forward_applied_main(name: &str) -> bool {
|
|||
/// `.git` behind either — so the caller's "does this repo already exist"
|
||||
/// check reads exactly the same before and after a failed attempt.
|
||||
pub async fn clone_config_into_proposed(dir: &Path, name: &str) -> bool {
|
||||
if !is_present().await {
|
||||
return false;
|
||||
}
|
||||
let Some(token) = core_token() else {
|
||||
let Some(reader) = reader_token().await else {
|
||||
return false;
|
||||
};
|
||||
let url = forge_git_url(&format!("{CONFIG_ORG}/{name}"));
|
||||
// `--branch main` fails outright against a repo with no commits, so an
|
||||
// empty repo reads as "nothing to take" instead of cloning to an unborn
|
||||
// HEAD that would then look like a seeded checkout.
|
||||
let out = crate::lifecycle::git_command_authed(&core_auth_header(&token))
|
||||
let out = crate::lifecycle::git_command_authed(&reader.auth_header())
|
||||
.args([
|
||||
"clone",
|
||||
"--branch",
|
||||
|
|
|
|||
Loading…
Reference in a new issue