Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
4f6a5e9
Add pack+sideload Teams app script to activity-protocol postdeploy (#…
Copilot Jul 17, 2026
e236622
Suppress local invoke hint for websocket agents
Copilot Jul 17, 2026
e7a4c35
Revert "Suppress local invoke hint for websocket agents"
Copilot Jul 17, 2026
b70acb6
Fix golangci-lint lll violation in teams_sideload_script.go
Copilot Jul 17, 2026
41006fd
Silence cspell for base64 icon constants in teams_sideload_script.go
Copilot Jul 17, 2026
451f2bf
Address Copilot review: build-only mode, Teams field limits, safe hints
Copilot Jul 17, 2026
1408d80
Avoid gosec G101 false positive in sideload run-command test
Copilot Jul 17, 2026
24d5454
Rename test var to clear gosec G101 (pw substring match)
Copilot Jul 17, 2026
2757e1d
Address 2nd Copilot review: manifest version, scopes, exit codes, quo…
Copilot Jul 17, 2026
1c0f3ae
Bound Teams manifest version components to the 65535 limit
Copilot Jul 17, 2026
1b677f5
Fix PS7.4+ native-error abort and qualify the no-admin claims
Copilot Jul 17, 2026
dc89ae6
Enforce script mode on rewrite and show build-only opt-out per shell
Copilot Jul 17, 2026
f5b4726
Re-chunk embedded base64 icons under the 125-char Go line limit
Copilot Jul 17, 2026
6b2de0a
Do not clobber user-owned files sharing the sideload script name
Copilot Jul 17, 2026
ba95231
Reject non-regular files, unique pwsh temp dir, guide fast-path condi…
Copilot Jul 17, 2026
f85eb4e
Enable the fast path only when all sideload scripts were written
Copilot Jul 17, 2026
cd21bb5
Always give a next action; scope the build-only flag in PowerShell
Copilot Jul 17, 2026
9291d75
fix(agents): guarantee monotonic Teams version and drop unrunnable sc…
Copilot Jul 17, 2026
d445022
fix(agents): harden version state file and use full atk login command
Copilot Jul 17, 2026
c1c9eb5
fix(agents): detect ATK 'Cannot get token' unauthenticated error
Copilot Jul 17, 2026
b54f5cc
fix(agents): scope generated Teams artifacts per agent
Copilot Jul 17, 2026
e64b0c9
fix(agents): reword comment to satisfy cspell
Copilot Jul 17, 2026
4d647d7
Key Teams sideload ownership on the stable bot name; add execution tests
Copilot Jul 17, 2026
5400a6e
Make the run hint shell-agnostic and stop the ps1 auto-opening a browser
Copilot Jul 17, 2026
f0ee571
Harden generated-file writes and make the run hint portable to Window…
Copilot Jul 17, 2026
b0997f5
fix(activity): key Teams script ownership on agent name; lock version…
Copilot Jul 17, 2026
6b72194
test(activity): use a unique sentinel in the refresh assertion
Copilot Jul 17, 2026
188a1a9
fix(activity): expansion-safe run hint, safer version lock, cd hint i…
Copilot Jul 17, 2026
445345f
fix(activity): Windows execution-policy bypass, shell-safe cd, faithf…
Copilot Jul 17, 2026
695c969
fix(agents): harden Teams sideload guide/scripts (round 21 review)
Copilot Jul 17, 2026
f122be1
fix(agents): shell-independent run hint + verifiable version-lock (ro…
Copilot Jul 17, 2026
1a51292
fix(agents): transactional pair write, agent-scoped legacy guide, loc…
Copilot Jul 17, 2026
a58ff27
fix(agents): consistent guide cd + tolerate npm install failure (roun…
Copilot Jul 17, 2026
2292c80
Slim down Teams pack+sideload script generation
Copilot Jul 23, 2026
a27b1ac
Correct stale comments/docs after slim-down; drop dead field
Copilot Jul 23, 2026
718c633
Guard atk auth login in the PowerShell sideload script
Copilot Jul 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,12 +1,67 @@
<!-- Generated by 'azd deploy' (activity protocol) for agent {{.AgentName}}. azd may replace this file on the next deploy; copy it elsewhere to keep manual edits. -->
# Connect {{.AgentName}} to Microsoft Teams

`azd deploy` already did the Azure side for you:

- Azure Bot: `{{.BotName}}` (Microsoft Teams channel enabled)
- Bot ID (msaAppId): `{{.MsaAppID}}` <- you will paste this as the bot id
- Bot ID (msaAppId): `{{.MsaAppID}}` <- you will paste this as the bot id{{if .TenantID}}
- Microsoft 365 tenant: `{{.TenantID}}` <- the bot is single-tenant; sign in / sideload here{{end}}

Two manual steps remain: (A) create a Teams app package, then (B) upload it.
They are the same for any activity-protocol agent.
{{if .ScriptsGenerated}}
## Fastest path — run the generated script

`azd deploy` also wrote a runnable **pack-and-sideload script** next to this guide
that does A and B for you in one command (the Bot ID is already baked in). Starting
from the **project root** (the folder where you ran `azd deploy`), `cd` into the
agent's source folder — where the script lives — and run it:

```powershell
cd {{.ServiceCdPwsh}} # from the project root, into the agent's source folder
powershell -NoProfile -ExecutionPolicy Bypass -File ./pack-and-sideload-teams-app.ps1 # Windows / PowerShell
```
```sh
cd {{.ServiceCdPosix}} # from the project root, into the agent's source folder
./pack-and-sideload-teams-app.sh # macOS / Linux
```

It builds the Teams app package and installs it **for you** (`atk install --scope
Personal` — no org-catalog admin approval needed), then prints an "Open in Teams"
link. It is idempotent, so you can re-run it safely. Custom app upload
(sideloading) must be enabled for your tenant; if it is turned off, a Teams admin
must enable it (see the restricted-tenant note below).

Prerequisites:

- **Node.js** (for `npm`) — the script installs the Microsoft 365 Agents Toolkit
CLI (`atk`) via npm if it is missing.
- A one-time **`atk auth login m365`** with your M365 account — the script launches
this for you if you are not signed in.{{if .TenantID}} Sign in with an account in
tenant `{{.TenantID}}` (the bot is single-tenant), or the install will fail.{{end}}
- `--scope Personal` installs only for you and needs **no org-catalog admin
approval** (an org-wide catalog upload does; see step B below). Custom app
upload still has to be enabled for your tenant — if it is off, a Teams admin
must turn it on (see the restricted-tenant note below).

To build the package without installing it (packaging still runs), set the
build-only opt-out for your shell:

```powershell
# PowerShell: scope the flag to this one run so later runs are not stuck in build-only mode
try { $env:SKIP_TEAMS_INSTALL = "1"; powershell -NoProfile -ExecutionPolicy Bypass -File ./pack-and-sideload-teams-app.ps1 } finally { $env:SKIP_TEAMS_INSTALL = $null }
```
```sh
SKIP_TEAMS_INSTALL=1 ./pack-and-sideload-teams-app.sh # macOS / Linux (per-command)
```

Prefer the manual / UI flow, a restricted tenant, or custom manifest edits?
Follow steps A and B below instead.
{{else}}
> **Note:** azd did not generate the pack-and-sideload script this time (writing
> it was skipped or failed), so follow the manual steps A and B below. Re-running
> `azd deploy` will try to write the script again.
{{end}}

## A. Create the Teams app package

Expand Down Expand Up @@ -87,11 +142,12 @@ Compress-Archive manifest.json,color.png,outline.png {{.AgentName}}-teams-app.zi
```

Sideload for yourself with the Microsoft 365 Agents Toolkit CLI (atk). `--scope Personal` is a
per-user install and needs NO Teams admin:
per-user install and needs no org-catalog admin approval (custom app upload must still be
enabled for your tenant):

```sh
npm install -g @microsoft/m365agentstoolkit-cli # one-time; requires Node.js
atk auth login # sign in with your M365 account
atk auth login m365 # sign in with your M365 account{{if .TenantID}} in tenant {{.TenantID}} (single-tenant bot){{end}}
atk install --file-path {{.AgentName}}-teams-app.zip --scope Personal
```

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
#!/usr/bin/env pwsh
Comment thread
v1212 marked this conversation as resolved.
# Pack and sideload the Microsoft Teams app for {{.AgentName}} -- one command.
#
# Generated by 'azd deploy' (activity protocol) for agent {{.AgentName}}. Every id below was filled in by
# azd from the deployed agent and its auto-created Azure Bot, so this script does
# NOT call Azure -- it just packages and installs the Teams app.
#
# What it does (idempotent -- safe to re-run):
# 1. builds a Teams app package (manifest.json + icons) in a temp dir,
# 2. ensures the Microsoft 365 Agents Toolkit CLI (atk) is installed,
# 3. installs the app FOR THE CURRENT USER (atk install --scope Personal --
# no org-catalog admin approval needed; your tenant must still allow custom
# app upload/sideloading, which a Teams admin can enable if it is off), and
# 4. prints an "Open in Teams" chat deep link.
#
# Prerequisites: Node.js (npm) for the atk CLI, and a one-time 'atk auth login m365'
# with your M365 account (this script launches it for you if you are not signed
# in). Set SKIP_TEAMS_INSTALL=1 to build the package only and skip the install.

$ErrorActionPreference = "Stop"

# ---- Ids baked in by azd deploy (do not edit) -------------------------------
$AgentName = "{{.AgentName}}"
$BotId = "{{.MsaAppID}}" # Teams manifest bots[].botId = the Azure Bot msaAppId
$TeamsAppId = "{{.TeamsAppId}}" # stable per agent, so re-runs update the same app

# Teams manifest v1.19 caps name.short at 30 and description.short at 80 chars,
# but agent names may be longer, so bound the constrained fields.
$shortName = if ($AgentName.Length -gt 30) { $AgentName.Substring(0, 30) } else { $AgentName }
$shortDesc = "Chat with $shortName in Microsoft Teams."
if ($shortDesc.Length -gt 80) { $shortDesc = $shortDesc.Substring(0, 80) }

# Teams treats a re-uploaded package (same app id) as an update only when the
# manifest version is higher, so derive a strictly increasing version from the
# current time -- no state file, lock, or counter to leave behind. Encode epoch
# seconds into two Teams-bounded components (each capped at 65535): minor = N /
# 65536, patch = N % 65536 (minor stays < 65535 until ~2106). Two runs in the same
# second get the same version; if that ever rejects the second upload as "not
# newer", just re-run a moment later.
$nowEpoch = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
$verMinor = [int]([math]::Floor($nowEpoch / 65536))
$verPatch = [int]($nowEpoch % 65536)
$pkgVersion = "1.$verMinor.$verPatch"

Write-Host "Agent: $AgentName"
Write-Host "Bot ID: $BotId"
Write-Host "Teams app id: $TeamsAppId"

# ---- Build the Teams app package in a temp dir ------------------------------
# Use a unique per-invocation directory (like the Bash script's mktemp -d) so
# concurrent runs -- including two different projects that share an agent name --
# never share a manifest/zip or overwrite each other's package.
$buildDir = Join-Path ([System.IO.Path]::GetTempPath()) ("teams-app-" + [Guid]::NewGuid().ToString("N"))
New-Item -ItemType Directory -Force -Path $buildDir | Out-Null

$manifest = [ordered]@{
'$schema' = "https://developer.microsoft.com/en-us/json-schemas/teams/v1.19/MicrosoftTeams.schema.json"
manifestVersion = "1.19"
version = $pkgVersion
id = $TeamsAppId
developer = [ordered]@{
name = "Microsoft Foundry"
websiteUrl = "https://www.example.com"
privacyUrl = "https://www.example.com/privacy"
termsOfUseUrl = "https://www.example.com/termsofuse"
}
icons = [ordered]@{ color = "color.png"; outline = "outline.png" }
name = [ordered]@{ short = $shortName; full = "$AgentName (Activity, Teams)" }
description = [ordered]@{
short = $shortDesc
full = "A Microsoft Foundry hosted agent on the Activity protocol. Send it a message in Teams."
}
accentColor = "#5B5FC7"
bots = @([ordered]@{ botId = $BotId; scopes = @("personal"); supportsFiles = $false; isNotificationOnly = $false })
permissions = @("identity")
validDomains = @()
}
# Write manifest.json as UTF-8 WITHOUT a BOM. Windows PowerShell 5.1's
# Set-Content -Encoding UTF8 prepends a BOM, which some Teams/atk JSON parsers
# reject; WriteAllText with an explicit no-BOM encoding is correct on 5.1 and 7.
$noBomUtf8 = New-Object System.Text.UTF8Encoding($false)
[System.IO.File]::WriteAllText(
(Join-Path $buildDir "manifest.json"), ($manifest | ConvertTo-Json -Depth 10), $noBomUtf8)

# ---- Write the icons (embedded PNGs -- no image tooling needed) --------------
# color.png is 192x192, outline.png is 32x32, per the Teams manifest icon rules.
$colorB64 = "{{.ColorPngB64}}"
$outlineB64 = "{{.OutlinePngB64}}"
[System.IO.File]::WriteAllBytes((Join-Path $buildDir "color.png"), [Convert]::FromBase64String($colorB64))
[System.IO.File]::WriteAllBytes((Join-Path $buildDir "outline.png"), [Convert]::FromBase64String($outlineB64))

# ---- Zip the package (files at the zip root, not in a subfolder) ------------
$zipPath = Join-Path $buildDir "$AgentName-teams-app.zip"
if (Test-Path $zipPath) { Remove-Item $zipPath -Force }
Compress-Archive `
-Path (Join-Path $buildDir "manifest.json"), (Join-Path $buildDir "color.png"), (Join-Path $buildDir "outline.png") `
-DestinationPath $zipPath -Force
Write-Host "Teams app package: $zipPath"

# Build-only mode: the package (with icons) is ready; skip the atk install.
if ($env:SKIP_TEAMS_INSTALL -eq "1") {
Write-Host "SKIP_TEAMS_INSTALL=1 - package built; skipping the per-user Teams install."
Write-Host "Sideload it manually (requires custom app upload to be enabled for your tenant):"
Write-Host " $zipPath"
Write-Host " Teams -> Apps -> Manage your apps -> Upload an app -> Upload a custom app"
return
}

# ---- Ensure the atk CLI is available ----------------------------------------
if (-not (Get-Command atk -ErrorAction SilentlyContinue)) {
if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
throw "Node.js/npm is required for the atk CLI (per-user Teams install). Install Node.js, then re-run this script."
}
Write-Host "Installing the Microsoft 365 Agents Toolkit CLI (atk)..."
# Tolerate a failed install: with the top-level $ErrorActionPreference = "Stop"
# and $PSNativeCommandUseErrorActionPreference (default on PowerShell 7.4+), a
# nonzero npm exit becomes a terminating error that would stop the script
# before the Get-Command check below emits the actionable manual-install
# message. Catch it and fall through, mirroring the bash script's `|| true`.
try {
npm install -g '@microsoft/m365agentstoolkit-cli' | Out-Null
} catch {
Write-Host "atk install via npm did not complete: $_"
}
if (-not (Get-Command atk -ErrorAction SilentlyContinue)) {
throw "Failed to install atk. Install it manually: npm i -g @microsoft/m365agentstoolkit-cli"
}
}

# atk is a native command; on PowerShell 7.4+ a nonzero exit combined with
# $ErrorActionPreference = "Stop" would terminate the script before we can inspect
# the output (e.g. the "not signed in" case). Run the probe installs with
# non-terminating error handling and return the combined output so the
# login-and-retry logic below can run.
function Invoke-AtkInstallProbe {
param([Parameter(Mandatory)][string]$Zip)
$ErrorActionPreference = "Continue"
atk install --file-path "$Zip" --scope Personal --interactive false 2>&1 | Out-String
}

# ---- Install the app for the current user (Personal scope) ------------------
Write-Host ""
Write-Host "Installing the Teams app for the current user (atk, scope Personal)..."
$installOut = Invoke-AtkInstallProbe -Zip $zipPath
Write-Host $installOut

# If atk reports the user is not signed in, launch an interactive login and retry.
if ($installOut -match "(?i)(not\s+(logged|signed)\s+in|auth.*required|please\s+login|login\s+first|no\s+account|cannot\s+get\s+token|log\s+in\s+the\s+correct\s+account)") {
Write-Host "Not signed in - launching 'atk auth login m365' (complete the sign-in prompt)..."
# atk is a native command; on PowerShell 7.4+ a nonzero exit (e.g. the user
# cancels the sign-in) combined with $ErrorActionPreference = "Stop" would
# terminate the script here, before the retry and the graceful fallback below.
# Run it with non-terminating error handling so a cancelled/failed sign-in
# falls through, mirroring the bash script's tolerant set +e block.
try {
$ErrorActionPreference = "Continue"
atk auth login m365
} catch {
Write-Host "atk auth login did not complete: $_"
} finally {
$ErrorActionPreference = "Stop"
}
$installOut = Invoke-AtkInstallProbe -Zip $zipPath
Write-Host $installOut
}

$titleId = $null
$m = [regex]::Match($installOut, 'TitleId:\s*(\S+)')
if ($m.Success) { $titleId = $m.Groups[1].Value.Trim() }

# Teams addresses a bot 1:1 as "28:<botId>". Once the app is installed for the
# user, this opens the conversation with the agent directly.
$chatLink = "https://teams.microsoft.com/l/chat/0/0?users=28:$BotId"

if ([string]::IsNullOrWhiteSpace($titleId)) {
Write-Host ""
Write-Host "Could not confirm the per-user install."
Write-Host "If you were prompted to sign in, run 'atk auth login m365' then re-run this script."
Write-Host "Or sideload the package manually (requires custom app upload to be enabled for your tenant):"
Write-Host " $zipPath"
Write-Host " Teams -> Apps -> Manage your apps -> Upload an app -> Upload a custom app"
# The install did not complete, so report failure (build-only mode already
# returned earlier). The package + manual steps above remain available.
exit 1
}

Write-Host ""
Write-Host "Installed for the current user (titleId: $titleId)."
Write-Host "Chat with the agent in Teams:"
Write-Host " $chatLink"
Loading
Loading