hive-matrix-mcp: add send_redact tool to delete matrix events
This commit is contained in:
parent
79af902d47
commit
1c0f7a72f7
4 changed files with 106 additions and 3 deletions
|
|
@ -162,6 +162,20 @@ struct MarkReadArgs {
|
|||
account: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
struct SendRedactArgs {
|
||||
room: String,
|
||||
/// Full matrix event id of the message to redact.
|
||||
event_id: String,
|
||||
/// Optional human-readable reason recorded on the redaction event.
|
||||
#[serde(default)]
|
||||
reason: Option<String>,
|
||||
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`).
|
||||
/// Omit to use the agent's primary account.
|
||||
#[serde(default)]
|
||||
account: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, JsonSchema)]
|
||||
struct ListRoomsArgs {
|
||||
/// Matrix account to act as (a `name` from `hyperhive.matrixAccounts`).
|
||||
|
|
@ -399,6 +413,24 @@ impl MatrixBridge {
|
|||
)
|
||||
}
|
||||
|
||||
#[tool(description = "Redact (delete) a specific matrix event in a room — \
|
||||
asks the homeserver to strip the event's content, optionally with a \
|
||||
`reason`. Works on your own events; redacting others' needs moderator \
|
||||
power level. Irreversible.")]
|
||||
async fn send_redact(&self, Parameters(args): Parameters<SendRedactArgs>) -> String {
|
||||
render(
|
||||
call(
|
||||
args.account,
|
||||
DaemonOp::SendRedact {
|
||||
room: args.room,
|
||||
event_id: args.event_id,
|
||||
reason: args.reason,
|
||||
},
|
||||
)
|
||||
.await,
|
||||
)
|
||||
}
|
||||
|
||||
#[tool(
|
||||
description = "List rooms this agent has joined. Each row has the room \
|
||||
id, canonical alias (when set), display name, and joined-member count."
|
||||
|
|
@ -523,7 +555,8 @@ impl MatrixBridge {
|
|||
#[tool_handler(instructions = "Matrix client for an agent on a hyperhive swarm. Use \
|
||||
`send_message` to post in a joined room, `send_dm` to message a \
|
||||
specific user, `send_reaction` to react with an emoji, `send_reply` \
|
||||
to thread a reply, `mark_read` to acknowledge an event. Discover \
|
||||
to thread a reply, `mark_read` to acknowledge an event, `send_redact` \
|
||||
to delete an event. Discover \
|
||||
rooms with `list_rooms`, members with `list_room_members`, recent \
|
||||
timeline with `read_room`. See pending invites with `list_invites`; \
|
||||
accept or reject an invite with `resolve_invite`; join a public \
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@
|
|||
//!
|
||||
//! Tool surface mirrors damocles-daemon's v0 set per the operator's call:
|
||||
//! `send_message`, `send_dm`, `send_reaction`, `send_reply`, `mark_read`,
|
||||
//! `list_rooms`, `list_room_members`, `read_room`. Plus a `ping` for the
|
||||
//! MCP bridge's liveness probe.
|
||||
//! `send_redact`, `list_rooms`, `list_room_members`, `read_room`. Plus a
|
||||
//! `ping` for the MCP bridge's liveness probe.
|
||||
|
||||
use matrix_sdk::{
|
||||
Client,
|
||||
|
|
@ -407,6 +407,32 @@ pub async fn mark_read(client: &Client, room_ref: &str, event_id: &str) -> Daemo
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn send_redact(
|
||||
client: &Client,
|
||||
room_ref: &str,
|
||||
event_id: &str,
|
||||
reason: Option<&str>,
|
||||
) -> DaemonResponse {
|
||||
let room = match resolve_room(client, room_ref).await {
|
||||
Ok(r) => r,
|
||||
Err(e) => return e,
|
||||
};
|
||||
let eid: OwnedEventId = match event_id.parse() {
|
||||
Ok(e) => e,
|
||||
Err(e) => return DaemonResponse::error(format!("invalid event_id {event_id}: {e}")),
|
||||
};
|
||||
// Redaction is irreversible: the homeserver drops the event content and
|
||||
// keeps only the shell + reason. A fresh txn id (None) is fine — this is
|
||||
// a one-shot operator/agent action, not a retried send.
|
||||
match room.redact(&eid, reason, None).await {
|
||||
Ok(resp) => DaemonResponse::ok(&serde_json::json!({
|
||||
"event_id": resp.event_id.to_string(),
|
||||
"target": eid.to_string(),
|
||||
})),
|
||||
Err(e) => DaemonResponse::error(format!("redact {eid}: {e}")),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn list_rooms(client: &Client) -> DaemonResponse {
|
||||
let mut rooms = Vec::new();
|
||||
for room in client.joined_rooms() {
|
||||
|
|
|
|||
|
|
@ -92,6 +92,18 @@ pub enum DaemonOp {
|
|||
#[serde(rename = "mark_read")]
|
||||
MarkRead { room: String, event_id: String },
|
||||
|
||||
/// Redact `event_id` in `room` — ask the homeserver to strip the
|
||||
/// event's content (matrix-spec `m.room.redaction`), optionally with
|
||||
/// a human-readable `reason`. The agent must have a high enough power
|
||||
/// level (its own events, or moderator rights for others'); the
|
||||
/// server rejects otherwise.
|
||||
#[serde(rename = "send_redact")]
|
||||
SendRedact {
|
||||
room: String,
|
||||
event_id: String,
|
||||
reason: Option<String>,
|
||||
},
|
||||
|
||||
/// List rooms the agent has joined. Returns each room's id +
|
||||
/// canonical alias (when present) + name + member count.
|
||||
#[serde(rename = "list_rooms")]
|
||||
|
|
@ -267,6 +279,33 @@ mod tests {
|
|||
matches!(back.op, DaemonOp::ListRooms);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn send_redact_round_trips_with_optional_reason() {
|
||||
let req = DaemonRequest {
|
||||
account: None,
|
||||
op: DaemonOp::SendRedact {
|
||||
room: "!r:s".to_owned(),
|
||||
event_id: "$e".to_owned(),
|
||||
reason: Some("spam".to_owned()),
|
||||
},
|
||||
};
|
||||
let line = serde_json::to_string(&req).unwrap();
|
||||
assert!(line.contains("\"method\":\"send_redact\""), "wire: {line}");
|
||||
let back: DaemonRequest = serde_json::from_str(&line).unwrap();
|
||||
match back.op {
|
||||
DaemonOp::SendRedact {
|
||||
room,
|
||||
event_id,
|
||||
reason,
|
||||
} => {
|
||||
assert_eq!(room, "!r:s");
|
||||
assert_eq!(event_id, "$e");
|
||||
assert_eq!(reason.as_deref(), Some("spam"));
|
||||
}
|
||||
other => panic!("wrong variant: {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unit_variant_op_parses_inside_envelope() {
|
||||
// A bare op with no fields still parses when wrapped.
|
||||
|
|
|
|||
|
|
@ -99,6 +99,11 @@ async fn dispatch_op(op: DaemonOp, client: &Client) -> DaemonResponse {
|
|||
DaemonOp::MarkRead { room, event_id } => {
|
||||
handlers::mark_read(client, &room, &event_id).await
|
||||
}
|
||||
DaemonOp::SendRedact {
|
||||
room,
|
||||
event_id,
|
||||
reason,
|
||||
} => handlers::send_redact(client, &room, &event_id, reason.as_deref()).await,
|
||||
DaemonOp::ListRooms => handlers::list_rooms(client).await,
|
||||
DaemonOp::ListInvites => handlers::list_invites(client),
|
||||
DaemonOp::JoinRoom { room } => handlers::join_room(client, &room).await,
|
||||
|
|
|
|||
Loading…
Reference in a new issue