Skip to content

Conversation

@axelsrz
Copy link
Member

@axelsrz axelsrz commented Nov 22, 2025

This pull request introduces support for Microsoft Copilot Studio (MCS) connectors throughout the codebase, including new channel and role types, OAuth authorization handling, and a dedicated connector client. The changes add the ability to recognize and process Copilot Studio requests, perform connector-specific OAuth flows, and interact with Copilot Studio via a new client implementation. Additionally, error resources are extended to handle connector-specific cases.

Copilot Studio Connector Support

  • Added copilot_studio as a new channel in Channels to represent Microsoft Copilot Studio requests.
  • Introduced connector_user as a new role type in RoleTypes for Copilot Studio users.

OAuth Authorization Enhancements

  • Implemented ConnectorUserAuthorization, a new OAuth handler for Copilot Studio connector requests, supporting token extraction, OBO exchange, and sign-in flow adjustments. [1] [2] [3] [4] [5] [6]
  • Updated ClaimsIdentity to store the original security token, and ensured JWT token validation attaches the raw token to the identity. [1] [2]

Connector Client Implementation

  • Added MCSConnectorClient and related classes to support sending activities to Copilot Studio via Power Apps Connector, with only send_to_conversation and reply_to_activity operations supported. [1] [2] [3]

Error Handling

  • Introduced new error messages specific to connector requests, including missing or expired security tokens.

Copilot AI review requested due to automatic review settings November 22, 2025 01:21
Copilot finished reviewing on behalf of axelsrz November 22, 2025 01:23
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds comprehensive support for Microsoft Copilot Studio (MCS) connectors, enabling agents to receive requests via Power Apps Connector and perform OAuth token exchanges using the On-Behalf-Of (OBO) flow to access Microsoft Graph APIs on behalf of users.

Key Changes:

  • Added new channel type copilot_studio and role type connector_user to handle MCS-specific requests
  • Implemented ConnectorUserAuthorization handler for extracting tokens from requests and performing OBO exchanges
  • Created MCSConnectorClient for sending activities back to Copilot Studio via Power Apps Connector
  • Extended ClaimsIdentity to store security tokens and updated JWT validation to attach tokens to identities

Reviewed changes

Copilot reviewed 18 out of 18 changed files in this pull request and generated 12 comments.

Show a summary per file
File Description
test_samples/copilot_studio_connector/requirements.txt Dependencies for the Copilot Studio connector sample
test_samples/copilot_studio_connector/appsettings.json Configuration template with token validation and OAuth settings
test_samples/copilot_studio_connector/app.py Sample application entry point demonstrating connector setup
test_samples/copilot_studio_connector/agent.py Sample agent showing connector message handling and Graph API calls
test_samples/copilot_studio_connector/README.md Documentation for setting up and using the connector sample
libraries/microsoft-agents-activity/microsoft_agents/activity/channels.py Added copilot_studio channel type
libraries/microsoft-agents-activity/microsoft_agents/activity/role_types.py Added connector_user role type
libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/claims_identity.py Extended to store security tokens
libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/authorization/jwt_token_validator.py Updated to attach raw tokens to claims identity
libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/authorization.py Registered ConnectorUserAuthorization handler
libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/init.py Exported ConnectorUserAuthorization
libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/init.py Exported ConnectorUserAuthorization
libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/app/oauth/_handlers/connector_user_authorization.py Core OBO token exchange logic for connector requests
libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/errors/error_resources.py Added connector-specific error messages
libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/mcs_connector_client.py Client for sending activities to Copilot Studio
libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/mcs/init.py Module initialization for MCS connector
libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/connector/init.py Exported MCSConnectorClient
libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/rest_channel_service_client_factory.py Factory logic to instantiate MCS client for connector requests

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

storage = MemoryStorage()

# Create the agent
agent = MyAgent(options)
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

The variable options is used but never defined. The commented-out code on line 43 suggests it should be created from configuration, but this is missing in the actual implementation. This will cause a NameError at runtime.

Copilot uses AI. Check for mistakes.
):
return MCSConnectorClient(
endpoint=service_url,
token=token,
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

The MCSConnectorClient constructor signature in rest_channel_service_client_factory.py doesn't match the actual implementation in mcs_connector_client.py. The factory passes endpoint and token parameters (line 126-128), but the actual __init__ method only accepts endpoint and client parameters (line 201). The token parameter is not used in the constructor.

Suggested change
token=token,

Copilot uses AI. Check for mistakes.

except Exception as ex:
logger.warning(f"Failed to parse JWT token for handler {self._id}: {ex}")
raise ex
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

Unreachable code: the raise ex statement on line 226 will never be executed because if an exception occurs, it will be caught and re-raised on line 227. Additionally, the comment on line 227 is unreachable and misleading since exceptions are being re-raised rather than allowed to continue.

Suggested change
raise ex

Copilot uses AI. Check for mistakes.

### Message Flow

1. Copilot Studio sends message to agent with recipient.role = "connectoruser"
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

The documentation on line 136 states "recipient.role = 'connectoruser'" but the actual value in the code is RoleTypes.connector_user which maps to "connectoruser" (no underscore). The documentation should match the actual enum value for clarity.

Suggested change
1. Copilot Studio sends message to agent with recipient.role = "connectoruser"
1. Copilot Studio sends message to agent with recipient.role = RoleTypes.connector_user ("connectoruser")

Copilot uses AI. Check for mistakes.
Comment on lines +115 to +123
UnexpectedConnectorRequestToken = ErrorMessage(
"Connector request did not contain a valid security token for handler: {0}",
-63018,
)

UnexpectedConnectorTokenExpiration = ErrorMessage(
"Connector token has expired for handler: {0}",
-63019,
)
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

The error messages defined here (UnexpectedConnectorRequestToken and UnexpectedConnectorTokenExpiration) are not actually used in the connector authorization implementation. The code in connector_user_authorization.py raises ValueError with custom messages instead of using these error resources (lines 110, 200-201, 206-207). This creates inconsistency in error handling.

Suggested change
UnexpectedConnectorRequestToken = ErrorMessage(
"Connector request did not contain a valid security token for handler: {0}",
-63018,
)
UnexpectedConnectorTokenExpiration = ErrorMessage(
"Connector token has expired for handler: {0}",
-63019,
)

Copilot uses AI. Check for mistakes.
Comment on lines +172 to +173
# TODO: (connector) Should raise an error instead of just returning
return input_token_response
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

[nitpick] TODO comment suggests that non-exchangeable tokens should raise an error, but the current implementation silently returns the original token. This could lead to confusion when the OBO exchange is expected to happen but doesn't. Consider raising an appropriate error or logging a warning at minimum.

Suggested change
# TODO: (connector) Should raise an error instead of just returning
return input_token_response
logger.warning(f"Token provided to OBO exchange is not exchangeable for handler: {self._id}")
raise ValueError("Token is not exchangeable and OBO exchange was requested.")

Copilot uses AI. Check for mistakes.
"GetAttachmentInfo is not supported for Microsoft Copilot Studio Connector"
)

async def get_attachment(self, attachment_id: str, view_id: str, **kwargs) -> bytes:
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

This method requires 3 positional arguments, whereas overridden AttachmentsBase.get_attachment requires 1.

Suggested change
async def get_attachment(self, attachment_id: str, view_id: str, **kwargs) -> bytes:
async def get_attachment(self, attachment_id: str, **kwargs) -> bytes:

Copilot uses AI. Check for mistakes.
from typing import Optional

from microsoft_agents.hosting.core import AgentApplication, TurnState
from microsoft_agents.activity import Activity, ActivityTypes, RoleTypes
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

Import of 'Activity' is not used.

Suggested change
from microsoft_agents.activity import Activity, ActivityTypes, RoleTypes
from microsoft_agents.activity import ActivityTypes, RoleTypes

Copilot uses AI. Check for mistakes.

import logging
import jwt
from datetime import datetime, timezone, timedelta
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

Import of 'timedelta' is not used.

Suggested change
from datetime import datetime, timezone, timedelta
from datetime import datetime, timezone

Copilot uses AI. Check for mistakes.
"""
# No concept of sign-out with ConnectorAuth
logger.debug("Sign-out called for ConnectorUserAuthorization (no-op)")
pass
Copy link

Copilot AI Nov 22, 2025

Choose a reason for hiding this comment

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

Unnecessary 'pass' statement.

Suggested change
pass

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings November 24, 2025 18:04
Copilot finished reviewing on behalf of axelsrz November 24, 2025 18:05
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 18 out of 18 changed files in this pull request and generated 6 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

All other operations will raise NotImplementedError.
"""

def __init__(self, endpoint: str, client: Optional[ClientSession] = None):
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

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

The constructor signature accepts a client parameter but the factory in rest_channel_service_client_factory.py (line 126-129) only passes endpoint and token. The token parameter is not handled by this constructor, which will cause incorrect instantiation.

Copilot uses AI. Check for mistakes.

except Exception as ex:
logger.warning(f"Failed to parse JWT token for handler {self._id}: {ex}")
raise ex
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

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

Unreachable comment after raise ex on line 226. Either remove the comment or remove the raise statement if the intention is to continue without expiration info.

Suggested change
raise ex

Copilot uses AI. Check for mistakes.
Comment on lines +172 to +173
# TODO: (connector) Should raise an error instead of just returning
return input_token_response
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

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

TODO comment indicates this silent failure path should raise an error instead. This should be addressed before merging, as it affects token exchange behavior.

Suggested change
# TODO: (connector) Should raise an error instead of just returning
return input_token_response
raise ValueError("Input token is not exchangeable for OBO flow")

Copilot uses AI. Check for mistakes.
# Get the connection that supports OBO
token_provider = self._connection_manager.get_connection(connection_name)
if not token_provider:
# TODO: (connector) use resource errors
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

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

The error message should use the ErrorResources pattern for consistency with the rest of the codebase, as indicated by this TODO comment.

Copilot uses AI. Check for mistakes.
content = await response.text()
if content:
data = await response.json()
# TODO: (connector) Validate response structure
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

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

Response structure validation is missing. This should be implemented to ensure the response contains expected fields before constructing ResourceResponse.

Copilot uses AI. Check for mistakes.
Comment on lines +214 to +215
# TODO: (connector) validate this decoding
jwt_token = jwt.decode(security_token, options={"verify_signature": False})
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

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

JWT token decoding is performed without signature validation (verify_signature: False). While this may be intentional for extracting metadata, the TODO suggests this needs validation to ensure security requirements are met.

Suggested change
# TODO: (connector) validate this decoding
jwt_token = jwt.decode(security_token, options={"verify_signature": False})
# Validate JWT decoding with signature verification
# TODO: Replace 'YOUR_PUBLIC_KEY' and ['RS256'] with actual key and algorithms as appropriate
jwt_token = jwt.decode(
security_token,
"YOUR_PUBLIC_KEY", # Replace with actual public key or secret
algorithms=["RS256"], # Replace with actual algorithm(s)
)

Copilot uses AI. Check for mistakes.
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.

2 participants