Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .scratch/teams-adapter/PRD.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ This slice is a single-platform vertical that proves Teams against the existing
- Enforce all documented checks: Bearer scheme; valid JWT; `iss == https://api.botframework.com`; `aud == bot Microsoft App ID`; validity window with 5-minute clock skew; RS256 signature against a key in the keys doc; token `serviceUrl` claim matches `Activity.serviceUrl`.
- Enforce channel endorsement: if `msteams` requires endorsement, the signing key's `endorsements` must include `msteams` or return HTTP 403. (Exact enforcement is spike-required.)
- Cache the JWKS keys (>=24h) and refresh on cache miss / key rotation. Never expose a config flag to disable validation. Return HTTP 403 on any validation failure.
- Use a maintained JWT/JWKS library (current `golang-jwt` / `lestrrat-go/jwx`), not the versions pinned by `msbotbuilder-go`.
- Implement JWT/JWKS validation with the standard library only (`crypto/rsa` over a public key rebuilt from the JWK `n`/`e`); no JWT library dependency. Resolved by the ADR 0007 spike (see its Spike Findings) — `golang-jwt`/`jwx` proved unnecessary and `msbotbuilder-go`'s pinned versions are not used.
Comment thread
ThomasK33 marked this conversation as resolved.

```go
// Illustrative shape only; not an implementation.
Expand Down Expand Up @@ -172,4 +172,4 @@ The load-bearing patterns hold without change: the small **Adapter** interface a

The single biggest divergence from the existing adapters is auth shape: inbound is JWT-over-JWKS rather than a shared-secret HMAC, and outbound replies are separate authenticated REST calls rather than a webhook response body. The biggest **Thread ID** divergence is that Teams must persist `serviceUrl` + `tenantId` + `conversation.id` to post later, which is heavier than Slack but exactly what the opaque-**Thread ID** contract was designed to allow.

Implementation must not begin until a spike confirms the unverified specifics enumerated in the ADR's Open Questions: exact `msteams` ack/turn semantics and timeout, whether any body-based reply shortcut exists, endorsement enforcement, the deployment model (single/multi-tenant/managed-identity), Markdown subset fidelity, `serviceUrl`/`conversation.id` persistence stability, proactive-install prerequisites, RSC mention behavior, the canonical `Actor.ID` key, and a hands-on production-readiness evaluation of `msbotbuilder-go`'s `connector/auth` against all inbound checks.
Implementation must not begin until a spike confirms the unverified specifics enumerated in the ADR's Open Questions: exact `msteams` ack/turn semantics and timeout, whether any body-based reply shortcut exists, endorsement enforcement, the deployment model (single/multi-tenant/managed-identity), Markdown subset fidelity, `serviceUrl`/`conversation.id` persistence stability, proactive-install prerequisites, RSC mention behavior, the canonical `Actor.ID` key, and live-token validation of the stdlib JWT/JWKS validator against real Teams-issued tokens (`msbotbuilder-go` was evaluated and rejected by the code spike — see ADR 0007 Spike Findings — so no hands-on evaluation of its `connector/auth` remains). The code spike exists (`spike/msteams-adapter`, PR #4); the remaining live-tenant checks are tracked in `.scratch/teams-adapter/issues/01-live-tenant-validation.md`.
37 changes: 37 additions & 0 deletions .scratch/teams-adapter/issues/01-live-tenant-validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# 01 - Live-tenant validation of the Teams adapter spike

Status: ready-for-human

## Summary

Validate the `spike/msteams-adapter` implementation (PR #4) against a real Azure Bot
resource and Teams tenant so ADR 0007 can move from Proposed to Accepted and the adapter
can land as experimental.

Public tracking: https://github.com/coder/chat/issues/6

## Tasks

Work through the `spike-required` markers in `adapters/msteams` (the ADR 0007 Open
Questions, now the live test plan), including:

- Inbound ack semantics and the real turn timeout for the `msteams` channel.
- Reply delivery as a separate Connector REST call (no body-reply shortcut).
- Exact channel-endorsement rule (the spike fails closed when `msteams` is absent).
- Single-tenant Azure Bot resource specifics (token URL, `aud`/`iss`).
- The stdlib JWT/JWKS validator against real Teams-issued tokens (carried over from
Open Question 9 after `msbotbuilder-go` was rejected).
- Teams Markdown fidelity, `serviceUrl`/`conversation.id` persistence stability,
proactive-posting prerequisites, mention behavior, canonical `Actor.ID`, and
`Activity.id` dedupe stability.

## Acceptance

- Every `spike-required` marker is confirmed or corrected against a live tenant.
- ADR 0007 flips to Accepted and the adapter lands as experimental.

## Comments

Created while recording the ADR 0007 spike findings on main (PR #14), so the remaining
live validation is tracked in this repo's `.scratch` issue tracker per
`docs/agents/issue-tracker.md`, with GitHub issue #6 as the public mirror.
27 changes: 26 additions & 1 deletion docs/adr/0007-teams-adapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Build a Teams **Platform Adapter** under `adapters/msteams` (adapter name `mstea
The adapter will:

- Expose a **Webhook Handler** at the bot messaging endpoint that decodes the inbound `Activity` as a **Supported Platform Shape** (permissive unknown-field handling), validates the inbound JWT, normalizes a `message` `Activity` into a runtime **Event**/**Message**/**Actor**, and hands it to **Runtime Dispatch**. Non-`message` activity types are **Ignored Events** in this slice; `invoke` activities are out of scope here -- Teams `invoke` is a transport that carries both command-style invokes (-> **Command Event**, ADR 0003) and card-action invokes (-> **Interaction Event**, ADR 0004).
- Own **inbound** JWT/JWKS validation enforcing all documented checks (Bearer, valid JWT, `iss`, `aud == App ID`, validity window with 5-minute skew, RS256 against the keys doc, `serviceUrl`-claim match), plus channel endorsement (return HTTP 403 when an `msteams` activity's signing key omits the `msteams` endorsement). JWKS keys are cached (>=24h) and refreshed on rotation. There is no flag to disable validation. Validation failures return HTTP 403. Use a maintained `golang-jwt` / `lestrrat-go/jwx`, not the versions pinned by `msbotbuilder-go`.
- Own **inbound** JWT/JWKS validation enforcing all documented checks (Bearer, valid JWT, `iss`, `aud == App ID`, validity window with 5-minute skew, RS256 against the keys doc, `serviceUrl`-claim match), plus channel endorsement (return HTTP 403 when an `msteams` activity's signing key omits the `msteams` endorsement). JWKS keys are cached (>=24h) and refreshed on rotation. There is no flag to disable validation. Validation failures return HTTP 403. JWT/JWKS validation is implemented with the standard library only (`crypto/rsa`; see Spike Findings) — no JWT library is added.
- Own **outbound** `client_credentials` token minting (`scope=https://api.botframework.com/.default`), cached in adapter process memory and refreshed lazily before expiry. **Runtime State** is not expanded to store adapter credentials, matching the Linear app-actor token-cache decision (ADR 0001).
- Mint an opaque **Thread ID** as a versioned serialization of the minimal `conversationReference` -- at least `{serviceUrl, conversation.id, tenantId, bot.id, channelId}` -- so out-of-webhook and proactive posting survive process restarts. `serviceUrl` is refreshed from each inbound `Activity` because Microsoft warns it can change. `ValidateThreadID` decodes this into a `ThreadRef` (`Adapter: msteams`, `Tenant: tenantId`, `Channel: conversation.id`, `Direct` from personal scope, `Raw` = stored `conversationReference`), so **Thread Handle** reconstruction works.
- Normalize: `Event.Adapter = msteams`; `Event.Tenant = conversation.tenantId`; `Event.ID = Activity.id`; `Event.Raw = Activity` (**Platform Escape Hatch**). `Message.Text = Activity.text` with the leading bot `<at>@bot</at>` stripped; `Message.Mentioned` from `entities[]`; inbound **Actor** from `from` (**Bot Kind** human); `BotActor()` from `recipient` (**Bot Kind** bot). `OnNewMention` fires from the bot's presence in `entities[]` Mention objects, never from text matching.
Expand Down Expand Up @@ -120,3 +120,28 @@ Rejected for this slice. The Slack precedent is a **Single-Install Adapter**, an
### Implement now and verify behavior during implementation

Rejected. The inbound ack/turn contract, endorsement enforcement, Markdown fidelity, `serviceUrl`/`conversation.id` persistence stability, proactive-install prerequisites, and the `msbotbuilder-go` auth implementation are documentation-only or unverified. Committing code before a spike confirms them risks building on wrong assumptions about the `msteams` channel. The decision is to design now and gate implementation on the spike.

## Spike Findings

A code spike of the adapter exists on the `spike/msteams-adapter` branch
([PR #4](https://github.com/coder/chat/pull/4)). It resolved the SDK-adoption decision in
**Open Question 9**:

- **`msbotbuilder-go` is not adopted.** It is rejected as unmaintained (dormant for years,
superseded transitive deps), confirming the assessment under Alternatives Considered.
- **Inbound JWT/JWKS validation is standard library only.** The spike implements every
mandatory inbound check with `crypto/rsa` over a public key rebuilt from the JWK
`n`/`e`, so the otherwise zero-dependency module gains no JWT library at all —
`golang-jwt`/`jwx` proved unnecessary. The Decision above records this stdlib-only
choice, which preserves the repo's zero-dependency, stdlib-direct stance (Slack/Linear
precedent).

Q9's remaining verification steps are superseded rather than resolved: with
`msbotbuilder-go` rejected, exercising *its* auth against a real Teams-issued token is
moot, and validating the replacement stdlib validator against real tokens carries over
into the live-validation checklist below.

The remaining Open Questions still require live validation against a real Azure Bot
resource and Teams tenant before this ADR moves to Accepted; that validation is tracked
in `.scratch/teams-adapter/issues/01-live-tenant-validation.md` (public tracking:
[issue #6](https://github.com/coder/chat/issues/6)).