219 lines
5.9 KiB
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)
|
|
}
|