replaces three hand-rolled six-arm matches (actions.rs ×2, state_snapshot.rs); approvals::kind_to_str delegates. a new kind can no longer silently miss one of them
1457 lines
61 KiB
Rust
1457 lines
61 KiB
Rust
//! Wire types shared between `hive-c0re` and the in-container harness.
|
|
|
|
use chrono::{DateTime, Utc};
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
pub mod assets;
|
|
pub mod paths;
|
|
pub mod priv_proto;
|
|
pub mod wire_time;
|
|
|
|
/// Server-side hard cap on `Recv.max` (see `AgentRequest::Recv`). Bounds
|
|
/// the size of a single round-trip so a confused caller can't drain the
|
|
/// entire inbox in one go and blow past wire-buffer sizes; everything
|
|
/// above the cap silently clamps. 5 keeps individual turns small — a big
|
|
/// backlog is drained over several recv calls instead of one giant pop.
|
|
/// Lives here so both the enforcing side (hive-c0re's `socket_server`) and
|
|
/// the hinting side (hive-ag3nt's wake prompt + tool docs) reference one
|
|
/// constant instead of a scattered magic value.
|
|
pub const RECV_BATCH_MAX: u32 = 5;
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Host admin socket — /run/hyperhive/host.sock
|
|
// -----------------------------------------------------------------------------
|
|
|
|
/// Requests on the host admin socket.
|
|
///
|
|
/// Wire format: one JSON object per line.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(tag = "cmd", rename_all = "snake_case")]
|
|
pub enum HostRequest {
|
|
/// Create and start a sub-agent container directly, bypassing the
|
|
/// approval queue. Privileged-context only. See
|
|
/// `docs/approvals.md::Approval kinds (wire shapes)`.
|
|
Spawn { name: String },
|
|
/// Submit a spawn request for the operator to approve. See
|
|
/// `docs/approvals.md::Approval kinds (wire shapes)` (`Spawn`).
|
|
RequestSpawn { name: String },
|
|
/// Stop a managed container (graceful).
|
|
Kill { name: String },
|
|
/// Tear down a sub-agent container, optionally purging state.
|
|
/// See `docs/approvals.md::Destroy semantics`.
|
|
Destroy {
|
|
name: String,
|
|
#[serde(default)]
|
|
purge: bool,
|
|
},
|
|
/// Stop and start a managed container without rebuilding config.
|
|
/// For "kick the container" operations that don't touch the flake or
|
|
/// nspawn flags. Mirrors `lifecycle::restart` (kill + start).
|
|
Restart { name: String },
|
|
/// Stop and restart all managed containers in sequence. Convenience
|
|
/// wrapper for `hivectl agents restart-all`; iterates the live
|
|
/// container list and restarts each one.
|
|
RestartAll,
|
|
/// Apply pending config to a managed container.
|
|
Rebuild { name: String },
|
|
/// List managed containers.
|
|
List,
|
|
/// List managed agents with their full status + technical state
|
|
/// (running / needs-login / needs-update / deployed sha / parent /
|
|
/// pending reminders) — the `hivectl agents list` roster view.
|
|
/// Reuses the dashboard's per-agent `ContainerView` aggregation.
|
|
AgentStatus,
|
|
/// Report this hive's canonical DNS domain
|
|
/// (`services.hyperhive.domain`) plus the browser-facing home /
|
|
/// forge / matrix URLs, daemon-sourced so custom forge/matrix
|
|
/// domains resolve correctly. Each URL is `None` when its subsystem
|
|
/// is unreachable from a browser (e.g. forge not behind the gateway,
|
|
/// matrix GUI disabled). Backs `hivectl open` + the federation
|
|
/// peer-config block (which reads the bare `domain`).
|
|
Urls,
|
|
/// List pending approval requests.
|
|
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. `new_parent = None`
|
|
/// promotes the agent to root, `Some(name)` sets a new parent.
|
|
/// Validation rules + bind-mount caveat documented in
|
|
/// `docs/agent-hierarchy.md::Current state`.
|
|
SetParent {
|
|
child: String,
|
|
new_parent: Option<String>,
|
|
},
|
|
/// Stop managed containers hive-wide in one operator action
|
|
/// (`hivectl stop`): agents plus the selected infra containers. `scope`
|
|
/// selects which classes; an all-false scope means **everything** (the
|
|
/// bare `hivectl stop`). `graceful` runs the per-agent quiesce (graceful
|
|
/// agent stop, issue tracker `graceful agent stop`) instead of a hard
|
|
/// stop. Agents stop via the lifecycle path; infra containers via the
|
|
/// host `container@<name>` units.
|
|
Stop {
|
|
#[serde(default)]
|
|
scope: LifecycleScope,
|
|
#[serde(default)]
|
|
graceful: bool,
|
|
},
|
|
/// Start managed containers hive-wide — the inverse of `Stop`
|
|
/// (`hivectl start`). Same `scope` semantics (all-false = everything);
|
|
/// no graceful flag (start is unconditional).
|
|
Start {
|
|
#[serde(default)]
|
|
scope: LifecycleScope,
|
|
},
|
|
}
|
|
|
|
/// Selects which container classes a hive-wide [`HostRequest::Stop`] /
|
|
/// [`HostRequest::Start`] touches. An all-false scope means **everything**
|
|
/// (the bare `hivectl stop` / `start`); set individual fields to restrict
|
|
/// (e.g. only `agents` → just the sub-agent containers). `agents` covers
|
|
/// every managed sub-agent container; the rest are the named infra
|
|
/// containers (`hive-ci`, `hive-forge`, `hive-gateway`, `hive-matrix`).
|
|
//
|
|
// A flat bag of independent flag toggles — one bool per selectable
|
|
// container class — is exactly the right shape here: each maps 1:1 to a
|
|
// `hivectl` `--ci` / `--forge` / `--gateway` / `--matrix` flag, and they're
|
|
// orthogonal (any subset is valid), so a state machine or two-variant enums
|
|
// would only obscure the mapping. Hence the `struct_excessive_bools` allow.
|
|
#[allow(clippy::struct_excessive_bools)]
|
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
pub struct LifecycleScope {
|
|
/// All sub-agent containers (`--agents`).
|
|
#[serde(default)]
|
|
pub agents: bool,
|
|
/// Specific sub-agents by logical name (`--agent <name>`, repeatable).
|
|
/// Additive with the rest of the scope; redundant when `agents` is set
|
|
/// (which already covers every sub-agent).
|
|
#[serde(default)]
|
|
pub agent_names: Vec<String>,
|
|
#[serde(default)]
|
|
pub ci: bool,
|
|
#[serde(default)]
|
|
pub forge: bool,
|
|
#[serde(default)]
|
|
pub gateway: bool,
|
|
#[serde(default)]
|
|
pub matrix: bool,
|
|
}
|
|
|
|
impl LifecycleScope {
|
|
/// True when nothing is explicitly selected — interpreted as "all
|
|
/// classes" (the bare `hivectl stop` / `start` with no scope flags).
|
|
pub fn is_everything(&self) -> bool {
|
|
!(self.agents || self.ci || self.forge || self.gateway || self.matrix)
|
|
&& self.agent_names.is_empty()
|
|
}
|
|
}
|
|
|
|
/// This hive's canonical domain plus the browser-facing URLs for its
|
|
/// web surfaces — the `Urls` request result. Every field is `None` when
|
|
/// the corresponding surface can't be reached from a browser (domain
|
|
/// unset, forge not behind the gateway, matrix GUI disabled), so the CLI
|
|
/// can give a precise hint instead of opening a dead link.
|
|
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
|
pub struct HiveUrls {
|
|
/// Canonical hive domain (`services.hyperhive.domain`).
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub domain: Option<String>,
|
|
/// Operator dashboard root (`https://<domain>/`).
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub home: Option<String>,
|
|
/// Forge browser URL (`HIVE_FORGE_PUBLIC_URL`) — only the
|
|
/// behind-gateway public URL; `None` on direct-port forge deploys.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub forge: Option<String>,
|
|
/// Matrix GUI (fluffychat) browser URL — `None` when the matrix GUI
|
|
/// is disabled.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub matrix: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct HostResponse {
|
|
pub ok: bool,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub error: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub agents: Option<Vec<String>>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub approvals: Option<Vec<Approval>>,
|
|
/// `Urls` result — this hive's domain plus the browser-facing
|
|
/// home / forge / matrix URLs. `None` for every other request kind.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub urls: Option<HiveUrls>,
|
|
/// `AgentStatus` result — one row per managed agent with its
|
|
/// running/health flags + technical state. `None` for every other
|
|
/// request kind.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub agent_statuses: Option<Vec<AgentStatusRow>>,
|
|
}
|
|
|
|
/// One row in the approval queue. `commit_ref` is overloaded per
|
|
/// `kind` — see `docs/approvals.md::Approval kinds (wire shapes)`
|
|
/// for the encoding table and lifecycle.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Approval {
|
|
pub id: i64,
|
|
pub agent: String,
|
|
#[serde(default)]
|
|
pub kind: ApprovalKind,
|
|
/// Kind-specific payload (git sha / inputs array / schedule
|
|
/// payload / empty). See the Approval struct doc.
|
|
pub commit_ref: String,
|
|
/// The canonical hive-c0re-vouched sha. For `ApplyCommit`: the sha
|
|
/// after the proposal fetch, tagged `proposal/<id>` (stable for the
|
|
/// approval's lifetime — manager amends in proposed don't change
|
|
/// what gets built). For `MergeConfigPr`: the reviewed PR head
|
|
/// pinned at submit; if the PR head drifts off it before merge,
|
|
/// hive-c0re refreshes this + re-renders the card for re-review.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub fetched_sha: Option<String>,
|
|
pub requested_at: DateTime<Utc>,
|
|
pub status: ApprovalStatus,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub resolved_at: Option<DateTime<Utc>>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub note: Option<String>,
|
|
/// Free-text description the manager attached at submission time;
|
|
/// shown on the dashboard approval card.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub description: Option<String>,
|
|
}
|
|
|
|
/// What action the approval, when granted, will trigger.
|
|
/// Variant-specific payload encoding + flow lives in
|
|
/// `docs/approvals.md::Approval kinds (wire shapes)`.
|
|
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum ApprovalKind {
|
|
/// Apply a manager-proposed config commit.
|
|
#[default]
|
|
ApplyCommit,
|
|
/// Create + start a new sub-agent container with the given name
|
|
/// (under the default `agent.nix` template).
|
|
Spawn,
|
|
/// Seed an agent's proposed config repo for manager customisation
|
|
/// (step 1 of the two-step spawn flow).
|
|
InitConfig,
|
|
/// Run `nix flake update [inputs...]` on the meta flake and commit
|
|
/// the resulting lock changes.
|
|
UpdateMetaInputs,
|
|
/// Add a scheduled prompt to the broker queue.
|
|
SchedulePrompt,
|
|
/// Merge an operator-reviewed config PR: hive-c0re verifies the
|
|
/// reviewed PR head, fast-forwards the forge config repo's `main`
|
|
/// to it, marks the PR merged, then runs the same deploy tail as
|
|
/// `ApplyCommit`. `commit_ref` = PR number; `fetched_sha` = the
|
|
/// reviewed PR head pinned at submit. See `docs/approvals.md`.
|
|
MergeConfigPr,
|
|
}
|
|
|
|
impl ApprovalKind {
|
|
/// Wire/UI string — the same value serde's `snake_case` rename
|
|
/// produces. The single source of truth for every place that needs
|
|
/// the kind as a `&'static str` (sqlite storage, dashboard events),
|
|
/// so adding a variant can't silently miss a hand-rolled match.
|
|
#[must_use]
|
|
pub fn as_str(self) -> &'static str {
|
|
match self {
|
|
ApprovalKind::ApplyCommit => "apply_commit",
|
|
ApprovalKind::Spawn => "spawn",
|
|
ApprovalKind::InitConfig => "init_config",
|
|
ApprovalKind::UpdateMetaInputs => "update_meta_inputs",
|
|
ApprovalKind::SchedulePrompt => "schedule_prompt",
|
|
ApprovalKind::MergeConfigPr => "merge_config_pr",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum ApprovalStatus {
|
|
Pending,
|
|
Approved,
|
|
Denied,
|
|
Failed,
|
|
/// Manager withdrew the request before the operator acted on it.
|
|
/// Distinct from `Denied` (operator decision) and `Failed`
|
|
/// (post-approval lifecycle error). See
|
|
/// `docs/approvals.md::Withdrawing a pending approval`.
|
|
Cancelled,
|
|
}
|
|
|
|
/// Reminder activity statistics for an agent over a time window.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ReminderStats {
|
|
/// Total reminders scheduled in the window (`created_at` >= cutoff).
|
|
pub scheduled: u64,
|
|
/// Reminders that have been delivered in the window (`sent_at` IS NOT NULL).
|
|
pub delivered: u64,
|
|
/// Reminders still pending in the window (`sent_at` IS NULL).
|
|
pub pending: u64,
|
|
}
|
|
|
|
impl HostResponse {
|
|
#[must_use]
|
|
pub fn success() -> Self {
|
|
Self {
|
|
ok: true,
|
|
error: None,
|
|
agents: None,
|
|
approvals: None,
|
|
urls: None,
|
|
agent_statuses: None,
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn error(message: impl Into<String>) -> Self {
|
|
Self {
|
|
ok: false,
|
|
error: Some(message.into()),
|
|
agents: None,
|
|
approvals: None,
|
|
urls: None,
|
|
agent_statuses: None,
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn list(agents: Vec<String>) -> Self {
|
|
Self {
|
|
ok: true,
|
|
error: None,
|
|
agents: Some(agents),
|
|
approvals: None,
|
|
urls: None,
|
|
agent_statuses: None,
|
|
}
|
|
}
|
|
|
|
#[must_use]
|
|
pub fn pending(approvals: Vec<Approval>) -> Self {
|
|
Self {
|
|
ok: true,
|
|
error: None,
|
|
agents: None,
|
|
approvals: Some(approvals),
|
|
urls: None,
|
|
agent_statuses: None,
|
|
}
|
|
}
|
|
|
|
/// `Urls` result — this hive's domain + browser-facing web URLs.
|
|
#[must_use]
|
|
pub fn urls(urls: HiveUrls) -> Self {
|
|
Self {
|
|
ok: true,
|
|
error: None,
|
|
agents: None,
|
|
approvals: None,
|
|
urls: Some(urls),
|
|
agent_statuses: None,
|
|
}
|
|
}
|
|
|
|
/// `AgentStatus` result — one row per managed agent.
|
|
#[must_use]
|
|
pub fn agent_statuses(rows: Vec<AgentStatusRow>) -> Self {
|
|
Self {
|
|
ok: true,
|
|
error: None,
|
|
agents: None,
|
|
approvals: None,
|
|
urls: None,
|
|
agent_statuses: Some(rows),
|
|
}
|
|
}
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Per-agent socket — /run/hyperhive/agents/<name>/mcp.sock on the host,
|
|
// bind-mounted into the container at /run/hive/mcp.sock.
|
|
// -----------------------------------------------------------------------------
|
|
|
|
/// A logical message between agents.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct Message {
|
|
pub from: String,
|
|
pub to: String,
|
|
pub body: String,
|
|
/// Optional broker row-id of the message this is a reply to.
|
|
/// Stored in the DB and echoed back on `Recv` so the dashboard can
|
|
/// render conversation threads. `None` for messages that start a
|
|
/// new thread. Ignored if the referenced id is unknown or out of
|
|
/// retention — purely advisory.
|
|
pub in_reply_to: Option<i64>,
|
|
}
|
|
|
|
/// One row of a broker inbox query — what the dashboard renders in
|
|
/// its operator-inbox section and what a per-agent web UI returns
|
|
/// from a `Recent` request. Lives in `hive_sh4re` so it can travel
|
|
/// over both the dashboard's `/api/state` and the agent socket
|
|
/// without an internal-to-wire conversion.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct InboxRow {
|
|
pub id: i64,
|
|
pub from: String,
|
|
pub body: String,
|
|
pub at: i64,
|
|
/// Row-id of the message this is a reply to, if any.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub in_reply_to: Option<i64>,
|
|
}
|
|
|
|
/// One delivered message in a `Recv` response.
|
|
/// See `docs/conventions.md::Broker delivery + ack cycle` for the
|
|
/// full delivery/ack/requeue story.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct DeliveredMessage {
|
|
pub from: String,
|
|
pub body: String,
|
|
/// Broker row id, tracked by the harness for `AckTurn`. Opaque to
|
|
/// claude. `default` for wire backwards-compat.
|
|
#[serde(default)]
|
|
pub id: i64,
|
|
/// `true` if this row was resurfaced by `RequeueInflight` (previously
|
|
/// popped, never acked). Formatter prepends a "may already be handled"
|
|
/// hint when set.
|
|
#[serde(default)]
|
|
pub redelivered: bool,
|
|
/// Row-id of the message this is a reply to, if any.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub in_reply_to: Option<i64>,
|
|
}
|
|
|
|
/// Reminder timing: either relative (wait N seconds) or absolute (at unix
|
|
/// timestamp).
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(tag = "timing_type", rename_all = "snake_case")]
|
|
pub enum ReminderTiming {
|
|
/// Remind after this many seconds from now.
|
|
InSeconds { seconds: u64 },
|
|
/// Remind at this unix timestamp (seconds since epoch).
|
|
At { unix_timestamp: i64 },
|
|
}
|
|
|
|
/// One row in the response to `GetLooseEnds`. Tagged enum so new
|
|
/// thread kinds can land without breaking existing handlers.
|
|
/// Per-flavour scoping + per-variant fields + clock-anomaly
|
|
/// saturation behaviour live in
|
|
/// `docs/conventions.md::Loose-ends wire shape`.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
|
pub enum LooseEnd {
|
|
/// A pending approval row.
|
|
Approval {
|
|
id: i64,
|
|
agent: String,
|
|
commit_ref: String,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
description: Option<String>,
|
|
age_seconds: u64,
|
|
},
|
|
/// An unanswered question row.
|
|
Question {
|
|
id: i64,
|
|
asker: String,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
target: Option<String>,
|
|
question: String,
|
|
age_seconds: u64,
|
|
},
|
|
/// A scheduled but un-delivered reminder row.
|
|
Reminder {
|
|
id: i64,
|
|
owner: String,
|
|
message: String,
|
|
due_at: DateTime<Utc>,
|
|
age_seconds: u64,
|
|
},
|
|
/// Undelivered inbox messages waiting to be `recv`'d by this agent.
|
|
/// Not cancellable — drain them with `recv`. Surfaced so an agent
|
|
/// doing a between-turns `get_loose_ends` sweep sees it still owes
|
|
/// itself a `recv` without having to poll the inbox separately. Only
|
|
/// emitted when `count > 0`.
|
|
PendingMessages {
|
|
/// Number of undelivered messages queued for this agent.
|
|
count: u64,
|
|
},
|
|
/// Unread matrix notifications in one or more rooms. Not cancellable —
|
|
/// use `mark_read` via the matrix MCP to clear. Injected by the
|
|
/// in-container harness (not hive-c0re) because the matrix daemon
|
|
/// runs inside the agent container.
|
|
UnreadMatrix {
|
|
/// Number of rooms with at least one unread notification.
|
|
rooms: u32,
|
|
/// Per-room summary: one line per room with truncated last-message
|
|
/// body when count is 1, or just the unread count otherwise. Empty
|
|
/// when the daemon returned no per-room detail.
|
|
#[serde(default)]
|
|
summary: String,
|
|
},
|
|
}
|
|
|
|
/// Kind discriminator for `CancelLooseEnd`. Per-kind store +
|
|
/// authorisation rules live in
|
|
/// `docs/conventions.md::Loose-ends wire shape`.
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum CancelLooseEndKind {
|
|
Question,
|
|
Reminder,
|
|
/// Withdraw a pending approval (manager surface only).
|
|
Approval,
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Bash-task on-disk schema (shared with `hive-bash-mcp`)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Lifecycle state of a bash task.
|
|
///
|
|
/// Canonical home for the bash-task persisted schema: `hive-bash-mcp`
|
|
/// (the daemon that writes the files) re-exports these from its
|
|
/// `protocol` module, and `hive-ag3nt` (the agent web UI that reads
|
|
/// them back for the running-tasks panel) deserializes the same type, so
|
|
/// the on-disk shape can't drift between writer and reader.
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum TaskStatus {
|
|
Pending,
|
|
Running,
|
|
Done,
|
|
TimedOut,
|
|
/// Daemon was restarted while the task was running; process is gone.
|
|
Interrupted,
|
|
/// Killed on request via `BashKill` (SIGINT or SIGKILL to the task's
|
|
/// process group). Distinct from `Interrupted` (daemon-restart) and
|
|
/// `TimedOut` (exceeded `timeout_secs`).
|
|
Killed,
|
|
}
|
|
|
|
/// Task metadata + result written to `<id>.json` under the bash-tasks dir.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct TaskFile {
|
|
pub id: String,
|
|
pub cmd: String,
|
|
/// Kill timeout in seconds. `None` means no timeout — task runs until
|
|
/// natural exit. Old task files with a numeric value are still readable
|
|
/// (serde coerces `u64` → `Some(u64)` is handled by the caller).
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub timeout_secs: Option<u64>,
|
|
pub status: TaskStatus,
|
|
pub created_at: i64,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub started_at: Option<i64>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub completed_at: Option<i64>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub exit_code: Option<i32>,
|
|
/// Last `SUMMARY_BYTES` of stdout (see `hive-bash-mcp` runner).
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub stdout_tail: Option<String>,
|
|
/// Last `SUMMARY_BYTES` of stderr (see `hive-bash-mcp` runner).
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub stderr_tail: Option<String>,
|
|
}
|
|
|
|
/// Unified request enum for both agent and manager sockets. The agent's
|
|
/// identity is the socket it arrived on. Privileged variants are marked
|
|
/// `*(privileged)*` — an agent socket returns `Err` for them server-side.
|
|
///
|
|
/// `AgentRequest` and `ManagerRequest` are type aliases for this enum;
|
|
/// existing callers continue to compile unchanged.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(tag = "cmd", rename_all = "snake_case")]
|
|
pub enum Request {
|
|
/// Send a message to another agent.
|
|
Send {
|
|
to: String,
|
|
body: String,
|
|
/// Optional id of the message being replied to. Stored in the
|
|
/// broker DB and returned on `Recv` so the dashboard can render
|
|
/// threads. Ignored if the id is unknown or out of retention.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
in_reply_to: Option<i64>,
|
|
},
|
|
/// Pop pending messages from this agent's inbox.
|
|
/// Delivery + ack cycle: see
|
|
/// `docs/conventions.md::Broker delivery + ack cycle`.
|
|
Recv {
|
|
#[serde(default)]
|
|
wait_seconds: Option<u64>,
|
|
#[serde(default)]
|
|
max: Option<u32>,
|
|
},
|
|
/// Non-mutating: how many pending messages are addressed to me?
|
|
/// Used by the harness to render a status line after each tool call.
|
|
Status,
|
|
/// Operator-injected message TO this agent (from this agent's own web
|
|
/// UI). Recipient is implicit — `from` is `"operator"`. Effectively the
|
|
/// per-agent equivalent of the old dashboard T4LK form, but scoped to
|
|
/// the agent whose page the operator is on.
|
|
OperatorMsg { body: String },
|
|
/// Wake-up event injected from inside the container. Recipient is
|
|
/// implicit (this agent); `from` is caller-chosen. See
|
|
/// `docs/conventions.md::Wake injection` for the trust model and
|
|
/// typical callers. The wake is persisted in the sqlite broker
|
|
/// like any other message — the agent can ack it via `AckUntil`.
|
|
Wake { from: String, body: String },
|
|
/// Last `limit` messages addressed to this agent, newest-first.
|
|
/// Non-mutating — pulls from the broker without delivering. The
|
|
/// per-agent web UI uses this to render its own inbox section.
|
|
Recent { limit: u64 },
|
|
/// Surface a question to either the operator or another agent.
|
|
/// Routing + shape: see
|
|
/// `docs/conventions.md::Question routing (Ask / Answer)`.
|
|
Ask {
|
|
question: String,
|
|
#[serde(default)]
|
|
options: Vec<String>,
|
|
#[serde(default)]
|
|
multi: bool,
|
|
#[serde(default)]
|
|
ttl_seconds: Option<u64>,
|
|
#[serde(default)]
|
|
to: Option<String>,
|
|
},
|
|
/// Answer a question previously routed to this agent via
|
|
/// `HelperEvent::QuestionAsked`. Authorised callers + threading
|
|
/// back via `HelperEvent::QuestionAnswered`: see
|
|
/// `docs/conventions.md::Question routing (Ask / Answer)`.
|
|
Answer { id: i64, answer: String },
|
|
/// Schedule a reminder message to be delivered to this agent at a
|
|
/// future time. The reminder lands in the agent's inbox as an auto-sent
|
|
/// message from `"reminder"`. Use for agent follow-ups (e.g. check task
|
|
/// status, retry failed operation). Message length is limited; pass
|
|
/// `file_path` to store in a file and get a path-reference message
|
|
/// instead.
|
|
Remind {
|
|
message: String,
|
|
timing: ReminderTiming,
|
|
#[serde(default)]
|
|
file_path: Option<String>,
|
|
},
|
|
/// Loose-ends view. On the agent socket: `None` = self; direct
|
|
/// children are always accessible; non-children require the
|
|
/// `query_agent_state` capability — rejected with an error otherwise;
|
|
/// `"*"` is always rejected (use the manager socket). On the manager
|
|
/// socket: `None` = manager self, `"*"` = hive-wide, any name =
|
|
/// that agent. See `docs/conventions.md::Loose-ends wire shape`.
|
|
GetLooseEnds {
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
agent: Option<String>,
|
|
},
|
|
/// Count of pending (un-delivered) reminders. On the agent socket:
|
|
/// same target rules as `GetLooseEnds` (self/children free;
|
|
/// non-children require `query_agent_state`; `"*"` rejected).
|
|
/// On the manager socket: `None` = self, any name = that agent.
|
|
/// Used by the harness's per-turn stats sink.
|
|
CountPendingReminders {
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
agent: Option<String>,
|
|
},
|
|
/// Reminder statistics: counts of scheduled, delivered, and pending
|
|
/// reminders over a time window. `since_secs` filters to reminders
|
|
/// created in the last N seconds (0 = all). On the agent socket:
|
|
/// same target rules as `GetLooseEnds` (self/children free;
|
|
/// non-children require `query_agent_state`; `"*"` rejected).
|
|
/// On the manager socket: `None` = self, any name = that agent.
|
|
ReminderRollup {
|
|
/// Only count reminders created in the last N seconds from now.
|
|
/// Pass 0 to include all reminders.
|
|
#[serde(default)]
|
|
since_secs: u64,
|
|
/// Whose reminders to roll up. `None` = the caller's own.
|
|
/// `Some("<name>")` = that agent's (requires `query_agent_state`
|
|
/// capability on the agent socket; always available on the manager
|
|
/// socket).
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
agent: Option<String>,
|
|
},
|
|
/// Set a free-text status string visible on the dashboard. The harness
|
|
/// writes `{state_dir}/hyperhive-status` locally before sending this
|
|
/// request; hive-c0re just triggers a dashboard rescan on receipt.
|
|
/// Pass an empty string to clear the status.
|
|
SetStatus { text: String },
|
|
/// Fetch identity + status for an agent. `name = None` =
|
|
/// self-introspection; `Some(<agent>)` = target query. See
|
|
/// `docs/conventions.md::Agent metadata`.
|
|
GetAgentMeta {
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
name: Option<String>,
|
|
},
|
|
/// Cancel an open thread the agent owns. Authorisation +
|
|
/// per-kind semantics in
|
|
/// `docs/conventions.md::Loose-ends wire shape`.
|
|
CancelLooseEnd { kind: CancelLooseEndKind, id: i64 },
|
|
/// Create a git repo *through hive-c0re*. Agents can't create
|
|
/// repos with their own forge token (`max_repo_creation = 0`); this is
|
|
/// the sanctioned path. hive-c0re creates `repo` in the c0re-owned
|
|
/// `agents` org, adds the calling agent as a write collaborator (not
|
|
/// owner), and applies operator-team branch protection so the author
|
|
/// can't merge its own PRs. Returns the new repo's full name.
|
|
CreateRepo { repo: String },
|
|
/// Mark every message popped since the last `AckTurn` as handled.
|
|
/// Harness↔broker pairing fired after `TurnOutcome::Ok`. See
|
|
/// `docs/conventions.md::Broker delivery + ack cycle`.
|
|
AckTurn,
|
|
/// Mark every inbox message with broker row id `<= up_to` as
|
|
/// handled (`acked_at` set), whether still pending or already
|
|
/// delivered. The agent-facing bulk-triage escape hatch for a
|
|
/// redelivered / accumulated backlog: instead of popping and
|
|
/// re-reading dozens of already-handled messages one turn at a
|
|
/// time, the agent acks everything up to the id it has seen.
|
|
/// Recipient-scoped — an agent can only ack its own rows. See
|
|
/// `docs/conventions.md::Broker delivery + ack cycle`.
|
|
AckUntil { up_to: i64 },
|
|
/// Requeue every popped-but-unacked message back into the inbox.
|
|
/// Harness fires this once at boot to recover from
|
|
/// crashed-mid-turn sessions. See
|
|
/// `docs/conventions.md::Broker delivery + ack cycle`.
|
|
RequeueInflight,
|
|
/// Harness → c0re: "I saw the `GracefulStop` signal, ran my
|
|
/// stop-checkpoint turn (durable `/state` flushed) and am exiting my
|
|
/// serve loop now." Lets the `GracefulStop` orchestration stop the
|
|
/// container immediately instead of waiting out its timeout fallback.
|
|
GracefulStopComplete,
|
|
/// *(capability-gated: `read_host_journal`)* Fetch recent lines
|
|
/// from the host journal. Filters are all optional; omitting all
|
|
/// returns the last `lines` entries from the global journal.
|
|
GetHostJournal {
|
|
/// Filter to a specific systemd unit (e.g. `hive-c0re.service`).
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
unit: Option<String>,
|
|
/// Machine name to pass to journalctl `-M` verbatim (e.g. `h-iris`).
|
|
/// The caller is responsible for the correct nspawn machine name.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
container: Option<String>,
|
|
/// Number of journal lines to return (default 30, max 100).
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
lines: Option<u32>,
|
|
/// Minimum syslog priority level.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
priority: Option<JournalPriority>,
|
|
/// Regex to match against log message fields (journalctl `--grep`).
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
grep: Option<String>,
|
|
/// Show entries on or newer than this timestamp (journalctl `--since`).
|
|
/// ISO 8601 or journalctl-accepted relative strings (e.g. `"-1h"`).
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
since: Option<String>,
|
|
/// Show entries on or older than this timestamp (journalctl `--until`).
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
until: Option<String>,
|
|
},
|
|
|
|
// ---- privileged (manager socket only for now) ---------------------------
|
|
/// *(privileged)* Initialise a brand-new agent's proposed config repo
|
|
/// and queue an approval for the operator to review.
|
|
RequestInitConfig {
|
|
name: String,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
description: Option<String>,
|
|
},
|
|
/// *(privileged)* Stop a sub-agent (graceful).
|
|
Kill { name: String },
|
|
/// *(privileged)* Start a previously-stopped sub-agent container.
|
|
Start { name: String },
|
|
/// *(privileged)* Restart a sub-agent container (stop + start).
|
|
Restart { name: String },
|
|
/// *(privileged)* Rebuild a sub-agent against the current hyperhive
|
|
/// flake + agent.nix. No approval required.
|
|
Update { name: String },
|
|
/// *(privileged)* Submit a config commit for the operator to approve.
|
|
RequestApplyCommit {
|
|
agent: String,
|
|
commit_ref: String,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
description: Option<String>,
|
|
},
|
|
/// *(privileged)* Fetch recent journal lines for a sub-agent container.
|
|
GetLogs {
|
|
agent: String,
|
|
#[serde(default)]
|
|
lines: Option<u32>,
|
|
},
|
|
/// *(privileged)* Queue an approval to run `nix flake update [inputs...]`.
|
|
RequestUpdateMetaInputs {
|
|
#[serde(default)]
|
|
inputs: Vec<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
description: Option<String>,
|
|
},
|
|
/// *(privileged)* Queue an approval to add a scheduled prompt.
|
|
RequestSchedulePrompt(SchedulePromptPayload),
|
|
/// *(privileged)* Cancel a scheduled prompt.
|
|
CancelSchedule {
|
|
id: i64,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
targets: Option<Vec<String>>,
|
|
},
|
|
/// *(privileged)* List every schedule in the queue.
|
|
ListSchedules,
|
|
/// List all containers that are topological descendants of the calling
|
|
/// agent (direct children + their subtrees). Scoped to the caller's
|
|
/// subtree; gated by the `lifecycle` tool group. The result includes all
|
|
/// known descendants regardless of whether the container is currently
|
|
/// running — use `running` to distinguish.
|
|
ListDescendants,
|
|
/// *(privileged)* Fire a scheduled prompt out of band immediately.
|
|
FireScheduleNow { id: i64 },
|
|
/// *(privileged)* Edit an existing schedule's mutable fields.
|
|
EditSchedule {
|
|
id: i64,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
body: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
description: Option<Option<String>>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
interval_seconds: Option<Option<u64>>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
next_fire_at_unix: Option<i64>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
targets_add: Option<Vec<String>>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
targets_remove: Option<Vec<String>>,
|
|
},
|
|
}
|
|
|
|
/// Backwards-compatible aliases. Both sockets now speak the unified `Request`
|
|
/// / `Response` wire; the server-side privilege gate rejects privileged
|
|
/// variants on agent sockets with `Err { message: "privileged variant..." }`.
|
|
pub type AgentRequest = Request;
|
|
pub type ManagerRequest = Request;
|
|
|
|
/// Unified response enum for both agent and manager sockets. Privileged
|
|
/// variants (`Logs`, `Schedules`) are never returned on agent sockets.
|
|
///
|
|
/// `AgentResponse` and `ManagerResponse` are type aliases for this enum.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
|
pub enum Response {
|
|
/// `Send` succeeded.
|
|
Ok,
|
|
/// Either `Send` failed or `Recv` errored.
|
|
Err { message: String },
|
|
/// `Recv` result: zero or more messages, FIFO-ordered, never
|
|
/// longer than the `max` the caller passed. Empty vec = nothing
|
|
/// pending (the "(empty)" path for the formatter). Per-row `id` +
|
|
/// `redelivered` carry the broker's row id (tracked by the harness
|
|
/// for `AckTurn`, and surfaced to claude as a `[msg #<id>]` marker
|
|
/// so `AckUntil` has something to reference) and the "previously
|
|
/// popped, not acked" flag — see `DeliveredMessage` for details.
|
|
Messages { messages: Vec<DeliveredMessage> },
|
|
/// `Status` result: how many pending messages are in this agent's inbox.
|
|
Status { unread: u64 },
|
|
/// `AckUntil` result: how many rows were newly marked handled.
|
|
Acked { count: u64 },
|
|
/// `Recent` result: newest-first inbox rows.
|
|
Recent { rows: Vec<InboxRow> },
|
|
/// `Ask` result: the queued question id. The answer lands later
|
|
/// as `HelperEvent::QuestionAnswered` in this agent's inbox.
|
|
QuestionQueued { id: i64 },
|
|
/// `GetLooseEnds` result: list of loose ends pending against
|
|
/// this agent. Ordered newest-first within each kind.
|
|
LooseEnds { loose_ends: Vec<LooseEnd> },
|
|
/// `CountPendingReminders` result.
|
|
PendingRemindersCount { count: u64 },
|
|
/// `ReminderRollup` result: reminder activity stats for the agent.
|
|
ReminderRollup(ReminderStats),
|
|
/// `GetAgentMeta` result. Per-field semantics + serde defaults
|
|
/// live in `docs/conventions.md::Agent metadata`.
|
|
AgentMeta {
|
|
name: String,
|
|
#[serde(default = "default_true")]
|
|
running: bool,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
hyperhive_rev: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
status_text: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
status_set_at: Option<i64>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
hive_name: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
swarm_name: Option<String>,
|
|
/// Matrix identities this agent can act as (one per configured +
|
|
/// live account). Empty for agents with no matrix provisioning.
|
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
matrix_accounts: Vec<MatrixIdentity>,
|
|
},
|
|
/// `GetLogs` result: journal lines for the requested container.
|
|
/// Returned on the manager socket only.
|
|
Logs { content: String },
|
|
/// `GetHostJournal` result: host journal lines matching the
|
|
/// requested filters. Returned on the agent socket when the agent
|
|
/// holds the `read_host_journal` capability.
|
|
HostJournal { content: String },
|
|
/// `ListSchedules` result. Snapshot of every schedule.
|
|
/// Returned on the manager socket only.
|
|
Schedules { schedules: Vec<WireSchedule> },
|
|
/// `CreateRepo` result: the new repo's full name (`agents/<repo>`)
|
|
/// and clone URL, so the agent can immediately `git clone` it.
|
|
RepoCreated {
|
|
full_name: String,
|
|
clone_url: String,
|
|
},
|
|
/// `ListDescendants` result: all descendant containers, with running
|
|
/// status. Ordered by topology depth (parents before children), then
|
|
/// alphabetically within each depth tier.
|
|
Containers { containers: Vec<ContainerInfo> },
|
|
/// `Recv` result when a graceful stop is pending for this agent
|
|
/// (set by hive-c0re's `GracefulStop` orchestration). Returned in
|
|
/// place of `Messages` — it doubles as the inbound fence: the harness
|
|
/// stops consuming normal inbox messages and instead runs one
|
|
/// stop-checkpoint turn (flush durable `/state`), then reports
|
|
/// `GracefulStopComplete` and exits its serve loop so the container
|
|
/// can be stopped cleanly. New sends keep queueing in the broker for
|
|
/// the agent's next start.
|
|
GracefulStop,
|
|
}
|
|
|
|
/// Backwards-compatible response aliases.
|
|
pub type AgentResponse = Response;
|
|
pub type ManagerResponse = Response;
|
|
|
|
/// One entry in a `ListDescendants` result.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct ContainerInfo {
|
|
/// Logical agent name (no `h-` prefix).
|
|
pub name: String,
|
|
/// Whether the container is currently running.
|
|
pub running: bool,
|
|
}
|
|
|
|
/// One row in a `HostRequest::AgentStatus` result — the operator-CLI
|
|
/// projection of the dashboard's per-agent `ContainerView`. Carries the
|
|
/// agent's running/health flags plus the technical state an operator
|
|
/// wants in a roster overview (`hivectl agents list`).
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct AgentStatusRow {
|
|
/// Logical agent name (no `h-` prefix).
|
|
pub name: String,
|
|
/// Whether the container is currently running.
|
|
pub running: bool,
|
|
/// Config commit is pending — the locked rev differs from the
|
|
/// agent's proposed/applied config (a rebuild would change it).
|
|
pub needs_update: bool,
|
|
/// The agent has no live claude session and is parked waiting for
|
|
/// the operator's re-auth flow.
|
|
pub needs_login: bool,
|
|
/// First 12 chars of the sha the meta flake currently has locked for
|
|
/// this agent's input. `None` when the agent has no locked rev yet.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub deployed_sha: Option<String>,
|
|
/// Count of this agent's pending reminders.
|
|
#[serde(default)]
|
|
pub pending_reminders: u64,
|
|
/// Parent in the topology tree. `None` marks a root-level agent.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub parent: Option<String>,
|
|
}
|
|
|
|
/// One matrix identity an agent can act as, surfaced in `GetAgentMeta`'s
|
|
/// `matrix_accounts`. Field names match the daemon's `matrix-accounts.json`
|
|
/// snapshot (written by `hive-matrix-mcp`'s account registry) so hive-c0re
|
|
/// deserializes the snapshot straight into `Vec<MatrixIdentity>`; the
|
|
/// snapshot's `live` / `is_primary` fields are ignored here (only live
|
|
/// accounts are listed).
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct MatrixIdentity {
|
|
/// Logical account name (the `account` arg on the matrix MCP tools).
|
|
pub name: String,
|
|
/// Matrix user id (`@user:server`). `None` if the session restored
|
|
/// without a known user id yet.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub user_id: Option<String>,
|
|
/// Homeserver base URL this account is on.
|
|
pub homeserver: String,
|
|
}
|
|
|
|
/// Serde default for the `running` field; keeps wire backwards-compat
|
|
/// with pre-running-field payloads. See
|
|
/// `docs/conventions.md::Agent metadata`.
|
|
fn default_true() -> bool {
|
|
true
|
|
}
|
|
|
|
// -----------------------------------------------------------------------------
|
|
// Manager socket — /run/hyperhive/manager/mcp.sock on the host, bind-mounted
|
|
// into the manager container at /run/hive/mcp.sock.
|
|
// -----------------------------------------------------------------------------
|
|
|
|
/// Logical name the broker uses for the manager.
|
|
pub const MANAGER_AGENT: &str = "ruth";
|
|
|
|
/// Logical name the broker uses for the human operator. Messages with
|
|
/// `to = OPERATOR_RECIPIENT` accumulate in sqlite and surface on the
|
|
/// dashboard's inbox view — they are never `recv`'d by an agent harness.
|
|
pub const OPERATOR_RECIPIENT: &str = "operator";
|
|
|
|
/// Reserved magic recipient — `send(to: "<parent>", ...)` is rewritten
|
|
/// by hive-c0re at delivery time to whoever `topology::parent_of(sender)`
|
|
/// returns, or to [`OPERATOR_RECIPIENT`] when the sender is a root agent
|
|
/// (no parent). Lets agents address their parent without hardcoding the
|
|
/// label, so runtime reparenting requires no agent-side restart. The
|
|
/// angle brackets are not valid in agent names (validators reject
|
|
/// `<`/`>`), so this name can never collide with a real recipient.
|
|
pub const PARENT_RECIPIENT: &str = "<parent>";
|
|
|
|
/// Reserved magic recipient — `send(to: "<children>", ...)` fans out to
|
|
/// every agent whose direct parent (per `topology.json`) is the sender.
|
|
/// Lets a sub-manager nudge its subtree without enumerating labels at
|
|
/// call-time; topology changes propagate for free. The angle brackets
|
|
/// are structurally safe — agent name validation rejects `<`/`>`.
|
|
/// Delivers to an empty set (no-op) for leaf agents that have no children.
|
|
pub const CHILDREN_RECIPIENT: &str = "<children>";
|
|
|
|
/// Sender hive-c0re uses for events it pushes into the manager's inbox.
|
|
/// Manager harness recognises this and parses the body as a `HelperEvent`.
|
|
pub const SYSTEM_SENDER: &str = "system";
|
|
|
|
/// Out-of-band events the host-side daemon pushes to the manager's inbox.
|
|
/// Serialised as JSON in `Message::body` (sender = `SYSTEM_SENDER`).
|
|
/// Per-variant triggers + the optional `sha`/`tag` semantics live in
|
|
/// `docs/approvals.md::Helper events to the manager`.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(tag = "event", rename_all = "snake_case")]
|
|
pub enum HelperEvent {
|
|
/// An approval transitioned to a terminal state.
|
|
ApprovalResolved {
|
|
id: i64,
|
|
agent: String,
|
|
commit_ref: String,
|
|
status: ApprovalStatus,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
note: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
sha: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
tag: Option<String>,
|
|
},
|
|
/// A new container was spawned. `ok = false` = spawn failed.
|
|
Spawned {
|
|
agent: String,
|
|
ok: bool,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
note: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
sha: Option<String>,
|
|
},
|
|
/// A container was rebuilt (auto-update or manual).
|
|
Rebuilt {
|
|
agent: String,
|
|
ok: bool,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
note: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
sha: Option<String>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
tag: Option<String>,
|
|
},
|
|
/// A new agent's proposed config repo was seeded post-`InitConfig`.
|
|
ConfigReady { agent: String },
|
|
/// A sub-agent's container was stopped (systemd unit down; state kept).
|
|
Killed { agent: String },
|
|
/// A sub-agent's container was torn down (state dirs preserved by default).
|
|
Destroyed { agent: String },
|
|
/// A sub-agent's container has no claude session yet.
|
|
NeedsLogin { agent: String },
|
|
/// A sub-agent just completed claude login.
|
|
LoggedIn { agent: String },
|
|
/// A sub-agent's recorded flake rev is stale relative to hyperhive.
|
|
NeedsUpdate { agent: String },
|
|
/// Container exited without an operator-initiated stop (crash).
|
|
ContainerCrash {
|
|
agent: String,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
note: Option<String>,
|
|
},
|
|
/// A question queued via `Ask` was answered. `id` matches the
|
|
/// originating `QuestionQueued.id`; `answerer` is `"operator"` /
|
|
/// a peer agent name / `"ttl-watchdog"` on expiry.
|
|
QuestionAnswered {
|
|
id: i64,
|
|
question: String,
|
|
answer: String,
|
|
answerer: String,
|
|
},
|
|
/// A peer (or the manager) asked this agent a question. Recipient
|
|
/// replies via `Answer { id, answer }`; the answer routes back to
|
|
/// the asker as `QuestionAnswered`.
|
|
QuestionAsked {
|
|
id: i64,
|
|
asker: String,
|
|
question: String,
|
|
#[serde(default)]
|
|
options: Vec<String>,
|
|
#[serde(default)]
|
|
multi: bool,
|
|
},
|
|
}
|
|
|
|
/// Submission payload for `RequestSchedulePrompt`. Lives outside the
|
|
/// enum so it can also serialize into the approval row's `commit_ref`
|
|
/// (the dispatcher re-parses it on approve and inserts the schedule).
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
pub struct SchedulePromptPayload {
|
|
/// Names of recipient agents. Operator + `root` allowed.
|
|
pub targets: Vec<String>,
|
|
/// Message body delivered to each target's inbox at fire time.
|
|
/// Same size budget as `Send.body` — soft cap at the broker level.
|
|
pub body: String,
|
|
/// Absolute unix timestamp (seconds) for the FIRST fire. For
|
|
/// recurring schedules the worker then re-arms in
|
|
/// `interval_seconds` steps.
|
|
pub first_fire_at_unix: i64,
|
|
/// `None` = one-shot. `Some(n > 0)` = recurring every `n` seconds.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub interval_seconds: Option<u64>,
|
|
/// Optional description shown on the dashboard approval card AND
|
|
/// stored on the resulting schedule row for the operator's
|
|
/// "what is this?" reference later.
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub description: Option<String>,
|
|
}
|
|
|
|
/// Named group of MCP tools an agent may be granted. The harness reads
|
|
/// `HIVE_TOOL_GROUPS` from the environment (a comma-separated list of
|
|
/// `snake_case` group names written by the meta renderer from per-agent
|
|
/// config) and expands it to the matching tool names for `--allowedTools`.
|
|
/// When the env var is absent the harness falls back to `AGENT_DEFAULT`.
|
|
/// See `docs/conventions.md::Tool groups`.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum ToolGroup {
|
|
/// `send`, `recv`, `ask`, `answer`
|
|
Messaging,
|
|
/// `get_agent_meta` (`set_status` is always-on — see `ALWAYS_ON_TOOLS`)
|
|
Meta,
|
|
/// `get_loose_ends`, `cancel_loose_end`, `remind`, `request_next_turn`
|
|
Inbox,
|
|
/// `kill`, `start`, `restart`, `update` - *(privileged)*
|
|
Lifecycle,
|
|
/// `request_init_config`, `request_apply_commit`,
|
|
/// `request_update_meta_inputs` - *(privileged)*
|
|
Approvals,
|
|
/// `request_schedule_prompt`, `fire_schedule_now`, `cancel_schedule`,
|
|
/// `edit_schedule`, `list_schedules` - *(privileged)*
|
|
Scheduling,
|
|
/// `get_logs` - *(privileged)*
|
|
Diagnostics,
|
|
/// `create_repo` — create git repos through hive-c0re (the only path
|
|
/// now that agents can't create them directly). Opt-in per
|
|
/// agent so the operator controls who can spin up repos.
|
|
Forge,
|
|
/// `run`, `status` (via `mcp__bash__*`)
|
|
Execution,
|
|
/// Claude built-in web egress tools: `WebFetch` (retrieve a URL) and
|
|
/// `WebSearch` (search the web). Both are omitted from `--tools` by
|
|
/// default; adding this group to an agent enables them in the session
|
|
/// and in `--allowedTools` so they run without a confirmation prompt.
|
|
/// Does not gate any MCP tools — `tools()` returns `&[]`.
|
|
WebTools,
|
|
}
|
|
|
|
impl ToolGroup {
|
|
/// The MCP tool names (without the `mcp__hyperhive__` prefix) in this group.
|
|
/// Returns `&[]` for `WebTools` — it enables Claude built-in tools,
|
|
/// not MCP tools; see `builtin_tools()`.
|
|
#[must_use]
|
|
pub fn tools(self) -> &'static [&'static str] {
|
|
match self {
|
|
Self::Messaging => &["send", "recv", "ack_until", "ask", "answer"],
|
|
Self::Meta => &["get_agent_meta"],
|
|
Self::Inbox => &[
|
|
"get_loose_ends",
|
|
"cancel_loose_end",
|
|
"remind",
|
|
"request_next_turn",
|
|
],
|
|
Self::Lifecycle => &["kill", "start", "restart", "update", "list_containers"],
|
|
Self::Approvals => &[
|
|
"request_init_config",
|
|
"request_apply_commit",
|
|
"request_update_meta_inputs",
|
|
],
|
|
Self::Scheduling => &[
|
|
"request_schedule_prompt",
|
|
"fire_schedule_now",
|
|
"cancel_schedule",
|
|
"edit_schedule",
|
|
"list_schedules",
|
|
],
|
|
Self::Diagnostics => &["get_logs"],
|
|
Self::Forge => &["create_repo"],
|
|
Self::Execution => &["run", "status"],
|
|
Self::WebTools => &[],
|
|
}
|
|
}
|
|
|
|
/// MCP tools that are always exposed regardless of which tool groups an
|
|
/// agent is granted. `set_status` lives here because the operator
|
|
/// dashboard depends on every agent being able to report its status
|
|
/// chip — gating it behind a group would let a misconfigured agent go
|
|
/// dark on the dashboard. The server-side `SetStatus` handler has no
|
|
/// tool-group check either (only length validation), so listing it here
|
|
/// keeps the `--allowedTools` list honest with that reality.
|
|
pub const ALWAYS_ON_TOOLS: &'static [&'static str] = &["set_status"];
|
|
|
|
/// The Claude built-in tool names enabled by this group. Only
|
|
/// `WebTools` returns a non-empty slice; all other groups return `&[]`
|
|
/// (they control MCP tools via `tools()` instead).
|
|
#[must_use]
|
|
pub fn builtin_tools(self) -> &'static [&'static str] {
|
|
match self {
|
|
Self::WebTools => &["WebFetch", "WebSearch"],
|
|
_ => &[],
|
|
}
|
|
}
|
|
|
|
/// Default tool groups for an agent harness. Used when `HIVE_TOOL_GROUPS` is unset.
|
|
pub const AGENT_DEFAULT: &'static [Self] =
|
|
&[Self::Messaging, Self::Meta, Self::Inbox, Self::Execution];
|
|
|
|
/// Convenience preset for a fully-privileged agent (all groups).
|
|
/// Use this as a starting point in `tool-groups.json` for root/manager agents.
|
|
pub const MANAGER_DEFAULT: &'static [Self] = &[
|
|
Self::Messaging,
|
|
Self::Meta,
|
|
Self::Inbox,
|
|
Self::Lifecycle,
|
|
Self::Approvals,
|
|
Self::Scheduling,
|
|
Self::Diagnostics,
|
|
Self::Execution,
|
|
];
|
|
|
|
/// Every known tool group in a stable order. Use this to enumerate
|
|
/// columns in the capabilities UI or any other place that needs the
|
|
/// full list without hard-coding it at the call site.
|
|
pub const ALL: &'static [Self] = &[
|
|
Self::Messaging,
|
|
Self::Meta,
|
|
Self::Inbox,
|
|
Self::Lifecycle,
|
|
Self::Approvals,
|
|
Self::Scheduling,
|
|
Self::Diagnostics,
|
|
Self::Forge,
|
|
Self::Execution,
|
|
Self::WebTools,
|
|
];
|
|
|
|
/// The `snake_case` wire name for this group (matches `serde(rename_all =
|
|
/// "snake_case")` serialisation).
|
|
#[must_use]
|
|
pub fn as_str(self) -> &'static str {
|
|
match self {
|
|
Self::Messaging => "messaging",
|
|
Self::Meta => "meta",
|
|
Self::Inbox => "inbox",
|
|
Self::Lifecycle => "lifecycle",
|
|
Self::Approvals => "approvals",
|
|
Self::Scheduling => "scheduling",
|
|
Self::Diagnostics => "diagnostics",
|
|
Self::Forge => "forge",
|
|
Self::Execution => "execution",
|
|
Self::WebTools => "web_tools",
|
|
}
|
|
}
|
|
|
|
/// Short human-readable description suitable for a tooltip or help text.
|
|
#[must_use]
|
|
pub fn description(self) -> &'static str {
|
|
match self {
|
|
Self::Messaging => "send, recv, ask, answer — core agent communication",
|
|
Self::Meta => {
|
|
"get_agent_meta — identity introspection (set_status is always available)"
|
|
}
|
|
Self::Inbox => {
|
|
"get_loose_ends, cancel_loose_end, remind, request_next_turn — self-scheduling"
|
|
}
|
|
Self::Lifecycle => {
|
|
"kill, start, restart, update, list_containers — container lifecycle (privileged)"
|
|
}
|
|
Self::Approvals => {
|
|
"request_init_config, request_apply_commit, request_update_meta_inputs — config change flow (privileged)"
|
|
}
|
|
Self::Scheduling => {
|
|
"request_schedule_prompt and related — operator-visible scheduled prompts (privileged)"
|
|
}
|
|
Self::Diagnostics => {
|
|
"get_logs — read a sub-agent container's systemd journal (privileged)"
|
|
}
|
|
Self::Forge => {
|
|
"create_repo — create git repos through hive-c0re (operator-gated merge)"
|
|
}
|
|
Self::Execution => {
|
|
"run, status — run shell commands via mcp__bash__run / mcp__bash__status"
|
|
}
|
|
Self::WebTools => "WebFetch, WebSearch — Claude built-in web egress; not MCP tools",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Per-agent capability grants. Stored in `meta/capabilities.json`
|
|
/// (same shape as `tool-groups.json`: `{ "alice": ["read_host_journal"] }`).
|
|
/// Capabilities control system-level access that hive-c0re enforces
|
|
/// Syslog priority levels for `GetHostJournal`. Serialised as lowercase
|
|
/// strings matching journalctl `-p` accepted values.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, schemars::JsonSchema)]
|
|
#[serde(rename_all = "lowercase")]
|
|
pub enum JournalPriority {
|
|
Emerg,
|
|
Alert,
|
|
Crit,
|
|
Err,
|
|
Warning,
|
|
Notice,
|
|
Info,
|
|
Debug,
|
|
}
|
|
|
|
impl JournalPriority {
|
|
/// Returns the lowercase string journalctl expects for `-p`.
|
|
#[must_use]
|
|
pub fn as_str(self) -> &'static str {
|
|
match self {
|
|
Self::Emerg => "emerg",
|
|
Self::Alert => "alert",
|
|
Self::Crit => "crit",
|
|
Self::Err => "err",
|
|
Self::Warning => "warning",
|
|
Self::Notice => "notice",
|
|
Self::Info => "info",
|
|
Self::Debug => "debug",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// at dispatch time; they are orthogonal to tool groups (which control
|
|
/// which MCP tools the harness exposes to claude).
|
|
///
|
|
/// Injected into containers as `HIVE_CAPABILITIES` (comma-separated
|
|
/// `snake_case`) via `meta::render_flake`. The harness reads this to
|
|
/// conditionally register capability-gated MCP tools so claude only
|
|
/// sees tools it can actually invoke. See `docs/conventions.md::Capabilities`.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum Capability {
|
|
/// Agent can lifecycle-manage the root agent (kill/start/restart)
|
|
/// on behalf of the hive when the root has crashed. Named capability
|
|
/// for the existing manager privilege — future topology enforcement
|
|
/// will gate this via the capability system instead of the hardcoded
|
|
/// `container == MANAGER_CONTAINER` check.
|
|
ManageRootAgent,
|
|
/// Agent can read the full host journal via `GetHostJournal`.
|
|
/// hive-c0re checks this capability before running journalctl.
|
|
/// MCP tool `get_host_journal` is only registered in the harness
|
|
/// when this capability is present.
|
|
ReadHostJournal,
|
|
/// Agent can query non-child agents via `GetLooseEnds`,
|
|
/// `CountPendingReminders`, and `ReminderRollup` on the agent
|
|
/// socket. Without this capability, targeting a non-child agent is
|
|
/// rejected with an error (direct children are always accessible
|
|
/// without any capability). The `"*"` hive-wide value is not
|
|
/// available on the agent socket even with this capability — use the
|
|
/// manager socket for swarm-wide scans.
|
|
QueryAgentState,
|
|
/// Agent can restart hive infrastructure containers (hive-ci,
|
|
/// hive-gateway, hive-forge) via the `restart` MCP tool. hive-c0re
|
|
/// checks this capability before routing the restart through
|
|
/// hive-priv; the concrete service allowlist lives root-side in
|
|
/// hive-priv. Deliberately generic ("infra admin") so future
|
|
/// privileged infra ops can hang off the same grant.
|
|
InfraAdmin,
|
|
}
|
|
|
|
impl Capability {
|
|
/// Every known capability in a stable order. Use this to enumerate
|
|
/// columns in the permissions UI or validate incoming capability strings.
|
|
pub const ALL: &'static [Self] = &[
|
|
Self::ManageRootAgent,
|
|
Self::ReadHostJournal,
|
|
Self::QueryAgentState,
|
|
Self::InfraAdmin,
|
|
];
|
|
|
|
/// Canonical `snake_case` name for this capability (matches serde).
|
|
#[must_use]
|
|
pub fn as_str(self) -> &'static str {
|
|
match self {
|
|
Self::ManageRootAgent => "manage_root_agent",
|
|
Self::ReadHostJournal => "read_host_journal",
|
|
Self::QueryAgentState => "query_agent_state",
|
|
Self::InfraAdmin => "infra_admin",
|
|
}
|
|
}
|
|
|
|
/// Short human-readable description suitable for a tooltip or help text.
|
|
#[must_use]
|
|
pub fn description(self) -> &'static str {
|
|
match self {
|
|
Self::ManageRootAgent => {
|
|
"lifecycle-manage the root/manager agent on hive crash recovery"
|
|
}
|
|
Self::ReadHostJournal => "read host journald via get_host_journal MCP tool",
|
|
Self::QueryAgentState => {
|
|
"query non-child agents' loose ends and reminder state via get_loose_ends"
|
|
}
|
|
Self::InfraAdmin => {
|
|
"restart hive infrastructure containers (hive-ci, hive-gateway, hive-forge) via the restart tool"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Schedule row shape on the wire — mirror of
|
|
/// `scheduled_prompts::Schedule` but in the public crate so the
|
|
/// dashboard and agent surfaces can deserialize without depending
|
|
/// on hive-c0re-internal types. Kept structurally identical to the
|
|
/// in-process type; the conversion is field-by-field in
|
|
/// `manager_server` / `dashboard`.
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct WireSchedule {
|
|
pub id: i64,
|
|
pub owner: String,
|
|
pub body: String,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub interval_seconds: Option<u64>,
|
|
pub next_fire_at_unix: DateTime<Utc>,
|
|
pub created_at_unix: DateTime<Utc>,
|
|
pub source: WireScheduleSource,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub cancelled_at_unix: Option<DateTime<Utc>>,
|
|
/// Set while the schedule is paused. Worker skips paused rows;
|
|
/// they keep their `next_fire_at_unix` so resuming at any time
|
|
/// fires at the next intended instant (no catch-up clamp needed
|
|
/// — a paused schedule simply slips its next fire).
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub paused_at_unix: Option<DateTime<Utc>>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub description: Option<String>,
|
|
pub targets: Vec<WireScheduleTarget>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
|
pub enum WireScheduleSource {
|
|
Operator,
|
|
Approval { id: i64 },
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct WireScheduleTarget {
|
|
pub target: String,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub cancelled_at_unix: Option<DateTime<Utc>>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub last_fired_at_unix: Option<DateTime<Utc>>,
|
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
|
pub last_result: Option<String>,
|
|
}
|