Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion application/single_app/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
62 changes: 53 additions & 9 deletions application/single_app/functions_orchestration_external_sources.py
Original file line number Diff line number Diff line change
@@ -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.289
Session-trusted agent and action sources in: 0.261.270
Hand-selected agent honoured while the agent preference is off in: 0.261.289

Check warning on line 6 in application/single_app/functions_orchestration_external_sources.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

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

No fetch, recall, plugin invocation, settings discovery, or credential persistence
occurs here. Content digests attest the exact retained payload, not a remote page
Expand All @@ -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

Check warning on line 16 in application/single_app/functions_orchestration_external_sources.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

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

Check warning on line 16 in application/single_app/functions_orchestration_external_sources.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
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

Check warning on line 18 in application/single_app/functions_orchestration_external_sources.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
the run's selection, and current scope, membership and governance are still rechecked.
"""

from dataclasses import dataclass
Expand All @@ -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,
Expand Down Expand Up @@ -235,19 +242,47 @@
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):

Check warning on line 246 in application/single_app/functions_orchestration_external_sources.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
"""The agent the user picked for this run: a selection, not proof of access.

Check warning on line 247 in application/single_app/functions_orchestration_external_sources.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

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

Picking an agent by hand is itself the permission to use it, so it lifts the

Check warning on line 249 in application/single_app/functions_orchestration_external_sources.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

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

Check warning on line 249 in application/single_app/functions_orchestration_external_sources.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains security control, sanitization, or audit marker. Recommendation%3A Confirm the change does not weaken auth, CSRF, CSP, XSS defenses, settings sanitization, redaction, audit logging, or tests.
user's general agent preference for that one agent. Current access to it is still

Check warning on line 250 in application/single_app/functions_orchestration_external_sources.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

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

Check warning on line 254 in application/single_app/functions_orchestration_external_sources.py

View workflow job for this annotation

GitHub Actions / malicious-pr-security-review

Important - Changed line contains AI, plugin, agent, or workspace boundary marker. Recommendation%3A Check whether prompts, chat history, uploaded documents, embeddings, citations, settings, or identity can cross a new boundary.
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":
Expand All @@ -263,7 +298,8 @@
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 [],
Expand Down Expand Up @@ -449,9 +485,13 @@
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
Expand Down Expand Up @@ -488,8 +528,12 @@
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":
Expand Down
8 changes: 7 additions & 1 deletion docs/admin/orchestration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.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.

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
Expand Down Expand Up @@ -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. |
Expand All @@ -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.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`. |
Expand Down
33 changes: 31 additions & 2 deletions docs/explanation/features/ORCHESTRATION_EXTERNAL_SOURCE_ACCESS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.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
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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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.

Expand All @@ -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`.

Expand Down Expand Up @@ -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
Expand Down
Loading
Loading