# Sequential Thinking Age-Encrypted Wrapper - Implementation Guide **Date**: 2025-10-13 **Status**: Ready for Implementation **Project**: SequentialThinkingForCHORUS **Priority**: High - Blocking agent intelligence improvements --- ## Executive Summary This document provides the **complete implementation plan** for the age-encrypted Sequential Thinking MCP wrapper as specified in the SequentialThinkingForCHORUS issue #1. This wrapper enables secure, production-grade deployment of Sequential Thinking capabilities while maintaining confidentiality through end-to-end encryption. **Two-Part Solution**: 1. **Part A**: Build the `seqthink-age` container (age-encrypted wrapper) 2. **Part B**: Integrate CHORUS agents with the encrypted wrapper --- ## Part A: seqthink-age Container Implementation ### Repository Structure ``` seqthink-age/ ├── cmd/ │ └── agewrap/ │ └── main.go # Wrapper entry point ├── pkg/ │ ├── ageio/ │ │ ├── crypto.go # SealFrame/OpenFrame + gzip │ │ ├── crypto_test.go │ │ └── testdata/ # Golden test vectors │ ├── policy/ │ │ ├── kaching.go # JWT verification │ │ ├── scope.go # Scope checking │ │ └── policy_test.go │ ├── proxy/ │ │ ├── handlers.go # HTTP handlers │ │ ├── sse.go # SSE streaming │ │ ├── health.go # Health checks │ │ └── proxy_test.go │ ├── mcpclient/ │ │ ├── client.go # Loopback MCP caller │ │ └── client_test.go │ └── observability/ │ ├── metrics.go # Prometheus metrics │ ├── logging.go # Structured logging │ └── pprof.go # Optional profiling ├── deploy/ │ ├── Dockerfile # Multi-stage build │ ├── entrypoint.sh # Process orchestration │ └── compose.example.yml # Local dev stack ├── secrets/ # .gitignored │ ├── age_identity.key.example │ └── age_recipients.txt.example ├── test/ │ ├── integration/ │ │ └── e2e_test.go │ └── fixtures/ │ └── jwt_samples.json ├── docs/ │ ├── API.md # API documentation │ ├── SECURITY.md # Security model │ └── WHOOSH_INTEGRATION.md # WHOOSH client guide ├── .github/ │ └── workflows/ │ └── ci.yml # Build, test, sign, push ├── Makefile ├── go.mod ├── go.sum ├── README.md └── LICENSE ``` --- ## Beat 1: Skeleton & Plaintext Correct ### Implementation Tasks #### 1.1 Repository Setup ```bash # Create repository structure mkdir -p seqthink-age/{cmd/agewrap,pkg/{ageio,policy,proxy,mcpclient,observability},deploy,test/{integration,fixtures},docs,.github/workflows} # Initialize Go module cd seqthink-age go mod init github.com/chorus-services/seqthink-age # Add dependencies go get github.com/gorilla/mux go get github.com/rs/zerolog go get github.com/prometheus/client_golang/prometheus go get filippo.io/age go get github.com/golang-jwt/jwt/v5 ``` #### 1.2 Main Entry Point **File**: `cmd/agewrap/main.go` ```go package main import ( "context" "fmt" "net/http" "os" "os/signal" "syscall" "time" "github.com/chorus-services/seqthink-age/pkg/mcpclient" "github.com/chorus-services/seqthink-age/pkg/proxy" "github.com/chorus-services/seqthink-age/pkg/observability" "github.com/rs/zerolog/log" ) type Config struct { Port string MCPLocalURL string LogLevel string MaxBodyMB int HealthTimeout time.Duration ShutdownTimeout time.Duration } func loadConfig() *Config { return &Config{ Port: getEnv("PORT", "8443"), MCPLocalURL: getEnv("MCP_LOCAL", "http://127.0.0.1:8000"), LogLevel: getEnv("LOG_LEVEL", "info"), MaxBodyMB: getEnvInt("MAX_BODY_MB", 4), HealthTimeout: 5 * time.Second, ShutdownTimeout: 30 * time.Second, } } func main() { cfg := loadConfig() // Initialize observability observability.InitLogger(cfg.LogLevel) metrics := observability.InitMetrics() log.Info(). Str("port", cfg.Port). Str("mcp_url", cfg.MCPLocalURL). Msg("Starting seqthink-age wrapper") // Create MCP client mcpClient := mcpclient.New(cfg.MCPLocalURL) // Wait for MCP server to be ready ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel() if err := waitForMCP(ctx, mcpClient); err != nil { log.Fatal().Err(err).Msg("MCP server not ready") } log.Info().Msg("✅ MCP server ready") // Create proxy server proxyServer := proxy.NewServer(proxy.ServerConfig{ MCPClient: mcpClient, Metrics: metrics, MaxBodyMB: cfg.MaxBodyMB, }) // Setup HTTP server srv := &http.Server{ Addr: ":" + cfg.Port, Handler: proxyServer.Handler(), ReadTimeout: 30 * time.Second, WriteTimeout: 90 * time.Second, IdleTimeout: 120 * time.Second, } // Start server in goroutine go func() { log.Info().Str("addr", srv.Addr).Msg("🚀 Wrapper listening") if err := srv.ListenAndServe(); err != http.ErrServerClosed { log.Fatal().Err(err).Msg("HTTP server failed") } }() // Wait for shutdown signal sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) <-sigChan log.Info().Msg("🛑 Shutting down gracefully...") // Graceful shutdown shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), cfg.ShutdownTimeout) defer shutdownCancel() if err := srv.Shutdown(shutdownCtx); err != nil { log.Error().Err(err).Msg("Shutdown error") } log.Info().Msg("✅ Shutdown complete") } func waitForMCP(ctx context.Context, client *mcpclient.Client) error { ticker := time.NewTicker(1 * time.Second) defer ticker.Stop() for { select { case <-ctx.Done(): return fmt.Errorf("timeout waiting for MCP server") case <-ticker.C: if err := client.Health(ctx); err == nil { return nil } log.Debug().Msg("Waiting for MCP server...") } } } func getEnv(key, defaultVal string) string { if val := os.Getenv(key); val != "" { return val } return defaultVal } func getEnvInt(key string, defaultVal int) int { // Implementation return defaultVal } ``` #### 1.3 MCP Client **File**: `pkg/mcpclient/client.go` ```go package mcpclient import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" ) type Client struct { baseURL string httpClient *http.Client } type ToolRequest struct { Tool string `json:"tool"` Payload map[string]interface{} `json:"payload"` } type ToolResponse struct { Result interface{} `json:"result"` Error string `json:"error,omitempty"` } func New(baseURL string) *Client { return &Client{ baseURL: baseURL, httpClient: &http.Client{ Timeout: 60 * time.Second, }, } } func (c *Client) Health(ctx context.Context) error { req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/health", nil) if err != nil { return err } resp, err := c.httpClient.Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return fmt.Errorf("health check failed: status %d", resp.StatusCode) } return nil } func (c *Client) CallTool(ctx context.Context, req *ToolRequest) (*ToolResponse, error) { jsonData, err := json.Marshal(req) if err != nil { return nil, fmt.Errorf("marshal request: %w", err) } httpReq, err := http.NewRequestWithContext(ctx, "POST", c.baseURL+"/mcp/tool", bytes.NewReader(jsonData)) if err != nil { return nil, err } httpReq.Header.Set("Content-Type", "application/json") resp, err := c.httpClient.Do(httpReq) if err != nil { return nil, fmt.Errorf("http request: %w", err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("read response: %w", err) } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("tool call failed: status %d, body: %s", resp.StatusCode, string(body)) } var toolResp ToolResponse if err := json.Unmarshal(body, &toolResp); err != nil { return nil, fmt.Errorf("unmarshal response: %w", err) } return &toolResp, nil } ``` #### 1.4 Proxy Handlers (Plaintext Phase) **File**: `pkg/proxy/server.go` ```go package proxy import ( "encoding/json" "io" "net/http" "github.com/chorus-services/seqthink-age/pkg/mcpclient" "github.com/chorus-services/seqthink-age/pkg/observability" "github.com/gorilla/mux" "github.com/rs/zerolog/log" ) type ServerConfig struct { MCPClient *mcpclient.Client Metrics *observability.Metrics MaxBodyMB int } type Server struct { config ServerConfig router *mux.Router } func NewServer(cfg ServerConfig) *Server { s := &Server{ config: cfg, router: mux.NewRouter(), } s.setupRoutes() return s } func (s *Server) Handler() http.Handler { return s.router } func (s *Server) setupRoutes() { // Health endpoints s.router.HandleFunc("/health", s.handleHealth).Methods("GET") s.router.HandleFunc("/ready", s.handleReady).Methods("GET") // MCP proxy endpoints s.router.HandleFunc("/mcp/tool", s.handleToolCall).Methods("POST") s.router.HandleFunc("/mcp/sse", s.handleSSE).Methods("GET") // Metrics (optional) if s.config.Metrics != nil { s.router.Handle("/metrics", s.config.Metrics.Handler()) } } func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // Check MCP server health if err := s.config.MCPClient.Health(ctx); err != nil { log.Warn().Err(err).Msg("MCP health check failed") http.Error(w, "MCP server unhealthy", http.StatusServiceUnavailable) return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) } func (s *Server) handleReady(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(map[string]string{"status": "ready"}) } func (s *Server) handleToolCall(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // Limit body size r.Body = http.MaxBytesReader(w, r.Body, int64(s.config.MaxBodyMB*1024*1024)) // Read request body body, err := io.ReadAll(r.Body) if err != nil { log.Warn().Err(err).Msg("Failed to read request body") http.Error(w, "Invalid request body", http.StatusBadRequest) return } // Parse as MCP tool request var toolReq mcpclient.ToolRequest if err := json.Unmarshal(body, &toolReq); err != nil { log.Warn().Err(err).Msg("Failed to parse tool request") http.Error(w, "Invalid JSON", http.StatusBadRequest) return } // Call MCP server resp, err := s.config.MCPClient.CallTool(ctx, &toolReq) if err != nil { log.Error().Err(err).Msg("MCP tool call failed") http.Error(w, "Tool execution failed", http.StatusInternalServerError) return } // Return response w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) json.NewEncoder(w).Encode(resp) } func (s *Server) handleSSE(w http.ResponseWriter, r *http.Request) { // SSE implementation (placeholder for Beat 1) http.Error(w, "SSE not yet implemented", http.StatusNotImplemented) } ``` #### 1.5 Dockerfile & Entrypoint **File**: `deploy/Dockerfile` ```dockerfile # Build stage FROM golang:1.22-alpine AS builder WORKDIR /build # Copy go mod files COPY go.mod go.sum ./ RUN go mod download # Copy source COPY . . # Build wrapper RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ -ldflags="-w -s" \ -o agewrap \ ./cmd/agewrap # Runtime stage FROM debian:stable-slim # Install MCP server (placeholder - adjust based on actual MCP distribution) RUN apt-get update && apt-get install -y \ ca-certificates \ curl \ && rm -rf /var/lib/apt/lists/* # Copy wrapper binary COPY --from=builder /build/agewrap /usr/local/bin/agewrap # Copy entrypoint COPY deploy/entrypoint.sh /entrypoint.sh RUN chmod +x /entrypoint.sh # Create non-root user RUN groupadd -g 1000 seqthink && \ useradd -u 1000 -g seqthink -s /bin/bash -d /home/seqthink -m seqthink # Runtime directories RUN mkdir -p /run/secrets /tmp/seqthink && \ chown -R seqthink:seqthink /tmp/seqthink USER seqthink EXPOSE 8443 ENTRYPOINT ["/entrypoint.sh"] ``` **File**: `deploy/entrypoint.sh` ```bash #!/bin/bash set -euo pipefail echo "🚀 Starting Sequential Thinking Wrapper" # Start MCP server on loopback echo "📡 Starting MCP server on 127.0.0.1:8000..." sequentialthinking --transport=http --port=8000 --bind=127.0.0.1 & MCP_PID=$! # Wait for MCP to be ready echo "⏳ Waiting for MCP server..." for i in {1..30}; do if curl -sf http://127.0.0.1:8000/health > /dev/null 2>&1; then echo "✅ MCP server ready" break fi if [ $i -eq 30 ]; then echo "❌ MCP server failed to start" exit 1 fi sleep 1 done # Start age wrapper echo "🔐 Starting age wrapper on :${PORT:-8443}..." exec agewrap ``` #### 1.6 Makefile **File**: `Makefile` ```makefile .PHONY: all build test lint clean image # Variables IMAGE_NAME ?= anthonyrawlins/seqthink-age IMAGE_TAG ?= latest GOBIN ?= $(shell go env GOPATH)/bin all: lint test build build: @echo "Building agewrap..." go build -o bin/agewrap ./cmd/agewrap test: @echo "Running tests..." go test -v -race -cover ./... lint: @echo "Running linters..." golangci-lint run ./... clean: @echo "Cleaning..." rm -rf bin/ image: @echo "Building Docker image..." docker build -f deploy/Dockerfile -t $(IMAGE_NAME):$(IMAGE_TAG) . image-push: @echo "Pushing image..." docker push $(IMAGE_NAME):$(IMAGE_TAG) run-local: @echo "Running locally..." MCP_LOCAL=http://127.0.0.1:8000 PORT=8443 go run ./cmd/agewrap compose-up: docker-compose -f deploy/compose.example.yml up --build compose-down: docker-compose -f deploy/compose.example.yml down ``` ### Beat 1 Acceptance Criteria - [ ] `make build` compiles successfully - [ ] `docker build` creates image - [ ] Container starts MCP server on loopback - [ ] Container starts wrapper on :8443 - [ ] `/health` endpoint returns 200 - [ ] POST to `/mcp/tool` with plaintext JSON returns tool result - [ ] Logs are structured JSON - [ ] Graceful shutdown works --- ## Beat 2: Crypto Envelope (Age + Gzip) ### Implementation Tasks #### 2.1 Age Crypto Implementation **File**: `pkg/ageio/crypto.go` ```go package ageio import ( "bytes" "compress/gzip" "fmt" "io" "filippo.io/age" ) // SealFrame encrypts and compresses plaintext func SealFrame(plaintext []byte, recipients []age.Recipient) ([]byte, error) { // 1. Gzip compress var compressed bytes.Buffer gzipWriter := gzip.NewWriter(&compressed) if _, err := gzipWriter.Write(plaintext); err != nil { return nil, fmt.Errorf("gzip write: %w", err) } if err := gzipWriter.Close(); err != nil { return nil, fmt.Errorf("gzip close: %w", err) } // 2. Age encrypt var encrypted bytes.Buffer encWriter, err := age.Encrypt(&encrypted, recipients...) if err != nil { return nil, fmt.Errorf("age encrypt: %w", err) } if _, err := encWriter.Write(compressed.Bytes()); err != nil { return nil, fmt.Errorf("encrypt write: %w", err) } if err := encWriter.Close(); err != nil { return nil, fmt.Errorf("encrypt close: %w", err) } return encrypted.Bytes(), nil } // OpenFrame decrypts and decompresses ciphertext func OpenFrame(ciphertext []byte, identities []age.Identity) ([]byte, error) { // 1. Age decrypt decReader, err := age.Decrypt(bytes.NewReader(ciphertext), identities...) if err != nil { return nil, fmt.Errorf("age decrypt: %w", err) } // 2. Gunzip decompress gzipReader, err := gzip.NewReader(decReader) if err != nil { return nil, fmt.Errorf("gzip reader: %w", err) } defer gzipReader.Close() plaintext, err := io.ReadAll(gzipReader) if err != nil { return nil, fmt.Errorf("decompress: %w", err) } return plaintext, nil } ``` **File**: `pkg/ageio/keys.go` ```go package ageio import ( "bufio" "fmt" "os" "strings" "filippo.io/age" ) // LoadIdentities loads age identities from file func LoadIdentities(path string) ([]age.Identity, error) { f, err := os.Open(path) if err != nil { return nil, fmt.Errorf("open identity file: %w", err) } defer f.Close() return age.ParseIdentities(f) } // LoadRecipients loads age recipients from file func LoadRecipients(path string) ([]age.Recipient, error) { f, err := os.Open(path) if err != nil { return nil, fmt.Errorf("open recipients file: %w", err) } defer f.Close() var recipients []age.Recipient scanner := bufio.NewScanner(f) for scanner.Scan() { line := strings.TrimSpace(scanner.Text()) if line == "" || strings.HasPrefix(line, "#") { continue } recipient, err := age.ParseX25519Recipient(line) if err != nil { return nil, fmt.Errorf("parse recipient: %w", err) } recipients = append(recipients, recipient) } if err := scanner.Err(); err != nil { return nil, fmt.Errorf("scan recipients: %w", err) } return recipients, nil } ``` #### 2.2 Update Proxy for Encryption **File**: `pkg/proxy/server.go` (additions) ```go type Server struct { config ServerConfig router *mux.Router identities []age.Identity // NEW recipients []age.Recipient // NEW } func NewServer(cfg ServerConfig) (*Server, error) { s := &Server{ config: cfg, router: mux.NewRouter(), } // Load age keys if configured if cfg.AgeIdentPath != "" { identities, err := ageio.LoadIdentities(cfg.AgeIdentPath) if err != nil { return nil, fmt.Errorf("load identities: %w", err) } s.identities = identities } if cfg.AgeRecipsPath != "" { recipients, err := ageio.LoadRecipients(cfg.AgeRecipsPath) if err != nil { return nil, fmt.Errorf("load recipients: %w", err) } s.recipients = recipients } s.setupRoutes() return s, nil } func (s *Server) handleToolCall(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // Check content type if encryption is enabled if s.identities != nil { contentType := r.Header.Get("Content-Type") if contentType != "application/age" { http.Error(w, "Content-Type must be application/age", http.StatusUnsupportedMediaType) return } } // Read encrypted body r.Body = http.MaxBytesReader(w, r.Body, int64(s.config.MaxBodyMB*1024*1024)) encryptedBody, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "Failed to read body", http.StatusBadRequest) return } // Decrypt if encryption enabled var plaintext []byte if s.identities != nil { plaintext, err = ageio.OpenFrame(encryptedBody, s.identities) if err != nil { log.Error().Err(err).Msg("Decryption failed") s.config.Metrics.RecordDecryptFailure() http.Error(w, "Decryption failed", http.StatusBadRequest) return } } else { plaintext = encryptedBody } // Parse tool request var toolReq mcpclient.ToolRequest if err := json.Unmarshal(plaintext, &toolReq); err != nil { http.Error(w, "Invalid JSON", http.StatusBadRequest) return } // Call MCP resp, err := s.config.MCPClient.CallTool(ctx, &toolReq) if err != nil { http.Error(w, "Tool execution failed", http.StatusInternalServerError) return } // Marshal response responseJSON, err := json.Marshal(resp) if err != nil { http.Error(w, "Failed to marshal response", http.StatusInternalServerError) return } // Encrypt response if encryption enabled var responseBody []byte if s.recipients != nil { responseBody, err = ageio.SealFrame(responseJSON, s.recipients) if err != nil { log.Error().Err(err).Msg("Encryption failed") http.Error(w, "Encryption failed", http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/age") } else { responseBody = responseJSON w.Header().Set("Content-Type", "application/json") } w.WriteHeader(http.StatusOK) w.Write(responseBody) } ``` ### Beat 2 Acceptance Criteria - [ ] Age encrypt/decrypt golden tests pass - [ ] Wrapper enforces `Content-Type: application/age` - [ ] End-to-end: encrypted POST → encrypted response - [ ] Decryption failures return 400 with proper logging - [ ] Metrics track decrypt successes/failures --- *[Continuing with Beats 3-6 would follow similar detailed patterns...]* --- ## Part B: CHORUS Agent Integration ### B.1 CHORUS AI Provider Extension **File**: `pkg/ai/seqthink_provider.go` ```go package ai import ( "bytes" "context" "encoding/json" "fmt" "io" "net/http" "time" "filippo.io/age" ) // SeqThinkProvider wraps age-encrypted Sequential Thinking type SeqThinkProvider struct { config ProviderConfig httpClient *http.Client recipients []age.Recipient identities []age.Identity jwt string // KACHING JWT for policy } type SeqThinkRequest struct { Tool string `json:"tool"` Payload SeqThinkPayload `json:"payload"` Policy SeqThinkPolicy `json:"policy"` TS time.Time `json:"ts"` Nonce string `json:"nonce"` } type SeqThinkPayload struct { Objective string `json:"objective"` MaxDepth int `json:"max_depth"` Reflect bool `json:"reflect"` } type SeqThinkPolicy struct { Caller string `json:"caller"` License string `json:"license"` // KACHING JWT Scope []string `json:"scope"` } func (p *SeqThinkProvider) ExecuteTask(ctx context.Context, request *TaskRequest) (*TaskResponse, error) { startTime := time.Now() // Build Sequential Thinking request seqReq := SeqThinkRequest{ Tool: "sequentialthinking", Payload: SeqThinkPayload{ Objective: p.formatTaskAsObjective(request), MaxDepth: 10, Reflect: true, }, Policy: SeqThinkPolicy{ Caller: request.AgentID, License: p.jwt, Scope: []string{"sequentialthinking.run"}, }, TS: time.Now(), Nonce: generateNonce(), } // Marshal to JSON plaintext, err := json.Marshal(seqReq) if err != nil { return nil, fmt.Errorf("marshal request: %w", err) } // Encrypt with age encrypted, err := ageio.SealFrame(plaintext, p.recipients) if err != nil { return nil, fmt.Errorf("encrypt request: %w", err) } // POST to wrapper httpReq, err := http.NewRequestWithContext(ctx, "POST", p.config.Endpoint+"/mcp/tool", bytes.NewReader(encrypted)) if err != nil { return nil, err } httpReq.Header.Set("Content-Type", "application/age") resp, err := p.httpClient.Do(httpReq) if err != nil { return nil, fmt.Errorf("http request: %w", err) } defer resp.Body.Close() // Read encrypted response encryptedResp, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("read response: %w", err) } // Decrypt response decrypted, err := ageio.OpenFrame(encryptedResp, p.identities) if err != nil { return nil, fmt.Errorf("decrypt response: %w", err) } // Parse thinking result var thinkingResult struct { Thoughts []ThoughtStep `json:"thoughts"` Conclusion string `json:"conclusion"` } if err := json.Unmarshal(decrypted, &thinkingResult); err != nil { return nil, fmt.Errorf("unmarshal response: %w", err) } // Convert to TaskResponse with structured reasoning return &TaskResponse{ Success: true, TaskID: request.TaskID, AgentID: request.AgentID, Provider: "seqthink", Response: thinkingResult.Conclusion, StructuredReasoning: &StructuredReasoning{ Thoughts: thinkingResult.Thoughts, Confidence: 0.9, // Extract from thinking result }, StartTime: startTime, EndTime: time.Now(), Duration: time.Since(startTime), }, nil } ``` ### B.2 Configuration **File**: `config/agent.yaml` (add seqthink provider) ```yaml ai_providers: # Existing providers... seqthink: type: "seqthink" endpoint: "http://seqthink-age:8443" # Age encryption age_ident_path: "/run/secrets/age_identity.key" age_recips_path: "/run/secrets/age_recipients.txt" # KACHING authentication kaching_jwt_path: "/run/secrets/kaching_jwt" # Sequential thinking config max_depth: 15 enable_reflection: true min_complexity: 7 # Only use for complex tasks role_model_mapping: roles: architect: provider: "seqthink" # Always use Sequential Thinking for architects fallback_provider: "resetdata" ``` --- ## Timeline & Resource Estimates ### Beat-by-Beat Timeline | Beat | Tasks | Duration | Resources | |------|-------|----------|-----------| | Beat 1 | Skeleton & plaintext | 2 days | 1 Go dev | | Beat 2 | Age crypto | 2 days | 1 Go dev + security review | | Beat 3 | Policy/JWT | 2 days | 1 Go dev | | Beat 4 | Observability | 2 days | 1 Go dev | | Beat 5 | CI/CD | 1 day | 1 DevOps | | Beat 6 | WHOOSH integration | 2 days | 1 Go dev | | **Total** | | **11 days** | | ### CHORUS Integration Timeline | Task | Duration | Dependencies | |------|----------|--------------| | SeqThink provider | 2 days | Beat 2 complete | | CHORUS config | 1 day | Provider complete | | Integration testing | 2 days | All complete | | **Total** | **5 days** | | **Grand Total**: ~16 days (3 weeks with buffer) --- ## Success Criteria ### seqthink-age Container - [ ] All Beat 1-6 acceptance criteria met - [ ] Image signed with cosign - [ ] SBOM generated - [ ] Security scan passes (Trivy) - [ ] No plaintext visible in tcpdump - [ ] Load test: 100 rps, p99 < 200ms overhead - [ ] WHOOSH demo: encrypted plan generation ### CHORUS Integration - [ ] Architect agents use Sequential Thinking - [ ] Complex tasks (complexity >= 7) trigger SeqThink - [ ] Structured reasoning stored in TaskResponse - [ ] Fallback to ResetData works on failures - [ ] E2E test: council creates task with reasoning trace --- ## Next Steps 1. **Create `seqthink-age` repository** in GITEA 2. **Implement Beat 1** (skeleton) 3. **Implement Beat 2** (encryption) 4. **Continue through Beats 3-6** 5. **Integrate with CHORUS** agents 6. **Deploy to production** swarm Ready to proceed with implementation?