Research Document: TinyClaw Slack Channel Adapter
Status: Review Author: Terraphim AI Date: 2026-03-09 Reviewers: AlexMikhalev Related Issues: #519, #590
Executive Summary
TinyClaw already has a mature channel-trait pattern with Telegram and Discord adapters (merged to main via PR #527, 220 tests passing). Adding Slack requires a new SlackChannel adapter using slack-morphism (v2.18.0) with Socket Mode -- no HTTP endpoint exposure needed. The adapter follows the exact same pattern as the existing Telegram/Discord adapters: implement the Channel trait, add a feature-gated dependency, extend config, and wire into build_channels_from_config().
Essential Questions Check
| Question | Answer | Evidence | |----------|--------|----------| | Energizing? | Yes | Multi-channel reach is core TinyClaw value; user explicitly requested Slack | | Leverages strengths? | Yes | Existing Channel trait + MessageBus + format module provide 80% of the plumbing | | Meets real need? | Yes | Slack is the dominant team chat platform; #519 lists it as Phase 2+ (now is Phase 2) |
Proceed: Yes (3/3)
Problem Statement
Description
Add Slack as a channel adapter to TinyClaw so users can interact with the Terraphim AI assistant from Slack DMs and channels.
Impact
- Slack is the primary team communication tool for most engineering organizations
- Without Slack support, TinyClaw is limited to Telegram (personal), Discord (community), and CLI (developer)
- Slack integration unlocks enterprise/team use cases
Success Criteria
- SlackChannel implements the Channel trait
- Socket Mode connection handles DMs and channel mentions
- Message formatting converts markdown to Slack Block Kit / mrkdwn
- Allowlist-based access control (consistent with Telegram/Discord pattern)
- Feature-gated (
slackfeature flag) to avoid pulling dependencies when not needed - Tests pass without requiring a live Slack workspace
Current State Analysis
Existing Implementation
TinyClaw was implemented in full (PR #527, merged 2026-02-13) with 220+ tests. The crate lives at crates/terraphim_tinyclaw/ on the main branch.
NOTE: The current working branch (dependabot/github_actions/docker/build-push-action-6) does NOT contain the tinyclaw crate. Work must be done from main or a branch off main.
Code Locations
| Component | Location (on main) | Purpose |
|-----------|----------|---------|
| Channel trait | crates/terraphim_tinyclaw/src/channel.rs | Channel trait + ChannelManager + build_channels_from_config() |
| Message bus | crates/terraphim_tinyclaw/src/bus.rs | InboundMessage / OutboundMessage + tokio mpsc bus |
| Telegram adapter | crates/terraphim_tinyclaw/src/channels/telegram.rs | Reference implementation (~150 LOC) |
| Discord adapter | crates/terraphim_tinyclaw/src/channels/discord.rs | Second reference (~130 LOC) |
| Channels mod | crates/terraphim_tinyclaw/src/channels/mod.rs | Feature-gated module declarations |
| Config | crates/terraphim_tinyclaw/src/config.rs | ChannelsConfig + per-channel config structs |
| Format | crates/terraphim_tinyclaw/src/format.rs | markdown_to_telegram_html(), chunk_message() |
Data Flow
Slack message (Socket Mode WebSocket)
-> SlackChannel::start() spawned task
-> Parse event, check allowlist
-> InboundMessage::new("slack", sender_id, channel_id, text)
-> bus.inbound_sender().send(inbound)
-> [agent loop processes, produces OutboundMessage]
-> ChannelManager::send(outbound)
-> SlackChannel::send(msg)
-> Slack Web API: chat.postMessage / chat.updateIntegration Points
Channeltrait (5 methods:name,start,stop,send,is_running,is_allowed)MessageBus(tokio mpsc, 1000-capacity bounded channels)build_channels_from_config()inchannel.rs(factory function)ChannelsConfigstruct inconfig.rs(feature-gated fields)format.rsfor platform-specific formatting + chunking
Constraints
Technical Constraints
- Socket Mode preferred: No public HTTP endpoint needed (matches TinyClaw's local-first philosophy)
- slack-morphism v2.18.0: Only mature Rust Slack library; uses tokio, hyper, serde -- compatible with existing stack
- Feature-gated: Must be behind
slackfeature flag (like telegram/discord) - Slack message limit: 4000 characters per message (Block Kit sections: 3000 chars)
- Slack ack timeout: Callbacks must respond within 2-3 seconds or Slack retries; use
tokio::spawnfor heavy processing - Two tokens required:
botToken(xoxb-) for API calls +appToken(xapp-) for Socket Mode
Business Constraints
- User wants this as the next step -- prioritize working integration over completeness
- Terraphim uses 1Password for secrets management (tokens should come from env vars /
op inject)
Non-Functional Requirements
| Requirement | Target | Rationale | |-------------|--------|-----------| | Startup latency | < 3s | Socket Mode WebSocket connection | | Message delivery | < 1s | After agent loop completes | | Memory per channel | < 5MB | Consistent with other adapters | | Reconnection | Automatic | slack-morphism handles multi-connection resilience |
Vital Few (Max 3 Essential Constraints)
| Constraint | Why It's Vital | Evidence | |------------|----------------|----------| | Socket Mode (no HTTP endpoint) | TinyClaw is a local/personal assistant -- no public server | Design doc #519, local-first architecture | | Feature-gated dependency | slack-morphism pulls in hyper/tokio-tungstenite -- must not bloat default builds | Existing pattern: telegram/discord are feature-gated | | 2-3s ack deadline | Slack will retry if callbacks block; must spawn async processing | slack-morphism docs explicitly warn about this |
Eliminated from Scope
| Eliminated Item | Why Eliminated | |-----------------|----------------| | HTTP Events API mode | Socket Mode sufficient; no public endpoint needed | | Thread support | Phase 2; basic DM + channel mention first | | Slash command registration | Phase 2; use TinyClaw's existing /command routing via message text | | Block Kit rich messages | Phase 2; plain mrkdwn formatting sufficient initially | | Streaming/typing indicators | Phase 2; OpenClaw supports this but not essential for MVP | | Multi-workspace support | Phase 2; single workspace first | | Channel access policies | Phase 2; allowlist per user sufficient initially | | File/media uploads | Phase 2; text messages first |
Dependencies
Internal Dependencies
| Dependency | Impact | Risk |
|------------|--------|------|
| Channel trait (channel.rs) | Must implement 6 methods | Low -- stable, well-tested |
| MessageBus (bus.rs) | Send InboundMessage, receive OutboundMessage | Low -- stable |
| format module | Need markdown_to_slack() + chunk_message for 4000 char limit | Low -- extend existing |
| Config (config.rs) | Add SlackConfig + wire into ChannelsConfig | Low -- follows pattern |
External Dependencies
| Dependency | Version | Risk | Alternative | |------------|---------|------|-------------| | slack-morphism | 2.18.0 | Low -- actively maintained, Feb 2025 release | None in Rust ecosystem | | tokio-tungstenite | (transitive via slack-morphism) | Low | N/A |
Risks and Unknowns
Known Risks
| Risk | Likelihood | Impact | Mitigation |
|------|------------|--------|------------|
| slack-morphism API differs from docs | Low | Medium | Check examples, test with real workspace |
| Socket Mode requires Slack app with specific scopes | Medium | Low | Document required scopes in README |
| Dependency conflicts with existing crates | Low | Medium | Feature-gate; test with --all-features |
| Slack rate limits on message send | Low | Low | chunk_message already handles this pattern |
Open Questions
- Which Slack app scopes are needed? -- Research Slack App configuration docs (bot events:
app_mention,message.im,message.channels; OAuth scopes:chat:write,connections:write) - Should we support Slack thread replies from the start? -- Recommend NO for MVP; add in Phase 2
- How does slack-morphism expose message events in Socket Mode? -- Via
SlackPushEventCallbackwithSlackEventCallbackBody::Messagevariant
Assumptions Explicitly Stated
| Assumption | Basis | Risk if Wrong | Verified? | |------------|-------|---------------|-----------| | slack-morphism Socket Mode works with tokio 1.x | Docs + examples use tokio | Would need alternative library | Yes (docs confirm) | | Socket Mode handles reconnection automatically | Docs: "multiple connections per token (default: 2)" | Would need manual reconnect logic | Yes (docs) | | Slack mrkdwn is close enough to standard markdown | Slack docs | Would need custom formatter | Partially -- differs in link syntax | | Single workspace is sufficient for MVP | User request context | Would need multi-workspace support sooner | No -- confirm with user |
Multiple Interpretations Considered
| Interpretation | Implications | Why Chosen/Rejected | |----------------|--------------|---------------------| | A: Add Slack to existing terraphim_agent | Would mix TUI/REPL concerns with chat adapters | Rejected -- terraphim_agent is a CLI tool, not a chat gateway | | B: Add Slack adapter to terraphim_tinyclaw (Channel trait) | Clean separation, follows existing pattern | Chosen -- consistent with design | | C: Create standalone terraphim_slack crate | Over-engineering for a single adapter | Rejected -- adapters belong in tinyclaw | | D: Reuse SLB TS Slack client as sidecar | Would add Node.js dependency, defeats Rust-native goal | Rejected -- TinyClaw is a single binary | | E: Use HTTP Events API (like SLB) instead of Socket Mode | Requires public endpoint, reverse proxy, webhook verification | Rejected -- TinyClaw runs locally, Socket Mode is simpler |
Socket Mode vs HTTP Events API Decision
The SLB production experience provides important context:
| Factor | Socket Mode (TinyClaw) | HTTP Events API (SLB) | |--------|----------------------|----------------------| | Public endpoint | NOT required | Required (webhook URL) | | Deployment | Local binary, laptop, VPS | Cloudflare Workers, serverless | | Connection model | Persistent WebSocket | Stateless HTTP POST per event | | Reconnection | Automatic (slack-morphism: 2 connections) | N/A (stateless) | | Ack mechanism | WebSocket ack frame | HTTP 200 within 3 seconds | | Signature verification | Not needed (app-level token authenticates) | Required (HMAC signing secret) | | Multi-workspace | One connection per workspace | One webhook URL serves all | | Complexity | Lower (no server, no TLS, no DNS) | Higher (needs public URL, HTTPS, verification) |
Decision: Socket Mode for TinyClaw. The SLB uses HTTP Events because Cloudflare Workers cannot maintain WebSocket connections. TinyClaw is a long-running local binary where Socket Mode is the natural fit.
Research Findings
Key Insights
-
TinyClaw is production-ready on main -- 220+ tests, Telegram + Discord adapters working. The channel-trait pattern is proven and stable.
-
slack-morphism is the only viable Rust Slack library -- v2.18.0 (Feb 2025), actively maintained, supports Socket Mode with automatic reconnection. Uses tokio, compatible with TinyClaw's async runtime.
-
The adapter pattern is mechanical -- Each existing adapter follows the same ~130-150 LOC pattern:
- Struct holding config +
Arc<AtomicBool>for running state start()spawns a tokio task that listens for events and forwards tobus.inbound_sender()send()calls the platform API to deliver messagesis_allowed()delegates tois_sender_allowed()helper
- Struct holding config +
-
Slack-specific considerations:
- Socket Mode requires TWO tokens: bot token (xoxb-) + app-level token (xapp-)
- Messages must be acked within 2-3 seconds (spawn processing as separate task)
- Slack uses
mrkdwn(not standard markdown):*bold*(not**bold**),_italic_,~strike~,<url|text>for links - Message limit is 4000 chars (section blocks: 3000)
-
OpenClaw Slack reference -- OpenClaw supports Socket Mode + HTTP Events API, threading, slash commands, streaming, channel policies. For TinyClaw MVP, only Socket Mode + basic DM/mention handling is needed.
Relevant Prior Art
| Project | Location | Relevance |
|---------|----------|-----------|
| slack-morphism | External | Primary Rust library; Socket Mode + events + Block Kit |
| OpenClaw Slack channel | External | Full-featured reference; config format, threading, policies |
| TinyClaw Telegram adapter | crates/terraphim_tinyclaw/src/channels/telegram.rs (main) | Direct template for Slack adapter structure |
| TinyClaw Discord adapter | crates/terraphim_tinyclaw/src/channels/discord.rs (main) | Second template showing simpler send pattern |
| Slack-Linear Bridge (TS) | ~/Projects/zestic/slack-linear-bridge/ | Production Slack bot -- shipped, multi-tenant, HTTP Events API on Cloudflare Workers |
| Slack-Linear Bridge (Rust) | ~/Projects/zestic-ai/slack-linear-bridge-rust/ | Phase 4 verified Rust migration -- 7-crate workspace, SlackApi trait with noop impl |
| OpenClaw Use Cases Analysis | ~/cto-executive-system/docs/OPENCLAW_USECASES_ANALYSIS.md | Multi-agent patterns, Telegram routing, scheduled tasks |
| OpenClaw Kimiko Workspace | ~/cto-executive-system/knowledge/openclaw-workspace-kimiko.md | Agent identity stack, multi-channel patterns |
| SLB ADR Suite | ~/cto-executive-system/projects/slack-linear-bridge/adr/ | 6 ADRs covering SDK strategy, LLM, deployment, multi-tenancy, architecture |
Key Findings from cto-executive-system Investigation
1. Production Slack Bot Already Exists (TypeScript)
The slack-linear-bridge project at ~/Projects/zestic/slack-linear-bridge/ is a shipped, production Slack bot:
- Cloudflare Workers + Hono + D1 + Queues
- HTTP Events API (NOT Socket Mode -- Workers has 90s connection limit)
- Multi-tenant: tenant resolved by channel mapping
- Anthropic Agent SDK with 9 tool skills
- Event dedup via KV, rate limiting per tenant
- Ack within 3s via queue-based async processing
src/slack/client.ts: thin wrapper aroundchat.postMessage,reactions.add/remove,users.infosrc/routes/slack-events.ts: webhook receiver with signature verification, bot-loop prevention, dedup
2. Rust Migration at Phase 4 (Not Production)
The slack-linear-bridge-rust project has a 7-crate Rust workspace:
bridge-integrations/src/slack.rsdefines aSlackApitrait withpost_final_message()-- currentlyNoopSlackApi- Real Slack API client NOT yet implemented in Rust
- Linear integration uses TS sidecar adapter pattern
- 15 tests passing, quality gates green
- Strategic decision: "Run TypeScript in production now. Migrate to Rust when triggers met."
3. Two Distinct Use Cases
| Aspect | Slack-Linear Bridge | TinyClaw Slack | |--------|-------------------|----------------| | Purpose | Project management bot (Linear CRUD, spec interviews) | Personal AI assistant (search, tools, KG) | | Deployment | Cloudflare Workers (serverless, multi-tenant) | Local binary (single-user, Socket Mode) | | Slack API | HTTP Events API (webhooks) | Socket Mode (WebSocket, no public endpoint) | | LLM | Anthropic Agent SDK (TS) | terraphim-llm-proxy / Ollama | | Auth | Slack signing secret + OAuth | Bot token + App token + allowlist | | Threading | Full thread support (thread_ts) | MVP: flat messages only | | Reactions | Eyes/checkmark ack pattern | Not in MVP |
4. Reusable Patterns from SLB
From the production TS implementation, these patterns should inform TinyClaw Slack:
- Bot message filtering:
if event.bot_id || event.subtype === "bot_message"-- prevent self-loops - Ack-first, process-async: Queue/spawn pattern to respond within 3s
- User info enrichment: Resolve Slack user ID to display name for session context
- Reaction-based status:
eyeson receipt,white_check_markon completion (Phase 2 for TinyClaw) - Message dedup: Event ID dedup to handle Slack retries (important for Socket Mode too)
5. No NanoClaw Implementation Found
No nanoclaw-specific code or documentation exists in ~/cto-executive-system/. The term "nanoclaw" appears only in external GitHub repos (forks/alternatives to OpenClaw). There is no Terraphim nanoclaw project.
Technical Spikes Needed
| Spike | Purpose | Estimated Effort | |-------|---------|------------------| | slack-morphism Socket Mode hello world | Verify library works with our tokio setup | 1-2 hours | | Slack App creation + token provisioning | Get bot token + app token for testing | 30 min | | Review SLB bot-loop prevention | Ensure TinyClaw handles Slack retry/dedup correctly | 30 min | | NanoClaw outgoing queue pattern | Evaluate if TinyClaw needs pre-connect message buffering | 30 min | | twin-slack WebSocket feasibility | Prototype tokio-tungstenite WS server simulating Socket Mode envelope format | 2-3 hours | | slack-morphism api_base_url override | Verify slack-morphism allows custom API base URL for twin routing | 1 hour |
NanoClaw Evaluation (github.com/qwibitai/nanoclaw)
NanoClaw is a TypeScript AI agent framework with multi-channel support. Its Slack adapter provides a well-tested reference implementation.
NanoClaw Architecture Overview
| Aspect | NanoClaw | TinyClaw |
|--------|----------|----------|
| Language | TypeScript (Node.js) | Rust |
| Slack library | @slack/bolt (official SDK) | slack-morphism (community Rust lib) |
| Transport | Socket Mode (socketMode: true) | Socket Mode (same) |
| Channel abstraction | Channel interface: connect/sendMessage/disconnect/ownsJid | Channel trait: start/stop/send/is_running/is_allowed |
| Registration | Factory pattern: registerChannel(name, factory) returns Channel \| null | build_channels_from_config() in ChannelManager |
| Config approach | .env file (dotenv-safe) | TOML with ${VAR} expansion |
| Tokens | SLACK_BOT_TOKEN + SLACK_APP_TOKEN from .env | Same two tokens from TOML/env vars |
| Message limit | 4000 chars (chunked) | 4000 chars (chunked) -- same |
| Test framework | vitest with mocked @slack/bolt | #[tokio::test] -- no mocks (project policy) |
| Skills system | Git-merged .claude/skills/add-slack/ | Feature-gated #[cfg(feature = "slack")] |
NanoClaw Slack Patterns Worth Adopting
1. Bot Self-Detection (MUST HAVE for MVP)
// NanoClaw: fetch bot user ID on connect, filter own messages
const authResult = await this.app.client.auth.test();
this.botUserId = authResult.user_id;
// In message handler:
if (event.bot_id || event.user === this.botUserId) return; // skip own messagesTinyClaw equivalent: Call auth.test after Socket Mode connect, store bot user ID, filter in event handler.
2. @Mention Translation (SHOULD HAVE for MVP)
// NanoClaw: translate <@UBOTID> to @AssistantName in incoming messages
if (event.text.includes(`<@${this.botUserId}>`)) {
text = text.replace(`<@${this.botUserId}>`, '').trim();
text = `@${this.assistantName} ${text}`;
}TinyClaw equivalent: Strip <@BOT_ID> from incoming text, optionally prepend assistant name.
3. User Name Resolution with Cache (SHOULD HAVE for MVP)
// NanoClaw: resolve Slack user ID to display name, cache in Map
private userNameCache = new Map<string, string>();
async getUserName(userId: string): Promise<string> {
if (this.userNameCache.has(userId)) return this.userNameCache.get(userId)!;
const result = await this.app.client.users.info({ user: userId });
const name = result.user?.profile?.display_name || result.user?.real_name || userId;
this.userNameCache.set(userId, name);
return name;
}TinyClaw equivalent: HashMap<String, String> behind RwLock, populate on first encounter.
4. Channel JID Ownership (NICE TO HAVE)
// NanoClaw: prefix-based routing
ownsJid(jid: string): boolean { return jid.startsWith('slack:'); }TinyClaw equivalent: Not needed -- TinyClaw uses channel: String field in InboundMessage for routing.
5. Outgoing Message Queue (DEFER to Phase 2)
// NanoClaw: buffer messages before connection, flush on connect
private outgoingQueue: Array<{jid: string, text: string}> = [];
async sendMessage(jid, text) {
if (!this.connected) { this.outgoingQueue.push({jid, text}); return; }
// ... send via API
}
async connect() {
// ... after connected:
for (const msg of this.outgoingQueue) await this.sendMessage(msg.jid, msg.text);
this.outgoingQueue = [];
}TinyClaw: MessageBus already handles queueing via tokio mpsc. Outgoing queue only needed if Slack connection drops mid-session.
6. Channel Metadata Sync (DEFER to Phase 2)
// NanoClaw: sync channel list with cursor pagination on startup
async syncChannelMetadata() {
let cursor;
do {
const result = await this.app.client.conversations.list({ cursor, limit: 200 });
// ... sync channel names to DB
cursor = result.response_metadata?.next_cursor;
} while (cursor);
}TinyClaw: Not needed for MVP (direct messages + mention handling sufficient).
7. Typing Indicator (NOT POSSIBLE)
// NanoClaw: no-op -- Slack bot API does not support typing indicators
async setTyping(): Promise<void> { /* no-op */ }TinyClaw: Same limitation. No typing indicator for bots in Slack API.
NanoClaw vs TinyClaw: Key Differences
| Pattern | NanoClaw Approach | TinyClaw Approach | Decision |
|---------|------------------|-------------------|----------|
| Channel lifecycle | connect() / disconnect() | start(bus) / stop() | TinyClaw -- bus injection is cleaner |
| Message routing | JID prefix (slack:channelId) | channel field in InboundMessage | TinyClaw -- explicit field is type-safe |
| Config validation | Runtime null checks on env vars | validate() method with anyhow errors | TinyClaw -- compile-time feature gates + validation |
| Error handling | try/catch with console.error | Result<T, anyhow::Error> propagation | TinyClaw -- Rust error handling is stronger |
| Bot detection | auth.test() + bot_id field check | Same pattern via slack-morphism | Adopt NanoClaw pattern |
| Message chunking | Manual split at 4000 chars | Reuse existing chunk_message() helper | TinyClaw -- helper already exists |
| Testing | vitest with full mocks | Digital twin (twin-slack) + real integration tests | TinyClaw -- digital twin for CI, real tokens for live tests |
| Graceful null factory | registerChannel() returns null if no creds | Feature gate excludes at compile time | TinyClaw -- zero-cost when disabled |
NanoClaw Evaluation Summary
NanoClaw's Slack adapter is a clean ~250 LOC implementation that validates our planned approach. The key takeaways:
- Socket Mode is correct: Both NanoClaw and our plan use Socket Mode. This is validated.
- Bot self-detection is essential: NanoClaw's
auth.test()+ message filtering pattern MUST be adopted. Without it, the bot creates infinite message loops. - @mention stripping improves UX: NanoClaw strips
<@BOTID>from messages before processing. This prevents the LLM from seeing raw Slack mention syntax. - User name cache is practical: Simple in-memory cache avoids repeated API calls. ~10 LOC in Rust.
- Outgoing queue is premature for MVP: TinyClaw's MessageBus handles this. Only needed if we want to buffer while Slack reconnects.
- Channel metadata sync is Phase 2: Only needed for multi-channel routing or channel picker UI.
- 4000 char limit confirmed: Same as our plan.
chunk_message()helper already exists in TinyClaw.
Updated MVP Scope (Post-NanoClaw Evaluation)
Based on NanoClaw patterns, add to MVP scope:
- Bot self-detection via
auth.test()on connect -- MUST HAVE - @mention stripping from incoming messages -- SHOULD HAVE
- User name resolution with in-memory cache -- SHOULD HAVE
- Message dedup for Slack retries (event ID tracking) -- SHOULD HAVE
Defer to Phase 2:
- Outgoing message queue (pre-connect buffering)
- Channel metadata sync
- Thread reply support
- Reaction-based status indicators
Testing Strategy: Digital Twin for Slack (twin-slack)
Problem
TinyClaw's "never use mocks in tests" policy creates a testing challenge for Slack:
- Real Slack tokens needed for integration tests
- CI cannot hold live Slack credentials safely
- NanoClaw uses vitest mocks -- not an option for us
- Existing Telegram/Discord adapters have no CI tests (only live tests gated by env vars)
Solution: twin-slack in zestic-ai/digital-twins (PRIVATE)
The digital-twins platform (private, zestic-ai org) provides stateful API twins for SaaS services. A new twin-slack crate will simulate the Slack API endpoints used by TinyClaw's Socket Mode adapter.
Why digital twin, not mocks:
- Real HTTP server with real request/response cycles (no mock objects)
- Stateful:
auth.testreturns a bot user ID,chat.postMessagestores messages,users.inforesolves users - Deterministic: TestClock for time control, no network flakiness
- Reusable: SLB Rust migration will also need Slack API twins
- Consistent: follows the established twin-core patterns (DashMapStore, EventBus, webhook delivery)
twin-slack Scope (Minimum for TinyClaw)
| Slack API Endpoint | Purpose | twin-slack Behavior |
|-------------------|---------|---------------------|
| auth.test | Bot self-identification | Returns configured bot_user_id, team_id |
| chat.postMessage | Send messages | Stores message in DashMapStore, returns ts |
| users.info | User name resolution | Returns configurable user profiles |
| conversations.list | Channel metadata sync | Returns configurable channel list |
| Socket Mode WebSocket | Event delivery | Simulates message and app_mention events |
Socket Mode simulation is the key technical challenge. Options:
- WebSocket server: twin-slack runs a WS endpoint that sends JSON event envelopes, accepts ack frames. TinyClaw's slack-morphism client connects to
ws://localhost:{port}instead ofwss://wss-primary.slack.com. - HTTP polling fallback: If WebSocket simulation is too complex for MVP, twin-slack can expose an HTTP endpoint where tests push events and pull acks. Less realistic but simpler.
Recommended: Option 1 (WebSocket). The tokio-tungstenite crate makes this straightforward and matches slack-morphism's actual connection model.
Boundary: Open Source vs Private
terraphim-ai (PUBLIC, open source)
crates/terraphim_tinyclaw/
src/channels/slack.rs -- Slack adapter (public)
src/channels/slack_test.rs -- Unit tests: config, formatting, allowlist (public)
tests/slack_integration.rs -- Integration test scaffold (public)
Uses env vars: SLACK_TEST_URL, SLACK_BOT_TOKEN, SLACK_APP_TOKEN
#[ignore] by default -- runs only when env vars set
zestic-ai/digital-twins (PRIVATE, internal)
crates/twin-slack/ -- Slack API digital twin (private)
src/lib.rs -- axum router, DashMapStore for messages/users
src/socket_mode.rs -- WebSocket event simulation
src/auth.rs -- auth.test endpoint
src/chat.rs -- chat.postMessage endpoint
src/users.rs -- users.info endpoint
tests/ -- twin-slack unit tests
specs/slack/ -- Slack API spec subset (private)
zestic-ai/tinyclaw-test-harness (PRIVATE, internal) -- OR in digital-twins/tests/
tests/tinyclaw_slack_e2e.rs -- End-to-end: spawn twin-slack, configure TinyClaw,
send simulated event, verify InboundMessage on busKey principle: The open source terraphim_tinyclaw crate has NO dependency on twin-slack. The integration test in the public repo uses generic env vars (SLACK_TEST_URL) that can point to either:
- twin-slack in CI (private infrastructure)
- Real Slack API in live testing
Test Layering
| Layer | Location | Runs in CI | Needs Tokens | Uses Twin |
|-------|----------|-----------|--------------|-----------|
| Unit tests | terraphim_tinyclaw (public) | Yes | No | No |
| Config validation | slack_test.rs | Yes | No | No |
| mrkdwn formatting | slack_test.rs | Yes | No | No |
| Allowlist logic | slack_test.rs | Yes | No | No |
| Message chunking | slack_test.rs | Yes | No | No |
| Bot ID filtering | slack_test.rs | Yes | No | No |
| Integration tests | terraphim_tinyclaw (public) | #[ignore] | Via env var | Optional |
| Connect + auth.test | slack_integration.rs | When configured | Yes | Yes (or real) |
| Send + receive message | slack_integration.rs | When configured | Yes | Yes (or real) |
| E2E tests | digital-twins (private) | Yes (private CI) | No (twin) | Yes |
| Full message round-trip | tinyclaw_slack_e2e.rs | Yes | No | Yes |
| Bot loop prevention | tinyclaw_slack_e2e.rs | Yes | No | Yes |
| Reconnection behavior | tinyclaw_slack_e2e.rs | Yes | No | Yes |
Integration Pattern in terraphim_tinyclaw (Public)
// tests/slack_integration.rs (public repo)
// Runs against either twin-slack or real Slack API
// Only runs when SLACK_TEST_URL is set
async Note: SlackConfig.api_base_url is an optional field (defaults to https://slack.com/api).
This URL override is the ONLY testing seam exposed in the public code. It follows the same pattern
used by Stripe SDKs (custom base URL) and is a legitimate configuration option, not a test-only hack.
twin-slack Implementation Sequence
- Phase 1 (with TinyClaw MVP):
auth.test+chat.postMessage+users.infoHTTP endpoints only. No WebSocket yet. TinyClaw unit tests cover logic; HTTP twin covers API contract. - Phase 2: WebSocket Socket Mode simulation. Full connect/disconnect/event delivery cycle. E2E tests in private repo.
- Phase 3: SDK validation tests (point official
@slack/web-apiNode SDK at twin-slack, verify responses parse correctly). Reusable by SLB Rust migration.
Effort Estimate
| Component | Effort | Location |
|-----------|--------|----------|
| twin-slack HTTP endpoints (auth, chat, users) | 4-6 hours | digital-twins (private) |
| twin-slack Socket Mode WebSocket | 6-8 hours | digital-twins (private) |
| Slack API spec subset | 2 hours | digital-twins (private) |
| TinyClaw api_base_url config field | 30 min | terraphim-ai (public) |
| TinyClaw integration test scaffold | 1-2 hours | terraphim-ai (public) |
| E2E test harness | 2-3 hours | digital-twins (private) |
Recommendations
Proceed/No-Proceed
Proceed. The channel-trait pattern is proven, slack-morphism is mature, and the adapter is mechanical (~200 LOC new code). Estimated effort: 4-6 hours including tests.
Scope Recommendations (MVP -- Updated Post-NanoClaw Evaluation)
SlackChannelimplementingChanneltrait via Socket ModeSlackConfigwithbot_token,app_token,allow_from- Handle: DMs + channel messages where bot is mentioned
markdown_to_slack_mrkdwn()formatting functionchunk_message()reuse with 4000 char limit- Feature flag:
slack = ["dep:slack-morphism"] - Bot self-detection:
auth.test()on connect, filter own messages (from NanoClaw) - @mention stripping: Remove
<@BOT_ID>from incoming text (from NanoClaw) - User name cache:
HashMap<String, String>behindRwLockfor display names (from NanoClaw) - Message dedup: Track event IDs to handle Slack retries (from SLB)
api_base_url: Option<String>in SlackConfig for twin/test URL override- Unit tests (config validation, formatting, allowlist, bot detection) -- run in CI, no tokens
- Integration test scaffold (
#[ignore], runs against twin-slack or real Slack via env vars)
Phase 2 (Future)
- Thread reply support (
OutboundMessage.reply_to-> Slackthread_ts) - Slash command handling
- Block Kit rich formatting
- Typing/ack reaction indicators
- File/media upload support
- Multi-workspace support
- Channel access policies (requireMention, allowlist/denylist per channel)
Risk Mitigation
- Feature-gate to avoid dependency bloat
- Socket Mode avoids public endpoint exposure
- Allowlist security matches existing pattern
- Use
tokio::spawnfor message processing to meet 2-3s ack deadline - Digital twin (twin-slack) in private repo for deterministic CI testing without Slack tokens
- No mock objects: twin-slack is a real HTTP server, compliant with "never use mocks" policy
- No private leakage: public repo uses
api_base_urloverride (same pattern as Stripe SDK), no import/dependency on twin-slack
Next Steps
If approved:
- Branch from
main(NOT current dependabot branch) - Create GitHub issue for Slack adapter implementation
- Create GitHub issue for twin-slack in
zestic-ai/digital-twins(private) - Proceed to Phase 2 (Design) -- detailed implementation plan
- Implement TinyClaw Slack adapter with
api_base_urltesting seam - Implement twin-slack HTTP endpoints (auth.test, chat.postMessage, users.info) in digital-twins
- Wire CI: private CI job spawns twin-slack, runs TinyClaw integration tests
- Test with real Slack workspace (live validation)
Appendix
Slack App Required Configuration
OAuth Bot Token Scopes:
chat:write-- Send messagesapp_mentions:read-- Receive mention eventsim:read-- Read DM messagesim:history-- Access DM historychannels:read-- List channelsconnections:write-- Socket Mode connections
Event Subscriptions (Bot Events):
app_mention-- When bot is @mentioned in a channelmessage.im-- DM messages to the bot
Socket Mode: Must be enabled in app settings
slack-morphism Cargo.toml
[dependencies]
slack-morphism = { version = "2.18", features = ["hyper_tokio"] }Reference: Channel Trait
Reference: Config Pattern
// In ChannelsConfig
pub slack: ,
// SlackConfig struct
Reference: TOML Config
[channels.slack]
bot_token = "${SLACK_BOT_TOKEN}"
app_token = "${SLACK_APP_TOKEN}"
allow_from = ["U01234567", "username"]Sources
- slack-morphism-rust GitHub
- slack-morphism Socket Mode docs
- slack-morphism crates.io
- OpenClaw Slack Channel docs
- Slack API Community Tools
- NanoClaw GitHub
- NanoClaw Slack adapter
- NanoClaw SPEC.md
- Slack-Linear Bridge (production TS)
- Slack-Linear Bridge (Rust migration)
- SLB ADR Suite
- Digital Twins Platform (PRIVATE -- zestic-ai org)
- Digital Twins Knowledge