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:
parent
97fb76ce99
commit
2c7e586f47
27 changed files with 815 additions and 748 deletions
|
|
@ -27,7 +27,6 @@ use hive_priv_sock::{
|
|||
PAUSED_MARKER_FILE, PRIV_SOCK, PrivEvent, PrivRequest, PrivResponse, PrivStream,
|
||||
PrivStreamLine, SIBLING_CONTAINERS,
|
||||
};
|
||||
use serde::Serialize;
|
||||
use tokio::io::{AsyncWriteExt, BufReader};
|
||||
use tokio::net::unix::OwnedWriteHalf;
|
||||
use tokio::net::{UnixListener, UnixStream};
|
||||
|
|
@ -413,18 +412,6 @@ async fn exec(
|
|||
ref token,
|
||||
} => write_github_token(agent_name, token),
|
||||
|
||||
PrivRequest::WriteAgentExtraForgeAccount {
|
||||
ref agent_name,
|
||||
ref label,
|
||||
ref base_url,
|
||||
ref token,
|
||||
} => write_extra_forge_account(agent_name, label, base_url, token),
|
||||
|
||||
PrivRequest::DeleteAgentExtraForgeAccount {
|
||||
ref agent_name,
|
||||
ref label,
|
||||
} => delete_extra_forge_account(agent_name, label),
|
||||
|
||||
PrivRequest::RegisterCiRunner { ref token } => register_ci_runner(token).await,
|
||||
|
||||
PrivRequest::ControlInfraContainer { container, action } => {
|
||||
|
|
@ -523,29 +510,6 @@ async fn exec_forge_admin(args: &[String]) -> Result<(String, String)> {
|
|||
run_forge_admin(args).await
|
||||
}
|
||||
|
||||
/// `WriteAgentExtraForgeAccount`: writes the token, then a
|
||||
/// `forge-<label>.json` sidecar carrying the base URL — there's no
|
||||
/// host-side nix config for extra forges, so this is the only place it's
|
||||
/// persisted.
|
||||
fn write_extra_forge_account(
|
||||
agent_name: &str,
|
||||
label: &str,
|
||||
base_url: &str,
|
||||
token: &str,
|
||||
) -> Result<(String, String)> {
|
||||
validate_agent_name(agent_name)?;
|
||||
validate_name_chars(label)?;
|
||||
let res = write_agent_state_file(
|
||||
agent_name,
|
||||
&format!("forge-{label}-token"),
|
||||
&format!("{token}\n"),
|
||||
)?;
|
||||
let meta = serde_json::to_string(&ForgeSidecar { base_url })
|
||||
.context("serialize forge account sidecar")?;
|
||||
write_agent_state_file(agent_name, &format!("forge-{label}.json"), &meta)?;
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// `StopContainer`.
|
||||
async fn stop_container(name: &str) -> Result<(String, String)> {
|
||||
validate_agent_name(name)?;
|
||||
|
|
@ -589,15 +553,6 @@ fn write_github_token(agent_name: &str, token: &str) -> Result<(String, String)>
|
|||
write_agent_state_file(agent_name, "github-token", &format!("{token}\n"))
|
||||
}
|
||||
|
||||
/// `DeleteAgentExtraForgeAccount`. Missing files are not an error
|
||||
/// (idempotent revoke).
|
||||
fn delete_extra_forge_account(agent_name: &str, label: &str) -> Result<(String, String)> {
|
||||
validate_agent_name(agent_name)?;
|
||||
validate_name_chars(label)?;
|
||||
delete_agent_state_file(agent_name, &format!("forge-{label}-token"))?;
|
||||
delete_agent_state_file(agent_name, &format!("forge-{label}.json"))
|
||||
}
|
||||
|
||||
/// `EnsureAgentSubvolume`.
|
||||
async fn exec_ensure_agent_subvolume(agent_name: &str) -> Result<(String, String)> {
|
||||
validate_agent_name(agent_name)?;
|
||||
|
|
@ -1531,18 +1486,6 @@ fn publish_file(path: &Path, content: &[u8], mode: u32, owner: Option<(u32, u32)
|
|||
staged.publish()
|
||||
}
|
||||
|
||||
/// Sidecar written alongside a dashboard-provisioned extra forge
|
||||
/// account's token (`forge-<label>.json`) so `hive-forge` can resolve
|
||||
/// the account's base URL. Read side: `hive-forge/src/client.rs`'s own
|
||||
/// (separately defined, deserialize-only) `ForgeSidecar` — same field
|
||||
/// name (`base_url`), no shared crate between `hive-priv` and
|
||||
/// `hive-forge` to hang a common type off, so the two structs are
|
||||
/// pinned to the same JSON key by convention, not by the compiler.
|
||||
#[derive(Serialize)]
|
||||
struct ForgeSidecar<'a> {
|
||||
base_url: &'a str,
|
||||
}
|
||||
|
||||
/// Shared helper for the `WriteAgent*Token` requests.
|
||||
/// Writes `content` to `AGENT_STATE_ROOT/<agent_name>/state/<filename>`,
|
||||
/// chowns to the agent user (derived from the state dir's existing owner),
|
||||
|
|
@ -1675,28 +1618,6 @@ fn write_agent_dir_file(
|
|||
Ok((String::new(), String::new()))
|
||||
}
|
||||
|
||||
/// Remove `AGENT_STATE_ROOT/<agent_name>/state/<filename>` if present.
|
||||
/// Idempotent revoke counterpart to [`write_agent_state_file`] — a
|
||||
/// missing file is success, not an error. `filename` must be a single
|
||||
/// plain component (no `/`, `.`, `..`); callers pass a pre-validated
|
||||
/// label into a fixed `forge-<label>-token` shape, same as the write
|
||||
/// side.
|
||||
fn delete_agent_state_file(agent_name: &str, filename: &str) -> Result<(String, String)> {
|
||||
ensure_plain_filename("delete_agent_state_file", filename)?;
|
||||
let path = PathBuf::from(AGENT_STATE_ROOT)
|
||||
.join(agent_name)
|
||||
.join("state")
|
||||
.join(filename);
|
||||
match std::fs::remove_file(&path) {
|
||||
Ok(()) => {
|
||||
tracing::info!(agent = %agent_name, file = %filename, "removed agent state file");
|
||||
}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {}
|
||||
Err(e) => return Err(e).with_context(|| format!("remove {}", path.display())),
|
||||
}
|
||||
Ok((String::new(), String::new()))
|
||||
}
|
||||
|
||||
/// btrfs superblock magic, as reported by `statfs(2)`'s `f_type`.
|
||||
const BTRFS_SUPER_MAGIC: i64 = 0x9123_683E;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue