Closes the #3110 split — lib.rs is now just the crate doc comment and the pub mod list. journal.rs's new doc comment fixes a pre-existing bug: the old JournalPriority doc text in lib.rs was actually half Capability's doc (a leftover from an earlier reorder that moved the code but not the comment above it).
323 lines
16 KiB
Rust
323 lines
16 KiB
Rust
//! 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::container::{ContainerInfo, MatrixIdentity};
|
|
use hive_sh4re::inbox::{CancelLooseEndKind, DeliveredMessage, InboxRow, LooseEnd};
|
|
use hive_sh4re::journal::JournalPriority;
|
|
use hive_sh4re::manager::SchedulePromptPayload;
|
|
use hive_sh4re::schedule::WireSchedule;
|
|
use hive_types::Ident;
|
|
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.
|
|
#[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<Ident>,
|
|
},
|
|
/// 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 },
|
|
/// 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>,
|
|
},
|
|
/// 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<Ident>,
|
|
},
|
|
/// 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)* 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>>,
|
|
},
|
|
}
|
|
|
|
/// Unified response enum for both agent and manager sockets. Privileged
|
|
/// variants (`Logs`, `Schedules`) are never returned on agent sockets.
|
|
#[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.
|
|
/// `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<DeliveredMessage>,
|
|
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<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> },
|
|
/// `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,
|
|
}
|