# MS Teams Provider Research > Exploratory notes for adding `msteams` as a new provider to Clawdbot. --- ## 1. Existing Provider Structure Analysis ### Directory Structure Pattern Each provider follows this structure (using Slack as reference): ``` src/slack/ ├── index.ts # Public exports (barrel file) ├── monitor.ts # Main event loop & message handling ├── monitor.test.ts # Unit tests ├── monitor.tool-result.test.ts # Integration tests ├── send.ts # Outbound message delivery ├── actions.ts # Platform API actions (reactions, edits, pins) ├── token.ts # Token resolution & validation └── probe.ts # Health check / connectivity validation ``` ### Key Files by Provider | Provider | Files | |----------|-------| | Telegram | bot.ts, monitor.ts, send.ts, probe.ts, token.ts, webhook.ts, download.ts, draft-stream.ts, pairing-store.ts | | Discord | monitor.ts, send.ts, probe.ts, token.ts | | Slack | monitor.ts, send.ts, actions.ts, probe.ts, token.ts | | Signal | monitor.ts, send.ts, probe.ts (uses signal-cli) | | iMessage | monitor.ts, send.ts, probe.ts (uses imsg CLI) | --- ## 2. Monitor Pattern (Event Loop) The `monitorXxxProvider()` function is the heart of each provider. Pattern from Slack: ```typescript export async function monitorSlackProvider(opts: MonitorSlackOpts = {}) { // 1. Load configuration const cfg = loadConfig(); // 2. Resolve tokens (options > env > config) const botToken = resolveSlackBotToken( opts.botToken ?? process.env.SLACK_BOT_TOKEN ?? cfg.slack?.botToken ); // 3. Create SDK client const app = new App({ token: botToken, appToken, socketMode: true, }); // 4. Authenticate and cache identity const auth = await app.client.auth.test({ token: botToken }); // 5. Set up caches (channel info, user info, message dedup) const channelCache = new Map(); const userCache = new Map(); const seenMessages = new Map(); // 6. Register event handlers app.event("message", async ({ event }) => { await handleMessage(event); }); // 7. Start and wait for abort signal await app.start(); await new Promise((resolve) => { opts.abortSignal?.addEventListener("abort", () => resolve()); }); await app.stop(); } ``` ### Message Processing Pipeline 1. **Validation**: Check message type, ignore bots, dedup check 2. **Channel Resolution**: Get channel metadata (name, type, topic) 3. **Authorization Checks**: DM policy, channel allowlist, user allowlist, mention requirements 4. **Media Download**: Fetch attachments with size limits 5. **Acknowledgment**: Send reaction to confirm receipt 6. **Envelope Construction**: Build `ctxPayload` with all message metadata 7. **System Event Logging**: `enqueueSystemEvent()` 8. **Reply Dispatcher Setup**: Configure typing indicators and threading 9. **Dispatch to Agent**: `dispatchReplyFromConfig()` --- ## 3. Gateway Integration ### Provider Manager (src/gateway/server-providers.ts) ```typescript // Status types per provider export type SlackRuntimeStatus = { running: boolean; lastStartAt?: number | null; lastStopAt?: number | null; lastError?: string | null; }; // Combined snapshot export type ProviderRuntimeSnapshot = { whatsapp: WebProviderStatus; telegram: TelegramRuntimeStatus; discord: DiscordRuntimeStatus; slack: SlackRuntimeStatus; signal: SignalRuntimeStatus; imessage: IMessageRuntimeStatus; }; // Manager interface export type ProviderManager = { getRuntimeSnapshot: () => ProviderRuntimeSnapshot; startProviders: () => Promise; startSlackProvider: () => Promise; stopSlackProvider: () => Promise; // ... per provider }; ``` ### Lifecycle Management ```typescript // State tracking let slackAbort: AbortController | null = null; let slackTask: Promise | null = null; let slackRuntime: SlackRuntimeStatus = { running: false }; const startSlackProvider = async () => { if (slackTask) return; // Already running const cfg = loadConfig(); if (cfg.slack?.enabled === false) return; const botToken = resolveSlackBotToken(...); if (!botToken) return; // Not configured slackAbort = new AbortController(); slackRuntime = { running: true, lastStartAt: Date.now() }; slackTask = monitorSlackProvider({ botToken, runtime: slackRuntimeEnv, abortSignal: slackAbort.signal, }) .catch(err => { slackRuntime.lastError = formatError(err); }) .finally(() => { slackAbort = null; slackTask = null; slackRuntime.running = false; }); }; ``` ### RuntimeEnv Pattern ```typescript // Minimal interface for provider DI export type RuntimeEnv = { log: typeof console.log; error: typeof console.error; exit: (code: number) => never; }; // Created from subsystem logger const logSlack = logProviders.child("slack"); const slackRuntimeEnv = runtimeForLogger(logSlack); ``` ### Config Hot-Reload (src/gateway/config-reload.ts) ```typescript const RELOAD_RULES: ReloadRule[] = [ { prefix: "slack", kind: "hot", actions: ["restart-provider:slack"] }, { prefix: "telegram", kind: "hot", actions: ["restart-provider:telegram"] }, // ... ]; ``` --- ## 4. Configuration Types ### Pattern from SlackConfig (src/config/types.ts) ```typescript export type SlackConfig = { enabled?: boolean; // Master toggle botToken?: string; // Primary credential appToken?: string; // Socket mode credential groupPolicy?: GroupPolicy; // "open" | "disabled" | "allowlist" textChunkLimit?: number; // Platform message limit mediaMaxMb?: number; // File size limit dm?: SlackDmConfig; // DM-specific settings channels?: Record; // Per-channel config actions?: SlackActionConfig; // Feature gating slashCommand?: SlackSlashCommandConfig; // Command config }; export type SlackDmConfig = { enabled?: boolean; policy?: DmPolicy; // "pairing" | "allowlist" | "open" | "disabled" allowFrom?: Array; groupEnabled?: boolean; groupChannels?: Array; }; export type SlackChannelConfig = { enabled?: boolean; requireMention?: boolean; users?: Array; // Per-channel allowlist skills?: string[]; // Skill filter systemPrompt?: string; // Channel-specific prompt }; export type SlackActionConfig = { reactions?: boolean; messages?: boolean; pins?: boolean; search?: boolean; // ... feature toggles }; ``` ### Where Provider Appears in Config - `ClawdbotConfig.slack` - main config block - `QueueModeByProvider.slack` - queue mode override - `AgentElevatedAllowFromConfig.slack` - elevated permissions - `HookMappingConfig.provider` - webhook routing --- ## 5. Zod Validation Schema ### Pattern (src/config/zod-schema.ts) ```typescript const SlackConfigSchema = z .object({ enabled: z.boolean().optional(), botToken: z.string().optional(), appToken: z.string().optional(), groupPolicy: GroupPolicySchema.optional().default("open"), textChunkLimit: z.number().optional(), mediaMaxMb: z.number().optional(), dm: SlackDmConfigSchema.optional(), channels: z.record(z.string(), SlackChannelConfigSchema).optional(), actions: SlackActionConfigSchema.optional(), }) .superRefine((value, ctx) => { // Cross-field validation if (value.dm?.policy === "open" && !value.dm?.allowFrom?.includes("*")) { ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["dm", "allowFrom"], message: 'slack.dm.policy="open" requires allowFrom to include "*"', }); } }) .optional(); ``` --- ## 6. Onboarding Flow ### Pattern (src/commands/onboard-providers.ts) ```typescript // 1. Status detection const slackConfigured = Boolean( process.env.SLACK_BOT_TOKEN || cfg.slack?.botToken ); // 2. Provider selection const selection = await prompter.multiselect({ message: "Select providers", options: [ { value: "slack", label: "Slack", hint: slackConfigured ? "configured" : "needs token" }, ], }); // 3. Credential collection if (selection.includes("slack")) { if (process.env.SLACK_BOT_TOKEN && !cfg.slack?.botToken) { const useEnv = await prompter.confirm({ message: "SLACK_BOT_TOKEN detected. Use env var?", }); if (!useEnv) { token = await prompter.text({ message: "Enter Slack bot token" }); } } // ... also collect app token for socket mode } // 4. DM policy configuration const policy = await selectPolicy({ label: "Slack", provider: "slack" }); cfg = setSlackDmPolicy(cfg, policy); ``` ### DM Policy Setter Helper ```typescript function setSlackDmPolicy(cfg: ClawdbotConfig, dmPolicy: DmPolicy) { const dm = cfg.slack?.dm ?? {}; const allowFrom = dmPolicy === "open" ? addWildcardAllowFrom(dm.allowFrom) : dm.allowFrom; return { ...cfg, slack: { ...cfg.slack, dm: { ...dm, policy: dmPolicy, ...(allowFrom ? { allowFrom } : {}) }, }, }; } ``` --- ## 7. Probe (Health Check) ### Pattern (src/slack/probe.ts) ```typescript export type SlackProbe = { ok: boolean; status?: number | null; error?: string | null; elapsedMs?: number | null; bot?: { id?: string; name?: string }; team?: { id?: string; name?: string }; }; export async function probeSlack( token: string, timeoutMs = 2500, ): Promise { const client = new WebClient(token); const start = Date.now(); try { const result = await withTimeout(client.auth.test(), timeoutMs); if (!result.ok) { return { ok: false, status: 200, error: result.error }; } return { ok: true, status: 200, elapsedMs: Date.now() - start, bot: { id: result.user_id, name: result.user }, team: { id: result.team_id, name: result.team }, }; } catch (err) { return { ok: false, status: err.status, error: err.message, elapsedMs: Date.now() - start }; } } ``` --- ## 8. Send Function ### Pattern (src/slack/send.ts) ```typescript export async function sendMessageSlack( to: string, message: string, opts: SlackSendOpts = {}, ): Promise { // 1. Parse recipient (user:X, channel:Y, #channel, @user, etc.) const recipient = parseRecipient(to); // 2. Resolve channel ID (open DM if needed) const { channelId } = await resolveChannelId(client, recipient); // 3. Chunk text to platform limit const chunks = chunkMarkdownText(message, chunkLimit); // 4. Upload media if present if (opts.mediaUrl) { await uploadSlackFile({ client, channelId, mediaUrl, threadTs }); } // 5. Send each chunk for (const chunk of chunks) { await client.chat.postMessage({ channel: channelId, text: chunk, thread_ts: opts.threadTs, }); } return { messageId, channelId }; } ``` --- ## 9. CLI Integration ### Dependencies (src/cli/deps.ts) ```typescript export type CliDeps = { sendMessageWhatsApp: typeof sendMessageWhatsApp; sendMessageTelegram: typeof sendMessageTelegram; sendMessageDiscord: typeof sendMessageDiscord; sendMessageSlack: typeof sendMessageSlack; sendMessageSignal: typeof sendMessageSignal; sendMessageIMessage: typeof sendMessageIMessage; }; export function createDefaultDeps(): CliDeps { return { sendMessageWhatsApp, sendMessageTelegram, // ... }; } ``` ### Send Command (src/commands/send.ts) ```typescript const provider = (opts.provider ?? "whatsapp").toLowerCase(); // Provider-specific delivery const results = await deliverOutboundPayloads({ cfg: loadConfig(), provider, to: resolvedTarget.to, payloads: [{ text: opts.message, mediaUrl: opts.media }], deps: { sendSlack: deps.sendMessageSlack, // ... }, }); ``` --- ## 10. Files to Create/Modify for MS Teams ### New Files (src/msteams/) ``` src/msteams/ ├── index.ts # Exports ├── monitor.ts # Bot Framework event loop ├── send.ts # Send via Graph API ├── probe.ts # Health check (Graph API /me) ├── token.ts # Token resolution ├── actions.ts # Optional: reactions, edits, etc. └── *.test.ts # Tests ``` ### Files to Modify | File | Changes | |------|---------| | `src/config/types.ts` | Add `MSTeamsConfig`, update `QueueModeByProvider`, `AgentElevatedAllowFromConfig`, `HookMappingConfig` | | `src/config/zod-schema.ts` | Add `MSTeamsConfigSchema` | | `src/gateway/server-providers.ts` | Add `MSTeamsRuntimeStatus`, lifecycle methods, update `ProviderRuntimeSnapshot`, `ProviderManager` | | `src/gateway/server.ts` | Add logger, runtimeEnv, pass to provider manager | | `src/gateway/config-reload.ts` | Add reload rule | | `src/gateway/server-methods/providers.ts` | Add status endpoint | | `src/cli/deps.ts` | Add `sendMessageMSTeams` | | `src/cli/program.ts` | Add to `--provider` options | | `src/commands/send.ts` | Add msteams case | | `src/commands/onboard-providers.ts` | Add wizard flow | | `src/commands/onboard-types.ts` | Add to `ProviderChoice` | | `docs/providers/msteams.md` | Documentation | --- ## 11. MS Teams SDK Options ### Option A: Bot Framework SDK (@microsoft/botframework) ```typescript import { CloudAdapter, ConfigurationBotFrameworkAuthentication } from "botbuilder"; // Pros: Full-featured, handles auth, typing indicators, cards // Cons: More complex, requires Azure Bot registration ``` ### Option B: Microsoft Graph API ```typescript import { Client } from "@microsoft/microsoft-graph-client"; // Pros: Simpler for basic messaging, direct API access // Cons: Less rich features, manual auth handling ``` ### Recommended: Bot Framework for receiving, Graph for some sends MS Teams bots use the Bot Framework for receiving messages (webhook-based), and can use either Bot Framework or Graph API for sending. ### Required Azure Resources 1. **Azure Bot Registration** - Bot identity and channel configuration 2. **App Registration** - OAuth for Graph API access 3. **Teams App Manifest** - Defines bot capabilities in Teams ### Credentials Needed ```typescript export type MSTeamsConfig = { enabled?: boolean; appId?: string; // Azure AD App ID appPassword?: string; // Azure AD App Secret tenantId?: string; // Optional: restrict to tenant // ... rest follows pattern }; ``` --- ## 12. Key Differences from Slack | Aspect | Slack | MS Teams | |--------|-------|----------| | Connection | Socket Mode (WebSocket) | Webhook (HTTP POST) | | Auth | Bot Token + App Token | Azure AD App ID + Secret | | Message ID | `ts` (timestamp) | Activity ID | | Threading | `thread_ts` | `replyToId` in conversation | | Channels | Channel ID | Channel ID + Team ID | | DMs | `conversations.open` | Proactive messaging with conversation reference | | Typing | `assistant.threads.setStatus` | `sendTypingActivity()` | | Reactions | `reactions.add` | Separate message with reaction | | Media | `files.uploadV2` | Attachments in activity | --- ## 13. Implementation Considerations ### Webhook vs Polling MS Teams uses webhooks exclusively (no polling option like Telegram). Need to: - Expose HTTP endpoint for Bot Framework - Handle activity validation (HMAC signature) - Consider tunneling for local dev (ngrok, Tailscale funnel) ### Proactive Messaging Unlike Slack where you can message any user, Teams requires: - User must have interacted with bot first, OR - Bot must be installed in team/chat, OR - Use Graph API with appropriate permissions ### Tenant Restrictions Enterprise Teams often restrict: - External app installations - Cross-tenant communication - Certain API permissions Config should support `tenantId` restriction. ### Cards and Adaptive Cards Teams heavily uses Adaptive Cards for rich UI. Consider supporting: - Basic text (markdown subset) - Adaptive Card JSON - Hero Cards for media --- ## Next Steps 1. **Research**: MS Teams Bot Framework SDK specifics 2. **Azure Setup**: Document bot registration process 3. **Implement**: Start with monitor.ts and basic send 4. **Test**: Local dev with ngrok/tunnel 5. **Docs**: Provider setup guide