diff --git a/.github/workflows/lint-ext-azure-ai-agents.yml b/.github/workflows/lint-ext-azure-ai-agents.yml new file mode 100644 index 00000000000..26b49b99ef5 --- /dev/null +++ b/.github/workflows/lint-ext-azure-ai-agents.yml @@ -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 diff --git a/cli/azd/extensions/azure.ai.agents/.golangci.yaml b/cli/azd/extensions/azure.ai.agents/.golangci.yaml new file mode 100644 index 00000000000..b88a74c6a0b --- /dev/null +++ b/cli/azd/extensions/azure.ai.agents/.golangci.yaml @@ -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 diff --git a/cli/azd/extensions/azure.ai.agents/go.mod b/cli/azd/extensions/azure.ai.agents/go.mod index ecfe58db95e..c34e350acb6 100644 --- a/cli/azd/extensions/azure.ai.agents/go.mod +++ b/cli/azd/extensions/azure.ai.agents/go.mod @@ -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 diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/debug.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/debug.go index c207f0f5ded..6b7272cb7ae 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/debug.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/debug.go @@ -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 diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go index f9d03e6af4e..7de32ade10d 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -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"` @@ -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 @@ -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 @@ -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( @@ -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() @@ -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) } @@ -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) } @@ -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) @@ -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) @@ -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) } diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_copy.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_copy.go index f3c82b77f81..c7cc67c1896 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_copy.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_copy.go @@ -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) } @@ -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) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go index 7cfbd660dfc..6123d640812 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code.go @@ -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) @@ -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) @@ -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) @@ -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) @@ -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] @@ -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 @@ -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) } @@ -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) } diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code_test.go index 4f63f8da4b7..06b3a1e68ac 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_from_code_test.go @@ -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) @@ -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) } @@ -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) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go index 7ec7503be9f..6fb935235a3 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init_test.go @@ -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) } @@ -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) } @@ -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) } diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/listen.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/listen.go index 46d26f73a58..10064e7b049 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/listen.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/listen.go @@ -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) @@ -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 diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/monitor.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/monitor.go index 3e11267778d..671f9ae6b16 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/monitor.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/monitor.go @@ -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) diff --git a/cli/azd/extensions/azure.ai.agents/internal/cmd/show.go b/cli/azd/extensions/azure.ai.agents/internal/cmd/show.go index d31e8761b1e..3a4c4e649f5 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/show.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/show.go @@ -59,7 +59,7 @@ replica configuration, and any error messages.`, action := &ShowAction{ AgentContext: agentContext, - flags: flags, + flags: flags, } return action.Run(ctx) diff --git a/cli/azd/extensions/azure.ai.agents/internal/exterrors/codes.go b/cli/azd/extensions/azure.ai.agents/internal/exterrors/codes.go index a3f72d01215..1fe7899a61d 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/exterrors/codes.go +++ b/cli/azd/extensions/azure.ai.agents/internal/exterrors/codes.go @@ -46,11 +46,12 @@ const ( // Error codes for auth errors. const ( + //nolint:gosec // error code identifier, not a credential CodeCredentialCreationFailed = "credential_creation_failed" CodeTenantLookupFailed = "tenant_lookup_failed" - CodeNotLoggedIn = "not_logged_in" - CodeLoginExpired = "login_expired" - CodeAuthFailed = "auth_failed" + CodeNotLoggedIn = "not_logged_in" + CodeLoginExpired = "login_expired" + CodeAuthFailed = "auth_failed" ) // Error codes for compatibility errors. diff --git a/cli/azd/extensions/azure.ai.agents/internal/exterrors/errors_test.go b/cli/azd/extensions/azure.ai.agents/internal/exterrors/errors_test.go index 7b8873a74a1..06e8e4859e3 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/exterrors/errors_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/exterrors/errors_test.go @@ -113,9 +113,9 @@ func TestFromPrompt(t *testing.T) { wantCode: CodeCancelled, }, { - name: "Non-auth error returns wrapped error", - err: status.Error(codes.Internal, "server error"), - contextMsg: "failed to prompt for subscription", + name: "Non-auth error returns wrapped error", + err: status.Error(codes.Internal, "server error"), + contextMsg: "failed to prompt for subscription", wantContain: "failed to prompt for subscription", }, { diff --git a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/models.go b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/models.go index 3290e100a07..2af66252fb3 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/models.go +++ b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/models.go @@ -287,7 +287,7 @@ type AgentContainerDetails struct { ProvisioningState string `json:"provisioning_state,omitempty"` State string `json:"state,omitempty"` UpdatedOn string `json:"updated_on,omitempty"` - Replicas []AgentContainerReplicaState `json:"replicas,omitempty"` + Replicas []AgentContainerReplicaState `json:"replicas,omitempty"` } // AgentContainerObject represents the details of an agent container diff --git a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/operations.go b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/operations.go index 137d4937a61..ac8482938a8 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/operations.go +++ b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_api/operations.go @@ -835,6 +835,7 @@ func (c *AgentClient) GetAgentContainerLogStream( // Use raw http.Client — its Do() returns after response headers arrive, // allowing the body to be read incrementally as a stream. httpClient := &http.Client{} + //nolint:gosec // request URL is built from trusted SDK endpoint + path components resp, err := httpClient.Do(req) if err != nil { if cancel != nil { @@ -844,7 +845,7 @@ func (c *AgentClient) GetAgentContainerLogStream( } if resp.StatusCode != http.StatusOK { - resp.Body.Close() + _ = resp.Body.Close() if cancel != nil { cancel() } diff --git a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map.go b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map.go index 04d3208d725..5332acbbee0 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map.go +++ b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map.go @@ -5,6 +5,7 @@ package agent_yaml import ( "fmt" + "math" "strings" "azureaiagent/internal/pkg/agents/agent_api" @@ -176,16 +177,6 @@ func CreatePromptAgentAPIRequest(promptAgent PromptAgent, buildConfig *AgentBuil return createAgentAPIRequest(promptAgent.AgentDefinition, promptDef) } -// Helper functions for type conversion (TODO: Implement based on answers to questions above) - -// extractFloat32FromOptions extracts a float32 value from ModelOptions -func extractFloat32FromOptions(options ModelOptions, key string) *float32 { - // TODO QUESTION: How is ModelOptions structured? Is it a map or typed struct? - // If it's map[string]interface{}: check options[key] and convert to float32 - // If it's typed struct: access specific fields - return nil // Placeholder -} - // convertYamlToolsToApiTools converts agent_yaml tools to agent_api tools func convertYamlToolsToApiTools(yamlTools []any) []any { var apiTools []any @@ -258,12 +249,16 @@ func convertYamlToolToApiTool(yamlTool any) (any, error) { return apiTool, nil case FileSearchTool: + maxResults, err := convertIntToInt32(tool.MaximumResultCount) + if err != nil { + return nil, fmt.Errorf("file_search maximumResultCount: %w", err) + } apiTool := agent_api.FileSearchTool{ Tool: agent_api.Tool{ Type: agent_api.ToolTypeFileSearch, }, VectorStoreIds: tool.VectorStoreIds, - MaxNumResults: convertIntToInt32(tool.MaximumResultCount), + MaxNumResults: maxResults, } // Set ranking options @@ -365,12 +360,15 @@ func convertPropertySchemaToInterface(schema PropertySchema) interface{} { } // Helper function to convert *int to *int32 -func convertIntToInt32(i *int) *int32 { +func convertIntToInt32(i *int) (*int32, error) { if i == nil { - return nil + return nil, nil + } + if *i > math.MaxInt32 || *i < math.MinInt32 { + return nil, fmt.Errorf("value %d overflows int32 range", *i) } i32 := int32(*i) - return &i32 + return &i32, nil } // Helper function to convert *float64 to *float32 @@ -382,20 +380,6 @@ func convertFloat64ToFloat32(f64 *float64) *float32 { return &f32 } -// mapInputSchemaToStructuredInputs converts PropertySchema to StructuredInputs -func mapInputSchemaToStructuredInputs(inputSchema *PropertySchema) map[string]agent_api.StructuredInputDefinition { - // TODO QUESTION: How does PropertySchema map to StructuredInputDefinition? - // PropertySchema might have parameters that become structured inputs - return nil // Placeholder -} - -// mapOutputSchemaToTextFormat converts PropertySchema to text response format -func mapOutputSchemaToTextFormat(outputSchema *PropertySchema) *agent_api.ResponseTextFormatConfiguration { - // TODO QUESTION: How does PropertySchema influence text formatting? - // PropertySchema might specify response structure that affects text config - return nil // Placeholder -} - // CreateHostedAgentAPIRequest creates a CreateAgentRequest for hosted agents func CreateHostedAgentAPIRequest(hostedAgent ContainerAgent, buildConfig *AgentBuildConfig) (*agent_api.CreateAgentRequest, error) { // Check if we have an image URL set via the build config diff --git a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/parse.go b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/parse.go index e81b3812e80..b22647ebfc3 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/parse.go +++ b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/parse.go @@ -57,7 +57,11 @@ func ExtractAgentDefinition(manifestYamlContent []byte) (any, error) { return nil, fmt.Errorf("template field must be a map, got %T", templateValue) } if len(template) == 0 { - return nil, fmt.Errorf("YAML content does not conform to AgentManifest format: template field is empty. See https://microsoft.github.io/AgentSchema/reference/agentmanifest for the expected format and https://github.com/microsoft-foundry/foundry-samples for examples") + return nil, fmt.Errorf( + "YAML content does not conform to AgentManifest format: template field is empty. " + + "See https://microsoft.github.io/AgentSchema/reference/agentmanifest for the expected format " + + "and https://github.com/microsoft-foundry/foundry-samples for examples", + ) } var err error templateBytes, err = yaml.Marshal(template) @@ -66,7 +70,11 @@ func ExtractAgentDefinition(manifestYamlContent []byte) (any, error) { } } else { // "template" field not found - return error - return nil, fmt.Errorf("YAML content does not conform to AgentManifest format: must contain 'template' field. See https://microsoft.github.io/AgentSchema/reference/agentmanifest for the expected format and https://github.com/microsoft-foundry/foundry-samples for examples") + return nil, fmt.Errorf( + "YAML content does not conform to AgentManifest format: must contain 'template' field. " + + "See https://microsoft.github.io/AgentSchema/reference/agentmanifest for the expected format " + + "and https://github.com/microsoft-foundry/foundry-samples for examples", + ) } var agentDef AgentDefinition diff --git a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/yaml.go b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/yaml.go index 3764d2f4451..2f3722fac39 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/yaml.go +++ b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/yaml.go @@ -57,7 +57,8 @@ const ( ConnectionKindAnonymous ConnectionKind = "anonymous" ) -// AgentDefinition The following is a specification for defining AI agents with structured metadata, inputs, outputs, tools, and templates. +// AgentDefinition The following is a specification for defining AI agents with structured metadata, inputs, outputs, tools, +// and templates. // It provides a way to create reusable and composable AI agents that can be executed with specific configurations. // The specification includes metadata about the agent, model configuration, input parameters, expected outputs, // available tools, and template configurations for prompt rendering. @@ -158,7 +159,8 @@ type RemoteConnection struct { type ApiKeyConnection struct { Connection `json:",inline" yaml:",inline"` Endpoint string `json:"endpoint" yaml:"endpoint"` - ApiKey string `json:"apiKey" yaml:"apiKey"` + //nolint:gosec // schema field name for manifest serialization, not embedded credential + ApiKey string `json:"apiKey" yaml:"apiKey"` } // AnonymousConnection represents a anonymousconnection. diff --git a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/registry_api/helpers.go b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/registry_api/helpers.go index f49edf1195c..c43c905e6c9 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/registry_api/helpers.go +++ b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/registry_api/helpers.go @@ -20,7 +20,8 @@ import ( // ParameterValues represents the user-provided values for manifest parameters type ParameterValues map[string]interface{} -func ProcessRegistryManifest(ctx context.Context, manifest *Manifest, azdClient *azdext.AzdClient) (*agent_yaml.AgentManifest, error) { +func ProcessRegistryManifest( + ctx context.Context, manifest *Manifest, azdClient *azdext.AzdClient) (*agent_yaml.AgentManifest, error) { // Convert the agent API definition into a MAML definition promptAgent, err := ConvertAgentDefinition(manifest.Template) if err != nil { @@ -271,7 +272,11 @@ func ConvertParameters(parameters map[string]OpenApiParameter) (*agent_yaml.Prop } // ProcessManifestParameters prompts the user for parameter values and injects them into the template -func ProcessManifestParameters(ctx context.Context, manifest *agent_yaml.AgentManifest, azdClient *azdext.AzdClient, noPrompt bool) (*agent_yaml.AgentManifest, error) { +func ProcessManifestParameters( + ctx context.Context, + manifest *agent_yaml.AgentManifest, + azdClient *azdext.AzdClient, + noPrompt bool) (*agent_yaml.AgentManifest, error) { // If no parameters are defined, return the manifest as-is if len(manifest.Parameters.Properties) == 0 { fmt.Println("The manifest does not contain parameters that need to be configured.") @@ -297,7 +302,11 @@ func ProcessManifestParameters(ctx context.Context, manifest *agent_yaml.AgentMa } // promptForYamlParameterValues prompts the user for values for each YAML parameter -func promptForYamlParameterValues(ctx context.Context, parameters agent_yaml.PropertySchema, azdClient *azdext.AzdClient, noPrompt bool) (ParameterValues, error) { +func promptForYamlParameterValues( + ctx context.Context, + parameters agent_yaml.PropertySchema, + azdClient *azdext.AzdClient, + noPrompt bool) (ParameterValues, error) { paramValues := make(ParameterValues) for _, property := range parameters.Properties { @@ -356,7 +365,8 @@ func promptForYamlParameterValues(ctx context.Context, parameters agent_yaml.Pro } // InjectParameterValuesIntoManifest replaces parameter placeholders in the manifest with actual values -func InjectParameterValuesIntoManifest(manifest *agent_yaml.AgentManifest, paramValues ParameterValues) (*agent_yaml.AgentManifest, error) { +func InjectParameterValuesIntoManifest( + manifest *agent_yaml.AgentManifest, paramValues ParameterValues) (*agent_yaml.AgentManifest, error) { // Convert manifest to YAML for processing manifestBytes, err := yaml.Marshal(manifest) if err != nil { @@ -379,7 +389,13 @@ func InjectParameterValuesIntoManifest(manifest *agent_yaml.AgentManifest, param } // promptForEnumValue prompts the user to select from enumerated values -func promptForEnumValue(ctx context.Context, paramName string, enumValues []string, defaultValue interface{}, azdClient *azdext.AzdClient, noPrompt bool) (interface{}, error) { +func promptForEnumValue( + ctx context.Context, + paramName string, + enumValues []string, + defaultValue interface{}, + azdClient *azdext.AzdClient, + noPrompt bool) (interface{}, error) { // Convert default value to string for comparison var defaultStr string if defaultValue != nil { @@ -424,7 +440,12 @@ func promptForEnumValue(ctx context.Context, paramName string, enumValues []stri } // promptForTextValue prompts the user for a text value -func promptForTextValue(ctx context.Context, paramName string, defaultValue interface{}, required bool, azdClient *azdext.AzdClient) (interface{}, error) { +func promptForTextValue( + ctx context.Context, + paramName string, + defaultValue interface{}, + required bool, + azdClient *azdext.AzdClient) (interface{}, error) { var defaultStr string if defaultValue != nil { defaultStr = fmt.Sprintf("%v", defaultValue) diff --git a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/registry_api/operations.go b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/registry_api/operations.go index 6792a8034da..b1b7530ffa7 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/registry_api/operations.go +++ b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/registry_api/operations.go @@ -26,7 +26,8 @@ type RegistryAgentManifestClient struct { // NewRegistryAgentManifestClient creates a new instance of RegistryAgentManifestClient func NewRegistryAgentManifestClient(registryName string, cred azcore.TokenCredential) *RegistryAgentManifestClient { - baseEndpoint := fmt.Sprintf("https://int.api.azureml-test.ms/agent-asset/v1.0/registries/%s/agentManifests", registryName) + baseEndpoint := fmt.Sprintf( + "https://int.api.azureml-test.ms/agent-asset/v1.0/registries/%s/agentManifests", registryName) userAgent := fmt.Sprintf("azd-ext-azure-ai-agents/%s", version.Version) @@ -55,7 +56,10 @@ func NewRegistryAgentManifestClient(registryName string, cred azcore.TokenCreden } // GetManifest retrieves a specific agent manifest from the registry -func (c *RegistryAgentManifestClient) GetManifest(ctx context.Context, manifestName string, manifestVersion string) (*Manifest, error) { +func (c *RegistryAgentManifestClient) GetManifest( + ctx context.Context, + manifestName string, + manifestVersion string) (*Manifest, error) { targetEndpoint := fmt.Sprintf("%s/%s/versions/%s", c.baseEndpoint, manifestName, manifestVersion) req, err := runtime.NewRequest(ctx, http.MethodGet, targetEndpoint) diff --git a/cli/azd/extensions/azure.ai.agents/internal/pkg/azure/foundry_projects_client.go b/cli/azd/extensions/azure.ai.agents/internal/pkg/azure/foundry_projects_client.go index 3909d95de69..9a5938a57cc 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/pkg/azure/foundry_projects_client.go +++ b/cli/azd/extensions/azure.ai.agents/internal/pkg/azure/foundry_projects_client.go @@ -143,7 +143,8 @@ func (c *FoundryProjectsClient) GetPagedConnections(ctx context.Context) (*Paged // GetConnectionWithCredentials retrieves a specific connection with its credentials func (c *FoundryProjectsClient) GetConnectionWithCredentials(ctx context.Context, name string) (*Connection, error) { - targetEndpoint := fmt.Sprintf("%s/connections/%s/getConnectionWithCredentials?api-version=%s", c.baseEndpoint, name, c.apiVersion) + targetEndpoint := fmt.Sprintf( + "%s/connections/%s/getConnectionWithCredentials?api-version=%s", c.baseEndpoint, name, c.apiVersion) req, err := runtime.NewRequest(ctx, http.MethodPost, targetEndpoint) if err != nil { diff --git a/cli/azd/extensions/azure.ai.agents/internal/project/parser.go b/cli/azd/extensions/azure.ai.agents/internal/project/parser.go index d4558cb76da..41aea0b60d0 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/project/parser.go +++ b/cli/azd/extensions/azure.ai.agents/internal/project/parser.go @@ -175,7 +175,11 @@ func (p *FoundryParser) SetIdentity(ctx context.Context, args *azdext.ProjectEve } // getProjectPrincipalID retrieves the principal ID from the Microsoft Foundry Project using Azure SDK -func getProjectPrincipalID(ctx context.Context, cred *azidentity.AzureDeveloperCLICredential, resourceID, subscriptionID string) (string, error) { +func getProjectPrincipalID( + ctx context.Context, + cred *azidentity.AzureDeveloperCLICredential, + resourceID, + subscriptionID string) (string, error) { // Create resources client client, err := armresources.NewClient(subscriptionID, cred, nil) if err != nil { @@ -208,7 +212,8 @@ func getProjectPrincipalID(ctx context.Context, cred *azidentity.AzureDeveloperC } // getApplicationID retrieves the application ID from the principal ID using Microsoft Graph API -func getApplicationID(ctx context.Context, cred *azidentity.AzureDeveloperCLICredential, principalID string) (string, error) { +func getApplicationID( + ctx context.Context, cred *azidentity.AzureDeveloperCLICredential, principalID string) (string, error) { // Create Graph client graphClient, err := graphsdk.NewGraphClient(cred, nil) if err != nil { @@ -233,7 +238,12 @@ func getApplicationID(ctx context.Context, cred *azidentity.AzureDeveloperCLICre } // getCognitiveServicesAccountLocation retrieves the location of a Cognitive Services account using Azure SDK -func getCognitiveServicesAccountLocation(ctx context.Context, cred *azidentity.AzureDeveloperCLICredential, subscriptionID, resourceGroupName, accountName string) (string, error) { +func getCognitiveServicesAccountLocation( + ctx context.Context, + cred *azidentity.AzureDeveloperCLICredential, + subscriptionID, + resourceGroupName, + accountName string) (string, error) { // Create cognitive services accounts client client, err := armcognitiveservices.NewAccountsClient(subscriptionID, cred, nil) if err != nil { @@ -384,7 +394,8 @@ func (p *FoundryParser) CoboPostDeploy(ctx context.Context, args *azdext.Project } // Get Microsoft Foundry region using SDK - aiFoundryRegion, err := getCognitiveServicesAccountLocation(ctx, cred, projectSubscriptionID, projectResourceGroup, projectAIFoundryName) + aiFoundryRegion, err := getCognitiveServicesAccountLocation( + ctx, cred, projectSubscriptionID, projectResourceGroup, projectAIFoundryName) if err != nil { return fmt.Errorf("failed to get Microsoft Foundry region: %w", err) } @@ -433,7 +444,8 @@ func (p *FoundryParser) CoboPostDeploy(ctx context.Context, args *azdext.Project // Get Microsoft Foundry Project endpoint using SDK fmt.Println("Retrieving Microsoft Foundry Project API endpoint...") - aiFoundryProjectEndpoint, err := getAIFoundryProjectEndpoint(ctx, cred, aiFoundryProjectResourceID, projectSubscriptionID) + aiFoundryProjectEndpoint, err := getAIFoundryProjectEndpoint( + ctx, cred, aiFoundryProjectResourceID, projectSubscriptionID) if err != nil { fmt.Fprintf(os.Stderr, "Warning: Failed to retrieve Microsoft Foundry Project API endpoint: %v\n", err) } else { @@ -460,7 +472,9 @@ func (p *FoundryParser) CoboPostDeploy(ctx context.Context, args *azdext.Project // Construct agent registration URI workspaceName := fmt.Sprintf("%s@%s@AML", projectAIFoundryName, projectName) - apiPath := fmt.Sprintf("/agents/v2.0/subscriptions/%s/resourceGroups/%s/providers/Microsoft.MachineLearningServices/workspaces/%s/agents/%s/versions?api-version=2025-05-15-preview", + apiPath := fmt.Sprintf( + "/agents/v2.0/subscriptions/%s/resourceGroups/%s/providers/Microsoft.MachineLearningServices/"+ + "workspaces/%s/agents/%s/versions?api-version=2025-05-15-preview", projectSubscriptionID, projectResourceGroup, workspaceName, agentName) uri := "" @@ -551,9 +565,12 @@ func assignAzureAIRole(ctx context.Context, cred *azidentity.AzureDeveloperCLICr return fmt.Errorf("failed to list role assignments: %w", err) } for _, assignment := range page.Value { - if assignment.Properties != nil && assignment.Properties.PrincipalID != nil && assignment.Properties.RoleDefinitionID != nil { + if assignment.Properties != nil && + assignment.Properties.PrincipalID != nil && + assignment.Properties.RoleDefinitionID != nil { // Filter by both principal ID and role definition ID - if *assignment.Properties.PrincipalID == principalID && *assignment.Properties.RoleDefinitionID == fullRoleDefinitionID { + if *assignment.Properties.PrincipalID == principalID && + *assignment.Properties.RoleDefinitionID == fullRoleDefinitionID { assignmentExists = true if assignment.Name != nil { existingAssignmentId = *assignment.Name @@ -645,7 +662,8 @@ func assignAzureAIRole(ctx context.Context, cred *azidentity.AzureDeveloperCLICr } // deactivateHelloWorldRevision deactivates the hello-world placeholder revision using Azure SDK -func deactivateHelloWorldRevision(ctx context.Context, cred *azidentity.AzureDeveloperCLICredential, resourceID string) error { +func deactivateHelloWorldRevision( + ctx context.Context, cred *azidentity.AzureDeveloperCLICredential, resourceID string) error { fmt.Println() fmt.Println("======================================") fmt.Println("Deactivating Hello-World Revision") @@ -828,7 +846,9 @@ func verifyAuthConfiguration(ctx context.Context, cred *azidentity.AzureDevelope if authConfig.Properties.GlobalValidation != nil && authConfig.Properties.GlobalValidation.UnauthenticatedClientAction != nil { - fmt.Printf(" Unauthenticated Action: %s\n", string(*authConfig.Properties.GlobalValidation.UnauthenticatedClientAction)) + fmt.Printf( + " Unauthenticated Action: %s\n", + string(*authConfig.Properties.GlobalValidation.UnauthenticatedClientAction)) } } else { fmt.Fprintln(os.Stderr, "Warning: Azure AD authentication is not configured") @@ -838,7 +858,11 @@ func verifyAuthConfiguration(ctx context.Context, cred *azidentity.AzureDevelope } // getContainerAppEndpoint retrieves the Container App FQDN using Azure SDK -func getContainerAppEndpoint(ctx context.Context, cred *azidentity.AzureDeveloperCLICredential, resourceID, subscriptionID string) (string, error) { +func getContainerAppEndpoint( + ctx context.Context, + cred *azidentity.AzureDeveloperCLICredential, + resourceID, + subscriptionID string) (string, error) { // Parse resource ID parsedResource, err := arm.ParseResourceID(resourceID) if err != nil { @@ -881,7 +905,11 @@ func getContainerAppEndpoint(ctx context.Context, cred *azidentity.AzureDevelope } // getAIFoundryProjectEndpoint retrieves the Microsoft Foundry Project API endpoint using Azure SDK -func getAIFoundryProjectEndpoint(ctx context.Context, cred *azidentity.AzureDeveloperCLICredential, resourceID, subscriptionID string) (string, error) { +func getAIFoundryProjectEndpoint( + ctx context.Context, + cred *azidentity.AzureDeveloperCLICredential, + resourceID, + subscriptionID string) (string, error) { // Create resources client client, err := armresources.NewClient(subscriptionID, cred, nil) if err != nil { @@ -934,7 +962,11 @@ func getAccessToken(ctx context.Context, cred *azidentity.AzureDeveloperCLICrede } // getLatestRevisionName retrieves the latest revision name for a Container App using Azure SDK -func getLatestRevisionName(ctx context.Context, cred *azidentity.AzureDeveloperCLICredential, resourceID, subscriptionID string) (string, error) { +func getLatestRevisionName( + ctx context.Context, + cred *azidentity.AzureDeveloperCLICredential, + resourceID, + subscriptionID string) (string, error) { // Parse resource ID parsedResource, err := arm.ParseResourceID(resourceID) if err != nil { @@ -1017,6 +1049,7 @@ func registerAgent(uri, token, resourceID, ingressSuffix string) string { req.Header.Set("authorization", "Bearer "+token) req.Header.Set("content-type", "application/json") + //nolint:gosec // URI is user-selected debug target and call is intentional for diagnostics resp, err := client.Do(req) if err != nil { fmt.Fprintf(os.Stderr, "Error making request: %v\n", err) @@ -1024,7 +1057,9 @@ func registerAgent(uri, token, resourceID, ingressSuffix string) string { } body, _ := io.ReadAll(resp.Body) - resp.Body.Close() + if closeErr := resp.Body.Close(); closeErr != nil { + fmt.Fprintf(os.Stderr, "Warning: failed to close response body: %v\n", closeErr) + } fmt.Printf("Response Status: %d\n", resp.StatusCode) fmt.Println("Response Body:") @@ -1077,6 +1112,7 @@ func testUnauthenticatedAccess(acaEndpoint string) { req.Header.Set("content-type", "application/json") + //nolint:gosec // endpoint is explicit caller input for connectivity verification resp, err := client.Do(req) if err != nil { fmt.Fprintf(os.Stderr, "Error making request: %v\n", err) @@ -1093,6 +1129,7 @@ func testUnauthenticatedAccess(acaEndpoint string) { if resp.StatusCode == 401 { fmt.Println("✓ Authentication enforced (got 401)") } else { + //nolint:gosec // formatted warning only; no untrusted HTML rendering context fmt.Fprintf(os.Stderr, "Warning: Expected 401, got %d\n", resp.StatusCode) } } @@ -1132,6 +1169,7 @@ func testDataPlane(endpoint, token, agentName, agentVersion string) { req.Header.Set("authorization", "Bearer "+token) req.Header.Set("content-type", "application/json") + //nolint:gosec // endpoint is explicit caller input for connectivity verification resp, err := client.Do(req) if err != nil { fmt.Fprintf(os.Stderr, "Error making request: %v\n", err) diff --git a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go index 31a1d6a3183..a81515e3bb3 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go +++ b/cli/azd/extensions/azure.ai.agents/internal/project/service_target_agent.go @@ -7,6 +7,7 @@ import ( "context" "encoding/base64" "fmt" + "math" "os" "path/filepath" "strings" @@ -97,7 +98,8 @@ func (p *AgentServiceTargetProvider) Initialize(ctx context.Context, serviceConf return exterrors.Dependency( exterrors.CodeMissingAzureSubscription, "AZURE_SUBSCRIPTION_ID is required: environment variable was not found in the current azd environment", - "run 'azd env get-values' to verify environment values, or initialize/project-bind with 'azd ai agent init --project-id ...'", + "run 'azd env get-values' to verify environment values, or initialize/project-bind "+ + "with 'azd ai agent init --project-id ...'", ) } @@ -133,6 +135,7 @@ func (p *AgentServiceTargetProvider) Initialize(ctx context.Context, serviceConf // Check if user has specified agent definition path via environment variable if envPath := os.Getenv("AGENT_DEFINITION_PATH"); envPath != "" { // Verify the file exists and has correct extension + //nolint:gosec // env path is an explicit user override; existence check is intentional if _, err := os.Stat(envPath); os.IsNotExist(err) { return exterrors.Validation( exterrors.CodeAgentDefinitionNotFound, @@ -259,9 +262,12 @@ func (p *AgentServiceTargetProvider) GetTargetResource( projectName := p.foundryProject.Name // Create Cognitive Services Projects client - projectsClient, err := armcognitiveservices.NewProjectsClient(p.foundryProject.SubscriptionID, p.credential, azure.NewArmClientOptions()) + projectsClient, err := armcognitiveservices.NewProjectsClient( + p.foundryProject.SubscriptionID, p.credential, azure.NewArmClientOptions()) if err != nil { - return nil, exterrors.Internal(exterrors.CodeCognitiveServicesClientFailed, fmt.Sprintf("failed to create Cognitive Services Projects client: %s", err)) + return nil, exterrors.Internal( + exterrors.CodeCognitiveServicesClientFailed, + fmt.Sprintf("failed to create Cognitive Services Projects client: %s", err)) } // Get the Microsoft Foundry project @@ -741,7 +747,8 @@ func (p *AgentServiceTargetProvider) deployArtifacts( "agentVersion": agentVersion, "label": "Agent endpoint", "clickable": "false", - "note": "For information on invoking the agent, see " + output.WithLinkFormat("https://aka.ms/azd-agents-invoke"), + "note": "For information on invoking the agent, see " + output.WithLinkFormat( + "https://aka.ms/azd-agents-invoke"), }, }) } @@ -847,10 +854,24 @@ func (p *AgentServiceTargetProvider) startAgentContainer( var minReplicas, maxReplicas *int32 if foundryAgentConfig.Container != nil && foundryAgentConfig.Container.Scale != nil { if foundryAgentConfig.Container.Scale.MinReplicas > 0 { + if foundryAgentConfig.Container.Scale.MinReplicas > math.MaxInt32 { + return exterrors.Validation( + exterrors.CodeInvalidServiceConfig, + fmt.Sprintf("minReplicas exceeds int32 range: %d", foundryAgentConfig.Container.Scale.MinReplicas), + fmt.Sprintf("set container.scale.minReplicas to a value <= %d", math.MaxInt32), + ) + } minReplicasInt32 := int32(foundryAgentConfig.Container.Scale.MinReplicas) minReplicas = &minReplicasInt32 } if foundryAgentConfig.Container.Scale.MaxReplicas > 0 { + if foundryAgentConfig.Container.Scale.MaxReplicas > math.MaxInt32 { + return exterrors.Validation( + exterrors.CodeInvalidServiceConfig, + fmt.Sprintf("maxReplicas exceeds int32 range: %d", foundryAgentConfig.Container.Scale.MaxReplicas), + fmt.Sprintf("set container.scale.maxReplicas to a value <= %d", math.MaxInt32), + ) + } maxReplicasInt32 := int32(foundryAgentConfig.Container.Scale.MaxReplicas) maxReplicas = &maxReplicasInt32 } @@ -924,7 +945,10 @@ func (p *AgentServiceTargetProvider) startAgentContainer( *containerInfo.ErrorMessage, ) } else { - errorMsg = fmt.Sprintf("operation failed (id: %s): container status is %q with no error details", operation.Body.ID, containerInfo.Status) + errorMsg = fmt.Sprintf( + "operation failed (id: %s): container status is %q with no error details", + operation.Body.ID, + containerInfo.Status) } return exterrors.Internal(exterrors.CodeContainerStartFailed, errorMsg)