@microsoft/agents-hosting 1.6.0-beta.9.gce9d8facd2 → 1.6.1
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/dist/package.json +4 -9
- package/dist/src/agent-client/agentClient.js +2 -2
- package/dist/src/agent-client/agentClient.js.map +1 -1
- package/dist/src/agent-client/agentResponseHandler.js +2 -2
- package/dist/src/agent-client/agentResponseHandler.js.map +1 -1
- package/dist/src/app/agentApplication.js +28 -2
- package/dist/src/app/agentApplication.js.map +1 -1
- package/dist/src/app/agentApplicationOptions.d.ts +10 -0
- package/dist/src/app/attachmentDownloader.js +5 -10
- package/dist/src/app/attachmentDownloader.js.map +1 -1
- package/dist/src/app/auth/authorizationManager.d.ts +4 -0
- package/dist/src/app/auth/authorizationManager.js +94 -67
- package/dist/src/app/auth/authorizationManager.js.map +1 -1
- package/dist/src/app/auth/handlers/azureBotAuthorization.js +2 -2
- package/dist/src/app/auth/handlers/azureBotAuthorization.js.map +1 -1
- package/dist/src/app/proactive/createConversationOptionsBuilder.js +10 -6
- package/dist/src/app/proactive/createConversationOptionsBuilder.js.map +1 -1
- package/dist/src/app/proactive/proactive.d.ts +1 -0
- package/dist/src/app/proactive/proactive.js +30 -15
- package/dist/src/app/proactive/proactive.js.map +1 -1
- package/dist/src/app/teamsAttachmentDownloader.js +6 -14
- package/dist/src/app/teamsAttachmentDownloader.js.map +1 -1
- package/dist/src/auth/authConfiguration.d.ts +2 -143
- package/dist/src/auth/authConfiguration.js +301 -257
- package/dist/src/auth/authConfiguration.js.map +1 -1
- package/dist/src/auth/jwt-middleware.js +2 -1
- package/dist/src/auth/jwt-middleware.js.map +1 -1
- package/dist/src/auth/msalConnectionManager.js +27 -14
- package/dist/src/auth/msalConnectionManager.js.map +1 -1
- package/dist/src/auth/msalTokenProvider.d.ts +7 -0
- package/dist/src/auth/msalTokenProvider.js +188 -44
- package/dist/src/auth/msalTokenProvider.js.map +1 -1
- package/dist/src/auth/settings.d.ts +327 -0
- package/dist/src/auth/settings.js +158 -0
- package/dist/src/auth/settings.js.map +1 -0
- package/dist/src/cloudAdapter.d.ts +58 -1
- package/dist/src/cloudAdapter.js +232 -52
- package/dist/src/cloudAdapter.js.map +1 -1
- package/dist/src/connector-client/connectorClient.d.ts +12 -8
- package/dist/src/connector-client/connectorClient.js +99 -82
- package/dist/src/connector-client/connectorClient.js.map +1 -1
- package/dist/src/errorHelper.js +85 -7
- package/dist/src/errorHelper.js.map +1 -1
- package/dist/src/getProductInfo.d.ts +23 -0
- package/dist/src/getProductInfo.js +71 -1
- package/dist/src/getProductInfo.js.map +1 -1
- package/dist/src/headerPropagation.d.ts +17 -2
- package/dist/src/headerPropagation.js +46 -24
- package/dist/src/headerPropagation.js.map +1 -1
- package/dist/src/httpClient.d.ts +60 -0
- package/dist/src/httpClient.js +173 -0
- package/dist/src/httpClient.js.map +1 -0
- package/dist/src/index.d.ts +2 -1
- package/dist/src/index.js +7 -2
- package/dist/src/index.js.map +1 -1
- package/dist/src/oauth/userTokenClient.d.ts +5 -4
- package/dist/src/oauth/userTokenClient.js +97 -78
- package/dist/src/oauth/userTokenClient.js.map +1 -1
- package/dist/src/observability/traces.d.ts +11 -4
- package/dist/src/observability/traces.js +39 -7
- package/dist/src/observability/traces.js.map +1 -1
- package/dist/src/utils/env.d.ts +50 -0
- package/dist/src/utils/env.js +113 -0
- package/dist/src/utils/env.js.map +1 -0
- package/dist/src/utils.d.ts +10 -0
- package/dist/src/utils.js +18 -0
- package/dist/src/utils.js.map +1 -0
- package/package.json +4 -9
- package/src/agent-client/agentClient.ts +2 -2
- package/src/agent-client/agentResponseHandler.ts +2 -2
- package/src/app/agentApplication.ts +30 -3
- package/src/app/agentApplicationOptions.ts +11 -0
- package/src/app/attachmentDownloader.ts +6 -8
- package/src/app/auth/authorizationManager.ts +84 -41
- package/src/app/auth/handlers/azureBotAuthorization.ts +2 -2
- package/src/app/proactive/createConversationOptionsBuilder.ts +8 -4
- package/src/app/proactive/proactive.ts +39 -23
- package/src/app/teamsAttachmentDownloader.ts +7 -11
- package/src/auth/authConfiguration.ts +317 -356
- package/src/auth/jwt-middleware.ts +1 -1
- package/src/auth/msalConnectionManager.ts +27 -10
- package/src/auth/msalTokenProvider.ts +180 -42
- package/src/auth/settings.ts +323 -0
- package/src/cloudAdapter.ts +291 -12
- package/src/connector-client/connectorClient.ts +116 -94
- package/src/errorHelper.ts +96 -7
- package/src/getProductInfo.ts +79 -1
- package/src/headerPropagation.ts +55 -23
- package/src/httpClient.ts +223 -0
- package/src/index.ts +5 -1
- package/src/oauth/userTokenClient.ts +103 -84
- package/src/observability/traces.ts +39 -8
- package/src/utils/env.ts +105 -0
- package/src/utils.ts +14 -0
package/src/cloudAdapter.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { MsalConnectionManager } from './auth/msalConnectionManager'
|
|
|
16
16
|
import { Activity, ActivityEventNames, ActivityTypes, Channels, ConversationReference, DeliveryModes, ConversationParameters, RoleTypes, ExceptionHelper } from '@microsoft/agents-activity'
|
|
17
17
|
import { Errors } from './errorHelper'
|
|
18
18
|
import { ResourceResponse } from './connector-client/resourceResponse'
|
|
19
|
-
import
|
|
19
|
+
import { randomUUID } from 'crypto'
|
|
20
20
|
import { debug } from '@microsoft/agents-telemetry'
|
|
21
21
|
import { StatusCodes } from './statusCodes'
|
|
22
22
|
import { InvokeResponse } from './invoke/invokeResponse'
|
|
@@ -28,8 +28,11 @@ import { HeaderPropagation, HeaderPropagationCollection, HeaderPropagationDefini
|
|
|
28
28
|
import { JwtPayload } from 'jsonwebtoken'
|
|
29
29
|
import { getTokenServiceEndpoint } from './oauth/customUserTokenAPI'
|
|
30
30
|
import { Connections } from './auth/connections'
|
|
31
|
+
import { parseBooleanEnv, suggestClosest } from './utils/env'
|
|
31
32
|
import { trace } from '@microsoft/agents-telemetry'
|
|
32
33
|
import { AdapterTraceDefinitions } from './observability'
|
|
34
|
+
import { applyAgenticHeaders } from './getProductInfo'
|
|
35
|
+
|
|
33
36
|
const logger = debug('agents:cloud-adapter')
|
|
34
37
|
|
|
35
38
|
/**
|
|
@@ -41,22 +44,238 @@ const logger = debug('agents:cloud-adapter')
|
|
|
41
44
|
* flow between agents and users across different channels, handling activities, attachments,
|
|
42
45
|
* and conversation continuations.
|
|
43
46
|
*/
|
|
47
|
+
/**
|
|
48
|
+
* Optional configuration for {@link CloudAdapter} runtime behavior.
|
|
49
|
+
*
|
|
50
|
+
* Defaults are conservative and match the .NET SDK's `AdapterOptions` defaults.
|
|
51
|
+
*
|
|
52
|
+
* Each option can also be supplied via an environment variable using the
|
|
53
|
+
* convention `CloudAdapterOptions__<propertyName>` — for example
|
|
54
|
+
* `CloudAdapterOptions__validateServiceUrl=true`. The prefix matches the
|
|
55
|
+
* .NET SDK's `IConfiguration.GetSection("CloudAdapterOptions")` section name,
|
|
56
|
+
* so a shared environment can configure both SDKs from the same variables.
|
|
57
|
+
* Both the `CloudAdapterOptions__` prefix and the property name are matched
|
|
58
|
+
* case-insensitively, so hosts that uppercase env-var names (some PaaS
|
|
59
|
+
* platforms) still work. Values supplied directly to the constructor always
|
|
60
|
+
* win over environment variables.
|
|
61
|
+
*/
|
|
62
|
+
export interface CloudAdapterOptions {
|
|
63
|
+
/**
|
|
64
|
+
* When `true`, the default `onTurnError` handler includes `error.stack` in
|
|
65
|
+
* its log output. Defaults to `false`.
|
|
66
|
+
*
|
|
67
|
+
* Env var: `CloudAdapterOptions__emitStackTrace`.
|
|
68
|
+
*/
|
|
69
|
+
emitStackTrace?: boolean
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* When `true`, an inbound activity whose `serviceUrl` host does not match
|
|
73
|
+
* the `serviceurl` claim on the caller's identity is rejected with HTTP 400.
|
|
74
|
+
* When `false` (default for backward compatibility), the mismatch is logged
|
|
75
|
+
* as a warning but the activity is still processed.
|
|
76
|
+
*
|
|
77
|
+
* **Recommended for production.** Setting this to `true` (either directly or
|
|
78
|
+
* via `CloudAdapterOptions__validateServiceUrl=true`) defends against
|
|
79
|
+
* confused-deputy / SSRF-style attacks where an attacker with a valid token
|
|
80
|
+
* for one service URL tries to send activities targeting a different one.
|
|
81
|
+
*
|
|
82
|
+
* Env var: `CloudAdapterOptions__validateServiceUrl`.
|
|
83
|
+
*/
|
|
84
|
+
validateServiceUrl?: boolean
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const DEFAULT_CLOUD_ADAPTER_OPTIONS: Required<CloudAdapterOptions> = {
|
|
88
|
+
emitStackTrace: false,
|
|
89
|
+
validateServiceUrl: false
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Env-var prefix for {@link CloudAdapterOptions} (matches .NET config section). */
|
|
93
|
+
const CLOUD_ADAPTER_OPTIONS_ENV_PREFIX = 'CloudAdapterOptions__'
|
|
94
|
+
const CLOUD_ADAPTER_OPTIONS_ENV_PREFIX_UPPER = CLOUD_ADAPTER_OPTIONS_ENV_PREFIX.toUpperCase()
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Declarative env-var parser for {@link CloudAdapterOptions}.
|
|
98
|
+
*
|
|
99
|
+
* Shape mirrors `envParser<K>` introduced in PR #1119
|
|
100
|
+
* (`packages/agents-hosting/src/auth/settings.ts`) so this loader can be
|
|
101
|
+
* swapped for the shared utility once that lands. Each entry is a
|
|
102
|
+
* `(rawValue) => parsedValue | undefined` function. Lookups are
|
|
103
|
+
* case-insensitive via the `upperKeys` map, matching the upstream
|
|
104
|
+
* convention and accommodating hosts that uppercase env-var names.
|
|
105
|
+
*
|
|
106
|
+
* Adding a new option to `CloudAdapterOptions` only requires adding an
|
|
107
|
+
* entry here.
|
|
108
|
+
*/
|
|
109
|
+
const cloudAdapterOptionsParser = (() => {
|
|
110
|
+
const schema: {
|
|
111
|
+
[K in keyof Required<CloudAdapterOptions>]: (raw: string | undefined) => CloudAdapterOptions[K]
|
|
112
|
+
} = {
|
|
113
|
+
emitStackTrace: parseBooleanEnv,
|
|
114
|
+
validateServiceUrl: parseBooleanEnv
|
|
115
|
+
}
|
|
116
|
+
const keys = Object.keys(schema) as Array<keyof CloudAdapterOptions>
|
|
117
|
+
const upperKeys = keys.reduce<Record<string, keyof CloudAdapterOptions>>((acc, key) => {
|
|
118
|
+
acc[key.toUpperCase()] = key
|
|
119
|
+
return acc
|
|
120
|
+
}, {})
|
|
121
|
+
return {
|
|
122
|
+
schema,
|
|
123
|
+
keys,
|
|
124
|
+
/**
|
|
125
|
+
* Resolves an env-var property name (case-insensitive) to its canonical
|
|
126
|
+
* `CloudAdapterOptions` key, or `undefined` when the name is unknown.
|
|
127
|
+
*/
|
|
128
|
+
resolveKey (property: string): keyof CloudAdapterOptions | undefined {
|
|
129
|
+
return upperKeys[property.toUpperCase()]
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
})()
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Per-process dedup set for configuration warnings. Without this, every
|
|
136
|
+
* `new CloudAdapter()` re-scans `process.env` and re-emits warnings for any
|
|
137
|
+
* typo'd `CloudAdapterOptions__*` key (or unparseable value), which spams
|
|
138
|
+
* stderr in multi-adapter scenarios (tests, proactive flows, DI containers).
|
|
139
|
+
*/
|
|
140
|
+
const warnedConfigKeys = new Set<string>()
|
|
141
|
+
|
|
142
|
+
function emitConfigWarning (envKey: string, message: string): void {
|
|
143
|
+
if (warnedConfigKeys.has(envKey)) return
|
|
144
|
+
warnedConfigKeys.add(envKey)
|
|
145
|
+
// Visible by default (writes synchronously to stderr) so users see typos
|
|
146
|
+
// and bad values without having to opt in via `DEBUG=agents:cloud-adapter:*`.
|
|
147
|
+
// Hosts that want to route or suppress can intercept `console.warn` or
|
|
148
|
+
// subscribe to the `agents:cloud-adapter:warn` debug namespace below.
|
|
149
|
+
console.warn(`[agents:cloud-adapter] ${message}`)
|
|
150
|
+
logger.warn(message)
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Scans `process.env` for keys with the `CloudAdapterOptions__` prefix
|
|
155
|
+
* (case-insensitive) and returns the parsed partial options. Unknown keys
|
|
156
|
+
* and values that fail to parse are warned about once per process via
|
|
157
|
+
* `console.warn` (so they are visible by default without enabling debug)
|
|
158
|
+
* and through the `agents:cloud-adapter:warn` debug channel for log
|
|
159
|
+
* aggregators. Hosts that want to route or suppress these diagnostics can
|
|
160
|
+
* intercept `console.warn` or filter the debug namespace.
|
|
161
|
+
*/
|
|
162
|
+
function loadCloudAdapterOptionsFromEnv (): CloudAdapterOptions {
|
|
163
|
+
const result: CloudAdapterOptions = {}
|
|
164
|
+
for (const [envKey, rawValue] of Object.entries(process.env)) {
|
|
165
|
+
const upper = envKey.toUpperCase()
|
|
166
|
+
if (!upper.startsWith(CLOUD_ADAPTER_OPTIONS_ENV_PREFIX_UPPER)) continue
|
|
167
|
+
const property = envKey.substring(CLOUD_ADAPTER_OPTIONS_ENV_PREFIX.length)
|
|
168
|
+
const canonical = cloudAdapterOptionsParser.resolveKey(property)
|
|
169
|
+
if (!canonical) {
|
|
170
|
+
const suggestion = suggestClosest(property, cloudAdapterOptionsParser.keys as readonly string[], 4)
|
|
171
|
+
const hint = suggestion ? ` Did you mean "${CLOUD_ADAPTER_OPTIONS_ENV_PREFIX}${suggestion}"?` : ''
|
|
172
|
+
emitConfigWarning(envKey, `Unknown CloudAdapterOptions env var: ${envKey} (ignored).${hint}`)
|
|
173
|
+
continue
|
|
174
|
+
}
|
|
175
|
+
const parsed = cloudAdapterOptionsParser.schema[canonical](rawValue) as any
|
|
176
|
+
if (parsed !== undefined) {
|
|
177
|
+
(result as any)[canonical] = parsed
|
|
178
|
+
} else if (rawValue !== undefined && rawValue.trim() !== '') {
|
|
179
|
+
// Known key, recognized but unparseable value (e.g. `yes`, `on`, `enabled`).
|
|
180
|
+
// For a security-relevant flag like `validateServiceUrl`, silent
|
|
181
|
+
// fallthrough is the dangerous failure mode — surface it.
|
|
182
|
+
// Note: parseBooleanEnv treats whitespace-only as unset, so don't warn
|
|
183
|
+
// on `' '`; only warn when the user actually typed something.
|
|
184
|
+
emitConfigWarning(
|
|
185
|
+
`${envKey}=${rawValue}`,
|
|
186
|
+
`Ignored ${envKey}=${rawValue}; expected one of true/false/1/0.`
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return result
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Resolves a `CloudAdapterOptions` instance from an explicit argument or, if
|
|
195
|
+
* absent, from environment variables. Values supplied in the explicit object
|
|
196
|
+
* win over env vars.
|
|
197
|
+
*/
|
|
198
|
+
function resolveCloudAdapterOptions (options?: CloudAdapterOptions): Required<CloudAdapterOptions> {
|
|
199
|
+
const fromEnv = loadCloudAdapterOptionsFromEnv()
|
|
200
|
+
return {
|
|
201
|
+
emitStackTrace: options?.emitStackTrace ?? fromEnv.emitStackTrace ?? DEFAULT_CLOUD_ADAPTER_OPTIONS.emitStackTrace,
|
|
202
|
+
validateServiceUrl: options?.validateServiceUrl ?? fromEnv.validateServiceUrl ?? DEFAULT_CLOUD_ADAPTER_OPTIONS.validateServiceUrl
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Removes CR/LF and other control characters from an attacker-controllable
|
|
208
|
+
* string before interpolating it into a log message, to prevent log forging
|
|
209
|
+
* / log injection (OWASP A09). Truncates excessively long values.
|
|
210
|
+
*/
|
|
211
|
+
function sanitizeForLog (value: string, max = 256): string {
|
|
212
|
+
// Strip C0 (\x00-\x1f) + DEL (\x7f) + C1 (\x80-\x9f) controls plus U+2028
|
|
213
|
+
// LINE SEPARATOR / U+2029 PARAGRAPH SEPARATOR — some log viewers and
|
|
214
|
+
// terminals treat them as line breaks, which would re-open the log-
|
|
215
|
+
// injection vector that the ASCII strip closes.
|
|
216
|
+
// eslint-disable-next-line no-control-regex
|
|
217
|
+
const cleaned = value.replace(/[\x00-\x1f\x7f-\x9f\u2028\u2029]/g, '?')
|
|
218
|
+
return cleaned.length > max ? cleaned.slice(0, max) + '…' : cleaned
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Truncates a JSON-serialized activity for log lines so a malformed or
|
|
223
|
+
* pathologically large inbound payload can't blow up the log pipeline.
|
|
224
|
+
*/
|
|
225
|
+
function truncateActivityForLog (activity: unknown, max = 1024): string {
|
|
226
|
+
try {
|
|
227
|
+
const json = JSON.stringify(activity)
|
|
228
|
+
if (!json) return '<unserializable>'
|
|
229
|
+
// Strip C0/DEL/C1 controls + U+2028/U+2029 from the serialized form so
|
|
230
|
+
// attacker-controlled fields (e.g. activity.text) can't forge log lines.
|
|
231
|
+
// eslint-disable-next-line no-control-regex
|
|
232
|
+
const sanitized = json.replace(/[\x00-\x1f\x7f-\x9f\u2028\u2029]/g, '?')
|
|
233
|
+
return sanitized.length > max ? `${sanitized.slice(0, max)}…(truncated)` : sanitized
|
|
234
|
+
} catch {
|
|
235
|
+
return '<unserializable>'
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
44
239
|
export class CloudAdapter extends BaseAdapter {
|
|
240
|
+
protected readonly authConfig: AuthConfiguration
|
|
241
|
+
protected _agentName?: string
|
|
242
|
+
|
|
45
243
|
/**
|
|
46
244
|
* Client for connecting to the Azure Bot Service
|
|
47
245
|
*/
|
|
48
246
|
connectionManager: Connections
|
|
49
247
|
|
|
248
|
+
private readonly _options: Required<CloudAdapterOptions>
|
|
249
|
+
|
|
50
250
|
/**
|
|
51
251
|
* Creates an instance of CloudAdapter.
|
|
52
252
|
* @param authConfig - The authentication configuration for securing communications.
|
|
53
253
|
* @param authProvider - No longer used.
|
|
54
254
|
* @param userTokenClient - No longer used.
|
|
255
|
+
* @param options - Optional runtime behavior overrides. See {@link CloudAdapterOptions}.
|
|
55
256
|
*/
|
|
56
|
-
constructor (authConfig?: AuthConfiguration, authProvider?: AuthProvider, userTokenClient?: UserTokenClient) {
|
|
257
|
+
constructor (authConfig?: AuthConfiguration, authProvider?: AuthProvider, userTokenClient?: UserTokenClient, options?: CloudAdapterOptions) {
|
|
57
258
|
super()
|
|
58
|
-
authConfig = getAuthConfigWithDefaults(authConfig)
|
|
259
|
+
this.authConfig = authConfig = getAuthConfigWithDefaults(authConfig)
|
|
59
260
|
this.connectionManager = new MsalConnectionManager(undefined, undefined, authConfig)
|
|
261
|
+
this._options = resolveCloudAdapterOptions(options)
|
|
262
|
+
|
|
263
|
+
// Install a CloudAdapter-aware default `onTurnError` that honors
|
|
264
|
+
// `emitStackTrace`. The base class default only logs the message; we
|
|
265
|
+
// preserve that user-facing behavior (trace + messages) but add an
|
|
266
|
+
// optional stack trace for operators when explicitly opted in.
|
|
267
|
+
this.onTurnError = async (context: TurnContext, error: Error) => {
|
|
268
|
+
const detail = this._options.emitStackTrace && error?.stack ? error.stack : `${error}`
|
|
269
|
+
logger.error(`\n [onTurnError] unhandled error: ${detail}`)
|
|
270
|
+
await context.sendTraceActivity(
|
|
271
|
+
'OnTurnError Trace',
|
|
272
|
+
`${error}`,
|
|
273
|
+
'https://www.botframework.com/schemas/error',
|
|
274
|
+
'TurnError'
|
|
275
|
+
)
|
|
276
|
+
await context.sendActivity('The agent encountered an error or bug.')
|
|
277
|
+
await context.sendActivity('To continue to run this agent, please fix the source code.')
|
|
278
|
+
}
|
|
60
279
|
}
|
|
61
280
|
|
|
62
281
|
/**
|
|
@@ -103,7 +322,7 @@ export class CloudAdapter extends BaseAdapter {
|
|
|
103
322
|
headers?: HeaderPropagationCollection
|
|
104
323
|
): Promise<ConnectorClient> {
|
|
105
324
|
return trace(AdapterTraceDefinitions.createConnectorClient, async ({ record }) => {
|
|
106
|
-
record({ serviceUrl, scope })
|
|
325
|
+
record({ serviceUrl, scopes: [scope] })
|
|
107
326
|
|
|
108
327
|
// get the correct token provider
|
|
109
328
|
const tokenProvider = this.connectionManager.getTokenProvider(identity, serviceUrl)
|
|
@@ -144,7 +363,7 @@ export class CloudAdapter extends BaseAdapter {
|
|
|
144
363
|
let connectorClient
|
|
145
364
|
const tokenProvider = this.connectionManager.getTokenProviderFromActivity(identity, activity)
|
|
146
365
|
const isAgentic = activity.isAgenticRequest()
|
|
147
|
-
|
|
366
|
+
const scopes: string[] = []
|
|
148
367
|
if (isAgentic) {
|
|
149
368
|
logger.debug('Activity is from an agentic source, using special scope', activity.recipient)
|
|
150
369
|
const agenticInstanceId = activity.getAgenticInstanceId()
|
|
@@ -159,8 +378,13 @@ export class CloudAdapter extends BaseAdapter {
|
|
|
159
378
|
headers
|
|
160
379
|
)
|
|
161
380
|
} else if (activity.recipient?.role?.toLowerCase() === RoleTypes.AgenticUser.toLowerCase() && agenticInstanceId && agenticUserId) {
|
|
162
|
-
|
|
163
|
-
|
|
381
|
+
const configuredScopes = tokenProvider.connectionSettings?.scopes
|
|
382
|
+
if (configuredScopes?.length) {
|
|
383
|
+
scopes.push(...configuredScopes)
|
|
384
|
+
} else {
|
|
385
|
+
scopes.push(tokenProvider.connectionSettings?.scope ?? ApxProductionScope)
|
|
386
|
+
}
|
|
387
|
+
const token = await tokenProvider.getAgenticUserToken(activity.getAgenticTenantId() ?? '', agenticInstanceId, agenticUserId, scopes)
|
|
164
388
|
|
|
165
389
|
connectorClient = ConnectorClient.createClientWithToken(
|
|
166
390
|
activity.serviceUrl!,
|
|
@@ -173,8 +397,8 @@ export class CloudAdapter extends BaseAdapter {
|
|
|
173
397
|
} else {
|
|
174
398
|
// ABS tokens will not have an azp/appid so use the botframework scope.
|
|
175
399
|
// Otherwise use the appId. This will happen when communicating back to another agent.
|
|
176
|
-
|
|
177
|
-
const token = await tokenProvider.getAccessToken(
|
|
400
|
+
scopes.push(identity.azp ?? identity.appid ?? 'https://api.botframework.com')
|
|
401
|
+
const token = await tokenProvider.getAccessToken(scopes[0])
|
|
178
402
|
connectorClient = ConnectorClient.createClientWithToken(
|
|
179
403
|
activity.serviceUrl!,
|
|
180
404
|
token,
|
|
@@ -183,7 +407,7 @@ export class CloudAdapter extends BaseAdapter {
|
|
|
183
407
|
}
|
|
184
408
|
record({
|
|
185
409
|
serviceUrl: activity.serviceUrl,
|
|
186
|
-
|
|
410
|
+
scopes,
|
|
187
411
|
activityIsAgentic: isAgentic
|
|
188
412
|
})
|
|
189
413
|
return connectorClient
|
|
@@ -201,6 +425,14 @@ export class CloudAdapter extends BaseAdapter {
|
|
|
201
425
|
} as JwtPayload
|
|
202
426
|
}
|
|
203
427
|
|
|
428
|
+
/**
|
|
429
|
+
* Sets the agent name for M365 agent header propagation.
|
|
430
|
+
* @param agentName The human-friendly agent name to set for header propagation.
|
|
431
|
+
*/
|
|
432
|
+
public setAgentName (agentName?: string): void {
|
|
433
|
+
this._agentName = agentName
|
|
434
|
+
}
|
|
435
|
+
|
|
204
436
|
/**
|
|
205
437
|
* Sets the connector client on the turn context.
|
|
206
438
|
*
|
|
@@ -355,7 +587,7 @@ export class CloudAdapter extends BaseAdapter {
|
|
|
355
587
|
const headers = new HeaderPropagation(request.headers)
|
|
356
588
|
if (headerPropagation && typeof headerPropagation === 'function') {
|
|
357
589
|
headerPropagation(headers)
|
|
358
|
-
logger.debug('Headers to propagate: ', headers)
|
|
590
|
+
logger.debug('Headers to propagate: ', { keys: Object.keys(headers.outgoing) })
|
|
359
591
|
}
|
|
360
592
|
|
|
361
593
|
const end = (status: StatusCodes, body?: unknown, isInvokeResponseOrExpectReplies: boolean = false) => {
|
|
@@ -388,11 +620,20 @@ export class CloudAdapter extends BaseAdapter {
|
|
|
388
620
|
record({ activity })
|
|
389
621
|
|
|
390
622
|
if (!this.isValidChannelActivity(activity)) {
|
|
623
|
+
logger.warn(`BadRequest: invalid activity body: ${truncateActivityForLog(activity)}`)
|
|
624
|
+
return end(StatusCodes.BAD_REQUEST)
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (!this.validateServiceUrl(request.user, activity)) {
|
|
391
628
|
return end(StatusCodes.BAD_REQUEST)
|
|
392
629
|
}
|
|
393
630
|
|
|
394
631
|
logger.debug('Received activity: ', activity)
|
|
395
632
|
|
|
633
|
+
if (isAgentic) {
|
|
634
|
+
applyAgenticHeaders(headers, activity, this._agentName)
|
|
635
|
+
}
|
|
636
|
+
|
|
396
637
|
const context = new TurnContext(this, activity, request.user!)
|
|
397
638
|
// if Delivery Mode == ExpectReplies, we don't need a connector client.
|
|
398
639
|
if (this.resolveIfConnectorClientIsNeeded(activity)) {
|
|
@@ -441,6 +682,41 @@ export class CloudAdapter extends BaseAdapter {
|
|
|
441
682
|
return true
|
|
442
683
|
}
|
|
443
684
|
|
|
685
|
+
/**
|
|
686
|
+
* Compares the host of `activity.serviceUrl` against the `serviceurl` claim
|
|
687
|
+
* on the caller's identity. When enabled and the hosts differ (or either
|
|
688
|
+
* URL is malformed), the request is rejected with HTTP 400; when disabled,
|
|
689
|
+
* the mismatch is logged as a warning.
|
|
690
|
+
*
|
|
691
|
+
* @returns `true` if the activity should continue processing; `false` if it
|
|
692
|
+
* should be rejected with a 400.
|
|
693
|
+
*/
|
|
694
|
+
private validateServiceUrl (identity: JwtPayload | undefined, activity: Activity): boolean {
|
|
695
|
+
if (!identity) return true
|
|
696
|
+
if (!activity.serviceUrl) return true
|
|
697
|
+
|
|
698
|
+
const claimValue = identity.serviceurl
|
|
699
|
+
if (typeof claimValue !== 'string') return true
|
|
700
|
+
|
|
701
|
+
let claimHost: string | undefined
|
|
702
|
+
let activityHost: string | undefined
|
|
703
|
+
try { claimHost = new URL(claimValue).hostname.toLowerCase() } catch { /* invalid */ }
|
|
704
|
+
try { activityHost = new URL(activity.serviceUrl).hostname.toLowerCase() } catch { /* invalid */ }
|
|
705
|
+
|
|
706
|
+
if (claimHost && activityHost && claimHost === activityHost) {
|
|
707
|
+
return true
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
const safeClaim = sanitizeForLog(claimValue)
|
|
711
|
+
const safeServiceUrl = sanitizeForLog(activity.serviceUrl)
|
|
712
|
+
if (this._options.validateServiceUrl) {
|
|
713
|
+
logger.error(`Invalid service URL Claim='${safeClaim}', ServiceUrl='${safeServiceUrl}'`)
|
|
714
|
+
return false
|
|
715
|
+
}
|
|
716
|
+
logger.warn(`Invalid service URL Claim='${safeClaim}', ServiceUrl='${safeServiceUrl}'`)
|
|
717
|
+
return true
|
|
718
|
+
}
|
|
719
|
+
|
|
444
720
|
/**
|
|
445
721
|
* Updates an activity.
|
|
446
722
|
* @param context - The TurnContext for the current turn.
|
|
@@ -599,7 +875,7 @@ export class CloudAdapter extends BaseAdapter {
|
|
|
599
875
|
activity.name = ActivityEventNames.CreateConversation
|
|
600
876
|
activity.channelId = channelId
|
|
601
877
|
activity.serviceUrl = serviceUrl
|
|
602
|
-
activity.id = createdConversationId ??
|
|
878
|
+
activity.id = createdConversationId ?? randomUUID()
|
|
603
879
|
activity.conversation = {
|
|
604
880
|
conversationType: undefined,
|
|
605
881
|
id: createdConversationId!,
|
|
@@ -609,6 +885,9 @@ export class CloudAdapter extends BaseAdapter {
|
|
|
609
885
|
}
|
|
610
886
|
activity.channelData = conversationParameters.channelData
|
|
611
887
|
activity.recipient = conversationParameters.agent
|
|
888
|
+
// For 1:1 conversations members[0] is the target user; for channel conversations
|
|
889
|
+
// (where members is absent) we fall back to the agent so from.id is always present.
|
|
890
|
+
activity.from = conversationParameters.members?.[0] ?? conversationParameters.agent
|
|
612
891
|
|
|
613
892
|
return activity
|
|
614
893
|
}
|