diff --git a/internal/cli/agent/frameworks/adk/python/generator.go b/internal/cli/agent/frameworks/adk/python/generator.go index 2e9e473d..70b37265 100644 --- a/internal/cli/agent/frameworks/adk/python/generator.go +++ b/internal/cli/agent/frameworks/adk/python/generator.go @@ -8,6 +8,7 @@ import ( "github.com/agentregistry-dev/agentregistry/internal/cli/agent/frameworks/common" "github.com/agentregistry-dev/agentregistry/pkg/models" + "github.com/agentregistry-dev/agentregistry/pkg/validators" ) //go:embed templates/* templates/agent/* templates/mcp_server/* dice-agent-instruction.md @@ -31,6 +32,11 @@ func (g *PythonGenerator) Generate(agentConfig *common.AgentConfig) error { return fmt.Errorf("agent config is required") } + // Python identifiers cannot contain hyphens (e.g., "my-agent" is parsed as + // "my minus agent"), so convert to underscores for the package directory and + // module name. + agentConfig.Name = validators.PythonSafeName(agentConfig.Name) + projectPackageDir := filepath.Join(agentConfig.Directory, agentConfig.Name) if err := os.MkdirAll(projectPackageDir, 0o755); err != nil { return fmt.Errorf("failed to create package directory: %w", err) diff --git a/pkg/validators/names.go b/pkg/validators/names.go index d76558df..9df7d095 100644 --- a/pkg/validators/names.go +++ b/pkg/validators/names.go @@ -57,20 +57,22 @@ func ValidateProjectName(name string) error { return nil } -// agentNameRegex enforces the strictest rule - names that work BOTH as Python identifiers AND as publishable agent names. -// Must start with a letter, followed by alphanumeric only, minimum 2 characters. -var agentNameRegex = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9]+$`) +// agentNameRegex enforces names that work as publishable agent names. +// Must start with a letter, end with a letter or digit, can contain hyphens in the middle. +// Dots are intentionally excluded to avoid .well-known URL path routing ambiguity. +// Minimum 2 characters. +var agentNameRegex = regexp.MustCompile(`^[a-zA-Z][a-zA-Z0-9-]*[a-zA-Z0-9]$`) // ValidateAgentName checks if the agent name is valid. -// Allowed: letters and digits only, must start with a letter, minimum 2 characters. -// Not allowed: underscores, dots, hyphens, or Python keywords. +// Allowed: letters, digits, and hyphens; must start with a letter, end with a letter or digit. +// Not allowed: underscores, dots, or Python keywords. func ValidateAgentName(name string) error { if name == "" { return fmt.Errorf("agent name cannot be empty") } if !agentNameRegex.MatchString(name) { - return fmt.Errorf("agent name must start with a letter and contain only letters and digits (minimum 2 characters)") + return fmt.Errorf("agent name must start with a letter, end with a letter or digit, and contain only letters, digits, and hyphens (minimum 2 characters)") } // Reject Python keywords to avoid issues in generated code @@ -81,6 +83,12 @@ func ValidateAgentName(name string) error { return nil } +// PythonSafeName converts an agent name to a valid Python identifier +// by replacing hyphens with underscores. +func PythonSafeName(name string) string { + return strings.ReplaceAll(name, "-", "_") +} + // ValidateMCPServerName checks if the MCP server name matches the required format. // Server name must be in format "namespace/name" where: // - namespace: starts/ends with alphanumeric, can contain dots and hyphens, min 2 chars diff --git a/pkg/validators/names_test.go b/pkg/validators/names_test.go index ec74d367..882844ef 100644 --- a/pkg/validators/names_test.go +++ b/pkg/validators/names_test.go @@ -46,23 +46,25 @@ func TestValidateAgentName(t *testing.T) { input string wantErr bool }{ - // Valid cases - letters and digits only, starts with letter, min 2 chars + // Valid cases - starts with letter, ends with letter or digit, min 2 chars {"valid simple", "myagent", false}, {"valid alphanumeric", "agent123", false}, {"valid mixed case", "MyAgent2", false}, {"valid two chars", "ab", false}, + {"valid with hyphen", "my-agent", false}, + {"valid multi-hyphen", "my-cool-agent", false}, - // Invalid - special characters not allowed - {"hyphen not allowed", "my-agent", true}, + // Invalid - special characters not allowed (except hyphens) {"dot not allowed", "my.agent", true}, {"underscore not allowed", "my_agent", true}, {"contains slash", "my/agent", true}, {"contains space", "my agent", true}, - // Invalid - must start with letter + // Invalid - must start with letter, end with letter or digit {"starts with number", "123agent", true}, {"starts with dot", ".agent", true}, {"starts with hyphen", "-agent", true}, + {"trailing hyphen rejected", "agent-", true}, // Invalid - too short {"single char", "a", true}, @@ -149,3 +151,25 @@ func TestValidateSkillName(t *testing.T) { }) } } + +func TestPythonSafeName(t *testing.T) { + tests := []struct { + name string + input string + expected string + }{ + {"converts hyphens", "my-agent", "my_agent"}, + {"no hyphens unchanged", "myagent", "myagent"}, + {"multiple hyphens", "my-cool-agent", "my_cool_agent"}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := PythonSafeName(tt.input) + if result != tt.expected { + t.Errorf("PythonSafeName(%q) = %q, want %q", + tt.input, result, tt.expected) + } + }) + } +}