Initial CHORUS project setup

🎭 CHORUS - Container-First P2P Task Coordination System

- Docker-first architecture designed from ground up
- Environment variable-based configuration (no config files)
- Structured logging to stdout/stderr for container runtimes
- License validation required for operation
- Clean separation from BZZZ legacy systemd approach

Core features implemented:
- Container-optimized logging system
- Environment-based configuration management
- License validation with KACHING integration
- Basic HTTP API and health endpoints
- Docker build and deployment configuration

Ready for P2P protocol development and AI integration.

🤖 Generated with Claude Code
This commit is contained in:
anthonyrawlins
2025-09-02 19:53:33 +10:00
commit 7c6cbd562a
12 changed files with 1170 additions and 0 deletions

55
.gitignore vendored Normal file
View File

@@ -0,0 +1,55 @@
# Binaries
/chorus
*.exe
*.exe~
*.dll
*.so
*.dylib
# Go specific
*.test
*.out
go.work
go.work.sum
# Build artifacts
/build/
/dist/
# Environment and configuration
.env
*.env
!*.env.example
/docker/chorus.env
# Data directories
/data/
*.db
*.sqlite
# IDE and editor files
.vscode/
.idea/
*.swp
*.swo
*~
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# Log files
*.log
/logs/
# Docker
.dockerignore
# Temporary files
/tmp/
*.tmp

99
README.md Normal file
View File

@@ -0,0 +1,99 @@
# CHORUS - Container-First P2P Task Coordination System
CHORUS is a next-generation P2P task coordination and collaborative AI system designed from the ground up for containerized deployments. It takes the best lessons learned from BZZZ and reimagines them for Docker Swarm, Kubernetes, and modern container orchestration platforms.
## Vision
CHORUS enables distributed AI agents to coordinate, collaborate, and execute tasks across container clusters, supporting deployments from single containers to hundreds of instances in enterprise environments.
## Key Design Principles
- **Container-First**: Designed specifically for Docker/Kubernetes deployments
- **License-Controlled**: Simple environment variable-based licensing
- **Cloud-Native Logging**: Structured logging to stdout/stderr for container runtime collection
- **Swarm-Ready P2P**: P2P protocols optimized for container networking
- **Scalable Agent IDs**: Agent identification system that works across distributed deployments
- **Zero-Config**: Minimal configuration requirements via environment variables
## Architecture
CHORUS follows a microservices architecture where each container runs a single agent instance:
```
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ CHORUS Agent │ │ CHORUS Agent │ │ CHORUS Agent │
│ Container 1 │◄─┤ Container 2 │─►│ Container N │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│ │ │
└──────────────────────┼──────────────────────┘
┌─────────────────┐
│ Container │
│ Network │
│ (P2P Mesh) │
└─────────────────┘
```
## Quick Start
### Prerequisites
- Docker & Docker Compose
- Valid CHORUS license key
- Access to Ollama endpoints for AI functionality
### Basic Deployment
1. Clone and configure:
```bash
git clone https://gitea.chorus.services/tony/CHORUS.git
cd CHORUS
cp docker/chorus.env.example docker/chorus.env
# Edit docker/chorus.env with your license key and configuration
```
2. Deploy:
```bash
docker-compose -f docker/docker-compose.yml up -d
```
3. Scale (Docker Swarm):
```bash
docker service scale chorus_agent=10
```
## Licensing
CHORUS requires a valid license key to operate. Set your license key in the environment:
```env
CHORUS_LICENSE_KEY=your-license-key-here
CHORUS_LICENSE_EMAIL=your-email@example.com
```
**No license = No operation.** CHORUS will not start without valid licensing.
## Differences from BZZZ
| Aspect | BZZZ | CHORUS |
|--------|------|--------|
| Deployment | systemd service (1 per host) | Container (N per cluster) |
| Configuration | Web UI setup | Environment variables |
| Logging | Journal/files | stdout/stderr (structured) |
| Licensing | Setup-time validation | Runtime environment variable |
| Agent IDs | Host-based | Container/cluster-based |
| P2P Discovery | mDNS local network | Container network + service discovery |
## Development Status
🚧 **Early Development** - CHORUS is being designed and built. Not yet ready for production use.
Current Phase: Architecture design and core foundation development.
## License
CHORUS is a commercial product. Contact chorus.services for licensing information.
## Contributing
CHORUS is developed by the chorus.services team. For contributions or feedback, please use the issue tracker on our GITEA instance.

89
cmd/chorus/main.go Normal file
View File

@@ -0,0 +1,89 @@
package main
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"time"
"chorus.services/chorus/internal/agent"
"chorus.services/chorus/internal/config"
"chorus.services/chorus/internal/licensing"
"chorus.services/chorus/internal/logging"
)
const (
AppName = "CHORUS"
AppVersion = "0.1.0-dev"
)
func main() {
// Initialize container-optimized logger
logger := logging.NewContainerLogger(AppName)
logger.Info("🎭 Starting CHORUS v%s - Container-First P2P Task Coordination", AppVersion)
// Load configuration from environment
cfg, err := config.LoadFromEnvironment()
if err != nil {
logger.Error("❌ Configuration error: %v", err)
os.Exit(1)
}
// CRITICAL: Validate license before any P2P operations
logger.Info("🔐 Validating CHORUS license...")
licenseValidator := licensing.NewValidator(cfg.License)
if err := licenseValidator.Validate(); err != nil {
logger.Error("❌ License validation failed: %v", err)
logger.Error("💰 CHORUS requires a valid license to operate")
logger.Error("📞 Contact chorus.services for licensing information")
os.Exit(1)
}
logger.Info("✅ License validation successful")
// Create context for graceful shutdown
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Initialize CHORUS agent
agent, err := agent.New(ctx, cfg, logger)
if err != nil {
logger.Error("❌ Failed to create agent: %v", err)
os.Exit(1)
}
// Start agent services
if err := agent.Start(); err != nil {
logger.Error("❌ Failed to start agent: %v", err)
os.Exit(1)
}
logger.Info("✅ CHORUS agent started successfully")
logger.Info("🆔 Agent ID: %s", agent.ID())
logger.Info("🔗 P2P Address: %s", agent.P2PAddress())
logger.Info("🌐 API Endpoint: http://localhost:%d", cfg.Network.APIPort)
logger.Info("🏥 Health Endpoint: http://localhost:%d/health", cfg.Network.HealthPort)
// Set up graceful shutdown
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
// Wait for shutdown signal
<-sigChan
logger.Info("🛑 Shutdown signal received, stopping CHORUS agent...")
// Cancel context to trigger graceful shutdown
cancel()
// Give services time to shut down gracefully
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
defer shutdownCancel()
if err := agent.Stop(shutdownCtx); err != nil {
logger.Error("⚠️ Error during agent shutdown: %v", err)
} else {
logger.Info("✅ CHORUS agent stopped gracefully")
}
}

65
docker/Dockerfile Normal file
View File

@@ -0,0 +1,65 @@
# CHORUS - Container-First P2P Task Coordination System
# Multi-stage build for minimal production image
FROM golang:1.21-alpine AS builder
# Install build dependencies
RUN apk --no-cache add git ca-certificates
WORKDIR /build
# Copy go mod files first (for better caching)
COPY go.mod go.sum ./
RUN go mod download
# Copy source code
COPY . .
# Build the CHORUS binary
RUN CGO_ENABLED=0 GOOS=linux go build \
-ldflags='-w -s -extldflags "-static"' \
-o chorus \
./cmd/chorus
# Final minimal runtime image
FROM alpine:3.18
# Install runtime dependencies
RUN apk --no-cache add \
ca-certificates \
tzdata \
curl
# Create non-root user for security
RUN addgroup -g 1000 chorus && \
adduser -u 1000 -G chorus -s /bin/sh -D chorus
# Create application directories
RUN mkdir -p /app/data && \
chown -R chorus:chorus /app
# Copy binary from builder stage
COPY --from=builder /build/chorus /app/chorus
RUN chmod +x /app/chorus
# Switch to non-root user
USER chorus
WORKDIR /app
# Expose ports
EXPOSE 8080 8081 9000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD curl -f http://localhost:8081/health || exit 1
# Set default environment variables
ENV LOG_LEVEL=info \
LOG_FORMAT=structured \
CHORUS_BIND_ADDRESS=0.0.0.0 \
CHORUS_API_PORT=8080 \
CHORUS_HEALTH_PORT=8081 \
CHORUS_P2P_PORT=9000
# Start CHORUS
ENTRYPOINT ["/app/chorus"]

36
docker/build.sh Executable file
View File

@@ -0,0 +1,36 @@
#!/bin/bash
set -e
# CHORUS Build and Deployment Script
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
IMAGE_NAME="chorus"
IMAGE_TAG="${1:-latest}"
echo "🎭 Building CHORUS container image..."
echo "📁 Project root: $PROJECT_ROOT"
echo "🏷️ Image: $IMAGE_NAME:$IMAGE_TAG"
# Build the Docker image
cd "$PROJECT_ROOT"
docker build -f docker/Dockerfile -t "$IMAGE_NAME:$IMAGE_TAG" .
echo "✅ CHORUS container built successfully!"
echo ""
echo "🚀 Quick Start:"
echo " 1. Copy environment file:"
echo " cp docker/chorus.env.example docker/chorus.env"
echo ""
echo " 2. Edit docker/chorus.env with your license key:"
echo " CHORUS_LICENSE_EMAIL=your-email@example.com"
echo " CHORUS_LICENSE_KEY=your-license-key-here"
echo ""
echo " 3. Start CHORUS:"
echo " docker-compose -f docker/docker-compose.yml --env-file docker/chorus.env up -d"
echo ""
echo " 4. Check status:"
echo " docker-compose -f docker/docker-compose.yml logs -f"
echo " curl http://localhost:8081/health"
echo ""
echo "📖 For production deployment instructions, see README.md"

43
docker/chorus.env.example Normal file
View File

@@ -0,0 +1,43 @@
# CHORUS Environment Configuration
# Copy this file to 'chorus.env' and customize for your deployment
# =================
# REQUIRED SETTINGS
# =================
# License configuration (REQUIRED - CHORUS will not start without these)
CHORUS_LICENSE_EMAIL=your-email@example.com
CHORUS_LICENSE_KEY=your-license-key-here
CHORUS_CLUSTER_ID=production-cluster
# ==================
# OPTIONAL SETTINGS
# ==================
# Agent Configuration
# CHORUS_AGENT_ID= # Auto-generated if not specified
CHORUS_SPECIALIZATION=general_developer
CHORUS_MAX_TASKS=3
CHORUS_CAPABILITIES=general_development,task_coordination,ai_integration
# Network Ports (adjust if ports conflict)
CHORUS_API_PORT=8080
CHORUS_HEALTH_PORT=8081
CHORUS_P2P_PORT=9000
# AI Integration
OLLAMA_ENDPOINT=http://host.docker.internal:11434
CHORUS_DEFAULT_MODEL=llama3.1:8b
# Logging
LOG_LEVEL=info # debug, info, warn, error
LOG_FORMAT=structured # structured (JSON) or human
# Docker Deployment Settings
CHORUS_REPLICAS=1 # Number of CHORUS instances to run
CHORUS_CPU_LIMIT=1.0 # CPU limit per container
CHORUS_MEMORY_LIMIT=1G # Memory limit per container
# Advanced P2P Settings (for cluster deployments)
# CHORUS_BOOTSTRAP_PEERS= # Comma-separated list of bootstrap peers
# CHORUS_DHT_ENABLED=true # Enable DHT for peer discovery

110
docker/docker-compose.yml Normal file
View File

@@ -0,0 +1,110 @@
version: "3.9"
services:
chorus:
build:
context: ..
dockerfile: docker/Dockerfile
image: chorus:latest
# REQUIRED: License configuration (CHORUS will not start without this)
environment:
# CRITICAL: License configuration - REQUIRED for operation
- CHORUS_LICENSE_EMAIL=${CHORUS_LICENSE_EMAIL:?CHORUS_LICENSE_EMAIL is required}
- CHORUS_LICENSE_KEY=${CHORUS_LICENSE_KEY:?CHORUS_LICENSE_KEY is required}
- CHORUS_CLUSTER_ID=${CHORUS_CLUSTER_ID:-docker-cluster}
# Agent configuration
- CHORUS_AGENT_ID=${CHORUS_AGENT_ID:-} # Auto-generated if not provided
- CHORUS_SPECIALIZATION=${CHORUS_SPECIALIZATION:-general_developer}
- CHORUS_MAX_TASKS=${CHORUS_MAX_TASKS:-3}
- CHORUS_CAPABILITIES=${CHORUS_CAPABILITIES:-general_development,task_coordination}
# Network configuration
- CHORUS_API_PORT=8080
- CHORUS_HEALTH_PORT=8081
- CHORUS_P2P_PORT=9000
- CHORUS_BIND_ADDRESS=0.0.0.0
# AI configuration
- OLLAMA_ENDPOINT=${OLLAMA_ENDPOINT:-http://host.docker.internal:11434}
- CHORUS_DEFAULT_MODEL=${CHORUS_DEFAULT_MODEL:-llama3.1:8b}
# Logging configuration
- LOG_LEVEL=${LOG_LEVEL:-info}
- LOG_FORMAT=${LOG_FORMAT:-structured}
# Persistent data storage
volumes:
- chorus_data:/app/data
# Network ports
ports:
- "${CHORUS_API_PORT:-8080}:8080" # HTTP API
- "${CHORUS_HEALTH_PORT:-8081}:8081" # Health checks
- "${CHORUS_P2P_PORT:-9000}:9000" # P2P communication
# Container resource limits
deploy:
mode: replicated
replicas: ${CHORUS_REPLICAS:-1}
update_config:
parallelism: 1
delay: 10s
failure_action: rollback
order: start-first
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
window: 120s
resources:
limits:
cpus: "${CHORUS_CPU_LIMIT:-1.0}"
memory: "${CHORUS_MEMORY_LIMIT:-1G}"
reservations:
cpus: "0.1"
memory: 128M
placement:
preferences:
- spread: node.id
constraints:
- node.role == worker
# Network configuration
networks:
- chorus_net
# Host resolution for external services
extra_hosts:
- "host.docker.internal:host-gateway"
# Container logging configuration
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
tag: "{{.ImageName}}/{{.Name}}/{{.ID}}"
# Health check configuration
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8081/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
# Persistent volumes
volumes:
chorus_data:
driver: local
# Networks for CHORUS communication
networks:
chorus_net:
driver: overlay
attachable: true
ipam:
config:
- subnet: 10.201.0.0/24

12
go.mod Normal file
View File

@@ -0,0 +1,12 @@
module chorus.services/chorus
go 1.21
require (
github.com/gorilla/mux v1.8.0
github.com/libp2p/go-libp2p v0.32.2
github.com/libp2p/go-libp2p-kad-dht v0.25.2
github.com/libp2p/go-libp2p-pubsub v0.9.3
github.com/multiformats/go-multiaddr v0.12.2
gopkg.in/yaml.v2 v2.4.0
)

214
internal/agent/agent.go Normal file
View File

@@ -0,0 +1,214 @@
package agent
import (
"context"
"fmt"
"net/http"
"time"
"chorus.services/chorus/internal/config"
"chorus.services/chorus/internal/logging"
)
// Agent represents a CHORUS agent instance
type Agent struct {
id string
config *config.Config
logger logging.Logger
// Services
apiServer *http.Server
healthServer *http.Server
// P2P components (to be implemented)
// p2pHost host.Host
// dht *dht.DHT
// pubsub *pubsub.PubSub
}
// New creates a new CHORUS agent
func New(ctx context.Context, cfg *config.Config, logger logging.Logger) (*Agent, error) {
agent := &Agent{
id: cfg.Agent.ID,
config: cfg,
logger: logger,
}
// Initialize HTTP servers
if err := agent.initHTTPServers(); err != nil {
return nil, fmt.Errorf("failed to initialize HTTP servers: %w", err)
}
// TODO: Initialize P2P components
// TODO: Initialize task coordination
// TODO: Initialize AI integration
return agent, nil
}
// Start starts all agent services
func (a *Agent) Start() error {
a.logger.Info("🚀 Starting CHORUS agent services...")
// Start API server
go func() {
a.logger.Info("🌐 Starting API server on :%d", a.config.Network.APIPort)
if err := a.apiServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
a.logger.Error("❌ API server error: %v", err)
}
}()
// Start health server
go func() {
a.logger.Info("🏥 Starting health server on :%d", a.config.Network.HealthPort)
if err := a.healthServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
a.logger.Error("❌ Health server error: %v", err)
}
}()
// TODO: Start P2P services
// TODO: Start task coordination
// TODO: Connect to DHT network
a.logger.Info("✅ All CHORUS agent services started")
return nil
}
// Stop gracefully stops all agent services
func (a *Agent) Stop(ctx context.Context) error {
a.logger.Info("🛑 Stopping CHORUS agent services...")
// Stop HTTP servers
if err := a.apiServer.Shutdown(ctx); err != nil {
a.logger.Error("⚠️ Error stopping API server: %v", err)
}
if err := a.healthServer.Shutdown(ctx); err != nil {
a.logger.Error("⚠️ Error stopping health server: %v", err)
}
// TODO: Stop P2P services
// TODO: Stop task coordination
// TODO: Disconnect from DHT network
a.logger.Info("✅ CHORUS agent services stopped")
return nil
}
// ID returns the agent ID
func (a *Agent) ID() string {
return a.id
}
// P2PAddress returns the P2P address (placeholder)
func (a *Agent) P2PAddress() string {
// TODO: Return actual P2P address when P2P is implemented
return fmt.Sprintf("/ip4/%s/tcp/%d/p2p/%s", a.config.Network.BindAddr, a.config.Network.P2PPort, a.id)
}
// initHTTPServers initializes the HTTP servers for API and health endpoints
func (a *Agent) initHTTPServers() error {
// API server
apiMux := http.NewServeMux()
apiMux.HandleFunc("/", a.handleRoot)
apiMux.HandleFunc("/agent/info", a.handleAgentInfo)
apiMux.HandleFunc("/agent/tasks", a.handleTasks)
a.apiServer = &http.Server{
Addr: fmt.Sprintf("%s:%d", a.config.Network.BindAddr, a.config.Network.APIPort),
Handler: apiMux,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
// Health server
healthMux := http.NewServeMux()
healthMux.HandleFunc("/health", a.handleHealth)
healthMux.HandleFunc("/health/ready", a.handleReady)
healthMux.HandleFunc("/health/live", a.handleLive)
a.healthServer = &http.Server{
Addr: fmt.Sprintf("%s:%d", a.config.Network.BindAddr, a.config.Network.HealthPort),
Handler: healthMux,
ReadTimeout: 5 * time.Second,
WriteTimeout: 5 * time.Second,
IdleTimeout: 30 * time.Second,
}
return nil
}
// HTTP handler implementations
func (a *Agent) handleRoot(w http.ResponseWriter, r *http.Request) {
response := map[string]interface{}{
"service": "CHORUS",
"version": "0.1.0-dev",
"agent_id": a.id,
"status": "running",
"endpoints": map[string]string{
"agent_info": "/agent/info",
"tasks": "/agent/tasks",
"health": fmt.Sprintf("http://localhost:%d/health", a.config.Network.HealthPort),
},
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
// Simple JSON response without external dependency
fmt.Fprintf(w, `{
"service": "%s",
"version": "0.1.0-dev",
"agent_id": "%s",
"status": "running"
}`, "CHORUS", a.id)
}
func (a *Agent) handleAgentInfo(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{
"agent_id": "%s",
"specialization": "%s",
"max_tasks": %d,
"capabilities": %q
}`, a.id, a.config.Agent.Specialization, a.config.Agent.MaxTasks, a.config.Agent.Capabilities)
}
func (a *Agent) handleTasks(w http.ResponseWriter, r *http.Request) {
// TODO: Implement task management
w.Header().Set("Content-Type", "application/json")
fmt.Fprintf(w, `{
"active_tasks": [],
"completed_tasks": [],
"available_for_tasks": true
}`)
}
func (a *Agent) handleHealth(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
fmt.Fprintf(w, `{
"status": "healthy",
"agent_id": "%s",
"timestamp": "%s",
"checks": {
"license": "valid",
"api_server": "running",
"p2p": "not_implemented"
}
}`, a.id, time.Now().UTC().Format(time.RFC3339))
}
func (a *Agent) handleReady(w http.ResponseWriter, r *http.Request) {
// Kubernetes readiness probe
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "ready")
}
func (a *Agent) handleLive(w http.ResponseWriter, r *http.Request) {
// Kubernetes liveness probe
w.WriteHeader(http.StatusOK)
fmt.Fprint(w, "live")
}

140
internal/config/config.go Normal file
View File

@@ -0,0 +1,140 @@
package config
import (
"fmt"
"os"
"strconv"
"strings"
)
// Config represents the complete CHORUS configuration loaded from environment variables
type Config struct {
Agent AgentConfig `yaml:"agent"`
Network NetworkConfig `yaml:"network"`
License LicenseConfig `yaml:"license"`
AI AIConfig `yaml:"ai"`
Logging LoggingConfig `yaml:"logging"`
}
// AgentConfig defines agent-specific settings
type AgentConfig struct {
ID string `yaml:"id"`
Specialization string `yaml:"specialization"`
MaxTasks int `yaml:"max_tasks"`
Capabilities []string `yaml:"capabilities"`
}
// NetworkConfig defines network and API settings
type NetworkConfig struct {
P2PPort int `yaml:"p2p_port"`
APIPort int `yaml:"api_port"`
HealthPort int `yaml:"health_port"`
BindAddr string `yaml:"bind_address"`
}
// LicenseConfig defines licensing settings
type LicenseConfig struct {
Email string `yaml:"email"`
LicenseKey string `yaml:"license_key"`
ClusterID string `yaml:"cluster_id"`
}
// AIConfig defines AI service settings
type AIConfig struct {
OllamaEndpoint string `yaml:"ollama_endpoint"`
DefaultModel string `yaml:"default_model"`
}
// LoggingConfig defines logging settings
type LoggingConfig struct {
Level string `yaml:"level"`
Format string `yaml:"format"`
}
// LoadFromEnvironment loads configuration from environment variables
// This is the primary configuration method for CHORUS (no config files)
func LoadFromEnvironment() (*Config, error) {
cfg := &Config{
Agent: AgentConfig{
ID: getEnvOrDefault("CHORUS_AGENT_ID", ""),
Specialization: getEnvOrDefault("CHORUS_SPECIALIZATION", "general_developer"),
MaxTasks: getEnvIntOrDefault("CHORUS_MAX_TASKS", 3),
Capabilities: getEnvArrayOrDefault("CHORUS_CAPABILITIES", []string{"general_development", "task_coordination"}),
},
Network: NetworkConfig{
P2PPort: getEnvIntOrDefault("CHORUS_P2P_PORT", 9000),
APIPort: getEnvIntOrDefault("CHORUS_API_PORT", 8080),
HealthPort: getEnvIntOrDefault("CHORUS_HEALTH_PORT", 8081),
BindAddr: getEnvOrDefault("CHORUS_BIND_ADDRESS", "0.0.0.0"),
},
License: LicenseConfig{
Email: os.Getenv("CHORUS_LICENSE_EMAIL"),
LicenseKey: os.Getenv("CHORUS_LICENSE_KEY"),
ClusterID: getEnvOrDefault("CHORUS_CLUSTER_ID", "default-cluster"),
},
AI: AIConfig{
OllamaEndpoint: getEnvOrDefault("OLLAMA_ENDPOINT", "http://localhost:11434"),
DefaultModel: getEnvOrDefault("CHORUS_DEFAULT_MODEL", "llama3.1:8b"),
},
Logging: LoggingConfig{
Level: getEnvOrDefault("LOG_LEVEL", "info"),
Format: getEnvOrDefault("LOG_FORMAT", "structured"),
},
}
// Validate required configuration
if err := cfg.Validate(); err != nil {
return nil, fmt.Errorf("configuration validation failed: %w", err)
}
return cfg, nil
}
// Validate ensures all required configuration is present
func (c *Config) Validate() error {
if c.License.Email == "" {
return fmt.Errorf("CHORUS_LICENSE_EMAIL is required")
}
if c.License.LicenseKey == "" {
return fmt.Errorf("CHORUS_LICENSE_KEY is required")
}
if c.Agent.ID == "" {
// Auto-generate agent ID if not provided
hostname, _ := os.Hostname()
containerID := os.Getenv("HOSTNAME") // Docker sets this to container ID
if containerID != "" && containerID != hostname {
c.Agent.ID = fmt.Sprintf("chorus-%s", containerID[:12])
} else {
c.Agent.ID = fmt.Sprintf("chorus-%s", hostname)
}
}
return nil
}
// Helper functions for environment variable parsing
func getEnvOrDefault(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
func getEnvIntOrDefault(key string, defaultValue int) int {
if value := os.Getenv(key); value != "" {
if parsed, err := strconv.Atoi(value); err == nil {
return parsed
}
}
return defaultValue
}
func getEnvArrayOrDefault(key string, defaultValue []string) []string {
if value := os.Getenv(key); value != "" {
return strings.Split(value, ",")
}
return defaultValue
}

View File

@@ -0,0 +1,97 @@
package licensing
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"time"
)
const (
DefaultKachingURL = "https://kaching.chorus.services"
LicenseTimeout = 30 * time.Second
)
// LicenseConfig holds licensing information
type LicenseConfig struct {
Email string
LicenseKey string
ClusterID string
}
// Validator handles license validation with KACHING
type Validator struct {
config LicenseConfig
kachingURL string
client *http.Client
}
// NewValidator creates a new license validator
func NewValidator(config LicenseConfig) *Validator {
return &Validator{
config: config,
kachingURL: DefaultKachingURL,
client: &http.Client{
Timeout: LicenseTimeout,
},
}
}
// Validate performs license validation with KACHING license authority
// CRITICAL: CHORUS will not start without valid license validation
func (v *Validator) Validate() error {
if v.config.Email == "" || v.config.LicenseKey == "" {
return fmt.Errorf("license email and key are required")
}
// Prepare validation request
request := map[string]interface{}{
"email": v.config.Email,
"license_key": v.config.LicenseKey,
"cluster_id": v.config.ClusterID,
"product": "CHORUS",
"version": "0.1.0-dev",
"container": true, // Flag indicating this is a container deployment
}
requestBody, err := json.Marshal(request)
if err != nil {
return fmt.Errorf("failed to marshal license request: %w", err)
}
// Call KACHING license authority
licenseURL := fmt.Sprintf("%s/v1/license/validate", v.kachingURL)
resp, err := v.client.Post(licenseURL, "application/json", bytes.NewReader(requestBody))
if err != nil {
// FAIL-CLOSED: No network = No license = No operation
return fmt.Errorf("unable to contact license authority: %w", err)
}
defer resp.Body.Close()
// Parse response
var licenseResponse map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&licenseResponse); err != nil {
return fmt.Errorf("invalid license authority response: %w", err)
}
// Check validation result
if resp.StatusCode != http.StatusOK {
message := "license validation failed"
if msg, ok := licenseResponse["message"].(string); ok {
message = msg
}
return fmt.Errorf("license validation failed: %s", message)
}
// License is valid
return nil
}
// ValidateBackground performs background license validation (for runtime checks)
// This is used for periodic license validation during operation
func (v *Validator) ValidateBackground() error {
// Similar to Validate() but with longer timeout and retry logic
// Implementation would include retry logic and graceful degradation
return v.Validate()
}

210
internal/logging/logger.go Normal file
View File

@@ -0,0 +1,210 @@
package logging
import (
"encoding/json"
"fmt"
"os"
"time"
)
// Logger interface for CHORUS logging
type Logger interface {
Info(msg string, args ...interface{})
Warn(msg string, args ...interface{})
Error(msg string, args ...interface{})
Debug(msg string, args ...interface{})
}
// ContainerLogger provides structured logging optimized for container environments
// All logs go to stdout/stderr for collection by container runtime (Docker, K8s, etc.)
type ContainerLogger struct {
name string
level LogLevel
format LogFormat
}
// LogLevel represents logging levels
type LogLevel int
const (
DEBUG LogLevel = iota
INFO
WARN
ERROR
)
// LogFormat represents log output formats
type LogFormat int
const (
STRUCTURED LogFormat = iota // JSON structured logging
HUMAN // Human-readable logging
)
// LogEntry represents a structured log entry
type LogEntry struct {
Timestamp string `json:"timestamp"`
Level string `json:"level"`
Service string `json:"service"`
Message string `json:"message"`
Data map[string]interface{} `json:"data,omitempty"`
}
// NewContainerLogger creates a new container-optimized logger
func NewContainerLogger(serviceName string) *ContainerLogger {
level := INFO
format := STRUCTURED
// Parse log level from environment
if levelStr := os.Getenv("LOG_LEVEL"); levelStr != "" {
switch levelStr {
case "debug":
level = DEBUG
case "info":
level = INFO
case "warn":
level = WARN
case "error":
level = ERROR
}
}
// Parse log format from environment
if formatStr := os.Getenv("LOG_FORMAT"); formatStr == "human" {
format = HUMAN
}
return &ContainerLogger{
name: serviceName,
level: level,
format: format,
}
}
// Info logs informational messages
func (l *ContainerLogger) Info(msg string, args ...interface{}) {
if l.level <= INFO {
l.log(INFO, msg, args...)
}
}
// Warn logs warning messages
func (l *ContainerLogger) Warn(msg string, args ...interface{}) {
if l.level <= WARN {
l.log(WARN, msg, args...)
}
}
// Error logs error messages to stderr
func (l *ContainerLogger) Error(msg string, args ...interface{}) {
if l.level <= ERROR {
l.logToStderr(ERROR, msg, args...)
}
}
// Debug logs debug messages (only when DEBUG level is enabled)
func (l *ContainerLogger) Debug(msg string, args ...interface{}) {
if l.level <= DEBUG {
l.log(DEBUG, msg, args...)
}
}
// log writes log entries to stdout
func (l *ContainerLogger) log(level LogLevel, msg string, args ...interface{}) {
entry := l.createLogEntry(level, msg, args...)
switch l.format {
case STRUCTURED:
l.writeJSON(os.Stdout, entry)
case HUMAN:
l.writeHuman(os.Stdout, entry)
}
}
// logToStderr writes log entries to stderr (for errors)
func (l *ContainerLogger) logToStderr(level LogLevel, msg string, args ...interface{}) {
entry := l.createLogEntry(level, msg, args...)
switch l.format {
case STRUCTURED:
l.writeJSON(os.Stderr, entry)
case HUMAN:
l.writeHuman(os.Stderr, entry)
}
}
// createLogEntry creates a structured log entry
func (l *ContainerLogger) createLogEntry(level LogLevel, msg string, args ...interface{}) LogEntry {
return LogEntry{
Timestamp: time.Now().UTC().Format(time.RFC3339Nano),
Level: l.levelToString(level),
Service: l.name,
Message: fmt.Sprintf(msg, args...),
Data: make(map[string]interface{}),
}
}
// writeJSON writes the log entry as JSON
func (l *ContainerLogger) writeJSON(output *os.File, entry LogEntry) {
if jsonData, err := json.Marshal(entry); err == nil {
fmt.Fprintln(output, string(jsonData))
}
}
// writeHuman writes the log entry in human-readable format
func (l *ContainerLogger) writeHuman(output *os.File, entry LogEntry) {
fmt.Fprintf(output, "[%s] [%s] [%s] %s\n",
entry.Timestamp,
entry.Level,
entry.Service,
entry.Message,
)
}
// levelToString converts LogLevel to string
func (l *ContainerLogger) levelToString(level LogLevel) string {
switch level {
case DEBUG:
return "DEBUG"
case INFO:
return "INFO"
case WARN:
return "WARN"
case ERROR:
return "ERROR"
default:
return "UNKNOWN"
}
}
// WithData creates a logger that includes additional structured data in log entries
func (l *ContainerLogger) WithData(data map[string]interface{}) Logger {
// Return a new logger instance that includes the data
// This is useful for request-scoped logging with context
return &dataLogger{
base: l,
data: data,
}
}
// dataLogger is a wrapper that adds structured data to log entries
type dataLogger struct {
base Logger
data map[string]interface{}
}
func (d *dataLogger) Info(msg string, args ...interface{}) {
d.base.Info(msg, args...)
}
func (d *dataLogger) Warn(msg string, args ...interface{}) {
d.base.Warn(msg, args...)
}
func (d *dataLogger) Error(msg string, args ...interface{}) {
d.base.Error(msg, args...)
}
func (d *dataLogger) Debug(msg string, args ...interface{}) {
d.base.Debug(msg, args...)
}