These notes describe the public architecture, preview contracts, security boundaries, configuration, and validation steps for the sample.
This repository contains a Foundry Hosted Agent. It does not include an Azure Functions application.
main.pystarts an Agent FrameworkResponsesHostServer.repo_digest_agent.pycreates the agent, calls the AI Gateway model route, and connects to Foundry Toolbox.github_mcp_middleware.pybounds GitHub tool arguments and compacts results before the follow-up model call.github_credential_validator.pyperforms an advisory (non-blocking) check of the selected credential and warns when it is broader than a fine-grained, read-only, repository-scoped token.toolbox.yamldeclares the Foundry Toolbox GitHub MCP source.agent.yamldescribes the hosted agent.azure.yamldeclares the Foundry project, hosted agent, scheduled routine, Bicep provider, and deployment hooks.infra/main.bicepis the subscription-scope deployment entry point.infra/foundry-models/main.bicepcreates the model-serving Foundry account and deployments.infra/foundry-agents/main.bicepcreates the agent project, storage, Container Registry, monitoring, connections, and RBAC.infra/ai-gateway/main.bicepcreates AI Gateway, its Connector Namespace, model provider, model registrations, runtime key, and monitoring.chat.pyis a local console client for the Responses endpoint.
The scheduled routine is named daily-repo-digest. It runs at 9 AM in the
configured timezone and asks the agent for a digest of
microsoft/agent-framework. The Responses API also supports interactive
requests.
flowchart LR
operator["Operator"]
agent["Foundry Hosted Agent"]
toolbox["Foundry Toolbox"]
gateway["AI Gateway"]
models["Foundry model deployments"]
github["GitHub MCP server"]
operator --> agent
agent -->|"model request and Api-Key"| gateway
agent -->|"Entra identity"| toolbox
toolbox -->|"project connection and Api-Key"| gateway
gateway -->|"managed identity and Foundry User RBAC"| models
gateway -->|"repository-scoped read-only credential"| github
Model requests use
<gateway>/default/models/openai/v1/chat/completions or the corresponding
Responses route. Tool requests use
<gateway>/default/toolservers/github/mcp.
The agent does not call a Foundry model endpoint directly. This keeps model
traffic behind AI Gateway policies. FoundryChatClient is intentionally not
used for model inference.
The tool path is:
Agent -> Foundry Toolbox -> AI Gateway -> GitHub MCP
toolbox.yaml declares one MCP source backed by the aigw-github project
connection. The postprovision hook stores the AI Gateway Api-Key in that
connection. FoundryToolbox authenticates the agent-to-Toolbox request with
Entra identity.
The GitHub source sets require_approval: "never" because all three exposed
operations are read-only and the scheduled routine cannot pause for approval.
AI Gateway also injects:
X-MCP-Readonly: trueX-MCP-Tools: list_pull_requests,list_issues,actions_listfailureMode: failClosed
Tool Search is not enabled. The agent has only three known tools and calls all three in one parallel tool round, so discovery would add latency without reducing the initial schema enough to help.
The recommended GitHub backend credential is a fine-grained personal access token or GitHub App installation token limited to the repository being summarized, with read-only access for:
- Metadata
- Actions
- Contents
- Issues
- Pull requests
Avoid repository write, administration, organization administration, workflow
write, or classic repo scope where possible.
During postprovision, the hook obtains the credential from GitHub CLI. GH_TOKEN
can supply an ephemeral token for local or CI deployment. A dedicated GitHub
CLI account can be selected with GITHUB_MCP_GH_USER.
Before storing the credential in the ToolServer, the validator runs an advisory, non-blocking check that:
- Requires
GITHUB_REPOSITORYto useowner/repositorysyntax. - Warns when the token is not a
github_pat_fine-grained personal access token or aghs_GitHub App installation access token, and links to the docs for creating one. - For fine-grained and installation tokens, calls the GitHub repository API and
warns if it lacks read access or exposes
admin,maintain,push, ortriagerepository permissions, and exercises the Contents, Issues, Pull requests, and Actions runs endpoints.
The check never blocks deployment; it only surfaces warnings. GitHub does not return the complete selected-repository boundary or every granular write permission through the coarse repository role flags, so for least privilege the operator should still scope the token to only that repository with the documented read-only permissions.
The GitHub credential is used only to configure the AI Gateway ToolServer. It
is not written to Bicep outputs, the azd environment, .env, or hosted-agent
environment variables. The target repository in the agent prompt is
configuration, not an authorization boundary.
If azd up prints a warning that the GitHub credential is a broad,
account-wide OAuth or classic token, provisioning still succeeds, but the
postprovision hook stores that credential in the cloud AI Gateway ToolServer.
Replace it with a fine-grained, repository-scoped, read-only token by following
both parts below: create the token in the GitHub portal, then re-apply it from
the command line.
Seeing
403 Forbiddenor empty MCP results for a public repo you do not own (for examplemicrosoft/agent-framework)? The token was applied, but it is being refused. Two common causes:
- Enterprise token-lifetime policy. The repo owner's enterprise can cap fine-grained token lifetimes. The Microsoft Open Source enterprise forbids fine-grained tokens whose lifetime is greater than 8 days and returns
403on every call (the body names the enterprise and links to your token's settings). Regenerate the token with an expiration of 7 days or less (step 3 below).- Missing public-repo scope. A fine-grained token cannot read a repository you do not own unless it is scoped with Repository access → Public repositories (read-only) (step 5 below). "Only select repositories" cannot include a repo you do not administer, so it yields no read access.
Confirm the exact reason by reading the response body:
curl -sS -H "Authorization: Bearer $GH_TOKEN" \ -H "Accept: application/vnd.github+json" \ https://api.github.com/repos/microsoft/agent-framework
- Open https://github.com/settings/personal-access-tokens/new. This is Settings → Developer settings → Personal access tokens → Fine-grained tokens → Generate new token.
- Token name: for example
foundry-ai-gateway-repo-digest. - Expiration: choose the shortest window that fits your rotation policy. If
the repository owner belongs to an enterprise that caps fine-grained token
lifetimes, you must stay within that cap or every API call returns
403. The Microsoft Open Source enterprise (which ownsmicrosoft/agent-framework) forbids fine-grained tokens with a lifetime greater than 8 days, so pick 7 days for that and other Microsoft-owned public repositories. - Resource owner: select the account or organization that owns the repository being summarized. For a public repository you do not own, select your own account.
- Repository access:
- For a repository you own or administer, choose Only select repositories and pick that single repository.
- For a public repository you do not own, choose Public repositories
(read-only). This grants the read-only
pullpermission with no repository-permission selection required; skip to step 7.
- Repository permissions (only when you selected a specific repository) —
set each of these to Read-only and leave everything else at No
access:
- Metadata (required; auto-selected)
- Actions
- Contents
- Issues
- Pull requests
- Click Generate token and copy the
github_pat_...value. You cannot view it again after leaving the page.
Avoid repository write, administration, organization administration, workflow
write, or classic repo scope.
Re-run provisioning with the fine-grained token exported as GH_TOKEN. The hook
prefers GH_TOKEN over the account-wide GitHub CLI login and writes the tighter
credential into the ToolServer.
macOS or Linux (bash):
read -rsp "Fine-grained GitHub token: " GH_TOKEN && echo
export GH_TOKEN
azd provisionmacOS (zsh — the default macOS shell). The read prompt syntax differs from
bash: the prompt goes inside the variable spec as VAR?prompt, and -p must
not be used (in zsh -p reads from a coprocess, so no prompt appears). Paste
one line at a time so read does not consume the following lines as input:
read -rs "GH_TOKEN?Fine-grained GitHub token: " && echo
export GH_TOKEN
azd provisionShell-agnostic alternative (hidden entry, works in both bash and zsh):
export GH_TOKEN="$(python3 -c 'import getpass; print(getpass.getpass("Fine-grained GitHub token: "))')"
azd provisionWindows (PowerShell 7):
$GH_TOKEN = Read-Host -Prompt "Fine-grained GitHub token" -AsSecureString
$env:GH_TOKEN = [System.Net.NetworkCredential]::new("", $GH_TOKEN).Password
azd provisionazd up also works in place of azd provision. A GitHub App installation
access token (ghs_...) is an equally accepted least-privilege credential.
Re-running provisioning with the fine-grained token should print
Using the active GitHub CLI login for GitHub MCP. with no credential warning.
To check the token before provisioning:
GITHUB_MCP_TOKEN_TO_VALIDATE="$GH_TOKEN" \
python3 github_credential_validator.py "$GITHUB_REPOSITORY"A clean token prints that it has the required read access and no coarse write role. GitHub does not return the complete selected-repository boundary or every granular write permission through the coarse repository role flags, so the advisory check cannot certify least privilege on its own; scoping the token in the portal as above is what enforces it.
The root deployment creates three resource groups:
foundryagentsfor the Foundry project, agent hosting, storage, Container Registry, and hosted-agent monitoring.foundrymodelsfor the separate Foundry model account and deployments.gatewayfor AI Gateway, Connector Namespace, Gateway monitoring, model provider, and model registrations.
The only cross-group runtime authorization is the AI Gateway system identity's
Foundry User role assignment on the model account. The public Azure
role-definition ID used by the template is
53ca6127-db72-4b80-b1b0-d745d6d5456d.
The AI Gateway module uses these preview contracts:
Microsoft.ApiManagement/service@2025-09-01-preview- SKU
AIGatewaywith capacity1 Microsoft.ApiManagement/service/workspaces/modelProviders@2025-09-01-previewMicrosoft.ApiManagement/service/workspaces/modelProviders/models@2025-09-01-previewMicrosoft.ApiManagement/service/workspaces/toolServers@2025-09-01-previewMicrosoft.ApiManagement/service/apiKeys@2025-09-01-previewMicrosoft.Web/connectorGateways@2026-05-01-preview
The Connector Namespace uses the same name, resource group, and location as the APIM AI Gateway service. It is a separate resource from the GitHub MCP ToolServer and supports connector-backed scenarios.
The default workspace is implicit for control-plane child resources. The postprovision hook waits for the workspace child collection before configuring the GitHub ToolServer.
The Foundry model provider uses:
kind: Foundry- The model account endpoint returned by Azure
- Foundry account and deployment resource IDs
authentication.kind: ManagedIdentity- Token resource
https://cognitiveservices.azure.com/
Each model registration includes:
apiFormat: OpenAIChatCompletions/openai/v1/chat/completions/openai/v1/responses- A token-limit policy derived from deployment capacity
deployment.modelNameset to the Foundry deployment name from the final segment of the deployment resource ID
The deployment name, not the backing catalog model name, is the OpenAI model
value sent through AI Gateway.
The default split is:
- Foundry, storage, Container Registry, and monitoring:
eastus2 - AI Gateway and Connector Namespace:
eastus2
The Foundry region allowlist is:
eastuseastus2westusnorthcentralusswedencentraljapaneast
The AI Gateway preview region allowlist is:
westus2westus3eastuswestcentralusswedencentraleastus2
The model defaults are:
| Purpose | Gateway deployment | Backing model | Version | Capacity |
|---|---|---|---|---|
| Full | gpt-latest |
gpt-5.6-sol |
2026-07-09 |
20 |
| Mini | gpt-mini-latest |
gpt-5.4-mini |
2026-03-17 |
200 |
If gpt-5.6-sol is unavailable, set modelName=gpt-5.5 and
modelVersion=2026-04-24.
Global Standard capacity maps to the Gateway token limit at 1,000 tokens per minute per capacity unit. Capacity 20 therefore registers 20,000 tokens per minute for the full deployment. Mini capacity 200 accommodates requests from the GitHub Copilot app, also called ghapp, when built-in tool definitions make the prompt substantially larger.
The agent caps each response at 2,048 output tokens. Azure quota evaluation uses an estimate based on the prompt and requested maximum output, so this cap reduces avoidable reservation pressure on the follow-up call after MCP tool execution.
Bicep creates apiKeys/default. The postprovision hook retrieves the Gateway
key through listSecrets and retains listValues as a preview-contract
fallback. It can reuse the saved azd environment value when a later
reprovision cannot retrieve the value again.
The Gateway key is sent in the explicit Api-Key header. It is not sent as
Authorization: Bearer. The backing Foundry account has local authentication
disabled, and provisioning does not call the Foundry listKeys action.
The runtime fails when the Gateway endpoint or key is missing. It has no direct Foundry fallback.
APIM deletion can include a live service phase, a soft-deleted service phase, and an identity cleanup interval. Name availability alone does not prove that identity cleanup has completed.
azure.yaml invokes the lifecycle scripts in two paths:
- Preprovision preserves a succeeded Gateway and removes only a terminal failed Gateway that matches the current azd environment ownership markers.
- Postdown completes the explicit teardown, waits for the live resource to disappear, purges the soft-deleted APIM service, and waits for identity cleanup.
The scripts use bounded exponential polling. Defaults are:
- Initial interval: 5 seconds
- Maximum interval: 30 seconds
- Identity quiet window: 180 seconds
- Total operation bound: 900 seconds
Tests can override these values with:
APIM_LIFECYCLE_POLL_INITIAL_SECONDSAPIM_LIFECYCLE_POLL_MAX_SECONDSAPIM_LIFECYCLE_IDENTITY_SETTLE_SECONDSAPIM_LIFECYCLE_OPERATION_TIMEOUT_SECONDS
The nonsecret .azure/<environment>/apim-lifecycle.state marker contains only
environment and resource identity, location, lifecycle state, time, and a
deployment or resource failure identifier.
Relevant public documentation:
The hosted agent uses language: docker and docker.remoteBuild: true. The
Foundry module creates a Premium Container Registry with admin credentials
disabled, grants the Foundry project identity AcrPull, and creates a
project-scoped managed-identity Container Registry connection.
The developer running a remote build needs Container Registry Tasks Contributor or an inherited broader role on the registry.
The agent and Gateway use separate Log Analytics workspaces and workspace-based Application Insights components. Gateway payload capture is disabled. Each platform boundary owns its telemetry.
The azd environment contains deployment metadata. Do not distribute its
.azure/<environment>/.env file.
Create the smaller local development file with:
./scripts/create-dev-env.shor:
pwsh ./scripts/create-dev-env.ps1The scripts refuse to overwrite .env unless explicitly forced, restrict the
file to the current user, and do not print the Gateway key.
The local file contains:
AZURE_AI_GATEWAY_ENDPOINT="https://<gateway-host-name>/"
AZURE_AI_GATEWAY_API_KEY="<gateway-api-key>"
AZURE_AI_GATEWAY_MODEL="gpt-latest"
AZURE_AI_GATEWAY_MINI_MODEL="gpt-mini-latest"
TOOLBOX_ENDPOINT="https://<foundry-toolbox-endpoint>"
TOOLBOX_NAME="repo-digest-tools"
GITHUB_REPOSITORY="<owner>/<repository>"For direct local execution, main.py disables only Microsoft OpenTelemetry SDK
self-telemetry when the Foundry hosting marker is absent. Hosted observability
remains enabled in Foundry.
Run:
git diff --check
uv sync --frozen
uv run python -m compileall -q .
uv run python -m unittest discover -s tests
bash tests/test-apim-lifecycle.sh
bash tests/test-ai-gateway-model-registration.sh
az bicep build --file infra/foundry-agents/main.bicep
az bicep build --file infra/ai-gateway/main.bicep
az bicep build --file infra/foundry-models/main.bicep
az bicep build --file infra/main.bicep
docker build .