Skip to content

Enable provider-agnostic client-side validation ('azd validate') for extension-provided provisioning providers #9016

Description

@vhvb1989

Summary

Extensions that ship their own provisioning provider (e.g. azure.ai.agents via microsoft.foundry, microsoft.azd.demo via demo) cannot participate in azd's client-side validation ("provision validation", formerly local-preflight). The validation dispatch is hard-wired to the Bicep provider only, so extension-registered checks register successfully but never run when provisioning goes through a non-Bicep provider.

This blocks PR #9007 (adds a resource-group location-mismatch check to azure.ai.agents) from working at all, and blocks the broader goal of centralizing warnings/suggestions in azd core instead of each extension inventing its own error/warning output.

Related:


Background: how validation works today

Core already has a full validation framework (internal/grpcserver/validation_service.go, pkg/infra/provisioning/validation_dispatcher.go):

  • Extensions declare the validation-provider capability and register checks via WithValidationCheck (gRPC RegisterValidationCheck).
  • Checks return structured ValidationCheckResult: severity (WARNING/ERROR), message, suggestion, diagnosticId, doc links.
  • Core dispatches them in parallel (ValidationService.DispatchChecks, wg.Go, grouped per extension, timeout-bounded).
  • Results render through a uniform report and use the (abort, err) pattern: ERROR cancels provision (exit 0), WARNING continues.

The blocking problem (root cause)

DispatchChecks is called from exactly one place in all of cli/azd:

  • pkg/infra/provisioning/bicep/bicep_provider.go:~2615 inside (*BicepProvider).validatePreflight.

Every other provisioning provider bypasses it:

  • ExternalProvisioningProvider.Deploy/Preview (internal/grpcserver/external_provisioning_provider.go) just forward over gRPC — no dispatch.
  • So extension providers (microsoft.foundry, demo) and the core Terraform provider never trigger any validation checks.

Empirically confirmed (both gaps):

  • Gap A (capability): azure.ai.agents/extension.yaml was missing the validation-provider capability, so registration was rejected with PermissionDenied, which crashed extension startup (WARNING: 1 extension did not start) and made azd provision fail with an unrelated transport is closing error.
  • Gap B (dispatch): Even after adding the capability so registration succeeds, the check still never runsazure.ai.agents provisions via microsoft.foundry, which never calls DispatchChecks. Provisioning went straight to the Foundry ARM deploy and failed with the exact InvalidResourceGroupLocation error the check was meant to prevent. The microsoft.azd.demo example reproduces the same: its demo_warning check registers but never fires when provisioning via the demo provider.

Why this matters for azure.ai.agents specifically: every agents init flow stamps infra.provider: microsoft.foundry (init.go, init_from_code.go, init_adopt.go). --infra/--infra=bicep eject leaves azure.yaml unchanged (foundry compiles on-disk Bicep itself); --infra=terraform stamps the core Terraform provider. None of these routes through the core Bicep provider, so a registered check is dead code.


Options considered

Option A — New provider-agnostic validate check type + core trigger before provision (RECOMMENDED)

Introduce a new validation checkType (e.g. "provision" / "pre-provision") with a lean, provider-agnostic context (env name, subscription, location, resource group, target scope — no ARM template, since foundry/terraform have none). Dispatch it from a provider-agnostic point, immediately before the provider runs:

  • Natural insertion site: provisioning.Manager.Deploy (pkg/infra/provisioning/manager.go:~99), right before m.provider.Deploy(ctx) — and the equivalent point in Manager.Preview.
  • This makes Bicep, Terraform, and every external/extension provider (foundry, demo) trigger the same checks identically.

Pros

Cons

  • New core plumbing: new checkType constant, new provider-agnostic context builder, new dispatch site in Manager.Deploy/Preview, plus telemetry + a config gate mirroring validation.provision.
  • Two dispatch sites coexist temporarily (Bicep's ARM-rich local-preflight context + the new lean provider-agnostic one) until unified later.

Option B — Enhance lifecycle hooks/events to emit validate-layer warnings/errors (not recommended)

Change the hook/event contract so a preprovision handler can return structured warnings/suggestions and feed them into the same report.

Pros: reuses existing subscription/registration; already provider-agnostic; fewer new user-facing concepts.

Cons (why it loses):

  • Semantic mismatch: hooks/events are side-effecting actions ("run this script"), not pure validators. Today EventDispatcher.RaiseEvent handlers return error only (a suggestion is possible solely via ErrorWithSuggestion, and only on the abort path — no "warn and continue" channel).
  • Retrofitting a WARN/continue channel changes the contract for both gRPC event handlers and azure.yaml script hooks (a script's stdout/exit code has no severity/suggestion/links shape).
  • Events run sequentially (event_dispatcher.go for loop, with a TODO to parallelize) vs. validation's parallel dispatch — would need a parallelism/ordering rework.
  • Mixing side effects into a "validation phase" breaks the guarantee that validation is read-only, and muddies both abstractions.

Recommendation

Implement Option A. It reuses the existing structured-result + uniform-report + parallel-dispatch + severity/abort machinery, keeps validation pure and read-only, works across all providers, and aligns with the validation.provision generalization already in flight (PR #8844).

Design nuance to decide early: either

  • (a) introduce a new checkType with a lean context now and converge with Bicep's local-preflight later (preferred — doesn't destabilize shipping Bicep checks), or
  • (b) hoist the existing local-preflight/validation.provision dispatch out of BicepProvider into Manager.Deploy and make ARM-specific context fields optional (avoids two sites, but forces every existing Bicep check to tolerate a missing ARM template).

Recommend (a).


Scope of core work (for the implementing session)

  1. New checkType + context

    • Add a checkType constant (e.g. "provision" / "pre-provision").
    • Define a provider-agnostic context schema + builder (env, subscription, location, resource group, target scope). Add new context key constants in pkg/azdext/validation_provider.go alongside the existing ValidationContext* keys, and a typed accessor on ValidationContext.
  2. Provider-agnostic dispatch site

    • Call ValidationCheckDispatcher.DispatchChecks(ctx, "<new-type>", ctxData) in provisioning.Manager.Deploy (before m.provider.Deploy) and Manager.Preview — resolving the dispatcher from IoC (already wired at cmd/container.go:~1031).
    • Reuse the Bicep site's severity mapping + (abort, err) handling + timeout bound (extensionValidationTimeout) + uniform report rendering (output/ux preflight/provision-validation report). Extract shared logic rather than copy-pasting from bicep_provider.go.
  3. Config gate + telemetry

    • Mirror the validation.provision gate from PR Rename client-side "preflight" to "provision validation" (#7113) #8844 (respect the same skip semantics / naming).
    • Emit the same validation telemetry (rule IDs invoked, warning/error counts, outcome). Follow the telemetry-doc sync rules in cli/azd/AGENTS.md (schema, matrix, privacy checklist, coverage test). Hash user-derived values.
  4. Docs

  5. Tests

    • Unit test the new dispatch site (provider-agnostic, fires for a non-Bicep provider).
    • Ideally an end-to-end style test using the microsoft.azd.demo extension's demo provider proving the check now fires (today it registers but never runs).

Coordination: align with / sequence after PR #8844 (rename local-preflightvalidation.provision) to avoid churn and reuse its config-gate + report + telemetry naming.


Key references (file:line)

  • Only dispatch site (Bicep-only): pkg/infra/provisioning/bicep/bicep_provider.go:~2615 (validatePreflight, DispatchChecks("local-preflight", …)).
  • Provider-agnostic wrapper (insertion point): pkg/infra/provisioning/manager.go:~99 (Manager.Deploym.provider.Deploy), :~274 (Manager.Preview).
  • External provider bridge (no dispatch): internal/grpcserver/external_provisioning_provider.go (Deploy/Preview).
  • Dispatcher + parallel execution: internal/grpcserver/validation_service.go:~214 (DispatchChecks), capability gate :~79.
  • Dispatcher interface: pkg/infra/provisioning/validation_dispatcher.go.
  • IoC wiring: cmd/container.go:~1031 (ValidationService as ValidationCheckDispatcher).
  • Extension SDK types + context keys: pkg/azdext/validation_provider.go.
  • Event/hook model (Option B reference): pkg/ext/event_dispatcher.go:~101 (RaiseEvent, sequential), :~167 (Invoke, pre/post).
  • The check to migrate once core lands: extensions/azure.ai.agents/internal/project/resource_group_location_check.go + registration in internal/cmd/listen.go.
  • Example provider that reproduces Gap B: extensions/microsoft.azd.demo/ (demo provider + demo_validation_check.go).

Acceptance criteria

  • A validation check registered by an extension with the new checkType runs before provision regardless of the provisioning provider (Bicep, Terraform, and external/extension providers such as microsoft.foundry and demo).
  • WARNING results are surfaced via the uniform report and provisioning continues; ERROR results cancel provisioning with the standard (abort, err) behavior.
  • The microsoft.azd.demo demo_warning check fires when provisioning via the demo provider (regression coverage).
  • Telemetry, config gate, and docs updated in sync per cli/azd/AGENTS.md.

Follow-up (tracked separately, this repo/session)

Once this lands and merges, update PR #9007 to: add the validation-provider capability to azure.ai.agents/extension.yaml, re-register ResourceGroupLocationCheck under the new checkType, and use the provider-agnostic context (resource group + location) instead of the ARM-template-centric local-preflight context.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions