agentleFS
aiappsgbbaiappsgbb/awesome-gbb/skills/ghcp-hosted-agents/SKILL.md

ghcp-hosted-agents

Skill8 starsChanged 12 days agoMITView on GitHub

Deploy Foundry hosted agents using GitHub Copilot SDK (GHCP) with BYOK authentication and Invocations protocol. Alternative to MAF (Agent + FoundryChatClient + ResponsesHostServer). Use when you need streaming SSE output, progressive skill discovery, or long tool loops (>120s) that hit Foundry gateway timeout with Responses protocol. USE FOR: ghcp sdk, copilot sdk, github copilot agent, CopilotClient, InvocationAgentServerHost, BYOK, bring your own key, invocations protocol, SSE streaming agent, long running agent, send_and_wait timeout, ghcp hosted agent, copilot client foundry. DO NOT USE FOR: MAF agents (use foundry-hosted-agents), prompt agents, declarative agents, general Azure deploy.

  • Reads credentials

We split this skill into the rules it gives an agent

11 rules. About 0 are common, 8 are distinctive, and 3 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈8

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 3

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Keep its BYOK identities and permissions.

    line 47
  2. Distinctive:

    If your slowest tool is >30s and you need streaming progress events to the caller, GHCP SDK is the better fit.

    line 93
  3. Very specific:

    After that anchor, subsequent generic 401 PermissionDenied model.call_failure events and session.info events containing …

    line 354
  4. Very specific:

    If assistant output arrives after the recognized provider-auth terminal, it establishes recovered assistant output.

    line 371
  5. Distinctive:

    Only that complete immediate-post-active sequence is retryable.

    line 377
  6. Distinctive:

    Important: Always parse both event types.

    line 406
  7. Distinctive:

    Use a two-phase workaround:

    line 416
  8. Distinctive:

    Invoke: Call the agent via Invocations endpoint, collect responses

    line 418
  9. Very specific:

    If you instead copy the canonical files into an existing repository and run azd deploy directly (direct-copy), azure.yaml alone is …

    line 535
  10. Distinctive:

    Verify what's installed:

    line 586
  11. Distinctive:

    If a grant may already exist, resolve its id idempotently first and only create (and only later revoke) the ones you actually add:

    line 698

≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

GHCP SDK Hosted Agents on Foundry

Deploy Foundry hosted agents using the GitHub Copilot SDK with BYOK (Bring Your Own Key) authentication. No GITHUB_TOKEN required. Uses the Invocations protocol (GA, version 2.0.0) with SSE streaming for unlimited tool-loop duration.

GA migration (v2.0.0). This skill now uses the unified single-file azure.yaml deploy shape shared with foundry-hosted-agents — the old two-file agent.yaml + separately-wired azure.yaml contract, the remote-build/manual services: wiring, and the tenant-ID-env deploy hook are all retired. See § "azure.yaml (unified GA deployment)" and § "Identity & RBAC for hosted agents" below.

RBAC correction (v2.0.9). Real model inference requires granting the hosted-agent instance identity the Foundry User role at BOTH the Foundry account and project scopes before invoking. The earlier "no agent role assignment needed" claim was validated only against an upstream echo agent that never called a real model, so it silently returned HTTP 401 on live inference. See § "Identity & RBAC for hosted agents".

When to Use GHCP SDK Instead of MAF

Shared deployment, distinct runtime. Select/review the consumer against the hosted contract. The MAF private BASIC evidence does not certify this GHCP/Invocations variant. Keep its BYOK identities and permissions; do not copy the MAF implicit-access assumption to a different account-level call.

The optional Python invoker now requires a durable record callback, or INVOCATION_EVIDENCE_FILE for the CLI. Package the canonical operation evidence helper beside it. It retains intent/HTTP metadata, rejects truncated or malformed SSE, and treats the template runtime's invocation_id as a custom ID, not proof of a retrievable Foundry response. Reconcile an uncertain effect; never repeat it merely to recover output. See the recovery contract.

ConsiderationMAFGHCP SDK
AuthDefaultAzureCredential → FoundryChatClientBYOK: DefaultAzureCredential → bearer token
ProtocolResponses API (request/response)Invocations (SSE streaming)
Tool loop limit~120s (Foundry gateway timeout)♾️ (SSE keeps connection alive)
Per-query overheadLow (1-3 internal turns)High (20-34 internal tool calls per query)
Skill discoverySkillsProvider.from_paths() (recommended) OR inline concat (legacy)SkillsProvider (default)
Custom tools@tool decoratorNot supported (use MCP servers instead)
ToolboxMCPStreamableHTTPTool (was client.get_toolbox(), removed 1.3.0)Not directly available
MaturityProduction-provenValidated (identical eval scores)

Use GHCP SDK when:

  • Slow tools mask the per-query overhead: web scraping, long-running tools, or multi-site scans.
  • You need streaming progress events to the caller for workflows that run beyond the ~120s Responses gateway limit.
  • The workload looks like bat-scraper: Playwright/tool calls take 30-60s each, so GHCP SDK orchestration overhead is acceptable.

Use MAF when:

  • Tools are fast: data queries, MCP retrieval, or single-shot tools where orchestration overhead dominates latency.
  • You need the faster runtime path; field measurements show MAF at ~19s vs GHCP SDK at >220s for the same fast-MCP workload (~10x faster).
  • Agent needs custom @tool functions, Foundry Toolbox (web_search, code_interpreter), or simpler deployment.

⚠️ Performance Caveat: Per-Query Overhead

Each CopilotClient query generates 20-34 internal tool calls for planning, skill discovery, tool selection, and related orchestration. That happens regardless of how many user-facing tool calls the agent actually makes. The overhead lives in CopilotClient itself; SkillsProvider is NOT the cause. Removing SkillsProvider does not eliminate the extra internal turns. Field measurement on an identical agent (gpt-5.4, same MCP tool set, same query) ran in ~19s on MAF but >220s on GHCP SDK when MCP tools were fast. The overhead is masked when tools are slow: bat-scraper Playwright calls take 30-60s each, so 20 internal calls vanish in the noise. Decision rule: if your slowest tool is <2s, you almost certainly want MAF. If your slowest tool is >30s and you need streaming progress events to the caller, GHCP SDK is the better fit.

Note on SkillsProvider cost (both runtimes). SkillsProvider is not GHCP-only — MAF supports it equally well via context_providers=[skills_provider] (see foundry-hosted-agents § Skill Loading). On both runtimes, SkillsProvider itself adds only +1 load_skill round-trip per skill the agent activates per query (typically 1-3 per query). The 20-34-call overhead above is the CopilotClient runtime, an entirely separate concern.


Runtime Pattern (GHCP SDK + Invocations)

Copy the reference template at references/container.py into the project root. Then adapt _load_mcp_servers() for your MCP server configuration.

The template provides:

  1. _init_byok() / _get_provider() — BYOK auth with DefaultAzureCredential
  2. _ensure_session() — Creates/resumes CopilotClient session with provider, skills, MCP
  3. _stream_response() — Subscribes to session events, yields SSE
  4. handle_invoke() — @app.invoke_handler that returns StreamingResponse
container.py → CopilotClient + InvocationAgentServerHost → port 8088
  ├── BYOK auth (ai.azure.com scope)
  ├── Skills via skill_directories parameter
  ├── MCP servers via mcp_servers parameter
  └── SSE streaming (no timeout on long tool loops)

Key points:

  • FOUNDRY_PROJECT_ENDPOINT is injected by the platform — never declare it under the azure.yaml agent service's environmentVariables list (see § "azure.yaml (unified GA deployment)")
  • BYOK scope is https://ai.azure.com/.default — NOT cognitiveservices.azure.com
  • Token is static per session — _get_provider() mints fresh token each session creation
  • working_directory should be $HOME (hosted agents sandbox filesystem at home dir)
  • system_message requires dict format: {"mode": "replace", "content": "..."}
  • PermissionHandler.approve_all auto-approves tool calls (required for unattended agents)

Why Invocations Protocol (Not Responses)

The GHCP SDK can also run behind ResponsesHostServer using GitHubCopilotAgent:

# ⚠️ This pattern DOES NOT WORK for long tool loops on Foundry
from agent_framework_foundry_hosting import ResponsesHostServer
from agent_framework.github import GitHubCopilotAgent

agent = GitHubCopilotAgent(...)
server = ResponsesHostServer(agent)
server.run()

Why it fails: GitHubCopilotAgent.send_and_wait() blocks until the full response completes. Default timeout is 60s (configurable). But Foundry's gateway has a ~120s hard timeout on non-streaming responses. Any tool-heavy workflow (web scraping, multi-site scanning) taking >120s returns a gateway timeout.

Invocations protocol solves this: SSE events stream continuously, keeping the connection alive. A query using 50+ tool calls over 5 minutes works perfectly because events flow throughout.

ApproachMax DurationWhy
ResponsesHostServer + send_and_wait~120sFoundry gateway timeout on non-streaming
InvocationAgentServerHost + SSEUnlimitedEvents stream continuously

Critical: the azure.yaml agent service must declare protocol: invocations only — do not add a responses protocol entry. InvocationAgentServerHost only serves /invocations; the /responses path returns 404 even if declared. See the troubleshooting row "responses protocol not declared" (bot 400) — dual protocols don't work. WHY: InvocationAgentServerHost is the only server type the GHCP SDK runtime ships with; serving Responses requires switching to the MAF runtime (ResponsesHostServer) entirely. External callers (bot, eval scripts) must POST to the Invocations SSE endpoint directly, or use azd ai agent invoke --protocol invocations. If you need oai.responses.create() for a Teams bot, use MAF runtime instead.


pyproject.toml

Copy references/pyproject.toml into your project root and update name and version.

Notes:

  • github-copilot-sdk provides CopilotClient, session management, event types
  • azure-ai-agentserver-invocations provides InvocationAgentServerHost
  • azure-ai-agentserver-core pinned EXACT (==2.0.0b7) — it's a transitive dep of -invocations declared as >=2.0.0b7 (unbounded upper); the exact pin stops a future core b8+ from silently breaking fresh container builds
  • azure-identity pinned to avoid pulling beta versions
  • prerelease = "if-necessary-or-explicit" needed for beta agentserver package

Dockerfile

Copy references/Dockerfile into your project root. No changes needed for most agents.


BYOK Authentication Deep Dive

BYOK (Bring Your Own Key) lets CopilotClient use your Foundry model deployment instead of GitHub's hosted models. No GITHUB_TOKEN needed.

How It Works

CopilotClient.create_session(provider={...})
  └── Routes LLM calls to your Foundry project endpoint
      └── Uses bearer token from DefaultAzureCredential
          └── Scope: https://ai.azure.com/.default

Provider Configuration

copilot.ProviderConfig is a TypedDict — at runtime it produces a plain dict, so the "class" form and the "dict" form below are byte-identical runtime objects. Use the class form when you want IDE / typing help; use the dict form when you want a one-liner.

What actually matters is the values: type="azure" + bare endpoint is the PRIMARY shape (matches the official Microsoft sample, ~2-3× faster); the legacy type="openai" + /openai/v1/ is still accepted for backward compat.

# RECOMMENDED — class form (TypedDict), gives you typing + IDE completion.
from copilot import ProviderConfig

provider = ProviderConfig(
    type="azure",                          # SDK adds api-version itself
    base_url=FOUNDRY_ENDPOINT,             # BARE project endpoint — no /openai/v1/
    wire_api="responses",
    bearer_token=token.token,              # from DefaultAzureCredential
)
# LEGACY — same wire shape, type='openai' kept for backward compat (2-3× slower).
provider = {
    "type": "openai",
    "base_url": f"{FOUNDRY_ENDPOINT}/openai/v1/",  # must end with /openai/v1/
    "bearer_token": token.token,
    "wire_api": "responses",
}

Provider shape decision matrix

Measured against gpt-5.4-mini on a live Foundry project (May 2026):

typebase_url suffixResultLatency
"azure"bare endpoint✅ Recommended~2.6s
"azure"/openai/v1/✅ Works~7.9s
"openai"/openai/v1/✅ Legacy compat~6.9s
"openai"bare endpoint❌ 400 Missing api-versionn/a

github-copilot-sdk 1.0.1 (GA, this skill) and prior 0.3.0 / 1.0.0b* preview lines all accept both type="azure" and the legacy type="openai" shapes; the legacy dict form is preserved across releases for backward compatibility. The 1.0 GA constructor is flat — pass github_token=... directly to CopilotClient(...) (no SubprocessConfig wrapper; auto_start kwarg removed — call await client.start() explicitly). The public import surface as of the GA release is from copilot import CopilotClient, PermissionHandler, ProviderConfig and from copilot.session_events import SessionEventType (matches the official Microsoft sample's main.py) — see references/container.py.

Common BYOK Mistakes

MistakeSymptomFix
Wrong scope401 UnauthorizedUse ai.azure.com not cognitiveservices.azure.com
type="openai" + bare endpoint400 Missing api-versionEither switch to type="azure" + bare, or append /openai/v1/
Token not refreshed401 after ~1hMint fresh token per session in _get_provider()
Permission error during deploy or invoke403 / PermissionDeniedHard FAIL except the exact immediate-post-active readiness envelope documented under § "Invoking the Agent". That narrow case retries the same path; all others require root-cause investigation. The two required instance grants (account + project) are a documented prerequisite, not an invoke-time workaround - do not add further grants to route around a failure

CopilotClient Session Parameters

# Recommended pattern (matches official Microsoft sample): explicit start().
# As of github-copilot-sdk 1.0 GA, CopilotClient takes flat kwargs — no
# SubprocessConfig wrapper, no auto_start. Construct, then await start().
client = CopilotClient()
await client.start()

session = await client.create_session(
    provider=provider,                    # ProviderConfig OR dict — see "Provider Configuration"
    model="gpt-5.4-mini",                 # Foundry model deployment name
    system_message={                      # MUST be dict, not string
        "mode": "replace",
        "content": "You are a helpful assistant.",
    },
    skill_directories=["/app/skills"],    # Paths to SKILL.md directories
    mcp_servers=[                         # MCP server configs (list[dict] or dict[str, dict])
        {"name": "playwright", "url": "https://my-mcp.azurecontainerapps.io/mcp"},
    ],
    working_directory=str(Path.home()),   # Must be $HOME for hosted agents
    streaming=True,                       # Enable streaming events
    on_permission_request=PermissionHandler.approve_all,  # Auto-approve tools
)

Session Event Types

Event TypeMeaningAction
SESSION_IDLEResponse completeStop streaming, yield done event
SESSION_ERRORAgent errorYield error event, stop
assistant.messageFinal assistant responseContains full response text
assistant.message_deltaStreaming content chunkAccumulate for progressive display
assistant.streaming_deltaRaw streaming deltaLower-level than message_delta
assistant.reasoningReasoning traceFull reasoning block
assistant.reasoning_deltaReasoning chunkStreaming reasoning
tool.execution_startTool call startedLog for debugging
tool.execution_completeTool call finishedLog for debugging
assistant.turn_startNew turnTrack turn count
assistant.turn_endTurn completeMay have more turns

Invoking the Agent

Primary path — azd ai agent invoke. Per the current GA CLI (verified against a locally installed azure.ai.agents 1.0.0-beta.4 extension), azd ai agent invoke DOES wrap the positional message argument correctly and accepts a raw JSON payload for the Invocations protocol:

azd ai agent invoke "<agent-name>" '{"input": "Say hello in one short sentence."}' \
  --protocol invocations \
  --output raw \
  --timeout 180
  • --protocol invocations selects the Invocations protocol (the CLI defaults to responses otherwise)
  • --output raw (or -o raw) dumps the unmodified server response (status line, headers, body) — the SSE data: lines are visible verbatim, which is what you want to grep for assistant.message / assistant.message_delta
  • --timeout <seconds> (or -t) bounds the request; the CLI default is 1800s, which is far longer than most smoke/CI budgets need

Immediately after a new version becomes active, the first model call can briefly return an HTTP-200 SSE readiness envelope while the platform's implicit agent permission finishes propagating. The exception is exact and has these guards:

  1. The first recognized auth failure must be a model.call_failure event with statusCode: 401 whose PermissionDenied error contains both Microsoft.CognitiveServices/accounts/OpenAI/responses/write and POST /openai/v1/responses.
  2. After that anchor, subsequent generic 401 PermissionDenied model.call_failure events and session.info events containing transient_auth_error may interleave. At least one transient-info event is required. The anchor must not repeat; a repeated anchored failure is outside the observed readiness envelope and remains a hard failure.
  3. The envelope must end with a terminal error containing Authentication failed with provider and HTTP 401.

Observed streams may include benign session.usage_info and assistant.turn_end lifecycle events inside the recognized envelope. The SSE epilogue is a separate event: done frame whose JSON payload contains a non-empty string invocation_id; accept it only after assistant success or the recognized provider-auth terminal. Untyped data outside that done frame, unknown typed events inside the envelope, or a malformed done payload remain hard failures.

If assistant output arrives after the recognized provider-auth terminal, it establishes recovered assistant output. Only after that recovery may session.usage_info and assistant.turn_end follow before the exact done frame. Those lifecycle events before assistant recovery do not convert the terminal failure into success.

Only that complete immediate-post-active sequence is retryable. Retry the same JSON positional invoke path with bounded backoff (six attempts, 15 seconds between attempts). Do not switch invocation methods and do not add a role grant. Missing, malformed, reordered, or unrelated terminal events remain a hard failure.

The older claim that azd ai agent invoke "does not wrap user input" was a pre-GA defect specific to earlier preview builds. It does not reproduce against the current GA CLI and is retired from this skill — do not carry it forward into new guidance.

Via curl or the reference script (advanced/optional)

For callers that need to parse the raw SSE stream directly (e.g. wiring a bot integration rather than a one-off smoke test), use references/invoke_agent.py as a library or standalone script:

export AZURE_AI_PROJECT_ENDPOINT="https://<account>.services.ai.azure.com/api/projects/<project>"
export AGENT_NAME="my-agent"
python references/invoke_agent.py "What is the capital of France?"

The invoke_invocations() function POSTs to {endpoint}/agents/{agent_name}/endpoint/protocols/invocations?api-version=v1 (GA endpoint API version literal v1 — no Foundry-Features preview header) and parses both assistant.message and assistant.message_delta events, preferring the complete message when available.

Important: Always parse both event types. Some responses only emit deltas without a final complete message event.


Evaluation

Two-Phase Eval (Invocations Protocol)

Foundry eval SDK's azure_ai_agent target does not route to hosted agent endpoints. Use a two-phase workaround:

  1. Invoke: Call the agent via Invocations endpoint, collect responses
  2. Score: Submit query+response pairs to Foundry evaluators

Eval Data Format for Task Adherence

Task Adherence evaluates whether the agent followed its system instructions. Include the system message in conversation array format:

eval_item = {
    "query": [
        {"role": "system", "content": "Your system prompt here..."},
        {"role": "user", "content": [{"type": "text", "text": "User question"}]},
    ],
    "response": [
        {"role": "assistant", "content": [{"type": "text", "text": "Agent response"}]},
    ],
}

Eval Judge Model Sensitivity

Judge ModelTask AdherenceOther Evaluators
gpt-5.47% (overly strict)Stable
gpt-5.4-mini86% (accurate)Stable

Always use gpt-5.4-mini as the judge model for Task Adherence. gpt-5.4 penalizes responses for claiming tool usage that can't be verified from response-only data. All other evaluators (Coherence, Intent Resolution, Task Completion) are stable across both judge models.

Validated Scores (14-query benchmark)

EvaluatorScoreNotes
Coherence100%Identical to MAF
Intent Resolution100%Identical to MAF
Task Adherence86%Identical to MAF (gpt-5.4-mini judge)
Task Completion64%Identical to MAF
Tool Usage (URLs)100%All responses contain source URLs

azure.yaml (unified GA deployment)

The old two-file contract (agent.yaml + a separately hand-wired azure.yaml) is gone. A single azure.yaml at the project root is now the source of truth for both the Foundry project/model deployment and the hosted agent itself — declared as a graph of services, each with a host field and a uses list of dependencies. This is the same unified shape foundry-hosted-agents uses, adapted for the GHCP SDK's Invocations protocol instead of MAF's Responses protocol.

MUST: Copy verbatim from references/yaml/azure.yaml. Do NOT redefine inline — the validator enforces single-source-of-truth. That file is the canonical azure.ai.project + azure.ai.agent service pair for a container-built GHCP SDK hosted agent connecting to a pre-provisioned Foundry project.

# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json
name: my-agent-project

requiredVersions:
  extensions:
    azure.ai.agents: '>=1.0.0-beta.4'

services:
  ai-project:
    host: azure.ai.project
    # Point at an EXISTING project instead of provisioning a new one.
    endpoint: ${FOUNDRY_PROJECT_ENDPOINT}

  my-agent:
    host: azure.ai.agent
    project: .
    language: docker
    uses:
      - ai-project
    kind: hosted
    name: my-agent
    protocols:
      - protocol: invocations
        version: 2.0.0
    environmentVariables:
      - name: AZURE_AI_MODEL_DEPLOYMENT_NAME
        value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME}
    container:
      resources:
        cpu: "1"
        memory: 2Gi

infra:
  provider: microsoft.foundry

Critical rules

RuleWhy
kind: hosted on the azure.ai.agent serviceMarks it as a containerized/source-built hosted agent (as opposed to a Foundry Agent Service prompt agent)
Protocol invocations, version 2.0.0Current GA protocol + version for the GHCP SDK's InvocationAgentServerHost. Do NOT add a responses protocol entry — see § "Why Invocations Protocol (Not Responses)"
container.resources nested objectNOT a top-level resources: list
NO FOUNDRY_PROJECT_ENDPOINT in environmentVariablesReserved — the platform injects it automatically, along with FOUNDRY_PROJECT_ARM_ID, FOUNDRY_AGENT_NAME, FOUNDRY_AGENT_VERSION, FOUNDRY_AGENT_SESSION_ID, and APPLICATIONINSIGHTS_CONNECTION_STRING. Declaring any of them yourself is redundant at best and risks shadowing the platform value
uses: [ai-project] on the agent serviceForms the dependency graph azd resolves at provision/deploy time — the agent can't reference the project's model deployment without it
Connect to an existing project via endpoint:Set services.ai-project.endpoint from ${FOUNDRY_PROJECT_ENDPOINT} to reuse a pre-provisioned Foundry project instead of having azd provision a new one
infra.provider: microsoft.foundrySelects the Foundry extension's bicep-less provisioning provider for the unified services graph

Guided init vs. direct-copy brownfield deploy

The recommended guided path adopts the manifest and wires the existing project/registry context into the active azd environment:

PROJECT_ID="/subscriptions/<sub-id>/resourceGroups/<rg>/providers/Microsoft.CognitiveServices/accounts/<account>/projects/<project>"
azd ai agent init -m ./azure.yaml \
  --project-id "$PROJECT_ID" \
  --deploy-mode container

If you instead copy the canonical files into an existing repository and run azd deploy directly (direct-copy), azure.yaml alone is insufficient. The azure.ai.agents service target requires the full project ARM ID, project endpoint, subscription, and bare ACR login server in the active azd environment:

azd env set AZURE_SUBSCRIPTION_ID "<sub-id>"
azd env set FOUNDRY_PROJECT_ENDPOINT \
  "https://<account>.services.ai.azure.com/api/projects/<project>"
azd env set AZURE_AI_PROJECT_ID \
  "/subscriptions/<sub-id>/resourceGroups/<rg>/providers/Microsoft.CognitiveServices/accounts/<account>/projects/<project>"
azd env set AZURE_CONTAINER_REGISTRY_ENDPOINT "<registry>.azurecr.io"
azd env set AZURE_AI_MODEL_DEPLOYMENT_NAME "<model-deployment>"
azd deploy my-agent --no-prompt

AZURE_CONTAINER_REGISTRY_ENDPOINT is a bare hostname — no https:// scheme and no trailing slash. This direct-copy limitation is the adoption wiring covered by Azure/azure-dev #8981: azd ai agent init ... --deploy-mode container wires it automatically; plain azd deploy cannot infer the omitted brownfield values.

Installing the azd Foundry extensions

azd ext install microsoft.foundry

microsoft.foundry is a beta meta-package — it contributes no commands of its own. Installing it pulls in every individual Foundry extension: azure.ai.agents (azd ai agent, the one this skill uses), azure.ai.connections, azure.ai.inspector, azure.ai.projects, azure.ai.routines, azure.ai.skills, and azure.ai.toolboxes. It is not a rename or replacement of azure.ai.agents — the agent extension still owns the azd ai agent command group, the azure.ai.agent service host value, and the microsoft.foundry provisioning identifier azd uses internally when it synthesizes infrastructure. Installing just azure.ai.agents also pulls in azure.ai.inspector (a dependency), so it's a valid narrower alternative to the meta-package:

# Just the agent surface (also pulls in azure.ai.inspector)
azd ext install azure.ai.agents

# Upgrade later
azd ext upgrade microsoft.foundry

Verify what's installed:

azd ext list --output json

azd env variables for the azure.ai.agents extension

The guided azd ai agent init path is bicep-less by default — it doesn't write an infra/ directory; azd synthesizes infrastructure from the services graph in azure.yaml. Guided init writes the brownfield values below automatically. A direct-copy deploy must set them explicitly before azd deploy:

Env varSet viaFormatWhy it matters
AZURE_SUBSCRIPTION_IDazd env set or guided initsubscription GUIDSelects the subscription containing the existing project and registry
FOUNDRY_PROJECT_ENDPOINTazd env set or guided inithttps://<account>.services.ai.azure.com/api/projects/<project>Deploy-time project lookup for direct-copy brownfield deploy; also injected by Foundry into the running container, but MUST NOT be listed under environmentVariables
AZURE_AI_PROJECT_IDazd env set or guided init/subscriptions/<sub-id>/resourceGroups/<rg>/providers/Microsoft.CognitiveServices/accounts/<account>/projects/<project>Full ARM ID required by the Foundry project resolver; an endpoint alone is insufficient
AZURE_CONTAINER_REGISTRY_ENDPOINTazd env set or guided init<registry>.azurecr.ioBare ACR login server used for container build/push; no scheme or trailing slash
AZURE_AI_MODEL_DEPLOYMENT_NAMEazd ai agent init records it automatically from the deployment you select; change later with azd env setdeployment name (e.g. gpt-5.4-mini)Projected through the agent service's environmentVariables entry in azure.yaml, and read by container.py as os.environ["AZURE_AI_MODEL_DEPLOYMENT_NAME"]

Deploy-time azd env vs. runtime container env. FOUNDRY_PROJECT_ENDPOINT MUST be set in the active azd environment for direct-copy brownfield deploy, but MUST NOT be projected through the agent's environmentVariables list. Foundry injects it into the running container. The same container-only prohibition applies to FOUNDRY_PROJECT_ARM_ID, FOUNDRY_AGENT_NAME, FOUNDRY_AGENT_VERSION, FOUNDRY_AGENT_SESSION_ID, and APPLICATIONINSIGHTS_CONNECTION_STRING.


Identity & RBAC for hosted agents

azd ai agent creates two distinct managed identities you must not confuse:

  • Blueprint identity — a stable Entra identity bound to the agent definition (visible via azd ai agent show). It handles deploy-time wiring (ACR pulls, version registration). The blueprint identity needs no data-plane role grants for inference.
  • Instance identity — a per-version (per-instance) service principal created for the active agent version. This identity is what the running agent presents when it calls the model through the project endpoint. It is the identity that must hold model-inference permission.

The blueprint identity needs no role assignments; only the instance identity does.

Required: grant the instance identity Foundry User at BOTH scopes

Real model inference through a hosted agent requires the instance identity to hold the Foundry User role (GUID 53ca6127-db72-4b80-b1b0-d745d6d5456d) at both of these scopes before the first invocation:

RoleAssigneeScopeWhy
Foundry User (53ca6127-db72-4b80-b1b0-d745d6d5456d)Instance identityFoundry accountModel gateway authorizes responses/write at account scope
Foundry User (53ca6127-db72-4b80-b1b0-d745d6d5456d)Instance identityFoundry projectData-plane project authorization for the agent's calls

Controlled Azure testing proved this is the minimum working contract: project-scope-only failed with 401, account-scope-only failed with 401, and both scopes together succeeded. Grant one without the other and real inference returns PermissionDenied / Microsoft.CognitiveServices/accounts/OpenAI/responses/write.

Why the earlier "no grant" contract was wrong. A prior build of this skill claimed the Foundry service granted the agent identity its permissions internally with no client-side assignment. That was validated only against an upstream echo agent that never called a real model, so the missing inference permission never surfaced. On a live model deployment it 401s deterministically. This is a persistent authorization gap, not a transient — do not paper over it in a retry classifier.

Scope construction (no hardcoded IDs)

AZURE_AI_PROJECT_ID is the full project ARM resource ID:

/subscriptions/<sub>/resourceGroups/<rg>/providers/Microsoft.CognitiveServices/accounts/<account>/projects/<project>

Derive both scopes from it with pure string manipulation:

project_scope="$AZURE_AI_PROJECT_ID"                 # full project ARM id
account_scope="${AZURE_AI_PROJECT_ID%/projects/*}"   # strip /projects/<name>

Grant the instance identity at both scopes, capturing each assignment's ARM id so teardown can revoke exactly what it created (never a standing grant):

account_assignment_id=$(az role assignment create \
  --role 53ca6127-db72-4b80-b1b0-d745d6d5456d \
  --assignee-object-id "$instance_principal_id" \
  --assignee-principal-type ServicePrincipal \
  --scope "$account_scope" \
  --query id -o tsv)
project_assignment_id=$(az role assignment create \
  --role 53ca6127-db72-4b80-b1b0-d745d6d5456d \
  --assignee-object-id "$instance_principal_id" \
  --assignee-principal-type ServicePrincipal \
  --scope "$project_scope" \
  --query id -o tsv)

If a grant may already exist, resolve its id idempotently first and only create (and only later revoke) the ones you actually add:

account_assignment_id=$(az role assignment list \
  --assignee "$instance_principal_id" \
  --role 53ca6127-db72-4b80-b1b0-d745d6d5456d \
  --scope "$account_scope" --query "[0].id" -o tsv)
# empty -> create it (owned by you); non-empty -> pre-existing, do not revoke

Discovering the instance identity (deterministic)

azd ai agent show --output json exposes both managed identities by explicit field path — do not scan the object heuristically:

azd ai agent show --output json
# .instance_identity.principal_id  -> the runtime per-version INSTANCE MI (grant this)
# .blueprint.principal_id          -> the blueprint identity (never grant)
# fallbacks: .versions.latest.instance_identity.principal_id
#            .versions.latest.blueprint.principal_id
INSTANCE_PID=$(azd ai agent show --output json | jq -er '.instance_identity.principal_id')
BLUEPRINT_PID=$(azd ai agent show --output json | jq -er '.blueprint.principal_id')

Discover and log both so the instance-vs-blueprint distinction is explicit, then grant only $INSTANCE_PID. The instance identity is populated shortly after the version goes active; if it is briefly empty, retry azd ai agent show up to twice with a short wait. $instance_principal_id above is this instance_identity.principal_id — never the blueprint.principal_id.

Least-privilege note. The grantor needs Microsoft.Authorization/roleAssignments/write scoped to grant only this one role GUID at the account and its descendants. Nothing broader is required.

Full-event-stream handling after granting

Role assignments take time to propagate. A valid dual-scope invocation may still emit an initial 401 (model.call_failure / transient_auth_error) before an internal retry succeeds within the same stream. Therefore, inspect the entire SSE event stream — never abort on the first transient event. Accept recovered assistant output (assistant.message / assistant.message_delta) even after an earlier 401 envelope; only treat the invocation as failed if the complete stream ends with no assistant output. The exact immediate-post-active readiness envelope and bounded backoff are documented under § "Invoking the Agent".

Cleanup / revocation

The two Foundry User grants are the only assignments you add. When tearing down (or after a one-shot validation), revoke strictly by the assignment id you captured at create time — deleting by --assignee/--role/--scope can match and remove a pre-existing standing grant you did not create:

az role assignment delete --ids "$account_assignment_id"
az role assignment delete --ids "$project_assignment_id"

Revocation is best-effort during teardown — a failed delete must not fail an otherwise-successful run (the instance identity is deleted with the agent version anyway). Never revoke an id you resolved as pre-existing in the idempotent-create step above (az role assignment list --assignee ... --role ... --scope ... --query "[0].id" returned non-empty before you created anything) — you did not create it, so it is not yours to delete.

Deploying user

The deploying user still needs a role to create/update the agent:

RoleScopeWhy
Foundry Project ManagerFoundry projectData-plane permissions to create/update agents

azd handles the ACR pull role (Container Registry Repository Reader on the project's managed identity) automatically as part of azd deploy.


Troubleshooting

ProblemCauseFix
401 on BYOKWrong scope (cognitiveservices.azure.com)Use https://ai.azure.com/.default
send_and_wait() timeout60s default + 120s gateway limitUse Invocations protocol instead of ResponsesHostServer
SSE response 0 charsParser only captures assistant.messageAlso capture assistant.message_delta as fallback
Task Adherence eval 0%Using gpt-5.4 as judge modelUse gpt-5.4-mini as judge
Agent feels much slower than expectedCopilotClient internal-turn overhead (20-34 internal calls per query)Use MAF runtime instead — see ## ⚠️ Performance Caveat: Per-Query Overhead
CopilotClient needs GITHUB_TOKENNot using BYOK providerPass provider dict to create_session()
system_message ignoredPassed as stringMust be dict: {"mode": "replace", "content": "..."}
Skills not loadingWrong path formatUse absolute paths in skill_directories list
Agent crashes on startupMissing azure-ai-agentserver-invocationsAdd to dependencies, use prerelease = "if-necessary-or-explicit"
Gateway timeout (502/504)Using Responses protocol for long queriesSwitch to Invocations protocol
Token expired during long queryBYOK token static per sessionToken has ~1h validity; for longer sessions, create new session
Token expired during eval runsLong eval runs (many scenarios) exhaust the BYOK tokenRefresh the provider token per invocation: call _get_provider() fresh before each _ensure_session(), or create a new session every ~30 min
Container can't resolve packagespip doesn't handle pre-release depsUse uv with pre-release settings in [tool.uv]
Missing [tool.setuptools] packages = []uv resolution fails without itAdd to pyproject.toml (see references/pyproject.toml)
CognitiveServices API version wrongUsing old 2024-10-01Use 2025-10-01-preview for agent management APIs
Hooks fail on Windowsshell: sh in a custom azd hookUse shell: pwsh for cross-platform
401 on real model inferenceInstance identity lacks Foundry User at the required scopesGrant Foundry User (53ca6127-db72-4b80-b1b0-d745d6d5456d) to the instance identity at BOTH the Foundry account scope AND the project scope before invoking; project-only and account-only both 401. See § "Identity & RBAC for hosted agents"
Immediate post-grant SSE model.call_failure 401 / transient_auth_errorInstance grants are correct but role-assignment propagation has not completedInspect the full event stream and retry the same JSON positional azd ai agent invoke path with bounded 15-second backoff (max six); accept recovered assistant output later in the stream. A 401 that never recovers after both grants is a hard FAIL
"responses protocol not declared" (bot 400)azure.yaml's agent service only declares invocations but bot/CLI calls via Responses API (oai.responses.create())Dual protocols don't work — InvocationAgentServerHost only serves /invocations; the /responses path returns 404 even if a second protocol entry is declared. Fix: Rewrite the bot to POST directly to the Invocations SSE endpoint (or use azd ai agent invoke --protocol invocations) and parse assistant.message + assistant.message_delta events. Alternative: Use MAF runtime (ResponsesHostServer) which natively serves responses.
ACR push 403 / RBAC errorDeploying user lacks AcrPush on the target ACRAssign AcrPush on the ACR, or use the guided azd ai agent init --deploy-mode container path, which wires the registry automatically (Azure/azure-dev #8981)
Evals show no telemetryAppInsights not connected to Foundry accountCreate AppInsights connection on the account (not project). Category: AppInsights, target: ARM resource ID, metadata: ApiType: Azure. APPLICATIONINSIGHTS_CONNECTION_STRING is reserved — platform injects it.
Agent traces missingAgent identity lacks telemetry RBACAssign Monitoring Metrics Publisher on AppInsights to the agent identity (from azd ai agent show). Project MI needs Log Analytics Data Reader on Log Analytics workspace.
gpt-4.1 encrypted content errorgpt-4.1 deprecated, doesn't support encrypted content required by GHCP SDKDefault to gpt-5.4-mini or gpt-5.4. Update AZURE_AI_MODEL_DEPLOYMENT_NAME in azure.yaml / azd env.
azd deploy says "deployed in <1s" and nothing shipsCopied a stripped-down azure.yaml missing the services: blockUse the canonical references/yaml/azure.yaml verbatim — see § "azure.yaml (unified GA deployment)"

Architecture Comparison

MAF (Responses Protocol):
  container.py → Agent + FoundryChatClient + ResponsesHostServer → port 8088
    └── Request/response: caller waits for full response
    └── Timeout: ~120s (Foundry gateway)

GHCP SDK (Invocations Protocol):
  container.py → CopilotClient + InvocationAgentServerHost → port 8088
    └── SSE streaming: events flow continuously
    └── Timeout: unlimited (connection kept alive by events)

Decision Matrix

FactorChoose MAFChoose GHCP SDK
Tool loop duration<120s>120s
Custom @tool functions✅ Required❌ Not supported
Foundry Toolbox✅ MCPStreamableHTTPTool (was get_toolbox(), removed 1.3.0)❌ Not available
Streaming output❌ Response only✅ Event stream
Progressive skills✅ SkillsProvider.from_paths() (or legacy concat)✅ SkillsProvider
MCP toolsclient.get_mcp_tool()mcp_servers parameter
Auth complexityLow (DAC → FoundryChatClient)Medium (BYOK token minting)
PackagesStable releasesPre-release beta
Deploy shapeUnified azure.yaml (foundry-hosted-agents)Unified azure.yaml (this skill) — same GA shape, different protocol/runtime

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.