Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 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
2 changes: 2 additions & 0 deletions cli/azd/extensions/azure.ai.agents/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ require (
gopkg.in/yaml.v3 v3.0.1
)

require github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices v1.8.0

Comment thread
Nathandrake229 marked this conversation as resolved.
Outdated
require (
dario.cat/mergo v1.0.2 // indirect
github.com/AlecAivazis/survey/v2 v2.3.7 // indirect
Expand Down
2 changes: 2 additions & 0 deletions cli/azd/extensions/azure.ai.agents/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthoriza
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v2 v2.2.0/go.mod h1:/pz8dyNQe+Ey3yBp/XuYz7oqX8YDNWVpPB0hH3XWfbc=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3 v3.0.0-beta.2 h1:qiir/pptnHqp6hV8QwV+IExYIf6cPsXBfUDUXQ27t2Y=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3 v3.0.0-beta.2/go.mod h1:jVRrRDLCOuif95HDYC23ADTMlvahB7tMdl519m9Iyjc=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices v1.8.0 h1:ZMGAqCZov8+7iFUPWKVcTaLgNXUeTlz20sIuWkQWNfg=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices v1.8.0/go.mod h1:BElPQ/GZtrdQ2i5uDZw3OKLE1we75W0AEWyeBR1TWQA=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices/v2 v2.0.0 h1:pxphC/uRZKNHNPbZ0duDDgKkefju2F03OkG5xF6byHQ=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices/v2 v2.0.0/go.mod h1:twcwRey+l1znKBL5TEzYiZMtiVkWfM7Pq8a9vY04xYc=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v1.3.0-beta.3 h1:4qfc7os3wRQcl+ImfeH9z0abWJzuV9IGcN1B9olmPTU=
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package cmd

import (
"context"
"fmt"
"log"
"os"
"path/filepath"
"regexp"
"strings"

"azureaiagent/internal/connections/pkg/connections"
"azureaiagent/internal/pkg/agents/agent_yaml"

"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"gopkg.in/yaml.v3"
)

// connectionRefPattern matches ${{connections.<name>.credentials.<key>}} references
// in agent manifest environment variable values.
var connectionRefPattern = regexp.MustCompile(`\$\{\{connections\.([^.]+)\.credentials\.([^}]+)\}\}`)

// resolveConnectionCredentials reads the agent manifest from projectDir,
// scans environment_variables for ${{connections.<name>.credentials.<key>}} patterns,
// fetches credential values from the Foundry data plane, and returns them as
// KEY=VALUE strings ready to inject into the agent process environment.
//
// This is additive to existing env var handling in run.go:
// - ${VAR} references are already resolved via loadAzdEnvironment
// - ${{connections...}} references are resolved here via data-plane API
// - Literal values pass through unchanged
//
// Returns nil (no error) if no manifest is found, no env vars are declared,
// or no connection references are present — the agent still starts normally.
func resolveConnectionCredentials(
ctx context.Context,
projectDir string,
endpoint string,
) ([]string, error) {
if endpoint == "" {
return nil, nil
}

// Find and parse the agent manifest
manifestPath := findManifestInDir(projectDir)
if manifestPath == "" {
return nil, nil
}

manifestBytes, err := os.ReadFile(manifestPath)

Check failure on line 53 in cli/azd/extensions/azure.ai.agents/internal/cmd/connection_credentials.go

View workflow job for this annotation

GitHub Actions / lint / golangci-lint (ubuntu-latest)

G304: Potential file inclusion via variable (gosec)
if err != nil {
log.Printf("run: could not read manifest %s: %v", manifestPath, err)
return nil, nil
}

// Try parsing as AgentManifest (agent.manifest.yaml — has "template:" wrapper)
var envVars []agent_yaml.EnvironmentVariable

manifest, err := agent_yaml.LoadAndValidateAgentManifest(manifestBytes)
if err == nil {
if containerAgent, ok := manifest.Template.(agent_yaml.ContainerAgent); ok &&
containerAgent.EnvironmentVariables != nil {
envVars = *containerAgent.EnvironmentVariables
}
}

// Fall back to parsing as ContainerAgent directly (agent.yaml — no wrapper)
if len(envVars) == 0 {
var agentDef agent_yaml.ContainerAgent
if yamlErr := yaml.Unmarshal(manifestBytes, &agentDef); yamlErr == nil &&
agentDef.EnvironmentVariables != nil {
envVars = *agentDef.EnvironmentVariables
}
Comment thread
Nathandrake229 marked this conversation as resolved.
}

if len(envVars) == 0 {
return nil, nil
}

// Scan for connection references
type connRef struct {
envName string // the env var name (e.g., TAVILY_API_KEY)
connName string // connection name (e.g., my-test-conn)
credKey string // credential key (e.g., x-api-key)
}

var refs []connRef
for _, ev := range envVars {
matches := connectionRefPattern.FindStringSubmatch(ev.Value)
if matches != nil {
refs = append(refs, connRef{
envName: ev.Name,
connName: matches[1],
credKey: matches[2],
})
}
}

if len(refs) == 0 {
return nil, nil
}

// Create data-plane credential and client
cred, err := azidentity.NewAzureDeveloperCLICredential(
&azidentity.AzureDeveloperCLICredentialOptions{},
)
if err != nil {
return nil, fmt.Errorf("failed to create credential for connection resolution: %w", err)
}

dpClient := connections.NewDataClient(endpoint, cred)

// Resolve each reference, caching per connection name
connCache := map[string]*connections.Connection{}
var result []string

for _, ref := range refs {
conn, cached := connCache[ref.connName]
if !cached {
conn, err = dpClient.GetConnectionWithCredentials(ctx, ref.connName)
if err != nil {
return nil, fmt.Errorf(
"failed to resolve credential for %s (connection %q): %w",
ref.envName, ref.connName, err,
)
}
connCache[ref.connName] = conn
}

// Look up the credential key
var credValue string
if ref.credKey == "key" && conn.Credentials != nil && conn.Credentials.Key != "" {
credValue = conn.Credentials.Key
} else if conn.Credentials != nil {
if v, ok := conn.Credentials.CustomKeys[ref.credKey]; ok {
credValue = v
}
}

if credValue == "" {
return nil, fmt.Errorf(
"credential key %q not found on connection %q (for env var %s)",
ref.credKey, ref.connName, ref.envName,
)
}

result = append(result, fmt.Sprintf("%s=%s", ref.envName, credValue))
// Log the key name only — NEVER log the value
log.Printf("run: resolved connection credential: %s (connection: %s, key: %s)",
ref.envName, ref.connName, ref.credKey)
}

if len(result) > 0 {
fmt.Fprintf(os.Stderr, " %d connection credential(s) resolved\n", len(result))
}

return result, nil
}

// findManifestInDir looks for an agent manifest or definition file in the given directory.
// Checks agent.yaml first (the definition the agent app uses), then agent.manifest.yaml.
// Returns the first file that exists and contains environment_variables with connection references.
func findManifestInDir(dir string) string {
// Check agent.yaml first — this is the file the agent app code references
candidates := []string{
Comment thread
trangevi marked this conversation as resolved.
"agent.yaml",
"agent.manifest.yaml",
"agent.yml",
"agent.manifest.yml",
}
for _, name := range candidates {
path := filepath.Join(dir, name)
if _, err := os.Stat(path); err == nil {
data, err := os.ReadFile(path)

Check failure on line 177 in cli/azd/extensions/azure.ai.agents/internal/cmd/connection_credentials.go

View workflow job for this annotation

GitHub Actions / lint / golangci-lint (ubuntu-latest)

G304: Potential file inclusion via variable (gosec)
if err == nil && strings.Contains(string(data), "${{connections.") {
return path
}
}
}
return ""
}
7 changes: 7 additions & 0 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ package cmd
import (
"fmt"

connectioncmd "azureaiagent/internal/connections/cmd"

"github.com/azure/azure-dev/cli/azd/pkg/azdext"
"github.com/fatih/color"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -61,5 +63,10 @@ func NewRootCommand() *cobra.Command {
rootCmd.AddCommand(newFilesCommand(extCtx))
rootCmd.AddCommand(newSessionCommand(extCtx))

// Connection commands — in separate package for easy lift-and-shift later.
// When the azd core namespace change lands, move this AddCommand call
// to the new root and update the import path.
rootCmd.AddCommand(connectioncmd.NewConnectionRootCommand(extCtx))

return rootCmd
}
11 changes: 11 additions & 0 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,17 @@ func runRun(ctx context.Context, flags *runFlags, noPrompt bool) error {
env = append(env, fmt.Sprintf("%s=%s", k, v))
}
env = appendFoundryEnvVars(env, azdEnvVars, runCtx.ServiceName)

// Resolve ${{connections.<name>.credentials.<key>}} references from the
// agent manifest's environment_variables section. These are fetched from
// the Foundry data plane at runtime and injected into the agent process.
if endpoint := azdEnvVars["AZURE_AI_PROJECT_ENDPOINT"]; endpoint != "" {
if connEnv, err := resolveConnectionCredentials(ctx, projectDir, endpoint); err != nil {
fmt.Fprintf(os.Stderr, "Warning: connection credential resolution failed: %s\n", err)
} else {
env = append(env, connEnv...)
}
Comment thread
Nathandrake229 marked this conversation as resolved.
Outdated
Comment thread
Nathandrake229 marked this conversation as resolved.
Outdated
}
}

url := fmt.Sprintf("http://localhost:%d", flags.port)
Expand Down
Loading
Loading