hyperhive/hive-sh4re/src/lib.rs

945 lines
38 KiB
Rust

//! Wire types shared between `hive-c0re` and the in-container harness.
use serde::{Deserialize, Serialize};
pub mod assets;
// -----------------------------------------------------------------------------
// 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,
},
/// 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<String>,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct HostResponse {
pub ok: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub agents: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub approvals: Option<Vec<Approval>>,
}
/// 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/<id>`. 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<String>,
pub requested_at: i64,
pub status: ApprovalStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub resolved_at: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub note: Option<String>,
/// Free-text description the manager attached at submission time;
/// shown on the dashboard approval card.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
/// What action the approval, when granted, will trigger.
/// Variant-specific payload encoding + flow lives in
/// `docs/approvals.md::Approval kinds (wire shapes)`.
#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum ApprovalKind {
/// Apply a manager-proposed config commit.
#[default]
ApplyCommit,
/// Create + start a new sub-agent container with the given name
/// (under the default `agent.nix` template).
Spawn,
/// Seed an agent's proposed config repo for manager customisation
/// (step 1 of the two-step spawn flow).
InitConfig,
/// Run `nix flake update [inputs...]` on the meta flake and commit
/// the resulting lock changes.
UpdateMetaInputs,
/// Add a scheduled prompt to the broker queue.
SchedulePrompt,
}
#[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<String>) -> Self {
Self {
ok: false,
error: Some(message.into()),
agents: None,
approvals: None,
}
}
#[must_use]
pub fn list(agents: Vec<String>) -> Self {
Self {
ok: true,
error: None,
agents: Some(agents),
approvals: None,
}
}
#[must_use]
pub fn pending(approvals: Vec<Approval>) -> Self {
Self {
ok: true,
error: None,
agents: None,
approvals: Some(approvals),
}
}
}
// -----------------------------------------------------------------------------
// Per-agent socket — /run/hyperhive/agents/<name>/mcp.sock on the host,
// bind-mounted into the container at /run/hive/mcp.sock.
// -----------------------------------------------------------------------------
/// A logical message between agents.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Message {
pub from: String,
pub to: String,
pub body: String,
/// Optional broker row-id of the message this is a reply to.
/// Stored in the DB and echoed back on `Recv` so the dashboard can
/// render conversation threads. `None` for messages that start a
/// new thread. Ignored if the referenced id is unknown or out of
/// retention — purely advisory.
pub in_reply_to: Option<i64>,
}
/// One row of a broker inbox query — what the dashboard renders in
/// its operator-inbox section and what a per-agent web UI returns
/// from a `Recent` request. Lives in `hive_sh4re` so it can travel
/// over both the dashboard's `/api/state` and the agent socket
/// without an internal-to-wire conversion.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InboxRow {
pub id: i64,
pub from: String,
pub body: String,
pub at: i64,
/// Row-id of the message this is a reply to, if any.
#[serde(skip_serializing_if = "Option::is_none")]
pub in_reply_to: Option<i64>,
}
/// One delivered message in a `Recv` response.
/// See `docs/conventions.md::Broker delivery + ack cycle` for the
/// full delivery/ack/requeue story.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DeliveredMessage {
pub from: String,
pub body: String,
/// Broker row id, tracked by the harness for `AckTurn`. Opaque to
/// claude. `default` for wire backwards-compat.
#[serde(default)]
pub id: i64,
/// `true` if this row was resurfaced by `RequeueInflight` (previously
/// popped, never acked). Formatter prepends a "may already be handled"
/// hint when set.
#[serde(default)]
pub redelivered: bool,
/// Row-id of the message this is a reply to, if any.
#[serde(skip_serializing_if = "Option::is_none")]
pub in_reply_to: Option<i64>,
}
/// Reminder timing: either relative (wait N seconds) or absolute (at unix
/// timestamp).
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "timing_type", rename_all = "snake_case")]
pub enum ReminderTiming {
/// Remind after this many seconds from now.
InSeconds { seconds: u64 },
/// Remind at this unix timestamp (seconds since epoch).
At { unix_timestamp: i64 },
}
/// One row in the response to `GetLooseEnds`. Tagged enum so new
/// thread kinds can land without breaking existing handlers.
/// Per-flavour scoping + per-variant fields + clock-anomaly
/// saturation behaviour live in
/// `docs/conventions.md::Loose-ends wire shape`.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum LooseEnd {
/// A pending approval row.
Approval {
id: i64,
agent: String,
commit_ref: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
description: Option<String>,
age_seconds: u64,
},
/// An unanswered question row.
Question {
id: i64,
asker: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
target: Option<String>,
question: String,
age_seconds: u64,
},
/// A scheduled but un-delivered reminder row.
Reminder {
id: i64,
owner: String,
message: String,
due_at: i64,
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,
}
/// Requests on a per-agent socket. The agent's identity is the socket
/// it came in on; `Send.from` is filled in by the server, not the client.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "cmd", rename_all = "snake_case")]
pub enum AgentRequest {
/// 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.
Wake { from: String, body: String },
/// Last `limit` messages addressed to this agent, newest-first.
/// Non-mutating — pulls from the broker without delivering. The
/// per-agent web UI uses this to render its own inbox section.
Recent { limit: u64 },
/// Surface a question to either the operator or another agent.
/// Routing + shape: see
/// `docs/conventions.md::Question routing (Ask / Answer)`.
Ask {
question: String,
#[serde(default)]
options: Vec<String>,
#[serde(default)]
multi: bool,
#[serde(default)]
ttl_seconds: Option<u64>,
#[serde(default)]
to: Option<String>,
},
/// Answer a question previously routed to this agent via
/// `HelperEvent::QuestionAsked`. Authorised callers + threading
/// back via `HelperEvent::QuestionAnswered`: see
/// `docs/conventions.md::Question routing (Ask / Answer)`.
Answer { id: i64, answer: String },
/// Schedule a reminder message to be delivered to this agent at a
/// future time. The reminder lands in the agent's inbox as an auto-sent
/// message from `"reminder"`. Use for agent follow-ups (e.g. check task
/// status, retry failed operation). Message length is limited; pass
/// `file_path` to store in a file and get a path-reference message
/// instead.
Remind {
message: String,
timing: ReminderTiming,
#[serde(default)]
file_path: Option<String>,
},
/// Loose-ends view: every pending row against THIS agent.
/// Per-flavour scoping in
/// `docs/conventions.md::Loose-ends wire shape`.
GetLooseEnds,
/// Count of this agent's pending (un-delivered) reminders. Used
/// by the harness's per-turn stats sink to snapshot "what was
/// queued at turn-end time" without paying for a full list.
CountPendingReminders,
/// Reminder statistics for this agent: counts of scheduled, delivered,
/// and pending reminders over a time window. Used by the stats page
/// to display reminder activity. `since_secs` filters to reminders
/// created in the last N seconds (0 = all reminders).
ReminderRollup {
/// Only count reminders created in the last N seconds from now.
/// Pass 0 to include all reminders.
#[serde(default)]
since_secs: u64,
},
/// Set a free-text status string visible on the dashboard. Persisted
/// to `{state_dir}/hyperhive-status` so it survives harness restarts.
/// Pass an empty string to clear the status.
SetStatus { text: String },
/// Fetch identity + status for an agent. `name = None` =
/// self-introspection; `Some(<agent>)` = target query. See
/// `docs/conventions.md::Agent metadata`.
GetAgentMeta {
#[serde(default, skip_serializing_if = "Option::is_none")]
name: Option<String>,
},
/// Cancel an open thread the agent owns. Authorisation +
/// per-kind semantics in
/// `docs/conventions.md::Loose-ends wire shape`.
CancelLooseEnd { kind: CancelLooseEndKind, id: i64 },
/// 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,
}
/// Responses on a per-agent socket.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum AgentResponse {
/// `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<DeliveredMessage> },
/// `Status` result: how many pending messages are in this agent's inbox.
Status { unread: u64 },
/// `Recent` result: newest-first inbox rows.
Recent { rows: Vec<InboxRow> },
/// `Ask` result: the queued question id. The answer lands later
/// as `HelperEvent::QuestionAnswered` in this agent's inbox.
QuestionQueued { id: i64 },
/// `GetLooseEnds` result: list of loose ends pending against
/// this agent. Ordered newest-first within each kind.
LooseEnds { loose_ends: Vec<LooseEnd> },
/// `CountPendingReminders` result.
PendingRemindersCount { count: u64 },
/// `ReminderRollup` result: reminder activity stats for the agent.
ReminderRollup(ReminderStats),
/// `GetAgentMeta` result. Per-field semantics + serde defaults
/// live in `docs/conventions.md::Agent metadata`.
AgentMeta {
name: String,
role: 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>,
},
}
/// 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 = "manager";
/// Logical name the broker uses for the human operator. Messages with
/// `to = OPERATOR_RECIPIENT` accumulate in sqlite and surface on the
/// dashboard's inbox view — they are never `recv`'d by an agent harness.
pub const OPERATOR_RECIPIENT: &str = "operator";
/// Reserved magic recipient — `send(to: "<parent>", ...)` is rewritten
/// by hive-c0re at delivery time to whoever `topology::parent_of(sender)`
/// returns, or to [`OPERATOR_RECIPIENT`] when the sender is a root agent
/// (no parent). Lets agents address their parent without hardcoding the
/// label, so runtime reparenting requires no agent-side restart. The
/// angle brackets are not valid in agent names (validators reject
/// `<`/`>`), so this name can never collide with a real recipient.
pub const PARENT_RECIPIENT: &str = "<parent>";
/// Sender hive-c0re uses for events it pushes into the manager's inbox.
/// Manager harness recognises this and parses the body as a `HelperEvent`.
pub const SYSTEM_SENDER: &str = "system";
/// Out-of-band events the host-side daemon pushes to the manager's inbox.
/// Serialised as JSON in `Message::body` (sender = `SYSTEM_SENDER`).
/// Per-variant triggers + the optional `sha`/`tag` semantics live in
/// `docs/approvals.md::Helper events to the manager`.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "event", rename_all = "snake_case")]
pub enum HelperEvent {
/// An approval transitioned to a terminal state.
ApprovalResolved {
id: i64,
agent: String,
commit_ref: String,
status: ApprovalStatus,
#[serde(default, skip_serializing_if = "Option::is_none")]
note: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
sha: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
tag: Option<String>,
},
/// A new container was spawned. `ok = false` = spawn failed.
Spawned {
agent: String,
ok: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
note: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
sha: Option<String>,
},
/// A container was rebuilt (auto-update or manual).
Rebuilt {
agent: String,
ok: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
note: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
sha: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
tag: Option<String>,
},
/// A new agent's proposed config repo was seeded post-`InitConfig`.
ConfigReady { agent: String },
/// A sub-agent's container was stopped (systemd unit down; state kept).
Killed { agent: String },
/// A sub-agent's container was torn down (state dirs preserved by default).
Destroyed { agent: String },
/// A sub-agent's container has no claude session yet.
NeedsLogin { agent: String },
/// A sub-agent just completed claude login.
LoggedIn { agent: String },
/// A sub-agent's recorded flake rev is stale relative to hyperhive.
NeedsUpdate { agent: String },
/// Container exited without an operator-initiated stop (crash).
ContainerCrash {
agent: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
note: Option<String>,
},
/// A question queued via `Ask` was answered. `id` matches the
/// originating `QuestionQueued.id`; `answerer` is `"operator"` /
/// a peer agent name / `"ttl-watchdog"` on expiry.
QuestionAnswered {
id: i64,
question: String,
answer: String,
answerer: String,
},
/// A peer (or the manager) asked this agent a question. Recipient
/// replies via `Answer { id, answer }`; the answer routes back to
/// the asker as `QuestionAnswered`.
QuestionAsked {
id: i64,
asker: String,
question: String,
#[serde(default)]
options: Vec<String>,
#[serde(default)]
multi: bool,
},
}
/// Requests on the manager socket. Manager has the agent surface (send/recv)
/// plus privileged lifecycle verbs.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "cmd", rename_all = "snake_case")]
pub enum ManagerRequest {
Send {
to: String,
body: String,
/// Optional id of the message being replied to. Mirror of
/// `AgentRequest::Send.in_reply_to`; see that doc.
#[serde(default, skip_serializing_if = "Option::is_none")]
in_reply_to: Option<i64>,
},
/// Same shape as `AgentRequest::Recv` — caller-tunable
/// `wait_seconds` (capped at 60s server-side, default 30s when
/// None) for first-message long-poll, plus `max` (default 1, cap
/// 32) to drain up to N popped rows in one round-trip.
Recv {
#[serde(default)]
wait_seconds: Option<u64>,
#[serde(default)]
max: Option<u32>,
},
/// Non-mutating: pending message count, used to render a status line
/// after each MCP tool call (mirrors `AgentRequest::Status`).
Status,
/// Operator-injected message TO the manager (from the manager's own web
/// UI). Same shape as `AgentRequest::OperatorMsg`.
OperatorMsg { body: String },
/// Last `limit` messages addressed to the manager, newest-first.
/// Non-mutating; mirror of `AgentRequest::Recent`.
Recent { limit: u64 },
/// Initialise a brand-new agent's proposed config repo and queue an
/// approval for the operator to review. On approval hive-c0re seeds
/// `/agents/<name>/config/` with the default `agent.nix` template,
/// giving the manager RW access so it can customise the config and
/// commit changes. After the `ConfigReady` event arrives, edit
/// `agent.nix`, commit, and call `request_apply_commit` — which
/// creates the container on the first deploy. Fails if a proposed
/// repo for this name already exists (use `request_apply_commit` to
/// update an existing agent's config).
RequestInitConfig {
name: String,
/// Optional description shown on the dashboard approval card.
#[serde(default, skip_serializing_if = "Option::is_none")]
description: Option<String>,
},
/// Stop a sub-agent (graceful).
Kill { name: String },
/// Start a previously-stopped sub-agent container.
Start { name: String },
/// Restart a sub-agent container (stop + start).
Restart { name: String },
/// Rebuild a sub-agent: re-applies the current hyperhive flake +
/// agent.nix, restarts the container. No approval required —
/// it's idempotent and the manager owns its own update cadence.
Update { name: String },
/// Submit a config commit for the user to approve. `commit_ref` must
/// be a commit sha (7-40 hex chars, short or full) in the agent's
/// proposed config repo — a branch or tag name is rejected so the
/// approval pins an immutable commit. On approval the host applies
/// the change via `nixos-container update`.
RequestApplyCommit {
agent: String,
commit_ref: String,
/// Optional description shown on the dashboard approval card so the
/// operator knows what the change does without opening the diff.
#[serde(default, skip_serializing_if = "Option::is_none")]
description: Option<String>,
},
/// Surface a question to either the operator or another agent.
/// Manager-flavour mirror of `AgentRequest::Ask` — routing + shape
/// docs in `docs/conventions.md::Question routing (Ask / Answer)`.
Ask {
question: String,
#[serde(default)]
options: Vec<String>,
#[serde(default)]
multi: bool,
#[serde(default)]
ttl_seconds: Option<u64>,
#[serde(default)]
to: Option<String>,
},
/// Answer a question previously routed to the manager via
/// `HelperEvent::QuestionAsked`. Mirror of `AgentRequest::Answer`;
/// see `docs/conventions.md::Question routing (Ask / Answer)`.
Answer { id: i64, answer: String },
/// Fetch recent journal lines for a sub-agent container. `agent`
/// is the logical agent name; hive-c0re resolves it to the
/// machine name (`gui` → `h-gui`) and runs `journalctl -M
/// <machine> -n <lines> --no-pager`, returning the output as a
/// string. Useful for diagnosing MCP registration failures,
/// startup crashes, and harness errors.
///
/// `lines` defaults to 50 when omitted.
GetLogs {
agent: String,
#[serde(default)]
lines: Option<u32>,
},
/// Mirror of `AgentRequest::Remind` on the manager surface — schedule
/// a reminder addressed to the manager itself. Same semantics: body
/// soft-caps at 4 KiB, oversize bodies auto-persist to
/// `/state/reminders/auto-<ts>.md` (the manager container's own state
/// mount) and the inbox sees a pointer.
Remind {
message: String,
timing: ReminderTiming,
#[serde(default)]
file_path: Option<String>,
},
/// Loose-ends view for the manager surface. The optional `agent`
/// field selects scope:
/// - `None` — the manager's own loose ends: approvals it
/// submitted + questions where it is asker/target + its own
/// pending reminders. This is the default.
/// - `Some("*")` — hive-wide: EVERY pending approval, unanswered
/// question, and pending reminder across the swarm.
/// - `Some("<name>")` — that specific agent's loose ends.
GetLooseEnds {
#[serde(default)]
agent: Option<String>,
},
/// Count of pending reminders. `agent` selects whose: `None` =
/// the manager's own, `Some("<name>")` = that agent's. Mirror of
/// `AgentRequest::CountPendingReminders` on the manager surface.
CountPendingReminders {
#[serde(default)]
agent: Option<String>,
},
/// Reminder statistics: counts of scheduled, delivered, and pending
/// reminders (manager-flavour). Mirror of `AgentRequest::ReminderRollup`.
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 manager's own,
/// `Some("<name>")` = that agent's.
#[serde(default)]
agent: Option<String>,
},
/// Mirror of `AgentRequest::SetStatus` on the manager surface.
SetStatus { text: String },
/// Mirror of `AgentRequest::GetAgentMeta` on the manager surface.
/// See `docs/conventions.md::Agent metadata`.
GetAgentMeta {
#[serde(default, skip_serializing_if = "Option::is_none")]
name: Option<String>,
},
/// Cancel an open thread (question or reminder). Manager surface
/// can cancel any row (no owner check) — same dispatch as
/// `AgentRequest::CancelLooseEnd` but with privileged auth.
CancelLooseEnd { kind: CancelLooseEndKind, id: i64 },
/// Mirror of `AgentRequest::AckTurn` on the manager surface — fired
/// by the manager harness after `TurnOutcome::Ok` to close out
/// every message popped during the turn.
AckTurn,
/// Mirror of `AgentRequest::RequeueInflight` on the manager
/// surface — fired exactly once on manager harness boot.
RequeueInflight,
/// Mirror of `AgentRequest::Wake` on the manager surface. See
/// `docs/conventions.md::Wake injection`.
Wake { from: String, body: String },
/// Queue an approval to run `nix flake update [inputs...]` on the
/// meta flake. `inputs` is the list of named inputs to update
/// (e.g. `["bitburner-agent", "nixpkgs"]`). Pass an empty list to
/// update ALL inputs. On operator approval hive-c0re runs the lock
/// update and commits the result. The `UpdateMetaInputs` approval
/// resolves with `ApprovalResolved` in the manager inbox.
RequestUpdateMetaInputs {
#[serde(default)]
inputs: Vec<String>,
/// Optional description shown on the dashboard approval card.
#[serde(default, skip_serializing_if = "Option::is_none")]
description: Option<String>,
},
/// Queue an approval to add a scheduled prompt. The requester
/// (caller of this request) is recorded as the schedule owner; on
/// operator approval hive-c0re inserts the schedule and the worker
/// fans the body out at fire time. Even agent-self schedules go
/// through approval — the existing `remind` MCP tool is the
/// unapproved self-wake path.
RequestSchedulePrompt(SchedulePromptPayload),
/// Cancel a scheduled prompt. `targets = None` cancels the whole
/// schedule; `Some(list)` cancels just those recipients,
/// auto-cancelling the parent when no active targets remain.
/// Authorization: manager can cancel its own schedules + any
/// sub-agent schedules (i.e. owner reachable via topology); the
/// operator surface bypasses this check.
CancelSchedule {
id: i64,
#[serde(default, skip_serializing_if = "Option::is_none")]
targets: Option<Vec<String>>,
},
/// List every schedule in the queue. Manager-side this is
/// unfiltered — the dashboard does the topology-filter for the
/// per-agent view.
ListSchedules,
/// Fire a scheduled prompt out of band. Runs the per-target
/// fan-out once immediately without touching
/// `next_fire_at_unix` on recurring schedules; one-shots are
/// consumed by the manual fire. Authorization mirrors
/// `CancelSchedule`: the manager can fire its own schedules and
/// any owned by a sub-agent in its subtree per topology.json;
/// the operator surface bypasses the check.
FireScheduleNow { id: i64 },
/// Edit an existing schedule's mutable fields. Partial PATCH
/// semantics: `None` / missing JSON key = leave alone,
/// `Some(_)` = set. `interval_seconds` and `description` are
/// doubly-wrapped so `Some(None)` (set explicit null) can flip
/// a recurring schedule back to one-shot / clear the
/// description, while plain `None` keeps the current value.
/// `targets_add` / `targets_remove` mutate the recipient list
/// in the same transaction; re-adding a previously-cancelled
/// target drops the tombstone (replace-on-conflict). Refuses
/// cancelled rows. Authorization mirrors `CancelSchedule`.
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>>,
},
}
/// 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 + `hm1nd` allowed.
pub targets: Vec<String>,
/// Message body delivered to each target's inbox at fire time.
/// Same size budget as `Send.body` — soft cap at the broker level.
pub body: String,
/// Absolute unix timestamp (seconds) for the FIRST fire. For
/// recurring schedules the worker then re-arms in
/// `interval_seconds` steps.
pub first_fire_at_unix: i64,
/// `None` = one-shot. `Some(n > 0)` = recurring every `n` seconds.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub interval_seconds: Option<u64>,
/// Optional description shown on the dashboard approval card AND
/// stored on the resulting schedule row for the operator's
/// "what is this?" reference later.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
/// Schedule row shape on the wire — mirror of
/// `scheduled_prompts::Schedule` but in the public crate so the
/// dashboard and agent surfaces can deserialize without depending
/// on hive-c0re-internal types. Kept structurally identical to the
/// in-process type; the conversion is field-by-field in
/// `manager_server` / `dashboard`.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WireSchedule {
pub id: i64,
pub owner: String,
pub body: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub interval_seconds: Option<u64>,
pub next_fire_at_unix: i64,
pub created_at_unix: i64,
pub source: WireScheduleSource,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cancelled_at_unix: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub targets: Vec<WireScheduleTarget>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum WireScheduleSource {
Operator,
Approval { id: i64 },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WireScheduleTarget {
pub target: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cancelled_at_unix: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_fired_at_unix: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub last_result: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ManagerResponse {
Ok,
Err {
message: String,
},
/// Mirror of `AgentResponse::Messages` on the manager surface.
/// Always-list shape: 0..=max popped rows, FIFO-ordered. Carries
/// per-row `id` + `redelivered` so the manager harness drives the
/// same ack + requeue-with-hint flow as a sub-agent.
Messages {
messages: Vec<DeliveredMessage>,
},
Status {
unread: u64,
},
/// Result of `Ask`: the queued question id. The actual answer
/// arrives later as a `HelperEvent::QuestionAnswered` in the
/// asker's inbox, so this returns immediately rather than blocking
/// the turn.
QuestionQueued {
id: i64,
},
/// `Recent` result: mirror of `AgentResponse::Recent`.
Recent {
rows: Vec<InboxRow>,
},
/// `GetLogs` result: journal lines for the requested container.
Logs {
content: String,
},
/// `ListSchedules` result. Snapshot of every schedule (active +
/// cancelled-but-not-yet-reaped); the dashboard does the
/// per-agent topology filter on top.
Schedules {
schedules: Vec<WireSchedule>,
},
/// `GetLooseEnds` result: hive-wide loose ends (approvals +
/// unanswered questions). Same `LooseEnd` variants as the
/// agent surface; the manager's view is unfiltered.
LooseEnds {
loose_ends: Vec<LooseEnd>,
},
/// `CountPendingReminders` result.
PendingRemindersCount {
count: u64,
},
/// `ReminderRollup` result: reminder activity stats for the manager.
ReminderRollup(ReminderStats),
/// Mirror of `AgentResponse::AgentMeta` on the manager surface.
/// See `docs/conventions.md::Agent metadata`.
AgentMeta {
name: String,
role: 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>,
},
}