Appearance
SILVIA Core Slack Bridge Reference
Version: 3.1
Namespace: CognitiveCode.Silvia.Extensions, Silvia.Extensions
Compile Flag: ENTERPRISE
Enterprise Slack Integration with Zero External Dependencies
SILVIA Slack Bridge connects SILVIA Core directly to Slack workspaces, enabling conversational AI deployment across enterprise communication channels. Users @mention SILVIA in any channel and receive threaded replies — processed through the same deterministic inference engine, LIVE CLI, and addressing protocol available in all other SILVIA integrations.
The bridge is implemented as two static classes in a single file with zero external dependencies. All HTTP operations use System.Net.Http and System.Net.HttpListener. Webhook payloads are verified with HMAC-SHA256 signature validation. A thread-safe queue architecture bridges Slack's asynchronous webhook delivery with SILVIA's single-threaded core, ensuring deterministic processing order without race conditions.
Why This Matters
Traditional chatbot frameworks require heavyweight SDKs, dependency trees of 15+ packages, and adapter layers that introduce latency and attack surface. SILVIA Slack Bridge uses exactly three Slack API operations — auth.test (once at startup), chat.postMessage (per response), and Events API webhook reception — in ~1,200 lines of fully auditable code with zero third-party risk.
| Metric | Typical Slack SDK | SILVIA Slack Bridge |
|---|---|---|
| External dependencies | 15+ packages (~2.5 MB) | 0 |
| Memory baseline | ~10 MB | ~20 KB |
| Memory per message | ~50 KB | ~4 KB |
| Processing overhead | 5–10 ms | < 1 ms |
| CVE exposure surface | 15+ packages | 0 external packages |
Architecture
The Slack integration comprises two static classes working in tandem:
| Class | Role |
|---|---|
| SilviaSlackBridge | Low-level Slack HTTP API: token management, HMAC-SHA256 signature verification, chat.postMessage, JSON helpers |
| SilviaSlackServer | Webhook HTTP listener, inbound event pipeline, thread-safe message queue, outbound response posting with chunking |
Request Flow
Slack workspace Your server
───────────────────────────────────────────────────────────────────
User: "@SILVIA help"
│
▼
Slack Events API ──POST──▶ SilviaSlackServer (HttpListener :8585)
│
├─ 200 OK (immediate ACK)
├─ event_id dedup
├─ HMAC-SHA256 verify
├─ bot message filter
├─ app_mention filter
├─ Addressing pipeline
├─ Enqueue(SlackQueueItem)
│
┌─────────┘
▼
Thread-safe ring buffer (32 slots)
│
▼
Host app main loop (FixedUpdate / tick)
│
├─ Dequeue() → SlackQueueItem
├─ core.GetResponseManaged(text)
├─ apiApp.GetTextOutput() (drain stack)
└─ SilviaSlackServer.PostResponse()
│
├─ Code-block wrapping
├─ Chunk splitting (>3800 chars)
└──▶ SilviaSlackBridge.PostMessageToSlack()
│
▼
Slack chat.postMessage API
│
▼
User sees threaded replySILVIA is single-threaded. The webhook listener runs on a background thread and the ThreadPool. The queue is the bridge between the two — writes happen on the webhook thread, reads happen on the main thread.
Setup
1. Create the Slack App
Go to https://api.slack.com/apps and create a new app (from scratch).
OAuth & Permissions — Bot Token Scopes:
| Scope | Purpose |
|---|---|
app_mentions:read | Receive events when users @SILVIA |
chat:write | Post responses back to channels |
Event Subscriptions:
- Enable Events: On
- Request URL: your public endpoint (see Exposing the Webhook)
- Subscribe to bot events:
app_mentiononly
Do NOT subscribe to
message.channelsormessage.im. These fire in addition toapp_mentionand cause duplicate processing. Theapp_mentionevent alone covers all@SILVIAmentions in channels the bot is invited to.
Install App to Workspace, then collect:
- Bot User OAuth Token (starts with
xoxb-) — from OAuth & Permissions - Signing Secret — from Basic Information
2. Set Environment Variables
Set these at the machine level so they persist across reboots and are available to services.
Windows PowerShell (elevated):
powershell
[System.Environment]::SetEnvironmentVariable("SLACK_BOT_TOKEN", "xoxb-your-token", "Machine")
[System.Environment]::SetEnvironmentVariable("SLACK_SIGNING_SECRET", "your-secret", "Machine")
[System.Environment]::SetEnvironmentVariable("SLACK_WEBHOOK_PORT", "8585", "Machine")
[System.Environment]::SetEnvironmentVariable("SLACK_ADMIN_CHANNEL", "C0A15177WJ0", "Machine")Linux/Mac:
bash
# Add to /etc/environment or your service's environment file
SLACK_BOT_TOKEN=xoxb-your-token
SLACK_SIGNING_SECRET=your-secret
SLACK_WEBHOOK_PORT=8585
SLACK_ADMIN_CHANNEL=C0A15177WJ0| Variable | Required | Description |
|---|---|---|
SLACK_BOT_TOKEN | Yes | Bot OAuth token (xoxb-...) |
SLACK_SIGNING_SECRET | Yes | Signing secret for HMAC verification |
SLACK_WEBHOOK_PORT | No | Listener port (default: 8585) |
SLACK_ADMIN_CHANNEL | No | Channel ID for startup/shutdown notices |
The admin channel is optional. If set, SILVIA posts "online" and "shutting down" messages there.
3. Initialize in Your Application
csharp
using CognitiveCode.Silvia.Extensions;
using Silvia.Extensions;
string botToken = Environment.GetEnvironmentVariable("SLACK_BOT_TOKEN",
EnvironmentVariableTarget.Machine);
string signingSecret = Environment.GetEnvironmentVariable("SLACK_SIGNING_SECRET",
EnvironmentVariableTarget.Machine);
string portStr = Environment.GetEnvironmentVariable("SLACK_WEBHOOK_PORT",
EnvironmentVariableTarget.Machine);
string adminChannel = Environment.GetEnvironmentVariable("SLACK_ADMIN_CHANNEL",
EnvironmentVariableTarget.Machine);
int port = 8585;
if (!string.IsNullOrEmpty(portStr)) {
int.TryParse(portStr, out port);
}
if (!string.IsNullOrEmpty(botToken) && !string.IsNullOrEmpty(signingSecret)) {
SilviaSlackBridge.Initialize(botToken, signingSecret);
SilviaSlackServer.Start(port, botToken, adminChannel);
}On startup, SilviaSlackServer.Start() calls Slack's auth.test API to resolve the bot's own user ID. This is required so the addressing pipeline can distinguish the bot's <@UID> from other users' mentions in the same message.
4. Expose the Webhook
Slack sends Events API payloads to a public HTTPS URL. You need to route that to your local HttpListener.
Development — ngrok:
bash
ngrok http 8585 --host-header=localhost:8585The --host-header rewrite is required. Without it, ngrok forwards the public hostname in the Host header and Windows HttpListener returns 400 Bad Request because it doesn't match the registered prefix.
Copy the ngrok HTTPS URL into the Slack app's Event Subscriptions Request URL field:
https://your-subdomain.ngrok-free.app/slack/The trailing /slack/ is required — that's the path HttpListener is bound to.
Production — URL ACL (no ngrok needed):
If running behind a reverse proxy or on a public-facing server, register the wildcard prefix so HttpListener accepts requests with any Host header:
powershell
netsh http add urlacl url=http://+:8585/slack/ user=EveryoneThis only needs to be run once (persists across reboots).
5. Process Messages on the Main Loop
In your application's main tick/update loop, dequeue one message per tick:
csharp
SlackQueueItem slackItem = SilviaSlackServer.Dequeue();
if (slackItem != null) {
Console.WriteLine("[Slack:" + slackItem.UserId + "] " + slackItem.Text);
bool ok = core.GetResponseManaged(slackItem.Text);
string output = core.ApiApp().GetTextOutput();
bool first = true;
while (output != null) {
string prefix = "";
if (first && !string.IsNullOrEmpty(slackItem.RespondingTo)) {
prefix = slackItem.RespondingTo + " ";
}
first = false;
bool isDiag = output.StartsWith("[") || output.StartsWith("Silvia:");
if (!isDiag) {
output = core.ApiApp().GetTextOutput();
continue;
}
SilviaSlackServer.PostResponse(slackItem.ChannelId, prefix + output,
slackItem.ThreadTs);
output = core.ApiApp().GetTextOutput();
}
}6. Shutdown
csharp
SilviaSlackServer.Stop();
SilviaSlackBridge.Shutdown();7. Invite the Bot
In each Slack channel where you want SILVIA to respond:
/invite @SILVIAAddressing Protocol
SILVIA's native addressing protocol using @! (recipient) and ?@! (return address) works transparently through Slack. The server translates between Slack's <@UID> markup and SILVIA's internal protocol.
Inbound: Slack to SILVIA
The addressing pipeline runs in order before the message is queued:
| Step | Method | Purpose |
|---|---|---|
| 1 | ParseReturnAddress | Extract ?@! prefix if present |
| 2 | CollectNonBotMentions | If no ?@!, collect all <@UID> that aren't the bot |
| 3 | StripAllMentions | Remove all <@UID> markup from the text |
| 4 | StripSlackAutoLinks | Convert <mailto:x|x> to x, <http://url|text> to url |
| 5 | CLI block | Reject text starting with $> |
Outbound: SILVIA to Slack
When the host dequeues a SlackQueueItem, its RespondingTo field contains the addressing result. The host prepends it to the first output message before calling PostResponse:
?@!Jasonin the original message → first reply starts withJason?@!@Brendan→ first reply starts with@Brendan?@!<@U07ABC>→ first reply starts with<@U07ABC>(native Slack ping)@Jason @Brendan @SILVIA help(no?@!) → first reply starts with<@U_JASON> <@U_BRENDAN>(native Slack pings to both)
Examples
| User sends | SlackQueueItem.Text | SlackQueueItem.RespondingTo |
|---|---|---|
@SILVIA help | help | null |
?@!Jason @SILVIA help | help | Jason |
?@!@Brendan @SILVIA help | help | @Brendan |
@SILVIA @Korey help | help | <@U0KOREY> |
@Jason @Brendan @SILVIA report | report | <@U_JASON> <@U_BRENDAN> |
@SILVIA test digest user@co.com | test digest user@co.com | null |
See Also: SILVIA LIVE CLI for full addressing protocol documentation.
Inbound Event Pipeline
Every POST to /slack/ goes through this pipeline:
1. GET? → health check JSON, return
2. Not POST? → 405, return
3. Read body
4. url_verification? → return challenge, return
5. *** 200 OK (immediate ACK — must beat Slack's 3-second retry window) ***
6. Log: event_id, retry header, callback type
7. Dedup by event_id (ring buffer, 64 slots) → drop if seen
8. Drop if X-Slack-Retry-Num header present
9. Missing signature headers? → drop
10. VerifySlackSignature() → drop if invalid
11. Contains "bot_id" or "subtype":"bot_message"? → drop (prevent loops)
12. Extract event section JSON → drop if missing
13. Event type != "app_mention"? → drop
14. Extract: user, text, channel, thread_ts, ts
15. Addressing pipeline (see above)
16. Empty after stripping? → drop
17. Starts with "$>"? → block, post rejection, return
18. Enqueue → if full (32), post overflow message, returnThe 200 ACK at step 5 is critical. Slack retries after 3 seconds if it doesn't receive a 200. All processing after the ACK runs without time pressure.
Outbound Response Pipeline
SilviaSlackServer.PostResponse() handles formatting for Slack:
Code block detection: If the output contains box-drawing characters (═, │, ─, ┌, └, ╔, ╚, ║, ╠) or exceeds 200 characters, it's wrapped in triple-backtick code blocks. This preserves formatted table and report layouts from SILVIA's console output.
Chunking: If a code-blocked message exceeds 3,800 characters, it's split on newline boundaries into multiple messages, each wrapped in its own code block. This avoids a Slack rendering bug where very large code blocks cause the API to duplicate the message.
Posting: Each chunk is sent via
SilviaSlackBridge.PostMessageToSlack()with the thread timestamp, so all chunks appear in the same thread.
Security
Webhook Signature Verification
Every inbound POST is verified with HMAC-SHA256 using the Signing Secret:
- Concatenate
v0:+ timestamp +:+ raw body - Compute HMAC-SHA256 with the signing secret as key
- Compare to the
X-Slack-Signatureheader using constant-time comparison (prevents timing attacks) - Reject if the timestamp is more than 5 minutes old (prevents replay attacks)
Event Deduplication
Slack may deliver the same event multiple times (retries, infrastructure). The server maintains a ring buffer of the last 64 event_id values. Any event whose ID was already seen is silently dropped.
Bot Loop Prevention
Messages containing "bot_id" or "subtype":"bot_message" are dropped before processing. This prevents infinite loops where SILVIA responds to her own messages.
CLI Command Blocking
Messages that start with $> after stripping are blocked. The CLI command prefix is not exposed through Slack. A rejection message is posted back to the user.
Credential Management
All credentials are read from environment variables at startup and held only in memory. They are never logged, serialized, or persisted. On shutdown, SilviaSlackBridge.Shutdown() clears the token and secret and disposes the HttpClient.
API Reference: SilviaSlackBridge
Low-level Slack HTTP operations. Namespace: CognitiveCode.Silvia.Extensions.
| Method | Description |
|---|---|
Initialize(botToken, signingSecret) | Set up HttpClient with Bearer auth. Call once at startup. Returns bool. |
Shutdown() | Dispose HttpClient, clear credentials. |
IsInitialized | Property: true if Initialize succeeded. |
VerifySlackSignature(timestamp, signature, body) | HMAC-SHA256 verification of a Slack webhook POST. Returns bool. |
PostMessageToSlack(channelId, text, threadTs) | Post a message via chat.postMessage. Thread-safe (locked). Returns bool. |
PostDirectMessage(channelId, text) | Convenience wrapper for PostMessageToSlack without threading. |
ParseSlackEvent(json, ...) | Extract event fields from raw JSON. Convenience for custom handlers. |
ExtractJsonValue(json, key) | Pull a string or primitive value from JSON by key prefix. |
EscapeJsonString(input) | Escape a string for embedding in a JSON value. |
API Reference: SilviaSlackServer
Webhook listener, queue, and response posting. Namespace: Silvia.Extensions.
Methods
| Method | Description |
|---|---|
Start(port, botToken, adminChannelId) | Start HttpListener, resolve bot user ID via auth.test, begin accepting webhooks. Returns bool. |
Stop() | Stop listener, send shutdown notice if admin channel is configured. |
Dequeue() | Pop one SlackQueueItem from the queue. Returns null if empty. Call from main thread. |
PostResponse(channelId, text, threadTs) | Format and post a SILVIA output to Slack. Handles code blocks and chunking. |
GetStatusReport() | Returns a formatted string with server state, queue depth, counters, and uptime. |
Properties
| Property | Type | Description |
|---|---|---|
IsRunning | bool | true if the listener is active. |
Port | int | The port the listener is bound to. |
QueueDepth | int | Current number of queued messages. |
TotalReceived | int | Lifetime count of messages enqueued. |
TotalDropped | int | Lifetime count of messages dropped (queue full). |
TotalProcessed | int | Lifetime count of messages dequeued. |
BotUserId | string | The bot's own Slack user ID (resolved at startup). |
StartedAt | DateTime | UTC timestamp of server start. |
LastMessageAt | DateTime | UTC timestamp of last message received. |
API Reference: SlackQueueItem
Represents a single inbound Slack message after pipeline processing.
| Field | Type | Description |
|---|---|---|
UserId | string | Slack user ID of the sender |
Username | string | Display name (falls back to user ID) |
Text | string | Cleaned message text (all markup stripped) |
ChannelId | string | Slack channel to reply in |
ThreadTs | string | Thread timestamp for reply threading |
RespondingTo | string | Addressing result: plain name, @Name, or <@UID> (may be null) |
ReceivedAt | DateTime | UTC timestamp when the message was received |
Health Check
GET /slack/ returns:
json
{"ok":true,"queue":0,"bridge":true}queue: current queue depthbridge: whetherSilviaSlackBridge.Initialize()succeeded
Use this endpoint for load balancer health probes and monitoring.
Best Practices
1. Subscribe Only to app_mention
Do not subscribe to message.channels or message.im in your Slack app event subscriptions. These fire in addition to app_mention for the same @SILVIA message, causing duplicate processing. The app_mention event alone covers all mentions in channels the bot is invited to.
2. Use Machine-Level Environment Variables
Session-level environment variables don't survive restarts and aren't available to services. Always set credentials at the machine level so they persist across reboots.
3. Set Up the Admin Channel
The optional admin channel provides operational visibility. When configured, SILVIA posts "online" and "shutting down" messages, making it easy to monitor deployment status across your team.
4. Monitor Queue Depth
The QueueDepth property and health check endpoint expose current queue state. If the queue consistently approaches 32, your main loop isn't dequeuing fast enough — check for blocking operations in your tick handler.
5. Use the Status Report for Diagnostics
SilviaSlackServer.GetStatusReport() returns a comprehensive snapshot of server state, queue metrics, and uptime. Expose this through an admin command or monitoring dashboard.
Troubleshooting
400 Bad Request from ngrok
Cause: Windows HttpListener rejects requests where the Host header doesn't match the registered prefix. ngrok forwards the public hostname by default.
Fix: Use --host-header=localhost:8585 when starting ngrok:
bash
ngrok http 8585 --host-header=localhost:8585Or register the wildcard prefix permanently:
powershell
netsh http add urlacl url=http://+:8585/slack/ user=EveryoneDuplicate Responses
Symptom: SILVIA posts the same reply 2–4 times.
Causes and fixes:
Slack event retries: If the server doesn't ACK within 3 seconds, Slack retries. The server sends
200 OKimmediately before any processing. If you're still seeing retries, check console logs forretry=values.Duplicate event subscriptions: If you subscribed to both
app_mentionandmessage.channels, Slack fires both events for the same message. Only subscribe toapp_mention.Slack rendering duplication: Very large code blocks (>4,000 chars) can cause Slack to visually duplicate a single API post. The chunking logic splits messages at 3,800 characters to avoid this.
Delayed events: If "Delayed Events" was enabled in the Slack app settings, Slack may queue events during downtime and deliver them all at once on reconnect. Disable this feature to avoid stale bursts.
Email Commands Fail with SMTP Syntax Error
Cause: Slack auto-links email addresses in message text, converting user@example.com to <mailto:user@example.com|user@example.com>. This markup passes through to SMTP drivers if not stripped.
Fix: The StripSlackAutoLinks step in the addressing pipeline converts these back to plain text automatically.
URL Verification Fails
When you first set the Request URL in the Slack app, Slack sends a one-time url_verification challenge. The server handles this automatically. If it fails:
- Confirm the URL ends with
/slack/ - Confirm the server is running and reachable
- Check console for
[Slack] URL verification challengelog - Slack requires HTTPS — use ngrok or a TLS-terminating proxy
Bot Not Responding
- Is the bot invited to the channel? (
/invite @SILVIA) - Is the bot's
app_mentionevent subscription active? - Are environment variables set at the machine level?
- Check console for
[Slack] Bot user ID resolved:— if this doesn't appear,auth.testfailed and mention filtering will be degraded. - Check console for
[Slack] Queuedmessages — if nothing appears, the event isn't reaching the server.
Production Deployment Checklist
- [ ] Machine-level environment variables configured
- [ ] HTTPS endpoint with valid certificate (or ngrok for dev)
- [ ] URL ACL registered if using
http://+:port/prefix - [ ] Slack app installed to workspace
- [ ] Only
app_mentionevent subscribed (nomessage.channels) - [ ] Bot invited to required channels (
/invite @SILVIA) - [ ] Admin channel ID set for startup/shutdown notifications
- [ ] Console logging monitored for
[Slack]prefixed messages - [ ] Firewall allows inbound on the webhook port
See Also
- ApiCore —
GetResponseManaged()processes dequeued messages - ApiApp —
GetTextOutput()drains the response stack - SILVIA LIVE CLI — Addressing protocol,
$>command syntax is disabled in SILVIA Slack Bridge - CI/CD & DevOps — Deployment patterns for SILVIA services
© Copyright Cognitive Code Corp. 2007-2026
SILVIA is a registered Trademark of Cognitive Code Corp.

