package swoosh import ( "encoding/json" "fmt" "io" "net/http" "time" ) // StartHTTPServer registers handlers and runs http.ListenAndServe. // This is a thin adapter layer; all state authority remains with executor. func StartHTTPServer(addr string, executor *Executor) error { mux := http.NewServeMux() // Core SWOOSH endpoints mux.HandleFunc("/transition", handleTransition(executor)) mux.HandleFunc("/state", handleState(executor)) mux.HandleFunc("/health", handleHealth(executor)) // WHOOSH-compatible endpoints (adapter layer only) mux.HandleFunc("/api/v1/opportunities/council", handleCouncilOpportunity(executor)) mux.HandleFunc("/api/v1/tasks", handleTasks(executor)) server := &http.Server{ Addr: addr, Handler: mux, ReadTimeout: 15 * time.Second, WriteTimeout: 15 * time.Second, } return server.ListenAndServe() } // handleTransition processes POST /transition // Body: TransitionProposal // Response: { success, error, state_hash, quarantined } func handleTransition(executor *Executor) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } body, err := io.ReadAll(r.Body) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]interface{}{ "success": false, "error": "failed to read request body", }) return } defer r.Body.Close() var proposal TransitionProposal if err := json.Unmarshal(body, &proposal); err != nil { writeJSON(w, http.StatusBadRequest, map[string]interface{}{ "success": false, "error": fmt.Sprintf("invalid transition proposal: %v", err), }) return } resultCh, err := executor.SubmitTransition(proposal) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]interface{}{ "success": false, "error": err.Error(), }) return } // Block on result channel result := <-resultCh response := map[string]interface{}{ "success": result.Success, "state_hash": result.NewState.StateHash, "quarantined": result.NewState.Policy.Quarantined, } if result.Error != nil { response["error"] = result.Error.Error() } statusCode := http.StatusOK if !result.Success { statusCode = http.StatusBadRequest } writeJSON(w, statusCode, response) } } // handleState processes GET /state // Optional query param: projection (reserved for future use) // Response: { state_hash, hlc_last, projection: {...} } func handleState(executor *Executor) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } snapshot := executor.GetStateSnapshot() // projection query param reserved for future filtering // For now, return full snapshot response := map[string]interface{}{ "state_hash": snapshot.StateHash, "hlc_last": snapshot.HLCLast, "projection": snapshot, } writeJSON(w, http.StatusOK, response) } } // handleHealth processes GET /health // Response: { licensed, quarantined, degraded, recovering, last_applied_hlc, last_applied_index } func handleHealth(executor *Executor) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } snapshot := executor.GetStateSnapshot() response := map[string]interface{}{ "licensed": snapshot.Boot.Licensed, "quarantined": snapshot.Policy.Quarantined, "degraded": snapshot.Control.Degraded, "recovering": snapshot.Control.Recovering, "hlc_last": snapshot.HLCLast, "state_hash": snapshot.StateHash, } writeJSON(w, http.StatusOK, response) } } // handleCouncilOpportunity processes POST /api/v1/opportunities/council // This is a WHOOSH-compatible adapter endpoint. // Maps external council opportunity to deterministic SWOOSH transitions. func handleCouncilOpportunity(executor *Executor) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } body, err := io.ReadAll(r.Body) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]interface{}{ "error": "failed to read request body", }) return } defer r.Body.Close() // Parse WHOOSH-style council opportunity payload var councilReq struct { CouncilID string `json:"council_id"` Roles []string `json:"roles"` WindowID string `json:"window_id"` HLC string `json:"hlc"` Description string `json:"description"` } if err := json.Unmarshal(body, &councilReq); err != nil { writeJSON(w, http.StatusBadRequest, map[string]interface{}{ "error": fmt.Sprintf("invalid council opportunity payload: %v", err), }) return } // For now, we cannot deterministically map arbitrary WHOOSH council // opportunities to catalogued SWOOSH transitions without knowing // the exact mapping between WHOOSH's council lifecycle and SWOOSH's // Council.Phase transitions (PLAN_ROLES|ELECT|TOOLING_SYNC|READY). // // Per instructions: if we cannot deterministically map input to one // catalogued transition using existing fields, respond with HTTP 501. // // Implementation note: When the SWOOSH reducer defines specific transitions // like "COUNCIL_PROFILES_LOADED" or "COUNCIL_QUORUM_CERT", this handler // should construct TransitionProposals for those specific transitions. // // Until then, return 501 Not Implemented. writeJSON(w, http.StatusNotImplemented, map[string]interface{}{ "error": "council opportunity mapping not yet implemented", "reason": "cannot deterministically map WHOOSH council lifecycle to SWOOSH transitions", "contact": "define COUNCIL_* transitions in reducer.go first", }) } } // handleTasks processes GET /api/v1/tasks // This is a WHOOSH-compatible adapter endpoint. // Can only serve data directly from executor.GetStateSnapshot() without inventing state. func handleTasks(executor *Executor) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } // Per instructions: return 501 unless we can serve data directly from // GetStateSnapshot() without inventing new internal state. // // The current OrchestratorState does not have a Tasks field or equivalent. // The Execution phase tracks ActiveWindowID and BeatIndex, but does not // store a list of available tasks. // // If SWOOSH's state machine adds a Tasks []Task field to OrchestratorState // in the future, this handler can return snapshot.Execution.Tasks. // // Until then, return 501 Not Implemented. writeJSON(w, http.StatusNotImplemented, map[string]interface{}{ "error": "task listing not yet implemented", "reason": "OrchestratorState does not contain task queue", "note": "SWOOSH uses deterministic state-machine, not task queues", }) } } // writeJSON is a helper to marshal and write JSON responses func writeJSON(w http.ResponseWriter, statusCode int, data interface{}) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(statusCode) if err := json.NewEncoder(w).Encode(data); err != nil { // If encoding fails, we've already written headers, so log to stderr fmt.Printf("error encoding JSON response: %v\n", err) } }