160 lines
4.6 KiB
Go
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
|
|
}
|