From 5460b7632ebaba2111b63a2c246c5fbadf805388 Mon Sep 17 00:00:00 2001 From: Paul Lizer Date: Tue, 13 Oct 2026 15:04:36 -0400 Subject: [PATCH] Fix Control Center action roles in orchestration workers Forward authenticated roles through action discovery and exact manifest resolution while preserving current access checks for preflight and retained results. Add worker-context and access-revocation regressions, document the fix, and bump the application patch version to 0.261.329. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- application/single_app/config.py | 2 +- ...unctions_orchestration_external_sources.py | 9 +- .../features/CONTROL_CENTER_ACTION.md | 14 +- .../ORCHESTRATION_EXTERNAL_SOURCE_ACCESS.md | 12 + ...NTER_ORCHESTRATION_ROLE_PROPAGATION_FIX.md | 108 +++++++++ ...test_control_center_orchestration_roles.py | 221 ++++++++++++++++++ .../test_orchestration_external_bootstrap.py | 9 +- .../test_orchestration_external_pre_effect.py | 5 +- .../test_orchestration_external_sources.py | 5 +- ...chestration_seeded_agent_preference_fix.py | 6 +- 10 files changed, 377 insertions(+), 14 deletions(-) create mode 100644 docs/explanation/fixes/CONTROL_CENTER_ORCHESTRATION_ROLE_PROPAGATION_FIX.md create mode 100644 functional_tests/test_control_center_orchestration_roles.py diff --git a/application/single_app/config.py b/application/single_app/config.py index 85b6d0c59..b1a5c8102 100644 --- a/application/single_app/config.py +++ b/application/single_app/config.py @@ -101,7 +101,7 @@ EXECUTOR_TYPE = 'thread' EXECUTOR_MAX_WORKERS = 30 SESSION_TYPE = 'filesystem' -VERSION = "0.261.328" +VERSION = "0.261.329" IS_DEVELOPMENT = is_development_env_enabled() # Opt-out for deployments where App Service Easy Auth is active but the platform diff --git a/application/single_app/functions_orchestration_external_sources.py b/application/single_app/functions_orchestration_external_sources.py index 2c3e733c0..f1c8e0d73 100644 --- a/application/single_app/functions_orchestration_external_sources.py +++ b/application/single_app/functions_orchestration_external_sources.py @@ -1,10 +1,11 @@ # functions_orchestration_external_sources.py """Server admission and current access for retained external content. -Version: 0.261.291 +Version: 0.261.329 Session-trusted agent and action sources in: 0.261.270 Hand-selected agent honoured while the agent preference is off in: 0.261.289 Stored agents accepted as any dictionary, and refusals logged by check, in: 0.261.291 (#1699) +Authenticated action roles forwarded to discovery and exact resolution in: 0.261.329 No fetch, recall, plugin invocation, settings discovery, or credential persistence occurs here. Content digests attest the exact retained payload, not a remote page @@ -310,7 +311,9 @@ def _catalog(self, source_type, identity, settings, *, seeded_agent=None): callback = self.agent_catalog_reader if source_type == "agent" else self.action_catalog_reader if callback is None: raise ResultUnavailableError("result_external_catalog_required") - if seeded_agent is None: + if source_type == "action": + catalog = callback(identity.user_id, settings=settings, user_roles=list(identity.roles)) + elif seeded_agent is None: catalog = callback(identity.user_id, settings=settings) else: # Narrowed to the run's selection exactly as planning and execution narrowed it. @@ -413,7 +416,7 @@ def _resolve_integration(self, source_type, selected, identity, settings): if self.action_resolver is None: raise ResultUnavailableError("result_external_resolver_required") resolved = self.action_resolver( - identity.user_id, selected["action_ref"], settings=settings, + identity.user_id, selected["action_ref"], settings=settings, user_roles=list(identity.roles), ) origin = get_action_origin(resolved) if not isinstance(resolved, dict): diff --git a/docs/explanation/features/CONTROL_CENTER_ACTION.md b/docs/explanation/features/CONTROL_CENTER_ACTION.md index 915bac9cf..c54ff89f7 100644 --- a/docs/explanation/features/CONTROL_CENTER_ACTION.md +++ b/docs/explanation/features/CONTROL_CENTER_ACTION.md @@ -6,6 +6,10 @@ The Control Center action lets a Control Center viewer ask questions about Simpl **Version implemented:** 0.261.301 +**Updated in version: 0.261.329.** Orchestration forwards authenticated roles +through worker preflight and retained-result authorization. See the +[role propagation fix](../fixes/CONTROL_CENTER_ORCHESTRATION_ROLE_PROPAGATION_FIX.md). + **Dependencies:** Semantic Kernel actions (`enable_semantic_kernel`), Chat Orchestration with Action Access for chat use, the V2 Control Center Dashboard, and the existing Control Center role settings. ## Technical specifications @@ -29,7 +33,14 @@ The action answers only for someone whose roles grant `can_view_dashboard`: an A 2. Resolving the action's manifest for a plan step refuses anyone else. 3. Every function re-reads the capabilities from the signed-in session before reading data. -Orchestration captures the caller's roles with their identity and passes them explicitly to the catalog and manifest resolution, including on its worker threads, where the run re-enters a request context carrying the authenticated session. A scheduled workflow run has no signed-in session, so the action refuses it. Any failure to determine access refuses the call. +Orchestration captures the caller's roles with their authenticated identity and +passes them explicitly to catalog discovery and exact manifest resolution. +Worker preflight, result admission, and retained-result reads do not need a Flask +request context: their external-source identity reader supplies those roles. +Actual plugin calls use the existing execution bridge to re-enter a request +context carrying the authenticated session. A scheduled workflow run has no +signed-in session, so the action refuses it. Any failure to determine access +refuses the call. The type is global-only. Personal and group workspaces cannot list, create or save it; an administrator creates it in **Admin Settings › Agents & Actions › Global Actions**. @@ -105,6 +116,7 @@ The prompt is passed in router state, never the URL, so a link cannot place text - `functional_tests/test_v2_control_center_action.py`: refusal without dashboard access and outside a signed-in request, role rules through the real capability function, bounded chartable rows, validation before reads, generic storage errors, no writes, kernel function parameters, schema registration, readiness rules and remedies, admin-only links that match real Admin Settings sections and the browser allowlist, and the readiness route. - `functional_tests/test_orchestration_action_catalog.py`: the action is offered and resolved only to dashboard viewers, never outside global scope, and the type is refused for personal and group governance. +- `functional_tests/test_control_center_orchestration_roles.py`: real worker preflight, acquisition checks, result admission, and fresh-provider retained reads with authenticated roles; denial after access changes and refusal of stale catalogs or saved/ambient roles. - `functional_tests/test_v2_control_center_dashboard.py`: the shared aggregation and reports. - `ui_tests/test_v2_control_center_dashboard.py`: the checklist, **Check again**, and the prompt hand-off into a new orchestrated chat. diff --git a/docs/explanation/features/ORCHESTRATION_EXTERNAL_SOURCE_ACCESS.md b/docs/explanation/features/ORCHESTRATION_EXTERNAL_SOURCE_ACCESS.md index 12cc4ef24..43b94cd3b 100644 --- a/docs/explanation/features/ORCHESTRATION_EXTERNAL_SOURCE_ACCESS.md +++ b/docs/explanation/features/ORCHESTRATION_EXTERNAL_SOURCE_ACCESS.md @@ -23,6 +23,12 @@ refused just before it ran. Each refusal is now logged with the check that faile See the [agent document type fix](../fixes/ORCHESTRATION_AGENT_DOCUMENT_TYPE_FIX.md) ([#1699](https://github.com/microsoft/simplechat/issues/1699)). +**Updated in version: 0.261.329.** Action discovery and exact manifest resolution +receive the authenticated external-source identity's roles explicitly. Control +Center actions therefore remain available to authorized callers during worker +preflight and retained-result reads without a Flask request context. See the +[Control Center role propagation fix](../fixes/CONTROL_CENTER_ORCHESTRATION_ROLE_PROPAGATION_FIX.md). + ## Purpose and scope An external Gather result is the content an authorized adapter actually returned, @@ -68,6 +74,12 @@ is refused as `result_external_source_unavailable`, and a narrowed catalog with more than one agent is refused. The pick only selects a record from the current catalog; its own name, scope and ID fields are never proof of access. Action steps never receive the pick. +For action steps, the provider passes `user_roles=list(identity.roles)` to both +the action catalog reader and exact manifest resolver. Injected action callbacks +must accept this keyword. These roles come from the current authenticated +external-source identity, never a saved run or plan; the absence of a Flask +request on a worker is not a reason to drop them. Agent callbacks do not receive +this action-specific keyword. Membership in a catalog alone is insufficient: the provider also calls `resolve_delegation_agent` or `resolve_action_manifest` for the exact stored integration. These existing APIs recheck scope membership, governance, enablement, diff --git a/docs/explanation/fixes/CONTROL_CENTER_ORCHESTRATION_ROLE_PROPAGATION_FIX.md b/docs/explanation/fixes/CONTROL_CENTER_ORCHESTRATION_ROLE_PROPAGATION_FIX.md new file mode 100644 index 000000000..b4976adbe --- /dev/null +++ b/docs/explanation/fixes/CONTROL_CENTER_ORCHESTRATION_ROLE_PROPAGATION_FIX.md @@ -0,0 +1,108 @@ +# Control Center Orchestration Role Propagation Fix + +**Version: 0.261.329** + +Fixed in version: **0.261.329**, recorded in +`application/single_app/config.py`. + +## Issue + +A signed-in Control Center viewer could ask orchestration to compare dashboard +activity and chart daily trends. Planning selected the Control Center action, but +execution reported that the action was unavailable. The dependent summary and +chart were not delivered. + +Application Insights and App Service console logs identified an +`external_source_preflight` refusal for `action_invoke`, with +`authority_reason=result_external_source_unavailable` and +`reason=selection_not_in_catalog`. The executor reported +`failure_code=integration_unavailable`. This happened before a Control Center +tool call, not while querying dashboard data. + +## Root cause + +Orchestration executes on a worker without the original Flask request context. +Its external-source identity reader captures the signed-in session's roles before +the worker starts and rechecks the actor's current conversation and user access. +Planning already supplied those roles to action discovery. + +`OrchestrationExternalSourceProvider` did not forward that identity's roles to +either the action catalog reader or the exact action manifest resolver. Those +boundaries correctly refuse Control Center access without an authenticated role. +Their fallback to the current request session found no session on the worker, so +the catalog hid an action the planner had just selected. + +Correcting discovery alone was insufficient: exact manifest resolution then +refused the same caller with `integration_access_denied`. + +## Changes + +The provider now supplies `user_roles=list(identity.roles)` when discovering +actions and resolving the selected action's exact manifest. Both operations use +the existing authenticated identity reader, not roles from a saved plan, run, +action record, or browser input. + +These shared boundaries apply before invocation, during session-trusted +acquisition checks, when admitting results, and when reopening retained results. +The agent catalog callback and hand-selected-agent narrowing are unchanged. +Injected action catalog readers and resolvers accept the existing `user_roles` +keyword used by their production counterparts. + +Permission rules have not changed: + +- Only a caller whose current roles and Control Center settings grant dashboard + access can use the global Control Center action. +- Current action enablement, governance, exact scope, and conversation access are + still checked. +- Saved or ambient roles cannot override the authenticated external-source + identity. +- Runs without a signed-in session remain refused. Plugin functions still check + the signed-in session through the existing execution bridge before reading data. + +No route, dashboard query, frontend control, deployment setting, or storage schema +changed. + +## Files modified + +| File | Change | +| --- | --- | +| `application/single_app/functions_orchestration_external_sources.py` | Forward authenticated roles to both action authorization boundaries. | +| `application/single_app/config.py` | Application patch version `0.261.329`. | +| `functional_tests/test_control_center_orchestration_roles.py` | Worker-context, exact-resolution, retained-result, and permission-revocation regressions. | +| Existing external-source, pre-effect, bootstrap, and seeded-agent tests | Accept and verify the action callback role parameter without changing the agent protocol. | +| `docs/explanation/features/CONTROL_CENTER_ACTION.md` | Explain worker preflight versus the plugin's signed-in execution bridge. | +| `docs/explanation/features/ORCHESTRATION_EXTERNAL_SOURCE_ACCESS.md` | Document the authenticated action callback contract. | + +## Validation + +The new regressions exercise the real external-source provider, action catalog, +and exact manifest resolver over existing isolated storage fixtures. Networking +is blocked. The role rule is the shipped Control Center capability function, +loaded through the existing bootstrap-free test helper. + +The three allowed-role worker cases and the independent exact-resolution case +failed against the provider before this fix. The worker cases reproduced +`selection_not_in_catalog`; the exact-resolution case reproduced +`integration_access_denied`. + +| Scenario | Before | After | +| --- | --- | --- | +| Admin or permitted dedicated Control Center viewer on a worker | Selected action refused before a tool call. | Preflight, acquisition checks, admission, persistence, and fresh-provider reads succeed. | +| Discovery succeeds but exact resolution has no Flask session | Manifest resolution refused the authorized caller. | Exact resolution receives the authenticated roles. | +| Dashboard access, action enablement, or governance revoked | Access must be refused. | Preflight and retained reads still refuse access. | +| Stale catalog or saved/ambient Admin roles | Must not grant access. | Exact resolution and current identity checks refuse access. | + +Related suites cover existing action governance, session identity capture, +session-trusted result access, bootstrap and preflight wiring, selected-agent +behavior, and the read-only Control Center plugin. The combined run passed +**533 tests and 276 subtests** with the application's pinned Flask, Werkzeug, +and Semantic Kernel versions in an isolated test environment. Documentation +coverage passed all seven checks; documentation site quality passed all six. + +Live deployment and chat retry are separate from this code fix and were not +performed. + +## Related documentation + +- [Control Center action](../features/CONTROL_CENTER_ACTION.md) +- [Retained orchestration external source access](../features/ORCHESTRATION_EXTERNAL_SOURCE_ACCESS.md) diff --git a/functional_tests/test_control_center_orchestration_roles.py b/functional_tests/test_control_center_orchestration_roles.py new file mode 100644 index 000000000..4cd2c6e57 --- /dev/null +++ b/functional_tests/test_control_center_orchestration_roles.py @@ -0,0 +1,221 @@ +# test_control_center_orchestration_roles.py +""" +Functional test for authenticated Control Center roles in orchestration workers. +Version: 0.261.329 +Implemented in: 0.261.329 + +The real external-source provider, action catalog and exact manifest resolver run +without a Flask request context. Signed-in roles must reach both authorization +boundaries before execution, on admission and on fresh-provider retained reads. +Current access changes still refuse the action; saved or ambient roles cannot +grant access. Existing storage fixtures block networking and restore module seams. +""" + +from concurrent.futures import ThreadPoolExecutor +from copy import deepcopy +import sys +from unittest.mock import Mock, patch + +import pytest +from flask import Flask, has_request_context, session + +from test_orchestration_external_sources import ExternalSourceWorld +from test_v2_control_center_action import _auth_functions +from functions_action_catalog import _action_ref, build_accessible_action_catalog, resolve_action_manifest +from functions_orchestration_context import resolve_action_catalog +from functions_orchestration_results import ResultUnavailableError +from test_support.versioning import assert_app_version_at_least + + +@pytest.fixture +def control_center_world(): + with ExternalSourceWorld("action_invoke") as world: + world.roles = ("Admin",) + world.settings.update( + per_user_semantic_kernel=False, allow_user_plugins=False, allow_group_plugins=False, + ) + world.control_center_action = world.services.add("actions", "global", "global", { + "id": "control-center", "name": "Control Center", "type": "control_center", "is_enabled": True, + }) + world.services.add("actions", "global", "global", { + "id": "tickets", "name": "Tickets", "type": "openapi", "is_enabled": True, + }) + world.action_selector = _action_ref("global", "global", "control-center") + world.run["plan"]["steps"][0]["arguments"]["action_ref"] = world.action_selector + world.services.containers["actions", "global"].query_items.side_effect = lambda **_kwargs: [ + deepcopy(action) for action in world.services.records["actions", "global"].values() + ] + world.services.governance.filter_governed_global_actions_for_user = Mock( + side_effect=lambda _actor, actions: [ + action for action in actions if action["id"] not in world.services.denied_actions + ], + ) + world.stack.enter_context(patch.dict(sys.modules, {"functions_authentication": _auth_functions()})) + yield world + + +def provider(world, **overrides): + return world.provider(**{ + "action_catalog_reader": resolve_action_catalog, + "action_resolver": resolve_action_manifest, + **overrides, + }) + + +def saved_output(world, current): + admitted = world.admit(current) + service = world.service(current, admitted) + task = world.persist(service, admitted) + return task.output("prepared") + + +def read_output(world, current, output): + return world.service(current).open_result(output, require_current_sources=True).read_value() + + +def test_version_includes_control_center_role_propagation(): + assert_app_version_at_least("0.261.329") + + +@pytest.mark.parametrize(("roles", "settings"), [ + (("Admin",), {}), + (("User", "ControlCenterAdmin"), {"require_member_of_control_center_admin": True}), + (("User", "ControlCenterDashboardReader"), { + "require_member_of_control_center_admin": True, + "require_member_of_control_center_dashboard_reader": True, + }), +]) +def test_worker_can_preflight_admit_and_reopen_control_center_results(control_center_world, roles, settings): + world = control_center_world + world.roles = roles + world.settings.update(settings) + catalog_reader = Mock(wraps=resolve_action_catalog) + resolver = Mock(wraps=resolve_action_manifest) + + def worker(): + if has_request_context(): + raise AssertionError("The worker must not inherit a Flask request context.") + current = provider(world, action_catalog_reader=catalog_reader, action_resolver=resolver) + world.preflight(current) + current.preflight_session_acquisition( + "action", producer=world.fixture.producer, selector=world.action_selector, + ) + output = saved_output(world, current) + return read_output( + world, provider(world, action_catalog_reader=catalog_reader, action_resolver=resolver), output, + ) + + with ThreadPoolExecutor(max_workers=1) as executor: + retained = executor.submit(worker).result() + + assert retained == world.prepared + assert catalog_reader.call_count >= 4 + assert resolver.call_count >= 4 + assert all(call.kwargs["user_roles"] == list(roles) for call in catalog_reader.call_args_list) + assert all(call.kwargs["user_roles"] == list(roles) for call in resolver.call_args_list) + + +def test_exact_resolution_receives_roles_independently_of_catalog_discovery(control_center_world): + world = control_center_world + catalog = build_accessible_action_catalog( + "owner", settings=world.settings, user_roles=list(world.roles), + ) + resolver = Mock(wraps=resolve_action_manifest) + current = provider(world, action_catalog_reader=Mock(return_value=catalog), action_resolver=resolver) + + world.preflight(current) + + assert resolver.call_count == 1 + assert resolver.call_args.kwargs["user_roles"] == ["Admin"] + + +def test_exact_resolution_refuses_a_stale_catalog_after_dashboard_access_changes(control_center_world): + world = control_center_world + catalog = build_accessible_action_catalog( + "owner", settings=world.settings, user_roles=list(world.roles), + ) + world.roles = ("User",) + resolver = Mock(wraps=resolve_action_manifest) + current = provider(world, action_catalog_reader=Mock(return_value=catalog), action_resolver=resolver) + + with pytest.raises(ResultUnavailableError) as raised: + world.preflight(current) + + assert raised.value.refusal_check == "integration_access_denied" + assert resolver.call_args.kwargs["user_roles"] == ["User"] + + +@pytest.mark.parametrize(("roles", "settings"), [ + (("User",), {}), + ((), {}), + (("User", "ControlCenterDashboardReader"), {}), + (("Admin",), {"require_member_of_control_center_admin": True}), +]) +def test_roles_without_dashboard_access_are_refused(control_center_world, roles, settings): + world = control_center_world + world.roles = roles + world.settings.update(settings) + resolver = Mock(wraps=resolve_action_manifest) + current = provider(world, action_resolver=resolver) + + with pytest.raises(ResultUnavailableError): + world.preflight(current) + with pytest.raises(ResultUnavailableError): + world.admit(current) + + resolver.assert_not_called() + + +def test_saved_and_ambient_admin_roles_cannot_override_the_authenticated_identity(control_center_world): + world = control_center_world + world.roles = ("User",) + world.run["user_roles"] = ["Admin"] + world.run["plan"]["user_roles"] = ["Admin"] + catalog_reader = Mock(wraps=resolve_action_catalog) + current = provider(world, action_catalog_reader=catalog_reader) + app = Flask("control_center_role_authority_test") + app.secret_key = "test-only" + + with app.test_request_context("/"): + session["user"] = {"oid": "owner", "roles": ["Admin"]} + with pytest.raises(ResultUnavailableError) as raised: + world.preflight(current) + + assert raised.value.code == "result_external_source_unavailable" + assert catalog_reader.call_args.kwargs["user_roles"] == ["User"] + + +@pytest.mark.parametrize("change", [ + "roles", "no_session_roles", "disabled", "deleted", "governance", "dashboard_role_setting", +]) +def test_current_access_changes_refuse_preflight_and_fresh_retained_reads(control_center_world, change): + world = control_center_world + current = provider(world) + world.preflight(current) + output = saved_output(world, current) + retained = read_output(world, provider(world), output) + assert retained == world.prepared + + if change == "roles": + world.roles = ("User",) + elif change == "no_session_roles": + world.roles = () + elif change == "disabled": + world.control_center_action["is_enabled"] = False + elif change == "deleted": + del world.services.records["actions", "global"]["control-center", "control-center"] + elif change == "governance": + world.services.denied_actions.add("control-center") + elif change == "dashboard_role_setting": + world.settings["require_member_of_control_center_admin"] = True + else: + raise AssertionError("Unknown access change.") + + with pytest.raises(ResultUnavailableError): + world.preflight(current) + with pytest.raises(ResultUnavailableError): + read_output(world, provider(world), output) + + +if __name__ == "__main__": + raise SystemExit(pytest.main([__file__, "-q"])) diff --git a/functional_tests/test_orchestration_external_bootstrap.py b/functional_tests/test_orchestration_external_bootstrap.py index da6d4ce94..fef90ec4c 100644 --- a/functional_tests/test_orchestration_external_bootstrap.py +++ b/functional_tests/test_orchestration_external_bootstrap.py @@ -1,11 +1,12 @@ # test_orchestration_external_bootstrap.py """ Functional test for application-owned session identity and acquisition wiring on retained results. -Version: 0.261.270 +Version: 0.261.329 Implemented in: 0.261.127 Single orchestration contract updated in: 0.261.139 Signed-in session roles replaced per-call Microsoft Graph reads in: 0.261.209 Session-trusted agent and action steps implemented in: 0.261.270 +Authenticated action roles forwarded in: 0.261.329 Runs the real bootstrap, session capture, current-access reader, retention and result store with only Cosmos storage doubled and networking blocked. Roles come @@ -652,13 +653,15 @@ def action_root(runtime, monkeypatch): } state = SimpleNamespace(catalog=[entry], description="Delegated mail reads.", metadata_reads=[]) - def resolve(user_id, action_ref, *, settings=None, user_groups=None): + def resolve(user_id, action_ref, *, settings=None, user_groups=None, user_roles=None): if user_id != USER_ID or action_ref != selector or not state.catalog: raise PermissionError("The action is unavailable.") return manifests.bind_action_origin({**entry, "description": state.description}, "personal", USER_ID) defaults = runtime.root.OrchestrationExternalSourceProvider.__init__.__kwdefaults__ - monkeypatch.setitem(defaults, "action_catalog_reader", lambda user_id, settings=None: deepcopy(state.catalog)) + monkeypatch.setitem( + defaults, "action_catalog_reader", lambda user_id, settings=None, user_roles=None: deepcopy(state.catalog), + ) monkeypatch.setitem(defaults, "action_resolver", resolve) def reread(*args, **kwargs): diff --git a/functional_tests/test_orchestration_external_pre_effect.py b/functional_tests/test_orchestration_external_pre_effect.py index 05fa19a74..f91a9b11b 100644 --- a/functional_tests/test_orchestration_external_pre_effect.py +++ b/functional_tests/test_orchestration_external_pre_effect.py @@ -1,8 +1,9 @@ # test_orchestration_external_pre_effect.py """ Functional tests for current and actual configuration support before acquisition. -Version: 0.261.127 +Version: 0.261.329 Implemented in: 0.261.127 +Authenticated action roles forwarded in: 0.261.329 Real provider authorization, scoped resolvers, metadata reconstruction, attestor projections and acquisition hooks run with only external/storage I/O doubled. @@ -64,7 +65,7 @@ def read_identity(*, user_id, conversation_id): raise world.identity_failure return source_module.CurrentExternalSourceIdentity(user_id, world.roles, True) - def catalog(user_id, *, settings): + def catalog(user_id, *, settings, user_roles=None): world.catalog_reads.append(user_id) action = world.action_store.items["action-one"] agent = world.agent_store.items["agent-one"] diff --git a/functional_tests/test_orchestration_external_sources.py b/functional_tests/test_orchestration_external_sources.py index c5356267b..85174af5d 100644 --- a/functional_tests/test_orchestration_external_sources.py +++ b/functional_tests/test_orchestration_external_sources.py @@ -1,10 +1,11 @@ # test_orchestration_external_sources.py """ Functional coverage for retained external Gather source admission and access. -Version: 0.261.270 +Version: 0.261.329 Implemented in: 0.261.127 Single orchestration contract updated in: 0.261.139 Agent and action sources trust the signed-in session in: 0.261.270 +Authenticated action roles forwarded in: 0.261.329 Real result contracts/store/readers, capability gates, scoped integration resolvers and memory authorization run with identity, configuration and storage @@ -148,7 +149,7 @@ def identity(self, *, user_id, conversation_id): user_id, self.roles, self.user_enable_agents, "owner@example.test", ) - def catalog(self, user_id, *, settings): + def catalog(self, user_id, *, settings, user_roles=None): self.catalog_reads += 1 if self.catalog_override is not None: return deepcopy(self.catalog_override) diff --git a/functional_tests/test_orchestration_seeded_agent_preference_fix.py b/functional_tests/test_orchestration_seeded_agent_preference_fix.py index 54fff269a..a32c7e3b5 100644 --- a/functional_tests/test_orchestration_seeded_agent_preference_fix.py +++ b/functional_tests/test_orchestration_seeded_agent_preference_fix.py @@ -2,8 +2,9 @@ # test_orchestration_seeded_agent_preference_fix.py """ Functional test for an agent the user picked running while their agent preference is off. -Version: 0.261.289 +Version: 0.261.329 Implemented in: 0.261.289 +Authenticated action roles forwarded in: 0.261.329 Picking an agent in the composer is itself the permission to use that agent, so planning and execution run it even while the user's general "Enable Agents" preference is off. The @@ -269,7 +270,8 @@ def test_action_sources_never_receive_the_agent_pick(): ) assert len(world.admit(current)) == 1 assert reader.call_count >= 3 - assert all(set(call.kwargs) == {"settings"} for call in reader.call_args_list) + assert all(set(call.kwargs) == {"settings", "user_roles"} for call in reader.call_args_list) + assert all(call.kwargs["user_roles"] == list(world.roles) for call in reader.call_args_list) if __name__ == "__main__":