//! Wire types shared between `hive-c0re` and the in-container harness. use chrono::{DateTime, Utc}; use hive_types::Ident; use serde::{Deserialize, Serialize}; pub mod assets; pub mod paths; pub mod wire_time; /// Server-side hard cap on `Recv.max` (see the `Recv` request). 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-agent's wake prompt + tool docs) reference one /// constant instead of a scattered magic value. pub const RECV_BATCH_MAX: u32 = 5; /// Banner prepended to a wake prompt / `recv` result when the message was /// redelivered after a harness restart (the turn that first drove it never /// acked). Shared between the harness serve loop (wake prompt) and the MCP /// server (`recv` tool result) so both surfaces phrase it identically. pub const REDELIVERY_HINT: &str = "[redelivered after harness restart — may already be handled]\n"; /// Banner prepended to a wake prompt when the previous turn was cut off by /// an explicit operator `/cancel` (SIGINT) rather than ending normally. Set /// once, read-and-cleared by the next turn's wake-prompt build — see /// `hive-agent`'s `post_cancel_turn` (sets it) and `handle_turn` (clears /// it). Lives here for the same reason as `REDELIVERY_HINT`: a single /// phrasing, not duplicated between call sites. pub const INTERRUPTED_HINT: &str = "[your previous turn was interrupted by the operator (/cancel) \ before it finished — check for new messages before resuming prior work]\n"; /// Shared "(N more message(s) pending …)" advisory appended after both the /// wake prompt body and the `recv` tool result whenever the inbox still has /// queued messages once the current message/batch is popped. Returns an empty /// string when `remaining == 0`. The leading `\n\n` separates it from the /// preceding body/message block, and the suggested `max` is clamped to the /// server-side recv cap so the hint never asks for more than one round-trip /// can deliver. One builder so the wake prompt (harness serve loop) and the /// in-turn recv result (MCP server) stay identical. #[must_use] pub fn pending_hint(remaining: u64) -> String { if remaining == 0 { return String::new(); } let batch = remaining.min(u64::from(RECV_BATCH_MAX)); format!( "\n\n({remaining} more message(s) pending in your inbox — call `mcp__hyperhive__recv` \ with `max: {batch}` to drain the next batch before acting. If the \ backlog is stale/already handled, `ack_until(up_to: )` \ clears everything up to that id in one call instead.)" ) } /// 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: Ident, #[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 `MergeConfigPr`: the /// reviewed PR head pinned at submit; if the PR head drifts off it /// before merge, hive-c0re cancels the stale approval and re-queues a /// fresh one for re-review. #[serde(default, skip_serializing_if = "Option::is_none")] pub fetched_sha: Option, pub requested_at: DateTime, 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 { /// 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 deploy tail. This is the /// sole config-change flow — a manager opens a PR on its /// `agent-configs/` repo and the operator reviews + approves it. /// `commit_ref` = PR number; `fetched_sha` = the reviewed PR head /// pinned at submit. See `docs/approvals.md`. #[default] 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::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, } // ----------------------------------------------------------------------------- // 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: Ident, 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: DateTime, 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, }, /// A dynamic, subsystem-pushed todo (loose-ends v2). Produced /// by an in-container subsystem (matrix / forge / bash) via /// `UpsertTodo`. Cleared by that subsystem (`ClearTodo`) or by the /// agent itself (`MarkTodoDone`, by `id`). Todo { id: i64, /// Producing subsystem marker (`"matrix"`, `"forge"`, `"bash"`, …). subsystem: String, /// Optional subsystem-specific key (matrix room id, bash task id). #[serde(default, skip_serializing_if = "Option::is_none")] subsystem_key: Option, summary: String, /// Optional free-text provenance (room name / task label). #[serde(default, skip_serializing_if = "Option::is_none")] source: Option, age_seconds: u64, }, } /// 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-agent` (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 `.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, pub status: TaskStatus, pub created_at: DateTime, #[serde(default, skip_serializing_if = "Option::is_none")] pub started_at: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub completed_at: Option>, #[serde(default, skip_serializing_if = "Option::is_none")] pub exit_code: Option, /// Last `SUMMARY_BYTES` of stdout (see `hive-bash-mcp` runner). #[serde(default, skip_serializing_if = "Option::is_none")] pub stdout_tail: Option, /// Last `SUMMARY_BYTES` of stderr (see `hive-bash-mcp` runner). #[serde(default, skip_serializing_if = "Option::is_none")] pub stderr_tail: Option, } /// 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 list-agents`). // // Four orthogonal, independently-observed facts about one agent, each // rendered as its own column/token by `hivectl list-agents` and read // individually by `--json` consumers. Any combination is meaningful // (a stopped agent can be paused and need an update), so folding them // into a state machine or nested flag structs would only add // `serde(flatten)` indirection to preserve the same flat JSON. Same // rationale as `LifecycleScope` in hive-host-sock. #[allow( clippy::struct_excessive_bools, reason = "flat wire projection of independent per-agent flags" )] #[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, /// Count of this agent's pending reminders. #[serde(default)] pub pending_reminders: u64, /// The agent's turn loop is parked (pause marker present in its /// harness dir): the container may well be up and serving, it just /// drives no turns. Orthogonal to `running` — an agent can be /// paused while stopped, and pause survives a restart. #[serde(default)] pub paused: bool, /// Parent in the topology tree. `None` marks a root-level agent. #[serde(default, skip_serializing_if = "Option::is_none")] pub parent: Option, } /// 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`; 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, /// Homeserver base URL this account is on. pub homeserver: String, } // ----------------------------------------------------------------------------- // 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"; /// Parse `s` as a [`Ident`] for use as `Message.from`, falling back to /// [`SYSTEM_SENDER`] on the (should-be-unreachable) case that `s` isn't /// ident-shaped. `Message.from` is always either a fixed sentinel literal /// (`SYSTEM_SENDER`, `OPERATOR_RECIPIENT`, `"scheduled"`, …) or an /// already-registered agent's own name reaching this point through /// hive-c0re's internal dispatch — never arbitrary external input — so /// this is a defensive fallback for a programming-bug case, not a /// validation gate. /// /// # Panics /// /// Never, unless [`SYSTEM_SENDER`] itself stops being ident-shaped (which /// would also be a programming bug, caught by `hive-types`' own tests). #[must_use] pub fn trusted_sender(s: &str) -> Ident { Ident::parse(s) .unwrap_or_else(|_| Ident::parse(SYSTEM_SENDER).expect("SYSTEM_SENDER is a valid Ident")) } /// 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 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, /// `get_agent_meta` (`set_status` is always-on — see `ALWAYS_ON_TOOLS`) Meta, /// `get_loose_ends`, `cancel_loose_end`, `remind` Inbox, /// `kill`, `start`, `restart`, `update` - *(privileged)* Lifecycle, /// `request_init_config`, `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"], Self::Lifecycle => &["kill", "start", "restart", "update", "list_containers"], Self::Approvals => &["request_init_config", "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. /// /// `compact` lives here too: it's pure self-management (no cross-agent /// effect, no privilege), gated server-side on context usage rather /// than on tool groups, and every agent should be able to reach for it /// regardless of which optional groups it's been granted — same /// reasoning as `set_status`. pub const ALWAYS_ON_TOOLS: &'static [&'static str] = &["set_status", "compact"]; /// 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 — self-scheduling", Self::Lifecycle => { "kill, start, restart, update, list_containers — container lifecycle (privileged)" } Self::Approvals => { "request_init_config, 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, pub next_fire_at_unix: DateTime, pub created_at_unix: DateTime, pub source: WireScheduleSource, #[serde(default, skip_serializing_if = "Option::is_none")] pub cancelled_at_unix: Option>, /// 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>, #[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, }