package runtime import ( "context" "errors" "fmt" "regexp" "github.com/krow/krow-backend/go-api/internal/authctx" "github.com/krow/krow-backend/go-api/internal/definition" "github.com/krow/krow-backend/go-api/internal/domain" "github.com/krow/krow-backend/go-api/internal/repo" ) var uuidPattern = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`) func isUUID(s string) bool { return uuidPattern.MatchString(s) } // Loader loads and validates authored definitions into runtime representations with tenant isolation. type Loader struct { repo *repo.DefinitionsRepo } // NewLoader builds a runtime definition loader over a storage repository. func NewLoader(db repo.Querier) *Loader { return &Loader{repo: repo.NewDefinitionsRepo(db)} } // LoadAgent loads an agent definition by id or definition_id, parsing it into a runtime representation. func (l *Loader) LoadAgent(ctx context.Context, ident authctx.Identity, idOrDefID string) (*Agent, error) { var ( rec domain.Record err error ) if isUUID(idOrDefID) { rec, err = l.repo.GetAgent(ctx, ident, idOrDefID) } else { rec, err = l.repo.GetAgentByDefinitionID(ctx, ident, idOrDefID) } if err != nil { return nil, err } if rec == nil { return nil, fmt.Errorf("%w: agent %q", ErrNotFound, idOrDefID) } rawMD, ok := rec["markdown"].(string) if !ok || rawMD == "" { return nil, fmt.Errorf("%w: missing markdown payload for agent %q", ErrInvalidDefinition, idOrDefID) } if err := definition.ValidateAgent(rawMD); err != nil { return nil, fmt.Errorf("%w: %v", ErrInvalidDefinition, err) } parsed, err := definition.ParseAgent(rawMD, definition.Options{}) if err != nil { return nil, fmt.Errorf("%w: %v", ErrInvalidDefinition, err) } agent := &Agent{ ID: parsed.ID, DatabaseID: rec["id"].(string), Name: parsed.Name, Description: parsed.Description, Status: parsed.Status, Version: parsed.Version, Visibility: rec["visibility"].(string), Pages: parsed.Pages, Icon: parsed.Icon, Reasoning: parsed.Reasoning, Trigger: parsed.Trigger, WebSearch: parsed.WebSearch, Instructions: parsed.Instructions, Skills: parsed.Skills, Subagents: parsed.Subagents, RawMarkdown: rawMD, } if rec["owner_user_id"] != nil { if uid, ok := rec["owner_user_id"].(string); ok && uid != "" { agent.OwnerUserID = &uid } } return agent, nil } // LoadSkill loads a skill definition by id or definition_id, parsing it into a runtime representation. func (l *Loader) LoadSkill(ctx context.Context, ident authctx.Identity, idOrDefID string) (*Skill, error) { var ( rec domain.Record err error ) if isUUID(idOrDefID) { rec, err = l.repo.GetSkill(ctx, ident, idOrDefID) } else { rec, err = l.repo.GetSkillByDefinitionID(ctx, ident, idOrDefID) } if err != nil { return nil, err } if rec == nil { return nil, fmt.Errorf("%w: skill %q", ErrNotFound, idOrDefID) } rawMD, ok := rec["markdown"].(string) if !ok || rawMD == "" { return nil, fmt.Errorf("%w: missing markdown payload for skill %q", ErrInvalidDefinition, idOrDefID) } if err := definition.ValidateSkill(rawMD); err != nil { return nil, fmt.Errorf("%w: %v", ErrInvalidDefinition, err) } parsed, err := definition.ParseSkill(rawMD, definition.Options{}) if err != nil { return nil, fmt.Errorf("%w: %v", ErrInvalidDefinition, err) } skill := &Skill{ ID: parsed.ID, DatabaseID: rec["id"].(string), Name: parsed.Name, Description: parsed.Description, Status: parsed.Status, Visibility: rec["visibility"].(string), Pages: parsed.Pages, Kind: parsed.Kind, Category: parsed.Category, Actions: parsed.Actions, Triggers: parsed.Triggers, Prompt: parsed.Prompt, SkillID: parsed.SkillID, Body: parsed.Body, RawMarkdown: rawMD, } if rec["owner_user_id"] != nil { if uid, ok := rec["owner_user_id"].(string); ok && uid != "" { skill.OwnerUserID = &uid } } return skill, nil } // ResolveAgentDependencies resolves all skill dependencies referenced by the agent within caller scope. func (l *Loader) ResolveAgentDependencies(ctx context.Context, ident authctx.Identity, agent *Agent) error { if len(agent.Skills) == 0 { agent.ResolvedSkills = []*Skill{} return nil } visited := make(map[string]*Skill) inProgress := make(map[string]bool) resolved := make([]*Skill, 0, len(agent.Skills)) for _, skillID := range agent.Skills { if _, ok := visited[skillID]; ok { // Deterministic deduplication continue } if inProgress[skillID] { return fmt.Errorf("%w: skill %q", ErrCircularDependency, skillID) } inProgress[skillID] = true skill, err := l.LoadSkill(ctx, ident, skillID) if err != nil { if errors.Is(err, ErrNotFound) { return fmt.Errorf("%w: skill %q", ErrDependencyMissing, skillID) } return err } if skill.Status != "active" { return fmt.Errorf("%w: skill %q has status %q", ErrDependencyInactive, skillID, skill.Status) } inProgress[skillID] = false visited[skillID] = skill resolved = append(resolved, skill) } agent.ResolvedSkills = resolved return nil } // LoadExecutableAgent loads an agent, verifies its published status, and resolves all active dependencies. func (l *Loader) LoadExecutableAgent(ctx context.Context, ident authctx.Identity, idOrDefID string) (*Agent, error) { agent, err := l.LoadAgent(ctx, ident, idOrDefID) if err != nil { return nil, err } switch agent.Status { case "published": // Eligible case "draft": return nil, fmt.Errorf("%w: agent %q is in draft status", ErrDraftAgent, agent.ID) case "archived": return nil, fmt.Errorf("%w: agent %q is archived", ErrArchivedAgent, agent.ID) default: return nil, fmt.Errorf("%w: agent %q has unsupported status %q", ErrNotExecutable, agent.ID, agent.Status) } if err := l.ResolveAgentDependencies(ctx, ident, agent); err != nil { return nil, err } return agent, nil } // LoadExecutableSkill loads a skill and verifies its active status. func (l *Loader) LoadExecutableSkill(ctx context.Context, ident authctx.Identity, idOrDefID string) (*Skill, error) { skill, err := l.LoadSkill(ctx, ident, idOrDefID) if err != nil { return nil, err } switch skill.Status { case "active": // Eligible case "inactive": return nil, fmt.Errorf("%w: skill %q is inactive", ErrInactiveSkill, skill.ID) default: return nil, fmt.Errorf("%w: skill %q has unsupported status %q", ErrNotExecutable, skill.ID, skill.Status) } return skill, nil }