iop/packages/go/agentconfig/validate.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))
}