@microsoft/agents-hosting
Overview
The @microsoft/agents-hosting package provides the necessary tools and components to create and host Microsoft Agents. This package includes a compatible API to migrate a bot using botbuilder from the BotFramework SDK.
Installation
To install the package:
npm install @microsoft/agents-hosting
Hosting integration APIs
To make hosting an agent independent of any single web framework, this package
exposes framework-agnostic primitives that the
@microsoft/agents-hosting-express and
@microsoft/agents-hosting-fastify packages build on:
createCloudAdapter(agent, authConfig, options?)— returns{ adapter, headerPropagation }for processing incoming activities. It preserves anAgentApplication's existing adapter;authConfigis used only when creating an adapter. Useoptions.configurationContextto scope preloaded configuration to this host. Use this from any web framework.CloudAdapterResult— return type ofcreateCloudAdapter.createAgentResponseHandler(adapter, agent, conversationState)— framework-agnostic handler(req, res, params) => Promise<void>for the authenticated SDK-specific Activity callback route.AgentResponseHandler,AgentResponseHandlerParams,AGENT_RESPONSE_ROUTE_PATH— supporting types and the canonical route path.WebResponse,NextFunction,WebRequestParamsCarrier— minimal structural interfaces (no Express/Fastify imports) used by the cross-framework helpers above.
Most consumers should keep using startServer/createAgentRequestHandler from the
Express or Fastify packages; reach for these APIs when adapting another framework.
This Activity callback flow is used for SDK-specific Activity-protocol delegation.
The Activity callback handler authenticates requests once through the supplied
CloudAdapter. That boundary validates the token for any configured host connection;
the handler then verifies that the caller application matches the delegated agent
recorded for that conversation. Existing route-level authorizeJWT middleware is
redundant but remains compatible. On configured or production hosts, missing,
invalid, expired, or wrong-audience tokens return 401. An authenticated caller
that does not match the delegated agent, or missing, malformed, or pre-upgrade
delegated state, returns 403.
Anonymous callbacks are supported only for unconfigured development hosts
outside production and emit a registration warning because peer ownership cannot
be verified. Pre-upgrade conversations must be restarted.
Pluggable configuration sources
Existing environment variables and direct runtime options continue to work
without changes. Additional asynchronous sources can be loaded into an
immutable host-scoped context and then consumed synchronously by auth
configuration, CloudAdapterOptions, outbound host validation, and
AgentApplication user authorization:
import {
AttachmentDownloader,
CloudAdapter,
createConfigurationContext,
createOutboundHostValidator,
ConfigurationSource,
} from '@microsoft/agents-hosting'
const source: ConfigurationSource = {
name: 'central-configuration',
async load () {
return {
format: 'document',
value: {
cloudAdapterOptions: {
validateServiceUrl: true
},
outboundHostValidator: {
enabled: true,
hosts: ['api.contoso.com']
}
}
}
}
}
const configurationContext = await createConfigurationContext([{
source,
mode: 'overrideEnvironment'
}])
const adapter = new CloudAdapter(
undefined,
undefined,
undefined,
{ configurationContext }
)
// Reuse the same policy for code-level outbound consumers such as attachment
// downloaders.
const outboundPolicy = createOutboundHostValidator({ configurationContext })
const downloader = new AttachmentDownloader('inputFiles', outboundPolicy)
For an explicitly selected JSON file, use the built-in file source:
import {
createConfigurationContext,
createJsonFileConfigurationSource
} from '@microsoft/agents-hosting'
const configurationContext = await createConfigurationContext([{
source: createJsonFileConfigurationSource('config.DEVELOPMENT.json'),
mode: 'overrideEnvironment'
}])
The helper loads strict JSON using the hierarchical document shape. It does not discover files, select environment-specific names, parse command-line options, or choose precedence. The application owns those policies and supplies the source mode. Missing files, malformed JSON, and non-object document roots fail loading without including file contents in diagnostics.
Context creation is atomic: if a source fails, no partial values are applied.
Create separate contexts for independently configured agents in one process.
Pass the same context to each agent's CloudAdapter and
AgentApplication. Existing applications may instead call
preloadConfigurationSources once before constructing consumers; that API is
the compatibility wrapper over the default process context.
Load the default context before constructing any configuration consumer. Default-context loading is atomic and retryable after failure, but a successful load can occur only once and cannot run concurrently. Constructing a consumer first consumes an empty default snapshot and prevents later preload. To adopt updated or rotated values, create a new immutable context and reconstruct its consumers; existing contexts and consumers are not mutated.
Every source must select an explicit mode:
fallback: below Bot Framework compatibility variables and supported modern__environment configuration.overrideEnvironment: above environment configuration and below direct runtime options.enforce: above direct runtime options.
Sources in the same mode are applied in registration order, with later values
winning. enforce can override values supplied directly by application code
and should be reserved for deliberate central policy enforcement.
Within the environment band, a modern Connections__* registry replaces flat
or prefixed Bot Framework authentication variables; the two representations
are not merged property by property. When no modern registry exists, the
legacy flat or prefixed variables remain supported. When a modern environment
registry has no ConnectionsMap__* entries, the environment auth loader
preserves backward compatibility by creating a wildcard/default route for the
first connection; connection insertion order determines the default only in
that case. External sources may omit connectionsMap when they define exactly
one connection, which is treated as the wildcard/default. External sources
that define multiple connections must provide an explicit wildcard/default
route.
Supported canonical path families are:
connections.<id>.settings.<property>connectionsMap.<index>.serviceUrlconnectionsMap.<index>.audienceconnectionsMap.<index>.connectioncloudAdapterOptions.<property>outboundHostValidator.<property>agentApplication.userAuthorization.handlers.<id>.settings.<property>
Sources return either { format: 'canonical', values } for canonical string
paths or { format: 'document', value } for hierarchical typed JSON.
Existing sources that return a bare canonical record remain supported for
backward compatibility; new sources should use the explicit tagged form.
Canonical paths use current property names only, except for the retained
WIDAssertionFile compatibility spelling (prefer federatedTokenFile).
Schema-shaped __
environment variables are also a permanent first-class input and bind to the
same hierarchy. Bot Framework-era MicrosoftApp*, prefixed connection, and
<handlerId>_* aliases remain supported through an isolated compatibility
adapter, but are not valid external-source paths. Invalid paths or values fail
loading without including the supplied value in diagnostics.
The provider-neutral hierarchical document shape is defined by
agents-configuration.schema.json.
Example Usage based on the AgentApplication object
import { AgentApplication, MemoryStorage, TurnContext, TurnState } from '@microsoft/agents-hosting'
const echo = new AgentApplication<TurnState>({ storage: new MemoryStorage() })
echo.onConversationUpdate('membersAdded', async (context: TurnContext) => {
await context.sendActivity('Welcome to the Echo sample, send a message to see the echo feature in action.')
})
echo.onActivity('message', async (context: TurnContext, state: TurnState) => {
let counter: number = state.getValue('conversation.counter') || 0
await context.sendActivity(`[${counter++}]You said: ${context.activity.text}`)
state.setValue('conversation.counter', counter)
})
Handling errors from direct proactive operations
adapter.continueConversation() sends errors from its callback to the adapter's
onTurnError handler. If that handler completes normally, continueConversation()
also completes normally, so a surrounding try/catch does not receive the
original error.
When the caller needs to inspect or retry a failed proactive operation, catch the
error inside the continuation callback and throw it after continueConversation()
completes:
import type { ResourceResponse } from '@microsoft/agents-hosting'
let sendResponse: ResourceResponse | undefined
let sendFailed = false
let sendError: unknown
await adapter.continueConversation(botAppId, reference, async (context) => {
try {
sendResponse = await context.sendActivity('Proactive message')
} catch (error) {
sendFailed = true
sendError = error
}
})
if (sendFailed) {
throw sendError
}
console.log('Sent activity:', sendResponse)
Because the callback catches the error, onTurnError is not called for that
failure. Applications that want centralized handling can perform it in the
callback's catch block.
Storage providers
Public storage options accept StorageProvider, which is Storage | StorageV2.
Providers can implement either contract. Custom V2 providers extend StorageV2,
so hosting can identify the structured contract with instanceof.
Hosting adapts a legacy Storage provider for default upsert operations. The
adapter cannot support create-only, replace, or version conditions.
Built-in V1 and V2 providers have separately named classes, avoiding overloaded
JavaScript methods while preserving every legacy constructor and return type:
const legacyStorage = new MemoryStorage()
const storageV2 = new MemoryStorageV2()
const fileStorageV2 = new FileStorageV2('./data')
V2 calls return keyed operation results. Read values are at
results[key].value; inspect status and version for operation outcomes.
Example Usage based on bot framework Activity Handler
Create an Echo bot using the ActivityHandler
// myHandler.ts
import { ActivityHandler, MessageFactory } from '@microsoft/agents-hosting'
export class MyHandler extends ActivityHandler {
constructor () {
super()
this.onMessage(async (context, next) => {
const replyText = `Agent: ${context.activity.text}`
await context.sendActivity(MessageFactory.text(replyText))
await next()
})
}
}
Host the bot with express
// index.ts
import express, { Response } from 'express'
import { Request, CloudAdapter, authorizeJWT, AuthConfiguration, loadAuthConfigFromEnv } from '@microsoft/agents-hosting'
import { EchoBot } from './myHandler'
const authConfig: AuthConfiguration = loadAuthConfigFromEnv()
const adapter = new CloudAdapter(authConfig)
const myHandler = new MyHandler()
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 myHandler.run(context))
})
Inbound token issuer validation
Inbound JWT signature, audience, lifetime, and tenant-claim consistency checks are always applied. Issuer allow-list validation is opt-in because AgentID blueprint identities can use runtime-selected tenant issuers that are not known when the host configuration is created.
Enable issuer validation only when every accepted issuer is represented by the
connection's configured tenant or issuers list:
validateIssuer=true
# Latest connection configuration format
Connections__serviceConnection__Settings__ValidateIssuer=true
Accepted boolean values are true, false, 1, and 0 (case-insensitive,
with surrounding whitespace ignored). Any other non-empty value causes
configuration loading to fail with a structured error that identifies the
configuration source and accepted values.
Outbound request host validation
OutboundHostValidator provides an opt-in allowlist for server-side requests made
to activity service URLs and attachment URLs. Enforcement is disabled by default.
It can be configured with environment variables:
OutboundHostValidator__Enabled=true
OutboundHostValidator__IncludeDefaultMicrosoftHosts=true
OutboundHostValidator__AllowPrivateNetworkAddresses=false
OutboundHostValidator__Hosts=contoso.com,fabrikam.com
Indexed host variables such as OutboundHostValidator__Hosts__0=contoso.com are
also supported. A host entry matches both the exact host and its subdomains, and
is normalized (scheme/port/path stripped; a leading *. is accepted and ignored).
When enforcement is enabled, CloudAdapter rejects inbound activities whose
serviceUrl host is not allowlisted, and it also rejects serviceurl claim
mismatches (equivalent to CloudAdapterOptions.validateServiceUrl=true).
For explicit configuration, reuse the same immutable policy in the adapter and attachment downloaders:
import {
AgentApplication,
AttachmentDownloader,
CloudAdapter,
OutboundHostValidator
} from '@microsoft/agents-hosting'
const outboundHostValidator = new OutboundHostValidator({
enabled: true,
hosts: ['contoso.com']
})
const adapter = new CloudAdapter(undefined, undefined, undefined, undefined, outboundHostValidator)
const agent = new AgentApplication({
adapter,
fileDownloaders: [new AttachmentDownloader('inputFiles', outboundHostValidator)]
})
When enforcement is enabled, URLs must use HTTPS and must not contain userinfo.
Both attachment downloaders validate the actual download destination and every
redirect before requesting it. They check all DNS answers and reject private,
shared (RFC 6598 100.64.0.0/10), loopback, link-local, unique-local,
IPv4-mapped forms of blocked IPv4 addresses, and unspecified addresses by default.
DNS failures and empty DNS answers also block the download.
Rejected attachments are skipped with a warning; no request is sent to the
rejected destination. Warnings use DEBUG=agents:attachmentDownloader:* or
DEBUG=agents:outboundHostValidator:* and do not include URL query strings or
credentials.
To deliberately access an allowlisted private or shared-network attachment destination,
set allowPrivateNetworkAddresses: true (or the equivalent environment variable
above). This exception does not bypass HTTPS, userinfo checks, or the host
allowlist. Existing custom OutboundUrlPolicy implementations remain supported;
enabled custom policies are checked at every hop and receive the same HTTPS and
public-address safeguards, without the built-in private-network exception.
Initial request headers, including authorization, are not forwarded to any redirect, even on the same origin. Services requiring authorization on a redirected endpoint must supply a directly accessible, allowed download URL. Redirects are limited to 50 hops.
Enforcement remains disabled by default. With enforcement disabled, existing
downloads retain native fetch redirects and their initial authentication
behavior, and do not perform the new DNS checks. Applications already enabling
validation may need to replace HTTP URLs and explicitly allow intended private
destinations. The synchronous isAllowed() check validates URL/host/literal
address policy; isAllowedAsync() additionally checks DNS answers and is used
by the attachment downloaders. Adapter service-URL checks remain synchronous.
Security boundaries: DNS checks occur before the request, but do not pin the connection to a validated address; use network egress controls as additional protection against DNS rebinding. Host allowlisting is not credential-audience authorization: an allowed Microsoft-hosted domain may contain tenant-controlled content. These controls do not change which initial attachment requests receive Connector credentials. In particular, legacy/default downloads can still send credentials to an activity-selected destination. Separating preauthenticated Teams/OneDrive downloads from authenticated Connector attachment operations is a separate compatibility-sensitive change.