Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
193 changes: 193 additions & 0 deletions docs/ai/design/2026-08-06-feature-slack-channel-connector.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
---
phase: design
title: Slack Channel Connector Design
description: Provider-neutral bridge architecture with a local Slack Socket Mode adapter
---

# Slack Channel Connector Design

## Architecture Overview

```mermaid
graph LR
U[Paired Slack user] -->|DM message.im / block action| SM[Slack Socket Mode]
SM --> SA[SlackAdapter]
SA -->|normalized message/action| BR[Provider-neutral ChannelBridge]
BR -->|TtyWriter| AG[Bound local agent]
AG -->|conversation + request store| OP[Output poller]
OP --> BR
BR --> R[Slack renderer/chunker]
R --> Q[Per-conversation delivery queue]
Q -->|chat.postMessage| WA[Slack Web API]
CS[(channels.json 0600)] --> BR
ID[(bounded event IDs)] --> SA
```

`channel-connector` remains unaware of agents. It owns provider adapters, normalized transport types, rendering, SDK integration, and local channel configuration. The CLI owns agent discovery, terminal writes, conversation/request polling, authorization policy coordination, and bridge lifecycle.

## Technology Choices

- `@slack/socket-mode`: official Socket Mode lifecycle and envelope acknowledgment.
- `@slack/web-api`: official credential validation and `chat.postMessage` calls, including platform errors/rate-limit metadata.
- Existing `marked` lexer: semantic Markdown tokenization shared conceptually with Telegram, with a dedicated Slack renderer.
- Vitest SDK mocks/fixtures: no network or credentials in automated tests.

## Data Models

```ts
interface BaseChannelEntry {
enabled: boolean;
createdAt: string;
}

type ChannelEntry =
| BaseChannelEntry & { type: 'telegram'; config: TelegramConfig }
| BaseChannelEntry & { type: 'slack'; config: SlackConfig };

interface SlackConfig {
appToken: string;
botToken: string;
appId: string;
botUserId: string;
workspaceId: string;
workspaceName?: string;
authorizedUserId?: string;
authorizedConversationId?: string;
transport: 'socket-mode';
audience: 'dm';
}
```

Normalized events gain optional stable identity/thread fields while remaining source-compatible:

```ts
interface IncomingMessage {
channelType: string;
chatId: string;
userId: string;
text: string;
timestamp: Date;
messageId?: string;
threadId?: string;
workspaceId?: string;
metadata?: Record<string, unknown>;
}

interface IncomingInteraction {
channelType: string;
chatId: string;
userId: string;
interactionId: string;
messageId: string;
actionId: string;
value: string;
workspaceId?: string;
timestamp: Date;
}
```

## Internal API Design

```ts
interface ChannelAdapter {
readonly type: string;
start(): Promise<void>;
stop(): Promise<void>;
sendMessage(chatId: string, text: string, options?: SendMessageOptions): Promise<SentMessage>;
onMessage(handler: MessageHandler): void;
isHealthy(): Promise<boolean>;
}

interface InteractiveChannelAdapter extends ChannelAdapter {
onInteraction(handler: InteractionHandler): void;
sendQuestion(chatId: string, question: ChannelQuestion): Promise<SentMessage>;
finalizeInteraction(chatId: string, messageId: string): Promise<void>;
}
```

The optional send return/options are backward-compatible at runtime; Telegram can ignore threading and return its message ID. The CLI uses a type guard rather than importing provider-specific methods.

## Slack Adapter Responsibilities

- Construct/inject official SDK clients.
- Validate `auth.test` identity during setup through a separate factory/service.
- Start/stop Socket Mode and expose connection health.
- Acknowledge every recognized envelope before awaiting agent work.
- Normalize only plain-text `message.im` events.
- Reject wrong team, bots/self, subtypes, missing IDs, shared-channel contexts, and unauthorized identities.
- Maintain a bounded bridge-lifetime event-ID set and mark IDs before handler dispatch.
- Normalize `block_actions`, acknowledge immediately, and pass authorized action values to the CLI.
- Render and enqueue outbound messages.

## Pairing and Authorization

1. Setup validates tokens and stores verified app/workspace/bot identity, but no Slack user.
2. Starting an unpaired bridge generates a CSPRNG pairing code with a ten-minute TTL and prints it only to the local terminal.
3. The adapter accepts only DM events for the configured workspace. A message matching the active code atomically stores `authorizedUserId` and `authorizedConversationId`; the code is invalidated.
4. All subsequent messages and interactions must match workspace, user, and conversation. Authorization is rechecked immediately before terminal writes to prevent workflow bypass.
5. Pairing messages are consumed by the bridge and never sent to the agent.

## Rendering, Chunking, and Delivery

- Parse CommonMark into semantic tokens before rendering each chunk.
- Translate headings/bold/italic/strike/code/links/lists into conservative Slack `mrkdwn` and escape `&`, `<`, and `>` unless deliberately producing a link.
- Do not enable name parsing; plain `@channel`, `@here`, and `@everyone` remain inert text.
- Split at token, paragraph, line, word, then Unicode code-point boundaries. Re-open fenced code per chunk.
- Keep each top-level `text` payload at or below 4,000 JavaScript characters.
- Send the first chunk normally, record its `ts`, and send remaining chunks with `thread_ts` equal to that parent.
- Use a bounded FIFO queue per conversation. A single worker preserves ordering, waits on rate-limit retry metadata, applies bounded exponential backoff to transient failures, and drops/reports overflow rather than consuming unbounded memory.

## Questions and Prompt Semantics

- Move question parsing/specification and terminal-key mapping out of the Telegram-specific service.
- Provider renderers implement question presentation; Slack uses Block Kit section/actions with stable action IDs and short opaque values.
- Active question state is keyed by an opaque request ID and bound to workspace, conversation, user, agent session, and expiry.
- The adapter acknowledges the Slack action before the CLI writes the digit/Escape key.
- Replays, stale actions, and mismatched identities are acknowledged and ignored.
- Non-question agent requests remain notifications. Generic Slack text is delivered as normal terminal input and is never reclassified as approval by message content.

## CLI and Setup Integration

- `channel connect <type> --name` dispatches to a provider setup strategy.
- Slack setup prompts secretly for app and bot tokens, validates with official SDKs, and persists the verified entry.
- `channel start` resolves a named entry regardless of type; omission retains the legacy exactly-one-Telegram behavior unless exactly one total channel exists.
- The runner uses an adapter factory and provider-neutral authorization/interaction helpers.
- List/status use provider display metadata rather than Telegram casts.
- Daemon arguments include only channel and agent names; tokens remain in `channels.json`.

## Security Boundaries

- Trust boundaries: Slack network → official SDK event → adapter validation → CLI authorization → local TTY; agent output → renderer → external Slack API.
- Tokens are password inputs, stored only in mode-`0600` config, never interpolated into shell commands or logs.
- IDs are exact-match allowlisted and treated as opaque Slack identifiers.
- Pairing codes use `crypto.randomBytes`, expire, are single-use, and use timing-safe comparison.
- External text is data. It is not executed, used as a path/URL, or automatically converted into privileged Slack mentions.
- Queue, text, block actions, event IDs, and question sessions have explicit bounds.
- SDK TLS verification stays enabled.

## Alternatives and Decisions

- Socket Mode is selected over a public Events API because the daemon is local-first and behind NAT/firewalls.
- A user-owned, undistributed app is selected over OAuth because the MVP is single-workspace and Marketplace distribution is incompatible with Socket Mode/remote-terminal policy.
- DM-only is selected over `app_mention` to minimize accidental exposure and scopes.
- Provider capabilities are selected over a single Telegram-shaped interface; providers can support interactions and threading without leaking SDK types into the CLI.
- The existing JSON secret store is retained for compatibility; a keychain abstraction is deferred.

## Non-Functional Requirements

- Incoming envelope acknowledgment begins synchronously and completes within Slack's three-second expectation.
- Normal online round trip remains within one existing two-second agent poll plus Slack API latency.
- Queue defaults are bounded (100 outbound jobs per conversation; 1,000 recent event IDs; ten-minute interaction/pairing TTL).
- Reconnects are delegated to the official Socket Mode client; `isHealthy` reflects connection lifecycle.
- All new code is mockable through injected SDK-shaped clients and clocks/sleep functions.
- No public API removal; Telegram remains fully supported.

## Official Platform References

- Socket Mode: https://docs.slack.dev/apis/events-api/using-socket-mode/
- Events/retries: https://docs.slack.dev/apis/events-api/
- Web API rate limits: https://docs.slack.dev/apis/web-api/rate-limits/
- Message formatting: https://docs.slack.dev/messaging/formatting-message-text/
- `chat.postMessage`: https://docs.slack.dev/reference/methods/chat.postmessage
- Interactivity: https://docs.slack.dev/interactivity/handling-user-interaction/
- App manifests: https://docs.slack.dev/app-manifests/
123 changes: 123 additions & 0 deletions docs/ai/implementation/2026-08-06-feature-slack-channel-connector.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
---
phase: implementation
title: Slack Channel Connector Implementation Guide
description: Living implementation record for the Slack Socket Mode connector
---

# Slack Channel Connector Implementation Guide

## Development Setup

- Active worktree: `.worktrees/feature-slack-channel-connector`
- Branch: `feature-slack-channel-connector`
- Base: latest `origin/main` at workspace creation
- Dependencies: deterministic `npm ci`; official Slack SDK packages are added through the lockfile.
- Automated tests use injected SDK clients and synthetic fixtures; no real credentials are required.

## Code Structure

- `packages/channel-connector/src/types.ts`: discriminated config and normalized provider-neutral events.
- `packages/channel-connector/src/adapters/`: Telegram and Slack transport implementations plus capability contracts.
- `packages/channel-connector/src/utils/`: provider-specific Markdown rendering/chunking and bounded delivery helpers.
- `packages/cli/src/services/channel/`: provider setup/factory, generic bridge runner, authorization, and structured questions.
- `packages/cli/src/commands/channel.ts`: provider-neutral connect/list/start/status UX.
- `web/content/docs/12-channel.md`: user setup, manifest, security, and manual validation.

## Implementation Notes

### Task 1.1 — Provider contracts

- Changed `types.ts`, `ChannelAdapter.ts`, public exports, and ConfigStore/manager tests.
- Red: ConfigStore test failed because `isSlackEntry` did not exist.
- Green/refactor: introduced a discriminated config union, Slack config, stable message/thread metadata, generic send results/options, question/interaction models, and an interactive adapter type guard.
- Evidence: 14 targeted tests and package typecheck pass.
- Design deviation: `sendMessage` permits `void` so the published Telegram implementation remains source-compatible; new providers return `SentMessage`.

### Task 1.2 — Slack renderer and chunker

- Added `slackMarkdown.ts` and public exports with `marked` token rendering, Slack control-character escaping, conservative formatting, semantic code splitting, and Unicode-safe hard splitting.
- Red: renderer test suite failed because the Slack utility did not exist.
- Green/refactor: four formatting/chunking tests and package typecheck pass.
- Edge cases: broad mentions remain literal, code chunks are independently fenced, and rendered chunks stay at or below 4,000 characters.

### Task 1.3 — Slack delivery queue

- Added official Slack SDK dependencies and `SlackDeliveryQueue`.
- Red: queue tests failed because the module did not exist.
- Green/refactor: per-conversation FIFO state, parent/thread sends, one explicit rate-limit retry, injected sleep, and queue bounds pass three tests plus typecheck.
- Security/performance: Web API payloads disable unfurls and queue state is removed when drained.

### Task 2.1 — Slack adapter

- Added `SlackAdapter` backed by official `SocketModeClient` and `WebClient`, with injectable SDK-shaped clients.
- Red: adapter suite failed because the module did not exist.
- Green/refactor: nine tests cover lifecycle/health, prompt acknowledgment order, normalization, idempotency, and identity/message filtering; typecheck passes.
- Trust boundary: stable event IDs are recorded before consumer dispatch and listener failures cannot reject the SDK event loop.

### Task 2.2 — Explicit pairing

- Added `SlackPairingSession` and unpaired adapter flow with persistence callback.
- Red: pairing utility/adapter tests failed on missing behavior.
- Green/refactor: CSPRNG code generation, timing-safe comparison, whitespace normalization, strict case, ten-minute expiry, single use, exact workspace/DM constraints, and consumed pairing input pass 13 tests plus typecheck.

### Tasks 2.3 and 3.1-3.3 — Questions, CLI, runtime, and docs

- Added Slack Block Kit question rendering and `SlackQuestionService`; valid option/Skip actions finalize once and write one digit/Escape.
- Added `channel connect slack` with hidden prompts plus official `apps.connections.open` and `auth.test` validation.
- Generalized runner input/output, provider construction, pairing persistence, bridge type metadata, list/status identity, and daemon launch without credential arguments.
- Added the exact minimal Slack manifest, pairing/security/troubleshooting guidance, and optional sandbox validation to channel docs.
- Red/green evidence: missing question service, setup behavior, app-token validation, and discriminated runner compilation each failed before implementation; 16 adapter tests, 22 targeted CLI tests, connector typecheck, and both package builds pass.
- Design deviation: the runner branches at its provider composition root rather than introducing a separate factory file; provider SDK details remain inside `channel-connector` and the branch is exhaustive over implemented providers.

## Integration Points

- Slack Socket Mode events enter `channel-connector`; agent discovery and TTY writes stay in the CLI.
- Agent conversation/request polling emits through a generic adapter interface.
- Slack credentials remain in `ConfigStore`; bridge metadata and daemon arguments contain names/IDs only.
- Telegram remains an implementation of the same contracts.

## Error Handling

- Reject malformed/unauthorized inbound events without terminal side effects.
- Acknowledge Slack envelopes/actions before asynchronous processing.
- Retry only rate-limit/transient outbound failures with explicit bounds.
- Preserve plain-text delivery fallback when rendering fails.
- Surface safe health/setup errors without tokens or raw SDK credential payloads.

## Performance Considerations

- Bounded recent-event and interaction maps.
- Bounded per-conversation queues with serialized workers.
- Semantic chunking before API calls; no conversation-history reads from Slack.
- Existing two-second agent output polling remains unchanged.

## Security Notes

- Exact workspace/user/conversation allowlist plus expiring CSPRNG pairing.
- No first-message authorization.
- No automatic mention parsing or generic approval inference.
- Mode-`0600` config/registry/log files and credential-free daemon argv.
- Official Slack SDK networking with normal TLS verification.

## Formal Final Security Review

The installed `ai-devkit:security-review` checklist was applied to the complete `origin/main...HEAD` diff on 2026-08-06. Result: no unresolved critical or high feature-specific findings.

- **Credentials and process exposure:** app/bot tokens enter through hidden prompts, are passed only to official SDK constructors, and are absent from daemon argv, bridge registry, status/list output, debug statements, and user-facing errors. Slack setup deliberately replaces SDK errors with a credential-safe message.
- **Storage and migration:** `channels.json` persists secrets in the existing local store and now forces `0600` after every write, including overwriting a permissive existing file. Telegram entries retain their prior shape. Missing/corrupt/unknown channel configurations do not construct a Slack adapter and therefore fail closed.
- **Inbound authorization:** exact workspace, paired user, and DM conversation IDs are required. Pairing uses 48 random bits encoded as 12 hex characters, timing-safe comparison, ten-minute expiry, single use, and persistence before runtime authorization. Persistence failure leaves the adapter unauthorized. Bot/self/subtype/non-DM/Slack Connect events are acknowledged and rejected.
- **Replay and acknowledgment:** Socket Mode event/action envelopes are acknowledged before consumer work. A bounded 1,000-ID bridge-lifetime set suppresses retries and reconnect duplicates; question state additionally binds conversation/message/value, expires after ten minutes, and is consumed before terminal input.
- **Approval boundary and terminal input:** ordinary authorized DM text uses the existing message-to-bound-TTY path and is never interpreted as approval. Only a current `AskUserQuestion` Block Kit action can call `sendKey`, and accepted values are exactly one generated option digit or Escape for Skip.
- **Outbound safety and availability:** Slack control characters are escaped in rendered content and question fallback text, so ordinary Markdown cannot create mentions. Output is capped at 4,000 characters per call, unfurls are disabled, queues are bounded to 100 jobs per conversation, retry occurs once, and an excessive `Retry-After` is capped at 60 seconds.
- **Dependencies:** the lockfile resolves official `@slack/socket-mode@3.0.0` and `@slack/web-api@8.0.0`. `npm audit --audit-level=critical --omit=dev` exits 0 with no critical advisory. Its 22 high, 7 moderate, and 2 low reports are pre-existing transitive dependency findings outside the introduced Slack SDK path; broad upgrades are outside this feature and should be handled separately.
- **Compatibility:** the discriminated provider seam preserves Telegram configuration and behavior; the full repository lint/build/test gate exercises existing Telegram suites.

Review-driven red/green fixes covered permissive existing config permissions, pairing-persistence fail-closed behavior and listener rejection isolation, question fallback mention escaping, excessive rate-limit delay, duplicate SDK acknowledgment, and expired interactive actions.

## Deviations and Follow-ups

- Event idempotency is bounded to the 1,000 most recent IDs for the bridge lifetime rather than time-expiring; this keeps memory bounded and covers Socket Mode retry/reconnect duplication without persistence.
- Structured questions are single-select in the MVP and expire after ten minutes. Multi-select continues through the existing terminal interaction rather than guessing Slack approval semantics.
- Security review findings and their TDD remediations are recorded in the formal review above. No blocking feature-specific findings remain.
- `npm audit --audit-level=critical --omit=dev` reports no critical advisories; existing lower-severity transitive advisories remain outside this scoped feature.
- Real Slack credential validation was intentionally not performed; the documented sandbox exercise remains optional manual validation.
Loading
Loading