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.
@@ -10,12 +10,12 @@
10
10
  | `CLIENT_SECRET` | `client_secret:` | production | Client secret for the client-credentials flow |
11
11
  | `TENANT_ID` | `tenant_id:` | single-tenant | Entra tenant for bot tokens and tenant-issuer validation |
12
12
  | `SERVICE_URL` | `service_url:` | no | Default Bot Framework URL for proactive sends (inbound requests always use the activity's own service URL) |
13
- | — | `skip_auth:` | no | Disables inbound validation — local development only |
13
+ | `DANGEROUSLY_ALLOW_UNAUTHENTICATED_REQUESTS` | `dangerously_allow_unauthenticated_requests:` | no | Disables inbound validation — local development only (`skip_auth:` is a deprecated alias) |
14
14
  | — | `messaging_endpoint:` | no | Inbound path, default `/api/messages` |
15
15
  | — | `default_connection_name:` | no | OAuth connection name for user sign-in, default `"graph"` |
16
16
  | — | `logger:`, `storage:`, `cloud:` | no | Logger (stdout default), state store (in-memory default), cloud environment for sovereign clouds |
17
17
 
18
- Without credentials the app logs a startup warning and rejects every inbound request unless `skip_auth: true` was set explicitly — the same behavior as the TypeScript, Python, and .NET SDKs.
18
+ Without credentials the app logs a startup warning and rejects every inbound request unless `dangerously_allow_unauthenticated_requests: true` was set explicitly — the same behavior as the TypeScript, Python, and .NET SDKs.
19
19
 
20
20
  ## One app instance
21
21
 
@@ -32,6 +32,33 @@ Each invoke family has named routes — their handler return values become the i
32
32
  - Feedback: `on_message_submit_feedback` (thumbs up/down from `add_feedback` — [guide](../in-depth-guides/feedback.md)), `on_message_submit` for any `message/submitAction`
33
33
  - `on_suggested_action_submit` for suggested-action submissions
34
34
 
35
+ ## Conversation updates
36
+
37
+ `on_conversation_update` matches any `conversationUpdate` activity (members added/removed, channel and team changes). The channel/team lifecycle sub-events also have named routes, matched on `channelData.eventType`:
38
+
39
+ ```ruby
40
+ teams.on_conversation_update { |ctx| } # any conversationUpdate activity
41
+ teams.on_channel_created { |ctx| ctx.post "Welcome to #{ctx.activity.channel_data.channel.name}!" }
42
+ teams.on_channel_deleted { |ctx| } # also: on_channel_renamed, on_channel_restored
43
+ teams.on_team_renamed { |ctx| } # also: on_team_archived, on_team_unarchived,
44
+ # on_team_deleted, on_team_hard_deleted, on_team_restored
45
+ ```
46
+
47
+ A generic `on_conversation_update` registered before a specific route sees the activity first; declare `|ctx, nxt|` and call `nxt.call` to continue to the specific route (see Middleware below).
48
+
49
+ ## Agent 365 lifecycle events
50
+
51
+ `on_agent_lifecycle` matches any `agentLifecycle` event (sent when this app runs as an Agent 365 agentic user); the variants also have named routes, matched on the activity's `valueType`:
52
+
53
+ ```ruby
54
+ teams.on_agent_lifecycle { |ctx| } # any agentLifecycle event
55
+ teams.on_agentic_user_identity_created { |ctx| } # also: identity_updated, manager_updated,
56
+ teams.on_agentic_user_enabled { |ctx| } # disabled, deleted, undeleted,
57
+ # workload_onboarding_updated
58
+ ```
59
+
60
+ The event value exposes `tenant_id`, `agentic_user_id`, `agentic_app_instance_id`, `agent_identity_blueprint_id`, `version`, and per-variant fields (`manager`, `deletion_reason`, `workload_name`, …).
61
+
35
62
  ## Meeting events
36
63
 
37
64
  ```ruby
@@ -32,6 +32,7 @@ ctx.update sent.id, "Done!"
32
32
  ```ruby
33
33
  ctx.post Teams::Api::MessageActivity.new("**markdown**", text_format: "markdown")
34
34
  # text_format: plain | markdown | xml | extendedmarkdown
35
+ # ("extendedmarkdown" is in public preview and may be subject to change)
35
36
 
36
37
  ctx.post Teams::Api::MessageActivity.new("Quarterly numbers")
37
38
  .add_ai_generated # "AI generated" badge
@@ -5,16 +5,17 @@ Build and run your first Teams bot in Ruby.
5
5
  ## Prerequisites
6
6
 
7
7
  - Ruby 4.0+
8
- - A Microsoft 365 tenant where you can register a Teams app ([developer program](https://developer.microsoft.com/microsoft-365/dev-program) tenants work)
8
+ - A Microsoft 365 tenant with custom app upload enabled ([developer program](https://developer.microsoft.com/microsoft-365/dev-program) tenants work)
9
9
  - A tunnel for local development ([Dev Tunnels](https://learn.microsoft.com/azure/developer/dev-tunnels/get-started), ngrok, or similar)
10
10
 
11
11
  ## Install
12
12
 
13
- Add the gem to your project:
13
+ Add the SDK and a Rack server for this standalone example:
14
14
 
15
15
  ```ruby
16
16
  # Gemfile
17
17
  gem "teams_rb"
18
+ gem "puma"
18
19
  ```
19
20
 
20
21
  ```sh
@@ -38,15 +39,15 @@ end
38
39
  run teams.to_rack
39
40
  ```
40
41
 
41
- And run it:
42
+ This example uses Puma; `teams_rb` itself does not require a server gem. Run it with:
42
43
 
43
44
  ```sh
44
- CLIENT_ID=... CLIENT_SECRET=... TENANT_ID=... bundle exec rackup -p 3978 -o 0.0.0.0
45
+ CLIENT_ID=... CLIENT_SECRET=... TENANT_ID=... bundle exec puma -p 3978
45
46
  ```
46
47
 
47
48
  The app listens for Teams activities on `POST /api/messages`, validates every inbound request against Microsoft's signing keys, and echoes any message back as a quoted reply.
48
49
 
49
- The three environment variables come from your bot registration — [Running in Teams](running-in-teams.md) walks through creating one and connecting Teams to your locally running bot. For local experiments without credentials, `Teams::App.new(skip_auth: true)` disables inbound validation (never use it beyond local testing; the app logs a loud warning when you do).
50
+ The three environment variables come from your bot registration — [Running in Teams](running-in-teams.md) walks through creating one and connecting Teams to your locally running bot. For local experiments without credentials, `Teams::App.new(dangerously_allow_unauthenticated_requests: true)` disables inbound validation (never use it beyond local testing; the app logs a loud warning when you do). The `DANGEROUSLY_ALLOW_UNAUTHENTICATED_REQUESTS` environment variable sets the same option.
50
51
 
51
52
  ## Where to go next
52
53
 
@@ -1,16 +1,18 @@
1
1
  # Running in Teams
2
2
 
3
- Connect your locally running bot to a real Teams client.
3
+ Connect your locally running bot to a real Teams client with the [Teams CLI](https://www.npmjs.com/package/@microsoft/teams.cli).
4
4
 
5
- ## 1. Register a bot
5
+ ## 1. Install and sign in
6
6
 
7
- Create a Teams app with a bot in the [Teams Developer Portal](https://dev.teams.microsoft.com) (or with Microsoft's Teams CLI). You need three values for the app's environment:
7
+ The CLI requires Node.js 20 or newer:
8
8
 
9
- - `CLIENT_ID` — the bot's Microsoft App ID
10
- - `CLIENT_SECRET` — a client secret for that app registration
11
- - `TENANT_ID` — your Entra tenant ID (single-tenant bots)
9
+ ```sh
10
+ npm install -g @microsoft/teams.cli
11
+ teams login
12
+ teams status
13
+ ```
12
14
 
13
- > If you plan to use [user authentication](../in-depth-guides/user-authentication.md), register the bot as an **Azure Bot resource** from the start (Azure portal → Create resource → Azure Bot → "Use existing app registration"). OAuth connection settings only exist there, and converting a Developer Portal registration later means deleting and recreating it.
15
+ `teams status` should report that sideloading is enabled. If it is disabled, your tenant administrator must enable custom app upload before you can install the bot.
14
16
 
15
17
  ## 2. Start a tunnel
16
18
 
@@ -22,23 +24,44 @@ devtunnel port create teams-bot -p 3978
22
24
  devtunnel host teams-bot
23
25
  ```
24
26
 
25
- Note the tunnel URL, e.g. `https://abc123-3978.euw.devtunnels.ms`. Dev Tunnels URLs are stable across restarts, so this is one-time setup.
27
+ Note the tunnel URL, such as `https://abc123-3978.euw.devtunnels.ms`. Dev Tunnels URLs are stable across restarts, so creation is a one-time step.
28
+
29
+ ## 3. Register the app and bot
26
30
 
27
- ## 3. Point the bot at the tunnel
31
+ From your project directory, pass the public endpoint to the CLI:
32
+
33
+ ```sh
34
+ teams app create \
35
+ --name my-teams-ruby-bot \
36
+ --endpoint https://<your-tunnel>/api/messages
37
+ ```
28
38
 
29
- In the bot registration, set the **messaging endpoint** to `https://<your-tunnel>/api/messages`, and make sure the **Microsoft Teams channel** is enabled.
39
+ The CLI creates the app registration, manifest, bot, and Teams app. It prints the Teams App ID, an **Install in Teams** link, and the three values your Ruby app needs:
40
+
41
+ - `CLIENT_ID` — the bot's Microsoft App ID
42
+ - `CLIENT_SECRET` — the bot's client secret
43
+ - `TENANT_ID` — the Entra tenant ID
44
+
45
+ > If you plan to use [user authentication](../in-depth-guides/user-authentication.md), create an Azure-hosted bot with the CLI's `--azure`, `--subscription`, and `--resource-group` options. OAuth connection settings are not available for Teams-managed bots.
46
+
47
+ You can instead configure the same resources manually in the [Teams Developer Portal](https://dev.teams.microsoft.com/apps).
30
48
 
31
49
  ## 4. Run and install
32
50
 
51
+ Start the bot with the credentials printed by the CLI:
52
+
33
53
  ```sh
34
- bundle exec rackup -p 3978 -o 0.0.0.0
54
+ CLIENT_ID=... CLIENT_SECRET=... TENANT_ID=... bundle exec puma -p 3978
35
55
  ```
36
56
 
37
- Install the app in Teams (Developer Portal → Preview in Teams) and send it a message. Inbound requests are JWT-validated with your real credentials — no `skip_auth` needed behind a tunnel.
57
+ Open the **Install in Teams** link and send the bot a message. If you need the link again, run `teams app get <teams-app-id> --install-link`.
58
+
59
+ Inbound requests are JWT-validated with your real credentials; no `dangerously_allow_unauthenticated_requests` is needed behind the tunnel.
38
60
 
39
61
  ## Troubleshooting
40
62
 
41
- - **No response in Teams**: check the messaging endpoint URL, the Teams channel, and that the tunnel and server are both running. The app logs every inbound activity at debug level and every rejected request at warn level.
42
- - **401s in your logs**: the requests are reaching you but failing validation — usually a `CLIENT_ID`/`TENANT_ID` mismatch with the registration.
63
+ - **No response in Teams**: run `teams app doctor <teams-app-id>`, then check that the tunnel and server are running.
64
+ - **401s in your logs**: the requests are reaching you but failing validation, usually because the running app has credentials from a different registration.
65
+ - **Sideloading disabled**: ask your tenant administrator to enable custom app upload.
43
66
  - **Teams caches aggressively**: after manifest changes, fully quit and reopen the Teams client.
44
67
  - **At-least-once delivery**: Bot Framework redelivers activities when your bot errors or responds slowly. If a handler's side effects must not repeat, deduplicate by `ctx.activity.id`.
@@ -10,5 +10,6 @@ Feature guides for building richer Teams apps:
10
10
  - [Streaming](streaming.md) — chunked responses, informative updates, events, typed errors
11
11
  - [User authentication](user-authentication.md) — OAuth sign-in, token exchange, Azure setup
12
12
  - [Tabs and remote functions](tabs.md) — calling the bot backend from a tab with SSO
13
+ - [Agent 365](agent-365.md) — running as an agentic user (experimental)
13
14
  - [Meeting events](meeting-events.md) — meeting start/end
14
15
  - [Observability](observability.md) — logging and middleware
@@ -48,6 +48,23 @@ end
48
48
 
49
49
  For richer action flows — opening a modal form from a card button — see [Dialogs](dialogs.md).
50
50
 
51
+ ## Dynamic typeahead search
52
+
53
+ An `Input.ChoiceSet` with `choices.data` (`Data.Query`) fetches its choices from your bot as the user types. Teams sends an `application/search` invoke; answer it with `on_card_search`:
54
+
55
+ ```ruby
56
+ teams.on_card_search do |ctx|
57
+ query = ctx.activity.value.query_text
58
+ Teams::Api::SearchResponse.new(
59
+ CITIES.grep(/#{Regexp.escape(query)}/i).map do |city|
60
+ Teams::Api::SearchInvokeResult.new(title: city, value: city)
61
+ end
62
+ )
63
+ end
64
+ ```
65
+
66
+ `ctx.activity.value` also carries `dataset` (the `Data.Query` dataset id, for cards with several dynamic inputs) and `query_options` with `skip`/`top` for paging large result sets.
67
+
51
68
  ## Regenerating
52
69
 
53
70
  The card classes are generated; don't hand-edit `lib/teams/cards/generated.rb`. To regenerate after an upstream card-model change:
@@ -0,0 +1,46 @@
1
+ # Agent 365 (experimental)
2
+
3
+ > Agent 365 support is experimental across the whole Teams SDK family. Expect
4
+ > changes.
5
+
6
+ Agent 365 gives an app a *stacked identity*: the app registration acts as an
7
+ **agentic app blueprint** (its client id *is* the blueprint id), a tenant can
8
+ instantiate **agentic apps** from it, and each app can carry **agentic users**
9
+ — user-shaped Entra identities that appear in Teams like people.
10
+
11
+ ## Inbound
12
+
13
+ Nothing to configure. When Teams delivers an activity to an agentic user, the
14
+ identity arrives on the activity's `recipient` account and the request carries
15
+ an Entra-issued token (accepted automatically alongside classic Bot Framework
16
+ tokens). The SDK scopes the whole turn to that identity — `ctx.post`, replies,
17
+ and streaming authenticate as the agent, not the app:
18
+
19
+ ```ruby
20
+ teams.on_message do |ctx|
21
+ identity = ctx.activity.recipient.agentic_identity
22
+ ctx.log.info "acting as agentic user #{identity.agentic_user_id}" if identity
23
+ ctx.reply "Hello from your agent"
24
+ end
25
+ ```
26
+
27
+ ## Lifecycle events
28
+
29
+ Teams reports agentic-user lifecycle changes as `agentLifecycle` events — see
30
+ [activity routing](../essentials/on-activity.md) for `on_agent_lifecycle` and
31
+ the per-variant routes.
32
+
33
+ ## Proactive
34
+
35
+ Build the identity explicitly (the blueprint defaults to the app's client id,
36
+ the tenant to the configured credentials tenant) and pass it to the proactive
37
+ senders:
38
+
39
+ ```ruby
40
+ identity = teams.agentic_identity(agentic_app_id: "...", agentic_user_id: "...")
41
+ teams.post(conversation_id, "Proactive hello from the agent", agentic_identity: identity)
42
+ ```
43
+
44
+ Omitting `agentic_user_id` acts as the agentic app itself. There is no
45
+ fallback between identity kinds: if an agentic token cannot be acquired the
46
+ call fails rather than silently sending as the wrong identity.
@@ -31,7 +31,7 @@ teams.on_sign_in do |ctx, token_response|
31
31
  end
32
32
  ```
33
33
 
34
- `ctx.sign_in` returns the token when the user is already signed in; otherwise it sends an OAuth card (to a 1:1 conversation when invoked from a group chat) and returns `nil`. The SDK's **default handlers** then complete the sign-in invokes automatically — both the interactive card path (`signin/verifyState`) and silent SSO (`signin/tokenExchange`) — and fire `on_sign_in`. You don't handle the invokes yourself.
34
+ `ctx.sign_in` returns the token when the user is already signed in; otherwise it sends an OAuth card and returns `nil`. In group chats and channels the card is delivered in the conversation as a targeted message only the requesting user sees; in channels the card always uses the sign-in button (silent SSO is not supported in channel scope). The SDK's **default handlers** then complete the sign-in invokes automatically — both the interactive card path (`signin/verifyState`) and silent SSO (`signin/tokenExchange`) — and fire `on_sign_in`. You don't handle the invokes yourself.
35
35
 
36
36
  `ctx.sign_out` clears the stored token.
37
37
 
@@ -16,6 +16,11 @@ module Teams
16
16
  raw["name"]
17
17
  end
18
18
 
19
+ # Discriminates agentLifecycle event variants (PascalCase wire values).
20
+ def value_type
21
+ raw["valueType"] || raw["value_type"]
22
+ end
23
+
19
24
  def text
20
25
  raw["text"]
21
26
  end
@@ -7,12 +7,13 @@ module Teams
7
7
  class ActivityContext
8
8
  attr_reader :app, :activity, :conversation_reference, :extra, :stream
9
9
 
10
- def initialize(app:, activity:, conversation_reference:, extra: {}, stream: nil)
10
+ def initialize(app:, activity:, conversation_reference:, extra: {}, stream: nil, api: nil)
11
11
  @app = app
12
12
  @activity = activity
13
13
  @conversation_reference = conversation_reference
14
14
  @extra = extra
15
15
  @stream = stream
16
+ @api = api
16
17
  end
17
18
 
18
19
  def ref
@@ -31,15 +32,17 @@ module Teams
31
32
  app.storage
32
33
  end
33
34
 
35
+ # The turn's API client; scoped to the activity's agentic identity when
36
+ # the app runs as an Agent 365 agent.
34
37
  def api
35
- app.api
38
+ @api || app.api
36
39
  end
37
40
 
38
41
  # The TypeScript, Python, and .NET SDKs call this operation `send`.
39
42
  # Ruby already defines Object#send for dynamic dispatch, so the public
40
43
  # Ruby API uses `post` to avoid shadowing a core language method.
41
44
  def post(activity_or_text)
42
- app.send_activity(conversation_reference, apply_targeted_defaults(activity_or_text))
45
+ app.send_activity(conversation_reference, apply_targeted_defaults(activity_or_text), api:)
43
46
  end
44
47
 
45
48
  def reply(activity_or_text)
@@ -93,8 +96,10 @@ module Teams
93
96
 
94
97
  # Starts the user sign-in flow: returns the token if the user is already
95
98
  # signed in, otherwise sends an OAuth card and returns nil. In group
96
- # conversations the card goes to a 1:1 conversation with the user (group
97
- # OAuth is not supported by Teams), like the TypeScript/Python SDKs.
99
+ # chats and channels the card is a targeted message visible only to the
100
+ # requesting user; channels omit the token exchange resource because
101
+ # channel scope cannot do the silent SSO exchange, so the card renders
102
+ # the sign-in button instead.
98
103
  def sign_in(connection_name: nil, oauth_card_text: "Please Sign In...", sign_in_button_text: "Sign In")
99
104
  connection_name ||= app.default_connection_name
100
105
 
@@ -109,19 +114,6 @@ module Teams
109
114
  # No token yet; continue with the OAuth card flow.
110
115
  end
111
116
 
112
- conversation_id = conversation_reference.conversation_id
113
- if activity.conversation.is_group
114
- one_on_one = api.conversations.create(
115
- members: [activity.from.to_h],
116
- tenant_id: activity.conversation.tenant_id
117
- )
118
- conversation_id = one_on_one.id
119
- # Deliberately posts the plain text notice into the group (matching
120
- # TypeScript and Python): group chats don't support SSO, so the card
121
- # itself goes to the 1:1 below while the group sees only the notice.
122
- post(oauth_card_text)
123
- end
124
-
125
117
  state = Base64.strict_encode64(JSON.generate(
126
118
  "connectionName" => connection_name,
127
119
  "conversation" => conversation_reference.to_h,
@@ -129,10 +121,14 @@ module Teams
129
121
  ))
130
122
  resource = api.bots.sign_in.get_resource(state:)
131
123
 
124
+ is_channel = activity.conversation.conversation_type == "channel"
125
+ recipient = activity.from.to_h
126
+ recipient = recipient.merge("isTargeted" => true) if activity.conversation.is_group
127
+
132
128
  card = {
133
129
  "text" => oauth_card_text,
134
130
  "connectionName" => connection_name,
135
- "tokenExchangeResource" => resource.token_exchange_resource&.to_h,
131
+ "tokenExchangeResource" => (resource.token_exchange_resource&.to_h unless is_channel),
136
132
  "tokenPostResource" => resource.token_post_resource&.to_h,
137
133
  "buttons" => [
138
134
  { "type" => "signin", "title" => sign_in_button_text, "value" => resource.sign_in_link }
@@ -141,13 +137,13 @@ module Teams
141
137
 
142
138
  payload = {
143
139
  "type" => "message",
144
- "recipient" => activity.from.to_h,
140
+ "recipient" => recipient,
145
141
  "attachments" => [
146
142
  { "contentType" => "application/vnd.microsoft.card.oauth", "content" => card }
147
143
  ]
148
144
  }
149
145
 
150
- app.send_activity(sign_in_reference(conversation_id), payload)
146
+ app.send_activity(conversation_reference, payload, api:)
151
147
  nil
152
148
  end
153
149
 
@@ -167,16 +163,6 @@ module Teams
167
163
 
168
164
  private
169
165
 
170
- def sign_in_reference(conversation_id)
171
- return conversation_reference if conversation_id == conversation_reference.conversation_id
172
-
173
- Api::ConversationReference.from_h(
174
- conversation_reference.to_h.merge(
175
- "conversation" => conversation_reference.conversation.to_h.merge("id" => conversation_id)
176
- )
177
- )
178
- end
179
-
180
166
  def incoming_targeted_sender
181
167
  return nil unless activity.message?
182
168
  return nil unless activity.recipient.is_targeted == true
@@ -55,6 +55,36 @@ module Teams
55
55
  read("tenantId", "tenant_id")
56
56
  end
57
57
 
58
+ def agentic_user_id
59
+ read("agenticUserId", "agentic_user_id")
60
+ end
61
+
62
+ def agentic_app_id
63
+ read("agenticAppId", "agentic_app_id")
64
+ end
65
+
66
+ def agentic_app_blueprint_id
67
+ read("agenticAppBlueprintId", "agentic_app_blueprint_id")
68
+ end
69
+
70
+ def callback_uri
71
+ read("callbackUri", "callback_uri")
72
+ end
73
+
74
+ # The Agent 365 identity scope this account carries (agentic accounts
75
+ # arrive with role "agenticUser" on the activity's recipient); nil
76
+ # unless the account belongs to an agentic app blueprint.
77
+ def agentic_identity
78
+ return nil unless agentic_app_blueprint_id
79
+
80
+ AgenticIdentity.new(
81
+ agentic_app_blueprint_id:,
82
+ agentic_app_id:,
83
+ agentic_user_id:,
84
+ tenant_id:
85
+ )
86
+ end
87
+
58
88
  def to_h
59
89
  body = raw.dup
60
90
  body["id"] = id if id
@@ -70,6 +100,10 @@ module Teams
70
100
  body["email"] = email if email
71
101
  body["userPrincipalName"] = user_principal_name if user_principal_name
72
102
  body["tenantId"] = tenant_id if tenant_id
103
+ body["agenticUserId"] = agentic_user_id if agentic_user_id
104
+ body["agenticAppId"] = agentic_app_id if agentic_app_id
105
+ body["agenticAppBlueprintId"] = agentic_app_blueprint_id if agentic_app_blueprint_id
106
+ body["callbackUri"] = callback_uri if callback_uri
73
107
  remove_aliases(body)
74
108
  end
75
109
 
@@ -83,6 +117,10 @@ module Teams
83
117
  body.delete("given_name")
84
118
  body.delete("user_principal_name")
85
119
  body.delete("tenant_id")
120
+ body.delete("agentic_user_id")
121
+ body.delete("agentic_app_id")
122
+ body.delete("agentic_app_blueprint_id")
123
+ body.delete("callback_uri")
86
124
  body
87
125
  end
88
126
  end
@@ -44,6 +44,98 @@ module Teams
44
44
  def end_time
45
45
  read("EndTime", "endTime", "end_time")
46
46
  end
47
+
48
+ # application/search invoke fields (Adaptive Card dynamic typeahead
49
+ # Input.ChoiceSet queries via choices.data / Data.Query).
50
+ def query_text
51
+ read("queryText", "query_text")
52
+ end
53
+
54
+ # Pagination options; skip and top read from the nested wrapper.
55
+ def query_options
56
+ value = read("queryOptions", "query_options")
57
+ value.is_a?(Hash) ? ActivityValue.new(value) : value
58
+ end
59
+
60
+ def kind
61
+ read("kind")
62
+ end
63
+
64
+ # The Data.Query dataset id authored on the Adaptive Card.
65
+ def dataset
66
+ read("dataset")
67
+ end
68
+
69
+ def skip
70
+ read("skip")
71
+ end
72
+
73
+ def top
74
+ read("top")
75
+ end
76
+
77
+ # Agent 365 agentLifecycle event value fields. The service sends the
78
+ # agenticAppInstanceId / agentIdentityBlueprintId wire keys (not the
79
+ # account-style names); all SDKs preserve them.
80
+ def tenant_id
81
+ read("tenantId", "tenant_id")
82
+ end
83
+
84
+ def agentic_user_id
85
+ read("agenticUserId", "agentic_user_id")
86
+ end
87
+
88
+ def agentic_app_instance_id
89
+ read("agenticAppInstanceId", "agentic_app_instance_id")
90
+ end
91
+
92
+ def agent_identity_blueprint_id
93
+ read("agentIdentityBlueprintId", "agent_identity_blueprint_id")
94
+ end
95
+
96
+ def version
97
+ read("version")
98
+ end
99
+
100
+ # Variant extras: manager (identity-created carries userId/email/
101
+ # displayName; manager-updated carries managerId), deletion reason,
102
+ # workload onboarding fields, and the created identity's expiry.
103
+ def manager
104
+ value = read("manager")
105
+ value.is_a?(Hash) ? ActivityValue.new(value) : value
106
+ end
107
+
108
+ def user_id
109
+ read("userId", "user_id")
110
+ end
111
+
112
+ def email
113
+ read("email")
114
+ end
115
+
116
+ def display_name
117
+ read("displayName", "display_name")
118
+ end
119
+
120
+ def manager_id
121
+ read("managerId", "manager_id")
122
+ end
123
+
124
+ def deletion_reason
125
+ read("deletionReason", "deletion_reason")
126
+ end
127
+
128
+ def workload_name
129
+ read("workloadName", "workload_name")
130
+ end
131
+
132
+ def workload_onboarding_state
133
+ read("workloadOnboardingState", "workload_onboarding_state")
134
+ end
135
+
136
+ def expiration_date_time
137
+ read("expirationDateTime", "expiration_date_time")
138
+ end
47
139
  end
48
140
  end
49
141
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teams
4
+ module Api
5
+ # The Agent 365 identity scope for SDK operations: an agentic app
6
+ # blueprint (the app registration itself), optionally narrowed to an
7
+ # agentic app instance and an agentic user attached to that instance.
8
+ class AgenticIdentity
9
+ attr_reader :agentic_app_blueprint_id, :agentic_app_id, :agentic_user_id, :tenant_id
10
+
11
+ def initialize(agentic_app_blueprint_id:, agentic_app_id: nil, agentic_user_id: nil, tenant_id: nil)
12
+ @agentic_app_blueprint_id = agentic_app_blueprint_id
13
+ @agentic_app_id = agentic_app_id
14
+ @agentic_user_id = agentic_user_id
15
+ @tenant_id = tenant_id
16
+ end
17
+
18
+ def to_h
19
+ body = { "agenticAppBlueprintId" => agentic_app_blueprint_id }
20
+ body["agenticAppId"] = agentic_app_id if agentic_app_id
21
+ body["agenticUserId"] = agentic_user_id if agentic_user_id
22
+ body["tenantId"] = tenant_id if tenant_id
23
+ body
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Teams
4
+ module Api
5
+ class AppInfo < Model
6
+ def id
7
+ read("id")
8
+ end
9
+
10
+ def version
11
+ read("version")
12
+ end
13
+ end
14
+ end
15
+ end
@@ -3,6 +3,10 @@
3
3
  module Teams
4
4
  module Api
5
5
  class ChannelData < Model
6
+ def app
7
+ wrap(read("app"), AppInfo)
8
+ end
9
+
6
10
  def tenant
7
11
  wrap(read("tenant"), TenantInfo)
8
12
  end
@@ -55,6 +59,7 @@ module Teams
55
59
 
56
60
  def to_h
57
61
  body = raw.dup
62
+ body["app"] = app.to_h if app
58
63
  body["tenant"] = tenant.to_h if tenant
59
64
  body["team"] = team.to_h if team
60
65
  body["channel"] = channel.to_h if channel