Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0fc77df1a9 | ||
|
|
746c522f4e | ||
|
|
df2394963a | ||
|
|
90e8158f37 |
32 changed files with 108 additions and 398 deletions
|
|
@ -80,19 +80,14 @@ module docs for the exact call chain.
|
||||||
|
|
||||||
The **manager (`ruth`)** differs from other agents in naming/bootstrap,
|
The **manager (`ruth`)** differs from other agents in naming/bootstrap,
|
||||||
its socket flavour and its default permissions; its prompt, tool
|
its socket flavour and its default permissions; its prompt, tool
|
||||||
allow-list and state dirs work as for every other agent. A hive with
|
allow-list and state dirs work as for every other agent.
|
||||||
`services.hyperhive.ruthless = true` runs no manager: `hive-c0re` skips
|
|
||||||
its root-agent create/start sweep
|
|
||||||
(`hive-c0re/src/workers/auto_update.rs`), and this section doesn't
|
|
||||||
apply.
|
|
||||||
|
|
||||||
<!-- vale write-good.Passive = NO -->
|
<!-- vale write-good.Passive = NO -->
|
||||||
|
|
||||||
- **Naming/bootstrap** — the manager's broker recipient name, state-dir
|
- **Naming/bootstrap** — the manager's broker recipient name, state-dir
|
||||||
key, and nixos-container name are all `ruth` (container `h-ruth`).
|
key, and nixos-container name are all `ruth` (container `h-ruth`).
|
||||||
`hive-c0re` spawns it directly at boot if missing, with no operator
|
Like every agent it comes from the swarm dashboard or
|
||||||
approval step — every other agent is created at swarm level
|
`swarmctl agent create`; `hive-c0re` creates nothing at boot.
|
||||||
(`swarmctl agent create`).
|
|
||||||
Roster-wise, `ruth` is just another entry.
|
Roster-wise, `ruth` is just another entry.
|
||||||
- **Wire-protocol** — `hive-core-agent-sock`'s unified `Request` enum
|
- **Wire-protocol** — `hive-core-agent-sock`'s unified `Request` enum
|
||||||
is the same on both socket flavours. Container lifecycle
|
is the same on both socket flavours. Container lifecycle
|
||||||
|
|
|
||||||
|
|
@ -106,9 +106,8 @@ notes) is
|
||||||
reuses prior config + login. With `purge = true` the agent's
|
reuses prior config + login. With `purge = true` the agent's
|
||||||
`/var/lib/hyperhive/{agents,applied}/<name>/` trees are also
|
`/var/lib/hyperhive/{agents,applied}/<name>/` trees are also
|
||||||
wiped (config history + creds + notes gone forever). The
|
wiped (config history + creds + notes gone forever). The
|
||||||
root/bootstrap container is destroyable like any other — hive-c0re
|
root/bootstrap container is destroyable like any other, and stays gone
|
||||||
recreates it on the next startup if it's absent, so destroying it's
|
until you create ruth again from the swarm dashboard.
|
||||||
transient.
|
|
||||||
|
|
||||||
## Meta flake
|
## Meta flake
|
||||||
|
|
||||||
|
|
@ -143,7 +142,7 @@ autoupdate flake-rev bump (one shot before per-agent rebuilds).
|
||||||
|
|
||||||
`meta::sync_agents(hive: &HiveEnv, agents: &[AgentSpec])` — `hive`
|
`meta::sync_agents(hive: &HiveEnv, agents: &[AgentSpec])` — `hive`
|
||||||
carries `hyperhive_flake`, `dashboard_port`, and the rest of the
|
carries `hyperhive_flake`, `dashboard_port`, and the rest of the
|
||||||
per-hive config — is the idempotent reconciler called by `spawn`,
|
per-hive config — is the idempotent reconciler called by `provision_container`,
|
||||||
`destroy`, `rebuild`, and the startup migration. Renders `flake.nix`
|
`destroy`, `rebuild`, and the startup migration. Renders `flake.nix`
|
||||||
from the agent list; if it differs from disk, runs
|
from the agent list; if it differs from disk, runs
|
||||||
`nix flake lock` + commits as `regenerate meta flake` (or
|
`nix flake lock` + commits as `regenerate meta flake` (or
|
||||||
|
|
@ -314,11 +313,12 @@ No state loss in either migration: claude creds, `/state/` notes, the
|
||||||
events DB, and both proposed + applied history all survive. The root
|
events DB, and both proposed + applied history all survive. The root
|
||||||
agent keeps its session; sub-agents stay logged in.
|
agent keeps its session; sub-agents stay logged in.
|
||||||
|
|
||||||
## The root/bootstrap container is hive-c0re-managed
|
## The root/bootstrap container
|
||||||
|
|
||||||
The root agent container runs through the **same lifecycle as
|
The root agent container runs through the **same lifecycle as
|
||||||
sub-agents**. On `hive-c0re serve` startup, if `ruth` is missing,
|
sub-agents**. Nothing creates it at hive boot: you create ruth from the
|
||||||
hive-c0re creates it. The root agent's flake lives at
|
swarm dashboard like any agent, and an existing ruth container starts with
|
||||||
|
the hive like every other. The root agent's flake lives at
|
||||||
`/var/lib/hyperhive/applied/ruth/`; its proposed config at
|
`/var/lib/hyperhive/applied/ruth/`; its proposed config at
|
||||||
`/var/lib/hyperhive/agents/ruth/config/`. The root agent can edit its own
|
`/var/lib/hyperhive/agents/ruth/config/`. The root agent can edit its own
|
||||||
`agent.nix` (visible inside the container at `/agents/ruth/config/`)
|
`agent.nix` (visible inside the container at `/agents/ruth/config/`)
|
||||||
|
|
@ -336,14 +336,13 @@ Differences from sub-agents:
|
||||||
and `/var/lib/hyperhive/applied` → `/applied` (RO) so the root agent
|
and `/var/lib/hyperhive/applied` → `/applied` (RO) so the root agent
|
||||||
can `git fetch` deployed/failed/denied tags from any agent's
|
can `git fetch` deployed/failed/denied tags from any agent's
|
||||||
authoritative applied repo (see "Root-agent view of applied" below).
|
authoritative applied repo (see "Root-agent view of applied" below).
|
||||||
- `socket_server::start_manager` binds the root agent's socket,
|
- Its socket comes from the generic per-agent registration, in the same
|
||||||
pure transport with no dedicated helpers — it uses the same
|
|
||||||
per-agent runtime dir as any other agent (`/run/hyperhive/agents/ruth/`),
|
per-agent runtime dir as any other agent (`/run/hyperhive/agents/ruth/`),
|
||||||
not a special manager-only path.
|
not a special manager-only path.
|
||||||
|
|
||||||
**Migration note** (for older hosts): drop any `containers.root =
|
**Migration note** (for older hosts): drop any `containers.root =
|
||||||
{ ... }` block from your host NixOS config. hyperhive creates and
|
{ ... }` block from your host NixOS config, and any `services.hyperhive.ruthless`
|
||||||
updates the root agent itself.
|
definition, then create ruth from the swarm dashboard.
|
||||||
|
|
||||||
## Root-agent policy
|
## Root-agent policy
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,9 @@ Beyond that:
|
||||||
restart and rebuild** — only an explicit purge (or a hive
|
restart and rebuild** — only an explicit purge (or a hive
|
||||||
`--purge`-style host operation, or the agent's own choices) touches
|
`--purge`-style host operation, or the agent's own choices) touches
|
||||||
them.
|
them.
|
||||||
- The **root/bootstrap agent is special**: it isn't really destroyable
|
- The **root/bootstrap agent has no guard**: destroying ruth is as final as
|
||||||
in practice — hive-c0re recreates it automatically on its next
|
for any agent, and she stays gone until you create her again from the
|
||||||
startup if it's ever gone.
|
swarm dashboard.
|
||||||
|
|
||||||
Everything below this point is implementation detail: exact table
|
Everything below this point is implementation detail: exact table
|
||||||
schemas, file layouts, and internal migration mechanics.
|
schemas, file layouts, and internal migration mechanics.
|
||||||
|
|
@ -432,11 +432,9 @@ an agent's state. The mechanics, for completeness:
|
||||||
- `PURG3` wipes `/var/lib/hyperhive/{agents,applied}/<name>/` — the
|
- `PURG3` wipes `/var/lib/hyperhive/{agents,applied}/<name>/` — the
|
||||||
union of everything `DESTR0Y` left behind.
|
union of everything `DESTR0Y` left behind.
|
||||||
|
|
||||||
`actions::destroy` implements the root/bootstrap agent's specialness as a soft policy
|
`actions::destroy` has no guard for the root/bootstrap agent: it destroys ruth
|
||||||
guard that refuses to destroy it, backstopped by
|
like any other agent, and she stays gone until you create her again from the
|
||||||
`auto_update::ensure_root_agent`, which recreates it on the next
|
swarm dashboard.
|
||||||
hive-c0re startup if it's ever absent — so even without the guard,
|
|
||||||
destroying it would only be transient.
|
|
||||||
|
|
||||||
### btrfs subvolumes for `/var/lib/hyperhive/agents/<name>`
|
### btrfs subvolumes for `/var/lib/hyperhive/agents/<name>`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -89,24 +89,20 @@ swarmctl forge make-admin mara
|
||||||
If the forge already has a local account with your username, the first
|
If the forge already has a local account with your username, the first
|
||||||
SSO login asks for its forge password once, to link the two.
|
SSO login asks for its forge password once, to link the two.
|
||||||
|
|
||||||
## 4 · Ruth's store identity
|
## 4 · Ruth
|
||||||
|
|
||||||
hive-c0re creates the manager agent, ruth, on its own at startup — so
|
hive-c0re creates no agent at startup. You create ruth, the manager agent,
|
||||||
unlike agents made through the swarm, she has no store identity yet:
|
like any other: open the swarm UI start page and click the paperclip, which
|
||||||
|
opens the create form with `ruth` filled in. The form declares her, creates
|
||||||
|
her config repo and deploys her to the hive you pick.
|
||||||
|
|
||||||
```bash
|
The swarm declares a new agent `paused`, so without a further step ruth runs
|
||||||
swarmctl agent create ruth --hive <hive> # on the swarm-controller's host
|
but drives no turns. Open `/agents` in the swarm UI and set her `up` from her
|
||||||
hivectl agent ruth rebuild # on ruth's hive: hands her the identity
|
card.
|
||||||
```
|
|
||||||
|
|
||||||
`agent create` declares an agent the swarm has no declaration for as
|
On a hive that already has a ruth container from an earlier release, create
|
||||||
`paused`, so without a further step ruth keeps running but drives no turns.
|
her once the same way, so that her config repo and store identity exist. The
|
||||||
`swarmctl` has no verb for wanted state: open `/agents` in the swarm UI and set
|
container keeps starting with the hive meanwhile.
|
||||||
her `up` from her card.
|
|
||||||
|
|
||||||
Within about ten minutes the controller creates her forge user and matrix
|
|
||||||
account and mints their tokens into the store, where she fetches them.
|
|
||||||
`swarmctl agent mint-forge-token ruth` skips the wait for the forge token.
|
|
||||||
|
|
||||||
## 5 · Matrix
|
## 5 · Matrix
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -174,9 +174,7 @@ Each row carries enough context that the caller renders
|
||||||
it directly as a bulleted list, no follow-up fetch needed.
|
it directly as a bulleted list, no follow-up fetch needed.
|
||||||
|
|
||||||
`GetLooseEnds` takes no target — it always returns the caller's own
|
`GetLooseEnds` takes no target — it always returns the caller's own
|
||||||
rows, on both the agent and manager sockets alike (the manager, `ruth`,
|
rows. `Reminder` rows always carry `owner == self`.
|
||||||
is a normal agent here, just deployed automatically with different
|
|
||||||
default permissions). `Reminder` rows always carry `owner == self`.
|
|
||||||
|
|
||||||
Per-variant fields:
|
Per-variant fields:
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -178,7 +178,7 @@ Power toggles never commit to the meta repo. Every operator power surface —
|
||||||
dashboard buttons, the MCP tools, and `hivectl stop/start/restart/kill` —
|
dashboard buttons, the MCP tools, and `hivectl stop/start/restart/kill` —
|
||||||
rides the queue through that power layer, so intent, lease serialization,
|
rides the queue through that power layer, so intent, lease serialization,
|
||||||
and crash-watch suppression can't drift per surface; the only direct starts
|
and crash-watch suppression can't drift per surface; the only direct starts
|
||||||
left are the root-agent bootstrap and infra containers (no lease, no
|
left are infra containers (no lease, no
|
||||||
harness). Cancelling a still-queued power DAG reverts `wanted` to the
|
harness). Cancelling a still-queued power DAG reverts `wanted` to the
|
||||||
observed state — a cancel means "don't do it," not "do it later." hive-c0re
|
observed state — a cancel means "don't do it," not "do it later." hive-c0re
|
||||||
seeds agents without a row from observed state on first touch (running ⇒
|
seeds agents without a row from observed state on first touch (running ⇒
|
||||||
|
|
|
||||||
|
|
@ -98,7 +98,7 @@ variants regardless of who sends them):
|
||||||
|
|
||||||
A small `Surface` trait, with one zero-sized impl, factors
|
A small `Surface` trait, with one zero-sized impl, factors
|
||||||
`hive-agent`'s wire types (`hive_core_agent_sock::{Request, Response}` —
|
`hive-agent`'s wire types (`hive_core_agent_sock::{Request, Response}` —
|
||||||
one unified enum shared by the agent and manager sockets) and its turn
|
one unified enum shared by every agent socket) and its turn
|
||||||
loop, so the loop itself has no per-role branches. See
|
loop, so the loop itself has no per-role branches. See
|
||||||
`hive-agent/src/main.rs`'s module
|
`hive-agent/src/main.rs`'s module
|
||||||
doc for the trait shape.
|
doc for the trait shape.
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use rmcp::schemars;
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||||
pub struct SendArgs {
|
pub struct SendArgs {
|
||||||
/// Logical agent name to deliver the message to (e.g. `"manager"`,
|
/// Logical agent name to deliver the message to (e.g. `"bob"`,
|
||||||
/// `"alice"`). `"operator"` is refused: reach the operator over matrix
|
/// `"alice"`). `"operator"` is refused: reach the operator over matrix
|
||||||
/// if you have it.
|
/// if you have it.
|
||||||
pub to: String,
|
pub to: String,
|
||||||
|
|
@ -106,7 +106,7 @@ pub struct SetStatusArgs {
|
||||||
|
|
||||||
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
#[derive(Debug, serde::Deserialize, schemars::JsonSchema)]
|
||||||
pub struct GetAgentMetaArgs {
|
pub struct GetAgentMetaArgs {
|
||||||
/// Logical name of the agent to query (e.g. `"iris"`, `"manager"`).
|
/// Logical name of the agent to query (e.g. `"iris"`, `"bob"`).
|
||||||
/// Omit to query your own identity + status — replaces the
|
/// Omit to query your own identity + status — replaces the
|
||||||
/// previous `whoami` self-introspection tool.
|
/// previous `whoami` self-introspection tool.
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|
|
||||||
|
|
@ -148,7 +148,7 @@ impl AgentServer {
|
||||||
let log = format!("{args:?}");
|
let log = format!("{args:?}");
|
||||||
let to = args.to.clone();
|
let to = args.to.clone();
|
||||||
// Check per-agent allow-list (services.hyperhive.agent.allowedRecipients). When no
|
// Check per-agent allow-list (services.hyperhive.agent.allowedRecipients). When no
|
||||||
// policy file is present (e.g. manager containers) the check is a no-op.
|
// policy file is present the check is a no-op.
|
||||||
if let Err(refusal) = crate::send_allow::check_send_allowed(&to) {
|
if let Err(refusal) = crate::send_allow::check_send_allowed(&to) {
|
||||||
return run_tool_envelope("send", log, async move { refusal }).await;
|
return run_tool_envelope("send", log, async move { refusal }).await;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,7 @@
|
||||||
/// Where the NixOS module writes the per-agent send allow-list (see
|
/// Where the NixOS module writes the per-agent send allow-list (see
|
||||||
/// `nix/agent-modules/mcp.nix`). Empty list = unrestricted (the
|
/// `nix/agent-modules/mcp.nix`). Empty list = unrestricted (the
|
||||||
/// default). Non-empty list constrains `mcp__hyperhive__send`'s `to`
|
/// default). Non-empty list constrains `mcp__hyperhive__send`'s `to`
|
||||||
/// field; the manager is always implicitly permitted regardless of
|
/// field.
|
||||||
/// the list contents.
|
|
||||||
const SEND_ALLOW_PATH: &str = "/etc/hyperhive/send-allow.json";
|
const SEND_ALLOW_PATH: &str = "/etc/hyperhive/send-allow.json";
|
||||||
|
|
||||||
/// Enforce the per-agent send allow-list. Returns `Ok` when the
|
/// Enforce the per-agent send allow-list. Returns `Ok` when the
|
||||||
|
|
|
||||||
|
|
@ -394,7 +394,7 @@ struct ExtraLink {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Best-effort: pull the last 30 messages addressed to us via the
|
/// Best-effort: pull the last 30 messages addressed to us via the
|
||||||
/// per-agent / manager socket. Empty list on any transport / decode
|
/// per-agent socket. Empty list on any transport / decode
|
||||||
/// failure — the inbox section is decorative, not authoritative.
|
/// failure — the inbox section is decorative, not authoritative.
|
||||||
async fn recent_inbox(socket: &std::path::Path) -> Vec<hive_sh4re::inbox::InboxRow> {
|
async fn recent_inbox(socket: &std::path::Path) -> Vec<hive_sh4re::inbox::InboxRow> {
|
||||||
const LIMIT: u64 = 30;
|
const LIMIT: u64 = 30;
|
||||||
|
|
|
||||||
|
|
@ -37,8 +37,8 @@ to track it:
|
||||||
- **`agent_config/`** — per-agent registries (permissions,
|
- **`agent_config/`** — per-agent registries (permissions,
|
||||||
resource limits, topology).
|
resource limits, topology).
|
||||||
- **`stats/`** — dashboard metrics aggregation + OTEL export.
|
- **`stats/`** — dashboard metrics aggregation + OTEL export.
|
||||||
- **`socket_server/`** — the unix-socket request server shared by
|
- **`socket_server/`** — the unix-socket request server for the
|
||||||
per-agent + manager sockets.
|
per-agent sockets.
|
||||||
- **`forge/`** — optional Forgejo wiring (`docs/integrations/forge.md`).
|
- **`forge/`** — optional Forgejo wiring (`docs/integrations/forge.md`).
|
||||||
- **`coordinator.rs`** — top-level wiring for `serve`.
|
- **`coordinator.rs`** — top-level wiring for `serve`.
|
||||||
- **`meta.rs`**, **`migrate.rs`** — the meta flake + schema/state
|
- **`meta.rs`**, **`migrate.rs`** — the meta flake + schema/state
|
||||||
|
|
|
||||||
|
|
@ -126,10 +126,9 @@ async fn ff_applied_main(
|
||||||
/// is cleared because its contents (the mcp socket) don't survive restarts
|
/// is cleared because its contents (the mcp socket) don't survive restarts
|
||||||
/// anyway. With `purge=true` the persistent trees are also wiped — config
|
/// anyway. With `purge=true` the persistent trees are also wiped — config
|
||||||
/// history, claude creds, notes — there is no undo.
|
/// history, claude creds, notes — there is no undo.
|
||||||
/// The bootstrap/root container is destroyable like any other: it's
|
/// The bootstrap/root container is destroyable like any other. Nothing
|
||||||
/// imperative infra that `auto_update::ensure_root_agent` recreates on the
|
/// recreates it afterwards: ruth stays gone until she is created again from
|
||||||
/// next hive-c0re startup if absent, so destroying it is transient rather
|
/// the swarm dashboard.
|
||||||
/// than something to refuse at the API.
|
|
||||||
///
|
///
|
||||||
/// Submits the teardown DAG and returns — it does not wait for the container to
|
/// Submits the teardown DAG and returns — it does not wait for the container to
|
||||||
/// go away. Same contract as every other lifecycle op (`rebuild`, `kill`,
|
/// go away. Same contract as every other lifecycle op (`rebuild`, `kill`,
|
||||||
|
|
|
||||||
|
|
@ -165,14 +165,11 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn apply_reconcile_adds_missing_live_agent() {
|
fn apply_reconcile_adds_missing_live_agent() {
|
||||||
let live = vec![
|
let live = vec!["alice".to_owned(), "newbie".to_owned()];
|
||||||
crate::lifecycle::MANAGER_NAME.to_owned(),
|
|
||||||
"newbie".to_owned(),
|
|
||||||
];
|
|
||||||
let (next, changed) = apply_reconcile(&BTreeSet::new(), &live, &[]);
|
let (next, changed) = apply_reconcile(&BTreeSet::new(), &live, &[]);
|
||||||
assert!(changed);
|
assert!(changed);
|
||||||
assert!(next.contains("newbie"));
|
assert!(next.contains("newbie"));
|
||||||
assert!(next.contains(crate::lifecycle::MANAGER_NAME));
|
assert!(next.contains("alice"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -496,7 +496,7 @@ impl Coordinator {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Snapshot the hive-wide configuration fields as a [`HiveEnv`].
|
/// Snapshot the hive-wide configuration fields as a [`HiveEnv`].
|
||||||
/// Pass the result to `lifecycle::spawn` / `rebuild` / `meta::sync_agents`
|
/// Pass the result to `lifecycle::provision_container` / `rebuild` / `meta::sync_agents`
|
||||||
/// instead of threading the individual fields separately.
|
/// instead of threading the individual fields separately.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn hive_env(&self) -> HiveEnv {
|
pub fn hive_env(&self) -> HiveEnv {
|
||||||
|
|
|
||||||
|
|
@ -296,7 +296,6 @@ async fn reload_gateway_nginx() {
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::lifecycle::MANAGER_NAME;
|
|
||||||
|
|
||||||
// ── legacy mode (frontend_dir = None) ──────────────────────────────
|
// ── legacy mode (frontend_dir = None) ──────────────────────────────
|
||||||
|
|
||||||
|
|
@ -309,13 +308,10 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn render_includes_manager() {
|
fn render_includes_every_agent() {
|
||||||
let names: Vec<String> = [MANAGER_NAME, "iris"]
|
let names: Vec<String> = ["alice", "iris"].iter().map(|s| (*s).to_owned()).collect();
|
||||||
.iter()
|
|
||||||
.map(|s| (*s).to_owned())
|
|
||||||
.collect();
|
|
||||||
let body = render(&names, None);
|
let body = render(&names, None);
|
||||||
assert!(body.contains(&format!("/agent/{MANAGER_NAME}/")));
|
assert!(body.contains("/agent/alice/"));
|
||||||
assert!(body.contains("/agent/iris/"));
|
assert!(body.contains("/agent/iris/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -461,13 +457,10 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn split_mode_includes_manager() {
|
fn split_mode_includes_every_agent() {
|
||||||
let names: Vec<String> = [MANAGER_NAME, "iris"]
|
let names: Vec<String> = ["alice", "iris"].iter().map(|s| (*s).to_owned()).collect();
|
||||||
.iter()
|
|
||||||
.map(|s| (*s).to_owned())
|
|
||||||
.collect();
|
|
||||||
let body = render(&names, Some(FAKE_FRONTEND));
|
let body = render(&names, Some(FAKE_FRONTEND));
|
||||||
assert!(body.contains(&format!("/agent/{MANAGER_NAME}/")));
|
assert!(body.contains("/agent/alice/"));
|
||||||
assert!(body.contains("/agent/iris/"));
|
assert!(body.contains("/agent/iris/"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,12 +34,6 @@ 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/`.
|
||||||
pub const MANAGER_NAME: &str = "ruth";
|
pub const MANAGER_NAME: &str = "ruth";
|
||||||
/// Container name of the manager. Uses the same `h-` prefix as sub-agents
|
|
||||||
/// so `nixos-container list` output is uniform and the list filter is
|
|
||||||
/// a single `starts_with(AGENT_PREFIX)` check. Logical name → container
|
|
||||||
/// name: `ruth` → `h-ruth`.
|
|
||||||
pub const MANAGER_CONTAINER: &str = "h-ruth";
|
|
||||||
|
|
||||||
/// Mount point of the per-agent runtime directory inside the container.
|
/// Mount point of the per-agent runtime directory inside the container.
|
||||||
pub const CONTAINER_RUNTIME_MOUNT: &str = "/run/hive";
|
pub const CONTAINER_RUNTIME_MOUNT: &str = "/run/hive";
|
||||||
|
|
||||||
|
|
@ -225,15 +219,6 @@ async fn port_collision(self_name: &str) -> Result<Option<String>> {
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn spawn(name: &str, hive: &HiveEnv, paths: &AgentPaths) -> Result<()> {
|
|
||||||
create_container(name, hive, paths).await?;
|
|
||||||
// Runtime dir must exist before nixos-container start (nspawn bind-mount
|
|
||||||
// source). Create it here so callers don't need a separate preamble step.
|
|
||||||
ensure_agent_runtime_dir(name)?;
|
|
||||||
write_dropins(name, hive, paths).await?;
|
|
||||||
priv_run("start", name).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// First-spawn pre-create provisioning — the job queue's `Provision`
|
/// First-spawn pre-create provisioning — the job queue's `Provision`
|
||||||
/// node. Fail fast on a port collision, set up the proposed/applied
|
/// node. Fail fast on a port collision, set up the proposed/applied
|
||||||
/// repos + state subvolume + claude/notes dirs, then register the new
|
/// repos + state subvolume + claude/notes dirs, then register the new
|
||||||
|
|
@ -266,16 +251,6 @@ pub async fn create_only(name: &str) -> Result<()> {
|
||||||
priv_run("create", name).await
|
priv_run("create", name).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// First-spawn provisioning + `nixos-container create`, without the
|
|
||||||
/// drop-in write or the start. `spawn` composes this with
|
|
||||||
/// `write_dropins` + start for direct callers (root-agent bootstrap);
|
|
||||||
/// the job queue instead runs `provision_container` (`Provision` node)
|
|
||||||
/// and `create_only` (`Create` node) as separate DAG steps.
|
|
||||||
pub async fn create_container(name: &str, hive: &HiveEnv, paths: &AgentPaths) -> Result<()> {
|
|
||||||
provision_container(name, hive, paths).await?;
|
|
||||||
create_only(name).await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Rebuild-path preamble, run by the job queue's `Prebuild` node: fail fast on
|
/// Rebuild-path preamble, run by the job queue's `Prebuild` node: fail fast on
|
||||||
/// a port collision, then make sure the applied repo + state dirs exist.
|
/// a port collision, then make sure the applied repo + state dirs exist.
|
||||||
/// Container untouched.
|
/// Container untouched.
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
//! Loose-ends aggregator. Assembles a `Vec<LooseEnd>` for a single agent
|
//! Loose-ends aggregator. Assembles a `Vec<LooseEnd>` for a single agent
|
||||||
//! (`for_agent`) — always the caller's own. `Request::GetLooseEnds` from
|
//! (`for_agent`) — always the caller's own. `Request::GetLooseEnds` from
|
||||||
//! either the agent or manager socket lands here. Reminders and todos are
|
//! an agent socket lands here. Reminders and todos are
|
||||||
//! agent-local (in-container `hive-agent` stores) and never sourced from
|
//! agent-local (in-container `hive-agent` stores) and never sourced from
|
||||||
//! here.
|
//! here.
|
||||||
//!
|
//!
|
||||||
|
|
|
||||||
|
|
@ -267,26 +267,17 @@ async fn cmd_serve(
|
||||||
// Copy value first for the dashboard + knowledge-webhook tasks.
|
// Copy value first for the dashboard + knowledge-webhook tasks.
|
||||||
let dashboard_port = env.dashboard_port;
|
let dashboard_port = env.dashboard_port;
|
||||||
let coord = Arc::new(Coordinator::open(&db, env, model_prices, build_slots)?);
|
let coord = Arc::new(Coordinator::open(&db, env, model_prices, build_slots)?);
|
||||||
socket_server::start_manager(coord.clone())?;
|
|
||||||
// Idempotent pre-flight: rewrite pre-meta-layout applied
|
// Idempotent pre-flight: rewrite pre-meta-layout applied
|
||||||
// repos, ensure proposed repos carry the `applied`
|
// repos, ensure proposed repos carry the `applied`
|
||||||
// remote, bootstrap the meta repo, repoint containers at
|
// remote, bootstrap the meta repo, repoint containers at
|
||||||
// `meta#<name>` (one-shot, guarded by a marker file).
|
// `meta#<name>` (one-shot, guarded by a marker file).
|
||||||
// Runs before manager auto-spawn so the new manager is
|
// Runs before the boot reconcile, so agents are built against meta.
|
||||||
// built against meta from the first attempt.
|
|
||||||
if let Err(e) = migrate::run(&coord).await {
|
if let Err(e) = migrate::run(&coord).await {
|
||||||
tracing::warn!(error = ?e, "startup migration failed");
|
tracing::warn!(error = ?e, "startup migration failed");
|
||||||
}
|
}
|
||||||
// Before any agent starts, so the bind mounts it gets from the stored
|
// Before any agent starts, so the bind mounts it gets from the stored
|
||||||
// permissions are the meta repo's grants until the swarm answers.
|
// permissions are the meta repo's grants until the swarm answers.
|
||||||
crate::workers::permissions_import::seed_local();
|
crate::workers::permissions_import::seed_local();
|
||||||
// Auto-create the root agent container if it isn't there yet. Block
|
|
||||||
// on this — without root the system has no manager harness.
|
|
||||||
// Failures are logged but allowed: a broken auto-spawn shouldn't
|
|
||||||
// make the dashboard unreachable for debugging.
|
|
||||||
if let Err(e) = auto_update::ensure_root_agent(&coord).await {
|
|
||||||
tracing::warn!(error = ?e, "auto-spawn root agent failed");
|
|
||||||
}
|
|
||||||
// Auto-update in the background — don't block service start.
|
// Auto-update in the background — don't block service start.
|
||||||
// Sub-agent rebuilds can take tens of seconds; we want the admin
|
// Sub-agent rebuilds can take tens of seconds; we want the admin
|
||||||
// socket up immediately.
|
// socket up immediately.
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ use anyhow::{Context, Result};
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
|
|
||||||
use crate::coordinator::Coordinator;
|
use crate::coordinator::Coordinator;
|
||||||
use crate::lifecycle::{self, AGENT_PREFIX, MANAGER_CONTAINER, MANAGER_NAME};
|
use crate::lifecycle::{self, AGENT_PREFIX};
|
||||||
use crate::meta;
|
use crate::meta;
|
||||||
|
|
||||||
const KILL_SWITCH: &str = "HIVE_SKIP_META_MIGRATION";
|
const KILL_SWITCH: &str = "HIVE_SKIP_META_MIGRATION";
|
||||||
|
|
@ -157,11 +157,7 @@ async fn enumerate_agents() -> Result<Vec<hive_types::Ident>> {
|
||||||
Ok(containers
|
Ok(containers
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|c| {
|
.filter_map(|c| {
|
||||||
let name = if c == MANAGER_CONTAINER {
|
let name = c.strip_prefix(AGENT_PREFIX)?;
|
||||||
MANAGER_NAME
|
|
||||||
} else {
|
|
||||||
c.strip_prefix(AGENT_PREFIX)?
|
|
||||||
};
|
|
||||||
hive_types::Ident::parse(name).ok()
|
hive_types::Ident::parse(name).ok()
|
||||||
})
|
})
|
||||||
.collect())
|
.collect())
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue