refactor(#2581): carve per-agent mcp.sock protocol into hive-agent-sock crate
This commit is contained in:
parent
f64ab47de0
commit
d0beec8a40
26 changed files with 603 additions and 542 deletions
|
|
@ -8,7 +8,7 @@ pub mod jobs;
|
|||
pub mod paths;
|
||||
pub mod wire_time;
|
||||
|
||||
/// Server-side hard cap on `Recv.max` (see `AgentRequest::Recv`). Bounds
|
||||
/// 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
|
||||
|
|
@ -352,401 +352,6 @@ pub struct TaskFile {
|
|||
pub stderr_tail: Option<String>,
|
||||
}
|
||||
|
||||
/// 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<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<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. 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>,
|
||||
},
|
||||
/// 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<String>,
|
||||
summary: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
source: Option<String>,
|
||||
},
|
||||
/// 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<String>,
|
||||
#[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<String>,
|
||||
},
|
||||
/// 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<String>,
|
||||
},
|
||||
/// 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("<name>")` = 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<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<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 },
|
||||
/// 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>>,
|
||||
},
|
||||
}
|
||||
|
||||
/// 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 #<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> },
|
||||
/// `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<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,
|
||||
}
|
||||
|
||||
/// Backwards-compatible response aliases.
|
||||
pub type AgentResponse = Response;
|
||||
pub type ManagerResponse = Response;
|
||||
|
||||
/// One entry in a `ListDescendants` result.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ContainerInfo {
|
||||
|
|
@ -802,13 +407,6 @@ pub struct MatrixIdentity {
|
|||
pub homeserver: 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.
|
||||
|
|
|
|||
Loading…
Reference in a new issue