//! Per-agent + manager socket wire types (`/run/hive/mcp.sock`). //! //! The unified `Request` / `Response` protocol spoken between an agent's //! in-container harness (and the manager) and the `hive-c0re` daemon over the //! per-agent mcp socket. Re-homed out of `hive-sh4re` so this one socket owns //! its own protocol crate, mirroring `hive-host-sock` / `hive-priv-sock`. The //! shared payload types it references (`Message`, `LooseEnd`, `Approval`, …) //! stay in `hive-sh4re`, which this crate depends on. use hive_sh4re::{ CancelLooseEndKind, ContainerInfo, DeliveredMessage, InboxRow, JournalPriority, LooseEnd, MatrixIdentity, ReminderStats, ReminderTiming, SchedulePromptPayload, WireSchedule, }; use serde::{Deserialize, Serialize}; /// serde `default` helper for `Response::AgentMeta::running` (absent = true). fn default_true() -> bool { true } /// 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. 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, #[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, }, /// Upsert a *todo* (loose-ends v2) from an in-container /// subsystem (matrix / forge / bash). `subsystem` is the producer /// marker; `key` is the optional subsystem-specific dedup key (a /// matrix room id, a bash task id). Re-pushing an identical keyed /// todo is a no-op; a new-or-changed one coalesces a wake to the /// agent. Keyless todos always insert as one-offs. UpsertTodo { subsystem: String, #[serde(default, skip_serializing_if = "Option::is_none")] key: Option, summary: String, #[serde(default, skip_serializing_if = "Option::is_none")] source: Option, }, /// Clear producer-resolved todo(s) by `(subsystem, key)`. `key = /// Some(k)` clears the one keyed row; `key = None` clears **all** of /// the subsystem's keyless todos (rows with no key can't be told /// apart — clear a specific one via `MarkTodoDone` by id). `all = /// true` wipes the producer's whole set (cancel-and-recreate on /// daemon restart). ClearTodo { subsystem: String, #[serde(default, skip_serializing_if = "Option::is_none")] key: Option, #[serde(default)] all: bool, }, /// List todos, optionally filtered to one `subsystem` (a producer /// enumerating its own set). `None` = all. ListTodos { #[serde(default, skip_serializing_if = "Option::is_none")] subsystem: Option, }, /// The agent marks one of its own todos done, by id. MarkTodoDone { id: i64 }, /// 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 }, /// 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, /// 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)* 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 (tracked by the harness /// for `AckTurn`, and surfaced to claude as a `[msg #]` marker /// so `AckUntil` has something to reference) and the "previously /// popped, not acked" flag — see `DeliveredMessage` for details. /// `remaining` is the inbox depth *after* this batch was popped — /// how many still-pending messages the caller could drain next. The /// harness surfaces it to claude ("N more pending") so an in-turn /// `recv` learns whether the inbox is drained, mirroring the count /// the wake prompt already carries. Messages { messages: Vec, remaining: u64, }, /// `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 }, /// `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, /// 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, }, /// `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 }, /// `CreateRepo` result: the new repo's full name (`agents/`) /// 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 }, /// `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;