hive-c0re: stop minting agent forge tokens
Delete ensure_user_for and mint_and_persist_agent_token, the user step of sync_agent (the per-rebuild re-mint, #4644) and of forge_after_first_spawn, and the hive-priv WriteAgentForgeToken request that wrote the token into the agent's state dir. hivectl forge create-user now refuses an agent and points at swarmctl agent mint-forge-token. mint_token, ensure_user_exists and TOKEN_SCOPES stay: provision_user_token and the core bootstrap still call them. Refs #3782
This commit is contained in:
parent
dd32a395f7
commit
b5d07d4df2
10 changed files with 59 additions and 126 deletions
|
|
@ -527,7 +527,7 @@ pub(crate) async fn resolve_approval_dag(
|
|||
let mut terminal_tag = None;
|
||||
match approval.kind {
|
||||
ApprovalKind::Spawn => {
|
||||
// Post-spawn forge bookkeeping (user, config repo mirror, meta
|
||||
// Post-spawn forge bookkeeping (config repo mirror, meta
|
||||
// access) — warn-only, then the resolution events + a rescan so
|
||||
// the dashboard reflects the post-spawn state either way.
|
||||
if result.is_ok() {
|
||||
|
|
@ -605,13 +605,10 @@ fn fetch_approval_for_worker(
|
|||
}
|
||||
|
||||
/// Forge bookkeeping run once after the very first container spawn:
|
||||
/// create the per-agent forge user, mirror the applied repo, and grant
|
||||
/// read access to core/meta. Also rescans containers so the dashboard
|
||||
/// reflects the post-spawn state.
|
||||
/// mirror the applied repo and grant read access to core/meta. The
|
||||
/// agent's forge user and token are swarm-controller's, not this
|
||||
/// hive's. Also rescans containers so the dashboard reflects the post-spawn state.
|
||||
async fn forge_after_first_spawn(coord: &Arc<Coordinator>, agent: &str) {
|
||||
if let Err(e) = crate::forge::ensure_user_for(agent).await {
|
||||
tracing::warn!(%agent, error = ?e, "forge: ensure_user after first spawn failed");
|
||||
}
|
||||
if let Err(e) = crate::forge::ensure_config_repo(agent).await {
|
||||
tracing::warn!(%agent, error = ?e, "forge: ensure_config_repo after first spawn failed");
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
//! Optional Forgejo wiring — per-agent user + token provisioning,
|
||||
//! Optional Forgejo wiring — per-agent account alignment,
|
||||
//! config-repo mirroring, meta read-access grants. Also seeds
|
||||
//! `internal/docs` — a private repo every agent gets read-only
|
||||
//! collaborator access to for operator-curated shared content.
|
||||
|
|
@ -21,7 +21,7 @@ pub use repos::{
|
|||
ensure_meta_remote, ensure_repo, ensure_shared_docs_repo, fast_forward_applied_main,
|
||||
fetch_config_main_into_applied, meta_read_access, push_config, push_meta, shared_docs_access,
|
||||
};
|
||||
pub use users::{core_token, ensure_user_for, provision_user_token};
|
||||
pub use users::{core_token, provision_user_token};
|
||||
|
||||
use std::sync::OnceLock;
|
||||
use std::time::{Duration, Instant};
|
||||
|
|
@ -212,9 +212,9 @@ pub(crate) fn api(token: &str) -> Result<Forgejo> {
|
|||
Forgejo::new(Auth::Token(token), url).context("build forgejo api client")
|
||||
}
|
||||
|
||||
/// Per-agent forge sync: ensure the agent has a forgejo user + token,
|
||||
/// a mirrored config repo, read access to `core/meta`, and the `meta`
|
||||
/// remote in its proposed repo. All operations are idempotent; failures
|
||||
/// Per-agent forge sync: align the agent's forge account (email,
|
||||
/// repo-creation lockdown), mirror its config repo, grant read access
|
||||
/// to `core/meta`, and wire the `meta` remote in its proposed repo. All operations are idempotent; failures
|
||||
/// are logged as warnings but don't abort the caller.
|
||||
///
|
||||
/// `core_token` is `core_token()` — passed in so callers that already
|
||||
|
|
@ -228,11 +228,10 @@ pub(crate) fn api(token: &str) -> Result<Forgejo> {
|
|||
/// dashboard warning (see [`ensure_all`]); the rebuild path ignores it and
|
||||
/// relies on the journal `warn!` lines alone (a rebuild is its own retry).
|
||||
pub async fn sync_agent(name: &str, core_token: Option<&str>) -> bool {
|
||||
// The agent's forge user and token are swarm-controller's now
|
||||
// (`MintAgentForgeToken`); this hive no longer creates or re-mints
|
||||
// either.
|
||||
let mut ok = true;
|
||||
if let Err(e) = ensure_user_for(name).await {
|
||||
tracing::warn!(%name, error = ?e, "forge: ensure_user failed");
|
||||
ok = false;
|
||||
}
|
||||
// Align email to match the git user.email set by meta::render_flake
|
||||
// so commits link to the agent's Forgejo profile. Best-effort;
|
||||
// also patches up agents created before this fix (old @hive.local).
|
||||
|
|
|
|||
|
|
@ -313,8 +313,8 @@ pub(super) async fn ensure_repo_creation_disabled(name: &str) {
|
|||
/// a monotonic clock so re-issuing doesn't collide with an existing
|
||||
/// token of the same name in the DB. `scopes` is the scope string
|
||||
/// passed to `forgejo admin user generate-access-token --scopes`;
|
||||
/// use `TOKEN_SCOPES` for agents, `CORE_TOKEN_SCOPES` for the
|
||||
/// bootstrap `core` user.
|
||||
/// use `TOKEN_SCOPES` for `hivectl forge create-user` accounts and
|
||||
/// `CORE_TOKEN_SCOPES` for the bootstrap `core` user.
|
||||
async fn mint_token(name: &str, scopes: &str) -> Result<String> {
|
||||
let token_name = format!(
|
||||
"{TOKEN_NAME_PREFIX}-{}",
|
||||
|
|
@ -349,16 +349,6 @@ async fn mint_token(name: &str, scopes: &str) -> Result<String> {
|
|||
Ok(token)
|
||||
}
|
||||
|
||||
/// Mint a fresh Forgejo access token for an agent and write it to the
|
||||
/// agent's state dir via hive-priv. hive-c0re runs unprivileged and
|
||||
/// cannot write to agent-owned (0755) state directories directly.
|
||||
async fn mint_and_persist_agent_token(name: &str) -> Result<()> {
|
||||
let token = mint_token(name, TOKEN_SCOPES).await?;
|
||||
crate::priv_client::write_agent_forge_token(name, &token)
|
||||
.await
|
||||
.with_context(|| format!("write forge-token for {name} via hive-priv"))
|
||||
}
|
||||
|
||||
/// Mint a fresh Forgejo access token for the `core` admin user and
|
||||
/// write it directly to `path`. Unlike agent tokens this path is owned
|
||||
/// by hive-c0re itself (under `/var/lib/hyperhive/`), so a direct
|
||||
|
|
@ -376,24 +366,11 @@ async fn mint_and_persist_core_token(path: &Path) -> Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensure `name` has a forgejo user + token file. Always re-mints the
|
||||
/// token so the on-disk file always reflects the current `TOKEN_SCOPES`.
|
||||
/// Safe to call on every spawn and on every hive-c0re startup.
|
||||
pub async fn ensure_user_for(name: &str) -> Result<()> {
|
||||
if !is_present().await {
|
||||
return Ok(());
|
||||
}
|
||||
ensure_user_exists(name, false, None).await?;
|
||||
ensure_user_email(name).await;
|
||||
mint_and_persist_agent_token(name).await
|
||||
}
|
||||
|
||||
/// Provision a forgejo user for `name` and return the freshly-minted
|
||||
/// token. Unlike [`ensure_user_for`], the token is **not** persisted to
|
||||
/// disk — the caller is responsible for storing it. Used by `hivectl
|
||||
/// forge create-user` for human (non-agent) accounts so we don't create
|
||||
/// stray `/var/lib/hyperhive/agents/<name>/` directories for users that
|
||||
/// aren't agents.
|
||||
/// token, which is **not** persisted to disk — the caller is responsible
|
||||
/// for storing it. Used by `hivectl forge create-user` for human
|
||||
/// (non-agent) accounts. Agent tokens are minted by swarm-controller
|
||||
/// (`swarm-controller/src/forge/agent_token.rs`), not here.
|
||||
///
|
||||
/// `password` picks the account password. `None` keeps the existing
|
||||
/// random-throwaway shape (caller doesn't need web UI access — token
|
||||
|
|
|
|||
|
|
@ -361,18 +361,6 @@ pub async fn set_agent_paused(agent_name: &str, paused: bool) -> Result<()> {
|
|||
.await?)
|
||||
}
|
||||
|
||||
/// Write the Forgejo access token for `agent_name` to
|
||||
/// `<agent_state_root>/<agent_name>/state/forge-token` via hive-priv
|
||||
/// (running as root). The file is written 0600 and chowned to the agent
|
||||
/// user so it is readable from inside the agent container.
|
||||
pub async fn write_agent_forge_token(agent_name: &str, token: &str) -> Result<()> {
|
||||
ok(call(&PrivRequest::WriteAgentForgeToken {
|
||||
agent_name: agent_name.to_owned(),
|
||||
token: token.to_owned(),
|
||||
})
|
||||
.await?)
|
||||
}
|
||||
|
||||
/// Write a Matrix access token for `agent_name` via hive-priv (running as
|
||||
/// root). `account: None` writes the hive-internal
|
||||
/// `<state>/matrix-token`; `account: Some(name)` writes
|
||||
|
|
|
|||
|
|
@ -600,29 +600,24 @@ async fn handle_forge_create_user(
|
|||
"hive-forge container not running — wait for hive-c0re to start it before provisioning forge users"
|
||||
);
|
||||
}
|
||||
let mut out = Vec::new();
|
||||
if agent_exists(name)? {
|
||||
if password.is_some() {
|
||||
// Agents authenticate by API token, never by password — refuse
|
||||
// rather than silently dropping a supplied one.
|
||||
// An agent's forge user and token are swarm-controller's: it mints
|
||||
// the token into the swarm secret store and the agent fetches it
|
||||
// from there. A hive-minted token would be one the swarm neither
|
||||
// tracks nor rotates.
|
||||
anyhow::bail!(
|
||||
"forge create-user: a password is for non-agent (operator) accounts only; '{name}' is an agent which authenticates via API token"
|
||||
"forge create-user: '{name}' is an agent; its forge token is minted by \
|
||||
swarm-controller — run `swarmctl agent mint-forge-token {name}` on the \
|
||||
controller host"
|
||||
);
|
||||
}
|
||||
crate::forge::ensure_user_for(name.as_str())
|
||||
.await
|
||||
.with_context(|| format!("forge create-user {name}"))?;
|
||||
let path = Coordinator::agent_notes_dir(name).join("forge-token");
|
||||
out.push(format!("forge: provisioned agent user '{name}'"));
|
||||
out.push(format!("token persisted at: {}", path.display()));
|
||||
} else {
|
||||
let token = crate::forge::provision_user_token(name.as_str(), password)
|
||||
.await
|
||||
.with_context(|| format!("forge create-user {name}"))?;
|
||||
out.push(format!(
|
||||
"forge: provisioned user '{name}' (not an agent — token not persisted)"
|
||||
));
|
||||
out.push(format!("token: {token}"));
|
||||
let mut out = vec![
|
||||
format!("forge: provisioned user '{name}' (not an agent — token not persisted)"),
|
||||
format!("token: {token}"),
|
||||
];
|
||||
if password.is_some() {
|
||||
out.push("password: set as supplied — use it to log into the forge web UI".to_owned());
|
||||
} else {
|
||||
|
|
@ -630,7 +625,6 @@ async fn handle_forge_create_user(
|
|||
"password: random throwaway (not surfaced — pass --password or --password-stdin to set one you can use)".to_owned(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(HostResponse::messages(out))
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -316,10 +316,10 @@ pub enum HostRequest {
|
|||
room: Option<String>,
|
||||
},
|
||||
/// Create or refresh a forge account + API token for `name`. Daemon-side
|
||||
/// equivalent of `hivectl forge create-user`: for an existing agent it
|
||||
/// provisions the account and persists the token to `<notes>/forge-token`;
|
||||
/// for a non-agent (operator/human) it mints a user and returns the token
|
||||
/// in [`HostResponse::messages`]. `password` is resolved client-side
|
||||
/// equivalent of `hivectl forge create-user`: for a non-agent
|
||||
/// (operator/human) it mints a user and returns the token in
|
||||
/// [`HostResponse::messages`]. An existing agent is refused: its token is
|
||||
/// swarm-controller's to mint. `password` is resolved client-side
|
||||
/// (inline flag or stdin) and only meaningful for non-agent accounts.
|
||||
ForgeCreateUser {
|
||||
name: Ident,
|
||||
|
|
|
|||
|
|
@ -468,19 +468,6 @@ pub enum PrivRequest {
|
|||
},
|
||||
|
||||
// --- Agent credential writes ---
|
||||
/// Write `forge-token` into `AGENT_STATE_ROOT/<agent_name>/state/forge-token`.
|
||||
///
|
||||
/// hive-priv validates `agent_name`, creates the state dir if absent,
|
||||
/// writes the file 0600, and chowns it to the state dir's owner so
|
||||
/// the agent process can read it. Required because hive-c0re runs
|
||||
/// unprivileged and cannot write to agent-owned state directories.
|
||||
WriteAgentForgeToken {
|
||||
/// Logical agent name (validated by `validate_agent_name`).
|
||||
agent_name: String,
|
||||
/// Token value. hive-priv appends a trailing newline before writing.
|
||||
token: String,
|
||||
},
|
||||
|
||||
/// Write a matrix access token into the agent's state dir. With
|
||||
/// `account: None` it targets the hive-internal `matrix-token`; with
|
||||
/// `account: Some(name)` it targets `matrix-token-<name>` for an extra
|
||||
|
|
@ -488,8 +475,10 @@ pub enum PrivRequest {
|
|||
/// `account` suffix as plain identifiers before building the path, so a
|
||||
/// crafted account name cannot traverse out of the state dir.
|
||||
///
|
||||
/// Same write semantics as `WriteAgentForgeToken` — validates names,
|
||||
/// creates dir, writes 0600, chowns to agent owner.
|
||||
/// hive-priv validates the names, creates the state dir if absent,
|
||||
/// writes the file 0600, and chowns it to the state dir's owner so the
|
||||
/// agent process can read it. Required because hive-c0re runs
|
||||
/// unprivileged and cannot write to agent-owned state directories.
|
||||
WriteAgentMatrixToken {
|
||||
/// Logical agent name (validated by `validate_agent_name`).
|
||||
agent_name: String,
|
||||
|
|
@ -512,7 +501,7 @@ pub enum PrivRequest {
|
|||
/// The operator-supplied GitHub personal access token (PAT) for the
|
||||
/// agent's GitHub integration (`services.hyperhive.agent.github.enable`). Same write
|
||||
/// semantics as
|
||||
/// `WriteAgentForgeToken` — validates `agent_name`, creates the state dir
|
||||
/// `WriteAgentMatrixToken` — validates `agent_name`, creates the state dir
|
||||
/// if absent, writes the file 0600, and chowns it to the agent so the
|
||||
/// `gh` wrapper / git credential helper can read it. No account suffix
|
||||
/// (single GitHub account per agent).
|
||||
|
|
@ -534,7 +523,7 @@ pub enum PrivRequest {
|
|||
/// `label` MUST be validated as a plain identifier (same rule as the
|
||||
/// matrix `account` suffix) before it goes into the filename — a
|
||||
/// crafted label could otherwise traverse out of the state dir. Same
|
||||
/// write semantics as `WriteAgentForgeToken` — validates `agent_name`,
|
||||
/// write semantics as `WriteAgentMatrixToken` — validates `agent_name`,
|
||||
/// creates the state dir if absent, writes both files 0600, chowns to
|
||||
/// the agent.
|
||||
WriteAgentExtraForgeAccount {
|
||||
|
|
|
|||
|
|
@ -403,11 +403,6 @@ async fn exec(
|
|||
paused,
|
||||
} => exec_set_agent_paused(agent_name, paused),
|
||||
|
||||
PrivRequest::WriteAgentForgeToken {
|
||||
ref agent_name,
|
||||
ref token,
|
||||
} => write_forge_token(agent_name, token),
|
||||
|
||||
PrivRequest::WriteAgentMatrixToken {
|
||||
ref agent_name,
|
||||
ref token,
|
||||
|
|
@ -619,12 +614,6 @@ fn exec_set_agent_paused(agent_name: &str, paused: bool) -> Result<(String, Stri
|
|||
set_agent_paused(agent_name, paused)
|
||||
}
|
||||
|
||||
/// `WriteAgentForgeToken`.
|
||||
fn write_forge_token(agent_name: &str, token: &str) -> Result<(String, String)> {
|
||||
validate_agent_name(agent_name)?;
|
||||
write_agent_state_file(agent_name, "forge-token", &format!("{token}\n"))
|
||||
}
|
||||
|
||||
/// `WriteAgentGithubToken`.
|
||||
fn write_github_token(agent_name: &str, token: &str) -> Result<(String, String)> {
|
||||
validate_agent_name(agent_name)?;
|
||||
|
|
@ -1441,7 +1430,7 @@ struct ForgeSidecar<'a> {
|
|||
base_url: &'a str,
|
||||
}
|
||||
|
||||
/// Shared helper for `WriteAgentForgeToken` and `WriteAgentMatrixToken`.
|
||||
/// 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),
|
||||
/// and chmods 0600. Running as root (hive-priv), so this succeeds
|
||||
|
|
|
|||
|
|
@ -248,15 +248,15 @@ impl ScopeArgs {
|
|||
|
||||
#[derive(Subcommand)]
|
||||
pub enum ForgeCmd {
|
||||
/// Create or refresh the Forgejo account + token for `<name>`.
|
||||
/// Create or refresh a non-agent Forgejo account + token for `<name>`.
|
||||
///
|
||||
/// For an existing agent, persists the token to its state dir; for a
|
||||
/// human/other account, prints the token to stdout. Set a password to
|
||||
/// enable forge web-UI login (otherwise it uses a random throwaway).
|
||||
/// Prints the token to stdout. Set a password to enable forge web-UI
|
||||
/// login (otherwise it uses a random throwaway). Refused for an
|
||||
/// existing agent: swarm-controller mints an agent's token
|
||||
/// (`swarmctl agent mint-forge-token <agent>`).
|
||||
CreateUser {
|
||||
/// Forgejo username. For agents: the container/agent name
|
||||
/// (`<n>` in `h-<n>`; manager uses the literal `manager`).
|
||||
/// For humans: any forgejo username — `mara`, `damocles`, etc.
|
||||
/// Forgejo username of a human/other account — `mara`,
|
||||
/// `damocles`, etc.
|
||||
name: String,
|
||||
/// Set the account password to this string instead of a random
|
||||
/// throwaway. Use this for operator accounts that need to log
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//! `hivectl forge create-user <name>` — provision a Forgejo account via the
|
||||
//! daemon (which owns the forge admin token + account-token persistence);
|
||||
//! daemon (which owns the forge admin token);
|
||||
//! hivectl just resolves the password client-side and relays the request.
|
||||
|
||||
use std::io::{self, Write};
|
||||
|
|
|
|||
Loading…
Reference in a new issue