Skip to content
Merged
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
22 changes: 22 additions & 0 deletions .github/workflows/lint-ext-azure-ai-agents.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: ext-azure-ai-agents-ci

on:
pull_request:
paths:
- "cli/azd/extensions/azure.ai.agents/**"
- ".github/workflows/lint-ext-azure-ai-agents.yml"
branches: [main]

concurrency:
group: ${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true

permissions:
contents: read
pull-requests: write

jobs:
lint:
uses: ./.github/workflows/lint-go.yml
with:
working-directory: cli/azd/extensions/azure.ai.agents
17 changes: 17 additions & 0 deletions cli/azd/extensions/azure.ai.agents/.golangci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
version: "2"

linters:
default: none
enable:
- gosec
- lll
- unused
- errorlint
settings:
lll:
line-length: 220
tab-width: 4

formatters:
enable:
- gofmt
2 changes: 1 addition & 1 deletion cli/azd/extensions/azure.ai.agents/go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module azureaiagent

go 1.25.0
go 1.26.0

require (
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0
Expand Down
1 change: 1 addition & 0 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func setupDebugLogging(flags *pflag.FlagSet) {
currentDate := time.Now().Format("2006-01-02")
logFileName := fmt.Sprintf("azd-ai-agents-%s.log", currentDate)

//nolint:gosec // log file name is generated locally from date and not user-controlled
logFile, err := os.OpenFile(logFileName, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
logFile = os.Stderr
Expand Down
53 changes: 17 additions & 36 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -699,38 +699,6 @@ func ensureAzureContext(
return azureContext, project, env, nil
}

func (a *InitAction) validateFlags(flags *initFlags) error {
if flags.manifestPointer != "" {
// Check if it's a valid URL
if _, err := url.ParseRequestURI(flags.manifestPointer); err != nil {
// If not a valid URL, check if it's an existing local file path
if _, fileErr := os.Stat(flags.manifestPointer); fileErr != nil {
return fmt.Errorf("manifest pointer '%s' is neither a valid URI nor an existing file path", flags.manifestPointer)
}
}
}

return nil
}

func (a *InitAction) promptForMissingValues(ctx context.Context, azdClient *azdext.AzdClient, flags *initFlags) error {
if flags.manifestPointer == "" {
resp, err := azdClient.Prompt().Prompt(ctx, &azdext.PromptRequest{
Options: &azdext.PromptOptions{
Message: "Enter the location of the agent manifest",
IgnoreHintKeys: true,
},
})
if err != nil {
return fmt.Errorf("prompting for agent manifest pointer: %w", err)
}

flags.manifestPointer = resp.Value
}

return nil
}

type FoundryProject struct {
SubscriptionId string `json:"subscriptionId"`
ResourceGroupName string `json:"resourceGroupName"`
Expand All @@ -749,7 +717,11 @@ func extractProjectDetails(projectResourceId string) (*FoundryProject, error) {

matches := regex.FindStringSubmatch(projectResourceId)
if matches == nil || len(matches) != 5 {
return nil, fmt.Errorf("the given Microsoft Foundry project ID does not match expected format: /subscriptions/[SUBSCRIPTION_ID]/resourceGroups/[RESOURCE_GROUP]/providers/Microsoft.CognitiveServices/accounts/[ACCOUNT_NAME]/projects/[PROJECT_NAME]")
return nil, fmt.Errorf(
"the given Microsoft Foundry project ID does not match expected format: " +
"/subscriptions/[SUBSCRIPTION_ID]/resourceGroups/[RESOURCE_GROUP]/providers/" +
"Microsoft.CognitiveServices/accounts/[ACCOUNT_NAME]/projects/[PROJECT_NAME]",
)
}

// Extract the components
Expand Down Expand Up @@ -1043,7 +1015,7 @@ func (a *InitAction) isRegistryUrl(manifestPointer string) (bool, *RegistryManif
func (a *InitAction) downloadAgentYaml(
ctx context.Context, manifestPointer string, targetDir string) (*agent_yaml.AgentManifest, string, error) {
if manifestPointer == "" {
return nil, "", fmt.Errorf("The path to an agent manifest need to be provided (manifestPointer cannot be empty).")
return nil, "", fmt.Errorf("the path to an agent manifest needs to be provided (manifestPointer cannot be empty)")
}

var content []byte
Expand All @@ -1052,12 +1024,13 @@ func (a *InitAction) downloadAgentYaml(
var urlInfo *GitHubUrlInfo
var ghCli *github.Cli
var console input.Console
var useGhCli bool = false
useGhCli := false

// Check if manifestPointer is a local file path or a URI
if a.isLocalFilePath(manifestPointer) {
// Handle local file path
fmt.Printf("Reading agent.yaml from local file: %s\n", manifestPointer)
//nolint:gosec // manifest path is an explicit user-provided local path
content, err = os.ReadFile(manifestPointer)
if err != nil {
return nil, "", exterrors.Validation(
Expand Down Expand Up @@ -1169,6 +1142,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")
//nolint:gosec // URL is constrained to GitHub API endpoint built from parsed GitHub URL
resp, err := a.httpClient.Do(req)
if err == nil {
defer resp.Body.Close()
Expand Down Expand Up @@ -1316,6 +1290,7 @@ func (a *InitAction) downloadAgentYaml(
}

// Create target directory if it doesn't exist
//nolint:gosec // project scaffold directory should be readable and traversable
if err := os.MkdirAll(targetDir, 0755); err != nil {
return nil, "", fmt.Errorf("creating target directory %s: %w", targetDir, err)
}
Expand Down Expand Up @@ -1796,12 +1771,14 @@ func downloadDirectoryContents(
return fmt.Errorf("failed to download file %s: %w", itemPath, err)
}

//nolint:gosec // downloaded project files are intended to be readable by project tooling
if err := os.WriteFile(itemLocalPath, []byte(fileContent), 0644); err != nil {
return fmt.Errorf("failed to write file %s: %w", itemLocalPath, err)
}
} else if itemType == "dir" {
// Recursively download subdirectory
fmt.Printf("Downloading directory: %s\n", itemPath)
//nolint:gosec // scaffolded directories are intended to be readable/traversable
if err := os.MkdirAll(itemLocalPath, 0755); err != nil {
return fmt.Errorf("failed to create directory %s: %w", itemLocalPath, err)
}
Expand Down Expand Up @@ -1831,6 +1808,7 @@ func downloadDirectoryContentsWithoutGhCli(
}
req.Header.Set("Accept", "application/vnd.github.v3+json")

//nolint:gosec // URL is explicitly constructed for GitHub contents API
resp, err := httpClient.Do(req)
if err != nil {
return fmt.Errorf("failed to get directory contents: %w", err)
Expand Down Expand Up @@ -1887,6 +1865,7 @@ func downloadDirectoryContentsWithoutGhCli(
}
fileReq.Header.Set("Accept", "application/vnd.github.v3.raw")

//nolint:gosec // URL is explicitly constructed for GitHub contents API
fileResp, err := httpClient.Do(fileReq)
if err != nil {
return fmt.Errorf("failed to download file %s: %w", itemPath, err)
Expand All @@ -1897,17 +1876,19 @@ func downloadDirectoryContentsWithoutGhCli(
}

fileContent, err := io.ReadAll(fileResp.Body)
fileResp.Body.Close()
_ = fileResp.Body.Close()
if err != nil {
return fmt.Errorf("failed to read file content %s: %w", itemPath, err)
}

//nolint:gosec // downloaded project files are intended to be readable by project tooling
if err := os.WriteFile(itemLocalPath, fileContent, 0644); err != nil {
return fmt.Errorf("failed to write file %s: %w", itemLocalPath, err)
}
} else if itemType == "dir" {
// Recursively download subdirectory
fmt.Printf("Downloading directory: %s\n", itemPath)
//nolint:gosec // scaffolded directories are intended to be readable/traversable
if err := os.MkdirAll(itemLocalPath, 0755); err != nil {
return fmt.Errorf("failed to create directory %s: %w", itemLocalPath, err)
}
Expand Down
12 changes: 10 additions & 2 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/init_copy.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ func copyDirectory(src, dst string) error {

if d.IsDir() {
// Create directory and continue processing its contents
//nolint:gosec // copied project directories should remain readable/traversable
return os.MkdirAll(dstPath, 0755)
}

Expand All @@ -172,23 +173,30 @@ func copyDirectory(src, dst string) error {
// copyFile copies a single file from src to dst.
func copyFile(src, dst string) error {
// Create the destination directory if it doesn't exist
//nolint:gosec // copied project directories should remain readable/traversable
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
return err
}

// Open source file
//nolint:gosec // source path is computed from validated copy traversal
srcFile, err := os.Open(src)
if err != nil {
return err
}
defer srcFile.Close()
defer func() {
_ = srcFile.Close()
}()

// Create destination file
//nolint:gosec // destination path is computed from validated copy traversal
dstFile, err := os.Create(dst)
if err != nil {
return err
}
defer dstFile.Close()
defer func() {
_ = dstFile.Close()
}()

// Copy file contents
_, err = srcFile.WriteTo(dstFile)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ func (a *InitFromCodeAction) scaffoldTemplate(ctx context.Context, azdClient *az
req.Header.Set("Accept", "application/vnd.github.v3+json")
setGitHubAuthHeader(req, ghToken)

//nolint:gosec // URL is explicitly constructed for GitHub API tree endpoint
resp, err := a.httpClient.Do(req)
if err != nil {
return fmt.Errorf("fetching repo tree: %w", err)
Expand Down Expand Up @@ -343,6 +344,7 @@ func (a *InitFromCodeAction) scaffoldTemplate(ctx context.Context, azdClient *az
// Create parent directories
dir := filepath.Dir(localPath)
if dir != "." {
//nolint:gosec // scaffolded directories are intended to be readable/traversable
if err := os.MkdirAll(dir, 0755); err != nil {
_ = spinner.Stop(ctx)
return fmt.Errorf("creating directory %s: %w", dir, err)
Expand All @@ -357,6 +359,7 @@ func (a *InitFromCodeAction) scaffoldTemplate(ctx context.Context, azdClient *az
}
setGitHubAuthHeader(fileReq, ghToken)

//nolint:gosec // URL is from GitHub tree API entries for the selected template
fileResp, err := a.httpClient.Do(fileReq)
if err != nil {
_ = spinner.Stop(ctx)
Expand All @@ -369,12 +372,13 @@ func (a *InitFromCodeAction) scaffoldTemplate(ctx context.Context, azdClient *az
}

content, err := io.ReadAll(fileResp.Body)
fileResp.Body.Close()
_ = fileResp.Body.Close()
if err != nil {
_ = spinner.Stop(ctx)
return fmt.Errorf("reading %s: %w", f.Path, err)
}

//nolint:gosec // scaffolded files should remain readable by project tooling
if err := os.WriteFile(localPath, content, 0644); err != nil {
_ = spinner.Stop(ctx)
return fmt.Errorf("writing %s: %w", localPath, err)
Expand Down Expand Up @@ -556,7 +560,7 @@ func (a *InitFromCodeAction) createDefinitionFromLocalAgent(ctx context.Context)
}
}

if selectedIdx < int32(len(projects)) {
if selectedIdx >= 0 && int(selectedIdx) < len(projects) {
// User selected an existing Foundry project
selectedProject := projects[selectedIdx]

Expand Down Expand Up @@ -641,7 +645,7 @@ func (a *InitFromCodeAction) createDefinitionFromLocalAgent(ctx context.Context)
}

deploymentIdx := *deployResp.Value
if deploymentIdx < int32(len(deployments)) {
if deploymentIdx >= 0 && int(deploymentIdx) < len(deployments) {
// User selected an existing deployment
d := deployments[deploymentIdx]
existingDeployment = &d
Expand Down Expand Up @@ -1238,6 +1242,7 @@ func (a *InitFromCodeAction) lookupAcrResourceId(ctx context.Context, subscripti
// writeDefinitionToSrcDir writes a ContainerAgent to a YAML file in the src directory and returns the path
func (a *InitFromCodeAction) writeDefinitionToSrcDir(definition *agent_yaml.ContainerAgent, srcDir string) (string, error) {
// Ensure the src directory exists
//nolint:gosec // scaffold directory should be readable/traversable for project tools
if err := os.MkdirAll(srcDir, 0755); err != nil {
return "", fmt.Errorf("creating src directory: %w", err)
}
Expand All @@ -1252,6 +1257,7 @@ func (a *InitFromCodeAction) writeDefinitionToSrcDir(definition *agent_yaml.Cont
}

// Write to the file
//nolint:gosec // generated manifest file should be readable by tooling and users
if err := os.WriteFile(definitionPath, content, 0644); err != nil {
return "", fmt.Errorf("writing definition to file: %w", err)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -426,6 +426,7 @@ func TestWriteDefinitionToSrcDir(t *testing.T) {
t.Errorf("path = %q, want %q", resultPath, expectedPath)
}

//nolint:gosec // test fixture path is created within test temp directory
content, err := os.ReadFile(resultPath)
if err != nil {
t.Fatalf("failed to read written file: %v", err)
Expand Down Expand Up @@ -467,6 +468,7 @@ func TestWriteDefinitionToSrcDir(t *testing.T) {

dir := t.TempDir()
existingFile := filepath.Join(dir, "agent.yaml")
//nolint:gosec // test fixture file permissions are intentional
if err := os.WriteFile(existingFile, []byte("old content"), 0644); err != nil {
t.Fatalf("write existing file: %v", err)
}
Expand All @@ -484,6 +486,7 @@ func TestWriteDefinitionToSrcDir(t *testing.T) {
t.Fatalf("unexpected error: %v", err)
}

//nolint:gosec // test fixture path is created within test temp directory
content, err := os.ReadFile(existingFile)
if err != nil {
t.Fatalf("failed to read file: %v", err)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,11 @@ func TestCopyDirectory_RefusesToCopyIntoSubtree(t *testing.T) {
src := filepath.Join(root, "src")
dst := filepath.Join(src, "child")

//nolint:gosec // test fixture directory permissions are intentional
if err := os.MkdirAll(src, 0755); err != nil {
t.Fatalf("mkdir src: %v", err)
}
//nolint:gosec // test fixture file permissions are intentional
if err := os.WriteFile(filepath.Join(src, "file.txt"), []byte("hello"), 0644); err != nil {
t.Fatalf("write src file: %v", err)
}
Expand All @@ -102,6 +104,7 @@ func TestCopyDirectory_NoOpWhenSamePath(t *testing.T) {
t.Parallel()

dir := t.TempDir()
//nolint:gosec // test fixture file permissions are intentional
if err := os.WriteFile(filepath.Join(dir, "file.txt"), []byte("hello"), 0644); err != nil {
t.Fatalf("write file: %v", err)
}
Expand All @@ -120,6 +123,7 @@ func TestValidateLocalContainerAgentCopy_AllowsReinitInPlace(t *testing.T) {

dir := t.TempDir()
manifestPointer := filepath.Join(dir, "agent.yaml")
//nolint:gosec // test fixture file permissions are intentional
if err := os.WriteFile(manifestPointer, []byte("name: test"), 0644); err != nil {
t.Fatalf("write agent.yaml: %v", err)
}
Expand Down
2 changes: 2 additions & 0 deletions cli/azd/extensions/azure.ai.agents/internal/cmd/listen.go
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ func kindEnvUpdate(ctx context.Context, azdClient *azdext.AzdClient, project *az
fullPath := filepath.Join(project.Path, servicePath)
agentYamlPath := filepath.Join(fullPath, "agent.yaml")

//nolint:gosec // agentYamlPath is resolved from project/service paths in current workspace
data, err := os.ReadFile(agentYamlPath)
if err != nil {
return fmt.Errorf("failed to read YAML file: %w", err)
Expand Down Expand Up @@ -207,6 +208,7 @@ func containerAgentHandling(ctx context.Context, azdClient *azdext.AzdClient, pr
fullPath := filepath.Join(project.Path, servicePath)
agentYamlPath := filepath.Join(fullPath, "agent.yaml")

//nolint:gosec // agentYamlPath is resolved from project/service paths in current workspace
data, err := os.ReadFile(agentYamlPath)
if err != nil {
return nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ This is useful for troubleshooting agent startup issues or monitoring agent beha

action := &MonitorAction{
AgentContext: agentContext,
flags: flags,
flags: flags,
}

return action.Run(ctx)
Expand Down
Loading
Loading