Files
kandev/apps/backend/internal/backendapp/task_agent_executor_compatibility.go
T
2026-09-19 21:26:36 +01:00

160 lines
4.6 KiB
Go

package backendapp
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/kandev/kandev/internal/agent/agents"
"github.com/kandev/kandev/internal/agent/registry"
"github.com/kandev/kandev/internal/agent/remoteauth"
agentsettingsmodels "github.com/kandev/kandev/internal/agent/settings/models"
"github.com/kandev/kandev/internal/task/models"
)
type taskAgentExecutorCompatibilityValidator struct {
profiles agentProfileCompatibilityReader
agentRegistry *registry.Registry
dynamicResolver profileExecutionValidator
}
type profileExecutionValidator interface {
ValidateProfile(context.Context, string) error
}
type agentProfileCompatibilityReader interface {
GetAgent(ctx context.Context, id string) (*agentsettingsmodels.Agent, error)
}
func (v taskAgentExecutorCompatibilityValidator) validateAgentFamily(
ctx context.Context,
profile *agentsettingsmodels.AgentProfile,
) (agents.Agent, error) {
if v.profiles == nil || v.agentRegistry == nil {
return nil, fmt.Errorf("agent compatibility registry is unavailable")
}
settingsAgent, err := v.profiles.GetAgent(ctx, profile.AgentID)
if err != nil {
return nil, fmt.Errorf("load agent family: %w", err)
}
if settingsAgent == nil {
return nil, fmt.Errorf("agent family is unavailable")
}
if settingsAgent.Name == agents.DynamicAgentID {
if v.dynamicResolver == nil {
return nil, fmt.Errorf("dynamic agent routing validator is unavailable")
}
return nil, nil
}
registeredAgent, ok := v.agentRegistry.Get(settingsAgent.Name)
if !ok || registeredAgent == nil || !registeredAgent.Enabled() || agents.IsVirtualAgent(registeredAgent) {
return nil, fmt.Errorf("agent family %q cannot execute", settingsAgent.Name)
}
return registeredAgent, nil
}
func (v taskAgentExecutorCompatibilityValidator) ValidateAgentProfileForExecutor(
ctx context.Context,
profile *agentsettingsmodels.AgentProfile,
executor *models.Executor,
executorProfile *models.ExecutorProfile,
) error {
if profile == nil {
return fmt.Errorf("agent profile is unavailable")
}
if executor == nil {
return fmt.Errorf("executor is unavailable")
}
if v.dynamicResolver != nil {
if err := v.dynamicResolver.ValidateProfile(ctx, profile.ID); err != nil {
return err
}
}
registeredAgent, err := v.validateAgentFamily(ctx, profile)
if err != nil {
return err
}
if registeredAgent == nil {
// Dynamic profiles have already passed the runtime validator and resolve
// to a concrete candidate at launch.
return nil
}
if !models.IsRemoteExecutorType(executor.Type) {
return nil
}
if executorProfile == nil {
return fmt.Errorf("remote executor profile is required")
}
return validateRemoteAgentCredentials(registeredAgent, executorProfile)
}
func validateRemoteAgentCredentials(agent agents.Agent, executorProfile *models.ExecutorProfile) error {
catalog := remoteauth.BuildCatalog([]agents.Agent{agent})
var spec *remoteauth.Spec
for i := range catalog.Specs {
if catalog.Specs[i].ID == agent.ID() {
spec = &catalog.Specs[i]
break
}
}
if spec == nil {
return fmt.Errorf("agent family %q has no remote credential specification", agent.ID())
}
if len(spec.Methods) == 0 {
return nil
}
configuredFiles, err := decodeRemoteCredentialIDs(executorProfile.Config["remote_credentials"])
if err != nil {
return fmt.Errorf("invalid remote_credentials: %w", err)
}
configuredFileSet := make(map[string]struct{}, len(configuredFiles))
for _, methodID := range configuredFiles {
configuredFileSet[methodID] = struct{}{}
}
configuredSecrets, err := decodeRemoteCredentialSecrets(executorProfile.Config["remote_auth_secrets"])
if err != nil {
return fmt.Errorf("invalid remote_auth_secrets: %w", err)
}
for _, method := range spec.Methods {
if method.Type != "env" {
if _, ok := configuredFileSet[method.MethodID]; ok {
return nil
}
continue
}
if secret, ok := configuredSecrets[method.MethodID]; ok && strings.TrimSpace(secret) != "" {
return nil
}
}
return fmt.Errorf("no credentials are configured for agent family %q", agent.ID())
}
func decodeRemoteCredentialIDs(raw string) ([]string, error) {
if strings.TrimSpace(raw) == "" {
return nil, nil
}
var values []string
if err := json.Unmarshal([]byte(raw), &values); err != nil {
return nil, err
}
return values, nil
}
func decodeRemoteCredentialSecrets(raw string) (map[string]string, error) {
if strings.TrimSpace(raw) == "" {
return nil, nil
}
var values map[string]*string
if err := json.Unmarshal([]byte(raw), &values); err != nil {
return nil, err
}
result := make(map[string]string, len(values))
for key, value := range values {
if value != nil {
result[key] = *value
}
}
return result, nil
}