Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c238ffe1ff | ||
|
|
cc67a05974 | ||
|
|
cce35c20e6 |
17 changed files with 437 additions and 182 deletions
20
CLAUDE.md
20
CLAUDE.md
|
|
@ -25,13 +25,19 @@ hand-maintained per-file tree drifts out of sync with the code.
|
||||||
### Rust workspace (`Cargo.toml` members)
|
### Rust workspace (`Cargo.toml` members)
|
||||||
|
|
||||||
- **`hive-c0re/`** — host daemon (runs as the unprivileged `hive-core`
|
- **`hive-c0re/`** — host daemon (runs as the unprivileged `hive-core`
|
||||||
user) plus two operator CLIs. `src/main.rs` is the `hive-c0re` binary
|
user). `src/main.rs` is the `hive-c0re` binary — **daemon-only**
|
||||||
(serve / spawn / kill / rebuild / approve / destroy / periodic vacuum
|
(`serve` + the periodic vacuum/sweep loops); the operator CLI lives in
|
||||||
loops); `src/bin/hivectl.rs` is the ad-hoc operator admin CLI. Owns
|
the separate `hivectl` crate, which talks to the daemon over the host
|
||||||
the sqlite broker, approval + question + reminder + schedule queues,
|
admin socket. Owns the sqlite broker, approval + question + reminder +
|
||||||
the meta flake, lifecycle (`nixos-container` shellouts), gateway /
|
schedule queues, the meta flake, lifecycle (`nixos-container`
|
||||||
forge / matrix provisioning, per-container stats, and the axum
|
shellouts), gateway / forge / matrix provisioning, per-container stats,
|
||||||
operator dashboard (`dashboard.rs`). Largest crate.
|
and the axum operator dashboard (`dashboard.rs`). Largest crate.
|
||||||
|
- **`hivectl/`** — standalone operator CLI (`hivectl` binary). Talks to
|
||||||
|
the `hive-c0re` daemon over the host admin socket (`hive-host-sock`
|
||||||
|
wire types) — does NOT link `hive-c0re`. Verbs: `agents <spawn|kill|
|
||||||
|
destroy|rebuild|restart|list|set-parent|…>`, `approvals <pending|
|
||||||
|
approve|deny>`, `forge`/`matrix`/`github`/`gateway` provisioning,
|
||||||
|
`choom`, `stop`/`start`, `wg`/`peer-config`.
|
||||||
- **`hive-ag3nt/`** — in-container harness; three sibling binaries for
|
- **`hive-ag3nt/`** — in-container harness; three sibling binaries for
|
||||||
every agent (`hive-agent` serve loop, `hive-agent-mcp`,
|
every agent (`hive-agent` serve loop, `hive-agent-mcp`,
|
||||||
`hive-agent-wake`). Turn-loop *policy* layer (`turn.rs`) over the `hive-claude`
|
`hive-agent-wake`). Turn-loop *policy* layer (`turn.rs`) over the `hive-claude`
|
||||||
|
|
|
||||||
17
Cargo.lock
generated
17
Cargo.lock
generated
|
|
@ -1665,6 +1665,23 @@ dependencies = [
|
||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "hivectl"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"bcrypt",
|
||||||
|
"clap",
|
||||||
|
"clap-markdown",
|
||||||
|
"clap_complete",
|
||||||
|
"hive-host-sock",
|
||||||
|
"hive-sh4re",
|
||||||
|
"indicatif",
|
||||||
|
"serde_json",
|
||||||
|
"tokio",
|
||||||
|
"tracing-subscriber",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hkdf"
|
name = "hkdf"
|
||||||
version = "0.12.4"
|
version = "0.12.4"
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ members = [
|
||||||
"hive-priv",
|
"hive-priv",
|
||||||
"hive-priv-sock",
|
"hive-priv-sock",
|
||||||
"hive-sh4re",
|
"hive-sh4re",
|
||||||
|
"hivectl",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ above was spawned by `alice`), and the operator can reparent any agent. The
|
||||||
bootstrap container (`ruth`) is just another root. Re-parenting is
|
bootstrap container (`ruth`) is just another root. Re-parenting is
|
||||||
operator-driven:
|
operator-driven:
|
||||||
|
|
||||||
- CLI: `hive-c0re set-parent <child> --parent <new>` (or `--root` to
|
- CLI: `hivectl agents set-parent <child> --parent <new>` (or `--root` to
|
||||||
promote). Exactly one of `--parent` / `--root` is required.
|
promote). Exactly one of `--parent` / `--root` is required.
|
||||||
- Dashboard: `POST /api/topology/set-parent` (form fields `child`,
|
- Dashboard: `POST /api/topology/set-parent` (form fields `child`,
|
||||||
optional `new_parent` — absent / empty ⇒ promote to root).
|
optional `new_parent` — absent / empty ⇒ promote to root).
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ request.
|
||||||
3. The operator reviews the PR **on the forge** (native diff, threaded
|
3. The operator reviews the PR **on the forge** (native diff, threaded
|
||||||
comments, CI status) and sees a matching card on the dashboard with a
|
comments, CI status) and sees a matching card on the dashboard with a
|
||||||
"review PR on forge" deep link. They click ◆ APPR0VE (or
|
"review PR on forge" deep link. They click ◆ APPR0VE (or
|
||||||
`hive-c0re approve <id>` on the CLI) once satisfied.
|
`hivectl approvals approve <id>` on the CLI) once satisfied.
|
||||||
4. On approve, `run_merge_config_pr`:
|
4. On approve, `run_merge_config_pr`:
|
||||||
- re-reads the live PR head and **aborts if it drifted** from the
|
- re-reads the live PR head and **aborts if it drifted** from the
|
||||||
reviewed `fetched_sha` (the submitter must push again, which queues
|
reviewed `fetched_sha` (the submitter must push again, which queues
|
||||||
|
|
@ -110,7 +110,7 @@ kind-specific payload carrier.
|
||||||
- `Spawn` — direct container creation from the agent's config repo.
|
- `Spawn` — direct container creation from the agent's config repo.
|
||||||
`commit_ref` is empty. Submitted via `HostRequest::RequestSpawn`
|
`commit_ref` is empty. Submitted via `HostRequest::RequestSpawn`
|
||||||
(operator-gated, the `◆ R3QU3ST SP4WN` dashboard button +
|
(operator-gated, the `◆ R3QU3ST SP4WN` dashboard button +
|
||||||
`hive-c0re request-spawn` CLI). The host-level `HostRequest::Spawn`
|
`hivectl agents request-spawn` CLI). The host-level `HostRequest::Spawn`
|
||||||
variant bypasses the approval queue entirely — privileged-context use
|
variant bypasses the approval queue entirely — privileged-context use
|
||||||
only (operator on the host shell, test scripts, one-off recoveries).
|
only (operator on the host shell, test scripts, one-off recoveries).
|
||||||
This is the **canonical first-spawn**: a new agent's `InitConfig`
|
This is the **canonical first-spawn**: a new agent's `InitConfig`
|
||||||
|
|
|
||||||
|
|
@ -304,7 +304,7 @@ step would re-fire).
|
||||||
`R3V1V3` queues a Spawn approval that reuses the kept state on
|
`R3V1V3` queues a Spawn approval that reuses the kept state on
|
||||||
approve (no re-login).
|
approve (no re-login).
|
||||||
- `PURG3` (opt-in via the dashboard button or
|
- `PURG3` (opt-in via the dashboard button or
|
||||||
`hive-c0re destroy --purge <name>`) — DESTR0Y plus wipes
|
`hivectl agents destroy --purge <name>`) — DESTR0Y plus wipes
|
||||||
`/var/lib/hyperhive/{agents,applied}/<name>/`. Config history,
|
`/var/lib/hyperhive/{agents,applied}/<name>/`. Config history,
|
||||||
claude creds, /state/ notes, and the harness dir are all gone.
|
claude creds, /state/ notes, and the harness dir are all gone.
|
||||||
No undo.
|
No undo.
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,16 @@ This document contains the help content for the `hivectl` command-line program.
|
||||||
* [`hivectl agents list`↴](#hivectl-agents-list)
|
* [`hivectl agents list`↴](#hivectl-agents-list)
|
||||||
* [`hivectl agents restart`↴](#hivectl-agents-restart)
|
* [`hivectl agents restart`↴](#hivectl-agents-restart)
|
||||||
* [`hivectl agents restart-all`↴](#hivectl-agents-restart-all)
|
* [`hivectl agents restart-all`↴](#hivectl-agents-restart-all)
|
||||||
|
* [`hivectl agents spawn`↴](#hivectl-agents-spawn)
|
||||||
|
* [`hivectl agents request-spawn`↴](#hivectl-agents-request-spawn)
|
||||||
|
* [`hivectl agents kill`↴](#hivectl-agents-kill)
|
||||||
|
* [`hivectl agents destroy`↴](#hivectl-agents-destroy)
|
||||||
|
* [`hivectl agents rebuild`↴](#hivectl-agents-rebuild)
|
||||||
|
* [`hivectl agents set-parent`↴](#hivectl-agents-set-parent)
|
||||||
|
* [`hivectl approvals`↴](#hivectl-approvals)
|
||||||
|
* [`hivectl approvals pending`↴](#hivectl-approvals-pending)
|
||||||
|
* [`hivectl approvals approve`↴](#hivectl-approvals-approve)
|
||||||
|
* [`hivectl approvals deny`↴](#hivectl-approvals-deny)
|
||||||
* [`hivectl wg`↴](#hivectl-wg)
|
* [`hivectl wg`↴](#hivectl-wg)
|
||||||
* [`hivectl wg init`↴](#hivectl-wg-init)
|
* [`hivectl wg init`↴](#hivectl-wg-init)
|
||||||
* [`hivectl wg peer`↴](#hivectl-wg-peer)
|
* [`hivectl wg peer`↴](#hivectl-wg-peer)
|
||||||
|
|
@ -58,6 +68,7 @@ Sibling to the `hive-c0re` daemon binary. Covers host-side admin operations that
|
||||||
* `github` — GitHub account provisioning: write an operator-supplied personal access token (PAT) into an agent's `github-token` state file so its `gh` wrapper + git credential helper can authenticate. Unlike forge/matrix there is no account creation — the operator supplies a PAT for an existing GitHub account. A CLI alternative to the dashboard credentials tab; the integration is on by default (`hyperhive.github.enable`), so no per-agent config is needed
|
* `github` — GitHub account provisioning: write an operator-supplied personal access token (PAT) into an agent's `github-token` state file so its `gh` wrapper + git credential helper can authenticate. Unlike forge/matrix there is no account creation — the operator supplies a PAT for an existing GitHub account. A CLI alternative to the dashboard credentials tab; the integration is on by default (`hyperhive.github.enable`), so no per-agent config is needed
|
||||||
* `gateway` — Gateway htpasswd user management. Add, remove, or list users in an htpasswd file used by the gateway's HTTP Basic auth (`services.hyperhive.gateway.auth`). Credentials are stored as `BCrypt` hashes — no extra service or PAM required
|
* `gateway` — Gateway htpasswd user management. Add, remove, or list users in an htpasswd file used by the gateway's HTTP Basic auth (`services.hyperhive.gateway.auth`). Credentials are stored as `BCrypt` hashes — no extra service or PAM required
|
||||||
* `agents` — Agent container management. Requires the hive-c0re daemon to be running (connects to the host admin socket)
|
* `agents` — Agent container management. Requires the hive-c0re daemon to be running (connects to the host admin socket)
|
||||||
|
* `approvals` — Operator approval queue: list pending requests, approve / deny them. Requires the hive-c0re daemon to be running (connects to the host admin socket)
|
||||||
* `wg` — WireGuard inter-hive mesh setup helpers (`services.hyperhive.swarm`)
|
* `wg` — WireGuard inter-hive mesh setup helpers (`services.hyperhive.swarm`)
|
||||||
* `peer-config` — Generate the federation peer-config block for THIS hive — the nix a peer operator pastes into their `services.hyperhive.swarm.peers` to trust + reach this hive. Emits `caCert` (+ a `cp` line for the cert) when this hive serves a self-signed CA, the WireGuard public key when the mesh key exists, and the `wireguard{Address,Endpoint}` you pass. The hive's own domain is filled in automatically from the running daemon (`services.hyperhive.domain`). Reads local state (the TLS CA cert, the wg key); never mutates. `wg init` calls this at the end, so a fresh mesh setup prints the hand-over block too
|
* `peer-config` — Generate the federation peer-config block for THIS hive — the nix a peer operator pastes into their `services.hyperhive.swarm.peers` to trust + reach this hive. Emits `caCert` (+ a `cp` line for the cert) when this hive serves a self-signed CA, the WireGuard public key when the mesh key exists, and the `wireguard{Address,Endpoint}` you pass. The hive's own domain is filled in automatically from the running daemon (`services.hyperhive.domain`). Reads local state (the TLS CA cert, the wg key); never mutates. `wg init` calls this at the end, so a fresh mesh setup prints the hand-over block too
|
||||||
* `choom` — Open an interactive Claude session inside an agent container
|
* `choom` — Open an interactive Claude session inside an agent container
|
||||||
|
|
@ -309,6 +320,12 @@ Agent container management. Requires the hive-c0re daemon to be running (connect
|
||||||
* `list` — Show all managed agents with their status (running / needs-login / needs-update) and technical state (deployed sha, parent, pending reminders). The host roster overview; reuses the dashboard's per-agent aggregation. Requires the daemon running
|
* `list` — Show all managed agents with their status (running / needs-login / needs-update) and technical state (deployed sha, parent, pending reminders). The host roster overview; reuses the dashboard's per-agent aggregation. Requires the daemon running
|
||||||
* `restart` — Stop and start a single agent container without rebuilding config. Useful for "kick the container" when the process is stuck or the container needs a clean restart without changing the NixOS config. Rides the job queue (serialized against in-flight rebuilds for the same agent); waits with live progress unless `--no-wait`
|
* `restart` — Stop and start a single agent container without rebuilding config. Useful for "kick the container" when the process is stuck or the container needs a clean restart without changing the NixOS config. Rides the job queue (serialized against in-flight rebuilds for the same agent); waits with live progress unless `--no-wait`
|
||||||
* `restart-all` — Restart ALL managed agent containers via one restart DAG each — unrelated agents overlap, each serializes on its own lease. Waits for the whole set with live progress unless `--no-wait`
|
* `restart-all` — Restart ALL managed agent containers via one restart DAG each — unrelated agents overlap, each serializes on its own lease. Waits for the whole set with live progress unless `--no-wait`
|
||||||
|
* `spawn` — Spawn a new agent container directly (`h-<name>`). Bypasses the approval queue — operator-on-the-host only. For approval-gated spawns, use `request-spawn`
|
||||||
|
* `request-spawn` — Queue a spawn request as an approval. The container is created on `hivectl approvals approve <id>` (or the dashboard APPR0VE button)
|
||||||
|
* `kill` — Stop a managed container (graceful)
|
||||||
|
* `destroy` — Tear down a sub-agent container. The container is removed; persistent state (config repos + Claude credentials) is kept by default. Pass `--purge` to also wipe the agent's state dirs (config + creds + notes). No undo
|
||||||
|
* `rebuild` — Apply pending config to a managed container
|
||||||
|
* `set-parent` — Move an agent in the topology tree. Set `--parent` to a new parent agent name, or pass `--root` to promote the agent to root (no parent). Exactly one is required. Refuses cycles and unknown agents
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -352,6 +369,133 @@ Restart ALL managed agent containers via one restart DAG each — unrelated agen
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## `hivectl agents spawn`
|
||||||
|
|
||||||
|
Spawn a new agent container directly (`h-<name>`). Bypasses the approval queue — operator-on-the-host only. For approval-gated spawns, use `request-spawn`
|
||||||
|
|
||||||
|
**Usage:** `hivectl agents spawn <NAME>`
|
||||||
|
|
||||||
|
###### **Arguments:**
|
||||||
|
|
||||||
|
* `<NAME>` — Agent name (e.g. `iris`)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## `hivectl agents request-spawn`
|
||||||
|
|
||||||
|
Queue a spawn request as an approval. The container is created on `hivectl approvals approve <id>` (or the dashboard APPR0VE button)
|
||||||
|
|
||||||
|
**Usage:** `hivectl agents request-spawn <NAME>`
|
||||||
|
|
||||||
|
###### **Arguments:**
|
||||||
|
|
||||||
|
* `<NAME>` — Agent name
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## `hivectl agents kill`
|
||||||
|
|
||||||
|
Stop a managed container (graceful)
|
||||||
|
|
||||||
|
**Usage:** `hivectl agents kill <NAME>`
|
||||||
|
|
||||||
|
###### **Arguments:**
|
||||||
|
|
||||||
|
* `<NAME>` — Agent name
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## `hivectl agents destroy`
|
||||||
|
|
||||||
|
Tear down a sub-agent container. The container is removed; persistent state (config repos + Claude credentials) is kept by default. Pass `--purge` to also wipe the agent's state dirs (config + creds + notes). No undo
|
||||||
|
|
||||||
|
**Usage:** `hivectl agents destroy [OPTIONS] <NAME>`
|
||||||
|
|
||||||
|
###### **Arguments:**
|
||||||
|
|
||||||
|
* `<NAME>` — Agent name
|
||||||
|
|
||||||
|
###### **Options:**
|
||||||
|
|
||||||
|
* `--purge`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## `hivectl agents rebuild`
|
||||||
|
|
||||||
|
Apply pending config to a managed container
|
||||||
|
|
||||||
|
**Usage:** `hivectl agents rebuild <NAME>`
|
||||||
|
|
||||||
|
###### **Arguments:**
|
||||||
|
|
||||||
|
* `<NAME>` — Agent name
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## `hivectl agents set-parent`
|
||||||
|
|
||||||
|
Move an agent in the topology tree. Set `--parent` to a new parent agent name, or pass `--root` to promote the agent to root (no parent). Exactly one is required. Refuses cycles and unknown agents
|
||||||
|
|
||||||
|
**Usage:** `hivectl agents set-parent [OPTIONS] <CHILD>`
|
||||||
|
|
||||||
|
###### **Arguments:**
|
||||||
|
|
||||||
|
* `<CHILD>` — Agent to move
|
||||||
|
|
||||||
|
###### **Options:**
|
||||||
|
|
||||||
|
* `--parent <PARENT>` — New parent agent name. Mutually exclusive with `--root`
|
||||||
|
* `--root` — Promote `child` to root (no parent)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## `hivectl approvals`
|
||||||
|
|
||||||
|
Operator approval queue: list pending requests, approve / deny them. Requires the hive-c0re daemon to be running (connects to the host admin socket)
|
||||||
|
|
||||||
|
**Usage:** `hivectl approvals <COMMAND>`
|
||||||
|
|
||||||
|
###### **Subcommands:**
|
||||||
|
|
||||||
|
* `pending` — List pending approval requests submitted by agents
|
||||||
|
* `approve` — Approve a pending request by id; the action runs immediately
|
||||||
|
* `deny` — Deny a pending request by id
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## `hivectl approvals pending`
|
||||||
|
|
||||||
|
List pending approval requests submitted by agents
|
||||||
|
|
||||||
|
**Usage:** `hivectl approvals pending`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## `hivectl approvals approve`
|
||||||
|
|
||||||
|
Approve a pending request by id; the action runs immediately
|
||||||
|
|
||||||
|
**Usage:** `hivectl approvals approve <ID>`
|
||||||
|
|
||||||
|
###### **Arguments:**
|
||||||
|
|
||||||
|
* `<ID>` — Approval id (from `hivectl approvals pending`)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
## `hivectl approvals deny`
|
||||||
|
|
||||||
|
Deny a pending request by id
|
||||||
|
|
||||||
|
**Usage:** `hivectl approvals deny <ID>`
|
||||||
|
|
||||||
|
###### **Arguments:**
|
||||||
|
|
||||||
|
* `<ID>` — Approval id
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## `hivectl wg`
|
## `hivectl wg`
|
||||||
|
|
||||||
WireGuard inter-hive mesh setup helpers (`services.hyperhive.swarm`).
|
WireGuard inter-hive mesh setup helpers (`services.hyperhive.swarm`).
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
//! `hive-c0re` library — module surface shared by the `hive-c0re`
|
//! `hive-c0re` library — the coordinator daemon's module surface
|
||||||
//! daemon binary and the `hivectl` operator CLI.
|
//! (coordinator, broker, axum dashboard, admin/manager/agent unix
|
||||||
|
//! sockets, background sweepers). Consumed by the `hive-c0re` daemon
|
||||||
|
//! binary (`src/main.rs`).
|
||||||
//!
|
//!
|
||||||
//! `hive-c0re` (daemon) keeps the systemd service shape it always had:
|
//! The operator CLI lives in the **standalone `hivectl` crate**, which
|
||||||
//! coordinator, broker, axum dashboard, admin/manager/agent unix
|
//! talks to the daemon over the host admin socket (`hive-host-sock` wire
|
||||||
//! sockets, background sweepers. `hivectl` (sibling bin under
|
//! types) rather than linking this crate — so `hive-c0re` is daemon-only,
|
||||||
//! `src/bin/hivectl.rs`) reuses a thin subset (`forge`, `matrix`,
|
//! not a library shared with a CLI.
|
||||||
//! `lifecycle`) to expose host-side administration verbs — manually
|
|
||||||
//! provisioning forge / matrix users for an agent, etc.
|
|
||||||
//!
|
//!
|
||||||
//! Every module is re-exported `pub` so anything in the crate is
|
//! Every module is re-exported `pub` so anything in the crate is
|
||||||
//! addressable from either binary; the lib doesn't have a curated
|
//! addressable from the daemon binary; the lib doesn't have a curated
|
||||||
//! surface beyond "this is where the modules live".
|
//! surface beyond "this is where the modules live".
|
||||||
//!
|
//!
|
||||||
//! Cohesive clusters live in directory submodules (`stores`, `stats`,
|
//! Cohesive clusters live in directory submodules (`stores`, `stats`,
|
||||||
|
|
@ -19,7 +19,6 @@
|
||||||
|
|
||||||
pub mod actions;
|
pub mod actions;
|
||||||
pub mod agent_config;
|
pub mod agent_config;
|
||||||
pub mod client;
|
|
||||||
pub mod container_view;
|
pub mod container_view;
|
||||||
pub mod coordinator;
|
pub mod coordinator;
|
||||||
pub mod dashboard;
|
pub mod dashboard;
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,14 @@ use tokio::process::Command;
|
||||||
|
|
||||||
use crate::coordinator::{AgentPaths, HiveEnv};
|
use crate::coordinator::{AgentPaths, HiveEnv};
|
||||||
|
|
||||||
/// Sub-agent container prefix. `nixos-container` caps the total container name
|
// `AGENT_PREFIX` (`h-`) + `container_name` are shared with the host-side
|
||||||
/// at 11 chars (it gets encoded into network interface names), so the agent
|
// `hivectl` CLI, so they live in `hive-host-sock`; re-exported here so this
|
||||||
/// name itself can be at most `MAX_AGENT_NAME` chars.
|
// module stays the daemon's facade for its own callsites.
|
||||||
pub const AGENT_PREFIX: &str = "h-";
|
pub use hive_host_sock::{AGENT_PREFIX, container_name};
|
||||||
|
|
||||||
|
/// Max agent-name length. `nixos-container` caps the total container name at
|
||||||
|
/// 11 chars (it gets encoded into network interface names) and `AGENT_PREFIX`
|
||||||
|
/// (`h-`) takes 2, so the agent name itself is at most `MAX_AGENT_NAME` chars.
|
||||||
pub const MAX_AGENT_NAME: usize = 9;
|
pub const MAX_AGENT_NAME: usize = 9;
|
||||||
/// Logical name of the manager agent (broker recipient, state-dir key,
|
/// Logical name of the manager agent (broker recipient, state-dir key,
|
||||||
/// meta flake attribute). All persistent state lives under `ruth/`.
|
/// meta flake attribute). All persistent state lives under `ruth/`.
|
||||||
|
|
@ -124,11 +128,6 @@ pub fn bridge_gateway_ip(subnet_cidr: &str) -> Option<String> {
|
||||||
Some(ip_str.to_owned())
|
Some(ip_str.to_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
pub fn container_name(name: &str) -> String {
|
|
||||||
format!("{AGENT_PREFIX}{name}")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Read the agent user's `(uid, gid)` from the container's nixos-managed
|
/// Read the agent user's `(uid, gid)` from the container's nixos-managed
|
||||||
/// `/etc/passwd`. Returns `None` when the container hasn't been built
|
/// `/etc/passwd`. Returns `None` when the container hasn't been built
|
||||||
/// yet, the passwd file is unparseable, or the agent user is missing
|
/// yet, the passwd file is unparseable, or the agent user is missing
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,8 @@
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{Context as _, Result, bail};
|
use anyhow::{Context as _, Result};
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use hive_host_sock::{HostRequest, HostResponse};
|
|
||||||
|
|
||||||
// Every module hangs off the `hive_c0re` library (see `src/lib.rs`).
|
// Every module hangs off the `hive_c0re` library (see `src/lib.rs`).
|
||||||
// The daemon and the `hivectl` sibling binary share the same module
|
// The daemon and the `hivectl` sibling binary share the same module
|
||||||
|
|
@ -12,7 +11,7 @@ use hive_host_sock::{HostRequest, HostResponse};
|
||||||
// explicit (any new daemon entry point reads off the next add).
|
// explicit (any new daemon entry point reads off the next add).
|
||||||
use hive_c0re::coordinator::{Coordinator, HiveEnv, ServeConfig};
|
use hive_c0re::coordinator::{Coordinator, HiveEnv, ServeConfig};
|
||||||
use hive_c0re::{
|
use hive_c0re::{
|
||||||
agent_sockets, auto_update, broker, client, crash_watch, dashboard, dashboard_events, forge,
|
agent_sockets, auto_update, broker, crash_watch, dashboard, dashboard_events, forge,
|
||||||
host_stats, job_queue, knowledge, matrix, mcp_sockets, migrate, reminder_scheduler,
|
host_stats, job_queue, knowledge, matrix, mcp_sockets, migrate, reminder_scheduler,
|
||||||
scheduled_prompts_worker, server, socket_server, sweep_health, warnings,
|
scheduled_prompts_worker, server, socket_server, sweep_health, warnings,
|
||||||
};
|
};
|
||||||
|
|
@ -91,52 +90,6 @@ enum Cmd {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
build_slots: Option<usize>,
|
build_slots: Option<usize>,
|
||||||
},
|
},
|
||||||
/// Spawn a new agent container directly (`hive-agent-<name>`). Bypasses
|
|
||||||
/// the approval queue — use only as an operator on the host. For
|
|
||||||
/// approval-gated spawns, use `request-spawn` instead.
|
|
||||||
Spawn { name: String },
|
|
||||||
/// Queue a spawn request as an approval. The container is created on
|
|
||||||
/// `approve <id>` (CLI) or the dashboard's APPR0VE button.
|
|
||||||
RequestSpawn { name: String },
|
|
||||||
/// Stop a managed container (graceful).
|
|
||||||
Kill { name: String },
|
|
||||||
/// Tear down a sub-agent container. Container is removed; persistent
|
|
||||||
/// state (config repos + Claude credentials) is kept by default. Pass
|
|
||||||
/// `--purge` to also wipe the agent's state dirs (config + creds +
|
|
||||||
/// notes). No undo.
|
|
||||||
Destroy {
|
|
||||||
name: String,
|
|
||||||
#[arg(long)]
|
|
||||||
purge: bool,
|
|
||||||
},
|
|
||||||
/// Apply pending config to a managed container.
|
|
||||||
Rebuild { name: String },
|
|
||||||
/// List managed containers.
|
|
||||||
List,
|
|
||||||
/// List pending approval requests submitted by the manager.
|
|
||||||
Pending,
|
|
||||||
/// Approve a pending request by id; the action runs immediately.
|
|
||||||
Approve { id: i64 },
|
|
||||||
/// Deny a pending request by id.
|
|
||||||
Deny { id: i64 },
|
|
||||||
/// Move an agent in the topology tree. Set `--parent` to a new
|
|
||||||
/// parent agent name; pass `--root` to promote the agent to root
|
|
||||||
/// (no parent). Refuses cycles and unknown agents. The manager
|
|
||||||
/// is reparentable like any other agent — its privileges come
|
|
||||||
/// from the privileged MCP socket, not its tree position.
|
|
||||||
SetParent {
|
|
||||||
child: String,
|
|
||||||
/// New parent agent name. Mutually exclusive with `--root`.
|
|
||||||
/// Exactly one of `--parent` / `--root` is required — clap
|
|
||||||
/// rejects both-absent calls so a fat-fingered
|
|
||||||
/// `hive-c0re set-parent alice` doesn't silently promote
|
|
||||||
/// alice to root.
|
|
||||||
#[arg(long, conflicts_with = "root", required_unless_present = "root")]
|
|
||||||
parent: Option<String>,
|
|
||||||
/// Promote `child` to root (no parent).
|
|
||||||
#[arg(long)]
|
|
||||||
root: bool,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
|
|
@ -206,37 +159,6 @@ async fn main() -> Result<()> {
|
||||||
}
|
}
|
||||||
cmd_serve(sc.env, sc.model_prices, sc.build_slots, db, &cli.socket).await
|
cmd_serve(sc.env, sc.model_prices, sc.build_slots, db, &cli.socket).await
|
||||||
}
|
}
|
||||||
Cmd::Spawn { name } => {
|
|
||||||
render(client::request(&cli.socket, HostRequest::Spawn { name }).await?)
|
|
||||||
}
|
|
||||||
Cmd::RequestSpawn { name } => {
|
|
||||||
render(client::request(&cli.socket, HostRequest::RequestSpawn { name }).await?)
|
|
||||||
}
|
|
||||||
Cmd::Kill { name } => {
|
|
||||||
render(client::request(&cli.socket, HostRequest::Kill { name }).await?)
|
|
||||||
}
|
|
||||||
Cmd::Destroy { name, purge } => {
|
|
||||||
render(client::request(&cli.socket, HostRequest::Destroy { name, purge }).await?)
|
|
||||||
}
|
|
||||||
Cmd::Rebuild { name } => {
|
|
||||||
render(client::request(&cli.socket, HostRequest::Rebuild { name }).await?)
|
|
||||||
}
|
|
||||||
Cmd::List => render(client::request(&cli.socket, HostRequest::List).await?),
|
|
||||||
Cmd::Pending => render(client::request(&cli.socket, HostRequest::Pending).await?),
|
|
||||||
Cmd::Approve { id } => {
|
|
||||||
render(client::request(&cli.socket, HostRequest::Approve { id }).await?)
|
|
||||||
}
|
|
||||||
Cmd::Deny { id } => render(client::request(&cli.socket, HostRequest::Deny { id }).await?),
|
|
||||||
Cmd::SetParent {
|
|
||||||
child,
|
|
||||||
parent,
|
|
||||||
root,
|
|
||||||
} => {
|
|
||||||
let new_parent = if root { None } else { parent };
|
|
||||||
render(
|
|
||||||
client::request(&cli.socket, HostRequest::SetParent { child, new_parent }).await?,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -648,11 +570,3 @@ fn spawn_broker_to_dashboard_forwarder(coord: Arc<Coordinator>) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render(resp: HostResponse) -> Result<()> {
|
|
||||||
println!("{}", serde_json::to_string_pretty(&resp)?);
|
|
||||||
if !resp.ok {
|
|
||||||
bail!(resp.error.unwrap_or_else(|| "request failed".to_owned()));
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -24,6 +24,12 @@
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
// Layout facts shared with the host-side `hivectl` CLI live in
|
||||||
|
// `hive-host-sock` (the crate hivectl links instead of the whole daemon);
|
||||||
|
// re-exported here so this module stays the daemon's single-source facade for
|
||||||
|
// its own callsites.
|
||||||
|
pub use hive_host_sock::{AGENTS_ROOT, GATEWAY_HTPASSWD, HOST_SOCKET, agent_state_dir};
|
||||||
|
|
||||||
/// Root of all hive-c0re persistent state.
|
/// Root of all hive-c0re persistent state.
|
||||||
// nix: bind-mount source `services.hyperhive.c0re.statePath` (hive-c0re.nix) — must match.
|
// nix: bind-mount source `services.hyperhive.c0re.statePath` (hive-c0re.nix) — must match.
|
||||||
pub const STATE_ROOT: &str = "/var/lib/hyperhive";
|
pub const STATE_ROOT: &str = "/var/lib/hyperhive";
|
||||||
|
|
@ -35,11 +41,6 @@ pub const STATE_ROOT: &str = "/var/lib/hyperhive";
|
||||||
// stay in sync; the privsep boundary prevents importing across the crate.
|
// stay in sync; the privsep boundary prevents importing across the crate.
|
||||||
pub const RUNTIME_ROOT: &str = "/run/hyperhive";
|
pub const RUNTIME_ROOT: &str = "/run/hyperhive";
|
||||||
|
|
||||||
/// Default host admin socket (`/run/hyperhive/host.sock`). Exposed as a
|
|
||||||
/// `&str` for the `--socket` / `--host-socket` clap `default_value` in
|
|
||||||
/// `main.rs` (hive-c0re) and `bin/hivectl.rs`.
|
|
||||||
pub const HOST_SOCKET: &str = "/run/hyperhive/host.sock";
|
|
||||||
|
|
||||||
/// `/run/hive-agent` — per-agent runtime socket dir root (web + bound
|
/// `/run/hive-agent` — per-agent runtime socket dir root (web + bound
|
||||||
/// markers), one subdir per agent.
|
/// markers), one subdir per agent.
|
||||||
// nix: agent container bind-mount / `RuntimeDirectory` (the harness nix modules) — must match.
|
// nix: agent container bind-mount / `RuntimeDirectory` (the harness nix modules) — must match.
|
||||||
|
|
@ -158,26 +159,12 @@ pub fn agent_sockets_file() -> PathBuf {
|
||||||
// single Rust-side source.
|
// single Rust-side source.
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
/// `agents/` — per-agent persistent state root (one subdir per agent,
|
/// `agents/` root (`AGENTS_ROOT`) as an owned `PathBuf`.
|
||||||
/// bind-mounted into each container as `/agents/<name>`). A `&str` (the
|
|
||||||
/// dashboard state-file allow-list uses it for `strip_prefix` /
|
|
||||||
/// `starts_with` checks), so it stays a const; [`agents_root`] wraps it.
|
|
||||||
// nix: agent container bind-mount source (harness modules / agent.nix template) — must match.
|
|
||||||
// priv-sock: `hive_priv_sock::AGENT_STATE_ROOT` is the same value and must stay in sync;
|
|
||||||
// the privsep boundary prevents importing across the crate.
|
|
||||||
pub const AGENTS_ROOT: &str = "/var/lib/hyperhive/agents";
|
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn agents_root() -> PathBuf {
|
pub fn agents_root() -> PathBuf {
|
||||||
PathBuf::from(AGENTS_ROOT)
|
PathBuf::from(AGENTS_ROOT)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `agents/<name>` — one agent's persistent state root.
|
|
||||||
#[must_use]
|
|
||||||
pub fn agent_state_dir(name: &str) -> PathBuf {
|
|
||||||
agents_root().join(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `applied/` — per-agent *applied* (deployed) config repos + rev markers,
|
/// `applied/` — per-agent *applied* (deployed) config repos + rev markers,
|
||||||
/// distinct from the proposed configs under `agents/<name>/config`.
|
/// distinct from the proposed configs under `agents/<name>/config`.
|
||||||
// nix: read by hive-c0re only, but paired with `agents/` in the deploy flow.
|
// nix: read by hive-c0re only, but paired with `agents/` in the deploy flow.
|
||||||
|
|
@ -253,12 +240,6 @@ pub fn gateway_agents_conf() -> PathBuf {
|
||||||
gateway_dir().join("agents.conf")
|
gateway_dir().join("agents.conf")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `gateway/gateway.htpasswd` — nginx basic-auth credential store for the
|
|
||||||
/// operator dashboard vhost. A `&str` (used as a `hivectl` clap
|
|
||||||
/// `default_value`), so it stays a const rather than a `PathBuf` fn.
|
|
||||||
// nix: read by the gateway container's nginx (hive-gateway.nix) — must match.
|
|
||||||
pub const GATEWAY_HTPASSWD: &str = "/var/lib/hyperhive/gateway/gateway.htpasswd";
|
|
||||||
|
|
||||||
/// `forge-core-token` — the hive-c0re forge account API token. A `&str`
|
/// `forge-core-token` — the hive-c0re forge account API token. A `&str`
|
||||||
/// (used in `Path::new` + user-facing `format!` messages), so it stays a
|
/// (used in `Path::new` + user-facing `format!` messages), so it stays a
|
||||||
/// const rather than a `PathBuf` fn.
|
/// const rather than a `PathBuf` fn.
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,51 @@
|
||||||
//! payload types it references (`Approval`, `AgentStatusRow`, `jobs::DagView`)
|
//! payload types it references (`Approval`, `AgentStatusRow`, `jobs::DagView`)
|
||||||
//! stay in `hive-sh4re`.
|
//! stay in `hive-sh4re`.
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use hive_sh4re::{AgentStatusRow, Approval, jobs};
|
use hive_sh4re::{AgentStatusRow, Approval, jobs};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
// ── Shared hive layout facts ──────────────────────────────────────────────
|
||||||
|
// Paths + names both the `hive-c0re` daemon and the host-side `hivectl` CLI
|
||||||
|
// must agree on. Homed here (the protocol crate both sides already depend on)
|
||||||
|
// so a standalone `hivectl` can reach them without linking the whole daemon
|
||||||
|
// crate. `hive-c0re`'s `paths` / `lifecycle` modules re-export these, staying
|
||||||
|
// the daemon's single-source facade for its own callsites.
|
||||||
|
|
||||||
|
/// Default host admin socket (`/run/hyperhive/host.sock`). Used as the
|
||||||
|
/// `--socket` / `--host-socket` clap `default_value` in the daemon and
|
||||||
|
/// `hivectl`.
|
||||||
|
pub const HOST_SOCKET: &str = "/run/hyperhive/host.sock";
|
||||||
|
|
||||||
|
/// `agents/` — per-agent persistent state root (one subdir per agent,
|
||||||
|
/// bind-mounted into each container as `/agents/<name>`).
|
||||||
|
// nix: agent container bind-mount source (harness modules / agent.nix template) — must match.
|
||||||
|
// priv-sock: `hive_priv_sock::AGENT_STATE_ROOT` is the same value and must stay in sync;
|
||||||
|
// the privsep boundary prevents importing across the crate.
|
||||||
|
pub const AGENTS_ROOT: &str = "/var/lib/hyperhive/agents";
|
||||||
|
|
||||||
|
/// `agents/<name>` — one agent's persistent state root.
|
||||||
|
#[must_use]
|
||||||
|
pub fn agent_state_dir(name: &str) -> PathBuf {
|
||||||
|
PathBuf::from(AGENTS_ROOT).join(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `gateway/gateway.htpasswd` — nginx basic-auth credential store for the
|
||||||
|
/// operator dashboard vhost. `hivectl`'s `--htpasswd-file` clap default.
|
||||||
|
// nix: read by the gateway container's nginx (hive-gateway.nix) — must match.
|
||||||
|
pub const GATEWAY_HTPASSWD: &str = "/var/lib/hyperhive/gateway/gateway.htpasswd";
|
||||||
|
|
||||||
|
/// nspawn machine-name prefix for agent containers (`h-<name>`). A single
|
||||||
|
/// `starts_with(AGENT_PREFIX)` filter enumerates managed containers.
|
||||||
|
pub const AGENT_PREFIX: &str = "h-";
|
||||||
|
|
||||||
|
/// Map an agent's logical name to its nspawn machine name (`h-<name>`).
|
||||||
|
#[must_use]
|
||||||
|
pub fn container_name(name: &str) -> String {
|
||||||
|
format!("{AGENT_PREFIX}{name}")
|
||||||
|
}
|
||||||
|
|
||||||
/// Requests on the host admin socket.
|
/// Requests on the host admin socket.
|
||||||
///
|
///
|
||||||
/// Wire format: one JSON object per line.
|
/// Wire format: one JSON object per line.
|
||||||
|
|
|
||||||
24
hivectl/Cargo.toml
Normal file
24
hivectl/Cargo.toml
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
[package]
|
||||||
|
name = "hivectl"
|
||||||
|
edition.workspace = true
|
||||||
|
version.workspace = true
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "hivectl"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
anyhow.workspace = true
|
||||||
|
bcrypt.workspace = true
|
||||||
|
clap.workspace = true
|
||||||
|
clap_complete.workspace = true
|
||||||
|
clap-markdown = "0.1"
|
||||||
|
hive-host-sock.workspace = true
|
||||||
|
hive-sh4re.workspace = true
|
||||||
|
indicatif.workspace = true
|
||||||
|
serde_json.workspace = true
|
||||||
|
tokio.workspace = true
|
||||||
|
tracing-subscriber.workspace = true
|
||||||
|
|
@ -42,10 +42,9 @@ async fn wait_for_dags_plain(socket: &Path, ids: Vec<u64>) -> Result<()> {
|
||||||
let mut failed: Vec<String> = Vec::new();
|
let mut failed: Vec<String> = Vec::new();
|
||||||
while !pending.is_empty() {
|
while !pending.is_empty() {
|
||||||
for id in pending.clone() {
|
for id in pending.clone() {
|
||||||
let resp =
|
let resp = crate::client::request(socket, hive_host_sock::HostRequest::QueueDag { id })
|
||||||
hive_c0re::client::request(socket, hive_host_sock::HostRequest::QueueDag { id })
|
.await
|
||||||
.await
|
.with_context(|| format!("connect to daemon socket {}", socket.display()))?;
|
||||||
.with_context(|| format!("connect to daemon socket {}", socket.display()))?;
|
|
||||||
let dags = resp.dags.unwrap_or_default();
|
let dags = resp.dags.unwrap_or_default();
|
||||||
if dags.is_empty() {
|
if dags.is_empty() {
|
||||||
// Evicted from the queue's history tail — it finished a
|
// Evicted from the queue's history tail — it finished a
|
||||||
|
|
@ -116,10 +115,9 @@ async fn wait_for_dags_animated(socket: &Path, ids: Vec<u64>) -> Result<()> {
|
||||||
while !pending.is_empty() {
|
while !pending.is_empty() {
|
||||||
let now = now_unix();
|
let now = now_unix();
|
||||||
for id in pending.clone() {
|
for id in pending.clone() {
|
||||||
let resp =
|
let resp = crate::client::request(socket, hive_host_sock::HostRequest::QueueDag { id })
|
||||||
hive_c0re::client::request(socket, hive_host_sock::HostRequest::QueueDag { id })
|
.await
|
||||||
.await
|
.with_context(|| format!("connect to daemon socket {}", socket.display()))?;
|
||||||
.with_context(|| format!("connect to daemon socket {}", socket.display()))?;
|
|
||||||
let dags = resp.dags.unwrap_or_default();
|
let dags = resp.dags.unwrap_or_default();
|
||||||
if dags.is_empty() {
|
if dags.is_empty() {
|
||||||
mp.println(format!("job #{id}: gone from queue history"))
|
mp.println(format!("job #{id}: gone from queue history"))
|
||||||
|
|
@ -20,12 +20,13 @@ use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use anyhow::{Context as _, Result, bail};
|
use anyhow::{Context as _, Result, bail};
|
||||||
use clap::{Args, Parser, Subcommand};
|
use clap::{Args, Parser, Subcommand};
|
||||||
|
use hive_host_sock::HostRequest;
|
||||||
|
|
||||||
|
/// The host admin socket client (`request`), split out so it lives with
|
||||||
|
/// hivectl rather than in the daemon crate.
|
||||||
|
mod client;
|
||||||
/// Rebuild-queue DAG progress rendering (`wait_for_dags` + the spinner /
|
/// Rebuild-queue DAG progress rendering (`wait_for_dags` + the spinner /
|
||||||
/// plain renderers), split out to keep this file manageable. `#[path]` keeps
|
/// plain renderers), split out to keep this file manageable.
|
||||||
/// the file under `bin/hivectl/` (a subdir cargo won't treat as its own
|
|
||||||
/// binary) rather than the sibling `bin/dag_progress.rs` a bare `mod` maps to.
|
|
||||||
#[path = "hivectl/dag_progress.rs"]
|
|
||||||
mod dag_progress;
|
mod dag_progress;
|
||||||
use dag_progress::wait_for_dags;
|
use dag_progress::wait_for_dags;
|
||||||
|
|
||||||
|
|
@ -94,6 +95,13 @@ enum Cmd {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
cmd: AgentsCmd,
|
cmd: AgentsCmd,
|
||||||
},
|
},
|
||||||
|
/// Operator approval queue: list pending requests, approve / deny them.
|
||||||
|
/// Requires the hive-c0re daemon to be running (connects to the host
|
||||||
|
/// admin socket).
|
||||||
|
Approvals {
|
||||||
|
#[command(subcommand)]
|
||||||
|
cmd: ApprovalsCmd,
|
||||||
|
},
|
||||||
/// WireGuard inter-hive mesh setup helpers (`services.hyperhive.swarm`).
|
/// WireGuard inter-hive mesh setup helpers (`services.hyperhive.swarm`).
|
||||||
///
|
///
|
||||||
/// One-time-setup convenience so nobody has to remember the `wg` dance:
|
/// One-time-setup convenience so nobody has to remember the `wg` dance:
|
||||||
|
|
@ -456,8 +464,8 @@ enum GithubCmd {
|
||||||
// Default htpasswd file path — the host-side location of the gateway's
|
// Default htpasswd file path — the host-side location of the gateway's
|
||||||
// credential store, pre-created by a tmpfiles rule when
|
// credential store, pre-created by a tmpfiles rule when
|
||||||
// `services.hyperhive.gateway.auth.enable = true`. Literal lives in
|
// `services.hyperhive.gateway.auth.enable = true`. Literal lives in
|
||||||
// `hive_c0re::paths`.
|
// `hive_host_sock`.
|
||||||
use hive_c0re::paths::GATEWAY_HTPASSWD as DEFAULT_HTPASSWD_FILE;
|
use hive_host_sock::GATEWAY_HTPASSWD as DEFAULT_HTPASSWD_FILE;
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum GatewayCmd {
|
enum GatewayCmd {
|
||||||
|
|
@ -564,9 +572,9 @@ enum QuotaCmd {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default host admin socket path. Shared with `hive-c0re`'s `main.rs`
|
// Default host admin socket path. Shared with `hive-c0re`'s `main.rs`
|
||||||
// default via `hive_c0re::paths::HOST_SOCKET` — the daemon binds there
|
// default via `hive_host_sock::HOST_SOCKET` — the daemon binds there
|
||||||
// and `hivectl agents` connects to it.
|
// and `hivectl agents` connects to it.
|
||||||
use hive_c0re::paths::HOST_SOCKET as DEFAULT_HOST_SOCKET;
|
use hive_host_sock::HOST_SOCKET as DEFAULT_HOST_SOCKET;
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
enum AgentsCmd {
|
enum AgentsCmd {
|
||||||
|
|
@ -600,6 +608,70 @@ enum AgentsCmd {
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
no_wait: bool,
|
no_wait: bool,
|
||||||
},
|
},
|
||||||
|
/// Spawn a new agent container directly (`h-<name>`). Bypasses the
|
||||||
|
/// approval queue — operator-on-the-host only. For approval-gated
|
||||||
|
/// spawns, use `request-spawn`.
|
||||||
|
Spawn {
|
||||||
|
/// Agent name (e.g. `iris`).
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
/// Queue a spawn request as an approval. The container is created on
|
||||||
|
/// `hivectl approvals approve <id>` (or the dashboard APPR0VE button).
|
||||||
|
RequestSpawn {
|
||||||
|
/// Agent name.
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
/// Stop a managed container (graceful).
|
||||||
|
Kill {
|
||||||
|
/// Agent name.
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
/// Tear down a sub-agent container. The container is removed; persistent
|
||||||
|
/// state (config repos + Claude credentials) is kept by default. Pass
|
||||||
|
/// `--purge` to also wipe the agent's state dirs (config + creds +
|
||||||
|
/// notes). No undo.
|
||||||
|
Destroy {
|
||||||
|
/// Agent name.
|
||||||
|
name: String,
|
||||||
|
#[arg(long)]
|
||||||
|
purge: bool,
|
||||||
|
},
|
||||||
|
/// Apply pending config to a managed container.
|
||||||
|
Rebuild {
|
||||||
|
/// Agent name.
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
|
/// Move an agent in the topology tree. Set `--parent` to a new parent
|
||||||
|
/// agent name, or pass `--root` to promote the agent to root (no
|
||||||
|
/// parent). Exactly one is required. Refuses cycles and unknown agents.
|
||||||
|
SetParent {
|
||||||
|
/// Agent to move.
|
||||||
|
child: String,
|
||||||
|
/// New parent agent name. Mutually exclusive with `--root`.
|
||||||
|
#[arg(long, conflicts_with = "root", required_unless_present = "root")]
|
||||||
|
parent: Option<String>,
|
||||||
|
/// Promote `child` to root (no parent).
|
||||||
|
#[arg(long)]
|
||||||
|
root: bool,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Operator approval queue: list pending requests and approve / deny them.
|
||||||
|
/// Requires the hive-c0re daemon (connects to the host admin socket).
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum ApprovalsCmd {
|
||||||
|
/// List pending approval requests submitted by agents.
|
||||||
|
Pending,
|
||||||
|
/// Approve a pending request by id; the action runs immediately.
|
||||||
|
Approve {
|
||||||
|
/// Approval id (from `hivectl approvals pending`).
|
||||||
|
id: i64,
|
||||||
|
},
|
||||||
|
/// Deny a pending request by id.
|
||||||
|
Deny {
|
||||||
|
/// Approval id.
|
||||||
|
id: i64,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
#[derive(Subcommand)]
|
||||||
|
|
@ -707,11 +779,8 @@ async fn main() -> Result<()> {
|
||||||
GatewayCmd::DeleteUser { file, username } => gateway_delete_user(&file, &username),
|
GatewayCmd::DeleteUser { file, username } => gateway_delete_user(&file, &username),
|
||||||
GatewayCmd::ListUsers { file } => gateway_list_users(&file),
|
GatewayCmd::ListUsers { file } => gateway_list_users(&file),
|
||||||
},
|
},
|
||||||
Cmd::Agents { cmd } => match cmd {
|
Cmd::Agents { cmd } => run_agents(&socket, cmd).await,
|
||||||
AgentsCmd::List { json } => agents_list(&socket, json).await,
|
Cmd::Approvals { cmd } => run_approvals(&socket, cmd).await,
|
||||||
AgentsCmd::Restart { name, no_wait } => agents_restart(&socket, &name, no_wait).await,
|
|
||||||
AgentsCmd::RestartAll { no_wait } => agents_restart_all(&socket, no_wait).await,
|
|
||||||
},
|
|
||||||
Cmd::Wg { cmd } => match cmd {
|
Cmd::Wg { cmd } => match cmd {
|
||||||
WgCmd::Init { address } => wg_init(&socket, address.as_deref()).await,
|
WgCmd::Init { address } => wg_init(&socket, address.as_deref()).await,
|
||||||
WgCmd::Peer {
|
WgCmd::Peer {
|
||||||
|
|
@ -848,7 +917,7 @@ async fn query_hive_domain(socket: &Path) -> Option<String> {
|
||||||
/// Best-effort query for this hive's domain + browser-facing web URLs
|
/// Best-effort query for this hive's domain + browser-facing web URLs
|
||||||
/// (`HostRequest::Urls`). `None` when the daemon is unreachable.
|
/// (`HostRequest::Urls`). `None` when the daemon is unreachable.
|
||||||
async fn query_hive_urls(socket: &Path) -> Option<hive_host_sock::HiveUrls> {
|
async fn query_hive_urls(socket: &Path) -> Option<hive_host_sock::HiveUrls> {
|
||||||
hive_c0re::client::request(socket, hive_host_sock::HostRequest::Urls)
|
crate::client::request(socket, hive_host_sock::HostRequest::Urls)
|
||||||
.await
|
.await
|
||||||
.ok()
|
.ok()
|
||||||
.and_then(|r| r.urls)
|
.and_then(|r| r.urls)
|
||||||
|
|
@ -1036,7 +1105,7 @@ async fn quota_enable(socket: &Path) -> Result<()> {
|
||||||
/// The daemon resolves the agent set + reads each subvolume's usage (it
|
/// The daemon resolves the agent set + reads each subvolume's usage (it
|
||||||
/// holds the privileged helper); the client just formats the returned rows.
|
/// holds the privileged helper); the client just formats the returned rows.
|
||||||
async fn quota_show(socket: &Path, name: Option<&str>) -> Result<()> {
|
async fn quota_show(socket: &Path, name: Option<&str>) -> Result<()> {
|
||||||
let resp = hive_c0re::client::request(
|
let resp = crate::client::request(
|
||||||
socket,
|
socket,
|
||||||
hive_host_sock::HostRequest::QuotaShow {
|
hive_host_sock::HostRequest::QuotaShow {
|
||||||
name: name.map(str::to_owned),
|
name: name.map(str::to_owned),
|
||||||
|
|
@ -1146,7 +1215,7 @@ fn human_bytes(n: u64) -> String {
|
||||||
/// "needs root" error fixes that first-run footgun, where running a
|
/// "needs root" error fixes that first-run footgun, where running a
|
||||||
/// privileged verb without sudo reported as a missing agent.
|
/// privileged verb without sudo reported as a missing agent.
|
||||||
fn agent_exists(name: &str) -> Result<bool> {
|
fn agent_exists(name: &str) -> Result<bool> {
|
||||||
let root = hive_c0re::paths::agent_state_dir(name);
|
let root = hive_host_sock::agent_state_dir(name);
|
||||||
match root.try_exists() {
|
match root.try_exists() {
|
||||||
Ok(found) => Ok(found),
|
Ok(found) => Ok(found),
|
||||||
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => bail!(
|
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => bail!(
|
||||||
|
|
@ -1170,10 +1239,10 @@ fn choom(name: &str, resume_session: Option<&str>) -> Result<()> {
|
||||||
if !agent_exists(name)? {
|
if !agent_exists(name)? {
|
||||||
bail!(
|
bail!(
|
||||||
"no such agent: '{name}' (no state dir under {}/)",
|
"no such agent: '{name}' (no state dir under {}/)",
|
||||||
hive_c0re::paths::AGENTS_ROOT
|
hive_host_sock::AGENTS_ROOT
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
let container = hive_c0re::lifecycle::container_name(name);
|
let container = hive_host_sock::container_name(name);
|
||||||
// Enter as the agent's unix user (== agent name) so claude reads the
|
// Enter as the agent's unix user (== agent name) so claude reads the
|
||||||
// right `$HOME/.claude`.
|
// right `$HOME/.claude`.
|
||||||
let target = format!("{name}@{container}");
|
let target = format!("{name}@{container}");
|
||||||
|
|
@ -1291,7 +1360,7 @@ async fn daemon_request(
|
||||||
req: hive_host_sock::HostRequest,
|
req: hive_host_sock::HostRequest,
|
||||||
label: &str,
|
label: &str,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let resp = hive_c0re::client::request(socket, req)
|
let resp = crate::client::request(socket, req)
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("connect to daemon socket {}", socket.display()))?;
|
.with_context(|| format!("connect to daemon socket {}", socket.display()))?;
|
||||||
if !resp.ok {
|
if !resp.ok {
|
||||||
|
|
@ -1337,7 +1406,7 @@ fn resolve_password(password: Option<&str>, password_stdin: bool) -> Result<Opti
|
||||||
/// admin tokens and the matrix creds dir, so hivectl no longer links the
|
/// admin tokens and the matrix creds dir, so hivectl no longer links the
|
||||||
/// matrix machinery — it just forwards the request and renders the reply.
|
/// matrix machinery — it just forwards the request and renders the reply.
|
||||||
async fn matrix_request(socket: &Path, req: hive_host_sock::HostRequest) -> Result<()> {
|
async fn matrix_request(socket: &Path, req: hive_host_sock::HostRequest) -> Result<()> {
|
||||||
let resp = hive_c0re::client::request(socket, req)
|
let resp = crate::client::request(socket, req)
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("connect to daemon socket {}", socket.display()))?;
|
.with_context(|| format!("connect to daemon socket {}", socket.display()))?;
|
||||||
if !resp.ok {
|
if !resp.ok {
|
||||||
|
|
@ -1509,7 +1578,7 @@ fn gateway_list_users(file: &Path) -> Result<()> {
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
async fn agents_restart(socket: &Path, name: &str, no_wait: bool) -> Result<()> {
|
async fn agents_restart(socket: &Path, name: &str, no_wait: bool) -> Result<()> {
|
||||||
let resp = hive_c0re::client::request(
|
let resp = crate::client::request(
|
||||||
socket,
|
socket,
|
||||||
hive_host_sock::HostRequest::Restart {
|
hive_host_sock::HostRequest::Restart {
|
||||||
name: name.to_owned(),
|
name: name.to_owned(),
|
||||||
|
|
@ -1533,7 +1602,7 @@ async fn agents_restart(socket: &Path, name: &str, no_wait: bool) -> Result<()>
|
||||||
/// or the raw JSON rows with `--json`. Reuses the dashboard's
|
/// or the raw JSON rows with `--json`. Reuses the dashboard's
|
||||||
/// `ContainerView` aggregation, so the CLI and the web UI never drift.
|
/// `ContainerView` aggregation, so the CLI and the web UI never drift.
|
||||||
async fn agents_list(socket: &Path, json: bool) -> Result<()> {
|
async fn agents_list(socket: &Path, json: bool) -> Result<()> {
|
||||||
let resp = hive_c0re::client::request(socket, hive_host_sock::HostRequest::AgentStatus)
|
let resp = crate::client::request(socket, hive_host_sock::HostRequest::AgentStatus)
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("connect to daemon socket {}", socket.display()))?;
|
.with_context(|| format!("connect to daemon socket {}", socket.display()))?;
|
||||||
if !resp.ok {
|
if !resp.ok {
|
||||||
|
|
@ -1605,7 +1674,7 @@ async fn agents_list(socket: &Path, json: bool) -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn agents_restart_all(socket: &Path, no_wait: bool) -> Result<()> {
|
async fn agents_restart_all(socket: &Path, no_wait: bool) -> Result<()> {
|
||||||
let resp = hive_c0re::client::request(socket, hive_host_sock::HostRequest::RestartAll)
|
let resp = crate::client::request(socket, hive_host_sock::HostRequest::RestartAll)
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("connect to daemon socket {}", socket.display()))?;
|
.with_context(|| format!("connect to daemon socket {}", socket.display()))?;
|
||||||
let agents = resp.agents.as_deref().unwrap_or(&[]);
|
let agents = resp.agents.as_deref().unwrap_or(&[]);
|
||||||
|
|
@ -1631,7 +1700,7 @@ async fn stop(
|
||||||
graceful: bool,
|
graceful: bool,
|
||||||
no_wait: bool,
|
no_wait: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let resp = hive_c0re::client::request(
|
let resp = crate::client::request(
|
||||||
socket,
|
socket,
|
||||||
hive_host_sock::HostRequest::Stop { scope, graceful },
|
hive_host_sock::HostRequest::Stop { scope, graceful },
|
||||||
)
|
)
|
||||||
|
|
@ -1646,7 +1715,7 @@ async fn stop(
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn start(socket: &Path, scope: hive_host_sock::LifecycleScope, no_wait: bool) -> Result<()> {
|
async fn start(socket: &Path, scope: hive_host_sock::LifecycleScope, no_wait: bool) -> Result<()> {
|
||||||
let resp = hive_c0re::client::request(socket, hive_host_sock::HostRequest::Start { scope })
|
let resp = crate::client::request(socket, hive_host_sock::HostRequest::Start { scope })
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("connect to daemon socket {}", socket.display()))?;
|
.with_context(|| format!("connect to daemon socket {}", socket.display()))?;
|
||||||
let rendered = render_lifecycle(&resp, "start queued");
|
let rendered = render_lifecycle(&resp, "start queued");
|
||||||
|
|
@ -1672,7 +1741,7 @@ async fn restart(
|
||||||
scope: hive_host_sock::LifecycleScope,
|
scope: hive_host_sock::LifecycleScope,
|
||||||
graceful: bool,
|
graceful: bool,
|
||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let resp = hive_c0re::client::request(
|
let resp = crate::client::request(
|
||||||
socket,
|
socket,
|
||||||
hive_host_sock::HostRequest::RestartScoped { scope, graceful },
|
hive_host_sock::HostRequest::RestartScoped { scope, graceful },
|
||||||
)
|
)
|
||||||
|
|
@ -1737,7 +1806,7 @@ async fn subvol_upgrade(socket: &Path, name: &str, yes: bool) -> Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("stopping {name} (releasing its state bind-mount)…");
|
println!("stopping {name} (releasing its state bind-mount)…");
|
||||||
let stop_resp = hive_c0re::client::request(
|
let stop_resp = crate::client::request(
|
||||||
socket,
|
socket,
|
||||||
hive_host_sock::HostRequest::Stop {
|
hive_host_sock::HostRequest::Stop {
|
||||||
scope: single_agent_scope(name),
|
scope: single_agent_scope(name),
|
||||||
|
|
@ -1774,7 +1843,7 @@ async fn subvol_upgrade(socket: &Path, name: &str, yes: bool) -> Result<()> {
|
||||||
// start-side failure (incl. the IPC call itself erroring) can't mask the
|
// start-side failure (incl. the IPC call itself erroring) can't mask the
|
||||||
// migration outcome below.
|
// migration outcome below.
|
||||||
println!("starting {name}…");
|
println!("starting {name}…");
|
||||||
let start_result = hive_c0re::client::request(
|
let start_result = crate::client::request(
|
||||||
socket,
|
socket,
|
||||||
hive_host_sock::HostRequest::Start {
|
hive_host_sock::HostRequest::Start {
|
||||||
scope: single_agent_scope(name),
|
scope: single_agent_scope(name),
|
||||||
|
|
@ -1899,6 +1968,67 @@ async fn subvol_snapshot_send(
|
||||||
/// touched container, then surface any aggregated per-target failure as a
|
/// touched container, then surface any aggregated per-target failure as a
|
||||||
/// non-zero exit. `verb` is the past-tense word printed per item
|
/// non-zero exit. `verb` is the past-tense word printed per item
|
||||||
/// (`stopped` / `started`).
|
/// (`stopped` / `started`).
|
||||||
|
/// Dispatch `hivectl agents <verb>` — container lifecycle over the host
|
||||||
|
/// admin socket.
|
||||||
|
async fn run_agents(socket: &Path, cmd: AgentsCmd) -> Result<()> {
|
||||||
|
match cmd {
|
||||||
|
AgentsCmd::List { json } => agents_list(socket, json).await,
|
||||||
|
AgentsCmd::Restart { name, no_wait } => agents_restart(socket, &name, no_wait).await,
|
||||||
|
AgentsCmd::RestartAll { no_wait } => agents_restart_all(socket, no_wait).await,
|
||||||
|
AgentsCmd::Spawn { name } => {
|
||||||
|
render(crate::client::request(socket, HostRequest::Spawn { name }).await?)
|
||||||
|
}
|
||||||
|
AgentsCmd::RequestSpawn { name } => {
|
||||||
|
render(crate::client::request(socket, HostRequest::RequestSpawn { name }).await?)
|
||||||
|
}
|
||||||
|
AgentsCmd::Kill { name } => {
|
||||||
|
render(crate::client::request(socket, HostRequest::Kill { name }).await?)
|
||||||
|
}
|
||||||
|
AgentsCmd::Destroy { name, purge } => {
|
||||||
|
render(crate::client::request(socket, HostRequest::Destroy { name, purge }).await?)
|
||||||
|
}
|
||||||
|
AgentsCmd::Rebuild { name } => {
|
||||||
|
render(crate::client::request(socket, HostRequest::Rebuild { name }).await?)
|
||||||
|
}
|
||||||
|
AgentsCmd::SetParent {
|
||||||
|
child,
|
||||||
|
parent,
|
||||||
|
root,
|
||||||
|
} => {
|
||||||
|
let new_parent = if root { None } else { parent };
|
||||||
|
render(
|
||||||
|
crate::client::request(socket, HostRequest::SetParent { child, new_parent })
|
||||||
|
.await?,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dispatch `hivectl approvals <verb>` — the operator approval queue.
|
||||||
|
async fn run_approvals(socket: &Path, cmd: ApprovalsCmd) -> Result<()> {
|
||||||
|
match cmd {
|
||||||
|
ApprovalsCmd::Pending => {
|
||||||
|
render(crate::client::request(socket, HostRequest::Pending).await?)
|
||||||
|
}
|
||||||
|
ApprovalsCmd::Approve { id } => {
|
||||||
|
render(crate::client::request(socket, HostRequest::Approve { id }).await?)
|
||||||
|
}
|
||||||
|
ApprovalsCmd::Deny { id } => {
|
||||||
|
render(crate::client::request(socket, HostRequest::Deny { id }).await?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pretty-print a `HostResponse` as JSON and bail on failure. Used by the
|
||||||
|
/// agent-lifecycle + approval verbs that just relay a daemon result verbatim.
|
||||||
|
fn render(resp: hive_host_sock::HostResponse) -> Result<()> {
|
||||||
|
println!("{}", serde_json::to_string_pretty(&resp)?);
|
||||||
|
if !resp.ok {
|
||||||
|
bail!(resp.error.unwrap_or_else(|| "request failed".to_owned()));
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
fn render_lifecycle(resp: &hive_host_sock::HostResponse, verb: &str) -> Result<()> {
|
fn render_lifecycle(resp: &hive_host_sock::HostResponse, verb: &str) -> Result<()> {
|
||||||
let items = resp.agents.as_deref().unwrap_or(&[]);
|
let items = resp.agents.as_deref().unwrap_or(&[]);
|
||||||
if items.is_empty() {
|
if items.is_empty() {
|
||||||
|
|
@ -23,8 +23,8 @@
|
||||||
defaultText = lib.literalExpression "hyperhive.packages.\${system}.default";
|
defaultText = lib.literalExpression "hyperhive.packages.\${system}.default";
|
||||||
description = ''
|
description = ''
|
||||||
hyperhive workspace package. Provides `/bin/hive-c0re`
|
hyperhive workspace package. Provides `/bin/hive-c0re`
|
||||||
(coordinator daemon + admin-socket CLI) and `/bin/hivectl`
|
(coordinator daemon) and `/bin/hivectl` (operator-facing host
|
||||||
(operator-facing host CLI for ad-hoc administration). Wired to
|
CLI for ad-hoc administration + the host admin socket). Wired to
|
||||||
this flake's `packages.<system>.default` by
|
this flake's `packages.<system>.default` by
|
||||||
`nixosModules.default` (via `lib.mkDefault`, so setting it here
|
`nixosModules.default` (via `lib.mkDefault`, so setting it here
|
||||||
wins).
|
wins).
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue