diff --git a/docs/tools/hivectl-cli.md b/docs/tools/hivectl-cli.md index 5100315c..fb5af985 100644 --- a/docs/tools/hivectl-cli.md +++ b/docs/tools/hivectl-cli.md @@ -36,7 +36,6 @@ This document contains the help content for the `hivectl` command-line program. * [`hivectl quota limit`↴](#hivectl-quota-limit) * [`hivectl subvol`↴](#hivectl-subvol) * [`hivectl subvol upgrade`↴](#hivectl-subvol-upgrade) -* [`hivectl open`↴](#hivectl-open) * [`hivectl completions`↴](#hivectl-completions) ## `hivectl` @@ -59,7 +58,6 @@ Sibling to the `hive-c0re` daemon binary. Covers host-side admin operations that * `restart` — Restart containers hive-wide — `stop` then `start` over the same scope. Bare `hivectl restart` restarts **everything** (all sub-agents plus the ci/forge/gateway/matrix infra containers); the same scope flags as `stop`/`start` narrow it (`--agents`, `--ci`, `--forge`, `--gateway`, `--matrix`, `--agent `). If the stop phase reports a failure the start phase is skipped so the operator can investigate. Requires the hive-c0re daemon * `quota` — Per-agent disk accounting + optional quotas via btrfs qgroups * `subvol` — btrfs subvolume management for agent state dirs -* `open` — Print (and best-effort open in a browser) a hive web surface URL * `completions` — Generate a shell completion script for `hivectl` and print it to stdout ###### **Options:** @@ -529,31 +527,6 @@ Convert an existing plain-dir agent state root into a btrfs subvolume in place. -## `hivectl open` - -Print (and best-effort open in a browser) a hive web surface URL. - -Resolves the URL from the running daemon (`HostRequest::Urls`), so custom forge / matrix domains work without guessing `forge.`. Prints the URL unconditionally — the reliable core, since the host is usually headless / driven over SSH where `xdg-open` is a no-op — then tries `xdg-open` as a convenience. Bare `hivectl open` opens the operator dashboard. - -**Usage:** `hivectl open [TARGET]` - -###### **Arguments:** - -* `` — Which surface to open. Defaults to the operator dashboard - - Default value: `home` - - Possible values: - - `home`: - The operator dashboard (`https:///`) - - `forge`: - The forge (Forgejo) web UI - - `matrix`: - The matrix GUI (fluffychat) - - - - ## `hivectl completions` Generate a shell completion script for `hivectl` and print it to stdout. diff --git a/docs/tools/hivectl.md b/docs/tools/hivectl.md index 519e8953..1ff0ac41 100644 --- a/docs/tools/hivectl.md +++ b/docs/tools/hivectl.md @@ -151,29 +151,3 @@ lands in a faithful copy of the agent's environment: the hyperhive/matrix MCP tools (which `--continue` needs to replay a tool-using history), and the role prompt. Each flag is included only when its file exists. - -## Open - -Print (and best-effort open in a browser) one of the hive's web surfaces. -Requires the `hive-c0re` daemon to be running. - -```bash -hivectl open # operator dashboard (same as `open home`) -hivectl open home # operator dashboard (https:///) -hivectl open forge # the forge (Forgejo) web UI -hivectl open matrix # the matrix GUI (fluffychat) -``` - -The URL is resolved from the running daemon (`HostRequest::Urls`), which -reads the per-surface public URLs from c0re's service env — so custom -forge / matrix domains resolve correctly instead of assuming -`forge.`. The URL is **always printed** (the reliable core, since -the host is usually headless / driven over SSH), then `xdg-open` is tried -as a convenience — a missing or failing opener is reported as a note, not -an error. - -A surface has no URL when it isn't browser-reachable: `home` needs -`services.hyperhive.domain`; `forge` needs -`services.hyperhive.forge.behindGateway = true`; `matrix` needs -`services.hyperhive.matrix.gui.enable = true`. In those cases the command -exits with a hint naming the option to set. diff --git a/hive-c0re/src/bin/hivectl.rs b/hive-c0re/src/bin/hivectl.rs index 6e050532..d65df71c 100644 --- a/hive-c0re/src/bin/hivectl.rs +++ b/hive-c0re/src/bin/hivectl.rs @@ -204,19 +204,6 @@ enum Cmd { #[command(subcommand)] cmd: SubvolCmd, }, - /// Print (and best-effort open in a browser) a hive web surface URL. - /// - /// Resolves the URL from the running daemon (`HostRequest::Urls`), so - /// custom forge / matrix domains work without guessing `forge.`. - /// Prints the URL unconditionally — the reliable core, since the host - /// is usually headless / driven over SSH where `xdg-open` is a no-op — - /// then tries `xdg-open` as a convenience. Bare `hivectl open` opens the - /// operator dashboard. - Open { - /// Which surface to open. Defaults to the operator dashboard. - #[arg(value_enum, default_value_t = OpenTarget::Home)] - target: OpenTarget, - }, /// Emit the full CLI reference as `CommonMark` to stdout. /// /// Hidden tooling command (not part of day-to-day operator admin): @@ -240,17 +227,6 @@ enum Cmd { }, } -/// Which hive web surface `hivectl open` targets. -#[derive(Copy, Clone, Debug, clap::ValueEnum)] -enum OpenTarget { - /// The operator dashboard (`https:///`). - Home, - /// The forge (Forgejo) web UI. - Forge, - /// The matrix GUI (fluffychat). - Matrix, -} - /// Shared scope flags for `hivectl stop` / `hivectl start`. With no flag /// set the verb targets **everything** (all sub-agents + every controllable /// infra container). Setting any flag restricts to the selected classes, @@ -652,7 +628,6 @@ async fn main() -> Result<()> { print!("{}", clap_markdown::help_markdown::()); Ok(()) } - Cmd::Open { target } => open_url(&socket, target).await, Cmd::Completions { shell } => { generate_completions(shell); Ok(()) @@ -660,41 +635,6 @@ async fn main() -> Result<()> { } } -/// `open ` — resolve the surface URL from the daemon, -/// print it, then best-effort `xdg-open` it. Printing is the reliable -/// core (headless / SSH hosts where no browser opener exists); the open -/// is convenience on top, so a missing/failed `xdg-open` is not an error. -async fn open_url(socket: &Path, target: OpenTarget) -> Result<()> { - let urls = query_hive_urls(socket).await.context( - "could not reach the hive-c0re daemon for URLs — is hive-c0re running? \ - (the socket is at /run/hyperhive/host.sock)", - )?; - let (url, hint) = match target { - OpenTarget::Home => ( - urls.home, - "the dashboard URL needs `services.hyperhive.domain` to be set", - ), - OpenTarget::Forge => ( - urls.forge, - "the public forge URL needs `services.hyperhive.forge.behindGateway = true`", - ), - OpenTarget::Matrix => ( - urls.matrix, - "the matrix GUI URL needs `services.hyperhive.matrix.gui.enable = true`", - ), - }; - let url = url.with_context(|| format!("no URL available for this surface — {hint}"))?; - println!("{url}"); - // Best-effort: many hosts are headless, so a missing opener or a - // non-zero exit is fine — the URL is already printed. - match std::process::Command::new("xdg-open").arg(&url).status() { - Ok(status) if status.success() => {} - Ok(status) => eprintln!("note: xdg-open exited with {status} (URL printed above)"), - Err(e) => eprintln!("note: could not run xdg-open ({e}) (URL printed above)"), - } - Ok(()) -} - /// Emit a shell completion script for `hivectl` to stdout. Walks the clap /// command tree (the single source of truth — same tree `markdown-docs` /// renders) so completions never drift from the actual verbs/flags. @@ -718,20 +658,14 @@ const WG_INTERFACE: &str = "wg-hive"; const HIVE_TLS_CA_PATH: &str = "/var/lib/hive-tls/ca.pem"; /// Best-effort query for this hive's domain from the running daemon -/// (`HostRequest::Urls`, which reads `HYPERHIVE_HIVE_DOMAIN` from c0re's -/// service env). `None` when the daemon is unreachable or the domain is -/// unset — callers decide whether that's fatal. +/// (`HostRequest::HiveDomain`, which reads `HYPERHIVE_HIVE_DOMAIN` from +/// c0re's service env). `None` when the daemon is unreachable or the +/// domain is unset — callers decide whether that's fatal. async fn query_hive_domain(socket: &Path) -> Option { - query_hive_urls(socket).await.and_then(|u| u.domain) -} - -/// Best-effort query for this hive's domain + browser-facing web URLs -/// (`HostRequest::Urls`). `None` when the daemon is unreachable. -async fn query_hive_urls(socket: &Path) -> Option { - hive_c0re::client::request(socket, hive_sh4re::HostRequest::Urls) + hive_c0re::client::request(socket, hive_sh4re::HostRequest::HiveDomain) .await .ok() - .and_then(|r| r.urls) + .and_then(|r| r.domain) } /// Require this hive's domain from the daemon for snippet generation. diff --git a/hive-c0re/src/server.rs b/hive-c0re/src/server.rs index 88817f6b..21402aec 100644 --- a/hive-c0re/src/server.rs +++ b/hive-c0re/src/server.rs @@ -155,12 +155,16 @@ async fn dispatch(req: &HostRequest, coord: Arc) -> HostResponse { .collect(); HostResponse::agent_statuses(rows) } - // The hive domain + per-surface public URLs are injected into - // c0re's service env by hive-c0re.nix; surface them so the - // operator CLI can fill in this hive's own identity (the - // federation peer-config block) and open the web surfaces - // (`hivectl open`). - HostRequest::Urls => HostResponse::urls(hive_urls()), + // The hive domain is injected into c0re's service env by + // hive-c0re.nix (`HYPERHIVE_HIVE_DOMAIN`); surface it so the + // operator CLI can fill in this hive's own identity. + HostRequest::HiveDomain => HostResponse::hive_domain( + // Treat an empty env value as unset — otherwise the CLI + // would emit `swarm.peers."" = …`, invalid nix. + std::env::var("HYPERHIVE_HIVE_DOMAIN") + .ok() + .filter(|d| !d.is_empty()), + ), HostRequest::Pending => HostResponse::pending(coord.approvals.pending()?), HostRequest::Approve { id } => { actions::approve(coord.clone(), *id).await?; @@ -257,7 +261,7 @@ async fn handle_restart_all() -> Result { error: Some(errors.join("; ")), agents: Some(ok_agents), approvals: None, - urls: None, + domain: None, agent_statuses: None, }) } @@ -375,25 +379,6 @@ fn is_broad_scope(scope: &LifecycleScope) -> bool { scope.agents || scope.is_everything() } -/// Assemble this hive's domain + browser-facing web URLs from c0re's -/// service env (injected by hive-c0re.nix). Each field is `None` when its -/// surface isn't browser-reachable (domain unset, forge not behind the -/// gateway, matrix GUI off), so the CLI can hint precisely instead of -/// opening a dead link. Scheme matches the existing `HIVE_FORGE_PUBLIC_URL` -/// convention (gateway terminates TLS, so https). -fn hive_urls() -> hive_sh4re::HiveUrls { - // Treat an empty env value as unset everywhere — an empty domain would - // otherwise render `swarm.peers."" = …` (invalid nix) and `https:///`. - let env = |k: &str| std::env::var(k).ok().filter(|v| !v.is_empty()); - let domain = env("HYPERHIVE_HIVE_DOMAIN"); - hive_sh4re::HiveUrls { - home: domain.as_ref().map(|d| format!("https://{d}/")), - forge: env("HIVE_FORGE_PUBLIC_URL"), - matrix: env("HIVE_MATRIX_PUBLIC_URL"), - domain, - } -} - async fn scoped_agents(scope: &LifecycleScope) -> Result> { use std::collections::BTreeSet; let mut set: BTreeSet = BTreeSet::new(); @@ -445,7 +430,7 @@ fn finish_lifecycle(ok_items: Vec, errors: &[String]) -> HostResponse { error: Some(errors.join("; ")), agents: Some(ok_items), approvals: None, - urls: None, + domain: None, agent_statuses: None, } } diff --git a/hive-sh4re/src/lib.rs b/hive-sh4re/src/lib.rs index 155cfd5e..be7a8e7a 100644 --- a/hive-sh4re/src/lib.rs +++ b/hive-sh4re/src/lib.rs @@ -50,13 +50,10 @@ pub enum HostRequest { /// Reuses the dashboard's per-agent `ContainerView` aggregation. AgentStatus, /// Report this hive's canonical DNS domain - /// (`services.hyperhive.domain`) plus the browser-facing home / - /// forge / matrix URLs, daemon-sourced so custom forge/matrix - /// domains resolve correctly. Each URL is `None` when its subsystem - /// is unreachable from a browser (e.g. forge not behind the gateway, - /// matrix GUI disabled). Backs `hivectl open` + the federation - /// peer-config block (which reads the bare `domain`). - Urls, + /// (`services.hyperhive.domain`), or `None` when unset. Lets the + /// operator CLI fill in the hive's own identity (e.g. the federation + /// peer-config block) without the operator retyping it. + HiveDomain, /// List pending approval requests. Pending, /// Approve a pending request by id; the action runs immediately. @@ -135,29 +132,6 @@ impl LifecycleScope { } } -/// This hive's canonical domain plus the browser-facing URLs for its -/// web surfaces — the `Urls` request result. Every field is `None` when -/// the corresponding surface can't be reached from a browser (domain -/// unset, forge not behind the gateway, matrix GUI disabled), so the CLI -/// can give a precise hint instead of opening a dead link. -#[derive(Debug, Clone, Default, Serialize, Deserialize)] -pub struct HiveUrls { - /// Canonical hive domain (`services.hyperhive.domain`). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub domain: Option, - /// Operator dashboard root (`https:///`). - #[serde(default, skip_serializing_if = "Option::is_none")] - pub home: Option, - /// Forge browser URL (`HIVE_FORGE_PUBLIC_URL`) — only the - /// behind-gateway public URL; `None` on direct-port forge deploys. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub forge: Option, - /// Matrix GUI (fluffychat) browser URL — `None` when the matrix GUI - /// is disabled. - #[serde(default, skip_serializing_if = "Option::is_none")] - pub matrix: Option, -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HostResponse { pub ok: bool, @@ -167,10 +141,10 @@ pub struct HostResponse { pub agents: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub approvals: Option>, - /// `Urls` result — this hive's domain plus the browser-facing - /// home / forge / matrix URLs. `None` for every other request kind. + /// This hive's canonical DNS domain — `HiveDomain` result. `None` + /// when the domain is unset (no `services.hyperhive.domain`). #[serde(default, skip_serializing_if = "Option::is_none")] - pub urls: Option, + pub domain: Option, /// `AgentStatus` result — one row per managed agent with its /// running/health flags + technical state. `None` for every other /// request kind. @@ -271,7 +245,7 @@ impl HostResponse { error: None, agents: None, approvals: None, - urls: None, + domain: None, agent_statuses: None, } } @@ -283,7 +257,7 @@ impl HostResponse { error: Some(message.into()), agents: None, approvals: None, - urls: None, + domain: None, agent_statuses: None, } } @@ -295,7 +269,7 @@ impl HostResponse { error: None, agents: Some(agents), approvals: None, - urls: None, + domain: None, agent_statuses: None, } } @@ -307,20 +281,20 @@ impl HostResponse { error: None, agents: None, approvals: Some(approvals), - urls: None, + domain: None, agent_statuses: None, } } - /// `Urls` result — this hive's domain + browser-facing web URLs. + /// `HiveDomain` result — this hive's canonical domain (or `None`). #[must_use] - pub fn urls(urls: HiveUrls) -> Self { + pub fn hive_domain(domain: Option) -> Self { Self { ok: true, error: None, agents: None, approvals: None, - urls: Some(urls), + domain, agent_statuses: None, } } @@ -333,7 +307,7 @@ impl HostResponse { error: None, agents: None, approvals: None, - urls: None, + domain: None, agent_statuses: Some(rows), } } diff --git a/nix/modules/hive-c0re.nix b/nix/modules/hive-c0re.nix index 52e8e07d..fd35d38a 100644 --- a/nix/modules/hive-c0re.nix +++ b/nix/modules/hive-c0re.nix @@ -953,18 +953,6 @@ in # falls back to `:3000`. HIVE_FORGE_PUBLIC_URL = "https://${config.services.hyperhive.forge.domain}"; } - // - lib.optionalAttrs - ( - config.services.hyperhive.matrix.gui.enable && config.services.hyperhive.matrix.gatewayHost != null - ) - { - # Browser-facing matrix GUI (fluffychat) URL — the gateway - # vhost (`matrix.`). Surfaced via the daemon's `Urls` - # request for `hivectl open matrix`. Absent when the GUI is off - # or no gatewayHost is set (no browser-reachable matrix vhost). - HIVE_MATRIX_PUBLIC_URL = "https://${config.services.hyperhive.matrix.gatewayHost}/"; - } // lib.optionalAttrs (config.services.hyperhive.swarm.peers != { }) { # Peer hives serialised as a JSON array of {domain, cert_fingerprint, # wireguard_address?} objects. Consumed by hive-ag3nt::identity::peers()