207 lines
6.7 KiB
Go
207 lines
6.7 KiB
Go
package agentconfig
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
var stableIDPattern = regexp.MustCompile(`^[a-z0-9][a-z0-9._-]*$`)
|
|
|
|
var validCapabilities = map[string]struct{}{
|
|
"approval_bypass": {},
|
|
"cancel": {},
|
|
"resume": {},
|
|
"run": {},
|
|
"status": {},
|
|
"unattended": {},
|
|
"writable_root_confinement": {},
|
|
}
|
|
|
|
var validModes = map[string]struct{}{
|
|
"": {},
|
|
"antigravity-print": {},
|
|
"codex-app-server": {},
|
|
"codex-exec": {},
|
|
"opencode-sse": {},
|
|
"persistent-lazy": {},
|
|
}
|
|
|
|
// Validate checks stable IDs, cross references, probes, capabilities, and the
|
|
// repository catalog's secret-free boundary.
|
|
func (c Catalog) Validate() error {
|
|
if c.Version != SchemaVersion {
|
|
return fmt.Errorf("agentconfig: unsupported catalog version %q", c.Version)
|
|
}
|
|
if len(c.Providers) == 0 || len(c.Models) == 0 || len(c.Profiles) == 0 {
|
|
return fmt.Errorf("agentconfig: providers, models, and profiles must be non-empty")
|
|
}
|
|
|
|
providers := make(map[string]Provider, len(c.Providers))
|
|
for _, provider := range c.Providers {
|
|
if err := validateID("provider", provider.ID); err != nil {
|
|
return err
|
|
}
|
|
if _, exists := providers[provider.ID]; exists {
|
|
return fmt.Errorf("agentconfig: duplicate provider id %q", provider.ID)
|
|
}
|
|
if strings.TrimSpace(provider.Command) == "" {
|
|
return fmt.Errorf("agentconfig: provider %q command is required", provider.ID)
|
|
}
|
|
if len(provider.VersionProbe.Args) == 0 {
|
|
return fmt.Errorf("agentconfig: provider %q version_probe.args is required", provider.ID)
|
|
}
|
|
if len(provider.Authentication.Args) == 0 {
|
|
return fmt.Errorf("agentconfig: provider %q authentication.args is required", provider.ID)
|
|
}
|
|
if err := validateTimeout("provider "+provider.ID+" version probe", provider.VersionProbe.TimeoutMS); err != nil {
|
|
return err
|
|
}
|
|
if err := validateTimeout("provider "+provider.ID+" authentication probe", provider.Authentication.TimeoutMS); err != nil {
|
|
return err
|
|
}
|
|
if len(provider.ModelProbe.Args) > 0 {
|
|
if err := validateTimeout("provider "+provider.ID+" model probe", provider.ModelProbe.TimeoutMS); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for field, expression := range map[string]string{
|
|
"success_pattern": provider.Authentication.SuccessPattern,
|
|
"unauthenticated_pattern": provider.Authentication.UnauthenticatedPattern,
|
|
} {
|
|
if expression != "" {
|
|
if _, err := regexp.Compile(expression); err != nil {
|
|
return fmt.Errorf("agentconfig: provider %q authentication.%s: %w", provider.ID, field, err)
|
|
}
|
|
}
|
|
}
|
|
if err := validateCapabilities("provider "+provider.ID, provider.Capabilities); err != nil {
|
|
return err
|
|
}
|
|
providers[provider.ID] = provider
|
|
}
|
|
|
|
models := make(map[string]Model, len(c.Models))
|
|
for _, model := range c.Models {
|
|
if err := validateID("model", model.ID); err != nil {
|
|
return err
|
|
}
|
|
if _, exists := models[model.ID]; exists {
|
|
return fmt.Errorf("agentconfig: duplicate model id %q", model.ID)
|
|
}
|
|
if _, exists := providers[model.Provider]; !exists {
|
|
return fmt.Errorf("agentconfig: model %q references unknown provider %q", model.ID, model.Provider)
|
|
}
|
|
if strings.TrimSpace(model.Target) == "" {
|
|
return fmt.Errorf("agentconfig: model %q target is required", model.ID)
|
|
}
|
|
models[model.ID] = model
|
|
}
|
|
|
|
profiles := make(map[string]struct{}, len(c.Profiles))
|
|
for _, profile := range c.Profiles {
|
|
if err := validateID("profile", profile.ID); err != nil {
|
|
return err
|
|
}
|
|
if _, exists := profiles[profile.ID]; exists {
|
|
return fmt.Errorf("agentconfig: duplicate profile id %q", profile.ID)
|
|
}
|
|
provider, providerExists := providers[profile.Provider]
|
|
if !providerExists {
|
|
return fmt.Errorf("agentconfig: profile %q references unknown provider %q", profile.ID, profile.Provider)
|
|
}
|
|
model, modelExists := models[profile.Model]
|
|
if !modelExists {
|
|
return fmt.Errorf("agentconfig: profile %q references unknown model %q", profile.ID, profile.Model)
|
|
}
|
|
if model.Provider != profile.Provider {
|
|
return fmt.Errorf(
|
|
"agentconfig: profile %q provider %q does not own model %q",
|
|
profile.ID, profile.Provider, profile.Model,
|
|
)
|
|
}
|
|
if _, exists := validModes[profile.Mode]; !exists {
|
|
return fmt.Errorf("agentconfig: profile %q has unsupported mode %q", profile.ID, profile.Mode)
|
|
}
|
|
if profile.ResponseIdleTimeoutMS < 0 || profile.StartupIdleTimeoutMS < 0 {
|
|
return fmt.Errorf("agentconfig: profile %q idle timeouts must be non-negative", profile.ID)
|
|
}
|
|
if profile.MaxConcurrency < 0 {
|
|
return fmt.Errorf("agentconfig: profile %q max_concurrency must be non-negative", profile.ID)
|
|
}
|
|
if err := validateCapabilities("profile "+profile.ID, profile.Capabilities); err != nil {
|
|
return err
|
|
}
|
|
providerCaps := make(map[string]struct{}, len(provider.Capabilities))
|
|
for _, capability := range provider.Capabilities {
|
|
providerCaps[capability] = struct{}{}
|
|
}
|
|
for _, capability := range profile.Capabilities {
|
|
if _, exists := providerCaps[capability]; !exists {
|
|
return fmt.Errorf(
|
|
"agentconfig: profile %q capability %q is not declared by provider %q",
|
|
profile.ID, capability, profile.Provider,
|
|
)
|
|
}
|
|
}
|
|
for _, env := range profile.Env {
|
|
if secretBearingEnv(env) {
|
|
return fmt.Errorf("agentconfig: profile %q env %q may contain a tracked secret", profile.ID, envKey(env))
|
|
}
|
|
}
|
|
profiles[profile.ID] = struct{}{}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateID(kind, id string) error {
|
|
if !stableIDPattern.MatchString(id) {
|
|
return fmt.Errorf("agentconfig: invalid %s id %q", kind, id)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateTimeout(label string, timeoutMS int) error {
|
|
if timeoutMS < 0 || timeoutMS > 120_000 {
|
|
return fmt.Errorf("agentconfig: %s timeout_ms must be between 0 and 120000", label)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func validateCapabilities(label string, capabilities []string) error {
|
|
seen := make(map[string]struct{}, len(capabilities))
|
|
for _, capability := range capabilities {
|
|
if _, exists := validCapabilities[capability]; !exists {
|
|
return fmt.Errorf("agentconfig: %s has invalid capability %q", label, capability)
|
|
}
|
|
if _, exists := seen[capability]; exists {
|
|
return fmt.Errorf("agentconfig: %s repeats capability %q", label, capability)
|
|
}
|
|
seen[capability] = struct{}{}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func secretBearingEnv(entry string) bool {
|
|
key := envKey(entry)
|
|
switch {
|
|
case key == "AUTHORIZATION",
|
|
key == "PASSWORD",
|
|
key == "TOKEN",
|
|
key == "SECRET",
|
|
key == "API_KEY",
|
|
strings.HasSuffix(key, "_PASSWORD"),
|
|
strings.HasSuffix(key, "_TOKEN"),
|
|
strings.HasSuffix(key, "_SECRET"),
|
|
strings.HasSuffix(key, "_API_KEY"),
|
|
strings.HasSuffix(key, "_ACCESS_KEY"):
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func envKey(entry string) string {
|
|
key, _, _ := strings.Cut(entry, "=")
|
|
return strings.ToUpper(strings.TrimSpace(key))
|
|
}
|