Files
Pulse/internal/api/agent_profiles_tools.go
rcourtman 9072b8eaa8 feat: enhance API router with multi-tenant authorization
Router & Middleware:
- Add auth context middleware for user/token extraction
- Add tenant middleware with authorization checking
- Refactor middleware chain ordering for proper isolation
- Add router helpers for common patterns

Authentication & SSO:
- Enhance auth with tenant-aware context
- Update OIDC, SAML, and SSO handlers for multi-tenant
- Add RBAC handler improvements
- Add security enhancements

New Test Coverage:
- API foundation tests
- Auth and authorization tests
- Router state and general tests
- SSO handler CRUD tests
- WebSocket isolation tests
- Resource handler tests
2026-01-24 22:42:23 +00:00

310 lines
8.2 KiB
Go

package api
import (
"context"
"fmt"
"strings"
"time"
"github.com/google/uuid"
"github.com/rcourtman/pulse-go-rewrite/internal/ai/tools"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/rcourtman/pulse-go-rewrite/internal/license"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
"github.com/rs/zerolog/log"
)
const aiProfileDescription = "Managed by Pulse Patrol"
// MCPAgentProfileManager manages agent profiles for MCP tools.
type MCPAgentProfileManager struct {
persistence *config.ConfigPersistence
licenseService *license.Service
validator *models.ProfileValidator
}
func NewMCPAgentProfileManager(persistence *config.ConfigPersistence, licenseService *license.Service) *MCPAgentProfileManager {
return &MCPAgentProfileManager{
persistence: persistence,
licenseService: licenseService,
validator: models.NewProfileValidator(),
}
}
func (m *MCPAgentProfileManager) ApplyAgentScope(_ context.Context, agentID, agentLabel string, settings map[string]interface{}) (string, string, bool, error) {
if err := m.requireLicense(); err != nil {
return "", "", false, err
}
if m.persistence == nil {
return "", "", false, fmt.Errorf("profile persistence unavailable")
}
if strings.TrimSpace(agentID) == "" {
return "", "", false, fmt.Errorf("agent ID is required")
}
if len(settings) == 0 {
return "", "", false, fmt.Errorf("settings are required")
}
if err := m.validateSettings(settings); err != nil {
return "", "", false, err
}
profileName := buildScopeProfileName(agentLabel, agentID)
now := time.Now()
username := "ai"
profiles, err := m.persistence.LoadAgentProfiles()
if err != nil {
return "", "", false, fmt.Errorf("failed to load profiles: %w", err)
}
created := true
var profile models.AgentProfile
for i := range profiles {
if profiles[i].Name == profileName {
created = false
profiles[i].Config = settings
profiles[i].Description = aiProfileDescription
profiles[i].UpdatedAt = now
profiles[i].UpdatedBy = username
profiles[i].Version++
profile = profiles[i]
break
}
}
if created {
profile = models.AgentProfile{
ID: uuid.New().String(),
Name: profileName,
Description: aiProfileDescription,
Config: settings,
Version: 1,
CreatedAt: now,
UpdatedAt: now,
CreatedBy: username,
UpdatedBy: username,
}
profiles = append(profiles, profile)
}
if err := m.persistence.SaveAgentProfiles(profiles); err != nil {
return "", "", false, fmt.Errorf("failed to save profile: %w", err)
}
if err := m.saveVersion(profile, "Patrol scope update"); err != nil {
log.Warn().Err(err).Msg("Failed to record profile version history")
}
changeAction := "update"
if created {
changeAction = "create"
}
m.logChange(models.ProfileChangeLog{
ID: uuid.New().String(),
ProfileID: profile.ID,
ProfileName: profile.Name,
Action: changeAction,
OldVersion: profile.Version - 1,
NewVersion: profile.Version,
User: username,
Timestamp: now,
})
if err := m.assignProfile(agentID, profile, username); err != nil {
return "", "", created, err
}
return profile.ID, profile.Name, created, nil
}
func (m *MCPAgentProfileManager) AssignProfile(_ context.Context, agentID, profileID string) (string, error) {
if err := m.requireLicense(); err != nil {
return "", err
}
if m.persistence == nil {
return "", fmt.Errorf("profile persistence unavailable")
}
agentID = strings.TrimSpace(agentID)
profileID = strings.TrimSpace(profileID)
if agentID == "" || profileID == "" {
return "", fmt.Errorf("agent ID and profile ID are required")
}
profiles, err := m.persistence.LoadAgentProfiles()
if err != nil {
return "", fmt.Errorf("failed to load profiles: %w", err)
}
var profile models.AgentProfile
found := false
for _, p := range profiles {
if p.ID == profileID {
profile = p
found = true
break
}
}
if !found {
return "", fmt.Errorf("profile %s not found", profileID)
}
if err := m.assignProfile(agentID, profile, "ai"); err != nil {
return "", err
}
return profile.Name, nil
}
func (m *MCPAgentProfileManager) GetAgentScope(_ context.Context, agentID string) (*tools.AgentScope, error) {
if err := m.requireLicense(); err != nil {
return nil, err
}
if m.persistence == nil {
return nil, fmt.Errorf("profile persistence unavailable")
}
agentID = strings.TrimSpace(agentID)
if agentID == "" {
return nil, fmt.Errorf("agent ID is required")
}
assignments, err := m.persistence.LoadAgentProfileAssignments()
if err != nil {
return nil, fmt.Errorf("failed to load assignments: %w", err)
}
var assignment *models.AgentProfileAssignment
for i := range assignments {
if assignments[i].AgentID == agentID {
assignment = &assignments[i]
break
}
}
if assignment == nil {
return nil, nil
}
profiles, err := m.persistence.LoadAgentProfiles()
if err != nil {
return nil, fmt.Errorf("failed to load profiles: %w", err)
}
for _, profile := range profiles {
if profile.ID == assignment.ProfileID {
return &tools.AgentScope{
AgentID: agentID,
ProfileID: profile.ID,
ProfileName: profile.Name,
ProfileVersion: assignment.ProfileVersion,
Settings: profile.Config,
}, nil
}
}
return nil, nil
}
func (m *MCPAgentProfileManager) assignProfile(agentID string, profile models.AgentProfile, username string) error {
assignments, err := m.persistence.LoadAgentProfileAssignments()
if err != nil {
return fmt.Errorf("failed to load assignments: %w", err)
}
trimmed := []models.AgentProfileAssignment{}
for _, a := range assignments {
if a.AgentID != agentID {
trimmed = append(trimmed, a)
}
}
trimmed = append(trimmed, models.AgentProfileAssignment{
AgentID: agentID,
ProfileID: profile.ID,
ProfileVersion: profile.Version,
UpdatedAt: time.Now(),
AssignedBy: username,
})
if err := m.persistence.SaveAgentProfileAssignments(trimmed); err != nil {
return fmt.Errorf("failed to save assignment: %w", err)
}
m.logChange(models.ProfileChangeLog{
ID: uuid.New().String(),
ProfileID: profile.ID,
ProfileName: profile.Name,
Action: "assign",
AgentID: agentID,
User: username,
Timestamp: time.Now(),
})
return nil
}
func (m *MCPAgentProfileManager) validateSettings(settings map[string]interface{}) error {
if m.validator == nil {
return nil
}
result := m.validator.Validate(settings)
if !result.Valid || len(result.Warnings) > 0 {
return fmt.Errorf("invalid settings: %s", formatValidationIssues(result))
}
return nil
}
func formatValidationIssues(result models.ValidationResult) string {
parts := make([]string, 0, len(result.Errors)+len(result.Warnings))
for _, err := range result.Errors {
parts = append(parts, fmt.Sprintf("%s (%s)", err.Key, err.Message))
}
for _, warn := range result.Warnings {
parts = append(parts, fmt.Sprintf("warning: %s (%s)", warn.Key, warn.Message))
}
if len(parts) == 0 {
return "unknown validation error"
}
return strings.Join(parts, "; ")
}
func (m *MCPAgentProfileManager) saveVersion(profile models.AgentProfile, note string) error {
versions, err := m.persistence.LoadAgentProfileVersions()
if err != nil {
return err
}
versions = append(versions, models.AgentProfileVersion{
ProfileID: profile.ID,
Version: profile.Version,
Name: profile.Name,
Description: profile.Description,
Config: profile.Config,
ParentID: profile.ParentID,
CreatedAt: time.Now(),
CreatedBy: "ai",
ChangeNote: note,
})
return m.persistence.SaveAgentProfileVersions(versions)
}
func (m *MCPAgentProfileManager) logChange(entry models.ProfileChangeLog) {
if err := m.persistence.AppendProfileChangeLog(entry); err != nil {
log.Warn().Err(err).Msg("Failed to log profile change")
}
}
func (m *MCPAgentProfileManager) requireLicense() error {
if m.licenseService == nil {
return nil
}
return m.licenseService.RequireFeature(license.FeatureAgentProfiles)
}
func buildScopeProfileName(agentLabel, agentID string) string {
label := strings.TrimSpace(agentLabel)
if label == "" || strings.EqualFold(label, agentID) {
return fmt.Sprintf("Patrol Scope: %s", agentID)
}
return fmt.Sprintf("Patrol Scope: %s (%s)", label, agentID)
}