teams_rb 2.0.1 → 2.0.3
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +48 -1
- data/README.md +94 -434
- data/docs/essentials/app-authentication.md +14 -4
- data/docs/essentials/app-basics.md +2 -2
- data/docs/essentials/on-activity.md +27 -0
- data/docs/essentials/sending-messages.md +1 -0
- data/docs/getting-started/quickstart.md +6 -5
- data/docs/getting-started/running-in-teams.md +37 -14
- data/docs/in-depth-guides/README.md +1 -0
- data/docs/in-depth-guides/adaptive-cards.md +17 -0
- data/docs/in-depth-guides/agent-365.md +46 -0
- data/docs/in-depth-guides/user-authentication.md +1 -1
- data/lib/teams/activity.rb +5 -0
- data/lib/teams/activity_context.rb +17 -31
- data/lib/teams/api/account.rb +38 -0
- data/lib/teams/api/activity_value.rb +92 -0
- data/lib/teams/api/agentic_identity.rb +27 -0
- data/lib/teams/api/app_info.rb +15 -0
- data/lib/teams/api/channel_data.rb +5 -0
- data/lib/teams/api/client.rb +26 -2
- data/lib/teams/api/conversation_client.rb +3 -1
- data/lib/teams/api/message_activity.rb +1 -0
- data/lib/teams/api/search.rb +41 -0
- data/lib/teams/app.rb +184 -21
- data/lib/teams/auth/jwt_validator.rb +173 -68
- data/lib/teams/auth/token_manager.rb +132 -0
- data/lib/teams/cloud_environment.rb +5 -1
- data/lib/teams/http_stream.rb +8 -3
- data/lib/teams/router.rb +71 -0
- data/lib/teams/version.rb +1 -1
- data/lib/teams.rb +3 -0
- metadata +20 -2
data/lib/teams/api/client.rb
CHANGED
|
@@ -12,17 +12,41 @@ module Teams
|
|
|
12
12
|
# Sub-clients are constructed eagerly like the other SDKs' ApiClient
|
|
13
13
|
# constructors, which also keeps the shared client thread-safe.
|
|
14
14
|
# oauth_url is the Bot Framework token service (cloud-dependent), used
|
|
15
|
-
# by the user token and bot sign-in clients.
|
|
16
|
-
|
|
15
|
+
# by the user token and bot sign-in clients. token_provider is a
|
|
16
|
+
# callable(agentic_identity) returning a bearer token; it enables
|
|
17
|
+
# for_agentic_identity scoping (the App wires it to its TokenManager).
|
|
18
|
+
def initialize(service_url:, http:, logger: nil, oauth_url: DEFAULT_OAUTH_URL, token_provider: nil)
|
|
17
19
|
@service_url = service_url.sub(%r{/+\z}, "")
|
|
18
20
|
@http = http
|
|
19
21
|
@logger = logger
|
|
22
|
+
@oauth_url = oauth_url
|
|
23
|
+
@token_provider = token_provider
|
|
20
24
|
@conversations = ConversationClient.new(service_url: @service_url, http:, logger:)
|
|
21
25
|
@teams = TeamClient.new(service_url: @service_url, http:, logger:)
|
|
22
26
|
@meetings = MeetingClient.new(service_url: @service_url, http:, logger:)
|
|
23
27
|
@users = UserClient.new(oauth_url:, http:, logger:)
|
|
24
28
|
@bots = BotClient.new(oauth_url:, http:, logger:)
|
|
25
29
|
end
|
|
30
|
+
|
|
31
|
+
# Returns a client scoped to an Agent 365 identity (and optionally a
|
|
32
|
+
# different service URL): requests authenticate with the identity's
|
|
33
|
+
# agentic token instead of the app token. The other SDKs call this
|
|
34
|
+
# clone(); Ruby avoids shadowing Object#clone, like post vs send.
|
|
35
|
+
# A nil identity scopes back to the app token.
|
|
36
|
+
def for_agentic_identity(agentic_identity, service_url: nil)
|
|
37
|
+
unless @token_provider
|
|
38
|
+
raise Error, "This API client was constructed without a token provider; agentic scoping is unavailable."
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
identity = agentic_identity
|
|
42
|
+
self.class.new(
|
|
43
|
+
service_url: service_url || @service_url,
|
|
44
|
+
http: Common::HttpClient.new(token: -> { @token_provider.call(identity) }),
|
|
45
|
+
logger: @logger,
|
|
46
|
+
oauth_url: @oauth_url,
|
|
47
|
+
token_provider: @token_provider
|
|
48
|
+
)
|
|
49
|
+
end
|
|
26
50
|
end
|
|
27
51
|
end
|
|
28
52
|
end
|
|
@@ -41,7 +41,9 @@ module Teams
|
|
|
41
41
|
def reply_to_activity(conversation_id, activity_id, activity, service_url: nil)
|
|
42
42
|
body = activity_to_h(activity)
|
|
43
43
|
body = body.merge("replyToId" => activity_id) if body.is_a?(Hash)
|
|
44
|
-
|
|
44
|
+
# Replies POST to the base activities collection; the reply
|
|
45
|
+
# semantics ride entirely in the body's replyToId.
|
|
46
|
+
path = "/v3/conversations/#{escape(conversation_id)}/activities"
|
|
45
47
|
url = absolute(path, service_url:)
|
|
46
48
|
@logger&.debug("Teams API POST #{url}")
|
|
47
49
|
http.post(url, json: body)
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
module Teams
|
|
4
4
|
module Api
|
|
5
5
|
class MessageActivity
|
|
6
|
+
# "extendedmarkdown" is in public preview and may be subject to change.
|
|
6
7
|
TEXT_FORMATS = %w[plain markdown xml extendedmarkdown].freeze
|
|
7
8
|
FEEDBACK_MODES = %w[default custom].freeze
|
|
8
9
|
AI_MESSAGE_ENTITY_TYPE = "https://schema.org/Message"
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Teams
|
|
4
|
+
module Api
|
|
5
|
+
# A single application/search result: title is the display text, value
|
|
6
|
+
# is submitted when the user selects it.
|
|
7
|
+
class SearchInvokeResult
|
|
8
|
+
def initialize(title:, value:)
|
|
9
|
+
@title = title
|
|
10
|
+
@value = value
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to_h
|
|
14
|
+
{ "title" => @title, "value" => @value }
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Response body for application/search invokes (Adaptive Card dynamic
|
|
19
|
+
# typeahead Input.ChoiceSet queries). results accepts SearchInvokeResult
|
|
20
|
+
# objects or {title:, value:} hashes.
|
|
21
|
+
class SearchResponse
|
|
22
|
+
def initialize(results, status_code: 200)
|
|
23
|
+
@results = results
|
|
24
|
+
@status_code = status_code
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_h
|
|
28
|
+
{
|
|
29
|
+
"statusCode" => @status_code,
|
|
30
|
+
"type" => "application/vnd.microsoft.search.searchResponse",
|
|
31
|
+
"value" => {
|
|
32
|
+
"results" => Array(@results).map do |result|
|
|
33
|
+
body = result.is_a?(Hash) ? result : result.to_h
|
|
34
|
+
Common::Hashes.deep_stringify_keys(body)
|
|
35
|
+
end
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
data/lib/teams/app.rb
CHANGED
|
@@ -5,6 +5,10 @@ require "logger"
|
|
|
5
5
|
module Teams
|
|
6
6
|
class App
|
|
7
7
|
DEFAULT_MESSAGING_ENDPOINT = "/api/messages"
|
|
8
|
+
DANGEROUSLY_ALLOW_UNAUTHENTICATED_REQUESTS_ENV_VAR = "DANGEROUSLY_ALLOW_UNAUTHENTICATED_REQUESTS"
|
|
9
|
+
MAX_PROCESSED_EXCHANGE_IDS = 1000
|
|
10
|
+
TRUE_ENV_VALUES = %w[1 true yes on].freeze
|
|
11
|
+
FALSE_ENV_VALUES = %w[0 false no off].freeze
|
|
8
12
|
|
|
9
13
|
attr_reader :api, :logger, :storage, :messaging_endpoint, :default_connection_name, :graph
|
|
10
14
|
|
|
@@ -22,7 +26,8 @@ module Teams
|
|
|
22
26
|
storage: Storage::MemoryStore.new,
|
|
23
27
|
api: nil,
|
|
24
28
|
token_manager: nil,
|
|
25
|
-
|
|
29
|
+
dangerously_allow_unauthenticated_requests: nil,
|
|
30
|
+
skip_auth: nil,
|
|
26
31
|
messaging_endpoint: DEFAULT_MESSAGING_ENDPOINT,
|
|
27
32
|
default_connection_name: "graph"
|
|
28
33
|
)
|
|
@@ -33,7 +38,9 @@ module Teams
|
|
|
33
38
|
@logger = logger
|
|
34
39
|
@storage = storage
|
|
35
40
|
@cloud = cloud
|
|
36
|
-
@
|
|
41
|
+
@dangerously_allow_unauthenticated_requests = resolve_unauthenticated_requests_option(
|
|
42
|
+
dangerously_allow_unauthenticated_requests, skip_auth
|
|
43
|
+
)
|
|
37
44
|
@messaging_endpoint = normalize_messaging_endpoint(messaging_endpoint)
|
|
38
45
|
@router = Router.new
|
|
39
46
|
|
|
@@ -42,7 +49,8 @@ module Teams
|
|
|
42
49
|
service_url:,
|
|
43
50
|
http: Common::HttpClient.new(token: -> { @token_manager.bot_token }),
|
|
44
51
|
logger:,
|
|
45
|
-
oauth_url: cloud.token_service_url
|
|
52
|
+
oauth_url: cloud.token_service_url,
|
|
53
|
+
token_provider: method(:token_for_agentic_identity)
|
|
46
54
|
)
|
|
47
55
|
@jwt_validator = Auth::JwtValidator.new(
|
|
48
56
|
client_id: @token_manager.client_id,
|
|
@@ -140,6 +148,14 @@ module Teams
|
|
|
140
148
|
self
|
|
141
149
|
end
|
|
142
150
|
|
|
151
|
+
# application/search invokes from Adaptive Card dynamic typeahead
|
|
152
|
+
# Input.ChoiceSet queries; the handler's return (Api::SearchResponse or
|
|
153
|
+
# hash) becomes the invoke response body.
|
|
154
|
+
def on_card_search(&block)
|
|
155
|
+
@router.on_card_search(&block)
|
|
156
|
+
self
|
|
157
|
+
end
|
|
158
|
+
|
|
143
159
|
# Called with (ctx, token_response) whenever a sign-in completes through
|
|
144
160
|
# the default token-exchange or verify-state handlers.
|
|
145
161
|
def on_sign_in(&block)
|
|
@@ -206,6 +222,36 @@ module Teams
|
|
|
206
222
|
self
|
|
207
223
|
end
|
|
208
224
|
|
|
225
|
+
# conversationUpdate activities plus their channel/team lifecycle
|
|
226
|
+
# sub-events (on_channel_created, on_team_renamed, ...), routed by
|
|
227
|
+
# channelData.eventType with the Python method names.
|
|
228
|
+
def on_conversation_update(&block)
|
|
229
|
+
@router.on_conversation_update(&block)
|
|
230
|
+
self
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
Router::CONVERSATION_UPDATE_EVENTS.each_key do |method_name|
|
|
234
|
+
define_method(method_name) do |&block|
|
|
235
|
+
@router.public_send(method_name, &block)
|
|
236
|
+
self
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# Agent 365 agentLifecycle events plus their per-variant routes
|
|
241
|
+
# (on_agentic_user_identity_created, on_agentic_user_enabled, ...),
|
|
242
|
+
# discriminated by the activity-level valueType.
|
|
243
|
+
def on_agent_lifecycle(&block)
|
|
244
|
+
@router.on_agent_lifecycle(&block)
|
|
245
|
+
self
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
Router::AGENT_LIFECYCLE_EVENTS.each_key do |method_name|
|
|
249
|
+
define_method(method_name) do |&block|
|
|
250
|
+
@router.public_send(method_name, &block)
|
|
251
|
+
self
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
209
255
|
# Message extension handlers (on_message_ext_query, on_message_ext_submit,
|
|
210
256
|
# on_message_ext_open, ...) route the composeExtension/* invokes with the
|
|
211
257
|
# TypeScript/Python route names. Handler return values (typed responses
|
|
@@ -237,11 +283,17 @@ module Teams
|
|
|
237
283
|
validate_inbound!(env, activity)
|
|
238
284
|
|
|
239
285
|
conversation_reference = Api::ConversationReference.from_activity(activity)
|
|
286
|
+
# Agentic activities carry the identity on the recipient account; the
|
|
287
|
+
# whole turn (context and stream) is scoped to it so replies
|
|
288
|
+
# authenticate as the agent, like the other SDKs' per-turn clients.
|
|
289
|
+
agentic_identity = activity.recipient.agentic_identity
|
|
290
|
+
turn_api = agentic_identity ? api.for_agentic_identity(agentic_identity) : api
|
|
240
291
|
context = ActivityContext.new(
|
|
241
292
|
app: self,
|
|
242
293
|
activity:,
|
|
243
294
|
conversation_reference:,
|
|
244
|
-
|
|
295
|
+
api: turn_api,
|
|
296
|
+
stream: HttpStream.new(app: self, conversation_reference:, api: turn_api)
|
|
245
297
|
)
|
|
246
298
|
result = run_handlers(context)
|
|
247
299
|
context.stream.close
|
|
@@ -254,21 +306,34 @@ module Teams
|
|
|
254
306
|
Response.new(status: 200)
|
|
255
307
|
end
|
|
256
308
|
|
|
309
|
+
# Builds an Api::AgenticIdentity for proactive agentic operations: the
|
|
310
|
+
# blueprint defaults to this app's client id and the tenant to the
|
|
311
|
+
# configured credentials tenant, like the Python SDK.
|
|
312
|
+
def agentic_identity(agentic_app_id: nil, agentic_user_id: nil, tenant_id: nil, agentic_app_blueprint_id: nil)
|
|
313
|
+
Api::AgenticIdentity.new(
|
|
314
|
+
agentic_app_blueprint_id: agentic_app_blueprint_id || client_id,
|
|
315
|
+
agentic_app_id:,
|
|
316
|
+
agentic_user_id:,
|
|
317
|
+
tenant_id: tenant_id || @token_manager.credentials&.tenant_id
|
|
318
|
+
)
|
|
319
|
+
end
|
|
320
|
+
|
|
257
321
|
# The TypeScript, Python, and .NET SDKs call this operation `send`.
|
|
258
322
|
# Ruby already defines Object#send for dynamic dispatch, so the public
|
|
259
323
|
# Ruby API uses `post` to avoid shadowing a core language method.
|
|
260
|
-
def post(conversation_id, activity_or_text, service_url: nil)
|
|
324
|
+
def post(conversation_id, activity_or_text, service_url: nil, agentic_identity: nil)
|
|
261
325
|
assert_string!(conversation_id, "conversation_id")
|
|
262
326
|
|
|
263
327
|
send_activity(
|
|
264
328
|
proactive_reference(conversation_id, service_url:),
|
|
265
|
-
activity_or_text
|
|
329
|
+
activity_or_text,
|
|
330
|
+
api: agentic_identity ? api.for_agentic_identity(agentic_identity) : api
|
|
266
331
|
)
|
|
267
332
|
end
|
|
268
333
|
|
|
269
334
|
# Proactive threaded replies use a ";messageid=" conversation ID like the
|
|
270
335
|
# TypeScript and Python SDKs; the service decides whether threading applies.
|
|
271
|
-
def reply(conversation_id, activity_id_or_activity, activity_or_text = nil, service_url: nil)
|
|
336
|
+
def reply(conversation_id, activity_id_or_activity, activity_or_text = nil, service_url: nil, agentic_identity: nil)
|
|
272
337
|
assert_string!(conversation_id, "conversation_id")
|
|
273
338
|
|
|
274
339
|
if activity_or_text
|
|
@@ -277,23 +342,24 @@ module Teams
|
|
|
277
342
|
post(
|
|
278
343
|
Teams.to_threaded_conversation_id(conversation_id, activity_id_or_activity),
|
|
279
344
|
activity_or_text,
|
|
280
|
-
service_url
|
|
345
|
+
service_url:,
|
|
346
|
+
agentic_identity:
|
|
281
347
|
)
|
|
282
348
|
else
|
|
283
|
-
post(conversation_id, activity_id_or_activity, service_url:)
|
|
349
|
+
post(conversation_id, activity_id_or_activity, service_url:, agentic_identity:)
|
|
284
350
|
end
|
|
285
351
|
end
|
|
286
352
|
|
|
287
353
|
# Sugar over post: the SDKs update by sending an activity that already
|
|
288
354
|
# carries an id, and post does exactly that. See AGENTS.md.
|
|
289
|
-
def update(conversation_id, activity_id, activity_or_text, service_url: nil)
|
|
355
|
+
def update(conversation_id, activity_id, activity_or_text, service_url: nil, agentic_identity: nil)
|
|
290
356
|
assert_string!(conversation_id, "conversation_id")
|
|
291
357
|
assert_string!(activity_id, "activity_id")
|
|
292
358
|
|
|
293
|
-
post(conversation_id, activity_with_id(activity_id, activity_or_text), service_url:)
|
|
359
|
+
post(conversation_id, activity_with_id(activity_id, activity_or_text), service_url:, agentic_identity:)
|
|
294
360
|
end
|
|
295
361
|
|
|
296
|
-
def send_activity(conversation_reference, activity_or_text)
|
|
362
|
+
def send_activity(conversation_reference, activity_or_text, api: self.api)
|
|
297
363
|
activity = activity_for_reference(conversation_reference, activity_or_text)
|
|
298
364
|
targeted = targeted_activity?(activity)
|
|
299
365
|
|
|
@@ -326,7 +392,8 @@ module Teams
|
|
|
326
392
|
# ending in ctx.post must not leak the SentActivity into the response.
|
|
327
393
|
def invoke_response_body(result)
|
|
328
394
|
case result
|
|
329
|
-
when Api::TaskModuleResponse, Api::MessagingExtensionResponse, Api::MessagingExtensionActionResponse
|
|
395
|
+
when Api::TaskModuleResponse, Api::MessagingExtensionResponse, Api::MessagingExtensionActionResponse,
|
|
396
|
+
Api::SearchResponse
|
|
330
397
|
result.to_h
|
|
331
398
|
when Hash
|
|
332
399
|
Common::Hashes.deep_stringify_keys(result)
|
|
@@ -396,7 +463,18 @@ module Teams
|
|
|
396
463
|
def register_default_oauth_handlers
|
|
397
464
|
app = self
|
|
398
465
|
|
|
399
|
-
|
|
466
|
+
# Each Teams client (desktop, web, mobile) that renders the OAuth card
|
|
467
|
+
# attempts its own silent exchange, so one sign-in can produce several
|
|
468
|
+
# signin/tokenExchange invokes sharing the same value id. Only the first
|
|
469
|
+
# runs the exchange and fires sign-in; a duplicate of a completed
|
|
470
|
+
# exchange answers 200 without re-emitting or chaining, and a duplicate
|
|
471
|
+
# arriving while the first is in flight waits and returns its result.
|
|
472
|
+
# Failed exchanges are not marked, so each attempt gets its own 412.
|
|
473
|
+
exchange_mutex = Mutex.new
|
|
474
|
+
processed_exchange_ids = []
|
|
475
|
+
exchange_locks = {}
|
|
476
|
+
|
|
477
|
+
perform_token_exchange = lambda do |ctx, nxt|
|
|
400
478
|
value = ctx.activity.value
|
|
401
479
|
begin
|
|
402
480
|
if value.raw["connectionName"] != app.default_connection_name
|
|
@@ -437,6 +515,43 @@ module Teams
|
|
|
437
515
|
end
|
|
438
516
|
end
|
|
439
517
|
|
|
518
|
+
@router.on_signin_token_exchange do |ctx, nxt|
|
|
519
|
+
exchange_id = ctx.activity.value.raw["id"]
|
|
520
|
+
next perform_token_exchange.call(ctx, nxt) unless exchange_id
|
|
521
|
+
|
|
522
|
+
lock = nil
|
|
523
|
+
duplicate = exchange_mutex.synchronize do
|
|
524
|
+
if processed_exchange_ids.include?(exchange_id)
|
|
525
|
+
true
|
|
526
|
+
else
|
|
527
|
+
lock = (exchange_locks[exchange_id] ||= { mutex: Mutex.new, settled: false, result: nil })
|
|
528
|
+
false
|
|
529
|
+
end
|
|
530
|
+
end
|
|
531
|
+
next Response.new(status: 200) if duplicate
|
|
532
|
+
|
|
533
|
+
outcome = lock[:mutex].synchronize do
|
|
534
|
+
if lock[:settled]
|
|
535
|
+
lock[:result]
|
|
536
|
+
else
|
|
537
|
+
result = perform_token_exchange.call(ctx, nxt)
|
|
538
|
+
if result.nil?
|
|
539
|
+
exchange_mutex.synchronize do
|
|
540
|
+
processed_exchange_ids << exchange_id
|
|
541
|
+
processed_exchange_ids.shift while processed_exchange_ids.length > MAX_PROCESSED_EXCHANGE_IDS
|
|
542
|
+
end
|
|
543
|
+
lock[:result] = Response.new(status: 200)
|
|
544
|
+
else
|
|
545
|
+
lock[:result] = result
|
|
546
|
+
end
|
|
547
|
+
lock[:settled] = true
|
|
548
|
+
result
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
exchange_mutex.synchronize { exchange_locks.delete(exchange_id) }
|
|
552
|
+
outcome
|
|
553
|
+
end
|
|
554
|
+
|
|
440
555
|
@router.on_signin_verify_state do |ctx, nxt|
|
|
441
556
|
begin
|
|
442
557
|
state = ctx.activity.value.raw["state"]
|
|
@@ -487,32 +602,80 @@ module Teams
|
|
|
487
602
|
end
|
|
488
603
|
end
|
|
489
604
|
|
|
605
|
+
# Resolves the auth-bypass setting like the Python SDK: the explicit
|
|
606
|
+
# option wins, then the deprecated skip_auth alias, then the env var,
|
|
607
|
+
# defaulting to false. Passing skip_auth always warns, even when the
|
|
608
|
+
# new option overrides it.
|
|
609
|
+
def resolve_unauthenticated_requests_option(explicit, skip_auth)
|
|
610
|
+
unless skip_auth.nil?
|
|
611
|
+
warn("skip_auth is deprecated; use dangerously_allow_unauthenticated_requests instead.", uplevel: 2)
|
|
612
|
+
end
|
|
613
|
+
return explicit unless explicit.nil?
|
|
614
|
+
return skip_auth unless skip_auth.nil?
|
|
615
|
+
|
|
616
|
+
parse_bool_env_var(DANGEROUSLY_ALLOW_UNAUTHENTICATED_REQUESTS_ENV_VAR) || false
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
def parse_bool_env_var(name)
|
|
620
|
+
value = ENV[name].to_s.strip.downcase
|
|
621
|
+
return nil if value.empty?
|
|
622
|
+
return true if TRUE_ENV_VALUES.include?(value)
|
|
623
|
+
return false if FALSE_ENV_VALUES.include?(value)
|
|
624
|
+
|
|
625
|
+
raise ArgumentError, "#{name} must be a boolean value: true/false, 1/0, yes/no, or on/off."
|
|
626
|
+
end
|
|
627
|
+
|
|
490
628
|
# The same two startup warnings the TypeScript, Python, and .NET SDKs
|
|
491
629
|
# log when no credentials are configured. Settled 2026-07-12: exact
|
|
492
|
-
# upstream branches only; credentials-plus-
|
|
630
|
+
# upstream branches only; credentials-plus-bypass stays silent like
|
|
493
631
|
# the other SDKs.
|
|
494
632
|
def warn_missing_credentials
|
|
495
633
|
return if @token_manager.client_id
|
|
496
634
|
|
|
497
|
-
if @
|
|
635
|
+
if @dangerously_allow_unauthenticated_requests
|
|
498
636
|
logger&.warn(
|
|
499
637
|
"No credentials configured (CLIENT_ID / CLIENT_SECRET / TENANT_ID), " \
|
|
500
|
-
"but
|
|
638
|
+
"but dangerously_allow_unauthenticated_requests is enabled. " \
|
|
639
|
+
"Bot will accept unauthenticated requests on #{@messaging_endpoint}."
|
|
501
640
|
)
|
|
502
641
|
else
|
|
503
642
|
logger&.warn(
|
|
504
|
-
"No credentials configured and
|
|
505
|
-
"Configure client authentication to securely receive
|
|
643
|
+
"No credentials configured and dangerously_allow_unauthenticated_requests is not enabled. " \
|
|
644
|
+
"All incoming requests will be rejected. Configure client authentication to securely receive " \
|
|
645
|
+
"messages, or set dangerously_allow_unauthenticated_requests: true for local development."
|
|
646
|
+
)
|
|
647
|
+
end
|
|
648
|
+
end
|
|
649
|
+
|
|
650
|
+
# Token dispatch for scoped API clients: no identity uses the app
|
|
651
|
+
# token; an identity with an agentic user uses the agentic user token;
|
|
652
|
+
# otherwise the agentic app token. Kinds never fall back to each other -
|
|
653
|
+
# failing is safer than authenticating under the wrong identity.
|
|
654
|
+
def token_for_agentic_identity(identity)
|
|
655
|
+
return @token_manager.bot_token unless identity
|
|
656
|
+
|
|
657
|
+
if identity.agentic_user_id
|
|
658
|
+
@token_manager.agentic_user_token(
|
|
659
|
+
@cloud.agent_bot_scope,
|
|
660
|
+
agentic_app_id: identity.agentic_app_id,
|
|
661
|
+
agentic_user_id: identity.agentic_user_id,
|
|
662
|
+
tenant_id: identity.tenant_id
|
|
663
|
+
)
|
|
664
|
+
else
|
|
665
|
+
@token_manager.agentic_app_token(
|
|
666
|
+
@cloud.agent_bot_scope,
|
|
667
|
+
agentic_app_id: identity.agentic_app_id,
|
|
668
|
+
tenant_id: identity.tenant_id
|
|
506
669
|
)
|
|
507
670
|
end
|
|
508
671
|
end
|
|
509
672
|
|
|
510
673
|
def validate_inbound!(env, activity)
|
|
511
|
-
return if @
|
|
674
|
+
return if @dangerously_allow_unauthenticated_requests
|
|
512
675
|
|
|
513
676
|
raise AuthenticationError, "CLIENT_ID is required for inbound validation" unless @jwt_validator
|
|
514
677
|
|
|
515
|
-
@jwt_validator.
|
|
678
|
+
@jwt_validator.validate_inbound_activity!(env["HTTP_AUTHORIZATION"], service_url: activity.service_url)
|
|
516
679
|
end
|
|
517
680
|
|
|
518
681
|
def run_handlers(context)
|