From 91c30a38f53edde38540a5711dca48ad7ccfb01e Mon Sep 17 00:00:00 2001 From: Paul Lizer Date: Wed, 7 Oct 2026 11:25:39 -0400 Subject: [PATCH 1/4] Honour a hand-picked agent in orchestration steps while agents are off An Ask an agent step for an agent the user picked failed with "This step's agent or action isn't available to you right now" whenever the user's enable_agents preference was off, which is the stored default for V2 users. Planning and execution treat a pick as permission and narrow the catalog to it, but OrchestrationExternalSourceProvider rechecked the step with the raw preference and an un-narrowed catalog, so _agent_request_gate refused agent_invoke (result_external_capability_unavailable). It looked like a shared-conversation failure only because the personal request had no pick. The provider now narrows the agent catalog to the run's seeded agent and counts the pick as permission for that agent only. Scope, membership, governance and enabled state are still rechecked on every run and read. Version 0.261.279. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- application/single_app/config.py | 2 +- ...unctions_orchestration_external_sources.py | 62 +++- docs/admin/orchestration.md | 8 +- .../ORCHESTRATION_EXTERNAL_SOURCE_ACCESS.md | 33 ++- ..._SELECTED_AGENT_DISABLED_PREFERENCE_FIX.md | 149 ++++++++++ ...chestration_seeded_agent_preference_fix.py | 276 ++++++++++++++++++ 6 files changed, 517 insertions(+), 13 deletions(-) create mode 100644 docs/explanation/fixes/ORCHESTRATION_SELECTED_AGENT_DISABLED_PREFERENCE_FIX.md create mode 100644 functional_tests/test_orchestration_seeded_agent_preference_fix.py diff --git a/application/single_app/config.py b/application/single_app/config.py index 7378b2436..0b2eb9674 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.278" +VERSION = "0.261.279" 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 484e84ba0..183d2c9fb 100644 --- a/application/single_app/functions_orchestration_external_sources.py +++ b/application/single_app/functions_orchestration_external_sources.py @@ -1,8 +1,9 @@ # functions_orchestration_external_sources.py """Server admission and current access for retained external content. -Version: 0.261.270 +Version: 0.261.279 Session-trusted agent and action sources in: 0.261.270 +Hand-selected agent honoured while the agent preference is off in: 0.261.279 No fetch, recall, plugin invocation, settings discovery, or credential persistence occurs here. Content digests attest the exact retained payload, not a remote page @@ -11,6 +12,11 @@ Agent and action sources trust the signed-in session the way classic chat does: every admission and read rechecks the user's current access to the conversation, the run's step and the exact agent or action, but their configuration is not attested or compared. + +An agent the user picked by hand for the run is itself the permission to use it, as +planning, execution and classic chat already treat it, so it is honoured while the +user's general agent preference is off. Only that agent is: the catalog is narrowed to +the run's selection, and current scope, membership and governance are still rechecked. """ from dataclasses import dataclass @@ -21,6 +27,7 @@ from functions_action_manifest import get_action_origin from functions_agent_delegation import agent_reference, resolve_delegation_agent from functions_orchestration_context import ( + CatalogResolutionError, build_capability_request_context, conversation_user_urls, resolve_action_catalog, @@ -235,19 +242,47 @@ def _current(self, producer, *, for_write): raise ResultUnavailableError("result_external_audience_unavailable") from exc return identity, settings, conversation, run, audience - def _catalog(self, source_type, identity, settings): + @staticmethod + def _seeded_agent(run): + """The agent the user picked for this run: a selection, not proof of access. + + Picking an agent by hand is itself the permission to use it, so it lifts the + user's general agent preference for that one agent. Current access to it is still + resolved from the catalog, never from the selection's own fields. + """ + seeds = run.get("seeds") + agent = seeds.get("agent") if type(seeds) is dict else None + if type(agent) is not dict or type(agent.get("name")) is not str or not agent["name"].strip(): + return None + return agent + + 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") - catalog = callback(identity.user_id, settings=settings) + if 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. + try: + catalog = callback(identity.user_id, seeds={"agent": seeded_agent}, settings=settings) + except CatalogResolutionError as exc: + if exc.code != "selected_agent_unavailable": + raise + raise ResultUnavailableError("result_external_source_unavailable") from exc if ( type(catalog) is not list or len(catalog) > MAX_CATALOG_ITEMS or any(type(item) is not dict for item in catalog) ): raise ResultUnavailableError("result_external_catalog_invalid") + if seeded_agent is not None and len(catalog) > 1: + raise ResultUnavailableError("result_external_source_unavailable") return catalog - def _capability(self, source_type, producer, identity, settings, catalog, *, invocation_user_urls=None): + def _capability( + self, source_type, producer, identity, settings, catalog, *, + seeded_agent=None, invocation_user_urls=None, + ): if any(settings.get(key) is not True for key in _REQUIRED_SETTINGS[source_type]): raise ResultUnavailableError("result_external_capability_unavailable") if source_type == "fact_memory": @@ -263,7 +298,8 @@ def _capability(self, source_type, producer, identity, settings, catalog, *, inv identity.user_id, { "user_roles": list(identity.roles), "user_email": identity.email, - "user_enable_agents": identity.user_enable_agents, + # The catalog is already narrowed to a hand-picked agent, which is its own permission. + "user_enable_agents": identity.user_enable_agents or seeded_agent is not None, }, "", catalog if source_type == "agent" else [], catalog if source_type == "action" else [], @@ -449,9 +485,13 @@ def _gather_invocation_state(self, producer, selector): self._step_arguments(run, producer) if run.get("memory_audience") is not None and run["memory_audience"] != audience: raise ResultUnavailableError("result_external_audience_unavailable") - catalog = self._catalog(source_type, identity, settings) if source_type in {"agent", "action"} else [] + seeded_agent = self._seeded_agent(run) if source_type == "agent" else None + catalog = ( + self._catalog(source_type, identity, settings, seeded_agent=seeded_agent) + if source_type in {"agent", "action"} else [] + ) self._capability( - source_type, producer, identity, settings, catalog, + source_type, producer, identity, settings, catalog, seeded_agent=seeded_agent, invocation_user_urls=self._invocation_user_urls(run) if source_type == "url" else None, ) resolved = None @@ -488,8 +528,12 @@ def _reference( self, *, producer, source_type, content_sha256, state, selector=None, previous=None, ): identity, settings, conversation, run, audience = state - catalog = self._catalog(source_type, identity, settings) if source_type in {"agent", "action"} else [] - self._capability(source_type, producer, identity, settings, catalog) + seeded_agent = self._seeded_agent(run) if source_type == "agent" else None + catalog = ( + self._catalog(source_type, identity, settings, seeded_agent=seeded_agent) + if source_type in {"agent", "action"} else [] + ) + self._capability(source_type, producer, identity, settings, catalog, seeded_agent=seeded_agent) revision = None configuration_suffix = None if source_type == "fact_memory": diff --git a/docs/admin/orchestration.md b/docs/admin/orchestration.md index 58b1ab52a..583ca3149 100644 --- a/docs/admin/orchestration.md +++ b/docs/admin/orchestration.md @@ -379,6 +379,11 @@ agent is always offered to the planner, even when the user turned agents off in classic interface's settings. Local agents run as plan steps, including agents that use Microsoft 365 actions. +Since **0.261.279**, a chosen agent's step also runs while that setting is off. The +setting is the agents button on the classic chat page (`/chats`). V2 has no control for +it, and it's saved as off for a user whose settings were saved before they turned agents +on, so many V2 users have it off. It still applies to agents the user didn't choose. + The opt-in adds no second action allowlist or approval system. Existing scope, ownership, group membership, enablement and governance rules still determine which actions are available, and access is checked again when work runs. An action removed or revoked @@ -702,7 +707,7 @@ then report a rejected proposal. | Every web search, linked-page or deep research step says a required retained result is unavailable or changed | Before 0.261.209, orchestration checked each user's roles through Microsoft Graph, which needs `Directory.Read.All`, and refused research whenever query or linked-page planning was on. | Upgrade to 0.261.209 or later; no Graph permission or settings change is needed. If a step still fails, check `sc_authority_reason` on its failure event in [orchestration failure diagnostics](../reference/logging-tags.md#orchestration-failure-diagnostics). | | Every step completed, but the run is partially completed with "A required retained result is unavailable or changed" | Before 0.261.237, settings read from Cosmos DB instead of the shared Redis copy came back as an SDK type that the access recheck refused. That happens while any settings save is in progress, for example the Cosmos throughput autoscale saving its status, and on every read when Redis is off. The failure event logs `sc_authority_reason=result_external_context_unavailable`. | Upgrade to 0.261.237 or later. No setting change is needed. See the [settings document type fix]({{ '/explanation/fixes/ORCHESTRATION_SETTINGS_DOCUMENT_TYPE_FIX/' | relative_url }}). | | Plans never propose deep research or reading a link | The user does not hold the required app role, or the capability is disabled in its own settings group. | Confirm the user holds `DeepResearchUser` or `UrlAccessUser` where your deployment requires them, and that the capability is enabled outside this page. | -| Plans never propose an agent | Semantic Kernel is off, the user has turned agents off in their own settings, or the user has no agent they can reach. | Confirm Semantic Kernel is enabled, then check the user's own agent setting and that at least one agent is shared with them. An agent the user picks, or tags with @, is always offered to the planner. | +| Plans never propose an agent | Semantic Kernel is off, the user's own agents setting is off, or the user has no agent they can reach. | Confirm Semantic Kernel is enabled, then check the user's own agents setting, the agents button on the classic chat page (`/chats`), and that at least one agent is shared with them. An agent the user picks, or tags with @, is always offered to the planner. | | The planner model shows "not in the current model list" | Its connection or model was removed or disabled, or connections were switched on or off since it was chosen. | Choose a listed model, or **Use the answer model (default)**. Until then planning keeps trying the saved model and fails rather than switching. | | Plans never propose Use an action | Action Access is off, Semantic Kernel is off, the capability is excluded, or no eligible action is available to this user. | Check the opt-in and capability selection, then the existing action scope and governance. Call agent actions are not eligible for direct use. | | An action step fails after plan approval | The action or its access changed, or its model/tool connection could not run. | Check current action access and configuration. Review the visible step failure; the run does not silently switch to an agent or another action. | @@ -711,6 +716,7 @@ then report a rejected proposal. | A Microsoft 365 step says plans can only read Microsoft 365 data | The action enables only send, invitation or mark-as-read functions, which plans never run. | Enable a read function on the action, or use it from chat without a plan. | | Before 0.261.270, a Microsoft 365 step said plans can't use Microsoft 365 in a shared conversation | Earlier versions refused Microsoft 365 steps in shared conversations. | Upgrade to 0.261.270 or later. The user's own request now counts as consent to share what the step reads. See [Microsoft 365 actions in plans](#microsoft-365-actions-in-plans). | | Before 0.261.270, every action or agent step failed with "A required retained result is unavailable or changed", while the same request worked with Orchestrate off | The step's configuration check refused it. For Microsoft 365 actions, the check couldn't see the action's Microsoft 365 selection. Local agents were refused outright. Failure events log `sc_authority_reason=external_configuration_unavailable` for `action_invoke` or `agent_invoke`. | Upgrade to 0.261.270 or later. Agent and action steps now trust the signed-in session, as manual chat does. See [Retained external-source authorization](#retained-external-source-authorization). | +| Before 0.261.279, an Ask an agent step for an agent the user picked, or tagged with @, failed with "This step's agent or action isn't available to you right now, so it didn't run" | The user's own agents setting was off. Planning offered the picked agent, but the step's access check didn't count the pick as permission. Failure events log `sc_authority_reason=result_external_capability_unavailable` for `agent_invoke`. It isn't specific to shared conversations: a request without a picked agent can still work because its plan uses an action instead. | Upgrade to 0.261.279 or later. Before upgrading, the user can turn agents on with the agents button on the classic chat page (`/chats`). See the [selected agent with agents turned off fix]({{ '/explanation/fixes/ORCHESTRATION_SELECTED_AGENT_DISABLED_PREFERENCE_FIX/' | relative_url }}). | | Before 0.261.270, an @mention of a person in a shared conversation was sent to the model when Orchestrate was on | Orchestrate planned every message without applying the shared conversation's send rule. | Upgrade to 0.261.270 or later. See [Shared conversations](#shared-conversations). | | A participant sees "Only the person who started this shared conversation can use Orchestrate here" | A request reached the planner from someone other than the person who started the shared conversation. The V2 composer normally answers their requests the classic way instead. | Turn off Orchestrate for that request, or ask the person who started the conversation to ask it. | | The person who started a shared conversation sees "Orchestrate can't be used in this shared conversation because an earlier version kept another participant's private copy of it" | Before 0.261.270, Orchestrate saved a private copy for whoever used it first, under the shared conversation's ID. Only one record can have that ID. | Turn off Orchestrate to ask the assistant. To restore Orchestrate, remove that participant's record from the conversations container: its ID is the shared conversation's ID. The `[ORCHESTRATION]` warning logs `sc_reason=shared_conversation_stale_copy`. | diff --git a/docs/explanation/features/ORCHESTRATION_EXTERNAL_SOURCE_ACCESS.md b/docs/explanation/features/ORCHESTRATION_EXTERNAL_SOURCE_ACCESS.md index 41eee2036..c5fae615d 100644 --- a/docs/explanation/features/ORCHESTRATION_EXTERNAL_SOURCE_ACCESS.md +++ b/docs/explanation/features/ORCHESTRATION_EXTERNAL_SOURCE_ACCESS.md @@ -9,6 +9,13 @@ roles of the signed-in session, as classic chat does, instead of reading Microsoft Graph on every call. Deep research can use its query and link planners again. See the [session identity fix](../fixes/ORCHESTRATION_SESSION_IDENTITY_FIX.md). +**Updated in version: 0.261.279.** An agent the user picked for a run is checked +against a catalog narrowed to that agent, and the user's `enable_agents` +preference no longer blocks it. Before this, an Ask an agent step for a picked +agent failed while the preference was off, even though planning and execution +accepted it. See the +[selected agent with agents turned off fix](../fixes/ORCHESTRATION_SELECTED_AGENT_DISABLED_PREFERENCE_FIX.md). + ## Purpose and scope An external Gather result is the content an authorized adapter actually returned, @@ -46,6 +53,14 @@ The provider requires these owner-supplied callbacks: Catalog callbacks default to `resolve_agent_catalog` and `resolve_action_catalog` from `functions_orchestration_context.py`. They must return current authorized catalogs, not the list saved on a run context. Catalogs are bounded to 4,096 entries. +For an agent step in a run with a picked agent (`run["seeds"]["agent"]`, a +dictionary with a non-blank string `name`), the provider passes the pick to the +agent catalog reader as `seeds={"agent": ...}`, so the catalog is narrowed to that +agent exactly as it was for planning and execution. A pick that no longer resolves +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. 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, @@ -299,7 +314,10 @@ timezone-aware `datetime_to_allow` has elapsed. Expiration permits access withou changing the stored restriction. Unknown statuses and malformed restrictions fail closed. Only `Admin` bypasses this restriction, matching `user_required`; it doesn't bypass the need for a current settings record. The `enable_agents` -preference remains effective even for admins. Its enabled default applies only +preference remains effective even for admins, except for an agent the user picked +for the run: a pick counts as permission for that agent only, because the catalog +is already narrowed to it, as classic chat, planning and execution treat a pick. +Its enabled default applies only to a valid current document where that optional preference is absent, never to a failed read. @@ -323,7 +341,10 @@ only the refusal's stable code in `authority_reason`. `access_failure` in `functions_orchestration_schema.py` reads that code, or the refusal's own `code`, and follows at most three `__cause__` links. Only `external_identity_session_unavailable` becomes the `external_session_required` -failure; every other refusal stays `result_unavailable`. Exception text is never +failure. Any other refusal of an agent or action step (`agent_invoke` or +`action_invoke`) becomes `integration_unavailable`, "This step's agent or action +isn't available to you right now", because those steps take no retained inputs. +Every other refusal stays `result_unavailable`. Exception text is never read, and the reason code is never shown to the user. Step, saved-wait, finalization and composition failure events log it as `sc_authority_reason`. @@ -1016,6 +1037,14 @@ settings, authentication, route or credential modules. No test contacts a tenant `functional_tests/test_orchestration_external_bootstrap.py` checks that the root captures session roles only inside a request context and makes no Graph calls. +`functional_tests/test_orchestration_seeded_agent_preference_fix.py` runs the real +provider and the real `resolve_agent_catalog` for runs with a picked agent. It +covers a picked personal or group agent with the `enable_agents` preference off, +catalog narrowing on every read, revocation of membership, governance, scope +settings and the agent's enabled state, a pick that doesn't lift the preference +for a different agent, picks that no longer resolve, catalog outages, and +malformed picks. + `functional_tests/test_orchestration_external_configuration.py` covers actual URL policy normalization, source-specific projections, keyed opaque revisions, definition/run comparisons using real SDK model objects, sticky re-resolution diff --git a/docs/explanation/fixes/ORCHESTRATION_SELECTED_AGENT_DISABLED_PREFERENCE_FIX.md b/docs/explanation/fixes/ORCHESTRATION_SELECTED_AGENT_DISABLED_PREFERENCE_FIX.md new file mode 100644 index 000000000..cb2216349 --- /dev/null +++ b/docs/explanation/fixes/ORCHESTRATION_SELECTED_AGENT_DISABLED_PREFERENCE_FIX.md @@ -0,0 +1,149 @@ +# Orchestration Selected Agent With Agents Turned Off Fix + +**Version: 0.261.279** + +Fixed in version: **0.261.279**, recorded in +`application/single_app/config.py`. + +## Issue + +A user picked their personal `m365` agent in the V2 composer, turned on Orchestrate and +asked "What are my latest emails?" in a shared conversation. Planning produced an +**Ask an agent** step for that agent, and the step failed with "This step's agent or +action isn't available to you right now, so it didn't run." The answering step then +reported "A required dependency did not complete." + +The same question worked in the user's own conversation, so the failure looked like a +shared-conversation problem. It wasn't one. + +The executor's failure event logged `OrchestrationInvocationDeniedError` with +`sc_authority_reason=result_external_capability_unavailable` for `agent_invoke`. + +## Root cause + +The user's own agents setting (`enable_agents`, the agents button on the classic chat +page) was off. V2 has no control for it, and `update_user_settings` stores it as off the +first time it saves a user's settings without it, so any V2 user who never turned agents +on in classic chat could hit this. + +Picking an agent by hand is itself permission to use that agent. Classic chat lets a +picked agent answer while the setting is off (`force_enable_agents`), and the planner, +plan editing and the execution harness treat a picked agent as enabled and narrow the +agent catalog to it. + +`OrchestrationExternalSourceProvider` didn't. It rechecks access to an agent step's exact +agent just before the step runs, and on every later read of the step's result. It used +the raw setting and an un-narrowed catalog, so the agent request gate +(`_agent_request_gate`) refused `agent_invoke` and the step was denied before it ran. The +deployment allowed Semantic Kernel, personal agents and `agent_invoke`, so the refusal +didn't come from the deployment settings check, which reports the same reason. + +The user's own conversation worked only because no agent was picked there. Without a +pick, the planner offers agents only while the setting is on, so that plan used a **Use +an action** step instead, and action steps don't depend on the agents setting. Sharing +played no part. The provider accepted the shared conversation's audience before its +capability check, and that check reads the same setting in personal and shared +conversations, so a picked agent fails the same way in a personal conversation. + +An earlier failure in the same deployment, logged with `external_configuration_unavailable`, +came from the configuration check that 0.261.270 removed for agent and action steps. See +the [session-trusted action and agent steps fix](ORCHESTRATION_SESSION_TRUSTED_ACTION_AGENT_STEPS_FIX.md). + +## Changes + +In `functions_orchestration_external_sources.py`: + +- `_seeded_agent(run)` reads the agent picked for the run, `run["seeds"]["agent"]`. Only a + dictionary with a non-blank string `name` counts. Anything else is treated as no pick, + so the setting still applies. +- `_catalog()` passes a pick to the agent catalog reader as `seeds={"agent": ...}`, so + `resolve_agent_catalog` narrows the catalog to that agent exactly as planning and + execution did. A pick that no longer resolves (`selected_agent_unavailable`) is + reported as `result_external_source_unavailable`, and a catalog outage still raises + `catalog_unavailable`. A narrowed catalog with more than one agent is refused. +- `_capability()` treats the agents setting as on when there's a pick. The catalog is + already narrowed, so this lifts the setting for the picked agent only. +- The check before a step runs (`_gather_invocation_state`), result admission and every + later read (`_reference`) all use the pick. Action steps never receive it. + +What doesn't change: + +- The provider still calls `resolve_delegation_agent` for the exact agent, so scope + settings, group membership, governance and the agent's enabled state are rechecked + every time the step runs or its result is read. +- A step for an agent the user didn't pick still needs the agents setting on. +- A pick only selects a record from the user's current catalog. Its own name, scope and + ID fields are never proof of access. + +## Files modified + +| File | Change | +| --- | --- | +| `application/single_app/functions_orchestration_external_sources.py` | Narrow the agent catalog to the run's picked agent and honour the pick in the capability check. | +| `application/single_app/config.py` | Version `0.261.279`. | +| `functional_tests/test_orchestration_seeded_agent_preference_fix.py` | New regression test. | +| `docs/admin/orchestration.md` | Picked agents run while the agents setting is off; troubleshooting row. | +| `docs/explanation/features/ORCHESTRATION_EXTERNAL_SOURCE_ACCESS.md` | Picked-agent catalog narrowing in the provider contract. | + +## Validation + +### Tests + +`functional_tests/test_orchestration_seeded_agent_preference_fix.py` runs the real +provider and the real `resolve_agent_catalog` over in-memory agents: + +- The reported case: a personal agent picked while the agents setting is off is checked, + admitted and read back. +- A picked group agent runs and stays readable with the setting on or off, and every + catalog read is narrowed to its group. +- Removing group membership, denying group-agent governance, turning off group agents or + disabling the agent still denies the check before the step and later reads. +- Without a pick, the setting still applies. +- A pick doesn't lift the setting for a different agent named in the plan step. +- A pick that no longer resolves (a removed agent, another group, or a name alone) is + unavailable, and a catalog outage is still reported as an outage. +- A narrowed catalog with more than one agent is refused. +- Malformed picks don't lift the setting, and action steps never receive the pick. + +16 of its 29 tests fail against the provider from 0.261.278, including the reported case. + +With the new file, these existing suites pass unchanged, 506 tests and 282 subtests in +all: `test_orchestration_external_sources.py`, `test_orchestration_session_trusted_sources.py`, +`test_orchestration_external_identity.py`, `test_orchestration_external_bootstrap.py`, +`test_orchestration_external_pre_effect.py`, `test_orchestration_external_preflight_adapter.py`, +`test_orchestration_agent_selection.py` and `test_orchestration_v2_plan_backend.py`. + +### Deployment check + +Read-only reads of the affected deployment confirmed that, with the fix, the picked agent +also passes the scope, governance and agent record checks that follow. It's a personal +`local` agent owned by the user and isn't disabled, and the deployment allows Semantic +Kernel, personal agents (with personal-agent governance off) and `agent_invoke`. + +### Before and after + +| Before | After | +| --- | --- | +| With the agents setting off, an Ask an agent step for a picked agent failed with "This step's agent or action isn't available to you right now". | The step runs, as the same agent does in chat. | +| The planner offered a picked agent that the step's access check then refused. | Planning, execution and the access check agree on the picked agent. | +| An agent the user didn't pick needed the agents setting on. | Unchanged. | + +## Limitations + +- A pick narrows the access check to that agent, so in a run with a pick, a step for any + other agent is refused even with the agents setting on. Planning and plan editing offer + only the picked agent, so a valid plan never contains one. +- Results that a picked agent's step already produced stay readable only while the user + can still reach that agent, as for any agent step. + +## Workaround for earlier versions + +Turn agents on with the agents button on the classic chat page (`/chats`). Or, when the +work is also available as one of the user's actions, ask without picking an agent so the +plan uses the action directly, as it did in the user's own conversation. + +## Related + +- [Retained orchestration external source access](../features/ORCHESTRATION_EXTERNAL_SOURCE_ACCESS.md) +- [Orchestration session-trusted action and agent steps fix](ORCHESTRATION_SESSION_TRUSTED_ACTION_AGENT_STEPS_FIX.md) +- [Actions and agents](../../admin/orchestration.md#actions-and-agents) diff --git a/functional_tests/test_orchestration_seeded_agent_preference_fix.py b/functional_tests/test_orchestration_seeded_agent_preference_fix.py new file mode 100644 index 000000000..15d875fad --- /dev/null +++ b/functional_tests/test_orchestration_seeded_agent_preference_fix.py @@ -0,0 +1,276 @@ +#!/usr/bin/env python3 +# 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.279 +Implemented in: 0.261.279 + +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 +external-source provider that rechecks each agent step before it runs ignored the pick, so +the step the planner had just produced was refused with +result_external_capability_unavailable. This test ensures the provider honours the pick for +that agent only: the catalog is narrowed to the run's selection, current scope, membership, +governance and enabled state are still rechecked, and an unpicked, unresolvable or +malformed selection still fails closed. +""" + +from copy import deepcopy +import os +import sys +import types +from unittest.mock import Mock, patch + +import pytest + +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from test_orchestration_external_sources import ExternalSourceWorld # noqa: E402 +from functions_orchestration_context import CatalogResolutionError, resolve_agent_catalog # noqa: E402 +from functions_orchestration_results import ResultUnavailableError # noqa: E402 +from test_support.orchestration_research import _definitions # noqa: E402 +from test_support.versioning import assert_app_version_at_least # noqa: E402 + + +REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +FIX_DOC = os.path.join( + REPO_ROOT, "docs", "explanation", "fixes", + "ORCHESTRATION_SELECTED_AGENT_DISABLED_PREFERENCE_FIX.md", +) +ADMIN_DOC = os.path.join(REPO_ROOT, "docs", "admin", "orchestration.md") + +# The composer's selection as the client sends it. It narrows the catalog; it never proves access. +SELECTION = {"name": "Lookup", "id": "agent-1", "is_group": True, "group_id": "group-a"} +PERSONAL_SELECTION = { + "name": "m365", "id": "owner_m365", "is_global": False, "is_group": False, "group_id": None, +} + + +def select_agent(world, agent=None): + world.run["seeds"] = {"agent": deepcopy(SELECTION if agent is None else agent)} + + +def production_catalog(world): + """Run the real resolve_agent_catalog over the fixture's agents, recording each lookup.""" + builder = Mock(side_effect=lambda user_id, *, settings, user_groups: world.catalog( + user_id, settings=settings, + )) + module = types.ModuleType("functions_agent_catalog") + module.build_accessible_agent_catalog = builder + module.build_agent_catalog_key = _definitions( + "functions_agent_catalog.py", names={"build_agent_catalog_key"}, + )["build_agent_catalog_key"] + world.stack.enter_context(patch.dict(sys.modules, {"functions_agent_catalog": module})) + return builder + + +def provider(world): + return world.provider(agent_catalog_reader=resolve_agent_catalog) + + +def save(world, current): + catalog = world.admit(current) + assert len(catalog) == 1 + assert next(iter(catalog.values())).source_type == "agent" + return world.persist(world.service(current, catalog), catalog).output("prepared") + + +def read(world, reader, output): + return world.service(reader).open_result(output, require_current_sources=True).read_value() + + +def test_version_includes_seeded_agent_preference_fix(): + assert_app_version_at_least("0.261.279") + + +def test_fix_and_troubleshooting_docs_describe_the_fix(): + with open(FIX_DOC, encoding="utf-8") as handle: + fix_doc = handle.read() + with open(ADMIN_DOC, encoding="utf-8") as handle: + admin_doc = handle.read() + assert "Fixed in version: **0.261.279**" in fix_doc + assert "result_external_capability_unavailable" in fix_doc + assert "| Before 0.261.279, an Ask an agent step" in admin_doc + assert "ORCHESTRATION_SELECTED_AGENT_DISABLED_PREFERENCE_FIX" in admin_doc + + +def test_the_reported_personal_agent_runs_while_agents_are_off(): + """The incident: a personal agent picked in the composer while Enable Agents was off.""" + with ExternalSourceWorld("agent_invoke") as world: + production_catalog(world) + world.services.add_agent("owner_m365", scope="personal", scope_id="owner", name="m365") + world.run["plan"]["steps"][0]["arguments"] = {"agent_name": "m365"} + world.agent_selector = "personal:owner:owner_m365" + select_agent(world, PERSONAL_SELECTION) + world.user_enable_agents = False + + current = provider(world) + current.preflight_session_acquisition( + "agent", producer=world.fixture.producer, selector=world.agent_selector, + ) + output = save(world, current) + assert read(world, provider(world), output) == world.prepared + + +@pytest.mark.parametrize("preference", (False, True)) +def test_a_picked_agent_runs_and_stays_readable_whatever_the_preference(preference): + with ExternalSourceWorld("agent_invoke") as world: + builder = production_catalog(world) + select_agent(world) + world.user_enable_agents = preference + + current = provider(world) + world.preflight(current) + current.preflight_session_acquisition( + "agent", producer=world.fixture.producer, selector=world.agent_selector, + ) + output = save(world, current) + assert read(world, provider(world), output) == world.prepared + assert builder.call_count >= 4 + assert all(call.kwargs["user_groups"] == ["group-a"] for call in builder.call_args_list) + + +@pytest.mark.parametrize("revoke", ("membership", "governance", "scope", "disabled")) +def test_a_picked_agent_still_needs_current_access(revoke): + with ExternalSourceWorld("agent_invoke") as world: + production_catalog(world) + select_agent(world) + world.user_enable_agents = False + output = save(world, provider(world)) + assert read(world, provider(world), output) == world.prepared + + if revoke == "membership": + world.services.roles.clear() + elif revoke == "governance": + world.services.denied_features.add("governance_group_agents") + elif revoke == "scope": + world.settings["allow_group_agents"] = False + else: + world.services.records["agents", "group"]["group-a", "agent-1"]["is_enabled"] = False + with pytest.raises(ResultUnavailableError): + read(world, provider(world), output) + with pytest.raises(ResultUnavailableError): + world.preflight(provider(world)) + + +def test_an_agent_the_user_did_not_pick_still_follows_the_preference(): + with ExternalSourceWorld("agent_invoke") as world: + builder = production_catalog(world) + output = save(world, provider(world)) + + world.user_enable_agents = False + with pytest.raises(ResultUnavailableError) as raised: + world.preflight(provider(world)) + assert raised.value.code == "result_external_capability_unavailable" + with pytest.raises(ResultUnavailableError): + read(world, provider(world), output) + assert all(call.kwargs["user_groups"] is None for call in builder.call_args_list) + + +def test_the_pick_lifts_the_preference_for_that_agent_only(): + with ExternalSourceWorld("agent_invoke") as world: + production_catalog(world) + world.services.add_agent("agent-2", scope="group", scope_id="group-a", name="Other") + world.run["plan"]["steps"][0]["arguments"] = {"agent_name": "Other"} + world.agent_selector = "group:group-a:agent-2" + select_agent(world) + world.user_enable_agents = False + + current = provider(world) + for attempt in (world.preflight, world.admit): + with pytest.raises(ResultUnavailableError) as raised: + attempt(current) + assert raised.value.code == "result_external_source_unavailable" + + # The same step runs once agents are on and nothing was picked. + del world.run["seeds"] + world.user_enable_agents = True + world.preflight(current) + + +@pytest.mark.parametrize("preference", (False, True)) +@pytest.mark.parametrize("selection", ( + {**SELECTION, "id": "agent-removed"}, + {**SELECTION, "group_id": "group-b"}, + {"name": "Lookup"}, +)) +def test_a_pick_that_no_longer_resolves_is_unavailable(selection, preference): + with ExternalSourceWorld("agent_invoke") as world: + production_catalog(world) + select_agent(world, selection) + world.user_enable_agents = preference + with pytest.raises(ResultUnavailableError) as raised: + world.preflight(provider(world)) + assert raised.value.code == "result_external_source_unavailable" + assert isinstance(raised.value.__cause__, CatalogResolutionError) + assert raised.value.__cause__.code == "selected_agent_unavailable" + + +@pytest.mark.parametrize("preference", (False, True)) +def test_a_narrowed_catalog_wider_than_the_pick_is_refused(preference): + with ExternalSourceWorld("agent_invoke") as world: + world.services.add_agent("agent-2", scope="group", scope_id="group-a", name="Other") + select_agent(world) + world.user_enable_agents = preference + reader = Mock(side_effect=lambda user_id, *, seeds, settings: world.catalog( + user_id, settings=settings, + )) + with pytest.raises(ResultUnavailableError) as raised: + world.preflight(world.provider(agent_catalog_reader=reader)) + assert raised.value.code == "result_external_source_unavailable" + assert reader.call_args.kwargs["seeds"] == {"agent": SELECTION} + + +def test_a_catalog_outage_is_not_reported_as_a_missing_pick(): + with ExternalSourceWorld("agent_invoke") as world: + builder = production_catalog(world) + builder.side_effect = RuntimeError("private storage diagnostic") + select_agent(world) + world.user_enable_agents = False + with pytest.raises(CatalogResolutionError) as raised: + world.preflight(provider(world)) + assert raised.value.code == "catalog_unavailable" + + +@pytest.mark.parametrize("seeds", ( + {"agent": {**SELECTION, "name": " "}}, + {"agent": {**SELECTION, "name": None}}, + {"agent": {**SELECTION, "name": ["Lookup"]}}, + {"agent": "Lookup"}, + {"agent": None}, + {}, + ["agent"], + None, +)) +def test_a_malformed_pick_does_not_lift_the_preference(seeds): + with ExternalSourceWorld("agent_invoke") as world: + world.run["seeds"] = deepcopy(seeds) + reader = Mock(side_effect=world.catalog) + current = world.provider(agent_catalog_reader=reader) + world.preflight(current) + + world.user_enable_agents = False + with pytest.raises(ResultUnavailableError) as raised: + world.preflight(current) + assert raised.value.code == "result_external_capability_unavailable" + assert reader.call_count == 2 + assert all(set(call.kwargs) == {"settings"} for call in reader.call_args_list) + + +def test_action_sources_never_receive_the_agent_pick(): + with ExternalSourceWorld("action_invoke") as world: + select_agent(world) + world.user_enable_agents = False + reader = Mock(side_effect=world.catalog) + current = world.provider(action_catalog_reader=reader) + world.preflight(current) + current.preflight_session_acquisition( + "action", producer=world.fixture.producer, selector=world.action_selector, + ) + assert len(world.admit(current)) == 1 + assert reader.call_count >= 3 + assert all(set(call.kwargs) == {"settings"} for call in reader.call_args_list) + + +if __name__ == "__main__": + raise SystemExit(pytest.main([__file__, "-q"])) From 2b2d06edfe617ae7da63549ecac16665146eda63 Mon Sep 17 00:00:00 2001 From: Paul Lizer Date: Wed, 7 Oct 2026 11:50:44 -0400 Subject: [PATCH 2/4] Renumber the selected-agent fix to 0.261.282 The base branch already uses 0.261.279 for the Control Center dashboard and V2 user settings releases. Move this fix's version references in the provider docstring, regression test, and fix, feature and admin docs to 0.261.282, the version config.py now carries. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../functions_orchestration_external_sources.py | 4 ++-- docs/admin/orchestration.md | 4 ++-- .../features/ORCHESTRATION_EXTERNAL_SOURCE_ACCESS.md | 2 +- ...ESTRATION_SELECTED_AGENT_DISABLED_PREFERENCE_FIX.md | 8 ++++---- .../test_orchestration_seeded_agent_preference_fix.py | 10 +++++----- 5 files changed, 14 insertions(+), 14 deletions(-) diff --git a/application/single_app/functions_orchestration_external_sources.py b/application/single_app/functions_orchestration_external_sources.py index 183d2c9fb..7d7c4a629 100644 --- a/application/single_app/functions_orchestration_external_sources.py +++ b/application/single_app/functions_orchestration_external_sources.py @@ -1,9 +1,9 @@ # functions_orchestration_external_sources.py """Server admission and current access for retained external content. -Version: 0.261.279 +Version: 0.261.282 Session-trusted agent and action sources in: 0.261.270 -Hand-selected agent honoured while the agent preference is off in: 0.261.279 +Hand-selected agent honoured while the agent preference is off in: 0.261.282 No fetch, recall, plugin invocation, settings discovery, or credential persistence occurs here. Content digests attest the exact retained payload, not a remote page diff --git a/docs/admin/orchestration.md b/docs/admin/orchestration.md index 583ca3149..631063d25 100644 --- a/docs/admin/orchestration.md +++ b/docs/admin/orchestration.md @@ -379,7 +379,7 @@ agent is always offered to the planner, even when the user turned agents off in classic interface's settings. Local agents run as plan steps, including agents that use Microsoft 365 actions. -Since **0.261.279**, a chosen agent's step also runs while that setting is off. The +Since **0.261.282**, a chosen agent's step also runs while that setting is off. The setting is the agents button on the classic chat page (`/chats`). V2 has no control for it, and it's saved as off for a user whose settings were saved before they turned agents on, so many V2 users have it off. It still applies to agents the user didn't choose. @@ -716,7 +716,7 @@ then report a rejected proposal. | A Microsoft 365 step says plans can only read Microsoft 365 data | The action enables only send, invitation or mark-as-read functions, which plans never run. | Enable a read function on the action, or use it from chat without a plan. | | Before 0.261.270, a Microsoft 365 step said plans can't use Microsoft 365 in a shared conversation | Earlier versions refused Microsoft 365 steps in shared conversations. | Upgrade to 0.261.270 or later. The user's own request now counts as consent to share what the step reads. See [Microsoft 365 actions in plans](#microsoft-365-actions-in-plans). | | Before 0.261.270, every action or agent step failed with "A required retained result is unavailable or changed", while the same request worked with Orchestrate off | The step's configuration check refused it. For Microsoft 365 actions, the check couldn't see the action's Microsoft 365 selection. Local agents were refused outright. Failure events log `sc_authority_reason=external_configuration_unavailable` for `action_invoke` or `agent_invoke`. | Upgrade to 0.261.270 or later. Agent and action steps now trust the signed-in session, as manual chat does. See [Retained external-source authorization](#retained-external-source-authorization). | -| Before 0.261.279, an Ask an agent step for an agent the user picked, or tagged with @, failed with "This step's agent or action isn't available to you right now, so it didn't run" | The user's own agents setting was off. Planning offered the picked agent, but the step's access check didn't count the pick as permission. Failure events log `sc_authority_reason=result_external_capability_unavailable` for `agent_invoke`. It isn't specific to shared conversations: a request without a picked agent can still work because its plan uses an action instead. | Upgrade to 0.261.279 or later. Before upgrading, the user can turn agents on with the agents button on the classic chat page (`/chats`). See the [selected agent with agents turned off fix]({{ '/explanation/fixes/ORCHESTRATION_SELECTED_AGENT_DISABLED_PREFERENCE_FIX/' | relative_url }}). | +| Before 0.261.282, an Ask an agent step for an agent the user picked, or tagged with @, failed with "This step's agent or action isn't available to you right now, so it didn't run" | The user's own agents setting was off. Planning offered the picked agent, but the step's access check didn't count the pick as permission. Failure events log `sc_authority_reason=result_external_capability_unavailable` for `agent_invoke`. It isn't specific to shared conversations: a request without a picked agent can still work because its plan uses an action instead. | Upgrade to 0.261.282 or later. Before upgrading, the user can turn agents on with the agents button on the classic chat page (`/chats`). See the [selected agent with agents turned off fix]({{ '/explanation/fixes/ORCHESTRATION_SELECTED_AGENT_DISABLED_PREFERENCE_FIX/' | relative_url }}). | | Before 0.261.270, an @mention of a person in a shared conversation was sent to the model when Orchestrate was on | Orchestrate planned every message without applying the shared conversation's send rule. | Upgrade to 0.261.270 or later. See [Shared conversations](#shared-conversations). | | A participant sees "Only the person who started this shared conversation can use Orchestrate here" | A request reached the planner from someone other than the person who started the shared conversation. The V2 composer normally answers their requests the classic way instead. | Turn off Orchestrate for that request, or ask the person who started the conversation to ask it. | | The person who started a shared conversation sees "Orchestrate can't be used in this shared conversation because an earlier version kept another participant's private copy of it" | Before 0.261.270, Orchestrate saved a private copy for whoever used it first, under the shared conversation's ID. Only one record can have that ID. | Turn off Orchestrate to ask the assistant. To restore Orchestrate, remove that participant's record from the conversations container: its ID is the shared conversation's ID. The `[ORCHESTRATION]` warning logs `sc_reason=shared_conversation_stale_copy`. | diff --git a/docs/explanation/features/ORCHESTRATION_EXTERNAL_SOURCE_ACCESS.md b/docs/explanation/features/ORCHESTRATION_EXTERNAL_SOURCE_ACCESS.md index c5fae615d..17b80d152 100644 --- a/docs/explanation/features/ORCHESTRATION_EXTERNAL_SOURCE_ACCESS.md +++ b/docs/explanation/features/ORCHESTRATION_EXTERNAL_SOURCE_ACCESS.md @@ -9,7 +9,7 @@ roles of the signed-in session, as classic chat does, instead of reading Microsoft Graph on every call. Deep research can use its query and link planners again. See the [session identity fix](../fixes/ORCHESTRATION_SESSION_IDENTITY_FIX.md). -**Updated in version: 0.261.279.** An agent the user picked for a run is checked +**Updated in version: 0.261.282.** An agent the user picked for a run is checked against a catalog narrowed to that agent, and the user's `enable_agents` preference no longer blocks it. Before this, an Ask an agent step for a picked agent failed while the preference was off, even though planning and execution diff --git a/docs/explanation/fixes/ORCHESTRATION_SELECTED_AGENT_DISABLED_PREFERENCE_FIX.md b/docs/explanation/fixes/ORCHESTRATION_SELECTED_AGENT_DISABLED_PREFERENCE_FIX.md index cb2216349..091584cad 100644 --- a/docs/explanation/fixes/ORCHESTRATION_SELECTED_AGENT_DISABLED_PREFERENCE_FIX.md +++ b/docs/explanation/fixes/ORCHESTRATION_SELECTED_AGENT_DISABLED_PREFERENCE_FIX.md @@ -1,8 +1,8 @@ # Orchestration Selected Agent With Agents Turned Off Fix -**Version: 0.261.279** +**Version: 0.261.282** -Fixed in version: **0.261.279**, recorded in +Fixed in version: **0.261.282**, recorded in `application/single_app/config.py`. ## Issue @@ -80,7 +80,7 @@ What doesn't change: | File | Change | | --- | --- | | `application/single_app/functions_orchestration_external_sources.py` | Narrow the agent catalog to the run's picked agent and honour the pick in the capability check. | -| `application/single_app/config.py` | Version `0.261.279`. | +| `application/single_app/config.py` | Version `0.261.282`. | | `functional_tests/test_orchestration_seeded_agent_preference_fix.py` | New regression test. | | `docs/admin/orchestration.md` | Picked agents run while the agents setting is off; troubleshooting row. | | `docs/explanation/features/ORCHESTRATION_EXTERNAL_SOURCE_ACCESS.md` | Picked-agent catalog narrowing in the provider contract. | @@ -105,7 +105,7 @@ provider and the real `resolve_agent_catalog` over in-memory agents: - A narrowed catalog with more than one agent is refused. - Malformed picks don't lift the setting, and action steps never receive the pick. -16 of its 29 tests fail against the provider from 0.261.278, including the reported case. +16 of its 29 tests fail against the provider without this fix, including the reported case. With the new file, these existing suites pass unchanged, 506 tests and 282 subtests in all: `test_orchestration_external_sources.py`, `test_orchestration_session_trusted_sources.py`, diff --git a/functional_tests/test_orchestration_seeded_agent_preference_fix.py b/functional_tests/test_orchestration_seeded_agent_preference_fix.py index 15d875fad..f19fba90e 100644 --- a/functional_tests/test_orchestration_seeded_agent_preference_fix.py +++ b/functional_tests/test_orchestration_seeded_agent_preference_fix.py @@ -2,8 +2,8 @@ # 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.279 -Implemented in: 0.261.279 +Version: 0.261.282 +Implemented in: 0.261.282 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 @@ -80,7 +80,7 @@ def read(world, reader, output): def test_version_includes_seeded_agent_preference_fix(): - assert_app_version_at_least("0.261.279") + assert_app_version_at_least("0.261.282") def test_fix_and_troubleshooting_docs_describe_the_fix(): @@ -88,9 +88,9 @@ def test_fix_and_troubleshooting_docs_describe_the_fix(): fix_doc = handle.read() with open(ADMIN_DOC, encoding="utf-8") as handle: admin_doc = handle.read() - assert "Fixed in version: **0.261.279**" in fix_doc + assert "Fixed in version: **0.261.282**" in fix_doc assert "result_external_capability_unavailable" in fix_doc - assert "| Before 0.261.279, an Ask an agent step" in admin_doc + assert "| Before 0.261.282, an Ask an agent step" in admin_doc assert "ORCHESTRATION_SELECTED_AGENT_DISABLED_PREFERENCE_FIX" in admin_doc From 340cabddd259805d47a4fc4708c102ac3bc01cc1 Mon Sep 17 00:00:00 2001 From: Paul Lizer Date: Wed, 7 Oct 2026 12:59:59 -0400 Subject: [PATCH 3/4] Renumber the selected-agent fix to 0.261.288 The base branch now uses 0.261.282 through 0.261.287, so the fix moves to the next free version. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .../functions_orchestration_external_sources.py | 4 ++-- docs/admin/orchestration.md | 4 ++-- .../features/ORCHESTRATION_EXTERNAL_SOURCE_ACCESS.md | 2 +- ...ESTRATION_SELECTED_AGENT_DISABLED_PREFERENCE_FIX.md | 6 +++--- .../test_orchestration_seeded_agent_preference_fix.py | 10 +++++----- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/application/single_app/functions_orchestration_external_sources.py b/application/single_app/functions_orchestration_external_sources.py index 7d7c4a629..f0ea8af9c 100644 --- a/application/single_app/functions_orchestration_external_sources.py +++ b/application/single_app/functions_orchestration_external_sources.py @@ -1,9 +1,9 @@ # functions_orchestration_external_sources.py """Server admission and current access for retained external content. -Version: 0.261.282 +Version: 0.261.288 Session-trusted agent and action sources in: 0.261.270 -Hand-selected agent honoured while the agent preference is off in: 0.261.282 +Hand-selected agent honoured while the agent preference is off in: 0.261.288 No fetch, recall, plugin invocation, settings discovery, or credential persistence occurs here. Content digests attest the exact retained payload, not a remote page diff --git a/docs/admin/orchestration.md b/docs/admin/orchestration.md index 631063d25..e0f47ce52 100644 --- a/docs/admin/orchestration.md +++ b/docs/admin/orchestration.md @@ -379,7 +379,7 @@ agent is always offered to the planner, even when the user turned agents off in classic interface's settings. Local agents run as plan steps, including agents that use Microsoft 365 actions. -Since **0.261.282**, a chosen agent's step also runs while that setting is off. The +Since **0.261.288**, a chosen agent's step also runs while that setting is off. The setting is the agents button on the classic chat page (`/chats`). V2 has no control for it, and it's saved as off for a user whose settings were saved before they turned agents on, so many V2 users have it off. It still applies to agents the user didn't choose. @@ -716,7 +716,7 @@ then report a rejected proposal. | A Microsoft 365 step says plans can only read Microsoft 365 data | The action enables only send, invitation or mark-as-read functions, which plans never run. | Enable a read function on the action, or use it from chat without a plan. | | Before 0.261.270, a Microsoft 365 step said plans can't use Microsoft 365 in a shared conversation | Earlier versions refused Microsoft 365 steps in shared conversations. | Upgrade to 0.261.270 or later. The user's own request now counts as consent to share what the step reads. See [Microsoft 365 actions in plans](#microsoft-365-actions-in-plans). | | Before 0.261.270, every action or agent step failed with "A required retained result is unavailable or changed", while the same request worked with Orchestrate off | The step's configuration check refused it. For Microsoft 365 actions, the check couldn't see the action's Microsoft 365 selection. Local agents were refused outright. Failure events log `sc_authority_reason=external_configuration_unavailable` for `action_invoke` or `agent_invoke`. | Upgrade to 0.261.270 or later. Agent and action steps now trust the signed-in session, as manual chat does. See [Retained external-source authorization](#retained-external-source-authorization). | -| Before 0.261.282, an Ask an agent step for an agent the user picked, or tagged with @, failed with "This step's agent or action isn't available to you right now, so it didn't run" | The user's own agents setting was off. Planning offered the picked agent, but the step's access check didn't count the pick as permission. Failure events log `sc_authority_reason=result_external_capability_unavailable` for `agent_invoke`. It isn't specific to shared conversations: a request without a picked agent can still work because its plan uses an action instead. | Upgrade to 0.261.282 or later. Before upgrading, the user can turn agents on with the agents button on the classic chat page (`/chats`). See the [selected agent with agents turned off fix]({{ '/explanation/fixes/ORCHESTRATION_SELECTED_AGENT_DISABLED_PREFERENCE_FIX/' | relative_url }}). | +| Before 0.261.288, an Ask an agent step for an agent the user picked, or tagged with @, failed with "This step's agent or action isn't available to you right now, so it didn't run" | The user's own agents setting was off. Planning offered the picked agent, but the step's access check didn't count the pick as permission. Failure events log `sc_authority_reason=result_external_capability_unavailable` for `agent_invoke`. It isn't specific to shared conversations: a request without a picked agent can still work because its plan uses an action instead. | Upgrade to 0.261.288 or later. Before upgrading, the user can turn agents on with the agents button on the classic chat page (`/chats`). See the [selected agent with agents turned off fix]({{ '/explanation/fixes/ORCHESTRATION_SELECTED_AGENT_DISABLED_PREFERENCE_FIX/' | relative_url }}). | | Before 0.261.270, an @mention of a person in a shared conversation was sent to the model when Orchestrate was on | Orchestrate planned every message without applying the shared conversation's send rule. | Upgrade to 0.261.270 or later. See [Shared conversations](#shared-conversations). | | A participant sees "Only the person who started this shared conversation can use Orchestrate here" | A request reached the planner from someone other than the person who started the shared conversation. The V2 composer normally answers their requests the classic way instead. | Turn off Orchestrate for that request, or ask the person who started the conversation to ask it. | | The person who started a shared conversation sees "Orchestrate can't be used in this shared conversation because an earlier version kept another participant's private copy of it" | Before 0.261.270, Orchestrate saved a private copy for whoever used it first, under the shared conversation's ID. Only one record can have that ID. | Turn off Orchestrate to ask the assistant. To restore Orchestrate, remove that participant's record from the conversations container: its ID is the shared conversation's ID. The `[ORCHESTRATION]` warning logs `sc_reason=shared_conversation_stale_copy`. | diff --git a/docs/explanation/features/ORCHESTRATION_EXTERNAL_SOURCE_ACCESS.md b/docs/explanation/features/ORCHESTRATION_EXTERNAL_SOURCE_ACCESS.md index 17b80d152..71d4a3f55 100644 --- a/docs/explanation/features/ORCHESTRATION_EXTERNAL_SOURCE_ACCESS.md +++ b/docs/explanation/features/ORCHESTRATION_EXTERNAL_SOURCE_ACCESS.md @@ -9,7 +9,7 @@ roles of the signed-in session, as classic chat does, instead of reading Microsoft Graph on every call. Deep research can use its query and link planners again. See the [session identity fix](../fixes/ORCHESTRATION_SESSION_IDENTITY_FIX.md). -**Updated in version: 0.261.282.** An agent the user picked for a run is checked +**Updated in version: 0.261.288.** An agent the user picked for a run is checked against a catalog narrowed to that agent, and the user's `enable_agents` preference no longer blocks it. Before this, an Ask an agent step for a picked agent failed while the preference was off, even though planning and execution diff --git a/docs/explanation/fixes/ORCHESTRATION_SELECTED_AGENT_DISABLED_PREFERENCE_FIX.md b/docs/explanation/fixes/ORCHESTRATION_SELECTED_AGENT_DISABLED_PREFERENCE_FIX.md index 091584cad..7662e1cdf 100644 --- a/docs/explanation/fixes/ORCHESTRATION_SELECTED_AGENT_DISABLED_PREFERENCE_FIX.md +++ b/docs/explanation/fixes/ORCHESTRATION_SELECTED_AGENT_DISABLED_PREFERENCE_FIX.md @@ -1,8 +1,8 @@ # Orchestration Selected Agent With Agents Turned Off Fix -**Version: 0.261.282** +**Version: 0.261.288** -Fixed in version: **0.261.282**, recorded in +Fixed in version: **0.261.288**, recorded in `application/single_app/config.py`. ## Issue @@ -80,7 +80,7 @@ What doesn't change: | File | Change | | --- | --- | | `application/single_app/functions_orchestration_external_sources.py` | Narrow the agent catalog to the run's picked agent and honour the pick in the capability check. | -| `application/single_app/config.py` | Version `0.261.282`. | +| `application/single_app/config.py` | Version `0.261.288`. | | `functional_tests/test_orchestration_seeded_agent_preference_fix.py` | New regression test. | | `docs/admin/orchestration.md` | Picked agents run while the agents setting is off; troubleshooting row. | | `docs/explanation/features/ORCHESTRATION_EXTERNAL_SOURCE_ACCESS.md` | Picked-agent catalog narrowing in the provider contract. | diff --git a/functional_tests/test_orchestration_seeded_agent_preference_fix.py b/functional_tests/test_orchestration_seeded_agent_preference_fix.py index f19fba90e..cf641de03 100644 --- a/functional_tests/test_orchestration_seeded_agent_preference_fix.py +++ b/functional_tests/test_orchestration_seeded_agent_preference_fix.py @@ -2,8 +2,8 @@ # 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.282 -Implemented in: 0.261.282 +Version: 0.261.288 +Implemented in: 0.261.288 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 @@ -80,7 +80,7 @@ def read(world, reader, output): def test_version_includes_seeded_agent_preference_fix(): - assert_app_version_at_least("0.261.282") + assert_app_version_at_least("0.261.288") def test_fix_and_troubleshooting_docs_describe_the_fix(): @@ -88,9 +88,9 @@ def test_fix_and_troubleshooting_docs_describe_the_fix(): fix_doc = handle.read() with open(ADMIN_DOC, encoding="utf-8") as handle: admin_doc = handle.read() - assert "Fixed in version: **0.261.282**" in fix_doc + assert "Fixed in version: **0.261.288**" in fix_doc assert "result_external_capability_unavailable" in fix_doc - assert "| Before 0.261.282, an Ask an agent step" in admin_doc + assert "| Before 0.261.288, an Ask an agent step" in admin_doc assert "ORCHESTRATION_SELECTED_AGENT_DISABLED_PREFERENCE_FIX" in admin_doc From 63f5d5fbbd0fce8a1cb9effc9076320734a4f36e Mon Sep 17 00:00:00 2001 From: Paul Lizer Date: Wed, 7 Oct 2026 13:05:48 -0400 Subject: [PATCH 4/4] Renumber the selected-agent fix to 0.261.289 The base branch now uses 0.261.288 for Ask AI in the agent and action editors, so the fix and VERSION move to the next free version. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- application/single_app/config.py | 2 +- .../functions_orchestration_external_sources.py | 4 ++-- docs/admin/orchestration.md | 4 ++-- .../features/ORCHESTRATION_EXTERNAL_SOURCE_ACCESS.md | 2 +- ...ESTRATION_SELECTED_AGENT_DISABLED_PREFERENCE_FIX.md | 6 +++--- .../test_orchestration_seeded_agent_preference_fix.py | 10 +++++----- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/application/single_app/config.py b/application/single_app/config.py index 2f06e0514..efe40558c 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.288" +VERSION = "0.261.289" 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 f0ea8af9c..935fcc87d 100644 --- a/application/single_app/functions_orchestration_external_sources.py +++ b/application/single_app/functions_orchestration_external_sources.py @@ -1,9 +1,9 @@ # functions_orchestration_external_sources.py """Server admission and current access for retained external content. -Version: 0.261.288 +Version: 0.261.289 Session-trusted agent and action sources in: 0.261.270 -Hand-selected agent honoured while the agent preference is off in: 0.261.288 +Hand-selected agent honoured while the agent preference is off in: 0.261.289 No fetch, recall, plugin invocation, settings discovery, or credential persistence occurs here. Content digests attest the exact retained payload, not a remote page diff --git a/docs/admin/orchestration.md b/docs/admin/orchestration.md index e0f47ce52..60409ae0d 100644 --- a/docs/admin/orchestration.md +++ b/docs/admin/orchestration.md @@ -379,7 +379,7 @@ agent is always offered to the planner, even when the user turned agents off in classic interface's settings. Local agents run as plan steps, including agents that use Microsoft 365 actions. -Since **0.261.288**, a chosen agent's step also runs while that setting is off. The +Since **0.261.289**, a chosen agent's step also runs while that setting is off. The setting is the agents button on the classic chat page (`/chats`). V2 has no control for it, and it's saved as off for a user whose settings were saved before they turned agents on, so many V2 users have it off. It still applies to agents the user didn't choose. @@ -716,7 +716,7 @@ then report a rejected proposal. | A Microsoft 365 step says plans can only read Microsoft 365 data | The action enables only send, invitation or mark-as-read functions, which plans never run. | Enable a read function on the action, or use it from chat without a plan. | | Before 0.261.270, a Microsoft 365 step said plans can't use Microsoft 365 in a shared conversation | Earlier versions refused Microsoft 365 steps in shared conversations. | Upgrade to 0.261.270 or later. The user's own request now counts as consent to share what the step reads. See [Microsoft 365 actions in plans](#microsoft-365-actions-in-plans). | | Before 0.261.270, every action or agent step failed with "A required retained result is unavailable or changed", while the same request worked with Orchestrate off | The step's configuration check refused it. For Microsoft 365 actions, the check couldn't see the action's Microsoft 365 selection. Local agents were refused outright. Failure events log `sc_authority_reason=external_configuration_unavailable` for `action_invoke` or `agent_invoke`. | Upgrade to 0.261.270 or later. Agent and action steps now trust the signed-in session, as manual chat does. See [Retained external-source authorization](#retained-external-source-authorization). | -| Before 0.261.288, an Ask an agent step for an agent the user picked, or tagged with @, failed with "This step's agent or action isn't available to you right now, so it didn't run" | The user's own agents setting was off. Planning offered the picked agent, but the step's access check didn't count the pick as permission. Failure events log `sc_authority_reason=result_external_capability_unavailable` for `agent_invoke`. It isn't specific to shared conversations: a request without a picked agent can still work because its plan uses an action instead. | Upgrade to 0.261.288 or later. Before upgrading, the user can turn agents on with the agents button on the classic chat page (`/chats`). See the [selected agent with agents turned off fix]({{ '/explanation/fixes/ORCHESTRATION_SELECTED_AGENT_DISABLED_PREFERENCE_FIX/' | relative_url }}). | +| Before 0.261.289, an Ask an agent step for an agent the user picked, or tagged with @, failed with "This step's agent or action isn't available to you right now, so it didn't run" | The user's own agents setting was off. Planning offered the picked agent, but the step's access check didn't count the pick as permission. Failure events log `sc_authority_reason=result_external_capability_unavailable` for `agent_invoke`. It isn't specific to shared conversations: a request without a picked agent can still work because its plan uses an action instead. | Upgrade to 0.261.289 or later. Before upgrading, the user can turn agents on with the agents button on the classic chat page (`/chats`). See the [selected agent with agents turned off fix]({{ '/explanation/fixes/ORCHESTRATION_SELECTED_AGENT_DISABLED_PREFERENCE_FIX/' | relative_url }}). | | Before 0.261.270, an @mention of a person in a shared conversation was sent to the model when Orchestrate was on | Orchestrate planned every message without applying the shared conversation's send rule. | Upgrade to 0.261.270 or later. See [Shared conversations](#shared-conversations). | | A participant sees "Only the person who started this shared conversation can use Orchestrate here" | A request reached the planner from someone other than the person who started the shared conversation. The V2 composer normally answers their requests the classic way instead. | Turn off Orchestrate for that request, or ask the person who started the conversation to ask it. | | The person who started a shared conversation sees "Orchestrate can't be used in this shared conversation because an earlier version kept another participant's private copy of it" | Before 0.261.270, Orchestrate saved a private copy for whoever used it first, under the shared conversation's ID. Only one record can have that ID. | Turn off Orchestrate to ask the assistant. To restore Orchestrate, remove that participant's record from the conversations container: its ID is the shared conversation's ID. The `[ORCHESTRATION]` warning logs `sc_reason=shared_conversation_stale_copy`. | diff --git a/docs/explanation/features/ORCHESTRATION_EXTERNAL_SOURCE_ACCESS.md b/docs/explanation/features/ORCHESTRATION_EXTERNAL_SOURCE_ACCESS.md index 71d4a3f55..08a934a7e 100644 --- a/docs/explanation/features/ORCHESTRATION_EXTERNAL_SOURCE_ACCESS.md +++ b/docs/explanation/features/ORCHESTRATION_EXTERNAL_SOURCE_ACCESS.md @@ -9,7 +9,7 @@ roles of the signed-in session, as classic chat does, instead of reading Microsoft Graph on every call. Deep research can use its query and link planners again. See the [session identity fix](../fixes/ORCHESTRATION_SESSION_IDENTITY_FIX.md). -**Updated in version: 0.261.288.** An agent the user picked for a run is checked +**Updated in version: 0.261.289.** An agent the user picked for a run is checked against a catalog narrowed to that agent, and the user's `enable_agents` preference no longer blocks it. Before this, an Ask an agent step for a picked agent failed while the preference was off, even though planning and execution diff --git a/docs/explanation/fixes/ORCHESTRATION_SELECTED_AGENT_DISABLED_PREFERENCE_FIX.md b/docs/explanation/fixes/ORCHESTRATION_SELECTED_AGENT_DISABLED_PREFERENCE_FIX.md index 7662e1cdf..069368a97 100644 --- a/docs/explanation/fixes/ORCHESTRATION_SELECTED_AGENT_DISABLED_PREFERENCE_FIX.md +++ b/docs/explanation/fixes/ORCHESTRATION_SELECTED_AGENT_DISABLED_PREFERENCE_FIX.md @@ -1,8 +1,8 @@ # Orchestration Selected Agent With Agents Turned Off Fix -**Version: 0.261.288** +**Version: 0.261.289** -Fixed in version: **0.261.288**, recorded in +Fixed in version: **0.261.289**, recorded in `application/single_app/config.py`. ## Issue @@ -80,7 +80,7 @@ What doesn't change: | File | Change | | --- | --- | | `application/single_app/functions_orchestration_external_sources.py` | Narrow the agent catalog to the run's picked agent and honour the pick in the capability check. | -| `application/single_app/config.py` | Version `0.261.288`. | +| `application/single_app/config.py` | Version `0.261.289`. | | `functional_tests/test_orchestration_seeded_agent_preference_fix.py` | New regression test. | | `docs/admin/orchestration.md` | Picked agents run while the agents setting is off; troubleshooting row. | | `docs/explanation/features/ORCHESTRATION_EXTERNAL_SOURCE_ACCESS.md` | Picked-agent catalog narrowing in the provider contract. | diff --git a/functional_tests/test_orchestration_seeded_agent_preference_fix.py b/functional_tests/test_orchestration_seeded_agent_preference_fix.py index cf641de03..54fff269a 100644 --- a/functional_tests/test_orchestration_seeded_agent_preference_fix.py +++ b/functional_tests/test_orchestration_seeded_agent_preference_fix.py @@ -2,8 +2,8 @@ # 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.288 -Implemented in: 0.261.288 +Version: 0.261.289 +Implemented in: 0.261.289 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 @@ -80,7 +80,7 @@ def read(world, reader, output): def test_version_includes_seeded_agent_preference_fix(): - assert_app_version_at_least("0.261.288") + assert_app_version_at_least("0.261.289") def test_fix_and_troubleshooting_docs_describe_the_fix(): @@ -88,9 +88,9 @@ def test_fix_and_troubleshooting_docs_describe_the_fix(): fix_doc = handle.read() with open(ADMIN_DOC, encoding="utf-8") as handle: admin_doc = handle.read() - assert "Fixed in version: **0.261.288**" in fix_doc + assert "Fixed in version: **0.261.289**" in fix_doc assert "result_external_capability_unavailable" in fix_doc - assert "| Before 0.261.288, an Ask an agent step" in admin_doc + assert "| Before 0.261.289, an Ask an agent step" in admin_doc assert "ORCHESTRATION_SELECTED_AGENT_DISABLED_PREFERENCE_FIX" in admin_doc