Files
CHORUS/pkg/integration/decision_publisher.go
anthonyrawlins 9bdcbe0447 Integrate BACKBEAT SDK and resolve KACHING license validation
Major integrations and fixes:
- Added BACKBEAT SDK integration for P2P operation timing
- Implemented beat-aware status tracking for distributed operations
- Added Docker secrets support for secure license management
- Resolved KACHING license validation via HTTPS/TLS
- Updated docker-compose configuration for clean stack deployment
- Disabled rollback policies to prevent deployment failures
- Added license credential storage (CHORUS-DEV-MULTI-001)

Technical improvements:
- BACKBEAT P2P operation tracking with phase management
- Enhanced configuration system with file-based secrets
- Improved error handling for license validation
- Clean separation of KACHING and CHORUS deployment stacks

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-06 07:56:26 +10:00

313 lines
9.7 KiB
Go

package integration
import (
"context"
"crypto/sha256"
"encoding/json"
"fmt"
"log"
"time"
"chorus/pkg/dht"
"chorus/pkg/ucxl"
)
// DecisionPublisher handles publishing decisions to encrypted DHT storage
type DecisionPublisher struct {
dhtStorage *dht.EncryptedDHTStorage
enabled bool
}
// Decision represents a decision made from a HMMM discussion
type Decision struct {
Type string `json:"type"` // Event type (approval, warning, etc.)
Content string `json:"content"` // Human-readable decision content
Participants []string `json:"participants"` // Who participated in the decision
ConsensusLevel float64 `json:"consensus_level"` // Strength of consensus (0.0-1.0)
Timestamp time.Time `json:"timestamp"` // When decision was made
DiscussionID string `json:"discussion_id"` // Source discussion ID
Confidence float64 `json:"confidence"` // AI confidence in decision extraction
Metadata map[string]interface{} `json:"metadata"` // Additional decision metadata
UCXLAddress string `json:"ucxl_address"` // Associated UCXL address
ExpiresAt *time.Time `json:"expires_at,omitempty"` // Optional expiration
Tags []string `json:"tags"` // Decision tags
RelatedDecisions []string `json:"related_decisions,omitempty"` // Related decision hashes
}
// PublishResult contains the result of publishing a decision
type PublishResult struct {
UCXLAddress string `json:"ucxl_address"`
DHTHash string `json:"dht_hash"`
Success bool `json:"success"`
PublishedAt time.Time `json:"published_at"`
Error string `json:"error,omitempty"`
}
// NewDecisionPublisher creates a new decision publisher
func NewDecisionPublisher(dhtStorage *dht.EncryptedDHTStorage, enabled bool) *DecisionPublisher {
return &DecisionPublisher{
dhtStorage: dhtStorage,
enabled: enabled,
}
}
// PublishDecision publishes a decision to the encrypted DHT storage
func (dp *DecisionPublisher) PublishDecision(ctx context.Context, ucxlAddr *ucxl.Address, decision *Decision) (*PublishResult, error) {
result := &PublishResult{
UCXLAddress: ucxlAddr.String(),
PublishedAt: time.Now(),
}
if !dp.enabled {
result.Error = "Decision publishing is disabled"
log.Printf("📤 Decision publishing skipped (disabled): %s", ucxlAddr.String())
return result, nil
}
// Enrich decision with UCXL address
decision.UCXLAddress = ucxlAddr.String()
// Serialize decision to JSON
decisionJSON, err := json.Marshal(decision)
if err != nil {
result.Error = fmt.Sprintf("failed to serialize decision: %v", err)
return result, fmt.Errorf("failed to serialize decision: %w", err)
}
// Determine creator role from UCXL address
creatorRole := ucxlAddr.Role
if creatorRole == "any" || creatorRole == "" {
creatorRole = "contributor" // Default role for decisions
}
// Store in encrypted DHT
err = dp.dhtStorage.StoreUCXLContent(
ucxlAddr.String(),
decisionJSON,
creatorRole,
"decision",
)
if err != nil {
result.Error = err.Error()
return result, fmt.Errorf("failed to store decision in DHT: %w", err)
}
// Generate content hash for reference
result.DHTHash = fmt.Sprintf("sha256:%x", sha256.Sum256(decisionJSON))
result.Success = true
log.Printf("📤 Decision published to DHT: %s (hash: %s)", ucxlAddr.String(), result.DHTHash[:16]+"...")
return result, nil
}
// RetrieveDecision retrieves a decision from the encrypted DHT storage
func (dp *DecisionPublisher) RetrieveDecision(ctx context.Context, ucxlAddr *ucxl.Address) (*Decision, error) {
if !dp.enabled {
return nil, fmt.Errorf("decision publishing is disabled")
}
// Retrieve from encrypted DHT
content, metadata, err := dp.dhtStorage.RetrieveUCXLContent(ucxlAddr.String())
if err != nil {
return nil, fmt.Errorf("failed to retrieve decision from DHT: %w", err)
}
// Verify content type
if metadata.ContentType != "decision" {
return nil, fmt.Errorf("content at address is not a decision (type: %s)", metadata.ContentType)
}
// Deserialize decision
var decision Decision
if err := json.Unmarshal(content, &decision); err != nil {
return nil, fmt.Errorf("failed to deserialize decision: %w", err)
}
log.Printf("📥 Decision retrieved from DHT: %s", ucxlAddr.String())
return &decision, nil
}
// ListDecisionsByRole lists decisions accessible by a specific role
func (dp *DecisionPublisher) ListDecisionsByRole(ctx context.Context, role string, limit int) ([]*Decision, error) {
if !dp.enabled {
return nil, fmt.Errorf("decision publishing is disabled")
}
// Get content metadata from DHT
metadataList, err := dp.dhtStorage.ListContentByRole(role, limit)
if err != nil {
return nil, fmt.Errorf("failed to list content by role: %w", err)
}
decisions := make([]*Decision, 0)
// Retrieve each decision
for _, metadata := range metadataList {
if metadata.ContentType != "decision" {
continue // Skip non-decisions
}
// Parse UCXL address
addr, err := ucxl.Parse(metadata.Address)
if err != nil {
log.Printf("⚠️ Invalid UCXL address in decision metadata: %s", metadata.Address)
continue
}
// Retrieve decision content
decision, err := dp.RetrieveDecision(ctx, addr)
if err != nil {
log.Printf("⚠️ Failed to retrieve decision %s: %v", metadata.Address, err)
continue
}
decisions = append(decisions, decision)
// Respect limit
if len(decisions) >= limit {
break
}
}
log.Printf("📋 Listed %d decisions for role: %s", len(decisions), role)
return decisions, nil
}
// UpdateDecision updates an existing decision or creates a new version
func (dp *DecisionPublisher) UpdateDecision(ctx context.Context, ucxlAddr *ucxl.Address, decision *Decision) (*PublishResult, error) {
if !dp.enabled {
result := &PublishResult{
UCXLAddress: ucxlAddr.String(),
PublishedAt: time.Now(),
Error: "Decision publishing is disabled",
}
return result, nil
}
// Check if decision already exists
existingDecision, err := dp.RetrieveDecision(ctx, ucxlAddr)
if err == nil {
// Decision exists, create related decision reference
decision.RelatedDecisions = append(decision.RelatedDecisions, dp.generateDecisionHash(existingDecision))
log.Printf("📝 Updating existing decision: %s", ucxlAddr.String())
} else {
log.Printf("📝 Creating new decision: %s", ucxlAddr.String())
}
// Publish the updated/new decision
return dp.PublishDecision(ctx, ucxlAddr, decision)
}
// SearchDecisions searches for decisions matching criteria
func (dp *DecisionPublisher) SearchDecisions(ctx context.Context, searchCriteria map[string]string, limit int) ([]*Decision, error) {
if !dp.enabled {
return nil, fmt.Errorf("decision publishing is disabled")
}
// Convert search criteria to DHT search query
query := &dht.SearchQuery{
Agent: searchCriteria["agent"],
Role: searchCriteria["role"],
Project: searchCriteria["project"],
Task: searchCriteria["task"],
ContentType: "decision",
Limit: limit,
}
// Parse time filters if provided
if createdAfter := searchCriteria["created_after"]; createdAfter != "" {
if t, err := time.Parse(time.RFC3339, createdAfter); err == nil {
query.CreatedAfter = t
}
}
if createdBefore := searchCriteria["created_before"]; createdBefore != "" {
if t, err := time.Parse(time.RFC3339, createdBefore); err == nil {
query.CreatedBefore = t
}
}
// Search DHT for matching decisions
searchResults, err := dp.dhtStorage.SearchContent(query)
if err != nil {
return nil, fmt.Errorf("failed to search decisions: %w", err)
}
decisions := make([]*Decision, 0, len(searchResults))
// Retrieve each decision
for _, metadata := range searchResults {
// Parse UCXL address
addr, err := ucxl.Parse(metadata.Address)
if err != nil {
log.Printf("⚠️ Invalid UCXL address in search results: %s", metadata.Address)
continue
}
// Retrieve decision content
decision, err := dp.RetrieveDecision(ctx, addr)
if err != nil {
log.Printf("⚠️ Failed to retrieve decision %s: %v", metadata.Address, err)
continue
}
decisions = append(decisions, decision)
}
log.Printf("🔍 Search found %d decisions", len(decisions))
return decisions, nil
}
// GetDecisionMetrics returns metrics about decisions in the system
func (dp *DecisionPublisher) GetDecisionMetrics(ctx context.Context) (map[string]interface{}, error) {
if !dp.enabled {
return map[string]interface{}{
"enabled": false,
"message": "Decision publishing is disabled",
}, nil
}
// Get DHT storage metrics
dhtMetrics := dp.dhtStorage.GetMetrics()
// Add decision-specific metrics
metrics := map[string]interface{}{
"enabled": true,
"dht_storage": dhtMetrics,
"last_updated": time.Now(),
}
return metrics, nil
}
// generateDecisionHash generates a hash for a decision to use in references
func (dp *DecisionPublisher) generateDecisionHash(decision *Decision) string {
// Create hash from key decision fields
hashData := fmt.Sprintf("%s_%s_%s_%d",
decision.Type,
decision.UCXLAddress,
decision.DiscussionID,
decision.Timestamp.Unix(),
)
hash := sha256.Sum256([]byte(hashData))
return fmt.Sprintf("decision_%x", hash[:8])
}
// IsEnabled returns whether decision publishing is enabled
func (dp *DecisionPublisher) IsEnabled() bool {
return dp.enabled
}
// Enable enables decision publishing
func (dp *DecisionPublisher) Enable() {
dp.enabled = true
log.Printf("📤 Decision publishing enabled")
}
// Disable disables decision publishing
func (dp *DecisionPublisher) Disable() {
dp.enabled = false
log.Printf("🚫 Decision publishing disabled")
}