first commit

This commit is contained in:
2026-08-24 13:06:29 +05:30
commit 7d12ebef3d
86 changed files with 39996 additions and 0 deletions

View File

@@ -0,0 +1,129 @@
package runtime
import (
"context"
"github.com/krow/krow-backend/go-api/internal/authctx"
"github.com/krow/krow-backend/go-api/internal/repo"
)
// AgentExecutor is the boundary interface for executing an authored agent.
type AgentExecutor interface {
ExecuteAgent(ctx context.Context, agent *Agent, input ExecutionInput) (*ExecutionResult, error)
}
// SkillExecutor is the boundary interface for executing an authored skill.
type SkillExecutor interface {
ExecuteSkill(ctx context.Context, skill *Skill, input ExecutionInput) (*ExecutionResult, error)
}
// UnavailableExecutor is the Phase 4F default stub executor that explicitly refuses execution
// until real AI / Owliver / LangGraph executors are installed in Phase 5.
type UnavailableExecutor struct{}
// ExecuteAgent implements AgentExecutor by returning ErrExecutorUnavailable.
func (u *UnavailableExecutor) ExecuteAgent(_ context.Context, agent *Agent, _ ExecutionInput) (*ExecutionResult, error) {
skillIDs := make([]string, len(agent.ResolvedSkills))
for i, s := range agent.ResolvedSkills {
skillIDs[i] = s.ID
}
return &ExecutionResult{
Success: false,
AgentID: agent.ID,
AgentVersion: agent.Version,
ResolvedSkills: skillIDs,
Error: ErrExecutorUnavailable,
}, ErrExecutorUnavailable
}
// ExecuteSkill implements SkillExecutor by returning ErrExecutorUnavailable.
func (u *UnavailableExecutor) ExecuteSkill(_ context.Context, skill *Skill, _ ExecutionInput) (*ExecutionResult, error) {
return &ExecutionResult{
Success: false,
Error: ErrExecutorUnavailable,
}, ErrExecutorUnavailable
}
// Engine coordinates runtime loading, eligibility checks, dependency resolution and execution.
type Engine struct {
Loader *Loader
AgentExec AgentExecutor
SkillExec SkillExecutor
}
// Option configures the runtime engine.
type Option func(*Engine)
// WithAgentExecutor overrides the agent executor implementation.
func WithAgentExecutor(exec AgentExecutor) Option {
return func(e *Engine) {
if exec != nil {
e.AgentExec = exec
}
}
}
// WithSkillExecutor overrides the skill executor implementation.
func WithSkillExecutor(exec SkillExecutor) Option {
return func(e *Engine) {
if exec != nil {
e.SkillExec = exec
}
}
}
// NewEngine builds a runtime engine over a database querier.
func NewEngine(db repo.Querier, opts ...Option) *Engine {
e := &Engine{
Loader: NewLoader(db),
AgentExec: &UnavailableExecutor{},
SkillExec: &UnavailableExecutor{},
}
for _, opt := range opts {
opt(e)
}
return e
}
// RunAgent loads an executable agent with dependencies and dispatches to the executor boundary.
func (e *Engine) RunAgent(ctx context.Context, ident authctx.Identity, idOrDefID string, input ExecutionInput) (*ExecutionResult, error) {
agent, err := e.Loader.LoadExecutableAgent(ctx, ident, idOrDefID)
if err != nil {
return &ExecutionResult{
Success: false,
Error: err,
}, err
}
res, err := e.AgentExec.ExecuteAgent(ctx, agent, input)
if res == nil {
res = &ExecutionResult{
Success: err == nil,
AgentID: agent.ID,
AgentVersion: agent.Version,
Error: err,
}
}
return res, err
}
// RunSkill loads an executable skill and dispatches to the executor boundary.
func (e *Engine) RunSkill(ctx context.Context, ident authctx.Identity, idOrDefID string, input ExecutionInput) (*ExecutionResult, error) {
skill, err := e.Loader.LoadExecutableSkill(ctx, ident, idOrDefID)
if err != nil {
return &ExecutionResult{
Success: false,
Error: err,
}, err
}
res, err := e.SkillExec.ExecuteSkill(ctx, skill, input)
if res == nil {
res = &ExecutionResult{
Success: err == nil,
Error: err,
}
}
return res, err
}

View File

@@ -0,0 +1,237 @@
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
}

View File

@@ -0,0 +1,890 @@
package runtime_test
import (
"context"
"errors"
"fmt"
"testing"
"github.com/krow/krow-backend/go-api/internal/authctx"
"github.com/krow/krow-backend/go-api/internal/repo"
"github.com/krow/krow-backend/go-api/internal/runtime"
"github.com/krow/krow-backend/go-api/internal/testutil"
)
type fixture struct {
h *testutil.Harness
loader *runtime.Loader
engine *runtime.Engine
defRepo *repo.DefinitionsRepo
org1 string
org2 string
userA authctx.Identity
userB authctx.Identity
userOther authctx.Identity
}
func newFixture(t *testing.T) *fixture {
t.Helper()
h := testutil.New(t)
ctx := context.Background()
var org2 string
if err := h.Pool.QueryRow(ctx,
`INSERT INTO organizations (name, slug) VALUES ('Second Org', 'second-org') RETURNING id::text`).
Scan(&org2); err != nil {
t.Fatalf("create second org: %v", err)
}
var userAID, userBID, userOtherID string
if err := h.Pool.QueryRow(ctx,
`INSERT INTO users (org_id, full_name, email, role, status) VALUES ($1::uuid, 'User A', 'user-a@example.test', 'admin', 'active') RETURNING id::text`,
h.OrgID).Scan(&userAID); err != nil {
t.Fatalf("create userA: %v", err)
}
if err := h.Pool.QueryRow(ctx,
`INSERT INTO users (org_id, full_name, email, role, status) VALUES ($1::uuid, 'User B', 'user-b@example.test', 'talent', 'active') RETURNING id::text`,
h.OrgID).Scan(&userBID); err != nil {
t.Fatalf("create userB: %v", err)
}
if err := h.Pool.QueryRow(ctx,
`INSERT INTO users (org_id, full_name, email, role, status) VALUES ($1::uuid, 'User Other', 'user-other@example.test', 'admin', 'active') RETURNING id::text`,
org2).Scan(&userOtherID); err != nil {
t.Fatalf("create userOther: %v", err)
}
f := &fixture{
h: h,
loader: runtime.NewLoader(h.Pool),
engine: runtime.NewEngine(h.Pool),
defRepo: repo.NewDefinitionsRepo(h.Pool),
org1: h.OrgID,
org2: org2,
userA: authctx.Identity{
UserID: userAID,
OrgID: h.OrgID,
Role: "admin",
Email: "user-a@example.test",
},
userB: authctx.Identity{
UserID: userBID,
OrgID: h.OrgID,
Role: "talent",
Email: "user-b@example.test",
},
userOther: authctx.Identity{
UserID: userOtherID,
OrgID: org2,
Role: "admin",
Email: "user-other@example.test",
},
}
return f
}
/* ── 1. Loader Tests ──────────────────────────────────────────────────────── */
func TestRuntimeLoader_Agent(t *testing.T) {
f := newFixture(t)
ctx := context.Background()
// 1. Published org agent
agentMD := `---
id: talent-scout
name: Talent Scout
description: Discovers matching candidates
status: published
version: 2
pages:
- candidates
icon: sparkles
reasoning: deep
trigger: manual
webSearch: true
skills:
- resume-evaluator
subagents:
- profile-enricher
---
## Instructions
Review candidate profiles with diligence.
`
rec, err := f.defRepo.InsertAgent(ctx, f.userA, repo.AgentInsertInput{
DefinitionID: "talent-scout",
OrgID: f.org1,
Visibility: "organization",
CreatedBy: &f.userA.UserID,
Markdown: agentMD,
Status: "published",
Version: 2,
Name: "Talent Scout",
Description: "Discovers matching candidates",
Pages: []string{"candidates"},
})
if err != nil {
t.Fatalf("insert agent: %v", err)
}
dbUUID := rec["id"].(string)
// Load by definition_id
agentByDefID, err := f.loader.LoadAgent(ctx, f.userB, "talent-scout")
if err != nil {
t.Fatalf("load agent by definition_id: %v", err)
}
if agentByDefID.ID != "talent-scout" || agentByDefID.DatabaseID != dbUUID {
t.Errorf("agent ID mismatch: %+v", agentByDefID)
}
if agentByDefID.Name != "Talent Scout" || agentByDefID.Version != 2 || agentByDefID.Status != "published" {
t.Errorf("agent fields mismatch: %+v", agentByDefID)
}
if agentByDefID.Icon != "sparkles" || agentByDefID.Reasoning != "deep" || !agentByDefID.WebSearch {
t.Errorf("agent config mismatch: %+v", agentByDefID)
}
if len(agentByDefID.Skills) != 1 || agentByDefID.Skills[0] != "resume-evaluator" {
t.Errorf("agent skills mismatch: %v", agentByDefID.Skills)
}
if agentByDefID.Instructions == "" || agentByDefID.RawMarkdown != agentMD {
t.Errorf("agent markdown/instructions mismatch")
}
// Load by UUID
agentByUUID, err := f.loader.LoadAgent(ctx, f.userA, dbUUID)
if err != nil {
t.Fatalf("load agent by UUID: %v", err)
}
if agentByUUID.ID != "talent-scout" {
t.Errorf("agent by UUID ID mismatch: %+v", agentByUUID)
}
// Personal agent for User B
persMD := `---
id: personal-agent
name: Personal Agent
status: draft
version: 1
pages:
- candidates
---
## Instructions
Personal instructions.
`
persRec, err := f.defRepo.InsertAgent(ctx, f.userB, repo.AgentInsertInput{
DefinitionID: "personal-agent",
OrgID: f.org1,
Visibility: "personal",
OwnerUserID: &f.userB.UserID,
CreatedBy: &f.userB.UserID,
Markdown: persMD,
Status: "draft",
Version: 1,
Name: "Personal Agent",
Pages: []string{"candidates"},
})
if err != nil {
t.Fatalf("insert personal agent: %v", err)
}
persUUID := persRec["id"].(string)
// Owner (User B) can load personal agent
persAgent, err := f.loader.LoadAgent(ctx, f.userB, "personal-agent")
if err != nil {
t.Fatalf("load own personal agent: %v", err)
}
if persAgent.DatabaseID != persUUID {
t.Errorf("personal agent uuid mismatch: %s != %s", persAgent.DatabaseID, persUUID)
}
// Other user in same org (User A) CANNOT load User B's personal agent -> ErrNotFound
_, err = f.loader.LoadAgent(ctx, f.userA, "personal-agent")
if !errors.Is(err, runtime.ErrNotFound) {
t.Errorf("userA loading userB personal agent: got error %v, want ErrNotFound", err)
}
// Outsider CANNOT load org agent from org 1 -> ErrNotFound
_, err = f.loader.LoadAgent(ctx, f.userOther, "talent-scout")
if !errors.Is(err, runtime.ErrNotFound) {
t.Errorf("outsider loading org1 agent: got error %v, want ErrNotFound", err)
}
// Missing agent -> ErrNotFound
_, err = f.loader.LoadAgent(ctx, f.userA, "nonexistent-agent")
if !errors.Is(err, runtime.ErrNotFound) {
t.Errorf("load nonexistent agent: got %v, want ErrNotFound", err)
}
}
func TestRuntimeLoader_Skill(t *testing.T) {
f := newFixture(t)
ctx := context.Background()
skillMD := `---
id: resume-evaluator
name: Resume Evaluator
description: Evaluates candidate resume text
status: active
pages:
- candidates
category: screening
actions:
- score
- summarize
triggers:
- resume
- cv
prompt: Evaluate the candidate resume thoroughly.
---
# Resume Evaluator Body
Detailed skill instructions.
`
rec, err := f.defRepo.InsertSkill(ctx, f.userA, repo.SkillInsertInput{
DefinitionID: "resume-evaluator",
OrgID: f.org1,
Visibility: "organization",
CreatedBy: &f.userA.UserID,
Markdown: skillMD,
Status: "active",
Name: "Resume Evaluator",
Description: "Evaluates candidate resume text",
Pages: []string{"candidates"},
})
if err != nil {
t.Fatalf("insert skill: %v", err)
}
skillUUID := rec["id"].(string)
// Load by definition_id
skillByDefID, err := f.loader.LoadSkill(ctx, f.userB, "resume-evaluator")
if err != nil {
t.Fatalf("load skill by definition_id: %v", err)
}
if skillByDefID.ID != "resume-evaluator" || skillByDefID.DatabaseID != skillUUID {
t.Errorf("skill ID mismatch: %+v", skillByDefID)
}
if skillByDefID.Name != "Resume Evaluator" || skillByDefID.Status != "active" {
t.Errorf("skill fields mismatch: %+v", skillByDefID)
}
if skillByDefID.Category != "screening" || len(skillByDefID.Actions) != 2 || len(skillByDefID.Triggers) != 2 {
t.Errorf("skill actions/triggers mismatch: %+v", skillByDefID)
}
if skillByDefID.Prompt == nil || *skillByDefID.Prompt != "Evaluate the candidate resume thoroughly." {
t.Errorf("skill prompt mismatch: %v", skillByDefID.Prompt)
}
if skillByDefID.RawMarkdown != skillMD {
t.Errorf("skill raw markdown not verbatim")
}
// Load by UUID
skillByUUID, err := f.loader.LoadSkill(ctx, f.userA, skillUUID)
if err != nil {
t.Fatalf("load skill by UUID: %v", err)
}
if skillByUUID.ID != "resume-evaluator" {
t.Errorf("skill by UUID mismatch: %+v", skillByUUID)
}
// Missing skill -> ErrNotFound
_, err = f.loader.LoadSkill(ctx, f.userA, "nonexistent-skill")
if !errors.Is(err, runtime.ErrNotFound) {
t.Errorf("missing skill: got %v, want ErrNotFound", err)
}
// Cross-org skill -> ErrNotFound
_, err = f.loader.LoadSkill(ctx, f.userOther, "resume-evaluator")
if !errors.Is(err, runtime.ErrNotFound) {
t.Errorf("cross-org skill: got %v, want ErrNotFound", err)
}
}
/* ── 2. Status Eligibility Tests ─────────────────────────────────────────── */
func TestRuntime_StatusEligibility(t *testing.T) {
f := newFixture(t)
ctx := context.Background()
// 1. Draft Agent
draftMD := `---
id: draft-agent
name: Draft Agent
status: draft
version: 1
pages:
- candidates
---
`
_, _ = f.defRepo.InsertAgent(ctx, f.userA, repo.AgentInsertInput{
DefinitionID: "draft-agent",
OrgID: f.org1,
Visibility: "organization",
CreatedBy: &f.userA.UserID,
Markdown: draftMD,
Status: "draft",
Version: 1,
Name: "Draft Agent",
Pages: []string{"candidates"},
})
_, err := f.loader.LoadExecutableAgent(ctx, f.userA, "draft-agent")
if !errors.Is(err, runtime.ErrDraftAgent) {
t.Errorf("draft agent: got %v, want ErrDraftAgent", err)
}
// 2. Archived Agent
archivedMD := `---
id: archived-agent
name: Archived Agent
status: archived
version: 1
pages:
- candidates
---
`
_, _ = f.defRepo.InsertAgent(ctx, f.userA, repo.AgentInsertInput{
DefinitionID: "archived-agent",
OrgID: f.org1,
Visibility: "organization",
CreatedBy: &f.userA.UserID,
Markdown: archivedMD,
Status: "archived",
Version: 1,
Name: "Archived Agent",
Pages: []string{"candidates"},
})
_, err = f.loader.LoadExecutableAgent(ctx, f.userA, "archived-agent")
if !errors.Is(err, runtime.ErrArchivedAgent) {
t.Errorf("archived agent: got %v, want ErrArchivedAgent", err)
}
// 3. Published Agent without dependencies -> Success
pubMD := `---
id: published-agent
name: Published Agent
status: published
version: 1
pages:
- candidates
---
`
_, _ = f.defRepo.InsertAgent(ctx, f.userA, repo.AgentInsertInput{
DefinitionID: "published-agent",
OrgID: f.org1,
Visibility: "organization",
CreatedBy: &f.userA.UserID,
Markdown: pubMD,
Status: "published",
Version: 1,
Name: "Published Agent",
Pages: []string{"candidates"},
})
pubAgent, err := f.loader.LoadExecutableAgent(ctx, f.userA, "published-agent")
if err != nil {
t.Fatalf("load published agent: %v", err)
}
if pubAgent.Status != "published" {
t.Errorf("published agent status: %s", pubAgent.Status)
}
// 4. Inactive Skill
inactiveSkillMD := `---
id: inactive-skill
name: Inactive Skill
status: inactive
pages:
- candidates
---
`
_, _ = f.defRepo.InsertSkill(ctx, f.userA, repo.SkillInsertInput{
DefinitionID: "inactive-skill",
OrgID: f.org1,
Visibility: "organization",
CreatedBy: &f.userA.UserID,
Markdown: inactiveSkillMD,
Status: "inactive",
Name: "Inactive Skill",
Pages: []string{"candidates"},
})
_, err = f.loader.LoadExecutableSkill(ctx, f.userA, "inactive-skill")
if !errors.Is(err, runtime.ErrInactiveSkill) {
t.Errorf("inactive skill: got %v, want ErrInactiveSkill", err)
}
// 5. Active Skill -> Success
activeSkillMD := `---
id: active-skill
name: Active Skill
status: active
pages:
- candidates
---
`
_, _ = f.defRepo.InsertSkill(ctx, f.userA, repo.SkillInsertInput{
DefinitionID: "active-skill",
OrgID: f.org1,
Visibility: "organization",
CreatedBy: &f.userA.UserID,
Markdown: activeSkillMD,
Status: "active",
Name: "Active Skill",
Pages: []string{"candidates"},
})
activeSkill, err := f.loader.LoadExecutableSkill(ctx, f.userA, "active-skill")
if err != nil {
t.Fatalf("load active skill: %v", err)
}
if activeSkill.Status != "active" {
t.Errorf("active skill status: %s", activeSkill.Status)
}
}
/* ── 3. Version Semantics Tests ──────────────────────────────────────────── */
func TestRuntime_VersionSemantics(t *testing.T) {
f := newFixture(t)
ctx := context.Background()
v5MD := `---
id: v5-agent
name: Version 5 Agent
version: 5
status: published
pages:
- candidates
---
`
_, err := f.defRepo.InsertAgent(ctx, f.userA, repo.AgentInsertInput{
DefinitionID: "v5-agent",
OrgID: f.org1,
Visibility: "organization",
CreatedBy: &f.userA.UserID,
Markdown: v5MD,
Status: "published",
Version: 5,
Name: "Version 5 Agent",
Pages: []string{"candidates"},
})
if err != nil {
t.Fatalf("insert v5 agent: %v", err)
}
agent, err := f.loader.LoadAgent(ctx, f.userA, "v5-agent")
if err != nil {
t.Fatalf("load v5 agent: %v", err)
}
if agent.Version != 5 {
t.Errorf("agent version = %d, want 5", agent.Version)
}
}
/* ── 4. Dependency Resolution Tests ──────────────────────────────────────── */
func TestRuntime_DependencyResolution(t *testing.T) {
f := newFixture(t)
ctx := context.Background()
// 1. Create active skill 1
skill1MD := `---
id: skill-one
name: Skill One
status: active
pages:
- candidates
---
`
_, _ = f.defRepo.InsertSkill(ctx, f.userA, repo.SkillInsertInput{
DefinitionID: "skill-one",
OrgID: f.org1,
Visibility: "organization",
CreatedBy: &f.userA.UserID,
Markdown: skill1MD,
Status: "active",
Name: "Skill One",
Pages: []string{"candidates"},
})
// 2. Create active skill 2 (personal for userB)
skill2MD := `---
id: skill-two
name: Skill Two
status: active
pages:
- candidates
---
`
_, _ = f.defRepo.InsertSkill(ctx, f.userB, repo.SkillInsertInput{
DefinitionID: "skill-two",
OrgID: f.org1,
Visibility: "personal",
OwnerUserID: &f.userB.UserID,
CreatedBy: &f.userB.UserID,
Markdown: skill2MD,
Status: "active",
Name: "Skill Two",
Pages: []string{"candidates"},
})
// 3. Create inactive skill 3
skill3MD := `---
id: skill-inactive
name: Skill Inactive
status: inactive
pages:
- candidates
---
`
_, _ = f.defRepo.InsertSkill(ctx, f.userA, repo.SkillInsertInput{
DefinitionID: "skill-inactive",
OrgID: f.org1,
Visibility: "organization",
CreatedBy: &f.userA.UserID,
Markdown: skill3MD,
Status: "inactive",
Name: "Skill Inactive",
Pages: []string{"candidates"},
})
// Agent with valid and duplicate dependencies
validDepMD := `---
id: dep-agent
name: Dependency Agent
status: published
version: 1
pages:
- candidates
skills:
- skill-one
- skill-two
- skill-one
---
`
_, _ = f.defRepo.InsertAgent(ctx, f.userB, repo.AgentInsertInput{
DefinitionID: "dep-agent",
OrgID: f.org1,
Visibility: "personal",
OwnerUserID: &f.userB.UserID,
CreatedBy: &f.userB.UserID,
Markdown: validDepMD,
Status: "published",
Version: 1,
Name: "Dependency Agent",
Pages: []string{"candidates"},
})
// User B resolves dep-agent: sees skill-one (org) and skill-two (personal), deduplicates skill-one
loadedAgent, err := f.loader.LoadExecutableAgent(ctx, f.userB, "dep-agent")
if err != nil {
t.Fatalf("load executable agent with deps: %v", err)
}
if len(loadedAgent.ResolvedSkills) != 2 {
t.Fatalf("expected 2 resolved skills (deduplicated), got %d", len(loadedAgent.ResolvedSkills))
}
if loadedAgent.ResolvedSkills[0].ID != "skill-one" || loadedAgent.ResolvedSkills[1].ID != "skill-two" {
t.Errorf("resolved skill IDs mismatch: %v, %v", loadedAgent.ResolvedSkills[0].ID, loadedAgent.ResolvedSkills[1].ID)
}
// Agent with missing dependency
missingDepMD := `---
id: missing-dep-agent
name: Missing Dep Agent
status: published
version: 1
pages:
- candidates
skills:
- nonexistent-skill
---
`
_, _ = f.defRepo.InsertAgent(ctx, f.userA, repo.AgentInsertInput{
DefinitionID: "missing-dep-agent",
OrgID: f.org1,
Visibility: "organization",
CreatedBy: &f.userA.UserID,
Markdown: missingDepMD,
Status: "published",
Version: 1,
Name: "Missing Dep Agent",
Pages: []string{"candidates"},
})
_, err = f.loader.LoadExecutableAgent(ctx, f.userA, "missing-dep-agent")
if !errors.Is(err, runtime.ErrDependencyMissing) {
t.Errorf("missing dep agent: got %v, want ErrDependencyMissing", err)
}
// Agent with inactive dependency
inactiveDepMD := `---
id: inactive-dep-agent
name: Inactive Dep Agent
status: published
version: 1
pages:
- candidates
skills:
- skill-inactive
---
`
_, _ = f.defRepo.InsertAgent(ctx, f.userA, repo.AgentInsertInput{
DefinitionID: "inactive-dep-agent",
OrgID: f.org1,
Visibility: "organization",
CreatedBy: &f.userA.UserID,
Markdown: inactiveDepMD,
Status: "published",
Version: 1,
Name: "Inactive Dep Agent",
Pages: []string{"candidates"},
})
_, err = f.loader.LoadExecutableAgent(ctx, f.userA, "inactive-dep-agent")
if !errors.Is(err, runtime.ErrDependencyInactive) {
t.Errorf("inactive dep agent: got %v, want ErrDependencyInactive", err)
}
// Agent with cross-org dependency: outsider creates agent referencing org1's skill
outsiderAgentMD := `---
id: outsider-agent
name: Outsider Agent
status: published
version: 1
pages:
- candidates
skills:
- skill-one
---
`
_, _ = f.defRepo.InsertAgent(ctx, f.userOther, repo.AgentInsertInput{
DefinitionID: "outsider-agent",
OrgID: f.org2,
Visibility: "organization",
CreatedBy: &f.userOther.UserID,
Markdown: outsiderAgentMD,
Status: "published",
Version: 1,
Name: "Outsider Agent",
Pages: []string{"candidates"},
})
_, err = f.loader.LoadExecutableAgent(ctx, f.userOther, "outsider-agent")
if !errors.Is(err, runtime.ErrDependencyMissing) {
t.Errorf("outsider cross-org dep: got %v, want ErrDependencyMissing", err)
}
}
/* ── 5. Executor Boundary Tests ──────────────────────────────────────────── */
type testEchoExecutor struct {
lastAgent *runtime.Agent
lastInput runtime.ExecutionInput
}
func (e *testEchoExecutor) ExecuteAgent(_ context.Context, agent *runtime.Agent, input runtime.ExecutionInput) (*runtime.ExecutionResult, error) {
e.lastAgent = agent
e.lastInput = input
return &runtime.ExecutionResult{
Success: true,
Output: fmt.Sprintf("executed %s with %s", agent.Name, input.Input),
AgentID: agent.ID,
AgentVersion: agent.Version,
}, nil
}
func (e *testEchoExecutor) ExecuteSkill(_ context.Context, skill *runtime.Skill, input runtime.ExecutionInput) (*runtime.ExecutionResult, error) {
return &runtime.ExecutionResult{
Success: true,
Output: fmt.Sprintf("executed skill %s", skill.Name),
}, nil
}
func TestRuntime_ExecutorBoundary(t *testing.T) {
f := newFixture(t)
ctx := context.Background()
pubMD := `---
id: exec-agent
name: Exec Agent
status: published
version: 1
pages:
- candidates
---
`
_, _ = f.defRepo.InsertAgent(ctx, f.userA, repo.AgentInsertInput{
DefinitionID: "exec-agent",
OrgID: f.org1,
Visibility: "organization",
CreatedBy: &f.userA.UserID,
Markdown: pubMD,
Status: "published",
Version: 1,
Name: "Exec Agent",
Pages: []string{"candidates"},
})
// 1. Default engine without configured executor returns ErrExecutorUnavailable
res, err := f.engine.RunAgent(ctx, f.userA, "exec-agent", runtime.ExecutionInput{
Identity: f.userA,
Input: "Hello agent",
})
if !errors.Is(err, runtime.ErrExecutorUnavailable) {
t.Errorf("default engine run agent: got %v, want ErrExecutorUnavailable", err)
}
if res.Success {
t.Errorf("default engine must not succeed without executor")
}
// 2. Custom executor plugged in
echo := &testEchoExecutor{}
customEngine := runtime.NewEngine(f.h.Pool, runtime.WithAgentExecutor(echo), runtime.WithSkillExecutor(echo))
resCustom, err := customEngine.RunAgent(ctx, f.userA, "exec-agent", runtime.ExecutionInput{
Identity: f.userA,
Input: "Hello agent",
})
if err != nil {
t.Fatalf("custom engine run agent failed: %v", err)
}
if !resCustom.Success || resCustom.Output != "executed Exec Agent with Hello agent" {
t.Errorf("custom executor output mismatch: %+v", resCustom)
}
if echo.lastAgent.ID != "exec-agent" || echo.lastInput.Input != "Hello agent" {
t.Errorf("executor received incorrect arguments: agent=%v, input=%v", echo.lastAgent, echo.lastInput)
}
}
func TestRuntime_PersonalSkillShadowing(t *testing.T) {
f := newFixture(t)
ctx := context.Background()
// Org skill
orgSkillMD := `---
id: shadow-skill
name: Org Shadow Skill
status: active
pages:
- candidates
---
`
_, _ = f.defRepo.InsertSkill(ctx, f.userA, repo.SkillInsertInput{
DefinitionID: "shadow-skill",
OrgID: f.org1,
Visibility: "organization",
CreatedBy: &f.userA.UserID,
Markdown: orgSkillMD,
Status: "active",
Name: "Org Shadow Skill",
Pages: []string{"candidates"},
})
// User B's personal skill with the SAME definition_id
persSkillMD := `---
id: shadow-skill
name: User B Personal Shadow Skill
status: active
pages:
- candidates
---
`
persRec, _ := f.defRepo.InsertSkill(ctx, f.userB, repo.SkillInsertInput{
DefinitionID: "shadow-skill",
OrgID: f.org1,
Visibility: "personal",
OwnerUserID: &f.userB.UserID,
CreatedBy: &f.userB.UserID,
Markdown: persSkillMD,
Status: "active",
Name: "User B Personal Shadow Skill",
Pages: []string{"candidates"},
})
persUUID := persRec["id"].(string)
// When User A loads shadow-skill -> gets Org Shadow Skill
skillA, err := f.loader.LoadSkill(ctx, f.userA, "shadow-skill")
if err != nil {
t.Fatalf("userA load shadow skill: %v", err)
}
if skillA.Name != "Org Shadow Skill" {
t.Errorf("userA got %s, want Org Shadow Skill", skillA.Name)
}
// When User B loads shadow-skill -> gets User B Personal Shadow Skill (shadow precedence)
skillB, err := f.loader.LoadSkill(ctx, f.userB, "shadow-skill")
if err != nil {
t.Fatalf("userB load shadow skill: %v", err)
}
if skillB.Name != "User B Personal Shadow Skill" || skillB.DatabaseID != persUUID {
t.Errorf("userB got %s, want User B Personal Shadow Skill", skillB.Name)
}
}
func TestRuntime_MalformedMarkdown(t *testing.T) {
f := newFixture(t)
ctx := context.Background()
// Broken YAML
brokenMD := `---
id: [broken-id
name: Invalid
---
`
_, _ = f.h.Pool.Exec(ctx, `INSERT INTO agent_definitions (definition_id, org_id, visibility, created_by, markdown, status, version, name, description, pages)
VALUES ('broken-agent', $1::uuid, 'organization', $2::uuid, $3::text, 'published', 1, 'Broken', '', ARRAY['candidates'])`,
f.org1, f.userA.UserID, brokenMD)
_, err := f.loader.LoadAgent(ctx, f.userA, "broken-agent")
if !errors.Is(err, runtime.ErrInvalidDefinition) {
t.Errorf("load broken agent: got %v, want ErrInvalidDefinition", err)
}
}
func TestRuntime_SkillExecution(t *testing.T) {
f := newFixture(t)
ctx := context.Background()
skillMD := `---
id: run-skill
name: Runnable Skill
status: active
pages:
- candidates
---
`
_, _ = f.defRepo.InsertSkill(ctx, f.userA, repo.SkillInsertInput{
DefinitionID: "run-skill",
OrgID: f.org1,
Visibility: "organization",
CreatedBy: &f.userA.UserID,
Markdown: skillMD,
Status: "active",
Name: "Runnable Skill",
Pages: []string{"candidates"},
})
// Default engine
res, err := f.engine.RunSkill(ctx, f.userA, "run-skill", runtime.ExecutionInput{
Identity: f.userA,
Input: "skill input",
})
if !errors.Is(err, runtime.ErrExecutorUnavailable) {
t.Errorf("default engine run skill: got %v, want ErrExecutorUnavailable", err)
}
if res.Success {
t.Errorf("default engine run skill must not succeed")
}
// Custom executor
echo := &testEchoExecutor{}
customEngine := runtime.NewEngine(f.h.Pool, runtime.WithSkillExecutor(echo))
resCustom, err := customEngine.RunSkill(ctx, f.userA, "run-skill", runtime.ExecutionInput{
Identity: f.userA,
})
if err != nil {
t.Fatalf("custom engine run skill failed: %v", err)
}
if !resCustom.Success || resCustom.Output != "executed skill Runnable Skill" {
t.Errorf("custom skill output mismatch: %+v", resCustom)
}
}

View File

@@ -0,0 +1,103 @@
package runtime
import (
"errors"
"fmt"
"github.com/krow/krow-backend/go-api/internal/authctx"
)
// Standard runtime errors.
var (
ErrNotFound = errors.New("runtime: definition not found")
ErrUnauthorized = errors.New("runtime: unauthorized")
ErrInvalidDefinition = errors.New("runtime: invalid definition")
ErrDraftAgent = errors.New("runtime: agent is in draft status and cannot be executed")
ErrArchivedAgent = errors.New("runtime: agent is archived and cannot be executed")
ErrInactiveSkill = errors.New("runtime: skill is inactive and cannot be executed")
ErrNotExecutable = errors.New("runtime: definition is not eligible for execution")
ErrDependencyMissing = errors.New("runtime: required skill dependency not found")
ErrDependencyInactive = errors.New("runtime: required skill dependency is inactive")
ErrCircularDependency = errors.New("runtime: circular dependency detected in skills")
ErrExecutorUnavailable = errors.New("runtime: AI executor is unavailable (deferred to Phase 5)")
)
// Agent represents an authored agent prepared for runtime execution.
type Agent struct {
ID string `json:"id"`
DatabaseID string `json:"databaseId"`
Name string `json:"name"`
Description string `json:"description"`
Status string `json:"status"`
Version int `json:"version"`
Visibility string `json:"visibility"`
OwnerUserID *string `json:"ownerUserId,omitempty"`
Pages []string `json:"pages"`
Icon string `json:"icon,omitempty"`
Reasoning string `json:"reasoning,omitempty"`
Trigger string `json:"trigger,omitempty"`
WebSearch bool `json:"webSearch,omitempty"`
Instructions string `json:"instructions"`
Skills []string `json:"skills"`
ResolvedSkills []*Skill `json:"resolvedSkills,omitempty"`
Subagents []string `json:"subagents,omitempty"`
RawMarkdown string `json:"rawMarkdown"`
}
// Skill represents an authored skill prepared for runtime execution.
type Skill struct {
ID string `json:"id"`
DatabaseID string `json:"databaseId"`
Name string `json:"name"`
Description string `json:"description"`
Status string `json:"status"`
Visibility string `json:"visibility"`
OwnerUserID *string `json:"ownerUserId,omitempty"`
Pages []string `json:"pages"`
Kind string `json:"kind,omitempty"`
Category string `json:"category,omitempty"`
Actions []string `json:"actions,omitempty"`
Triggers []string `json:"triggers,omitempty"`
Prompt *string `json:"prompt,omitempty"`
SkillID *string `json:"skillId,omitempty"`
Body string `json:"body"`
RawMarkdown string `json:"rawMarkdown"`
}
// ExecutionInput provides caller context and payload to the execution boundary.
type ExecutionInput struct {
Identity authctx.Identity `json:"identity"`
TargetID string `json:"targetId"`
Input string `json:"input"`
Parameters map[string]any `json:"parameters,omitempty"`
Context map[string]any `json:"context,omitempty"`
}
// ExecutionResult captures the outcome of an execution attempt.
type ExecutionResult struct {
Success bool `json:"success"`
Output string `json:"output,omitempty"`
AgentID string `json:"agentId,omitempty"`
AgentVersion int `json:"agentVersion,omitempty"`
ResolvedSkills []string `json:"resolvedSkills,omitempty"`
Error error `json:"error,omitempty"`
}
// RuntimeError is a structured error containing context for execution failures.
type RuntimeError struct {
Code string `json:"code"`
Message string `json:"message"`
Target string `json:"target,omitempty"`
Cause error `json:"-"`
}
func (e *RuntimeError) Error() string {
if e.Target != "" {
return fmt.Sprintf("%s: %s (%s)", e.Code, e.Message, e.Target)
}
return fmt.Sprintf("%s: %s", e.Code, e.Message)
}
func (e *RuntimeError) Unwrap() error {
return e.Cause
}