Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
18 changes: 9 additions & 9 deletions cli/azd/extensions/azure.ai.agents/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -409,11 +409,10 @@ connection must exist on the selected project before `azd deploy` runs.

### Declarative sibling connection

To let `azd provision` create the connection, declare an
`azure.ai.connection` sibling. For this declarative path, both the agent's
`registryConnectionId` and `uses` identify the sibling's azure.yaml service key,
which is also the Foundry connection name provisioned by the current Projects
extension:
To let azd manage the connection, declare an `azure.ai.connection` sibling. For
this declarative path, both the agent's `registryConnectionId` and `uses`
identify the sibling's azure.yaml service key, which is also the Foundry
connection name reconciled by the Connections extension:

```yaml
services:
Expand Down Expand Up @@ -453,10 +452,11 @@ services:
version: 1.0.0
```

Set the referenced credential environment values, run `azd provision`, and then
run `azd deploy`. Omitting the sibling from `uses`, disabling it with a deployment
condition, or omitting image passthrough causes validation to fail before agent
deployment.
Set the referenced credential environment values and run `azd up`, or run
`azd provision` followed by `azd deploy`. Provision creates the Project;
deploy reconciles the Connection before the dependent Agent. Omitting the
sibling from `uses`, disabling it with a deployment condition, or omitting image
passthrough causes validation to fail before Agent deployment.

## Private networking for `host: azure.ai.project`

Expand Down
9 changes: 1 addition & 8 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -3537,7 +3537,7 @@ func (a *InitAction) addToProject(ctx context.Context, targetDir string, agentMa
// Emit the sibling Foundry resource services (project + deployments,
// connections, toolboxes) and wire the agent's uses: to them. A selected
// existing project contributes its endpoint so provision reuses it.
emittedConnections, err := emitResourceServices(
_, err = emitResourceServices(
ctx, a.azdClient, a.serviceNameOverride,
projectNameHint(ctx, a.azdClient, a.environment.Name, a.selectedFoundryProject),
a.selectedFoundryProject.Endpoint(),
Expand All @@ -3546,13 +3546,6 @@ func (a *InitAction) addToProject(ctx context.Context, targetDir string, agentMa
if err != nil {
return err
}
recordPendingConnectionProvision(
ctx,
a.azdClient,
a.environment.Name,
emittedConnections,
)

printAgentAddedMessage(agentDef.Name)

// Replace the legacy hardcoded `azd up` / `azd deploy` hint with the
Expand Down
4 changes: 2 additions & 2 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/listen.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func preprovisionHandler(ctx context.Context, azdClient *azdext.AzdClient, args
); err != nil {
return err
}
connections, err := collectConnections(
connections, err := collectLegacyConnections(
args.Project.Services,
args.Project.Path,
)
Expand Down Expand Up @@ -241,7 +241,7 @@ func predeployHandler(ctx context.Context, azdClient *azdext.AzdClient, args *az
); err != nil {
return err
}
connections, err := collectConnections(
connections, err := collectLegacyConnections(
args.Project.Services,
args.Project.Path,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ func ResolveAfterInit(state *State, readmeExists func(relativePath string) bool)
}
out = append(out, Suggestion{
Command: "azd provision",
Description: "set up your Foundry project, models, and connections",
Description: "set up your Foundry project and models",
Priority: priority,
})
priority++
Expand Down Expand Up @@ -246,17 +246,10 @@ func ResolveAfterInit(state *State, readmeExists func(relativePath string) bool)
)
// Toolbox sub-branch: configured toolboxes declare one or more
// whose azd-injected TOOLBOX_<NAME>_MCP_ENDPOINT variable is
// not yet present in the azd environment. The variable is
// written by `azd provision` (listen.go::registerToolboxEnvVars)
// after the azure.ai.toolbox service target publishes the toolbox version,
// so the canonical fix is provision — NOT `azd env set`, which
// the generic manual-vars sub-branch below would otherwise
// suggest. We also surface `azd ai agent doctor` as a follow-up
// so the user can check whether the toolbox already exists in
// their Foundry project. The actual live existence check
// belongs in doctor's local.toolboxes (one HTTP GET per
// toolbox); ResolveAfterInit is offline by contract and must
// not initiate Foundry API calls.
// not yet present. Split Toolboxes publish it from their deploy
// target. Bundled and legacy Toolboxes retain their existing
// migration/provision guidance. ResolveAfterInit remains offline
// and does not probe Foundry.
if hasToolboxEndpoints && hasBundledToolboxEndpoints {
priority = appendBundledToolboxGuidance(
&out,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,15 +117,6 @@ func TestResolveAfterInit(t *testing.T) {
wantPrimaryHas: "azd provision",
wantTrailing: "azd deploy",
},
{
name: "new connection in existing project → provision",
state: &State{
HasProjectEndpoint: true,
PendingProvisionReasons: []string{"connection"},
},
wantPrimaryHas: "azd provision",
wantTrailing: "azd deploy",
},
{
name: "provision needed with missing Azure context → env set before provision",
state: &State{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ package cmd
import (
"context"
"fmt"
"log"
"slices"
"strings"

Expand Down Expand Up @@ -46,7 +45,6 @@ const (
pendingReasonModelDeployment = "model_deployment"
pendingReasonACR = "acr"
pendingReasonAppInsights = "app_insights"
pendingReasonConnection = "connection"
)

// parsePendingProvisionReasons splits the comma-separated env-var
Expand Down Expand Up @@ -109,31 +107,6 @@ func addPendingProvisionReason(
})
}

// recordPendingConnectionProvision marks connections that need
// provision after init writes a connection service.
// A signal write failure only produces a warning.
func recordPendingConnectionProvision(
ctx context.Context,
azdClient *azdext.AzdClient,
envName string,
emitted int,
) {
if emitted <= 0 {
return
}
if _, err := addPendingProvisionReason(
ctx,
azdClient,
envName,
pendingReasonConnection,
); err != nil {
log.Printf(
"warning: could not record pending connection provision: %v",
err,
)
}
}

// removePendingProvisionReason drops a reason tag from the
// AI_AGENT_PENDING_PROVISION env var. Idempotent: removing a tag
// that was not present is a no-op (no write performed). Used when
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,89 +204,6 @@ func TestRemovePendingProvisionReason(t *testing.T) {
})
}

func TestRecordPendingConnectionProvision(t *testing.T) {
t.Parallel()

t.Run("emitted writes connection reason", func(t *testing.T) {
t.Parallel()

envServer := &testEnvironmentServiceServer{
environments: map[string]*azdext.Environment{
"test-env": {Name: "test-env"},
},
}
azdClient := newTestAzdClient(t, envServer, &testWorkflowServiceServer{})

recordPendingConnectionProvision(
context.Background(), azdClient, "test-env", 1)
require.Equal(
t,
pendingReasonConnection,
envServer.values["test-env"][pendingProvisionEnvVar],
)
})

t.Run("zero emitted is no-op", func(t *testing.T) {
t.Parallel()

envServer := &testEnvironmentServiceServer{
environments: map[string]*azdext.Environment{
"test-env": {Name: "test-env"},
},
}
azdClient := newTestAzdClient(t, envServer, &testWorkflowServiceServer{})

recordPendingConnectionProvision(
context.Background(), azdClient, "test-env", 0)
_, hit := envServer.values["test-env"][pendingProvisionEnvVar]
require.False(t, hit)
})

t.Run("zero emitted preserves existing reason", func(t *testing.T) {
t.Parallel()

envServer := &testEnvironmentServiceServer{
environments: map[string]*azdext.Environment{
"test-env": {Name: "test-env"},
},
values: map[string]map[string]string{
"test-env": {pendingProvisionEnvVar: "connection"},
},
}
azdClient := newTestAzdClient(t, envServer, &testWorkflowServiceServer{})

recordPendingConnectionProvision(
context.Background(), azdClient, "test-env", 0)
require.Equal(
t,
"connection",
envServer.values["test-env"][pendingProvisionEnvVar],
)
})

t.Run("sorts with existing reasons", func(t *testing.T) {
t.Parallel()

envServer := &testEnvironmentServiceServer{
environments: map[string]*azdext.Environment{
"test-env": {Name: "test-env"},
},
values: map[string]map[string]string{
"test-env": {pendingProvisionEnvVar: "project"},
},
}
azdClient := newTestAzdClient(t, envServer, &testWorkflowServiceServer{})

recordPendingConnectionProvision(
context.Background(), azdClient, "test-env", 2)
require.Equal(
t,
"connection,project",
envServer.values["test-env"][pendingProvisionEnvVar],
)
})
}

func TestClearPendingProvisionReasons(t *testing.T) {
t.Parallel()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -530,106 +530,27 @@ func collectLegacyProjectDeployments(
return out, nil
}

// collectConnections gathers the connections declared across all
// azure.ai.connection services. Falls back to the connections bundled on the
// agent service when no connection service carries any, so a pre-split
// azure.yaml still provisions without re-running init.
func collectConnections(
// collectLegacyConnections gathers connections bundled on pre-split Agent
// services. Split host: azure.ai.connection services are reconciled by the
// Connections extension and are intentionally not parsed here.
func collectLegacyConnections(
services map[string]*azdext.ServiceConfig,
projectRoot string,
) ([]project.Connection, error) {
var out []project.Connection
for _, svc := range sortedServices(services) {
if svc.Host != AiConnectionHost {
continue
}
props, err := resolvedResourceServiceProps(
svc,
projectRoot,
)
if err != nil {
return nil, err
}
if props == nil {
continue
}
var conn *project.Connection
if err := project.UnmarshalStruct(props, &conn); err != nil {
return nil, fmt.Errorf("parsing connection service %q config: %w", svc.Name, err)
}
if conn != nil {
if conn.Name == "" {
conn.Name = svc.Name
}
out = append(out, *conn)
}
}
if len(out) > 0 {
return out, nil
}
legacy, err := collectLegacyAgentConfigs(
services,
projectRoot,
)
if err != nil {
return nil, err
}
var out []project.Connection
for _, cfg := range legacy {
out = append(out, cfg.Connections...)
}
return out, nil
}

// collectToolboxes gathers the toolboxes declared across all azure.ai.toolbox
// services. Falls back to the toolboxes bundled on the agent service when no
// toolbox service carries any, so a pre-split azure.yaml still provisions
// without re-running init.
func collectToolboxes(
services map[string]*azdext.ServiceConfig,
projectRoot string,
) ([]project.Toolbox, error) {
var out []project.Toolbox
for _, svc := range sortedServices(services) {
if svc.Host != AiToolboxHost {
continue
}
props, err := resolvedResourceServiceProps(
svc,
projectRoot,
)
if err != nil {
return nil, err
}
if props == nil {
continue
}
var toolbox *project.Toolbox
if err := project.UnmarshalStruct(props, &toolbox); err != nil {
return nil, fmt.Errorf("parsing toolbox service %q config: %w", svc.Name, err)
}
if toolbox != nil {
if toolbox.Name == "" {
toolbox.Name = svc.Name
}
out = append(out, *toolbox)
}
}
if len(out) > 0 {
return out, nil
}
legacy, err := collectLegacyAgentConfigs(
services,
projectRoot,
)
if err != nil {
return nil, err
}
for _, cfg := range legacy {
out = append(out, cfg.Toolboxes...)
}
return out, nil
}

// collectAgentToolConnections gathers the tool connections declared on agent
// services. Tool connections stay on the agent service (they are agent tool
// configuration), so toolbox enrichment still needs them alongside the
Expand Down
Loading
Loading