177 lines
7.1 KiB
Rust
177 lines
7.1 KiB
Rust
//! Wire types for the daemon ↔ stdio-MCP-bridge unix socket protocol.
|
|
//!
|
|
//! Same shape as `damocles-daemon`'s `DaemonRequest`/`DaemonResponse`
|
|
//! (which this is forked from in spirit). Each request is a single
|
|
//! JSON line; each response is a single JSON line back. The stdio MCP
|
|
//! bridge holds a fresh connection per tool call — claude's tool
|
|
//! lifecycle is shorter than a persistent matrix-sdk Client wants to
|
|
//! live, so the daemon stays alive and the MCP reconnects per call.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
/// Request from the stdio MCP bridge to the daemon. The MCP bridge
|
|
/// owns the on-wire shape claude sees; this enum is the internal
|
|
/// shape the daemon dispatches over.
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
#[serde(tag = "method")]
|
|
pub enum DaemonRequest {
|
|
/// Post a plain-text or markdown message to a room. `room` accepts
|
|
/// either a matrix room id (`!abc:server`) or a canonical alias
|
|
/// (`#name:server`); the daemon resolves aliases server-side.
|
|
#[serde(rename = "send_message")]
|
|
SendMessage { room: String, body: String },
|
|
|
|
/// Open (or reuse) a DM with `user_id` and post `body`. Creates
|
|
/// the DM room if one doesn't already exist between this agent
|
|
/// and the user.
|
|
#[serde(rename = "send_dm")]
|
|
SendDm { user_id: String, body: String },
|
|
|
|
/// React to a specific event with an emoji `key`. Matrix-spec
|
|
/// `m.reaction` annotation.
|
|
#[serde(rename = "send_reaction")]
|
|
SendReaction {
|
|
room: String,
|
|
event_id: String,
|
|
key: String,
|
|
},
|
|
|
|
/// Reply to `event_id` in `room` with `body` as a threaded reply.
|
|
/// Sets the `m.in_reply_to` relation so matrix clients render the
|
|
/// thread.
|
|
#[serde(rename = "send_reply")]
|
|
SendReply {
|
|
room: String,
|
|
event_id: String,
|
|
body: String,
|
|
},
|
|
|
|
/// Mark `event_id` (in `room`) as read for this agent. Sends a
|
|
/// read receipt; bumps the room's "unread" indicator down on
|
|
/// matrix clients (and for other agents).
|
|
#[serde(rename = "mark_read")]
|
|
MarkRead { room: String, event_id: String },
|
|
|
|
/// List rooms the agent has joined. Returns each room's id +
|
|
/// canonical alias (when present) + name + member count.
|
|
#[serde(rename = "list_rooms")]
|
|
ListRooms,
|
|
|
|
/// List the members of a room. Each entry carries the matrix
|
|
/// user id + the resolved display name (when set).
|
|
#[serde(rename = "list_room_members")]
|
|
ListRoomMembers { room: String },
|
|
|
|
/// Read the last `limit` events from a room's timeline. Caller
|
|
/// gets each event's id, sender, `server_ts`, type, and body (best-
|
|
/// effort plain-text extraction from `m.text` / `m.notice` etc.).
|
|
#[serde(rename = "read_room")]
|
|
ReadRoom { room: String, limit: Option<usize> },
|
|
|
|
/// List rooms this agent has been invited to but not yet joined.
|
|
/// Returns each room's id, canonical alias (when present), and
|
|
/// display name.
|
|
#[serde(rename = "list_invites")]
|
|
ListInvites,
|
|
|
|
/// Join a room by id (`!abc:server`) or alias (`#name:server`).
|
|
/// Accepts a pending invite if one exists; also joins public rooms
|
|
/// the agent hasn't been explicitly invited to. After joining the
|
|
/// room will appear in `list_rooms`.
|
|
#[serde(rename = "join_room")]
|
|
JoinRoom { room: String },
|
|
|
|
/// Resolve a pending invite to `room` (id or alias) by either
|
|
/// accepting it (join) or rejecting it (decline + leave). For rooms
|
|
/// you were *invited* to; `join_room` is the path for joining a
|
|
/// public room you weren't invited to.
|
|
#[serde(rename = "resolve_invite")]
|
|
ResolveInvite { room: String, action: InviteAction },
|
|
|
|
/// Invite `user_id` (`@user:server`) to `room` (id or alias). The
|
|
/// calling agent must already be a member with a high enough power
|
|
/// level to invite. Idempotent-ish: inviting an already-joined or
|
|
/// already-invited user surfaces the matrix error from the server.
|
|
#[serde(rename = "invite_user")]
|
|
InviteUser { room: String, user_id: String },
|
|
|
|
/// Return the count of rooms with unread notifications. Used by
|
|
/// the harness `get_loose_ends` to surface unread matrix activity
|
|
/// without exposing message content.
|
|
#[serde(rename = "unread_count")]
|
|
UnreadCount,
|
|
|
|
/// Return per-room unread summaries. For rooms with exactly one
|
|
/// unread notification, attempts to include the sender + truncated
|
|
/// body; rooms with multiple unreads carry only the count. Used by
|
|
/// `get_loose_ends` and the wake-signal formatter.
|
|
#[serde(rename = "unread_summary")]
|
|
UnreadSummary,
|
|
|
|
/// Liveness probe — fast "are you up?" round-trip that doesn't
|
|
/// touch matrix-sdk. Not used by the in-tree stdio MCP bridge
|
|
/// (which surfaces a daemon-down condition as a normal tool-call
|
|
/// connect error); reserved for external clients that want an
|
|
/// explicit health check without doing real work.
|
|
#[serde(rename = "ping")]
|
|
Ping,
|
|
}
|
|
|
|
/// Whether to accept or reject a pending invite in
|
|
/// [`DaemonRequest::ResolveInvite`]. Serialises as `"accept"` /
|
|
/// `"reject"` on the wire.
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum InviteAction {
|
|
/// Accept the invite — join the room (same effect as `join_room`).
|
|
Accept,
|
|
/// Reject the invite — decline it and leave the room.
|
|
Reject,
|
|
}
|
|
|
|
/// One entry in the [`DaemonRequest::UnreadSummary`] response payload.
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
pub struct RoomUnread {
|
|
/// Canonical alias (`#name:server`) or room id (`!id:server`).
|
|
pub label: String,
|
|
/// Server-side push-notification count for this room. Always ≥ 1.
|
|
pub count: u32,
|
|
/// Truncated body of the last message in the room. Present only when
|
|
/// `count == 1` and the fetch succeeded; absent otherwise.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub last_body: Option<String>,
|
|
/// Sender of the last message (`@user:server`). Present when
|
|
/// `last_body` is present.
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub last_sender: Option<String>,
|
|
}
|
|
|
|
/// Response shape: `ok` carries the payload (any JSON; the MCP bridge
|
|
/// passes it back to claude as the tool result), `error` carries a
|
|
/// human-readable error string.
|
|
#[derive(Debug, Serialize, Deserialize)]
|
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
|
pub enum DaemonResponse {
|
|
Ok { payload: serde_json::Value },
|
|
Error { message: String },
|
|
}
|
|
|
|
impl DaemonResponse {
|
|
/// Convenience: build an Ok response from any serializable value.
|
|
/// Serialisation failure (impossible for the small handler structs
|
|
/// we use, but the API is generic) surfaces as a structured
|
|
/// `{"serialise_error": "..."}` payload so callers see WHY the
|
|
/// data is missing instead of a silent `null`.
|
|
pub fn ok<T: Serialize>(payload: &T) -> Self {
|
|
let payload = serde_json::to_value(payload)
|
|
.unwrap_or_else(|e| serde_json::json!({ "serialise_error": e.to_string() }));
|
|
Self::Ok { payload }
|
|
}
|
|
|
|
/// Convenience: build an Error response from any `Display` value.
|
|
pub fn error(msg: impl std::fmt::Display) -> Self {
|
|
Self::Error {
|
|
message: msg.to_string(),
|
|
}
|
|
}
|
|
}
|