Files
pulse/internal/ai/tools/tools_kubernetes.go
T
rcourtman 72945d04f4 Harden Pulse Assistant approval planning
Persist approval requests as governed planned actions with context confidence and stream that plan into Pulse Assistant approvals.
Reuse the planned action identity when approved actions execute so the unified audit trail stays continuous.
2026-04-23 23:36:57 +01:00

978 lines
31 KiB
Go

package tools
import (
"context"
"encoding/json"
"fmt"
"strings"
"github.com/rcourtman/pulse-go-rewrite/internal/agentexec"
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
)
type kubernetesClusterTarget struct {
ID string
AgentID string
Name string
DisplayName string
Server string
Context string
Version string
Status string
}
// registerKubernetesTools registers the pulse_kubernetes tool
func (e *PulseToolExecutor) registerKubernetesTools() {
e.registry.Register(RegisteredTool{
Definition: Tool{
Name: "pulse_kubernetes",
Description: `Query and control Kubernetes clusters, nodes, pods, and deployments. Query: clusters, nodes, pods, deployments. Control: scale, restart, delete_pod, exec, logs.`,
InputSchema: InputSchema{
Type: "object",
Properties: map[string]PropertySchema{
"type": {
Type: "string",
Description: "Operation type",
Enum: []string{"clusters", "nodes", "pods", "deployments", "scale", "restart", "delete_pod", "exec", "logs"},
},
"cluster": {
Type: "string",
Description: "Cluster name or ID",
},
"namespace": {
Type: "string",
Description: "Kubernetes namespace (default: 'default')",
},
"deployment": {
Type: "string",
Description: "Deployment name (for scale, restart)",
},
"pod": {
Type: "string",
Description: "Pod name (for delete_pod, exec, logs)",
},
"container": {
Type: "string",
Description: "Container name (for exec, logs - uses first container if omitted)",
},
"command": {
Type: "string",
Description: "Command to execute (for exec)",
},
"replicas": {
Type: "integer",
Description: "Desired replica count (for scale)",
},
"lines": {
Type: "integer",
Description: "Number of log lines to return (for logs, default: 100)",
},
"status": {
Type: "string",
Description: "Filter by pod phase: Running, Pending, Failed, Succeeded (for pods)",
},
"limit": {
Type: "integer",
Description: "Maximum number of results (default: 100)",
},
"offset": {
Type: "integer",
Description: "Number of results to skip",
},
},
Required: []string{"type"},
},
},
Handler: func(ctx context.Context, exec *PulseToolExecutor, args map[string]interface{}) (CallToolResult, error) {
return exec.executeKubernetes(ctx, args)
},
Governance: ToolGovernance{
ActionMode: ToolActionMixed,
ApprovalPolicy: "read/list/log actions are safe; scale, restart, delete, and exec subactions require approval in controlled mode",
Summary: "Reads Kubernetes topology and runs governed workload-control subactions.",
},
})
}
// executeKubernetes routes to the appropriate kubernetes handler based on type
func (e *PulseToolExecutor) executeKubernetes(ctx context.Context, args map[string]interface{}) (CallToolResult, error) {
resourceType, _ := args["type"].(string)
switch resourceType {
case "clusters":
return e.executeGetKubernetesClusters(ctx)
case "nodes":
return e.executeGetKubernetesNodes(ctx, args)
case "pods":
return e.executeGetKubernetesPods(ctx, args)
case "deployments":
return e.executeGetKubernetesDeployments(ctx, args)
// Control operations
case "scale":
return e.executeKubernetesScale(ctx, args)
case "restart":
return e.executeKubernetesRestart(ctx, args)
case "delete_pod":
return e.executeKubernetesDeletePod(ctx, args)
case "exec":
return e.executeKubernetesExec(ctx, args)
case "logs":
return e.executeKubernetesLogs(ctx, args)
default:
return NewErrorResult(fmt.Errorf("unknown type: %s. Use: clusters, nodes, pods, deployments, scale, restart, delete_pod, exec, logs", resourceType)), nil
}
}
func (e *PulseToolExecutor) kubernetesReadState() (unifiedresources.ReadState, error) {
if rs := e.getCanonicalReadState(); rs != nil {
return rs, nil
}
return nil, fmt.Errorf("read state not available")
}
func matchKubernetesCluster(cluster *unifiedresources.K8sClusterView, clusterArg string) bool {
if cluster == nil {
return false
}
return cluster.ID() == clusterArg ||
cluster.ClusterID() == clusterArg ||
cluster.Name() == clusterArg ||
cluster.ClusterName() == clusterArg ||
cluster.SourceName() == clusterArg
}
func newKubernetesClusterTarget(cluster *unifiedresources.K8sClusterView) *kubernetesClusterTarget {
if cluster == nil {
return nil
}
name := strings.TrimSpace(cluster.SourceName())
if name == "" {
name = strings.TrimSpace(cluster.Name())
}
displayName := strings.TrimSpace(cluster.ClusterName())
if displayName == "" {
displayName = name
}
if displayName == "" {
displayName = strings.TrimSpace(cluster.ID())
}
clusterID := strings.TrimSpace(cluster.ClusterID())
if clusterID == "" {
clusterID = strings.TrimSpace(cluster.ID())
}
return &kubernetesClusterTarget{
ID: clusterID,
AgentID: cluster.AgentID(),
Name: name,
DisplayName: displayName,
Server: cluster.Server(),
Context: cluster.Context(),
Version: cluster.Version(),
Status: string(cluster.Status()),
}
}
func findKubernetesCluster(rs unifiedresources.ReadState, clusterArg string) *unifiedresources.K8sClusterView {
for _, cluster := range rs.K8sClusters() {
if matchKubernetesCluster(cluster, clusterArg) {
return cluster
}
}
return nil
}
func (e *PulseToolExecutor) executeGetKubernetesClusters(_ context.Context) (CallToolResult, error) {
rs, err := e.kubernetesReadState()
if err != nil {
return NewErrorResult(err), nil
}
clusters := rs.K8sClusters()
if len(clusters) == 0 {
return NewTextResult("No Kubernetes clusters found. Kubernetes monitoring may not be configured."), nil
}
var summaries []KubernetesClusterSummary
for _, c := range clusters {
var clusterNodes []unifiedresources.K8sNodeView
for _, node := range rs.K8sNodes() {
if node.ParentID() == c.ID() {
clusterNodes = append(clusterNodes, *node)
}
}
readyNodes := 0
for _, node := range clusterNodes {
if node.Ready() {
readyNodes++
}
}
podCount := 0
for _, pod := range rs.Pods() {
if pod.ParentID() == c.ID() {
podCount++
}
}
depCount := 0
for _, dep := range rs.K8sDeployments() {
if dep.ParentID() == c.ID() {
depCount++
}
}
summaries = append(summaries, KubernetesClusterSummary{
ID: c.ID(),
Name: c.SourceName(),
DisplayName: c.ClusterName(),
Server: c.Server(),
Version: c.Version(),
Status: string(c.Status()),
NodeCount: len(clusterNodes),
PodCount: podCount,
DeploymentCount: depCount,
ReadyNodes: readyNodes,
})
}
response := EmptyKubernetesClustersResponse()
response.Clusters = summaries
response.Total = len(summaries)
return NewJSONResult(response.NormalizeCollections()), nil
}
func (e *PulseToolExecutor) executeGetKubernetesNodes(_ context.Context, args map[string]interface{}) (CallToolResult, error) {
clusterArg, _ := args["cluster"].(string)
if clusterArg == "" {
return NewErrorResult(fmt.Errorf("cluster is required")), nil
}
rs, err := e.kubernetesReadState()
if err != nil {
return NewErrorResult(err), nil
}
clusterView := findKubernetesCluster(rs, clusterArg)
if clusterView == nil {
return NewTextResult(fmt.Sprintf("Kubernetes cluster '%s' not found.", clusterArg)), nil
}
cluster := newKubernetesClusterTarget(clusterView)
var nodes []KubernetesNodeSummary
for _, node := range rs.K8sNodes() {
if node.ParentID() != clusterView.ID() {
continue
}
nodes = append(nodes, KubernetesNodeSummary{
UID: node.NodeUID(),
Name: node.NodeName(),
Ready: node.Ready(),
Unschedulable: node.Unschedulable(),
Roles: node.Roles(),
KubeletVersion: node.KubeletVersion(),
ContainerRuntimeVersion: node.ContainerRuntimeVersion(),
OSImage: node.OSImage(),
Architecture: node.Architecture(),
CapacityCPU: node.CapacityCPU(),
CapacityMemoryBytes: node.CapacityMemoryBytes(),
CapacityPods: node.CapacityPods(),
AllocatableCPU: node.AllocCPU(),
AllocatableMemoryBytes: node.AllocMemoryBytes(),
AllocatablePods: node.AllocPods(),
})
}
response := EmptyKubernetesNodesResponse()
response.Cluster = cluster.DisplayName
response.Nodes = nodes
response.Total = len(nodes)
return NewJSONResult(response.NormalizeCollections()), nil
}
func (e *PulseToolExecutor) executeGetKubernetesPods(_ context.Context, args map[string]interface{}) (CallToolResult, error) {
clusterArg, _ := args["cluster"].(string)
if clusterArg == "" {
return NewErrorResult(fmt.Errorf("cluster is required")), nil
}
namespaceFilter, _ := args["namespace"].(string)
statusFilter, _ := args["status"].(string)
limit := intArg(args, "limit", 100)
offset := intArg(args, "offset", 0)
rs, err := e.kubernetesReadState()
if err != nil {
return NewErrorResult(err), nil
}
clusterView := findKubernetesCluster(rs, clusterArg)
if clusterView == nil {
return NewTextResult(fmt.Sprintf("Kubernetes cluster '%s' not found.", clusterArg)), nil
}
cluster := newKubernetesClusterTarget(clusterView)
var pods []KubernetesPodSummary
totalClusterPods := 0
filteredCount := 0
for _, pod := range rs.Pods() {
if pod.ParentID() != clusterView.ID() {
continue
}
totalClusterPods++
if namespaceFilter != "" && pod.Namespace() != namespaceFilter {
continue
}
if statusFilter != "" && !strings.EqualFold(pod.PodPhase(), statusFilter) {
continue
}
filteredCount++
}
currentFilteredIndex := 0
addedCount := 0
for _, pod := range rs.Pods() {
if pod.ParentID() != clusterView.ID() {
continue
}
if namespaceFilter != "" && pod.Namespace() != namespaceFilter {
continue
}
if statusFilter != "" && !strings.EqualFold(pod.PodPhase(), statusFilter) {
continue
}
if currentFilteredIndex < offset {
currentFilteredIndex++
continue
}
if addedCount >= limit {
break
}
currentFilteredIndex++
addedCount++
var containers []KubernetesPodContainerSummary
pods = append(pods, KubernetesPodSummary{
UID: pod.PodUID(),
Name: pod.Name(),
Namespace: pod.Namespace(),
NodeName: "",
Phase: pod.PodPhase(),
Reason: "",
Restarts: pod.Restarts(),
QoSClass: "",
OwnerKind: pod.OwnerKind(),
OwnerName: pod.OwnerName(),
Containers: containers,
})
}
response := EmptyKubernetesPodsResponse()
response.Cluster = cluster.DisplayName
response.Pods = pods
response.Total = totalClusterPods
response.Filtered = filteredCount
return NewJSONResult(response.NormalizeCollections()), nil
}
func (e *PulseToolExecutor) executeGetKubernetesDeployments(_ context.Context, args map[string]interface{}) (CallToolResult, error) {
clusterArg, _ := args["cluster"].(string)
if clusterArg == "" {
return NewErrorResult(fmt.Errorf("cluster is required")), nil
}
namespaceFilter, _ := args["namespace"].(string)
limit := intArg(args, "limit", 100)
offset := intArg(args, "offset", 0)
rs, err := e.kubernetesReadState()
if err != nil {
return NewErrorResult(err), nil
}
clusterView := findKubernetesCluster(rs, clusterArg)
if clusterView == nil {
return NewTextResult(fmt.Sprintf("Kubernetes cluster '%s' not found.", clusterArg)), nil
}
cluster := newKubernetesClusterTarget(clusterView)
var deployments []KubernetesDeploymentSummary
totalClusterDeployments := 0
for _, dep := range rs.K8sDeployments() {
if dep.ParentID() == clusterView.ID() {
totalClusterDeployments++
}
}
filteredCount := 0
currentFilteredIndex := 0
for _, dep := range rs.K8sDeployments() {
if dep.ParentID() != clusterView.ID() {
continue
}
if namespaceFilter != "" && dep.Namespace() != namespaceFilter {
continue
}
filteredCount++
if currentFilteredIndex < offset {
currentFilteredIndex++
continue
}
if len(deployments) >= limit {
break
}
currentFilteredIndex++
deployments = append(deployments, KubernetesDeploymentSummary{
UID: dep.DeploymentUID(),
Name: dep.Name(),
Namespace: dep.Namespace(),
DesiredReplicas: dep.DesiredReplicas(),
UpdatedReplicas: dep.UpdatedReplicas(),
ReadyReplicas: dep.ReadyReplicas(),
AvailableReplicas: dep.AvailableReplicas(),
})
}
response := EmptyKubernetesDeploymentsResponse()
response.Cluster = cluster.DisplayName
response.Deployments = deployments
response.Total = totalClusterDeployments
response.Filtered = filteredCount
return NewJSONResult(response.NormalizeCollections()), nil
}
// ========== Kubernetes Control Operations ==========
// findAgentForKubernetesCluster finds the agent for a Kubernetes cluster.
func (e *PulseToolExecutor) findAgentForKubernetesCluster(clusterArg string) (string, *kubernetesClusterTarget, error) {
rs, err := e.kubernetesReadState()
if err != nil {
return "", nil, fmt.Errorf("state not available: %w", err)
}
cluster := newKubernetesClusterTarget(findKubernetesCluster(rs, clusterArg))
if cluster == nil {
return "", nil, fmt.Errorf("kubernetes cluster '%s' not found", clusterArg)
}
if cluster.AgentID == "" {
return "", nil, fmt.Errorf("cluster '%s' has no agent configured - kubectl commands cannot be executed", clusterArg)
}
return cluster.AgentID, cluster, nil
}
// validateKubernetesResourceID validates a Kubernetes resource identifier (namespace, pod, deployment, container names)
func validateKubernetesResourceID(value string) error {
if value == "" {
return fmt.Errorf("value cannot be empty")
}
// Kubernetes resource names must be valid DNS subdomains: lowercase, alphanumeric, '-' and '.'
// Max 253 characters
if len(value) > 253 {
return fmt.Errorf("value too long (max 253 characters)")
}
for _, c := range value {
if !((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '-' || c == '.') {
return fmt.Errorf("invalid character '%c' in resource name", c)
}
}
return nil
}
// buildKubectlExecCommand builds a kubectl exec command safely.
// It runs the command via "sh -c" inside the pod and shell-escapes all
// user-controlled values to prevent host shell injection.
func buildKubectlExecCommand(namespace, pod, container, command string) string {
base := fmt.Sprintf("kubectl -n %s exec %s", shellEscape(namespace), shellEscape(pod))
if container != "" {
base += fmt.Sprintf(" -c %s", shellEscape(container))
}
return base + fmt.Sprintf(" -- sh -c %s", shellEscape(command))
}
// executeKubernetesScale scales a deployment
func (e *PulseToolExecutor) executeKubernetesScale(ctx context.Context, args map[string]interface{}) (CallToolResult, error) {
clusterArg, _ := args["cluster"].(string)
namespace, _ := args["namespace"].(string)
deployment, _ := args["deployment"].(string)
replicas := intArg(args, "replicas", -1)
approvalID, _ := args["_approval_id"].(string)
approvalID = strings.TrimSpace(approvalID)
if clusterArg == "" {
return NewErrorResult(fmt.Errorf("cluster is required")), nil
}
if deployment == "" {
return NewErrorResult(fmt.Errorf("deployment is required")), nil
}
if replicas < 0 {
return NewErrorResult(fmt.Errorf("replicas is required and must be >= 0")), nil
}
if namespace == "" {
namespace = "default"
}
// Validate identifiers
if err := validateKubernetesResourceID(namespace); err != nil {
return NewErrorResult(fmt.Errorf("invalid namespace: %w", err)), nil
}
if err := validateKubernetesResourceID(deployment); err != nil {
return NewErrorResult(fmt.Errorf("invalid deployment: %w", err)), nil
}
// Check control level
if e.controlLevel == ControlLevelReadOnly {
return NewTextResult("Kubernetes control operations are not available in read-only mode."), nil
}
agentID, cluster, err := e.findAgentForKubernetesCluster(clusterArg)
if err != nil {
return NewTextResult(err.Error()), nil
}
// Build command
command := fmt.Sprintf("kubectl -n %s scale deployment %s --replicas=%d", shellEscape(namespace), shellEscape(deployment), replicas)
clusterScope := cluster.ID
if clusterScope == "" {
clusterScope = clusterArg
}
approvalTargetID := fmt.Sprintf("%s:%s:deployment:%s", clusterScope, namespace, deployment)
// Check if pre-approved (validated + single-use).
preApproved := consumeApprovalWithValidation(args, e.orgID, command, "kubernetes", approvalTargetID)
requiresApproval := !e.isAutonomous && e.controlLevel == ControlLevelControlled
// Request approval if needed
if !preApproved && !e.isAutonomous && e.controlLevel == ControlLevelControlled {
displayName := cluster.DisplayName
approvalID := e.createApprovalRecord(command, "kubernetes", approvalTargetID, displayName, fmt.Sprintf("Scale deployment %s to %d replicas", deployment, replicas))
return NewTextResult(formatKubernetesApprovalNeeded("scale", deployment, namespace, displayName, command, approvalID)), nil
}
if e.agentServer == nil {
return NewErrorResult(fmt.Errorf("no agent server available")), nil
}
result, err := e.executeCommandWithAudit(
ctx,
"pulse_kubernetes",
fmt.Sprintf("%s:%s:deployment:%s", clusterScope, namespace, deployment),
approvalID,
requiresApproval,
agentID,
agentexec.ExecuteCommandPayload{
Command: command,
TargetType: "agent",
TargetID: "",
},
"pulse_kubernetes",
fmt.Sprintf("scale deployment %s to %d replicas", deployment, replicas),
)
if err != nil {
return NewErrorResult(fmt.Errorf("failed to execute kubectl: %w", err)), nil
}
output := result.Stdout
if result.Stderr != "" {
output += "\n" + result.Stderr
}
if result.ExitCode == 0 {
return NewTextResult(fmt.Sprintf("✓ Successfully scaled deployment '%s' to %d replicas in namespace '%s'. Action complete - no verification needed.\n%s", deployment, replicas, namespace, output)), nil
}
return NewTextResult(fmt.Sprintf("kubectl command failed (exit code %d):\n%s", result.ExitCode, output)), nil
}
// executeKubernetesRestart restarts a deployment via rollout restart
func (e *PulseToolExecutor) executeKubernetesRestart(ctx context.Context, args map[string]interface{}) (CallToolResult, error) {
clusterArg, _ := args["cluster"].(string)
namespace, _ := args["namespace"].(string)
deployment, _ := args["deployment"].(string)
approvalID, _ := args["_approval_id"].(string)
approvalID = strings.TrimSpace(approvalID)
if clusterArg == "" {
return NewErrorResult(fmt.Errorf("cluster is required")), nil
}
if deployment == "" {
return NewErrorResult(fmt.Errorf("deployment is required")), nil
}
if namespace == "" {
namespace = "default"
}
// Validate identifiers
if err := validateKubernetesResourceID(namespace); err != nil {
return NewErrorResult(fmt.Errorf("invalid namespace: %w", err)), nil
}
if err := validateKubernetesResourceID(deployment); err != nil {
return NewErrorResult(fmt.Errorf("invalid deployment: %w", err)), nil
}
// Check control level
if e.controlLevel == ControlLevelReadOnly {
return NewTextResult("Kubernetes control operations are not available in read-only mode."), nil
}
agentID, cluster, err := e.findAgentForKubernetesCluster(clusterArg)
if err != nil {
return NewTextResult(err.Error()), nil
}
// Build command
command := fmt.Sprintf("kubectl -n %s rollout restart deployment/%s", shellEscape(namespace), shellEscape(deployment))
clusterScope := cluster.ID
if clusterScope == "" {
clusterScope = clusterArg
}
approvalTargetID := fmt.Sprintf("%s:%s:deployment:%s", clusterScope, namespace, deployment)
// Check if pre-approved (validated + single-use).
preApproved := consumeApprovalWithValidation(args, e.orgID, command, "kubernetes", approvalTargetID)
requiresApproval := !e.isAutonomous && e.controlLevel == ControlLevelControlled
// Request approval if needed
if !preApproved && !e.isAutonomous && e.controlLevel == ControlLevelControlled {
displayName := cluster.DisplayName
approvalID := e.createApprovalRecord(command, "kubernetes", approvalTargetID, displayName, fmt.Sprintf("Restart deployment %s", deployment))
return NewTextResult(formatKubernetesApprovalNeeded("restart", deployment, namespace, displayName, command, approvalID)), nil
}
if e.agentServer == nil {
return NewErrorResult(fmt.Errorf("no agent server available")), nil
}
result, err := e.executeCommandWithAudit(
ctx,
"pulse_kubernetes",
fmt.Sprintf("%s:%s:deployment:%s", clusterScope, namespace, deployment),
approvalID,
requiresApproval,
agentID,
agentexec.ExecuteCommandPayload{
Command: command,
TargetType: "agent",
TargetID: "",
},
"pulse_kubernetes",
fmt.Sprintf("restart deployment %s", deployment),
)
if err != nil {
return NewErrorResult(fmt.Errorf("failed to execute kubectl: %w", err)), nil
}
output := result.Stdout
if result.Stderr != "" {
output += "\n" + result.Stderr
}
if result.ExitCode == 0 {
return NewTextResult(fmt.Sprintf("✓ Successfully initiated rollout restart for deployment '%s' in namespace '%s'. Action complete - pods will restart gradually.\n%s", deployment, namespace, output)), nil
}
return NewTextResult(fmt.Sprintf("kubectl command failed (exit code %d):\n%s", result.ExitCode, output)), nil
}
// executeKubernetesDeletePod deletes a pod
func (e *PulseToolExecutor) executeKubernetesDeletePod(ctx context.Context, args map[string]interface{}) (CallToolResult, error) {
clusterArg, _ := args["cluster"].(string)
namespace, _ := args["namespace"].(string)
pod, _ := args["pod"].(string)
approvalID, _ := args["_approval_id"].(string)
approvalID = strings.TrimSpace(approvalID)
if clusterArg == "" {
return NewErrorResult(fmt.Errorf("cluster is required")), nil
}
if pod == "" {
return NewErrorResult(fmt.Errorf("pod is required")), nil
}
if namespace == "" {
namespace = "default"
}
// Validate identifiers
if err := validateKubernetesResourceID(namespace); err != nil {
return NewErrorResult(fmt.Errorf("invalid namespace: %w", err)), nil
}
if err := validateKubernetesResourceID(pod); err != nil {
return NewErrorResult(fmt.Errorf("invalid pod: %w", err)), nil
}
// Check control level
if e.controlLevel == ControlLevelReadOnly {
return NewTextResult("Kubernetes control operations are not available in read-only mode."), nil
}
agentID, cluster, err := e.findAgentForKubernetesCluster(clusterArg)
if err != nil {
return NewTextResult(err.Error()), nil
}
// Build command
command := fmt.Sprintf("kubectl -n %s delete pod %s", shellEscape(namespace), shellEscape(pod))
clusterScope := cluster.ID
if clusterScope == "" {
clusterScope = clusterArg
}
approvalTargetID := fmt.Sprintf("%s:%s:pod:%s", clusterScope, namespace, pod)
// Check if pre-approved (validated + single-use).
preApproved := consumeApprovalWithValidation(args, e.orgID, command, "kubernetes", approvalTargetID)
requiresApproval := !e.isAutonomous && e.controlLevel == ControlLevelControlled
// Request approval if needed
if !preApproved && !e.isAutonomous && e.controlLevel == ControlLevelControlled {
displayName := cluster.DisplayName
approvalID := e.createApprovalRecord(command, "kubernetes", approvalTargetID, displayName, fmt.Sprintf("Delete pod %s", pod))
return NewTextResult(formatKubernetesApprovalNeeded("delete_pod", pod, namespace, displayName, command, approvalID)), nil
}
if e.agentServer == nil {
return NewErrorResult(fmt.Errorf("no agent server available")), nil
}
result, err := e.executeCommandWithAudit(
ctx,
"pulse_kubernetes",
fmt.Sprintf("%s:%s:pod:%s", clusterScope, namespace, pod),
approvalID,
requiresApproval,
agentID,
agentexec.ExecuteCommandPayload{
Command: command,
TargetType: "agent",
TargetID: "",
},
"pulse_kubernetes",
fmt.Sprintf("delete pod %s", pod),
)
if err != nil {
return NewErrorResult(fmt.Errorf("failed to execute kubectl: %w", err)), nil
}
output := result.Stdout
if result.Stderr != "" {
output += "\n" + result.Stderr
}
if result.ExitCode == 0 {
return NewTextResult(fmt.Sprintf("✓ Successfully deleted pod '%s' in namespace '%s'. If managed by a controller, a new pod will be created.\n%s", pod, namespace, output)), nil
}
return NewTextResult(fmt.Sprintf("kubectl command failed (exit code %d):\n%s", result.ExitCode, output)), nil
}
// executeKubernetesExec executes a command inside a pod
func (e *PulseToolExecutor) executeKubernetesExec(ctx context.Context, args map[string]interface{}) (CallToolResult, error) {
clusterArg, _ := args["cluster"].(string)
namespace, _ := args["namespace"].(string)
pod, _ := args["pod"].(string)
container, _ := args["container"].(string)
command, _ := args["command"].(string)
approvalID, _ := args["_approval_id"].(string)
approvalID = strings.TrimSpace(approvalID)
if clusterArg == "" {
return NewErrorResult(fmt.Errorf("cluster is required")), nil
}
if pod == "" {
return NewErrorResult(fmt.Errorf("pod is required")), nil
}
if command == "" {
return NewErrorResult(fmt.Errorf("command is required")), nil
}
if namespace == "" {
namespace = "default"
}
// Validate identifiers
if err := validateKubernetesResourceID(namespace); err != nil {
return NewErrorResult(fmt.Errorf("invalid namespace: %w", err)), nil
}
if err := validateKubernetesResourceID(pod); err != nil {
return NewErrorResult(fmt.Errorf("invalid pod: %w", err)), nil
}
if container != "" {
if err := validateKubernetesResourceID(container); err != nil {
return NewErrorResult(fmt.Errorf("invalid container: %w", err)), nil
}
}
// Check control level
if e.controlLevel == ControlLevelReadOnly {
return NewTextResult("Kubernetes control operations are not available in read-only mode."), nil
}
agentID, cluster, err := e.findAgentForKubernetesCluster(clusterArg)
if err != nil {
return NewTextResult(err.Error()), nil
}
// Build kubectl command safely to prevent shell metacharacter breakout on the host.
kubectlCmd := buildKubectlExecCommand(namespace, pod, container, command)
clusterScope := cluster.ID
if clusterScope == "" {
clusterScope = clusterArg
}
approvalTargetID := fmt.Sprintf("%s:%s:pod:%s", clusterScope, namespace, pod)
// Check if pre-approved (validated + single-use).
preApproved := consumeApprovalWithValidation(args, e.orgID, kubectlCmd, "kubernetes", approvalTargetID)
requiresApproval := !e.isAutonomous && e.controlLevel == ControlLevelControlled
// Request approval if needed
if !preApproved && !e.isAutonomous && e.controlLevel == ControlLevelControlled {
displayName := cluster.DisplayName
approvalID := e.createApprovalRecord(kubectlCmd, "kubernetes", approvalTargetID, displayName, fmt.Sprintf("Execute command in pod %s", pod))
return NewTextResult(formatKubernetesApprovalNeeded("exec", pod, namespace, displayName, kubectlCmd, approvalID)), nil
}
if e.agentServer == nil {
return NewErrorResult(fmt.Errorf("no agent server available")), nil
}
result, err := e.executeCommandWithAudit(
ctx,
"pulse_kubernetes",
fmt.Sprintf("%s:%s:pod:%s", clusterScope, namespace, pod),
approvalID,
requiresApproval,
agentID,
agentexec.ExecuteCommandPayload{
Command: kubectlCmd,
TargetType: "agent",
TargetID: "",
},
"pulse_kubernetes",
fmt.Sprintf("execute command in pod %s", pod),
)
if err != nil {
return NewErrorResult(fmt.Errorf("failed to execute kubectl: %w", err)), nil
}
output := result.Stdout
if result.Stderr != "" {
output += "\n" + result.Stderr
}
// Always show output explicitly to prevent LLM hallucination
if result.ExitCode == 0 {
if output == "" {
return NewTextResult(fmt.Sprintf("Command executed in pod '%s' (exit code 0).\n\nOutput:\n(no output)", pod)), nil
}
return NewTextResult(fmt.Sprintf("Command executed in pod '%s' (exit code 0).\n\nOutput:\n%s", pod, output)), nil
}
if output == "" {
return NewTextResult(fmt.Sprintf("Command in pod '%s' exited with code %d.\n\nOutput:\n(no output)", pod, result.ExitCode)), nil
}
return NewTextResult(fmt.Sprintf("Command in pod '%s' exited with code %d.\n\nOutput:\n%s", pod, result.ExitCode, output)), nil
}
// executeKubernetesLogs retrieves pod logs
func (e *PulseToolExecutor) executeKubernetesLogs(ctx context.Context, args map[string]interface{}) (CallToolResult, error) {
clusterArg, _ := args["cluster"].(string)
namespace, _ := args["namespace"].(string)
pod, _ := args["pod"].(string)
container, _ := args["container"].(string)
lines := intArg(args, "lines", 100)
if clusterArg == "" {
return NewErrorResult(fmt.Errorf("cluster is required")), nil
}
if pod == "" {
return NewErrorResult(fmt.Errorf("pod is required")), nil
}
if namespace == "" {
namespace = "default"
}
// Validate identifiers
if err := validateKubernetesResourceID(namespace); err != nil {
return NewErrorResult(fmt.Errorf("invalid namespace: %w", err)), nil
}
if err := validateKubernetesResourceID(pod); err != nil {
return NewErrorResult(fmt.Errorf("invalid pod: %w", err)), nil
}
if container != "" {
if err := validateKubernetesResourceID(container); err != nil {
return NewErrorResult(fmt.Errorf("invalid container: %w", err)), nil
}
}
// Logs is a read operation, but still requires a connected agent
agentID, _, err := e.findAgentForKubernetesCluster(clusterArg)
if err != nil {
return NewTextResult(err.Error()), nil
}
// Build kubectl command - logs is read-only so no approval needed
var kubectlCmd string
if container != "" {
kubectlCmd = fmt.Sprintf("kubectl -n %s logs %s -c %s --tail=%d", shellEscape(namespace), shellEscape(pod), shellEscape(container), lines)
} else {
kubectlCmd = fmt.Sprintf("kubectl -n %s logs %s --tail=%d", shellEscape(namespace), shellEscape(pod), lines)
}
if e.agentServer == nil {
return NewErrorResult(fmt.Errorf("no agent server available")), nil
}
result, err := e.agentServer.ExecuteCommand(ctx, agentID, agentexec.ExecuteCommandPayload{
Command: kubectlCmd,
TargetType: "agent",
TargetID: "",
})
if err != nil {
return NewErrorResult(fmt.Errorf("failed to execute kubectl: %w", err)), nil
}
output := result.Stdout
if result.Stderr != "" && result.ExitCode != 0 {
output += "\n" + result.Stderr
}
if result.ExitCode == 0 {
if output == "" {
return NewTextResult(fmt.Sprintf("No logs found for pod '%s' in namespace '%s'", pod, namespace)), nil
}
return NewTextResult(fmt.Sprintf("Logs from pod '%s' (last %d lines):\n%s", pod, lines, output)), nil
}
return NewTextResult(fmt.Sprintf("kubectl logs failed (exit code %d):\n%s", result.ExitCode, output)), nil
}
// formatKubernetesApprovalNeeded formats an approval-required response for Kubernetes operations
func formatKubernetesApprovalNeeded(action, resource, namespace, cluster, command, approvalID string) string {
payload := map[string]interface{}{
"type": "approval_required",
"approval_id": approvalID,
"action": action,
"resource": resource,
"namespace": namespace,
"cluster": cluster,
"command": command,
"how_to_approve": "Click the approval button in the chat to execute this action.",
"do_not_retry": true,
}
payload = enrichApprovalRequiredPayload(payload, approvalID)
b, _ := json.Marshal(payload)
return "APPROVAL_REQUIRED: " + string(b)
}