Skip to content

Implement message tool — channel messaging (Discord/Matrix) #117

@rookdaemon

Description

@rookdaemon

Summary

Add a message tool that sends messages to communication channels (Discord webhooks, Matrix rooms, or generic HTTP webhooks). Currently daemon-engine has no way to proactively communicate — it can only respond to incoming requests via the HTTP gateway.

This is a P1 gap identified in the gap analysis. Without this, the agent cannot alert, notify, or communicate through channels that Rook uses daily.

Specification

Tool Name: message

Parameters

interface MessageParams {
  /** Channel identifier (matches a key in config channels section). */
  channel: string;
  /** Message content to send. */
  content: string;
}

JSON Schema (for LLM)

{
  "type": "object",
  "properties": {
    "channel": { "type": "string", "description": "Channel identifier (e.g., 'discord-general', 'matrix-ops'). Must match a configured channel." },
    "content": { "type": "string", "description": "Message content to send." }
  },
  "required": ["channel", "content"]
}

Configuration (in daemon.yaml)

channels:
  discord-general:
    type: discord-webhook
    url: ${DISCORD_WEBHOOK_URL}
  matrix-ops:
    type: matrix
    homeserver: ${MATRIX_HOMESERVER}
    room_id: ${MATRIX_ROOM_ID}
    access_token: ${MATRIX_ACCESS_TOKEN}
  alerts:
    type: webhook
    url: ${ALERT_WEBHOOK_URL}
    method: POST
    headers:
      Content-Type: application/json
    body_template: '{"text": "{{content}}"}'

Behavior

Channel Resolution:

  1. Read channels from context.config (passed via ToolContext)
  2. Look up channel parameter in config
  3. If not found → throw: "Unknown channel: {channel}. Available channels: {list}"

Channel Types:

discord-webhook:

  • POST to webhook URL with JSON body: { "content": "{content}" }
  • Content-Type: application/json
  • Truncate content to 2000 chars (Discord limit)

matrix:

  • PUT to {homeserver}/_matrix/client/v3/rooms/{room_id}/send/m.room.message/{txnId}
  • Authorization: Bearer {access_token}
  • Body: { "msgtype": "m.text", "body": "{content}" }
  • Generate txnId from timestamp

webhook (generic):

  • POST (or configured method) to URL
  • Apply body_template with {{content}} placeholder replaced
  • Use configured headers
  • Fallback body: { "content": "{content}" }
  1. On success → return: "Message sent to {channel}"
  2. On failure → throw: "Failed to send to {channel}: {status} {statusText}"

Implementation Guide

Files to Create/Modify

  1. Create src/tools/message.ts — Follow existing tool pattern:

    • Export const message: ToolDefinition<MessageParams>
    • Export messageWithEnv(params, env, config) for testability
    • Implement channel type dispatchers as private functions
    • Use env.http.fetch() for all HTTP calls
  2. Modify src/tools/registry.ts — Add to createBuiltInRegistry():

    const { message } = await import("./message.js");
    registry.register("message", message);
  3. Modify src/config.ts (or equivalent config types) — Add channels section to Config type:

    interface ChannelConfig {
      type: "discord-webhook" | "matrix" | "webhook";
      url?: string;
      homeserver?: string;
      room_id?: string;
      access_token?: string;
      method?: string;
      headers?: Record<string, string>;
      body_template?: string;
    }
    
    // Add to Config:
    channels?: Record<string, ChannelConfig>;
  4. Create test/tools/message.test.ts — Test cases (mock env.http.fetch):

    • Sends Discord webhook with correct format
    • Sends Matrix message with correct endpoint and auth
    • Sends generic webhook with body_template substitution
    • Throws on unknown channel
    • Throws on HTTP error response
    • Truncates Discord messages to 2000 chars
    • Lists available channels in error message

Key Implementation Notes

  • Use env.http.fetch() — not global fetch
  • Config access: The ToolContext already has an optional config?: Config field. The message tool needs context.config?.channels
  • No external dependencies — pure HTTP calls via Environment abstraction
  • Start simple: Discord webhook is the highest priority (Rook's primary channel). Matrix and generic webhook can be follow-up if needed, but include the abstractions now
  • Security: Never log webhook URLs or access tokens. Log only channel name and success/failure

Reference: Discord Webhook Call

async function sendDiscord(url: string, content: string, env: Environment): Promise<void> {
  const truncated = content.length > 2000 ? content.slice(0, 1997) + "..." : content;
  const response = await env.http.fetch(url, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ content: truncated }),
  });
  if (!response.ok) {
    throw new Error(`Discord webhook failed: ${response.status} ${response.statusText}`);
  }
}

Acceptance Criteria

  • src/tools/message.ts exists and exports message and messageWithEnv
  • Tool is registered as "message" in createBuiltInRegistry()
  • Discord webhook channel type works
  • Generic webhook channel type works with body_template
  • Config type updated with channels section
  • Throws descriptive error for unknown channels
  • All test cases pass
  • Uses env.http.fetch() (not global fetch)
  • No secrets logged
  • npm run build succeeds
  • npm run lint passes

Metadata

Metadata

Labels

P1High priorityenhancementNew feature or request

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions