Skip to content
Open
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/docker_image_publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,11 @@ jobs:
uses: Azure/docker-login@v2
with:
# Container registry username
username: ${{ secrets.MAIN_ACR_USERNAME }}
username: ${{ secrets.ACR_USERNAME }}
# Container registry password
password: ${{ secrets.MAIN_ACR_PASSWORD }}
password: ${{ secrets.ACR_PASSWORD }}
# Container registry server url
login-server: ${{ secrets.MAIN_ACR_LOGIN_SERVER }}
login-server: ${{ secrets.ACR_LOGIN_SERVER }}
- name: Normalize branch name for tag
run: |
REF="${GITHUB_REF_NAME}"
Expand Down
96 changes: 93 additions & 3 deletions application/single_app/semantic_kernel_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@
from semantic_kernel_plugins.plugin_loader import discover_plugins
from semantic_kernel_plugins.openapi_plugin_factory import OpenApiPluginFactory
import app_settings_cache
try:
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
except ImportError:
DefaultAzureCredential = None
get_bearer_token_provider = None



Expand Down Expand Up @@ -295,26 +300,32 @@ def merge_fields(primary, fallback):
debug_print(f"[SK Loader] Using user APIM with global fallback")
merged = merge_fields(u_apim, g_apim if global_apim_enabled and any_filled(*g_apim) else (None, None, None, None))
endpoint, key, deployment, api_version = merged
endpoint_is_user_supplied = True
# 2. User APIM enabled but no user APIM values, and global APIM enabled and present: use global APIM
elif user_apim_enabled and global_apim_enabled and any_filled(*g_apim):
debug_print(f"[SK Loader] Using global APIM (user APIM enabled but not present)")
endpoint, key, deployment, api_version = g_apim
endpoint_is_user_supplied = False
# 3. User GPT config is FULLY filled: use user GPT (all fields filled)
elif all_filled(*u_gpt) and can_use_agent_endpoints:
debug_print(f"[SK Loader] Using agent GPT config (all fields filled)")
endpoint, key, deployment, api_version = u_gpt
endpoint_is_user_supplied = True
# 4. User GPT config is PARTIALLY filled, global APIM is NOT enabled: merge user GPT with global GPT
elif any_filled(*u_gpt) and not global_apim_enabled and can_use_agent_endpoints:
debug_print(f"[SK Loader] Using agent GPT config (partially filled, merging with global GPT, global APIM not enabled)")
endpoint, key, deployment, api_version = merge_fields(u_gpt, g_gpt)
endpoint_is_user_supplied = True
# 5. Global APIM enabled and present: use global APIM
elif global_apim_enabled and any_filled(*g_apim):
debug_print(f"[SK Loader] Using global APIM (fallback)")
endpoint, key, deployment, api_version = g_apim
endpoint_is_user_supplied = False
# 6. Fallback to global GPT config
else:
debug_print(f"[SK Loader] Using global GPT config (fallback)")
endpoint, key, deployment, api_version = g_gpt
endpoint_is_user_supplied = False

result = {
"endpoint": endpoint,
Expand All @@ -337,6 +348,9 @@ def merge_fields(primary, fallback):
"max_completion_tokens": agent.get("max_completion_tokens", -1), # -1 meant use model default determined by the service, 35-trubo is 4096, 4o is 16384, 4.1 is at least 32768
"agent_type": agent_type or "local",
"other_settings": other_settings,
# Security: track whether the endpoint was user/agent-supplied vs system-controlled.
# Managed identity must NOT be used with user-supplied endpoints to prevent token theft.
"endpoint_is_user_supplied": endpoint_is_user_supplied,
}

print(f"[SK Loader] Final resolved config for {agent.get('name')}: endpoint={bool(endpoint)}, key={bool(key)}, deployment={deployment}")
Expand Down Expand Up @@ -721,6 +735,20 @@ def normalize(s):
print(f"[SK Loader] Error loading agent-specific plugins: {e}")
log_event(f"[SK Loader] Error loading agent-specific plugins: {e}", level=logging.ERROR, exceptionTraceback=True)

def _build_mi_token_provider(endpoint):
"""Build a bearer token provider for managed identity auth.

Selects the correct Azure Cognitive Services scope based on whether the
endpoint is in the US Government cloud (.azure.us) or the commercial cloud.
"""
scope = (
"https://cognitiveservices.azure.us/.default"
if ".azure.us" in (endpoint or "")
else "https://cognitiveservices.azure.com/.default"
)
return get_bearer_token_provider(DefaultAzureCredential(), scope)


def load_single_agent_for_kernel(kernel, agent_cfg, settings, context_obj, redis_client=None, mode_label="global"):
"""
DRY helper to load a single agent (default agent) for the kernel.
Expand Down Expand Up @@ -758,7 +786,15 @@ def load_single_agent_for_kernel(kernel, agent_cfg, settings, context_obj, redis

log_event(f"[SK Loader] Agent config resolved for {agent_cfg.get('name')} - endpoint: {bool(agent_config.get('endpoint'))}, key: {bool(agent_config.get('key'))}, deployment: {agent_config.get('deployment')}, max_completion_tokens: {agent_config.get('max_completion_tokens')}", level=logging.INFO)

if AzureChatCompletion and agent_config["endpoint"] and agent_config["key"] and agent_config["deployment"]:
auth_type = settings.get('azure_openai_gpt_authentication_type', '')
use_managed_identity = (
auth_type == 'managed_identity'
and not apim_enabled
and not agent_config.get("key")
and bool(DefaultAzureCredential)
and not agent_config.get("endpoint_is_user_supplied", False)
)
if AzureChatCompletion and agent_config["endpoint"] and (agent_config["key"] or use_managed_identity) and agent_config["deployment"]:
Comment thread
vivche marked this conversation as resolved.
Comment thread
vivche marked this conversation as resolved.
print(f"[SK Loader] Azure config valid for {agent_config['name']}, creating chat service...")
if apim_enabled:
log_event(
Expand All @@ -779,6 +815,24 @@ def load_single_agent_for_kernel(kernel, agent_cfg, settings, context_obj, redis
api_version=agent_config["api_version"],
# default_headers={"Ocp-Apim-Subscription-Key": agent_config["key"]}
)
elif use_managed_identity:
log_event(
f"[SK Loader] Initializing Managed Identity AzureChatCompletion for agent: {agent_config['name']} ({mode_label})",
{
"aoai_endpoint": agent_config["endpoint"],
"aoai_deployment": agent_config["deployment"],
"agent_name": agent_config["name"]
},
level=logging.INFO
)
_token_provider = _build_mi_token_provider(agent_config.get("endpoint"))
chat_service = AzureChatCompletion(
service_id=service_id,
deployment_name=agent_config["deployment"],
endpoint=agent_config["endpoint"],
ad_token_provider=_token_provider,
api_version=agent_config["api_version"],
)
else:
log_event(
f"[SK Loader] Initializing GPT Direct AzureChatCompletion for agent: {agent_config['name']} ({mode_label})",
Expand Down Expand Up @@ -1521,7 +1575,16 @@ def load_semantic_kernel(kernel: Kernel, settings):
agent_config = resolve_agent_config(agent_cfg, settings)
chat_service = None
service_id = f"aoai-chat-{agent_config['name'].replace(' ', '').lower()}"
if AzureChatCompletion and agent_config["endpoint"] and agent_config["key"] and agent_config["deployment"]:
_ma_auth_type = settings.get('azure_openai_gpt_authentication_type', '')
_ma_apim_enabled = settings.get("enable_gpt_apim", False)
_ma_use_mi = (
_ma_auth_type == 'managed_identity'
and not _ma_apim_enabled
and not agent_config.get("key")
and bool(DefaultAzureCredential)
and not agent_config.get("endpoint_is_user_supplied", False)
)
if AzureChatCompletion and agent_config["endpoint"] and (agent_config["key"] or _ma_use_mi) and agent_config["deployment"]:
try:
try:
chat_service = kernel.get_service(service_id=service_id)
Expand All @@ -1548,6 +1611,15 @@ def load_semantic_kernel(kernel: Kernel, settings):
api_version=agent_config["api_version"],
# default_headers={"Ocp-Apim-Subscription-Key": agent_config["key"]}
)
elif _ma_use_mi:
_token_provider = _build_mi_token_provider(agent_config.get("endpoint"))
chat_service = AzureChatCompletion(
service_id=service_id,
deployment_name=agent_config["deployment"],
endpoint=agent_config["endpoint"],
ad_token_provider=_token_provider,
api_version=agent_config["api_version"],
)
else:
chat_service = AzureChatCompletion(
service_id=service_id,
Expand Down Expand Up @@ -1631,7 +1703,16 @@ def load_semantic_kernel(kernel: Kernel, settings):
orchestrator_config = resolve_agent_config(orchestrator_cfg, settings)
service_id = f"aoai-chat-{orchestrator_config['name']}"
chat_service = None
if AzureChatCompletion and orchestrator_config["endpoint"] and orchestrator_config["key"] and orchestrator_config["deployment"]:
_orch_auth_type = settings.get('azure_openai_gpt_authentication_type', '')
_orch_apim_enabled = settings.get("enable_gpt_apim", False)
_orch_use_mi = (
_orch_auth_type == 'managed_identity'
and not _orch_apim_enabled
and not orchestrator_config.get("key")
and bool(DefaultAzureCredential)
and not orchestrator_config.get("endpoint_is_user_supplied", False)
)
if AzureChatCompletion and orchestrator_config["endpoint"] and (orchestrator_config["key"] or _orch_use_mi) and orchestrator_config["deployment"]:
try:
chat_service = kernel.get_service(service_id=service_id)
except Exception:
Expand All @@ -1657,6 +1738,15 @@ def load_semantic_kernel(kernel: Kernel, settings):
api_version=orchestrator_config["api_version"],
# default_headers={"Ocp-Apim-Subscription-Key": orchestrator_config["key"]}
)
elif _orch_use_mi:
_token_provider = _build_mi_token_provider(orchestrator_config.get("endpoint"))
chat_service = AzureChatCompletion(
service_id=service_id,
deployment_name=orchestrator_config["deployment"],
endpoint=orchestrator_config["endpoint"],
ad_token_provider=_token_provider,
api_version=orchestrator_config["api_version"],
)
else:
chat_service = AzureChatCompletion(
service_id=service_id,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# Agent Managed Identity SK Loader Fix

**Fixed/Implemented in version:** **0.238.025**
**GitHub Issue:** [#769 — Agents fail silently when using Managed Identity authentication](https://github.com/microsoft/simplechat/issues/769)

## Issue Description

When using **Azure Managed Identity (MI)** for Azure OpenAI authentication, agents configured through
the **Model & Connection** page (Step 2 of the agent wizard) failed silently — the agent never loaded,
fell back to plain GPT-4.1 with no tools or instructions, and fabricated responses instead of calling
real APIs (e.g., ServiceNow).

## Root Cause Analysis

### How Agent Config Is Resolved

`resolve_agent_config()` in `semantic_kernel_loader.py` (~line 107) figures out which endpoint/key/
deployment to use for an agent by running through a **decision tree** (~line 291):

```
# 1. User APIM enabled and any user APIM values set → use user APIM
# 2. User APIM enabled but empty, global APIM enabled → use global APIM
# 3. Agent GPT config is FULLY filled → use agent GPT config
# 4. Agent GPT config is PARTIALLY filled, global APIM off → merge agent GPT with global GPT
# 5. Global APIM enabled → use global APIM
# 6. Fallback → use global GPT config entirely
```

### The Failure

When an agent is configured with only the deployment name set (endpoint and key left blank), the
decision tree hits **case 4** — it merges the agent's partial config with global settings:

```
Agent-level: endpoint='', key='', deployment='gpt-4.1', api_version=''
```

After merge with global settings:
- `endpoint` = global endpoint ✓
- `deployment` = `'gpt-4.1'` ✓
- `key` = global key = **`None`** ✗ (MI auth — no API key is stored in settings)

The gate condition at ~line 768 then fails:

```python
if AzureChatCompletion and agent_config["endpoint"] and agent_config["key"] and agent_config["deployment"]:
```

`agent_config["key"]` is `None` → **condition is False** → falls into the `else` block:

```
[SK Loader] Azure config INVALID for servicenow_support_agent:
- AzureChatCompletion available: True
- endpoint: True
- key: False ← THIS IS THE FAILURE
- deployment: True
```

Returns `None, None` → no agent loaded → chat uses plain GPT-4.1 with no tools/instructions
→ GPT fabricates responses instead of calling the actual API.

## Files Modified

| File | Lines Changed |
|------|--------------|
| `application/single_app/semantic_kernel_loader.py` | ~43-47, ~767-768, ~810-829, ~1530-1532, ~1548-1558, ~1636-1638, ~1655-1665 |
| `application/single_app/config.py` | VERSION bump |

## Fix

### 1. Added Azure Identity imports (~line 43)

```python
try:
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
except ImportError:
DefaultAzureCredential = None
get_bearer_token_provider = None
```

### 2. Added MI detection before each gate (~line 767)

At each of the 3 `AzureChatCompletion` creation sites (single agent, multi-agent specialist,
multi-agent orchestrator):

```python
auth_type = settings.get('azure_openai_gpt_authentication_type', '')
use_managed_identity = (auth_type == 'managed_identity') and not apim_enabled and not agent_config.get("key")
```

`use_managed_identity` is `True` when ALL of:
- Global auth type is `managed_identity`
- APIM is not enabled (APIM uses subscription keys, not MI)
- No API key is present (if a key exists, use it directly)

### 3. Updated gate condition to accept MI (~line 768)

Before:
```python
if AzureChatCompletion and agent_config["endpoint"] and agent_config["key"] and agent_config["deployment"]:
```

After:
```python
if AzureChatCompletion and agent_config["endpoint"] and (agent_config["key"] or use_managed_identity) and agent_config["deployment"]:
```

### 4. Added MI branch for AzureChatCompletion creation (~line 789)

Between the existing APIM branch and direct-key branch, a new `elif use_managed_identity:` block:

```python
elif use_managed_identity:
# Detect gov vs commercial cloud from endpoint URL
_scope = "https://cognitiveservices.azure.us/.default" if ".azure.us" in (agent_config.get("endpoint") or "") else "https://cognitiveservices.azure.com/.default"
_token_provider = get_bearer_token_provider(DefaultAzureCredential(), _scope)
chat_service = AzureChatCompletion(
service_id=service_id,
deployment_name=agent_config["deployment"],
endpoint=agent_config["endpoint"],
ad_token_provider=_token_provider, # ← MI token, not api_key
api_version=agent_config["api_version"],
)
```

The scope is auto-detected: endpoints containing `.azure.us` use the Azure Government scope;
all others use the commercial Azure scope.

## Auth Flow After Fix

```
User sends message
→ SK Loader resolves agent config (case 4: merge agent partial + global GPT)
→ endpoint = global endpoint, key = None (MI), deployment = 'gpt-4.1'
→ use_managed_identity = True (auth_type='managed_identity', key=None, APIM=off)
→ Gate passes: (agent_config["key"] or use_managed_identity) = True
→ AzureChatCompletion created with ad_token_provider (DefaultAzureCredential)
→ Agent loads with full instructions + ServiceNow tools (OpenAPI plugin)
→ Agent calls queryAssets → OpenAPI plugin injects Bearer token → ServiceNow returns real data
→ Real results displayed (no fabrication)
```