Major security, observability, and configuration improvements:
## Security Hardening
- Implemented configurable CORS (no more wildcards)
- Added comprehensive auth middleware for admin endpoints
- Enhanced webhook HMAC validation
- Added input validation and rate limiting
- Security headers and CSP policies
## Configuration Management
- Made N8N webhook URL configurable (WHOOSH_N8N_BASE_URL)
- Replaced all hardcoded endpoints with environment variables
- Added feature flags for LLM vs heuristic composition
- Gitea fetch hardening with EAGER_FILTER and FULL_RESCAN options
## API Completeness
- Implemented GetCouncilComposition function
- Added GET /api/v1/councils/{id} endpoint
- Council artifacts API (POST/GET /api/v1/councils/{id}/artifacts)
- /admin/health/details endpoint with component status
- Database lookup for repository URLs (no hardcoded fallbacks)
## Observability & Performance
- Added OpenTelemetry distributed tracing with goal/pulse correlation
- Performance optimization database indexes
- Comprehensive health monitoring
- Enhanced logging and error handling
## Infrastructure
- Production-ready P2P discovery (replaces mock implementation)
- Removed unused Redis configuration
- Enhanced Docker Swarm integration
- Added migration files for performance indexes
## Code Quality
- Comprehensive input validation
- Graceful error handling and failsafe fallbacks
- Backwards compatibility maintained
- Following security best practices
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
371 lines
12 KiB
Go
371 lines
12 KiB
Go
package council
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
"github.com/rs/zerolog/log"
|
|
"go.opentelemetry.io/otel/attribute"
|
|
|
|
"github.com/chorus-services/whoosh/internal/tracing"
|
|
)
|
|
|
|
// CouncilComposer manages the formation and orchestration of project kickoff councils
|
|
type CouncilComposer struct {
|
|
db *pgxpool.Pool
|
|
ctx context.Context
|
|
cancel context.CancelFunc
|
|
}
|
|
|
|
// NewCouncilComposer creates a new council composer service
|
|
func NewCouncilComposer(db *pgxpool.Pool) *CouncilComposer {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
return &CouncilComposer{
|
|
db: db,
|
|
ctx: ctx,
|
|
cancel: cancel,
|
|
}
|
|
}
|
|
|
|
// Close shuts down the council composer
|
|
func (cc *CouncilComposer) Close() error {
|
|
cc.cancel()
|
|
return nil
|
|
}
|
|
|
|
// FormCouncil creates a council composition for a project kickoff
|
|
func (cc *CouncilComposer) FormCouncil(ctx context.Context, request *CouncilFormationRequest) (*CouncilComposition, error) {
|
|
ctx, span := tracing.StartCouncilSpan(ctx, "form_council", "")
|
|
defer span.End()
|
|
|
|
startTime := time.Now()
|
|
councilID := uuid.New()
|
|
|
|
// Add tracing attributes
|
|
span.SetAttributes(
|
|
attribute.String("council.id", councilID.String()),
|
|
attribute.String("project.name", request.ProjectName),
|
|
attribute.String("repository.name", request.Repository),
|
|
attribute.String("project.brief", request.ProjectBrief),
|
|
)
|
|
|
|
// Add goal.id and pulse.id if available in the request
|
|
if request.GoalID != "" {
|
|
span.SetAttributes(attribute.String("goal.id", request.GoalID))
|
|
}
|
|
if request.PulseID != "" {
|
|
span.SetAttributes(attribute.String("pulse.id", request.PulseID))
|
|
}
|
|
|
|
log.Info().
|
|
Str("council_id", councilID.String()).
|
|
Str("project_name", request.ProjectName).
|
|
Str("repository", request.Repository).
|
|
Msg("🎭 Forming project kickoff council")
|
|
|
|
// Create core council agents (always required)
|
|
coreAgents := make([]CouncilAgent, len(CoreCouncilRoles))
|
|
for i, roleName := range CoreCouncilRoles {
|
|
agentID := fmt.Sprintf("council-%s-%s", strings.ReplaceAll(request.ProjectName, " ", "-"), roleName)
|
|
coreAgents[i] = CouncilAgent{
|
|
AgentID: agentID,
|
|
RoleName: roleName,
|
|
AgentName: cc.formatRoleName(roleName),
|
|
Required: true,
|
|
Deployed: false,
|
|
Status: "pending",
|
|
}
|
|
}
|
|
|
|
// Determine optional agents based on project characteristics
|
|
optionalAgents := cc.selectOptionalAgents(request)
|
|
|
|
// Create council composition
|
|
composition := &CouncilComposition{
|
|
CouncilID: councilID,
|
|
ProjectName: request.ProjectName,
|
|
CoreAgents: coreAgents,
|
|
OptionalAgents: optionalAgents,
|
|
CreatedAt: startTime,
|
|
Status: "forming",
|
|
}
|
|
|
|
// Store council composition in database
|
|
err := cc.storeCouncilComposition(ctx, composition, request)
|
|
if err != nil {
|
|
tracing.SetSpanError(span, err)
|
|
span.SetAttributes(attribute.String("council.formation.status", "failed"))
|
|
return nil, fmt.Errorf("failed to store council composition: %w", err)
|
|
}
|
|
|
|
// Add success metrics to span
|
|
span.SetAttributes(
|
|
attribute.Int("council.core_agents.count", len(coreAgents)),
|
|
attribute.Int("council.optional_agents.count", len(optionalAgents)),
|
|
attribute.Int64("council.formation.duration_ms", time.Since(startTime).Milliseconds()),
|
|
attribute.String("council.formation.status", "completed"),
|
|
)
|
|
|
|
log.Info().
|
|
Str("council_id", councilID.String()).
|
|
Int("core_agents", len(coreAgents)).
|
|
Int("optional_agents", len(optionalAgents)).
|
|
Dur("formation_time", time.Since(startTime)).
|
|
Msg("✅ Council composition formed")
|
|
|
|
return composition, nil
|
|
}
|
|
|
|
// selectOptionalAgents determines which optional council agents should be included
|
|
func (cc *CouncilComposer) selectOptionalAgents(request *CouncilFormationRequest) []CouncilAgent {
|
|
var selectedAgents []CouncilAgent
|
|
|
|
// Analyze project brief and characteristics to determine needed optional roles
|
|
brief := strings.ToLower(request.ProjectBrief)
|
|
|
|
// Data/AI projects
|
|
if strings.Contains(brief, "ai") || strings.Contains(brief, "machine learning") ||
|
|
strings.Contains(brief, "data") || strings.Contains(brief, "analytics") {
|
|
selectedAgents = append(selectedAgents, cc.createOptionalAgent("data-ai-architect", request.ProjectName))
|
|
}
|
|
|
|
// Privacy/compliance sensitive projects
|
|
if strings.Contains(brief, "privacy") || strings.Contains(brief, "personal data") ||
|
|
strings.Contains(brief, "gdpr") || strings.Contains(brief, "compliance") {
|
|
selectedAgents = append(selectedAgents, cc.createOptionalAgent("privacy-data-governance-officer", request.ProjectName))
|
|
}
|
|
|
|
// Regulated industries
|
|
if strings.Contains(brief, "healthcare") || strings.Contains(brief, "finance") ||
|
|
strings.Contains(brief, "banking") || strings.Contains(brief, "regulated") {
|
|
selectedAgents = append(selectedAgents, cc.createOptionalAgent("compliance-legal-liaison", request.ProjectName))
|
|
}
|
|
|
|
// Performance-critical systems
|
|
if strings.Contains(brief, "performance") || strings.Contains(brief, "high-load") ||
|
|
strings.Contains(brief, "scale") || strings.Contains(brief, "benchmark") {
|
|
selectedAgents = append(selectedAgents, cc.createOptionalAgent("performance-benchmarking-analyst", request.ProjectName))
|
|
}
|
|
|
|
// User-facing applications
|
|
if strings.Contains(brief, "user interface") || strings.Contains(brief, "ui") ||
|
|
strings.Contains(brief, "ux") || strings.Contains(brief, "frontend") {
|
|
selectedAgents = append(selectedAgents, cc.createOptionalAgent("ui-ux-designer", request.ProjectName))
|
|
}
|
|
|
|
// Mobile applications
|
|
if strings.Contains(brief, "mobile") || strings.Contains(brief, "ios") ||
|
|
strings.Contains(brief, "android") || strings.Contains(brief, "app store") {
|
|
selectedAgents = append(selectedAgents, cc.createOptionalAgent("ios-macos-developer", request.ProjectName))
|
|
}
|
|
|
|
// Games or graphics-intensive applications
|
|
if strings.Contains(brief, "game") || strings.Contains(brief, "graphics") ||
|
|
strings.Contains(brief, "rendering") || strings.Contains(brief, "3d") {
|
|
selectedAgents = append(selectedAgents, cc.createOptionalAgent("engine-programmer", request.ProjectName))
|
|
}
|
|
|
|
// Integration-heavy projects
|
|
if strings.Contains(brief, "integration") || strings.Contains(brief, "api") ||
|
|
strings.Contains(brief, "microservice") || strings.Contains(brief, "third-party") {
|
|
selectedAgents = append(selectedAgents, cc.createOptionalAgent("integration-architect", request.ProjectName))
|
|
}
|
|
|
|
// Cost-sensitive or enterprise projects
|
|
if strings.Contains(brief, "budget") || strings.Contains(brief, "cost") ||
|
|
strings.Contains(brief, "enterprise") || strings.Contains(brief, "licensing") {
|
|
selectedAgents = append(selectedAgents, cc.createOptionalAgent("cost-licensing-steward", request.ProjectName))
|
|
}
|
|
|
|
return selectedAgents
|
|
}
|
|
|
|
// createOptionalAgent creates an optional council agent
|
|
func (cc *CouncilComposer) createOptionalAgent(roleName, projectName string) CouncilAgent {
|
|
agentID := fmt.Sprintf("council-%s-%s", strings.ReplaceAll(projectName, " ", "-"), roleName)
|
|
return CouncilAgent{
|
|
AgentID: agentID,
|
|
RoleName: roleName,
|
|
AgentName: cc.formatRoleName(roleName),
|
|
Required: false,
|
|
Deployed: false,
|
|
Status: "pending",
|
|
}
|
|
}
|
|
|
|
// formatRoleName converts role key to human-readable name
|
|
func (cc *CouncilComposer) formatRoleName(roleName string) string {
|
|
// Convert kebab-case to Title Case
|
|
parts := strings.Split(roleName, "-")
|
|
for i, part := range parts {
|
|
parts[i] = strings.Title(part)
|
|
}
|
|
return strings.Join(parts, " ")
|
|
}
|
|
|
|
// storeCouncilComposition stores the council composition in the database
|
|
func (cc *CouncilComposer) storeCouncilComposition(ctx context.Context, composition *CouncilComposition, request *CouncilFormationRequest) error {
|
|
// Store council metadata
|
|
councilQuery := `
|
|
INSERT INTO councils (id, project_name, repository, project_brief, status, created_at, task_id, issue_id, external_url, metadata)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
|
`
|
|
|
|
metadataJSON, _ := json.Marshal(request.Metadata)
|
|
|
|
_, err := cc.db.Exec(ctx, councilQuery,
|
|
composition.CouncilID,
|
|
composition.ProjectName,
|
|
request.Repository,
|
|
request.ProjectBrief,
|
|
composition.Status,
|
|
composition.CreatedAt,
|
|
request.TaskID,
|
|
request.IssueID,
|
|
request.ExternalURL,
|
|
metadataJSON,
|
|
)
|
|
|
|
if err != nil {
|
|
return fmt.Errorf("failed to store council metadata: %w", err)
|
|
}
|
|
|
|
// Store council agents
|
|
for _, agent := range composition.CoreAgents {
|
|
err = cc.storeCouncilAgent(ctx, composition.CouncilID, agent)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to store core agent %s: %w", agent.AgentID, err)
|
|
}
|
|
}
|
|
|
|
for _, agent := range composition.OptionalAgents {
|
|
err = cc.storeCouncilAgent(ctx, composition.CouncilID, agent)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to store optional agent %s: %w", agent.AgentID, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// storeCouncilAgent stores a single council agent in the database
|
|
func (cc *CouncilComposer) storeCouncilAgent(ctx context.Context, councilID uuid.UUID, agent CouncilAgent) error {
|
|
query := `
|
|
INSERT INTO council_agents (council_id, agent_id, role_name, agent_name, required, deployed, status, created_at)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
|
|
`
|
|
|
|
_, err := cc.db.Exec(ctx, query,
|
|
councilID,
|
|
agent.AgentID,
|
|
agent.RoleName,
|
|
agent.AgentName,
|
|
agent.Required,
|
|
agent.Deployed,
|
|
agent.Status,
|
|
)
|
|
|
|
return err
|
|
}
|
|
|
|
// GetCouncilComposition retrieves a council composition by ID
|
|
func (cc *CouncilComposer) GetCouncilComposition(ctx context.Context, councilID uuid.UUID) (*CouncilComposition, error) {
|
|
// First, get the council metadata
|
|
councilQuery := `
|
|
SELECT id, project_name, status, created_at
|
|
FROM councils
|
|
WHERE id = $1
|
|
`
|
|
|
|
var composition CouncilComposition
|
|
var status string
|
|
var createdAt time.Time
|
|
|
|
err := cc.db.QueryRow(ctx, councilQuery, councilID).Scan(
|
|
&composition.CouncilID,
|
|
&composition.ProjectName,
|
|
&status,
|
|
&createdAt,
|
|
)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query council: %w", err)
|
|
}
|
|
|
|
composition.Status = status
|
|
composition.CreatedAt = createdAt
|
|
|
|
// Get all agents for this council
|
|
agentQuery := `
|
|
SELECT agent_id, role_name, agent_name, required, deployed, status, deployed_at
|
|
FROM council_agents
|
|
WHERE council_id = $1
|
|
ORDER BY required DESC, role_name ASC
|
|
`
|
|
|
|
rows, err := cc.db.Query(ctx, agentQuery, councilID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query council agents: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
// Separate core and optional agents
|
|
var coreAgents []CouncilAgent
|
|
var optionalAgents []CouncilAgent
|
|
|
|
for rows.Next() {
|
|
var agent CouncilAgent
|
|
var deployedAt *time.Time
|
|
|
|
err := rows.Scan(
|
|
&agent.AgentID,
|
|
&agent.RoleName,
|
|
&agent.AgentName,
|
|
&agent.Required,
|
|
&agent.Deployed,
|
|
&agent.Status,
|
|
&deployedAt,
|
|
)
|
|
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to scan agent row: %w", err)
|
|
}
|
|
|
|
agent.DeployedAt = deployedAt
|
|
|
|
if agent.Required {
|
|
coreAgents = append(coreAgents, agent)
|
|
} else {
|
|
optionalAgents = append(optionalAgents, agent)
|
|
}
|
|
}
|
|
|
|
if err = rows.Err(); err != nil {
|
|
return nil, fmt.Errorf("error iterating agent rows: %w", err)
|
|
}
|
|
|
|
composition.CoreAgents = coreAgents
|
|
composition.OptionalAgents = optionalAgents
|
|
|
|
log.Info().
|
|
Str("council_id", councilID.String()).
|
|
Str("project_name", composition.ProjectName).
|
|
Int("core_agents", len(coreAgents)).
|
|
Int("optional_agents", len(optionalAgents)).
|
|
Msg("Retrieved council composition")
|
|
|
|
return &composition, nil
|
|
}
|
|
|
|
// UpdateCouncilStatus updates the status of a council
|
|
func (cc *CouncilComposer) UpdateCouncilStatus(ctx context.Context, councilID uuid.UUID, status string) error {
|
|
query := `UPDATE councils SET status = $1, updated_at = NOW() WHERE id = $2`
|
|
_, err := cc.db.Exec(ctx, query, status, councilID)
|
|
return err
|
|
} |