From 576c6bb87058c93ef703d16a4e345c998214d2ab Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Thu, 27 Aug 2026 09:57:11 +0000 Subject: [PATCH 1/3] =?UTF-8?q?docs(adr):=20record=20ADR=200007=20spike=20?= =?UTF-8?q?finding=20=E2=80=94=20Q9=20resolved=20(stdlib=20JWT,=20no=20new?= =?UTF-8?q?=20deps)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Teams adapter spike (PR #4) resolved Open Question 9: msbotbuilder-go is rejected as unmaintained, and inbound JWT/JWKS validation was implemented with the standard library only (crypto/rsa), adding zero new dependencies. Recorded on main so the finding survives the spike branch. Remaining open questions still require live-tenant validation (issue #6); the ADR status stays Proposed until then. --- _Generated with [`mux`](https://github.com/coder/mux) • Model: `anthropic:claude-fable-5` • Thinking: `xhigh`_ --- docs/adr/0007-teams-adapter.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/adr/0007-teams-adapter.md b/docs/adr/0007-teams-adapter.md index c0055a1..db540f9 100644 --- a/docs/adr/0007-teams-adapter.md +++ b/docs/adr/0007-teams-adapter.md @@ -120,3 +120,21 @@ 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 **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. This is a deliberate deviation from this ADR's + "use a maintained `golang-jwt`" note, chosen to preserve the repo's zero-dependency, + stdlib-direct stance (Slack/Linear precedent). + +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 [issue #6](https://github.com/coder/chat/issues/6). From 447819820261994d3facf31e5c8ba9d6d25cc291 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Thu, 27 Aug 2026 10:07:41 +0000 Subject: [PATCH 2/3] =?UTF-8?q?docs(adr):=20address=20codex=20review=20?= =?UTF-8?q?=E2=80=94=20align=20Decision/PRD=20with=20stdlib=20choice,=20sc?= =?UTF-8?q?ope=20Q9,=20add=20.scratch=20tracker=20issue?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Decision now records the stdlib-only JWT/JWKS choice (no contradictory 'use golang-jwt/jwx' instruction left in ADR or PRD). - Spike Findings scoped: Q9's SDK-adoption decision is resolved; its live-token verification steps are superseded and carry over into the live-validation checklist. - Live validation now tracked in .scratch/teams-adapter/issues/01 per docs/agents/issue-tracker.md, with GitHub issue #6 as public mirror. --- _Generated with [`mux`](https://github.com/coder/mux) • Model: `anthropic:claude-fable-5` • Thinking: `xhigh`_ --- .scratch/teams-adapter/PRD.md | 2 +- .../issues/01-live-tenant-validation.md | 37 +++++++++++++++++++ docs/adr/0007-teams-adapter.md | 19 +++++++--- 3 files changed, 51 insertions(+), 7 deletions(-) create mode 100644 .scratch/teams-adapter/issues/01-live-tenant-validation.md diff --git a/.scratch/teams-adapter/PRD.md b/.scratch/teams-adapter/PRD.md index dfa0f93..40cb6c0 100644 --- a/.scratch/teams-adapter/PRD.md +++ b/.scratch/teams-adapter/PRD.md @@ -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. ```go // Illustrative shape only; not an implementation. diff --git a/.scratch/teams-adapter/issues/01-live-tenant-validation.md b/.scratch/teams-adapter/issues/01-live-tenant-validation.md new file mode 100644 index 0000000..daf9cf3 --- /dev/null +++ b/.scratch/teams-adapter/issues/01-live-tenant-validation.md @@ -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. diff --git a/docs/adr/0007-teams-adapter.md b/docs/adr/0007-teams-adapter.md index db540f9..7c78cc4 100644 --- a/docs/adr/0007-teams-adapter.md +++ b/docs/adr/0007-teams-adapter.md @@ -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 `@bot` 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. @@ -124,17 +124,24 @@ Rejected. The inbound ack/turn contract, endorsement enforcement, Markdown fidel ## 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 **Open Question 9**: +([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. This is a deliberate deviation from this ADR's - "use a maintained `golang-jwt`" note, chosen to preserve the repo's zero-dependency, - stdlib-direct stance (Slack/Linear precedent). + `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 [issue #6](https://github.com/coder/chat/issues/6). +in `.scratch/teams-adapter/issues/01-live-tenant-validation.md` (public tracking: +[issue #6](https://github.com/coder/chat/issues/6)). From 794f2e89a8404e7fbb88a26e6bdd05c7683dd31d Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Thu, 27 Aug 2026 10:15:27 +0000 Subject: [PATCH 3/3] =?UTF-8?q?docs(prd):=20update=20Teams=20implementatio?= =?UTF-8?q?n=20gate=20=E2=80=94=20live-token=20stdlib=20validation,=20not?= =?UTF-8?q?=20msbotbuilder-go=20evaluation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The PRD's final implementation gate still required a hands-on production-readiness evaluation of msbotbuilder-go, which the ADR 0007 spike already performed and rejected. The gate now requires live-token validation of the stdlib JWT/JWKS validator instead. --- _Generated with [`mux`](https://github.com/coder/mux) • Model: `anthropic:claude-fable-5` • Thinking: `xhigh`_ --- .scratch/teams-adapter/PRD.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.scratch/teams-adapter/PRD.md b/.scratch/teams-adapter/PRD.md index 40cb6c0..0c42bfb 100644 --- a/.scratch/teams-adapter/PRD.md +++ b/.scratch/teams-adapter/PRD.md @@ -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`.