package httpapi import ( "database/sql" "errors" "net/http" "os" "strings" "inbox/internal/base/httpx" "inbox/internal/base/timeutil" "inbox/internal/inboxapp" sqlitestore "inbox/internal/store/sqlite" ) type HandlerOptions = inboxapp.Options type Services = inboxapp.Services type Handler struct { Services } func NewHandler(store *sqlitestore.Store, clock timeutil.Clock) http.Handler { return NewHandlerWithOptions(store, clock, HandlerOptions{}) } func NewHandlerWithOptions(store *sqlitestore.Store, clock timeutil.Clock, opts HandlerOptions) http.Handler { return NewHandlerWithServices(inboxapp.BuildServices(store, clock, opts)) } func NewHandlerWithServices(services Services) http.Handler { handler := &Handler{Services: services} mux := http.NewServeMux() handler.register(mux) return httpx.CORSMiddleware(mux) } func writeSystemFSError(w http.ResponseWriter, err error) { var pathErr *os.PathError if isValidationError(err) || errors.As(err, &pathErr) { httpx.WriteError(w, http.StatusBadRequest, err.Error()) return } httpx.WriteError(w, http.StatusInternalServerError, err.Error()) } func (h *Handler) register(mux *http.ServeMux) { mux.HandleFunc("GET /api/v2/config/roles", h.listRoles) mux.HandleFunc("GET /api/v2/config/roles/{roleName}", h.getRoleDetail) mux.HandleFunc("PUT /api/v2/config/roles/{roleName}", h.putRole) mux.HandleFunc("PUT /api/v2/config/roles/{roleName}/prompts/{promptKind}", h.putRolePrompt) mux.HandleFunc("PUT /api/v2/config/roles/{roleName}/config", h.putRoleConfig) mux.HandleFunc("GET /api/v2/config/skills", h.listSkills) mux.HandleFunc("PUT /api/v2/config/skills/{skillKey}", h.putSkill) mux.HandleFunc("DELETE /api/v2/config/skills/{skillKey}", h.deleteSkill) mux.HandleFunc("PUT /api/v2/config/roles/{roleName}/skills/{skillKey}", h.putRoleSkillBinding) mux.HandleFunc("GET /api/v2/runtime/roles/{roleName}", h.getResolvedRole) mux.HandleFunc("GET /api/v2/projects", h.listProjects) mux.HandleFunc("POST /api/v2/projects", h.createProject) mux.HandleFunc("GET /api/v2/workspaces", h.listWorkspaces) mux.HandleFunc("POST /api/v2/workspaces", h.createWorkspace) mux.HandleFunc("POST /api/v2/workspaces/provision", h.provisionWorkspace) mux.HandleFunc("GET /api/v2/topics", h.listTopics) mux.HandleFunc("POST /api/v2/topics", h.createTopic) mux.HandleFunc("GET /api/v2/topics/{topicID}", h.getTopic) mux.HandleFunc("POST /api/v2/topics/{topicID}/confirm", h.confirmTopicPlan) mux.HandleFunc("POST /api/v2/topics/{topicID}/stop", h.stopTopic) mux.HandleFunc("DELETE /api/v2/topics/{topicID}", h.deleteTopic) mux.HandleFunc("GET /api/v2/topics/{topicID}/messages", h.listTopicMessages) mux.HandleFunc("POST /api/v2/topics/{topicID}/messages", h.createTopicMessage) mux.HandleFunc("GET /api/v2/topics/{topicID}/lanes", h.listLanes) mux.HandleFunc("POST /api/v2/topics/{topicID}/lanes", h.createLane) mux.HandleFunc("GET /api/v2/lanes/{laneID}", h.getLane) mux.HandleFunc("PATCH /api/v2/lanes/{laneID}", h.patchLane) mux.HandleFunc("POST /api/v2/lanes/{laneID}/start", h.startLane) mux.HandleFunc("POST /api/v2/lanes/{laneID}/stop", h.stopLane) mux.HandleFunc("GET /api/v2/topics/{topicID}/tasks", h.listTasks) mux.HandleFunc("POST /api/v2/topics/{topicID}/tasks", h.createTask) mux.HandleFunc("GET /api/v2/lanes/{laneID}/tasks", h.listLaneTasks) mux.HandleFunc("GET /api/v2/tasks/{taskID}", h.getTask) mux.HandleFunc("PATCH /api/v2/tasks/{taskID}", h.patchTask) mux.HandleFunc("GET /api/v2/tasks/{taskID}/dependencies", h.listTaskDependencies) mux.HandleFunc("GET /api/v2/tasks/{taskID}/events", h.listTaskEvents) mux.HandleFunc("POST /api/v2/tasks/{taskID}/events", h.appendTaskEvent) mux.HandleFunc("GET /api/v2/topics/{topicID}/human-tasks", h.listTopicHumanTasks) mux.HandleFunc("POST /api/v2/human-tasks/{taskID}/answer", h.answerHumanTask) mux.HandleFunc("GET /api/v2/topics/{topicID}/workflow-runs", h.listWorkflowRuns) mux.HandleFunc("POST /api/v2/topics/{topicID}/workflow-runs", h.createWorkflowRun) mux.HandleFunc("GET /api/v2/workflow-runs/{runID}", h.getWorkflowRun) mux.HandleFunc("PATCH /api/v2/workflow-runs/{runID}", h.patchWorkflowRun) mux.HandleFunc("GET /api/v2/workflow-runs/{runID}/logs", h.listWorkflowRunLogs) mux.HandleFunc("POST /api/v2/workflow-runs/{runID}/logs", h.appendWorkflowRunLog) mux.HandleFunc("GET /api/v2/system/fs/dirs", h.listDirectories) mux.HandleFunc("POST /api/v2/system/fs/dirs", h.createDirectory) mux.HandleFunc("GET /api/v2/dashboard/messages", h.dashboardMessages) mux.HandleFunc("GET /api/v2/dashboard/topics", h.dashboardTopics) mux.HandleFunc("GET /api/v2/dashboard/topics/records", h.dashboardTopicRecords) mux.HandleFunc("GET /api/v2/dashboard/roles", h.dashboardRoles) mux.HandleFunc("GET /api/v2/dashboard/dispatch", h.dashboardDispatch) mux.HandleFunc("GET /api/v2/dashboard/dispatch/live", h.dashboardDispatchLive) mux.HandleFunc("GET /api/v2/dashboard/workflow/board", h.dashboardWorkflowBoard) mux.HandleFunc("GET /api/v2/dashboard/spaces/{space}/topics", h.dashboardSpaceTopics) mux.HandleFunc("GET /api/v2/dashboard/spaces/{space}/messages", h.dashboardSpaceMessages) mux.HandleFunc("POST /api/v2/runtime/lanes/{laneID}/tasks/next", h.runtimeNextTaskExecution) mux.HandleFunc("POST /api/v2/runtime/task-executions/{runID}/complete", h.runtimeTaskExecutionComplete) } func writeStoreError(w http.ResponseWriter, err error) { switch { case errors.Is(err, sql.ErrNoRows): httpx.WriteError(w, http.StatusNotFound, err.Error()) case isValidationError(err): httpx.WriteError(w, http.StatusBadRequest, err.Error()) default: httpx.WriteError(w, http.StatusInternalServerError, err.Error()) } } func isValidationError(err error) bool { if err == nil { return false } msg := strings.ToLower(err.Error()) return strings.Contains(msg, "required") || strings.Contains(msg, "invalid") }