Pulse/internal/ai/tools/execution_profile.go
2026-08-16 10:14:16 +01:00

107 lines
4.5 KiB
Go

package tools
import (
"fmt"
"github.com/rcourtman/pulse-go-rewrite/internal/agentcapabilities"
)
// ExecutionProfile is the core-owned, request-local execution posture for
// one Assistant/Patrol run. It is deliberately an opaque integer with no
// JSON tags, string round-trip, or public constructor from external input:
// enterprise and transport code can never serialize, select, or relax a
// profile. Profiles are applied by core call sites only (chat service and
// the investigation adapter), and their policy is enforced twice - during
// provider projection and again at registry execution.
type ExecutionProfile int
const (
// ProfileInteractiveAssistant is the default interactive chat
// posture: the operator's configured control level and autonomy
// govern, and the interactive question tool is available.
ProfileInteractiveAssistant ExecutionProfile = iota
// ProfilePatrolDetection is the scheduled Patrol detection posture:
// non-interactive, no infrastructure mutations, and Pulse-state
// mutations restricted to the finding lifecycle and observer-proposal tools
// (patrol_report_finding / patrol_assess_finding /
// patrol_propose_observer). The legacy direct-resolve alias is deliberately
// absent: all existing-finding closure crosses the assessed lifecycle.
ProfilePatrolDetection
// ProfilePatrolInvestigation is the Patrol investigation posture:
// non-interactive and structurally read-only - no infrastructure and
// no Pulse-state mutations. Typed remediation leaves the profile only
// as a side-effect-free, mutation-none action proposal.
ProfilePatrolInvestigation
)
// Valid reports whether the profile is one of the closed set. Like the
// invocation-class vocabulary, execution profiles are a closed
// vocabulary: an unknown value must be rejected, never silently treated
// as the permissive interactive default.
func (p ExecutionProfile) Valid() bool {
switch p {
case ProfileInteractiveAssistant, ProfilePatrolDetection, ProfilePatrolInvestigation:
return true
default:
return false
}
}
// NonInteractive reports whether the profile forbids interactive user
// input. This is deliberately independent of autonomous mode: suppressing
// questions grants no mutation authority, and mutation policy never
// loosens because a run is interactive. Unknown profiles are treated as
// non-interactive: an unclassifiable posture must never gain the
// interactive default's permissions.
func (p ExecutionProfile) NonInteractive() bool {
return p != ProfileInteractiveAssistant
}
// patrolDetectionPulseStateAllowlist names the only tools whose
// pulse-state mutations the detection profile permits. A blanket
// pulse-state allowance would also permit alert dismissal and knowledge
// writes, which detection has no business performing.
func patrolDetectionPulseStateAllowlist() map[string]bool {
return map[string]bool{
agentcapabilities.PatrolAssessFindingToolName: true,
agentcapabilities.PatrolProposeObserverToolName: true,
agentcapabilities.PatrolReportFindingToolName: true,
}
}
// ApplyExecutionProfile applies the profile's policy to this executor
// instance (normally a request-scoped clone). Both Patrol profiles deny
// infrastructure mutations, clear any inherited autonomous mode, and mark
// the executor non-interactive; they differ only in pulse-state policy.
// Unknown profiles are rejected outright (panic on programmer error)
// rather than falling through to the interactive default's permissions.
func (e *PulseToolExecutor) ApplyExecutionProfile(profile ExecutionProfile) {
if !profile.Valid() {
panic(fmt.Sprintf("unknown execution profile %d: profiles are a closed vocabulary and cannot default to interactive permissions", int(profile)))
}
e.executionProfile = profile
switch profile {
case ProfilePatrolDetection:
e.isAutonomous = false
e.denyInfrastructureMutations = true
e.pulseStateAllowlist = patrolDetectionPulseStateAllowlist()
case ProfilePatrolInvestigation:
e.isAutonomous = false
e.denyInfrastructureMutations = true
e.pulseStateAllowlist = map[string]bool{}
default:
e.denyInfrastructureMutations = false
// Interactive Assistant is conversation/read/session authority.
// Finding and knowledge lifecycle writes remain explicit product/API
// operations and are never model-invokable.
e.pulseStateAllowlist = map[string]bool{}
}
}
// ExecutionProfile returns the profile applied to this executor instance.
func (e *PulseToolExecutor) ExecutionProfile() ExecutionProfile {
if e == nil {
return ProfileInteractiveAssistant
}
return e.executionProfile
}