Python: address PR review on declarative toolbox sample

Two security fixes for PR #5933:

1. Add safe_mode flag to WorkflowFactory (default True) mirroring
   AgentFactory. Gates =Env.* exposure inside DeclarativeWorkflowState
   PowerFx symbols via _safe_mode_context, so workflow YAML loaded from
   untrusted sources no longer leaks the host's full os.environ snapshot
   into PowerFx evaluation. The flag is also forwarded to the
   internally-constructed AgentFactory so inline agent definitions
   follow the same policy.

2. Pin the invoke_foundry_toolbox_mcp sample's _client_provider to the
   resolved toolbox endpoint. The bearer-authenticated httpx client is
   now only returned when MCPToolInvocation.server_url matches the
   toolbox URL case-insensitively; any other URL gets None (the default
   unauthenticated path), preventing the Foundry AAD bearer token from
   being attached to a mis-configured or injected server URL. Mirrors
   the .NET sample's httpClientProvider guard.

The sample is updated to opt in to safe_mode=False because its YAML
intentionally uses =Env.FOUNDRY_TOOLBOX_* to keep configuration in env
vars under the developer's control.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Peter Ibekwe
2026-05-18 14:43:26 -07:00
Unverified
parent 800a58d6c1
commit c30f104b20
3 changed files with 57 additions and 4 deletions
@@ -93,6 +93,7 @@ import asyncio
import contextlib
import json
import os
import sys
from collections.abc import Iterator
from pathlib import Path
from typing import Any
@@ -445,7 +446,23 @@ async def main() -> None:
follow_redirects=True,
)
async def _client_provider(_inv: MCPToolInvocation) -> httpx.AsyncClient | None:
async def _client_provider(invocation: MCPToolInvocation) -> httpx.AsyncClient | None:
# Pin the bearer-authenticated client to the resolved toolbox URL.
# The Foundry AAD bearer token is scoped to ``https://ai.azure.com``
# but we still refuse to attach it to any URL we did not provision —
# if the YAML resolves a different ``serverUrl`` (e.g. via a tampered
# ``Env.*`` value or a config injection), returning ``None`` causes
# ``DefaultMCPToolHandler`` to fall back to an unauthenticated client,
# which will fail to authenticate to the proxy instead of forwarding
# the token outbound. Mirrors the .NET sample's
# ``httpClientProvider`` URL guard.
if invocation.server_url.casefold() != toolbox_endpoint.casefold():
print(
f"[security] Refusing to attach Foundry bearer token to unexpected MCP URL: "
f"{invocation.server_url}",
file=sys.stderr,
)
return None
return http_client
async with (
@@ -456,6 +473,14 @@ async def main() -> None:
factory = WorkflowFactory(
agents={AGENT_NAME: summary_agent},
mcp_tool_handler=mcp_handler,
# The workflow YAML references ``=Env.FOUNDRY_TOOLBOX_*`` to keep
# the sample's toolbox URL / tool names configurable without
# editing the YAML. ``WorkflowFactory`` defaults to ``safe_mode=True``
# which would block those expressions; this sample opts in to the
# less-safe mode because we control both the YAML and the env
# vars. Do NOT copy this flag into a workflow that loads YAML
# from untrusted sources.
safe_mode=False,
)
workflow_path = Path(__file__).parent / "workflow.yaml"