Skip to content
Open
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
6 changes: 6 additions & 0 deletions internal/cli/agent/frameworks/adk/python/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down
20 changes: 14 additions & 6 deletions pkg/validators/names.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Comment on lines +68 to 69
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am confused, because using underscores didn't cause problems. arctl agent init adk python test-agent failed but arctl agent init adk python test_agent worked. So I am unsure how the latter worked when the comment in L68 suggests otherwise

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to validate it, but the CLI build against the PR, and the commit 1ab39e4, the PR is based on, fails when supplying test_agent as a name.
Also in names-test.go, there is a specific assessment that tests that _ is not allowed.

{"underscore not allowed", "my_agent", true},

I just did not change it.
Also, agentNameRegex in the 1ab39e4 and PR does not allow _.
So I'm also not sure how _ ever worked.

The comment here restates what tests and regexp show.

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
Expand All @@ -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
Expand Down
32 changes: 28 additions & 4 deletions pkg/validators/names_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down Expand Up @@ -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)
}
})
}
}
Loading