mirror of
https://github.com/microsoft/semantic-kernel.git
synced 2026-10-12 02:19:06 +00:00
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:
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user