npm.io
1.10.1 • Published 1 week ago

@microsoft/agents-hosting

Licence
MIT
Version
1.10.1
Deps
7
Size
2.7 MB
Vulns
2
Weekly
0
Stars
91

@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 an AgentApplication's existing adapter; authConfig is used only when creating an adapter. Use options.configurationContext to scope preloaded configuration to this host. Use this from any web framework.
  • CloudAdapterResult — return type of createCloudAdapter.
  • 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>.serviceUrl
  • connectionsMap.<index>.audience
  • connectionsMap.<index>.connection
  • cloudAdapterOptions.<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.

Keywords