Pulse/internal/agentcapabilities/tool_call.go
rcourtman ee8a24e14a backend and governance: MCP contract, agent capabilities, API, and release-control
Manifest-backed MCP tools, prompts, and resources with surface affordance contracts; agent capability manifest and governance projection; API contract tests and capability route projection; operations-loop and intelligence-funnel telemetry; release-control subsystem documentation, registry, and tooling; licensing and configuration.
2026-06-23 17:26:15 +01:00

230 lines
6.9 KiB
Go

package agentcapabilities
import (
"errors"
"fmt"
"strings"
)
// ToolCallParams is the shared request/response tool-call parameter shape used
// by native Pulse Assistant execution and external-agent adapters.
type ToolCallParams struct {
Name string `json:"name"`
Arguments map[string]any `json:"arguments,omitempty"`
}
// ToolCallKind classifies a Pulse Intelligence tool call for safety gates and
// workflow transitions. Native Assistant currently uses it for FSM decisions,
// while the vocabulary lives in the shared core so external-agent adapters and
// future surfaces do not grow separate read/write heuristics.
type ToolCallKind int
const (
// ToolCallKindResolve covers discovery/query tools that establish resource
// context and also count as safe verification reads.
ToolCallKindResolve ToolCallKind = iota
// ToolCallKindRead covers read-only tools such as logs, metrics, status,
// and config inspection.
ToolCallKindRead
// ToolCallKindWrite covers tools that can change Pulse state or target-side
// infrastructure state.
ToolCallKindWrite
// ToolCallKindUserInput covers interactive tools that ask the user for
// missing information and do not advance infrastructure workflow state.
ToolCallKindUserInput
)
func (k ToolCallKind) String() string {
switch k {
case ToolCallKindResolve:
return "resolve"
case ToolCallKindRead:
return "read"
case ToolCallKindWrite:
return "write"
case ToolCallKindUserInput:
return "user_input"
default:
return "unknown"
}
}
// NormalizeToolCallParams returns the canonical tool-call parameter shape
// shared by native Assistant execution and external-agent adapters.
func NormalizeToolCallParams(params ToolCallParams) ToolCallParams {
params.Name = strings.TrimSpace(params.Name)
params.Arguments = CloneToolArguments(params.Arguments)
if params.Arguments == nil {
params.Arguments = map[string]any{}
}
return params
}
// ValidateToolCallParams checks the normalized tool-call contract before a
// surface attempts capability lookup or execution.
func ValidateToolCallParams(params ToolCallParams) error {
if strings.TrimSpace(params.Name) == "" {
return fmt.Errorf("tool name is required")
}
return nil
}
// PrepareToolRegistryExecution normalizes and validates the direct in-process
// registry entrypoint used by native Assistant execution. It returns a
// ready-to-return shared error result when the tool-call params contract is
// invalid, so registry surfaces do not duplicate failure text or envelope
// construction.
func PrepareToolRegistryExecution(name string, args map[string]any) (ToolCallParams, ToolResult, bool) {
params := NormalizeToolCallParams(ToolCallParams{
Name: name,
Arguments: args,
})
if err := ValidateToolCallParams(params); err != nil {
return params, NewInvalidToolCallParamsResult(err), false
}
return params, ToolResult{}, true
}
// ClassifyToolCall classifies a provider/registry tool call for safety gates
// and workflow state transitions. Unknown tools default to write so newly
// introduced tools cannot bypass governed-action checks accidentally.
func ClassifyToolCall(toolName string, args map[string]interface{}) ToolCallKind {
action, _ := args["action"].(string)
actionLower := strings.ToLower(action)
operation, _ := args["operation"].(string)
operationLower := strings.ToLower(operation)
switch strings.TrimSpace(toolName) {
case PulseQuestionToolName:
return ToolCallKindUserInput
case PulseQueryToolName, PulseDiscoveryToolName:
return ToolCallKindResolve
case PulseMetricsToolName, PulseStorageToolName, PulsePMGToolName, PulseSummarizeToolName:
return ToolCallKindRead
case PulseAlertsToolName:
switch actionLower {
case "resolve", "dismiss":
return ToolCallKindWrite
default:
return ToolCallKindRead
}
case PulseKubernetesToolName:
switch actionLower {
case "scale", "restart", "delete_pod", "exec":
return ToolCallKindWrite
default:
return ToolCallKindRead
}
case PulseKnowledgeToolName:
switch actionLower {
case "remember", "note", "save":
return ToolCallKindWrite
default:
return ToolCallKindRead
}
case PulseReadToolName, LegacyAssistantFetchURLToolName:
return ToolCallKindRead
case PulseControlToolName:
return ToolCallKindWrite
case PulseDockerToolName:
switch actionLower {
case "control", "update", "check_updates", "trigger_update":
return ToolCallKindWrite
default:
return ToolCallKindRead
}
case PulseFileEditToolName:
switch actionLower {
case "read":
return ToolCallKindRead
case "write", "append":
return ToolCallKindWrite
default:
return ToolCallKindRead
}
case PulseRunCommandToolName, PulseControlGuestToolName, PulseControlDockerToolName,
LegacyAssistantRunCommandToolName, LegacyAssistantSetResourceURLToolName:
return ToolCallKindWrite
case PulseSearchResourcesToolName, PulseGetResourceToolName, PulseGetTopologyToolName,
PulseListInfrastructureToolName, PulseGetConnectionHealthToolName:
return ToolCallKindResolve
case PulseGetDockerLogsToolName, PulseGetPerformanceMetricsToolName,
PulseGetTemperaturesToolName, PulseGetBaselinesToolName, PulseGetPatternsToolName:
return ToolCallKindRead
case PatrolGetFindingsToolName:
return ToolCallKindRead
case PatrolReportFindingToolName, PatrolResolveFindingToolName:
return ToolCallKindWrite
}
if toolCallActionIsWrite(actionLower) || toolCallActionIsWrite(operationLower) {
return ToolCallKindWrite
}
if toolCallActionIsRead(actionLower) || toolCallActionIsRead(operationLower) {
return ToolCallKindRead
}
return ToolCallKindWrite
}
func toolCallActionIsWrite(action string) bool {
switch action {
case "start", "stop", "restart", "delete",
"shutdown", "reboot", "write", "append",
"update", "trigger", "resolve", "dismiss",
"control":
return true
default:
return false
}
}
func toolCallActionIsRead(action string) bool {
switch action {
case "get", "list", "search", "query",
"read", "logs", "status", "health",
"describe", "inspect", "show":
return true
default:
return false
}
}
// NewInvalidToolCallParamsResult returns the stable shared error result for a
// registry tool-call request that fails the shared params contract.
func NewInvalidToolCallParamsResult(err error) ToolResult {
if err == nil {
err = errors.New("tool call params are invalid")
}
return NewToolErrorResult(fmt.Errorf("invalid tools/call params: %w", err))
}
// NewUnknownToolResult returns the stable shared error result for a registry
// tool-call request that names no registered tool.
func NewUnknownToolResult(name string) ToolResult {
return NewToolErrorResult(fmt.Errorf("unknown tool: %s", strings.TrimSpace(name)))
}
// NewControlToolsDisabledToolResult returns the stable shared text result used
// when a direct registry execution attempts a control tool at read-only control
// level.
func NewControlToolsDisabledToolResult() ToolResult {
return NewToolTextResult(ControlToolsDisabledMessage)
}