Watch
0
0
Fork
You've already forked hyperhive
0

forge: external forge accounts live in swarm bao; the agent fetches them itself

An operator now links an agent's external forge account (label, base URL,
token) in the swarm UI. swarm-controller stores it at
swarm/agents/<agent>/forge/<label>. There is no index: the store's
listing of the agent's forge/ directory is the set of accounts.

In the agent, hive-agent-forge-accounts (oneshot + 2-minute timer, as
the agent user, under its own store certificate) lists
swarm/agents/<agent>/forge/ with the `list` #4866 grants an agent on its
own metadata subtree, reads each account, and writes
<state>/forge-<label>-token and forge-<label>.json in the names and shape
hive-forge -f already reads. An empty listing (a 404, which `bao kv list
-format=json` answers with `{}` and an empty stderr) is zero accounts; a
denial or an unreachable store fails the unit. It never deletes: files
for labels not listed, including ones the hive wrote, stay as they are.

Removed: the dashboard FORGES tab (credentials.js/html section and its
CSS), hive-c0re's extra_forges.rs and its routes, priv_client's
extra-forge calls, and hive-priv's WriteAgentExtraForgeAccount /
DeleteAgentExtraForgeAccount with their helpers. The GITHUB tab and
WriteAgentGithubToken stay.

Also: persistence.md's matrix avatar note names the exit-75 restart on a
changed account listing, not the dashboard, as what brings a linked
account up.

Refs #4348
This commit is contained in:
atlas 2026-10-01 17:50:24 +02:00
commit 2c7e586f47
27 changed files with 815 additions and 748 deletions

View file

@ -9,7 +9,7 @@ never `curl` it directly.
Reads credentials from the environment (`HIVE_FORGE_URL`,
`HYPERHIVE_STATE_DIR`); `-f/--forge <label>` retargets a
dashboard-provisioned external forge account instead. The active repo
swarm-UI-linked external forge account instead. The active repo
resolves `-r/--repo` > the `origin` remote of the cwd's git checkout >
`HIVE_FORGE_REPO` (last-resort override, unset by default) > a hard
error — see `client::Client::from_env`.

View file

@ -84,7 +84,7 @@ impl Client {
/// `json_mode` comes from the global `--json` flag — per-verb
/// output formatters key off it via `Client::json_mode`.
/// `forge_label` (from the global `-f/--forge` flag) targets a
/// dashboard-provisioned external forge account instead of the
/// swarm-UI-linked external forge account instead of the
/// internal forge — see [`resolve_credentials`].
pub fn from_env(
repo_override: Option<String>,
@ -398,18 +398,17 @@ pub fn index(n: u64) -> Result<i64> {
/// Resolve the `(base_url, token)` pair the client authenticates with.
/// `None` (the default) resolves the internal forge exactly as before:
/// `HIVE_FORGE_URL` (default [`DEFAULT_URL`]) + `read_token()`.
/// `Some(label)` (from `-f/--forge <label>`) instead resolves a
/// dashboard-provisioned external forge account: the token comes from
/// `Some(label)` (from `-f/--forge <label>`) instead resolves an
/// external forge account linked in the swarm UI: the token comes from
/// `${HYPERHIVE_STATE_DIR}/forge-<label>-token` and the base URL from
/// the `base_url` key of the sibling `forge-<label>.json` sidecar —
/// the exact same two files `dashboard/extra_forges.rs` writes, so the
/// read side can't drift from the write side. An unknown label (either
/// file missing) is a clear error listing the labels actually found in
/// the state dir, not a raw file-not-found. A label outside the plain
/// identifier charset the dashboard accepts (e.g. a typo containing
/// `/` or `..`) is rejected up front with the same charset spelled out,
/// rather than silently building a nonsense/traversing path and
/// surfacing a confusing file error later.
/// the two files `nix/agent-modules/forge-accounts.nix` writes. An
/// unknown label (either file missing) is a clear error listing the
/// labels actually found in the state dir, not a raw file-not-found. A
/// label outside the plain identifier charset the swarm controller
/// accepts (e.g. a typo containing `/` or `..`) is rejected up front
/// with the same charset spelled out, rather than silently building a
/// nonsense/traversing path and surfacing a confusing file error later.
pub(crate) fn resolve_credentials(forge_label: Option<&str>) -> Result<(String, String)> {
let Some(label) = forge_label else {
let base = std::env::var("HIVE_FORGE_URL").unwrap_or_else(|_| DEFAULT_URL.to_owned());
@ -419,8 +418,8 @@ pub(crate) fn resolve_credentials(forge_label: Option<&str>) -> Result<(String,
if !is_plain_ident(label) {
bail!(
"hive-forge: invalid --forge label {label:?} — must be lowercase \
letters, digits, and hyphens only (same rule the dashboard's \
FORGES tab enforces)"
letters, digits, and hyphens only (same rule the swarm UI's \
link form enforces)"
);
}
@ -445,30 +444,29 @@ pub(crate) fn resolve_credentials(forge_label: Option<&str>) -> Result<(String,
bail!("hive-forge: no such forge {label:?} — provisioned forges: {known}");
}
/// Plain-identifier check matching `dashboard/extra_forges.rs`'s
/// `is_plain_ident` (itself matching hive-priv's `validate_name_chars`)
/// — lowercase ascii + digits + hyphens only. Rejecting anything else
/// up front (rather than just letting a weird label fail to resolve a
/// file) turns a confusing "no such forge" surprise into a precise
/// "that's not a valid label" one, and incidentally means a label like
/// `../../etc` can't be used to build a path outside the state dir.
/// Plain-identifier check matching the label rule of swarm-controller's
/// `put_forge_account` (`hive_types::Ident`) — lowercase ascii, digits and
/// hyphens only. Rejecting anything else up front (rather than just
/// letting a weird label fail to resolve a file) turns a confusing "no
/// such forge" surprise into a precise "that's not a valid label" one,
/// and incidentally means a label like `../../etc` can't be used to
/// build a path outside the state dir.
fn is_plain_ident(s: &str) -> bool {
!s.is_empty()
&& s.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
}
/// Sidecar shape `dashboard/extra_forges.rs` writes alongside each
/// `forge-<label>-token` file: just the base URL, pinned to the same
/// `base_url` JSON key the write side uses.
/// Sidecar shape `nix/agent-modules/forge-accounts.nix` writes alongside
/// each `forge-<label>-token` file: just the base URL, under the
/// `base_url` key that unit's `jq` filter spells.
#[derive(Deserialize)]
struct ForgeSidecar {
base_url: String,
}
/// Scan the state dir for every `forge-<label>-token` file (mirroring
/// `dashboard/extra_forges.rs`'s own listing logic) and return the
/// labels found, sorted. Used to build a helpful "did you mean one of
/// Scan the state dir for every `forge-<label>-token` file and return
/// the labels found, sorted. Used to build a helpful "did you mean one of
/// these" error when `--forge <label>` doesn't resolve. Best-effort:
/// an unreadable state dir yields an empty list rather than erroring
/// (the caller already has its own error to report).

View file

@ -8,7 +8,7 @@
//! cwd's git checkout > `HIVE_FORGE_REPO` (last-resort override, unset
//! by default) > a hard error — see `client::Client::from_env`.
//!
//! The global `-f/--forge <label>` flag targets a dashboard-provisioned
//! The global `-f/--forge <label>` flag targets a swarm-UI-linked
//! external forge account instead: it reads `forge-<label>-token` +
//! `forge-<label>.json` (base URL) from the state dir rather than
//! `HIVE_FORGE_URL`/`forge-token`. See `client::Client::from_env`.
@ -45,9 +45,8 @@ struct Cli {
/// any verb.
#[arg(short = 'r', long, global = true)]
repo: Option<String>,
/// Act as a dashboard-provisioned external forge account (by its
/// FORGES-tab label) instead of the internal forge. Independent of
/// `-r/--repo`.
/// Act as an external forge account linked in the swarm UI (by its
/// label) instead of the internal forge. Independent of `-r/--repo`.
#[arg(short = 'f', long, global = true)]
forge: Option<String>,
/// Emit JSON instead of the default human-readable output (for verbs