Implement Beat 1: Sequential Thinking Age-Encrypted Wrapper (Skeleton)

This commit completes Beat 1 of the SequentialThinkingForCHORUS implementation,
providing a functional plaintext skeleton for the age-encrypted wrapper.

## Deliverables

### 1. Main Wrapper Entry Point
- `cmd/seqthink-wrapper/main.go`: HTTP server on :8443
- Configuration loading from environment variables
- Graceful shutdown handling
- MCP server readiness checking with timeout

### 2. MCP Client Package
- `pkg/seqthink/mcpclient/client.go`: HTTP client for MCP server
- Communicates with MCP server on localhost:8000
- Health check endpoint
- Tool call endpoint with 120s timeout

### 3. Proxy Server Package
- `pkg/seqthink/proxy/server.go`: HTTP handlers for wrapper
- Health and readiness endpoints
- Tool call proxy (plaintext for Beat 1)
- SSE endpoint placeholder
- Metrics endpoint integration

### 4. Observability Package
- `pkg/seqthink/observability/logger.go`: Structured logging with zerolog
- `pkg/seqthink/observability/metrics.go`: Prometheus metrics
- Counters for requests, errors, decrypt/encrypt failures, policy denials
- Request duration histogram

### 5. Docker Infrastructure
- `deploy/seqthink/Dockerfile`: Multi-stage build
- `deploy/seqthink/entrypoint.sh`: Startup orchestration
- `deploy/seqthink/mcp_stub.py`: Minimal MCP server for testing

### 6. Build System Integration
- Updated `Makefile` with `build-seqthink` target
- Uses GOWORK=off and -mod=mod for clean builds
- `docker-seqthink` target for container builds

## Testing

Successfully builds with:
```
make build-seqthink
```

Binary successfully starts and waits for MCP server connection.

## Next Steps

Beat 2 will add:
- Age encryption/decryption (pkg/seqthink/ageio)
- Content-Type: application/age enforcement
- SSE streaming with encrypted frames
- Golden tests for crypto round-trips

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
anthonyrawlins
2025-10-13 08:35:43 +11:00
parent dd8be05e9c
commit 3ce9811826
11 changed files with 2424 additions and 9 deletions

View File

@@ -0,0 +1,86 @@
# Sequential Thinking Age-Encrypted Wrapper
# Beat 1: Plaintext skeleton - encryption added in Beat 2
# Stage 1: Build Go wrapper
FROM golang:1.23-alpine AS go-builder
WORKDIR /build
# Install build dependencies
RUN apk add --no-cache git make
# Copy go mod files
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
# Build the wrapper binary
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo \
-ldflags '-w -s -extldflags "-static"' \
-o seqthink-wrapper \
./cmd/seqthink-wrapper
# Stage 2: Build Python MCP server
FROM python:3.11-slim AS python-builder
WORKDIR /mcp
# Install Sequential Thinking MCP server dependencies
# Note: For Beat 1, we'll use a minimal Python HTTP server
# Full MCP server integration happens in later beats
RUN pip install --no-cache-dir \
fastapi==0.109.0 \
uvicorn[standard]==0.27.0 \
pydantic==2.5.3
# Copy MCP server stub (to be replaced with real implementation)
COPY deploy/seqthink/mcp_stub.py /mcp/server.py
# Stage 3: Runtime
FROM debian:bookworm-slim
# Install runtime dependencies
RUN apt-get update && \
apt-get install -y --no-install-recommends \
ca-certificates \
python3 \
python3-pip && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
# Install Python packages in runtime
RUN pip3 install --no-cache-dir --break-system-packages \
fastapi==0.109.0 \
uvicorn[standard]==0.27.0 \
pydantic==2.5.3
# Create non-root user
RUN useradd -r -u 1000 -m -s /bin/bash seqthink
# Copy binaries
COPY --from=go-builder /build/seqthink-wrapper /usr/local/bin/
COPY --from=python-builder /mcp/server.py /opt/mcp/server.py
# Copy entrypoint
COPY deploy/seqthink/entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh
# Setup directories
RUN mkdir -p /etc/seqthink /var/log/seqthink && \
chown -R seqthink:seqthink /etc/seqthink /var/log/seqthink
# Switch to non-root user
USER seqthink
WORKDIR /home/seqthink
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:8443/health || exit 1
# Expose wrapper port (MCP server on 127.0.0.1:8000 is internal only)
EXPOSE 8443
# Run entrypoint
ENTRYPOINT ["/entrypoint.sh"]

View File

@@ -0,0 +1,27 @@
#!/bin/bash
set -e
echo "🚀 Starting Sequential Thinking Age Wrapper (Beat 1)"
# Start MCP server on loopback
echo "📡 Starting MCP server on 127.0.0.1:8000..."
python3 /opt/mcp/server.py &
MCP_PID=$!
# Wait for MCP server to be ready
echo "⏳ Waiting for MCP server to be ready..."
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 wrapper
echo "🔐 Starting wrapper on :8443..."
exec seqthink-wrapper

View File

@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""
Sequential Thinking MCP Server Stub (Beat 1)
This is a minimal implementation for testing the wrapper infrastructure.
In later beats, this will be replaced with the full Sequential Thinking MCP server.
"""
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Dict, Any, Optional
import uvicorn
app = FastAPI(title="Sequential Thinking MCP Server Stub")
class ToolRequest(BaseModel):
tool: str
payload: Dict[str, Any]
class ToolResponse(BaseModel):
result: Optional[Any] = None
error: Optional[str] = None
@app.get("/health")
async def health():
"""Health check endpoint"""
return {"status": "ok"}
@app.post("/mcp/tool")
async def call_tool(request: ToolRequest) -> ToolResponse:
"""
Tool call endpoint - stub implementation
In Beat 1, this just echoes back the request to verify the wrapper works.
Later beats will implement the actual Sequential Thinking logic.
"""
if request.tool != "mcp__sequential-thinking__sequentialthinking":
return ToolResponse(
error=f"Unknown tool: {request.tool}"
)
# Stub response for Sequential Thinking tool
payload = request.payload
thought_number = payload.get("thoughtNumber", 1)
total_thoughts = payload.get("totalThoughts", 5)
thought = payload.get("thought", "")
next_thought_needed = payload.get("nextThoughtNeeded", True)
return ToolResponse(
result={
"thoughtNumber": thought_number,
"totalThoughts": total_thoughts,
"thought": thought,
"nextThoughtNeeded": next_thought_needed,
"message": "Beat 1 stub - Sequential Thinking not yet implemented"
}
)
if __name__ == "__main__":
uvicorn.run(
app,
host="127.0.0.1",
port=8000,
log_level="info"
)