Skip to content

Feature/teams bot#268

Closed
jmunckhof wants to merge 4 commits intoColeMurray:mainfrom
jmunckhof:feature/teams-bot
Closed

Feature/teams bot#268
jmunckhof wants to merge 4 commits intoColeMurray:mainfrom
jmunckhof:feature/teams-bot

Conversation

@jmunckhof
Copy link
Copy Markdown
Contributor

Summary

Adds a Microsoft Teams bot integration as a new Cloudflare Worker (@open-inspect/teams-bot). Users can @mention the bot in channels or DM it directly to trigger background coding sessions — same UX as the existing Slack bot but for Teams.

What it does

  • Message handling: Receives Bot Framework activities, validates JWT tokens against Microsoft's OpenID metadata, strips @mentions, and routes messages
  • Repo classification: Uses an LLM classifier (Anthropic) to determine the target GitHub repo from the user's message. Falls back to Adaptive Card dropdown when ambiguous
  • Session lifecycle: Creates sessions via control plane, sends prompts, and maps threads to sessions in KV. Follow-up messages in the same thread go to the existing session
  • Completion callbacks: Receives HMAC-signed callbacks from the control plane when the agent finishes, extracts results from session events, and posts a plain text reply
  • User preferences: settings / preferences command shows an Adaptive Card to pick model and reasoning effort, stored per-user in KV
  • Configurable typing indicators: typingMode setting (always / response / never) controls when typing indicators are shown
  • Session reset: reset / new command clears the current session

Changes across packages

Package What changed
teams-bot (new) Full Hono worker: message handler, JWT validator, Teams REST client, LLM repo classifier, completion extractor, Adaptive Cards, structured logger
shared Added TeamsCallbackContext, TeamsBotSettings, typingMode to shared types
control-plane Added typingMode validation in integration settings, teams config in resolved settings response, repos endpoint for bot access
web Added Teams integration settings page (model, reasoning effort, typing mode)
terraform New workers-teams.tf module, gated behind enable_teams_bot variable
.github/workflows Added teams-bot build step and Microsoft env vars to Terraform CI

Test coverage

60 unit tests across 7 files:

File Tests What's covered
index.test.ts 7 Health endpoint, JWT auth, message routing, deduplication, reset/settings commands, empty text handling
callbacks.test.ts 4 Payload validation, HMAC signature verification, missing secret handling
classifier/index.test.ts 11 Auto-select (single repo, channel association), LLM classification at all confidence levels, error fallback
classifier/repos.test.ts 13 Fetch + normalize, local/KV caching, fallback on failure, channel filtering, case-insensitive lookups
completion/cards.test.ts 12 Text rendering, reasoning effort suffix, failed sessions, artifacts, truncation, Create PR links
completion/extractor.test.ts 10 Event extraction, artifact fallback, tool call summaries, cursor pagination, token ordering
utils/repo.test.ts 3 Repo ID normalization

Setup

Requires an Azure Bot registration. Set enable_teams_bot = true in Terraform and provide:

  • MICROSOFT_APP_ID
  • MICROSOFT_APP_PASSWORD
  • MICROSOFT_TENANT_ID

See packages/teams-bot/README.md for full setup instructions.

Test plan

  • npm test -w @open-inspect/teams-bot — all 60 tests pass
  • npm run typecheck — no type errors
  • npm run build -w @open-inspect/teams-bot — builds successfully
  • Deploy with enable_teams_bot = true and test in Teams:
    • @mention bot in channel → creates session, replies with results
    • DM bot → creates session, follow-up messages go to same session
    • reset → clears session
    • settings → shows model/effort picker
    • Typing indicators respect typingMode setting

Adds a new teams-bot Cloudflare Worker that bridges Microsoft Teams with
Open-Inspect coding sessions. Includes Bot Framework JWT validation,
session lifecycle management, repo classification, completion callbacks,
user preference settings, and configurable typing indicators.
60 tests across 7 files covering callbacks, classifier, completion,
and main app endpoints.
Copy link
Copy Markdown
Owner

@ColeMurray ColeMurray left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Automated Review]

Overall this is a well-structured addition that follows existing bot patterns. Strong test coverage (60 tests) and clean integration with shared types, control plane, web UI, and Terraform.

One critical functional bug and one security issue detailed in inline comments below.

{
binding_name = "TEAMS_BOT"
service_name = "open-inspect-teams-bot-${local.name_suffix}"
}
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Automated Review] Critical — Completion callbacks will never reach the Teams bot.

This Terraform adds a TEAMS_BOT service binding to the control plane worker, but the control plane code never uses it. The CallbackNotificationService.getBinding() in src/session/callback-notification-service.ts has no case "teams" branch:

private getBinding(source: string | null): Fetcher | undefined {
  switch (source) {
    case "linear": return this.env.LINEAR_BOT;
    case "slack":  return this.env.SLACK_BOT;
    default:       return this.env.SLACK_BOT; // ← "teams" falls here
  }
}

When a prompt is sent with source: "teams" (as done in index.ts:151), the completion callback will be routed to the Slack bot instead of the Teams bot. The CallbackServiceEnv interface also lacks a TEAMS_BOT binding.

Fix needed in packages/control-plane/src/session/callback-notification-service.ts:

  1. Add TEAMS_BOT?: Fetcher to CallbackServiceEnv
  2. Add case "teams": return this.env.TEAMS_BOT; to getBinding()

Comment thread packages/teams-bot/src/callbacks.ts Outdated
const expectedHex = Array.from(new Uint8Array(expectedSig))
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
return signature === expectedHex;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Automated Review] Security — Use constant-time comparison for HMAC signatures.

This uses === for HMAC comparison which is vulnerable to timing attacks. The linear-bot and github-bot packages correctly use timingSafeEqual from @open-inspect/shared.

import { timingSafeEqual } from "@open-inspect/shared";
// ...
return timingSafeEqual(signature, expectedHex);

if (typeof payload.iss === "string" && issuer && payload.iss !== issuer) {
// Bot Framework may use different issuers for different channels
// Be lenient — accept any Microsoft-issued token
if (!payload.iss.includes("login.microsoftonline.com") && payload.iss !== issuer) {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Automated Review] Suggestion — Redundant inner condition.

The outer if already checks payload.iss !== issuer, so the inner payload.iss !== issuer is always true. This can be simplified:

if (typeof payload.iss === "string" && issuer && payload.iss !== issuer) {
  if (!payload.iss.includes("login.microsoftonline.com")) {
    log.warn("jwt.validation", { outcome: "invalid_issuer", iss: payload.iss });
    return false;
  }
}

Comment thread packages/teams-bot/src/index.ts Outdated
const integrationConfig = await getTeamsIntegrationConfig(env, repo.fullName, traceId);

// Show typing indicator for new sessions (after we have the config)
if (shouldShowTyping(integrationConfig.typingMode, "message")) {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Automated Review] Suggestion — This call is dead code.

shouldShowTyping(mode, "message") always returns false because the function only returns true when trigger === "response". With valid TeamsTypingMode values being "response" or "never", this branch can never fire. Same applies to line 632.

If this is intentional scaffolding for a future "always" mode, a brief comment would help. Otherwise consider removing it to avoid confusion.

* Adaptive Card for session started notification.
*/

export function buildSessionStartedCard(
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Automated Review] Nit — Dead code. buildSessionStartedCard is never imported anywhere. The README acknowledges it as "unused, kept for reference." In a brand new package, it's better to start without dead code — it can always be added back when needed.

* This file exists for organizational consistency with the adaptive-cards directory.
*/

export { buildCompletionText } from "../completion/cards";
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Automated Review] Nit — Unused re-export module. This file re-exports buildCompletionText from ../completion/cards but is never imported anywhere in the package. Consider removing.

Comment thread packages/teams-bot/src/types/index.ts Outdated
import type { TeamsCallbackContext } from "@open-inspect/shared";

// Keep backward-compatible alias
export type TeamsBotCallbackContext = TeamsCallbackContext;
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Automated Review] Nit — Unnecessary backward-compat alias. TeamsBotCallbackContext is defined as a "backward-compatible alias" for TeamsCallbackContext, but this is a brand new package with no prior consumers. This can be removed.

* plane to the Teams bot is available, so we can show typing during the full
* agent execution — not just the completion callback.
*/
export type TeamsTypingMode = "response" | "never";
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Automated Review] Nit — PR description mismatch. The PR description states typingMode supports always / response / never, but the TeamsTypingMode type only defines "response" | "never". Consider updating the PR description to match the implementation.

- Add TEAMS_BOT service binding to CallbackNotificationService so
  completion callbacks route to the Teams bot instead of Slack
- Use timingSafeEqual for HMAC signature verification
- Remove redundant issuer check in JWT validator
- Remove dead shouldShowTyping("message") calls and unused helper
- Delete unused session-progress.ts and completion.ts re-export
- Remove unnecessary TeamsBotCallbackContext backward-compat alias

Co-Authored-By: Claude Code <noreply@anthropic.com>
@ColeMurray
Copy link
Copy Markdown
Owner

closing due to inactivity

@ColeMurray ColeMurray closed this Mar 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants