package app import ( "fmt" "log/slog" "net/http" "strings" "github.com/bitechdev/ResolveSpec/pkg/resolvespec" "github.com/uptrace/bunrouter" "git.warky.dev/wdevs/amcs/internal/store" ) func registerResolveSpecAdminRoutes(mux *http.ServeMux, db *store.DB, middleware func(http.Handler) http.Handler, logger *slog.Logger) error { rs := resolvespec.NewHandlerWithBun(db.Bun()) registerResolveSpecGuards(rs) for _, model := range resolveSpecModels() { if err := rs.RegisterModel(model.schema, model.entity, model.model); err != nil { return fmt.Errorf("register resolvespec model %s.%s: %w", model.schema, model.entity, err) } } rsRouter := bunrouter.New() resolvespec.SetupBunRouterRoutes(rsRouter, rs, nil) rsMount := http.StripPrefix("/api/rs", rsRouter) protectedRSMount := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" && strings.HasSuffix(r.URL.Path, "/") { trimmed := strings.TrimRight(r.URL.Path, "/") if trimmed == "" { trimmed = "/" } clone := r.Clone(r.Context()) clone.URL.Path = trimmed if clone.URL.RawPath != "" { clone.URL.RawPath = strings.TrimRight(clone.URL.RawPath, "/") if clone.URL.RawPath == "" { clone.URL.RawPath = "/" } } r = clone } if r.Method == http.MethodOptions { rsMount.ServeHTTP(w, r) return } middleware(rsMount).ServeHTTP(w, r) }) mux.Handle("/api/rs/", protectedRSMount) mux.Handle("/api/rs", http.RedirectHandler("/api/rs/openapi", http.StatusTemporaryRedirect)) if logger != nil { logger.Info("resolvespec admin api enabled", slog.String("prefix", "/api/rs"), slog.Int("models", len(resolveSpecModels())), ) } return nil } func registerResolveSpecGuards(rs *resolvespec.Handler) { mutableByEntity := map[string]map[string]struct{}{ "projects": { "create": {}, "update": {}, "delete": {}, }, "thoughts": { "create": {}, "update": {}, "delete": {}, }, "plans": { "create": {}, "update": {}, "delete": {}, }, "learnings": { "create": {}, "update": {}, "delete": {}, }, "agent_personas": { "create": {}, "update": {}, "delete": {}, }, "agent_parts": { "create": {}, "update": {}, "delete": {}, }, "agent_traits": { "create": {}, "update": {}, "delete": {}, }, "agent_skills": { "create": {}, "update": {}, "delete": {}, }, "agent_guardrails": { "create": {}, "update": {}, "delete": {}, }, "stored_files": { "update": {}, "delete": {}, }, } rs.Hooks().Register(resolvespec.BeforeHandle, func(hookCtx *resolvespec.HookContext) error { switch hookCtx.Operation { case "read", "meta": return nil case "create", "update", "delete": allowedOps, ok := mutableByEntity[hookCtx.Entity] if !ok { hookCtx.Abort = true hookCtx.AbortCode = http.StatusForbidden hookCtx.AbortMessage = fmt.Sprintf("operation %q is not allowed for %s.%s", hookCtx.Operation, hookCtx.Schema, hookCtx.Entity) return fmt.Errorf("forbidden operation") } if _, ok := allowedOps[hookCtx.Operation]; !ok { hookCtx.Abort = true hookCtx.AbortCode = http.StatusForbidden hookCtx.AbortMessage = fmt.Sprintf("operation %q is not allowed for %s.%s", hookCtx.Operation, hookCtx.Schema, hookCtx.Entity) return fmt.Errorf("forbidden operation") } return nil default: hookCtx.Abort = true hookCtx.AbortCode = http.StatusBadRequest hookCtx.AbortMessage = fmt.Sprintf("unsupported operation %q", hookCtx.Operation) return fmt.Errorf("unsupported operation") } }) } type resolveSpecModel struct { schema string entity string model any }