mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Split tool registration authority and make the registry append-only
Review of 3073a5061 found the remaining registration hole: Register
rejected canonical descriptor overrides but still accepted a canonical
NAME with a nil override, inheriting the canonical descriptor while
replacing the governed handler in the map - an extension could re-register
pulse_read and bypass its execution-intent enforcement.
Registration authority is now split. registerBuiltin is the unexported
construction-time path for canonical Pulse tools: shared descriptor
mandatory, overrides rejected. RegisterExtension - the only path exposed
through PulseToolExecutor.RegisterTool - rejects every canonical tool
name outright and requires the extension to declare its own descriptor.
Both paths are append-only: a name registers exactly once, so no later
registration can swap out an already-governed handler.
Proofs cover the exact bypass (canonical name, nil override), extension
and builtin duplicate rejection, builtin override rejection, and
descriptor-less extension rejection. Tests that previously swapped
handlers by re-registering now use fresh executors per scenario.
Contract prose and source pins updated.
This commit is contained in:
@@ -4601,6 +4601,16 @@ invocation, and recomputes the offered governance action mode, so the
|
||||
offered schema and the enforcement boundary can never disagree.
|
||||
Control-level blocks keep returning the operator guidance message;
|
||||
policy blocks return the shared invocation-blocked result.
|
||||
Registration authority on the Assistant tool registry is split:
|
||||
`registerBuiltin` is the construction-time path for canonical Pulse
|
||||
tools (shared descriptor mandatory, override rejected), while
|
||||
`RegisterExtension` - the only path exposed through
|
||||
`PulseToolExecutor.RegisterTool` - rejects every canonical tool name
|
||||
outright and requires the extension to declare its own descriptor.
|
||||
Registry entries are append-only: a name registers exactly once, so a
|
||||
later registration (with or without a descriptor override) can never
|
||||
replace an already-governed handler such as pulse_read's
|
||||
execution-intent-enforcing exec path.
|
||||
The classification vocabulary is closed: descriptor validation rejects
|
||||
any class outside the known kinds and mutation targets, and
|
||||
`InvocationPolicy.Allows` independently denies unknown mutation targets
|
||||
|
||||
@@ -603,13 +603,17 @@ func TestExecuteCommand_ErrorAndApprovalPaths(t *testing.T) {
|
||||
t.Fatalf("expected error with exit code 1")
|
||||
}
|
||||
|
||||
exec.RegisterTool(tools.RegisteredTool{
|
||||
// Registry entries are append-only, so the approval scenario uses a
|
||||
// fresh executor instead of swapping the handler in place.
|
||||
approvalExec := tools.NewPulseToolExecutor(tools.ExecutorConfig{})
|
||||
approvalExec.RegisterTool(tools.RegisteredTool{
|
||||
Invocation: tools.StaticInvocation(agentcapabilities.ToolCallKindWrite, agentcapabilities.MutationPulseState),
|
||||
Definition: tools.Tool{Name: agentcapabilities.PulseRunCommandToolName},
|
||||
Handler: func(ctx context.Context, exec *tools.PulseToolExecutor, args map[string]interface{}) (tools.CallToolResult, error) {
|
||||
return tools.NewTextResult("APPROVAL_REQUIRED: requires approval"), nil
|
||||
},
|
||||
})
|
||||
svc = &Service{executor: approvalExec}
|
||||
|
||||
_, _, err = svc.ExecuteCommand(context.Background(), "uptime", "")
|
||||
if err == nil {
|
||||
@@ -648,40 +652,34 @@ func TestExecuteCommandUsesSharedResultTextProjection(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestExecuteAssistantTool_ErrorsAndSuccess(t *testing.T) {
|
||||
exec := tools.NewPulseToolExecutor(tools.ExecutorConfig{})
|
||||
exec.RegisterTool(tools.RegisteredTool{
|
||||
Invocation: tools.StaticInvocation(agentcapabilities.ToolCallKindWrite, agentcapabilities.MutationPulseState),
|
||||
Definition: tools.Tool{Name: "test_tool"},
|
||||
Handler: func(ctx context.Context, exec *tools.PulseToolExecutor, args map[string]interface{}) (tools.CallToolResult, error) {
|
||||
return tools.NewErrorResult(context.DeadlineExceeded), nil
|
||||
},
|
||||
// Registry entries are append-only, so each handler scenario runs on
|
||||
// a fresh executor instead of swapping the handler in place.
|
||||
newService := func(handler func(ctx context.Context, exec *tools.PulseToolExecutor, args map[string]interface{}) (tools.CallToolResult, error)) *Service {
|
||||
exec := tools.NewPulseToolExecutor(tools.ExecutorConfig{})
|
||||
exec.RegisterTool(tools.RegisteredTool{
|
||||
Invocation: tools.StaticInvocation(agentcapabilities.ToolCallKindWrite, agentcapabilities.MutationPulseState),
|
||||
Definition: tools.Tool{Name: "test_tool"},
|
||||
Handler: handler,
|
||||
})
|
||||
return &Service{executor: exec}
|
||||
}
|
||||
|
||||
svc := newService(func(ctx context.Context, exec *tools.PulseToolExecutor, args map[string]interface{}) (tools.CallToolResult, error) {
|
||||
return tools.NewErrorResult(context.DeadlineExceeded), nil
|
||||
})
|
||||
|
||||
svc := &Service{executor: exec}
|
||||
|
||||
_, err := svc.ExecuteAssistantTool(context.Background(), "test_tool", map[string]interface{}{})
|
||||
if err == nil {
|
||||
if _, err := svc.ExecuteAssistantTool(context.Background(), "test_tool", map[string]interface{}{}); err == nil {
|
||||
t.Fatalf("expected tool error")
|
||||
}
|
||||
|
||||
exec.RegisterTool(tools.RegisteredTool{
|
||||
Invocation: tools.StaticInvocation(agentcapabilities.ToolCallKindWrite, agentcapabilities.MutationPulseState),
|
||||
Definition: tools.Tool{Name: "test_tool"},
|
||||
Handler: func(ctx context.Context, exec *tools.PulseToolExecutor, args map[string]interface{}) (tools.CallToolResult, error) {
|
||||
return tools.NewTextResult("POLICY_BLOCKED: nope"), nil
|
||||
},
|
||||
svc = newService(func(ctx context.Context, exec *tools.PulseToolExecutor, args map[string]interface{}) (tools.CallToolResult, error) {
|
||||
return tools.NewTextResult("POLICY_BLOCKED: nope"), nil
|
||||
})
|
||||
_, err = svc.ExecuteAssistantTool(context.Background(), "test_tool", map[string]interface{}{})
|
||||
if err == nil {
|
||||
if _, err := svc.ExecuteAssistantTool(context.Background(), "test_tool", map[string]interface{}{}); err == nil {
|
||||
t.Fatalf("expected policy blocked error")
|
||||
}
|
||||
|
||||
exec.RegisterTool(tools.RegisteredTool{
|
||||
Invocation: tools.StaticInvocation(agentcapabilities.ToolCallKindWrite, agentcapabilities.MutationPulseState),
|
||||
Definition: tools.Tool{Name: "test_tool"},
|
||||
Handler: func(ctx context.Context, exec *tools.PulseToolExecutor, args map[string]interface{}) (tools.CallToolResult, error) {
|
||||
return tools.NewTextResult("ok"), nil
|
||||
},
|
||||
svc = newService(func(ctx context.Context, exec *tools.PulseToolExecutor, args map[string]interface{}) (tools.CallToolResult, error) {
|
||||
return tools.NewTextResult("ok"), nil
|
||||
})
|
||||
output, err := svc.ExecuteAssistantTool(context.Background(), "test_tool", map[string]interface{}{})
|
||||
if err != nil || output != "ok" {
|
||||
|
||||
@@ -809,7 +809,7 @@ func (e *PulseToolExecutor) SetProtectedGuests(vmids []string) {
|
||||
|
||||
// RegisterTool allows tests or extensions to add tools at runtime.
|
||||
func (e *PulseToolExecutor) RegisterTool(tool RegisteredTool) {
|
||||
e.registry.Register(tool)
|
||||
e.registry.RegisterExtension(tool)
|
||||
}
|
||||
|
||||
// Runtime setter methods for updating providers after creation
|
||||
|
||||
@@ -189,11 +189,11 @@ func TestPulseToolExecutor_GetReadStatePrefersUnifiedResourceProvider(t *testing
|
||||
|
||||
func TestToolRegistry_ListTools(t *testing.T) {
|
||||
registry := NewToolRegistry()
|
||||
registry.Register(RegisteredTool{
|
||||
registry.RegisterExtension(RegisteredTool{
|
||||
Invocation: StaticInvocation(agentcapabilities.ToolCallKindRead, agentcapabilities.MutationNone),
|
||||
Definition: Tool{Name: "read"},
|
||||
})
|
||||
registry.Register(RegisteredTool{
|
||||
registry.RegisterExtension(RegisteredTool{
|
||||
Invocation: StaticInvocation(agentcapabilities.ToolCallKindWrite, agentcapabilities.MutationInfrastructure),
|
||||
Definition: Tool{Name: "control"},
|
||||
RequireControl: true,
|
||||
@@ -232,7 +232,7 @@ func TestToolRegistry_ListToolsReturnsIndependentDefinitions(t *testing.T) {
|
||||
properties := map[string]PropertySchema{
|
||||
"mode": {Type: "string", Enum: enum},
|
||||
}
|
||||
registry.Register(RegisteredTool{
|
||||
registry.RegisterExtension(RegisteredTool{
|
||||
Invocation: StaticInvocation(agentcapabilities.ToolCallKindWrite, agentcapabilities.MutationPulseState),
|
||||
Definition: Tool{
|
||||
Name: "read",
|
||||
@@ -308,7 +308,7 @@ func TestToolGovernanceUsesSharedAgentCapabilityShape(t *testing.T) {
|
||||
|
||||
func TestToolRegistry_ExecuteControlToolReadOnlyUsesAssistantAndPatrolGuidance(t *testing.T) {
|
||||
registry := NewToolRegistry()
|
||||
registry.Register(RegisteredTool{
|
||||
registry.RegisterExtension(RegisteredTool{
|
||||
Invocation: StaticInvocation(agentcapabilities.ToolCallKindWrite, agentcapabilities.MutationPulseState),
|
||||
Definition: Tool{Name: "control"},
|
||||
RequireControl: true,
|
||||
@@ -335,7 +335,7 @@ func TestToolRegistryExecuteNormalizesSharedToolCallParams(t *testing.T) {
|
||||
registry := NewToolRegistry()
|
||||
var gotArgs map[string]interface{}
|
||||
var gotArgsLenBeforeMutation int
|
||||
registry.Register(RegisteredTool{
|
||||
registry.RegisterExtension(RegisteredTool{
|
||||
Invocation: StaticInvocation(agentcapabilities.ToolCallKindWrite, agentcapabilities.MutationPulseState),
|
||||
Definition: Tool{Name: "test_tool"},
|
||||
Handler: func(ctx context.Context, e *PulseToolExecutor, args map[string]interface{}) (CallToolResult, error) {
|
||||
@@ -368,7 +368,7 @@ func TestToolRegistryExecuteNormalizesSharedToolCallParams(t *testing.T) {
|
||||
assert.True(t, interpreted.IsError)
|
||||
assert.Contains(t, interpreted.Text, "invalid tools/call params: tool name is required")
|
||||
|
||||
registry.Register(RegisteredTool{
|
||||
registry.RegisterExtension(RegisteredTool{
|
||||
Invocation: StaticInvocation(agentcapabilities.ToolCallKindWrite, agentcapabilities.MutationPulseState),
|
||||
Definition: Tool{Name: "empty_args"},
|
||||
Handler: func(ctx context.Context, e *PulseToolExecutor, args map[string]interface{}) (CallToolResult, error) {
|
||||
@@ -387,14 +387,14 @@ func TestToolRegistryExecuteNormalizesSharedToolCallParams(t *testing.T) {
|
||||
func TestToolRegistryExecuteNormalizesSharedToolResult(t *testing.T) {
|
||||
registry := NewToolRegistry()
|
||||
handlerContent := []Content{{Type: "text", Text: "ok"}}
|
||||
registry.Register(RegisteredTool{
|
||||
registry.RegisterExtension(RegisteredTool{
|
||||
Invocation: StaticInvocation(agentcapabilities.ToolCallKindWrite, agentcapabilities.MutationPulseState),
|
||||
Definition: Tool{Name: "read"},
|
||||
Handler: func(ctx context.Context, e *PulseToolExecutor, args map[string]interface{}) (CallToolResult, error) {
|
||||
return CallToolResult{Content: handlerContent}, nil
|
||||
},
|
||||
})
|
||||
registry.Register(RegisteredTool{
|
||||
registry.RegisterExtension(RegisteredTool{
|
||||
Invocation: StaticInvocation(agentcapabilities.ToolCallKindWrite, agentcapabilities.MutationPulseState),
|
||||
Definition: Tool{Name: "empty"},
|
||||
Handler: func(ctx context.Context, e *PulseToolExecutor, args map[string]interface{}) (CallToolResult, error) {
|
||||
|
||||
@@ -164,7 +164,7 @@ func TestRegisterRejectsOverridesForCanonicalToolNames(t *testing.T) {
|
||||
t.Fatal("overriding a canonical tool's descriptor must panic")
|
||||
}
|
||||
}()
|
||||
registry.Register(RegisteredTool{
|
||||
registry.RegisterExtension(RegisteredTool{
|
||||
Definition: Tool{Name: agentcapabilities.PulseControlToolName},
|
||||
Invocation: StaticInvocation(agentcapabilities.ToolCallKindRead, agentcapabilities.MutationNone),
|
||||
})
|
||||
@@ -185,3 +185,73 @@ func TestReadOnlyDockerProjectsAsReadScopeOnly(t *testing.T) {
|
||||
}
|
||||
t.Fatal("pulse_docker missing from read-only governance projection")
|
||||
}
|
||||
|
||||
func TestRegistrationAuthorityIsSplitAndAppendOnly(t *testing.T) {
|
||||
// The bypass this guards: an extension registering a canonical name
|
||||
// WITHOUT a descriptor override previously inherited the canonical
|
||||
// descriptor and silently replaced the governed handler.
|
||||
t.Run("extension cannot claim a canonical name even without an override", func(t *testing.T) {
|
||||
registry := NewToolRegistry()
|
||||
defer func() {
|
||||
if recover() == nil {
|
||||
t.Fatal("registering pulse_read as an extension must panic")
|
||||
}
|
||||
}()
|
||||
registry.RegisterExtension(RegisteredTool{
|
||||
Definition: Tool{Name: agentcapabilities.PulseReadToolName},
|
||||
Handler: func(ctx context.Context, e *PulseToolExecutor, args map[string]interface{}) (CallToolResult, error) {
|
||||
return NewTextResult("hijacked"), nil
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("extension registrations are append-only", func(t *testing.T) {
|
||||
registry := NewToolRegistry()
|
||||
tool := RegisteredTool{
|
||||
Definition: Tool{Name: "extension_tool"},
|
||||
Invocation: StaticInvocation(agentcapabilities.ToolCallKindRead, agentcapabilities.MutationNone),
|
||||
}
|
||||
registry.RegisterExtension(tool)
|
||||
defer func() {
|
||||
if recover() == nil {
|
||||
t.Fatal("duplicate extension registration must panic")
|
||||
}
|
||||
}()
|
||||
registry.RegisterExtension(tool)
|
||||
})
|
||||
|
||||
t.Run("builtin registrations are append-only", func(t *testing.T) {
|
||||
registry := NewToolRegistry()
|
||||
tool := RegisteredTool{Definition: Tool{Name: agentcapabilities.PulseQueryToolName}}
|
||||
registry.registerBuiltin(tool)
|
||||
defer func() {
|
||||
if recover() == nil {
|
||||
t.Fatal("duplicate builtin registration must panic")
|
||||
}
|
||||
}()
|
||||
registry.registerBuiltin(tool)
|
||||
})
|
||||
|
||||
t.Run("builtin rejects descriptor overrides", func(t *testing.T) {
|
||||
registry := NewToolRegistry()
|
||||
defer func() {
|
||||
if recover() == nil {
|
||||
t.Fatal("builtin registration with an override must panic")
|
||||
}
|
||||
}()
|
||||
registry.registerBuiltin(RegisteredTool{
|
||||
Definition: Tool{Name: agentcapabilities.PulseQueryToolName},
|
||||
Invocation: StaticInvocation(agentcapabilities.ToolCallKindRead, agentcapabilities.MutationNone),
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("extension without a descriptor is rejected", func(t *testing.T) {
|
||||
registry := NewToolRegistry()
|
||||
defer func() {
|
||||
if recover() == nil {
|
||||
t.Fatal("extension registration without a descriptor must panic")
|
||||
}
|
||||
}()
|
||||
registry.RegisterExtension(RegisteredTool{Definition: Tool{Name: "undescribed_tool"}})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -119,38 +119,67 @@ func (p InvocationPolicy) Allows(class agentcapabilities.InvocationClass) bool {
|
||||
}
|
||||
}
|
||||
|
||||
// Register adds a tool to the registry. Every registered tool must have a
|
||||
// canonical invocation descriptor whose cases exactly cover the schema
|
||||
// enum of its discriminator; a tool that cannot be classified must not be
|
||||
// registerable, so this panics on programmer error rather than degrading
|
||||
// to an unclassified (and therefore ungovernable) tool.
|
||||
func (r *ToolRegistry) Register(tool RegisteredTool) {
|
||||
// Registration authority is split so extension code can never claim or
|
||||
// replace a canonical tool. registerBuiltin is the construction-time path
|
||||
// for canonical Pulse tools; RegisterExtension is the only path exposed
|
||||
// outside executor construction and rejects every canonical name. Both
|
||||
// paths are append-only: a name registers exactly once, so a later
|
||||
// registration can never swap out an already-governed handler.
|
||||
|
||||
// registerBuiltin adds a canonical Pulse tool during executor
|
||||
// construction. The tool must classify through the shared descriptor
|
||||
// table (no override) and its descriptor must validate against the schema
|
||||
// enum; a tool that cannot be classified must not be registerable, so
|
||||
// this panics on programmer error rather than degrading to an
|
||||
// unclassified (and therefore ungovernable) tool.
|
||||
func (r *ToolRegistry) registerBuiltin(tool RegisteredTool) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
tool.Definition = tool.Definition.NormalizeCollections()
|
||||
name := tool.Definition.Name
|
||||
canonical, isCanonical := agentcapabilities.InvocationDescriptorFor(name)
|
||||
var descriptor agentcapabilities.InvocationDescriptor
|
||||
switch {
|
||||
case isCanonical && tool.Invocation != nil:
|
||||
if tool.Invocation != nil {
|
||||
// A canonical tool name must classify through the shared table;
|
||||
// an override could silently relax its safety classification.
|
||||
panic(fmt.Sprintf("tool %q is canonical; its invocation descriptor comes from agentcapabilities/invocation.go and cannot be overridden", name))
|
||||
case isCanonical:
|
||||
descriptor = canonical
|
||||
case tool.Invocation != nil:
|
||||
descriptor = tool.Invocation.Clone()
|
||||
default:
|
||||
panic(fmt.Sprintf("tool %q is builtin; its invocation descriptor comes from agentcapabilities/invocation.go and cannot be overridden", name))
|
||||
}
|
||||
canonical, isCanonical := agentcapabilities.InvocationDescriptorFor(name)
|
||||
if !isCanonical {
|
||||
panic(fmt.Sprintf("tool %q has no canonical invocation descriptor; declare one in agentcapabilities/invocation.go", name))
|
||||
}
|
||||
r.store(name, tool, canonical)
|
||||
}
|
||||
|
||||
// RegisterExtension adds a non-canonical tool (tests, extensions). It
|
||||
// rejects every canonical tool name outright - even without a descriptor
|
||||
// override, re-registering a canonical name would swap out its governed
|
||||
// handler - and requires the extension to declare its own invocation
|
||||
// descriptor.
|
||||
func (r *ToolRegistry) RegisterExtension(tool RegisteredTool) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
tool.Definition = tool.Definition.NormalizeCollections()
|
||||
name := tool.Definition.Name
|
||||
if _, isCanonical := agentcapabilities.InvocationDescriptorFor(name); isCanonical {
|
||||
panic(fmt.Sprintf("tool %q is a canonical Pulse tool and cannot be registered as an extension", name))
|
||||
}
|
||||
if tool.Invocation == nil {
|
||||
panic(fmt.Sprintf("extension tool %q must declare its own invocation descriptor", name))
|
||||
}
|
||||
r.store(name, tool, tool.Invocation.Clone())
|
||||
}
|
||||
|
||||
// store validates and appends one registration. Callers hold r.mu.
|
||||
func (r *ToolRegistry) store(name string, tool RegisteredTool, descriptor agentcapabilities.InvocationDescriptor) {
|
||||
if _, exists := r.tools[name]; exists {
|
||||
panic(fmt.Sprintf("tool %q is already registered; registry entries are append-only", name))
|
||||
}
|
||||
if err := descriptor.Validate(name, discriminatorEnum(tool.Definition, descriptor.Discriminator)); err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
tool.Invocation = &descriptor
|
||||
if _, exists := r.tools[name]; !exists {
|
||||
r.order = append(r.order, name)
|
||||
}
|
||||
r.order = append(r.order, name)
|
||||
r.tools[name] = tool
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
|
||||
// registerAlertsTools registers the pulse_alerts tool
|
||||
func (e *PulseToolExecutor) registerAlertsTools() {
|
||||
e.registry.Register(RegisteredTool{
|
||||
e.registry.registerBuiltin(RegisteredTool{
|
||||
Definition: Tool{
|
||||
Name: agentcapabilities.PulseAlertsToolName,
|
||||
Description: `Manage alerts and AI patrol findings.
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
|
||||
// registerControlTools registers the pulse_control tool
|
||||
func (e *PulseToolExecutor) registerControlTools() {
|
||||
e.registry.Register(RegisteredTool{
|
||||
e.registry.registerBuiltin(RegisteredTool{
|
||||
Definition: Tool{
|
||||
Name: agentcapabilities.PulseControlToolName,
|
||||
Description: `WRITE operations: control canonical resources that explicitly advertise shared Pulse actions (for example Proxmox guests and supported app-containers) or execute state-modifying commands. Some canonical resources are read-only and will reject pulse_control even when their type is vm or system-container. Read-only and Docker-only workflows are exposed through their own governed tools.`,
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
// registerDiscoveryTools registers the pulse_discovery tool
|
||||
func (e *PulseToolExecutor) registerDiscoveryTools() {
|
||||
e.registry.Register(RegisteredTool{
|
||||
e.registry.registerBuiltin(RegisteredTool{
|
||||
Definition: Tool{
|
||||
Name: agentcapabilities.PulseDiscoveryToolName,
|
||||
Description: `Get or run AI-discovered service details from Pulse-owned resource access. action="get" returns existing discovery and triggers discovery only when missing. action="run" forces a fresh discovery run for a known resource. action="list" searches existing discoveries. Resource details must already be known from the current context or a prior resource lookup.`,
|
||||
|
||||
@@ -50,7 +50,7 @@ var (
|
||||
|
||||
// registerDockerTools registers the pulse_docker tool
|
||||
func (e *PulseToolExecutor) registerDockerTools() {
|
||||
e.registry.Register(RegisteredTool{
|
||||
e.registry.registerBuiltin(RegisteredTool{
|
||||
Definition: Tool{
|
||||
Name: agentcapabilities.PulseDockerToolName,
|
||||
Description: `Manage Docker containers, updates, and Swarm services. Actions: control, updates, check_updates, update, services, tasks, swarm.`,
|
||||
|
||||
@@ -33,7 +33,7 @@ type ExecutionProvenance struct {
|
||||
|
||||
// registerFileTools registers the file editing tool
|
||||
func (e *PulseToolExecutor) registerFileTools() {
|
||||
e.registry.Register(RegisteredTool{
|
||||
e.registry.registerBuiltin(RegisteredTool{
|
||||
Definition: Tool{
|
||||
Name: agentcapabilities.PulseFileEditToolName,
|
||||
Description: `Edit files on remote hosts, containers, VMs, and Docker containers safely.
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
|
||||
func TestFileTools_Registry(t *testing.T) {
|
||||
exec := NewPulseToolExecutor(ExecutorConfig{})
|
||||
exec.registerFileTools()
|
||||
tools := exec.registry.ListTools(InvocationPolicy{ControlLevel: ControlLevelControlled})
|
||||
|
||||
found := false
|
||||
|
||||
@@ -79,7 +79,7 @@ type KnowledgeEntry struct {
|
||||
|
||||
// registerKnowledgeTools registers the pulse_knowledge tool
|
||||
func (e *PulseToolExecutor) registerKnowledgeTools() {
|
||||
e.registry.Register(RegisteredTool{
|
||||
e.registry.registerBuiltin(RegisteredTool{
|
||||
Definition: Tool{
|
||||
Name: agentcapabilities.PulseKnowledgeToolName,
|
||||
Description: `Manage AI knowledge, notes, and incident analysis.
|
||||
|
||||
@@ -37,7 +37,7 @@ type kubernetesClusterTarget struct {
|
||||
|
||||
// registerKubernetesTools registers the pulse_kubernetes tool
|
||||
func (e *PulseToolExecutor) registerKubernetesTools() {
|
||||
e.registry.Register(RegisteredTool{
|
||||
e.registry.registerBuiltin(RegisteredTool{
|
||||
Definition: Tool{
|
||||
Name: agentcapabilities.PulseKubernetesToolName,
|
||||
Description: `Query and control Kubernetes clusters, nodes, pods, and deployments. Query: clusters, nodes, pods, deployments. Control: scale, restart, delete_pod, exec, logs.`,
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
// registerMetricsTools registers the pulse_metrics tool
|
||||
func (e *PulseToolExecutor) registerMetricsTools() {
|
||||
e.registry.Register(RegisteredTool{
|
||||
e.registry.registerBuiltin(RegisteredTool{
|
||||
Definition: Tool{
|
||||
Name: agentcapabilities.PulseMetricsToolName,
|
||||
Description: `Get performance metrics, baselines, and sensor data.
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
// These tools are only functional during a patrol run when patrolFindingCreator is set.
|
||||
func (e *PulseToolExecutor) registerPatrolTools() {
|
||||
// patrol_report_finding — LLM calls this to create a finding
|
||||
e.registry.Register(RegisteredTool{
|
||||
e.registry.registerBuiltin(RegisteredTool{
|
||||
Definition: Tool{
|
||||
Name: agentcapabilities.PatrolReportFindingToolName,
|
||||
Description: `Report an infrastructure finding discovered during patrol investigation.
|
||||
@@ -89,7 +89,7 @@ Returns: {"ok": true, "finding_id": "...", "is_new": true/false} on success.`,
|
||||
})
|
||||
|
||||
// patrol_resolve_finding — LLM calls this to resolve an active finding
|
||||
e.registry.Register(RegisteredTool{
|
||||
e.registry.registerBuiltin(RegisteredTool{
|
||||
Definition: Tool{
|
||||
Name: agentcapabilities.PatrolResolveFindingToolName,
|
||||
Description: `Resolve an active patrol finding after verifying the issue is no longer present.
|
||||
@@ -121,7 +121,7 @@ Returns: {"ok": true, "resolved": true} on success.`,
|
||||
})
|
||||
|
||||
// patrol_get_findings — LLM calls this to check existing active findings
|
||||
e.registry.Register(RegisteredTool{
|
||||
e.registry.registerBuiltin(RegisteredTool{
|
||||
Definition: Tool{
|
||||
Name: agentcapabilities.PatrolGetFindingsToolName,
|
||||
Description: `Get currently active patrol findings. Use this to check what findings already exist before reporting new ones (avoids duplicates) and to identify findings that may need resolution.
|
||||
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
|
||||
// registerPMGTools registers the pulse_pmg tool
|
||||
func (e *PulseToolExecutor) registerPMGTools() {
|
||||
e.registry.Register(RegisteredTool{
|
||||
e.registry.registerBuiltin(RegisteredTool{
|
||||
Definition: Tool{
|
||||
Name: agentcapabilities.PulsePMGToolName,
|
||||
Description: `Query Proxmox Mail Gateway status and statistics. Types: status, mail_stats, queues, spam.`,
|
||||
|
||||
@@ -2148,7 +2148,7 @@ func logRoutingMismatchDebug(targetHost string, childKinds, childIDs []string) {
|
||||
|
||||
// registerQueryTools registers the pulse_query tool
|
||||
func (e *PulseToolExecutor) registerQueryTools() {
|
||||
e.registry.Register(RegisteredTool{
|
||||
e.registry.registerBuiltin(RegisteredTool{
|
||||
Definition: Tool{
|
||||
Name: agentcapabilities.PulseQueryToolName,
|
||||
Description: `Query and search canonical infrastructure resources. Start here to discover systems, workloads, storage, and disks by name. Actions: search, get, config, topology, list, health.`,
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
// registerReadTools registers the read-only pulse_read tool
|
||||
// This tool is ALWAYS classified as ToolKindRead and will never trigger VERIFYING state
|
||||
func (e *PulseToolExecutor) registerReadTools() {
|
||||
e.registry.Register(RegisteredTool{
|
||||
e.registry.registerBuiltin(RegisteredTool{
|
||||
Definition: Tool{
|
||||
Name: agentcapabilities.PulseReadToolName,
|
||||
Description: `Execute read-only operations on infrastructure (exec, file, find, tail, logs). Rejects write commands. Use target_host for agent-routed reads, or resource_id for API-backed native resource logs such as supported TrueNAS app-containers. When Pulse has attached resource context, use target_host="current_resource" or resource_id="current_resource" for the attached resource instead of copying redacted identifiers.`,
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
|
||||
// registerStorageTools registers the pulse_storage tool
|
||||
func (e *PulseToolExecutor) registerStorageTools() {
|
||||
e.registry.Register(RegisteredTool{
|
||||
e.registry.registerBuiltin(RegisteredTool{
|
||||
Definition: Tool{
|
||||
Name: agentcapabilities.PulseStorageToolName,
|
||||
Description: `Query storage pools, backups, snapshots, Ceph, replication, RAID, and disk health. Use the "type" parameter to select what to query.`,
|
||||
|
||||
@@ -23,7 +23,7 @@ import (
|
||||
// the chat session so this tool can return AI-generated synthesis
|
||||
// in the same shape.
|
||||
func (e *PulseToolExecutor) registerSummarizeTools() {
|
||||
e.registry.Register(RegisteredTool{
|
||||
e.registry.registerBuiltin(RegisteredTool{
|
||||
Definition: Tool{
|
||||
Name: agentcapabilities.PulseSummarizeToolName,
|
||||
Description: `Generate a retrospective summary of one resource or a fleet across a time window. Use this when the operator asks questions like "what's been happening with pve1 this week" or "where should I look across my fleet" — answers grounded in metric stats, alerts, storage state, disk health, and Patrol findings within the window.
|
||||
|
||||
@@ -18559,6 +18559,12 @@ func TestContract_PulseMCPAdapterProjectsAgentCapabilitiesManifest(t *testing.T)
|
||||
`agentcapabilities.NewUnknownToolResult(name)`,
|
||||
`agentcapabilities.NewControlToolsDisabledToolResult()`,
|
||||
`return result.NormalizeCollections(), err`,
|
||||
// Registration authority is split: builtin registration is
|
||||
// construction-only, extensions can never claim or replace a
|
||||
// canonical name, and entries are append-only.
|
||||
`func (r *ToolRegistry) registerBuiltin(tool RegisteredTool)`,
|
||||
`func (r *ToolRegistry) RegisterExtension(tool RegisteredTool)`,
|
||||
`registry entries are append-only`,
|
||||
// Invocation-level enforcement runs before the handler and
|
||||
// consumes the same descriptor the projection filters with.
|
||||
`class = tool.Invocation.Classify(args)`,
|
||||
|
||||
Reference in New Issue
Block a user