@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.
Files changed (94) hide show
  1. package/dist/package.json +4 -9
  2. package/dist/src/agent-client/agentClient.js +2 -2
  3. package/dist/src/agent-client/agentClient.js.map +1 -1
  4. package/dist/src/agent-client/agentResponseHandler.js +2 -2
  5. package/dist/src/agent-client/agentResponseHandler.js.map +1 -1
  6. package/dist/src/app/agentApplication.js +28 -2
  7. package/dist/src/app/agentApplication.js.map +1 -1
  8. package/dist/src/app/agentApplicationOptions.d.ts +10 -0
  9. package/dist/src/app/attachmentDownloader.js +5 -10
  10. package/dist/src/app/attachmentDownloader.js.map +1 -1
  11. package/dist/src/app/auth/authorizationManager.d.ts +4 -0
  12. package/dist/src/app/auth/authorizationManager.js +94 -67
  13. package/dist/src/app/auth/authorizationManager.js.map +1 -1
  14. package/dist/src/app/auth/handlers/azureBotAuthorization.js +2 -2
  15. package/dist/src/app/auth/handlers/azureBotAuthorization.js.map +1 -1
  16. package/dist/src/app/proactive/createConversationOptionsBuilder.js +10 -6
  17. package/dist/src/app/proactive/createConversationOptionsBuilder.js.map +1 -1
  18. package/dist/src/app/proactive/proactive.d.ts +1 -0
  19. package/dist/src/app/proactive/proactive.js +30 -15
  20. package/dist/src/app/proactive/proactive.js.map +1 -1
  21. package/dist/src/app/teamsAttachmentDownloader.js +6 -14
  22. package/dist/src/app/teamsAttachmentDownloader.js.map +1 -1
  23. package/dist/src/auth/authConfiguration.d.ts +2 -143
  24. package/dist/src/auth/authConfiguration.js +301 -257
  25. package/dist/src/auth/authConfiguration.js.map +1 -1
  26. package/dist/src/auth/jwt-middleware.js +2 -1
  27. package/dist/src/auth/jwt-middleware.js.map +1 -1
  28. package/dist/src/auth/msalConnectionManager.js +27 -14
  29. package/dist/src/auth/msalConnectionManager.js.map +1 -1
  30. package/dist/src/auth/msalTokenProvider.d.ts +7 -0
  31. package/dist/src/auth/msalTokenProvider.js +188 -44
  32. package/dist/src/auth/msalTokenProvider.js.map +1 -1
  33. package/dist/src/auth/settings.d.ts +327 -0
  34. package/dist/src/auth/settings.js +158 -0
  35. package/dist/src/auth/settings.js.map +1 -0
  36. package/dist/src/cloudAdapter.d.ts +58 -1
  37. package/dist/src/cloudAdapter.js +232 -52
  38. package/dist/src/cloudAdapter.js.map +1 -1
  39. package/dist/src/connector-client/connectorClient.d.ts +12 -8
  40. package/dist/src/connector-client/connectorClient.js +99 -82
  41. package/dist/src/connector-client/connectorClient.js.map +1 -1
  42. package/dist/src/errorHelper.js +85 -7
  43. package/dist/src/errorHelper.js.map +1 -1
  44. package/dist/src/getProductInfo.d.ts +23 -0
  45. package/dist/src/getProductInfo.js +71 -1
  46. package/dist/src/getProductInfo.js.map +1 -1
  47. package/dist/src/headerPropagation.d.ts +17 -2
  48. package/dist/src/headerPropagation.js +46 -24
  49. package/dist/src/headerPropagation.js.map +1 -1
  50. package/dist/src/httpClient.d.ts +60 -0
  51. package/dist/src/httpClient.js +173 -0
  52. package/dist/src/httpClient.js.map +1 -0
  53. package/dist/src/index.d.ts +2 -1
  54. package/dist/src/index.js +7 -2
  55. package/dist/src/index.js.map +1 -1
  56. package/dist/src/oauth/userTokenClient.d.ts +5 -4
  57. package/dist/src/oauth/userTokenClient.js +97 -78
  58. package/dist/src/oauth/userTokenClient.js.map +1 -1
  59. package/dist/src/observability/traces.d.ts +11 -4
  60. package/dist/src/observability/traces.js +39 -7
  61. package/dist/src/observability/traces.js.map +1 -1
  62. package/dist/src/utils/env.d.ts +50 -0
  63. package/dist/src/utils/env.js +113 -0
  64. package/dist/src/utils/env.js.map +1 -0
  65. package/dist/src/utils.d.ts +10 -0
  66. package/dist/src/utils.js +18 -0
  67. package/dist/src/utils.js.map +1 -0
  68. package/package.json +4 -9
  69. package/src/agent-client/agentClient.ts +2 -2
  70. package/src/agent-client/agentResponseHandler.ts +2 -2
  71. package/src/app/agentApplication.ts +30 -3
  72. package/src/app/agentApplicationOptions.ts +11 -0
  73. package/src/app/attachmentDownloader.ts +6 -8
  74. package/src/app/auth/authorizationManager.ts +84 -41
  75. package/src/app/auth/handlers/azureBotAuthorization.ts +2 -2
  76. package/src/app/proactive/createConversationOptionsBuilder.ts +8 -4
  77. package/src/app/proactive/proactive.ts +39 -23
  78. package/src/app/teamsAttachmentDownloader.ts +7 -11
  79. package/src/auth/authConfiguration.ts +317 -356
  80. package/src/auth/jwt-middleware.ts +1 -1
  81. package/src/auth/msalConnectionManager.ts +27 -10
  82. package/src/auth/msalTokenProvider.ts +180 -42
  83. package/src/auth/settings.ts +323 -0
  84. package/src/cloudAdapter.ts +291 -12
  85. package/src/connector-client/connectorClient.ts +116 -94
  86. package/src/errorHelper.ts +96 -7
  87. package/src/getProductInfo.ts +79 -1
  88. package/src/headerPropagation.ts +55 -23
  89. package/src/httpClient.ts +223 -0
  90. package/src/index.ts +5 -1
  91. package/src/oauth/userTokenClient.ts +103 -84
  92. package/src/observability/traces.ts +39 -8
  93. package/src/utils/env.ts +105 -0
  94. package/src/utils.ts +14 -0
@@ -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 * as uuid from 'uuid'
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
- let scope
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
- scope = tokenProvider.connectionSettings?.scope ?? ApxProductionScope
163
- const token = await tokenProvider.getAgenticUserToken(activity.getAgenticTenantId() ?? '', agenticInstanceId, agenticUserId, [scope])
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
- scope = identity.azp ?? identity.appid ?? 'https://api.botframework.com'
177
- const token = await tokenProvider.getAccessToken(scope)
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
- scope,
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 ?? uuid.v4()
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
  }