hivectl: drop forge create-user; SSO makes a human's forge account
The forge now creates a human's account on their first authelia login, so the verb has no job left. Deletes it, HostRequest::ForgeCreateUser, its handler, provision_user_token, change_user_password and the hive's TOKEN_SCOPES. change_user_password also passed the password as an argument to `forgejo admin user change-password`, so it showed in the container's process list. ensure_user_exists and mint_token stay for the `core` bootstrap, their one caller now. ensure_user_exists loses its password parameter: only the deleted path set one. Refs #3782
This commit is contained in:
parent
113f3fe6e2
commit
ef494af188
10 changed files with 40 additions and 227 deletions
|
|
@ -6,7 +6,6 @@ This document contains the help content for the `hivectl` command-line program.
|
||||||
|
|
||||||
* [`hivectl`↴](#hivectl)
|
* [`hivectl`↴](#hivectl)
|
||||||
* [`hivectl forge`↴](#hivectl-forge)
|
* [`hivectl forge`↴](#hivectl-forge)
|
||||||
* [`hivectl forge create-user`↴](#hivectl-forge-create-user)
|
|
||||||
* [`hivectl forge reconcile-config`↴](#hivectl-forge-reconcile-config)
|
* [`hivectl forge reconcile-config`↴](#hivectl-forge-reconcile-config)
|
||||||
* [`hivectl matrix`↴](#hivectl-matrix)
|
* [`hivectl matrix`↴](#hivectl-matrix)
|
||||||
* [`hivectl matrix create-user`↴](#hivectl-matrix-create-user)
|
* [`hivectl matrix create-user`↴](#hivectl-matrix-create-user)
|
||||||
|
|
@ -63,13 +62,13 @@ This document contains the help content for the `hivectl` command-line program.
|
||||||
|
|
||||||
## `hivectl`
|
## `hivectl`
|
||||||
|
|
||||||
Sibling to the `hive-c0re` daemon binary. Covers host-side admin operations that don't go through the broker — manual user provisioning on the bundled forge + matrix containers, plus future recovery / debugging verbs.
|
Sibling to the `hive-c0re` daemon binary. Covers host-side admin operations that don't go through the broker — manual user provisioning on the bundled matrix container, plus future recovery / debugging verbs.
|
||||||
|
|
||||||
**Usage:** `hivectl [OPTIONS] <COMMAND>`
|
**Usage:** `hivectl [OPTIONS] <COMMAND>`
|
||||||
|
|
||||||
###### **Subcommands:**
|
###### **Subcommands:**
|
||||||
|
|
||||||
* `forge` — Forgejo user provisioning
|
* `forge` — Reconcile an agent's config between this hive and the forge
|
||||||
* `matrix` — matrix-tuwunel user provisioning
|
* `matrix` — matrix-tuwunel user provisioning
|
||||||
* `github` — GitHub account provisioning
|
* `github` — GitHub account provisioning
|
||||||
* `gateway` — Gateway htpasswd user management
|
* `gateway` — Gateway htpasswd user management
|
||||||
|
|
@ -95,38 +94,18 @@ Sibling to the `hive-c0re` daemon binary. Covers host-side admin operations that
|
||||||
|
|
||||||
## `hivectl forge`
|
## `hivectl forge`
|
||||||
|
|
||||||
Forgejo user provisioning.
|
Reconcile an agent's config between this hive and the forge.
|
||||||
|
|
||||||
Manual entry point to the same idempotent provisioning c0re runs at boot — for recovery, ad-hoc reprovisioning, or fixing one agent without bouncing the daemon.
|
A human's first SSO login to the forge makes their forge account.
|
||||||
|
|
||||||
**Usage:** `hivectl forge <COMMAND>`
|
**Usage:** `hivectl forge <COMMAND>`
|
||||||
|
|
||||||
###### **Subcommands:**
|
###### **Subcommands:**
|
||||||
|
|
||||||
* `create-user` — Create or refresh a non-agent Forgejo account + token for `<name>`
|
|
||||||
* `reconcile-config` — Show + reconcile the divergence between an agent's local applied config checkout and its forge `agent-configs/<agent>` main
|
* `reconcile-config` — Show + reconcile the divergence between an agent's local applied config checkout and its forge `agent-configs/<agent>` main
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## `hivectl forge create-user`
|
|
||||||
|
|
||||||
Create or refresh a non-agent Forgejo account + token for `<name>`.
|
|
||||||
|
|
||||||
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>`).
|
|
||||||
|
|
||||||
**Usage:** `hivectl forge create-user [OPTIONS] <NAME>`
|
|
||||||
|
|
||||||
###### **Arguments:**
|
|
||||||
|
|
||||||
* `<NAME>` — Forgejo username of a human/other account — `mara`, `damocles`, etc
|
|
||||||
|
|
||||||
###### **Options:**
|
|
||||||
|
|
||||||
* `--password <PASSWORD>` — Set the account password to this string instead of a random throwaway. Use this for operator accounts that need to log into the forge web UI. Mutually exclusive with `--password-stdin`. WARNING: the password is visible in shell history + process listings; prefer `--password-stdin` for anything sensitive
|
|
||||||
* `--password-stdin` — Read the password from stdin (single line, trailing newline stripped) instead of an inline flag. Mutually exclusive with `--password`
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## `hivectl forge reconcile-config`
|
## `hivectl forge reconcile-config`
|
||||||
|
|
||||||
Show + reconcile the divergence between an agent's local applied config checkout and its forge `agent-configs/<agent>` main.
|
Show + reconcile the divergence between an agent's local applied config checkout and its forge `agent-configs/<agent>` main.
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,7 @@ pub use repos::{
|
||||||
ensure_meta_remote, ensure_repo, ensure_shared_docs_repo, fast_forward_applied_main,
|
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,
|
fetch_config_main_into_applied, meta_read_access, push_config, push_meta, shared_docs_access,
|
||||||
};
|
};
|
||||||
pub use users::{core_token, provision_user_token};
|
pub use users::core_token;
|
||||||
|
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ use forgejo_api::structs::{EditUserOption, UpdateUserAvatarOption};
|
||||||
use forgejo_api::{ApiErrorKind, ForgejoError};
|
use forgejo_api::{ApiErrorKind, ForgejoError};
|
||||||
use reqwest::StatusCode;
|
use reqwest::StatusCode;
|
||||||
|
|
||||||
use super::{CONFIG_ORG, api, forge_admin, is_present};
|
use super::{CONFIG_ORG, api, forge_admin};
|
||||||
|
|
||||||
const TOKEN_NAME_PREFIX: &str = "hyperhive";
|
const TOKEN_NAME_PREFIX: &str = "hyperhive";
|
||||||
// Where the host-side `core` admin token lives. Used by hive-c0re itself
|
// Where the host-side `core` admin token lives. Used by hive-c0re itself
|
||||||
|
|
@ -58,18 +58,12 @@ fn config_org_avatar_png_path() -> std::path::PathBuf {
|
||||||
this process was started outside that unit",
|
this process was started outside that unit",
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
/// Per-agent token scopes (broad-but-not-admin) for tokens hive-c0re
|
|
||||||
/// mints itself on the **internal** forge. See `docs/integrations/forge.md::Token
|
|
||||||
/// scopes` for the per-scope rationale. Not `pub(super)` — external
|
|
||||||
/// forges (`dashboard/extra_forges.rs`) take an operator-pasted token
|
|
||||||
/// verbatim, so their scope is whatever the operator's remote account
|
|
||||||
/// happened to grant; we never mint there and don't need to know it.
|
|
||||||
const TOKEN_SCOPES: &str = "read:user,write:user,read:notification,write:notification,write:repository,write:issue,write:organization,write:misc";
|
|
||||||
|
|
||||||
/// Bootstrap `core` token scopes — adds `read:admin,write:admin` on
|
/// Bootstrap `core` token scopes — adds `read:admin,write:admin` on
|
||||||
/// top of `TOKEN_SCOPES` so the host daemon can drive
|
/// top of the agent scopes (swarm-controller's `AGENT_TOKEN_SCOPES`)
|
||||||
/// `/api/v1/admin/*`. Site-admin membership alone isn't enough: the
|
/// so the host daemon can drive `/api/v1/admin/*`. Site-admin
|
||||||
/// token's own scope gate runs before the user-permission check.
|
/// membership alone isn't enough: the token's own scope gate runs
|
||||||
|
/// before the user-permission check.
|
||||||
/// See `docs/integrations/forge.md::Token scopes`.
|
/// See `docs/integrations/forge.md::Token scopes`.
|
||||||
const CORE_TOKEN_SCOPES: &str = "read:admin,write:admin,read:user,write:user,read:notification,write:notification,write:repository,write:issue,write:organization,write:misc";
|
const CORE_TOKEN_SCOPES: &str = "read:admin,write:admin,read:user,write:user,read:notification,write:notification,write:repository,write:issue,write:organization,write:misc";
|
||||||
|
|
||||||
|
|
@ -137,18 +131,20 @@ fn is_forbidden(e: &ForgejoError) -> bool {
|
||||||
/// Ensure a forgejo user named `name` exists. Idempotent: forgejo
|
/// Ensure a forgejo user named `name` exists. Idempotent: forgejo
|
||||||
/// returns a "user already exists" error which we treat as success.
|
/// returns a "user already exists" error which we treat as success.
|
||||||
/// `admin` adds `--admin` (site admin) — used for the bootstrap
|
/// `admin` adds `--admin` (site admin) — used for the bootstrap
|
||||||
/// `core` user that drives the API. `password` picks the initial
|
/// `core` user that drives the API. The password is random and never
|
||||||
/// account password: `None` uses `--random-password` (the existing
|
/// read: the account authenticates by token.
|
||||||
/// agent provisioning shape — the password is never read, agents auth
|
async fn ensure_user_exists(name: &str, admin: bool) -> Result<()> {
|
||||||
/// by token); `Some(pw)` uses `--password <pw>` so the operator path
|
|
||||||
/// in `hivectl` can set a real password for matrix-style web-UI login.
|
|
||||||
async fn ensure_user_exists(name: &str, admin: bool, password: Option<&str>) -> Result<()> {
|
|
||||||
let email = agent_email(name);
|
let email = agent_email(name);
|
||||||
let mut args = vec!["user", "create", "--username", name, "--email", &email];
|
let mut args = vec![
|
||||||
match password {
|
"user",
|
||||||
Some(pw) => args.extend(["--password", pw, "--must-change-password=false"]),
|
"create",
|
||||||
None => args.extend(["--random-password", "--must-change-password=false"]),
|
"--username",
|
||||||
}
|
name,
|
||||||
|
"--email",
|
||||||
|
&email,
|
||||||
|
"--random-password",
|
||||||
|
"--must-change-password=false",
|
||||||
|
];
|
||||||
if admin {
|
if admin {
|
||||||
args.push("--admin");
|
args.push("--admin");
|
||||||
}
|
}
|
||||||
|
|
@ -174,30 +170,6 @@ async fn ensure_user_exists(name: &str, admin: bool, password: Option<&str>) ->
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the forgejo password for an existing user. Used by the operator
|
|
||||||
/// path in `hivectl forge create-user --password` so re-running on an
|
|
||||||
/// already-created account still updates the password (covers the
|
|
||||||
/// "I forgot the password I set last week" case + the "argus retried
|
|
||||||
/// the verb to verify the fix" case — `forgejo admin user create`
|
|
||||||
/// silently skips a password change once the account exists). Idempotent
|
|
||||||
/// from the operator's point of view: same password input → same final
|
|
||||||
/// account state.
|
|
||||||
async fn change_user_password(name: &str, password: &str) -> Result<()> {
|
|
||||||
let args = [
|
|
||||||
"user",
|
|
||||||
"change-password",
|
|
||||||
"--username",
|
|
||||||
name,
|
|
||||||
"--password",
|
|
||||||
password,
|
|
||||||
];
|
|
||||||
forge_admin(&args)
|
|
||||||
.await
|
|
||||||
.with_context(|| format!("forgejo admin user change-password {name}"))?;
|
|
||||||
tracing::info!(%name, "forge: changed user password");
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Idempotently align the Forgejo account email to `agent_email(name)`.
|
/// Idempotently align the Forgejo account email to `agent_email(name)`.
|
||||||
/// Existing agents were created with `{name}@hive.local`; this corrects
|
/// Existing agents were created with `{name}@hive.local`; this corrects
|
||||||
/// that so git commits (which use `{name}@hyperhive`) link to profiles.
|
/// that so git commits (which use `{name}@hyperhive`) link to profiles.
|
||||||
|
|
@ -313,8 +285,7 @@ pub(super) async fn ensure_repo_creation_disabled(name: &str) {
|
||||||
/// a monotonic clock so re-issuing doesn't collide with an existing
|
/// 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
|
/// token of the same name in the DB. `scopes` is the scope string
|
||||||
/// passed to `forgejo admin user generate-access-token --scopes`;
|
/// passed to `forgejo admin user generate-access-token --scopes`;
|
||||||
/// use `TOKEN_SCOPES` for `hivectl forge create-user` accounts and
|
/// the bootstrap `core` user, its one caller, passes `CORE_TOKEN_SCOPES`.
|
||||||
/// `CORE_TOKEN_SCOPES` for the bootstrap `core` user.
|
|
||||||
async fn mint_token(name: &str, scopes: &str) -> Result<String> {
|
async fn mint_token(name: &str, scopes: &str) -> Result<String> {
|
||||||
let token_name = format!(
|
let token_name = format!(
|
||||||
"{TOKEN_NAME_PREFIX}-{}",
|
"{TOKEN_NAME_PREFIX}-{}",
|
||||||
|
|
@ -366,37 +337,6 @@ async fn mint_and_persist_core_token(path: &Path) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Provision a forgejo user for `name` and return the freshly-minted
|
|
||||||
/// 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
|
|
||||||
/// alone is enough). `Some(pw)` sets `pw` as the password, including
|
|
||||||
/// running `forgejo admin user change-password` if the account already
|
|
||||||
/// exists, so the operator can log into the forge web UI afterwards.
|
|
||||||
/// Idempotent: re-running with the same `Some(pw)` lands on the same
|
|
||||||
/// final state.
|
|
||||||
pub async fn provision_user_token(name: &str, password: Option<&str>) -> Result<String> {
|
|
||||||
if !is_present().await {
|
|
||||||
anyhow::bail!(
|
|
||||||
"hive-forge container not running — wait for hive-c0re to start it before provisioning forge users"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
ensure_user_exists(name, false, password).await?;
|
|
||||||
if let Some(pw) = password {
|
|
||||||
// `user create` silently no-ops on an existing account, so
|
|
||||||
// we run change-password unconditionally when the caller
|
|
||||||
// asked for a specific password — keeps the verb idempotent
|
|
||||||
// for "set or reset" use.
|
|
||||||
change_user_password(name, pw).await?;
|
|
||||||
}
|
|
||||||
ensure_user_email(name).await;
|
|
||||||
mint_token(name, TOKEN_SCOPES).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set `core`'s Forgejo avatar to the hyperhive logo once, then
|
/// Set `core`'s Forgejo avatar to the hyperhive logo once, then
|
||||||
/// remember it so subsequent startups don't re-upload. Best-effort
|
/// remember it so subsequent startups don't re-upload. Best-effort
|
||||||
/// — any non-2xx is logged at the caller; the project runs fine
|
/// — any non-2xx is logged at the caller; the project runs fine
|
||||||
|
|
@ -555,7 +495,7 @@ pub(super) async fn ensure_core_user_and_token() -> Result<String> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ensure_user_exists("core", true, None).await?;
|
ensure_user_exists("core", true).await?;
|
||||||
mint_and_persist_core_token(path).await?;
|
mint_and_persist_core_token(path).await?;
|
||||||
let raw = std::fs::read_to_string(path)
|
let raw = std::fs::read_to_string(path)
|
||||||
.with_context(|| format!("read {CORE_TOKEN_PATH} after mint"))?;
|
.with_context(|| format!("read {CORE_TOKEN_PATH} after mint"))?;
|
||||||
|
|
|
||||||
|
|
@ -868,9 +868,9 @@ async fn finish_user_provisioning(name: &str, access_token: &str) -> Result<()>
|
||||||
/// can pass [`random_password`] to keep the existing throwaway
|
/// can pass [`random_password`] to keep the existing throwaway
|
||||||
/// behaviour.
|
/// behaviour.
|
||||||
///
|
///
|
||||||
/// **Not idempotent** (unlike [`crate::forge::provision_user_token`]): the
|
/// **Not idempotent**: the matrix `/register` endpoint returns
|
||||||
/// matrix `/register` endpoint returns `M_USER_IN_USE` (HTTP 400) on a
|
/// `M_USER_IN_USE` (HTTP 400) on a second call for the same localpart,
|
||||||
/// second call for the same localpart, appservice-authorised or not.
|
/// appservice-authorised or not.
|
||||||
/// Callers re-running this for a known-existing matrix user should expect
|
/// Callers re-running this for a known-existing matrix user should expect
|
||||||
/// a hard error from this fn and route to a password-reset path instead.
|
/// a hard error from this fn and route to a password-reset path instead.
|
||||||
pub async fn provision_user_token(
|
pub async fn provision_user_token(
|
||||||
|
|
|
||||||
|
|
@ -233,9 +233,6 @@ async fn dispatch(req: &HostRequest, coord: Arc<Coordinator>) -> HostResponse {
|
||||||
HostRequest::MatrixInvite { user, room } => {
|
HostRequest::MatrixInvite { user, room } => {
|
||||||
handle_matrix_invite(user, room.as_deref()).await?
|
handle_matrix_invite(user, room.as_deref()).await?
|
||||||
}
|
}
|
||||||
HostRequest::ForgeCreateUser { name, password } => {
|
|
||||||
handle_forge_create_user(name, password.as_deref()).await?
|
|
||||||
}
|
|
||||||
HostRequest::ReconcileConfigStatus { agent, verbose } => {
|
HostRequest::ReconcileConfigStatus { agent, verbose } => {
|
||||||
crate::forge::reconcile_config_status(agent.as_str(), *verbose).await?
|
crate::forge::reconcile_config_status(agent.as_str(), *verbose).await?
|
||||||
}
|
}
|
||||||
|
|
@ -591,43 +588,6 @@ async fn handle_matrix_create_user(
|
||||||
Ok(HostResponse::messages(out))
|
Ok(HostResponse::messages(out))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_forge_create_user(
|
|
||||||
name: &hive_types::Ident,
|
|
||||||
password: Option<&str>,
|
|
||||||
) -> Result<HostResponse> {
|
|
||||||
if !crate::forge::is_present().await {
|
|
||||||
anyhow::bail!(
|
|
||||||
"hive-forge container not running — wait for hive-c0re to start it before provisioning forge users"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if agent_exists(name)? {
|
|
||||||
// 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: '{name}' is an agent; its forge token is minted by \
|
|
||||||
swarm-controller — run `swarmctl agent mint-forge-token {name}` on the \
|
|
||||||
controller host"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let token = crate::forge::provision_user_token(name.as_str(), password)
|
|
||||||
.await
|
|
||||||
.with_context(|| format!("forge create-user {name}"))?;
|
|
||||||
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 {
|
|
||||||
out.push(
|
|
||||||
"password: random throwaway (not surfaced — pass --password or --password-stdin to set one you can use)".to_owned(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
Ok(HostResponse::messages(out))
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_set_agent_github_token(agent: &str, token: &str) -> Result<HostResponse> {
|
async fn handle_set_agent_github_token(agent: &str, token: &str) -> Result<HostResponse> {
|
||||||
crate::priv_client::write_agent_github_token(agent, token)
|
crate::priv_client::write_agent_github_token(agent, token)
|
||||||
.await
|
.await
|
||||||
|
|
|
||||||
|
|
@ -315,17 +315,6 @@ pub enum HostRequest {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
room: Option<String>,
|
room: Option<String>,
|
||||||
},
|
},
|
||||||
/// Create or refresh a forge account + API token for `name`. Daemon-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,
|
|
||||||
#[serde(default)]
|
|
||||||
password: Option<String>,
|
|
||||||
},
|
|
||||||
/// Report the divergence between agent `agent`'s local applied config
|
/// Report the divergence between agent `agent`'s local applied config
|
||||||
/// checkout and its forge `agent-configs/<agent>` `main`. The daemon
|
/// checkout and its forge `agent-configs/<agent>` `main`. The daemon
|
||||||
/// fetches forge `main` read-only and returns a human-readable report
|
/// fetches forge `main` read-only and returns a human-readable report
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ use std::path::PathBuf;
|
||||||
long_about = "\
|
long_about = "\
|
||||||
Sibling to the `hive-c0re` daemon binary. Covers host-side admin \
|
Sibling to the `hive-c0re` daemon binary. Covers host-side admin \
|
||||||
operations that don't go through the broker — manual user \
|
operations that don't go through the broker — manual user \
|
||||||
provisioning on the bundled forge + matrix containers, plus future \
|
provisioning on the bundled matrix container, plus future \
|
||||||
recovery / debugging verbs.\
|
recovery / debugging verbs.\
|
||||||
"
|
"
|
||||||
)]
|
)]
|
||||||
|
|
@ -28,11 +28,10 @@ pub struct Cli {
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
pub enum Cmd {
|
pub enum Cmd {
|
||||||
/// Forgejo user provisioning.
|
/// Reconcile an agent's config between this hive and the forge.
|
||||||
///
|
///
|
||||||
/// Manual entry point to the same idempotent provisioning c0re runs at
|
/// A human's first SSO login to the forge makes their forge
|
||||||
/// boot — for recovery, ad-hoc reprovisioning, or fixing one agent
|
/// account.
|
||||||
/// without bouncing the daemon.
|
|
||||||
Forge {
|
Forge {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
cmd: ForgeCmd,
|
cmd: ForgeCmd,
|
||||||
|
|
@ -248,30 +247,6 @@ impl ScopeArgs {
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
pub enum ForgeCmd {
|
pub enum ForgeCmd {
|
||||||
/// Create or refresh a non-agent Forgejo account + token for `<name>`.
|
|
||||||
///
|
|
||||||
/// 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 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
|
|
||||||
/// into the forge web UI. Mutually exclusive with
|
|
||||||
/// `--password-stdin`. WARNING: the password is visible in
|
|
||||||
/// shell history + process listings; prefer `--password-stdin`
|
|
||||||
/// for anything sensitive.
|
|
||||||
#[arg(long)]
|
|
||||||
password: Option<String>,
|
|
||||||
/// Read the password from stdin (single line, trailing newline
|
|
||||||
/// stripped) instead of an inline flag. Mutually exclusive with
|
|
||||||
/// `--password`.
|
|
||||||
#[arg(long, conflicts_with = "password")]
|
|
||||||
password_stdin: bool,
|
|
||||||
},
|
|
||||||
/// Show + reconcile the divergence between an agent's local applied
|
/// Show + reconcile the divergence between an agent's local applied
|
||||||
/// config checkout and its forge `agent-configs/<agent>` main.
|
/// config checkout and its forge `agent-configs/<agent>` main.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
//! `hivectl forge create-user <name>` — provision a Forgejo account via the
|
//! `hivectl forge reconcile-config <agent>` — show and reconcile an
|
||||||
//! daemon (which owns the forge admin token);
|
//! agent's config divergence via the daemon.
|
||||||
//! hivectl just resolves the password client-side and relays the request.
|
|
||||||
|
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
@ -9,29 +8,7 @@ use anyhow::Result;
|
||||||
use hive_host_sock::{HostRequest, ReconcileDirection};
|
use hive_host_sock::{HostRequest, ReconcileDirection};
|
||||||
|
|
||||||
use crate::cli::ReconcileFrom;
|
use crate::cli::ReconcileFrom;
|
||||||
use crate::util::{daemon_request, resolve_password};
|
use crate::util::daemon_request;
|
||||||
|
|
||||||
pub(crate) async fn forge_create_user(
|
|
||||||
socket: &Path,
|
|
||||||
name: &str,
|
|
||||||
password: Option<&str>,
|
|
||||||
password_stdin: bool,
|
|
||||||
) -> Result<()> {
|
|
||||||
// Resolve the password client-side (inline flag or stdin read); the
|
|
||||||
// daemon never touches this process's stdin. The is-present check, the
|
|
||||||
// agent-vs-operator branch, and token persistence now live in the
|
|
||||||
// daemon handler.
|
|
||||||
let password = resolve_password(password, password_stdin)?;
|
|
||||||
daemon_request(
|
|
||||||
socket,
|
|
||||||
hive_host_sock::HostRequest::ForgeCreateUser {
|
|
||||||
name: crate::util::parse_ident(name)?,
|
|
||||||
password,
|
|
||||||
},
|
|
||||||
"forge",
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `hivectl forge reconcile-config <agent> [--from <forge|local>] [--verbose]`.
|
/// `hivectl forge reconcile-config <agent> [--from <forge|local>] [--verbose]`.
|
||||||
/// Always shows the divergence first (daemon computes it read-only), then
|
/// Always shows the divergence first (daemon computes it read-only), then
|
||||||
|
|
|
||||||
|
|
@ -45,7 +45,7 @@ mod github;
|
||||||
mod watch;
|
mod watch;
|
||||||
use github::github_set_token;
|
use github::github_set_token;
|
||||||
mod forge;
|
mod forge;
|
||||||
use forge::{forge_create_user, forge_reconcile_config};
|
use forge::forge_reconcile_config;
|
||||||
mod agents;
|
mod agents;
|
||||||
use agents::{agents_list, run_agent};
|
use agents::{agents_list, run_agent};
|
||||||
mod power;
|
mod power;
|
||||||
|
|
@ -66,11 +66,6 @@ async fn main() -> Result<()> {
|
||||||
let socket = cli.socket;
|
let socket = cli.socket;
|
||||||
match cli.cmd {
|
match cli.cmd {
|
||||||
Cmd::Forge { cmd } => match cmd {
|
Cmd::Forge { cmd } => match cmd {
|
||||||
ForgeCmd::CreateUser {
|
|
||||||
name,
|
|
||||||
password,
|
|
||||||
password_stdin,
|
|
||||||
} => forge_create_user(&socket, &name, password.as_deref(), password_stdin).await,
|
|
||||||
ForgeCmd::ReconcileConfig {
|
ForgeCmd::ReconcileConfig {
|
||||||
agent,
|
agent,
|
||||||
from,
|
from,
|
||||||
|
|
|
||||||
|
|
@ -30,12 +30,10 @@ use super::Client;
|
||||||
/// of its re-mints, and a later sweep of those must never match this one.
|
/// of its re-mints, and a later sweep of those must never match this one.
|
||||||
pub const AGENT_TOKEN_NAME: &str = "swarm-agent";
|
pub const AGENT_TOKEN_NAME: &str = "swarm-agent";
|
||||||
|
|
||||||
/// The scopes an agent token carries. Byte-identical to `hive-c0re`'s
|
/// The scopes an agent token carries. Byte-identical to the `TOKEN_SCOPES`
|
||||||
/// `forge::users::TOKEN_SCOPES`, so the move from hive to swarm changes
|
/// `hive-c0re` minted with, so the move from hive to swarm changes nothing an
|
||||||
/// nothing an agent can do. Narrowing it is a separate decision.
|
/// agent can do. Narrowing it is a separate decision. A test here pins the
|
||||||
///
|
/// literal.
|
||||||
/// ⚠️ Duplicated across the crate boundary until the last `hive-c0re` caller
|
|
||||||
/// of that constant goes. A test here pins the literal.
|
|
||||||
pub const AGENT_TOKEN_SCOPES: &str = "read:user,write:user,read:notification,write:notification,write:repository,write:issue,write:organization,write:misc";
|
pub const AGENT_TOKEN_SCOPES: &str = "read:user,write:user,read:notification,write:notification,write:repository,write:issue,write:organization,write:misc";
|
||||||
|
|
||||||
/// How often [`spawn`] re-checks every agent's token.
|
/// How often [`spawn`] re-checks every agent's token.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue