Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
8410b07
feat(agents): private networking for host: microsoft.foundry
m5i-work Jun 18, 2026
bffcab3
fix(agents): preserve network ${VAR} refs through eject
m5i-work Jun 18, 2026
ec4fcec
test(agents): add Foundry private-networking E2E harness
m5i-work Jun 18, 2026
f296fbf
test(agents): run network E2E phases 0-4 without --image
m5i-work Jun 18, 2026
72c52b5
fix(agents): correct network bicep preflight + network-mode output ca…
m5i-work Jun 18, 2026
65196bf
test(agents): make network E2E harness run green end-to-end
m5i-work Jun 18, 2026
da8f5b7
test(agents): build phase-5 image in ABAC-enabled ACR
m5i-work Jun 18, 2026
7dbe27a
test(agents): grant private ACR pull to Foundry project identity
m5i-work Jun 18, 2026
0813c19
docs(agents): add BYO image VNet cheatsheet
m5i-work Jun 18, 2026
75939f5
docs(agents): move private networking guide to docs
m5i-work Jun 18, 2026
9e99a4a
fix(agents): surface managed-network isolation output
m5i-work Jun 18, 2026
6caace1
fix(agents): keep managed-network data plane reachable
m5i-work Jun 18, 2026
6111b2e
feat(agents): secure-by-default private networking for Foundry services
m5i-work Jun 22, 2026
36e16d5
docs(agents): use 'azd ai agent init --image' and assert real state i…
m5i-work Jun 22, 2026
050986c
docs(agents): order init before azure.yaml in managed-egress cheatsheet
m5i-work Jun 22, 2026
6a82c36
docs(agents): simplify private-networking cheatsheets
m5i-work Jun 22, 2026
f1f0d06
test(agents): fix jumpbox fallback for DNS-reference deploy
m5i-work Jun 23, 2026
1d3f748
test(agents): exclude private-networking e2e harness from PR
m5i-work Jun 23, 2026
115e1e1
Merge remote-tracking branch 'origin/huimiu/foundry-azure-yaml' into …
m5i-work Jun 23, 2026
ea84132
test(agents): ignore bicep generator metadata in ARM drift check
m5i-work Jun 23, 2026
56e9532
feat(agents): validate distinct subnet names; drop debug log
m5i-work Jun 23, 2026
16e3459
Merge remote-tracking branch 'origin/huimiu/foundry-azure-yaml' into …
m5i-work Jun 23, 2026
52196bc
feat(agents): refuse Terraform eject when network: is declared
m5i-work Jun 23, 2026
5dd48f9
docs+test(agents): document Bicep eject customization; verify full ne…
m5i-work Jun 23, 2026
d60e802
docs(agents): merge networking Requirements/Known limitations into on…
m5i-work Jun 23, 2026
0f838e8
docs(agents): rename networking quick guides to parallel Scenario 1-3…
m5i-work Jun 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions cli/azd/extensions/azure.ai.agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ Use `--no-inspector` to run only the local agent process:
azd ai agent run --no-inspector
```

## Private networking for `host: microsoft.foundry`

Foundry services can be provisioned as network-secured, VNet-bound accounts by
adding a `network:` block to `azure.yaml`. See
[Private networking for `host: microsoft.foundry`](docs/private-networking.md)
for the schema reference, BYO-image requirements, and VNet deployment
cheatsheet.

## Local Development

### Prerequisites
Expand Down
314 changes: 314 additions & 0 deletions cli/azd/extensions/azure.ai.agents/docs/private-networking.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,314 @@
# Private networking for `host: azure.ai.agent`

A Foundry service can be provisioned as a **network-secured (VNet-bound)**
account by adding a `network:` block to the service body in `azure.yaml`. When
`network:` is omitted the account uses public networking (unchanged behavior).

When `network:` is present, azd always provisions an **account private
endpoint** and disables public network access — the data plane is never left
public. Dependent stores (Cosmos DB, AI Search, Storage) stay platform-managed.

The block models two orthogonal axes:

- **Egress** (agent runtime network) — set `agentSubnet` to inject the agent
into your subnet (BYO VNet), or omit it to use the Microsoft-managed network.
`isolationMode` tunes the managed network's outbound posture and is valid only
when `agentSubnet` is omitted.
- **Ingress** (account data plane) — `peSubnet` is **required** and always
yields an account private endpoint, so callers (`azd deploy`,
`azd ai agent invoke`) must reach the account from inside the VNet, a peered
VNet, or VPN.

```yaml
services:
my-project:
host: azure.ai.agent
network:
Comment thread
m5i-work marked this conversation as resolved.
# ----- Egress: agent runtime network (pick ONE) -----
#
# (a) Managed egress (shown live below): omit agentSubnet so the agent
# runs in the Microsoft-managed network. isolationMode is valid only
# in this mode.
isolationMode: AllowOnlyApprovedOutbound # or AllowInternetOutbound (default)
#
# (b) BYO egress: inject the agent into your subnet instead. Replace the
# isolationMode line above with an agentSubnet block (same VNet as
# peSubnet in v1):
# agentSubnet:
# vnet: ${AZURE_VNET_ID}
# name: agent-subnet
# prefix: 192.168.10.0/24 # omit prefix to reference an existing subnet

# ----- Ingress: account private endpoint (REQUIRED) -----
peSubnet:
vnet: ${AZURE_VNET_ID} # ARM id of the VNet (must already exist)
name: pe-subnet
prefix: 192.168.11.0/24 # omit prefix to reference an existing subnet

# ----- Private DNS (optional) -----
dns:
resourceGroup: rg-private-dns # omit to let azd create + link the zones
subscription: ${AZURE_DNS_SUBSCRIPTION_ID} # optional; defaults to the deployment subscription
agents:
- name: my-agent
kind: hosted
project: src/my-agent
image: myprivacr.azurecr.io/agents/my-agent:v1 # BYO image required
```

> You do not hand-author the `agents:` entry above. Run
> `azd ai agent init --no-prompt --agent-name my-agent --image <registry/image:tag>`
> to scaffold it (it writes `agent.yaml`); then add the `network:` block to the
> generated service.

> The example above uses **managed egress** so every field — including
> `isolationMode` — is shown as valid YAML. For **BYO egress**, swap the
> `isolationMode` line for an `agentSubnet` block (see comment `(b)` and
> Scenario 2 below); `isolationMode` is then invalid and must be removed.

### Field reference

| Field | Rule |
| --- | --- |
| `agentSubnet` | Optional. Present: the agent is injected into this customer subnet (BYO egress). Absent: the agent uses the Microsoft-managed network (managed egress). |
| `peSubnet` | **Required.** Subnet for the account private endpoint. Establishes the private data plane (public access disabled). |
| `isolationMode` | Optional. `AllowInternetOutbound` or `AllowOnlyApprovedOutbound`. Valid **only** when `agentSubnet` is omitted (managed egress). |
| subnet `vnet` | Required. ARM id of the VNet that holds (or will hold) the subnet. Supports `${VAR}`. When `agentSubnet` is present, it must reference the same VNet as `peSubnet`. |
| subnet `name` | Required. Subnet name. |
| subnet `prefix` | Optional. Omit to reference an existing subnet; set to create the subnet with that CIDR. |
| `dns.resourceGroup` | Omitted: azd creates and links the AI private DNS zones. Set: azd references existing zones in that resource group. Requires `peSubnet`. |
| `dns.subscription` | Optional. Defaults to the deployment subscription. Accepts a bare GUID or `${VAR}`. |

### Environment variables

Network fields support `${VAR}` references resolved client-side from the azd
environment (run `azd env set <KEY> <value>`). The variable names are
user-chosen; the example above uses:

| Variable | Format | Used by |
| --- | --- | --- |
| `AZURE_VNET_ID` | ARM resource id of an existing `Microsoft.Network/virtualNetworks` | subnet `vnet` |
| `AZURE_DNS_SUBSCRIPTION_ID` | bare GUID or `/subscriptions/<guid>` | `network.dns.subscription` |

### Limitations

- **Single VNet (v1).** When `agentSubnet` is present it must live in the same
VNet as `peSubnet`; azd errors otherwise. Cross-VNet topologies (agent and
account private endpoint in different VNets) are deferred — they need
customer-managed peering plus DNS-zone links to both VNets, which azd does not
provision. Managed egress is unaffected (it needs only the `peSubnet` VNet).
- **BYO container image required.** Secured agents must reference a pre-built
image via `agents[].image`; local build into a private ACR is not supported in
v1. The developer owns the registry's SKU, private endpoint, DNS, and firewall.
- **Brownfield (`endpoint:`) ignores `network:`.** When `endpoint:` is set the
account's network posture is fixed by whoever created it; azd warns and does
not reconcile `network:`.
- **One default-DNS account per VNet.** Without a `dns:` block azd links the
three `privatelink.*` AI zones to your VNet, and a VNet may hold only one link
per namespace. A second account (or a brownfield hub that pre-links the zones)
must use `dns:` **reference** mode to bind the private endpoint without
re-linking.
- **Terraform IaC is not supported for private networking (v1).** Bicep-only
today; `azd ai agent init --infra=terraform` is refused when `network:` is
declared. Eject Bicep instead (see *Scenario 3 — Eject and customize the
Bicep*).

### Scenario 1 — Managed egress: private account, agent on Microsoft's network

Omit `agentSubnet` so the hosted-agent runtime uses a Microsoft-managed network
instead of your VNet. `peSubnet` is still required: the account data plane stays
private behind an account private endpoint in your VNet, reachable from inside
the VNet / VPN.

Scaffold the agent with a pre-built (BYO) image (writes `azure.yaml` and
`agent.yaml`):

```bash
azd ai agent init --no-prompt --agent-name my-agent \
--image myprivacr.azurecr.io/agents/my-agent:v1
```

Then add a `network:` block to the generated service in `azure.yaml` (omit
`agentSubnet` for managed egress; `isolationMode` is valid only in this mode):

```yaml
name: my-agent
infra:
provider: microsoft.foundry

services:
my-agent:
host: azure.ai.agent
deployments: []
network:
isolationMode: AllowInternetOutbound # managed-egress outbound posture
peSubnet:
vnet: ${AZURE_VNET_ID}
name: pe-subnet
prefix: 192.168.11.0/24
```

`azd ai agent init --image` already created and selected an azd environment and
set `AZD_AGENT_SKIP_ACR=true` (BYO image → no ACR build). Set the deployment
inputs on that environment and provision:

```bash
azd env set AZURE_SUBSCRIPTION_ID "<sub>"
azd env set AZURE_LOCATION westus
azd env set AZURE_RESOURCE_GROUP "<rg>"
azd env set AZURE_VNET_ID "<vnet-resource-id>"
azd provision --no-prompt
```

Grant the Foundry project MI ACR pull permission, then run deploy/invoke from a
host that can reach the account private endpoint:

```bash
azd deploy --no-prompt
azd ai agent invoke --new-session "hello"
```

> **`isolationMode` note.** When set, azd provisions the account's V2
> managed network (`managednetworks/default`) with the chosen isolation mode.
> `AllowOnlyApprovedOutbound` additionally requires approved outbound rules for
> the agent to reach dependent resources; for the platform-managed stores used
> here those are managed by the Foundry platform.

### Scenario 2 — BYO egress: agent injected into your VNet subnet

ACR requirements:

- The BYO image must be pullable by the Foundry **project managed identity**.
- For ABAC-enabled ACR, grant the project MI `Container Registry Repository Reader`.
- For private-only ACR, use Premium SKU, an ACR private endpoint, and a
`privatelink.azurecr.io` DNS zone linked to the VNet. Disable public access
only after the image is pushed.

Scaffold the agent with a pre-built (BYO) image — this writes `azure.yaml` and
`agent.yaml` for you, so there is no hand-edited manifest to keep in sync:

```bash
azd ai agent init --no-prompt --agent-name my-agent \
--image myprivacr.azurecr.io/agents/my-agent:v1
```

Then add a `network:` block to the generated service in `azure.yaml`:

```yaml
services:
my-agent:
host: azure.ai.agent
network:
agentSubnet: # omit the whole block for managed egress
vnet: ${AZURE_VNET_ID}
name: agent-subnet
prefix: 192.168.10.0/24 # omit prefix to reference an existing subnet
peSubnet: # required: makes the data plane private
vnet: ${AZURE_VNET_ID}
name: pe-subnet
prefix: 192.168.11.0/24
```

Configure and provision (`init --image` already created/selected the env and set
`AZD_AGENT_SKIP_ACR=true`):

```bash
azd env set AZURE_SUBSCRIPTION_ID "<sub>"
azd env set AZURE_LOCATION westus
azd env set AZURE_RESOURCE_GROUP "<rg>"
azd env set AZURE_VNET_ID "<vnet-resource-id>"
azd provision --no-prompt
```

Deploy and invoke from a host that can reach the Foundry private endpoint:

```bash
azd deploy --no-prompt
azd ai agent invoke --new-session "hello"
```

Common failures:

- `403 Public access is disabled`: the data plane is private in every
network-bound mode — run deploy/invoke from inside the VNet, a peered VNet, or
VPN.
- `ImageError: registry authentication failed`: grant ACR pull permission to the Foundry project MI.

### Scenario 3 — Eject and customize the Bicep (advanced)

The synthesized template covers the common private-networking shapes. When you
need something it doesn't express — an extra subnet, a private endpoint for a BYO
dependent store, custom DNS wiring, a non-default account property, additional
`networkInjections` rules — eject the Bicep, edit it directly, and let azd
provision your edited tree.

```bash
# 1. Scaffold + declare a network: block in azure.yaml (see Scenarios 1–2
# above), then eject the infrastructure:
azd ai agent init --infra # writes ./infra/ from azure.yaml
```

Eject writes the **full** Bicep tree from your `network:` block —
`infra/main.bicep`, `infra/modules/{resources,network,subnet,private-endpoint-dns,acr}.bicep`,
and `infra/main.parameters.json` — and **preserves `${VAR}` placeholders**
(resolved from the azd environment at provision time). `azure.yaml` is left
unchanged: `infra.provider` stays `microsoft.foundry`.

```bash
# 2. Edit the ejected Bicep to taste. Two worked examples:

# (a) infra/modules/network.bicep — add a subnet the network: schema can't
# express (e.g. for a future dependent-store private endpoint). Pick a CIDR
# free in your VNet space. Use the '<vnet>/<subnet>' name form (vnet is an
# existing resource in a possibly different RG):
#
# resource extraStoreSubnet 'Microsoft.Network/virtualNetworks/subnets@2024-05-01' = {
# name: '${vnetName}/byo-store-pe-subnet'
# properties: {
# addressPrefix: '192.168.30.0/24'
# privateEndpointNetworkPolicies: 'Disabled'
# }
# dependsOn: [ peSubnet ]
# }

# (b) infra/modules/resources.bicep — set an extra account property directly on
# the foundryAccount resource, e.g. merge a tag:
#
# tags: union(tags, { editedByPowerUser: 'true' })
```

```bash
# 3. Provision the edited tree. azd detects ./infra/main.bicep and compiles it
# instead of synthesizing from azure.yaml:
azd env set AZURE_SUBSCRIPTION_ID "<sub>"
azd env set AZURE_LOCATION westus
azd env set AZURE_RESOURCE_GROUP "<rg>"
azd env set AZURE_VNET_ID "<vnet-resource-id>"
azd provision --no-prompt

# 4. Deploy and invoke from a host with line-of-sight to the account private
# endpoint (inside the VNet, a peered VNet, or VPN):
azd deploy --no-prompt
azd ai agent invoke --new-session "hello"
```

**How it works.** Once `./infra/main.bicep` exists, azd provisions it directly
and **stops synthesizing from `azure.yaml`** — your edited Bicep is now the
source of truth. Your `main.parameters.json` values are layered over azd's
host-derived parameters (subscription, location, resource group, project name,
`principalId`); you win on keys you set, and azd fills in the rest.

**Notes.**

- Re-running `azd ai agent init --infra` is **refused** while `./infra/` exists,
so your edits are never overwritten — delete `./infra/` to regenerate from
`azure.yaml`.
- After ejecting, further `network:` edits in `azure.yaml` have **no effect**;
change the Bicep directly.
- `infra/main.arm.json` is intentionally not ejected (it would go stale the
moment you edit `main.bicep`); azd compiles `main.bicep` on each provision.
- **Terraform is not supported for private networking.**
`azd ai agent init --infra=terraform` is refused for a service that declares
`network:` (the Terraform module has no VNet / private-endpoint / DNS
resources, so ejecting it would silently provision a public account). Use
`--infra` (Bicep) and customize as above.
8 changes: 5 additions & 3 deletions cli/azd/extensions/azure.ai.agents/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,11 @@ require (

require github.com/denormal/go-gitignore v0.0.0-20180930084346-ae8ad1d07817

require golang.org/x/term v0.44.0
require (
go.opentelemetry.io/otel v1.43.0
go.opentelemetry.io/otel/trace v1.43.0
golang.org/x/term v0.44.0
)

require (
dario.cat/mergo v1.0.2 // indirect
Expand Down Expand Up @@ -107,10 +111,8 @@ require (
github.com/yuin/goldmark v1.7.16 // indirect
github.com/yuin/goldmark-emoji v1.0.6 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.43.0 // indirect
go.opentelemetry.io/otel/metric v1.43.0 // indirect
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
go.opentelemetry.io/otel/trace v1.43.0 // indirect
go.uber.org/atomic v1.11.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
golang.org/x/crypto v0.53.0 // indirect
Expand Down
17 changes: 17 additions & 0 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/init_infra.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,10 @@ func ejectInfra(projectRoot, provider string) error {
RawAzureYAML: rawYAML,
ServiceName: svcName,
AcceptedHosts: project.FoundryServiceHosts,
// Eject writes a static infra/ tree. Keep ${VAR} references verbatim so
// the ejected main.parameters.json stays environment-portable; the
// on-disk provision flow resolves them from the azd environment.
PreserveVarRefs: true,
})
if err != nil {
// Reuse the provider's vocabulary so eject and provision report
Expand All @@ -134,6 +138,19 @@ func ejectInfra(projectRoot, provider string) error {
}

if provider == project.TerraformProviderName {
// Private networking is Bicep-only today: the Terraform module has no
// VNet / private-endpoint / DNS / networkInjections resources, so ejecting
// it for a network: service would silently drop the config and provision a
// public account — the exact silent public fallback the network: contract
// forbids. Fail fast instead of ejecting an insecure template.
if res.NetworkMode != synthesis.NetworkModeNone {
return exterrors.Validation(
exterrors.CodeInfraEjectNetworkUnsupported,
"private networking (the service's network: block) is not yet supported with Terraform",
"eject Bicep instead with `azd ai agent init --infra` (or `--infra=bicep`), "+
"or remove the network: block to provision a public account with Terraform",
)
}
return ejectTerraform(projectRoot, infraDir, res.Parameters)
}
return ejectBicep(infraDir, res.Parameters)
Expand Down
Loading