fix(validation): support hyphenated rig names in agent IDs (GH#854)
Rewrites ValidateAgentID to scan right-to-left for known role tokens instead of relying on fixed position parsing. This allows rig names containing hyphens (e.g., ob-my-project-witness). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -146,6 +146,8 @@ func isNamedRole(s string) bool {
|
||||
// - Per-rig named: <prefix>-<rig>-<role>-<name> (e.g., gt-gastown-polecat-nux)
|
||||
//
|
||||
// The prefix can be any rig's configured prefix (gt-, bd-, etc.).
|
||||
// Rig names may contain hyphens (e.g., my-project), so we parse by scanning
|
||||
// for known role tokens from the right side of the ID.
|
||||
// Returns nil if the ID is valid, or an error describing the issue.
|
||||
func ValidateAgentID(id string) error {
|
||||
if id == "" {
|
||||
@@ -165,7 +167,7 @@ func ValidateAgentID(id string) error {
|
||||
return fmt.Errorf("agent ID must include content after prefix (got %q)", id)
|
||||
}
|
||||
|
||||
// Case 1: Town-level roles (gt-mayor, gt-deacon)
|
||||
// Case 1: Single part after prefix - must be town-level role
|
||||
if len(parts) == 1 {
|
||||
role := parts[0]
|
||||
if isTownLevelRole(role) {
|
||||
@@ -177,53 +179,52 @@ func ValidateAgentID(id string) error {
|
||||
return fmt.Errorf("invalid agent role %q (valid: %s)", role, strings.Join(ValidAgentRoles, ", "))
|
||||
}
|
||||
|
||||
// Case 2: Rig-level roles (<prefix>-<rig>-witness, <prefix>-<rig>-refinery)
|
||||
if len(parts) == 2 {
|
||||
rig, role := parts[0], parts[1]
|
||||
|
||||
if isRigLevelRole(role) {
|
||||
if rig == "" {
|
||||
return fmt.Errorf("rig name cannot be empty in %q", id)
|
||||
}
|
||||
return nil // Valid rig-level singleton agent
|
||||
// For 2+ parts, scan from the right to find a known role.
|
||||
// This allows rig names to contain hyphens (e.g., "my-project").
|
||||
roleIdx := -1
|
||||
var role string
|
||||
for i := len(parts) - 1; i >= 0; i-- {
|
||||
if isValidRole(parts[i]) {
|
||||
roleIdx = i
|
||||
role = parts[i]
|
||||
break
|
||||
}
|
||||
|
||||
if isNamedRole(role) {
|
||||
return fmt.Errorf("agent role %q requires name: <prefix>-<rig>-%s-<name> (got %q)", role, role, id)
|
||||
}
|
||||
|
||||
if isTownLevelRole(role) {
|
||||
return fmt.Errorf("town-level agent %q cannot have rig suffix (expected <prefix>-%s, got %q)", role, role, id)
|
||||
}
|
||||
|
||||
// First part might be a rig name with second part being something invalid
|
||||
return fmt.Errorf("invalid agent format: expected <prefix>-<rig>-<role>[-<name>] where role is one of: %s (got %q)", strings.Join(ValidAgentRoles, ", "), id)
|
||||
}
|
||||
|
||||
// Case 3: Named roles (<prefix>-<rig>-crew-<name>, <prefix>-<rig>-polecat-<name>)
|
||||
if len(parts) >= 3 {
|
||||
rig, role := parts[0], parts[1]
|
||||
name := strings.Join(parts[2:], "-") // Allow hyphens in names
|
||||
if roleIdx == -1 {
|
||||
return fmt.Errorf("invalid agent format: no valid role found in %q (valid roles: %s)", id, strings.Join(ValidAgentRoles, ", "))
|
||||
}
|
||||
|
||||
if isNamedRole(role) {
|
||||
if rig == "" {
|
||||
return fmt.Errorf("rig name cannot be empty in %q", id)
|
||||
}
|
||||
if name == "" {
|
||||
return fmt.Errorf("agent name cannot be empty in %q", id)
|
||||
}
|
||||
return nil // Valid named agent
|
||||
}
|
||||
// Extract rig (everything before role) and name (everything after role)
|
||||
rig := strings.Join(parts[:roleIdx], "-")
|
||||
name := strings.Join(parts[roleIdx+1:], "-")
|
||||
|
||||
if isRigLevelRole(role) {
|
||||
return fmt.Errorf("agent role %q cannot have name suffix (expected <prefix>-<rig>-%s, got %q)", role, role, id)
|
||||
}
|
||||
|
||||
if isTownLevelRole(role) {
|
||||
// Validate based on role type
|
||||
if isTownLevelRole(role) {
|
||||
if rig != "" || name != "" {
|
||||
return fmt.Errorf("town-level agent %q cannot have rig/name suffixes (expected <prefix>-%s, got %q)", role, role, id)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("invalid agent format: expected <prefix>-<rig>-<role>-<name> where role is one of: %s (got %q)", strings.Join(NamedRoles, ", "), id)
|
||||
if isRigLevelRole(role) {
|
||||
if rig == "" {
|
||||
return fmt.Errorf("agent role %q requires rig: <prefix>-<rig>-%s (got %q)", role, role, id)
|
||||
}
|
||||
if name != "" {
|
||||
return fmt.Errorf("agent role %q cannot have name suffix (expected <prefix>-<rig>-%s, got %q)", role, role, id)
|
||||
}
|
||||
return nil // Valid rig-level singleton agent
|
||||
}
|
||||
|
||||
if isNamedRole(role) {
|
||||
if rig == "" {
|
||||
return fmt.Errorf("rig name cannot be empty in %q", id)
|
||||
}
|
||||
if name == "" {
|
||||
return fmt.Errorf("agent role %q requires name: <prefix>-<rig>-%s-<name> (got %q)", role, role, id)
|
||||
}
|
||||
return nil // Valid named agent
|
||||
}
|
||||
|
||||
return fmt.Errorf("invalid agent ID format: %q", id)
|
||||
|
||||
@@ -220,6 +220,14 @@ func TestValidateAgentID(t *testing.T) {
|
||||
{"valid bd-beads-polecat-pearl", "bd-beads-polecat-pearl", false, ""},
|
||||
{"valid bd-beads-witness", "bd-beads-witness", false, ""},
|
||||
|
||||
// Valid: hyphenated rig names (GH#854)
|
||||
{"hyphenated rig witness", "ob-my-project-witness", false, ""},
|
||||
{"hyphenated rig refinery", "gt-foo-bar-refinery", false, ""},
|
||||
{"hyphenated rig crew", "bd-my-cool-project-crew-fang", false, ""},
|
||||
{"hyphenated rig polecat", "gt-some-long-rig-name-polecat-nux", false, ""},
|
||||
{"hyphenated rig and name", "gt-my-rig-polecat-war-boy", false, ""},
|
||||
{"multi-hyphen rig crew", "ob-a-b-c-d-crew-dave", false, ""},
|
||||
|
||||
// Invalid: no prefix (missing hyphen)
|
||||
{"no prefix", "mayor", true, "must have a prefix followed by '-'"},
|
||||
|
||||
@@ -230,8 +238,8 @@ func TestValidateAgentID(t *testing.T) {
|
||||
{"unknown role", "gt-gastown-admin", true, "invalid agent format"},
|
||||
|
||||
// Invalid: town-level with rig (put role first)
|
||||
{"mayor with rig suffix", "gt-gastown-mayor", true, "cannot have rig suffix"},
|
||||
{"deacon with rig suffix", "gt-beads-deacon", true, "cannot have rig suffix"},
|
||||
{"mayor with rig suffix", "gt-gastown-mayor", true, "cannot have rig/name suffixes"},
|
||||
{"deacon with rig suffix", "gt-beads-deacon", true, "cannot have rig/name suffixes"},
|
||||
|
||||
// Invalid: per-rig role without rig
|
||||
{"witness alone", "gt-witness", true, "requires rig"},
|
||||
|
||||
Reference in New Issue
Block a user