@open-everywhere/provider-teams 0.1.4 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/NOTICE +34 -4
  2. package/README.md +1 -1
  3. package/agent/app-manifest.ts +133 -0
  4. package/agent/channels/teams-engagement.ts +1 -1
  5. package/agent/channels/teams-front.ts +1 -1
  6. package/agent/channels/teams-graph-boundary.ts +14 -55
  7. package/agent/channels/teams-graph-roles.ts +62 -0
  8. package/agent/channels/teams-read.ts +1 -1
  9. package/agent/channels/teams.ts +4 -1
  10. package/agent/docs-generated.ts +1 -1
  11. package/agent/ops.ts +41 -3
  12. package/agent/register.ts +2 -2
  13. package/dist/agent/app-manifest.d.ts +23 -0
  14. package/dist/agent/app-manifest.d.ts.map +1 -0
  15. package/dist/agent/app-manifest.js +113 -0
  16. package/dist/agent/app-manifest.js.map +1 -0
  17. package/dist/agent/channels/teams-engagement.js +1 -1
  18. package/dist/agent/channels/teams-engagement.js.map +1 -1
  19. package/dist/agent/channels/teams-front.js +1 -1
  20. package/dist/agent/channels/teams-front.js.map +1 -1
  21. package/dist/agent/channels/teams-graph-boundary.d.ts +2 -22
  22. package/dist/agent/channels/teams-graph-boundary.d.ts.map +1 -1
  23. package/dist/agent/channels/teams-graph-boundary.js +8 -51
  24. package/dist/agent/channels/teams-graph-boundary.js.map +1 -1
  25. package/dist/agent/channels/teams-graph-roles.d.ts +25 -0
  26. package/dist/agent/channels/teams-graph-roles.d.ts.map +1 -0
  27. package/dist/agent/channels/teams-graph-roles.js +58 -0
  28. package/dist/agent/channels/teams-graph-roles.js.map +1 -0
  29. package/dist/agent/channels/teams-read.js +1 -1
  30. package/dist/agent/channels/teams-read.js.map +1 -1
  31. package/dist/agent/channels/teams.d.ts.map +1 -1
  32. package/dist/agent/channels/teams.js +4 -1
  33. package/dist/agent/channels/teams.js.map +1 -1
  34. package/dist/agent/docs-generated.js +1 -1
  35. package/dist/agent/docs-generated.js.map +1 -1
  36. package/dist/agent/ops.d.ts +2 -2
  37. package/dist/agent/ops.d.ts.map +1 -1
  38. package/dist/agent/ops.js +27 -3
  39. package/dist/agent/ops.js.map +1 -1
  40. package/dist/agent/register.js +2 -2
  41. package/dist/agent/register.js.map +1 -1
  42. package/package.json +12 -4
@@ -10,6 +10,6 @@ export const PROVIDER_DOCS: readonly DocEntry[] = [
10
10
  path: "docs/deploy/surfaces/teams.md",
11
11
  title: "Microsoft Teams",
12
12
  summary: "Run Open Everywhere as a Microsoft Teams bot: @mention it in a team channel, drop it into a group chat, or DM it 1:1. It acts under one bot identity shared by the deployment — never under a member's account and never with a member's…",
13
- markdown: "# Microsoft Teams\n\nRun Open Everywhere as a Microsoft Teams bot: `@mention` it in a team channel, drop it into a group chat, or DM it 1:1. It acts under one bot identity shared by the deployment — never under a member's account and never with a member's permissions — with per-conversation memory.\n\nThis page is the Teams-specific setup runbook. For the concept of a *surface* and how surfaces plug in, see [Surfaces](../../extending/surfaces.md). For how memory is scoped per tenant and conversation, see [Memory](../../capabilities/memory.md). For how Teams' feature set compares to the other surfaces — including which features are built but not yet verified against a live tenant — see the [surface support matrix](./support-matrix.md).\n\nTeams is the richest native fit for the silo model — its `tenant → team → channel` hierarchy maps directly onto `org → workspace → surface`, with no impedance mismatch.\n\n## How the silo model maps\n\nOpen Everywhere's memory is [opt-in domains and silos](../../capabilities/memory.md), not an\nautomatic per-tenant tier stack. A Teams conversation is a **surface**; the tenant is the\nnatural `workspace` identity key many channels' surface keys share, but sharing memory across\nthem still needs an explicit `domain` (or the `org` pseudo-domain fallback) — nothing pools\nautomatically, and (see \"Trust\" below) `visibility` is *not* what gates it.\n\n| Open Everywhere concept | Teams concept | Source field |\n| --- | --- | --- |\n| `org` (deployment tenant) | the Open Everywhere deployment (one org today, from `deploymentIdentity.org`) | tracked config |\n| `workspace` (identity key) | the **Microsoft tenant** (the Entra/Azure AD org) | `tenant_id` |\n| `surface` (partition key) | the **conversation** — a team channel, a group chat, or a 1:1 | `conversation_id` |\n| `mode` | `channel` (team channel) · `mpim` (group chat) · `dm` (1:1) | `scope` |\n| `visibility` | `public` only for a confirmed team-wide channel; else `private` | `membership_type` |\n| `trust` | `internal` only for a positively-verified in-tenant channel; else `external` | `membership_type` + `tenant_id` |\n\n`scope` maps via `teamsMode`: `personal → dm`, `groupChat → mpim`, `channel → channel`.\n\n**Visibility is tracked but isn't the memory gate — trust is (below).** A team channel is\nonly treated as **public** when `membershipType == \"standard\"` — the org/team-wide channel\neveryone on the team can see; everything else (1:1 chats, group chats, a `private` or\n`shared` channel, or an unconfirmed one) is `private`. But a **private** in-tenant channel\nstill shares its domain's or the org's brain exactly like a public one does — visibility just\nisn't the thing `teamsTrust` reads. The catch is the *positive-confirmation* requirement\n`teamsVisibility` and `teamsTrust` both share: inbound Teams activities almost never carry\n`membershipType` (it's a Graph/Teams-API property, not a guaranteed field on a Bot Framework\nActivity), so both default `private`/`external` until a Graph-API enrichment supplies it —\ncorrect-but-conservative rather than assuming either bit.\n\n**Trust fails closed too.** Trust is the [memory gate](../../capabilities/memory.md): an `external` surface gets read-only `public` memory at most, never an internal domain or the org brain. Only positive facts produce a verdict (`teamsTrust`):\n\n- `membershipType == \"shared\"` — a Teams **Shared Channel**, which can include people from *other* tenants (the Teams analog of Slack Connect) — is `external`, always. No override reaches it.\n- `membershipType == \"standard\"` or `\"private\"` with a known `tenant_id` is a positively-identified in-tenant channel — `internal`.\n- **Anything unverifiable** — absent `membershipType` (the common case), an unrecognised value, or a missing tenant id — produces NO verdict, so the surface falls through to the next rung of the trust chain. With nothing declared it binds `external`. **Two things promote it, not one.** A **register declaration** covering the surface (`defineSurface({ key: \"teams/<tenantId>\", trust: \"internal\" })`, the section below) binds it `internal` — and a declaration counts at any rung, so a tenant line covers every room under the tenant. A **single-tenant deployment** can instead use the blanket `OE_TRUST=internal` (the same escape hatch Discord/Linear use). Both sit BELOW the platform verdict in `identity/binding.ts`, so neither overrides a positive `external` one; a `public` declaration outranks both and demotes. What neither does is fence an ambiguous activity out of the org brain: once you declare the tenant, a room with no `membershipType` reaches it. Weigh that reach before you pick the grain.\n- 1:1 and group chats are personal silos (the channel-trust gate doesn't apply), but the **DM-partner gate** still fails closed: Teams doesn't positively verify who the 1:1 partner is, so a DM gets the per-user personal store plus read-only `public` — not the `dm`-shared layer.\n\n### Vouching a Teams surface — `agent/identity/surfaces.ts`\n\nTeams declares its vouched surfaces where every other platform does: `agent/identity/surfaces.ts`,\nconfig-as-code reviewed by pull request. No per-surface vouch is an environment variable; the only\nenv trust lever is the blanket `OE_TRUST=internal` described above. A surface key\ncarries your Microsoft 365 tenant id, which is an identifier, not a credential — the same class as\nthe Discord guild id and the GitHub org already in that file. Note what that still costs on a public\nrepository: the id names your organisation, permanently, because the history goes public with the\ntree. That is a disclosure to weigh, not a secret to protect.\n\n**Declare the tenant. That is the vouch.**\n\n```ts\ndefineSurface({ key: \"teams/<tenantId>\", trust: \"internal\" }),\n```\n\nA declaration counts at **any rung**. Trust resolution is most-specific-wins with a fallback to the\ntenant, so this one line makes every channel and thread under that tenant bind `internal` — the same\nworkspace-grain shape the Discord guild and GitHub org lines use. Declare a single channel instead\n(`teams/<tenantId>/<threadId>`) when you want to narrow it to one room.\n\n**One line serves both planes, the cross-surface single window included.** The Teams single-window\nprivacy floor reads `isVouchedSurface`, and that reader walks the declaration rungs like every other\ntrust read (**OPE-1521**), so a channel covered by the tenant vouch both binds `internal` for memory\nand joins a shared cross-surface window.\n\nIt did not always. The floor used to be an exact-key match no tenant declaration satisfied, so a\ntenant line kept boot green while the channel quietly failed to join the window — one register, two\ngrain semantics, and nothing said so. If you are reading an older deploy's config, that is why a\nsecond channel-grain line sits beside a tenant line. Such a line changes no verdict now, and this\ndeployment's own config dropped its copy (**OPE-1533**).\n\n`OE_TRUST=internal` opens that floor too, regardless of declaration grain — it is the deploy-wide\nblanket described above, and it satisfies the window check without any per-surface vouch.\n\n**What a tenant declaration reaches.** Every room under it, including a cross-tenant\nConnect/**shared** channel that can carry people from other organisations. Inbound activities usually\nomit `membershipType` (see below), so trust falls back to the vouch rather than to a positive\nin-tenant signal. Declare the tenant where you trust the tenant; declare single channels where you do\nnot.\n\n**Narrowing one whole team out of a tenant.** A Teams channel keys as `teams/<tenantId>/<threadId>`,\nand the team is nowhere in that key, so a **group rung** names it instead:\n\n```ts\ndefineSurface({ key: \"teams/<tenantId>\", trust: \"internal\" }),\ndefineSurface({ key: \"teams/<tenantId>/team:<teamId>\", trust: \"public\" }),\n```\n\nThe group rung outranks the tenant entry, so every channel of that team binds `external` and stays\nout of the shared window, while the rest of the tenant keeps the tenant line. The window floor reads\nthe same rung as the binding and memory planes (**OPE-1538**); before that it skipped the tier and\nadmitted a room the other two planes had already demoted.\n\n🔴 **`preflight` reports a working Teams container line as a dead declaration.** It folds traffic from\naudit rows, which name one workspace and one room and no rung above them. Two Teams shapes name a\ncontainer:\n\n- the **group rung** above — no traffic row carries the team;\n- a **channel line** (`teams/<tenantId>/<channelId>`), because a Teams room keys on the CONVERSATION\n id. A reply chain keys as `<channelId>;messageid=<root>`, and only a post at the channel root keys\n as the channel itself.\n\nBoth report as dead declarations while they work. Where such a line is the only cover for the rooms\nunder it, those rooms also report as undeclared traffic — a red that flips the ops report's `ready`\nto false. Do not delete either line on that advice. Deleting the group rung above returns every\nchannel of that team to the tenant's `internal`; deleting a channel line returns every reply chain in\nit to `external`. **OPE-1560** carries the audit-row half that closes this, and the\ndead-declaration check says so itself.\n\n**Fail closed.** A surface with no entry stays `external`. There is no default vouch.\n\n**A vouch does not put a channel in a memory domain.** It says the channel is `internal`; which\nchannels share a brain is a separate decision in `agent/memory/config/domains/<name>.ts`. The two lists stay\nseparate on purpose — a window roster derived from the vouch list would enroll every channel you\nvouch in one cross-surface window and one silo, and it would make the window's own un-vouched-source\ncheck vacuous. Write the key in both places.\n\nThis vouch is precise; `OE_TRUST=internal` is the blunt single-tenant alternative described above,\nwhich vouches every unverifiable surface on every platform at once.\n\n## Prerequisites\n\n- An **Azure subscription** and **Entra ID (Azure AD)** admin rights — you need to create an Azure Bot resource and an app registration, and someone with admin rights to upload/approve the Teams app.\n - **No work tenant, or no admin rights in yours?** Start a **[Microsoft 365 Business Basic 1-month free trial](https://www.microsoft.com/microsoft-365/business/microsoft-365-business-basic)** — a full tenant where you *are* Global Admin, with 25 free seats for the trial month; after that, one Business Basic seat (a few £/$ a month, monthly commit) keeps it alive, and free **guest** accounts cover extra test users. Two day-one moves: turn OFF recurring billing (the trial otherwise auto-converts to an *annual* term) and enable custom app upload (Teams admin center → Teams apps → Setup policies — can take up to 24h to appear/propagate on a fresh tenant). Mind the SKU: the cheaper \"Business Basic *(no Teams)*\" variant won't work. (The old [Microsoft 365 Developer Program](https://developer.microsoft.com/microsoft-365/dev-program) sandbox is closed to unaffiliated developers since 2024 — it now requires a Visual Studio Pro/Enterprise *standard* subscription or a Microsoft partner program; Microsoft's own Teams docs point everyone else at the trial/purchase route. An Azure subscription for the Bot resource is still needed; the free F0 tier covers it and the Teams channel is free.)\n- A **deployed Open Everywhere instance** — see the [docs index](../../README.md) and [setup runbook](../setup-runbook.md). Teams requires a **public HTTPS endpoint**: the Bot Framework Connector calls *your* messaging endpoint, so it cannot reach `localhost`.\n - For local development, expose your instance with a tunnel — e.g. a [VS Code dev tunnel](https://learn.microsoft.com/azure/developer/dev-tunnels/), `ngrok`, or Cloudflare Tunnel — and use the public `https://…` URL as the messaging endpoint.\n\n## Step 1 — Register the Azure Bot + Entra app\n\n1. In the [Azure Portal](https://portal.azure.com), **Create a resource → Azure Bot** (the modern replacement for *Bot Channels Registration*; either works).\n2. For the bot's identity, choose **Single-tenant** and set `MICROSOFT_TENANT_ID` to that tenant's Directory ID. (Microsoft deprecated new **multi-tenant** Azure Bot creation after **31 July 2025** — fresh registrations are Single-tenant or User-Assigned Managed Identity only, and this integration uses the app-id + secret flow, i.e. Single-tenant. `MICROSOFT_TENANT_ID` is therefore effectively required for any new bot, not optional.) Azure either creates a new **Entra app registration** for you or lets you point at an existing one.\n3. Note the app registration's **Application (client) ID** — this is `MICROSOFT_APP_ID`.\n4. In the app registration, go to **Certificates & secrets → New client secret**, create one, and copy its **Value** immediately — this is `MICROSOFT_APP_PASSWORD`. (Azure calls it a \"client secret\"; the Bot Framework calls it the \"app password\" — same thing.)\n5. For a single-tenant bot, copy the **Directory (tenant) ID** — this is `MICROSOFT_TENANT_ID`.\n6. On the Azure Bot resource, open **Configuration** and set the **Messaging endpoint** to:\n\n ```\n https://<your-deploy-url>/eve/v1/teams\n ```\n\n (`/eve/v1/teams` is the route Open Everywhere's Teams channel listens on.)\n7. On the Azure Bot resource, open **Channels** and **enable the Microsoft Teams channel**.\n\n## Step 2 — Package the Teams app\n\nThe Azure Bot exists, but Teams needs an **app package** (a manifest + icons) to surface the bot inside Teams. A ready-made, templated package ships in the repo at [`teams-app/`](../../../teams-app/) — a `manifest.json` (already scoped for `team` + `groupChat` + `personal`) plus placeholder icons and a build/install README.\n\n1. Fill the package: replace `{{MICROSOFT_APP_ID}}` (your `MICROSOFT_APP_ID`, used for both `id` and `bots[].botId`), `{{DEPLOY_DOMAIN}}` (your deploy host), and `{{DEVELOPER_NAME}}`, then zip it flat — the `teams-app/README.md` has a one-liner. Replace the placeholder `color.png` / `outline.png` with real brand icons before any org-wide install.\n - Prefer a GUI? The [Developer Portal for Teams](https://dev.teams.microsoft.com/) (formerly *App Studio*) → **Apps → New app** → **App features → Bot** → select the bot by its app ID does the same thing interactively.\n - The manifest's `privacyUrl` and `termsOfUseUrl` are **not** placeholders: they point at `/privacy` and `/terms` on your own deploy host, and **the deployment serves both**. Filling `{{DEPLOY_DOMAIN}}` is all they need. The TEXT of both is yours — declare it as `policyPages.privacy` and `policyPages.terms` in `agent/settings/settings.ts` (see [the configuration reference](../configuration-reference.md#agent-settings-behaviour-config-as-code)). Undeclared, each route still answers `200` with a short page saying no document is published yet, so a sideload never breaks; publish real text before an **org-wide** upload, where a reviewer reads them.\n2. The package already declares the three **scopes** — `team` (channel surfaces), `groupChat` (mpim surfaces), `personal` (dm surfaces). Trim them in `manifest.json` if you want a narrower install.\n3. The package declares three **RSC (resource-specific consent) permissions** (all Application), under `authorization.permissions.resourceSpecific`, paired with a `webApplicationInfo` block (`id` = your app id, `resource` = any URI):\n - **`ChannelMessage.Read.Group`** — read a **team's channel** messages, and receive *every* channel message activity rather than only @mentions. Granted by a **team owner** at install time.\n - **`ChatMessage.Read.Chat`** — the chat analogue: read a **chat's** messages (group chats, and the chats behind meetings) and receive every chat message activity. Granted by any **chat member** authorised to install apps in that chat.\n - **`ChannelMember.Read.Group`** — read a **team channel's** membership list. Granted by a **team owner** at install time, same as `ChannelMessage.Read.Group`. It powers the routine fire-eligibility room check (see [Proactive routines](#proactive-routines)) — see that section for what changes when it's missing.\n\n None of the three lives in the Azure app registration's API-permissions blade — RSC exists only in the Teams manifest — and none goes through Microsoft's Protected-API approval queue (which is what `ChannelMessage.Read.All` would need). Keeping `ChannelMessage.Read.Group` is also what keeps Microsoft's own per-team boundary on channel reads; see [Which boundary governs your reads](#which-boundary-governs-your-reads). **@mentions reach the bot without any of them**, so if you only want mention-driven behaviour you can remove *all three* along with the `authorization` and `webApplicationInfo` blocks; keep them for ambient watching, [channel/thread history reads](#reading-channelthread-history), and enforced [routine fire-eligibility](#proactive-routines) checks.\n\n > **What `ChatMessage.Read.Chat` does and doesn't change.** It is what makes Microsoft *deliver* non-mention group-chat messages to OE at all — without it they never arrive, so nothing downstream could ever act on them. It does **not** make OE answer everything a chat says. OE's own ingress admits a non-mention chat message only when the chat is **engaged** — somebody `@mentioned` the bot there and has not asked it to stop following. An un-engaged chat wakes nothing — except an answer to a question the agent itself parked there, which is always admitted. The cross-surface window's privacy floor still admits `channel` scope only (a group chat is a per-person surface, not an org container), so a chat never joins the shared window. See the [gotcha below](#gotchas).\n - **`webApplicationInfo` is required whenever `authorization` is present.** An RSC manifest with `authorization` but no `webApplicationInfo` fails to upload with a generic *\"We couldn't parse the app manifest from your app package\"* — even though the JSON is schema-valid. Teams needs the pair to bind the RSC grant to your Entra app.\n - **Getting the grant onto the team is the hard part, and the Teams client UI is unreliable for it.** A client/admin-center install can appear to succeed (consent dialog shows) yet silently fail to persist the grant — the app responds to @mentions (the Azure Bot delivers those without a team install) while ambient stays dark. **Verify** with Graph: `GET https://graph.microsoft.com/beta/teams/{team-group-id}/permissionGrants` — you want an entry with `permission` = `ChannelMessage.Read.Group` and `clientAppId` = your app id (an empty `value: []` means no grant). If the UI won't take, the **reliable** path is to install via Graph with consent in one call:\n ```\n POST /v1.0/teams/{team-id}/installedApps\n { \"teamsApp@odata.bind\": \"https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/{catalog-app-id}\",\n \"consentedPermissionSet\": { \"resourceSpecificPermissions\":\n [ { \"permissionValue\": \"ChannelMessage.Read.Group\", \"permissionType\": \"application\" } ] } }\n ```\n Use the **catalog** `teamsApp.id` (find it via `GET /appCatalogs/teamsApps?$filter=externalId eq '{your-app-id}'`), **not** the manifest/app id — the latter returns *\"non existent App definition\"*. The caller needs delegated `TeamsAppInstallation.ReadWriteAndConsentForTeam` (plus `AppCatalog.Read.All` for the lookup). The grant appears in `permissionGrants` ~10–20 s later. Team RSC is enabled by default (`ManagedByMicrosoft`), so this is rarely a tenant-policy block.\n - **Adding a permission to an *already-installed* app**: Teams keys an app by the `id` GUID (not by where you uploaded it), and RSC consent is captured at **install time into a specific team or chat** — bumping the catalog version does *not* retroactively grant a new RSC permission to a team/chat that already has the app. Bump `version` (e.g. `1.0.1` → `1.0.2`), repackage, then either re-add (client) or use the Graph install-with-consent above.\n - **This applied to `ChatMessage.Read.Chat` at `version` `1.0.2`**, and now applies to `ChannelMember.Read.Group` at `version` `1.0.3`: every team **or chat** that installed an earlier version keeps its old grant set. To pick up a new permission: rebuild the package, re-upload it to the catalog (Admin Center → *Teams apps → Manage apps →* the app *→ Update*), then **re-add the app to each affected chat/team** — or run the Graph install-with-consent call above with the new permission name (`permissionType: \"application\"`) in the `resourceSpecificPermissions` array. For a **chat** the endpoint is `POST /v1.0/chats/{chat-id}/installedApps` rather than `/teams/{team-id}/installedApps`, and the verification read is `GET https://graph.microsoft.com/beta/chats/{chat-id}/permissionGrants`. Until that re-consent happens the permission is inert — exactly like the UI-consent-silently-failed case above. **This is the path existing installs use to pick up `ChannelMember.Read.Group`** (new installs get it automatically as part of the default manifest — see [Proactive routines](#proactive-routines)).\n4. **Install / sideload** the app:\n - Org-wide: an admin uploads it in the [Teams Admin Center](https://admin.teams.microsoft.com) (**Teams apps → Manage apps → Upload**).\n - Single team: **Apps → Manage your apps → Upload an app** in the Teams client (custom-app upload must be enabled by your tenant admin).\n\n## Step 3 — Configure the environment\n\nSet these on your deployment (see the Microsoft Teams section of `.env.example`):\n\n```bash\nMICROSOFT_APP_ID=<application-client-id>\nMICROSOFT_APP_PASSWORD=<client-secret-value>\n# Required for any new (Single-tenant) bot — the Directory (tenant) ID:\nMICROSOFT_TENANT_ID=<directory-tenant-id>\n```\n\nRestart the instance so the Teams channel picks up the credentials. **Identity binds automatically**: the channel captures the tenant, conversation, scope, and (when present) membership type from each inbound activity, and `bindTeams` resolves the workspace, surface, mode, and visibility with no further config.\n\n## Step 4 — Invoke it\n\n1. **In a team channel:** `@Open Everywhere` followed by a request. An `@mention` is how a conversation STARTS, and it also **engages** that reply chain: later plain messages in it wake the bot with no new mention, until someone asks it to stop following (it then goes quiet there until the next `@mention`). A non-mention message in a chain the bot was never invited into is still dropped. The follow-ups depend on the `ChannelMessage.Read.Group` RSC grant — without it Microsoft delivers only `@mention` activities, so the bot answers once per mention.\n2. **In a 1:1 chat:** message the bot directly — no `@mention` needed; personal chats are always for it.\n3. Expected behaviour: a typing indicator, then a Markdown reply in-thread. A DM keeps its own per-user memory, so what it learns in a 1:1 does not appear in the channel. Channel learnings stay siloed because an unconfirmed channel binds **`external` trust**, not because it is private — see [Gotchas](#gotchas).\n\n## What the agent calls itself here\n\nA Teams session's IDENTITY — the name it introduces itself by — comes from one place only: the\n[`identity.name`](../configuration-reference.md) setting. Set it and it **wins on every surface**,\nbyte-identically — the right answer for a deployment that spans Slack and Teams from one window:\none name, everywhere. Unset, the session renders the generic identity line (\"this team's agent\").\nIt never borrows a name from another surface, and (since OPE-610) never from this surface's own\nTeams display name either — that is a HANDLE, not an identity; see below.\n\nSeparately, the session also carries a HANDLE line — \"your mention/handle here appears as X\" — so\nthe agent recognises its own mentions and backscroll lines under the app's real Teams name. Two\nGraph reads feed it, tried in this order:\n\n1. **The Teams app-catalog name** — the `teams-app/manifest.json` `name.short` the Teams client\n renders beside a message. The agent reads it from Microsoft Graph\n (`GET /appCatalogs/teamsApps?$filter=externalId eq '…'&$expand=appDefinitions`). This call needs\n a tenant-admin-consented `AppCatalog.Read.All`. Without that consent, or if the call fails for\n any other reason, the agent falls straight to the next option below.\n2. **The display name of your own Entra app registration** — the name you gave the app in Step 1.\n The agent reads it from Microsoft Graph\n (`GET /servicePrincipals(appId='…')?$select=displayName`) through the same app-only token the\n read adapter mints, and it needs **no extra Graph permission** — measured against a\n single-tenant app that carries only an RSC grant, where the implicit self-read answered 200. A\n multi-tenant install is reasoned to behave the same (the service principal exists in each\n consenting tenant) but is not measured.\n\nIf your `teams-app/manifest.json` `name.short` and the app registration's display name are the\nsame string, both options answer identically and there is nothing to think about. They can differ\nin case or spacing (`open-everywhere` vs `Open Everywhere` is a real example) — grant\n`AppCatalog.Read.All` if you want the exact catalog string; otherwise the registration name is\nthe handle a session shows.\n\nEither read is resolved once per instance, so a rename or a newly granted consent takes effect on\nthe next cold start. If neither lookup can answer — no token, an unconsented tenant, a network\nfailure — the session simply carries no handle line; the identity line above is unaffected either\nway.\n\n## Reading channel/thread history\n\nTeams supports the generic `read_channel`/`read_thread` verbs (the same tools Slack and Discord\nuse), backed by Microsoft **Graph** rather than the Bot Framework Connector — the Connector token\nthis integration already mints (`MICROSOFT_APP_ID`/`MICROSOFT_APP_PASSWORD`/`MICROSOFT_TENANT_ID`)\ncan post activities but exposes no history-read API. The read adapter (`agent/channels/teams-read.ts`)\nmints its own app-only Graph token via the same client-credential flow, same three env vars.\n\n- **Permission needed:** the app registration's Graph API permissions need something that can read\n channel messages. If your Teams app package already grants the per-team RSC permission\n `ChannelMessage.Read.Group` (Step 2 above — the same grant that enables *ambient* watching), reads\n work in that team with **no extra Azure configuration**. This is the shipped default, and it is\n the option to keep — read [Which boundary governs your reads](#which-boundary-governs-your-reads)\n before you choose the other one. Missing every option surfaces as a Graph 401/403 — fail-loud,\n never a silent empty read.\n- **Teams has two team ids, and Graph takes the second one.** An inbound activity carries the Bot\n Connector's team id — a `19:…@thread.tacv2` string. Every Graph URL under `/teams/{team-id}/…`\n takes the team's **Microsoft 365 group id** (a GUID) instead; Graph keeps the `19:` spelling as a\n team's `internalId`. For these read verbs and for the [since-last-wake catch-up](#reading-channelthread-history),\n Open Everywhere resolves the one into the other over the Connector\n (`GET {serviceUrl}/v3/teams/{teamId}`, whose `aadGroupId` is the group id) and caches the answer\n per team, so you configure nothing. A team whose group id will not resolve gets **no read** and a\n stated reason, never a read of the wrong team. **Every** Graph caller resolves the id this way,\n the routine fire-eligibility room-membership probe included (see\n [Proactive routines](#proactive-routines)).\n\n### The since-last-wake catch-up read\n\nA standing Teams seat reads its own room on every wake, so it sees most of what was said while it\nwas idle. That read uses the same Graph credential as the verbs above, and it makes **two calls**,\nbecause Graph splits one channel's history across two endpoints:\n\n| Call | What Graph returns |\n| --- | --- |\n| `/teams/{group-id}/channels/{channel-id}/messages` | the channel's **top-level** messages only — a reply is never in it |\n| `…/messages/{root-id}/replies` | that **one reply chain's** replies, and not its root |\n\nThe seat is bound to a channel, and each wake arrives inside one reply chain. So the catch-up reads\nboth and merges them: the chain call is added whenever the activity names a chain, which a Teams\nchannel activity always does (its conversation id carries `;messageid=<root>`). Reading only the\nchannel is what made a chain that *had* carried the message read as an empty room.\n\n**Know this bound.** The window is the channel's top-level messages plus the **one** chain the wake\nsits in. A reply in another of the channel's reply chains does not reach the seat, because Graph\nexposes no channel-wide replies endpoint — reading every chain would cost one call per chain. So an\nempty catch-up is not proof the channel was quiet.\n\nThe read is **fail-soft**: a wake is never *failed* by its catch-up, and one endpoint refusing does\nnot throw away what the other returned. It does cost time — the two Graph calls and their two audit\nwrites run inline, before the seat is woken, though all of it sits behind the Bot Framework ack.\nThe audit row is what makes the read readable: without one, a 404, an empty page and a bound that\nexcluded everything all reach you as the same silence. One row per Graph call, action\n**`teams.catch_up`**:\n\n| Field | What it says |\n| --- | --- |\n| `resource` | `channel-messages`, `thread-replies`, or `none` when the address could not be built |\n| `status` | `ok`, `error`, or `unaddressable` |\n| `httpStatus` | the HTTP status, when Graph refused the call |\n| `path` | the Graph resource path the call addressed, host and query stripped |\n| `raw` | how many messages **Graph** returned |\n| `kept` | how many survived this surface's visibility gate and the since-bound |\n| `since` | the window's lower bound |\n\n`raw: 0` is Graph answering nothing. `raw` above zero with `kept: 0` is Open Everywhere's own bound\nor gate dropping what Graph sent. No message text and no author name goes in the row.\n\n`kept` is not \"what the seat read\". The shared catch-up path then drops the trigger message itself,\napplies the domain's admission policy and caps the window by tokens, so a counted message can still\nnot reach the seat.\n\n### Which boundary governs your reads\n\nMicrosoft accepts the one app-only Graph token Open Everywhere mints under **two very different\nauthorizations**. They are not alternatives of equal weight, and the runbook used to present them as\nif they were.\n\n| | Per-team (RSC) | Tenant-wide |\n|---|---|---|\n| **Permission** | `ChannelMessage.Read.Group`, granted by a team owner at install | any of `ChannelMessage.Read.All`, `Group.Read.All`, `Group.ReadWrite.All`, admin-consented on the Entra app registration |\n| **What Microsoft checks** | the grant on **that team**, on every request | nothing about teams |\n| **Reach** | the teams the app is installed in | **every channel message in the tenant** |\n| **What holds the line** | Microsoft | only Open Everywhere's own domain allowlist (`memory/config/domains/`) — application code |\n\n**A tenant-wide permission is boundary-REMOVING, not merely the other way to do it.** It deletes\nMicrosoft's per-team check. The agent's reads are then fenced by nothing but Open Everywhere's own\nallowlist, so a bug or a misconfiguration in that allowlist reaches the whole tenant rather than one\nteam. Grant it only when RSC is genuinely unavailable to you, and know that is the trade you made.\n\nTwo of the three tenant-wide permissions are easy to hold by accident: `Group.Read.All` and\n`Group.ReadWrite.All` are commonly consented for unrelated reasons, and either one gives the app\ntenant-wide channel-message reach whether or not anyone intended it. Microsoft lists all three as\nthe higher-privileged application permissions for\n[list channel messages](https://learn.microsoft.com/en-us/graph/api/channel-list-messages) and\n[list message replies](https://learn.microsoft.com/en-us/graph/api/chatmessage-list-replies).\n\n**How to see which one you are on.** Open Everywhere reads the answer out of its own Graph token —\nthe token's `roles` claim carries the tenant-wide permissions, and an RSC grant never appears there\n(RSC lives on the team resource, not on the app registration). Two places report it:\n\n- **The ops preflight.** Run the `preflight` tool of the `open-everywhere-ops` MCP server. With\n Teams credentials in your `.env.local` it mints a token and reports a `teams: channel-read\n boundary` check — `ok` for `per-team`, `info` for `tenant-wide` with the roles to remove. With no\n Teams credentials it reports nothing. A failed mint degrades to one `info` line; the preflight\n never blocks on it.\n- **The audit trail.** The deployment writes a `teams.read_boundary` row whenever the determination\n changes — on the first Graph read after a cold start, and again if you change the permissions. The\n row carries the boundary, which tenant-wide roles were found, and what is left enforcing the\n fence. Every Teams `read.refused` row also carries a `boundary` field. Both carry ids and verdicts\n only, never message content or any part of the token.\n\n> **The detection is one-sided, on purpose.** A tenant-wide role in the token is proof the boundary\n> is gone. Its absence is not proof an RSC grant exists — it means the token cannot reach past a\n> team by itself, so any read that *succeeds* went through Microsoft's per-team check. A deployment\n> with neither gets Graph 403s. The check is a denylist of the three roles above, so a *new*\n> tenant-wide permission Microsoft adds later would read as `per-team` until the list is updated;\n> there is no positive RSC signal to match instead.\n- **Domain `reach` entries use the SANITIZED channel id**, not the raw Teams id. A Teams channel\n id looks like `19:abc…@thread.tacv2`; Open Everywhere's identity binding filesystem-sanitizes every\n channel/domain key (colons and `@` become `-`) before it's used as a memory/allowlist grain — the\n same sanitization Slack/Discord ids never visibly need (they're already filesystem-safe). So a\n domain listing a Teams channel should use the sanitized form, e.g.\n `\"teams/<tenant-id>/19-abc-thread.tacv2\"`, not the raw `19:abc@thread.tacv2` string.\n- **A resource grant uses the SAME sanitized id**, for the same reason and with one addition. Write\n `teams:channel/19-abc-thread.tacv2` and `teams:person/29-1abc…`, never the raw ids — the address\n grammar refuses a second colon, so a pasted raw id fails at config load with a \"malformed resource\"\n error rather than in front of a member. The Teams provider restores the raw Bot Framework id at the\n transport edge, from an index it writes whenever an activity arrives. **One consequence to know:**\n a container or a person this deployment has never seen an inbound activity from cannot be restored,\n so the grant is refused out loud at send time. Grant a room the bot is in, and a person who has\n spoken to it or in one of its channels. **A granted container write starts a NEW thread in the\n channel.** A Teams channel activity arrives on one reply chain (`…;messageid=<root>`), and the\n index records the channel at BOTH grains, so the plain channel id you write in the roster is the id\n the write lands in — never whichever reply chain the bot last saw traffic in.\n- **Scope, and refusals are audited.** Same allowlist as every surface: the current channel plus any\n other Teams channel the session's [memory domain](../../capabilities/memory.md) lists in\n `reach`. An out-of-scope channel is refused before any Graph call, and every refusal writes a\n `read.refused` audit row naming the (sanitized) channel id, the caller's domain and the verdict —\n ids only, never message content. Query it with `GET /control/v1/audit?since=<iso>&actions=read.refused`.\n- **Ordering:** Graph's channel-messages and thread-replies endpoints support no `$orderby` and are\n observed to return messages **newest-first** (the opposite of Slack's thread-replies API) — the\n adapter always sorts by `createdDateTime` itself, so this is invisible to callers.\n\n## Proactive routines\n\nScheduled [routines](../../capabilities/proactivity-and-routines.md) work on Teams — with one platform rule worth knowing: Bot Framework only allows **proactive** messages to a conversation the bot has a stored *conversation reference* for (`serviceUrl` + conversation id, captured on a prior inbound turn); there is no cold-send to an arbitrary user or channel. Open Everywhere captures that reference off every inbound activity and stores it on the routine at creation, so any routine you create in-thread posts back **into the same conversation** with nothing to configure. A routine missing its reference fails closed — it runs dry (logged, schedule held) rather than posting anywhere else. Spend caps apply to channel/group-chat routines — and to personal (1:1) chat routines too, same as Slack: a fire never gets the admin bypass, because there is no attending admin to bypass on its behalf.\n\n**Creator eligibility at fire time.** Every clock fire re-checks the routine's creator, mirroring\nSlack (see [Proactivity & routines](../../capabilities/proactivity-and-routines.md)): a creator\nremoved from the team channel, or whose Entra account is disabled, **skips** that one fire rather\nthan posting — logged as a `routine.skip` row, never a failure. Two Graph app-only permissions\ndrive it:\n\n- **Room membership** — `GET /teams/{team-id}/channels/{channel-id}/members`. The **least\n privileged** application permission is `ChannelMember.Read.Group`, an **RSC** grant of the same\n family as `ChannelMessage.Read.Group`. **The shipped manifest carries it by default** (added at\n `version` `1.0.3`, paired with `ChannelMessage.Read.Group` under the same philosophy — see\n [Step 2](#step-2--package-the-teams-app)), so a new install enforces the room-membership check\n with no extra Azure configuration, on the same per-team boundary [described\n above](#which-boundary-governs-your-reads). An install that predates `1.0.3` needs the version\n bump + re-consent flow in Step 2 to pick it up — RSC grants never backfill on their own.\n `ChannelMember.Read.All` is the tenant-wide alternative, with the identical boundary trade-off.\n- **Account status** — `GET /users/{aad-object-id}?$select=accountEnabled`. This one has **no RSC\n equivalent** — a directory user lookup isn't scoped to a team — so it always needs the\n tenant-wide application permission `User.Read.All`, admin-consented on the Entra app\n registration. It stays a manual, eyes-open operator step; it cannot ship in the default manifest.\n\nMissing either permission is **not an outage**: a Graph 401/403 on either call is read as\n\"unresolvable\", exactly like a network hiccup, and the routine keeps firing on schedule — an\nunresolved check never skips a fire. `User.Read.All` still needs a deliberate grant to make its\nhalf of the check enforce instead of silently passing through.\n\n**The room check enforces, exactly as Slack's does.** A creator Graph confirms is no longer in the\nchannel stops getting that routine — the same outcome and the same `creator_removed_from_room` audit\nrow the Slack path writes. Only an answer the probe could not **get** lets the fire through, and\nevery such fire writes a `routine.eligibility_unresolved` row naming the check and the cause:\n\n| Cause on the row | What it means | What to do |\n| --- | --- | --- |\n| `read_error` | The Graph call failed — a network error, a 5xx, or a 401/403 for a permission this install does not hold. | Confirm `ChannelMember.Read.Group` is consented (Step 2). |\n| `team_id_unresolved` | The Bot Connector could not give this team's Microsoft 365 group id, which the Graph URL takes. | Check the Connector credential and the team's own state. |\n| `roster_incomplete` | The channel roster is larger than the probe's page bound, so the creator was neither found nor ruled out. | Nothing to fix — the bound is deliberate. |\n| `malformed_response` | Graph answered with something that is not a member page. | Report it; the probe refuses to guess a verdict from it. |\n| `no_provider` | No Teams provider is booted. | Only reachable on a misconfigured instance. |\n| `missing_ids` | The routine posts into a channel, but carries no team id or no creator Azure AD object id, so the roster cannot be asked about. | Expected on a routine created before those ids were captured. Re-create it to bring it under the check. |\n\nRead those rows before you conclude the check is working. A fire that was **really** checked writes\nnone of them, so the row's presence — not its absence — is what tells you a fire went unchecked. A\n**personal (1:1) chat** routine writes none either, and that is not a gap: a 1:1 chat has no channel\nroster, so room membership is not a question that exists there. The account-status half still runs.\n\n## Direct messages and the account directory\n\nTeams is the one surface that cannot cold-address a person: a Bot Framework write needs a **regional**\n`serviceUrl` and a Microsoft-minted conversation id, and both exist only on an inbound activity. Open\nEverywhere therefore records a conversation reference for every inbound activity it acts on — a **1:1\nchat** reference from a personal chat, keyed on the person, and a **team** reference from a channel,\nkeyed on the team. The rows are refreshed by later activities, because Microsoft does not promise a\ntenant keeps its region, and they live in the operational store, not in configuration.\n\nTwo capabilities read that record, and both are used by [identity\nlinking](../../how-it-works/identity.md):\n\n- **Direct messages.** The agent posts into a stored 1:1 chat. Where it holds none and the deployment\n has seen exactly one tenant, it opens the chat with `POST /v3/conversations` and remembers it.\n- **The account directory** — \"who here is called that?\" — reads the Bot Connector roster\n (`pagedmembers`) of the teams it has references for. It needs no Graph permission at all: the bot's\n own credential carries it. Graph is deliberately *not* used, because Graph has no Bot Framework\n `29:` id and that is the id a Teams identity link is keyed on.\n\nWhat that means for you, in three lines:\n\n- A deployment whose bot has never been added to a **team channel** has no account directory. Add it\n to one channel and the directory works from the next message.\n- **A person must have the Teams app installed in the personal scope** before the agent can open a\n 1:1 chat with them. Microsoft refuses the open otherwise, and Open Everywhere reports the refusal\n with its reason rather than dropping the message.\n- A deployment serving **two tenants** will not open a cold 1:1 chat for somebody it has never met —\n it refuses rather than guess which organisation the account belongs to. A single message from that\n person to the bot fixes it permanently.\n\n## Gotchas\n\n- **Public endpoint is mandatory.** The Bot Framework Connector pushes activities to your messaging endpoint over HTTPS; it can't reach `localhost`. Use a tunnel for local work and update the messaging endpoint whenever the tunnel URL changes.\n- **The Bot Framework SDK is archived; this transport is not.** Microsoft archived the Bot Framework SDK and the Bot Framework Emulator on GitHub, stopped maintaining them, and ends support tickets for the SDK on 31 December 2025, pointing at the Microsoft 365 Agents SDK and the Teams SDK as successors (Bot Framework basics, read 2026-09-02). **Open Everywhere does not use that SDK.** `packages/providers/teams/agent/channels/teams-connector.ts` calls the Bot Framework **Connector REST API** directly — `POST /v3/conversations/{id}/activities` and its reply and update siblings — under a client-credentials token eve mints. The Connector REST API is the Azure Bot Service protocol, and Microsoft has not archived it. So this is a long-horizon transport risk, not a defect: re-read it if Microsoft deprecates the Connector REST surface itself, or if a Teams feature we want turns out to be Agents-SDK-only.\n- **Tenant ID is required.** New Azure Bots are Single-tenant (multi-tenant creation was deprecated after 31 July 2025), so you **must** set `MICROSOFT_TENANT_ID` to the app registration's Directory (tenant) ID — auth fails without it. A mismatch between the registration's tenant and your env is the most common \"the bot never responds\" cause. (Legacy multi-tenant bots registered before the cutoff still work and omit it.)\n- **Channels default to private *and* external — and it is the EXTERNAL half that contains memory.** Because inbound activities usually omit `membershipType`, every team channel is treated as `private` **and** binds `external` trust. The `external` trust is what does the work: read-only `public` memory at most, no internal domain, no org brain. 🔴 The `private` half contains NOTHING on its own. Visibility is tracked and audited; no code reads it to gate memory (`resolveMemoryPlan` branches on mode and trust), and there is no per-channel \"surface tier\" for a channel's learnings to stay in. So when the Graph-API enrichment below lands and a channel positively confirms `internal`, a private channel with no domain falls straight to the org fallback and WRITES the org brain — privacy will not hold it back. This is intentional fail-closed behaviour today, for the trust reason only. To light up shared memory today, either declare the surface in `agent/identity/surfaces.ts` (the vouch above — the precise option), or set `OE_TRUST=internal` on a single-tenant deployment you vouch for, or wait for the Graph-API enrichment that positively confirms `membershipType`; until then channel memory is correct-but-conservative.\n- **The app password (client secret) expires.** Entra client secrets have a finite lifetime (default ~6–24 months). When it expires the bot silently stops authenticating — rotate it in **Certificates & secrets** and update `MICROSOFT_APP_PASSWORD`.\n- **A group chat answers when it is ENGAGED, and `ChatMessage.Read.Chat` alone does not engage it.** The RSC permission is necessary (without it Microsoft never delivers non-mention chat activities) but not sufficient: OE's ingress admits a plain chat message only where somebody `@mentioned` the bot in that chat and has not asked it to stop following. Granting the permission therefore does not turn every message in every chat into an agent session — which is what it used to do, before the engagement seam gated that branch. The cross-surface single window is separate and still admits `scope === \"channel\"` only — a group chat is a per-person surface, not an org container, and admitting one would let it join a private shared window (the same channels-only rule Slack follows). **Undirected ambient watching** — waking on messages in a room nobody invited the bot into — therefore stays a **team-channel** capability.\n\n## End-to-end testing (Graph API driver)\n\nThe [e2e driver pack](../../../packages/core/scripts/e2e/) drives each surface with a real message and reads the reply back. Slack and Discord can inject a synthetic wake — Slack by signing a forged event with the deployment's own signing secret, Discord via a second driver bot. **Teams can't:** the Bot Framework JWT on a real inbound Activity is *Microsoft-signed*, and we don't hold the key, so there's no way to forge an event the deployment will trust. Instead the Teams driver drives a **genuine** message — it `@mentions` the bot over *delegated Microsoft Graph* (a real test user's session), which makes Azure deliver a real Activity that wakes the bot, then reads OE's reply back from the message's native reply chain. Scenario `J-TEAMS` is the repeatable version of the manual \"mention it and watch it reply\" check.\n\n**One-time setup:**\n\n1. **Register a separate Entra app** as a **public client** (this is *not* OE's bot app registration): in the app registration, **Authentication → Advanced settings → Allow public client flows → Yes**, and under **API permissions** add the *delegated* Microsoft Graph perms `ChannelMessage.Send`, `ChannelMessage.Read.All`, and `offline_access`, then have a tenant admin **Grant admin consent**. Note its **Application (client) ID**.\n2. The **test user** whose session you'll authenticate must be a **member of the team** OE is installed in (the same team/channel `J-TEAMS` posts into).\n3. Mint the refresh token and discover the ids — run once:\n\n ```bash\n export E2E_TEAMS_TENANT_ID=<directory-tenant-id>\n export E2E_TEAMS_CLIENT_ID=<the public-client app's client id>\n npm run e2e:teams-auth\n ```\n\n It runs a device-code sign-in (open the URL, enter the code), prints the `export E2E_TEAMS_REFRESH_TOKEN='…'` line, and lists your teams + channels so you can copy the team and channel ids. The token is printed to stdout only — it's never written to a file; paste it into your shell / CI secrets.\n4. **Set the six `E2E_TEAMS_*` vars** and run the scenario:\n\n ```bash\n export E2E_TEAMS_TENANT_ID=<directory-tenant-id>\n export E2E_TEAMS_CLIENT_ID=<public-client app id>\n export E2E_TEAMS_REFRESH_TOKEN=<from the helper above>\n export E2E_TEAMS_TEAM_ID=<from the helper's listing>\n export E2E_TEAMS_CHANNEL_ID=<from the helper's listing>\n export E2E_TEAMS_BOT_APP_ID=<OE's MICROSOFT_APP_ID>\n E2E_ONLY=J-TEAMS npm run test:e2e-driver\n ```\n\n Use the `E2E_ONLY` env var to run a single scenario, not `-- --only J-TEAMS`: the root\n `test:e2e-driver` delegates to a workspace script via `npm run`, and the inner `npm`\n swallows a forwarded `--only` as its own (deprecated) config flag rather than passing it\n through — so the flag form silently runs the *whole* pack. `E2E_ONLY` bypasses npm entirely.\n\nWithout these vars the scenario self-skips (fail-closed), naming exactly what's missing — the same posture as every other driver half.\n\n**The 1:1 DM plane (`J-TEAMS-DM`, OPE-972).** This leg drives the bot's personal (1:1) chat, not\na channel. It rides the SAME six `E2E_TEAMS_*` vars as `J-TEAMS` — no extra fixture var — but needs\ntwo more things true of the deployment and the driver app, and derives both at RUNTIME rather than\nthrough an env var:\n\n1. **The driver user has OE installed in the PERSONAL scope.** Microsoft auto-provisions the 1:1\n chat only after that install exists. `resolveDmChat()` finds the install row itself\n (`GET /me/teamwork/installedApps?$expand=teamsApp`, matched on `teamsApp.externalId ===\n E2E_TEAMS_BOT_APP_ID`) — no new var carries this.\n2. **The driver app's Entra registration has a `Chat.*` delegated Graph permission consented** —\n `Chat.ReadWrite`, or `ChatMessage.Send` + `Chat.Read` separately. The one-time setup above only\n grants `ChannelMessage.*` (channel messages), which is a *different* Graph permission family\n from chat messages; Graph 403s a `/chats/{id}/messages` call under either scope alone. Add the\n `Chat.ReadWrite` delegated permission to the same app registration used above and have a tenant\n admin **Grant admin consent** for it — the existing refresh token picks up the new scope on its\n next `.default` refresh, no re-auth needed.\n\nEither gap self-skips the leg with the exact missing thing named (`ctx.skip`, never a false red) —\nrun `E2E_ONLY=J-TEAMS-DM npm run test:e2e-driver` and read the skip reason.\n",
13
+ markdown: "# Microsoft Teams\n\nRun Open Everywhere as a Microsoft Teams bot: `@mention` it in a team channel, drop it into a group chat, or DM it 1:1. It acts under one bot identity shared by the deployment — never under a member's account and never with a member's permissions — with per-conversation memory.\n\nThis page is the Teams-specific setup runbook. For the concept of a *surface* and how surfaces plug in, see [Surfaces](../../extending/surfaces.md). For how memory is scoped per tenant and conversation, see [Memory](../../capabilities/memory.md). For how Teams' feature set compares to the other surfaces — including which features are built but not yet verified against a live tenant — see the [surface support matrix](./support-matrix.md).\n\nTeams is the richest native fit for the silo model — its `tenant → team → channel` hierarchy maps directly onto `org → workspace → surface`, with no impedance mismatch.\n\n## How the silo model maps\n\nOpen Everywhere's memory is [opt-in domains and silos](../../capabilities/memory.md), not an\nautomatic per-tenant tier stack. A Teams conversation is a **surface**; the tenant is the\nnatural `workspace` identity key many channels' surface keys share, but sharing memory across\nthem still needs an explicit `domain` (or the `org` pseudo-domain fallback) — nothing pools\nautomatically, and (see \"Trust\" below) `visibility` is *not* what gates it.\n\n| Open Everywhere concept | Teams concept | Source field |\n| --- | --- | --- |\n| `org` (deployment tenant) | the Open Everywhere deployment (one org today, from `deploymentIdentity.org`) | tracked config |\n| `workspace` (identity key) | the **Microsoft tenant** (the Entra/Azure AD org) | `tenant_id` |\n| `surface` (partition key) | the **conversation** — a team channel, a group chat, or a 1:1 | `conversation_id` |\n| `mode` | `channel` (team channel) · `mpim` (group chat) · `dm` (1:1) | `scope` |\n| `visibility` | `public` only for a confirmed team-wide channel; else `private` | `membership_type` |\n| `trust` | `internal` only for a positively-verified in-tenant channel; else `external` | `membership_type` + `tenant_id` |\n\n`scope` maps via `teamsMode`: `personal → dm`, `groupChat → mpim`, `channel → channel`.\n\n**Visibility is tracked but isn't the memory gate — trust is (below).** A team channel is\nonly treated as **public** when `membershipType == \"standard\"` — the org/team-wide channel\neveryone on the team can see; everything else (1:1 chats, group chats, a `private` or\n`shared` channel, or an unconfirmed one) is `private`. But a **private** in-tenant channel\nstill shares its domain's or the org's brain exactly like a public one does — visibility just\nisn't the thing `teamsTrust` reads. The catch is the *positive-confirmation* requirement\n`teamsVisibility` and `teamsTrust` both share: inbound Teams activities almost never carry\n`membershipType` (it's a Graph/Teams-API property, not a guaranteed field on a Bot Framework\nActivity), so both default `private`/`external` until a Graph-API enrichment supplies it —\ncorrect-but-conservative rather than assuming either bit.\n\n**Trust fails closed too.** Trust is the [memory gate](../../capabilities/memory.md): an `external` surface gets read-only `public` memory at most, never an internal domain or the org brain. Only positive facts produce a verdict (`teamsTrust`):\n\n- `membershipType == \"shared\"` — a Teams **Shared Channel**, which can include people from *other* tenants (the Teams analog of Slack Connect) — is `external`, always. No override reaches it.\n- `membershipType == \"standard\"` or `\"private\"` with a known `tenant_id` is a positively-identified in-tenant channel — `internal`.\n- **Anything unverifiable** — absent `membershipType` (the common case), an unrecognised value, or a missing tenant id — produces NO verdict, so the surface falls through to the next rung of the trust chain. With nothing declared it binds `external`. **Two things promote it, not one.** A **register declaration** covering the surface (`defineSurface({ key: \"teams/<tenantId>\", trust: \"internal\" })`, the section below) binds it `internal` — and a declaration counts at any rung, so a tenant line covers every room under the tenant. A **single-tenant deployment** can instead use the blanket `OE_TRUST=internal` (the same escape hatch Discord/Linear use). Both sit BELOW the platform verdict in `identity/binding.ts`, so neither overrides a positive `external` one; a `public` declaration outranks both and demotes. What neither does is fence an ambiguous activity out of the org brain: once you declare the tenant, a room with no `membershipType` reaches it. Weigh that reach before you pick the grain.\n- 1:1 and group chats are personal silos (the channel-trust gate doesn't apply), but the **DM-partner gate** still fails closed: Teams doesn't positively verify who the 1:1 partner is, so a DM gets the per-user personal store plus read-only `public` — not the `dm`-shared layer.\n\n### Vouching a Teams surface — `agent/identity/surfaces.ts`\n\nTeams declares its vouched surfaces where every other platform does: `agent/identity/surfaces.ts`,\nconfig-as-code reviewed by pull request. No per-surface vouch is an environment variable; the only\nenv trust lever is the blanket `OE_TRUST=internal` described above. A surface key\ncarries your Microsoft 365 tenant id, which is an identifier, not a credential — the same class as\nthe Discord guild id and the GitHub org already in that file. Note what that still costs on a public\nrepository: the id names your organisation, permanently, because the history goes public with the\ntree. That is a disclosure to weigh, not a secret to protect.\n\n**Declare the tenant. That is the vouch.**\n\n```ts\ndefineSurface({ key: \"teams/<tenantId>\", trust: \"internal\" }),\n```\n\nA declaration counts at **any rung**. Trust resolution is most-specific-wins with a fallback to the\ntenant, so this one line makes every channel and thread under that tenant bind `internal` — the same\nworkspace-grain shape the Discord guild and GitHub org lines use. Declare a single channel instead\n(`teams/<tenantId>/<threadId>`) when you want to narrow it to one room.\n\n**One line serves both planes, the cross-surface single window included.** The Teams single-window\nprivacy floor reads `isVouchedSurface`, and that reader walks the declaration rungs like every other\ntrust read (**OPE-1521**), so a channel covered by the tenant vouch both binds `internal` for memory\nand joins a shared cross-surface window.\n\nIt did not always. The floor used to be an exact-key match no tenant declaration satisfied, so a\ntenant line kept boot green while the channel quietly failed to join the window — one register, two\ngrain semantics, and nothing said so. If you are reading an older deploy's config, that is why a\nsecond channel-grain line sits beside a tenant line. Such a line changes no verdict now, and this\ndeployment's own config dropped its copy (**OPE-1533**).\n\n`OE_TRUST=internal` opens that floor too, regardless of declaration grain — it is the deploy-wide\nblanket described above, and it satisfies the window check without any per-surface vouch.\n\n**What a tenant declaration reaches.** Every room under it, including a cross-tenant\nConnect/**shared** channel that can carry people from other organisations. Inbound activities usually\nomit `membershipType` (see below), so trust falls back to the vouch rather than to a positive\nin-tenant signal. Declare the tenant where you trust the tenant; declare single channels where you do\nnot.\n\n**Narrowing one whole team out of a tenant.** A Teams channel keys as `teams/<tenantId>/<threadId>`,\nand the team is nowhere in that key, so a **group rung** names it instead:\n\n```ts\ndefineSurface({ key: \"teams/<tenantId>\", trust: \"internal\" }),\ndefineSurface({ key: \"teams/<tenantId>/team:<teamId>\", trust: \"public\" }),\n```\n\nThe group rung outranks the tenant entry, so every channel of that team binds `external` and stays\nout of the shared window, while the rest of the tenant keeps the tenant line. The window floor reads\nthe same rung as the binding and memory planes (**OPE-1538**); before that it skipped the tier and\nadmitted a room the other two planes had already demoted.\n\n🔴 **`preflight` reports a working Teams container line as a dead declaration.** It folds traffic from\naudit rows, which name one workspace and one room and no rung above them. Two Teams shapes name a\ncontainer:\n\n- the **group rung** above — no traffic row carries the team;\n- a **channel line** (`teams/<tenantId>/<channelId>`), because a Teams room keys on the CONVERSATION\n id. A reply chain keys as `<channelId>;messageid=<root>`, and only a post at the channel root keys\n as the channel itself.\n\nBoth report as dead declarations while they work. Where such a line is the only cover for the rooms\nunder it, those rooms also report as undeclared traffic — a red that flips the ops report's `ready`\nto false. Do not delete either line on that advice. Deleting the group rung above returns every\nchannel of that team to the tenant's `internal`; deleting a channel line returns every reply chain in\nit to `external`. **OPE-1560** carries the audit-row half that closes this, and the\ndead-declaration check says so itself.\n\n**Fail closed.** A surface with no entry stays `external`. There is no default vouch.\n\n**A vouch does not put a channel in a memory domain.** It says the channel is `internal`; which\nchannels share a brain is a separate decision in `agent/memory/config/domains/<name>.ts`. The two lists stay\nseparate on purpose — a window roster derived from the vouch list would enroll every channel you\nvouch in one cross-surface window and one silo, and it would make the window's own un-vouched-source\ncheck vacuous. Write the key in both places.\n\nThis vouch is precise; `OE_TRUST=internal` is the blunt single-tenant alternative described above,\nwhich vouches every unverifiable surface on every platform at once.\n\n## Prerequisites\n\n- An **Azure subscription** and **Entra ID (Azure AD)** admin rights — you need to create an Azure Bot resource and an app registration, and someone with admin rights to upload/approve the Teams app.\n - **No work tenant, or no admin rights in yours?** Start a **[Microsoft 365 Business Basic 1-month free trial](https://www.microsoft.com/microsoft-365/business/microsoft-365-business-basic)** — a full tenant where you *are* Global Admin, with 25 free seats for the trial month; after that, one Business Basic seat (a few £/$ a month, monthly commit) keeps it alive, and free **guest** accounts cover extra test users. Two day-one moves: turn OFF recurring billing (the trial otherwise auto-converts to an *annual* term) and enable custom app upload (Teams admin center → Teams apps → Setup policies — can take up to 24h to appear/propagate on a fresh tenant). Mind the SKU: the cheaper \"Business Basic *(no Teams)*\" variant won't work. (The old [Microsoft 365 Developer Program](https://developer.microsoft.com/microsoft-365/dev-program) sandbox is closed to unaffiliated developers since 2024 — it now requires a Visual Studio Pro/Enterprise *standard* subscription or a Microsoft partner program; Microsoft's own Teams docs point everyone else at the trial/purchase route. An Azure subscription for the Bot resource is still needed; the free F0 tier covers it and the Teams channel is free.)\n- A **deployed Open Everywhere instance** — see the [docs index](../../README.md) and [setup runbook](../setup-runbook.md). Teams requires a **public HTTPS endpoint**: the Bot Framework Connector calls *your* messaging endpoint, so it cannot reach `localhost`.\n - For local development, expose your instance with a tunnel — e.g. a [VS Code dev tunnel](https://learn.microsoft.com/azure/developer/dev-tunnels/), `ngrok`, or Cloudflare Tunnel — and use the public `https://…` URL as the messaging endpoint.\n\n## Step 1 — Register the Azure Bot + Entra app\n\n1. In the [Azure Portal](https://portal.azure.com), **Create a resource → Azure Bot** (the modern replacement for *Bot Channels Registration*; either works).\n2. For the bot's identity, choose **Single-tenant** and set `MICROSOFT_TENANT_ID` to that tenant's Directory ID. (Microsoft deprecated new **multi-tenant** Azure Bot creation after **31 July 2025** — fresh registrations are Single-tenant or User-Assigned Managed Identity only, and this integration uses the app-id + secret flow, i.e. Single-tenant. `MICROSOFT_TENANT_ID` is therefore effectively required for any new bot, not optional.) Azure either creates a new **Entra app registration** for you or lets you point at an existing one.\n3. Note the app registration's **Application (client) ID** — this is `MICROSOFT_APP_ID`.\n4. In the app registration, go to **Certificates & secrets → New client secret**, create one, and copy its **Value** immediately — this is `MICROSOFT_APP_PASSWORD`. (Azure calls it a \"client secret\"; the Bot Framework calls it the \"app password\" — same thing.)\n5. For a single-tenant bot, copy the **Directory (tenant) ID** — this is `MICROSOFT_TENANT_ID`.\n6. On the Azure Bot resource, open **Configuration** and set the **Messaging endpoint** to:\n\n ```\n https://<your-deploy-url>/eve/v1/teams\n ```\n\n (`/eve/v1/teams` is the route Open Everywhere's Teams channel listens on.)\n7. On the Azure Bot resource, open **Channels** and **enable the Microsoft Teams channel**.\n\n## Step 2 — Package the Teams app\n\nThe Azure Bot exists, but Teams needs an **app package** (a manifest + icons) to surface the bot inside Teams. The manifest is **rendered**, not static (OPE-2334): the Teams bundle renders it from this deployment's settings, so the app carries the configured [`identity.name`](../configuration-reference.md) in `name.short`, `name.full` and `description.full` (\"Open Everywhere\" when no name is configured). Get it from the ops MCP tool — `surface_manifest { \"surface\": \"teams\", \"baseUrl\": \"https://<the deployment's host>\" }` — which also substitutes your deploy host. An explicit `appName` (at most 30 characters) overrides the configured name. The placeholder icons and a build/install README ship at [`teams-app/`](../../../teams-app/). The manifest is already scoped for `team` + `groupChat` + `personal`.\n\n1. Fill the package: save the rendered manifest as `teams-app/manifest.json`, replace `{{MICROSOFT_APP_ID}}` (your `MICROSOFT_APP_ID`, used for both `id` and `bots[].botId`) and `{{DEVELOPER_NAME}}`, then zip it flat — the `teams-app/README.md` has a one-liner. Replace the placeholder `color.png` / `outline.png` with real brand icons before any org-wide install.\n - Prefer a GUI? The [Developer Portal for Teams](https://dev.teams.microsoft.com/) (formerly *App Studio*) → **Apps → New app** → **App features → Bot** → select the bot by its app ID does the same thing interactively.\n - The manifest's `privacyUrl` and `termsOfUseUrl` are **not** placeholders: they point at `/privacy` and `/terms` on your own deploy host, and **the deployment serves both**. The host `surface_manifest` substitutes is all they need. The TEXT of both is yours — declare it as `policyPages.privacy` and `policyPages.terms` in `agent/settings/settings.ts` (see [the configuration reference](../configuration-reference.md#agent-settings-behaviour-config-as-code)). Undeclared, each route still answers `200` with a short page saying no document is published yet, so a sideload never breaks; publish real text before an **org-wide** upload, where a reviewer reads them.\n2. The package already declares the three **scopes** — `team` (channel surfaces), `groupChat` (mpim surfaces), `personal` (dm surfaces). Trim them in `manifest.json` if you want a narrower install.\n3. The package declares three **RSC (resource-specific consent) permissions** (all Application), under `authorization.permissions.resourceSpecific`, paired with a `webApplicationInfo` block (`id` = your app id, `resource` = any URI):\n - **`ChannelMessage.Read.Group`** — read a **team's channel** messages, and receive *every* channel message activity rather than only @mentions. Granted by a **team owner** at install time.\n - **`ChatMessage.Read.Chat`** — the chat analogue: read a **chat's** messages (group chats, and the chats behind meetings) and receive every chat message activity. Granted by any **chat member** authorised to install apps in that chat.\n - **`ChannelMember.Read.Group`** — read a **team channel's** membership list. Granted by a **team owner** at install time, same as `ChannelMessage.Read.Group`. It powers the routine fire-eligibility room check (see [Proactive routines](#proactive-routines)) — see that section for what changes when it's missing.\n\n None of the three lives in the Azure app registration's API-permissions blade — RSC exists only in the Teams manifest — and none goes through Microsoft's Protected-API approval queue (which is what `ChannelMessage.Read.All` would need). Keeping `ChannelMessage.Read.Group` is also what keeps Microsoft's own per-team boundary on channel reads; see [Which boundary governs your reads](#which-boundary-governs-your-reads). **@mentions reach the bot without any of them**, so if you only want mention-driven behaviour you can remove *all three* along with the `authorization` and `webApplicationInfo` blocks; keep them for ambient watching, [channel/thread history reads](#reading-channelthread-history), and enforced [routine fire-eligibility](#proactive-routines) checks.\n\n > **What `ChatMessage.Read.Chat` does and doesn't change.** It is what makes Microsoft *deliver* non-mention group-chat messages to OE at all — without it they never arrive, so nothing downstream could ever act on them. It does **not** make OE answer everything a chat says. OE's own ingress admits a non-mention chat message only when the chat is **engaged** — somebody `@mentioned` the bot there and has not asked it to stop following. An un-engaged chat wakes nothing — except an answer to a question the agent itself parked there, which is always admitted. The cross-surface window's privacy floor still admits `channel` scope only (a group chat is a per-person surface, not an org container), so a chat never joins the shared window. See the [gotcha below](#gotchas).\n - **`webApplicationInfo` is required whenever `authorization` is present.** An RSC manifest with `authorization` but no `webApplicationInfo` fails to upload with a generic *\"We couldn't parse the app manifest from your app package\"* — even though the JSON is schema-valid. Teams needs the pair to bind the RSC grant to your Entra app.\n - **Getting the grant onto the team is the hard part, and the Teams client UI is unreliable for it.** A client/admin-center install can appear to succeed (consent dialog shows) yet silently fail to persist the grant — the app responds to @mentions (the Azure Bot delivers those without a team install) while ambient stays dark. **Verify** with Graph: `GET https://graph.microsoft.com/beta/teams/{team-group-id}/permissionGrants` — you want an entry with `permission` = `ChannelMessage.Read.Group` and `clientAppId` = your app id (an empty `value: []` means no grant). If the UI won't take, the **reliable** path is to install via Graph with consent in one call:\n ```\n POST /v1.0/teams/{team-id}/installedApps\n { \"teamsApp@odata.bind\": \"https://graph.microsoft.com/v1.0/appCatalogs/teamsApps/{catalog-app-id}\",\n \"consentedPermissionSet\": { \"resourceSpecificPermissions\":\n [ { \"permissionValue\": \"ChannelMessage.Read.Group\", \"permissionType\": \"application\" } ] } }\n ```\n Use the **catalog** `teamsApp.id` (find it via `GET /appCatalogs/teamsApps?$filter=externalId eq '{your-app-id}'`), **not** the manifest/app id — the latter returns *\"non existent App definition\"*. The caller needs delegated `TeamsAppInstallation.ReadWriteAndConsentForTeam` (plus `AppCatalog.Read.All` for the lookup). The grant appears in `permissionGrants` ~10–20 s later. Team RSC is enabled by default (`ManagedByMicrosoft`), so this is rarely a tenant-policy block.\n - **Adding a permission to an *already-installed* app**: Teams keys an app by the `id` GUID (not by where you uploaded it), and RSC consent is captured at **install time into a specific team or chat** — bumping the catalog version does *not* retroactively grant a new RSC permission to a team/chat that already has the app. Bump `version` (e.g. `1.0.1` → `1.0.2`), repackage, then either re-add (client) or use the Graph install-with-consent above.\n - **This applied to `ChatMessage.Read.Chat` at `version` `1.0.2`**, and now applies to `ChannelMember.Read.Group` at `version` `1.0.3`: every team **or chat** that installed an earlier version keeps its old grant set. To pick up a new permission: rebuild the package, re-upload it to the catalog (Admin Center → *Teams apps → Manage apps →* the app *→ Update*), then **re-add the app to each affected chat/team** — or run the Graph install-with-consent call above with the new permission name (`permissionType: \"application\"`) in the `resourceSpecificPermissions` array. For a **chat** the endpoint is `POST /v1.0/chats/{chat-id}/installedApps` rather than `/teams/{team-id}/installedApps`, and the verification read is `GET https://graph.microsoft.com/beta/chats/{chat-id}/permissionGrants`. Until that re-consent happens the permission is inert — exactly like the UI-consent-silently-failed case above. **This is the path existing installs use to pick up `ChannelMember.Read.Group`** (new installs get it automatically as part of the default manifest — see [Proactive routines](#proactive-routines)).\n4. **Install / sideload** the app:\n - Org-wide: an admin uploads it in the [Teams Admin Center](https://admin.teams.microsoft.com) (**Teams apps → Manage apps → Upload**).\n - Single team: **Apps → Manage your apps → Upload an app** in the Teams client (custom-app upload must be enabled by your tenant admin).\n\n## Step 3 — Configure the environment\n\nSet these on your deployment (see the Microsoft Teams section of `.env.example`):\n\n```bash\nMICROSOFT_APP_ID=<application-client-id>\nMICROSOFT_APP_PASSWORD=<client-secret-value>\n# Required for any new (Single-tenant) bot — the Directory (tenant) ID:\nMICROSOFT_TENANT_ID=<directory-tenant-id>\n```\n\nRestart the instance so the Teams channel picks up the credentials. **Identity binds automatically**: the channel captures the tenant, conversation, scope, and (when present) membership type from each inbound activity, and `bindTeams` resolves the workspace, surface, mode, and visibility with no further config.\n\n## Step 4 — Invoke it\n\n1. **In a team channel:** `@Open Everywhere` followed by a request. An `@mention` is how a conversation STARTS, and it also **engages** that reply chain: later plain messages in it wake the bot with no new mention, until someone asks it to stop following (it then goes quiet there until the next `@mention`). A non-mention message in a chain the bot was never invited into is still dropped. The follow-ups depend on the `ChannelMessage.Read.Group` RSC grant — without it Microsoft delivers only `@mention` activities, so the bot answers once per mention.\n2. **In a 1:1 chat:** message the bot directly — no `@mention` needed; personal chats are always for it.\n3. Expected behaviour: a typing indicator, then a Markdown reply in-thread. A DM keeps its own per-user memory, so what it learns in a 1:1 does not appear in the channel. Channel learnings stay siloed because an unconfirmed channel binds **`external` trust**, not because it is private — see [Gotchas](#gotchas).\n\n## What the agent calls itself here\n\nA Teams session's IDENTITY — the name it introduces itself by — comes from one place only: the\n[`identity.name`](../configuration-reference.md) setting. Set it and it **wins on every surface**,\nbyte-identically — the right answer for a deployment that spans Slack and Teams from one window:\none name, everywhere. Unset, the session renders the generic identity line (\"this team's agent\").\nIt never borrows a name from another surface, and (since OPE-610) never from this surface's own\nTeams display name either — that is a HANDLE, not an identity; see below.\n\nSeparately, the session also carries a HANDLE line — \"your mention/handle here appears as X\" — so\nthe agent recognises its own mentions and backscroll lines under the app's real Teams name. Two\nGraph reads feed it, tried in this order:\n\n1. **The Teams app-catalog name** — the app manifest's `name.short` the Teams client\n renders beside a message. The agent reads it from Microsoft Graph\n (`GET /appCatalogs/teamsApps?$filter=externalId eq '…'&$expand=appDefinitions`). This call needs\n a tenant-admin-consented `AppCatalog.Read.All`. Without that consent, or if the call fails for\n any other reason, the agent falls straight to the next option below.\n2. **The display name of your own Entra app registration** — the name you gave the app in Step 1.\n The agent reads it from Microsoft Graph\n (`GET /servicePrincipals(appId='…')?$select=displayName`) through the same app-only token the\n read adapter mints, and it needs **no extra Graph permission** — measured against a\n single-tenant app that carries only an RSC grant, where the implicit self-read answered 200. A\n multi-tenant install is reasoned to behave the same (the service principal exists in each\n consenting tenant) but is not measured.\n\nIf your app manifest's `name.short` and the app registration's display name are the\nsame string, both options answer identically and there is nothing to think about. They can differ\nin case or spacing (`open-everywhere` vs `Open Everywhere` is a real example) — grant\n`AppCatalog.Read.All` if you want the exact catalog string; otherwise the registration name is\nthe handle a session shows.\n\nEither read is resolved once per instance, so a rename or a newly granted consent takes effect on\nthe next cold start. If neither lookup can answer — no token, an unconsented tenant, a network\nfailure — the session simply carries no handle line; the identity line above is unaffected either\nway.\n\n## Reading channel/thread history\n\nTeams supports the generic `read_channel`/`read_thread` verbs (the same tools Slack and Discord\nuse), backed by Microsoft **Graph** rather than the Bot Framework Connector — the Connector token\nthis integration already mints (`MICROSOFT_APP_ID`/`MICROSOFT_APP_PASSWORD`/`MICROSOFT_TENANT_ID`)\ncan post activities but exposes no history-read API. The read adapter (`agent/channels/teams-read.ts`)\nmints its own app-only Graph token via the same client-credential flow, same three env vars.\n\n- **Permission needed:** the app registration's Graph API permissions need something that can read\n channel messages. If your Teams app package already grants the per-team RSC permission\n `ChannelMessage.Read.Group` (Step 2 above — the same grant that enables *ambient* watching), reads\n work in that team with **no extra Azure configuration**. This is the shipped default, and it is\n the option to keep — read [Which boundary governs your reads](#which-boundary-governs-your-reads)\n before you choose the other one. Missing every option surfaces as a Graph 401/403 — fail-loud,\n never a silent empty read.\n- **Teams has two team ids, and Graph takes the second one.** An inbound activity carries the Bot\n Connector's team id — a `19:…@thread.tacv2` string. Every Graph URL under `/teams/{team-id}/…`\n takes the team's **Microsoft 365 group id** (a GUID) instead; Graph keeps the `19:` spelling as a\n team's `internalId`. For these read verbs and for the [since-last-wake catch-up](#reading-channelthread-history),\n Open Everywhere resolves the one into the other over the Connector\n (`GET {serviceUrl}/v3/teams/{teamId}`, whose `aadGroupId` is the group id) and caches the answer\n per team, so you configure nothing. A team whose group id will not resolve gets **no read** and a\n stated reason, never a read of the wrong team. **Every** Graph caller resolves the id this way,\n the routine fire-eligibility room-membership probe included (see\n [Proactive routines](#proactive-routines)).\n\n### The since-last-wake catch-up read\n\nA standing Teams seat reads its own room on every wake, so it sees most of what was said while it\nwas idle. That read uses the same Graph credential as the verbs above, and it makes **two calls**,\nbecause Graph splits one channel's history across two endpoints:\n\n| Call | What Graph returns |\n| --- | --- |\n| `/teams/{group-id}/channels/{channel-id}/messages` | the channel's **top-level** messages only — a reply is never in it |\n| `…/messages/{root-id}/replies` | that **one reply chain's** replies, and not its root |\n\nThe seat is bound to a channel, and each wake arrives inside one reply chain. So the catch-up reads\nboth and merges them: the chain call is added whenever the activity names a chain, which a Teams\nchannel activity always does (its conversation id carries `;messageid=<root>`). Reading only the\nchannel is what made a chain that *had* carried the message read as an empty room.\n\n**Know this bound.** The window is the channel's top-level messages plus the **one** chain the wake\nsits in. A reply in another of the channel's reply chains does not reach the seat, because Graph\nexposes no channel-wide replies endpoint — reading every chain would cost one call per chain. So an\nempty catch-up is not proof the channel was quiet.\n\nThe read is **fail-soft**: a wake is never *failed* by its catch-up, and one endpoint refusing does\nnot throw away what the other returned. It does cost time — the two Graph calls and their two audit\nwrites run inline, before the seat is woken, though all of it sits behind the Bot Framework ack.\nThe audit row is what makes the read readable: without one, a 404, an empty page and a bound that\nexcluded everything all reach you as the same silence. One row per Graph call, action\n**`teams.catch_up`**:\n\n| Field | What it says |\n| --- | --- |\n| `resource` | `channel-messages`, `thread-replies`, or `none` when the address could not be built |\n| `status` | `ok`, `error`, or `unaddressable` |\n| `httpStatus` | the HTTP status, when Graph refused the call |\n| `path` | the Graph resource path the call addressed, host and query stripped |\n| `raw` | how many messages **Graph** returned |\n| `kept` | how many survived this surface's visibility gate and the since-bound |\n| `since` | the window's lower bound |\n\n`raw: 0` is Graph answering nothing. `raw` above zero with `kept: 0` is Open Everywhere's own bound\nor gate dropping what Graph sent. No message text and no author name goes in the row.\n\n`kept` is not \"what the seat read\". The shared catch-up path then drops the trigger message itself,\napplies the domain's admission policy and caps the window by tokens, so a counted message can still\nnot reach the seat.\n\n### Which boundary governs your reads\n\nMicrosoft accepts the one app-only Graph token Open Everywhere mints under **two very different\nauthorizations**. They are not alternatives of equal weight, and the runbook used to present them as\nif they were.\n\n| | Per-team (RSC) | Tenant-wide |\n|---|---|---|\n| **Permission** | `ChannelMessage.Read.Group`, granted by a team owner at install | any of `ChannelMessage.Read.All`, `Group.Read.All`, `Group.ReadWrite.All`, admin-consented on the Entra app registration |\n| **What Microsoft checks** | the grant on **that team**, on every request | nothing about teams |\n| **Reach** | the teams the app is installed in | **every channel message in the tenant** |\n| **What holds the line** | Microsoft | only Open Everywhere's own domain allowlist (`memory/config/domains/`) — application code |\n\n**A tenant-wide permission is boundary-REMOVING, not merely the other way to do it.** It deletes\nMicrosoft's per-team check. The agent's reads are then fenced by nothing but Open Everywhere's own\nallowlist, so a bug or a misconfiguration in that allowlist reaches the whole tenant rather than one\nteam. Grant it only when RSC is genuinely unavailable to you, and know that is the trade you made.\n\nTwo of the three tenant-wide permissions are easy to hold by accident: `Group.Read.All` and\n`Group.ReadWrite.All` are commonly consented for unrelated reasons, and either one gives the app\ntenant-wide channel-message reach whether or not anyone intended it. Microsoft lists all three as\nthe higher-privileged application permissions for\n[list channel messages](https://learn.microsoft.com/en-us/graph/api/channel-list-messages) and\n[list message replies](https://learn.microsoft.com/en-us/graph/api/chatmessage-list-replies).\n\n**How to see which one you are on.** Open Everywhere reads the answer out of its own Graph token —\nthe token's `roles` claim carries the tenant-wide permissions, and an RSC grant never appears there\n(RSC lives on the team resource, not on the app registration). Two places report it:\n\n- **The ops preflight.** Run the `preflight` tool of the `open-everywhere-ops` MCP server. With\n Teams credentials in your `.env.local` it mints a token and reports a `teams: channel-read\n boundary` check — `ok` for `per-team`, `info` for `tenant-wide` with the roles to remove. With no\n Teams credentials it reports nothing. A failed mint degrades to one `info` line; the preflight\n never blocks on it.\n- **The audit trail.** The deployment writes a `teams.read_boundary` row whenever the determination\n changes — on the first Graph read after a cold start, and again if you change the permissions. The\n row carries the boundary, which tenant-wide roles were found, and what is left enforcing the\n fence. Every Teams `read.refused` row also carries a `boundary` field. Both carry ids and verdicts\n only, never message content or any part of the token.\n\n> **The detection is one-sided, on purpose.** A tenant-wide role in the token is proof the boundary\n> is gone. Its absence is not proof an RSC grant exists — it means the token cannot reach past a\n> team by itself, so any read that *succeeds* went through Microsoft's per-team check. A deployment\n> with neither gets Graph 403s. The check is a denylist of the three roles above, so a *new*\n> tenant-wide permission Microsoft adds later would read as `per-team` until the list is updated;\n> there is no positive RSC signal to match instead.\n- **Domain `reach` entries use the SANITIZED channel id**, not the raw Teams id. A Teams channel\n id looks like `19:abc…@thread.tacv2`; Open Everywhere's identity binding filesystem-sanitizes every\n channel/domain key (colons and `@` become `-`) before it's used as a memory/allowlist grain — the\n same sanitization Slack/Discord ids never visibly need (they're already filesystem-safe). So a\n domain listing a Teams channel should use the sanitized form, e.g.\n `\"teams/<tenant-id>/19-abc-thread.tacv2\"`, not the raw `19:abc@thread.tacv2` string.\n- **A resource grant uses the SAME sanitized id**, for the same reason and with one addition. Write\n `teams:channel/19-abc-thread.tacv2` and `teams:person/29-1abc…`, never the raw ids — the address\n grammar refuses a second colon, so a pasted raw id fails at config load with a \"malformed resource\"\n error rather than in front of a member. The Teams provider restores the raw Bot Framework id at the\n transport edge, from an index it writes whenever an activity arrives. **One consequence to know:**\n a container or a person this deployment has never seen an inbound activity from cannot be restored,\n so the grant is refused out loud at send time. Grant a room the bot is in, and a person who has\n spoken to it or in one of its channels. **A granted container write starts a NEW thread in the\n channel.** A Teams channel activity arrives on one reply chain (`…;messageid=<root>`), and the\n index records the channel at BOTH grains, so the plain channel id you write in the roster is the id\n the write lands in — never whichever reply chain the bot last saw traffic in.\n- **Scope, and refusals are audited.** Same allowlist as every surface: the current channel plus any\n other Teams channel the session's [memory domain](../../capabilities/memory.md) lists in\n `reach`. An out-of-scope channel is refused before any Graph call, and every refusal writes a\n `read.refused` audit row naming the (sanitized) channel id, the caller's domain and the verdict —\n ids only, never message content. Query it with `GET /control/v1/audit?since=<iso>&actions=read.refused`.\n- **Ordering:** Graph's channel-messages and thread-replies endpoints support no `$orderby` and are\n observed to return messages **newest-first** (the opposite of Slack's thread-replies API) — the\n adapter always sorts by `createdDateTime` itself, so this is invisible to callers.\n\n## Proactive routines\n\nScheduled [routines](../../capabilities/proactivity-and-routines.md) work on Teams — with one platform rule worth knowing: Bot Framework only allows **proactive** messages to a conversation the bot has a stored *conversation reference* for (`serviceUrl` + conversation id, captured on a prior inbound turn); there is no cold-send to an arbitrary user or channel. Open Everywhere captures that reference off every inbound activity and stores it on the routine at creation, so any routine you create in-thread posts back **into the same conversation** with nothing to configure. A routine missing its reference fails closed — it runs dry (logged, schedule held) rather than posting anywhere else. Spend caps apply to channel/group-chat routines — and to personal (1:1) chat routines too, same as Slack: a fire never gets the admin bypass, because there is no attending admin to bypass on its behalf.\n\n**Creator eligibility at fire time.** Every clock fire re-checks the routine's creator, mirroring\nSlack (see [Proactivity & routines](../../capabilities/proactivity-and-routines.md)): a creator\nremoved from the team channel, or whose Entra account is disabled, **skips** that one fire rather\nthan posting — logged as a `routine.skip` row, never a failure. Two Graph app-only permissions\ndrive it:\n\n- **Room membership** — `GET /teams/{team-id}/channels/{channel-id}/members`. The **least\n privileged** application permission is `ChannelMember.Read.Group`, an **RSC** grant of the same\n family as `ChannelMessage.Read.Group`. **The shipped manifest carries it by default** (added at\n `version` `1.0.3`, paired with `ChannelMessage.Read.Group` under the same philosophy — see\n [Step 2](#step-2--package-the-teams-app)), so a new install enforces the room-membership check\n with no extra Azure configuration, on the same per-team boundary [described\n above](#which-boundary-governs-your-reads). An install that predates `1.0.3` needs the version\n bump + re-consent flow in Step 2 to pick it up — RSC grants never backfill on their own.\n `ChannelMember.Read.All` is the tenant-wide alternative, with the identical boundary trade-off.\n- **Account status** — `GET /users/{aad-object-id}?$select=accountEnabled`. This one has **no RSC\n equivalent** — a directory user lookup isn't scoped to a team — so it always needs the\n tenant-wide application permission `User.Read.All`, admin-consented on the Entra app\n registration. It stays a manual, eyes-open operator step; it cannot ship in the default manifest.\n\nMissing either permission is **not an outage**: a Graph 401/403 on either call is read as\n\"unresolvable\", exactly like a network hiccup, and the routine keeps firing on schedule — an\nunresolved check never skips a fire. `User.Read.All` still needs a deliberate grant to make its\nhalf of the check enforce instead of silently passing through.\n\n**The room check enforces, exactly as Slack's does.** A creator Graph confirms is no longer in the\nchannel stops getting that routine — the same outcome and the same `creator_removed_from_room` audit\nrow the Slack path writes. Only an answer the probe could not **get** lets the fire through, and\nevery such fire writes a `routine.eligibility_unresolved` row naming the check and the cause:\n\n| Cause on the row | What it means | What to do |\n| --- | --- | --- |\n| `read_error` | The Graph call failed — a network error, a 5xx, or a 401/403 for a permission this install does not hold. | Confirm `ChannelMember.Read.Group` is consented (Step 2). |\n| `team_id_unresolved` | The Bot Connector could not give this team's Microsoft 365 group id, which the Graph URL takes. | Check the Connector credential and the team's own state. |\n| `roster_incomplete` | The channel roster is larger than the probe's page bound, so the creator was neither found nor ruled out. | Nothing to fix — the bound is deliberate. |\n| `malformed_response` | Graph answered with something that is not a member page. | Report it; the probe refuses to guess a verdict from it. |\n| `no_provider` | No Teams provider is booted. | Only reachable on a misconfigured instance. |\n| `missing_ids` | The routine posts into a channel, but carries no team id or no creator Azure AD object id, so the roster cannot be asked about. | Expected on a routine created before those ids were captured. Re-create it to bring it under the check. |\n\nRead those rows before you conclude the check is working. A fire that was **really** checked writes\nnone of them, so the row's presence — not its absence — is what tells you a fire went unchecked. A\n**personal (1:1) chat** routine writes none either, and that is not a gap: a 1:1 chat has no channel\nroster, so room membership is not a question that exists there. The account-status half still runs.\n\n## Direct messages and the account directory\n\nTeams is the one surface that cannot cold-address a person: a Bot Framework write needs a **regional**\n`serviceUrl` and a Microsoft-minted conversation id, and both exist only on an inbound activity. Open\nEverywhere therefore records a conversation reference for every inbound activity it acts on — a **1:1\nchat** reference from a personal chat, keyed on the person, and a **team** reference from a channel,\nkeyed on the team. The rows are refreshed by later activities, because Microsoft does not promise a\ntenant keeps its region, and they live in the operational store, not in configuration.\n\nTwo capabilities read that record, and both are used by [identity\nlinking](../../how-it-works/identity.md):\n\n- **Direct messages.** The agent posts into a stored 1:1 chat. Where it holds none and the deployment\n has seen exactly one tenant, it opens the chat with `POST /v3/conversations` and remembers it.\n- **The account directory** — \"who here is called that?\" — reads the Bot Connector roster\n (`pagedmembers`) of the teams it has references for. It needs no Graph permission at all: the bot's\n own credential carries it. Graph is deliberately *not* used, because Graph has no Bot Framework\n `29:` id and that is the id a Teams identity link is keyed on.\n\nWhat that means for you, in three lines:\n\n- A deployment whose bot has never been added to a **team channel** has no account directory. Add it\n to one channel and the directory works from the next message.\n- **A person must have the Teams app installed in the personal scope** before the agent can open a\n 1:1 chat with them. Microsoft refuses the open otherwise, and Open Everywhere reports the refusal\n with its reason rather than dropping the message.\n- A deployment serving **two tenants** will not open a cold 1:1 chat for somebody it has never met —\n it refuses rather than guess which organisation the account belongs to. A single message from that\n person to the bot fixes it permanently.\n\n## Gotchas\n\n- **Public endpoint is mandatory.** The Bot Framework Connector pushes activities to your messaging endpoint over HTTPS; it can't reach `localhost`. Use a tunnel for local work and update the messaging endpoint whenever the tunnel URL changes.\n- **The Bot Framework SDK is archived; this transport is not.** Microsoft archived the Bot Framework SDK and the Bot Framework Emulator on GitHub, stopped maintaining them, and ends support tickets for the SDK on 31 December 2025, pointing at the Microsoft 365 Agents SDK and the Teams SDK as successors (Bot Framework basics, read 2026-09-02). **Open Everywhere does not use that SDK.** `packages/providers/teams/agent/channels/teams-connector.ts` calls the Bot Framework **Connector REST API** directly — `POST /v3/conversations/{id}/activities` and its reply and update siblings — under a client-credentials token eve mints. The Connector REST API is the Azure Bot Service protocol, and Microsoft has not archived it. So this is a long-horizon transport risk, not a defect: re-read it if Microsoft deprecates the Connector REST surface itself, or if a Teams feature we want turns out to be Agents-SDK-only.\n- **Tenant ID is required.** New Azure Bots are Single-tenant (multi-tenant creation was deprecated after 31 July 2025), so you **must** set `MICROSOFT_TENANT_ID` to the app registration's Directory (tenant) ID — auth fails without it. A mismatch between the registration's tenant and your env is the most common \"the bot never responds\" cause. (Legacy multi-tenant bots registered before the cutoff still work and omit it.)\n- **Channels default to private *and* external — and it is the EXTERNAL half that contains memory.** Because inbound activities usually omit `membershipType`, every team channel is treated as `private` **and** binds `external` trust. The `external` trust is what does the work: read-only `public` memory at most, no internal domain, no org brain. 🔴 The `private` half contains NOTHING on its own. Visibility is tracked and audited; no code reads it to gate memory (`resolveMemoryPlan` branches on mode and trust), and there is no per-channel \"surface tier\" for a channel's learnings to stay in. So when the Graph-API enrichment below lands and a channel positively confirms `internal`, a private channel with no domain falls straight to the org fallback and WRITES the org brain — privacy will not hold it back. This is intentional fail-closed behaviour today, for the trust reason only. To light up shared memory today, either declare the surface in `agent/identity/surfaces.ts` (the vouch above — the precise option), or set `OE_TRUST=internal` on a single-tenant deployment you vouch for, or wait for the Graph-API enrichment that positively confirms `membershipType`; until then channel memory is correct-but-conservative.\n- **The app password (client secret) expires.** Entra client secrets have a finite lifetime (default ~6–24 months). When it expires the bot silently stops authenticating — rotate it in **Certificates & secrets** and update `MICROSOFT_APP_PASSWORD`.\n- **A group chat answers when it is ENGAGED, and `ChatMessage.Read.Chat` alone does not engage it.** The RSC permission is necessary (without it Microsoft never delivers non-mention chat activities) but not sufficient: OE's ingress admits a plain chat message only where somebody `@mentioned` the bot in that chat and has not asked it to stop following. Granting the permission therefore does not turn every message in every chat into an agent session — which is what it used to do, before the engagement seam gated that branch. The cross-surface single window is separate and still admits `scope === \"channel\"` only — a group chat is a per-person surface, not an org container, and admitting one would let it join a private shared window (the same channels-only rule Slack follows). **Undirected ambient watching** — waking on messages in a room nobody invited the bot into — therefore stays a **team-channel** capability.\n\n## End-to-end testing (Graph API driver)\n\nThe [e2e driver pack](../../../packages/core/scripts/e2e/) drives each surface with a real message and reads the reply back. Slack and Discord can inject a synthetic wake — Slack by signing a forged event with the deployment's own signing secret, Discord via a second driver bot. **Teams can't:** the Bot Framework JWT on a real inbound Activity is *Microsoft-signed*, and we don't hold the key, so there's no way to forge an event the deployment will trust. Instead the Teams driver drives a **genuine** message — it `@mentions` the bot over *delegated Microsoft Graph* (a real test user's session), which makes Azure deliver a real Activity that wakes the bot, then reads OE's reply back from the message's native reply chain. Scenario `J-TEAMS` is the repeatable version of the manual \"mention it and watch it reply\" check.\n\n**One-time setup:**\n\n1. **Register a separate Entra app** as a **public client** (this is *not* OE's bot app registration): in the app registration, **Authentication → Advanced settings → Allow public client flows → Yes**, and under **API permissions** add the *delegated* Microsoft Graph perms `ChannelMessage.Send`, `ChannelMessage.Read.All`, and `offline_access`, then have a tenant admin **Grant admin consent**. Note its **Application (client) ID**.\n2. The **test user** whose session you'll authenticate must be a **member of the team** OE is installed in (the same team/channel `J-TEAMS` posts into).\n3. Mint the refresh token and discover the ids — run once:\n\n ```bash\n export E2E_TEAMS_TENANT_ID=<directory-tenant-id>\n export E2E_TEAMS_CLIENT_ID=<the public-client app's client id>\n npm run e2e:teams-auth\n ```\n\n It runs a device-code sign-in (open the URL, enter the code), prints the `export E2E_TEAMS_REFRESH_TOKEN='…'` line, and lists your teams + channels so you can copy the team and channel ids. The token is printed to stdout only — it's never written to a file; paste it into your shell / CI secrets.\n4. **Set the six `E2E_TEAMS_*` vars** and run the scenario:\n\n ```bash\n export E2E_TEAMS_TENANT_ID=<directory-tenant-id>\n export E2E_TEAMS_CLIENT_ID=<public-client app id>\n export E2E_TEAMS_REFRESH_TOKEN=<from the helper above>\n export E2E_TEAMS_TEAM_ID=<from the helper's listing>\n export E2E_TEAMS_CHANNEL_ID=<from the helper's listing>\n export E2E_TEAMS_BOT_APP_ID=<OE's MICROSOFT_APP_ID>\n E2E_ONLY=J-TEAMS npm run test:e2e-driver\n ```\n\n Use the `E2E_ONLY` env var to run a single scenario, not `-- --only J-TEAMS`: the root\n `test:e2e-driver` delegates to a workspace script via `npm run`, and the inner `npm`\n swallows a forwarded `--only` as its own (deprecated) config flag rather than passing it\n through — so the flag form silently runs the *whole* pack. `E2E_ONLY` bypasses npm entirely.\n\nWithout these vars the scenario self-skips (fail-closed), naming exactly what's missing — the same posture as every other driver half.\n\n**The 1:1 DM plane (`J-TEAMS-DM`, OPE-972).** This leg drives the bot's personal (1:1) chat, not\na channel. It rides the SAME six `E2E_TEAMS_*` vars as `J-TEAMS` — no extra fixture var — but needs\ntwo more things true of the deployment and the driver app, and derives both at RUNTIME rather than\nthrough an env var:\n\n1. **The driver user has OE installed in the PERSONAL scope.** Microsoft auto-provisions the 1:1\n chat only after that install exists. `resolveDmChat()` finds the install row itself\n (`GET /me/teamwork/installedApps?$expand=teamsApp`, matched on `teamsApp.externalId ===\n E2E_TEAMS_BOT_APP_ID`) — no new var carries this.\n2. **The driver app's Entra registration has a `Chat.*` delegated Graph permission consented** —\n `Chat.ReadWrite`, or `ChatMessage.Send` + `Chat.Read` separately. The one-time setup above only\n grants `ChannelMessage.*` (channel messages), which is a *different* Graph permission family\n from chat messages; Graph 403s a `/chats/{id}/messages` call under either scope alone. Add the\n `Chat.ReadWrite` delegated permission to the same app registration used above and have a tenant\n admin **Grant admin consent** for it — the existing refresh token picks up the new scope on its\n next `.default` refresh, no re-auth needed.\n\nEither gap self-skips the leg with the exact missing thing named (`ctx.skip`, never a false red) —\nrun `E2E_ONLY=J-TEAMS-DM npm run test:e2e-driver` and read the skip reason.\n",
14
14
  },
15
15
  ];
package/agent/ops.ts CHANGED
@@ -5,7 +5,11 @@
5
5
  // WHY THIS CANNOT BE AN ENV CHECK. Both boundaries use the SAME three env vars and the SAME token
6
6
  // mint. The difference is which Graph permission the tenant consented, which lives in the token's
7
7
  // `roles` claim — so the probe mints a token and decodes it. teams-graph-boundary.ts owns the
8
- // predicate and the reasoning; this file is only the ops skin over it.
8
+ // reasoning and teams-graph-roles.ts the pure predicate; this file is only the ops skin over it.
9
+ //
10
+ // It imports core through the LIGHT door (`adapter-api-ops.js`), and the predicate from
11
+ // teams-graph-roles.ts, which imports no core (OPE-2219). The main barrel loads most of core, so one
12
+ // import of it here added about 1.9 s to the ops MCP server's start. Boundary rule 4d pins this.
9
13
  //
10
14
  // This file (not core) owns the Teams env-key knowledge — MICROSOFT_APP_ID / _PASSWORD /
11
15
  // _TENANT_ID and their TEAMS_* fallbacks are the bundle's vendor fingerprints (boundary rule 6).
@@ -18,8 +22,22 @@
18
22
  // path is a documented, supported configuration, and `missing` would make `ready` false and stop
19
23
  // an operator who has made an informed choice. It is advice, exactly like the two-role DB nudge.
20
24
 
21
- import { registerPreflightProbe, registerSurfaceCredentialProbe, type Check } from "@open-everywhere/core/agent/adapter-api.js";
22
- import { boundaryOfGraphToken, TENANT_WIDE_CHANNEL_READ_ROLES, type TeamsReadBoundary } from "./channels/teams-graph-boundary.js";
25
+ import {
26
+ configuredAgentName,
27
+ registerPreflightProbe,
28
+ registerSurfaceAppManifest,
29
+ registerSurfaceCredentialProbe,
30
+ type Check,
31
+ } from "@open-everywhere/core/agent/adapter-api-ops.js";
32
+ import {
33
+ TEAMS_APP_CONSOLE_URL,
34
+ TEAMS_APP_MANIFEST_COLLECT,
35
+ TEAMS_APP_MANIFEST_STEPS,
36
+ TEAMS_APP_NAME_MAX,
37
+ teamsAppManifestTemplate,
38
+ type TeamsAppManifestOptions,
39
+ } from "./app-manifest.js";
40
+ import { boundaryOfGraphToken, TENANT_WIDE_CHANNEL_READ_ROLES, type TeamsReadBoundary } from "./channels/teams-graph-roles.js";
23
41
 
24
42
  const TOKEN_LOGIN_BASE = "https://login.microsoftonline.com";
25
43
  const GRAPH_TOKEN_SCOPE = "https://graph.microsoft.com/.default";
@@ -126,4 +144,24 @@ export async function teamsBoundaryProbe(env: NodeJS.ProcessEnv): Promise<readon
126
144
  export function registerTeamsOps(): void {
127
145
  registerPreflightProbe("teams", teamsBoundaryProbe);
128
146
  registerSurfaceCredentialProbe("teams", () => Boolean(process.env.MICROSOFT_APP_ID));
147
+ // THE TEAMS APP MANIFEST (OPE-2334) — rendered from the settings on every call, so
148
+ // `surface_manifest { surface: "teams" }` names the app with the configured `identity.name`. An
149
+ // explicit `appName` still wins. It replaces the static `teams-app/manifest.json`.
150
+ registerSurfaceAppManifest("teams", {
151
+ format: "json",
152
+ consoleUrl: TEAMS_APP_CONSOLE_URL,
153
+ steps: TEAMS_APP_MANIFEST_STEPS,
154
+ collect: TEAMS_APP_MANIFEST_COLLECT,
155
+ template: (opts) => teamsAppManifestTemplate(teamsManifestOptionsFromSettings(opts)),
156
+ appNameMax: TEAMS_APP_NAME_MAX,
157
+ });
158
+ }
159
+
160
+ /** The manifest options for this deployment: the caller's explicit name over the settings. */
161
+ function teamsManifestOptionsFromSettings(opts: { readonly appName?: string } = {}): TeamsAppManifestOptions {
162
+ const agentName = configuredAgentName();
163
+ return {
164
+ ...(opts.appName === undefined ? {} : { appName: opts.appName }),
165
+ ...(agentName === undefined ? {} : { agentName }),
166
+ };
129
167
  }
package/agent/register.ts CHANGED
@@ -115,8 +115,8 @@ export function resolveTeamsTarget(r: Routine): DispatchTarget | null {
115
115
  // scope resolves to "channel" (capped — the conservative arm).
116
116
  const mode = teamsMode(conversationType);
117
117
  // OPE-486 — the fire's WAKE ORIGIN, parity with resolveSlackTarget (routine-dispatch.ts). Absent
118
- // before this fix, so a Teams fire could never divert into the unified window (fire-window.ts's
119
- // planFireWindow fails closed on a missing origin). serviceUrl/conversationId are already
118
+ // before this fix, so a Teams fire could never divert into the unified window (fire-work.ts's
119
+ // planFireWork fails closed on a missing origin). serviceUrl/conversationId are already
120
120
  // non-empty-checked above (isTrustedTeamsServiceUrl re-validates serviceUrl in the codec's
121
121
  // validate arm); no replyToActivityId — a proactive fire has no reply chain to anchor to, matching
122
122
  // teams-front.ts's own ambient-origin mint (`{ platform: "teams", serviceUrl, conversationId }`).
@@ -0,0 +1,23 @@
1
+ /** The token the URLs carry where the deployment's own host belongs (core's DEPLOY_HOST_PLACEHOLDER). */
2
+ export declare const TEAMS_DEPLOY_HOST_PLACEHOLDER = "YOUR-DEPLOY-URL";
3
+ /** The app name the manifest carries when neither an explicit name nor `identity.name` is given. */
4
+ export declare const TEAMS_DEFAULT_APP_NAME = "Open Everywhere";
5
+ /** Teams' limit on `name.short`. A longer one fails the package upload. */
6
+ export declare const TEAMS_APP_NAME_MAX = 30;
7
+ /** Options for {@link teamsAppManifestTemplate}. */
8
+ export interface TeamsAppManifestOptions {
9
+ /** An EXPLICIT app name (the ops tool's `appName`). It wins over {@link agentName}. */
10
+ readonly appName?: string;
11
+ /** The deployment's configured name — the `identity.name` setting. Absent ⇒ {@link TEAMS_DEFAULT_APP_NAME}. */
12
+ readonly agentName?: string;
13
+ }
14
+ /** The ready-to-zip Teams app manifest (schema 1.17), named for this deployment. */
15
+ export declare function teamsAppManifestTemplate(opts?: TeamsAppManifestOptions): string;
16
+ /** Where the operator uploads the package. */
17
+ export declare const TEAMS_APP_CONSOLE_URL = "https://admin.teams.microsoft.com";
18
+ /** The ordered steps, from the manifest to an installed app. */
19
+ export declare const TEAMS_APP_MANIFEST_STEPS: readonly string[];
20
+ /** What the operator brings back. Teams needs no value from the package itself: the bot's
21
+ * credentials come from the Azure Bot resource (docs/deploy/surfaces/teams.md). */
22
+ export declare const TEAMS_APP_MANIFEST_COLLECT: readonly string[];
23
+ //# sourceMappingURL=app-manifest.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-manifest.d.ts","sourceRoot":"","sources":["../../agent/app-manifest.ts"],"names":[],"mappings":"AAgBA,yGAAyG;AACzG,eAAO,MAAM,6BAA6B,oBAAoB,CAAC;AAE/D,oGAAoG;AACpG,eAAO,MAAM,sBAAsB,oBAAoB,CAAC;AAExD,2EAA2E;AAC3E,eAAO,MAAM,kBAAkB,KAAK,CAAC;AASrC,oDAAoD;AACpD,MAAM,WAAW,uBAAuB;IACtC,uFAAuF;IACvF,QAAQ,CAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAC1B,+GAA+G;IAC/G,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;CAC7B;AAYD,oFAAoF;AACpF,wBAAgB,wBAAwB,CAAC,IAAI,GAAE,uBAA4B,GAAG,MAAM,CA6DnF;AAED,8CAA8C;AAC9C,eAAO,MAAM,qBAAqB,sCAAsC,CAAC;AAEzE,gEAAgE;AAChE,eAAO,MAAM,wBAAwB,EAAE,SAAS,MAAM,EAMrD,CAAC;AAEF;oFACoF;AACpF,eAAO,MAAM,0BAA0B,EAAE,SAAS,MAAM,EAIvD,CAAC"}
@@ -0,0 +1,113 @@
1
+ // THE Teams app manifest — rendered, never static (OPE-2334).
2
+ //
3
+ // This used to be the static `teams-app/manifest.json`, with the brand written into `name.short`,
4
+ // `name.full` and `description.full`. A deployment is meant to be locally namable, so the manifest
5
+ // is now a TEMPLATE the deployment's configured name renders, the same way the Slack manifest is.
6
+ // `agent/ops.ts` registers it with core's `registerSurfaceAppManifest`, so the ops MCP tool
7
+ // `surface_manifest { surface: "teams" }` serves it with this deployment's host substituted.
8
+ //
9
+ // 🔴 THIS MODULE IMPORTS NOTHING, DELIBERATELY — the same rule as the Slack bundle's manifest. The
10
+ // caller reads the settings and passes the name in; this file holds only the vendor's own knowledge.
11
+ //
12
+ // TWO PLACEHOLDERS STAY FOR A HUMAN. `{{MICROSOFT_APP_ID}}` is the Azure Bot's application id and
13
+ // `{{DEVELOPER_NAME}}` is the publishing org. Neither is a deployment setting this plane can read,
14
+ // so `teams-app/README.md` walks the operator through both. The host is the shared
15
+ // `YOUR-DEPLOY-URL` placeholder, which `surface_manifest` substitutes.
16
+ /** The token the URLs carry where the deployment's own host belongs (core's DEPLOY_HOST_PLACEHOLDER). */
17
+ export const TEAMS_DEPLOY_HOST_PLACEHOLDER = "YOUR-DEPLOY-URL";
18
+ /** The app name the manifest carries when neither an explicit name nor `identity.name` is given. */
19
+ export const TEAMS_DEFAULT_APP_NAME = "Open Everywhere";
20
+ /** Teams' limit on `name.short`. A longer one fails the package upload. */
21
+ export const TEAMS_APP_NAME_MAX = 30;
22
+ /** Teams' limit on `name.full`. */
23
+ const TEAMS_APP_FULL_NAME_MAX = 100;
24
+ /** The manifest `version`. RSC consent is captured at install time, so a permission change needs a
25
+ * bump (scripts/teams-provider-integration-test.ts pins it past the bumps that carried each grant). */
26
+ const TEAMS_APP_MANIFEST_VERSION = "1.0.3";
27
+ /** The app name a manifest carries: the explicit name, else the configured name cut to Teams' limit,
28
+ * else the default. */
29
+ function teamsManifestAppName(opts = {}) {
30
+ const explicit = opts.appName?.trim();
31
+ if (explicit)
32
+ return explicit;
33
+ const configured = opts.agentName?.trim();
34
+ if (configured)
35
+ return configured.slice(0, TEAMS_APP_NAME_MAX).trim();
36
+ return TEAMS_DEFAULT_APP_NAME;
37
+ }
38
+ /** The ready-to-zip Teams app manifest (schema 1.17), named for this deployment. */
39
+ export function teamsAppManifestTemplate(opts = {}) {
40
+ const name = teamsManifestAppName(opts);
41
+ const fullNamed = `${name} — autonomous teammate`;
42
+ const host = TEAMS_DEPLOY_HOST_PLACEHOLDER;
43
+ return JSON.stringify({
44
+ $schema: "https://developer.microsoft.com/en-us/json-schemas/teams/v1.17/MicrosoftTeams.schema.json",
45
+ manifestVersion: "1.17",
46
+ version: TEAMS_APP_MANIFEST_VERSION,
47
+ id: "{{MICROSOFT_APP_ID}}",
48
+ developer: {
49
+ name: "{{DEVELOPER_NAME}}",
50
+ websiteUrl: `https://${host}`,
51
+ // The deployment serves both pages itself (packages/core/agent/pages/policy-pages.ts).
52
+ privacyUrl: `https://${host}/privacy`,
53
+ termsOfUseUrl: `https://${host}/terms`,
54
+ },
55
+ name: {
56
+ short: name,
57
+ full: fullNamed.length <= TEAMS_APP_FULL_NAME_MAX ? fullNamed : name,
58
+ },
59
+ description: {
60
+ short: "An autonomous teammate with per-conversation memory.",
61
+ // OPE-1954: the agent acts under ONE shared bot credential, never on a person's behalf.
62
+ full: `${name} runs as a Teams bot: @mention it in a team channel, drop it into a group chat, or DM it 1:1. ` +
63
+ "It acts under one bot identity shared by this deployment — never under your account and never with your " +
64
+ "permissions — and its memory is scoped per tenant and per conversation. Learnings stay siloed to a " +
65
+ "conversation until that channel is confirmed team-wide.",
66
+ },
67
+ icons: { color: "color.png", outline: "outline.png" },
68
+ accentColor: "#5865F2",
69
+ bots: [
70
+ {
71
+ botId: "{{MICROSOFT_APP_ID}}",
72
+ scopes: ["team", "groupChat", "personal"],
73
+ supportsFiles: false,
74
+ isNotificationOnly: false,
75
+ supportsCalling: false,
76
+ supportsVideo: false,
77
+ },
78
+ ],
79
+ permissions: ["identity", "messageTeamMembers"],
80
+ // REQUIRED whenever `authorization` is present: without it the upload fails with a generic
81
+ // "couldn't parse the app manifest".
82
+ webApplicationInfo: { id: "{{MICROSOFT_APP_ID}}", resource: `api://${host}/{{MICROSOFT_APP_ID}}` },
83
+ // The RSC grants that make Microsoft deliver non-@mention activities (teams-app/README.md).
84
+ authorization: {
85
+ permissions: {
86
+ resourceSpecific: [
87
+ { name: "ChannelMessage.Read.Group", type: "Application" },
88
+ { name: "ChatMessage.Read.Chat", type: "Application" },
89
+ { name: "ChannelMember.Read.Group", type: "Application" },
90
+ ],
91
+ },
92
+ },
93
+ validDomains: [host],
94
+ }, null, 2);
95
+ }
96
+ /** Where the operator uploads the package. */
97
+ export const TEAMS_APP_CONSOLE_URL = "https://admin.teams.microsoft.com";
98
+ /** The ordered steps, from the manifest to an installed app. */
99
+ export const TEAMS_APP_MANIFEST_STEPS = [
100
+ "Save the manifest below as manifest.json in the teams-app/ directory, beside color.png and outline.png.",
101
+ "Replace {{MICROSOFT_APP_ID}} with the Azure Bot's Application (client) ID (MICROSOFT_APP_ID) — it appears three times and must match everywhere.",
102
+ "Replace {{DEVELOPER_NAME}} with your org's name.",
103
+ "Zip it flat: zip -j app.zip manifest.json color.png outline.png (Teams requires the manifest at the zip root).",
104
+ "Upload it: Teams → Apps → Manage your apps → Upload a custom app for one team, or the Teams Admin Center → Manage apps → Upload new app for the org.",
105
+ ];
106
+ /** What the operator brings back. Teams needs no value from the package itself: the bot's
107
+ * credentials come from the Azure Bot resource (docs/deploy/surfaces/teams.md). */
108
+ export const TEAMS_APP_MANIFEST_COLLECT = [
109
+ "Azure Bot → Configuration → Microsoft App ID = MICROSOFT_APP_ID",
110
+ "The app registration's client secret = MICROSOFT_APP_PASSWORD",
111
+ "The Entra tenant id = MICROSOFT_TENANT_ID",
112
+ ];
113
+ //# sourceMappingURL=app-manifest.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"app-manifest.js","sourceRoot":"","sources":["../../agent/app-manifest.ts"],"names":[],"mappings":"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"}
@@ -16,7 +16,7 @@
16
16
  // 🔴 THE PLATFORM CONSTRAINT, AND WHY IT IS NOT A DEFECT. Microsoft delivers a NON-MENTION message
17
17
  // to a bot only where the tenant has consented to the matching resource-specific-consent (RSC)
18
18
  // grant: `ChannelMessage.Read.Group` for a team channel, `ChatMessage.Read.Chat` for a chat (both
19
- // are in `teams-app/manifest.json`). Without it, only @mention activities ever arrive, so the seam
19
+ // are in the rendered Teams app manifest, `agent/app-manifest.ts`). Without it, only @mention activities ever arrive, so the seam
20
20
  // is armed but nothing can exercise it — the surface answers once per mention, exactly as before.
21
21
  // There is NO API that reports whether a tenant granted an RSC permission, so this module states
22
22
  // the dependency out loud instead of guessing, and logs the named reason ONCE per scope while no
@@ -1 +1 @@
1
- {"version":3,"file":"teams-engagement.js","sourceRoot":"","sources":["../../../agent/channels/teams-engagement.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"teams-engagement.js","sourceRoot":"","sources":["../../../agent/channels/teams-engagement.ts"],"names":[],"mappings":"AAAA,gGAAgG;AAChG,mGAAmG;AACnG,oGAAoG;AACpG,kFAAkF;AAClF,EAAE;AACF,mGAAmG;AACnG,uBAAuB;AACvB,iGAAiG;AACjG,gGAAgG;AAChG,4EAA4E;AAC5E,kGAAkG;AAClG,kGAAkG;AAClG,oGAAoG;AACpG,iDAAiD;AACjD,EAAE;AACF,mGAAmG;AACnG,+FAA+F;AAC/F,kGAAkG;AAClG,kIAAkI;AAClI,kGAAkG;AAClG,iGAAiG;AACjG,iGAAiG;AACjG,mGAAmG;AACnG,EAAE;AACF,0FAA0F;AAC1F,kGAAkG;AAClG,qGAAqG;AACrG,qGAAqG;AACrG,4FAA4F;AAC5F,mGAAmG;AACnG,qGAAqG;AACrG,8FAA8F;AAC9F,iGAAiG;AACjG,mGAAmG;AAGnG,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrF,OAAO,EACL,mBAAmB,EACnB,mBAAmB,EACnB,iBAAiB,EACjB,gBAAgB,EAChB,qBAAqB,EACrB,kBAAkB,GAGnB,MAAM,4CAA4C,CAAC;AAEpD;;4DAE4D;AAC5D,MAAM,UAAU,cAAc,CAAC,KAA6B;IAC1D,IAAI,KAAK,KAAK,UAAU;QAAE,OAAO,SAAS,CAAC;IAC3C,OAAO,KAAK,KAAK,WAAW,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC,CAAC,2BAA2B,CAAC;AACvF,CAAC;AAED;0EAC0E;AAC1E,MAAM,UAAU,0BAA0B,CAAC,KAA6B;IACtE,MAAM,KAAK,GAAG,cAAc,CAAC,KAAK,CAAC,IAAI,cAAc,CAAC;IACtD,MAAM,IAAI,GAAG,KAAK,KAAK,WAAW,CAAC,CAAC,CAAC,YAAY,CAAC,CAAC,CAAC,SAAS,CAAC;IAC9D,OAAO,CACL,kCAAkC,IAAI,2BAA2B,KAAK,uCAAuC;QAC7G,uGAAuG,CACxG,CAAC;AACJ,CAAC;AAED;;;;;;;;;;GAUG;AACH,MAAM,UAAU,oBAAoB,CAAC,CAAuB;IAC1D,8FAA8F;IAC9F,+FAA+F;IAC/F,mGAAmG;IACnG,yEAAyE;IACzE,MAAM,IAAI,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC;IAC7B,OAAO,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,IAAI,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,KAAK,KAAK,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;AAC5G,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAA6B,EAAE,cAAkC;IACrG,IAAI,CAAC,cAAc,IAAI,cAAc,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IACrE,OAAO,SAAS,CAAC,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,cAAc,CAAC;AAChE,CAAC;AAED,kGAAkG;AAClG,EAAE;AACF,qGAAqG;AACrG,gGAAgG;AAChG,6FAA6F;AAC7F,kGAAkG;AAClG,mBAAmB;AAEnB,MAAM,kBAAkB,GAAG,IAAI,GAAG,EAAU,CAAC;AAC7C,MAAM,qBAAqB,GAAG,IAAI,GAAG,EAAU,CAAC;AAEhD;;uEAEuE;AACvE,MAAM,UAAU,qBAAqB,CAAC,KAA6B;IACjE,IAAI,cAAc,CAAC,KAAK,CAAC,KAAK,SAAS;QAAE,kBAAkB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AACzE,CAAC;AAED,4EAA4E;AAC5E,MAAM,UAAU,uBAAuB,CAAC,KAA6B;IACnE,OAAO,kBAAkB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AACvC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,kBAAkB,CAAC,KAA6B;IAC9D,IAAI,cAAc,CAAC,KAAK,CAAC,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC,CAAC,uBAAuB;IAC9E,IAAI,kBAAkB,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,qBAAqB,CAAC,GAAG,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACpF,qBAAqB,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IACjC,oGAAoG;IACpG,gGAAgG;IAChG,oGAAoG;IACpG,kGAAkG;IAClG,OAAO,CAAC,IAAI,CAAC,8FAA8F,0BAA0B,CAAC,KAAK,CAAC,2JAA2J,CAAC,CAAC;IACzS,OAAO,IAAI,CAAC;AACd,CAAC;AAED,+CAA+C;AAC/C,MAAM,UAAU,qBAAqB;IACnC,kBAAkB,CAAC,KAAK,EAAE,CAAC;IAC3B,qBAAqB,CAAC,KAAK,EAAE,CAAC;AAChC,CAAC;AAiCD;;;;;;;;;GASG;AACH,SAAS,kBAAkB,CAAC,CAAuB;IACjD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,cAAc,CAAC;YAC1B,MAAM,EAAE,CAAC,CAAC,IAAI,CAAC,EAAE;YACjB,QAAQ,EAAE,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,YAAY,CAAC,QAAQ;YAC/C,cAAc,EAAE,CAAC,CAAC,YAAY,CAAC,EAAE;YACjC,SAAS,EAAE,CAAC,CAAC,cAAc;YAC3B,MAAM,EAAE,CAAC,CAAC,MAAM;YAChB,KAAK,EAAE,CAAC,CAAC,KAAK;YACd,cAAc,EAAE,mBAAmB,CAAC,CAAC,CAAC,WAAW,CAAC;SACnD,CAAC,CAAC;QACH,OAAO,gBAAgB,CAAC,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,EAAE,CAAC,CAAC;IAChG,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,CAAuB,EACvB,eAAwB,EACxB,IAAI,GAAwB,EAAE;IAE9B,MAAM,MAAM,GAAG,qBAAqB,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;IACjE,IAAI,CAAC,MAAM;QAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,0EAA0E,EAAE,CAAC;IAC/H,MAAM,SAAS,GAAG,kBAAkB,CAAC,CAAC,CAAC,CAAC;IACxC,IAAI,CAAC,SAAS;QAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,MAAM,EAAE,8DAA8D,EAAE,CAAC;IAEtH,IAAI,CAAC,CAAC,cAAc;QAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,SAAS,CAAC,GAAG,EAAE,CAAC;IAE5E,8FAA8F;IAC9F,8FAA8F;IAC9F,sCAAsC;IACtC,qBAAqB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IAE/B,MAAM,IAAI,GAAG,IAAI,CAAC,aAAa,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE,CAAC,mBAAmB,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC;IAC7E,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IAC1C,MAAM,QAAQ,GAAG,kBAAkB,CAAC;QAClC,SAAS,EAAE,GAAG,KAAK,IAAI;QACvB,OAAO,EAAE,GAAG,EAAE,OAAO,IAAI,KAAK;QAC9B,WAAW,EAAE,eAAe;QAC5B,YAAY,EAAE,KAAK,EAAE,sDAAsD;QAC3E,QAAQ,EAAE,IAAI,EAAE,yEAAyE;KAC1F,CAAC,CAAC;IACH,IAAI,QAAQ,CAAC,OAAO;QAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC;IAClF,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,OAAO,EAAE,GAAG,EAAE,OAAO,KAAK,IAAI,EAAE,CAAC;AAC3F,CAAC;AAED;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CAAC,CAAuB,EAAE,IAAI,GAAwB,EAAE;IAC5F,MAAM,MAAM,GAAG,qBAAqB,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,YAAY,CAAC,EAAE,CAAC,CAAC;IACjE,IAAI,CAAC,MAAM;QAAE,OAAO,IAAI,CAAC;IACzB,MAAM,SAAS,GAAG,kBAAkB,CAAC,CAAC,CAAC,CAAC;IACxC,IAAI,CAAC,SAAS;QAAE,OAAO,IAAI,CAAC;IAC5B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;IACzD,MAAM,IAAI,GACR,IAAI,CAAC,WAAW,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,CAAC,iBAAiB,CAAC,CAAC,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;IACtH,MAAM,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC,CAAC;IACrC,mGAAmG;IACnG,6EAA6E;IAC7E,kBAAkB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC;IAC5B,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,kGAAkG;AAClG,EAAE;AACF,+FAA+F;AAC/F,mGAAmG;AACnG,qGAAqG;AACrG,6FAA6F;AAC7F,mGAAmG;AACnG,6FAA6F;AAC7F,EAAE;AACF,8FAA8F;AAC9F,yFAAyF;AACzF,kGAAkG;AAClG,gGAAgG;AAChG,+CAA+C;AAC/C,EAAE;AACF,qGAAqG;AACrG,oGAAoG;AACpG,gDAAgD;AAChD,EAAE;AACF,gGAAgG;AAChG,mGAAmG;AACnG,6FAA6F;AAC7F,iGAAiG;AACjG,iGAAiG;AACjG,gGAAgG;AAChG,gGAAgG;AAEhG;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,SAAoB;IAC9D,MAAM,IAAI,GAAG,qBAAqB,CAAC,SAAS,CAAC,CAAC;IAC9C,IAAI,CAAC;QACH,OAAO,CAAC,MAAM,mBAAmB,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,UAAU,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,EAAE,OAAO,KAAK,IAAI,CAAC;IACnI,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,OAAO,CAAC,IAAI,CAAC,+DAA+D,IAAI,CAAC,SAAS,CAAC,UAAU,8CAA8C,MAAM,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAClK,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;8CAE8C;AAC9C,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,SAAoB,EAAE,KAAa;IAC5E,MAAM,IAAI,GAAG,qBAAqB,CAAC,SAAS,CAAC,CAAC;IAC9C,+FAA+F;IAC/F,iGAAiG;IACjG,kBAAkB;IAClB,MAAM,iBAAiB,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC,CAAC;AACxF,CAAC;AAED;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,SAAoB,EAAE,KAAa,EAAE,SAAiB;IAC7F,MAAM,IAAI,GAAG,qBAAqB,CAAC,SAAS,CAAC,CAAC;IAC9C,MAAM,iBAAiB,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,SAAS,EAAE,CAAC,CAAC;AAClG,CAAC;AAED;;;;;;;;;GASG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,CAAuB,EAAE,IAAI,GAAwB,EAAE;IAC9F,IAAI,CAAC,CAAC,cAAc;QAAE,OAAO,IAAI,CAAC,CAAC,oDAAoD;IACvF,+FAA+F;IAC/F,8FAA8F;IAC9F,0EAA0E;IAC1E,MAAM,OAAO,GAAG,MAAM,sBAAsB,CAAC,CAAC,EAAE,oBAAoB,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,CAAC;IAC/E,OAAO,OAAO,CAAC,IAAI,KAAK,SAAS,CAAC;AACpC,CAAC"}
@@ -33,7 +33,7 @@
33
33
  // domain becomes a judged channel-watch wake in the shared window. NOTE the platform reality:
34
34
  // Teams only delivers non-mention messages to a bot whose app carries the matching RSC permission
35
35
  // — `ChannelMessage.Read.Group` for a team channel, `ChatMessage.Read.Chat` for a chat (both are
36
- // in teams-app/manifest.json). Without the relevant one, only @mention activities arrive and Leg A
36
+ // in the rendered Teams app manifest, agent/app-manifest.ts). Without the relevant one, only @mention activities arrive and Leg A
37
37
  // is simply never exercised (no code path depends on it firing).
38
38
  //
39
39
  // CHAT SCOPE IS DELIBERATELY NOT LEG A. The plan's privacy floor (teams-supervisor.ts