diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 00000000..9a4d5038 --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,15 @@ +{ + "name": "microsoft-agents-sdk", + "owner": { "name": "Microsoft" }, + "metadata": { + "description": "Claude Code skills for building agents with the Microsoft Agents SDK" + }, + "plugins": [ + { + "name": "agents-for-js", + "source": "./agent-plugins/agents-for-js", + "description": "Skills for building agents with the Microsoft 365 Agents SDK for TypeScript/JavaScript", + "version": "1.0.0" + } + ] +} diff --git a/agent-plugins/CONTRIBUTING.md b/agent-plugins/CONTRIBUTING.md new file mode 100644 index 00000000..7b8b933f --- /dev/null +++ b/agent-plugins/CONTRIBUTING.md @@ -0,0 +1,105 @@ +# Contributing to Agent Plugins + +This guide explains how to add new skills, update existing skills, and add new plugins to this marketplace. + +--- + +## How It's Organized + +``` +.claude-plugin/marketplace.json ← top-level marketplace registry +agent-plugins/ + agents-for-js/ ← a plugin + plugin.json ← plugin metadata + skills/ + / + SKILL.md ← the skill content +``` + +The **marketplace registry** (`/.claude-plugin/marketplace.json`) lists the plugins available for install. Each **plugin** groups related skills for a language or platform. Each **skill** is a Markdown file with a YAML frontmatter block that tells the AI assistant when to activate it. + +--- + +## Adding a New Skill to an Existing Plugin + +1. **Create a directory** under the plugin's `skills/` folder. Name it after the skill using lowercase and hyphens: + + ``` + agent-plugins/agents-for-js/skills/my-new-skill/ + ``` + +2. **Create `SKILL.md`** in that directory with a YAML frontmatter block: + + ```markdown + --- + name: my-new-skill + description: Use when [trigger condition that activates this skill] + --- + + # Skill Title + + Skill content here... + ``` + +3. **Write a precise `description`** — this is the trigger condition the AI uses to decide when to load the skill. Be specific: + - Good: `Use when any code imports @microsoft/agents-hosting or when building a new agent` + - Too vague: `Use for agents` + - Too broad: `Use when working with JavaScript` + +4. **No registration needed** — skills are auto-discovered from the `skills/` directory via the `"skills": "skills/"` entry in `plugin.json`. + +--- + +## Updating an Existing Skill + +Edit the `SKILL.md` file directly. There is no build step — changes take effect the next time the plugin is loaded. +[Updating the version](#versioning) will cause most agentic clients to update the skill automatically. + +--- + +## Adding a New Plugin + +A plugin groups skills for a new language, platform, or use case (e.g., a `agents-for-dotnet` plugin). + +1. **Create a plugin directory** under `agent-plugins/`: + + ``` + agent-plugins/agents-for-dotnet/ + ``` + +2. **Add `plugin.json`**: + + ```json + { + "name": "agents-for-dotnet", + "description": "Skills for building agents with the Microsoft 365 Agents SDK for .NET", + "version": "1.0.0", + "author": { + "name": "Microsoft" + }, + "license": "MIT", + "keywords": ["microsoft", "agents", "teams", "dotnet"], + "skills": "skills/" + } + ``` + +3. **Add skills** following the steps in [Adding a New Skill](#adding-a-new-skill-to-an-existing-plugin). + +4. **Register the plugin** in `/.claude-plugin/marketplace.json` by adding an entry to the `plugins` array: + + ```json + { + "name": "agents-for-dotnet", + "source": "./agent-plugins/agents-for-dotnet", + "description": "Skills for building agents with the Microsoft 365 Agents SDK for .NET", + "version": "1.0.0" + } + ``` + +5. **Document it** in `agent-plugins/README.md` — add the plugin to the Available Plugins table and list its skills. + +--- + +## Versioning + +Bump the `version` field in `plugin.json` when making significant changes to a plugin's skills. This helps users know when to reinstall. diff --git a/agent-plugins/README.md b/agent-plugins/README.md new file mode 100644 index 00000000..98c0f5e8 --- /dev/null +++ b/agent-plugins/README.md @@ -0,0 +1,69 @@ +# Agent Plugins + +This directory contains AI coding assistant plugins for the Microsoft Agents SDK. + +Plugins provide skills — contextual guidance that activates automatically when you work on relevant code. When you import `@microsoft/agents-hosting`, your assistant gets Agents SDK knowledge loaded into context. + +## Available Plugins + +### `agents-for-js` + +Skills for building agents with the Microsoft 365 Agents SDK for TypeScript/JavaScript. + +| Skill | Activates when... | +|-------|-------------------| +| `agents-sdk-typescript` | Code imports `@microsoft/agents-hosting`, `@microsoft/agents-hosting-express`, or related packages, or when building a new agent | +| `agents-sdk-debugging` | Resolving problems with a Microsoft Agents SDK agent | +| `azure-agents-sdk-provision` | Provisioning Azure Bot resources, configuring identity credentials, or setting up OAuth via `az` CLI | + +--- + +## Installing the Plugin Marketplace + +The plugin marketplace is hosted at the root of this repository (`.claude-plugin/marketplace.json`). + +### Claude Code + +Run these commands inside Claude Code: + +``` +/plugin marketplace add microsoft/Agents +``` + +Then install the plugin: + +``` +/plugin install agents-for-js@microsoft-agents-sdk +``` + +Skills activate automatically based on what you're working on — no manual loading needed. + +To verify installation: + +``` +/plugin +``` + +### GitHub Copilot CLI + +Add the marketplace: + +``` +/plugin marketplace add microsoft/Agents +``` + +Then install the plugin: + +``` +/plugin install agents-for-js@microsoft-agents-sdk +``` + +--- + +## How Skills Work + +Skills are Markdown files with a YAML frontmatter block that defines a `name` and `description`. The `description` is used by the AI assistant to decide when to activate the skill — it acts as a trigger condition. + +When a skill activates, its full content is loaded into the assistant's context, giving it precise knowledge of the SDK's APIs, patterns, and common mistakes. + +To browse skill content directly, see the [`agents-for-js/skills/`](./agents-for-js/skills/) directory. diff --git a/agent-plugins/agents-for-js/.claude-plugin/plugin.json b/agent-plugins/agents-for-js/.claude-plugin/plugin.json new file mode 100644 index 00000000..57d01cad --- /dev/null +++ b/agent-plugins/agents-for-js/.claude-plugin/plugin.json @@ -0,0 +1,4 @@ +{ + "name": "agents-for-js", + "description": "Skills for building agents with the Microsoft 365 Agents SDK for TypeScript/JavaScript" +} diff --git a/agent-plugins/agents-for-js/plugin.json b/agent-plugins/agents-for-js/plugin.json new file mode 100644 index 00000000..ad12f5dc --- /dev/null +++ b/agent-plugins/agents-for-js/plugin.json @@ -0,0 +1,11 @@ +{ + "name": "agents-for-js", + "description": "Skills for building agents with the Microsoft 365 Agents SDK for TypeScript/JavaScript", + "version": "1.0.0", + "author": { + "name": "Microsoft" + }, + "license": "MIT", + "keywords": ["microsoft", "agents", "teams", "copilot-studio", "m365"], + "skills": "skills/" +} diff --git a/agent-plugins/agents-for-js/skills/agents-sdk-debugging/SKILL.md b/agent-plugins/agents-for-js/skills/agents-sdk-debugging/SKILL.md new file mode 100644 index 00000000..e1a40a76 --- /dev/null +++ b/agent-plugins/agents-for-js/skills/agents-sdk-debugging/SKILL.md @@ -0,0 +1,277 @@ +--- +name: agents-sdk-debugging +description: Use when attempting to resolve problems with an agent built using Microsoft Agents SDK @microsoft/agents-hosting and related packages. +--- + +# Debugging Agents Built with Microsoft Agents SDK + +## Overview + +Most agent failures fall into one of three categories: the code doesn't build or start, the configuration is wrong, or the agent isn't reachable. Work through this checklist in order — each step confirms a prerequisite for the next. + +## Checklist + +You MUST create a task for each of these items and complete them in order: + +1. Make sure the code builds successfully. +2. Make sure the application starts and runs without crashing. +3. Make sure the application opens a port and listens for incoming requests. +4. Validate the `.env` configuration. +5. Validate the bot's credentials against Azure AD. +6. Use the Agents Playground to test the agent end-to-end locally. + +--- + +### 1. Build the code + +```bash +npm run build +``` + +Expected: exits with code 0, no errors. Fix any TypeScript or import errors before continuing. + +--- + +### 2. Start the application + +Run with debug logging enabled to get detailed output from the SDK internals: + +```bash +DEBUG=agents:* npm start +``` + +Or for anonymous local dev: + +```bash +DEBUG=agents:* node ./dist/index.js +``` + +The `DEBUG=agents:*` flag enables verbose logging across all SDK namespaces. Scope down to reduce noise: + +```bash +DEBUG=agents:* npm start # everything +DEBUG=agents:authorization:* npm start # all auth (most useful starting point) +DEBUG=agents:msal npm start # token acquisition only +``` + + +#### Auth & connections + +| Namespace | What it logs | +|---|---| +| `agents:authorization:connections` | Auth connections loaded at startup (clientId, tenantId, authType); which connection is selected per request | +| `agents:authorization:manager` | Auth handlers configured at startup (type, scopes); which handler is invoked per request | +| `agents:authorization:azurebot` | Azure Bot sign-in flow detail (token exchange, magic code, SSO) | +| `agents:authorization:agentic` | Agentic auth flow detail (token acquisition, OBO) | +| `agents:authorization` | High-level authorization middleware decisions | +| `agents:msal` | MSAL token acquisition (token requests, cache hits, OBO) | +| `agents:jwt-middleware` | Incoming JWT validation | +| `agents:authConfiguration` | Auth configuration loading | + +#### Adapter & request handling + +| Namespace | What it logs | +|---|---| +| `agents:cloud-adapter` | Incoming request processing, activity dispatch | +| `agents:base-adapter` | Base adapter lifecycle | +| `agents:connector-client` | Outbound calls to the Bot Connector service | +| `agents:user-token-client` | User token client requests | + +#### Application & state + +| Namespace | What it logs | +|---|---| +| `agents:app` | AgentApplication routing and lifecycle | +| `agents:activity-handler` | ActivityHandler event dispatch | +| `agents:state` | State read/write operations | +| `agents:turnState` | Turn state access | +| `agents:memory-storage` | MemoryStorage read/write | +| `agents:middleware` | Middleware pipeline execution | + +#### Streaming, attachments & transcripts + +| Namespace | What it logs | +|---|---| +| `agents:streamingResponse` | Streaming response lifecycle | +| `agents:attachmentDownloader` | Attachment download requests | +| `agents:M365AttachmentDownloader` | M365-specific attachment downloads | +| `agents:file-transcript-logger` | File transcript write operations | +| `agents:rest-client` | REST client calls (transcript middleware) | + +#### Agent-to-agent + +| Namespace | What it logs | +|---|---| +| `agents:agent-client` | Outbound agent client calls and response handling | + +Watch for crash output. Common startup errors: + +- **`Cannot find module`** — missing `npm install`, or `dist/` not built yet +- **`ERR_MODULE_NOT_FOUND`** — check `"type": "module"` in `package.json` and that imports use `.js` extensions +- **Port already in use** — another process is on port 3978; kill it or set `PORT` in `.env` + +If the agent starts cleanly, you should see output like: + +``` +Server listening on port 3978 +``` + +--- + +### 3. Confirm the agent is reachable + +```bash +curl -s -o /dev/null -w "%{http_code}" \ + -X POST http://localhost:3978/api/messages \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +| Response | Meaning | +|---|---| +| `401` | Agent is running, auth is active — this is correct for a configured agent | +| `200` | Agent is running with auth disabled (blank `clientId`) — correct for anonymous local dev | +| `000` or connection refused | Agent is not running, wrong port, or crashed on startup | + +--- + +### 4. Validate `.env` configuration + +Configuration mistakes are the most common source of failures. Check each area below. + +#### 4a. Confirm the file is being loaded + +The SDK requires `node --env-file .env` (Node 20+) or a manual `dotenv` call. If you're using `npm start`, check that `package.json` uses `--env-file`: + +```json +"start": "node --env-file .env ./dist/index.js" +``` + +Without `--env-file`, environment variables silently don't load and the agent starts with no auth config. + +#### 4b. Check for the correct env var format + +The SDK uses the **modern `connections__` format**. Using the legacy flat format (`clientId=`, `clientSecret=`, `tenantId=`) still works but is only for backwards compatibility. Mixing the two formats causes silent misconfiguration. + +**Modern format (use this):** +``` +connections__serviceConnection__settings__clientId= +connections__serviceConnection__settings__clientSecret= +connections__serviceConnection__settings__tenantId= +connectionsMap__0__connection=serviceConnection +connectionsMap__0__serviceUrl=* +``` + +**Legacy format (avoid for new agents):** +``` +clientId= +clientSecret= +tenantId= +``` + +#### 4c. Check double-underscore separators + +A single underscore (`_`) is not the same as a double underscore (`__`). The SDK uses `__` to separate path segments. A typo like `connections_serviceConnection_settings_clientId` will be silently ignored. + +#### 4d. Check `connectionsMap` entries + +If you have multiple connections, each must have a `connectionsMap` entry. The first entry whose `serviceUrl` pattern matches the incoming request wins. Always include a `serviceUrl=*` fallback as the last entry. + +With a single connection, `connectionsMap` can be omitted — the SDK defaults to `serviceUrl=*`. + +#### 4e. Check OAuth handler variables + +If your agent uses user sign-in (`authorization: { graph: { ... } }`), the OAuth connection name must be set: + +``` +graph_connectionName=GraphOAuthConnection +``` + +The prefix (`graph`) must match the key used in the `authorization` config in your code. A mismatch causes the sign-in flow to fail silently or with a cryptic error. + +--- + +### 5. Validate bot credentials against Azure AD + +Once the `.env` looks correct, confirm the credentials actually work by requesting a token: + +```bash +curl -s -X POST \ + "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" \ + -d "grant_type=client_credentials\ +&client_id=$clientId\ +&client_secret=$clientSecret\ +&scope=https://api.botframework.com/.default" \ + | jq '{token_type, expires_in, error, error_description}' +``` + +A successful response includes `access_token`. Common errors: + +| Error code | Cause | +|---|---| +| `AADSTS700016` | `clientId` not found in tenant — wrong ID or wrong tenant | +| `AADSTS7000215` | Invalid `clientSecret` — expired or incorrect | +| `AADSTS90002` | `tenantId` not found | + +--- + +### 6. Test with Agents Playground + +The Agents Playground acts as a mock connector and channel client. It lets you test the full message flow locally without deploying to Azure or configuring a real Bot resource. + +**Install:** +```bash +npm install -g agentsplayground +``` + +**Run against an anonymous agent (no `.env` needed):** +```bash +npm test +``` + +This assumes your `package.json` has: +```json +"start:anon": "node ./dist/index.js", +"test-tool": "agentsplayground -c emulator", +"test": "npm-run-all -p -r start:anon test-tool" +``` + +**Run against an authenticated agent:** +```bash +agentsplayground -c msteams \ + --client-id \ + --client-secret \ + --tenant-id +``` + +**Channel options** (`-c`): `msteams`, `webchat`, `directline`, `emulator`, `agents` + +If the playground connects but messages don't get responses, the agent is running but a message handler may be missing or the route isn't matching. Add a fallback handler to confirm: + +```typescript +this.onActivity('message', async (ctx: TurnContext) => { + await ctx.sendActivity(`Echo: ${ctx.activity.text}`) +}, [], RouteRank.Last) +``` + +--- + +### Validate an OAuth connection name + +OAuth connection names can only be tested end-to-end through a real sign-in flow: + +**Azure Portal → Your Bot Resource → Settings → OAuth Connection Settings → [your connection] → Test Connection** + +This confirms the connection name matches, the OAuth app has the right scopes, and the redirect URI (`https://token.botframework.com/.auth/web/redirect`) is registered on the app registration. + +## Contributing + +If you hit a problem this skill couldn't solve, found a workaround, or noticed something wrong or outdated, that's valuable — please help improve this skill for everyone. + +Draft a suggested issue title and body based on the conversation, then ask the user to open it at: https://github.com/microsoft/agents/issues/new + +A good issue includes: +- What the user was trying to do +- What went wrong (errors, unexpected behavior) +- What worked — including any workaround found during this conversation +- Relevant code or config snippets diff --git a/agent-plugins/agents-for-js/skills/agents-sdk-typescript/SKILL.md b/agent-plugins/agents-for-js/skills/agents-sdk-typescript/SKILL.md new file mode 100644 index 00000000..e01b70be --- /dev/null +++ b/agent-plugins/agents-for-js/skills/agents-sdk-typescript/SKILL.md @@ -0,0 +1,502 @@ +--- +name: agents-sdk-typescript +description: Use when any code imports @microsoft/agents-hosting, @microsoft/agents-hosting-express, or related Agents SDK packages, or when building a new agent with the Microsoft 365 Agents SDK for TypeScript +--- + +## Overview + +The Microsoft 365 Agents SDK builds multichannel agents for Teams, Copilot Studio, and web chat. + +| Package | Purpose | +|---|---| +| `@microsoft/agents-hosting` | Core: AgentApplication, CloudAdapter, TurnContext, TurnState, storage | +| `@microsoft/agents-hosting-express` | `startServer()` convenience wrapper | +| `@microsoft/agents-hosting-storage-blob` | Azure Blob Storage backend | +| `@microsoft/agents-hosting-storage-cosmos` | CosmosDB backend | +| `@microsoft/agents-hosting-dialogs` | Dialog system | + +Requires Node 18+. Use `node --env-file .env` (Node 20+) to load environment variables. + +## Azure Resources Required + +**Microsoft Entra App Registration** +- `clientId` — Application (client) ID +- `clientSecret` — Certificates & secrets +- `tenantId` — Directory (tenant) ID + +**Azure Bot Resource** +- Messaging endpoint: `https:///api/messages` +- Microsoft App ID must match `clientId` + +Local dev: Use Bot Framework Emulator. No Azure Bot needed until deployment. Leave `clientId` blank to skip auth validation. + +## Environment Variables + +### Modern format (recommended) + +Uses `connections__` and `connectionsMap__` prefixes. Double underscores (`__`) separate path segments; `.settings.` is stripped automatically. + +**Single connection (most common):** +``` +connections__serviceConnection__settings__clientId= +connections__serviceConnection__settings__clientSecret= +connections__serviceConnection__settings__tenantId= +connectionsMap__0__connection=serviceConnection +connectionsMap__0__serviceUrl=* +``` + +**How `connectionsMap` works:** +Each entry maps a `serviceUrl` pattern to a named connection. The first matching entry wins. +- `serviceUrl=*` — matches any service URL (use as the default/fallback) +- `serviceUrl` is treated as a regex for all other values + +`connectionsMap` can be omitted when there is only one connection — the SDK defaults it to `serviceUrl=*`. + +**Multiple connections** (e.g. different identities for different channels): +``` +connections__mainConn__settings__clientId= +connections__mainConn__settings__clientSecret= +connections__mainConn__settings__tenantId= + +connections__teamsConn__settings__clientId= +connections__teamsConn__settings__clientSecret= +connections__teamsConn__settings__tenantId= + +connectionsMap__0__connection=teamsConn +connectionsMap__0__serviceUrl=https://smba.trafficmanager.net/.* +connectionsMap__1__connection=mainConn +connectionsMap__1__serviceUrl=* +``` + +Optional `audience` field on a map entry restricts matching to activities whose JWT `aud` claim equals that value: +``` +connectionsMap__0__connection=teamsConn +connectionsMap__0__serviceUrl=* +connectionsMap__0__audience= +``` + +**Available connection settings fields:** +`clientId`, `clientSecret`, `tenantId`, `authority`, `certPemFile`, `certKeyFile`, `sendX5C`, `connectionName`, `scope` + +### Legacy format — backwards compatibility only + +> **Never use the legacy format for new agents.** It exists solely for backwards compatibility with older BotFramework-based bots. Always use the modern `connections__` format above. + +``` +clientId= +clientSecret= +tenantId= +``` + +Both `startServer()` and `loadAuthConfigFromEnv()` auto-detect the format. Leave `clientId` blank locally to skip auth. + +### OAuth authorization handler variables + +These control the user sign-in flow configured via `authorization: { [id]: { ... } }`. The `id` is the key used in the authorization options (e.g. `graph`). + +**AzureBot handler** (default — user OAuth flow via Azure Bot Service): +``` +graph_connectionName=GraphOAuthConnection # required — OAuth connection name in Azure Bot resource +graph_connectionTitle=Sign in with Microsoft +graph_connectionText=Please sign in to continue +graph_maxAttempts=3 # max magic code attempts (default: 2) +graph_enableSso=false # disable SSO (default: true) + +# OBO (on-behalf-of) — auto-exchange on routes using exchangeToken() +graph_obo_connection=OBOConnection +graph_obo_scopes=https://graph.microsoft.com/.default,Mail.Read + +# Custom error messages +graph_messages_invalidCode=That code was invalid, please try again. +graph_messages_invalidCodeFormat=Please enter the 6-digit code from the sign-in card. +graph_messages_maxAttemptsExceeded=Too many failed attempts. Please try again later. +``` + +**Agentic handler** (agent-to-agent, no user prompt): +``` +myHandler_type=agentic +myHandler_scopes=https://graph.microsoft.com/.default # comma-separated, required +myHandler_altBlueprintConnectionName=altConn # optional +``` + +## Quick Start + +```typescript +import { startServer } from '@microsoft/agents-hosting-express' +import { AgentApplication, MemoryStorage, TurnContext, TurnState } from '@microsoft/agents-hosting' + +class MyAgent extends AgentApplication { + constructor() { + super({ storage: new MemoryStorage() }) + + this.onConversationUpdate('membersAdded', async (ctx: TurnContext) => { + await ctx.sendActivity('Hello! Send me a message.') + }) + + this.onActivity('message', async (ctx: TurnContext, state: TurnState) => { + let counter: number = state.getValue('conversation.counter') || 0 + await ctx.sendActivity(`[${counter++}] You said: ${ctx.activity.text}`) + state.setValue('conversation.counter', counter) + }) + } +} + +startServer(new MyAgent()) +``` + +Run: `node --env-file .env dist/index.js` + +## Server Setup + +### Option A: startServer() (preferred) + +`startServer(agent)` creates an Express app with: +- `express.json()` + `authorizeJWT()` middleware +- `POST /api/messages` route +- Listens on `PORT` env var (default 3978) +- **Returns the Express instance** — add extra routes to the return value + +```typescript +import { startServer } from '@microsoft/agents-hosting-express' + +const server = startServer(agent) +server.get('/health', (_req, res) => res.json({ ok: true })) +``` + +### Option B: Manual Express + +```typescript +import express, { Response } from 'express' +import { Request, CloudAdapter, authorizeJWT, loadAuthConfigFromEnv } from '@microsoft/agents-hosting' + +const authConfig = loadAuthConfigFromEnv() +const adapter = new CloudAdapter(authConfig) +const app = express() + +app.use(express.json()) +app.use(authorizeJWT(authConfig)) + +app.post('/api/messages', async (req: Request, res: Response) => { + await adapter.process(req, res, async (context) => await agent.run(context)) +}) + +app.listen(process.env.PORT || 3978) +``` + +### Proactive Messaging + +Save a reference during a turn, then use `adapter.continueConversation` from any route. `req.user` comes from `authorizeJWT` middleware. + +```typescript +import { ConversationReference } from '@microsoft/agents-activity' + +// During a turn — save the reference +const ref = ctx.activity.getConversationReference() +conversationReferences[ref.conversation.id] = ref + +// In a proactive route +app.get('/api/notify', async (req, res) => { + for (const ref of Object.values(conversationReferences)) { + await adapter.continueConversation(req.user!, ref, async (ctx) => { + await ctx.sendActivity('Proactive message') + }) + } + res.json({ ok: true }) +}) +``` + +## Validating Your Configuration + +### 1. Validate bot credentials (clientId / clientSecret / tenantId) + +This tests that your Entra app registration credentials are correct and can authenticate with the Bot Framework. A successful response includes `access_token`; an error response includes `error` and `error_description`. + +```bash +curl -s -X POST \ + "https://login.microsoftonline.com/$tenantId/oauth2/v2.0/token" \ + -d "grant_type=client_credentials\ +&client_id=$clientId\ +&client_secret=$clientSecret\ +&scope=https://api.botframework.com/.default" \ + | jq '{token_type, expires_in, error, error_description}' +``` + +Common errors: +- `AADSTS700016` — `clientId` not found in tenant (wrong ID or wrong tenant) +- `AADSTS7000215` — invalid `clientSecret` (expired or incorrect) +- `AADSTS90002` — `tenantId` not found + +### 2. Validate the agent is running and reachable + +```bash +curl -s -o /dev/null -w "%{http_code}" \ + -X POST http://localhost:3978/api/messages \ + -H "Content-Type: application/json" \ + -d '{}' +``` + +- `401` — agent is running; JWT auth rejected the empty request (expected — means auth is working) +- `000` or connection refused — agent is not running or wrong port +- `200` — agent is running with auth disabled (local dev with blank `clientId`) + +### 3. Validate an OAuth connection name + +OAuth connection names (used by `graph_connectionName`) can only be tested end-to-end through a real sign-in flow. Use the Azure portal: + +**Azure Portal → Your Bot Resource → Settings → OAuth Connection Settings → [your connection] → Test Connection** + +This confirms the connection name matches, the OAuth app has the right scopes, and the redirect URI is configured correctly. + +## Local Testing with Agents Playground + +The Agents Playground lets you test your agent locally without deploying to Azure or configuring a Bot resource. It acts as a mock connector service and channel client. + +**Install:** +```bash +npm install -g agentsplayground +``` + +### Recommended package.json scripts + +Include these scripts when creating a new agent's `package.json`. The `test` script starts the agent and playground together in parallel — no separate terminals needed: + +```json +"scripts": { + "prebuild": "npm ci", + "build": "tsc --build", + "prestart": "npm run build", + "prestart:anon": "npm run build", + "start:anon": "node ./dist/index.js", + "start": "node --env-file .env ./dist/index.js", + "test-tool": "agentsplayground -c emulator", + "test": "npm-run-all -p -r start:anon test-tool" +} +``` + +- `npm start` — builds and runs with `.env` credentials +- `npm test` — builds, starts the agent without auth (`start:anon`), and launches the playground in parallel. Use this for quick local dev without needing an `.env` file. +- `-c emulator` — uses the emulator channel (no auth required). Change to `msteams`, `webchat`, etc. as needed. +- Requires `npm-run-all` as a dev dependency: `npm install -D npm-run-all` + +**With authentication** (for testing OAuth/sign-in flows): +```bash +agentsplayground -c msteams \ + --client-id \ + --client-secret \ + --tenant-id +``` + +**Channel options** (`-c`): `msteams`, `webchat`, `directline`, `emulator`, `agents` + +## AgentApplication Patterns + +**Routing** +```typescript +this.onMessage('/cmd', handler) // exact command +this.onActivity('message', handler) // all messages +this.onConversationUpdate('membersAdded', handler) +this.onActivity('invoke', handler) +this.onActivity('message', fallback, [], RouteRank.Last) // fallback +``` + +**TurnState** — dot-notation keys auto-scoped to conversation/user/temp: +```typescript +const count = state.getValue('conversation.counter') || 0 +state.setValue('conversation.counter', count + 1) +state.deleteValue('conversation.counter') +``` + +**Storage backends** + +| Backend | Use case | +|---|---| +| `MemoryStorage` | Local dev only — not persistent | +| `BlobsStorage` | Azure Blob — production | +| `CosmosDbPartitionedStorage` | CosmosDB — production | + +## Authorization (User Token Flow) + +```typescript +class MyAgent extends AgentApplication { + constructor() { + super({ + storage: new MemoryStorage(), + authorization: { + graph: { + name: 'GraphOAuthConnection', // OAuth connection name in Azure Bot resource + title: 'Sign in with Microsoft', + text: 'Please sign in to continue', + } + } + }) + + this.onSignInSuccess(async (ctx, state) => { + const { token } = await this.authorization.getToken(ctx, 'graph') + // use token to call external APIs + await ctx.sendActivity('Signed in!') + }) + + this.onSignInFailure(async (ctx, state, authId, err) => { + await ctx.sendActivity(`Sign-in failed: ${err}`) + }) + + // Protect a route — SDK sends the OAuth card automatically if not signed in + this.onActivity('message', async (ctx, state) => { + const { token } = await this.authorization.getToken(ctx, 'graph') + // token is guaranteed here — route won't run until user is signed in + }, ['graph']) + + // Sign out from all providers + this.onMessage('/logout', async (ctx, state) => { + await this.authorization.signOut(ctx, state) + await ctx.sendActivity('Signed out.') + }) + } +} +``` + +`name` can also be provided via environment variable `graph_connectionName` (where `graph` is the handler key) and omitted from code. + +**OBO (on-behalf-of) — exchange user token for a downstream service token:** +```typescript +const { token } = await this.authorization.exchangeToken(ctx, 'graph', { + scopes: ['https://graph.microsoft.com/.default'] +}) +// use token to call Graph or other downstream APIs +``` + +**Agentic auth** (agent-to-agent, no user prompt): +```typescript +authorization: { agentic: { type: 'agentic' } } +// env: agentic_type=agentic, agentic_scopes=https://graph.microsoft.com/.default +``` + +## Cards + +Import `CardFactory` and `MessageFactory` from `@microsoft/agents-hosting`. Import `ActionTypes` from `@microsoft/agents-activity`. + +**Adaptive Card** (from a JSON template): +```typescript +import AdaptiveCard from './resources/myCard.json' + +const card = CardFactory.adaptiveCard(AdaptiveCard) +await ctx.sendActivity(MessageFactory.attachment(card)) +``` + +**Hero Card:** +```typescript +const card = CardFactory.heroCard( + 'Card Title', + CardFactory.images(['https://example.com/image.jpg']), + CardFactory.actions([ + { type: ActionTypes.OpenUrl, title: 'Learn more', value: 'https://example.com' } + ]) +) +await ctx.sendActivity(MessageFactory.attachment(card)) +``` + +**Thumbnail Card:** +```typescript +const card = CardFactory.thumbnailCard('Title', images, actions, { + subtitle: 'Subtitle', + text: 'Body text' +}) +await ctx.sendActivity(MessageFactory.attachment(card)) +``` + +Other factories: `CardFactory.animationCard`, `CardFactory.audioCard`, `CardFactory.videoCard`, `CardFactory.receiptCard`. + +## Streaming + +```typescript +this.onActivity('message', async (ctx: TurnContext) => { + ctx.streamingResponse.setFeedbackLoop(true) + ctx.streamingResponse.setGeneratedByAILabel(true) + ctx.streamingResponse.queueInformativeUpdate('Working on it...') + ctx.streamingResponse.queueTextChunk('Part 1 ') + ctx.streamingResponse.queueTextChunk('Part 2') + await ctx.streamingResponse.endStream() // required +}) +``` + +## Common Mistakes + +**1. Wrong field name on ConversationReference** + +```typescript +// WRONG +const ref: ConversationReference = { bot: { id: appId } } + +// CORRECT +const ref: ConversationReference = { agent: { id: appId } } +``` + +**2. JwtPayload.aud is string | string[]** + +```typescript +// WRONG +const appId = payload.aud + +// CORRECT +const appId = Array.isArray(payload.aud) ? payload.aud[0] : payload.aud +``` + +**3. Adapter callbacks swallow exceptions** + +```typescript +// WRONG — error is lost +await adapter.continueConversation(identity, ref, async (ctx) => { + throw new Error('something failed') // swallowed +}) + +// CORRECT — capture and rethrow +let capturedError: Error | undefined +await adapter.continueConversation(identity, ref, async (ctx) => { + try { + await doWork(ctx) + } catch (err) { + capturedError = err as Error + } +}) +if (capturedError) throw capturedError +``` + +**4. startServer() returns Express — add routes to the return value** + +```typescript +// WRONG — routes never registered +const app = express() +startServer(agent) +app.get('/health', handler) + +// CORRECT +const server = startServer(agent) +server.get('/health', handler) +``` + +### Wrong method names — `activity()` and `message()` don't exist + +`AgentApplication` uses `on`-prefixed method names. Common wrong guesses: + +```typescript +// ❌ wrong — these methods don't exist +app.activity('message', handler) +app.message(handler) + +// ✅ correct +app.onActivity('message', handler) +app.onMessage('/help', handler) +app.onConversationUpdate('membersAdded', handler) +``` + +## Contributing + +If you hit a problem this skill couldn't solve, found a workaround, or noticed something wrong or outdated, that's valuable — please help improve this skill for everyone. + +Draft a suggested issue title and body based on the conversation, then ask the user to open it at: https://github.com/microsoft/agents/issues/new + +A good issue includes: +- What the user was trying to do +- What went wrong (errors, unexpected behavior) +- What worked — including any workaround found during this conversation +- Relevant code or config snippets diff --git a/agent-plugins/agents-for-js/skills/azure-agents-sdk-provision/Create_SSO_AppRegistration.bicep b/agent-plugins/agents-for-js/skills/azure-agents-sdk-provision/Create_SSO_AppRegistration.bicep new file mode 100644 index 00000000..dd0461f9 --- /dev/null +++ b/agent-plugins/agents-for-js/skills/azure-agents-sdk-provision/Create_SSO_AppRegistration.bicep @@ -0,0 +1,110 @@ +// ============================================================================= +// Create_SSO_AppRegistration.bicep +// Phase 1 — Creates the base Entra ID App Registration for an Azure Bot Agent with SSO configuration for basic graph authentication +// with Teams SSO support. +// +// +// Requirements: +// - Bicep CLI 0.26.0+ +// - Microsoft Graph Bicep extension enabled +// +// Output example: +// newAppId: The appId of the newly created App Registration +// newAppObjectId: The objectId of the newly created App Registration +// ============================================================================= + +extension microsoftGraph + +// ------------------------------------------------------- +// Parameters +// ------------------------------------------------------- +// NAME OF THE APP REGISTRATION +param APP_NAME string +// OBJECT ID OF THE OWNER OF THE APP REGISTRATION, THIS SHOULD BE THE CREATING USER'S OBJECT ID +param OWNER_OBJECT_ID string +// GENERATED OAUTH SCOPE ID FOR PRE-AUTHORIZATION (GUID FORMAT) +param OAUTH_SCOPE_ID string +// REDIRECT URI FOR THE AZURE BOT SERVICE (defaulted for all commerical regions) +param ABS_REDIRECTURI string = 'https://token.botframework.com/.auth/web/redirect' + +// ------------------------------------------------------- +// App Registration +// ------------------------------------------------------- +resource botApp 'Microsoft.Graph/applications@v1.0' = { + // uniqueName makes this resource idempotent and referenceable in Phase 2 + uniqueName: APP_NAME + displayName: APP_NAME + signInAudience: 'AzureADMyOrg' + + // identifierUris intentionally empty at creation — set in Phase 2 + identifierUris: [] + + web: { + redirectUris: [ + ABS_REDIRECTURI + ] + implicitGrantSettings: { + enableAccessTokenIssuance: false + enableIdTokenIssuance: false + } + } + + owners: { + relationships: [ + OWNER_OBJECT_ID + ] + } + + // Microsoft Graph delegated permissions to support user login, Additional permissions can be added as needed to support other Graph API calls + requiredResourceAccess: [ + { + resourceAppId: '00000003-0000-0000-c000-000000000000' // Microsoft Graph + resourceAccess: [ + { id: 'e1fe6dd8-ba31-4d61-89e7-88639da4683d', type: 'Scope' } // User.Read + { id: '37f7f235-527c-4136-accd-4a02d197296e', type: 'Scope' } // openid + { id: '14dad69e-099b-42c9-810b-d002981feec1', type: 'Scope' } // profile + ] + } + ] +} + +// ------------------------------------------------------- +// Update App Registration with API configuration for Bot Framework / Teams SSO +// This will update the App Registration with the required API configuration for Bot Framework / Teams SSO +// ------------------------------------------------------- + +resource botAppUp1 'Microsoft.Graph/applications@v1.0' = { + uniqueName: APP_NAME + displayName: APP_NAME + + // api://botid-{appId} is required for Bot Framework / Teams SSO + identifierUris: [ + 'api://botid-${botApp.appId}' + ] + + api: { + // Re-specify scope to preserve it during the in-place update + oauth2PermissionScopes: [ + { + id: OAUTH_SCOPE_ID + adminConsentDescription: 'Allow the app to access the agent on behalf of the signed-in user.' + adminConsentDisplayName: 'Access the agent as a user' + isEnabled: true + type: 'User' + userConsentDescription: 'Allow this app to access the agent on your behalf.' + userConsentDisplayName: 'Access the agent as a user' + value: 'access_as_user' + } + ] + } +} + +// ------------------------------------------------------- +// Outputs +// ------------------------------------------------------- +output newAppId string = botApp.appId +output newAppObjectId string = botApp.id + + + + diff --git a/agent-plugins/agents-for-js/skills/azure-agents-sdk-provision/Create_SSO_PreAuthorize.bicep b/agent-plugins/agents-for-js/skills/azure-agents-sdk-provision/Create_SSO_PreAuthorize.bicep new file mode 100644 index 00000000..0595151d --- /dev/null +++ b/agent-plugins/agents-for-js/skills/azure-agents-sdk-provision/Create_SSO_PreAuthorize.bicep @@ -0,0 +1,52 @@ +// ============================================================================= +// Create_SSO_PreAuthorize.bicep +// Phase 2 — Updates the base Entra ID App Registration to add Preauthorized Applications to the API configuration to support Teams SSO +// +// Requirements: +// - Bicep CLI 0.26.0+ +// - Microsoft Graph Bicep extension enabled +// +// ============================================================================= + +extension microsoftGraph + +// ------------------------------------------------------- +// Parameters +// ------------------------------------------------------- +// NAME OF THE APP REGISTRATION +param APP_NAME string +// GENERATED OAUTH SCOPE ID FOR PRE-AUTHORIZATION (GUID FORMAT) +param OAUTH_SCOPE_ID string + +// ------------------------------------------------------- +// Update App Registration with Preauthorized Applications for Teams SSO +// ------------------------------------------------------- +resource botAppUp3 'Microsoft.Graph/applications@v1.0' = { + uniqueName: APP_NAME + displayName: APP_NAME + + api: { + // Pre-authorized Microsoft first-party Teams/Office clients + preAuthorizedApplications: [ + // Microsoft Teams (web) + { appId: '4345a7b9-9a63-4910-a426-35363201d503', delegatedPermissionIds: [OAUTH_SCOPE_ID] } + // Office 365 / Outlook Online + { appId: '00000002-0000-0ff1-ce00-000000000000', delegatedPermissionIds: [OAUTH_SCOPE_ID] } + // Microsoft Teams (mobile/desktop) + { appId: '27922004-5251-4030-b22d-91ecd9a37ea4', delegatedPermissionIds: [OAUTH_SCOPE_ID] } + // Microsoft 365 web + { appId: '4765445b-32c6-49b0-83e6-1d93765276ca', delegatedPermissionIds: [OAUTH_SCOPE_ID] } + // Office UWP / PWA + { appId: '0ec893e0-5785-4de6-99da-4ed124e5296c', delegatedPermissionIds: [OAUTH_SCOPE_ID] } + // Teams web app + { appId: 'bc59ab01-8403-45c6-8796-ac3ef710b3e3', delegatedPermissionIds: [OAUTH_SCOPE_ID] } + // Microsoft Office (desktop) + { appId: 'd3590ed6-52b3-4102-aeff-aad2292ab01c', delegatedPermissionIds: [OAUTH_SCOPE_ID] } + // Teams mobile client + { appId: '5e3ce6c0-2b1f-4285-8d4b-75ee78787346', delegatedPermissionIds: [OAUTH_SCOPE_ID] } + // Teams mobile app + { appId: '1fec8e78-bce4-4aaf-ab1b-5451cc387264', delegatedPermissionIds: [OAUTH_SCOPE_ID] } + ] + } +} + diff --git a/agent-plugins/agents-for-js/skills/azure-agents-sdk-provision/ProvisionABS.bicep b/agent-plugins/agents-for-js/skills/azure-agents-sdk-provision/ProvisionABS.bicep new file mode 100644 index 00000000..ba9da82e --- /dev/null +++ b/agent-plugins/agents-for-js/skills/azure-agents-sdk-provision/ProvisionABS.bicep @@ -0,0 +1,37 @@ +param BOT_NAME string = 'MattBBot01' +param BOT_DEPLOYMENT_TYPE string = 'SingleTenant' +param APP_ID string = '982f1994-71b1-4cea-be76-77e0649f7b16' +param TENANT_ID string = '367c5af9-6300-4248-99bc-72288021c775' +param DEPLOYMENT_AZURE_REGION string = 'global' + +resource botService 'Microsoft.BotService/botServices@2023-09-15-preview' = { + name: BOT_NAME + location: DEPLOYMENT_AZURE_REGION + sku: { + name: 'S1' + } + kind: 'azurebot' + properties: { + endpoint: '' + displayName: BOT_NAME + msaAppType: BOT_DEPLOYMENT_TYPE + msaAppId: APP_ID + msaAppTenantId: TENANT_ID + isStreamingSupported: false + schemaTransformationVersion: '1.3' + tenantId: TENANT_ID + } +} + +resource symbolicname 'Microsoft.BotService/botServices/channels@2023-09-15-preview' = { + parent: botService + name: 'MsTeamsChannel' + location: DEPLOYMENT_AZURE_REGION + properties: { + channelName: 'MsTeamsChannel' + properties: { + acceptedTerms: true + isEnabled: true + } + } +} diff --git a/agent-plugins/agents-for-js/skills/azure-agents-sdk-provision/SKILL.md b/agent-plugins/agents-for-js/skills/azure-agents-sdk-provision/SKILL.md new file mode 100644 index 00000000..fccd7eb4 --- /dev/null +++ b/agent-plugins/agents-for-js/skills/azure-agents-sdk-provision/SKILL.md @@ -0,0 +1,660 @@ +--- +name: azure-agents-sdk-provision +description: Use when provisioning Azure resources for a Microsoft Agents SDK application - creating an Azure Bot resource, configuring identity credentials, adding Teams channel, or setting up OAuth connections via az CLI +--- + +# Azure Agents SDK Provisioning + +## Overview + +Provisions Azure Bot resources for M365 Agents SDK apps using `az` CLI commands. Three auth types available; each produces a config block for `appsettings.json` (dotnet) or env vars (Node.js). + +**See `agents-sdk-typescript` skill for env var format (Node.js).** + +## Prerequisites + +```bash +az login +az account set --subscription "" +# Create resource group if needed +az group create --name "" --location eastus +``` + +## Auth Type Selection + +| Auth Type | No Secret | Works Off-Azure | App Registration | JS SDK | +|-----------|:---------:|:---------------:|:----------------:|:------:| +| `UserManagedIdentity` | ✅ | ❌ | ❌ | ✅ | +| `FederatedCredentials` | ✅ | ✅ | ✅ | ✅ | +| `ClientSecret` | ❌ | ✅ | ✅ | ✅ | + +### UserManagedIdentity + +The bot authenticates as an Azure Managed Identity — a system-managed credential that Azure rotates automatically. No app registration, no secrets, no expiry management. + +**Use when:** The bot is hosted on Azure (App Service, Container Apps, AKS, Azure Functions). The hosting platform injects the identity token; it cannot work outside Azure. + +**Implies:** +- Azure assigns a `clientId` for the identity; no secret is ever stored or transmitted +- The identity is scoped to the resource group where it's created +- Simplest operational model — nothing to rotate, nothing to leak +- If the host is compromised, the attacker can only act as that identity (blast radius limited to assigned roles) + +**Not suitable for:** Local dev (no Azure runtime to inject tokens), cross-tenant scenarios, off-Azure CI/CD pipelines. + +--- + +### FederatedCredentials + +The bot has an App Registration (for a stable `clientId` and tenant-scoped identity), but instead of a secret, it uses a Managed Identity to prove ownership via a federated credential. The MSI's `principalId` is registered as a trusted subject on the app — Azure AD accepts the MSI's token as proof that the app is authorized. + +**Use when:** You need an App Registration (e.g. for OAuth scopes, Graph API access, cross-tenant identity) but don't want to manage a client secret. Common for production workloads still hosted on Azure. + +**Implies:** +- Still requires Azure hosting (MSI token still injected by the platform) +- Two Azure resources: a Managed Identity + an App Registration linked by the federated credential +- No secret ever exists — the FIC relationship is the credential +- Slightly more setup complexity than pure MSI, but unlocks app-registration capabilities (API permissions, OAuth connections, service principal) +- If the MSI or app registration is deleted, the trust breaks — both must be managed together + +**Not suitable for:** Local dev, off-Azure deployments. + +--- + +### ClientSecret + +The bot has an App Registration with a generated client secret. The secret is stored in config and sent to Azure AD to obtain tokens. Classic service principal authentication. + +**Use when:** The bot runs outside Azure (local dev, on-prem, other cloud), the JS SDK is in use, or you need the quickest path to a working bot without MSI infrastructure. +**How to execute:** Use Option C in Step 1 below, which runs two Bicep deployments to create the app registration with Teams SSO support, then generates a client secret. + +**Implies:** +- A secret exists and must be protected — store in Key Vault, GitHub Secrets, or environment secret manager; **never in source control** +- Secrets expire (default 1–2 years) and must be rotated before expiry, or the bot stops authenticating +- Widest attack surface: a leaked secret allows anyone to authenticate as the bot from anywhere +- Easiest to use in CI/CD and local dev (just set env vars) +- `az ad app credential reset --append` adds a new secret without invalidating existing ones — always use `--append`, then remove the old secret key ID after deploying + +**Not suitable for:** High-security production environments where secret management overhead is unacceptable. + +--- + +## Step 1: Create Identity & Credentials + +### Option A: UserManagedIdentity + +```bash +RESULT=$(az identity create \ + --resource-group "$RESOURCE_GROUP" \ + --name "$BOT_NAME" \ + --output json) + +CLIENT_ID=$(echo $RESULT | jq -r '.clientId') +TENANT_ID=$(echo $RESULT | jq -r '.tenantId') +RESOURCE_ID=$(echo $RESULT | jq -r '.id') +``` + +Config output: +```json +{ + "ClientId": "", + "TenantId": "", + "ResourceId": "", + "AzureBotAppType": "UserAssignedMSI", + "ServiceConnection.Settings": { + "AuthType": "UserManagedIdentity", + "ClientId": "", + "Scopes": ["https://api.botframework.com/.default"] + } +} +``` + +### Option B: FederatedCredentials + +```bash +# 1. Create managed identity +MSI=$(az identity create \ + --resource-group "$RESOURCE_GROUP" \ + --name "$BOT_NAME" --output json) +MSI_CLIENT_ID=$(echo $MSI | jq -r '.clientId') +MSI_PRINCIPAL_ID=$(echo $MSI | jq -r '.principalId') +TENANT_ID=$(echo $MSI | jq -r '.tenantId') + +# 2. Create app registration +APP=$(az ad app create \ + --display-name "$BOT_NAME" \ + --sign-in-audience "AzureADMyOrg" --output json) +APP_ID=$(echo $APP | jq -r '.appId') + +# 3. Create federated credential (subject = MSI principalId, NOT clientId) +az ad app federated-credential create \ + --id "$APP_ID" \ + --parameters "{ + \"name\": \"agent\", + \"description\": \"Agent-to-Channel\", + \"issuer\": \"https://login.microsoftonline.com/${TENANT_ID}/v2.0\", + \"subject\": \"${MSI_PRINCIPAL_ID}\", + \"audiences\": [\"api://AzureADTokenExchange\"] + }" + +# 4. Create service principal +az ad sp create --id "$APP_ID" --output none +``` + +Config output: +```json +{ + "ClientId": "", + "TenantId": "", + "AzureBotAppType": "SingleTenant", + "ServiceConnection.Settings": { + "AuthType": "FederatedCredentials", + "AuthorityEndpoint": "https://login.microsoftonline.com/", + "ClientId": "", + "FederatedClientId": "", + "Scopes": ["https://api.botframework.com/.default"] + } +} +``` + +### Option C: ClientSecret + +Uses two Bicep deployments to create a Teams SSO-capable app registration, then generates a client secret. + +**Prerequisites:** Bicep CLI 0.26.0+ (`az bicep install`) with the Microsoft Graph Bicep extension. Account requires Application Administrator or Global Administrator role. + +**Step 0 — Verify active tenant matches the intended tenant:** + +Run this before collecting any other inputs. If the user specified a tenant domain (e.g. `asdkt3.onmicrosoft.com`), confirm it matches before proceeding. + +```bash +az account show --query "{tenantId:tenantId, tenantDomain:tenantDefaultDomain, subscription:name}" --output table +``` + +If the active tenant does not match the intended tenant, switch first: + +```bash +az login --tenant +az account set --subscription "" +``` + +**Ask the user for:** +- `APP_NAME` — display name for the Entra app registration (must be unique in the tenant) +- `RESOURCE_GROUP` — run the command below to show available resource groups, then ask the user to pick one: + +```bash +az group list --query "[].{Name:name, Location:location}" --output table +``` + +- Config format — ask: **"Which config format do you need — dotnet (`appsettings.json`) or Node.js (`.env`)?"** + +**Step 1 — Check if app already exists (handle re-run after partial failure):** + +```bash +EXISTING_APP_ID=$(az ad app list --display-name "$APP_NAME" --query "[0].appId" -o tsv) +if [ -n "$EXISTING_APP_ID" ]; then + echo "App '$APP_NAME' already exists (appId: $EXISTING_APP_ID) — resuming." + APP_ID="$EXISTING_APP_ID" + # Re-derive scope ID from the existing app rather than generating a new GUID + OAUTH_SCOPE_ID=$(az ad app show --id "$APP_ID" \ + --query "api.oauth2PermissionScopes[?value=='access_as_user'].id | [0]" -o tsv) + if [ -z "$OAUTH_SCOPE_ID" ]; then + echo "ERROR: Existing app has no 'access_as_user' scope. Delete the app and re-run." + exit 1 + fi + echo "Re-using OAUTH_SCOPE_ID: $OAUTH_SCOPE_ID — skipping Phase 1, proceeding to Phase 2." +fi +``` + +**Phase 1 — Create app registration with `access_as_user` scope and identifier URI:** + +> **Skip this phase if APP_ID and OAUTH_SCOPE_ID are already set** from the resume block above — the app registration already exists. + +```bash +# Acquire owner object ID from the signed-in user +OWNER_OBJECT_ID=$(az ad signed-in-user show --query id -o tsv) + +# Generate a new GUID for the OAuth scope (use PowerShell on Windows) +OAUTH_SCOPE_ID=$(powershell -NoProfile -Command "[guid]::NewGuid().ToString()") + +RESULT=$(az deployment group create \ + --resource-group "$RESOURCE_GROUP" \ + --template-file ".claude\skills\azure-agents-sdk-provision\Create_SSO_AppRegistration.bicep" \ + --parameters "APP_NAME=$APP_NAME" "OWNER_OBJECT_ID=$OWNER_OBJECT_ID" "OAUTH_SCOPE_ID=$OAUTH_SCOPE_ID" \ + --output json) + +APP_ID=$(echo $RESULT | jq -r '.properties.outputs.newAppId.value') +``` + +**Phase 2 — Pre-authorize Teams/Office host clients for SSO:** + +```bash +az deployment group create \ + --resource-group "$RESOURCE_GROUP" \ + --template-file ".claude\skills\azure-agents-sdk-provision\Create_SSO_PreAuthorize.bicep" \ + --parameters "APP_NAME=$APP_NAME" "OAUTH_SCOPE_ID=$OAUTH_SCOPE_ID" +``` + +**Phase 2b — Verify deployment:** + +Run after Phase 2 completes. All three checks must pass before generating a secret. + +```bash +echo "=== Verifying app registration ===" + +IDENTIFIER_URI=$(az ad app show --id "$APP_ID" --query "identifierUris[0]" -o tsv) +[ "$IDENTIFIER_URI" = "api://botid-$APP_ID" ] \ + && echo "PASS Identifier URI: $IDENTIFIER_URI" \ + || echo "FAIL Identifier URI — expected 'api://botid-$APP_ID', got '$IDENTIFIER_URI'" + +SCOPE_CHECK=$(az ad app show --id "$APP_ID" \ + --query "api.oauth2PermissionScopes[?value=='access_as_user'].id | [0]" -o tsv) +[ -n "$SCOPE_CHECK" ] \ + && echo "PASS access_as_user scope present (id: $SCOPE_CHECK)" \ + || echo "FAIL access_as_user scope missing" + +PRE_AUTH_COUNT=$(az ad app show --id "$APP_ID" \ + --query "length(api.preAuthorizedApplications)" -o tsv 2>/dev/null || echo 0) +[ "${PRE_AUTH_COUNT:-0}" -ge 9 ] \ + && echo "PASS Pre-authorized clients: $PRE_AUTH_COUNT" \ + || echo "FAIL Pre-authorized clients: ${PRE_AUTH_COUNT:-0} (expected 9 — re-run Phase 2)" +``` + +If any check fails, do not proceed. Re-run the failed phase before continuing. + +**Phase 3 — Register service principal and create client secret:** + +```bash +# Ignore "already in use" error if the service principal already exists +az ad sp create --id "$APP_ID" --output none + +# --append adds the new secret alongside any existing ones (safe for running bots) +SECRET_RESULT=$(az ad app credential reset \ + --id "$APP_ID" \ + --append \ + --output json) + +CLIENT_SECRET=$(echo $SECRET_RESULT | jq -r '.password') +TENANT_ID=$(echo $SECRET_RESULT | jq -r '.tenant') + +# Retrieve the expiry date of the generated secret +SECRET_EXPIRY=$(az ad app credential list --id "$APP_ID" --query "[0].endDateTime" -o tsv) +``` + +Record these values — `CLIENT_SECRET` is **not retrievable again**: +- `APP_ID` — App ID (Client ID) +- `CLIENT_SECRET` — client secret +- `TENANT_ID` — tenant ID +- `SECRET_EXPIRY` — secret expiry date (rotate before this date or the bot stops authenticating) + +Always surface the expiry date prominently in the output to the user. + +**Config output — dotnet (`appsettings.json`):** + +Store secret in Key Vault or environment secret store — never in source. + +```json +{ + "Connections": { + "ServiceConnection": { + "Settings": { + "AuthType": "ClientSecret", + "AuthorityEndpoint": "https://login.microsoftonline.com/", + "ClientId": "", + "ClientSecret": "", + "Scopes": ["https://api.botframework.com/.default"] + } + } + } +} +``` + +**Config output — Node.js (`.env`):** + +``` +connections__serviceConnection__settings__clientId= +connections__serviceConnection__settings__clientSecret= +connections__serviceConnection__settings__tenantId= +connectionsMap__0__connection=serviceConnection +connectionsMap__0__serviceUrl=* +``` + +> Run with: `node --env-file .env dist/index.js` (Node 20+) + +**Teams app manifest snippet (`manifest.json`):** + +Add this block to enable SSO in Teams. The `resource` value must match the identifier URI set on the app registration. + +```json +"webApplicationInfo": { + "id": "", + "resource": "api://botid-" +} +``` + +**Secret rotation (for existing bots):** + +Do **not** use `az ad app credential reset` without `--append` on a running bot — it immediately invalidates all existing secrets and causes downtime. The safe rotation pattern is: + +```bash +# Step 1 — Add a NEW secret alongside the existing one (--append keeps old secret live) +NEW_SECRET_RESULT=$(az ad app credential reset \ + --id "$APP_ID" \ + --append \ + --output json) +# Record the new secret and its key ID +NEW_SECRET=$(echo $NEW_SECRET_RESULT | jq -r '.password') +NEW_KEY_ID=$(az ad app credential list --id "$APP_ID" \ + --query "sort_by(@, &endDateTime)[-1].keyId" -o tsv) + +# Step 2 — Deploy the new secret to config/Key Vault, verify the bot is healthy + +# Step 3 — Remove the OLD secret by its key ID (list first to find it) +az ad app credential list --id "$APP_ID" --query "[].{keyId:keyId, expiry:endDateTime}" -o table +OLD_KEY_ID= +az ad app credential delete --id "$APP_ID" --key-id "$OLD_KEY_ID" +``` + +--- + +## Step 2: Create Azure Bot Resource + +> **Do NOT use `az bot create`** — the `az bot` command group hardcodes API version `2021-05-01-preview`, which Azure has retired. This fails even on the latest Azure CLI. Use `az rest` directly instead. + +```bash +SUBSCRIPTION=$(az account show --query id --output tsv) + +# UserAssignedMSI +az rest --method PUT \ + --uri "https://management.azure.com/subscriptions/${SUBSCRIPTION}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.BotService/botServices/${BOT_NAME}?api-version=2022-09-15" \ + --body "{\"location\":\"global\",\"sku\":{\"name\":\"F0\"},\"kind\":\"azurebot\",\"properties\":{\"displayName\":\"${BOT_NAME}\",\"msaAppId\":\"${CLIENT_ID}\",\"msaAppType\":\"UserAssignedMSI\",\"msaAppMSIResourceId\":\"${RESOURCE_ID}\",\"msaAppTenantId\":\"${TENANT_ID}\",\"endpoint\":\"\"}}" + +# SingleTenant (FederatedCredentials or ClientSecret) +az rest --method PUT \ + --uri "https://management.azure.com/subscriptions/${SUBSCRIPTION}/resourceGroups/${RESOURCE_GROUP}/providers/Microsoft.BotService/botServices/${BOT_NAME}?api-version=2022-09-15" \ + --body "{\"location\":\"global\",\"sku\":{\"name\":\"F0\"},\"kind\":\"azurebot\",\"properties\":{\"displayName\":\"${BOT_NAME}\",\"msaAppId\":\"${APP_ID}\",\"msaAppType\":\"SingleTenant\",\"msaAppTenantId\":\"${TENANT_ID}\",\"endpoint\":\"\"}}" +``` + +### Add Teams Channel (optional) + +```bash +az bot teams create \ + --resource-group "$RESOURCE_GROUP" \ + --name "$BOT_NAME" +``` + +--- + +## Step 3: Apply Config + +**dotnet (`appsettings.json`):** +```json +{ + "Connections": { + "ServiceConnection": { + "Settings": { + "AuthType": "...", + "ClientId": "...", + "...": "..." + } + } + } +} +``` + +**Node.js (`.env`):** + +Use `connections____settings__` (double underscore separators). Always add a `connectionsMap` entry. + +*ClientSecret:* +``` +connections__serviceConnection__settings__clientId= +connections__serviceConnection__settings__clientSecret= +connections__serviceConnection__settings__tenantId= +connectionsMap__0__connection=serviceConnection +connectionsMap__0__serviceUrl=* +``` + +*UserManagedIdentity* (hosted on Azure — no secret needed): +``` +connections__serviceConnection__settings__clientId= +connectionsMap__0__connection=serviceConnection +connectionsMap__0__serviceUrl=* +``` + +*FederatedCredentials:* +``` +connections__serviceConnection__settings__clientId= +connections__serviceConnection__settings__FICClientId= +connections__serviceConnection__settings__tenantId= +connectionsMap__0__connection=serviceConnection +connectionsMap__0__serviceUrl=* +``` + +> Run with: `node --env-file .env dist/index.js` (Node 20+) + +--- + +## OAuth Connection Setup (Post-Creation) + +Adds a user sign-in OAuth connection to an existing bot. + +### ClientSecret bots (AadV2) + +For bots using ClientSecret auth, use the `Aadv2` service provider. You can reuse the bot's existing app registration or create a separate OAuth app. + +```bash +az bot authsetting create \ + --resource-group "$RESOURCE_GROUP" \ + --name "$BOT_NAME" \ + --setting-name "$OAUTH_CONNECTION_NAME" \ + --client-id "$APP_ID" \ + --client-secret "$CLIENT_SECRET" \ + --provider-scope-string "https://graph.microsoft.com/User.Read openid profile" \ + --service "Aadv2" \ + --parameters tenantId="$TENANT_ID" +``` + +**Required: Add redirect URI to the app registration** — without this, users will get `AADSTS500113: No reply address is registered`: + +```bash +az ad app update \ + --id "$APP_ID" \ + --web-redirect-uris "https://token.botframework.com/.auth/web/redirect" +``` + +**Node.js env var:** +``` +graph_connectionName=GraphOAuthConnection +``` + +### FIC bots (AadV2WithFic) + +Auth type `AadV2WithFic` only. Requires the bot to have a managed identity already configured. + +```bash +# 1. Get bot's MSI principal ID +MSI_PRINCIPAL_ID=$(az identity show \ + --resource-group "$RESOURCE_GROUP" \ + --name "$BOT_NAME" \ + --query "principalId" --output tsv) +MSI_CLIENT_ID=$(az identity show \ + --resource-group "$RESOURCE_GROUP" \ + --name "$BOT_NAME" \ + --query "clientId" --output tsv) + +# 2. Create OAuth app registration (reuse existing or create new) +OAUTH_APP=$(az ad app create \ + --display-name "${BOT_NAME}-oauth" \ + --sign-in-audience "AzureADMyOrg" --output json) +OAUTH_APP_ID=$(echo $OAUTH_APP | jq -r '.appId') +TENANT_ID=$(az account list --query "[?isDefault].tenantId | [0]" --output tsv) + +# 3. Create federated credential on OAuth app +az ad app federated-credential create \ + --id "$OAUTH_APP_ID" \ + --parameters "{ + \"name\": \"agent-oauth\", + \"description\": \"OAuth Agent-to-Channel\", + \"issuer\": \"https://login.microsoftonline.com/${TENANT_ID}/v2.0\", + \"subject\": \"${MSI_PRINCIPAL_ID}\", + \"audiences\": [\"api://AzureADTokenExchange\"] + }" + +# 4. Set application ID URI and create the access_as_user scope +az ad app update \ + --id "$OAUTH_APP_ID" \ + --identifier-uris "api://botid-$OAUTH_APP_ID" + +OAUTH_OBJECT_ID=$(az ad app show --id "$OAUTH_APP_ID" --query id --output tsv) +# uuidgen on Linux/macOS/WSL; python3 fallback for Windows Git Bash +SCOPE_ID=$(uuidgen 2>/dev/null || python3 -c "import uuid; print(uuid.uuid4())") + +az rest --method PATCH \ + --uri "https://graph.microsoft.com/v1.0/applications/$OAUTH_OBJECT_ID" \ + --headers "Content-Type=application/json" \ + --body "{ + \"api\": { + \"oauth2PermissionScopes\": [{ + \"id\": \"$SCOPE_ID\", + \"adminConsentDescription\": \"Allow the app to access the bot on behalf of the signed-in user.\", + \"adminConsentDisplayName\": \"Access the bot as a user\", + \"isEnabled\": true, + \"type\": \"User\", + \"userConsentDescription\": \"Allow this app to access the bot on your behalf.\", + \"userConsentDisplayName\": \"Access the bot as a user\", + \"value\": \"access_as_user\" + }] + } + }" + +# 5. Register OAuth connection on bot +az bot authsetting create \ + --resource-group "$RESOURCE_GROUP" \ + --name "$BOT_NAME" \ + --setting-name "$OAUTH_CONNECTION_NAME" \ + --client-id "$OAUTH_APP_ID" \ + --client-secret "" \ + --provider-scope-string "api://${OAUTH_APP_ID}/access_as_user" \ + --service "Aadv2WithFic" \ + --parameters \ + tenantId="$TENANT_ID" \ + tokenExchangeUrl="api://${OAUTH_APP_ID}" \ + federatedClientId="$MSI_CLIENT_ID" + +# 6. Add redirect URI to OAuth app registration +az ad app update \ + --id "$OAUTH_APP_ID" \ + --web-redirect-uris "https://token.botframework.com/.auth/web/redirect" +``` + +### Teams SSO (optional) + +Pre-authorize the Teams client apps to allow silent token acquisition. The `access_as_user` scope was created in step 4 above and `SCOPE_ID`/`OAUTH_OBJECT_ID` must still be set in the same shell session. + +```bash +az rest --method PATCH \ + --uri "https://graph.microsoft.com/v1.0/applications/$OAUTH_OBJECT_ID" \ + --headers "Content-Type=application/json" \ + --body "{ + \"api\": { + \"preAuthorizedApplications\": [ + { \"appId\": \"1fec8e78-bce4-4aaf-ab1b-5451cc387264\", \"delegatedPermissionIds\": [\"$SCOPE_ID\"] }, + { \"appId\": \"5e3ce6c0-2b1f-4285-8d4b-75ee78787346\", \"delegatedPermissionIds\": [\"$SCOPE_ID\"] }, + { \"appId\": \"d3590ed6-52b3-4102-aeff-aad2292ab01c\", \"delegatedPermissionIds\": [\"$SCOPE_ID\"] }, + { \"appId\": \"bc59ab01-8403-45c6-8796-ac3ef710b3e3\", \"delegatedPermissionIds\": [\"$SCOPE_ID\"] }, + { \"appId\": \"0ec893e0-5785-4de6-99da-4ed124e5296c\", \"delegatedPermissionIds\": [\"$SCOPE_ID\"] }, + { \"appId\": \"4765445b-32c6-49b0-83e6-1d93765276ca\", \"delegatedPermissionIds\": [\"$SCOPE_ID\"] }, + { \"appId\": \"27922004-5251-4030-b22d-91ecd9a37ea4\", \"delegatedPermissionIds\": [\"$SCOPE_ID\"] }, + { \"appId\": \"00000002-0000-0ff1-ce00-000000000000\", \"delegatedPermissionIds\": [\"$SCOPE_ID\"] }, + { \"appId\": \"4345a7b9-9a63-4910-a426-35363201d503\", \"delegatedPermissionIds\": [\"$SCOPE_ID\"] } + ] + } + }" +``` + +These are the Microsoft host client app IDs that must be pre-authorized for SSO to work across all Teams and Office surfaces: + +| App ID | Client | +|--------|--------| +| `1fec8e78-bce4-4aaf-ab1b-5451cc387264` | Teams desktop / mobile | +| `5e3ce6c0-2b1f-4285-8d4b-75ee78787346` | Teams web | +| `d3590ed6-52b3-4102-aeff-aad2292ab01c` | Microsoft Office desktop | +| `bc59ab01-8403-45c6-8796-ac3ef710b3e3` | Teams iOS | +| `0ec893e0-5785-4de6-99da-4ed124e5296c` | Office Mobile (iOS) | +| `4765445b-32c6-49b0-83e6-1d93765276ca` | Microsoft Teams (secondary client) | +| `27922004-5251-4030-b22d-91ecd9a37ea4` | Skype for Business Online | +| `00000002-0000-0ff1-ce00-000000000000` | Microsoft Office (Outlook Web / legacy) | +| `4345a7b9-9a63-4910-a426-35363201d503` | Office Online | + +> **If starting a new session**, re-derive the variables before running the above: +> ```bash +> OAUTH_OBJECT_ID=$(az ad app show --id "$OAUTH_APP_ID" --query id --output tsv) +> SCOPE_ID=$(az ad app show --id "$OAUTH_APP_ID" \ +> --query "api.oauth2PermissionScopes[?value=='access_as_user'].id | [0]" --output tsv) +> ``` + +### API Permissions (optional) + +Add delegated Microsoft Graph permissions when the bot calls the Graph API on behalf of the signed-in user: + +```bash +# Add delegated permissions (00000003-... is the well-known Graph API app ID) +az ad app permission add \ + --id "$OAUTH_APP_ID" \ + --api 00000003-0000-0000-c000-000000000000 \ + --api-permissions \ + e1fe6dd8-ba31-4d61-89e7-88639da4683d=Scope \ + 37f7f235-527c-4136-accd-4a02d197296e=Scope \ + 14dad69e-099b-42c9-810b-d002981feec1=Scope + +# Grant admin consent (requires Global Admin or Privileged Role Admin) +az ad app permission admin-consent --id "$OAUTH_APP_ID" +``` + +Common delegated Graph permission IDs: + +| Permission | GUID | +|---|---| +| `User.Read` | `e1fe6dd8-ba31-4d61-89e7-88639da4683d` | +| `openid` | `37f7f235-527c-4136-accd-4a02d197296e` | +| `profile` | `14dad69e-099b-42c9-810b-d002981feec1` | +| `email` | `64a6cdd6-aab1-4aad-a773-0ae5ec9b9f9b` | +| `offline_access` | `7427e0e9-2fba-42fe-b0c0-848c9e6a8182` | + +### Exchangeable Token (optional) + +Add `--parameters exchangeableToken="true"` to the `az bot authsetting create` command. + +--- + +## Common Mistakes + +| Mistake | Fix | +|---------|-----| +| Using `az bot create` | Broken — hardcodes retired API version `2021-05-01-preview`. Use `az rest --method PUT` with `api-version=2022-09-15` instead (see Step 2) | +| Wrong tenant active in `az` session | Run `az account show` before starting and verify `tenantDefaultDomain` matches the intended tenant — commands silently succeed in the wrong tenant | +| Duplicate app name | Run `az ad app list --display-name "$APP_NAME"` before deploying — duplicate names cause confusing Bicep errors | +| FIC subject uses `clientId` | Use `principalId` (object ID) from `az identity create` | +| Skipped `az ad sp create` | Always create service principal after `az ad app create` | +| Wrong `app-type` | `UserAssignedMSI` for MSI bots; `SingleTenant` for app-reg bots | +| Client secret committed to source | Use Key Vault, env secrets, or GitHub Secrets | +| Secret expiry not tracked | Always retrieve and surface the expiry date with `az ad app credential list --id "$APP_ID" --query "[0].endDateTime"` — default is ~1 year | +| `credential reset` without `--append` on a running bot | Immediately invalidates all existing secrets — causes downtime. Always use `--append`, deploy the new secret, then delete the old key ID | +| Re-running Phase 1 with a new GUID after a partial failure | Generates a second `access_as_user` scope on the existing app. Instead, re-derive `OAUTH_SCOPE_ID` from the existing app (see Step 1 resume block) | +| OAuth app not found | Run `az ad sp create --id ` if bot can't find the app | +| `AADSTS500113: No reply address is registered` | Add `https://token.botframework.com/.auth/web/redirect` as a redirect URI on the app registration — applies to **both** ClientSecret and FIC OAuth app registrations | +| Teams SSO token exchange fails silently | The `access_as_user` scope must be created on the OAuth app registration (step 4 of FIC flow) **before** pre-authorizing Teams clients; and `--provider-scope-string` must reference `access_as_user`, not `user_impersonation` | +| `uuidgen: command not found` on Windows Git Bash | Use `python3 -c "import uuid; print(uuid.uuid4())"` instead of `uuidgen` | + +## Contributing + +If you hit a problem this skill couldn't solve, found a workaround, or noticed something wrong or outdated, that's valuable — please help improve this skill for everyone. + +Draft a suggested issue title and body based on the conversation, then ask the user to open it at: https://github.com/microsoft/agents/issues/new + +A good issue includes: +- What the user was trying to do +- What went wrong (errors, unexpected behavior) +- What worked — including any workaround found during this conversation +- Relevant code or config snippets diff --git a/agent-plugins/agents-for-js/skills/azure-agents-sdk-provision/bicepconfig.json b/agent-plugins/agents-for-js/skills/azure-agents-sdk-provision/bicepconfig.json new file mode 100644 index 00000000..42dab57e --- /dev/null +++ b/agent-plugins/agents-for-js/skills/azure-agents-sdk-provision/bicepconfig.json @@ -0,0 +1,5 @@ +{ + "extensions": { + "microsoftGraph": "br:mcr.microsoft.com/bicep/extensions/microsoftgraph/v1.0:1.0.0" + } +} diff --git a/package.json b/package.json index a626d563..053c5214 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,8 @@ { "type": "module", "scripts": { - "lint": "eslint" + "lint": "eslint", + "validate-plugin": "claude plugin validate ." }, "devDependencies": { "@eslint/js": "^9.31.0",