Skip to content

feat(channels): add native Microsoft Teams channel — full E2E implementation#724

Open
olbboy wants to merge 19 commits intonextlevelbuilder:devfrom
olbboy:feat/channels-teams-phase2
Open

feat(channels): add native Microsoft Teams channel — full E2E implementation#724
olbboy wants to merge 19 commits intonextlevelbuilder:devfrom
olbboy:feat/channels-teams-phase2

Conversation

@olbboy
Copy link
Copy Markdown

@olbboy olbboy commented Apr 6, 2026

Summary

Complete Microsoft Teams channel implementation for GoClaw — from Azure Bot Framework integration to sideload app package generation.

  • Teams channel via Bot Framework REST API (webhook-based, single-tenant & multi-tenant)
  • App package generator — CLI, HTTP API, and Web UI download button
  • DB instance factory — Teams instances created via UI persist and load on restart
  • Security hardened — 2 adversarial reviews, policy bypass fix, JWKS validation

What's Included

1. Teams Channel Core (internal/channels/teams/)

  • channel.go — Channel struct, New(), Start()/Stop() lifecycle
  • auth.go — JWT token validation against Microsoft JWKS endpoints (cached, rotated)
  • webhook.go — HTTP webhook handler for incoming Bot Framework activities
  • send.go — Outbound message delivery with retry, typing indicators, rate limiting
  • format.go — LLM markdown → Teams-compatible markdown sanitization
  • types.go — Bot Framework activity types, conversation references
  • factory.go — DB instance factory for InstanceLoader integration

2. App Package Generator (internal/channels/teams/appmanifest/)

  • generate.goGenerateZIP(opts) → manifest.json + icons ZIP (v1.19 schema)
  • generate_test.go — 23 unit tests (validation, truncation, Unicode, icons, ZIP structure)
  • Embedded default icons (192x192 color + 32x32 outline PNG)

3. CLI Command (cmd/teams_cmd.go)

```bash
goclaw teams app-package --name "My Bot" --bot-id UUID -o teams-app.zip
goclaw teams app-package --name "My Bot" --bot-id UUID --stdout > app.zip
```

4. HTTP API (internal/http/teams_app_package.go)

```
GET /v1/teams/app-package?name=Bot&bot_id=UUID
GET /v1/teams/app-package?name=Bot&instance_id=UUID (resolves bot_id from DB)
```
Response: application/zip with Content-Disposition: attachment

5. Web UI

  • Download button on Teams channel detail page
  • Teams registered in channel schemas, status utils, contact filters
  • i18n keys for en/vi/zh

6. Security & Hardening

  • JWT validation with JWKS key rotation and caching
  • Policy bypass fix (DM/group policy enforcement)
  • Mutex contention fix in concurrent message handling
  • JWKS fetch body limit increased to 4MB
  • PNG magic byte + 500KB cap for custom icons
  • Unicode-safe truncation (rune-based, not byte-based)
  • Webhook path deduplication (prevents mux.Handle panic)
  • Channel type guard on instance credential lookup

Architecture

```
Microsoft Teams → POST /webhooks/teams → JWT validation → webhook handler
→ parse activity → route to agent via message bus → agent response
→ format markdown → chunk if >28KB → send via Bot Framework REST API
```

Config-based channel (backward compat) + DB instance factory (UI-created instances).
Both coexist safely — webhook paths deduplicated in Manager.WebhookHandlers().

Files Changed (50 files, +3844 lines)

Area Files Key Changes
Channel core 8 .go + 4 _test.go Auth, webhook, send, format, types, factory
App package 2 .go + 2 _test.go + 2 .png Generator, HTTP handler, icons
CLI 1 .go `goclaw teams app-package` command
Gateway wiring 4 .go Server setter, factory registration, webhook dedup
Web UI 6 files Download button, schemas, i18n (en/vi/zh)
Docs 6 files Architecture, channels, changelog, HTTP API, journal
Config 2 .go TeamsConfig struct, env var overlay

Test Coverage

Suite Tests Status
Teams channel (auth, webhook, send, format) 19 Pass
App manifest generator 23 Pass
HTTP handler 14+ Pass
Race detector All suites Clean
Go build (PG + SQLite) - Clean
Go vet - Clean
Web UI (pnpm build) - Clean

E2E Verification

Tested on live GoClaw instance with real Azure Bot credentials:

  • Created Teams channel instance via Web UI
  • Downloaded app package ZIP — valid manifest.json with correct bot_id
  • CLI generated valid ZIP with `--stdout` and `-o` modes
  • HTTP API returned correct ZIP with proper Content-Type/Disposition headers
  • Unicode names (Vietnamese) preserved correctly in manifest
  • Long names truncated at 30 chars (rune-aware)

Test plan

  • `go test -race ./internal/channels/teams/...` passes
  • `go test -race ./internal/http/ -run TestTeamsApp` passes
  • `go build ./...` and `go build -tags sqliteonly ./...` clean
  • `cd ui/web && pnpm build` clean
  • CLI: `goclaw teams app-package --name "Bot" --bot-id UUID -o test.zip` produces valid ZIP
  • HTTP: `GET /v1/teams/app-package?name=Bot&bot_id=UUID` returns valid ZIP
  • Web UI: Download button visible on Teams channel detail page
  • No secrets in committed code

olbboy and others added 13 commits April 6, 2026 09:53
…REST API

Implements Teams channel following the Feishu webhook-based pattern:
- JWT validation with JWKS key cache (OpenID Connect metadata)
- Azure AD client credentials flow for reply tokens
- SSRF protection: serviceURL validated against known Bot Framework domains
- SingleTenant + MultiTenant bot type support
- Frontend schemas for channel creation/editing in UI
- Docs updated with Teams channel documentation
18 tests covering: config validation, JWT helpers, webhook parsing,
bot mention stripping, peer kind detection, serviceURL SSRF protection.

Fix: allow trafficmanager.net domains (real Teams serviceURL host).
Teams is config-only (no DB instance factory yet), so registration
must happen outside the instanceLoader guard. Extract to separate
registerTeamsChannel() function called unconditionally.
Bot Framework Connector tokens (issuer: api.botframework.com) don't
carry Azure AD "tid" claim. Only reject when tid is present AND
mismatches — prevents false rejection of legitimate Bot Framework
requests for SingleTenant bots.

Verified via live E2E test with Azure Bot Service.
…indings

Fixes from adversarial code review (2 critical, 5 high, 7 medium):

CRITICAL:
- Validate JWKS URI from OpenID metadata against Microsoft domain allowlist
- Restrict trafficmanager.net to smba.trafficmanager.net only (prevent token exfil)

HIGH:
- Add http.MaxBytesReader (1MB) on webhook body to prevent DoS
- Add io.LimitReader (1MB) on JWKS/OIDC fetch responses
- Add 5s cooldown on JWKS forced refresh to prevent thundering herd
- Add IsRunning() guard on webhook handler (503 after Stop)

MEDIUM:
- Add jwt.WithLeeway(30s) for clock skew tolerance
- Validate tenantID is a valid UUID at construction time
- Validate From.ID and Conversation.ID are non-empty before processing
- Add GroupPolicy to TeamsConfig + frontend schema
- Floor token expiry at 30s to prevent tight retry loop
- Check Conversation.ID in conversationUpdate before storing serviceURL
…nnel

Round 2 adversarial review fixes:

CRITICAL:
- Add CheckPolicy() call in handleMessage — DMPolicy/GroupPolicy were
  configured but never enforced at message time (silent auth bypass)
- Move storeServiceURL after policy check (don't cache URLs from rejected senders)

HIGH:
- Validate BotType enum (reject unknown values like "FooBar")
- Move HTTP fetches outside mutex in forceRefreshKeys — prevents blocking
  all concurrent JWT validations during JWKS refresh

MEDIUM:
- Disable HTTP redirect following in fetchJSON (defense-in-depth)
Add GOCLAW_TEAMS_BOT_ID, GOCLAW_TEAMS_BOT_PASSWORD, GOCLAW_TEAMS_TENANT_ID
env vars matching the pattern of other channels (Telegram, Discord, etc.).
Auto-enables Teams channel when bot_id + bot_password are set via env.
…, chunking

Phase 2 of Teams channel implementation:

1. Rate limiting + 429 retry (send.go):
   - Exponential backoff with jitter, 3 retries max
   - Retry-After header support, capped at 120s
   - Retries on 429/5xx, fails immediately on 4xx

2. Typing indicator (channel.go, webhook.go):
   - Reuses typing.Controller with 3s keepalive (Teams auto-expires in 3s)
   - Starts on inbound message, stops on outbound reply
   - Cleanup on channel Stop()

3. Markdown sanitization (format.go):
   - Teams renders markdown natively (textFormat: "markdown")
   - Strip HTML tags outside code blocks (XSS prevention)
   - Strip strikethrough markers (inconsistent across platforms)
   - Convert markdown tables to code fences for monospace

4. Message chunking (format.go, channel.go):
   - Split at paragraph > line > word > force boundaries
   - Code fence (```) continuity across chunks
   - 80KB limit (verified against official Microsoft docs)
   - Replaces previous 25KB truncation

39 unit tests pass. Verified against official Microsoft Learn documentation.
Bot Framework JWKS response is ~1.8MB (many RSA keys). The previous
1MB io.LimitReader truncated the response, causing json.Decode to fail
with "unexpected EOF". Discovered during live E2E testing.
Generate Teams-compatible sideload packages (manifest.json + icons ZIP)
from channel config via three surfaces:

- CLI: `goclaw teams app-package --name "Bot" -o teams-app.zip`
- HTTP: `GET /v1/teams/app-package?name=Bot&bot_id=xxx`
- Web UI: download button on Teams channel detail page

Core library at internal/channels/teams/appmanifest/ with embedded
default icons, PNG validation, Unicode-safe truncation, and v1.19
manifest schema. Includes unit + HTTP handler tests (37 total).

Also registers "teams" as valid channel type in instance CRUD handlers
and adds i18n keys for en/vi/zh.
…el loading

Register teams.Factory in instance loader so DB-created Teams instances
load on gateway restart with healthy status. Enables app package download
from DB instances. Config-based channel still handles actual messages.
…ld mapping

- Deduplicate webhook paths in Manager.WebhookHandlers() to prevent
  mux.Handle panic when config + DB Teams channels share same path
- Read bot_type/tenant_id from both credentials and config in factory
  (credentials priority, config fallback for UI-created instances)
- Add missing "slack", "zalo_personal" to IsDefaultChannelInstance
@olbboy
Copy link
Copy Markdown
Author

olbboy commented Apr 6, 2026

Superseded by comprehensive Teams channel PR combining Phase 1 + Phase 2 + App Package + Factory.

@olbboy olbboy closed this Apr 6, 2026
@olbboy olbboy reopened this Apr 6, 2026
@olbboy olbboy changed the title feat(channels): Teams Phase 2 — retry, typing, format, chunking feat(channels): add native Microsoft Teams channel — full E2E implementation Apr 6, 2026
claude added 6 commits April 7, 2026 14:43
UI select fields send "inherit" for block_reply when user selects
the default option. coerceStringBools only handled "true"/"false",
causing JSON unmarshal failure for all channels with inherit selected.
Delete the key so it becomes nil (= inherit gateway default).
UI select fields send "true"/"false"/"inherit" as strings for bool
config fields (block_reply, etc). Previously only coerced at load time,
meaning bad data persisted in DB and broke channel reload.

Now:
- Export CoerceStringBools for reuse across handlers
- Normalize at save time in both HTTP and WS create/update handlers
- Keep load-time coercion as fallback for existing bad data
- Handle "inherit" → delete key (nil = inherit gateway default)
When agent (LLM) calls config_permissions.grant with a bare chat ID as
scope (e.g. Teams conversation ID), auto-replace with the full
"group:{channel}:{chatID}" format from the caller's context. Prevents
scope mismatch between grant and check, which caused /addwriter to
silently fail on Teams group chats.
When no file writers exist yet for a group scope, allow the first caller
instead of denying. Prevents deadlock where no one can grant permissions
because no one has permissions. Mirrors Telegram /addwriter bootstrap.
When no file writers are configured for a group, inject a system prompt
telling the agent it has full tool access. Prevents LLM from
self-restricting based on training assumptions about group permissions.
- CheckFileWriterPermission: handle ListFileWriters error properly —
  on DB error, fall through to normal permission check instead of
  granting bootstrap access to all users
- Add warning log when duplicate webhook path is skipped
- Fix duplicate comment in gateway_channels_setup.go
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