Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
05d71b0
Add support for init from code
trangevi Feb 10, 2026
d99fa3b
Code comment
trangevi Feb 10, 2026
3bf9416
Addressing some comments
trangevi Feb 10, 2026
abc96b0
make sure src is provided for local code story for now
trangevi Feb 10, 2026
f819843
some fixes
trangevi Feb 10, 2026
0bb11ef
Add manual defaulting
trangevi Feb 11, 2026
51f5df8
Refactoring a bunch based on expected experience
trangevi Feb 12, 2026
fc1d695
broken spinner
trangevi Feb 13, 2026
c6581fa
Remove env var print
trangevi Feb 13, 2026
e96d6b1
Add support for --project-id
trangevi Feb 13, 2026
099f4a1
Missing env var
trangevi Feb 17, 2026
e5729a9
Address feedback
trangevi Feb 18, 2026
6c75c7e
Switch to public repo
trangevi Feb 18, 2026
b01af47
Changes for existing ACR
trangevi Feb 19, 2026
44476ca
More for existing ACR support
trangevi Feb 19, 2026
319113c
app insights changes
trangevi Feb 20, 2026
870ae3d
Point to branch for now
trangevi Feb 20, 2026
16e3b1f
tests
trangevi Feb 20, 2026
7084b72
Merge branch 'main' into trangevi/init-from-code
trangevi Feb 20, 2026
8bacb51
Integrate with azd extension model handling
trangevi Feb 20, 2026
f8c821c
Move model handling
trangevi Feb 20, 2026
3f04250
Update cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go
trangevi Feb 20, 2026
90c1363
Apply suggestion from @Copilot
trangevi Feb 20, 2026
6db93f7
Apply suggestions from code review
trangevi Feb 20, 2026
8ea106c
Proper http client handling
trangevi Feb 20, 2026
aecfb79
Merge branch 'trangevi/init-from-code' of https://github.com/Azure/az…
trangevi Feb 20, 2026
8d78814
Remove custom string matching helpers in favor of strings.Contains (#…
Copilot Feb 20, 2026
820824a
cspell
trangevi Feb 20, 2026
aa3a00e
Merge branch 'trangevi/init-from-code' of https://github.com/Azure/az…
trangevi Feb 20, 2026
f6a8301
Fix path traversal vulnerability in init_from_code template file down…
Copilot Feb 20, 2026
aaab505
fix: capture error from loadAiCatalog in selectNewModel (#6831)
Copilot Feb 20, 2026
59ffa2d
Code review
trangevi Feb 23, 2026
34da3d6
Put the status check before attempting to use the response
trangevi Feb 23, 2026
581ca56
Merge branch 'trangevi/init-from-code' of https://github.com/Azure/az…
trangevi Feb 23, 2026
293409b
Fix path handling
trangevi Feb 23, 2026
f8505ac
Remove unused code
trangevi Feb 23, 2026
b112af1
cspell
trangevi Feb 23, 2026
d47ac2a
Address PR comment
trangevi Feb 24, 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
7 changes: 7 additions & 0 deletions cli/azd/.vscode/cspell.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,13 @@ overrides:
words:
- aoai
- azdaiagent
- filename: extensions/azure.ai.agents/internal/cmd/init_from_code.go
words:
- aoai
- aiservices
- azdaiagent
- myregistry
- posixpath
- filename: extensions/azure.ai.agents/internal/cmd/listen.go
words:
- hostedagent
Expand Down
1 change: 1 addition & 0 deletions cli/azd/extensions/azure.ai.agents/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/appcontainers/armappcontainers v1.1.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthorization/v3 v3.0.0-beta.2
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/cognitiveservices/armcognitiveservices v1.8.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v1.2.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armresources v1.2.0
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resources/armsubscriptions v1.3.0
github.com/azure/azure-dev/cli/azd v0.0.0-20260210235925-af2fc5ae4ef3
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 @@ -21,6 +21,8 @@ github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/authorization/armauthoriza
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/containerregistry/armcontainerregistry v1.2.0 h1:DWlwvVV5r/Wy1561nZ3wrpI1/vDIBRY/Wd1HWaRBZWA=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/containerregistry/armcontainerregistry v1.2.0/go.mod h1:E7ltexgRDmeJ0fJWv0D/HLwY2xbDdN+uv+X2uZtOx3w=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0 h1:PTFGRSlMKCQelWwxUyYVEUqseBJVemLyqWJjvMyt0do=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/internal/v2 v2.0.0/go.mod h1:LRr2FzBTQlONPPa5HREE5+RjSCTXl7BwOvYOaWTqCaI=
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/managementgroups/armmanagementgroups v1.0.0 h1:pPvTJ1dY0sA35JOeFq6TsY2xj6Z85Yo23Pj4wCCvu4o=
Expand Down
188 changes: 120 additions & 68 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"regexp"
"strconv"
"strings"
"time"

"azureaiagent/internal/pkg/agents/agent_yaml"
"azureaiagent/internal/pkg/agents/registry_api"
Expand Down Expand Up @@ -68,6 +69,7 @@ type InitAction struct {
environment *azdext.Environment
flags *initFlags
deploymentDetails []project.Deployment
httpClient *http.Client
}

// GitHubUrlInfo holds parsed information from a GitHub URL
Expand Down Expand Up @@ -130,46 +132,63 @@ func newInitCommand(rootFlags *rootFlagsDefinition) *cobra.Command {
return fmt.Errorf("failed waiting for debugger: %w", err)
}

azureContext, projectConfig, environment, err := ensureAzureContext(ctx, flags, azdClient)
if err != nil {
return fmt.Errorf("failed to ground into a project context: %w", err)
var httpClient = &http.Client{
Timeout: 30 * time.Second,
}

credential, err := azidentity.NewAzureDeveloperCLICredential(&azidentity.AzureDeveloperCLICredentialOptions{
TenantID: azureContext.Scope.TenantId,
AdditionallyAllowedTenants: []string{"*"},
})
if err != nil {
return fmt.Errorf("failed to create azure credential: %w", err)
}
if flags.manifestPointer != "" {
azureContext, projectConfig, environment, err := ensureAzureContext(ctx, flags, azdClient)
if err != nil {
return fmt.Errorf("failed to ground into a project context: %w", err)
}

console := input.NewConsole(
false, // noPrompt
true, // isTerminal
input.Writers{Output: os.Stdout},
input.ConsoleHandles{
Stderr: os.Stderr,
Stdin: os.Stdin,
Stdout: os.Stdout,
},
nil, // formatter
nil, // externalPromptCfg
)

action := &InitAction{
azdClient: azdClient,
// azureClient: azure.NewAzureClient(credential),
azureContext: azureContext,
// composedResources: getComposedResourcesResponse.Resources,
console: console,
credential: credential,
projectConfig: projectConfig,
environment: environment,
flags: flags,
}
credential, err := azidentity.NewAzureDeveloperCLICredential(&azidentity.AzureDeveloperCLICredentialOptions{
TenantID: azureContext.Scope.TenantId,
AdditionallyAllowedTenants: []string{"*"},
})
if err != nil {
return fmt.Errorf("failed to create azure credential: %w", err)
}

console := input.NewConsole(
false, // noPrompt
true, // isTerminal
input.Writers{Output: os.Stdout},
input.ConsoleHandles{
Stderr: os.Stderr,
Stdin: os.Stdin,
Stdout: os.Stdout,
},
nil, // formatter
nil, // externalPromptCfg
)

action := &InitAction{
azdClient: azdClient,
// azureClient: azure.NewAzureClient(credential),
azureContext: azureContext,
// composedResources: getComposedResourcesResponse.Resources,
console: console,
credential: credential,
projectConfig: projectConfig,
environment: environment,
flags: flags,
httpClient: httpClient,
}

if err := action.Run(ctx); err != nil {
return fmt.Errorf("failed to run start action: %w", err)
}
} else {
action := &InitFromCodeAction{
azdClient: azdClient,
flags: flags,
httpClient: httpClient,
}

if err := action.Run(ctx); err != nil {
return fmt.Errorf("failed to run start action: %w", err)
if err := action.Run(ctx); err != nil {
return fmt.Errorf("failed to run init from code action: %w", err)
}
}

return nil
Expand Down Expand Up @@ -197,21 +216,35 @@ func (a *InitAction) Run(ctx context.Context) error {
color.Green("Initializing AI agent project...")
fmt.Println()

// If --project-id is given
if a.flags.projectResourceId != "" {
// projectResourceId is a string of the format
// /subscriptions/[AZURE_SUBSCRIPTION]/resourceGroups/[AZURE_RESOURCE_GROUP]/providers/Microsoft.CognitiveServices/accounts/[AI_ACCOUNT_NAME]/projects/[AI_PROJECT_NAME]
// extract each of those fields from the string, issue an error if it doesn't match the format
fmt.Println("Setting up your azd environment to use the provided Microsoft Foundry project resource ID...")
if err := a.parseAndSetProjectResourceId(ctx); err != nil {
return fmt.Errorf("failed to parse project resource ID: %w", err)
// If src path is absolute, convert it to relative path compared to the azd project path
if a.flags.src != "" && filepath.IsAbs(a.flags.src) {
projectResponse, err := a.azdClient.Project().Get(ctx, &azdext.EmptyRequest{})
if err != nil {
return fmt.Errorf("failed to get project path: %w", err)
}

color.Green("\nYour azd environment has been initialized to use your existing Microsoft Foundry project.")
relPath, err := filepath.Rel(projectResponse.Project.Path, a.flags.src)
if err != nil {
return fmt.Errorf("failed to convert src path to relative path: %w", err)
}
a.flags.src = relPath
}

// If --manifest is given
if a.flags.manifestPointer != "" {
// If --project-id is given
if a.flags.projectResourceId != "" {
// projectResourceId is a string of the format
// /subscriptions/[AZURE_SUBSCRIPTION]/resourceGroups/[AZURE_RESOURCE_GROUP]/providers/Microsoft.CognitiveServices/accounts/[AI_ACCOUNT_NAME]/projects/[AI_PROJECT_NAME]
// extract each of those fields from the string, issue an error if it doesn't match the format
fmt.Println("Setting up your azd environment to use the provided Microsoft Foundry project resource ID...")
if err := a.parseAndSetProjectResourceId(ctx); err != nil {
return fmt.Errorf("failed to parse project resource ID: %w", err)
}

color.Green("\nYour azd environment has been initialized to use your existing Microsoft Foundry project.")
}

// Validate that the manifest pointer is either a valid URL or existing file path
isValidURL := false
isValidFile := false
Expand Down Expand Up @@ -244,26 +277,16 @@ func (a *InitAction) Run(ctx context.Context) error {
color.Green("\nAI agent definition added to your azd project successfully!")
}

// // Validate command flags
// if err := a.validateFlags(flags); err != nil {
// return err
// }

// // Prompt for any missing input values
// if err := a.promptForMissingValues(ctx, a.azdClient, flags); err != nil {
// return fmt.Errorf("collecting required information: %w", err)
// }

return nil
}

func ensureProject(ctx context.Context, flags *initFlags, azdClient *azdext.AzdClient) (*azdext.ProjectConfig, error) {
projectResponse, err := azdClient.Project().Get(ctx, &azdext.EmptyRequest{})
if err != nil {
fmt.Println("Lets get your project initialized.")
fmt.Println("Let's get your project initialized.")

// Environment creation is handled separately in ensureEnvironment
initArgs := []string{"init", "--minimal"}
initArgs := []string{"init", "-t", "Azure-Samples/azd-ai-starter-basic"}

// We don't have a project yet
// Dispatch a workflow to init the project
Expand Down Expand Up @@ -481,7 +504,8 @@ func ensureAzureContext(

if azureContext.Scope.SubscriptionId == "" {
fmt.Print()
fmt.Println("It looks like we first need to connect to your Azure subscription.")
fmt.Println("We need to connect to your Azure subscription. This will be the subscription which contains your ")
fmt.Println("Foundry project and where your resources will be provisioned.")

subscriptionResponse, err := azdClient.Prompt().PromptSubscription(ctx, &azdext.PromptSubscriptionRequest{})
if err != nil {
Expand Down Expand Up @@ -515,7 +539,7 @@ func ensureAzureContext(
if azureContext.Scope.Location == "" {
fmt.Println()
fmt.Println(
"Next, we need to select a default Azure location that will be used as the target for your infrastructure.",
"Next, we need to select a default Azure location that will be used as the target for your resources.",
)

locationResponse, err := azdClient.Prompt().PromptLocation(ctx, &azdext.PromptLocationRequest{
Expand Down Expand Up @@ -999,7 +1023,7 @@ func (a *InitAction) downloadAgentYaml(
req, err := http.NewRequestWithContext(ctx, http.MethodGet, fileApiUrl, nil)
if err == nil {
req.Header.Set("Accept", "application/vnd.github.v3.raw")
resp, err := http.DefaultClient.Do(req)
resp, err := a.httpClient.Do(req)
if err == nil {
defer resp.Body.Close()
if resp.StatusCode == http.StatusOK {
Expand Down Expand Up @@ -1176,7 +1200,7 @@ func (a *InitAction) downloadAgentYaml(
if isHostedContainer {
// For container agents, download the entire parent directory
fmt.Println("Downloading full directory for container agent")
err := downloadParentDirectory(ctx, urlInfo, targetDir, ghCli, console, useGhCli)
err := downloadParentDirectory(ctx, urlInfo, targetDir, ghCli, console, useGhCli, a.httpClient)
if err != nil {
return nil, "", fmt.Errorf("downloading parent directory: %w", err)
}
Expand Down Expand Up @@ -1322,6 +1346,20 @@ func (a *InitAction) addToProject(ctx context.Context, targetDir string, agentMa
}

func (a *InitAction) populateContainerSettings(ctx context.Context) (*project.ContainerSettings, error) {
if a.flags.NoPrompt {
fmt.Printf("No prompt mode enabled, using default container settings\n")
return &project.ContainerSettings{
Resources: &project.ResourceSettings{
Memory: project.DefaultMemory,
Cpu: project.DefaultCpu,
},
Scale: &project.ScaleSettings{
MinReplicas: project.DefaultMinReplicas,
MaxReplicas: project.DefaultMaxReplicas,
},
}, nil
}

// Default values
defaultMemory := project.DefaultMemory
defaultCpu := project.DefaultCpu
Expand Down Expand Up @@ -1526,7 +1564,7 @@ func (a *InitAction) parseGitHubUrl(ctx context.Context, manifestPointer string)
}

func downloadParentDirectory(
ctx context.Context, urlInfo *GitHubUrlInfo, targetDir string, ghCli *github.Cli, console input.Console, useGhCli bool) error {
ctx context.Context, urlInfo *GitHubUrlInfo, targetDir string, ghCli *github.Cli, console input.Console, useGhCli bool, httpClient *http.Client) error {

// Get parent directory by removing the filename from the file path
pathParts := strings.Split(urlInfo.FilePath, "/")
Expand All @@ -1544,7 +1582,7 @@ func downloadParentDirectory(
return fmt.Errorf("failed to download directory contents with GH CLI: %w", err)
}
} else {
if err := downloadDirectoryContentsWithoutGhCli(ctx, urlInfo.RepoSlug, parentDirPath, urlInfo.Branch, targetDir); err != nil {
if err := downloadDirectoryContentsWithoutGhCli(ctx, urlInfo.RepoSlug, parentDirPath, urlInfo.Branch, targetDir, httpClient); err != nil {
return fmt.Errorf("failed to download directory contents without GH CLI: %w", err)
}
}
Expand Down Expand Up @@ -1624,7 +1662,7 @@ func downloadDirectoryContents(
}

func downloadDirectoryContentsWithoutGhCli(
ctx context.Context, repoSlug string, dirPath string, branch string, localPath string) error {
ctx context.Context, repoSlug string, dirPath string, branch string, localPath string, httpClient *http.Client) error {

// Get directory contents using GitHub API directly
apiUrl := fmt.Sprintf("https://api.github.com/repos/%s/contents/%s", repoSlug, dirPath)
Expand All @@ -1638,7 +1676,7 @@ func downloadDirectoryContentsWithoutGhCli(
}
req.Header.Set("Accept", "application/vnd.github.v3+json")

resp, err := http.DefaultClient.Do(req)
resp, err := httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to get directory contents: %w", err)
}
Expand Down Expand Up @@ -1694,17 +1732,17 @@ func downloadDirectoryContentsWithoutGhCli(
}
fileReq.Header.Set("Accept", "application/vnd.github.v3.raw")

fileResp, err := http.DefaultClient.Do(fileReq)
fileResp, err := httpClient.Do(fileReq)
if err != nil {
return fmt.Errorf("failed to download file %s: %w", itemPath, err)
}
defer fileResp.Body.Close()

if fileResp.StatusCode != http.StatusOK {
return fmt.Errorf("failed to download file %s: status %d", itemPath, fileResp.StatusCode)
}

fileContent, err := io.ReadAll(fileResp.Body)
fileResp.Body.Close()
if err != nil {
return fmt.Errorf("failed to read file content %s: %w", itemPath, err)
}
Expand All @@ -1720,11 +1758,25 @@ func downloadDirectoryContentsWithoutGhCli(
}

// Recursively download directory contents
if err := downloadDirectoryContentsWithoutGhCli(ctx, repoSlug, itemPath, branch, itemLocalPath); err != nil {
if err := downloadDirectoryContentsWithoutGhCli(ctx, repoSlug, itemPath, branch, itemLocalPath, httpClient); err != nil {
return fmt.Errorf("failed to download subdirectory %s: %w", itemPath, err)
}
}
}

return nil
}

func (a *InitAction) setEnvVar(ctx context.Context, key, value string) error {
_, err := a.azdClient.Environment().SetValue(ctx, &azdext.SetEnvRequest{
EnvName: a.environment.Name,
Key: key,
Value: value,
})
if err != nil {
return fmt.Errorf("failed to set environment variable %s=%s: %w", key, value, err)
}

fmt.Printf("Set environment variable: %s=%s\n", key, value)
return nil
}
Loading
Loading