@microsoft/agents-hosting 1.7.2 → 1.8.0-beta.34.g5d543f96c0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +64 -2
  2. package/dist/package.json +3 -3
  3. package/dist/src/agent-client/agentClient.d.ts +18 -6
  4. package/dist/src/agent-client/agentClient.js +34 -18
  5. package/dist/src/agent-client/agentClient.js.map +1 -1
  6. package/dist/src/agent-client/agentResponseHandler.d.ts +14 -9
  7. package/dist/src/agent-client/agentResponseHandler.js +10 -5
  8. package/dist/src/agent-client/agentResponseHandler.js.map +1 -1
  9. package/dist/src/agent-client/createAgentResponseHandler.d.ts +7 -2
  10. package/dist/src/agent-client/createAgentResponseHandler.js +55 -8
  11. package/dist/src/agent-client/createAgentResponseHandler.js.map +1 -1
  12. package/dist/src/app/adaptiveCards/activityValueParsers.d.ts +1 -4
  13. package/dist/src/app/adaptiveCards/activityValueParsers.js +2 -5
  14. package/dist/src/app/adaptiveCards/activityValueParsers.js.map +1 -1
  15. package/dist/src/app/adaptiveCards/adaptiveCardsActions.d.ts +18 -0
  16. package/dist/src/app/adaptiveCards/adaptiveCardsActions.js +53 -23
  17. package/dist/src/app/adaptiveCards/adaptiveCardsActions.js.map +1 -1
  18. package/dist/src/app/agentApplication.d.ts +3 -0
  19. package/dist/src/app/agentApplication.js +126 -78
  20. package/dist/src/app/agentApplication.js.map +1 -1
  21. package/dist/src/app/attachmentDownloader.d.ts +4 -1
  22. package/dist/src/app/attachmentDownloader.js +27 -5
  23. package/dist/src/app/attachmentDownloader.js.map +1 -1
  24. package/dist/src/app/auth/handlers/agenticAuthorization.d.ts +1 -1
  25. package/dist/src/app/auth/handlers/azureBotAuthorization.d.ts +1 -1
  26. package/dist/src/app/auth/types.d.ts +3 -4
  27. package/dist/src/app/index.d.ts +2 -0
  28. package/dist/src/app/index.js.map +1 -1
  29. package/dist/src/app/streaming/streamingResponse.js +2 -9
  30. package/dist/src/app/streaming/streamingResponse.js.map +1 -1
  31. package/dist/src/app/teamsAttachmentDownloader.d.ts +3 -1
  32. package/dist/src/app/teamsAttachmentDownloader.js +12 -1
  33. package/dist/src/app/teamsAttachmentDownloader.js.map +1 -1
  34. package/dist/src/auth/authConfiguration.js +12 -0
  35. package/dist/src/auth/authConfiguration.js.map +1 -1
  36. package/dist/src/auth/index.d.ts +2 -1
  37. package/dist/src/auth/index.js +2 -1
  38. package/dist/src/auth/index.js.map +1 -1
  39. package/dist/src/auth/jwt-middleware.d.ts +6 -0
  40. package/dist/src/auth/jwt-middleware.js +263 -13
  41. package/dist/src/auth/jwt-middleware.js.map +1 -1
  42. package/dist/src/auth/msal/msalConnectionSettings.d.ts +8 -0
  43. package/dist/src/auth/msal/msalHttpRetryHandlerHelper.d.ts +34 -0
  44. package/dist/src/auth/msal/msalHttpRetryHandlerHelper.js +126 -0
  45. package/dist/src/auth/msal/msalHttpRetryHandlerHelper.js.map +1 -0
  46. package/dist/src/auth/msal/msalTokenProvider.d.ts +1 -0
  47. package/dist/src/auth/msal/msalTokenProvider.js +23 -14
  48. package/dist/src/auth/msal/msalTokenProvider.js.map +1 -1
  49. package/dist/src/auth/settings.d.ts +18 -0
  50. package/dist/src/auth/settings.js +8 -2
  51. package/dist/src/auth/settings.js.map +1 -1
  52. package/dist/src/cloudAdapter.d.ts +29 -2
  53. package/dist/src/cloudAdapter.js +44 -2
  54. package/dist/src/cloudAdapter.js.map +1 -1
  55. package/dist/src/errorHelper.js +137 -101
  56. package/dist/src/errorHelper.js.map +1 -1
  57. package/dist/src/index.d.ts +1 -0
  58. package/dist/src/index.js +1 -0
  59. package/dist/src/index.js.map +1 -1
  60. package/dist/src/invoke/adaptiveCardInvokeValue.d.ts +5 -0
  61. package/dist/src/oauth/userTokenClient.d.ts +0 -1
  62. package/dist/src/oauth/userTokenClient.js +0 -1
  63. package/dist/src/oauth/userTokenClient.js.map +1 -1
  64. package/dist/src/observability/traces.js +3 -2
  65. package/dist/src/observability/traces.js.map +1 -1
  66. package/dist/src/outboundHostValidator.d.ts +47 -0
  67. package/dist/src/outboundHostValidator.js +149 -0
  68. package/dist/src/outboundHostValidator.js.map +1 -0
  69. package/dist/src/state/agentState.d.ts +5 -0
  70. package/dist/src/state/agentState.js +16 -3
  71. package/dist/src/state/agentState.js.map +1 -1
  72. package/package.json +3 -3
  73. package/src/agent-client/agentClient.ts +46 -23
  74. package/src/agent-client/agentResponseHandler.ts +14 -9
  75. package/src/agent-client/createAgentResponseHandler.ts +70 -9
  76. package/src/app/adaptiveCards/activityValueParsers.ts +3 -9
  77. package/src/app/adaptiveCards/adaptiveCardsActions.ts +52 -24
  78. package/src/app/agentApplication.ts +135 -74
  79. package/src/app/attachmentDownloader.ts +25 -5
  80. package/src/app/auth/handlers/agenticAuthorization.ts +1 -1
  81. package/src/app/auth/handlers/azureBotAuthorization.ts +1 -1
  82. package/src/app/auth/types.ts +3 -3
  83. package/src/app/index.ts +8 -0
  84. package/src/app/streaming/streamingResponse.ts +2 -8
  85. package/src/app/teamsAttachmentDownloader.ts +11 -1
  86. package/src/auth/authConfiguration.ts +12 -0
  87. package/src/auth/index.ts +2 -1
  88. package/src/auth/jwt-middleware.ts +298 -14
  89. package/src/auth/msal/msalConnectionSettings.ts +10 -0
  90. package/src/auth/msal/msalHttpRetryHandlerHelper.ts +161 -0
  91. package/src/auth/msal/msalTokenProvider.ts +32 -14
  92. package/src/auth/settings.ts +18 -3
  93. package/src/cloudAdapter.ts +56 -3
  94. package/src/errorHelper.ts +155 -114
  95. package/src/index.ts +1 -0
  96. package/src/invoke/adaptiveCardInvokeValue.ts +5 -0
  97. package/src/oauth/userTokenClient.ts +0 -1
  98. package/src/observability/traces.ts +2 -1
  99. package/src/outboundHostValidator.ts +172 -0
  100. package/src/state/agentState.ts +24 -6
@@ -14,11 +14,18 @@ import { normalizeIncomingActivity } from '../activityWireCompat'
14
14
  import { Errors } from '../errorHelper'
15
15
  import { debug } from '@microsoft/agents-telemetry'
16
16
  import { ConversationState } from '../state'
17
+ import { getAuthorizedAudience } from '../auth/jwt-middleware'
17
18
 
18
19
  const logger = debug('agents:agent-client')
19
20
 
20
21
  interface ConversationReferenceState {
21
22
  conversationReference: ConversationReference
23
+ expectedAgentClientId?: string
24
+ }
25
+
26
+ interface DelegatedConversationState {
27
+ conversationReference: ConversationReference
28
+ expectedAgentClientId: string
22
29
  }
23
30
 
24
31
  /**
@@ -31,7 +38,7 @@ export interface AgentResponseHandlerParams {
31
38
  }
32
39
 
33
40
  /**
34
- * Framework-agnostic handler signature for the agent response controller endpoint.
41
+ * Framework-agnostic handler signature for the SDK-specific Activity callback endpoint.
35
42
  *
36
43
  * @remarks
37
44
  * The handler is intended to be invoked by a thin framework-specific wrapper (such
@@ -45,7 +52,7 @@ export type AgentResponseHandler = (
45
52
  ) => Promise<void>
46
53
 
47
54
  /**
48
- * Creates a framework-agnostic handler for the agent response controller endpoint.
55
+ * Creates a framework-agnostic handler for the authenticated Activity callback endpoint.
49
56
  *
50
57
  * This is the core, Express-free implementation used by:
51
58
  * - `configureResponseController` in `@microsoft/agents-hosting-express`
@@ -54,6 +61,11 @@ export type AgentResponseHandler = (
54
61
  * Both wrappers register the canonical route
55
62
  * `POST /api/agentresponse/v3/conversations/:conversationId/activities/:activityId`
56
63
  * and forward the parsed body + path parameters to the handler returned here.
64
+ * The handler returns `401` for failed JWT authentication and `403` when the
65
+ * authenticated caller does not match the delegated agent stored for the
66
+ * conversation, or when that delegated state is missing or malformed.
67
+ *
68
+ * This callback contract is SDK-specific and is not the open A2A protocol.
57
69
  *
58
70
  * @param adapter - The CloudAdapter used for processing activities and managing conversations.
59
71
  * @param agent - The ActivityHandler containing the agent logic.
@@ -65,21 +77,57 @@ export const createAgentResponseHandler = (
65
77
  agent: ActivityHandler,
66
78
  conversationState: ConversationState
67
79
  ): AgentResponseHandler => {
80
+ const appId = adapter.getClientId() ?? ''
81
+ const anonymousDevelopment = appId.length === 0 && process.env.NODE_ENV !== 'production'
82
+ if (anonymousDevelopment) {
83
+ emitAgentResponseWarning(
84
+ 'The agent-response endpoint is using anonymous authentication outside production. Callback ownership cannot be cryptographically verified; configure a client ID before deployment.'
85
+ )
86
+ }
87
+
68
88
  return async (req: Request, res: WebResponse, params: AgentResponseHandlerParams) => {
89
+ let middlewareError: any
90
+ let nextCalled = false
91
+ await adapter.authorizeRequest(req, res, (err?: any) => {
92
+ nextCalled = true
93
+ middlewareError = err
94
+ })
95
+ if (middlewareError) {
96
+ throw middlewareError
97
+ }
98
+ if (!nextCalled || res.headersSent) {
99
+ return
100
+ }
101
+
69
102
  if (!req.body) {
70
103
  throw ExceptionHelper.generateException(TypeError, Errors.MissingRequestBody)
71
104
  }
72
105
  const incoming = normalizeIncomingActivity(req.body)
73
106
  const activity = Activity.fromObject(incoming)
74
107
 
75
- logger.debug('received response: ', activity)
108
+ logger.debug('received delegated agent response')
76
109
 
77
- const connection = adapter.connectionManager.getDefaultConnection()
78
- const appId = connection?.connectionSettings?.clientId ?? ''
79
-
80
- const myTurnContext = new TurnContext(adapter, activity, CloudAdapter.createIdentity(appId))
110
+ const continuationAudience = getAuthorizedAudience(req) ?? appId
111
+ const myTurnContext = new TurnContext(adapter, activity, CloudAdapter.createIdentity(continuationAudience))
81
112
  const conversationDataAccessor = conversationState.createProperty<ConversationReferenceState>(params.conversationId)
82
- const conversationRefState = await conversationDataAccessor.get(myTurnContext, undefined, { channelId: activity.channelId!, conversationId: params.conversationId })
113
+ const incomingChannelId = activity.channelId!
114
+ const conversationRefState = await conversationDataAccessor.get(myTurnContext, undefined, { channelId: incomingChannelId, conversationId: params.conversationId })
115
+ if (!isDelegatedConversationState(conversationRefState)) {
116
+ res.status(403).send({ 'agent-response-auth-error': 'caller is not authorized for this conversation' })
117
+ return
118
+ }
119
+
120
+ const callerClientId = req.user?.azp ?? req.user?.appid
121
+ const anonymousCaller = anonymousDevelopment && req.user?.name === 'anonymous'
122
+ const delegatedAgentMatches = anonymousCaller || (
123
+ typeof callerClientId === 'string' &&
124
+ callerClientId.length > 0 &&
125
+ callerClientId.toLowerCase() === conversationRefState.expectedAgentClientId.toLowerCase()
126
+ )
127
+ if (!delegatedAgentMatches) {
128
+ res.status(403).send({ 'agent-response-auth-error': 'caller is not authorized for this conversation' })
129
+ return
130
+ }
83
131
 
84
132
  const callback = async (turnContext: TurnContext) => {
85
133
  activity.applyConversationReference(conversationRefState.conversationReference)
@@ -88,7 +136,7 @@ export const createAgentResponseHandler = (
88
136
  let response: unknown
89
137
  let responseContentType: string | undefined
90
138
  if (activity.type === ActivityTypes.EndOfConversation) {
91
- await conversationDataAccessor.delete(turnContext, { channelId: activity.channelId!, conversationId: activity.conversation!.id })
139
+ await conversationState.delete(myTurnContext, { channelId: incomingChannelId, conversationId: params.conversationId })
92
140
 
93
141
  applyActivityToTurnContext(turnContext, activity)
94
142
  await agent.run(turnContext)
@@ -101,6 +149,7 @@ export const createAgentResponseHandler = (
101
149
  } else {
102
150
  response = await turnContext.sendActivity(activity)
103
151
  }
152
+
104
153
  if (responseContentType !== undefined) {
105
154
  res.setHeader('content-type', responseContentType)
106
155
  }
@@ -111,6 +160,18 @@ export const createAgentResponseHandler = (
111
160
  }
112
161
  }
113
162
 
163
+ function isDelegatedConversationState (state: ConversationReferenceState | undefined): state is DelegatedConversationState {
164
+ return typeof state?.conversationReference === 'object' &&
165
+ state.conversationReference !== null &&
166
+ typeof state.expectedAgentClientId === 'string' &&
167
+ state.expectedAgentClientId.trim().length > 0
168
+ }
169
+
170
+ function emitAgentResponseWarning (message: string): void {
171
+ console.warn(`[agents:agent-client] ${message}`)
172
+ logger.warn(message)
173
+ }
174
+
114
175
  const applyActivityToTurnContext = (turnContext: TurnContext, activity: Activity) => {
115
176
  turnContext.activity.channelData = activity.channelData
116
177
  turnContext.activity.code = activity.code
@@ -51,10 +51,7 @@ export function parseValueContinuation (value: unknown): string {
51
51
  }
52
52
 
53
53
  interface ValueAction {
54
- action: {
55
- type: string;
56
- verb: string;
57
- }
54
+ action: AdaptiveCardInvokeAction
58
55
  }
59
56
 
60
57
  /**
@@ -70,7 +67,7 @@ export function parseValueActionExecuteSelector (value: unknown): ValueAction |
70
67
  const actionZodSchema = z.object({
71
68
  type: z.string().min(1),
72
69
  verb: z.string().min(1)
73
- })
70
+ }).passthrough()
74
71
  const actionValueExecuteSelector = z.object({
75
72
  action: actionZodSchema
76
73
  })
@@ -80,10 +77,7 @@ export function parseValueActionExecuteSelector (value: unknown): ValueAction |
80
77
  }
81
78
  const parsedValue = safeParsedValue.data
82
79
  return {
83
- action: {
84
- type: parsedValue.action.type,
85
- verb: parsedValue.action.verb
86
- }
80
+ action: parsedValue.action as unknown as AdaptiveCardInvokeAction
87
81
  }
88
82
  }
89
83
 
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import { Activity, ActivityTypes, ExceptionHelper } from '@microsoft/agents-activity'
7
+ import { debug } from '@microsoft/agents-telemetry'
7
8
  import { AdaptiveCardInvokeResponse, AgentApplication, CardFactory, INVOKE_RESPONSE_KEY, InvokeResponse, MessageFactory, RouteSelector, TurnContext, TurnState } from '../../'
8
9
  import { AdaptiveCardActionExecuteResponseType } from './adaptiveCardActionExecuteResponseType'
9
10
  import { parseAdaptiveCardInvokeAction, parseValueActionExecuteSelector, parseValueDataset, parseValueSearchQuery } from './activityValueParsers'
@@ -16,6 +17,7 @@ export const ACTION_INVOKE_NAME = 'adaptiveCard/action'
16
17
  const ACTION_EXECUTE_TYPE = 'Action.Execute'
17
18
  const DEFAULT_ACTION_SUBMIT_FILTER = 'verb'
18
19
  const SEARCH_INVOKE_NAME = 'application/search'
20
+ const logger = debug('agents:adaptive-cards')
19
21
 
20
22
  enum AdaptiveCardInvokeResponseType {
21
23
  /**
@@ -100,12 +102,30 @@ export class AdaptiveCardsActions<TState extends TurnState> {
100
102
  * @param verb - A string, RegExp, RouteSelector, or an array of these to match the action verb.
101
103
  * @param handler - A function to handle the action execution.
102
104
  * @returns The Teams application instance.
105
+ *
106
+ * @remarks
107
+ * The delivery behavior depends on the action trigger and
108
+ * `adaptiveCardsOptions.actionExecuteResponseType`:
109
+ *
110
+ * | Response type | Automatic trigger | Manual trigger |
111
+ * | --- | --- | --- |
112
+ * | `REPLACE_FOR_INTERACTOR` | User response | User response |
113
+ * | `REPLACE_FOR_ALL` | User response | Shared update |
114
+ * | `NEW_MESSAGE_FOR_ALL` | User response | New message |
115
+ *
116
+ * - **User response** — Return an Adaptive Card invoke response only to the requesting or interacting user.
117
+ * - **Shared update** — Update the shared activity for all users and return an Adaptive Card invoke response to the interacting user.
118
+ * - **New message** — Send a new activity for all users and return a message acknowledgement to the interacting user.
119
+ *
120
+ * Automatic refresh actions always return only an Adaptive Card invoke response.
121
+ * Updating or sending a refreshable card while processing an automatic refresh
122
+ * would cause Teams to invoke the refresh action again.
103
123
  */
104
124
  public actionExecute<TData = Record<string, any>>(
105
125
  verb: string | RegExp | RouteSelector | (string | RegExp | RouteSelector)[],
106
126
  handler: (context: TurnContext, state: TState, data: TData) => Promise<AdaptiveCard | string>
107
127
  ): AgentApplication<TState> {
108
- let actionExecuteResponseType = this._app.options.adaptiveCardsOptions?.actionExecuteResponseType ?? AdaptiveCardActionExecuteResponseType.REPLACE_FOR_INTERACTOR;
128
+ const actionExecuteResponseType = this._app.options.adaptiveCardsOptions?.actionExecuteResponseType ?? AdaptiveCardActionExecuteResponseType.REPLACE_FOR_INTERACTOR;
109
129
  (Array.isArray(verb) ? verb : [verb]).forEach((v) => {
110
130
  const selector = createActionExecuteSelector(v)
111
131
  this._app.addRoute(
@@ -113,6 +133,7 @@ export class AdaptiveCardsActions<TState extends TurnState> {
113
133
  async (context, state) => {
114
134
  const a = context?.activity
115
135
  const invokeAction = parseValueActionExecuteSelector(a.value)
136
+ const isAutomaticRefresh = (a.value as { trigger?: string } | undefined)?.trigger === 'automatic'
116
137
  if (
117
138
  a?.type !== ActivityTypes.Invoke ||
118
139
  a?.name !== ACTION_INVOKE_NAME ||
@@ -121,13 +142,11 @@ export class AdaptiveCardsActions<TState extends TurnState> {
121
142
  throw ExceptionHelper.generateException(Error, Errors.UnexpectedActionExecute, undefined, { activityType: invokeAction?.action.type ?? 'undefined' })
122
143
  }
123
144
 
124
- if (invokeAction.action.verb !== v) {
125
- // TODO: add logger to this class
126
- console.log(`AdaptiveCards.actionExecute() triggered for verb: ${invokeAction.action.verb} does not match expected verb: ${v}`)
145
+ if (typeof v === 'string' && invokeAction.action.verb !== v) {
146
+ logger.warn(`AdaptiveCards.actionExecute() triggered for verb: ${invokeAction.action.verb} does not match expected verb: ${v}`)
127
147
  }
128
148
 
129
- // TODO: review any, and check verb
130
- const result = await handler(context, state, ((a.value as any).action as TData) ?? {} as TData)
149
+ const result = await handler(context, state, (invokeAction.action as unknown as TData) ?? {} as TData)
131
150
  if (!context.turnState.get(INVOKE_RESPONSE_KEY)) {
132
151
  let response: AdaptiveCardInvokeResponse
133
152
  if (typeof result === 'string') {
@@ -138,20 +157,14 @@ export class AdaptiveCardsActions<TState extends TurnState> {
138
157
  }
139
158
  await sendInvokeResponse(context, response)
140
159
  } else {
141
- if (
142
- result.refresh &&
143
- actionExecuteResponseType !== AdaptiveCardActionExecuteResponseType.NEW_MESSAGE_FOR_ALL
144
- ) {
145
- actionExecuteResponseType = AdaptiveCardActionExecuteResponseType.REPLACE_FOR_ALL
146
- }
147
-
148
- const activity = MessageFactory.attachment(CardFactory.adaptiveCard(result))
149
160
  response = {
150
161
  statusCode: 200,
151
162
  type: AdaptiveCardInvokeResponseType.ADAPTIVE,
152
163
  value: result
153
164
  }
154
- if (
165
+ if (isAutomaticRefresh) {
166
+ await sendInvokeResponse(context, response)
167
+ } else if (
155
168
  actionExecuteResponseType === AdaptiveCardActionExecuteResponseType.NEW_MESSAGE_FOR_ALL
156
169
  ) {
157
170
  await sendInvokeResponse(context, {
@@ -159,10 +172,11 @@ export class AdaptiveCardsActions<TState extends TurnState> {
159
172
  type: AdaptiveCardInvokeResponseType.MESSAGE,
160
173
  value: 'Your response was sent to the app' as any
161
174
  })
162
- await context.sendActivity(activity)
175
+ await context.sendActivity(MessageFactory.attachment(CardFactory.adaptiveCard(result)))
163
176
  } else if (
164
177
  actionExecuteResponseType === AdaptiveCardActionExecuteResponseType.REPLACE_FOR_ALL
165
178
  ) {
179
+ const activity = MessageFactory.attachment(CardFactory.adaptiveCard(result))
166
180
  activity.id = context.activity.replyToId
167
181
  await context.updateActivity(activity)
168
182
  await sendInvokeResponse(context, response)
@@ -270,12 +284,15 @@ function createActionExecuteSelector (verb: string | RegExp | RouteSelector): Ro
270
284
  } else if (verb instanceof RegExp) {
271
285
  return (context: TurnContext) => {
272
286
  const a = context?.activity
273
- const valueAction = parseValueActionExecuteSelector(a.value)
274
287
  const isInvoke =
275
288
  a?.type === ActivityTypes.Invoke &&
276
- a?.name === ACTION_INVOKE_NAME &&
277
- valueAction?.action?.type === ACTION_EXECUTE_TYPE
278
- if (isInvoke && typeof valueAction.action.verb === 'string') {
289
+ a?.name === ACTION_INVOKE_NAME
290
+ if (!isInvoke) {
291
+ return Promise.resolve(false)
292
+ }
293
+
294
+ const valueAction = tryParseActionExecuteSelectorValue(a.value)
295
+ if (valueAction?.action?.type === ACTION_EXECUTE_TYPE && typeof valueAction.action.verb === 'string') {
279
296
  return Promise.resolve(verb.test(valueAction.action.verb))
280
297
  } else {
281
298
  return Promise.resolve(false)
@@ -284,12 +301,15 @@ function createActionExecuteSelector (verb: string | RegExp | RouteSelector): Ro
284
301
  } else {
285
302
  return (context: TurnContext) => {
286
303
  const a = context?.activity
287
- const valueAction = parseValueActionExecuteSelector(a.value)
288
304
  const isInvoke =
289
305
  a?.type === ActivityTypes.Invoke &&
290
- a?.name === ACTION_INVOKE_NAME &&
291
- valueAction?.action?.type === ACTION_EXECUTE_TYPE
292
- if (isInvoke && valueAction.action?.verb === verb) {
306
+ a?.name === ACTION_INVOKE_NAME
307
+ if (!isInvoke) {
308
+ return Promise.resolve(false)
309
+ }
310
+
311
+ const valueAction = tryParseActionExecuteSelectorValue(a.value)
312
+ if (valueAction?.action?.type === ACTION_EXECUTE_TYPE && valueAction.action?.verb === verb) {
293
313
  return Promise.resolve(true)
294
314
  } else {
295
315
  return Promise.resolve(false)
@@ -298,6 +318,14 @@ function createActionExecuteSelector (verb: string | RegExp | RouteSelector): Ro
298
318
  }
299
319
  }
300
320
 
321
+ function tryParseActionExecuteSelectorValue (value: unknown): ReturnType<typeof parseValueActionExecuteSelector> {
322
+ try {
323
+ return parseValueActionExecuteSelector(value)
324
+ } catch {
325
+ return undefined
326
+ }
327
+ }
328
+
301
329
  function createActionSubmitSelector (verb: string | RegExp | RouteSelector, filter: string): RouteSelector {
302
330
  if (typeof verb === 'function') {
303
331
  return verb
@@ -678,7 +678,7 @@ export class AgentApplication<TState extends TurnState> {
678
678
 
679
679
  if (!authorized) {
680
680
  const managed = trace(AgentApplicationTraceDefinitions.run)
681
- managed.record({ authorized, activity: context.activity })
681
+ managed.record({ authorized, activity: Activity.fromObject(context.activity) })
682
682
  managed.end()
683
683
  // We don't log a message here because it is handled by the authorization manager and could cause confusion during mid sign-in operations.
684
684
  return false
@@ -719,84 +719,90 @@ export class AgentApplication<TState extends TurnState> {
719
719
  * Executes the turn processing logic for the given context, including routing and handler execution.
720
720
  */
721
721
  private async runTurn (context: TurnContext): Promise<boolean> {
722
- return trace(AgentApplicationTraceDefinitions.run, async ({ record }) => {
723
- record({ authorized: true, activity: context.activity })
722
+ const managed = trace(AgentApplicationTraceDefinitions.run)
723
+ managed.record({ authorized: true, activity: Activity.fromObject(context.activity) })
724
724
 
725
- try {
726
- if (this._authorizationManager.handlers.length > 0) {
727
- context.turnState.set(AgentApplication.UserAuthorizationKey, this._authorization)
728
- }
725
+ try {
726
+ if (this._authorizationManager.handlers.length > 0) {
727
+ context.turnState.set(AgentApplication.UserAuthorizationKey, this._authorization)
728
+ }
729
729
 
730
- const connections = this._options.connections ?? this._adapter?.connectionManager
731
- if (connections) {
732
- context.turnState.set(AgentApplication.ConnectionsKey, connections)
733
- }
730
+ const connections = this._options.connections ?? this._adapter?.connectionManager
731
+ if (connections) {
732
+ context.turnState.set(AgentApplication.ConnectionsKey, connections)
733
+ }
734
734
 
735
- if (this._options.startTypingTimer) {
736
- this.startTypingTimer(context)
737
- }
735
+ if (this._options.startTypingTimer) {
736
+ this.startTypingTimer(context)
737
+ }
738
738
 
739
- if (this._options.removeRecipientMention && context.activity.type === ActivityTypes.Message) {
740
- context.activity.removeRecipientMention()
741
- }
739
+ if (this._options.removeRecipientMention && context.activity.type === ActivityTypes.Message) {
740
+ context.activity.removeRecipientMention()
741
+ }
742
742
 
743
- if (this._options.normalizeMentions && context.activity.type === ActivityTypes.Message) {
744
- context.activity.normalizeMentions()
745
- }
743
+ if (this._options.normalizeMentions && context.activity.type === ActivityTypes.Message) {
744
+ context.activity.normalizeMentions()
745
+ }
746
746
 
747
- const { storage, turnStateFactory } = this._options
748
- const state = turnStateFactory()
749
- await state.load(context, storage)
747
+ const { storage, turnStateFactory } = this._options
748
+ const state = turnStateFactory()
749
+ await state.load(context, storage)
750
750
 
751
- const route = await this.getRoute(context)
751
+ const route = await this.getRoute(context)
752
752
 
753
- record({ routeMatched: route !== undefined })
753
+ managed.record({ routeMatched: route !== undefined })
754
754
 
755
- if (!route) {
756
- logger.debug('No matching route found for activity:', context.activity)
757
- return false
758
- }
755
+ if (!route) {
756
+ logger.debug('No matching route found for activity:', context.activity)
757
+ return false
758
+ }
759
759
 
760
- const fileDownloaders = this._options.fileDownloaders
761
- if (Array.isArray(fileDownloaders) && fileDownloaders.length > 0) {
762
- await trace(AgentApplicationTraceDefinitions.downloadFiles, async ({ record }) => {
763
- record({ attachmentsCount: context.activity.attachments?.length })
764
- for (let i = 0; i < fileDownloaders.length; i++) {
765
- await fileDownloaders[i].downloadAndStoreFiles(context, state)
766
- }
767
- })
768
- }
760
+ const fileDownloaders = this._options.fileDownloaders
761
+ if (Array.isArray(fileDownloaders) && fileDownloaders.length > 0) {
762
+ await managed.child(AgentApplicationTraceDefinitions.downloadFiles, async ({ record }) => {
763
+ record({ attachmentsCount: context.activity.attachments?.length })
764
+ for (let i = 0; i < fileDownloaders.length; i++) {
765
+ await fileDownloaders[i].downloadAndStoreFiles(context, state)
766
+ }
767
+ })
768
+ }
769
769
 
770
- if (this._beforeTurn.length > 0) {
771
- await trace(AgentApplicationTraceDefinitions.beforeTurn, async () => {
772
- if (!(await this.callEventHandlers(context, state, this._beforeTurn))) {
773
- await state.save(context, storage)
774
- return false
775
- }
776
- })
777
- }
770
+ if (this._beforeTurn.length > 0) {
771
+ const continueTurn = await managed.child(AgentApplicationTraceDefinitions.beforeTurn, async () => {
772
+ if (!(await this.callEventHandlers(context, state, this._beforeTurn))) {
773
+ await state.save(context, storage)
774
+ return false
775
+ }
778
776
 
779
- await trace(AgentApplicationTraceDefinitions.routeHandler, async ({ record }) => {
780
- record({ isInvoke: route.isInvokeRoute, isAgentic: route.isAgenticRoute })
781
- await route.handler(context, state)
777
+ return true
782
778
  })
783
779
 
784
- if (this._afterTurn.length > 0) {
785
- await trace(AgentApplicationTraceDefinitions.afterTurn, async () => {
786
- if (await this.callEventHandlers(context, state, this._afterTurn)) {
787
- await state.save(context, storage)
788
- }
789
- })
780
+ if (!continueTurn) {
781
+ return false
790
782
  }
783
+ }
791
784
 
792
- return true
793
- } catch (err: any) {
794
- logger.error(err)
795
- throw err
796
- } finally {
797
- this.stopTypingTimer(context)
785
+ await managed.child(AgentApplicationTraceDefinitions.routeHandler, async ({ record }) => {
786
+ record({ isInvoke: route.isInvokeRoute, isAgentic: route.isAgenticRoute })
787
+ await route.handler(context, state)
788
+ })
789
+
790
+ if (this._afterTurn.length > 0) {
791
+ await managed.child(AgentApplicationTraceDefinitions.afterTurn, async () => {
792
+ if (await this.callEventHandlers(context, state, this._afterTurn)) {
793
+ await state.save(context, storage)
794
+ }
795
+ })
798
796
  }
799
- })
797
+
798
+ return true
799
+ } catch (err: any) {
800
+ logger.error(err)
801
+ throw managed.fail(err)
802
+ } finally {
803
+ this.stopTypingTimer(context)
804
+ managed.end()
805
+ }
800
806
  }
801
807
 
802
808
  /**
@@ -1093,22 +1099,77 @@ export class AgentApplication<TState extends TurnState> {
1093
1099
  handler: (context: TurnContext) => Promise<any>
1094
1100
  ) {
1095
1101
  const activity = Activity.fromObject(context.activity)
1096
- this.continueConversationAsync(context.identity, activity.getConversationReference(), async (ctx) => {
1097
- try {
1098
- Object.assign(ctx.activity, activity)
1099
- await handler(ctx)
1100
- } catch (err) {
1101
- if (this.adapter.onTurnError && err instanceof Error) {
1102
- await this.adapter.onTurnError(ctx, err)
1103
- } else {
1104
- throw err
1102
+ const adapter = context.adapter
1103
+ const identity = context.identity
1104
+ const createErrorContext = () => new TurnContext(adapter, Activity.fromObject(activity), identity)
1105
+
1106
+ try {
1107
+ const reference = activity.getConversationReference()
1108
+ this.continueConversationAsync(identity, reference, async (ctx) => {
1109
+ try {
1110
+ Object.assign(ctx.activity, activity)
1111
+ await handler(ctx)
1112
+ } catch (err) {
1113
+ await this.handleLongRunningCallError(ctx, activity, err)
1105
1114
  }
1106
- }
1107
- }).catch(err => {
1108
- logger.error(`Unhandled error in long-running call for activity '${activity.type}' (id: ${activity.id}):`, err)
1115
+ }).catch(async err => {
1116
+ await this.handleLongRunningCallError(createErrorContext(), activity, err)
1117
+ })
1118
+ } catch (err) {
1119
+ this.handleLongRunningCallError(createErrorContext(), activity, err).catch(handlerErr => {
1120
+ logger.error(`Unhandled error in long-running error handler for activity '${activity.type}' (id: ${activity.id}):`, this.normalizeLongRunningCallError(handlerErr))
1121
+ })
1122
+ }
1123
+ }
1124
+
1125
+ private async handleLongRunningCallError (
1126
+ context: TurnContext,
1127
+ activity: Activity,
1128
+ err: unknown
1129
+ ): Promise<void> {
1130
+ const error = this.normalizeLongRunningCallError(err)
1131
+
1132
+ try {
1133
+ await context.adapter.onTurnError(context, error)
1134
+ } catch (handlerErr) {
1135
+ logger.error(`Unhandled error in long-running error handler for activity '${activity.type}' (id: ${activity.id}):`, this.normalizeLongRunningCallError(handlerErr))
1136
+ }
1137
+ }
1138
+
1139
+ private normalizeLongRunningCallError (err: unknown): Error {
1140
+ if (err instanceof Error) {
1141
+ return err
1142
+ }
1143
+
1144
+ return ExceptionHelper.generateException(Error, Errors.UnknownErrorType, undefined, {
1145
+ errorMessage: this.stringifyThrownValue(err)
1109
1146
  })
1110
1147
  }
1111
1148
 
1149
+ private stringifyThrownValue (value: unknown): string {
1150
+ if (typeof value === 'string') {
1151
+ return value
1152
+ }
1153
+
1154
+ if (value === undefined) {
1155
+ return 'undefined'
1156
+ }
1157
+
1158
+ if (value === null) {
1159
+ return 'null'
1160
+ }
1161
+
1162
+ if (typeof value === 'object') {
1163
+ try {
1164
+ return JSON.stringify(value) ?? Object.prototype.toString.call(value)
1165
+ } catch {
1166
+ return Object.prototype.toString.call(value)
1167
+ }
1168
+ }
1169
+
1170
+ return String(value)
1171
+ }
1172
+
1112
1173
  /**
1113
1174
  * Creates a selector function for activity types.
1114
1175
  *
@@ -11,6 +11,8 @@ import { Attachment } from '@microsoft/agents-activity'
11
11
  import { AuthProvider } from '../auth/authProvider'
12
12
  import { debug } from '@microsoft/agents-telemetry'
13
13
  import { loadAuthConfigFromEnv, MsalTokenProvider } from '../auth'
14
+ import { createOutboundHostValidator, OutboundUrlPolicy } from '../outboundHostValidator'
15
+ import { Connections } from '../auth/connections'
14
16
 
15
17
  const logger = debug('agents:attachmentDownloader')
16
18
 
@@ -27,16 +29,19 @@ const logger = debug('agents:attachmentDownloader')
27
29
  export class AttachmentDownloader<TState extends TurnState = TurnState> implements InputFileDownloader<TState> {
28
30
  private _httpClient: HttpClient
29
31
  private _stateKey: string
32
+ private readonly _hostValidator: OutboundUrlPolicy
30
33
 
31
34
  /**
32
35
  * Creates an instance of AttachmentDownloader.
33
36
  * This class is responsible for downloading input files from attachments.
34
37
  *
35
38
  * @param stateKey The key to store files in state. Defaults to 'inputFiles'.
39
+ * @param outboundHostValidator Optional shared policy used to validate outbound attachment URLs.
36
40
  */
37
- public constructor (stateKey: string = 'inputFiles') {
41
+ public constructor (stateKey: string = 'inputFiles', outboundHostValidator?: OutboundUrlPolicy) {
38
42
  this._httpClient = new HttpClient()
39
43
  this._stateKey = stateKey
44
+ this._hostValidator = outboundHostValidator ?? createOutboundHostValidator()
40
45
  }
41
46
 
42
47
  /**
@@ -52,10 +57,18 @@ export class AttachmentDownloader<TState extends TurnState = TurnState> implemen
52
57
  return Promise.resolve([])
53
58
  }
54
59
 
55
- // TODO: from adapter
56
- const authProvider: AuthProvider = new MsalTokenProvider()
57
-
58
- const accessToken = await authProvider.getAccessToken(loadAuthConfigFromEnv(), 'https://api.botframework.com')
60
+ let accessToken = ''
61
+ const connectionManager = (context.adapter as { connectionManager?: Connections })?.connectionManager
62
+ const identity = context.identity
63
+ if (connectionManager && identity) {
64
+ const scope = identity.azp ?? identity.appid ?? 'https://api.botframework.com'
65
+ logger.debug(`Using adapter connection manager token provider for attachment downloads with scope ${scope}`)
66
+ accessToken = await connectionManager.getTokenProviderFromActivity(identity, context.activity).getAccessToken(scope)
67
+ } else {
68
+ logger.debug('Using MsalTokenProvider fallback for attachment downloads')
69
+ const authProvider: AuthProvider = new MsalTokenProvider()
70
+ accessToken = await authProvider.getAccessToken(loadAuthConfigFromEnv(), 'https://api.botframework.com')
71
+ }
59
72
 
60
73
  const files: InputFile[] = []
61
74
  for (const attachment of attachments) {
@@ -90,6 +103,13 @@ export class AttachmentDownloader<TState extends TurnState = TurnState> implemen
90
103
  if (accessToken.length > 0) {
91
104
  headers.Authorization = `Bearer ${accessToken}`
92
105
  }
106
+ if (!this._hostValidator.isAllowed(attachment.contentUrl)) {
107
+ let host = '[invalid-url]'
108
+ try { host = new URL(attachment.contentUrl).hostname } catch { /* invalid */ }
109
+ logger.warn(`Attachment contentUrl host is not in the configured allowed hosts. Host='${host}'`)
110
+ return undefined
111
+ }
112
+
93
113
  const response = await this._httpClient.get(attachment.contentUrl, {
94
114
  headers,
95
115
  responseType: 'arraybuffer'
@@ -22,7 +22,7 @@ const logger = debug('agents:authorization:agentic')
22
22
  * where `{handlerId}` is the handler's unique identifier and `{propertyName}` matches the property name.
23
23
  *
24
24
  * @example
25
- * ```env
25
+ * ```
26
26
  * # For a handler with id "myAuth":
27
27
  * AgentApplication__UserAuthorization__handlers__myAuth__settings__type=AgenticUserAuthorization
28
28
  * AgentApplication__UserAuthorization__handlers__myAuth__settings__scopes=api://scope1 api://scope2