@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.
- package/README.md +64 -2
- package/dist/package.json +3 -3
- package/dist/src/agent-client/agentClient.d.ts +18 -6
- package/dist/src/agent-client/agentClient.js +34 -18
- package/dist/src/agent-client/agentClient.js.map +1 -1
- package/dist/src/agent-client/agentResponseHandler.d.ts +14 -9
- package/dist/src/agent-client/agentResponseHandler.js +10 -5
- package/dist/src/agent-client/agentResponseHandler.js.map +1 -1
- package/dist/src/agent-client/createAgentResponseHandler.d.ts +7 -2
- package/dist/src/agent-client/createAgentResponseHandler.js +55 -8
- package/dist/src/agent-client/createAgentResponseHandler.js.map +1 -1
- package/dist/src/app/adaptiveCards/activityValueParsers.d.ts +1 -4
- package/dist/src/app/adaptiveCards/activityValueParsers.js +2 -5
- package/dist/src/app/adaptiveCards/activityValueParsers.js.map +1 -1
- package/dist/src/app/adaptiveCards/adaptiveCardsActions.d.ts +18 -0
- package/dist/src/app/adaptiveCards/adaptiveCardsActions.js +53 -23
- package/dist/src/app/adaptiveCards/adaptiveCardsActions.js.map +1 -1
- package/dist/src/app/agentApplication.d.ts +3 -0
- package/dist/src/app/agentApplication.js +126 -78
- package/dist/src/app/agentApplication.js.map +1 -1
- package/dist/src/app/attachmentDownloader.d.ts +4 -1
- package/dist/src/app/attachmentDownloader.js +27 -5
- package/dist/src/app/attachmentDownloader.js.map +1 -1
- package/dist/src/app/auth/handlers/agenticAuthorization.d.ts +1 -1
- package/dist/src/app/auth/handlers/azureBotAuthorization.d.ts +1 -1
- package/dist/src/app/auth/types.d.ts +3 -4
- package/dist/src/app/index.d.ts +2 -0
- package/dist/src/app/index.js.map +1 -1
- package/dist/src/app/streaming/streamingResponse.js +2 -9
- package/dist/src/app/streaming/streamingResponse.js.map +1 -1
- package/dist/src/app/teamsAttachmentDownloader.d.ts +3 -1
- package/dist/src/app/teamsAttachmentDownloader.js +12 -1
- package/dist/src/app/teamsAttachmentDownloader.js.map +1 -1
- package/dist/src/auth/authConfiguration.js +12 -0
- package/dist/src/auth/authConfiguration.js.map +1 -1
- package/dist/src/auth/index.d.ts +2 -1
- package/dist/src/auth/index.js +2 -1
- package/dist/src/auth/index.js.map +1 -1
- package/dist/src/auth/jwt-middleware.d.ts +6 -0
- package/dist/src/auth/jwt-middleware.js +263 -13
- package/dist/src/auth/jwt-middleware.js.map +1 -1
- package/dist/src/auth/msal/msalConnectionSettings.d.ts +8 -0
- package/dist/src/auth/msal/msalHttpRetryHandlerHelper.d.ts +34 -0
- package/dist/src/auth/msal/msalHttpRetryHandlerHelper.js +126 -0
- package/dist/src/auth/msal/msalHttpRetryHandlerHelper.js.map +1 -0
- package/dist/src/auth/msal/msalTokenProvider.d.ts +1 -0
- package/dist/src/auth/msal/msalTokenProvider.js +23 -14
- package/dist/src/auth/msal/msalTokenProvider.js.map +1 -1
- package/dist/src/auth/settings.d.ts +18 -0
- package/dist/src/auth/settings.js +8 -2
- package/dist/src/auth/settings.js.map +1 -1
- package/dist/src/cloudAdapter.d.ts +29 -2
- package/dist/src/cloudAdapter.js +44 -2
- package/dist/src/cloudAdapter.js.map +1 -1
- package/dist/src/errorHelper.js +137 -101
- package/dist/src/errorHelper.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/invoke/adaptiveCardInvokeValue.d.ts +5 -0
- package/dist/src/oauth/userTokenClient.d.ts +0 -1
- package/dist/src/oauth/userTokenClient.js +0 -1
- package/dist/src/oauth/userTokenClient.js.map +1 -1
- package/dist/src/observability/traces.js +3 -2
- package/dist/src/observability/traces.js.map +1 -1
- package/dist/src/outboundHostValidator.d.ts +47 -0
- package/dist/src/outboundHostValidator.js +149 -0
- package/dist/src/outboundHostValidator.js.map +1 -0
- package/dist/src/state/agentState.d.ts +5 -0
- package/dist/src/state/agentState.js +16 -3
- package/dist/src/state/agentState.js.map +1 -1
- package/package.json +3 -3
- package/src/agent-client/agentClient.ts +46 -23
- package/src/agent-client/agentResponseHandler.ts +14 -9
- package/src/agent-client/createAgentResponseHandler.ts +70 -9
- package/src/app/adaptiveCards/activityValueParsers.ts +3 -9
- package/src/app/adaptiveCards/adaptiveCardsActions.ts +52 -24
- package/src/app/agentApplication.ts +135 -74
- package/src/app/attachmentDownloader.ts +25 -5
- package/src/app/auth/handlers/agenticAuthorization.ts +1 -1
- package/src/app/auth/handlers/azureBotAuthorization.ts +1 -1
- package/src/app/auth/types.ts +3 -3
- package/src/app/index.ts +8 -0
- package/src/app/streaming/streamingResponse.ts +2 -8
- package/src/app/teamsAttachmentDownloader.ts +11 -1
- package/src/auth/authConfiguration.ts +12 -0
- package/src/auth/index.ts +2 -1
- package/src/auth/jwt-middleware.ts +298 -14
- package/src/auth/msal/msalConnectionSettings.ts +10 -0
- package/src/auth/msal/msalHttpRetryHandlerHelper.ts +161 -0
- package/src/auth/msal/msalTokenProvider.ts +32 -14
- package/src/auth/settings.ts +18 -3
- package/src/cloudAdapter.ts +56 -3
- package/src/errorHelper.ts +155 -114
- package/src/index.ts +1 -0
- package/src/invoke/adaptiveCardInvokeValue.ts +5 -0
- package/src/oauth/userTokenClient.ts +0 -1
- package/src/observability/traces.ts +2 -1
- package/src/outboundHostValidator.ts +172 -0
- 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
|
|
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
|
|
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
|
|
108
|
+
logger.debug('received delegated agent response')
|
|
76
109
|
|
|
77
|
-
const
|
|
78
|
-
const
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
278
|
-
|
|
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
|
-
|
|
292
|
-
|
|
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
|
-
|
|
723
|
-
|
|
722
|
+
const managed = trace(AgentApplicationTraceDefinitions.run)
|
|
723
|
+
managed.record({ authorized: true, activity: Activity.fromObject(context.activity) })
|
|
724
724
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
725
|
+
try {
|
|
726
|
+
if (this._authorizationManager.handlers.length > 0) {
|
|
727
|
+
context.turnState.set(AgentApplication.UserAuthorizationKey, this._authorization)
|
|
728
|
+
}
|
|
729
729
|
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
730
|
+
const connections = this._options.connections ?? this._adapter?.connectionManager
|
|
731
|
+
if (connections) {
|
|
732
|
+
context.turnState.set(AgentApplication.ConnectionsKey, connections)
|
|
733
|
+
}
|
|
734
734
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
735
|
+
if (this._options.startTypingTimer) {
|
|
736
|
+
this.startTypingTimer(context)
|
|
737
|
+
}
|
|
738
738
|
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
739
|
+
if (this._options.removeRecipientMention && context.activity.type === ActivityTypes.Message) {
|
|
740
|
+
context.activity.removeRecipientMention()
|
|
741
|
+
}
|
|
742
742
|
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
743
|
+
if (this._options.normalizeMentions && context.activity.type === ActivityTypes.Message) {
|
|
744
|
+
context.activity.normalizeMentions()
|
|
745
|
+
}
|
|
746
746
|
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
747
|
+
const { storage, turnStateFactory } = this._options
|
|
748
|
+
const state = turnStateFactory()
|
|
749
|
+
await state.load(context, storage)
|
|
750
750
|
|
|
751
|
-
|
|
751
|
+
const route = await this.getRoute(context)
|
|
752
752
|
|
|
753
|
-
|
|
753
|
+
managed.record({ routeMatched: route !== undefined })
|
|
754
754
|
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
755
|
+
if (!route) {
|
|
756
|
+
logger.debug('No matching route found for activity:', context.activity)
|
|
757
|
+
return false
|
|
758
|
+
}
|
|
759
759
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
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
|
-
|
|
780
|
-
record({ isInvoke: route.isInvokeRoute, isAgentic: route.isAgenticRoute })
|
|
781
|
-
await route.handler(context, state)
|
|
777
|
+
return true
|
|
782
778
|
})
|
|
783
779
|
|
|
784
|
-
if (
|
|
785
|
-
|
|
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
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
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
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
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
|
-
|
|
1108
|
-
|
|
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
|
-
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
* ```
|
|
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
|