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 // versions resolves a pinned version back to the definition that answered. // See LoadAgentVersion. versions *repo.VersionsRepo } // NewLoader builds a runtime definition loader over a storage repository. func NewLoader(db repo.Querier) *Loader { return &Loader{ repo: repo.NewDefinitionsRepo(db), versions: repo.NewVersionsRepo(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, Tools: parsed.Tools, // The spec's corpora, and the ONLY place they come from. A model that // asked to search a source its agent was not granted is asking for a // list it has no way to set — see tools.Context.KnowledgeSources. KnowledgeSources: parsed.Sources, 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 } /* ── Pinned versions ────────────────────────────────────────────────────── */ // LoadAgentVersion loads an agent AS IT WAS at a published version. // // The current definition is not consulted at all — that is the point. An agent // edited since a conversation began is a different agent, and a run that quietly // switched to it would answer a question the reader never asked with tools they // were never offered. // // Falls back to the current definition when the version is not in the history, // and does so deliberately rather than failing. Every definition published // before migration 000010 has no snapshot; refusing those would break every // existing conversation to enforce a rule that could not have been followed // when they started. The fallback is recorded by the caller, so a run that // could not pin is visible rather than silent. func (l *Loader) LoadAgentVersion(ctx context.Context, ident authctx.Identity, idOrDefID string, version int) (*Agent, bool, error) { current, err := l.LoadExecutableAgent(ctx, ident, idOrDefID) if err != nil { return nil, false, err } if version <= 0 || version == current.Version { return current, false, nil } snapshot, err := l.versions.Load(ctx, ident, repo.KindAgent, current.ID, version) if err != nil || snapshot == nil { // No snapshot for that number. The run continues on the current // definition — see the note above — and the caller records that it // could not pin. return current, true, nil } parsed, err := definition.ParseAgent(snapshot.Markdown, definition.Options{}) if err != nil { return current, true, nil } // Rebuilt from the snapshot, keeping the identity fields that belong to the // row rather than to the definition text. pinned := *current pinned.Name = parsed.Name pinned.Description = parsed.Description pinned.Version = snapshot.Version pinned.Pages = parsed.Pages pinned.Reasoning = parsed.Reasoning pinned.Instructions = parsed.Instructions pinned.Skills = parsed.Skills pinned.Tools = parsed.Tools pinned.KnowledgeSources = parsed.Sources pinned.Subagents = parsed.Subagents pinned.RawMarkdown = snapshot.Markdown return &pinned, false, nil }