@microsoft/agents-hosting 1.7.2 → 1.8.0-beta.36.gb033f0abc1
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 +140 -81
- 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.d.ts +56 -0
- package/dist/src/app/streaming/streamingResponse.js +250 -26
- 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.d.ts +6 -0
- package/dist/src/observability/traces.js +20 -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 +149 -77
- 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 +276 -22
- 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 +19 -1
- package/src/outboundHostValidator.ts +172 -0
- package/src/state/agentState.ts +24 -6
|
@@ -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
|
|
@@ -48,7 +48,7 @@ interface AzureBotActiveHandler extends ActiveAuthorizationHandler {
|
|
|
48
48
|
* where `{handlerId}` is the handler's unique identifier and `{propertyName}` matches the property name.
|
|
49
49
|
*
|
|
50
50
|
* @example
|
|
51
|
-
* ```
|
|
51
|
+
* ```
|
|
52
52
|
* # For a handler with id "myAuth":
|
|
53
53
|
* AgentApplication__UserAuthorization__handlers__myAuth__settings__azureBotOAuthConnectionName=MyConnection
|
|
54
54
|
* AgentApplication__UserAuthorization__handlers__myAuth__settings__oboScopes=api://scope1 api://scope2
|
package/src/app/auth/types.ts
CHANGED
|
@@ -126,7 +126,7 @@ export interface AuthorizationHandlerTokenOptions {
|
|
|
126
126
|
* where `{handlerId}` is the handler's unique identifier and `{propertyName}` matches the property name.
|
|
127
127
|
*
|
|
128
128
|
* @example
|
|
129
|
-
* ```
|
|
129
|
+
* ```
|
|
130
130
|
* # For a handler with id "myAuth":
|
|
131
131
|
* AgentApplication__UserAuthorization__handlers__myAuth__settings__azureBotOAuthConnectionName=MyConnection
|
|
132
132
|
* AgentApplication__UserAuthorization__handlers__myAuth__settings__oboScopes=api://scope1 api://scope2
|
|
@@ -170,7 +170,7 @@ export interface AzureBotAuthorizationOptionsLegacy {
|
|
|
170
170
|
* @deprecated
|
|
171
171
|
* Messages configuration for the AzureBotAuthorization handler.
|
|
172
172
|
*/
|
|
173
|
-
interface AzureBotAuthorizationOptionsMessages {
|
|
173
|
+
export interface AzureBotAuthorizationOptionsMessages {
|
|
174
174
|
/**
|
|
175
175
|
* @deprecated Use `invalidSignInRetryMessage` instead.
|
|
176
176
|
* Message displayed when an invalid code is entered.
|
|
@@ -198,7 +198,7 @@ interface AzureBotAuthorizationOptionsMessages {
|
|
|
198
198
|
* @deprecated
|
|
199
199
|
* Settings for on-behalf-of token acquisition.
|
|
200
200
|
*/
|
|
201
|
-
interface AzureBotAuthorizationOptionsOBO {
|
|
201
|
+
export interface AzureBotAuthorizationOptionsOBO {
|
|
202
202
|
/**
|
|
203
203
|
* @deprecated Use `oboConnectionName` instead.
|
|
204
204
|
* Connection name to use for on-behalf-of token acquisition.
|
package/src/app/index.ts
CHANGED
|
@@ -3,6 +3,14 @@ export * from './agentApplicationBuilder'
|
|
|
3
3
|
export * from './agentApplicationOptions'
|
|
4
4
|
export * from './appRoute'
|
|
5
5
|
export { Authorization } from './auth/authorization'
|
|
6
|
+
export type { AgenticAuthorizationOptions, AzureBotAuthorizationOptions } from './auth/handlers'
|
|
7
|
+
export type {
|
|
8
|
+
AuthorizationOptions,
|
|
9
|
+
AuthorizationHandlerTokenOptions,
|
|
10
|
+
AzureBotAuthorizationOptionsLegacy,
|
|
11
|
+
AzureBotAuthorizationOptionsMessages,
|
|
12
|
+
AzureBotAuthorizationOptionsOBO
|
|
13
|
+
} from './auth/types'
|
|
6
14
|
export * from './routeHandler'
|
|
7
15
|
export * from './routeList'
|
|
8
16
|
export * from './routeRank'
|
|
@@ -11,6 +11,10 @@ import { debug } from '@microsoft/agents-telemetry'
|
|
|
11
11
|
import { Errors } from '../../errorHelper'
|
|
12
12
|
|
|
13
13
|
const logger = debug('agents:streamingResponse')
|
|
14
|
+
const teamsStreamTimedOut = 'Content stream finished due to exceeded streaming time.'
|
|
15
|
+
const defaultStreamingTakingTooLongMessage = 'The response is taking longer than expected. Please wait while we continue to generate the response.'
|
|
16
|
+
const m365StreamingTimeoutMs = 105000
|
|
17
|
+
const m365WorkingNoticeIntervalMs = 35000
|
|
14
18
|
|
|
15
19
|
/**
|
|
16
20
|
* Results for streaming response operations.
|
|
@@ -57,6 +61,13 @@ export class StreamingResponse {
|
|
|
57
61
|
private _finalMessage?: Activity
|
|
58
62
|
private _canceled = false
|
|
59
63
|
private _userCanceled = false
|
|
64
|
+
private _streamTimedOut = false
|
|
65
|
+
private _streamTimeoutNotificationSent = false
|
|
66
|
+
private _lastInformationalMessageSent = ''
|
|
67
|
+
private _streamStartedAt?: number
|
|
68
|
+
private _keepAliveTimer?: ReturnType<typeof setTimeout>
|
|
69
|
+
private _streamTimeoutTimer?: ReturnType<typeof setTimeout>
|
|
70
|
+
private _streamingTakingTooLongMessage = defaultStreamingTakingTooLongMessage
|
|
60
71
|
|
|
61
72
|
// Queue for outgoing activities
|
|
62
73
|
private _queue: Array<() => Activity> = []
|
|
@@ -127,12 +138,30 @@ export class StreamingResponse {
|
|
|
127
138
|
return this._isStreamingChannel
|
|
128
139
|
}
|
|
129
140
|
|
|
141
|
+
/**
|
|
142
|
+
* Gets the message sent when streaming takes longer than the channel allows.
|
|
143
|
+
*/
|
|
144
|
+
public get streamingTakingTooLongMessage (): string {
|
|
145
|
+
return this._streamingTakingTooLongMessage
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Sets the message sent when streaming takes longer than the channel allows.
|
|
150
|
+
*/
|
|
151
|
+
public set streamingTakingTooLongMessage (message: string) {
|
|
152
|
+
this._streamingTakingTooLongMessage = message
|
|
153
|
+
}
|
|
154
|
+
|
|
130
155
|
/**
|
|
131
156
|
* Queues an informative update to be sent to the client.
|
|
132
157
|
*
|
|
133
158
|
* @param {string} text Text of the update to send.
|
|
134
159
|
*/
|
|
135
160
|
public queueInformativeUpdate (text: string): void {
|
|
161
|
+
if (this.isM365Copilot()) {
|
|
162
|
+
this._lastInformationalMessageSent = text
|
|
163
|
+
}
|
|
164
|
+
|
|
136
165
|
if (!this.isStreamingChannel || !text.trim() || this._canceled) {
|
|
137
166
|
return
|
|
138
167
|
}
|
|
@@ -203,9 +232,23 @@ export class StreamingResponse {
|
|
|
203
232
|
|
|
204
233
|
// Queue final message
|
|
205
234
|
this._ended = true
|
|
235
|
+
this.clearStreamTimers()
|
|
206
236
|
|
|
207
237
|
if (!this.isStreamingChannel) {
|
|
208
|
-
|
|
238
|
+
// A failed streaming send may still be updating the timed-out activity.
|
|
239
|
+
// Preserve ordering so that the completed response is always the last update.
|
|
240
|
+
await this.waitForQueue()
|
|
241
|
+
|
|
242
|
+
if (this._streamTimeoutNotificationSent && !this._message && !this._finalMessage && !this._attachments?.length) {
|
|
243
|
+
return StreamingResponseResult.Success
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const finalMessage = this.createFinalMessage()
|
|
247
|
+
if (this._streamTimedOut) {
|
|
248
|
+
await this.updateActivity(finalMessage)
|
|
249
|
+
} else {
|
|
250
|
+
await this.sendActivity(finalMessage)
|
|
251
|
+
}
|
|
209
252
|
return StreamingResponseResult.Success
|
|
210
253
|
}
|
|
211
254
|
|
|
@@ -214,6 +257,13 @@ export class StreamingResponse {
|
|
|
214
257
|
|
|
215
258
|
// Wait for the queue to drain
|
|
216
259
|
await this.waitForQueue()
|
|
260
|
+
|
|
261
|
+
// The final streaming send itself can report that the channel timed out. In
|
|
262
|
+
// that case the queued final activity is discarded and must be applied as
|
|
263
|
+
// an update after the timeout recovery has completed.
|
|
264
|
+
if (this._streamTimedOut) {
|
|
265
|
+
await this.updateActivity(this.createFinalMessage())
|
|
266
|
+
}
|
|
217
267
|
return StreamingResponseResult.Success
|
|
218
268
|
}
|
|
219
269
|
|
|
@@ -223,6 +273,7 @@ export class StreamingResponse {
|
|
|
223
273
|
*/
|
|
224
274
|
public async reset () : Promise<void> {
|
|
225
275
|
await this.waitForQueue()
|
|
276
|
+
this.clearStreamTimers()
|
|
226
277
|
|
|
227
278
|
this._queueSync = undefined
|
|
228
279
|
this._queue = []
|
|
@@ -230,6 +281,10 @@ export class StreamingResponse {
|
|
|
230
281
|
this._ended = false
|
|
231
282
|
this._canceled = false
|
|
232
283
|
this._userCanceled = false
|
|
284
|
+
this._streamTimedOut = false
|
|
285
|
+
this._streamTimeoutNotificationSent = false
|
|
286
|
+
this._lastInformationalMessageSent = ''
|
|
287
|
+
this._streamStartedAt = undefined
|
|
233
288
|
this._message = ''
|
|
234
289
|
this._nextSequence = 1
|
|
235
290
|
this._streamId = undefined
|
|
@@ -358,6 +413,29 @@ export class StreamingResponse {
|
|
|
358
413
|
return this._message
|
|
359
414
|
}
|
|
360
415
|
|
|
416
|
+
/**
|
|
417
|
+
* Ends the streaming portion of a response while allowing the underlying operation to continue.
|
|
418
|
+
*
|
|
419
|
+
* @param message Message sent to notify the user that streaming has stopped.
|
|
420
|
+
* @returns Whether the streaming response was stopped.
|
|
421
|
+
*/
|
|
422
|
+
public async sendStreamTimedOutNotification (message: string): Promise<boolean> {
|
|
423
|
+
if (this._ended || this._canceled || !this.isStreamingChannel) {
|
|
424
|
+
return false
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Ensure any in-flight queued sends complete before we send the timeout notification.
|
|
428
|
+
await this.waitForQueue()
|
|
429
|
+
|
|
430
|
+
await this.sendActivity(this.createStreamStoppedMessage(message))
|
|
431
|
+
this._streamTimeoutNotificationSent = true
|
|
432
|
+
this._isStreamingChannel = false
|
|
433
|
+
this._queue = []
|
|
434
|
+
this._chunkQueued = false
|
|
435
|
+
this.clearStreamTimers()
|
|
436
|
+
return true
|
|
437
|
+
}
|
|
438
|
+
|
|
361
439
|
/**
|
|
362
440
|
* Waits for the outgoing activity queue to be empty.
|
|
363
441
|
*
|
|
@@ -446,6 +524,10 @@ export class StreamingResponse {
|
|
|
446
524
|
activity.attachments = this._attachments
|
|
447
525
|
this._nextSequence++ // Increment sequence for final message, even if not streaming.
|
|
448
526
|
|
|
527
|
+
if (this._streamTimedOut && this._streamId) {
|
|
528
|
+
activity.id = this._streamId
|
|
529
|
+
}
|
|
530
|
+
|
|
449
531
|
if (this.isStreamingChannel) {
|
|
450
532
|
activity.entities.push({
|
|
451
533
|
type: 'streaminfo',
|
|
@@ -457,6 +539,72 @@ export class StreamingResponse {
|
|
|
457
539
|
return activity
|
|
458
540
|
}
|
|
459
541
|
|
|
542
|
+
/**
|
|
543
|
+
* Creates the final streaming notification used when streaming stops but processing continues.
|
|
544
|
+
*/
|
|
545
|
+
private createStreamStoppedMessage (message: string): Activity {
|
|
546
|
+
return Activity.fromObject({
|
|
547
|
+
type: 'message',
|
|
548
|
+
text: message || 'No text was streamed',
|
|
549
|
+
entities: [{
|
|
550
|
+
type: 'streaminfo',
|
|
551
|
+
streamType: 'final',
|
|
552
|
+
streamResult: this._message ? 'success' : 'error',
|
|
553
|
+
streamSequence: this._nextSequence++
|
|
554
|
+
}]
|
|
555
|
+
})
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Creates a checkpoint or final notification for a channel streaming timeout.
|
|
560
|
+
*/
|
|
561
|
+
private createStreamTimedOutMessage (addStreamFinal = false): Activity {
|
|
562
|
+
const text = this._message
|
|
563
|
+
? `${this._message}\n\n${this.streamingTakingTooLongMessage}\n`
|
|
564
|
+
: this.streamingTakingTooLongMessage
|
|
565
|
+
const activity = Activity.fromObject({
|
|
566
|
+
type: 'message',
|
|
567
|
+
text,
|
|
568
|
+
entities: []
|
|
569
|
+
})
|
|
570
|
+
|
|
571
|
+
if (this._streamId) {
|
|
572
|
+
activity.id = this._streamId
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
if (addStreamFinal) {
|
|
576
|
+
activity.entities!.push({
|
|
577
|
+
type: 'streaminfo',
|
|
578
|
+
streamType: 'final',
|
|
579
|
+
streamId: this._streamId,
|
|
580
|
+
streamResult: this._message ? 'success' : 'error',
|
|
581
|
+
streamSequence: this._nextSequence++
|
|
582
|
+
})
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
return activity
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/**
|
|
589
|
+
* Creates the last streaming update sent before proactively finalizing an
|
|
590
|
+
* M365 Copilot stream.
|
|
591
|
+
*/
|
|
592
|
+
private createStreamTimedOutStreamingUpdate (): Activity {
|
|
593
|
+
const text = this._message
|
|
594
|
+
? `${this._message}\n\n${this.streamingTakingTooLongMessage}\n`
|
|
595
|
+
: this.streamingTakingTooLongMessage
|
|
596
|
+
|
|
597
|
+
return Activity.fromObject({
|
|
598
|
+
type: 'typing',
|
|
599
|
+
text,
|
|
600
|
+
entities: [{
|
|
601
|
+
type: 'streaminfo',
|
|
602
|
+
streamType: 'streaming',
|
|
603
|
+
streamSequence: this._nextSequence++
|
|
604
|
+
}]
|
|
605
|
+
})
|
|
606
|
+
}
|
|
607
|
+
|
|
460
608
|
/**
|
|
461
609
|
* Sends an activity to the client and saves the stream ID returned.
|
|
462
610
|
*
|
|
@@ -465,16 +613,15 @@ export class StreamingResponse {
|
|
|
465
613
|
* @private
|
|
466
614
|
*/
|
|
467
615
|
private async sendActivity (activity: Activity): Promise<void> {
|
|
468
|
-
|
|
469
|
-
|
|
616
|
+
this.startStreamTimers()
|
|
617
|
+
|
|
618
|
+
// Only streaming activities should carry stream metadata. Once streaming
|
|
619
|
+
// has stopped, the completed response must be sent as an independent
|
|
620
|
+
// message without an orphaned streamId entity.
|
|
621
|
+
const streamInfo = activity.entities?.find(entity => entity.type?.toLowerCase() === 'streaminfo')
|
|
622
|
+
if (this._streamId && streamInfo) {
|
|
470
623
|
activity.id = this._streamId
|
|
471
|
-
|
|
472
|
-
activity.entities = []
|
|
473
|
-
}
|
|
474
|
-
if (!activity.entities[0]) {
|
|
475
|
-
activity.entities[0] = {} as Entity
|
|
476
|
-
}
|
|
477
|
-
activity.entities[0].streamId = this._streamId
|
|
624
|
+
streamInfo.streamId = this._streamId
|
|
478
625
|
}
|
|
479
626
|
|
|
480
627
|
if (this._citations && this._citations.length > 0 && !this._ended) {
|
|
@@ -507,25 +654,138 @@ export class StreamingResponse {
|
|
|
507
654
|
if (!this._streamId) {
|
|
508
655
|
this._streamId = response?.id
|
|
509
656
|
}
|
|
657
|
+
if (this.isM365Copilot() && this.isStreamingChannel && !this._ended) {
|
|
658
|
+
this.scheduleKeepAlive()
|
|
659
|
+
}
|
|
510
660
|
await new Promise((resolve) => setTimeout(resolve, this.delayInMs))
|
|
511
661
|
} catch (error) {
|
|
512
|
-
const
|
|
662
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
513
663
|
this._canceled = true
|
|
514
|
-
this._queueSync = undefined
|
|
515
664
|
this._queue = []
|
|
665
|
+
this._chunkQueued = false
|
|
666
|
+
this.clearStreamTimers()
|
|
516
667
|
|
|
517
668
|
// MS Teams code list: https://learn.microsoft.com/en-us/microsoftteams/platform/bots/streaming-ux?tabs=jsts#error-codes
|
|
518
|
-
|
|
669
|
+
const normalizedMessage = message.toLowerCase()
|
|
670
|
+
if (normalizedMessage.includes('contentstreamnotallowed') && normalizedMessage.includes(teamsStreamTimedOut.toLowerCase())) {
|
|
671
|
+
logger.warn('Client canceled due to exceeded allowed streaming time.', { originalError: message })
|
|
672
|
+
this._streamTimedOut = true
|
|
673
|
+
this._canceled = false
|
|
674
|
+
this._isStreamingChannel = false
|
|
675
|
+
await this.updateActivity(this.createStreamTimedOutMessage())
|
|
676
|
+
} else if (normalizedMessage.includes('contentstreamnotallowed')) {
|
|
519
677
|
logger.warn('Streaming content is not allowed by the client side.', { originalError: message })
|
|
520
678
|
this._userCanceled = true
|
|
521
|
-
} else if (
|
|
679
|
+
} else if (normalizedMessage.includes('badargument') && normalizedMessage.includes('streaming api is not enabled')) {
|
|
522
680
|
logger.warn('Interaction does not support streaming. Defaulting to non-streaming response.', { originalError: message })
|
|
523
681
|
this._canceled = false
|
|
524
682
|
this._isStreamingChannel = false
|
|
683
|
+
} else {
|
|
684
|
+
logger.error('Exception during StreamingResponse sendActivity.', { originalError: message })
|
|
525
685
|
}
|
|
526
686
|
}
|
|
527
687
|
}
|
|
528
688
|
|
|
689
|
+
/**
|
|
690
|
+
* Updates the existing streaming activity without allowing failures to escape the queue.
|
|
691
|
+
*/
|
|
692
|
+
private async updateActivity (activity: Activity): Promise<void> {
|
|
693
|
+
try {
|
|
694
|
+
await this._context.updateActivity(activity)
|
|
695
|
+
} catch (error) {
|
|
696
|
+
logger.warn('Exception during StreamingResponse updateActivity.', { originalError: (error as Error).message })
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* Starts M365 Copilot keep-alive and maximum-duration timers.
|
|
702
|
+
*/
|
|
703
|
+
private startStreamTimers (): void {
|
|
704
|
+
if (!this.isM365Copilot() || !this.isStreamingChannel || this._streamStartedAt !== undefined) {
|
|
705
|
+
return
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
this._streamStartedAt = Date.now()
|
|
709
|
+
this.scheduleKeepAlive()
|
|
710
|
+
this._streamTimeoutTimer = setTimeout(() => {
|
|
711
|
+
this.handleM365StreamTimeout().catch(error => {
|
|
712
|
+
logger.error('Error handling the M365 Copilot streaming timeout.', { originalError: (error as Error).message })
|
|
713
|
+
})
|
|
714
|
+
}, m365StreamingTimeoutMs)
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
/**
|
|
718
|
+
* Schedules an informative update before M365 Copilot's idle timeout.
|
|
719
|
+
*/
|
|
720
|
+
private scheduleKeepAlive (): void {
|
|
721
|
+
if (!this.isM365Copilot() || !this.isStreamingChannel || this._ended) {
|
|
722
|
+
return
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (this._keepAliveTimer) {
|
|
726
|
+
clearTimeout(this._keepAliveTimer)
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
this._keepAliveTimer = setTimeout(() => {
|
|
730
|
+
this._keepAliveTimer = undefined
|
|
731
|
+
if (!this._ended && this.isStreamingChannel && this._queue.length === 0) {
|
|
732
|
+
this.queueInformativeUpdate(this._lastInformationalMessageSent.trim()
|
|
733
|
+
? this._lastInformationalMessageSent
|
|
734
|
+
: this.streamingTakingTooLongMessage)
|
|
735
|
+
}
|
|
736
|
+
}, m365WorkingNoticeIntervalMs)
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
/**
|
|
740
|
+
* Finalizes an M365 Copilot stream that reached the channel duration limit.
|
|
741
|
+
*/
|
|
742
|
+
private async handleM365StreamTimeout (): Promise<void> {
|
|
743
|
+
if (this._ended || this._canceled || !this.isStreamingChannel || !this.isM365Copilot()) {
|
|
744
|
+
return
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
logger.warn('M365 Copilot streaming response reached the maximum duration. Finalizing the stream and continuing in non-streaming mode.', {
|
|
748
|
+
streamId: this._streamId,
|
|
749
|
+
elapsedMs: this._streamStartedAt === undefined ? undefined : Date.now() - this._streamStartedAt
|
|
750
|
+
})
|
|
751
|
+
|
|
752
|
+
const timedOutActivities = this._message
|
|
753
|
+
? [this.createStreamTimedOutStreamingUpdate(), this.createStreamTimedOutMessage(true)]
|
|
754
|
+
: [this.createStreamTimedOutMessage(true)]
|
|
755
|
+
|
|
756
|
+
this._streamTimeoutNotificationSent = true
|
|
757
|
+
this._isStreamingChannel = false
|
|
758
|
+
this._queue = []
|
|
759
|
+
this._chunkQueued = false
|
|
760
|
+
this.clearStreamTimers()
|
|
761
|
+
|
|
762
|
+
// Queue these behind any in-flight send. Clearing pending activities and
|
|
763
|
+
// using the same drain prevents out-of-order stream sequences.
|
|
764
|
+
for (const activity of timedOutActivities) {
|
|
765
|
+
this.queueActivity(() => activity)
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
await this.waitForQueue()
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Stops active channel timeout timers.
|
|
773
|
+
*/
|
|
774
|
+
private clearStreamTimers (): void {
|
|
775
|
+
if (this._keepAliveTimer) {
|
|
776
|
+
clearTimeout(this._keepAliveTimer)
|
|
777
|
+
this._keepAliveTimer = undefined
|
|
778
|
+
}
|
|
779
|
+
if (this._streamTimeoutTimer) {
|
|
780
|
+
clearTimeout(this._streamTimeoutTimer)
|
|
781
|
+
this._streamTimeoutTimer = undefined
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
private isM365Copilot (): boolean {
|
|
786
|
+
return this._context.activity.channelId?.toLocaleLowerCase() === Channels.M365Copilot.toLocaleLowerCase()
|
|
787
|
+
}
|
|
788
|
+
|
|
529
789
|
/**
|
|
530
790
|
* Loads default values for the streaming response.
|
|
531
791
|
*/
|
|
@@ -537,14 +797,8 @@ export class StreamingResponse {
|
|
|
537
797
|
if (activity.deliveryMode === DeliveryModes.ExpectReplies) {
|
|
538
798
|
this._isStreamingChannel = false
|
|
539
799
|
} else if (Channels.Msteams === activity.channelIdChannel) {
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
// TODO: Enable streaming for agentic requests when supported.
|
|
543
|
-
this._isStreamingChannel = false
|
|
544
|
-
} else {
|
|
545
|
-
this._isStreamingChannel = true
|
|
546
|
-
this._delayInMs = 1000
|
|
547
|
-
}
|
|
800
|
+
this._isStreamingChannel = true
|
|
801
|
+
this._delayInMs = 1000
|
|
548
802
|
} else if (Channels.Webchat === activity.channelId || Channels.Directline === activity.channelId || Channels.Emulator === activity.channelId) {
|
|
549
803
|
this._isStreamingChannel = true
|
|
550
804
|
this._delayInMs = 500
|
|
@@ -11,6 +11,7 @@ import { TurnContext } from '../turnContext'
|
|
|
11
11
|
import { TurnState } from './turnState'
|
|
12
12
|
import { HttpClient } from '../httpClient'
|
|
13
13
|
import { z } from 'zod'
|
|
14
|
+
import { createOutboundHostValidator, OutboundUrlPolicy } from '../outboundHostValidator'
|
|
14
15
|
|
|
15
16
|
const logger = debug('agents:M365AttachmentDownloader')
|
|
16
17
|
|
|
@@ -20,10 +21,12 @@ const logger = debug('agents:M365AttachmentDownloader')
|
|
|
20
21
|
export class M365AttachmentDownloader<TState extends TurnState = TurnState> implements InputFileDownloader<TState> {
|
|
21
22
|
private _httpClient: HttpClient
|
|
22
23
|
private _stateKey: string
|
|
24
|
+
private readonly _hostValidator: OutboundUrlPolicy
|
|
23
25
|
|
|
24
|
-
public constructor (stateKey: string = 'inputFiles') {
|
|
26
|
+
public constructor (stateKey: string = 'inputFiles', outboundHostValidator?: OutboundUrlPolicy) {
|
|
25
27
|
this._httpClient = new HttpClient()
|
|
26
28
|
this._stateKey = stateKey
|
|
29
|
+
this._hostValidator = outboundHostValidator ?? createOutboundHostValidator()
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
/**
|
|
@@ -69,6 +72,13 @@ export class M365AttachmentDownloader<TState extends TurnState = TurnState> impl
|
|
|
69
72
|
const contentSchema = z.object({ downloadUrl: z.string().url() })
|
|
70
73
|
const parsed = contentSchema.safeParse(attachment.content)
|
|
71
74
|
const downloadUrl = parsed.success ? parsed.data.downloadUrl : attachment.contentUrl
|
|
75
|
+
if (!this._hostValidator.isAllowed(downloadUrl)) {
|
|
76
|
+
let host = '[invalid-url]'
|
|
77
|
+
try { host = new URL(downloadUrl).hostname } catch { /* invalid */ }
|
|
78
|
+
logger.warn(`Attachment downloadUrl host is not in the configured allowed hosts. Host='${host}'`)
|
|
79
|
+
return undefined
|
|
80
|
+
}
|
|
81
|
+
|
|
72
82
|
const response = await this._httpClient.get(downloadUrl, { responseType: 'arraybuffer' })
|
|
73
83
|
|
|
74
84
|
const content = Buffer.from(response.data as ArrayBuffer)
|
|
@@ -8,6 +8,7 @@ import { loadEnvSettings, AuthConfiguration, envParser, envParserUtils, LoadEnv,
|
|
|
8
8
|
|
|
9
9
|
export { type AuthConfiguration, type ConnectionSettings, type ConnectionSettingsBase, type MsalConnectionSettings, type SidecarConnectionSettings, AuthType, resolveAuthority, type ConnectionMapItem, resolveAuthType } from './settings'
|
|
10
10
|
import { prune } from '../utils'
|
|
11
|
+
import { parseBooleanEnv } from '../utils/env'
|
|
11
12
|
import { ExceptionHelper } from '@microsoft/agents-activity'
|
|
12
13
|
import { Errors } from '../errorHelper'
|
|
13
14
|
|
|
@@ -28,6 +29,7 @@ function summarizeAuthConfiguration (authConfig: AuthConfiguration) {
|
|
|
28
29
|
authorityEndpoint: config.authorityEndpoint ? redactUrl(config.authorityEndpoint) : undefined,
|
|
29
30
|
scopes: (config.scopes ? redactScopes(config.scopes) : undefined) as any,
|
|
30
31
|
issuers: config.issuers?.map(redactUrl).filter(e => e !== undefined),
|
|
32
|
+
validateIssuer: config.validateIssuer,
|
|
31
33
|
federatedClientId: redactString(config.federatedClientId, true),
|
|
32
34
|
certPemFile: redactString(config.certPemFile),
|
|
33
35
|
certKeyFile: redactString(config.certKeyFile),
|
|
@@ -40,6 +42,7 @@ function summarizeAuthConfiguration (authConfig: AuthConfiguration) {
|
|
|
40
42
|
alternateBlueprintConnectionName: undefined, // Alias of altBlueprintConnectionName, avoid logging duplicate info
|
|
41
43
|
azureRegion: config.azureRegion,
|
|
42
44
|
sendX5C: config.sendX5C,
|
|
45
|
+
msalRetryCount: config.msalRetryCount,
|
|
43
46
|
sidecarBaseUrl: config.sidecarBaseUrl ? redactUrl(config.sidecarBaseUrl) : undefined,
|
|
44
47
|
serviceName: config.serviceName,
|
|
45
48
|
blueprintServiceName: config.blueprintServiceName,
|
|
@@ -96,6 +99,10 @@ const connectionsEnv = {
|
|
|
96
99
|
idpmResource: envParserUtils.bypass,
|
|
97
100
|
azureRegion: envParserUtils.bypass,
|
|
98
101
|
sendX5C: (value) => ({ value: value === 'true' }),
|
|
102
|
+
msalRetryCount: (value) => {
|
|
103
|
+
const n = parseInt(value, 10)
|
|
104
|
+
return { value: Number.isFinite(n) && n >= 0 ? n : undefined }
|
|
105
|
+
},
|
|
99
106
|
sidecarBaseUrl: envParserUtils.bypass,
|
|
100
107
|
serviceName: envParserUtils.bypass,
|
|
101
108
|
blueprintServiceName: envParserUtils.bypass,
|
|
@@ -114,6 +121,7 @@ const connectionsEnv = {
|
|
|
114
121
|
}
|
|
115
122
|
return { value: value.split(/\s+/).filter(Boolean) }
|
|
116
123
|
},
|
|
124
|
+
validateIssuer: (value) => ({ value: parseBooleanEnv(value) }),
|
|
117
125
|
}),
|
|
118
126
|
default (connections?: AuthConfiguration['connections'], connectionsMap?: AuthConfiguration['connectionsMap']) {
|
|
119
127
|
const conn = connections ?? this.connections
|
|
@@ -230,6 +238,7 @@ const legacyBotFrameworkEnv = {
|
|
|
230
238
|
WIDAssertionFile: envParserUtils.redirect(connectionsEnv.parser, 'WIDAssertionFile'),
|
|
231
239
|
azureRegion: envParserUtils.redirect(connectionsEnv.parser, 'azureRegion'),
|
|
232
240
|
sendX5C: envParserUtils.redirect(connectionsEnv.parser, 'sendX5C'),
|
|
241
|
+
msalRetryCount: envParserUtils.redirect(connectionsEnv.parser, 'msalRetryCount'),
|
|
233
242
|
authType: envParserUtils.redirect(connectionsEnv.parser, 'authType'),
|
|
234
243
|
federatedTokenFile: envParserUtils.redirect(connectionsEnv.parser, 'federatedTokenFile'),
|
|
235
244
|
idpmResource: envParserUtils.redirect(connectionsEnv.parser, 'idpmResource'),
|
|
@@ -239,6 +248,7 @@ const legacyBotFrameworkEnv = {
|
|
|
239
248
|
bypassLocalNetworkRestriction: envParserUtils.redirect(connectionsEnv.parser, 'bypassLocalNetworkRestriction'),
|
|
240
249
|
requestTimeout: envParserUtils.redirect(connectionsEnv.parser, 'requestTimeout'),
|
|
241
250
|
retryCount: envParserUtils.redirect(connectionsEnv.parser, 'retryCount'),
|
|
251
|
+
validateIssuer: envParserUtils.redirect(connectionsEnv.parser, 'validateIssuer'),
|
|
242
252
|
}),
|
|
243
253
|
process (env: LoadEnv) {
|
|
244
254
|
return legacyPrefixEnv.process.call(this, env)
|
|
@@ -265,6 +275,7 @@ const legacyPrefixEnv = {
|
|
|
265
275
|
WIDAssertionFile: envParserUtils.redirect(connectionsEnv.parser, 'WIDAssertionFile'),
|
|
266
276
|
azureRegion: envParserUtils.redirect(connectionsEnv.parser, 'azureRegion'),
|
|
267
277
|
sendX5C: envParserUtils.redirect(connectionsEnv.parser, 'sendX5C'),
|
|
278
|
+
msalRetryCount: envParserUtils.redirect(connectionsEnv.parser, 'msalRetryCount'),
|
|
268
279
|
authType: envParserUtils.redirect(connectionsEnv.parser, 'authType'),
|
|
269
280
|
federatedTokenFile: envParserUtils.redirect(connectionsEnv.parser, 'federatedTokenFile'),
|
|
270
281
|
idpmResource: envParserUtils.redirect(connectionsEnv.parser, 'idpmResource'),
|
|
@@ -274,6 +285,7 @@ const legacyPrefixEnv = {
|
|
|
274
285
|
bypassLocalNetworkRestriction: envParserUtils.redirect(connectionsEnv.parser, 'bypassLocalNetworkRestriction'),
|
|
275
286
|
requestTimeout: envParserUtils.redirect(connectionsEnv.parser, 'requestTimeout'),
|
|
276
287
|
retryCount: envParserUtils.redirect(connectionsEnv.parser, 'retryCount'),
|
|
288
|
+
validateIssuer: envParserUtils.redirect(connectionsEnv.parser, 'validateIssuer'),
|
|
277
289
|
}),
|
|
278
290
|
process (env: LoadEnv, prefix?: string) {
|
|
279
291
|
const settings: Partial<AuthConfiguration> = {}
|
package/src/auth/index.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
export * from './authConfiguration'
|
|
2
2
|
export * from './authConstants'
|
|
3
3
|
export * from './authProvider'
|
|
4
|
-
export * from './
|
|
4
|
+
export * from './connections'
|
|
5
5
|
export * from './request'
|
|
6
|
+
export * from './msal/msalTokenProvider'
|
|
6
7
|
export * from './msal/msalTokenCredential'
|
|
7
8
|
export * from './connectionManager'
|
|
8
9
|
export * from './connections'
|