Skip to content

feat(agents): support private non-ACR registry connections - #9586

Draft
Wei Meng (m5i-work) wants to merge 1 commit into
Azure:mainfrom
m5i-work:m5i/9582-private-registry
Draft

feat(agents): support private non-ACR registry connections#9586
Wei Meng (m5i-work) wants to merge 1 commit into
Azure:mainfrom
m5i-work:m5i/9582-private-registry

Conversation

@m5i-work

@m5i-work Wei Meng (m5i-work) commented Aug 14, 2026

Copy link
Copy Markdown
Member

Summary

  • add optional registryConnectionId authoring for hosted container agents
  • map it to definition.container_configuration.registry_connection_id
  • add non-interactive azd ai agent init --registry-connection <name-or-id> support
  • verify external connections by Foundry connection name or ID when an existing project is available
  • validate sibling azure.ai.connection dependencies through uses
  • keep public-image, ACR, and code-deploy behavior unchanged when the property is unset
  • preserve the property through unified azure.yaml and legacy agent-definition round trips

The implementation is registry-neutral: it does not inspect registry vendors, hostnames, token-exchange semantics, or vendor-specific credential fields.

Fixes #9582

Important

This extension PR depends on the core image-passthrough capability tracked by #9587 and implemented in draft PR #9588. The extension follow-up will replace its current remoteBuild workaround with docker.imagePassthrough after the core SDK change is available.

Tested scenarios

A real E2E was run in westus2 using a private JFrog Docker repository, a brownfield Foundry project, and JFrog OIDC token exchange bound to the Foundry project managed identity. Anonymous manifest access returned 401 before deployment, confirming that the image was private.

1. Existing connection through non-interactive init

Command:

azd ai agent init --no-prompt \
  --agent-name private-registry-init-agent \
  --image <private-jfrog-host>/<repository>/echo-agent:<tag> \
  --project-id <foundry-project-resource-id> \
  --registry-connection private-registry-init

azd deploy --no-prompt

azd ai agent invoke private-registry-init-agent \
  --protocol invocations \
  --new-session \
  --input-file request.json

Key generated azure.yaml output:

services:
  private-registry-init-agent:
    host: azure.ai.agent
    image: <private-jfrog-host>/<repository>/echo-agent:<tag>
    uses:
      - existing-foundry-project
    registryConnectionId: private-registry-init

The existing external connection was not added to uses.

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:

Agent:    private-registry-init-agent (remote, invocations protocol)
Session:  <session-id> (assigned by server)
Trace ID: <trace-id>

"reply": "Hello! You said: 'Hello from the azd init private-registry demo'."
Server responded in 2.484s

The Foundry session logstream showed container startup, GET /readiness returning 200, and both invocation requests returning 200.

2. Declarative brownfield project and sibling connection

Configuration:

infra:
  provider: microsoft.foundry

services:
  existing-foundry-project:
    host: azure.ai.project
    endpoint: https://<account>.services.ai.azure.com/api/projects/<project>

  private-registry-declarative:
    host: azure.ai.connection
    uses:
      - existing-foundry-project
    category: CustomKeys
    target: ${REGISTRY_URL}
    authType: CustomKeys
    credentials:
      keys:
        audience: ${REGISTRY_AUDIENCE}
        tokenEndpoint: ${REGISTRY_TOKEN_ENDPOINT}
        body.provider_name: ${REGISTRY_PROVIDER}
    metadata:
      type: registry_connection
      mode: oauth_token_exchange

  private-registry-yaml-agent:
    host: azure.ai.agent
    uses:
      - existing-foundry-project
      - private-registry-declarative
    kind: hosted
    name: private-registry-yaml-agent
    image: <private-jfrog-host>/<repository>/echo-agent:<tag>
    registryConnectionId: private-registry-declarative
    protocols:
      - protocol: invocations
        version: 1.0.0

Commands:

azd env set AZURE_AI_PROJECT_ID <foundry-project-resource-id>
azd env set REGISTRY_URL https://<private-jfrog-host>
azd env set REGISTRY_AUDIENCE <entra-audience-app-id>
azd env set REGISTRY_TOKEN_ENDPOINT /access/api/v1/oidc/token
azd env set REGISTRY_PROVIDER <oidc-provider-name>

azd provision --no-prompt
azd deploy --no-prompt

azd ai agent invoke private-registry-yaml-agent \
  --protocol invocations \
  --new-session \
  --input-file request.json

Key provisioning output:

SUCCESS: Your application was provisioned in Azure in 36 seconds.

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:

private-registry-declarative  Done
private-registry-yaml-agent   Done
SUCCESS: Your application was deployed to Azure in 48 seconds.

Agent:    private-registry-yaml-agent (remote, invocations protocol)
Session:  <session-id> (assigned by server)
Trace ID: <trace-id>

"reply": "Hello! You said: 'Hello from the declarative brownfield private-registry demo'."
Server responded in 2.562s

The deployed definition contained container_configuration.registry_connection_id: private-registry-declarative, and the Foundry session logstream showed readiness and invocation HTTP 200 responses.

E2E finding and core dependency

The exact declarative configuration exposed an existing core lifecycle gap before the extension deploy phase:

Pulling container source image
Error response from daemon: ... Authentication is required

Core Docker framework Package runs before extension service-target Package. With a top-level image and no local build artifact, core treats the image as an external source image and pulls and tags it locally before the extension can classify it as a deployment-by-reference artifact.

The agents extension currently relies on:

docker:
  remoteBuild: true

to suppress core local Build and Package for prebuilt images. Extension Publish then short-circuits, so an ACR Tasks build does not run. This is an existing, semantically incorrect workaround: remoteBuild means an ACR Tasks build, not passthrough of an already-published image.

The successful declarative E2E used that existing workaround to complete deployment and invocation, but it is not the intended final contract. The core gap is tracked by #9587, and draft PR #9588 adds the separate docker.imagePassthrough signal.

Before this PR is marked ready:

  1. land and release the core capability from feat: add container image passthrough #9588;
  2. update the extension SDK dependency;
  3. generate docker.imagePassthrough: true for prebuilt-image init;
  4. update the declarative example to use image passthrough rather than remote build;
  5. rerun both JFrog E2E scenarios with no remoteBuild workaround.

Trace capture

The E2E retained sanitized local artifacts for CLI demo preparation:

  • PTY transcripts for init, provision, deploy, and invoke
  • generated and declarative azure.yaml files
  • deployed agent-version JSON
  • raw invocation response headers and bodies
  • CLI trace IDs and APIM/request/invocation/session correlation IDs
  • hosted-agent session SSE logstreams

No 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

cd cli/azd/extensions/azure.ai.agents
go build ./...
go test ./... -short
go fix ./...

# Repository/schema checks
git diff --check
jq empty schemas/azure.ai.agent.json

Focused coverage includes:

  • authoring and legacy round trips
  • schema constraints
  • exact REST placement and omission
  • code-deploy omission
  • public non-ACR and ACR behavior without a registry connection
  • non-interactive flag validation and generated configuration
  • external connection name/ID lookup
  • sibling connection uses validation
  • arbitrary credentials.keys.body.* pass-through

Adds registry-neutral hosted-agent authoring, non-interactive init support, Foundry connection validation, and registry_connection_id REST mapping.\n\nFixes Azure#9582
@m5i-work Wei Meng (m5i-work) added feature Feature request area/extensions Extensions (general) ext-agents azure.ai.agents extension labels Aug 14, 2026
@azure-pipelines

Copy link
Copy Markdown
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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/extensions Extensions (general) ext-agents azure.ai.agents extension feature Feature request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[azure.ai.agents] Support private non-ACR registry connections for hosted agents

1 participant