Feature/teams bot#268
Conversation
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.
ColeMurray
left a comment
There was a problem hiding this comment.
[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}" | ||
| } |
There was a problem hiding this comment.
[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:
- Add
TEAMS_BOT?: FetchertoCallbackServiceEnv - Add
case "teams": return this.env.TEAMS_BOT;togetBinding()
| const expectedHex = Array.from(new Uint8Array(expectedSig)) | ||
| .map((b) => b.toString(16).padStart(2, "0")) | ||
| .join(""); | ||
| return signature === expectedHex; |
There was a problem hiding this comment.
[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) { |
There was a problem hiding this comment.
[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;
}
}| const integrationConfig = await getTeamsIntegrationConfig(env, repo.fullName, traceId); | ||
|
|
||
| // Show typing indicator for new sessions (after we have the config) | ||
| if (shouldShowTyping(integrationConfig.typingMode, "message")) { |
There was a problem hiding this comment.
[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( |
There was a problem hiding this comment.
[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"; |
There was a problem hiding this comment.
[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.
| import type { TeamsCallbackContext } from "@open-inspect/shared"; | ||
|
|
||
| // Keep backward-compatible alias | ||
| export type TeamsBotCallbackContext = TeamsCallbackContext; |
There was a problem hiding this comment.
[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"; |
There was a problem hiding this comment.
[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>
|
closing due to inactivity |
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
settings/preferencescommand shows an Adaptive Card to pick model and reasoning effort, stored per-user in KVtypingModesetting (always/response/never) controls when typing indicators are shownreset/newcommand clears the current sessionChanges across packages
teams-bot(new)sharedTeamsCallbackContext,TeamsBotSettings,typingModeto shared typescontrol-planetypingModevalidation in integration settings, teams config in resolved settings response, repos endpoint for bot accesswebterraformworkers-teams.tfmodule, gated behindenable_teams_botvariable.github/workflowsTest coverage
60 unit tests across 7 files:
index.test.tscallbacks.test.tsclassifier/index.test.tsclassifier/repos.test.tscompletion/cards.test.tscompletion/extractor.test.tsutils/repo.test.tsSetup
Requires an Azure Bot registration. Set
enable_teams_bot = truein Terraform and provide:MICROSOFT_APP_IDMICROSOFT_APP_PASSWORDMICROSOFT_TENANT_IDSee
packages/teams-bot/README.mdfor full setup instructions.Test plan
npm test -w @open-inspect/teams-bot— all 60 tests passnpm run typecheck— no type errorsnpm run build -w @open-inspect/teams-bot— builds successfullyenable_teams_bot = trueand test in Teams:reset→ clears sessionsettings→ shows model/effort pickertypingModesetting