feat(agents): support private non-ACR registry connections - #9586
feat(agents): support private non-ACR registry connections#9586Wei Meng (m5i-work) wants to merge 12 commits into
Conversation
|
Azure Pipelines: Successfully started running 1 pipeline(s). 6 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
d59590b to
5fab3c1
Compare
|
Azure Pipelines: Successfully started running 1 pipeline(s). 6 pipeline(s) were filtered out due to trigger conditions. There may be pipelines that require an authorized user to comment /azp run to run. |
There was a problem hiding this comment.
Pull request overview
Adds registry-neutral private container registry support for hosted Foundry agents, backed by core image passthrough.
Changes:
- Adds
docker.imagePassthroughacross core lifecycle, schemas, protobuf, and targets. - Adds registry connection authoring, validation, REST mapping, and dependency checks.
- Updates init flows, documentation, and tests.
Reviewed changes
Copilot reviewed 47 out of 47 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
schemas/v1.0/azure.yaml.json |
Adds passthrough schema. |
schemas/alpha/azure.yaml.json |
Adds alpha passthrough schema. |
docs/reference/azure-yaml-schema.md |
Documents Docker options. |
cli/azd/pkg/project/service_target_containerapp.go |
Routes passthrough publishing. |
cli/azd/pkg/project/service_target_containerapp_test.go |
Tests Container Apps passthrough. |
cli/azd/pkg/project/service_target_appservice.go |
Routes App Service passthrough. |
cli/azd/pkg/project/service_target_appservice_test.go |
Tests App Service passthrough. |
cli/azd/pkg/project/service_target_aks.go |
Supports AKS passthrough. |
cli/azd/pkg/project/mapper_registry.go |
Maps the new Docker field. |
cli/azd/pkg/project/mapper_registry_test.go |
Tests protobuf mappings. |
cli/azd/pkg/project/framework_service_docker.go |
Defines ImagePassthrough. |
cli/azd/pkg/project/framework_service_docker_test.go |
Tests Docker lifecycle behavior. |
cli/azd/pkg/project/container_helper.go |
Implements passthrough artifacts. |
cli/azd/pkg/project/container_helper_test.go |
Tests passthrough validation. |
cli/azd/pkg/azdext/models.pb.go |
Regenerates protobuf models. |
cli/azd/grpc/proto/models.proto |
Adds the wire field. |
cli/azd/extensions/azure.ai.agents/schemas/azure.ai.agent.json |
Adds registry connection schema. |
cli/azd/extensions/azure.ai.agents/internal/synthesis/synthesizer_test.go |
Tests arbitrary credential keys. |
cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go |
Integrates connection deployment. |
cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent_test.go |
Tests agent lifecycle integration. |
cli/azd/extensions/azure.ai.agents/internal/project/image_passthrough.go |
Bridges the new protobuf field. |
cli/azd/extensions/azure.ai.agents/internal/project/foundry_dependencies.go |
Validates sibling connections. |
cli/azd/extensions/azure.ai.agents/internal/project/foundry_dependencies_test.go |
Tests dependency validation. |
cli/azd/extensions/azure.ai.agents/internal/project/doc_examples_test.go |
Extends schema example checks. |
cli/azd/extensions/azure.ai.agents/internal/project/agent_definition.go |
Preserves connection authoring. |
cli/azd/extensions/azure.ai.agents/internal/project/agent_definition_test.go |
Tests definition round trips. |
cli/azd/extensions/azure.ai.agents/internal/pkg/containerref/reference.go |
Validates image references. |
cli/azd/extensions/azure.ai.agents/internal/pkg/containerref/reference_test.go |
Tests reference validation. |
cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/yaml.go |
Adds the YAML property. |
cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/parse_test.go |
Tests YAML round trips. |
cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map.go |
Maps to the REST request. |
cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map_test.go |
Tests request mapping. |
cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/models.go |
Adds the REST field. |
cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/models_test.go |
Tests JSON placement. |
cli/azd/extensions/azure.ai.agents/internal/cmd/init.go |
Adds registry init support. |
cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go |
Tests init validation and generation. |
cli/azd/extensions/azure.ai.agents/internal/cmd/init_reuse_project_agent_test.go |
Covers reuse flag detection. |
cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go |
Selects passthrough for images. |
cli/azd/extensions/azure.ai.agents/internal/cmd/init_foundry_resources_helpers.go |
Verifies existing connections. |
cli/azd/extensions/azure.ai.agents/internal/cmd/init_foundry_resources_helpers_test.go |
Tests connection lookup. |
cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go |
Updates adopted image lifecycles. |
cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt_deploymode_test.go |
Tests adopted deploy modes. |
cli/azd/extensions/azure.ai.agents/internal/cmd/hosted_container_config.go |
Centralizes lifecycle selection. |
cli/azd/extensions/azure.ai.agents/internal/cmd/hosted_container_config_test.go |
Tests lifecycle selection. |
cli/azd/extensions/azure.ai.agents/internal/cmd/helpers.go |
Clarifies the legacy ACR signal. |
cli/azd/extensions/azure.ai.agents/docs/private-networking.md |
Updates private ACR examples. |
cli/azd/docs/environment-variables.md |
Updates skip-ACR documentation. |
Suppressed comments (2)
cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go:1866
- [azd-code-reviewer] This is not the narrow legacy-shape check described in the PR: core's ServiceConfig-to-proto mapper always converts the value-typed Docker options into a non-nil pointer (
cli/azd/pkg/project/mapper_registry.go:129-132), even whendockerwas absent. Consequently any hand-authored image can still be forced onto the pre-built path whenever the environment-levelAZD_AGENT_SKIP_ACRis true. Preserve actual docker-property presence (or another reliable legacy marker) before using this compatibility branch.
if p.serviceConfig.GetDocker() != nil &&
!DockerImagePassthrough(p.serviceConfig.GetDocker()) &&
p.shouldSkipACRForEnvironment(ctx) {
cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go:1632
- [azd-code-reviewer] This image check runs before the existing-config check below, so a service with authoritative
codeConfigurationplus a leftover top-level image is silently switched back to container deploy and its code configuration is removed. Keep the documented “respect sample code config” behavior by only enabling passthrough when code deploy is not configured.
if strings.TrimSpace(svc.GetImage()) != "" {
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
a6d4726 to
cd58991
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 51 out of 52 changed files in this pull request and generated 1 comment.
Suppressed comments (4)
cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go:726
- [azd-code-reviewer] The Docker framework has already run
ContainerHelper.Packageand appended its passthrough artifact before the service target'sPackagemethod is called (service_manager.go:421-431). Calling the container package RPC again returns a second identical artifact, so normalazd packageresults contain duplicates. Let the target return an empty result here and retain the framework-owned artifact already inserviceContext.Package.
if DockerImagePassthrough(serviceConfig.GetDocker()) {
progress("Packaging pre-built container image")
artifacts, err := p.packageContainer(ctx, serviceConfig, serviceContext)
if err != nil {
return nil, err
}
return &azdext.ServicePackageResult{Artifacts: artifacts}, nil
cli/azd/extensions/azure.ai.agents/internal/pkg/containerref/reference.go:16
- [azd-code-reviewer] This alternation accepts either a tag or a digest, but rejects the valid OCI form
registry.example.com/team/agent:v1@sha256:.... Core passthrough explicitly preserves tag-and-digest references, so registry-backed agents currently reject an image that the underlying lifecycle supports. Make the tag and digest independent optional groups and add this case to the table test.
`(?::[\w][\w.-]{0,127}|@sha256:[0-9a-fA-F]{64})?$`,
cli/azd/extensions/azure.ai.agents/internal/cmd/init.go:4514
- [azd-code-reviewer] Manifest-backed registry images are only checked for presence here. Unlike
--image, a manifest containingimage: agent:v1plusregistryConnectionIdcompletes init and writes an invalid project, then fails later during package/deploy whenvalidateRegistryConnectionDefinitionrequires an explicit registry host. Apply the same fully-qualified reference validation during init so all authoring paths fail before mutating the project.
if preBuiltImageForInit(agentManifest, a.flags.image) == "" {
return exterrors.Validation(
exterrors.CodeInvalidParameter,
"a registry connection requires a pre-built image",
"Pass --image <registry/image:tag> or provide an image in the hosted-agent manifest",
)
}
cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go:1715
- [azd-code-reviewer] Adoption detects a pre-built image only from the core service-level
image.AgentDefinitionFromResolvedServicealso supports images supplied by an inline/file-referenced legacy definition, and the registry-connection branch above accepts that resolved image, but this check then misses it and configures a source build/ACR instead of passthrough. Derive the effective image fromresolvedAgent.Imagewhen the service field is empty.
// An adopted service that already declares an image also uses passthrough,
// even when --image was not supplied during this init. An explicit code mode
// overrides a leftover image.
if strings.TrimSpace(svc.GetImage()) != "" && flags.deployMode != "code" {
if err := applyContainerDeployToService(ctx, azdClient, serviceName, svc, svc.GetImage()); err != nil {
return false, err
}
return false, nil
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 52 out of 53 changed files in this pull request and generated 1 comment.
Suppressed comments (3)
cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go:1653
- [azd-code-reviewer] The adopted-project path also treats any nonempty existing service/definition image as valid when
--registry-connectionis supplied. Because early flag validation defers to the manifest when--imageis absent, an unqualified or URL-form image is persisted with passthrough and only fails later during deployment. ValidateeffectiveImagewith the shared container-reference validator here.
if effectiveImage == "" && hasDefinition {
cli/azd/extensions/azure.ai.agents/internal/cmd/init.go:4514
- [azd-code-reviewer] This only checks that a manifest-supplied image is nonempty. Unlike
--image, an image such asagent:v1orhttps://registry.example.com/agent:v1passes init and is not rejected until deployment. Apply the same fully-qualified-reference validation to the effective image before persisting the registry connection.
if preBuiltImageForInit(agentManifest, a.flags.image) == "" {
cli/azd/extensions/azure.ai.agents/internal/pkg/containerref/reference.go:16
- [azd-code-reviewer] This grammar hard-codes SHA-256 digests, so otherwise valid fully qualified OCI references using another digest algorithm (for example
registry.example.com/team/agent@sha512:<digest>) are rejected by init and deployment. Reuse the standard distribution/reference parser and separately require an explicit registry component rather than maintaining a narrower custom grammar.
`(?::[\w][\w.-]{0,127})?(?:@sha256:[0-9a-fA-F]{64})?$`,
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 52 out of 53 changed files in this pull request and generated no new comments.
Suppressed comments (2)
cli/azd/extensions/azure.ai.agents/internal/cmd/init.go:4514
- [azd-code-reviewer] This only checks that a manifest supplies some image.
--registry-connection -m agent.yamltherefore accepts an unqualified value such asagent:v1, even though the init contract requires a fully qualified registry/repository and deployment later rejects it. Run the same image validation used for--imageagainst the effective manifest image before modifying the project.
if preBuiltImageForInit(agentManifest, a.flags.image) == "" {
cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go:1711
- [azd-code-reviewer] The adopted definition was resolved above, but passthrough considers only the service-level image. For a supported local
$ref/legacy definition whose image exists only inresolvedAgent.Image, this falls through to a source build (and an explicit registry connection can be written withremoteBuild: true), so core never receives the required service image/passthrough pair. Promote the resolved image to the service-levelimageand use it when selecting the Docker lifecycle, or reject this authoring shape during adoption.
// An adopted service that already declares an image also uses passthrough,
// even when --image was not supplied during this init. An explicit code mode
// overrides a leftover image.
if strings.TrimSpace(svc.GetImage()) != "" && flags.deployMode != "code" {
if err := applyContainerDeployToService(ctx, azdClient, serviceName, svc, svc.GetImage()); err != nil {
Adds registry-neutral hosted-agent authoring, non-interactive init support, Foundry connection validation, and registry_connection_id REST mapping.\n\nFixes Azure#9582
5f21a11 to
11f4c5e
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 32 out of 32 changed files in this pull request and generated no new comments.
Suppressed comments (4)
cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go:214
- [azd-code-reviewer] This early guard only detects a literal inline
registryConnectionId. Supported$refand legacyagent.yamldefinitions can carry the property whileServiceConfigPropscontains only$refor no definition, soInitializereturns successfully and core can pull/build the private image beforePackageresolves and rejects the configuration. Make registry-backed file definitions visible to pre-framework validation, or explicitly reject/migrate this shape before core container processing.
if props == nil || props.GetFields()["registryConnectionId"] == nil {
return nil
cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go:1871
- [azd-code-reviewer] References accepted by the shared validator can still fail earlier in
AgentDefinitionFromService:agent_definition.go:773usescontainerImageRefRe, which rejects registry ports and tag-plus-digest references. Thus--image localhost:5000/agent:v1oragent:v1@sha256:...passes init but fails deployment before reaching this check. Replace the legacy loader checks with the shared validator and add loader-level coverage.
if !containerref.IsFullyQualified(image) {
cli/azd/extensions/azure.ai.agents/internal/cmd/init.go:2089
- [azd-code-reviewer] This verifies every manifest-provided registry reference as if it were external. If the manifest also declares a matching
ConnectionResource, that connection is intentionally created later byemitResourceServices, so lookup against an existing project fails before init can generate the siblingazure.ai.connectionservice. Skip remote verification for locally declared connection resources and letusesenforce their provisioning order.
if err := a.verifyRegistryConnection(ctx); err != nil {
return err
cli/azd/extensions/azure.ai.agents/internal/cmd/init_adopt.go:1710
- [azd-code-reviewer] Explicit code mode bypasses the existing-image branch, but
applyCodeDeployToServiceremoves onlydockerand leaves an existingregistryConnectionId. The adopted project is therefore rewritten to an invalid code-plus-registry configuration, and the next command fails schema/Initialize validation. Clear the registry reference when switching to code deploy, or reject the switch before mutating the service.
// An adopted service that already declares an image also uses passthrough,
// even when --image was not supplied during this init. An explicit code mode
// overrides a leftover image.
if strings.TrimSpace(svc.GetImage()) != "" && flags.deployMode != "code" {
Travis Angevine (trangevi)
left a comment
There was a problem hiding this comment.
approving, obviously pending the underlying core changes
Summary
registryConnectionIdauthoring for hosted container agentsdefinition.container_configuration.registry_connection_idazd ai agent init --registry-connection <name-or-id>supportazure.ai.connectiondependencies throughusesdocker.imagePassthroughfor flag-based, manifest-based, and private-registry pre-built imagesazure.yamland legacy agent-definition round tripsThe implementation is registry-neutral: it does not inspect registry vendors, hostnames, token-exchange semantics, or vendor-specific credential fields.
Fixes #9582
Important
This PR is stacked on #9588 and now adopts its core image-passthrough capability. Both existing BYO-image flows and private non-ACR registry flows generate
docker.imagePassthrough: trueand delegate the container artifact lifecycle to core azd.Tested scenarios
A real E2E was run in
westus2using a private JFrog Docker repository, a brownfield Foundry project, and JFrog OIDC token exchange bound to the Foundry project managed identity. Anonymous manifest access returned401before deployment, confirming that the image was private.1. Initialize from a pre-created registry connection
The original live run used a pre-created connection. After the portal or Azure CLI creates it, reference its name during non-interactive agent init:
Key generated
azure.yamloutput:The existing external connection was not added to
uses.azd provisionis intentionally omitted after init because both the Foundry project and the connection now exist; init records the existing project deployment context and the agent can deploy directly.Key deployed-agent output:
{ "version": "1", "status": "active", "definition": { "container_configuration": { "image": "<private-jfrog-host>/<repository>/echo-agent:<tag>", "registry_connection_id": "private-registry-init" }, "protocol_versions": [ { "protocol": "invocations", "version": "1.0.0" } ] } }Key invocation output:
The Foundry session logstream showed container startup,
GET /readinessreturning200, and both invocation requests returning200.2. Declarative brownfield project and sibling connection
Configuration:
Commands:
Key provisioning output:
The provisioned project connection reported:
{ "name": "private-registry-declarative", "properties": { "category": "CustomKeys", "authType": "CustomKeys", "metadata": { "type": "registry_connection", "mode": "oauth_token_exchange" } } }Key deployment and invocation output:
The deployed definition contained
container_configuration.registry_connection_id: private-registry-declarative, and the Foundry session logstream showed readiness and invocation HTTP200responses.Core image passthrough integration
The original declarative E2E exposed a core lifecycle gap before the extension deploy phase:
Without an explicit passthrough signal, the core Docker framework treated the top-level
imageas an external source image and attempted to pull and tag it locally before the extension could deploy it by reference. The earlier E2E useddocker.remoteBuildto avoid that pull, butremoteBuilddescribes an ACR Tasks build rather than an already-published image and was not the intended contract.The current implementation is stacked on #9588 and replaces that workaround:
docker.imagePassthrough: true; source build meansdocker.remoteBuild, disabled for VNET-injected projects;AZD_AGENT_SKIP_ACRremains only as a legacy provisioning signal; new configurations do not use it to select the container lifecycle;image+docker+AZD_AGENT_SKIP_ACR=trueshape;The live JFrog results above validate the Foundry connection, registry token exchange, hosted-container startup, and invocation path. They were captured before the final core passthrough wiring, while the current lifecycle integration is covered by the automated tests listed below.
Trace capture
The E2E retained sanitized local artifacts for CLI demo preparation:
azure.yamlfilesNo Application Insights connection was configured for this temporary project, so runtime traces came from Foundry hosted-session logstreams rather than App Insights. The artifact bundle passed a scan against the JFrog username and administrator token.
All temporary Foundry, Entra, JFrog, session, and image resources were deleted after the run; the test resource group was empty afterward.
Automated validation
Focused coverage includes:
usesvalidationcredentials.keys.body.*pass-through