Files
doormile_backend/controllers/aiRegistryController.go

219 lines
5.9 KiB
Go

package controllers
import (
"errors"
"doormile/db"
"doormile/internal/ai/registry"
"doormile/utils"
"github.com/gofiber/fiber/v2"
)
// The AI agent registry: /admin/ai/* for the console's Agent Studio and
// /internal/ai/registry for AI_engine. Every route here sits behind
// DoormileStaffOnly (admin) or InternalKeyAuth (internal); writes additionally
// require roleid 1. Logic lives in internal/ai/registry — these handlers only
// translate HTTP.
// registryError maps a registry error to a response. Validation messages are
// written for operators and returned as-is; anything else is logged, not leaked.
func registryError(c *fiber.Ctx, err error, what string) error {
var v *registry.ValidationError
switch {
case errors.As(err, &v):
return utils.BadRequest(c, v.Msg)
case errors.Is(err, registry.ErrNotFound):
return utils.NotFound(c, what+" not found")
default:
utils.Error("ai registry: "+what, "error", err.Error())
return utils.Internal(c, "failed to update the agent registry")
}
}
// actorOf is the caller as the registry audit records it: the user id and the
// email from the token (set by AuthMiddleware).
func actorOf(c *fiber.Ctx) registry.Actor {
userID, _ := c.Locals("userid").(int)
email, _ := c.Locals("email").(string)
return registry.Actor{UserID: userID, Email: email}
}
func loadRegistry(c *fiber.Ctx) (*registry.Snapshot, bool) {
snap, err := registry.Load(db.DB)
if err != nil {
utils.Error("ai registry: load", "error", err.Error())
_ = utils.Internal(c, "failed to read the agent registry")
return nil, false
}
return snap, true
}
// GetAIAgents — GET /admin/ai/agents
func GetAIAgents(c *fiber.Ctx) error {
snap, ok := loadRegistry(c)
if !ok {
return nil
}
return utils.List(c, snap.Agents, int64(len(snap.Agents)))
}
// GetAIAgent — GET /admin/ai/agents/:id, the agent with its skills and tools.
func GetAIAgent(c *fiber.Ctx) error {
snap, ok := loadRegistry(c)
if !ok {
return nil
}
id := c.Params("id")
for _, a := range snap.Agents {
if a.Agentid != id {
continue
}
skills := []registry.SkillView{}
used := map[string]bool{}
for _, s := range snap.Skills {
if s.Agentid == id {
skills = append(skills, s)
for _, t := range s.Tools {
used[t] = true
}
}
}
tools := []registry.ToolView{}
for _, t := range snap.Tools {
if used[t.Toolname] {
tools = append(tools, t)
}
}
return utils.OK(c, fiber.Map{"agent": a, "skills": skills, "tools": tools})
}
return utils.NotFound(c, "agent not found")
}
// GetAISkills — GET /admin/ai/skills[?agent=]
func GetAISkills(c *fiber.Ctx) error {
snap, ok := loadRegistry(c)
if !ok {
return nil
}
agent := c.Query("agent")
out := []registry.SkillView{}
for _, s := range snap.Skills {
if agent == "" || s.Agentid == agent {
out = append(out, s)
}
}
return utils.List(c, out, int64(len(out)))
}
// GetAITools — GET /admin/ai/tools[?kind=]
func GetAITools(c *fiber.Ctx) error {
snap, ok := loadRegistry(c)
if !ok {
return nil
}
kind := c.Query("kind")
out := []registry.ToolView{}
for _, t := range snap.Tools {
if kind == "" || t.Kind == kind {
out = append(out, t)
}
}
return utils.List(c, out, int64(len(out)))
}
// PatchAISkill — PATCH /admin/ai/skills/:id {enabled?, thresholds?}
func PatchAISkill(c *fiber.Ctx) error {
var p registry.SkillPatch
if err := c.BodyParser(&p); err != nil {
return utils.BadRequest(c, "invalid request body")
}
if err := registry.UpdateSkill(db.DB, c.Params("id"), p, actorOf(c)); err != nil {
return registryError(c, err, "skill")
}
snap, ok := loadRegistry(c)
if !ok {
return nil
}
for _, s := range snap.Skills {
if s.Skillid == c.Params("id") {
return utils.OK(c, s)
}
}
return utils.NotFound(c, "skill not found")
}
// CreateAISkill — POST /admin/ai/skills
func CreateAISkill(c *fiber.Ctx) error {
var n registry.NewSkill
if err := c.BodyParser(&n); err != nil {
return utils.BadRequest(c, "invalid request body")
}
id, err := registry.CreateSkill(db.DB, n, actorOf(c))
if err != nil {
return registryError(c, err, "skill")
}
snap, ok := loadRegistry(c)
if !ok {
return nil
}
for _, s := range snap.Skills {
if s.Skillid == id {
return utils.Created(c, s)
}
}
return utils.Internal(c, "skill was created but could not be read back")
}
// PatchAIAgent — PATCH /admin/ai/agents/:id {autonomous?, model?, confirm?}
func PatchAIAgent(c *fiber.Ctx) error {
var p registry.AgentPatch
if err := c.BodyParser(&p); err != nil {
return utils.BadRequest(c, "invalid request body")
}
if err := registry.UpdateAgent(db.DB, c.Params("id"), p, actorOf(c)); err != nil {
return registryError(c, err, "agent")
}
snap, ok := loadRegistry(c)
if !ok {
return nil
}
for _, a := range snap.Agents {
if a.Agentid == c.Params("id") {
return utils.OK(c, a)
}
}
return utils.NotFound(c, "agent not found")
}
// GetAIRegistryAudit — GET /admin/ai/audit[?limit=]
func GetAIRegistryAudit(c *fiber.Ctx) error {
rows, err := registry.ListAudit(db.DB, c.QueryInt("limit", 100))
if err != nil {
utils.Error("ai registry: audit", "error", err.Error())
return utils.Internal(c, "failed to read the registry audit")
}
return utils.List(c, rows, int64(len(rows)))
}
// GetInternalAIRegistry — GET /internal/ai/registry, for AI_engine.
//
// Sends an ETag and honours If-None-Match, so the engine can poll every few
// seconds and receive a 304 with no body until something actually changes.
func GetInternalAIRegistry(c *fiber.Ctx) error {
snap, ok := loadRegistry(c)
if !ok {
return nil
}
// Proof the engine is following the registry — see GetAIStatus. Counted
// for 304s too: an unchanged registry is still a successful read.
markRegistryRead()
tag := registry.ETag(snap)
c.Set(fiber.HeaderETag, tag)
c.Set(fiber.HeaderCacheControl, "no-cache")
if c.Get(fiber.HeaderIfNoneMatch) == tag {
return c.SendStatus(fiber.StatusNotModified)
}
return utils.OK(c, snap)
}