From 452945b9d96f68997986a088d8ce26a43080cd08 Mon Sep 17 00:00:00 2001 From: Joel Mut Date: Tue, 14 Jul 2026 17:07:34 +0200 Subject: [PATCH] Add to-prod skill --- .../skills/agents-sdk-to-prod/SKILL.md | 91 +++++++++++++++++++ .../references/attachments.md | 44 +++++++++ .../references/downstream-auth.md | 50 ++++++++++ .../references/environment-secrets.md | 67 ++++++++++++++ .../references/header-propagation.md | 20 ++++ .../references/health-readiness.md | 38 ++++++++ .../references/jwt-authentication.md | 90 ++++++++++++++++++ .../references/observability.md | 29 ++++++ .../references/payload-limit.md | 23 +++++ .../references/persistent-storage.md | 43 +++++++++ .../references/proactive-endpoints.md | 72 +++++++++++++++ .../references/rate-limiting.md | 45 +++++++++ .../references/required-baseline.md | 91 +++++++++++++++++++ .../references/safe-error-handling.md | 20 ++++ .../references/transcripts.md | 28 ++++++ 15 files changed, 751 insertions(+) create mode 100644 agent-plugins/agents-for-js/skills/agents-sdk-to-prod/SKILL.md create mode 100644 agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/attachments.md create mode 100644 agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/downstream-auth.md create mode 100644 agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/environment-secrets.md create mode 100644 agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/header-propagation.md create mode 100644 agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/health-readiness.md create mode 100644 agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/jwt-authentication.md create mode 100644 agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/observability.md create mode 100644 agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/payload-limit.md create mode 100644 agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/persistent-storage.md create mode 100644 agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/proactive-endpoints.md create mode 100644 agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/rate-limiting.md create mode 100644 agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/required-baseline.md create mode 100644 agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/safe-error-handling.md create mode 100644 agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/transcripts.md diff --git a/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/SKILL.md b/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/SKILL.md new file mode 100644 index 00000000..0f5a34cf --- /dev/null +++ b/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/SKILL.md @@ -0,0 +1,91 @@ +--- +name: agents-sdk-to-prod +description: Guide the user to create or adapt a Microsoft 365 Agents SDK sample into a production-ready sample with selectable features. +disable-model-invocation: true +--- + +This skill is designed to help and guide the user to create a Microsoft 365 Agents SDK sample or adapt an existing one into a production-ready sample with selectable features and best practices. It will ask the user to select which production features they want to implement, and then guide them through the process of implementing those features in their sample. + +Use the supported feature references before making implementation decisions. Read only the references for the baseline and the selected or detected features. + +## Supported Features +- `required-baseline`: Production baseline checklist, required auth shape, safe defaults, and a minimal hardened sample shape. Reference: [required-baseline.md](./references/required-baseline.md). +- `persistent-storage`: Replace volatile memory storage with Azure Blob Storage or Azure Cosmos DB. Reference: [persistent-storage.md](./references/persistent-storage.md). +- `health-readiness`: Add health and readiness endpoints for hosting platforms and dependency probes. Reference: [health-readiness.md](./references/health-readiness.md). +- `jwt-authentication`: Require JWT authentication for the messages endpoint and protected custom routes. Reference: [jwt-authentication.md](./references/jwt-authentication.md). +- `rate-limiting`: Add rate limiting to public endpoints. Reference: [rate-limiting.md](./references/rate-limiting.md). +- `downstream-auth`: Protect downstream API access with user authorization handlers and token exchange. Reference: [downstream-auth.md](./references/downstream-auth.md). +- `payload-limit`: Limit JSON request body size when owning the Express app or custom routes. Reference: [payload-limit.md](./references/payload-limit.md). +- `proactive-endpoints`: Secure proactive and custom endpoints, store conversation references, and require persistent storage for proactive flows. Reference: [proactive-endpoints.md](./references/proactive-endpoints.md). +- `environment-secrets`: Configure production environment settings and keep secrets out of source control. Reference: [environment-secrets.md](./references/environment-secrets.md). +- `safe-error-handling`: Add safe agent error handling with generic user messages and server-side diagnostics. Reference: [safe-error-handling.md](./references/safe-error-handling.md). +- `header-propagation`: Propagate only approved outbound headers such as trace and correlation IDs. Reference: [header-propagation.md](./references/header-propagation.md). +- `attachments`: Handle attachments defensively with downloader setup and validation. Reference: [attachments.md](./references/attachments.md). +- `observability`: Enable SDK diagnostics and OpenTelemetry tracing. Reference: [observability.md](./references/observability.md). +- `transcripts`: Store transcripts only when required and document retention, access, and disclosure expectations. Reference: [transcripts.md](./references/transcripts.md). + +## Feature Rules +- Do not combine Blob turn-state storage and Cosmos turn-state storage; ask the user to choose one. +- Prefer `persistent-storage` with Blob Storage when a selected feature needs durable state and the user has not chosen a store. +- `proactive-endpoints` requires persistent storage. +- `transcripts` can use Blob transcript storage even when Cosmos DB is used for turn state. +- `payload-limit` usually means owning the Express setup instead of relying only on `startServer` defaults. + +## Workflow +Ask the user if they want to create a new sample or adapt an existing one, unless the user already specified it. +If the user called the skill without specifying a task: either create or adapt/update an existing sample, ask the user which one they want to do. + +Important: a user can ask questions and reason about the production features during any step of the workflow, the skill will answer and provide guidance to reach a common understanding of the production features and their implementation, but the skill should not lose track of the workflow and the selected features, and should not let the user get lost in the reasoning process. + +Important: make sure to validate the sample builds and runs after implementing the selected features, and before reporting the final results to the user. + +### Create a new sample +1. Ask the user to specify the name of the new sample, unless the user already specified it. + 1. Alongside asking the name, show suggestions for a name based on the context provided by the user. +2. Ask the user to select which production features they want to implement in their new sample, unless the user already specified them. + 1. If the selected features require or it is recommended to implement other features, suggest those features to the user. +3. Read [required-baseline.md](./references/required-baseline.md) and the references for the selected features. +4. Create a new sample with the required baseline plus the selected features. + +Example of a required baseline for a new sample: +When creating a new sample, start with a minimal empty-agent shape and add only the features the user selects or mentions. +Usually related files are: +- `src/` folder with TypeScript code + - `index.ts` with the agent and message handler + - `agent.ts` with the agent and adapter setup +- `package.json` with dependencies and scripts +- `tsconfig.json` with TypeScript settings +- `.env` with environment variable settings +- `README.md` with setup, run, and production notes + +#### Report +List the selected features in the new sample, and if the user wants to implement additional features, suggest which ones are missing or unclear and how to implement them. + +Show the following numbered list: +```md +1. `{{ list item }}`: {{ file reference and line number if applicable }} {{ note about the implementation, recommendation, etc. , if applicable }} +``` + +### Adapt an existing sample +1. Inspect the existing sample to determine which production features are already implemented. +2. Ask the user which additional features they want to implement or modify. + 1. If the selected features require or it is recommended to implement other features, suggest those features to the user. +3. Read [required-baseline.md](./references/required-baseline.md) and the references for detected or selected features. +4. Update the sample with the required baseline plus the selected features. + +If more than one sample is detected in the repo, ask these kind of questions (adapt as needed): +- If user specified a sample, use that one. If another sample is detected, ask if the user wants to adapt that one as well. +- Adapt both samples or just one? +- Which sample? + +#### Report +List the detected, missing, and unclear features in the existing sample. If the user wants to implement additional features, suggest which ones are missing or unclear and how to implement them. + +Show the following numbered list: +```md +1. `{{ list item }}`: {{ file reference and line number if applicable }} {{ note about the implementation, recommendation, etc. , if applicable }} +``` + +## References +- SDK repo: https://github.com/microsoft/Agents/blob/main/README.md +- SDK samples: https://github.com/microsoft/Agents/blob/main/samples/README.md diff --git a/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/attachments.md b/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/attachments.md new file mode 100644 index 00000000..dcfbbd2b --- /dev/null +++ b/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/attachments.md @@ -0,0 +1,44 @@ +## Handle attachments defensively + +When to use it: Add attachment downloaders only for samples that actually need to inspect or persist user files. + +Why it matters: Attachments can contain malware, sensitive data, very large payloads, or URLs that should not be fetched by your service. Production agents should validate content type, size, source, scan policy, and retention before processing files. + +Use the SDK downloaders for channel-aware downloads: + +```ts +import { + AgentApplication, + InputFile, + M365AttachmentDownloader, + TurnContext, + TurnState, +} from '@microsoft/agents-hosting' + +const agent = new AgentApplication({ + fileDownloaders: [ + new M365AttachmentDownloader('inputFiles'), + ], +}) +``` + +Then validate the downloaded files inside the handler that uses them. In this example, the downloader stores files in `state.inputFiles` before the message handler runs, and the handler rejects files that do not match the sample's allow-list: + +```ts +agent.onActivity('message', async (context: TurnContext, state: TurnState) => { + const files = state.getValue('inputFiles') ?? [] + const allowedTypes = new Set(['image/png', 'image/jpeg', 'application/pdf']) + const maxBytes = 5 * 1024 * 1024 + + for (const file of files) { + if (!allowedTypes.has(file.contentType) || file.content.length > maxBytes) { + await context.sendActivity('One or more attachments could not be processed.') + return + } + } + + // Process the validated files here. +}) +``` + +Do not download arbitrary user-provided URLs outside the channel attachment flow unless you have an allow-list, network egress controls, timeout limits, and malware scanning in place. diff --git a/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/downstream-auth.md b/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/downstream-auth.md new file mode 100644 index 00000000..58b68d65 --- /dev/null +++ b/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/downstream-auth.md @@ -0,0 +1,50 @@ +## Protect downstream user access + +When to use it: Use user authorization handlers when a sample calls Microsoft Graph, Copilot Studio, Power Platform, or any downstream API on behalf of the signed-in user. + +Why it matters: Inbound JWT authentication proves the channel or caller can send the activity. It does not automatically prove that the user has consented to the downstream API scopes your sample wants to call. + +Require an authorization handler on routes that need user tokens: + +```ts +import { AgentApplication, TurnContext, TurnState } from '@microsoft/agents-hosting' + +const agent = new AgentApplication({ + authorization: { + graph: { + title: 'Sign in', + text: 'Sign in to continue.', + }, + }, +}) + +agent.onActivity('message', async (context: TurnContext, _state: TurnState) => { + const token = await agent.authorization.exchangeToken(context, 'graph', { + scopes: ['https://graph.microsoft.com/.default'], + }) + + if (!token.token) { + await context.sendActivity('Sign in is required before continuing.') + return + } + + // Call the downstream API with token.token here. +}, ['graph']) +``` + +Configure the handler and OBO connection through environment settings: + +```env +AgentApplication__UserAuthorization__Handlers__graph__Settings__azureBotOAuthConnectionName=GraphConnection +AgentApplication__UserAuthorization__Handlers__graph__Settings__title=Sign in +AgentApplication__UserAuthorization__Handlers__graph__Settings__text=Sign in to continue. +AgentApplication__UserAuthorization__Handlers__graph__Settings__oboConnectionName=graphOboConnection +AgentApplication__UserAuthorization__Handlers__graph__Settings__oboScopes=https://graph.microsoft.com/.default + +connections__graphOboConnection__settings__clientId= +connections__graphOboConnection__settings__clientSecret= +connections__graphOboConnection__settings__tenantId= +connections__graphOboConnection__settings__authorityEndpoint=https://login.microsoftonline.com +``` + +Request the smallest set of downstream scopes needed for the sample, handle sign-out, and avoid logging access tokens or decoded token contents. diff --git a/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/environment-secrets.md b/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/environment-secrets.md new file mode 100644 index 00000000..14c8ccf2 --- /dev/null +++ b/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/environment-secrets.md @@ -0,0 +1,67 @@ +## Configure secrets and environment + +When to use it: Use environment configuration for every deployed sample. Put values in your host's secret/config system, not in committed `.env` files. + +Why it matters: The current SDK auth loader supports multiple named connections and service URL mapping. Keeping this format consistent makes samples portable across Bot Framework, Teams, and additional authenticated callers. + +Minimal production auth settings: + +```env +NODE_ENV=production + +connections__serviceConnection__settings__clientId= +connections__serviceConnection__settings__clientSecret= +connections__serviceConnection__settings__tenantId= +connections__serviceConnection__settings__authorityEndpoint=https://login.microsoftonline.com + +connectionsMap__0__connection=serviceConnection +connectionsMap__0__serviceUrl=* +``` + +For production, prefer service URL and audience-specific mappings when the target channels are known. The `serviceUrl` value is interpreted as a regular expression by the connection manager. + +```env +connectionsMap__0__connection=teamsConnection +connectionsMap__0__serviceUrl=^https://smba\.trafficmanager\.net/teams/ +connectionsMap__0__audience= +``` + +Keep `connectionsMap__0__serviceUrl=*` for local development, broad multi-channel samples, or when your deployment intentionally accepts multiple channel service URLs. Use `CloudAdapterOptions__validateServiceUrl=true` either way so inbound activity service URLs are checked against the authenticated token claim. + +Storage settings for the snippets above: + +```env +BLOB_CONTAINER_ID= +BLOB_STORAGE_CONNECTION_STRING= + +COSMOS_DATABASE_ID= +COSMOS_CONTAINER_ID= +COSMOS_ENDPOINT= +COSMOS_KEY= +``` + +Avoid logging raw environment values. The SDK redacts sensitive auth settings in its own logs, but application logs should follow the same rule. + +Prefer secretless or rotatable credentials in production when your host supports them: + +```env +# User-assigned managed identity +connections__serviceConnection__settings__authType=UserManagedIdentity +connections__serviceConnection__settings__clientId= +connections__serviceConnection__settings__tenantId= + +# Workload identity +connections__serviceConnection__settings__authType=WorkloadIdentity +connections__serviceConnection__settings__clientId= +connections__serviceConnection__settings__tenantId= +connections__serviceConnection__settings__federatedTokenFile= + +# Certificate credential +connections__serviceConnection__settings__authType=Certificate +connections__serviceConnection__settings__clientId= +connections__serviceConnection__settings__tenantId= +connections__serviceConnection__settings__certPemFile= +connections__serviceConnection__settings__certKeyFile= +``` + +Use `clientSecret` only when the host cannot use managed identity, workload identity, federated credentials, or certificates, and rotate it through your secret store. diff --git a/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/header-propagation.md b/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/header-propagation.md new file mode 100644 index 00000000..73c04c93 --- /dev/null +++ b/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/header-propagation.md @@ -0,0 +1,20 @@ +## Propagate headers intentionally + +When to use it: Configure header propagation when downstream connector calls or API clients need correlation IDs, trace context, or a product header. + +Why it matters: Blindly forwarding inbound headers can leak `Authorization`, cookies, or tenant-specific data to services that should not receive them. The SDK lets you add or propagate only the headers you choose. + +Use an allow-list: + +```ts +import { AgentApplication, TurnState } from '@microsoft/agents-hosting' + +const agent = new AgentApplication({ + headerPropagation: (headers) => { + headers.propagate(['traceparent', 'tracestate', 'x-correlation-id']) + headers.add({ 'x-agent-component': 'orders-agent' }) + }, +}) +``` + +Do not propagate `authorization`, `cookie`, API keys, or user-provided headers unless the downstream service explicitly requires them and is authorized to receive them. diff --git a/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/health-readiness.md b/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/health-readiness.md new file mode 100644 index 00000000..244ceaf3 --- /dev/null +++ b/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/health-readiness.md @@ -0,0 +1,38 @@ +## Add health and readiness endpoints + +When to use it: Add health endpoints when the sample runs in a container, App Service, Azure Container Apps, Kubernetes, or any environment with load balancers and probes. + +Why it matters: Health probes tell the platform whether the process is alive. Readiness checks tell the platform whether dependencies such as storage are reachable before traffic is routed to the instance. + +Use `beforeListen` when `startServer` owns the Express app: + +```ts +import { AgentApplication, TurnState } from '@microsoft/agents-hosting' +import { startServer } from '@microsoft/agents-hosting-express' +import { BlobsStorage } from '@microsoft/agents-hosting-storage-blob' + +const storage = new BlobsStorage( + process.env.BLOB_CONTAINER_ID!, + process.env.BLOB_STORAGE_CONNECTION_STRING! +) +const agent = new AgentApplication({ storage }) + +startServer(agent, { + beforeListen: (app) => { + app.get('/health', (_req, res) => { + res.status(200).json({ status: 'ok' }) + }) + + app.get('/ready', async (_req, res) => { + try { + await storage.read(['readiness']) + res.status(200).json({ status: 'ready' }) + } catch { + res.status(503).json({ status: 'not ready' }) + } + }) + }, +}) +``` + +Keep health routes lightweight and avoid exposing secrets, tenant IDs, tokens, or detailed exception messages. diff --git a/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/jwt-authentication.md b/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/jwt-authentication.md new file mode 100644 index 00000000..00937393 --- /dev/null +++ b/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/jwt-authentication.md @@ -0,0 +1,90 @@ +## Require JWT authentication + +When to use it: Require JWT authentication for `/api/messages` and any HTTP endpoint that continues a conversation, creates a conversation, updates state, calls a downstream API, or sends a proactive message. + +Why it matters: In development, anonymous requests can be useful. In production, requests must prove they come from an expected channel or trusted caller. The SDK enforces `clientId` when `NODE_ENV=production`. + +The shortest path is `startServer`, which loads auth configuration from the environment and applies JWT auth to the messages route: + +```ts +import { AgentApplication, TurnState } from '@microsoft/agents-hosting' +import { startServer } from '@microsoft/agents-hosting-express' + +const agent = new AgentApplication() + +startServer(agent) +``` + +Enable `CloudAdapter` service URL validation in production. This rejects authenticated activities whose `activity.serviceUrl` host does not match the caller's `serviceurl` claim. It helps prevent confused-deputy and SSRF-style attacks where a valid token is reused with a different service URL. + +Use the environment variable when you rely on SDK-created adapters: + +```env +CloudAdapterOptions__validateServiceUrl=true +CloudAdapterOptions__emitStackTrace=false +``` + +Or provide an adapter explicitly: + +```ts +import { + AgentApplication, + CloudAdapter, + loadAuthConfigFromEnv, + TurnState, +} from '@microsoft/agents-hosting' +import { startServer } from '@microsoft/agents-hosting-express' + +const adapter = new CloudAdapter( + loadAuthConfigFromEnv(), + undefined, + undefined, + { validateServiceUrl: true } +) + +const agent = new AgentApplication({ adapter }) +startServer(agent) +``` + +When you own the Express app, apply `authorizeJWT` to the protected route: + +```ts +import express, { Response } from 'express' +import { + AgentApplication, + AuthConfiguration, + CloudAdapter, + Request, + TurnState, + authorizeJWT, + loadAuthConfigFromEnv, +} from '@microsoft/agents-hosting' + +const authConfig: AuthConfiguration = loadAuthConfigFromEnv() +const adapter = new CloudAdapter(authConfig) +const agent = new AgentApplication() +const app = express() + +app.use(express.json()) + +app.post('/api/messages', authorizeJWT(authConfig), async (req: Request, res: Response) => { + await adapter.process(req, res, async (context) => { + await agent.run(context) + }) +}) +``` + +For framework adapters that can provide Express-compatible request and response objects, use `createAgentRequestHandler`: + +```ts +import express from 'express' +import { AgentApplication, TurnState } from '@microsoft/agents-hosting' +import { createAgentRequestHandler } from '@microsoft/agents-hosting-express' + +const agent = new AgentApplication() +const handler = createAgentRequestHandler(agent) +const app = express() + +app.use(express.json()) +app.post('/api/messages', handler) +``` diff --git a/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/observability.md b/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/observability.md new file mode 100644 index 00000000..e3dbae48 --- /dev/null +++ b/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/observability.md @@ -0,0 +1,29 @@ +## Enable observability + +When to use it: Enable observability before load testing and keep it on in production with appropriate sampling and retention. + +Why it matters: Production agents need traces and logs for authentication failures, storage latency, channel requests, connector calls, and turn processing. + +For local diagnostics, enable SDK debug namespaces: + +```sh +DEBUG=agents:authorization:*,agents:jwt-middleware:*,agents:app:*,agents:state:* node index.js +``` + +For production, wire OpenTelemetry before the agent starts: + +```ts +import { NodeSDK } from '@opentelemetry/sdk-node' +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http' + +const sdk = new NodeSDK({ + serviceName: 'my-agent-service', + traceExporter: new OTLPTraceExporter({ + url: process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + }), +}) + +sdk.start() +``` + +The Agents SDK emits spans and logs from its instrumented components when OpenTelemetry is configured. Use your observability backend to alert on high error rates, auth failures, storage failures, and dependency latency. diff --git a/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/payload-limit.md b/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/payload-limit.md new file mode 100644 index 00000000..438bb2af --- /dev/null +++ b/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/payload-limit.md @@ -0,0 +1,23 @@ +## Limit request payload size + +When to use it: Add body limits when you host the Express app yourself or expose custom JSON endpoints. + +Why it matters: Samples often use `express.json()` with defaults. Production services should reject unexpectedly large payloads before they reach the adapter or sample logic. + +Use an explicit limit on the Express JSON parser: + +```ts +import express from 'express' +import { authorizeJWT, loadAuthConfigFromEnv } from '@microsoft/agents-hosting' + +const authConfig = loadAuthConfigFromEnv() +const app = express() + +app.use(express.json({ limit: '1mb' })) + +app.post('/api/messages', authorizeJWT(authConfig), async (req, res) => { + // Process the activity here. +}) +``` + +If you need body parser settings that `startServer` does not expose, own the Express setup and use `createAgentRequestHandler` or `CloudAdapter.process` directly. diff --git a/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/persistent-storage.md b/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/persistent-storage.md new file mode 100644 index 00000000..ec153da3 --- /dev/null +++ b/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/persistent-storage.md @@ -0,0 +1,43 @@ +## Replace volatile storage + +When to use it: Use persistent storage for any sample that keeps user, conversation, proactive, auth, or dialog state across turns. + +Why it matters: `MemoryStorage` is lost when the process restarts and is not shared across replicas. Production agents should use a durable backing service that works across restarts and multiple instances. + +Use Azure Blob storage when you need straightforward durable state: + +```ts +import { AgentApplication, TurnState } from '@microsoft/agents-hosting' +import { BlobsStorage } from '@microsoft/agents-hosting-storage-blob' + +const storage = new BlobsStorage( + process.env.BLOB_CONTAINER_ID!, + process.env.BLOB_STORAGE_CONNECTION_STRING! +) + +const agent = new AgentApplication({ storage }) +``` + +Use Azure Cosmos DB when you need database-backed state for higher scale or stricter partitioned storage needs: + +```ts +import { AgentApplication, TurnState } from '@microsoft/agents-hosting' +import { + CosmosDbPartitionedStorage, + CosmosDbPartitionedStorageOptions, +} from '@microsoft/agents-hosting-storage-cosmos' + +const cosmosOptions: CosmosDbPartitionedStorageOptions = { + databaseId: process.env.COSMOS_DATABASE_ID!, + containerId: process.env.COSMOS_CONTAINER_ID!, + cosmosClientOptions: { + endpoint: process.env.COSMOS_ENDPOINT!, + key: process.env.COSMOS_KEY!, + }, +} + +const storage = new CosmosDbPartitionedStorage(cosmosOptions) +const agent = new AgentApplication({ storage }) +``` + +Fail startup in production if the required Blob or Cosmos storage settings are missing. diff --git a/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/proactive-endpoints.md b/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/proactive-endpoints.md new file mode 100644 index 00000000..9866713a --- /dev/null +++ b/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/proactive-endpoints.md @@ -0,0 +1,72 @@ +## Secure proactive and custom endpoints + +When to use it: Use this for endpoints outside the normal channel message path, such as `/api/proactive/*`, administrative operations, callbacks, scheduled jobs, or any endpoint called by another service. + +Why it matters: `authorizeJWT` validates the token. Your application should still decide which callers are allowed to perform the operation. + +Add an allow-list after `authorizeJWT`: + +```ts +import express, { Request, Response } from 'express' +import { authorizeJWT, loadAuthConfigFromEnv } from '@microsoft/agents-hosting' +import type { JwtPayload } from 'jsonwebtoken' + +const authConfig = loadAuthConfigFromEnv() +const app = express() +app.use(express.json()) + +const allowedCallers = (process.env.ALLOWED_CALLERS ?? '') + .split(',') + .map((value) => value.trim()) + .filter(Boolean) + +function requireAllowedCaller (req: Request, res: Response, next: express.NextFunction) { + const user = (req as any).user as JwtPayload | undefined + const callerId = user?.appid ?? user?.azp ?? user?.sub ?? '' + + if (!allowedCallers.includes(callerId)) { + res.status(403).json({ error: 'Caller is not allowed.' }) + return + } + + next() +} + +app.post( + '/api/proactive/continue/:conversationId', + authorizeJWT(authConfig), + requireAllowedCaller, + async (req, res) => { + // Continue the conversation here. + } +) +``` + +In production, fail startup when `ALLOWED_CALLERS` is empty for protected custom endpoints. + +## Store proactive references securely + +When to use it: Use this when a sample sends proactive messages, creates conversations, or stores conversation references for later use. + +Why it matters: A conversation reference includes routing information such as `serviceUrl` and conversation IDs. Treat it as sensitive operational state and store it in the same persistent, access-controlled storage used by the agent. + +Configure proactive storage explicitly: + +```ts +import { AgentApplication, TurnState } from '@microsoft/agents-hosting' +import { BlobsStorage } from '@microsoft/agents-hosting-storage-blob' + +const storage = new BlobsStorage( + process.env.BLOB_CONTAINER_ID!, + process.env.BLOB_STORAGE_CONNECTION_STRING! +) + +const agent = new AgentApplication({ + storage, + proactive: { storage }, +}) +``` + +Prefer storing references captured from real inbound turns. Use default Teams service endpoints from builders only as a fallback when no real `serviceUrl` has been observed for the target conversation, and keep custom proactive HTTP endpoints protected with JWT plus caller allow-lists. + +If proactive messages require user authorization handlers, keep the default `failOnUnsignedInConnections` behavior so `continueConversation()` fails when a required user connection has not signed in. Set it to `false` only for flows where skipped or partially authorized proactive work is explicitly acceptable. diff --git a/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/rate-limiting.md b/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/rate-limiting.md new file mode 100644 index 00000000..1e926d84 --- /dev/null +++ b/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/rate-limiting.md @@ -0,0 +1,45 @@ +## Add rate limiting + +When to use it: Rate limit public endpoints, especially `/api/messages`, invoke endpoints, task module endpoints, and custom proactive endpoints. + +Why it matters: Rate limiting reduces accidental overload and basic abuse. It does not replace authentication, authorization, bot service throttling, WAF rules, or channel-specific limits. + +Use `startServer` options for the messages route: + +```ts +import { AgentApplication, TurnState } from '@microsoft/agents-hosting' +import { startServer } from '@microsoft/agents-hosting-express' + +const agent = new AgentApplication() + +startServer(agent, { + rateLimitOptions: { + windowMs: 15 * 60 * 1000, + max: 100, + }, +}) +``` + +When you own the Express app, add `express-rate-limit` before `authorizeJWT`: + +```ts +import express from 'express' +import rateLimit from 'express-rate-limit' +import { authorizeJWT, loadAuthConfigFromEnv } from '@microsoft/agents-hosting' + +const authConfig = loadAuthConfigFromEnv() +const messagesRateLimiter = rateLimit({ + windowMs: 15 * 60 * 1000, + max: 100, + standardHeaders: true, + legacyHeaders: false, +}) + +const app = express() +app.use(express.json()) +app.post('/api/messages', messagesRateLimiter, authorizeJWT(authConfig), async (req, res) => { + // Process the activity here. +}) +``` + +Tune limits for your channel traffic, expected concurrency, retry policy, and load testing results. diff --git a/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/required-baseline.md b/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/required-baseline.md new file mode 100644 index 00000000..bdb3dd14 --- /dev/null +++ b/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/required-baseline.md @@ -0,0 +1,91 @@ +# Production-ready sample hardening + +Agents SDK samples are intentionally small so that each feature is easy to see. Before adapting a sample for production, add the operational and security pieces that a short sample usually leaves out. This guide shows those pieces as additive changes that can be applied to any JavaScript or TypeScript sample. + +The examples use the public APIs already exposed by the SDK. The same hardening model also applies conceptually to other SDK languages. + +> [!IMPORTANT] +> Production readiness depends on your hosting environment, data classification, compliance requirements, and channel configuration. Treat this guide as the baseline checklist for an Agents SDK sample, then apply your organization's security review. + +## Production baseline checklist + +- Use persistent state instead of `MemoryStorage`. +- Require JWT authentication on the agent messages endpoint and any custom HTTP endpoint that changes data or sends messages. +- Enable `CloudAdapterOptions__validateServiceUrl=true` so activity `serviceUrl` values must match the authenticated caller's `serviceurl` claim. +- Restrict `connectionsMap` entries to expected service URLs and audiences where possible. +- Require user authorization handlers before calling downstream APIs on behalf of a user. +- Add rate limiting on public HTTP endpoints. +- Limit JSON request body size when you own the Express app. +- Add unauthenticated health endpoints and readiness checks for dependencies. +- Store secrets in your hosting platform's secret store, not in source control. +- Set `NODE_ENV=production` so missing required auth settings fail at startup. +- Log server-side failures, but send generic user-facing error messages. +- Enable SDK debug namespaces or OpenTelemetry exporters for production diagnostics. +- Propagate only approved outbound headers. +- Download attachments only when the scenario needs them, and validate type, size, and source. +- Store transcripts only when there is a product, support, audit, or compliance requirement. + +## Minimal hardened sample shape + +This combines the main pieces without adding sample-specific behavior: + +```ts +import { + AgentApplication, + CloudAdapter, + loadAuthConfigFromEnv, + TurnContext, + TurnState, +} from '@microsoft/agents-hosting' +import { startServer } from '@microsoft/agents-hosting-express' +import { BlobsStorage } from '@microsoft/agents-hosting-storage-blob' + +const storage = new BlobsStorage( + process.env.BLOB_CONTAINER_ID!, + process.env.BLOB_STORAGE_CONNECTION_STRING! +) +const adapter = new CloudAdapter( + loadAuthConfigFromEnv(), + undefined, + undefined, + { validateServiceUrl: true } +) + +const agent = new AgentApplication({ + adapter, + storage, + proactive: { storage }, + startTypingTimer: true, +}) + +agent.onActivity('message', async (context: TurnContext, state: TurnState) => { + const counter = state.getValue('conversation.counter') ?? 0 + await context.sendActivity(`[${counter}] You said: ${context.activity.text}`) + state.setValue('conversation.counter', counter + 1) +}) + +agent.onError(async (context, error) => { + console.error('Unhandled turn error:', error) + await context.sendActivity('Sorry, something went wrong while processing your request.') +}) + +startServer(agent, { + rateLimitOptions: { + windowMs: 15 * 60 * 1000, + max: 100, + }, + beforeListen: (app) => { + app.get('/health', (_req, res) => res.status(200).json({ status: 'ok' })) + app.get('/ready', async (_req, res) => { + try { + await storage.read(['readiness']) + res.status(200).json({ status: 'ready' }) + } catch { + res.status(503).json({ status: 'not ready' }) + } + }) + }, +}) +``` + +Use this shape as the starting point, then add sample-specific routes, auth handlers, proactive messaging, transcripts, and downstream API clients only when the scenario requires them. diff --git a/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/safe-error-handling.md b/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/safe-error-handling.md new file mode 100644 index 00000000..88ccbc34 --- /dev/null +++ b/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/safe-error-handling.md @@ -0,0 +1,20 @@ +## Add safe error handling + +When to use it: Add an error handler to any sample that can call external services, use storage, process files, run long operations, or handle user input. + +Why it matters: Users need a clean failure message. Operators need enough server-side detail to diagnose the issue. Do not send stack traces, tokens, connection strings, or downstream response bodies to users. + +Use `agent.onError`: + +```ts +import { AgentApplication, TurnContext, TurnState } from '@microsoft/agents-hosting' + +const agent = new AgentApplication() + +agent.onError(async (context: TurnContext, error: Error) => { + console.error('Unhandled turn error:', error) + await context.sendActivity('Sorry, something went wrong while processing your request.') +}) +``` + +Use structured logging in production and include correlation IDs from the hosting platform when available. diff --git a/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/transcripts.md b/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/transcripts.md new file mode 100644 index 00000000..1ca66e08 --- /dev/null +++ b/agent-plugins/agents-for-js/skills/agents-sdk-to-prod/references/transcripts.md @@ -0,0 +1,28 @@ +## Store transcripts intentionally + +When to use it: Enable transcript storage when you need support diagnostics, audit trails, supervised review, or compliance retention. + +Why it matters: Transcripts can contain user content, personal data, attachments, and business-sensitive information. Storing them increases operational value and privacy responsibility. + +Use Blob-backed transcripts instead of local files: + +```ts +import { AgentApplication, TurnState } from '@microsoft/agents-hosting' +import { BlobsStorage, BlobsTranscriptStore } from '@microsoft/agents-hosting-storage-blob' + +const storage = new BlobsStorage( + process.env.BLOB_CONTAINER_ID!, + process.env.BLOB_STORAGE_CONNECTION_STRING! +) +const transcriptLogger = new BlobsTranscriptStore( + process.env.BLOB_STORAGE_CONNECTION_STRING!, + process.env.BLOB_CONTAINER_ID! +) + +const agent = new AgentApplication({ + storage, + transcriptLogger, +}) +``` + +Before enabling transcripts in production, define retention, deletion, encryption, access control, and customer disclosure requirements.