# Deploy to Foundry

Agent Mission Control developer docs > Claude skill on Microsoft Foundry

> Build the image, create an immutable agent version with deploy.py, grant the instance identity, and avoid the known deploy gotchas.

**Before you start:** A built agent, a container registry the project can pull from, and the `Foundry Project Manager` role on the project.

## 1. Build the image

```bash
docker build --platform linux/amd64 -f invoice-summarizer-agent/Dockerfile \
  -t invoice-summarizer-agent:$(git rev-parse --short HEAD) .

# or build server-side, with no local push:
az acr build --registry <registry> --platform linux/amd64 \
  --image invoice-summarizer-agent:<git-sha> \
  -f invoice-summarizer-agent/Dockerfile --no-logs .
```

- `linux/amd64` is required: the platform is x86_64 only.
- **Tag by git SHA. Never `:latest`, never a reused tag.** The platform caches by tag, so a tag rebuilt after a version points at it is a fix that never shipped. `deploy.py` refuses `:latest`.

## 2. Configure .env

`.env`
```bash
FOUNDRY_PROJECT_ENDPOINT=https://<account>.services.ai.azure.com/api/projects/<project>
ANTHROPIC_FOUNDRY_RESOURCE=<account>
AGENT_IMAGE=<registry>.azurecr.io/invoice-summarizer-agent:<git-sha>

ANTHROPIC_DEFAULT_OPUS_MODEL=<opus-deployment-name>
ANTHROPIC_DEFAULT_SONNET_MODEL=<sonnet-deployment-name>
ANTHROPIC_DEFAULT_HAIKU_MODEL=<haiku-deployment-name>
CLAUDE_AGENT_MODEL=sonnet

MAX_TURNS=<from a measured run>
MAX_USD_PER_RUN=<from a measured run>
RUN_TIMEOUT_SECONDS=3300
SESSION_IDLE_TIMEOUT_SECONDS=3600

STEP_TOKENS=on
EMIT_STEP_ACTIVITY_LOG=true

HOSTED_CPU=2
HOSTED_MEMORY=4Gi

# only used to print a ready-to-paste role assignment
AZURE_SUBSCRIPTION_ID=<subscription-id>
AZURE_RESOURCE_GROUP=<resource-group>
```

There are no secrets in this file. `ANTHROPIC_FOUNDRY_RESOURCE` is the resource name only. Sandbox tiers are 0.5/1Gi, 1/2Gi and 2/4Gi; start on 2/4Gi. Guessing low doesn't give a slow run, it gives a container that dies at turn start with no envelope.

## 3. Run deploy.py

```bash
pip install "azure-ai-projects>=2.6.0" azure-identity python-dotenv
az login
python deploy.py
```

`deploy.py` doesn't build or push. It creates a version from the image, waits for it to become `active`, routes 100% of the endpoint to it, and prints both agent principals and the endpoint URL.

`deploy.py: the version definition`
```python
HostedAgentDefinition(
    cpu=CPU,
    memory=MEMORY,
    container_configuration=ContainerConfiguration(image=image),
    environment_variables=agent_environment(foundry_resource),
    protocol_versions=[ProtocolVersionRecord(protocol="responses", version="2.0.0")],
    session_configuration=SessionConfiguration(
        idle_timeout_seconds=timedelta(seconds=SESSION_IDLE_TIMEOUT_SECONDS)),
)
```

The version's environment sets `CLAUDE_CODE_USE_FOUNDRY=1`, `ANTHROPIC_FOUNDRY_RESOURCE`, the three pins, `CLAUDE_AGENT_MODEL`, `MAX_TURNS`, `MAX_USD_PER_RUN` (required, no defaults), `RUN_TIMEOUT_SECONDS`, `CLAUDE_CODE_MAX_RETRIES` (default 5), `STEP_TOKENS`, `EMIT_STEP_ACTIVITY_LOG`, and any integration settings you add. All of it is immutable per version, so "what limit was this run held to" is answerable from the deployment record.

### Version payload gotchas

- **Protocol `responses` version `2.0.0`, not `1.0.0`.** `1.0.0` gives an opaque `internal server error` on every request while the container looks healthy.
- **Session idle timeout on every version.** Without it every run longer than 15 minutes dies. It needs `SessionConfiguration`, and the field is a `timedelta`, not an integer.
- **No `AGENT_*` or `FOUNDRY_*` names** in `environment_variables`: the platform rejects the whole version as reserved. Container defaults for those names go in the Dockerfile. The platform injects `FOUNDRY_AGENT_VERSION`, `FOUNDRY_AGENT_NAME` and `FOUNDRY_PROJECT_ENDPOINT`, which you can read.
- **Rollback is a routing change**, not a rebuild. Versions are immutable and retained; point the version selector at a previous one.

## 4. Grant the identities

A hosted agent carries two principals: a blueprint and an **instance identity**. The sandbox authenticates as the instance identity. Granting the blueprint looks right in the portal and produces `401 Principal does not have access to API/Operation`. The instance identity is stable across versions, so you grant it once.

`Model access (printed by deploy.py)`
```bash
az role assignment create \
  --assignee-object-id <instance-identity-principal-id> \
  --assignee-principal-type ServicePrincipal \
  --role "Foundry User" \
  --scope "/subscriptions/<subscription-id>/resourceGroups/<resource-group>/providers/Microsoft.CognitiveServices/accounts/<account>"
```

- **Account scope, not project.** Claude Code calls the account's `/anthropic/v1/messages` endpoint itself, and account-level endpoints aren't proxied by the project.
- **Use `--assignee-object-id` with `--assignee-principal-type ServicePrincipal`.** The plain `--assignee` form does a Graph lookup a non-privileged account can't perform on these principals.
- **Image pull uses the account and project managed identities,** not the agent's. `image_pull_failed`, `UnauthorizedAcrPull` or `InvalidAcrPullCredentials` mean they lack classic **AcrPull** on the registry. `Container Registry Repository Reader` is inert on a registry in legacy permissions mode. The registry also needs `azureADAuthenticationAsArmPolicy` enabled.
- **Every outbound server** the skill calls must allow the same instance identity: an app role and, if the server keeps one, its caller allowlist.

### Outbound integrations

An unattended agent that must write to a Microsoft 365 document library or any Graph-backed store needs an app-to-app MCP server the brain calls. Foundry's built-in document-library tool is read-only grounding and refuses managed identities. When that API is app-only, no person can hold a token for it, so `{"mode":"preflight"}` is the only test. Adding a delegated scope so a person can probe it removes the server's security property.

## Runtime notes

- `CLAUDE_CODE_MAX_RETRIES` is bounded at 5. Foundry returns no rate-limit headers, and the CLI's default backoff makes a hard failure look like a hang.
- There is no per-request caller authorization on the data plane. Any principal with project data-plane access can invoke a run and spend up to `MAX_USD_PER_RUN`. Don't route a production agent to callers beyond the deploying team, and say so in its README.
- Set an Azure budget and action group on the subscription. It's the only bound that survives restarts.

## Windows workstations

- The Azure CLI can crash with `UnicodeEncodeError: 'charmap'` while streaming output. Set `$env:PYTHONIOENCODING = "utf-8"` and use `az acr build --no-logs`. The server-side operation usually succeeded, so check state before retrying.
- `docker push` through Docker Desktop's proxy can die partway. Use `az acr build`.
- Git Bash rewrites a leading `/subscriptions/...` argument into a Windows path. Run scope-bearing `az` commands from PowerShell or set `MSYS_NO_PATHCONV=1`. JMESPath filters break in PowerShell; run those from Bash.

## Related

- [Verify the agent and register it in AMC](https://www.agentmissioncontrol.ai/developers/foundry-verify-and-register/)
- [Troubleshoot a Foundry hybrid agent](https://www.agentmissioncontrol.ai/developers/foundry-troubleshooting/)
- [Invocation modes and session lifetime](https://www.agentmissioncontrol.ai/developers/foundry-invocation-and-sessions/)

---
Source: https://www.agentmissioncontrol.ai/developers/foundry-deploy/ · Last updated 2026-10-21 · Full index: https://www.agentmissioncontrol.ai/llms.txt
