Files
pulse/internal/ai/tools/executor.go
T
rcourtman f530c76d17 Author Impact on AI-generated Patrol findings via tool schema
Extend the patrol_report_finding LLM tool schema with an optional
impact parameter and propagate it through PatrolFindingInput,
patrolFindingCreatorAdapter.CreateFinding, and into Finding.Impact so
LLM-authored Patrol findings carry consequence-if-ignored copy at
detection time alongside the curated catalogs already in place for
runtime failures and threshold alerts.

Updates the patrol system prompt with an "Authoring Impact" section
that instructs the LLM to write concrete operational consequences
(named workloads, jobs, recovery windows) rather than echoing severity
or category, and to leave impact empty rather than fabricate a
consequence when one is genuinely unknown. The eval-pass prompt gets a
shorter version of the same guidance.

Adds two unit tests: one covering an authored impact passing through
to PatrolFindingInput.Impact, and one covering the omitted-impact case
where the contract is honored verbatim with no synthesized default.
Updates the ai-runtime contract to pin the tool-schema authoring rule
and the no-fabrication invariant.
2026-05-08 19:28:02 +01:00

1051 lines
38 KiB
Go

package tools
import (
"context"
"strings"
"sync"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/agentexec"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
"github.com/rcourtman/pulse-go-rewrite/internal/recovery"
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
"github.com/rs/zerolog/log"
)
// UnifiedResourceProvider gives the executor access to the unified resource
// registry so that tool handlers can read physical disks, Ceph clusters, etc.
// from the canonical model instead of raw StateSnapshot fields.
type UnifiedResourceProvider interface {
GetByType(t unifiedresources.ResourceType) []unifiedresources.Resource
}
// ServerVersion is the version of the MCP tool implementation
const ServerVersion = "1.0.0"
// StateProvider is a type alias for models.SnapshotProvider.
// Kept for local convenience; all new code should use models.SnapshotProvider directly.
type StateProvider = models.SnapshotProvider
// RecoveryPointsProvider provides paged access to persisted recovery points.
// Tool handlers should prefer this over legacy state backup arrays when present.
type RecoveryPointsProvider interface {
ListPoints(ctx context.Context, opts recovery.ListPointsOptions) ([]recovery.RecoveryPoint, int, error)
}
// CommandPolicy evaluates command security
type CommandPolicy interface {
Evaluate(command string) agentexec.PolicyDecision
}
// AgentServer executes commands on agents
type AgentServer interface {
GetConnectedAgents() []agentexec.ConnectedAgent
ExecuteCommand(ctx context.Context, agentID string, cmd agentexec.ExecuteCommandPayload) (*agentexec.CommandResultPayload, error)
}
// MetadataUpdater updates resource metadata
type MetadataUpdater interface {
SetResourceURL(resourceType, resourceID, url string) error
}
// FindingsManager manages patrol findings
type FindingsManager interface {
ResolveFinding(findingID, note string) error
DismissFinding(findingID, reason, note string) error
}
// MetricsHistoryProvider provides historical metrics for trend analysis
type MetricsHistoryProvider interface {
GetResourceMetrics(resourceID string, period time.Duration) ([]MetricPoint, error)
GetAllMetricsSummary(period time.Duration) (map[string]ResourceMetricsSummary, error)
}
// BaselineProvider provides learned baselines for anomaly detection
type BaselineProvider interface {
GetBaseline(resourceID, metric string) *MetricBaseline
GetAllBaselines() map[string]map[string]*MetricBaseline // resourceID -> metric -> baseline
}
// PatternProvider provides detected patterns and predictions
type PatternProvider interface {
GetPatterns() []Pattern
GetPredictions() []Prediction
}
// AlertProvider provides active alerts
type AlertProvider interface {
GetActiveAlerts() []ActiveAlert
GetRecentlyResolved(minutes int) []models.ResolvedAlert
}
// FindingsProvider provides patrol findings
type FindingsProvider interface {
GetActiveFindings() []Finding
GetDismissedFindings() []Finding
}
// PatrolFindingCreator is set on the executor during a patrol run to allow
// patrol-specific tools (patrol_report_finding, patrol_resolve_finding,
// patrol_get_findings) to create, resolve, and query findings.
// Outside of a patrol run this is nil, and the tools return a clear error.
type PatrolFindingCreator interface {
CreateFinding(input PatrolFindingInput) (findingID string, isNew bool, err error)
ResolveFinding(findingID, reason string) error
GetActiveFindings(resourceID, minSeverity string) []PatrolFindingInfo
}
// PatrolFindingsChecker tracks whether a patrol run has queried existing findings.
// Tools can use this to enforce calling patrol_get_findings before reporting or resolving.
type PatrolFindingsChecker interface {
HasCheckedFindings() bool
}
// PatrolFindingInput contains the structured parameters the LLM passes
// to the patrol_report_finding tool.
type PatrolFindingInput struct {
Key string `json:"key"`
Severity string `json:"severity"`
Category string `json:"category"`
ResourceID string `json:"resource_id"`
ResourceName string `json:"resource_name"`
ResourceType string `json:"resource_type"`
Title string `json:"title"`
Description string `json:"description"`
Impact string `json:"impact,omitempty"`
Recommendation string `json:"recommendation,omitempty"`
Evidence string `json:"evidence,omitempty"`
}
// PatrolFindingInfo is a lightweight view of a finding returned by
// PatrolFindingCreator.GetActiveFindings.
type PatrolFindingInfo struct {
ID string `json:"id"`
Key string `json:"key,omitempty"`
Severity string `json:"severity"`
Category string `json:"category"`
ResourceID string `json:"resource_id"`
ResourceName string `json:"resource_name"`
ResourceType string `json:"resource_type"`
Title string `json:"title"`
Description string `json:"description"`
DetectedAt string `json:"detected_at"`
}
// BackupProvider provides backup information
type BackupProvider interface {
GetBackups() models.Backups
GetPBSInstances() []models.PBSInstance
}
// ReplicationProvider provides replication job information.
type ReplicationProvider interface {
GetReplicationJobs() []models.ReplicationJob
}
// ConnectionHealthProvider provides instance connection health information.
type ConnectionHealthProvider interface {
GetConnectionHealth() map[string]bool
}
// GuestConfigProvider provides guest configuration data (VM/system container).
type GuestConfigProvider interface {
GetGuestConfig(guestType, instance, node string, vmID int) (map[string]interface{}, error)
}
// AppContainerConfigRequest describes a canonical configuration read against an
// API-backed app-container resource.
type AppContainerConfigRequest struct {
OrgID string
ResourceID string
ProviderUID string
Name string
Host string
Platform string
}
// AppContainerConfigResult captures canonical configuration/state metadata for
// an API-backed app-container resource.
type AppContainerConfigResult struct {
ResourceID string
ProviderUID string
Name string
Host string
Platform string
Status string
Version string
HumanVersion string
Notes string
CustomApp bool
UpgradeAvailable bool
ImageUpdatesAvailable bool
ContainerCount int
UsedHostIPs []string
Images []string
Ports []PortInfo
Networks []NetworkInfo
Mounts []MountInfo
Containers []AppContainerConfigContainer
}
// AppContainerConfigProvider executes canonical configuration reads for
// API-backed app-container resources such as TrueNAS-managed applications.
type AppContainerConfigProvider interface {
GetConfig(ctx context.Context, req AppContainerConfigRequest) (*AppContainerConfigResult, error)
}
// DiskHealthProvider provides disk health information from host agents
type DiskHealthProvider interface {
GetHosts() []*unifiedresources.HostView
}
// UpdatesProvider provides Docker update operations for MCP tools
type UpdatesProvider interface {
GetPendingUpdates(hostID string) []ContainerUpdateInfo
TriggerUpdateCheck(hostID string) (DockerCommandStatus, error)
UpdateContainer(hostID, containerID, containerName string) (DockerCommandStatus, error)
IsUpdateActionsEnabled() bool
}
// AppContainerActionRequest describes a canonical control action against an
// API-backed app-container resource.
type AppContainerActionRequest struct {
OrgID string
ResourceID string
ProviderUID string
Name string
Host string
Platform string
Action string
}
// AppContainerActionResult captures the post-action view of an app-container
// after a native provider operation completes.
type AppContainerActionResult struct {
ResourceID string
ProviderUID string
Name string
Host string
Platform string
Action string
Status string
Output string
}
// AppContainerActionProvider executes canonical actions for API-backed
// app-container resources such as TrueNAS-managed applications.
type AppContainerActionProvider interface {
ExecuteAction(ctx context.Context, req AppContainerActionRequest) (*AppContainerActionResult, error)
}
// AppContainerReadRequest describes a canonical read-only diagnostic request
// against an API-backed app-container resource.
type AppContainerReadRequest struct {
OrgID string
ResourceID string
ProviderUID string
Name string
Host string
Platform string
Container string
Lines int
}
// AppContainerReadResult captures a bounded read-only diagnostic result for an
// API-backed app-container.
type AppContainerReadResult struct {
ResourceID string
ProviderUID string
Name string
Host string
Platform string
Container string
Lines int
Output string
}
// AppContainerReadProvider executes canonical read-only diagnostics for
// API-backed app-container resources such as TrueNAS-managed applications.
type AppContainerReadProvider interface {
ReadLogs(ctx context.Context, req AppContainerReadRequest) (*AppContainerReadResult, error)
}
// DiscoveryProvider provides AI-powered infrastructure discovery
type DiscoveryProvider interface {
GetDiscovery(id string) (*ResourceDiscoveryInfo, error)
GetDiscoveryByResource(resourceType, targetID, resourceID string) (*ResourceDiscoveryInfo, error)
ListDiscoveries() ([]*ResourceDiscoveryInfo, error)
ListDiscoveriesByType(resourceType string) ([]*ResourceDiscoveryInfo, error)
ListDiscoveriesByTarget(targetID string) ([]*ResourceDiscoveryInfo, error)
FormatForAIContext(discoveries []*ResourceDiscoveryInfo) string
// TriggerDiscovery initiates discovery for a resource and returns the result
TriggerDiscovery(ctx context.Context, resourceType, targetID, resourceID string) (*ResourceDiscoveryInfo, error)
}
// ResolvedResourceInfo contains the minimal information needed for tool validation.
// This is an interface to avoid import cycles with the chat package.
type ResolvedResourceInfo interface {
GetResourceID() string
GetResourceType() string
GetTargetHost() string
GetAgentID() string
GetAdapter() string
GetVMID() int
GetNode() string
GetAllowedActions() []string
// New structured identity methods
GetProviderUID() string
GetKind() string
GetAliases() []string
}
// ResourceRegistration contains all fields needed to register a discovered resource.
// This structured approach replaces the long parameter list for clarity.
type ResourceRegistration struct {
// Identity
Kind string // Technology/transport kind: "node", "vm", "system-container", "app-container", etc. (drives routing)
ProviderUID string // Stable provider ID (container ID, VMID, pod UID)
Name string // Primary display name
Aliases []string // Additional names that resolve to this resource
// Scope
HostUID string
HostName string
ParentUID string
ParentKind string
ClusterUID string
Namespace string
// Legacy fields (for backwards compatibility)
VMID int
Node string
LocationChain []string
// Executor paths
Executors []ExecutorRegistration
}
// ExecutorRegistration describes how an executor can reach a resource.
type ExecutorRegistration struct {
ExecutorID string
Adapter string
Actions []string
Priority int
}
// ResolvedContextProvider provides session-scoped resource resolution.
// Query and discovery tools add resources; action tools validate against them.
// This interface is implemented by the chat package's ResolvedContext.
type ResolvedContextProvider interface {
// AddResolvedResource adds a resource that was found via query/discovery.
// Uses the new structured registration format.
AddResolvedResource(reg ResourceRegistration)
// GetResolvedResourceByID retrieves a resource by its canonical ID (kind:provider_uid)
GetResolvedResourceByID(resourceID string) (ResolvedResourceInfo, bool)
// GetResolvedResourceByAlias retrieves a resource by any of its aliases
GetResolvedResourceByAlias(alias string) (ResolvedResourceInfo, bool)
// ValidateResourceForAction checks if a resource can perform an action
// Returns the resource if valid, error if not found or action not allowed
ValidateResourceForAction(resourceID, action string) (ResolvedResourceInfo, error)
// HasAnyResources returns true if at least one resource has been discovered
HasAnyResources() bool
// WasRecentlyAccessed checks if a resource was accessed within the given time window.
// Used for routing validation to distinguish "this turn" from "session-wide" context.
WasRecentlyAccessed(resourceID string, window time.Duration) bool
// GetRecentlyAccessedResources returns resource IDs accessed within the given time window.
GetRecentlyAccessedResources(window time.Duration) []string
// MarkExplicitAccess marks a resource as recently accessed, indicating user intent.
// Call this for single-resource operations (get, explicit select) but NOT for bulk
// operations (list, search) to avoid poisoning routing validation.
MarkExplicitAccess(resourceID string)
}
// RecentAccessWindow is the time window used to determine "recently referenced" resources.
// Resources accessed within this window are considered to be from the current turn/exchange.
const RecentAccessWindow = 30 * time.Second
// ResourceDiscoveryInfo represents discovered information about a resource
type ResourceDiscoveryInfo struct {
ID string `json:"id"`
ResourceType string `json:"resource_type"`
ResourceID string `json:"resource_id"`
TargetID string `json:"target_id,omitempty"`
AgentID string `json:"agent_id,omitempty"`
Hostname string `json:"hostname"`
ServiceType string `json:"service_type"`
ServiceName string `json:"service_name"`
ServiceVersion string `json:"service_version"`
Category string `json:"category"`
CLIAccess string `json:"cli_access"`
Facts []DiscoveryFact `json:"facts"`
ConfigPaths []string `json:"config_paths"`
DataPaths []string `json:"data_paths"`
LogPaths []string `json:"log_paths,omitempty"` // Log file paths or commands (e.g., journalctl)
Ports []DiscoveryPortInfo `json:"ports"`
BindMounts []DiscoveryMount `json:"bind_mounts,omitempty"` // For Docker: host->container path mappings
UserNotes string `json:"user_notes,omitempty"`
Confidence float64 `json:"confidence"`
AIReasoning string `json:"ai_reasoning,omitempty"`
DiscoveredAt time.Time `json:"discovered_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// DiscoveryPortInfo represents a listening port discovered on a resource
type DiscoveryPortInfo struct {
Port int `json:"port"`
Protocol string `json:"protocol"`
Process string `json:"process,omitempty"`
Address string `json:"address,omitempty"`
}
// DiscoveryMount represents a bind mount (host path -> container path)
type DiscoveryMount struct {
ContainerName string `json:"container_name,omitempty"` // Docker container name (for Docker inside LXC/VM)
Source string `json:"source"` // Host path (where to actually write files)
Destination string `json:"destination"` // Container path (what the service sees)
Type string `json:"type,omitempty"` // Mount type: bind, volume, tmpfs
ReadOnly bool `json:"read_only,omitempty"`
}
// DiscoveryFact represents a discovered fact about a resource
type DiscoveryFact struct {
Category string `json:"category"`
Key string `json:"key"`
Value string `json:"value"`
Source string `json:"source,omitempty"`
Confidence float64 `json:"confidence,omitempty"` // 0-1 confidence for this fact
}
// ControlLevel represents the AI's permission level for infrastructure control
type ControlLevel string
const (
// ControlLevelReadOnly - AI can only query, no control tools available
ControlLevelReadOnly ControlLevel = "read_only"
// ControlLevelControlled - AI can execute with per-command approval
ControlLevelControlled ControlLevel = "controlled"
// ControlLevelAutonomous - AI executes without approval (requires Pro license)
ControlLevelAutonomous ControlLevel = "autonomous"
)
// ExecutorConfig holds all dependencies for the tool executor
type ExecutorConfig struct {
// Required providers
StateProvider StateProvider
Policy CommandPolicy
AgentServer AgentServer
// Optional providers - patrol context
MetricsHistory MetricsHistoryProvider
BaselineProvider BaselineProvider
PatternProvider PatternProvider
AlertProvider AlertProvider
FindingsProvider FindingsProvider
// Optional providers - infrastructure
BackupProvider BackupProvider
ReplicationProvider ReplicationProvider
ConnectionHealth ConnectionHealthProvider
RecoveryPointsProvider RecoveryPointsProvider
GuestConfigProvider GuestConfigProvider
AppContainerConfigProvider AppContainerConfigProvider
DiskHealthProvider DiskHealthProvider
UpdatesProvider UpdatesProvider
// Optional providers - management
MetadataUpdater MetadataUpdater
FindingsManager FindingsManager
AgentProfileManager AgentProfileManager
// Optional providers - intelligence
IncidentRecorderProvider IncidentRecorderProvider
EventCorrelatorProvider EventCorrelatorProvider
KnowledgeStoreProvider KnowledgeStoreProvider
// Optional providers - discovery
DiscoveryProvider DiscoveryProvider
// Optional providers - unified resources
UnifiedResourceProvider UnifiedResourceProvider
AppContainerActionProvider AppContainerActionProvider
AppContainerReadProvider AppContainerReadProvider
ActionAuditStore unifiedresources.ResourceStore
// Optional typed read access to current infrastructure state.
// When provided, tool handlers should prefer this over models.StateSnapshot iteration.
ReadState unifiedresources.ReadState
// Control settings
ControlLevel ControlLevel
ProtectedGuests []string // VMIDs that AI cannot control
OrgID string // Tenant/org scope for approval records
}
// PulseToolExecutor implements ToolExecutor for Pulse-specific tools
type PulseToolExecutor struct {
// Core providers
stateProvider StateProvider
policy CommandPolicy
agentServer AgentServer
// Patrol context providers
metricsHistory MetricsHistoryProvider
baselineProvider BaselineProvider
patternProvider PatternProvider
alertProvider AlertProvider
findingsProvider FindingsProvider
// Infrastructure context providers
backupProvider BackupProvider
replicationProvider ReplicationProvider
connectionHealth ConnectionHealthProvider
// Paged recovery points access for snapshot/backup tools.
recoveryPointsProvider RecoveryPointsProvider
guestConfigProvider GuestConfigProvider
appContainerConfigProvider AppContainerConfigProvider
diskHealthProvider DiskHealthProvider
updatesProvider UpdatesProvider
// Management providers
metadataUpdater MetadataUpdater
findingsManager FindingsManager
agentProfileManager AgentProfileManager
// Intelligence providers
incidentRecorderProvider IncidentRecorderProvider
eventCorrelatorProvider EventCorrelatorProvider
knowledgeStoreProvider KnowledgeStoreProvider
// Discovery provider
discoveryProvider DiscoveryProvider
// Unified resources provider
unifiedResourceProvider UnifiedResourceProvider
appContainerActionProvider AppContainerActionProvider
appContainerReadProvider AppContainerReadProvider
actionAuditStore unifiedresources.ResourceStore
// Typed state reader. Nil means "legacy-only": tools must fall back to StateSnapshot access.
readState unifiedresources.ReadState
// Control settings
controlLevel ControlLevel
protectedGuests []string
// Current execution context
targetType string
targetID string
isAutonomous bool
orgID string
// Session-scoped resolved context for resource validation
// This is set per-session by the agentic loop before tool execution
resolvedContext ResolvedContextProvider
// Telemetry callback for recording metrics
// This is optional - if nil, no telemetry is recorded
telemetryCallback TelemetryCallback
// Patrol finding creator — set only during a patrol run, nil otherwise.
// Enables patrol_report_finding, patrol_resolve_finding, patrol_get_findings tools.
patrolFindingCreatorMu sync.RWMutex
patrolFindingCreator PatrolFindingCreator
// Tool registry
registry *ToolRegistry
}
// TelemetryCallback is called when the executor needs to record telemetry.
// This allows the chat layer to handle metrics without import cycles.
type TelemetryCallback interface {
// RecordStrictResolutionBlock records when strict resolution blocks an action
RecordStrictResolutionBlock(tool, action string)
// RecordAutoRecoveryAttempt records an auto-recovery attempt
RecordAutoRecoveryAttempt(errorCode, tool string)
// RecordAutoRecoverySuccess records a successful auto-recovery
RecordAutoRecoverySuccess(errorCode, tool string)
// RecordRoutingMismatchBlock records when routing validation blocks an operation
// that targeted a parent host when a child resource was recently referenced.
// targetKind: "node" (the kind being targeted)
// childKind: "system-container", "vm", "app-container" (the kind of the more specific resource)
RecordRoutingMismatchBlock(tool, targetKind, childKind string)
}
// NewPulseToolExecutor creates a new Pulse tool executor with the given configuration
func NewPulseToolExecutor(cfg ExecutorConfig) *PulseToolExecutor {
e := &PulseToolExecutor{
stateProvider: cfg.StateProvider,
policy: cfg.Policy,
agentServer: cfg.AgentServer,
metricsHistory: cfg.MetricsHistory,
baselineProvider: cfg.BaselineProvider,
patternProvider: cfg.PatternProvider,
alertProvider: cfg.AlertProvider,
findingsProvider: cfg.FindingsProvider,
backupProvider: cfg.BackupProvider,
replicationProvider: cfg.ReplicationProvider,
connectionHealth: cfg.ConnectionHealth,
recoveryPointsProvider: cfg.RecoveryPointsProvider,
guestConfigProvider: cfg.GuestConfigProvider,
appContainerConfigProvider: cfg.AppContainerConfigProvider,
diskHealthProvider: cfg.DiskHealthProvider,
updatesProvider: cfg.UpdatesProvider,
metadataUpdater: cfg.MetadataUpdater,
findingsManager: cfg.FindingsManager,
agentProfileManager: cfg.AgentProfileManager,
incidentRecorderProvider: cfg.IncidentRecorderProvider,
eventCorrelatorProvider: cfg.EventCorrelatorProvider,
knowledgeStoreProvider: cfg.KnowledgeStoreProvider,
discoveryProvider: cfg.DiscoveryProvider,
unifiedResourceProvider: cfg.UnifiedResourceProvider,
appContainerActionProvider: cfg.AppContainerActionProvider,
appContainerReadProvider: cfg.AppContainerReadProvider,
actionAuditStore: cfg.ActionAuditStore,
readState: cfg.ReadState,
controlLevel: cfg.ControlLevel,
protectedGuests: cfg.ProtectedGuests,
orgID: normalizeExecutorOrgID(cfg.OrgID),
registry: NewToolRegistry(),
}
// Auto-wire backup, replication, and connection health adapters from
// stateProvider when no explicit provider was injected. Each closure
// captures stateProvider and fetches the specific field on demand —
// this avoids importing the full StateGetter interface into adapters.go.
if sp := e.stateProvider; sp != nil {
getSnapshot := func() models.StateSnapshot { return sp.ReadSnapshot() }
if e.backupProvider == nil {
e.backupProvider = NewBackupMCPAdapter(
func() models.Backups { return getSnapshot().Backups },
func() []models.PBSInstance { return getSnapshot().PBSInstances },
)
}
if e.replicationProvider == nil {
e.replicationProvider = NewReplicationMCPAdapter(
func() []models.ReplicationJob { return getSnapshot().ReplicationJobs },
)
}
if e.connectionHealth == nil {
e.connectionHealth = NewConnectionHealthMCPAdapter(
func() map[string]bool { return getSnapshot().ConnectionHealth },
)
}
}
// Register all tools
e.registerTools()
return e
}
// Clone returns an executor instance with shared immutable providers and fresh
// session-scoped mutable state. This prevents concurrent sessions from
// overwriting each other's routing and approval context.
func (e *PulseToolExecutor) Clone() *PulseToolExecutor {
if e == nil {
return nil
}
clone := &PulseToolExecutor{
stateProvider: e.stateProvider,
policy: e.policy,
agentServer: e.agentServer,
metricsHistory: e.metricsHistory,
baselineProvider: e.baselineProvider,
patternProvider: e.patternProvider,
alertProvider: e.alertProvider,
findingsProvider: e.findingsProvider,
backupProvider: e.backupProvider,
replicationProvider: e.replicationProvider,
connectionHealth: e.connectionHealth,
recoveryPointsProvider: e.recoveryPointsProvider,
guestConfigProvider: e.guestConfigProvider,
appContainerConfigProvider: e.appContainerConfigProvider,
diskHealthProvider: e.diskHealthProvider,
updatesProvider: e.updatesProvider,
metadataUpdater: e.metadataUpdater,
findingsManager: e.findingsManager,
agentProfileManager: e.agentProfileManager,
incidentRecorderProvider: e.incidentRecorderProvider,
eventCorrelatorProvider: e.eventCorrelatorProvider,
knowledgeStoreProvider: e.knowledgeStoreProvider,
discoveryProvider: e.discoveryProvider,
unifiedResourceProvider: e.unifiedResourceProvider,
appContainerActionProvider: e.appContainerActionProvider,
appContainerReadProvider: e.appContainerReadProvider,
actionAuditStore: e.actionAuditStore,
readState: e.readState,
controlLevel: e.controlLevel,
protectedGuests: append([]string(nil), e.protectedGuests...),
targetType: e.targetType,
targetID: e.targetID,
isAutonomous: e.isAutonomous,
orgID: e.orgID,
telemetryCallback: e.telemetryCallback,
registry: e.registry,
}
clone.patrolFindingCreator = e.GetPatrolFindingCreator()
return clone
}
func normalizeExecutorOrgID(orgID string) string {
normalized := strings.TrimSpace(orgID)
if normalized == "" {
return "default"
}
return normalized
}
func (e *PulseToolExecutor) getReadState() unifiedresources.ReadState {
if readState := e.getCanonicalReadState(); readState != nil {
return readState
}
if e.stateProvider == nil {
return nil
}
// Compatibility bridge for legacy-only wiring: derive a typed ReadState view
// from the latest snapshot so tool handlers can stay platform-agnostic.
rr := unifiedresources.NewRegistry(nil)
rr.IngestSnapshot(e.stateProvider.ReadSnapshot())
return rr
}
func (e *PulseToolExecutor) getCanonicalReadState() unifiedresources.ReadState {
if e.readState != nil {
return e.readState
}
if readState, ok := e.unifiedResourceProvider.(unifiedresources.ReadState); ok && readState != nil {
return readState
}
return nil
}
func (e *PulseToolExecutor) hasReadState() bool {
if e.readState != nil || e.stateProvider != nil {
return true
}
readState, ok := e.unifiedResourceProvider.(unifiedresources.ReadState)
return ok && readState != nil
}
func (e *PulseToolExecutor) hasCanonicalReadState() bool {
return e.getCanonicalReadState() != nil
}
// SetContext sets the current execution context
func (e *PulseToolExecutor) SetContext(targetType, targetID string, autonomous bool) {
e.targetType = targetType
e.targetID = targetID
e.isAutonomous = autonomous
}
// SetAutonomousMode updates only the execution mode, preserving any
// session-scoped target context already attached to this executor clone.
func (e *PulseToolExecutor) SetAutonomousMode(enabled bool) {
e.isAutonomous = enabled
}
// SetOrgID sets the org scope used when creating approval records.
func (e *PulseToolExecutor) SetOrgID(orgID string) {
e.orgID = normalizeExecutorOrgID(orgID)
}
// SetControlLevel updates the control level
func (e *PulseToolExecutor) SetControlLevel(level ControlLevel) {
e.controlLevel = level
}
// SetProtectedGuests updates the protected guests list
func (e *PulseToolExecutor) SetProtectedGuests(vmids []string) {
e.protectedGuests = vmids
}
// RegisterTool allows tests or extensions to add tools at runtime.
func (e *PulseToolExecutor) RegisterTool(tool RegisteredTool) {
e.registry.Register(tool)
}
// Runtime setter methods for updating providers after creation
// SetMetadataUpdater sets the metadata updater
func (e *PulseToolExecutor) SetMetadataUpdater(updater MetadataUpdater) {
e.metadataUpdater = updater
}
// SetFindingsManager sets the findings manager
func (e *PulseToolExecutor) SetFindingsManager(manager FindingsManager) {
e.findingsManager = manager
}
// SetMetricsHistory sets the metrics history provider
func (e *PulseToolExecutor) SetMetricsHistory(provider MetricsHistoryProvider) {
e.metricsHistory = provider
}
// SetRecoveryPointsProvider sets a paged recovery points provider used by snapshot/backup tools.
func (e *PulseToolExecutor) SetRecoveryPointsProvider(provider RecoveryPointsProvider) {
e.recoveryPointsProvider = provider
}
// SetBaselineProvider sets the baseline provider
func (e *PulseToolExecutor) SetBaselineProvider(provider BaselineProvider) {
e.baselineProvider = provider
}
// SetPatternProvider sets the pattern provider
func (e *PulseToolExecutor) SetPatternProvider(provider PatternProvider) {
e.patternProvider = provider
}
// SetAlertProvider sets the alert provider
func (e *PulseToolExecutor) SetAlertProvider(provider AlertProvider) {
e.alertProvider = provider
}
// SetFindingsProvider sets the findings provider
func (e *PulseToolExecutor) SetFindingsProvider(provider FindingsProvider) {
e.findingsProvider = provider
}
// SetBackupProvider sets the backup provider
func (e *PulseToolExecutor) SetBackupProvider(provider BackupProvider) {
e.backupProvider = provider
}
// SetGuestConfigProvider sets the guest config provider
func (e *PulseToolExecutor) SetGuestConfigProvider(provider GuestConfigProvider) {
e.guestConfigProvider = provider
}
// SetAppContainerConfigProvider sets the provider used for canonical native
// app-container configuration reads.
func (e *PulseToolExecutor) SetAppContainerConfigProvider(provider AppContainerConfigProvider) {
e.appContainerConfigProvider = provider
}
// SetDiskHealthProvider sets the disk health provider
func (e *PulseToolExecutor) SetDiskHealthProvider(provider DiskHealthProvider) {
e.diskHealthProvider = provider
}
// SetAgentProfileManager sets the agent profile manager
func (e *PulseToolExecutor) SetAgentProfileManager(manager AgentProfileManager) {
e.agentProfileManager = manager
}
// SetUpdatesProvider sets the updates provider for Docker container updates
func (e *PulseToolExecutor) SetUpdatesProvider(provider UpdatesProvider) {
e.updatesProvider = provider
}
// SetIncidentRecorderProvider sets the incident recorder provider
func (e *PulseToolExecutor) SetIncidentRecorderProvider(provider IncidentRecorderProvider) {
e.incidentRecorderProvider = provider
}
// SetEventCorrelatorProvider sets the event correlator provider
func (e *PulseToolExecutor) SetEventCorrelatorProvider(provider EventCorrelatorProvider) {
e.eventCorrelatorProvider = provider
}
// SetKnowledgeStoreProvider sets the knowledge store provider for notes
func (e *PulseToolExecutor) SetKnowledgeStoreProvider(provider KnowledgeStoreProvider) {
e.knowledgeStoreProvider = provider
}
// SetDiscoveryProvider sets the discovery provider for infrastructure discovery
func (e *PulseToolExecutor) SetDiscoveryProvider(provider DiscoveryProvider) {
e.discoveryProvider = provider
}
// SetUnifiedResourceProvider sets the unified resource provider
func (e *PulseToolExecutor) SetUnifiedResourceProvider(provider UnifiedResourceProvider) {
e.unifiedResourceProvider = provider
}
// SetAppContainerActionProvider sets the provider used for canonical native
// app-container control actions.
func (e *PulseToolExecutor) SetAppContainerActionProvider(provider AppContainerActionProvider) {
e.appContainerActionProvider = provider
}
// SetAppContainerReadProvider sets the provider used for canonical native
// app-container read-only diagnostics.
func (e *PulseToolExecutor) SetAppContainerReadProvider(provider AppContainerReadProvider) {
e.appContainerReadProvider = provider
}
// SetActionAuditStore sets the durable store used to persist action audit and lifecycle events.
func (e *PulseToolExecutor) SetActionAuditStore(store unifiedresources.ResourceStore) {
e.actionAuditStore = store
}
// GetActionAuditStore returns the durable store used to persist action audit and lifecycle events.
func (e *PulseToolExecutor) GetActionAuditStore() unifiedresources.ResourceStore {
return e.actionAuditStore
}
// SetResolvedContext sets the session-scoped resolved context for resource validation.
// This should be called by the agentic loop before executing tools for a session.
func (e *PulseToolExecutor) SetResolvedContext(ctx ResolvedContextProvider) {
e.resolvedContext = ctx
}
// SetTelemetryCallback sets the telemetry callback for recording metrics
func (e *PulseToolExecutor) SetTelemetryCallback(cb TelemetryCallback) {
e.telemetryCallback = cb
}
// SetPatrolFindingCreator sets (or clears) the patrol finding creator.
// This must be set before a patrol run and cleared after.
func (e *PulseToolExecutor) SetPatrolFindingCreator(creator PatrolFindingCreator) {
e.patrolFindingCreatorMu.Lock()
e.patrolFindingCreator = creator
e.patrolFindingCreatorMu.Unlock()
}
// GetPatrolFindingCreator returns the current patrol finding creator (may be nil).
func (e *PulseToolExecutor) GetPatrolFindingCreator() PatrolFindingCreator {
e.patrolFindingCreatorMu.RLock()
defer e.patrolFindingCreatorMu.RUnlock()
return e.patrolFindingCreator
}
// GetResolvedContext returns the current resolved context (may be nil)
func (e *PulseToolExecutor) GetResolvedContext() ResolvedContextProvider {
return e.resolvedContext
}
// ListTools returns the list of available tools
func (e *PulseToolExecutor) ListTools() []Tool {
tools := e.registry.ListTools(e.controlLevel)
if len(tools) == 0 {
return tools
}
available := make([]Tool, 0, len(tools))
for _, tool := range tools {
if e.isToolAvailable(tool.Name) {
available = append(available, tool)
}
}
return available
}
// ListToolGovernance returns the governed manifest for currently available tools.
func (e *PulseToolExecutor) ListToolGovernance() []ToolGovernanceDescriptor {
tools := e.registry.ListToolGovernance(e.controlLevel)
if len(tools) == 0 {
return tools
}
available := make([]ToolGovernanceDescriptor, 0, len(tools))
for _, tool := range tools {
if e.isToolAvailable(tool.Name) {
available = append(available, tool)
}
}
return available
}
func (e *PulseToolExecutor) isToolAvailable(name string) bool {
switch name {
// Check tool availability based on primary requirements
case "pulse_query":
return e.hasReadState()
case "pulse_metrics":
return e.hasReadState() || e.metricsHistory != nil || e.baselineProvider != nil || e.patternProvider != nil
case "pulse_storage":
return e.hasReadState() || e.unifiedResourceProvider != nil || e.backupProvider != nil || e.diskHealthProvider != nil || e.recoveryPointsProvider != nil
case "pulse_docker":
return e.hasReadState() || e.updatesProvider != nil
case "pulse_kubernetes":
return e.hasCanonicalReadState()
case "pulse_alerts":
return e.alertProvider != nil || e.findingsProvider != nil || e.findingsManager != nil || e.hasReadState()
case "pulse_read":
return e.agentServer != nil || (e.appContainerReadProvider != nil && e.hasReadState())
case "pulse_control":
return (e.agentServer != nil || e.appContainerActionProvider != nil) && e.hasReadState()
case "pulse_file_edit":
return e.agentServer != nil
case "pulse_discovery":
return e.discoveryProvider != nil
case "pulse_knowledge":
return e.knowledgeStoreProvider != nil || e.incidentRecorderProvider != nil || e.eventCorrelatorProvider != nil
case "pulse_pmg":
return e.hasReadState()
case "patrol_report_finding", "patrol_resolve_finding", "patrol_get_findings":
// Always available when registered; handler checks patrolFindingCreator at runtime
return e.GetPatrolFindingCreator() != nil
default:
return e.hasReadState()
}
}
// ExecuteTool executes a tool and returns the result
func (e *PulseToolExecutor) ExecuteTool(ctx context.Context, name string, args map[string]interface{}) (CallToolResult, error) {
log.Debug().
Str("tool", name).
Interface("args", args).
Msg("Executing Pulse tool")
return e.registry.Execute(ctx, e, name, args)
}
// registerTools registers all available tools
func (e *PulseToolExecutor) registerTools() {
// All tools registered below (12 tools total)
// pulse_query - search, get, config, topology, list, health
e.registerQueryTools()
// pulse_metrics - performance, temperatures, network, diskio, disks, baselines, patterns
e.registerMetricsTools()
// pulse_storage - pools, config, backups, snapshots, ceph, replication, pbs_jobs, raid, disk_health, resource_disks
e.registerStorageTools()
// pulse_docker - control, updates, check_updates, update, services, tasks, swarm
e.registerDockerTools()
// pulse_kubernetes - clusters, nodes, pods, deployments
e.registerKubernetesTools()
// pulse_alerts - list, findings, resolved, resolve, dismiss
e.registerAlertsTools()
// pulse_read - read-only operations (exec, file, find, tail, logs)
// This is ALWAYS classified as ToolKindRead and never triggers VERIFYING
e.registerReadTools()
// pulse_control - guest control, run commands (requires control permission)
// NOTE: For read-only command execution, use pulse_read instead
e.registerControlTools()
// pulse_file_edit - read, append, write files (requires control permission)
e.registerFileTools()
// pulse_discovery - get, list discoveries
e.registerDiscoveryTools()
// pulse_knowledge - remember, recall, incidents, correlate, relationships
e.registerKnowledgeTools()
// pulse_pmg - status, mail_stats, queues, spam
e.registerPMGTools()
// patrol_report_finding, patrol_resolve_finding, patrol_get_findings
// These are always registered but only functional when patrolFindingCreator is set.
e.registerPatrolTools()
}