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
3 changes: 3 additions & 0 deletions python/.github/skills/python-code-quality/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ Following the "too many type checkers" approach, type checkers are split by targ
`# type: ignore[code]`. Suppress relaxed-pyright friction with `# pyright: ignore[rule]`.
- **Samples** add `pyright` to `pyrefly` + `ty` — mypy/zuban can't resolve script-style
sample layouts (numeric-prefixed dirs, duplicate `main.py`), but pyright handles them.
- `pyrefly.samples.toml` enables fallback lookup for standalone sibling imports.
The ty sample profile uses first-party `root = ["."]` for PEP 723 sibling helpers,
preserving automatic extra-path lookup for the existing samples.
- The strict source-pyright (`[tool.pyright]`) enforces `reportUnnecessaryTypeIgnoreComment`
and excludes tests/samples; the relaxed test/sample pyright configs do not flag unnecessary
ignores.
Expand Down
16 changes: 16 additions & 0 deletions python/packages/foundry_hosting/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,22 @@ The Responses host continues regular agents through its existing session store a
existing checkpoint store. A callable does not make arbitrary instance fields persistent; state needed by later
requests must remain in the supported stores.

For Responses integrations, use a factory when an MCP connection, provider, tool
cache or client carries request identity. A Toolbox's streamable-HTTP writer
inherits the context of the request that **connects** it; sharing that connection
across callers can retain the first call ID. Create the Toolbox and its skills
provider inside the factory, not at process startup.

Factory agents are entered/exited for each request, including failed entry and
cancellation. `Agent` manages context-managed clients and MCP tools, but it does
not automatically manage every context provider or external credential. The
[integration samples](../../samples/04-hosting/foundry-hosted-agents/) explicitly
own their SDK transports, credentials and providers: Search is request-owned;
Memory binds a fresh provider/project client to the trusted user and current call;
custom Cosmos state uses user **and** sandbox namespaces with conditional writes.
Only close resources that the factory creates and owns, never a supplied shared
client or somebody else's credential.

## Responses agent history and storage

The caller's `POST /responses` **`store` flag** controls whether the *outer* response is retrievable and whether
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,10 @@ class _ToolboxAuth(httpx.Auth):
asynchronous :class:`~azure.core.credentials_async.AsyncTokenCredential`
credentials are supported: the async flow awaits an async credential's
``get_token``, while the sync flow requires a synchronous credential. The
per-request ``x-agent-foundry-call-id`` is read from the request-scoped context
populated by the hosting endpoint; it resolves to a fresh value on each request
and is absent (no header) for protocol ``1.0.0`` or local development.
``x-agent-foundry-call-id`` is read from the hosting context inherited by the
MCP transport task. That task captures context at connection time, so hosted
callers must use a request-owned toolbox/connection rather than sharing one
across requests. The header is absent when no call ID is supplied.
"""

def __init__(self, credential: AzureCredentialTypes, scope: str) -> None:
Expand Down Expand Up @@ -166,15 +167,17 @@ class FoundryToolbox(MCPStreamableHTTPTool):
- forwards the platform per-request call-id (``x-agent-foundry-call-id``) so the
Foundry MCP proxy can resolve the caller context server-side.

The call-id forwarding is transparent: it is read from the request-scoped context
the hosting endpoint binds on each request, so no per-request wiring is needed.
The call-id is read from the context inherited by the MCP connection's writer.
Construct this toolbox inside a request-scoped agent factory so it captures
the current caller's context, not a preceding request's.
Because the toolbox endpoint is a first-party Foundry service, forwarding the
opaque caller token to it is safe.

Like any MCP tool, the connection lifecycle is driven by the agent: the hosting
server enters the agent, which connects the toolbox on first use and closes it
(and the HTTP client it owns) at shutdown. Using it as an ``async with`` context
manager directly is supported but not required.
server enters a factory-created agent for its request and closes its toolbox
and owned HTTP client afterward. An instance-owned agent instead keeps that
connection until shutdown and is not appropriate for differing caller contexts.
Using it as an ``async with`` context manager directly is also supported.

Examples:
.. code-block:: python
Expand All @@ -185,14 +188,19 @@ class FoundryToolbox(MCPStreamableHTTPTool):
from azure.identity import DefaultAzureCredential

credential = DefaultAzureCredential()
# The hosting server enters the agent, which connects/closes the toolbox.
toolbox = FoundryToolbox(credential)
agent = Agent(
client=FoundryChatClient(credential=credential),
tools=toolbox,
default_options={"store": False},
)
await ResponsesHostServer(agent).run_async()


def create_agent():
return Agent(
client=FoundryChatClient(credential=credential),
tools=FoundryToolbox(credential),
)


await ResponsesHostServer(agent=create_agent).run_async()

See the Responses Toolbox sample for explicit ownership and cleanup of
the request's project/model transports and credentials as well.
"""

def __init__(
Expand Down
2 changes: 2 additions & 0 deletions python/pyrefly.samples.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
# real mistakes (bad imports, wrong attribute/module access) without forcing readers to
# wade through casts and overload gymnastics just to satisfy third-party SDK stubs.
project-includes = ["samples"]
# Standalone hosted scripts import siblings from their own deployment directory.
enable-fallback-search-path = true
project-excludes = [
"**/autogen-migration/**",
"**/semantic-kernel-migration/**",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317"
> request and forwards the platform's per-request `x-agent-foundry-call-id`. See
> [`04-hosting/foundry-hosted-agents/responses/foundry_toolbox_mcp_skills`](../../../../04-hosting/foundry-hosted-agents/responses/foundry_toolbox_mcp_skills)
> for the minimal version of this pattern. Hosted runs connect the toolbox because
> `ResponsesHostServer` enters the agent; `console_app.py` and `evals.py` do it explicitly with
> `ResponsesHostServer` enters a **new factory-created agent per request**;
> `console_app.py` and `evals.py` do it explicitly with
> `async with agent:`.

> **The hosted agent's managed identity needs the `Foundry User` role.** This is the single most
Expand All @@ -53,16 +54,14 @@ export OTEL_EXPORTER_OTLP_ENDPOINT="http://localhost:4317"
> dereferences the project-level skill resource, which does require the role. The toolbox answers
> with a bare JSON-RPC `-32603` and no `data`, so nothing in the error names the cause.
>
> The container logs the identity to grant it to at startup:
>
> ```
> Agent managed identity (grant it the Foundry User role): <client-id>
> ```
> Obtain the hosted managed identity's principal ID from the platform/resource
> configuration when granting the role. Startup diagnostics log only which
> variables are present, not identity/session/call values or tokens.
>
> ```bash
> az role assignment create --assignee-object-id <agent-identity-object-id> \
> --assignee-principal-type ServicePrincipal --role "Foundry User" \
> --scope /subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.CognitiveServices/accounts/<account>
> --scope /subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.CognitiveServices/accounts/<account>/projects/<project>
> ```
>
> Resolve the object id with `az ad sp list --filter "startswith(displayName,'<account>')" -o table`
Expand All @@ -89,12 +88,51 @@ uv run --prerelease=allow python/samples/02-agents/harness/build_your_own_claw/c
uv run --prerelease=allow python/samples/02-agents/harness/build_your_own_claw/claw_step04_production_ready/hosted.py
```

The hosted version **disables file access and shell** on the container. In a shared, hosted environment, giving the model arbitrary read/write access to the container filesystem or letting it run shell commands is a serious security risk (data exfiltration, tampering, persistence), and the local confirmations vault the shell operates on doesn't exist there. Background agents and Monty CodeAct (alpha) remain enabled. If you need file access when hosted, pass an external `file_access_store` (for example, one backed by Azure Blob Storage) instead of the container disk.
The hosted version **disables file access and shell** on the container. In a
shared, hosted environment, arbitrary filesystem/shell access is a
data-exfiltration and tampering risk, and the local confirmations vault does not
exist there. Background research and Monty CodeAct remain enabled. An external
`file_access_store` must enforce trusted user/sandbox access before it can be
enabled; using a Blob Storage backend alone is not an authorization boundary.

File memory **stays enabled** when hosted, but its store has to move. The harness writes file memory
to `{cwd}/agent-file-memory` by default, and the deployed code directory (`/app`) is mounted
**read-only** on Foundry hosted agents, so the default directory fails. `hosted.py` therefore passes a
`FileSystemAgentFileStore` rooted at `~/.claw/agent-file-memory`, which is writable.
`FileSystemAgentFileStore` rooted at
`$HOME/.claw/agent-file-memory/<trusted-user-and-sandbox-hash>`, which is writable.
The hash comes from `FoundryRequestScope.storage_key`; hosted scope validation
uses the configured sandbox plus trusted user/call IDs, never a caller-selected
conversation or MAF session ID. File memory is **sandbox-specific**, unlike the
intentional user-wide sharing in the
[Foundry Memory sample](../../../../04-hosting/foundry-hosted-agents/responses/foundry_memory/).

`ResponsesHostServer(agent=create_agent, history_source="agent_server")` builds
new clients, Toolbox/skills providers, CodeAct and harness providers for each
request. The sample injects its own context-managed client into
`build_claw_agent`, leaving ownership of supplied/shared clients elsewhere
unchanged. That client closes only its own model/project transports and
credential; it captures the current platform call ID, not an earlier caller's.
History/approval state is persisted by the host, not retained on provider
instances. Local hosts keep their original builder defaults.

Request-lifetime middleware releases only this agent's background-provider
tasks for the active MAF session, in a `finally` path on success, failure or
cancellation. An idempotent cleanup hook on the outer stream also handles a
stream closed **before its first update**, while the iterator's `finally`
handles partial consumption and run errors. Outstanding research is cancelled
and joined before the request's transports close, using the provider's finite
**30-second** default. A child that ignores
cancellation is abandoned and logged when that bound expires, so it cannot
hold transport teardown open indefinitely. Complete/collect research within a
turn; unfinished runtime tasks cannot be resumed by a later factory-created
agent. Cleanup failure is logged without identity values and does not replace
an existing run failure.

Local runs use `AzureCliCredential` and are single-user development only.
Hosted runs use managed identity; project/Toolbox/Purview permissions and
resources require separate configuration. The unrelated Telegram sample also
requires externally configured Telegram/Key Vault credentials. None of those
credential-gated behaviors or deployments is proven by an offline smoke check.

### Deploy to Foundry

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ def _build_skills_provider(credential: TokenCredential) -> tuple[SkillsProvider,
platform's per-request ``x-agent-foundry-call-id``.

Note that reading a skill's body requires the caller identity to hold the ``Foundry User`` role
on the Foundry account. Discovery does not, so a missing grant shows up as skills that load and
on the Foundry project. Discovery does not, so a missing grant shows up as skills that load and
advertise fine but fail on first use — see the README.
"""
sources: list[SkillsSource] = [FileSkillsSource(str(_SKILLS_DIR), script_runner=subprocess_script_runner)]
Expand All @@ -211,10 +211,9 @@ def _build_skills_provider(credential: TokenCredential) -> tuple[SkillsProvider,
# (for example a literal ``{{TOOLBOX_MCP_SERVER_URL}}``). Silently skipping it makes a
# deployment error look identical to a deliberate opt-out, so warn instead.
logger.warning(
"Foundry skills disabled: TOOLBOX_MCP_SERVER_URL is set but is not an http(s) URL (got %r). "
"Foundry skills disabled: TOOLBOX_MCP_SERVER_URL is set but is not an http(s) URL. "
"If this looks like an unsubstituted placeholder, check the environment variable wiring "
"in azure.yaml / agent.manifest.yaml.",
toolbox_url,
)
else:
logger.info("Foundry skills disabled. Set TOOLBOX_MCP_SERVER_URL to enable them.")
Expand Down Expand Up @@ -293,6 +292,7 @@ def _build_purview_middleware(credential: TokenCredential | None = None) -> list
async def build_claw_agent(
*,
credential: TokenCredential | None = None,
client: FoundryChatClient | None = None,
project_endpoint: str | None = None,
model: str | None = None,
default_options: Mapping[str, Any] | None = None,
Expand All @@ -308,6 +308,7 @@ async def build_claw_agent(

Args:
credential: Azure credential for the Foundry chat client. Defaults to AzureCliCredential.
client: Optional preconfigured client. The caller owns its resources and middleware.
project_endpoint: Optional Foundry project endpoint override.
model: Optional model deployment override.
default_options: Optional per-agent default chat options, such as ``{"store": False}`` for hosting.
Expand Down Expand Up @@ -341,12 +342,13 @@ async def build_claw_agent(

# <create_client>
resolved_credential = credential or AzureCliCredential()
client = FoundryChatClient(
project_endpoint=project_endpoint,
model=model,
credential=resolved_credential,
middleware=_build_purview_middleware(purview_credential),
)
if client is None:
client = FoundryChatClient(
project_endpoint=project_endpoint,
model=model,
credential=resolved_credential,
middleware=_build_purview_middleware(purview_credential),
)
# </create_client>

skills_provider, skills_tools = _build_skills_provider(resolved_credential)
Expand Down
Loading
Loading