diff --git a/python/samples/concepts/setup/ALL_SETTINGS.md b/python/samples/concepts/setup/ALL_SETTINGS.md index db477e4b0e12..9bd03d72ed30 100644 --- a/python/samples/concepts/setup/ALL_SETTINGS.md +++ b/python/samples/concepts/setup/ALL_SETTINGS.md @@ -49,7 +49,7 @@ | | [AzureResponsesAgent](../../../semantic_kernel/agents/open_ai/azure_responses_agent.py) | deployment_name,
api_key,
endpoint,
api_version,
base_url | AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME,
AZURE_OPENAI_API_KEY,
AZURE_OPENAI_ENDPOINT,
AZURE_OPENAI_API_VERSION,
AZURE_OPENAI_BASE_URL | Yes,
No,
Yes,
Yes,
No | [AzureOpenAISettings](../../../semantic_kernel/connectors/ai/open_ai/settings/azure_open_ai_settings.py) | | Azure AI | [AzureAIAgent](../../../semantic_kernel/agents/azure_ai/azure_ai_agent.py) | model_deployment_name,
endpoint,
agent_id,
bing_connection_id,
azure_ai_search_connection_id,
azure_ai_search_index_name,
api_version | AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME,
AZURE_AI_AGENT_ENDPOINT,
AZURE_AI_AGENT_AGENT_ID,
AZURE_AI_AGENT_BING_CONNECTION_ID,
AZURE_AI_AGENT_AZURE_AI_SEARCH_CONNECTION_ID,
AZURE_AI_AGENT_AZURE_AI_SEARCH_INDEX_NAME,
AZURE_AI_AGENT_API_VERSION | Yes,
Yes,
No,
No,
No,
No,
No | [AzureAIAgentSettings](../../../semantic_kernel/agents/azure_ai/azure_ai_agent_settings.py) | | Bedrock | [BedrockAgent](../../../semantic_kernel/agents/bedrock/bedrock_agent.py) | agent_resource_role_arn,
foundation_model | BEDROCK_AGENT_AGENT_RESOURCE_ROLE_ARN,
BEDROCK_AGENT_FOUNDATION_MODEL | Yes,
Yes | [BedrockAgentSettings](../../../semantic_kernel/agents/bedrock/bedrock_agent_settings.py) | -| Copilot Studio | [CopilotStudioAgent](../../../semantic_kernel/agents/copilot_studio/copilot_studio_agent.py) | app_client_id,
tenant_id,
environment_id,
agent_identifier,
cloud,
copilot_agent_type,
custom_power_platform_cloud,
client_secret,
client_certificate,
user_assertion,
auth_mode | COPILOT_STUDIO_AGENT_APP_CLIENT_ID,
COPILOT_STUDIO_AGENT_TENANT_ID,
COPILOT_STUDIO_AGENT_ENVIRONMENT_ID,
COPILOT_STUDIO_AGENT_AGENT_IDENTIFIER,
COPILOT_STUDIO_AGENT_CLOUD,
COPILOT_STUDIO_AGENT_COPILOT_AGENT_TYPE,
COPILOT_STUDIO_AGENT_CUSTOM_POWER_PLATFORM_CLOUD,
COPILOT_STUDIO_AGENT_CLIENT_SECRET,
COPILOT_STUDIO_AGENT_CLIENT_CERTIFICATE,
COPILOT_STUDIO_AGENT_USER_ASSERTION,
COPILOT_STUDIO_AGENT_AUTH_MODE | No (varies by mode) | [CopilotStudioAgentSettings](../../../semantic_kernel/agents/copilot_studio/copilot_studio_agent_settings.py) | +| Copilot Studio | [CopilotStudioAgent](../../../semantic_kernel/agents/copilot_studio/copilot_studio_agent.py) | app_client_id,
tenant_id,
environment_id,
agent_identifier,
cloud,
copilot_agent_type,
custom_power_platform_cloud,
user_assertion,
auth_mode | COPILOT_STUDIO_AGENT_APP_CLIENT_ID,
COPILOT_STUDIO_AGENT_TENANT_ID,
COPILOT_STUDIO_AGENT_ENVIRONMENT_ID,
COPILOT_STUDIO_AGENT_AGENT_IDENTIFIER,
COPILOT_STUDIO_AGENT_CLOUD,
COPILOT_STUDIO_AGENT_COPILOT_AGENT_TYPE,
COPILOT_STUDIO_AGENT_CUSTOM_POWER_PLATFORM_CLOUD,
COPILOT_STUDIO_AGENT_USER_ASSERTION,
COPILOT_STUDIO_AGENT_AUTH_MODE | No (varies by configuration) | [CopilotStudioAgentSettings](../../../semantic_kernel/agents/copilot_studio/copilot_studio_agent_settings.py) | diff --git a/python/samples/getting_started_with_agents/copilot_studio/README.md b/python/samples/getting_started_with_agents/copilot_studio/README.md index 5df7248f3f44..5918a7327ec1 100644 --- a/python/samples/getting_started_with_agents/copilot_studio/README.md +++ b/python/samples/getting_started_with_agents/copilot_studio/README.md @@ -84,10 +84,6 @@ COPILOT_STUDIO_AGENT_AGENT_IDENTIFIER= COPILOT_STUDIO_AGENT_AUTH_MODE=interactive ``` -## Create an Application Registration in Entra ID – Service Principal Login - -> **Warning**: Service Principal login is **not yet supported** in the current version of the `CopilotStudioClient`. - ## Creating a `CopilotStudioAgent` Client If all required environment variables are set correctly, you don't need to manually create or pass a `client`. Semantic Kernel will automatically construct the client using the environment configuration. @@ -96,11 +92,9 @@ However, if you need to override any environment variables—such as when specif ```python client: CopilotClient = CopilotStudioAgent.create_client( - auth_mode: CopilotStudioAgentAuthMode | Literal["interactive", "service"] | None = None, + auth_mode: CopilotStudioAgentAuthMode | Literal["interactive"] | None = None, agent_identifier: str | None = None, app_client_id: str | None = None, - client_secret: str | None = None, - client_certificate: str | None = None, cloud: PowerPlatformCloud | None = None, copilot_agent_type: AgentType | None = None, custom_power_platform_cloud: str | None = None, diff --git a/python/semantic_kernel/agents/copilot_studio/copilot_studio_agent.py b/python/semantic_kernel/agents/copilot_studio/copilot_studio_agent.py index 4c8da0fb21bd..effeec5b4073 100644 --- a/python/semantic_kernel/agents/copilot_studio/copilot_studio_agent.py +++ b/python/semantic_kernel/agents/copilot_studio/copilot_studio_agent.py @@ -4,12 +4,11 @@ import sys from collections.abc import AsyncIterable, Awaitable, Callable, Sequence from os import environ, path -from pathlib import Path from typing import TYPE_CHECKING, Any, ClassVar, Literal from microsoft_agents.activity import ActivityTypes from microsoft_agents.copilotstudio.client import AgentType, CopilotClient, PowerPlatformCloud -from msal import ConfidentialClientApplication, PublicClientApplication +from msal import PublicClientApplication from msal_extensions import FilePersistence, PersistedTokenCache, build_encrypted_persistence from pydantic import ValidationError @@ -69,18 +68,10 @@ def __init__( *, settings: CopilotStudioAgentSettings, cache_path: str, - mode: CopilotStudioAgentAuthMode, - client_secret: str | None = None, - client_certificate: Path | None = None, - user_assertion: str | None = None, scopes: Sequence[str] | None = None, ) -> None: self.settings = settings self.cache = self._get_msal_token_cache(cache_path) - self.mode = mode - self.client_secret = client_secret - self.client_cert_path = client_certificate - self.user_assertion = user_assertion self.scopes = scopes or ["https://api.powerplatform.com/.default"] @staticmethod @@ -105,52 +96,7 @@ def _get_msal_token_cache(cache_path: str, fallback_to_plaintext=True) -> Persis def acquire(self) -> str: """Return a valid bearer token or raise AgentInitializationException.""" - if self.mode is CopilotStudioAgentAuthMode.SERVICE: - # SERVICE auth wiring is present but not yet supported end-to-end. - logger.warning("SERVICE authentication mode is not yet supported; falling back to error.") - raise AgentInitializationException( - "Copilot Studio SERVICE authentication is not available yet. Please use INTERACTIVE mode instead." - ) - - match self.mode: - case CopilotStudioAgentAuthMode.SERVICE: - return self._acquire_service_token() # unreachable until the guard is removed - case _: - return self._acquire_interactive_token() - - def _new_confidential_client(self, **extra_kwargs) -> ConfidentialClientApplication: - return ConfidentialClientApplication( - client_id=self.settings.app_client_id, - authority=f"https://login.microsoftonline.com/{self.settings.tenant_id}", - token_cache=self.cache, - **extra_kwargs, - ) - - def _acquire_service_token(self) -> str: - if not self.client_secret and not self.client_cert_path: - raise AgentInitializationException( - "client_secret *or* client_certificate is required for service-to-service auth." - ) - - kwargs: dict[str, Any] = {} - if self.client_secret: - kwargs["client_credential"] = self.client_secret - else: # certificate - if not self.client_cert_path: - raise AgentInitializationException( - "If no client_secret is provided, a client_certificate is required for service-to-service auth." - ) - kwargs["client_credential"] = { - "private_key": Path(self.client_cert_path).read_text(), - "thumbprint": self._cert_thumbprint(self.client_cert_path), - } - - app = self._new_confidential_client(**kwargs) - - # proactive caching - result = app.acquire_token_silent(self.scopes, account=None) or app.acquire_token_for_client(scopes=self.scopes) - - return self._unwrap(result) + return self._acquire_interactive_token() # interactive def _acquire_interactive_token(self) -> str: @@ -174,18 +120,6 @@ def _unwrap(result: dict[str, Any]) -> str: _log_auth_failure(result) raise AgentInitializationException("Authentication failed; see logs for category and correlation code.") - @staticmethod - def _cert_thumbprint(cert_path: Path) -> str: - import hashlib - import ssl - - pem_bytes = Path(cert_path).read_bytes() - der_bytes = ssl.PEM_cert_to_DER_cert(pem_bytes.decode()) - # SHA-1 is not used here as a security primitive; it is required to compute the X.509 certificate thumbprint - # (the `x5t` JWT header value), which MSAL and Microsoft Entra ID mandate to be a SHA-1 digest for the - # `thumbprint` client credential. Hence the `usedforsecurity=False` flag. - return hashlib.sha1(der_bytes, usedforsecurity=False).hexdigest().upper() # CodeQL [SM02167] x5t thumbprint - # endregion @@ -329,11 +263,9 @@ def model_post_init(self, __ctx: Any) -> None: @staticmethod def create_client( *, - auth_mode: CopilotStudioAgentAuthMode | Literal["interactive", "service"] | None = None, + auth_mode: CopilotStudioAgentAuthMode | Literal["interactive"] | None = None, agent_identifier: str | None = None, app_client_id: str | None = None, - client_secret: str | None = None, - client_certificate: str | None = None, cloud: PowerPlatformCloud | None = None, copilot_agent_type: AgentType | None = None, custom_power_platform_cloud: str | None = None, @@ -346,12 +278,10 @@ def create_client( """Create the Copilot Studio Agent Client. Args: - auth_mode: The authentication mode. This can be either `interactive` or `service`. + auth_mode: The authentication mode. Only `interactive` is currently supported. agent_identifier: The agent identifier. This is the `Schema Name` of the agent from the Copilot Studio Advanced Metadata settings. app_client_id: The app client ID. This is the app ID of the app registration configured in Entra. - client_secret: The client secret. This is the secret of the app registration. - client_certificate: The client certificate. This is the certificate of the app registration. cloud: The cloud environment. copilot_agent_type: The type of Copilot agent. custom_power_platform_cloud: The custom Power Platform cloud. @@ -378,8 +308,6 @@ def create_client( custom_power_platform_cloud=custom_power_platform_cloud, env_file_path=env_file_path, env_file_encoding=env_file_encoding, - client_secret=client_secret, - client_certificate=client_certificate, user_assertion=user_assertion, auth_mode=auth_mode, ) @@ -397,12 +325,6 @@ def create_client( token = _CopilotStudioAgentTokenFactory( settings=connection_settings, cache_path=cache_file, - mode=connection_settings.auth_mode, - client_secret=connection_settings.client_secret.get_secret_value() - if connection_settings.client_secret - else None, - client_certificate=Path(client_certificate) if client_certificate else None, - user_assertion=user_assertion, ).acquire() return CopilotClient(connection_settings, token) diff --git a/python/semantic_kernel/agents/copilot_studio/copilot_studio_agent_settings.py b/python/semantic_kernel/agents/copilot_studio/copilot_studio_agent_settings.py index 797363345ac6..9c99d686210f 100644 --- a/python/semantic_kernel/agents/copilot_studio/copilot_studio_agent_settings.py +++ b/python/semantic_kernel/agents/copilot_studio/copilot_studio_agent_settings.py @@ -7,7 +7,7 @@ AgentType, PowerPlatformCloud, ) -from pydantic import Field, SecretStr +from pydantic import Field from semantic_kernel.kernel_pydantic import KernelBaseSettings from semantic_kernel.utils.feature_stage_decorator import experimental @@ -17,8 +17,7 @@ class CopilotStudioAgentAuthMode(str, Enum): """The Copilot Studio agent authentication mode.""" - INTERACTIVE = "interactive" # user authentication - SERVICE = "service" # client-credentials (app secret/cert) + INTERACTIVE = "interactive" @experimental @@ -34,7 +33,5 @@ class CopilotStudioAgentSettings(KernelBaseSettings): cloud: PowerPlatformCloud = Field(default=PowerPlatformCloud.UNKNOWN) copilot_agent_type: AgentType = Field(default=AgentType.PUBLISHED) custom_power_platform_cloud: str | None = None - client_secret: SecretStr | None = None - client_certificate: str | None = None user_assertion: str | None = None auth_mode: CopilotStudioAgentAuthMode = Field(default=CopilotStudioAgentAuthMode.INTERACTIVE) diff --git a/python/tests/unit/agents/copilot_studio/test_copilot_studio_agent.py b/python/tests/unit/agents/copilot_studio/test_copilot_studio_agent.py index fa0ce044ef70..3c3374e2f1bc 100644 --- a/python/tests/unit/agents/copilot_studio/test_copilot_studio_agent.py +++ b/python/tests/unit/agents/copilot_studio/test_copilot_studio_agent.py @@ -7,7 +7,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest -from pydantic import SecretStr, ValidationError +from pydantic import ValidationError try: import microsoft_agents.copilotstudio.client # type: ignore # noqa: F401 @@ -226,7 +226,6 @@ def test_setup_resources_happy_path(tmp_path, monkeypatch): dummy_settings.app_client_id = "appid" dummy_settings.tenant_id = "tenantid" dummy_settings.auth_mode = CopilotStudioAgentAuthMode.INTERACTIVE - dummy_settings.client_secret = SecretStr("test-secret") monkeypatch.setattr( "semantic_kernel.agents.copilot_studio.copilot_studio_agent.CopilotStudioAgentSettings", @@ -246,7 +245,7 @@ def test_setup_resources_happy_path(tmp_path, monkeypatch): tenant_id="tenantid", environment_id="env-id", agent_identifier="agent-name", - auth_mode=CopilotStudioAgentAuthMode.SERVICE, + auth_mode=CopilotStudioAgentAuthMode.INTERACTIVE, ) mock_client_ctor.assert_called_once_with(dummy_settings, "fake-token") @@ -255,19 +254,6 @@ def test_setup_resources_happy_path(tmp_path, monkeypatch): class DummyCache: pass - class FakeAppSilent: - def __init__(self, client_id, authority, token_cache, client_credential=None, **kwargs): - pass - - def get_accounts(self): - return [{"home_account_id": "acct1"}] - - def acquire_token_silent(self, scopes, account): - return {"access_token": "silent-token"} - - def acquire_token_interactive(self, scopes): - pytest.skip("Unexpected interactive flow in silent test") - class FakeAppInteractive: def __init__(self, client_id, authority, token_cache): pass @@ -306,54 +292,28 @@ def stub_cache(monkeypatch): staticmethod(lambda cache_path, fallback_to_plaintext=True: DummyCache()), ) - @pytest.mark.parametrize( - "fake_app, expected_token, mode", - [ - pytest.param( - FakeAppSilent, - "silent-token", - CopilotStudioAgentAuthMode.SERVICE, - marks=pytest.mark.skip(reason="Skipping SERVICE auth mode test as the mode is not yet supported."), - ), - (FakeAppInteractive, "interactive-token", CopilotStudioAgentAuthMode.INTERACTIVE), - ], - ) - def test_acquire_token_success(monkeypatch, tmp_path, fake_app, expected_token, mode): + def test_acquire_token_success(monkeypatch, tmp_path): settings = CopilotStudioAgentSettings(app_client_id="id", tenant_id="tid") cache_path = str(tmp_path / "cache.bin") - monkeypatch.setattr(csa_mod, "PublicClientApplication", fake_app) - monkeypatch.setattr(csa_mod, "ConfidentialClientApplication", fake_app) - - client_secret = None - if fake_app == FakeAppSilent: - client_secret = "test-secret" + monkeypatch.setattr(csa_mod, "PublicClientApplication", FakeAppInteractive) factory = _CopilotStudioAgentTokenFactory( settings=settings, cache_path=cache_path, - mode=mode, - client_secret=client_secret, - client_certificate=None, - user_assertion=None, ) token = factory.acquire() - assert token == expected_token + assert token == "interactive-token" def test_acquire_token_error(monkeypatch, tmp_path): settings = CopilotStudioAgentSettings(app_client_id="id", tenant_id="tid") cache_path = str(tmp_path / "cache.bin") monkeypatch.setattr(csa_mod, "PublicClientApplication", FakeAppError) - monkeypatch.setattr(csa_mod, "ConfidentialClientApplication", FakeAppError) factory = _CopilotStudioAgentTokenFactory( settings=settings, cache_path=cache_path, - mode=CopilotStudioAgentAuthMode.INTERACTIVE, - client_secret=None, - client_certificate=None, - user_assertion=None, ) with pytest.raises(AgentInitializationException): factory.acquire()