Files
clawdbot/tmp/msteams-provider-research.md
Onur 01e737e90e docs: add MS Teams provider research document
Initial research and implementation guide for adding msteams as a new
messaging provider. Includes:

- Provider structure patterns from existing implementations
- Gateway integration requirements
- Config types and validation schemas
- Onboarding flow patterns
- MS Teams Bot Framework SDK considerations
- Files to create/modify checklist

This is exploratory work - implementation plan to follow.
2026-01-09 11:03:32 +01:00

16 KiB

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:

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<string, ChannelInfo>();
  const userCache = new Map<string, UserInfo>();
  const seenMessages = new Map<string, number>();

  // 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<void>((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)

// 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<void>;
  startSlackProvider: () => Promise<void>;
  stopSlackProvider: () => Promise<void>;
  // ... per provider
};

Lifecycle Management

// State tracking
let slackAbort: AbortController | null = null;
let slackTask: Promise<unknown> | 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

// 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)

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)

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<string, SlackChannelConfig>;  // Per-channel config
  actions?: SlackActionConfig;                    // Feature gating
  slashCommand?: SlackSlashCommandConfig;         // Command config
};

export type SlackDmConfig = {
  enabled?: boolean;
  policy?: DmPolicy;                              // "pairing" | "allowlist" | "open" | "disabled"
  allowFrom?: Array<string | number>;
  groupEnabled?: boolean;
  groupChannels?: Array<string | number>;
};

export type SlackChannelConfig = {
  enabled?: boolean;
  requireMention?: boolean;
  users?: Array<string | number>;                 // 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)

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)

// 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

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)

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<SlackProbe> {
  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)

export async function sendMessageSlack(
  to: string,
  message: string,
  opts: SlackSendOpts = {},
): Promise<SlackSendResult> {
  // 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)

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)

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)

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

import { Client } from "@microsoft/microsoft-graph-client";

// Pros: Simpler for basic messaging, direct API access
// Cons: Less rich features, manual auth handling

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

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