package controllers import ( "encoding/json" "io" "net/http" "time" "doormile/utils" "github.com/gofiber/fiber/v2" ) // Proxying AI_engine's live agent state to the console. // // The console's Agents page has run on a hand-maintained snapshot // (krow_talent_app/src/lib/agentNetwork.js, dated 16-20 Sep) because the engine // exposed no endpoint it could read. The engine now serves GET /agents/status // from core/health.py — but the console cannot call it directly: that surface // is a ClusterIP Service on port 8700, and the console is a browser. // // So this proxies it. Staff-only, like the rest of /admin/ai. // // ─── Fails soft, on purpose ──────────────────────────────────────────────── // // A 503 here means "ask the snapshot", not "something is broken". The engine is // not deployed in Kubernetes yet, AI_ENGINE_BASE_URL is empty by default, and // the Agents page must keep rendering in all of those cases. The console reads // the status code and falls back; it never shows an error for this. // // Short timeout for the same reason: an unreachable engine must not hold a // console request open. Two seconds is longer than an in-cluster call needs and // shorter than anyone will wait. // AIEngineBaseURL is set at boot from config. Empty disables the proxy. var AIEngineBaseURL string var engineClient = &http.Client{Timeout: 2 * time.Second} // GET /api/v1/admin/ai/engine/agents func GetAIEngineAgents(c *fiber.Ctx) error { if AIEngineBaseURL == "" { // Not an error: the engine has no in-cluster address configured, which // is the default and is true today. The console falls back. return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{ "code": "AI_ENGINE_NOT_CONFIGURED", "message": "AI_ENGINE_BASE_URL is not set; the console should use its snapshot.", }) } resp, err := engineClient.Get(AIEngineBaseURL + "/agents/status") if err != nil { utils.Warn("GetAIEngineAgents: engine unreachable", "error", err) return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{ "code": "AI_ENGINE_UNREACHABLE", "message": "The engine did not answer; the console should use its snapshot.", }) } defer resp.Body.Close() // Cap the read. This is a trusted in-cluster service, but a proxy that // streams an unbounded body into a console response is a bad shape // regardless of who is on the other end. body, err := io.ReadAll(io.LimitReader(resp.Body, 256*1024)) if err != nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{ "code": "AI_ENGINE_UNREADABLE", "message": "Could not read the engine's response.", }) } if resp.StatusCode != http.StatusOK { // The engine answers 503 on /agents/status when it has no agent state // (a non-production run mode). Pass that through rather than // reinterpreting it. return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{ "code": "AI_ENGINE_NO_STATE", "message": "The engine is running but reports no agent state.", }) } var payload map[string]interface{} if err := json.Unmarshal(body, &payload); err != nil { return c.Status(fiber.StatusServiceUnavailable).JSON(fiber.Map{ "code": "AI_ENGINE_BAD_JSON", "message": "The engine's response was not JSON.", }) } return utils.OK(c, payload) }