Migrate azure.ai.agents fail-fast validations into azd local preflight ("azd validate") — tracking issue
- Dominant language
- Go
- Stars
- 569
- Forks
- 364
- Avg merge
- 2d 19h
- Merged PRs (30d)
- 136
Description
## Summary
This is a **parent / tracking issue** to move fail-fast validations that today live in the `azure.ai.agents` extension (and additional gates the AI Toolkit / Foundry Toolkit for VS Code performs but azd does not) into azd's **local preflight** ("azd validate") so common misconfigurations are **spot-checked fast, before provisioning**, instead of failing minutes later during `azd deploy`.
Each row in the tables below should become its **own sub-issue linked to this one**, with a focused PR. Any agent/contributor picking up an item should read the "Context" section first, then implement + link the sub-issue here.
---
---
## ⭐ Priority 0 — Start here: top production provision errors
An analysis of hosted-agent deploy/provision failures over a 30-day window ranked the top real-world ARM errors. **None of the three existing preflight checks (`role_assignment_permissions`, `ai_model_quota`, `reserved_resource_names`) detect them.** The top 5 messages collapse into **2 error classes**, both on `cmd.provision` and both **core azd (not agent-specific)** — so they have **no extension/process-boundary concern** and are the easiest, highest-value checks to add directly to `validatePreflight`. **Do these first.**
| Priority | New check (RuleID) | Error class it catches | Prod count | Severity | Feasibility |
|---|---|---|---|---|---|
| **P0-1** | `resource_group_location_mismatch` | `InvalidResourceGroupLocation` — requested `AZURE_LOCATION` ≠ location of the **already-existing** resource group (`Invalid resource group location 'eastus2'. The Resource group already exists in location 'eastus'.`) | **87** (rows 1,3,4,5) | **Error** | **Trivial** — azd **already fetches** the existing RG location in preflight via `resolveResourceGroupLocation` (`bicep_provider.go:3184`); today it's only used for the snapshot and never compared to the requested location. Add the comparison + abort with guidance ("re-use the existing location, or delete/rename the resource group"). |
| **P0-2** | `role_assignment_redeploy_immutable` | `RoleAssignmentUpdateNotPermitted` — `Tenant ID, application ID, principal ID, and scope are not allowed to be updated.` A redeploy mutates an immutable role assignment (non-deterministic GUID name). | **30** (row 2) | **Warning** | **Medium/heuristic** — inspect the ARM snapshot for `Microsoft.Authorization/roleAssignments` whose `name` isn't a deterministic `guid(scope, principalId, roleId)`; warn that redeploys will fail and suggest making assignments idempotent (`ifNotExists` / deterministic `guid()`). The existing `role_assignment_permissions` check already parses these resources from the snapshot and can be extended. |
> These sit **above** Tables A–C: they are core provision errors (not routed through the extension), they are the highest-frequency user-fixable failures observed, and P0-1's required data is already resolved in the current preflight code.
## Context & investigation notes
### How azd local preflight ("validate") works today
- Framework: `cli/azd/pkg/infra/provisioning/bicep/local_preflight.go` + `bicep_provider.go` (`validatePreflight`, ~L2562-2694); UX via `cli/azd/pkg/output/ux/preflight_report.go`.
- A check is `PreflightCheck{RuleID, Fn}` where `Fn(ctx, *validationContext) ([]PreflightCheckResult, error)`. Register via `AddCheck`.
- Each result has **`Severity` (`PreflightCheckWarning` | `PreflightCheckError`)**, `DiagnosticID`, `Message`, `Suggestion`, `Links`.
- Flow: run checks → render `ux.PreflightReport` (warnings first, then errors) → **any Error ⇒ abort (exit 0, not a failure)**; **only Warnings ⇒ `Confirm "Proceed with deployment despite the warnings above?"` (default yes); "no" ⇒ intentional abort** → then ARM `ValidatePreflight`.
- Gated by `provision.preflight: off` user config. Telemetry: outcome / rule IDs / diagnostic IDs / warn+error counts.
- Existing checks: `role_assignment_permissions`, `ai_model_quota`, `reserved_resource_names`.
This model is exactly the desired UX: **push hard errors into warnings-with-suggestions + an abort option**, keeping `Error` only where proceeding is futile.
### Deciding rule for what can move to **pre-provision** validate today
A validation is authoritative at pre-provision time **iff its subject already exists and is not created/set by the provision itself**:
- ✅ **Local/static inputs** — `azure.yaml`, the agent manifest (`agent.yaml`), the source dir, and provision *inputs* like `AZURE_LOCATION`. These exist before provision → ideal "spot-fast" wins (today they only fail at `azd deploy`).
- ❌ **Azure state that provision creates/sets** — the Foundry project, ACR, `FOUNDRY_PROJECT_ENDPOINT`, `AZURE_AI_PROJECT_ID`, deployed agents/versions. At pre-provision these don't exist yet → need a **post-provision / deploy-phase preflight hook** (larger platform work; see "Deferred").
### Already validated by `azd ai init` — do NOT move
`azd ai init` runs **before** `azd up` / `azd provision`, so validations it already performs are effectively "left of provision" and are caught fast today. **These should stay in `init` and are intentionally excluded from the migration tables:**
| # | Check | Current location (init) | Why it stays in init |
|---|---|---|---|
| A9 | deploy-mode / runtime / dep-resolution enums | `internal/cmd/init.go:3615-3643` | Flag/config validated at init, before provision |
| A10 | code deploy needs runtime + entryPoint (`--no-prompt`) | `internal/cmd/init.go:3644-3659` | Init-time input validation |
| A17 | .NET TFM vs selected runtime mismatch | `internal/cmd/init_validate.go:31-109` | Init-time advisory |
| B9 | unsupported Python ZIP runtime | covered by the init runtime enum (`init.go`) | Runtime already gated at init |
| B11 | Foundry `protocols` block validity | `internal/cmd/init_from_code.go:1231-1327` | Protocols validated at init |
> Note: these run only when `azd ai init` is used. Hand-authored `azure.yaml`/`agent.yaml` (no init) would bypass them — acceptable under the assumption that init precedes provision. If we later want to also guard the no-init path, revisit these as pre-provision checks.
### Two caveats every sub-issue must respect
1. **Bicep-only today.** `validatePreflight` runs only in the Bicep provider. Agents on Terraform, or agents added to an already-provisioned env, won't hit this gate — those still need deploy-time coverage.
2. **Process boundary / ownership.** Agent checks live in the `azure.ai.agents` **extension**; the Bicep preflight runs in **core azd**. "No framework change" still means core azd must evaluate the local/manifest logic itself. Cleanest fit: a small **agent-manifest preflight check** registered in core that **no-ops when there are no `azure.ai.agent` services**, either sharing a manifest-parsing lib or extracting one. Avoid leaking target-specific logic per `cli/azd/AGENTS.md` ("core stays target-agnostic").
### Assumptions
- Preflight runs as part of provision (before provision), and users run `azd up` **or** `azd provision` then `azd deploy`. So anything validated pre-provision (or already at `azd ai init`) need not be re-checked at deploy.
---
## Table A — Local/static checks that CAN move into pre-provision `validate` now
Deploy-time (or toolkit-only) validations **not** already covered by `azd ai init`. For each: reimplement (or share) the check in core azd as a `PreflightCheckFn`, no-op when no `azure.ai.agent` service exists. Recommended severity given.
| # | Check | Current location | Recommended severity |
|---|---|---|---|
| A6 | `azure.yaml` present/parseable | `internal/cmd/doctor/checks_local.go:281-338`; `internal/pkg/agents/agent_yaml/parse.go:18-30` | Error |
| A15 | Manifest schema: kind, name (1-63), rai_policy, params (also re-read at deploy) | `internal/pkg/agents/agent_yaml/parse.go`, `parameters.go` | Error |
| A11 | bundled Python deps present in source (`.dist-info`) | `internal/project/service_target_agent.go:1680-1756` | Warning |
| A12 | bundled .NET has `.csproj` | `internal/project/service_target_agent.go:1555-1577` | Error |
| B7 | multiple `.csproj`, no explicit .NET entry point *(toolkit-only)* | toolkit `zipPackageHelper.ts:240-245` | Error |
| B8 | entry-point ↔ `.csproj` assembly mismatch *(toolkit-only)* | toolkit `zipPackageHelper.ts:228-238` | Warning |
| B10 | entry-point command syntax (unterminated quotes) *(toolkit-only)* | toolkit `hostedAgentDeploySelectionService.ts:1514-1576` | Error |
| B6 | prebuilt image ref format | partial in `service_target_agent.go:955-961`; toolkit `:1860-1874` | Error |
| B5 | ACR **login-server format** (not existence) *(toolkit-only)* | toolkit `constants.ts:36-37` | Error |
| A16 | service-path / path-traversal safety | `internal/project/service_target_agent.go:196-307` | Error |
| A13 | source-dir size approaching 250 MB (early approx of final zip) | `internal/project/service_target_agent.go:1536-1547,1665-1672` | Warning |
| A14 | `AZURE_LOCATION` set (low value; provision already needs it) | `internal/project/service_target_agent.go:1770-1778` | Error |
| B12 | `$ref` circular / bad ref in `azure.yaml` *(toolkit-only; only if core parses $ref)* | toolkit `hostedAgentProjectConfig.ts:321-361` | Error |
## Table B — CONDITIONAL (pre-provision only in the BYO case)
| # | Check | Condition | Severity |
|---|---|---|---|
| B1 | BYO ACR is network-restricted (public access Disabled / defaultAction Deny) *(toolkit-only)* | Only when ACR is a **pre-existing BYO** registry (one ARM GET). No-op when provision creates the ACR. | Warning (allow BYOI override) |
## Table C — DEFERRED: need a post-provision / deploy-phase preflight hook (NOT addable to today's pre-provision validate)
These depend on Azure state created/set by provision. Track here but they require the larger platform hook (generalize preflight into a phase-agnostic report+confirm + a gRPC surface for extensions/service-targets to contribute results).
| # | Check | Blocker | Toolkit ref / azd ref |
|---|---|---|---|
| A1/A2 | dev role / `roleAssignments/write` on Foundry project | project often created by this provision | azd `developer_rbac_check.go:171-232` |
| A3 / A19 / A20 | ACR push role / firewall / auth | ACR often created by provision; reactive-after-push today | azd `service_target_agent.go:606-638,781-822` |
| A7 | `FOUNDRY_PROJECT_ENDPOINT` set/valid | written post-provision | azd `checks_local.go:99-155` |
| A8 | `AZURE_AI_PROJECT_ID` valid | written post-provision | azd `agent_identity_rbac.go:129-158` |
| A18 | existing agent name → new-version notice | needs deployed Foundry project | azd `init.go:649-766` |
| B2 | deployment-type incompatibility (ACA vs ADC) *(toolkit-only)* | needs deployed project + existing agent | toolkit `hostedAgentDeploySelectionService.ts:2088-2151` |
| B4 | ACR creation blocked by subscription policy *(toolkit-only)* | concerns the provision itself; ARM reports it | toolkit `acrOperationHelper.ts:165-178` |
---
## Suggested first step
Prototype a single **no-op-safe "agent manifest preflight" check** registered in core azd that bundles the manifest/source group (A15, A12, B7, B10) and returns `PreflightCheckResult`s. Use it to establish the shared manifest-parsing seam, then peel off remaining Table A rows as sub-issues.
## How to use this issue
- [ ] Create a sub-issue per row (or per logical group) in Tables A/B, link it here.
- [ ] Each sub-issue: implement the `PreflightCheckFn`, wire severity/Suggestion/Links, add table-driven tests, honor `--no-prompt` (Warnings auto-proceed, Errors abort).
- [ ] Table C items: gate behind the deploy-phase preflight hook epic (separate design).
_Notes compiled from an investigation comparing `cli/azd/extensions/azure.ai.agents` against the Foundry Toolkit for VS Code (microsoft/skylight, `vscode/extension/ai-foundry`) hosted-agent flows._
Contributor guide
Assessment
This issue has not been assessed yet.