Skip to content

feat(messaging): add Microsoft Teams channel adapter#607

Open
slvnlrt wants to merge 4 commits into
spacedriveapp:mainfrom
slvnlrt:pr/teams-adapter
Open

feat(messaging): add Microsoft Teams channel adapter#607
slvnlrt wants to merge 4 commits into
spacedriveapp:mainfrom
slvnlrt:pr/teams-adapter

Conversation

@slvnlrt

@slvnlrt slvnlrt commented Jun 29, 2026

Copy link
Copy Markdown

What

Adds a Microsoft Teams channel adapter, built on the Azure Bot Service / Bot Framework Connector protocol. An agent can now talk to people in Teams DMs, channels, and group chats.

Capabilities

  • Text replies and @mention handling (channels and group chats are open on mention; DMs use an allowlist).
  • Adaptive Cards and card buttons. interactive_elements render as Action.Submit; a click returns to the agent as MessageContent::Interaction.
  • Typing indicator while the agent works.
  • Inbound files and images, delivered as MessageContent::Media.
  • DM allowlist with a "*" wildcard for org-wide use.

Architecture

  • Inbound: an axum webhook server (POST /api/messages) validates the Azure-signed JWT (RS256, JWKS from login.botframework.com, iss/aud/exp required), parses Activities into InboundMessage, then applies the permission filter before dispatch.
  • Outbound: replies and proactive broadcasts POST to the Bot Connector with an Azure AD client-credentials token. A serviceUrl SSRF allowlist (*.botframework.com / *.trafficmanager.net) gates every send, and a per-conversation serviceUrl sidecar lets proactive sends survive restarts. The inbound-media download bearer is gated to the same allowlist.
  • Config: a [messaging.teams] section shaped like the other adapters, hot-reloadable permissions via the config watcher, and the client secret sourced from the environment (env:TEAMS_CLIENT_SECRET).

Adapter parity

  • Reply / status / receipts / errors follow the existing adapters. Typing is surfaced via send_status; retry and backoff are inherited from the messaging manager; outbound errors are classified permanent vs retryable.
  • Residual gaps: one Teams bot per instance (no named multi-instance yet); outbound reactions are not supported (they require Microsoft Graph rather than the Bot Connector).

Verification

  • just gate-pr passes (969 lib tests, cargo check --all-targets, clippy, fmt, typegen).
  • Validated end to end against a real Teams bot (Azure Bot F0 + a public tunnel): real JWT accepted, DM allowlist enforced, Adaptive Cards rendered, button click round-tripped to the agent, inbound image downloaded, reply delivered.

Notes

  • One new dependency: jsonwebtoken (ring-based, no OpenSSL).
  • Teams requires a public HTTPS endpoint; setup is documented in docs/content/docs/(messaging)/teams-setup.mdx.
  • No migrations.
  • A follow-up PR adds Teams to the Channels admin UI (connect / delete / status from Settings).

Add a Microsoft Teams channel adapter built on the Azure Bot Service /
Bot Framework Connector protocol.

Inbound: an axum webhook server (POST /api/messages) validates the
Azure-signed JWT (RS256, JWKS from login.botframework.com, iss/aud/exp),
parses Activities into InboundMessage, and enforces per-conversation
permissions (DM allowlist with a "*" wildcard; channels open on @mention).

Outbound: replies and proactive broadcasts go to the Bot Connector with an
Azure AD client-credentials token, guarded by a serviceUrl SSRF allowlist
(*.botframework.com / *.trafficmanager.net), with a per-conversation
serviceUrl sidecar so proactive sends survive restarts.

Capabilities: text and @mentions; Adaptive Cards and card buttons
(interactive_elements render as Action.Submit; a click returns as
MessageContent::Interaction); a typing indicator; inbound file and image
attachments (MessageContent::Media, with the download bearer gated to
allowlisted hosts).

Config: a [messaging.teams] section (shaped like the other adapters),
hot-reloadable permissions via the config watcher, and the client secret
sourced from the environment. Setup guide in docs/content/docs/(messaging).

Adds one dependency, jsonwebtoken (ring-based). One Teams bot per instance.
@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

💤 Files selected but had no reviewable changes (1)
  • src/messaging/teams.rs
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 09a71778-0cd3-43de-a154-36a5383515a1

📥 Commits

Reviewing files that changed from the base of the PR and between 104df40 and 79bc8e0.

📒 Files selected for processing (1)
  • src/messaging/teams.rs

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Adds end-to-end Microsoft Teams messaging support: TOML schema types, credential loading with secret redaction, TeamsPermissions with DM allowlisting and channel filtering, delivery target parsing for opaque Teams conversation IDs, platform metadata extraction, mention invocation detection, secrets registry entries, daemon startup and hot-reload wiring, and a new Teams setup documentation page.

Changes

Microsoft Teams Adapter

Layer / File(s) Summary
Config contracts and adapter surface
src/config/toml_schema.rs, src/config/types.rs, src/config.rs, src/messaging.rs
Adds TomlTeamsConfig/TomlTeamsInstanceConfig TOML structs with port/bind defaults, introduces TeamsConfig/TeamsInstanceConfig domain types with Debug secret redaction and SystemSecrets mapping for TEAMS_* env vars, extends MessagingConfig with teams: Option<TeamsConfig>, registers Teams in is_named_adapter_platform and build_adapter_validation_states, and re-exports TeamsPermissions. Exposes the Teams messaging module. Existing tests update MessagingConfig literals to include teams: None.
Config loading, permissions, and secrets
src/config/load.rs, src/config/permissions.rs, src/secrets/store.rs
Implements from_toml_inner Teams parsing: resolves app_id/client_secret/tenant_id via resolve_env_value, disables credential-less instances with a warning, and returns None when no credentials exist. Defines TeamsPermissions with channel_filter and dm_allowed_users; is_allowed blocks DMs when the list is empty, allows via explicit ID or "*" wildcard, and allows channels when channel_filter is None or matches. Registers Teams secrets in system_secret_registry. Adds test suites for loading, redaction, permissions, and secret categorization.
Delivery target routing and conversation metadata
src/messaging/target.rs, src/conversation/channels.rs
Extends parse_delivery_target, resolve_broadcast_target, and normalize_target with teams: arms; adds normalize_teams_target and multi-part conversation-ID parsing in parse_named_instance_target. extract_platform_meta gains a "teams" branch persisting only teams_service_url and teams_conversation_type (excluding teams_conversation_id). Adds Teams test suites for both.
Teams mention invocation
src/agent/channel.rs
compute_listen_mode_invocation checks message.metadata["teams_mentioned"] == "true" to set invoked_by_mention, with two unit tests.
Daemon startup and hot-reload wiring
src/main.rs, src/config/watcher.rs
initialize_agents gains teams_permissions parameter, initializes ArcSwap<TeamsPermissions>, builds and registers the Teams adapter when credentials are present, and warns on named instances (single-listener limitation). run() threads teams_permissions through all initialize_agents and spawn_file_watcher call sites. spawn_file_watcher gains a teams_permissions parameter, hot-reloads permissions on config change, and hot-starts the adapter when not already running.
Documentation
docs/content/docs/(messaging)/messaging.mdx, docs/content/docs/(messaging)/teams-setup.mdx
Updates the supported platforms table to include Teams; adds a full Teams setup page covering Azure Bot/Entra registration, app package manifest, TOML config block, DM allowlisting, capabilities, and limitations.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related issues

Possibly related PRs

  • spacedriveapp/spacebot#249: Teams is integrated as a named-adapter-capable platform using the same adapter identity and routing model introduced in PR #249.
  • spacedriveapp/spacebot#306: Both PRs modify the extracted config submodules and the same reload/wiring path, especially src/config/load.rs, src/config/permissions.rs, src/config/types.rs, and src/config/watcher.rs.
  • spacedriveapp/spacebot#347: Both PRs add a new per-adapter permissions handle and hot-start adapter support through the same spawn_file_watcher/initialize_agents integration points.

Suggested reviewers

  • jamiepine
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding a Microsoft Teams channel adapter.
Description check ✅ Passed The description matches the PR and covers the Teams adapter, config, and capabilities.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread src/messaging/teams.rs
app_id: &str,
jwks: &JwksCache,
) -> anyhow::Result<()> {
// Strip "Bearer " prefix.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Auth scheme is case-insensitive; this avoids rejecting valid authorization: bearer ... headers (and tolerates extra whitespace) without changing the JWT validation logic.

Suggested change
// Strip "Bearer " prefix.
// Strip "Bearer " prefix (scheme is case-insensitive).
let (scheme, token) = auth_header
.split_once(' ')
.with_context(|| "Authorization header is not a Bearer token")?;
if !scheme.eq_ignore_ascii_case("bearer") {
anyhow::bail!("Authorization header is not a Bearer token");
}
let token = token.trim();

Comment thread src/messaging/teams.rs
///
/// This guards against attacker-supplied serviceUrl values that could be used
/// to exfiltrate the Bot Connector bearer token.
fn is_allowed_service_url(url: &str) -> bool {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Allowlisting all of .trafficmanager.net is pretty broad (anyone can create an Azure Traffic Manager profile under their own <name>.trafficmanager.net), so a forged/compromised inbound serviceUrl could potentially turn into token exfil. If Teams is always smba.trafficmanager.net here, tightening the host check reduces the blast radius.

Suggested change
fn is_allowed_service_url(url: &str) -> bool {
host.ends_with(".botframework.com") || host == "smba.trafficmanager.net"

Comment thread src/messaging/teams.rs
sidecar_path: self.sidecar_path.clone(),
};

let app = Router::new()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since handle_messages reads raw Bytes, it might be worth setting an explicit request body limit (similar to src/api/server.rs) to avoid a trivial memory DoS on the public webhook.

Suggested change
let app = Router::new()
let app = Router::new()
.route("/api/messages", post(handle_messages))
.route("/health", get(handle_health))
.layer(axum::extract::DefaultBodyLimit::max(10 * 1024 * 1024)) // 10 MiB
.with_state(state);

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/content/docs/`(messaging)/teams-setup.mdx:
- Around line 67-68: The Teams DM allowlist docs are using the wrong identifier
format; update the guidance around dm_allowed_users in teams-setup.mdx to tell
users to use AAD object IDs or UPN strings, not 29:... MRI values. Make sure the
examples and surrounding explanation in the Teams setup section clearly match
the actual matching logic used by the bot so users configure the allowlist with
the correct identifier.

In `@src/config/watcher.rs`:
- Around line 698-735: The hot-reload Teams config path in
watcher::apply_config_change (the block that calls build_teams_adapter and
manager.register_and_start for "teams") silently skips unsupported named
instances, unlike cold start. Add the same warning used during startup when
config.messaging.teams.instances contains named Teams instances and hot reload
cannot start them, so the user is told they were ignored; keep the warning close
to the existing Teams enabled/adapter-start logic so it triggers on live config
edits too.
- Around line 264-270: The Teams permissions reload path in watcher.rs currently
only updates when messaging.teams still exists, so disabling Teams or removing
its bindings can leave the existing adapter running with open access via
TeamsPermissions::from_config fallback. Update the reload logic around
teams_permissions and TeamsPermissions::from_config to fail closed: when Teams
is disabled or no longer configured, stop/disable the existing Teams listener or
replace its permissions with a restrictive “deny all” state instead of reusing
the permissive fallback, and keep the reload behavior strict when config changes
remove Teams access.

In `@src/messaging/target.rs`:
- Around line 166-216: The Teams target resolution in target::BroadcastTarget
parsing is incorrectly inferring named instances from opaque conversation IDs by
using the first segment after "teams:" and is_valid_instance_name. Update this
logic to derive the adapter only from an unambiguous source such as persisted
platform metadata or a configured instance lookup, and keep the conversation id
parsing separate from adapter identity. Ensure the BroadcastTarget branch for
"teams" no longer promotes arbitrary IDs like teams:a or teams:prod into adapter
keys unless the instance is explicitly known.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a391978f-fb9f-40a3-b180-e3ec1c2c0f57

📥 Commits

Reviewing files that changed from the base of the PR and between ac52277 and e61e094.

⛔ Files ignored due to path filters (3)
  • Cargo.lock is excluded by !**/*.lock, !**/*.lock
  • Cargo.toml is excluded by !**/*.toml
  • docs/content/docs/(messaging)/meta.json is excluded by !**/*.json
📒 Files selected for processing (15)
  • docs/content/docs/(messaging)/messaging.mdx
  • docs/content/docs/(messaging)/teams-setup.mdx
  • src/agent/channel.rs
  • src/config.rs
  • src/config/load.rs
  • src/config/permissions.rs
  • src/config/toml_schema.rs
  • src/config/types.rs
  • src/config/watcher.rs
  • src/conversation/channels.rs
  • src/main.rs
  • src/messaging.rs
  • src/messaging/target.rs
  • src/messaging/teams.rs
  • src/secrets/store.rs

Comment on lines +67 to +68
# Teams user IDs allowed to DM the bot. "*" allows everyone.
dm_allowed_users = ["*"]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Document the actual Teams DM allowlist identifier.

The implementation allows DMs by matching AAD object IDs (or UPN strings), but this guide tells users to paste 29:... MRI values into dm_allowed_users. That will make the setup look correct while DMs still fail closed.

Suggested fix
-# Teams user IDs allowed to DM the bot. "*" allows everyone.
+# Entra/AAD user IDs (or UPN strings) allowed to DM the bot. "*" allows everyone.
 dm_allowed_users = ["*"]
@@
-- To allow specific people, list their Teams user IDs (the `29:...` MRI strings).
+- To allow specific people, list the Entra/AAD object IDs (or the exact UPN string) that Spacebot logs as `sender_id`.
 - To allow everyone (org-wide deployments), set `dm_allowed_users = ["*"]`.
@@
-You usually do not know a user's `29:...` MRI up front. The simplest path is to **`@mention` the bot in a channel** (channel messages are not gated), or set the `"*"` wildcard. To allowlist one person, send a DM once (it is dropped), read the dropped `sender_id` from the logs, and add that value.
+You usually do not know a user's allowlist value up front. The simplest path is to **`@mention` the bot in a channel** (channel messages are not gated), or set the `"*"` wildcard. To allowlist one person, send a DM once (it is dropped), read the logged `sender_id`, and add that exact value.

Also applies to: 83-90

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/content/docs/`(messaging)/teams-setup.mdx around lines 67 - 68, The
Teams DM allowlist docs are using the wrong identifier format; update the
guidance around dm_allowed_users in teams-setup.mdx to tell users to use AAD
object IDs or UPN strings, not 29:... MRI values. Make sure the examples and
surrounding explanation in the Teams setup section clearly match the actual
matching logic used by the bot so users configure the allowlist with the correct
identifier.

Comment thread src/config/watcher.rs
Comment on lines +264 to +270
if let Some(ref perms) = teams_permissions
&& let Some(teams_config) = &config.messaging.teams
{
let new_perms = TeamsPermissions::from_config(teams_config, &config.bindings);
perms.store(Arc::new(new_perms));
tracing::info!("teams permissions reloaded");
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Fail closed when Teams is removed or disabled.

TeamsPermissions::from_config(...) falls back to channel_filter = None, and is_allowed() treats that as open channel/group access. Because this watcher never stops an already-running Teams adapter, flipping [messaging.teams].enabled = false or removing Teams bindings can leave the listener up with broader access than before.

Suggested fix
-                if let Some(ref perms) = teams_permissions
-                    && let Some(teams_config) = &config.messaging.teams
-                {
-                    let new_perms = TeamsPermissions::from_config(teams_config, &config.bindings);
-                    perms.store(Arc::new(new_perms));
-                    tracing::info!("teams permissions reloaded");
-                }
+                if let Some(ref permissions) = teams_permissions {
+                    let new_permissions = match &config.messaging.teams {
+                        Some(teams_config) if teams_config.enabled => {
+                            TeamsPermissions::from_config(teams_config, &config.bindings)
+                        }
+                        _ => TeamsPermissions {
+                            channel_filter: Some(Vec::new()),
+                            dm_allowed_users: Vec::new(),
+                        },
+                    };
+                    permissions.store(Arc::new(new_permissions));
+                    tracing::info!("teams permissions reloaded");
+                }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if let Some(ref perms) = teams_permissions
&& let Some(teams_config) = &config.messaging.teams
{
let new_perms = TeamsPermissions::from_config(teams_config, &config.bindings);
perms.store(Arc::new(new_perms));
tracing::info!("teams permissions reloaded");
}
if let Some(ref permissions) = teams_permissions {
let new_permissions = match &config.messaging.teams {
Some(teams_config) if teams_config.enabled => {
TeamsPermissions::from_config(teams_config, &config.bindings)
}
_ => TeamsPermissions {
channel_filter: Some(Vec::new()),
dm_allowed_users: Vec::new(),
},
};
permissions.store(Arc::new(new_permissions));
tracing::info!("teams permissions reloaded");
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/config/watcher.rs` around lines 264 - 270, The Teams permissions reload
path in watcher.rs currently only updates when messaging.teams still exists, so
disabling Teams or removing its bindings can leave the existing adapter running
with open access via TeamsPermissions::from_config fallback. Update the reload
logic around teams_permissions and TeamsPermissions::from_config to fail closed:
when Teams is disabled or no longer configured, stop/disable the existing Teams
listener or replace its permissions with a restrictive “deny all” state instead
of reusing the permissive fallback, and keep the reload behavior strict when
config changes remove Teams access.

Comment thread src/config/watcher.rs
Comment thread src/messaging/target.rs
slvnlrt added 2 commits June 29, 2026 12:04
…ing, doc

- target.rs: Teams runs a single listener in v1, so broadcast/delivery target
  resolution always uses the adapter "teams" and keeps the full conversation id
  (colons preserved). Drop the instance-name heuristic that misread opaque ids
  like a DM "a:1-..." as a named instance "teams:a" and mis-routed proactive
  sends to a non-existent adapter. Update tests; add DM-id regressions.
- watcher.rs: warn on named [[messaging.teams.instances]] during hot reload,
  matching cold start (named instances are not started in v1).
- permissions.rs: correct the dm_allowed_users doc to match the actual compare
  target (the inbound activity.from.id MRI), not AAD object IDs.

Note: the hot-reload "fail-open on disable" raised in review is the shared
behavior of every adapter (removing config never stops a running adapter), not
Teams-specific; tightening only Teams would break cross-adapter parity, so it
is intentionally out of scope here.
Collapse a nested if in card_to_adaptive and annotate build_teams_adapter
with #[allow(clippy::too_many_arguments)] (discrete args mirror
TeamsAdapter::new). Keeps the clippy-all gate clean.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/messaging/target.rs (1)

175-191: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use full conversation_id names here.

conv_id / bare_conv_id break the repo’s no-abbreviation rule and make this branch less consistent with the surrounding Teams terminology. Rename them to conversation_id / bare_conversation_id. As per coding guidelines, "Don't abbreviate variable names. Use queue not q, message not msg, channel not ch. Common abbreviations like config are fine."

Also applies to: 673-677

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/messaging/target.rs` around lines 175 - 191, The Teams target branch uses
abbreviated local names that violate the repo’s no-abbreviation naming rule. In
the logic in target resolution around the `BroadcastTarget` construction, rename
`conv_id` to `conversation_id` and `bare_conv_id` to `bare_conversation_id`, and
update the corresponding uses so the naming stays consistent with the
surrounding Teams terminology and the existing `channel`/`platform_meta` flow.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/messaging/target.rs`:
- Around line 175-191: The Teams target branch uses abbreviated local names that
violate the repo’s no-abbreviation naming rule. In the logic in target
resolution around the `BroadcastTarget` construction, rename `conv_id` to
`conversation_id` and `bare_conv_id` to `bare_conversation_id`, and update the
corresponding uses so the naming stays consistent with the surrounding Teams
terminology and the existing `channel`/`platform_meta` flow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: da9968d6-a5e7-4636-a50a-39258f8c2449

📥 Commits

Reviewing files that changed from the base of the PR and between e61e094 and 104df40.

📒 Files selected for processing (4)
  • src/config/permissions.rs
  • src/config/watcher.rs
  • src/messaging/target.rs
  • src/messaging/teams.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/config/watcher.rs
  • src/config/permissions.rs

slvnlrt added a commit to slvnlrt/spacebot that referenced this pull request Jun 29, 2026
Mirror the fixes landed on the upstream PR branches (spacedriveapp#607/spacedriveapp#608):
- target.rs: single-instance broadcast/delivery routing — the adapter is
  always "teams" and the full conversation id is preserved; no instance is
  inferred from opaque ids (a DM "a:1-..." was misread as "teams:a"). Tests
  updated + DM-id regressions added.
- watcher.rs: warn on named [[messaging.teams.instances]] during hot reload.
- permissions.rs: dm_allowed_users doc now matches the actual compare target
  (the inbound activity.from.id MRI).
- teams.rs + api/messaging.rs: clippy cleanups (collapsible-if, too_many_arguments).
@slvnlrt

slvnlrt commented Jun 29, 2026

Copy link
Copy Markdown
Author

Thanks for the review @coderabbitai. Addressed in 9a94d957 (logic) and 104df402 (clippy). Outcome per finding, verified against the code:

1. DM allowlist identifier (teams-setup.mdx) — doc was already correct; fixed the misleading code comment instead.
The adapter matches dm_allowed_users against the inbound activity.from.id, which for Teams is the user's MRI (29:...), not an AAD object ID. So the setup guide telling users to use 29:... values is right. The misleading part was the TeamsPermissions::dm_allowed_users doc comment ("AAD object IDs (or UPN strings)"), which I corrected to reflect the actual compare target. (Richer identity matching, e.g. aadObjectId, is a possible future enhancement, but today the match is on from.id.)

2. Hot-reload named-instance warning (watcher.rs) — fixed.
The config-watcher hot-start path now warns on named [[messaging.teams.instances]], matching the cold-start warning in main.rs.

3. Fail-open on Teams disable (watcher.rs) — intentionally not changed (parity).
This is the shared hot-reload behavior of every adapter: the permission-reload block only runs when the platform is still present in config (see the identical Signal block just above), and the watcher never stops a running adapter when its config is removed. Making only Teams fail-closed here would break cross-adapter parity and would need a stop-adapter mechanism the watcher doesn't currently have. Tightening hot-reload teardown is a reasonable improvement, but as a cross-cutting change across all adapters rather than something Teams-specific.

4. Adapter inference from opaque conversation IDs (target.rs) — fixed; this was a real routing bug.
A DM conversation id like teams:a:1-... was misread as a named instance teams:a because the first segment (a) passes is_valid_instance_name, mis-routing proactive broadcasts to a non-existent adapter. Since v1 runs a single Teams listener, both parse_named_instance_target and resolve_broadcast_target now always resolve the adapter to teams and keep the full conversation id (colons preserved); no instance is inferred. Added regression tests for the DM-id case.

just gate-pr is green (970 lib tests, clippy, fmt, typegen).

@coderabbitai

coderabbitai Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@slvnlrt Thanks for the detailed follow-up — this all makes sense.

Good catches on the actual fixes, especially the opaque Teams conversation ID routing bug and the hot-reload warning parity. And agreed on Point 3: changing only Teams there would be inconsistent with the current watcher behavior across adapters, so that teardown/fail-closed behavior is better handled as a broader follow-up rather than in this PR.

Also appreciate the clarification on DM allowlisting: matching against activity.from.id / Teams MRI (29:...) is an important distinction.

🐇👍


✏️ Learnings added
Learnt from: slvnlrt
URL: https://github.com/spacedriveapp/spacebot/pull/607

Timestamp: 2026-06-29T10:24:25.311Z
Learning: In the Rust Teams messaging adapter, Teams DM allowlisting matches `dm_allowed_users` against the inbound Microsoft Teams `activity.from.id` value, which is the Teams MRI (for example `29:...`), not an Entra/AAD object ID. Documentation and future reviews for `src/config/permissions.rs` and Teams setup should treat MRI values as the current supported identifier format.

Learnt from: slvnlrt
URL: https://github.com/spacedriveapp/spacebot/pull/607

Timestamp: 2026-06-29T10:24:25.311Z
Learning: In the Rust config hot-reload flow, `src/config/watcher.rs` currently behaves consistently across messaging adapters by reloading permissions only when that platform still exists in config and by not stopping running adapters when a platform config is removed. Requests to make Teams fail-closed on config removal should be treated as a cross-adapter watcher/teardown enhancement, not a Teams-specific fix.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

slvnlrt added a commit to slvnlrt/spacebot that referenced this pull request Jun 29, 2026
…acklog

Roadmap: add an 'Upstream delivery' block (spacedriveapp#605/spacedriveapp#607/spacedriveapp#608 + state) and a
single 'Deferred / out-of-scope — tracked backlog' table (outbound/inbound
reactions, select menus, streaming, multi-bot, v3 approval cards, the
cross-adapter hot-reload teardown follow-up from the spacedriveapp#607 review, richer
identity, facilitation). Handoff: refresh TL;DR/NEXT/branch-map for the
upstreaming state (HEADs, the three PR branches).
… in broadcast

resolve_broadcast_target yields the bare MS conversation id, but the
service_urls map (and inbound capture) key on the runtime-prefixed id
(teams:<id>). broadcast passed the bare id straight to send_activity, so the
lookup missed -> 'no serviceUrl for routing key'. Reconstruct the prefixed key
via broadcast_routing_key (idempotent); send_activity strips it again for the
Bot Connector URL. + regression test. Compiles clean (release build verified).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant