Imported from dirien/from-zero-to-ai-agent-building-with-azure-ai-agent-service (
AGENTS.md). Install upstream withnpx skills add dirien/from-zero-to-ai-agent-building-with-azure-ai-agent-service. Copyright stays with the author.
AGENTS.md
For AI coding agents working in this repo. Humans: read
README.mdinstead.
What this repo is
A three-stage demo for a Pulumi/Microsoft Foundry talk: "From Prompt Agent to
Production: AI Agents on Azure AI Foundry". Mirrors the structure of the AWS
Bedrock AgentCore talk at
/Users/dirien/Tools/repos/from-works-on-my-machine-to-production-ready-ai-agents-with-amazon-bedrock-agent-core.
Single fraud-detection narrative — the same agent grows from a local Python script to a containerized Foundry-hosted production agent.
Stages
| Stage | Path | Foundry agent kind | What it ships |
|---|---|---|---|
| 1 | local-prototype/ |
none — orchestration runs locally | MAF Agent + OpenAIChatCompletionClient against Foundry Local, plain-Python tool functions, served behind agent-framework-foundry-hosting's ResponsesHostServer on port 8088 (the same Responses protocol Foundry hosted agents speak in production). server.py boots the host; demo_simulation.py is an HTTP client that POSTs to /responses. |
| 2 | basic-foundry-deployment/ |
hosted (container_app) |
Pulumi: Foundry account + gpt-4o-mini + RAI content-safety policy (HIGH filters + Jailbreak / Indirect-Attack / Protected-Material blocks) + ACR + pulumi-docker-build image + AgentV2 kind=hosted via the bridged dirien/azurefoundry TF provider. Container source is byte-for-byte identical to Stage 1; only the chat-client construction switches on FOUNDRY_PROJECT_ENDPOINT. |
| 3 | advanced-foundry-deployment/ |
three hosted (container_app) + one prompt |
Pulumi: ACR + image builds + MCP server (FastMCP) + 3 hosted MAF agents (fraud-triage / investigator-agent / fraud-action) + Foundry MemoryStoreV2 + ToolboxV2 + Foundry IQ (Azure AI Search + KnowledgeSource + KnowledgeBase + ProjectIndex) + APIM (AI Gateway) + Application Insights. Plus the declarative fraud-gateway-agent from Stage 2 preserved for the governance demo. Two evaluation modes — cloud-mode quality eval and AI red-team scan — render in the Foundry portal. See advanced-foundry-deployment/story.md for the talk script. |
Conventions
- Pulumi only for IaC.
pulumi-azure-native+ the bridgeddirien/azurefoundryprovider. No Bicep, noazapi, no Terraform native. - Python everywhere. No TypeScript anywhere in this repo.
swedencentralfor everything (Foundry, ACR, Container Apps, AI Search). Foundry's Responses API + vector stores don't run inwesteurope— only inaustraliaeast / eastus / eastus2 / francecentral / japaneast / swedencentral / uksouth. Sweden Central has the widest EU model coverage.- uv for all Python package management.
pyproject.tomlper project,uv syncto install. - ESC envs imported in
Pulumi.<stack>.yaml(per-stack), never inPulumi.yaml. Stage 2 + 3 each define their ownPulumi.dev.yamlwithenvironment: [foundry-talk/dev].
Bridged provider
The TF-bridged dirien/azurefoundry provider supplies typed Pulumi resources for the Foundry data plane (agents, files, vector stores). It lives at:
- Source:
https://github.com/dirien/terraform-provider-azurefoundry - Local clone (for hacking):
/Users/dirien/Tools/repos/terraform-provider-azurefoundry - TF Registry:
registry.terraform.io/dirien/azurefoundry - Bridge command:
pulumi package add terraform-provider registry.terraform.io/dirien/azurefoundry 0.8.4(both Stage 2 and Stage 3 are pinned at 0.8.4 — needed forToolboxV2,KnowledgeSource,KnowledgeBase,ProjectIndex, plus theheaders+allowed_toolsfields onmcptool blocks). - Resource catalogue (v0.8.4):
Agent,AgentV2,File,FileV2,VectorStore,VectorStoreV2,MemoryStoreV2,ToolboxV2,KnowledgeSource,KnowledgeBase,ProjectIndex. Project connections still live on the ARM plane (useazure-native:cognitiveservices:ProjectConnectionfrompulumi-azure-native).
Provider config (typed):
project_endpoint— required- Auth:
oidc_token+client_id+tenant_id(ESC OIDC, preferred); falls back toclient_secret,api_key,use_azure_cli, orDefaultAzureCredentialchain. - Reads
ARM_OIDC_TOKEN/ARM_CLIENT_ID/ARM_TENANT_IDfrom env (Pulumi ESC compatibility), so no provider block fields needed when running underpulumi env run ediri/pulumi-idp/auth.
Commands
ESC env imports are in each stack's Pulumi.dev.yaml, so the env auto-loads — no pulumi env run prefix needed for infra/. Stage 1's runner is pure Python (no Pulumi project) so it does need the prefix to inject env vars.
| Task | Command |
|---|---|
| Install stage deps | cd <stage>/infra && uv sync |
| Preview a stage | cd <stage>/infra && pulumi preview |
| Deploy a stage | cd <stage>/infra && pulumi up --yes |
| Destroy a stage | cd <stage>/infra && pulumi destroy --yes |
| Run Stage 1 demo | cd local-prototype && ./run.sh |
| Run Stage 2 demo | cd basic-foundry-deployment && ./test.sh (default Tokyo alert; or ./test.sh "ALERT: ..." for a custom payload) |
| Run Stage 3 functional demo | cd advanced-foundry-deployment && ./test.sh agents (or gateway) |
| Run Stage 3 quality eval | cd advanced-foundry-deployment && ./test.sh eval |
| Run Stage 3 AI red team | cd advanced-foundry-deployment && ./test.sh redteam |
| Refresh stack state | pulumi up -r --yes (use -r after external az deletes) |
| Refresh ESC OIDC token | pulumi env run ediri/pulumi-idp/auth -- bash -c 'az login --service-principal --username "$ARM_CLIENT_ID" --federated-token "$ARM_OIDC_TOKEN" --tenant "$ARM_TENANT_ID"' (every ~50 min — az CLI caches the federated token) |
| Bump bridged provider | edit infra/Pulumi.yaml parameter line, then cd infra && pulumi package add terraform-provider registry.terraform.io/dirien/azurefoundry <version> (regenerates the local SDK) |
Gotchas
- ESC OIDC token expires every hour. If
azCLI commands start failing withAADSTS700024, re-issue viapulumi env run ediri/pulumi-idp/auth -- bash -c 'az login --service-principal --username "$ARM_CLIENT_ID" --federated-token "$ARM_OIDC_TOKEN" --tenant "$ARM_TENANT_ID"'.pulumi-azure-nativeand the bridged provider's OIDC path pick up the env vars natively and refresh on each run — only theazCLI caches. Same goes for any Python script usingDefaultAzureCredentialagainsthttps://ai.azure.com/.default— it'll fail with the sameAADSTS700024once the token expires. - Custom subdomain reservation. Deleting a Foundry cognitive account leaves a soft-delete with a 48-hour subdomain reservation. Purge it before recreating:
az cognitiveservices account purge --name <name> --location <region> --resource-group <rg>. - RBAC propagation. Managed-identity role assignments take 10–30 min to flow through Foundry's access-check cache. Symptom: 401 from the Responses API immediately after
pulumi up. Wait and retry — don't add more role assignments. - MAF doesn't load
.envfiles automatically. Either set env vars in the shell (recommended — Pulumi ESC handles this) or calldotenv.load_dotenv()at the start of your script. pulumi package add terraform-provider <name>defaults toregistry.opentofu.org. Always prefix withregistry.terraform.io/for the bridged azurefoundry provider. Also: after a fresh upstream release, the TF registry sync is faster than Pulumi's resolver — you can see the version listed athttps://registry.terraform.io/v1/providers/dirien/azurefoundrybutpulumi package addstill says "Could not resolve a version". Wait 30–60s and retry.- Stage 1 has no cloud dependency. Stage 1 runs entirely on Foundry Local — no Azure account, no Stage 2 prerequisite. Stage 2 and Stage 3 are independently deployable (each has its own resource group + Foundry account).
- Pyright unresolved-import warnings for
agent_framework,pulumi_azurefoundry,pulumi_azure_native,azure.ai.evaluation,azure.ai.projectsare expected false positives — the venvs live under each stage's.venvand editor-side Pyright doesn't index them. Ignore. project.nameis the ARM path, not the bare project name.cognitiveservices.Project.namereturns<account>/<project>(e.g.foundry-talk-dev/fraud-detection). When constructing the Foundry data-planeproject_endpointfor the bridged provider, use the literalPROJECT_NAMEconstant, notproject.name.apply(...). Symptom: every FileV2/VectorStoreV2/AgentV2 upload returns 404 "project does not exist".westeuropedoes not support the Responses API and therefore no Foundry vector stores, nopromptagent kind. Supported regions (as of 2026-04):australiaeast / eastus / eastus2 / francecentral / japaneast / swedencentral / uksouth. Symptom: HTTP 400 "Vector stores are not supported in this region." See https://learn.microsoft.com/en-us/azure/ai-foundry/openai/how-to/responses#region-availability.- Hosted-agent runtime can return
status: "failed"with empty output after apulumi upthat bumped the agent's version. The new agent version takes 2–5 min to warm up. Symptom:./test.sh agentsproduces blank decisions, or./test.sh evalshowsdecision_actual=UNKNOWN. Wait and retry. Also: a single corrupted session (agent_session_id) can poison itself permanently — every call against that exact scope returns 500 forever. Use--freshonagentsmode to suffix user-IDs with a per-run UUID, sidestepping any poisoned scope. - Toolbox preview opt-in header isn't auto-injected. When a
kind=promptagent is wired to consume anazurefoundry_toolbox_v2.consumer_endpoint, Foundry's runtime calls the toolbox MCP endpoint without theFoundry-Features: Toolboxes=V1Previewheader → HTTP 403 "Access denied". Workaround: set the header explicitly on the agent'smcptool block via theheadersmap (added in v0.8.1). Drop this once the runtime auto-injects. - CognitiveSearch project connection needs
ApiKeyauth (notAAD) for the portal Indexes tab to populate. AAD-auth connections work for KB enumeration but the portal's index lister uses the connection's stored credentials directly. UseauthType=ApiKeywithcredentials.key = <search_admin_key>fromsearch.list_admin_key_output(...). Plus the project MI needs bothSearch Index Data ContributorandSearch Service Contributoron the search service (not justSearch Index Data Reader— the docs are explicit about this for keyless retrieval at agent runtime). azurefoundry_project_indexis required for Foundry IQ → Indexes tab to populate. The portal callslistProjectIndexesResolveragainst the project, which walks a Foundry-managed catalog separate from the underlying Search indexes. Just having aKnowledgeBasedoesn't auto-register an index. Bridged provider addedProjectIndexin v0.8.2; v0.8.3 fixed PUT→PATCH routing; v0.8.4 fixed the body shape (connectionName/indexNameflat, not nested underazureSearch).- Portal users need both
Azure AI UserANDAzure AI Project Manageron the Foundry account. The portal's "no permission" page only mentionsAzure AI Userbut several build-side surfaces (Evaluations, Red team, Guardrails, agent playground tracing) gate on Project Manager. Set viapulumi config set portalUserObjectIds "<oid1>,<oid2>"on the Stage 3 stack. - AI Red Teaming SDK has two paths — local PyRIT callback vs cloud-mode. The local path (
RedTeam(...).scan(target=callable)) runs but does not render in the new Foundry portal Red team tab. For portal-visible runs, use the cloud-mode pattern:AzureAIAgentTarget+openai_client.evals.create()+project_client.beta.evaluation_taxonomies.create()+evals.runs.create(data_source.type=azure_ai_red_team). Seeadvanced-foundry-deployment/red_team/run_redteam.py. azure-ai-evaluationlegacyevaluate()runner ignores async target callables silently. Async targets produce unawaited coroutines that the evaluators can't score →total: 0, passed: 0, failed: 0. Always use sync targets. Also: the bulk runner spawns parallel workers and threading-locks inside the target only serialise within a single worker — for true serial execution use the cloud-modeazure_ai_target_completionspath instead.- Stage 3 governed agent's MCP tools cannot all be inlined in a Toolbox. Foundry's toolset API allows only
mcp / web_search / azure_ai_search / openapi / a2a_preview / code_interpreter / file_searchin toolboxes.functionandmemory_searchmust stay inline on the agent. Symptom onpulumi up:Tool type 'memory_search_preview' is not supported in toolsets.
Prerequisites for running
- Membership in the
ediriPulumi org (thepulumi-idp/authESC env lives there). - Python 3.11+ with
uvon PATH. pulumiCLI ≥ 3.231.azCLI 2.84+ on PATH (Stage 3 only — used byaz acr build).
Do NOT
- Never commit secrets. Pulumi ESC or
pulumi config set --secretonly. - Never introduce a third Pulumi provider. Azure-native + dirien/azurefoundry is the entire dependency set.
- Never add a fourth stage without updating both
README.mdand the table above. - Never put
environment:imports inPulumi.yaml— always in the per-stackPulumi.<stack>.yaml.