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>
453 lines
12 KiB
Markdown
453 lines
12 KiB
Markdown
# SWOOSH API Integration Guide
|
|
|
|
**Date:** 2025-10-25
|
|
**CHORUS Commit:** `17673c3` (v0.5.5)
|
|
**Integration Method:** HTTP REST API
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
This document describes the HTTP API layer for SWOOSH, designed to integrate with CHORUS agents at commit `17673c3` using the existing WHOOSH HTTP integration pattern.
|
|
|
|
**Key Principle:** The `Executor` is the **single source of truth**. The API layer is a thin adapter that submits transitions and retrieves snapshots. No state is maintained outside the executor.
|
|
|
|
---
|
|
|
|
## Architecture
|
|
|
|
```
|
|
CHORUS Agents (TCP/libp2p mesh)
|
|
↓
|
|
HTTP REST API
|
|
↓
|
|
SWOOSH API Layer (api.go)
|
|
↓
|
|
Executor (executor.go)
|
|
↓
|
|
Reducer (reducer.go) ← Single source of truth
|
|
↓
|
|
WAL + Snapshot
|
|
```
|
|
|
|
### Strict Constraints
|
|
|
|
1. **No state outside Executor**: API handlers only call `SubmitTransition()` or `GetStateSnapshot()`
|
|
2. **No background goroutines**: All concurrency managed by Executor's single-threaded loop
|
|
3. **No invented transitions**: Only catalogued transitions in `reducer.go` are valid
|
|
4. **Deterministic mapping required**: External requests must map to exactly one transition
|
|
5. **501 for unmappable requests**: If input cannot be deterministically mapped, return HTTP 501
|
|
|
|
---
|
|
|
|
## API Endpoints
|
|
|
|
### Core SWOOSH Endpoints
|
|
|
|
#### `POST /transition`
|
|
|
|
Submit a state transition proposal.
|
|
|
|
**Request:**
|
|
```json
|
|
{
|
|
"current_state_hash": "abc123...",
|
|
"transition": "LICENSE_GRANTED",
|
|
"inputs_hash": "def456...",
|
|
"signer": "node-001",
|
|
"idem_key": "unique-operation-id",
|
|
"hlc": "1-0-0000000000000001",
|
|
"window_id": "window-1",
|
|
"evidence": ["ucxl://proof/123"]
|
|
}
|
|
```
|
|
|
|
**Response (200 OK):**
|
|
```json
|
|
{
|
|
"success": true,
|
|
"state_hash": "new-hash-xyz...",
|
|
"quarantined": false
|
|
}
|
|
```
|
|
|
|
**Response (400 Bad Request):**
|
|
```json
|
|
{
|
|
"success": false,
|
|
"error": "guard rejected transition",
|
|
"state_hash": "unchanged-hash",
|
|
"quarantined": false
|
|
}
|
|
```
|
|
|
|
**Implementation:**
|
|
- Calls `executor.SubmitTransition(proposal)`
|
|
- Blocks on result channel
|
|
- Returns transition outcome
|
|
|
|
---
|
|
|
|
#### `GET /state`
|
|
|
|
Retrieve current orchestrator state snapshot.
|
|
|
|
**Query Parameters:**
|
|
- `projection` (reserved for future use)
|
|
|
|
**Response (200 OK):**
|
|
```json
|
|
{
|
|
"state_hash": "abc123...",
|
|
"hlc_last": "5-0-0000000000000005",
|
|
"projection": {
|
|
"Meta": {...},
|
|
"Boot": {...},
|
|
"Ingestion": {...},
|
|
"Council": {...},
|
|
"Environment": {...},
|
|
"Execution": {...},
|
|
"Control": {...},
|
|
"Policy": {...}
|
|
}
|
|
}
|
|
```
|
|
|
|
**Implementation:**
|
|
- Calls `executor.GetStateSnapshot()` (deep copy)
|
|
- Returns full state structure
|
|
|
|
---
|
|
|
|
#### `GET /health`
|
|
|
|
Health check endpoint for monitoring.
|
|
|
|
**Response (200 OK):**
|
|
```json
|
|
{
|
|
"licensed": true,
|
|
"quarantined": false,
|
|
"degraded": false,
|
|
"recovering": false,
|
|
"hlc_last": "10-0-0000000000000010",
|
|
"state_hash": "current-hash"
|
|
}
|
|
```
|
|
|
|
**Implementation:**
|
|
- Calls `executor.GetStateSnapshot()`
|
|
- Extracts health-relevant fields
|
|
|
|
---
|
|
|
|
### WHOOSH-Compatible Adapter Endpoints
|
|
|
|
#### `POST /api/v1/opportunities/council`
|
|
|
|
**Status:** ⚠️ **501 Not Implemented**
|
|
|
|
**Reason:** Cannot deterministically map WHOOSH council lifecycle to SWOOSH transitions without defining specific `COUNCIL_*` transitions in `reducer.go`.
|
|
|
|
**Future Implementation:**
|
|
Once `reducer.go` defines transitions like:
|
|
- `COUNCIL_PROFILES_LOADED`
|
|
- `COUNCIL_QUORUM_CERT`
|
|
- `COUNCIL_ELECT_COMPLETE`
|
|
|
|
This handler will:
|
|
1. Parse WHOOSH council opportunity payload
|
|
2. Map to appropriate SWOOSH transition
|
|
3. Build `TransitionProposal`
|
|
4. Submit via `executor.SubmitTransition()`
|
|
|
|
**Current Response (501 Not Implemented):**
|
|
```json
|
|
{
|
|
"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"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
#### `GET /api/v1/tasks`
|
|
|
|
**Status:** ⚠️ **501 Not Implemented**
|
|
|
|
**Reason:** `OrchestratorState` does not contain a task queue. SWOOSH uses deterministic state-machine phases, not task lists.
|
|
|
|
**Architecture Note:** SWOOSH's state machine tracks execution phases (`PLAN`, `WORK`, `REVIEW`, `REVERB`) but not individual tasks. Tasks are managed externally (e.g., GITEA issues) and referenced via:
|
|
- `Execution.ActiveWindowID`
|
|
- `Execution.BeatIndex`
|
|
- Evidence arrays in transition proposals
|
|
|
|
**Current Response (501 Not Implemented):**
|
|
```json
|
|
{
|
|
"error": "task listing not yet implemented",
|
|
"reason": "OrchestratorState does not contain task queue",
|
|
"note": "SWOOSH uses deterministic state-machine, not task queues"
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## Integration with CHORUS
|
|
|
|
### Docker Compose Configuration
|
|
|
|
**In `/home/tony/chorus/project-queues/active/CHORUS/docker/docker-compose.yml`:**
|
|
|
|
```yaml
|
|
services:
|
|
chorus:
|
|
image: anthonyrawlins/chorus:0.5.48
|
|
environment:
|
|
- WHOOSH_API_BASE_URL=${SWOOSH_API_BASE_URL:-http://swoosh:8080}
|
|
- WHOOSH_API_ENABLED=true
|
|
# ... other CHORUS env vars
|
|
|
|
swoosh:
|
|
image: your-registry/swoosh:latest
|
|
ports:
|
|
- target: 8080
|
|
published: 8800
|
|
protocol: tcp
|
|
mode: ingress
|
|
environment:
|
|
- SWOOSH_LISTEN_ADDR=:8080
|
|
- SWOOSH_WAL_DIR=/app/data/wal
|
|
- SWOOSH_SNAPSHOT_DIR=/app/data/snapshots
|
|
- SWOOSH_DATABASE_DB_HOST=postgres
|
|
- SWOOSH_DATABASE_DB_PORT=5432
|
|
- SWOOSH_DATABASE_DB_NAME=swoosh
|
|
# ... other SWOOSH config
|
|
volumes:
|
|
- swoosh_data:/app/data
|
|
networks:
|
|
- tengig
|
|
- chorus_ipvlan
|
|
healthcheck:
|
|
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
start_period: 40s
|
|
```
|
|
|
|
### Transition Mapping
|
|
|
|
When CHORUS communicates with SWOOSH, external API calls must map to internal transitions:
|
|
|
|
| CHORUS Request | SWOOSH Transition | Status |
|
|
|----------------|-------------------|--------|
|
|
| `POST /api/v1/opportunities/council` | `COUNCIL_PROFILES_LOADED` (proposed) | 501 - Not Implemented |
|
|
| `POST /api/v1/councils/{id}/claims` | `COUNCIL_ROLE_CLAIMED` (proposed) | 501 - Not Implemented |
|
|
| `GET /api/v1/tasks` | N/A (state query, not transition) | 501 - Not Implemented |
|
|
| `POST /api/v1/tasks/{id}/claim` | `EXECUTION_TASK_CLAIMED` (proposed) | 501 - Not Implemented |
|
|
| `POST /api/v1/tasks/{id}/complete` | `EXECUTION_TASK_COMPLETE` (proposed) | 501 - Not Implemented |
|
|
|
|
**Action Required:** Define these transitions in `reducer.go` to enable WHOOSH-compatible endpoints.
|
|
|
|
---
|
|
|
|
## Running SWOOSH Server
|
|
|
|
### Build
|
|
|
|
```bash
|
|
cd /home/tony/chorus/SWOOSH
|
|
go build -o build/swoosh-server ./cmd/swoosh-server
|
|
```
|
|
|
|
### Run
|
|
|
|
```bash
|
|
export SWOOSH_LISTEN_ADDR=:8080
|
|
export SWOOSH_WAL_DIR=/path/to/wal
|
|
export SWOOSH_SNAPSHOT_DIR=/path/to/snapshots
|
|
|
|
./build/swoosh-server
|
|
```
|
|
|
|
### Test
|
|
|
|
```bash
|
|
# Health check
|
|
curl http://localhost:8080/health
|
|
|
|
# Get state
|
|
curl http://localhost:8080/state
|
|
|
|
# Submit transition
|
|
curl -X POST http://localhost:8080/transition \
|
|
-H "Content-Type: application/json" \
|
|
-d '{
|
|
"current_state_hash": "genesis",
|
|
"transition": "LICENSE_GRANTED",
|
|
"inputs_hash": "test",
|
|
"signer": "test-node",
|
|
"idem_key": "unique-1",
|
|
"hlc": "1-0-0000000000000001",
|
|
"window_id": "window-1",
|
|
"evidence": []
|
|
}'
|
|
```
|
|
|
|
---
|
|
|
|
## Testing
|
|
|
|
```bash
|
|
cd /home/tony/chorus/SWOOSH
|
|
go test -v ./...
|
|
```
|
|
|
|
**Test Coverage:**
|
|
- ✅ `POST /transition` - Success and failure cases
|
|
- ✅ `GET /state` - Snapshot retrieval
|
|
- ✅ `GET /health` - Health status
|
|
- ✅ `POST /api/v1/opportunities/council` - Returns 501
|
|
- ✅ `GET /api/v1/tasks` - Returns 501
|
|
|
|
---
|
|
|
|
## Next Steps for Full CHORUS Integration
|
|
|
|
### 1. Define CHORUS-Compatible Transitions
|
|
|
|
In `reducer.go`, add:
|
|
|
|
```go
|
|
case "COUNCIL_PROFILES_LOADED":
|
|
if state.Council.Phase != "PLAN_ROLES" {
|
|
return false, ErrInvalidPhase
|
|
}
|
|
// Parse PlannedRoles from proposal.Evidence
|
|
// Update state.Council.PlannedRoles
|
|
state.Council.Phase = "ELECT"
|
|
state.Council.Epoch++
|
|
return true, nil
|
|
|
|
case "COUNCIL_ROLE_CLAIMED":
|
|
if state.Council.Phase != "ELECT" {
|
|
return false, ErrInvalidPhase
|
|
}
|
|
// Parse CouncilMember from proposal.Evidence
|
|
// Append to state.Council.Members
|
|
// Check if quorum reached
|
|
if quorumReached(state.Council.Members) {
|
|
state.Council.Phase = "TOOLING_SYNC"
|
|
}
|
|
state.Council.Epoch++
|
|
return true, nil
|
|
|
|
// ... additional transitions
|
|
```
|
|
|
|
### 2. Update API Handlers
|
|
|
|
Once transitions are defined, update `api.go`:
|
|
|
|
```go
|
|
func handleCouncilOpportunity(executor *Executor) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
// Parse request
|
|
var req CouncilOpportunityRequest
|
|
json.NewDecoder(r.Body).Decode(&req)
|
|
|
|
// Build transition proposal
|
|
proposal := TransitionProposal{
|
|
TransitionName: "COUNCIL_PROFILES_LOADED",
|
|
Evidence: []string{
|
|
encodeCouncilRoles(req.Roles),
|
|
},
|
|
HLC: req.HLC,
|
|
WindowID: req.WindowID,
|
|
// ... other fields
|
|
}
|
|
|
|
// Submit to executor
|
|
resultCh, _ := executor.SubmitTransition(proposal)
|
|
result := <-resultCh
|
|
|
|
// Return response
|
|
writeJSON(w, http.StatusOK, result)
|
|
}
|
|
}
|
|
```
|
|
|
|
### 3. Implement GuardProvider
|
|
|
|
Create production guard implementations:
|
|
|
|
```go
|
|
type ProductionGuardProvider struct {
|
|
kachingClient *KachingClient
|
|
backbeatClient *BackbeatClient
|
|
hmmmClient *HMMMClient
|
|
shhhClient *SHHHClient
|
|
mcpHealthClient *MCPHealthClient
|
|
}
|
|
|
|
func (p *ProductionGuardProvider) Evaluate(t TransitionProposal, s OrchestratorState) (GuardOutcome, error) {
|
|
outcome := GuardOutcome{}
|
|
|
|
// Check KACHING license
|
|
outcome.LicenseOK = p.kachingClient.ValidateLicense(s.Boot.NodeID)
|
|
|
|
// Check BACKBEAT heartbeat
|
|
outcome.BackbeatOK = p.backbeatClient.CheckHeartbeat()
|
|
|
|
// Check HMMM quorum
|
|
outcome.QuorumOK = p.hmmmClient.CheckQuorum()
|
|
|
|
// Check SHHH policy
|
|
outcome.PolicyOK = p.shhhClient.CheckPolicy(t)
|
|
|
|
// Check MCP health
|
|
outcome.MCPHealthy = p.mcpHealthClient.CheckHealth()
|
|
|
|
return outcome, nil
|
|
}
|
|
```
|
|
|
|
### 4. Add Persistent WAL/Snapshot Stores
|
|
|
|
Replace in-memory implementations with BadgerDB WAL and atomic file snapshots (already defined in `wal.go` and `snapshot.go`).
|
|
|
|
---
|
|
|
|
## Design Compliance Checklist
|
|
|
|
- ✅ **Single source of truth**: Only `Executor` mutates state
|
|
- ✅ **No external state**: API handlers have no local caches or maps
|
|
- ✅ **Deterministic transitions only**: All mutations via `reducer.go`
|
|
- ✅ **HTTP 501 for unmappable requests**: WHOOSH endpoints return 501 until transitions defined
|
|
- ✅ **Blocking on executor**: All API calls wait for executor result channel
|
|
- ✅ **Deep copies for reads**: `GetStateSnapshot()` returns isolated state
|
|
- ✅ **No background goroutines**: Single-threaded executor model preserved
|
|
- ✅ **Standard library only**: Uses `net/http` and `encoding/json`
|
|
- ✅ **Compiles successfully**: `go build ./...` passes
|
|
- ✅ **Test coverage**: All handlers tested
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
The HTTP API layer (`api.go`) is a **thin, stateless adapter** that:
|
|
|
|
1. Accepts HTTP requests
|
|
2. Maps to `TransitionProposal` structures
|
|
3. Submits to `executor.SubmitTransition()`
|
|
4. Blocks on result channel
|
|
5. Returns HTTP response
|
|
|
|
**No orchestration logic lives in the API layer.** All state transitions, validation, and persistence are handled by the `Executor` and `Reducer`.
|
|
|
|
WHOOSH-compatible endpoints return **HTTP 501 Not Implemented** until corresponding transitions are defined in `reducer.go`, ensuring we never mutate state without deterministic transitions.
|
|
|
|
For CHORUS integration at commit `17673c3`, simply point `WHOOSH_API_BASE_URL` to SWOOSH's HTTP endpoint and define the required transitions.
|