package handlers import ( "context" "database/sql" "log" "github.com/gin-gonic/gin" "armature/storage" ) // ── Capability detection ──────────────────────────────────────────── // DetectCapabilities probes the runtime environment and returns a map // of capability name → available. Called once at startup and again on // each admin GET /capabilities request. func DetectCapabilities(db *sql.DB, isPostgres bool, objStore storage.ObjectStore, workspaceRoot string) map[string]bool { caps := map[string]bool{ "postgres": false, "pgvector": false, "object_storage": false, "s3": false, "workspace": false, } // postgres: dialect check if isPostgres { caps["postgres"] = true } // pgvector: query pg_extension (PG only) if isPostgres && db != nil { var one int err := db.QueryRow("SELECT 1 FROM pg_extension WHERE extname = 'vector'").Scan(&one) caps["pgvector"] = err == nil } // object_storage: configured and healthy if objStore != nil { if err := objStore.Healthy(context.Background()); err == nil { caps["object_storage"] = true } } // s3: specific backend check if objStore != nil && objStore.Backend() == "s3" { caps["s3"] = true } // workspace: root exists and is writable if workspaceRoot != "" && storage.IsPathWritable(workspaceRoot) { caps["workspace"] = true } return caps } // ── Manifest parsing ──────────────────────────────────────────────── // Capabilities holds the parsed capabilities block from a package manifest. type Capabilities struct { Required []string Optional []string } // ParseCapabilities extracts capabilities.required and capabilities.optional // from a manifest map. Returns zero value + false if no capabilities block. func ParseCapabilities(manifest map[string]any) (Capabilities, bool) { capsRaw, ok := manifest["capabilities"].(map[string]any) if !ok { return Capabilities{}, false } var caps Capabilities if req, ok := capsRaw["required"].([]any); ok { for _, v := range req { if s, ok := v.(string); ok && s != "" { caps.Required = append(caps.Required, s) } } } if opt, ok := capsRaw["optional"].([]any); ok { for _, v := range opt { if s, ok := v.(string); ok && s != "" { caps.Optional = append(caps.Optional, s) } } } return caps, true } // CheckRequiredCapabilities returns the subset of required capabilities // that are not present in the detected set. func CheckRequiredCapabilities(required []string, detected map[string]bool) []string { var missing []string for _, r := range required { if !detected[r] { missing = append(missing, r) } } return missing } // capabilityHelpText returns operator-facing remediation hints keyed by // capability name. func capabilityHelpText(missing []string) map[string]string { hints := map[string]string{ "postgres": "Switch DB_DRIVER to postgres and configure DATABASE_URL.", "pgvector": "Run `CREATE EXTENSION vector;` in your PostgreSQL database.", "object_storage": "Configure STORAGE_BACKEND (pvc or s3) and ensure the backend is healthy.", "s3": "Set STORAGE_BACKEND=s3 and configure S3_ENDPOINT, S3_BUCKET, S3_ACCESS_KEY, S3_SECRET_KEY.", "workspace": "Set WORKSPACE_ROOT to a writable directory path.", } result := make(map[string]string, len(missing)) for _, m := range missing { if h, ok := hints[m]; ok { result[m] = h } else { result[m] = "Unknown capability — check package documentation." } } return result } // ── Admin endpoint ────────────────────────────────────────────────── // CapabilitiesHandler serves the admin capabilities endpoint. type CapabilitiesHandler struct { detect func() map[string]bool } // NewCapabilitiesHandler creates a handler that re-probes capabilities on // each request so the response reflects current state. func NewCapabilitiesHandler(detect func() map[string]bool) *CapabilitiesHandler { return &CapabilitiesHandler{detect: detect} } // GetCapabilities returns detected environment capabilities. // GET /api/v1/admin/capabilities func (h *CapabilitiesHandler) GetCapabilities(c *gin.Context) { caps := h.detect() log.Printf("[capabilities] probed: %v", caps) c.JSON(200, caps) }