Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@
EXECUTOR_TYPE = 'thread'
EXECUTOR_MAX_WORKERS = 30
SESSION_TYPE = 'filesystem'
VERSION = "0.260.022"
VERSION = "0.260.023"
IS_DEVELOPMENT = is_development_env_enabled()

SESSION_COOKIE_SAMESITE = os.getenv('SESSION_COOKIE_SAMESITE', 'Lax')
Expand Down
19 changes: 19 additions & 0 deletions application/single_app/functions_authentication.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
import base64
import json

from flask import has_request_context

from config import *
from functions_appinsights import log_event
from functions_settings import *
Expand Down Expand Up @@ -1065,6 +1067,23 @@
return user.get('oid')
return None

def get_current_user_id_or_none():
"""Return the current user id, or None when no request-scoped identity exists.

Startup and background callers run outside a Flask request context, where reading the
session proxy raises RuntimeError. Use this helper only where the identity is optional
context, such as scoping optional plugin or endpoint lookups. Authorization decisions must

Check warning on line 1075 in application/single_app/functions_authentication.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.

Check warning on line 1075 in application/single_app/functions_authentication.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains secret or sensitive data source marker. Recommendation%3A Pair this source with any nearby network, logging, serialization, or process execution sink before approving.
keep calling get_current_user_id() so a missing request context fails loudly instead of
silently resolving to an unauthenticated identity.
"""
if not has_request_context():
return None

try:

Check warning on line 1082 in application/single_app/functions_authentication.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
return get_current_user_id()
except Exception:

Check warning on line 1084 in application/single_app/functions_authentication.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
return None

def get_current_user_info():
user = session.get("user")
if not user:
Expand Down
46 changes: 28 additions & 18 deletions application/single_app/semantic_kernel_loader.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
resolve_openai_style_request_api_version,
)
from functions_appinsights import log_event, get_appinsights_logger
from functions_authentication import get_current_user_id
from functions_authentication import get_current_user_id_or_none
from semantic_kernel_plugins.plugin_health_checker import PluginHealthChecker, PluginErrorRecovery
from semantic_kernel_plugins.logged_plugin_loader import create_logged_plugin_loader
from semantic_kernel_plugins.plugin_invocation_logger import get_plugin_logger
Expand Down Expand Up @@ -357,8 +357,18 @@
if persisted_group_id:
return persisted_group_id

scope_user_id = get_current_user_id_or_none()
if not scope_user_id:
debug_print("[SK_LOADER] No request-scoped user available while resolving group endpoint scope.")
log_event(

Check warning on line 363 in application/single_app/semantic_kernel_loader.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
"[SK_LOADER] Group endpoint resolution could not determine a group scope.",
level=logging.WARNING,
extra={"agent_name": agent.get("name")}

Check warning on line 366 in application/single_app/semantic_kernel_loader.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
)
return ""

try:
return require_active_group(get_current_user_id())
return require_active_group(scope_user_id)
except ValueError as err:
debug_print(f"[SK_LOADER] No active group available while resolving group endpoint scope: {err}")
log_event(
Expand Down Expand Up @@ -529,11 +539,13 @@
endpoints.extend([{**endpoint, "_endpoint_scope": "group"} for endpoint in get_group_model_endpoints(group_id)])
elif not is_global_agent:
if allow_custom_agent_endpoints:
user_settings = get_user_settings(get_current_user_id())
endpoints.extend([
{**endpoint, "_endpoint_scope": "user"}
for endpoint in user_settings.get("settings", {}).get("personal_model_endpoints", [])
])
endpoint_user_id = get_current_user_id_or_none()
if endpoint_user_id:
user_settings = get_user_settings(endpoint_user_id)
endpoints.extend([
{**endpoint, "_endpoint_scope": "user"}
for endpoint in user_settings.get("settings", {}).get("personal_model_endpoints", [])

Check warning on line 547 in application/single_app/semantic_kernel_loader.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
])
endpoints.extend([{**endpoint, "_endpoint_scope": "global"} for endpoint in (settings.get("model_endpoints", []) or [])])

return endpoints
Expand Down Expand Up @@ -647,11 +659,13 @@
endpoints.extend([{**endpoint, "_endpoint_scope": "group"} for endpoint in get_group_model_endpoints(group_id)])
elif not is_global_agent:
if allow_custom_agent_endpoints:
user_settings = get_user_settings(get_current_user_id())
endpoints.extend([
{**endpoint, "_endpoint_scope": "user"}
for endpoint in user_settings.get("settings", {}).get("personal_model_endpoints", [])
])
endpoint_user_id = get_current_user_id_or_none()
if endpoint_user_id:
user_settings = get_user_settings(endpoint_user_id)
endpoints.extend([
{**endpoint, "_endpoint_scope": "user"}
for endpoint in user_settings.get("settings", {}).get("personal_model_endpoints", [])

Check warning on line 667 in application/single_app/semantic_kernel_loader.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
])
endpoints.extend([{**endpoint, "_endpoint_scope": "global"} for endpoint in (settings.get("model_endpoints", []) or [])])

endpoint_cfg = next((e for e in endpoints if e.get("id") == endpoint_id), None)
Expand Down Expand Up @@ -1924,7 +1938,7 @@
else:
plugin_mode = mode_label

resolved_user_id = get_current_user_id()
resolved_user_id = get_current_user_id_or_none()
group_id = agent_config.get("group_id") if agent_is_group else None
print(f"[SK_LOADER] Agent scope - is_global: {agent_is_global}, is_group: {agent_is_group}, plugin_mode: {plugin_mode}, group_id: {group_id}")
load_agent_specific_plugins(
Expand Down Expand Up @@ -2289,11 +2303,7 @@

# Use the logged plugin loader for custom plugins
try:
user_id = None
try:
user_id = get_current_user_id()
except Exception:
pass # User ID is optional for plugin loading
user_id = get_current_user_id_or_none()

# Load plugins with enhanced logging
results = logged_loader.load_multiple_plugins(plugin_manifests, user_id)
Expand Down
118 changes: 118 additions & 0 deletions docs/explanation/fixes/SEMANTIC_KERNEL_STARTUP_REQUEST_CONTEXT_FIX.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Semantic Kernel Startup Request Context Fix (v0.260.023)

Fixed/Implemented in version: **0.260.023**

Refs: [#1327](https://github.com/microsoft/simplechat/issues/1327)

Check warning on line 5 in docs/explanation/fixes/SEMANTIC_KERNEL_STARTUP_REQUEST_CONTEXT_FIX.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains external connection or remote asset marker. Recommendation%3A Review whether changed code can send prompts, files, credentials, cookies, settings, logs, or user data to a new sink.

## Issue Summary

Starting SimpleChat directly with `python application/single_app/app.py` (including via `uv run`)
aborted at startup with:

```
RuntimeError: Working outside of request context.
```

The failure only appeared once at least one action had been assigned to an agent and saved. Before any

Check warning on line 16 in docs/explanation/fixes/SEMANTIC_KERNEL_STARTUP_REQUEST_CONTEXT_FIX.md

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
action was assigned, the same configuration started normally, which made the problem look intermittent.

Container and App Service deployments were unaffected, because they use the gunicorn entrypoint
(`ENTRYPOINT ["python3", "-m", "gunicorn", "-c", "/app/gunicorn.conf.py", "app:app"]`). The
`if __name__ == '__main__':` block never runs there, so initialization happens through the
`@app.before_request` hook, where a request context exists.

## Root Cause

`initialize_application(force=True)` runs at module scope in the direct-run path, outside any Flask
request context. With Semantic Kernel enabled and `per_user_semantic_kernel` disabled, that reaches
global agent loading:

`initialize_semantic_kernel()` -> `load_semantic_kernel()` -> `load_single_agent_for_kernel()`

Inside `load_single_agent_for_kernel`, the `if agent_config.get("actions_to_load"):` branch called
`get_current_user_id()` without a guard. That function reads the Flask `session` proxy, which raises
`RuntimeError` when there is no request context, so startup aborted.

The `actions_to_load` branch is what introduced the failing call, which is why the crash only began
after an action was attached to an agent and persisted.

The equivalent lookup in `load_plugins_for_kernel` was already wrapped in `try/except` with a `None`
fallback. That inconsistency is why global plugin loading succeeded earlier in the same startup
sequence while agent-specific plugin loading failed.

Three further identity lookups in the same module had the same latent defect. They are not reached in a
global-agent-only configuration, but they would fail the same way in per-user and group scopes.

## Files Modified

- `application/single_app/functions_authentication.py`
- `application/single_app/semantic_kernel_loader.py`
- `application/single_app/config.py`
- `functional_tests/test_semantic_kernel_startup_without_request_context.py` (new)

## Code Changes Summary

1. Added `get_current_user_id_or_none()` to `functions_authentication.py`. It returns `None` when
`has_request_context()` is false and otherwise delegates to `get_current_user_id()`. `flask` is now
imported explicitly for `has_request_context` rather than relying on the `config` star-import.

2. Left `get_current_user_id()` unchanged. Authorization callers must keep failing loudly when there is
no request context, so the fallback is deliberately opt-in at each call site rather than applied
globally. `has_request_context()` is the established pattern for this across the codebase.

3. Routed all five identity lookups in `semantic_kernel_loader.py` through the new helper:

| Location | Change |
|---|---|
| `load_single_agent_for_kernel` agent plugin loading | The reported crash. Now resolves through the safe helper. |
| `resolve_agent_config.get_group_scope_id` | Resolves the identity first and returns an empty scope with the existing warning when it is absent. |
| `resolve_agent_config.get_agent_model_endpoint_candidates` | Skips personal endpoint collection when no identity is available. |
| `resolve_agent_config.resolve_foundry_endpoint_config` | Skips personal endpoint collection when no identity is available. |
| `load_plugins_for_kernel` | Replaced the ad-hoc `try/except` with the shared helper. |

4. Short-circuited the group and personal endpoint lookups instead of passing an unresolved identity
downstream. `require_active_group()` and `get_user_settings()` perform Cosmos reads keyed on the user
id, so forwarding `None` would only have traded the `RuntimeError` for a Cosmos lookup error.

## Behavior

Startup now completes in the direct-run path. The kernel and agent plugins load with no resolved user
identity, the same way global plugin loading already did.

Hosted deployments are unaffected. The fallback only applies when there is no request context at all;
inside a request the identity resolves exactly as before, including returning `None` for an
unauthenticated request.

## Validation

`functional_tests/test_semantic_kernel_startup_without_request_context.py` covers both halves of the fix:

- **Behavioral.** `get_current_user_id()` still raises `RuntimeError` outside a request context, so the
fail-loud property authorization code depends on is preserved. `get_current_user_id_or_none()` returns
`None` outside a request context, resolves the session `oid` inside an authenticated request, and
returns `None` for an unauthenticated request.
- **Structural.** An AST pass over `semantic_kernel_loader.py` asserts the module imports the safe helper,
makes no direct `get_current_user_id()` call, and never passes an identity call straight into
`require_active_group()` or `get_user_settings()`.

Both structural rules were verified to fail when the original defect is reintroduced, so the test is a
genuine regression guard rather than a restatement of the current source:

- Restoring the unguarded call reports `must not call get_current_user_id() directly; found at line(s) [1941]`.
- Passing the identity straight into `require_active_group()` reports the leaked-argument violation.

Result with the fix applied: `3/3 tests passed`.

The behavioral half stubs `config`, `functions_settings`, `functions_appinsights`, and `functions_debug`
in `sys.modules`, because importing the real `config` builds live Azure Cosmos clients at import time.
The stub re-exports the same Flask names `config.py` re-exports, since `functions_authentication` reaches
`session` through `from config import *`.

## Impact

- Local development works for configurations that assign actions to agents. Previously the only
workarounds were removing every agent action or running gunicorn locally.
- The three latent call sites are fixed alongside the reported one, so per-user and group scopes cannot
fail the same way from a non-request caller.
- Plugin and endpoint loading no longer depends on a request-scoped identity being present when the
identity is only optional context.
1 change: 1 addition & 0 deletions docs/explanation/fixes/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ category: Version History
---

- [Chat Document Search File Name and Divider Artifact Fix](CHAT_DOCUMENT_SEARCH_FILENAME_AND_DIVIDER_FIX.md)
- [Semantic Kernel Startup Request Context Fix](SEMANTIC_KERNEL_STARTUP_REQUEST_CONTEXT_FIX.md)
- [Data Management Restore Route Endpoint Collision Fix](DATA_MANAGEMENT_RESTORE_ROUTE_ENDPOINT_COLLISION_FIX.md)
- [Font Size and 200 Percent Zoom Fix](FONT_SIZE_AND_200_PERCENT_ZOOM_FIX.md)
- [Public Workspace Prompt Migration Fix](PUBLIC_WORKSPACE_PROMPT_MIGRATION_FIX.md)
Expand Down
11 changes: 11 additions & 0 deletions docs/explanation/release_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,17 @@

For feature-focused and fix-focused drill-downs by version, see [Features by Version](https://github.com/microsoft/simplechat/tree/main/docs/explanation/features) and [Fixes by Version](https://github.com/microsoft/simplechat/tree/main/docs/explanation/fixes).

### **(v0.260.023)**

#### Bug Fixes

* **Running Simple Chat Directly No Longer Fails To Start When An Agent Has Actions**
* Starting Simple Chat with `python app.py` (including via `uv run`) aborted with `RuntimeError: Working outside of request context` whenever any agent had an action assigned. The app started normally until the first action was saved, which made the failure look intermittent.
* Semantic Kernel initialization runs before any request exists on that path, but agent plugin loading read the signed-in user from the Flask session. It now resolves the user only when a request is actually in progress and otherwise loads with no user identity, matching how global plugin loading already behaved.
* Container and App Service deployments were never affected, because they start through gunicorn and initialize during the first request. Their behavior is unchanged.
* Three further identity lookups used for group scope and personal model endpoints had the same latent problem and were corrected at the same time.
* (Ref: `semantic_kernel_loader.py`, `functions_authentication.py`, `get_current_user_id_or_none`, issue #1327)

### **(v0.260.021)**

#### Bug Fixes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ def test_group_agent_endpoint_scope_resolution() -> None:

explicit_index = loader_content.index("if explicit_group_scope_id:")
persisted_index = loader_content.index("persisted_group_id = str(agent.get(\"group_id\") or \"\").strip()")
active_group_index = loader_content.index("return require_active_group(get_current_user_id())")
active_group_index = loader_content.index("return require_active_group(scope_user_id)")
assert explicit_index < persisted_index < active_group_index, (
"Group scope precedence should be explicit scope, then persisted group_id, then active group fallback."
)
Expand Down
Loading
Loading