Python: [Breaking] Remove unsupported service auth mode from Copilot Studio agent (#14306)

### Motivation and Context

The Copilot Studio agent exposed a `SERVICE` authentication mode that
was never reachable — it was guarded to always raise before its
implementation ran. Its dormant credential handling also triggered
certificate-related static analysis alerts.

### Description

Removes the service authentication path along with its settings,
parameters, tests, and documentation. `CopilotStudioAgentAuthMode` is
kept with its `INTERACTIVE` member, which is the only supported mode.
Interactive authentication is unchanged.

Service authentication can be reintroduced later as a complete, tested
feature.

### Contribution Checklist

- [x] The code builds clean without any errors or warnings
- [x] The PR follows the [SK Contribution
Guidelines](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md)
and the [pre-submission formatting
script](https://github.com/microsoft/semantic-kernel/blob/main/CONTRIBUTING.md#development-scripts)
raises no violations
- [x] All unit tests pass, and I have added new tests where possible
- [x] I didn't break anyone 😄

---------

Copilot-Session: 25dd6e2a-f759-4148-a630-40110e90eff2
This commit is contained in:
SergeyMenshykh
2026-08-20 11:36:32 +01:00
committed by GitHub
parent 5e1f1fb87d
commit b39d95a344
5 changed files with 13 additions and 140 deletions
@@ -49,7 +49,7 @@
| | [AzureResponsesAgent](../../../semantic_kernel/agents/open_ai/azure_responses_agent.py) | deployment_name, <br> api_key, <br> endpoint, <br> api_version, <br> base_url | AZURE_OPENAI_RESPONSES_DEPLOYMENT_NAME, <br> AZURE_OPENAI_API_KEY, <br> AZURE_OPENAI_ENDPOINT, <br> AZURE_OPENAI_API_VERSION, <br> AZURE_OPENAI_BASE_URL | Yes, <br> No, <br> Yes, <br> Yes, <br> 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, <br> endpoint, <br> agent_id, <br> bing_connection_id, <br> azure_ai_search_connection_id, <br> azure_ai_search_index_name, <br> api_version | AZURE_AI_AGENT_MODEL_DEPLOYMENT_NAME, <br> AZURE_AI_AGENT_ENDPOINT, <br> AZURE_AI_AGENT_AGENT_ID, <br> AZURE_AI_AGENT_BING_CONNECTION_ID, <br> AZURE_AI_AGENT_AZURE_AI_SEARCH_CONNECTION_ID, <br> AZURE_AI_AGENT_AZURE_AI_SEARCH_INDEX_NAME, <br> AZURE_AI_AGENT_API_VERSION | Yes, <br> Yes, <br> No, <br> No, <br> No, <br> No, <br> 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, <br> foundation_model | BEDROCK_AGENT_AGENT_RESOURCE_ROLE_ARN, <br> BEDROCK_AGENT_FOUNDATION_MODEL | Yes, <br> 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, <br> tenant_id, <br> environment_id, <br> agent_identifier, <br> cloud, <br> copilot_agent_type, <br> custom_power_platform_cloud, <br> client_secret, <br> client_certificate, <br> user_assertion, <br> auth_mode | COPILOT_STUDIO_AGENT_APP_CLIENT_ID, <br> COPILOT_STUDIO_AGENT_TENANT_ID, <br> COPILOT_STUDIO_AGENT_ENVIRONMENT_ID, <br> COPILOT_STUDIO_AGENT_AGENT_IDENTIFIER, <br> COPILOT_STUDIO_AGENT_CLOUD, <br> COPILOT_STUDIO_AGENT_COPILOT_AGENT_TYPE, <br> COPILOT_STUDIO_AGENT_CUSTOM_POWER_PLATFORM_CLOUD, <br> COPILOT_STUDIO_AGENT_CLIENT_SECRET, <br> COPILOT_STUDIO_AGENT_CLIENT_CERTIFICATE, <br> COPILOT_STUDIO_AGENT_USER_ASSERTION, <br> 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, <br> tenant_id, <br> environment_id, <br> agent_identifier, <br> cloud, <br> copilot_agent_type, <br> custom_power_platform_cloud, <br> user_assertion, <br> auth_mode | COPILOT_STUDIO_AGENT_APP_CLIENT_ID, <br> COPILOT_STUDIO_AGENT_TENANT_ID, <br> COPILOT_STUDIO_AGENT_ENVIRONMENT_ID, <br> COPILOT_STUDIO_AGENT_AGENT_IDENTIFIER, <br> COPILOT_STUDIO_AGENT_CLOUD, <br> COPILOT_STUDIO_AGENT_COPILOT_AGENT_TYPE, <br> COPILOT_STUDIO_AGENT_CUSTOM_POWER_PLATFORM_CLOUD, <br> COPILOT_STUDIO_AGENT_USER_ASSERTION, <br> COPILOT_STUDIO_AGENT_AUTH_MODE | No (varies by configuration) | [CopilotStudioAgentSettings](../../../semantic_kernel/agents/copilot_studio/copilot_studio_agent_settings.py) |
@@ -84,10 +84,6 @@ COPILOT_STUDIO_AGENT_AGENT_IDENTIFIER=<your-agent-id>
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,
@@ -4,12 +4,11 @@ import logging
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 @@ class _CopilotStudioAgentTokenFactory:
*,
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 @@ class _CopilotStudioAgentTokenFactory:
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 @@ class _CopilotStudioAgentTokenFactory:
_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 @@ class CopilotStudioAgent(Agent):
@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 @@ class CopilotStudioAgent(Agent):
"""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 @@ class CopilotStudioAgent(Agent):
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 @@ class CopilotStudioAgent(Agent):
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)
@@ -7,7 +7,7 @@ from microsoft_agents.copilotstudio.client import (
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 @@ from semantic_kernel.utils.feature_stage_decorator import experimental
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)
@@ -7,7 +7,7 @@ from typing import TypeVar
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 @@ if copilot_installed:
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 @@ if copilot_installed:
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 @@ if copilot_installed:
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 @@ if copilot_installed:
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()