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 a5409085adc..d48df6a0a1f 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go +++ b/cli/azd/extensions/azure.ai.agents/internal/cmd/init.go @@ -572,10 +572,10 @@ func (a *InitAction) Run(ctx context.Context) error { } // Prompt for deploy mode (code vs container) for hosted agents. - // Code deploy is currently only supported for Python projects. + // Code deploy is supported for Python and .NET projects. if _, ok := agentManifest.Template.(agent_yaml.ContainerAgent); ok { - isPython := isPythonProject(targetDir) - deployMode, err := promptDeployMode(ctx, a.azdClient, a.flags.noPrompt, isPython) + showCodeDeploy := isPythonProject(targetDir) || isDotnetProject(targetDir) + deployMode, err := promptDeployMode(ctx, a.azdClient, a.flags.noPrompt, showCodeDeploy) if err != nil { return fmt.Errorf("prompting for deploy mode: %w", err) } @@ -1631,6 +1631,12 @@ func (a *InitAction) addToProject(ctx context.Context, targetDir string, agentMa if agentDef.Kind == agent_yaml.AgentKindHosted { if a.isCodeDeploy { serviceConfig.Language = "python" + // If the agent uses a dotnet runtime, set language to csharp + if ca, ok := agentManifest.Template.(agent_yaml.ContainerAgent); ok && + ca.CodeConfiguration != nil && + strings.HasPrefix(ca.CodeConfiguration.Runtime, "dotnet_") { + serviceConfig.Language = "csharp" + } } else { serviceConfig.Docker = &azdext.DockerProjectOptions{ RemoteBuild: true, 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 91fb94283ab..260467dd124 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 @@ -470,12 +470,12 @@ func (a *InitFromCodeAction) createDefinitionFromLocalAgent(ctx context.Context) agentKind := agent_yaml.AgentKindHosted // Prompt user for deploy mode (container vs code) - // Code deploy is only available for Python projects + // Code deploy is available for Python and .NET projects srcDir := a.flags.src if srcDir == "" { srcDir, _ = os.Getwd() } - showCodeDeploy := isPythonProject(srcDir) + showCodeDeploy := isPythonProject(srcDir) || isDotnetProject(srcDir) deployMode, err := promptDeployMode(ctx, a.azdClient, a.flags.noPrompt, showCodeDeploy) if err != nil { return nil, err @@ -860,6 +860,18 @@ func (a *InitFromCodeAction) addToProject(ctx context.Context, targetDir string, language := "python" if !isCodeDeploy { language = "docker" + } else { + // Detect language from agent.yaml runtime + // Re-read agent.yaml to detect the language for azure.yaml service config + langDetectPath := filepath.Join(a.projectConfig.Path, targetDir, "agent.yaml") + if data, err := os.ReadFile(langDetectPath); err == nil { //nolint:gosec // path from project config + var langDef agent_yaml.ContainerAgent + if err := yaml.Unmarshal(data, &langDef); err == nil && + langDef.CodeConfiguration != nil && + strings.HasPrefix(langDef.CodeConfiguration.Runtime, "dotnet_") { + language = "csharp" + } + } } serviceConfig := &azdext.ServiceConfig{ @@ -899,7 +911,7 @@ func deriveStartupCommand(projectPath, targetDir string) string { if data, err := os.ReadFile(agentYamlPath); err == nil { //nolint:gosec // path is constructed from project config var agentDef agent_yaml.ContainerAgent if err := yaml.Unmarshal(data, &agentDef); err == nil && agentDef.CodeConfiguration != nil { - return "python " + agentDef.CodeConfiguration.EntryPoint + return agent_yaml.RuntimeCmdPrefix(agentDef.CodeConfiguration.Runtime) + " " + agentDef.CodeConfiguration.EntryPoint } } return "python main.py" @@ -1062,6 +1074,59 @@ func promptDeployMode(ctx context.Context, azdClient *azdext.AzdClient, noPrompt return deployModeChoices[*deployModeResp.Value].Value, nil } +// detectDefaultEntryPoint returns a sensible default entry point based on the runtime and source directory. +// TODO: reuse this logic in the `run` command (tracked as future work item). +func detectDefaultEntryPoint(srcDir, runtime string) string { + if strings.HasPrefix(runtime, "dotnet_") { + // Look for .csproj file and derive DLL name from or project filename + entries, err := os.ReadDir(srcDir) + if err == nil { + for _, e := range entries { + if !e.IsDir() && strings.HasSuffix(e.Name(), ".csproj") { + dllName := strings.TrimSuffix(e.Name(), ".csproj") + ".dll" + // Try to parse from the csproj + csprojPath := filepath.Join(srcDir, e.Name()) + if data, readErr := os.ReadFile(csprojPath); readErr == nil { //nolint:gosec // path from user project + if asmName := extractAssemblyName(string(data)); asmName != "" { + dllName = asmName + ".dll" + } + } + return dllName + } + } + } + return "App.dll" + } + + // Python default + if _, err := os.Stat(filepath.Join(srcDir, "app.py")); err == nil { + return "app.py" + } + return "main.py" +} + +// extractAssemblyName parses the property from a .csproj file content. +// Returns empty string if not found. +func extractAssemblyName(csprojContent string) string { + const startTag = "" + const endTag = "" + start := strings.Index(csprojContent, startTag) + if start < 0 { + return "" + } + start += len(startTag) + end := strings.Index(csprojContent[start:], endTag) + if end < 0 { + return "" + } + name := strings.TrimSpace(csprojContent[start : start+end]) + if name == "" || strings.ContainsAny(name, "$()") { + // Skip MSBuild property references like $(MSBuildProjectName) + return "" + } + return name +} + // promptCodeConfig prompts for code deploy configuration (runtime, entry point, // dependency resolution). When noPrompt is true, defaults are used without prompting. func promptCodeConfig(ctx context.Context, azdClient *azdext.AzdClient, srcDir string, noPrompt bool) (*agent_yaml.CodeConfiguration, error) { @@ -1069,18 +1134,47 @@ func promptCodeConfig(ctx context.Context, azdClient *azdext.AzdClient, srcDir s srcDir = "." } - // Prompt for runtime - runtimeChoices := []*azdext.SelectChoice{ - {Label: "Python 3.11", Value: "python_3_11"}, - {Label: "Python 3.12", Value: "python_3_12"}, - {Label: "Python 3.13", Value: "python_3_13"}, + // Prompt for runtime — filter choices based on detected project type + var runtimeChoices []*azdext.SelectChoice + isDotnet := isDotnetProject(srcDir) + isPython := isPythonProject(srcDir) + + if isDotnet && !isPython { + runtimeChoices = []*azdext.SelectChoice{ + {Label: ".NET 9", Value: "dotnet_9"}, + {Label: ".NET 8", Value: "dotnet_8"}, + {Label: ".NET 10", Value: "dotnet_10"}, + } + } else if isPython && !isDotnet { + runtimeChoices = []*azdext.SelectChoice{ + {Label: "Python 3.11", Value: "python_3_11"}, + {Label: "Python 3.12", Value: "python_3_12"}, + {Label: "Python 3.13", Value: "python_3_13"}, + } + } else { + // Mixed or unknown — show all options + runtimeChoices = []*azdext.SelectChoice{ + {Label: "Python 3.11", Value: "python_3_11"}, + {Label: "Python 3.12", Value: "python_3_12"}, + {Label: "Python 3.13", Value: "python_3_13"}, + {Label: ".NET 9", Value: "dotnet_9"}, + {Label: ".NET 8", Value: "dotnet_8"}, + {Label: ".NET 10", Value: "dotnet_10"}, + } } var runtime string if noPrompt { - runtime = "python_3_12" + if isDotnet && !isPython { + runtime = "dotnet_9" + } else { + runtime = "python_3_12" // default to python for backward compatibility (including mixed repos) + } } else { - defaultIdx := int32(1) // Python 3.12 is the default + defaultIdx := int32(0) // First item in the filtered list + if isPython && !isDotnet { + defaultIdx = 1 // Python 3.12 + } runtimeResp, err := azdClient.Prompt().Select(ctx, &azdext.SelectRequest{ Options: &azdext.SelectOptions{ Message: "Select the runtime for your agent", @@ -1098,10 +1192,7 @@ func promptCodeConfig(ctx context.Context, azdClient *azdext.AzdClient, srcDir s } // Prompt for entry point - defaultEntryPoint := "main.py" - if _, statErr := os.Stat(filepath.Join(srcDir, "app.py")); statErr == nil { - defaultEntryPoint = "app.py" - } + defaultEntryPoint := detectDefaultEntryPoint(srcDir, runtime) var entryPoint string if noPrompt { @@ -1178,3 +1269,21 @@ func isPythonProject(dir string) bool { } return false } + +// isDotnetProject returns true if the directory contains a .csproj file. +// NOTE: .fsproj (F#) is not yet supported by the packaging path (packageDotnetBundled/detectDefaultEntryPoint). +func isDotnetProject(dir string) bool { + if dir == "" { + dir = "." + } + entries, err := os.ReadDir(dir) + if err != nil { + return false + } + for _, e := range entries { + if !e.IsDir() && strings.HasSuffix(e.Name(), ".csproj") { + return true + } + } + return false +} 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 7133b9b6bf5..9c570fd627f 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 @@ -817,3 +817,64 @@ func TestPromptProtocols_Interactive(t *testing.T) { }) } } + +func TestDetectDefaultEntryPoint(t *testing.T) { + tests := []struct { + name string + files []string + runtime string + want string + }{ + { + name: "dotnet with csproj", + files: []string{"MyAgent.csproj", "Program.cs"}, + runtime: "dotnet_9", + want: "MyAgent.dll", + }, + { + name: "dotnet_8 with csproj", + files: []string{"EchoAgent.csproj", "Program.cs", "NuGet.config"}, + runtime: "dotnet_8", + want: "EchoAgent.dll", + }, + { + name: "dotnet_10 no csproj fallback", + files: []string{"Program.cs"}, + runtime: "dotnet_10", + want: "App.dll", + }, + { + name: "python with app.py", + files: []string{"app.py", "requirements.txt"}, + runtime: "python_3_12", + want: "app.py", + }, + { + name: "python without app.py", + files: []string{"requirements.txt"}, + runtime: "python_3_12", + want: "main.py", + }, + { + name: "python with main.py", + files: []string{"main.py", "requirements.txt"}, + runtime: "python_3_11", + want: "main.py", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dir := t.TempDir() + for _, f := range tt.files { + if err := os.WriteFile(filepath.Join(dir, f), []byte(""), 0600); err != nil { + t.Fatal(err) + } + } + got := detectDefaultEntryPoint(dir, tt.runtime) + if got != tt.want { + t.Errorf("detectDefaultEntryPoint() = %q, want %q", got, tt.want) + } + }) + } +} 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 06cd03ba68b..021d1c67341 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 @@ -14,6 +14,15 @@ import ( "go.yaml.in/yaml/v3" ) +// RuntimeCmdPrefix returns the command prefix for a given runtime string. +// For example, "python_3_12" -> "python", "dotnet_9" -> "dotnet". +func RuntimeCmdPrefix(runtime string) string { + if strings.HasPrefix(runtime, "dotnet_") { + return "dotnet" + } + return "python" +} + // AgentBuildOption represents an option for building agent definitions type AgentBuildOption func(*AgentBuildConfig) @@ -357,7 +366,8 @@ func CreateHostedAgentAPIRequest(hostedAgent ContainerAgent, buildConfig *AgentB // Code deploy path if hostedAgent.CodeConfiguration != nil { - entryPoint := []string{"python", hostedAgent.CodeConfiguration.EntryPoint} + cmdPrefix := RuntimeCmdPrefix(hostedAgent.CodeConfiguration.Runtime) + entryPoint := []string{cmdPrefix, hostedAgent.CodeConfiguration.EntryPoint} depRes := "" if hostedAgent.CodeConfiguration.DependencyResolution != nil { depRes = *hostedAgent.CodeConfiguration.DependencyResolution diff --git a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map_test.go b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map_test.go index f2e00e98090..999d7189f89 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map_test.go +++ b/cli/azd/extensions/azure.ai.agents/internal/pkg/agents/agent_yaml/map_test.go @@ -1350,3 +1350,89 @@ func TestCreateHostedAgentAPIRequest_NoSkillsRejected( t.Errorf("error = %q, want 'at least one skill'", err) } } + +func TestCreateAgentAPIRequest_CodeDeploy_DotnetRuntime(t *testing.T) { + depRes := "remote_build" + agent := ContainerAgent{ + AgentDefinition: AgentDefinition{ + Name: "dotnet-agent", + Kind: AgentKindHosted, + }, + Protocols: []ProtocolVersionRecord{ + {Protocol: "responses", Version: "1.0.0"}, + }, + CodeConfiguration: &CodeConfiguration{ + Runtime: "dotnet_9", + EntryPoint: "MyAgent.dll", + DependencyResolution: &depRes, + }, + } + + req, err := CreateAgentAPIRequestFromDefinition(agent) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + codeDef, ok := req.Definition.(agent_api.HostedAgentDefinition) + if !ok { + t.Fatalf("expected CodeBasedHostedAgentDefinition, got %T", req.Definition) + } + + // Verify entry_point is ["dotnet", "MyAgent.dll"] + wantEntryPoint := []string{"dotnet", "MyAgent.dll"} + if len(codeDef.CodeConfiguration.EntryPoint) != 2 || + codeDef.CodeConfiguration.EntryPoint[0] != wantEntryPoint[0] || + codeDef.CodeConfiguration.EntryPoint[1] != wantEntryPoint[1] { + t.Errorf("EntryPoint = %v, want %v", codeDef.CodeConfiguration.EntryPoint, wantEntryPoint) + } + + // Verify runtime is passed through + if codeDef.CodeConfiguration.Runtime != "dotnet_9" { + t.Errorf("Runtime = %q, want %q", codeDef.CodeConfiguration.Runtime, "dotnet_9") + } + + // Verify dependency resolution + if codeDef.CodeConfiguration.DependencyResolution != "remote_build" { + t.Errorf("DependencyResolution = %q, want %q", codeDef.CodeConfiguration.DependencyResolution, "remote_build") + } +} + +func TestCreateAgentAPIRequest_CodeDeploy_PythonRuntime(t *testing.T) { + depRes := "bundled" + agent := ContainerAgent{ + AgentDefinition: AgentDefinition{ + Name: "python-agent", + Kind: AgentKindHosted, + }, + Protocols: []ProtocolVersionRecord{ + {Protocol: "invocations", Version: "1.0.0"}, + }, + CodeConfiguration: &CodeConfiguration{ + Runtime: "python_3_12", + EntryPoint: "main.py", + DependencyResolution: &depRes, + }, + } + + req, err := CreateAgentAPIRequestFromDefinition(agent) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + codeDef, ok := req.Definition.(agent_api.HostedAgentDefinition) + if !ok { + t.Fatalf("expected CodeBasedHostedAgentDefinition, got %T", req.Definition) + } + + // Verify entry_point is ["python", "main.py"] + wantEntryPoint := []string{"python", "main.py"} + if len(codeDef.CodeConfiguration.EntryPoint) != 2 || + codeDef.CodeConfiguration.EntryPoint[0] != wantEntryPoint[0] || + codeDef.CodeConfiguration.EntryPoint[1] != wantEntryPoint[1] { + t.Errorf("EntryPoint = %v, want %v", codeDef.CodeConfiguration.EntryPoint, wantEntryPoint) + } + + if codeDef.CodeConfiguration.Runtime != "python_3_12" { + t.Errorf("Runtime = %q, want %q", codeDef.CodeConfiguration.Runtime, "python_3_12") + } +} diff --git a/cli/azd/extensions/azure.ai.agents/internal/project/config.go b/cli/azd/extensions/azure.ai.agents/internal/project/config.go index bb82f3e49e5..2fd95882e99 100644 --- a/cli/azd/extensions/azure.ai.agents/internal/project/config.go +++ b/cli/azd/extensions/azure.ai.agents/internal/project/config.go @@ -18,6 +18,7 @@ const ( ) // CodeDeployRegions lists the regions that currently support code deploy (ZIP upload). +// TODO: replace with dynamic region discovery API when available. var CodeDeployRegions = []string{"westus2", "canadacentral", "northcentralus"} // ResourceTier defines a preset CPU and memory allocation for container resources. 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 fbaac59af18..d4be6e75d8d 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 @@ -5,6 +5,7 @@ package project import ( "archive/zip" + "bytes" "context" "crypto/sha256" "encoding/base64" @@ -15,6 +16,7 @@ import ( "io/fs" "net/http" "os" + "os/exec" "path/filepath" "regexp" "slices" @@ -948,7 +950,23 @@ func (p *AgentServiceTargetProvider) packageCodeDeploy(serviceConfig *azdext.Ser // Source directory is the service's relative path srcDir := filepath.Dir(p.agentDefinitionPath) + // Load agent.yaml to check runtime and dependency resolution for dotnet bundled mode + if data, err := os.ReadFile(p.agentDefinitionPath); err == nil { //nolint:gosec // path from internal state + var agentDef agent_yaml.ContainerAgent + if err := yaml.Unmarshal(data, &agentDef); err == nil && agentDef.CodeConfiguration != nil { + isDotnet := strings.HasPrefix(agentDef.CodeConfiguration.Runtime, "dotnet_") + isBundled := false // default is remote_build (matches promptCodeConfig and deployHostedCodeAgent defaults) + if agentDef.CodeConfiguration.DependencyResolution != nil { + isBundled = *agentDef.CodeConfiguration.DependencyResolution == "bundled" + } + if isDotnet && isBundled { + return p.packageDotnetBundled(srcDir) + } + } + } + // Exclusion patterns + // TODO: support a .azdignore or similar ignore file for user-configurable exclusions. excludeDirs := map[string]bool{ "__pycache__": true, ".venv": true, @@ -958,10 +976,16 @@ func (p *AgentServiceTargetProvider) packageCodeDeploy(serviceConfig *azdext.Ser ".mypy_cache": true, ".pytest_cache": true, ".azure": true, + // .NET directories (for remote_build, exclude build artifacts) + "bin": true, + "obj": true, + ".vs": true, } excludeExts := map[string]bool{ - ".pyc": true, - ".pyo": true, + ".pyc": true, + ".pyo": true, + ".user": true, + ".suo": true, } excludeFiles := map[string]bool{ ".env": true, @@ -1064,6 +1088,144 @@ func (p *AgentServiceTargetProvider) packageCodeDeploy(serviceConfig *azdext.Ser return "", "", fmt.Errorf("failed to close temp file: %w", err) } + // Enforce maximum ZIP size (250 MB) + const maxZipSize = 250 * 1024 * 1024 + fi, err := os.Stat(tmpPath) + if err != nil { + return "", "", fmt.Errorf("failed to stat ZIP file: %w", err) + } + if fi.Size() > maxZipSize { + return "", "", fmt.Errorf( + "code package too large: %d MB (max 250 MB). Reduce package size by excluding unnecessary files or using remote_build for dependency resolution", + fi.Size()/(1024*1024), + ) + } + + sha256Hex := hex.EncodeToString(hasher.Sum(nil)) + success = true + + return tmpPath, sha256Hex, nil +} + +// packageDotnetBundled runs "dotnet publish" for the .NET project and creates a ZIP of the published output. +func (p *AgentServiceTargetProvider) packageDotnetBundled(srcDir string) (string, string, error) { + // Find the .csproj file + entries, err := os.ReadDir(srcDir) + if err != nil { + return "", "", fmt.Errorf("failed to read source directory: %w", err) + } + + var csprojPath string + for _, e := range entries { + if !e.IsDir() && strings.HasSuffix(e.Name(), ".csproj") { + csprojPath = filepath.Join(srcDir, e.Name()) + break + } + } + if csprojPath == "" { + return "", "", fmt.Errorf("no .csproj file found in %s; required for dotnet bundled packaging", srcDir) + } + + // Create temp directory for publish output + publishDir, err := os.MkdirTemp("", "azd-dotnet-publish-*") + if err != nil { + return "", "", fmt.Errorf("failed to create temp dir for dotnet publish: %w", err) + } + defer os.RemoveAll(publishDir) + + // Run dotnet publish targeting linux (hosted agents run on linux) + fmt.Fprintf(os.Stderr, "Running 'dotnet publish' for bundled packaging...\n") + cmd := exec.Command("dotnet", "publish", csprojPath, //nolint:gosec // csprojPath is derived from user's project directory + "-c", "Release", + "-r", "linux-x64", + "--self-contained", "false", + "-o", publishDir, + ) + cmd.Dir = srcDir + var publishOutput bytes.Buffer + cmd.Stdout = io.MultiWriter(os.Stderr, &publishOutput) + cmd.Stderr = io.MultiWriter(os.Stderr, &publishOutput) + if err := cmd.Run(); err != nil { + return "", "", fmt.Errorf("dotnet publish failed: %w\nOutput:\n%s", err, publishOutput.String()) + } + + // ZIP the publish output + tmpFile, err := os.CreateTemp("", "azd-code-deploy-*.zip") + if err != nil { + return "", "", fmt.Errorf("failed to create temp file for ZIP: %w", err) + } + tmpPath := tmpFile.Name() + + success := false + defer func() { + if !success { + _ = tmpFile.Close() + _ = os.Remove(tmpPath) + } + }() + + hasher := sha256.New() + multiWriter := io.MultiWriter(tmpFile, hasher) + zipWriter := zip.NewWriter(multiWriter) + + err = filepath.WalkDir(publishDir, func(path string, d fs.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + + relPath, relErr := filepath.Rel(publishDir, path) + if relErr != nil { + return relErr + } + + if relPath == "." { + return nil + } + + relPath = filepath.ToSlash(relPath) + + if d.IsDir() { + return nil + } + + fileData, readErr := os.ReadFile(path) //nolint:gosec // path from WalkDir within temp publish dir + if readErr != nil { + return fmt.Errorf("failed to read %s: %w", relPath, readErr) + } + + w, createErr := zipWriter.Create(relPath) + if createErr != nil { + return fmt.Errorf("failed to create ZIP entry %s: %w", relPath, createErr) + } + + if _, writeErr := w.Write(fileData); writeErr != nil { + return fmt.Errorf("failed to write ZIP entry %s: %w", relPath, writeErr) + } + + return nil + }) + + if err != nil { + return "", "", fmt.Errorf("failed to walk publish directory: %w", err) + } + + if err := zipWriter.Close(); err != nil { + return "", "", fmt.Errorf("failed to close ZIP: %w", err) + } + + if err := tmpFile.Close(); err != nil { + return "", "", fmt.Errorf("failed to close temp file: %w", err) + } + + // Enforce maximum ZIP size (250 MB) — same limit as packageCodeDeploy + const maxZipSizeBundled = 250 * 1024 * 1024 + if fi, statErr := os.Stat(tmpPath); statErr == nil && fi.Size() > maxZipSizeBundled { + return "", "", fmt.Errorf( + "bundled package too large: %d MB (max 250 MB). Consider using remote_build for dependency resolution", + fi.Size()/(1024*1024), + ) + } + sha256Hex := hex.EncodeToString(hasher.Sum(nil)) success = true @@ -1133,7 +1295,8 @@ func (p *AgentServiceTargetProvider) deployHostedCodeAgent( if agentDef.CodeConfiguration != nil { fmt.Fprintf(os.Stderr, "Runtime: %s\n", agentDef.CodeConfiguration.Runtime) - fmt.Fprintf(os.Stderr, "Entry Point: [\"python\", \"%s\"]\n", agentDef.CodeConfiguration.EntryPoint) + cmdPrefix := agent_yaml.RuntimeCmdPrefix(agentDef.CodeConfiguration.Runtime) + fmt.Fprintf(os.Stderr, "Entry Point: [\"%s\", \"%s\"]\n", cmdPrefix, agentDef.CodeConfiguration.EntryPoint) depRes := "remote_build" if agentDef.CodeConfiguration.DependencyResolution != nil { depRes = *agentDef.CodeConfiguration.DependencyResolution