Guard Semantic Kernel startup identity lookups against missing request context - #1330
Merged
Paul Lizer (paullizer) merged 1 commit intoAug 21, 2026
Conversation
…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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1327
Problem
Starting SimpleChat directly with
python application/single_app/app.py(including viauv run) aborted with: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 andper_user_semantic_kerneldisabled that reaches:initialize_semantic_kernel()→load_semantic_kernel()→load_single_agent_for_kernel()Inside
load_single_agent_for_kernel, theif agent_config.get("actions_to_load"):branch calledget_current_user_id()without a guard. That reads the Flasksessionproxy, which raises when there is no request context. Theactions_to_loadbranch 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_kernelwas already wrapped intry/exceptwith aNonefallback. 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 returnNoneoutside 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 confirmNoneis 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— addedget_current_user_id_or_none(), which returnsNonewhenhas_request_context()is false and otherwise delegates.has_request_context()is already the established pattern for this across the codebase, and the*_or_nonenaming 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:load_single_agent_for_kernelagent plugin loadingresolve_agent_config.get_group_scope_idresolve_agent_config.get_agent_model_endpoint_candidatesresolve_agent_config.resolve_foundry_endpoint_configload_plugins_for_kerneltry/exceptwith the shared helperThe group and personal-endpoint sites also short-circuit rather than forwarding an unresolved identity —
require_active_group()andget_user_settings()perform Cosmos reads keyed on the user id, so passingNonewould only have traded theRuntimeErrorfor 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
Nonefor an unauthenticated request.Validation
New
functional_tests/test_semantic_kernel_startup_without_request_context.pycovers both halves:get_current_user_id()still raises outside a request context;get_current_user_id_or_none()returnsNoneoutside, resolves the sessionoidinside an authenticated request, and returnsNonefor an unauthenticated request.get_current_user_id()call, and never passes an identity call straight intorequire_active_group()orget_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:
must not call get_current_user_id() directly; found at line(s) [1941]require_active_group()reports the leaked-argument violationResult 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 threeroute_tests/policy contract tests, and the docs coverage and docs quality tests.Note on
test_group_agent_endpoint_scope_resolution.pyThis 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 toreturn 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
Developmentwith the changes stashed — they fail identically without this branch.Other changes
config.py→0.260.023docs/explanation/fixes/SEMANTIC_KERNEL_STARTUP_REQUEST_CONTEXT_FIX.md+ index entryv0.260.023sectionNo documentation inventory work was needed — this adds no
enable_*key, admin tab, action plugin, or chat control, anddocs/_data/app_surface.ymlis unaffected.