//! Wire types shared between `hive-c0re` and the in-container harness. use serde::{Deserialize, Serialize}; pub mod assets; pub mod priv_proto; // ----------------------------------------------------------------------------- // 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 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, }, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct HostResponse { pub ok: bool, #[serde(default, skip_serializing_if = "Option::is_none")] pub error: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub agents: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub approvals: Option>, } /// 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, /// `ApplyCommit` only: the canonical hive-c0re-vouched sha after /// the proposal fetch, tagged `proposal/`. Stable for the /// approval's lifetime — manager amends in proposed don't change /// what gets built. #[serde(default, skip_serializing_if = "Option::is_none")] pub fetched_sha: Option, pub requested_at: i64, pub status: ApprovalStatus, #[serde(default, skip_serializing_if = "Option::is_none")] pub resolved_at: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub note: Option, /// 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, } /// 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, } #[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, } } #[must_use] pub fn error(message: impl Into) -> Self { Self { ok: false, error: Some(message.into()), agents: None, approvals: None, } } #[must_use] pub fn list(agents: Vec) -> Self { Self { ok: true, error: None, agents: Some(agents), approvals: None, } } #[must_use] pub fn pending(approvals: Vec) -> Self { Self { ok: true, error: None, agents: None, approvals: Some(approvals), } } } // ----------------------------------------------------------------------------- // Per-agent socket — /run/hyperhive/agents//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, } /// 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, } /// 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, } /// 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, age_seconds: u64, }, /// An unanswered question row. Question { id: i64, asker: String, #[serde(default, skip_serializing_if = "Option::is_none")] target: Option, question: String, age_seconds: u64, }, /// A scheduled but un-delivered reminder row. Reminder { id: i64, owner: String, message: String, due_at: i64, age_seconds: 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, } /// 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, }, /// 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, #[serde(default)] max: Option, }, /// 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. /// /// When `transient` is `true` the server delivers the wake signal /// through an in-process channel only — no sqlite write, no /// redelivery on restart. Use this for ephemeral notifications (e.g. /// bash task completions) where persistence is unnecessary and would /// cause duplicate delivery after a harness restart. Wake { from: String, body: String, #[serde(default)] transient: bool, }, /// 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, #[serde(default)] multi: bool, #[serde(default)] ttl_seconds: Option, #[serde(default)] to: Option, }, /// 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, }, /// 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, }, /// 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, }, /// 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("")` = 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, }, /// 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()` = target query. See /// `docs/conventions.md::Agent metadata`. GetAgentMeta { #[serde(default, skip_serializing_if = "Option::is_none")] name: Option, }, /// Cancel an open thread the agent owns. Authorisation + /// per-kind semantics in /// `docs/conventions.md::Loose-ends wire shape`. CancelLooseEnd { kind: CancelLooseEndKind, id: i64 }, /// 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, /// 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, /// *(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, /// 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, /// Number of journal lines to return (default 30, max 100). #[serde(default, skip_serializing_if = "Option::is_none")] lines: Option, /// Minimum syslog priority level. #[serde(default, skip_serializing_if = "Option::is_none")] priority: Option, /// Regex to match against log message fields (journalctl `--grep`). #[serde(default, skip_serializing_if = "Option::is_none")] grep: Option, /// 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, /// Show entries on or older than this timestamp (journalctl `--until`). #[serde(default, skip_serializing_if = "Option::is_none")] until: Option, }, // ---- 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, }, /// *(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, }, /// *(privileged)* Fetch recent journal lines for a sub-agent container. GetLogs { agent: String, #[serde(default)] lines: Option, }, /// *(privileged)* Queue an approval to run `nix flake update [inputs...]`. RequestUpdateMetaInputs { #[serde(default)] inputs: Vec, #[serde(default, skip_serializing_if = "Option::is_none")] description: Option, }, /// *(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>, }, /// *(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, #[serde(default, skip_serializing_if = "Option::is_none")] description: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] interval_seconds: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] next_fire_at_unix: Option, #[serde(default, skip_serializing_if = "Option::is_none")] targets_add: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] targets_remove: Option>, }, } /// 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 (opaque to claude; /// tracked by the harness for `AckTurn`) and the "previously /// popped, not acked" flag — see `DeliveredMessage` for details. Messages { messages: Vec }, /// `Status` result: how many pending messages are in this agent's inbox. Status { unread: u64 }, /// `Recent` result: newest-first inbox rows. Recent { rows: Vec }, /// `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 }, /// `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, #[serde(default, skip_serializing_if = "Option::is_none")] status_text: Option, #[serde(default, skip_serializing_if = "Option::is_none")] status_set_at: Option, #[serde(default, skip_serializing_if = "Option::is_none")] hive_name: Option, #[serde(default, skip_serializing_if = "Option::is_none")] swarm_name: Option, }, /// `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 }, /// `ListDescendants` result: all descendant containers, with running /// status. Ordered by topology depth (parents before children), then /// alphabetically within each depth tier. Containers { containers: Vec }, } /// 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, } /// 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: "", ...)` 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 = ""; /// Reserved magic recipient — `send(to: "", ...)` 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 = ""; /// 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, #[serde(default, skip_serializing_if = "Option::is_none")] sha: Option, #[serde(default, skip_serializing_if = "Option::is_none")] tag: Option, }, /// A new container was spawned. `ok = false` = spawn failed. Spawned { agent: String, ok: bool, #[serde(default, skip_serializing_if = "Option::is_none")] note: Option, #[serde(default, skip_serializing_if = "Option::is_none")] sha: Option, }, /// A container was rebuilt (auto-update or manual). Rebuilt { agent: String, ok: bool, #[serde(default, skip_serializing_if = "Option::is_none")] note: Option, #[serde(default, skip_serializing_if = "Option::is_none")] sha: Option, #[serde(default, skip_serializing_if = "Option::is_none")] tag: Option, }, /// 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, }, /// 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, #[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, /// 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, /// 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, } /// 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, /// `set_status`, `get_agent_meta` 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, /// `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", "ask", "answer"], Self::Meta => &["set_status", "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::Execution => &["run", "status"], Self::WebTools => &[], } } /// 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::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::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 => "set_status, get_agent_meta — identity and status", 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::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, } 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, ]; /// 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", } } /// 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" } } } } /// 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, pub next_fire_at_unix: i64, pub created_at_unix: i64, pub source: WireScheduleSource, #[serde(default, skip_serializing_if = "Option::is_none")] pub cancelled_at_unix: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub description: Option, pub targets: Vec, } #[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, #[serde(default, skip_serializing_if = "Option::is_none")] pub last_fired_at_unix: Option, #[serde(default, skip_serializing_if = "Option::is_none")] pub last_result: Option, }