Skip to content

Guard Semantic Kernel startup identity lookups against missing request context - #1330

Merged
Paul Lizer (paullizer) merged 1 commit into
Developmentfrom
paullizer-sk-startup-request-context-guard
Aug 21, 2026
Merged

Guard Semantic Kernel startup identity lookups against missing request context#1330
Paul Lizer (paullizer) merged 1 commit into
Developmentfrom
paullizer-sk-startup-request-context-guard

Conversation

@paullizer

Copy link
Copy Markdown
Contributor

Fixes #1327

Problem

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

RuntimeError: Working outside of request context.

The failure only appeared once at least one action had been assigned to an agent and saved, which made it look intermittent — the same configuration started fine beforehand.

Container and App Service deployments were never affected. They use the gunicorn entrypoint, so the if __name__ == '__main__': block never runs and initialization happens through @app.before_request, where a request context exists.

Root cause

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

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 reads the Flask session proxy, which raises when there is no request context. The actions_to_load branch is what introduces the 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.

Approach

Considered making get_current_user_id() itself return None outside a request context, but that would affect 365 call sites, and its raising behavior is a fail-loud property authorization code depends on. Auditing every caller to confirm None is never treated as "allow" isn't a defensible trade for this bug.

Instead the guard is expressed once as a named helper:

  • functions_authentication.py — added get_current_user_id_or_none(), which returns None when has_request_context() is false and otherwise delegates. has_request_context() is already the established pattern for this across the codebase, and the *_or_none naming matches existing helpers.
  • get_current_user_id() is unchanged, so authorization callers keep failing loudly.
  • semantic_kernel_loader.py — routed all five identity lookups through the helper:
Location Change
load_single_agent_for_kernel agent plugin loading The reported crash
resolve_agent_config.get_group_scope_id Latent twin
resolve_agent_config.get_agent_model_endpoint_candidates Latent twin
resolve_agent_config.resolve_foundry_endpoint_config Latent twin
load_plugins_for_kernel Replaced the ad-hoc try/except with the shared helper

The group and personal-endpoint sites also short-circuit rather than forwarding an unresolved identity — require_active_group() and get_user_settings() perform Cosmos reads keyed on the user id, so passing None would only have traded the RuntimeError for a Cosmos error.

Behavior

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

Hosted deployments are unaffected. The fallback applies only 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

New functional_tests/test_semantic_kernel_startup_without_request_context.py covers both halves:

  • Behavioralget_current_user_id() still raises outside a request context; get_current_user_id_or_none() returns None outside, resolves the session oid inside an authenticated request, and returns None for an unauthenticated request.
  • Structural — an AST pass asserts the loader 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 this 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.

Also run and passing: the related loader/agent tests (test_global_agent_scope_gate, test_default_model_selection_fallback, test_governance_enforcement_logic, test_foundry_endpoint_resolution, test_foundry_agent_endpoint_resolution, test_model_endpoint_protocol_inference, test_local_agent_cognitive_services_scope, test_workflow_auto_invoke_attempt_settings), all three route_tests/ policy contract tests, and the docs coverage and docs quality tests.

Note on test_group_agent_endpoint_scope_resolution.py

This test asserted on the exact literal return require_active_group(get_current_user_id()) as a positional marker for verifying scope precedence. The marker was updated to return require_active_group(scope_user_id); the precedence assertion it exists to enforce is unchanged and still passes.

Other test failures observed locally were confirmed pre-existing by baselining against Development with the changes stashed — they fail identically without this branch.

Other changes

  • config.py0.260.023
  • docs/explanation/fixes/SEMANTIC_KERNEL_STARTUP_REQUEST_CONTEXT_FIX.md + index entry
  • Release notes entry under a new v0.260.023 section

No documentation inventory work was needed — this adds no enable_* key, admin tab, action plugin, or chat control, and docs/_data/app_surface.yml is unaffected.

…t context

Running SimpleChat directly (python app.py) initializes Semantic Kernel at
module scope, outside any Flask request context. Loading an agent with actions
assigned called get_current_user_id() unguarded, which reads the Flask session
proxy and raised "RuntimeError: Working outside of request context", aborting
startup. Gunicorn deployments were unaffected because initialization happens in
a before_request hook.

Add get_current_user_id_or_none(), which returns None when there is no request
context, and route the five identity lookups in semantic_kernel_loader.py
through it. get_current_user_id() is left unchanged so authorization callers
keep failing loudly rather than silently degrading to no identity.

The group scope and personal endpoint lookups also short-circuit rather than
forwarding an unresolved identity, since require_active_group() and
get_user_settings() perform Cosmos reads keyed on the user id.

Fixes #1327

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Comment thread functional_tests/test_semantic_kernel_startup_without_request_context.py Dismissed
@paullizer
Paul Lizer (paullizer) merged commit fa94275 into Development Aug 21, 2026
11 of 12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants