Feat v0.8.2 capability negotiation (#69)
All checks were successful
CI/CD / detect-changes (push) Successful in 4s
CI/CD / test-frontend (push) Has been skipped
CI/CD / test-runners (push) Has been skipped
CI/CD / e2e-smoke (push) Has been skipped
CI/CD / test-go-pg (push) Successful in 2m47s
CI/CD / test-sqlite (push) Successful in 2m52s
CI/CD / build-and-deploy (push) Successful in 30s

Co-authored-by: Jeffrey Smith <jasafpro@gmail.com>
Co-committed-by: Jeffrey Smith <jasafpro@gmail.com>
This commit was merged in pull request #69.
This commit is contained in:
2026-04-03 09:14:00 +00:00
committed by xcaliber
parent 435f972ded
commit 00ef970163
16 changed files with 593 additions and 96 deletions

View File

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