Major enhancements: - Added production-grade durability guarantees with fsync operations - Implemented BadgerDB WAL for crash recovery and persistence - Added comprehensive HTTP API (GET/POST /state, POST /command) - Exported ComputeStateHash for external use in genesis initialization - Enhanced snapshot system with atomic write-fsync-rename sequence - Added API integration documentation and durability guarantees docs New files: - api.go: HTTP server implementation with state and command endpoints - api_test.go: Comprehensive API test suite - badger_wal.go: BadgerDB-based write-ahead log - cmd/swoosh/main.go: CLI entry point with API server - API_INTEGRATION.md: API usage and integration guide - DURABILITY.md: Durability guarantees and recovery procedures - CHANGELOG.md: Version history and changes - RELEASE_NOTES.md: Release notes for v1.0.0 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
237 lines
7.4 KiB
Go
237 lines
7.4 KiB
Go
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)
|
|
}
|
|
}
|