Slack bot
Wire serveChannels to Slack webhooks with the channels-slack-bot example.
Slack bot
The channels-slack-bot.ts example shows end-to-end Slack integration: inbound mentions, streaming replies, outbound ChatToolset tools, and approval cards on mutating actions.
Prerequisites
-
Install peers:
npm install chat @chat-adapter/slack fastify -
Create a Slack app and set
SLACK_BOT_TOKENandSLACK_SIGNING_SECRETper@chat-adapter/slackdocs. -
Expose a local port through a tunnel (
ngrok,cloudflared, …) and point Slack's Event Subscriptions Request URL at:<tunnel>/api/agents/support/channels/slack/webhookPoint Interactivity at:
<tunnel>/api/agents/support/channels/slack/interaction -
Run the example:
tsx packages/agents/examples/channels-slack-bot.ts
Full wiring
import { Maniac, OpenAICompatibleModel, StaticPermissionPolicy } from "@maniac-ai/agents";
import { SqliteMemory } from "@maniac-ai/agents/memory/sqlite";
import { ChatToolset, buildFastifyHandler, serveChannels } from "@maniac-ai/agents/channels";
import { Chat } from "chat";
import { createSlackAdapter } from "@chat-adapter/slack";
import Fastify from "fastify";
const chat = new Chat({ adapters: { slack: createSlackAdapter() } });
const policy = new StaticPermissionPolicy(
[{
id: "approve-chat-writes",
principal: "*",
scope: { toolset: "chat", arg_constraints: [] },
effect: "require_approval"
}],
{ allowed: true, requires_approval: false }
);
const app = new Maniac({
model: new OpenAICompatibleModel({ slug: "gpt-4o-mini" }),
memory: new SqliteMemory({ path: "./.channels-bot.sqlite" }),
policy
});
app.agent({
id: "support",
instructions: [
"You are a Slack support bot. Be concise and friendly.",
"Use chat_* tools when asked to post, react, edit, or DM.",
"Mutating tools require human approval — explain what you intend to do."
].join(" "),
toolsets: [new ChatToolset({ chat, preset: "messenger", requireApproval: true })]
});
const server = serveChannels(app, "support", {
chat,
threadContext: { maxMessages: 10 },
inlineMedia: ["image/*"],
inlineLinks: [
{ match: "youtube.com", mimeType: "video/*" },
{ match: "youtu.be", mimeType: "video/*" }
]
});
const fastify = Fastify({ logger: true });
const handler = buildFastifyHandler(server);
fastify.post("/api/agents/:agentId/channels/:platform/webhook", handler);
fastify.post("/api/agents/:agentId/channels/:platform/interaction", handler);
await fastify.listen({ host: "0.0.0.0", port: 3000 });serveChannels behavior
Each inbound handle(event) call:
- Resolves
threadId/resourceIdviaThreadIdResolver - Subscribes to the platform thread and optionally backfills recent history into
prefixMessages - Prefixes group-thread messages with the speaker's display name
- Streams
Maniac.chatStreamthroughtoChatStreamintothread.post - On
status="paused", posts an approval card (or plain-text fallback whencards: false)
ChatToolset presets
ChatToolset exposes outbound chat actions to the model:
new ChatToolset({
chat,
preset: "messenger", // chat_post, chat_react, chat_edit, chat_dm, …
requireApproval: true // runner-level gate for mutating tools
});requireApproval: true pauses writes even when no explicit policy matches; an explicit require_approval policy still overrides per tool.
Webhook hardening
buildWebhookHandler (and buildFastifyHandler) support:
- Raw-byte
verifyRequestwithslackSigningSecret - Atomic
IdempotencyStore.claim(in-memory default; passnullto opt out) maxBodyBytescap → HTTP 413- SHA-256 body-hash dedup keys
agentIdURL enforcementThreadIdResolution.platformThreadIdfor native thread lookup
Multimodal inbound
Image and file parts flow through to the model as ContentParts — Maniac.chat / chatStream accept string | MessageContent, not flattened text. Configure inlineMedia and inlineLinks on serveChannels to control which attachments become model-visible parts.