Skip to main content

Azure AI Foundry hosted agents

5-minute deploy · BYO container · Foundry-managed VM-isolated sandbox

This is the hosted-agent path on Azure AI Foundry: you package your agent (LangGraph here) as a container, register it as a Foundry hosted agent, and Foundry runs it in a VM-isolated sandbox behind the project's agent endpoint. Because the sandbox runs your code, waxell-observe installs and runs inside the container just like any other Python dependency — the Foundry hosting boundary is transparent to the SDK.

What you get

  • Every LLM call your agent makes through the project's model deployment → captured as a child LLM span on the parent run with real model id, token counts, and cost.
  • Every tool call in the agent loop → captured as tool spans, no manual per-tool wrapping (the LangGraph instrumentor handles it).
  • Policies scoped to this agent — budget caps, tool blocks — enforced inside the container before the LLM/tool call fires.
  • Runs stream natively into your Waxell dashboard — no App Insights pull, no telemetry export wiring.

The sandbox is just another Python host. Your agent code is unchanged; Azure handles isolation, scaling, and lifecycle.

Prerequisites

azd ext install azure.ai.agents   # provides the `azd ai agent` commands
azd auth login

You also need:

  1. An Azure AI Foundry project with a model deployment (e.g. gpt-5-mini).
  2. The Foundry Project Manager role on the project (required by azd provision / azd deploy).
  3. Docker running locally — azd builds the agent image and pushes it to your project's Azure Container Registry.

Project layout

Three files: requirements.txt, Dockerfile, main.py.

requirements.txt:

waxell-observe
langchain-azure-ai[hosting]>=1.2.4
langgraph
langchain-core
langchain-openai
azure-identity
azure-ai-projects>=2.1.0

Dockerfile — the container serves the OpenAI Responses protocol that Foundry hosted agents speak. The langchain-azure-ai[hosting] extra requires pip 26 or newer, so upgrade pip before installing:

FROM python:3.12-slim

WORKDIR /app

ENV PYTHONUNBUFFERED=1 \
PORT=8088

# langchain-azure-ai's [hosting] extra requires pip >= 26
RUN pip install --no-cache-dir --upgrade "pip>=26"

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY main.py .

EXPOSE 8088

CMD ["python", "main.py"]

main.py — arm waxell-observe first, build a LangGraph graph, and serve it with ResponsesHostServer (which also auto-exposes the /readiness health endpoint Foundry probes — you don't implement it):

import os

# Arm waxell-observe before anything else runs. init() reads
# WAXELL_API_KEY / WAXELL_API_URL from env and auto-instruments
# LangGraph / LangChain.
if os.environ.get("WAXELL_API_KEY"):
try:
import waxell_observe as waxell

waxell.init()
except Exception: # never let observability break the runtime
pass

from langchain_core.tools import tool


@tool
def lookup_account(account_id: str) -> dict:
"""Look up an account's tier and region."""
return {"account_id": account_id, "tier": "enterprise", "region": "eastus"}


def _model():
"""Chat model bound to the project's model deployment via the
agent's own Entra identity — no API keys in the container."""
from azure.ai.projects import AIProjectClient
from azure.identity import DefaultAzureCredential, get_bearer_token_provider
from langchain_openai import ChatOpenAI

deployment = os.environ.get("MODEL_DEPLOYMENT_NAME", "gpt-5-mini")
cred = DefaultAzureCredential()
client = AIProjectClient(
endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], # injected by Foundry
credential=cred,
)
oai = client.get_openai_client()
token_provider = get_bearer_token_provider(
cred, "https://ai.azure.com/.default"
)
return ChatOpenAI(
base_url=str(oai.base_url),
model=deployment,
api_key="placeholder", # replaced per-request by the bearer token
default_headers={"Authorization": f"Bearer {token_provider()}"},
)


def build_graph():
from langgraph.prebuilt import create_react_agent

graph = create_react_agent(
_model(),
tools=[lookup_account],
prompt="You are a concise account assistant. One or two sentences.",
)
# LangGraph runs are attributed to graph.name — set it to the
# agent name you want to see in Waxell.
graph.name = "my-foundry-agent"
return graph


def main():
from langchain_azure_ai.agents.hosting import ResponsesHostServer

server = ResponsesHostServer(build_graph())
server.run(host="0.0.0.0", port=int(os.environ.get("PORT", "8088")))


if __name__ == "__main__":
main()

Foundry injects FOUNDRY_PROJECT_ENDPOINT into the container and the hosted agent authenticates to the model deployment with its own dedicated Entra identity — no model API keys anywhere in your image.

Deploy

azd ai agent init      # scaffolds azure.yaml for the agent
azd ai agent run # optional: build + run locally first
azd provision
azd deploy # builds the image, pushes to ACR, registers an immutable agent version

Set two environment variables on the agent version (in azure.yaml, or in the Foundry portal's agent-version settings):

  • WAXELL_API_URLhttps://api.waxell.dev (or your dedicated tenant URL).
  • WAXELL_API_KEY — your Waxell API key. Reference it from a Foundry CustomKeys connection rather than pasting a plaintext value into the version config.

Each azd deploy creates an immutable agent version, so you can canary a new build with a weighted traffic split before cutting over.

Invoke

Run the agent from the Foundry portal playground, or call its agent endpoint via the Responses API. Then check Waxell:

wax runs list --limit 5

You'll see the run under agent_name=my-foundry-agent — parent agent span, a chat gpt-5-mini LLM child span with real token counts and cost, and a tool span for each lookup_account call.

Network egress

Hosted-agent sandboxes egress over the public internet, so api.waxell.dev (HTTPS) is reachable out of the box. If your project restricts outbound traffic with a VNet, allow HTTPS egress to your Waxell endpoint so runs keep streaming.

Policy enforcement inside the runtime

The full Waxell policy stack runs unchanged inside the hosted-agent container — there is no Foundry-specific shim or escape hatch. Policies you scope to my-foundry-agent (budget caps, tool blocks, kill switches) are evaluated at the gate before each LLM and tool call, and a block verdict aborts the agent before the offending call lands — the run surfaces as Blocked and the violation is recorded in your Waxell governance dashboard.

Connect your Foundry project to Waxell

Connecting the project itself (separate from the SDK inside your container) makes your hosted agent appear in Agent Discovery and brings the project's prompt-based agents under the same roof — discovery, trace pull, and content-filter policy push.

  1. Create an app registration (service principal) in Entra: note the tenant id and client id, and create a client secret.

  2. Grant the service principal three roles:

    RoleScopeWhat it enables
    Cognitive Services UserFoundry accountAgent read (Agent Discovery sync)
    Cognitive Services ContributorFoundry accountContent-filter policy push + attach to model deployments
    Log Analytics ReaderSubscription (or the App Insights workspace)Trace pull for prompt-based agents
  3. Enable tracing on the project: agent → Traces tab → Connect → create an Application Insights resource. This requires the Microsoft.OperationalInsights and Microsoft.Insights resource providers to be registered on the subscription.

  4. In Waxell: Agent Discovery → Azure AI Foundry → enter the project endpoint, tenant/client ids, subscription, resource group, and account name, paste the client secret, and Sync.