mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-09 18:15:50 +00:00
Separate command connectivity from monitoring evidence
Name command transport explicitly in shared query results and preserve unobserved connection state in topology and Assistant inventory context. Keep parent-node transport distinct from a direct guest connection so monitoring evidence cannot imply command access or collection downtime. Existing execution policy and approval checks remain authoritative.
This commit is contained in:
@@ -1619,3 +1619,77 @@ so this assessment does not qualify model use of that corrected action.
|
||||
Storage-fault ground truth, reliable diagnosis, approved/rejected actions and
|
||||
independent recovery remain open. The supported autonomous provider dependency
|
||||
and wider independent-Pro-environment gate remain unchanged.
|
||||
|
||||
|
||||
### Command connection evidence correction plan
|
||||
|
||||
Live command connectivity and retained monitoring observations are independent
|
||||
facts. The shared tool contract will name command-agent connections explicitly,
|
||||
including parent-node connections, without changing routing or execution policy.
|
||||
Topology built without a command-connection snapshot must omit connection flags,
|
||||
execution hints and connected counts rather than manufacture false/zero values.
|
||||
An observed empty snapshot still reports disconnected/zero. Assistant inventory
|
||||
context must preserve the same observation boundary. Existing permission,
|
||||
approval and invocation checks remain authoritative.
|
||||
|
||||
Regression matrix: current Docker inventory and metrics with disconnected and
|
||||
connected command transport, read-only control with a connected agent, parent
|
||||
node versus guest connection, topology without a connection observation versus
|
||||
an observed empty set, and Assistant's seeded inventory. Run affected tools/chat
|
||||
packages and focused race proof on pulse-dev.
|
||||
|
||||
Browser matrix after rebuilding the local Pro backend: `/patrol` at 1440x1000,
|
||||
900x1000 and 390x1000, actual captured query results showing disconnected and
|
||||
connected command transport beside unchanged monitored workload evidence.
|
||||
Exercise tool details open/closed, keyboard focus/activation, deepest output
|
||||
scrolling, Escape, reload and persisted result presentation. Inspect pixels and
|
||||
bind receipts to the final source and binary. Controlled rendering proof does
|
||||
not qualify model interpretation, autonomous Patrol or infrastructure actions.
|
||||
|
||||
|
||||
### Command connection evidence qualification
|
||||
|
||||
The original projection failed the new regression because it labelled command
|
||||
transport as generic agent connectivity and emitted connected-agent counts from
|
||||
an inventory-only seed. Canonical guest search also promoted a parent-node
|
||||
connection into a direct guest connection. The shared projection now retains
|
||||
those distinctions. Existing host aliases remain available for non-guest
|
||||
resources. No routing, approval, execution or provider policy boundary changes.
|
||||
|
||||
Four canonical query cases pass: no command connection, connected read-only
|
||||
transport, a direct guest connection without a parent connection, and connected
|
||||
transport with control enabled. Current workload state and CPU remain available
|
||||
in every case and no command is executed. Separate checks prove that topology
|
||||
without a command snapshot omits connection and execution hints and connected
|
||||
counts, while an observed empty snapshot retains false/zero. Assistant inventory
|
||||
context inherits that same unobserved state.
|
||||
|
||||
Final source proof on pulse-dev used Go1.26.8 and GOMAXPROCS4. The full tools
|
||||
package passed in 59.456s and chat in 6.402s. Focused race checks passed in 1.048s
|
||||
and 1.030s. The Pro runtime cross-build passed and the installed local binary
|
||||
SHA256 is `bcaf748107211ee733a6dc0f4d17220d9b4d1ce1918c25bde27cf3d10c0d6379`.
|
||||
The managed development process restarted onto that artifact and `/api/health`
|
||||
reported healthy. No production agent was replaced.
|
||||
|
||||
Playwright exercised nine captured results at `/patrol`, 1440x1000, 900x1000
|
||||
and 390x1000. Inputs, outputs and completed states match exactly before and after
|
||||
controlled session reload. Hover, keyboard focus/activation, expansion/collapse,
|
||||
deepest output scrolling, Escape and session selection passed. Pixel inspection
|
||||
covered each distinct connection state, unchanged workload metrics and restored
|
||||
mobile results. Backend and renderer hashes remained unchanged. The artificial
|
||||
route-check warning and controlled persistence fixtures retain their earlier
|
||||
qualification limits. No model request was part of this proof.
|
||||
A read-only settings check confirms the cached `provider_refusal` still carries
|
||||
its original `2026-09-05T19:46:39Z` timestamp and `patrol_capable=false`.
|
||||
|
||||
Private source bindings, logs, captured outputs, runtime process/health receipts
|
||||
and browser proof are at workspace-relative `tmp/patrol-command-context/`.
|
||||
The change still requires its scoped pre-commit and landing checks. The preceding
|
||||
PR #1935 head `4d302109cee0758a132ff150935630b50114cc05` has no reported failures
|
||||
but its Build and Test and Core E2E runs are pending behind live earlier runs
|
||||
on the same branch. Those workflows are not restarted or cancelled.
|
||||
|
||||
This correction establishes the connection evidence contract, not reliable
|
||||
interpretation. Native configuration-read model use, storage-fault diagnosis,
|
||||
approved/rejected action outcomes and independent recovery remain open, as do
|
||||
the supported autonomous provider dependency and independent-environment gate.
|
||||
|
||||
@@ -10201,7 +10201,7 @@
|
||||
},
|
||||
{
|
||||
"id": "patrol-assistant-customer-outcome-qualification",
|
||||
"summary": "The explicit redesign plan and source-bound receipts remain in docs/qualification/PATROL_ASSISTANT_CUSTOMER_JOURNEY.md. The goal is open. Model judgment owns diagnosis, with observations, hypotheses, proposals, executions and independently verified outcomes kept distinct. The recorded baseline has 127 paid installations, 71 with Patrol enabled, 23 with Assistant calls and fourteen verified resolutions from one installation. Schema17 outcome/provider/cost fields had no adoption. These are not representative success, false-alarm or missed-problem rates. Shared provenance, history, risk, missing-access and continuity corrections landed through PR1928/1929. Proposal-as-proof and proxy-driven diagnostic completion policy were removed. PR1934 merged the tool-ID and transcript-identity correction at 6b0abc3bee9ffa81f6ab298b5b67ee11369688a0 with all checks passing. Its captured-response browser replay preserves seven healthy and sixteen dependency tool records. The two ordinary subscription Assistant cases took 82.835s and 204.384s. Their primary decisions had useful evidence, but storage exclusion, recovery implications and config-not-found errors remain unqualified. Independent Docker fixtures prove injection, negative controls and deadline cleanup, not completed recovery or model/action competence. The current scoped Docker measurement correction preserves per-direction observation presence, separates corrected history from ambiguous legacy rows, removes image-layer ratios from capacity, and carries optional rates through resource/browser conversion. Legacy workload direction availability, Docker-host first-disk history and network presence remain distinct consumer/source follow-ups. The current configuration-read correction resolves identity and placement from canonical inventory, preserves explicit query restrictions, and distinguishes unavailable capability/provider observations from missing resources with the shared tool error bit. Fourteen contract cases, full tools package, targeted race checks and captured-result browser rendering pass. Both corrections are pushed to PR1935 at 355ac1f0a481d6dbc9a7ff3977bced0956711979 with exact worker hooks passing and remote checks pending. A fresh ordinary storage assessment took 177.869s and thirteen successful reads. It identified the actual capacity alert and preserved limited I/O history, but contradictory capacity assurance, unsupported artifact attribution and a misplaced CPU-peak time keep diagnosis unqualified. All thirteen rendered tool records match persistence. Desktop/mobile browser proof and its artificial route-warning limit are recorded in the plan. Command connection versus telemetry freshness and unsupported filter handling remain tool-context review items. Claude Max explicitly refused autonomous Patrol readiness. Cached refusal and API409 enforcement remain intact. Ordinary Assistant is not autonomous qualification. Separate paid-provider approval remains pending and no paid request occurred. Reliable interpretation, real-model retest of config reads, storage/backup and approved/rejected action outcomes remain required local work. Independent volunteered Pro environments remain a separate wider-readiness gate.",
|
||||
"summary": "The explicit redesign plan and source-bound receipts remain in docs/qualification/PATROL_ASSISTANT_CUSTOMER_JOURNEY.md. The goal is open. Model judgment owns diagnosis, with observations, hypotheses, proposals, executions and independently verified outcomes kept distinct. The recorded baseline has 127 paid installations, 71 with Patrol enabled, 23 with Assistant calls and fourteen verified resolutions from one installation. Schema17 outcome/provider/cost fields had no adoption. These are not representative success, false-alarm or missed-problem rates. Shared provenance, history, risk, missing-access and continuity corrections landed through PR1928/1929. Proposal-as-proof and proxy-driven diagnostic completion policy were removed. PR1934 merged the tool-ID and transcript-identity correction at 6b0abc3bee9ffa81f6ab298b5b67ee11369688a0 with all checks passing. Its captured-response browser replay preserves seven healthy and sixteen dependency tool records. The two ordinary subscription Assistant cases took 82.835s and 204.384s. Their primary decisions had useful evidence, but storage exclusion, recovery implications and config-not-found errors remain unqualified. Independent Docker fixtures prove injection, negative controls and deadline cleanup, not completed recovery or model/action competence. The current scoped Docker measurement correction preserves per-direction observation presence, separates corrected history from ambiguous legacy rows, removes image-layer ratios from capacity, and carries optional rates through resource/browser conversion. Legacy workload direction availability, Docker-host first-disk history and network presence remain distinct consumer/source follow-ups. The current configuration-read correction resolves identity and placement from canonical inventory, preserves explicit query restrictions, and distinguishes unavailable capability/provider observations from missing resources with the shared tool error bit. Fourteen contract cases, full tools package, targeted race checks and captured-result browser rendering pass. Both corrections are pushed to PR1935 at 355ac1f0a481d6dbc9a7ff3977bced0956711979 with exact worker hooks passing and remote checks pending. A fresh ordinary storage assessment took 177.869s and thirteen successful reads. It identified the actual capacity alert and preserved limited I/O history, but contradictory capacity assurance, unsupported artifact attribution and a misplaced CPU-peak time keep diagnosis unqualified. All thirteen rendered tool records match persistence. Desktop/mobile browser proof and its artificial route-warning limit are recorded in the plan. The current shared connection projection names command transport explicitly, omits unqueried connection and execution hints/counts from topology and Assistant inventory, and separates parent-node transport from direct guest connectivity. Four canonical connection/control cases, unobserved/empty topology, inventory context, full tools/chat packages, focused race checks and nine captured browser results pass. Scoped landing is pending. Unsupported filter handling remains a tool-context review item. Claude Max explicitly refused autonomous Patrol readiness. Cached refusal and API409 enforcement remain intact. Ordinary Assistant is not autonomous qualification. Separate paid-provider approval remains pending and no paid request occurred. Reliable interpretation, real-model retest of config reads, storage/backup and approved/rejected action outcomes remain required local work. Independent volunteered Pro environments remain a separate wider-readiness gate.",
|
||||
"owner": "project-owner",
|
||||
"status": "planned",
|
||||
"recorded_at": "2026-09-05",
|
||||
|
||||
@@ -25,6 +25,22 @@ that same result. Successful reads retain their content and execution provenance
|
||||
|
||||
## Purpose
|
||||
|
||||
Shared query projections name command transport explicitly through
|
||||
`command_agent_connected`, `node_command_agent_connected` and the corresponding
|
||||
topology counts. These observations do not establish monitoring freshness or
|
||||
installation state. A topology built without a connection snapshot omits command
|
||||
flags, execution hints and connected counts. An observed empty snapshot preserves
|
||||
false/zero. Assistant's inventory seed carries that same absence semantics.
|
||||
The parent node's connection cannot become a direct guest connection merely
|
||||
because provider placement names that node. Existing command routing, control,
|
||||
approval and invocation enforcement remain authoritative. A `can_execute` hint
|
||||
reflects connected transport with control enabled, not approval for an operation.
|
||||
`TestCommandConnectivityDoesNotReplaceMonitoringEvidence`,
|
||||
`TestTopologyOmitsUnobservedCommandConnections` and
|
||||
`TestAssistantInventoryDoesNotInventCommandConnectionObservations` cover these
|
||||
projection and continuity boundaries. Existing persisted tool records are not
|
||||
rewritten, and this contract does not qualify model diagnosis or recovery.
|
||||
|
||||
Native app-container configuration reads resolve identity, provider and placement
|
||||
from current canonical inventory. Optional session discovery cannot fabricate a
|
||||
not-found result or replace current placement with a stale execution target.
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
{
|
||||
"version": 1,
|
||||
"base_sha": "0fcb2ee147354de770dfc4b0b9672d8c2c9dceb2",
|
||||
"verified_at": "2026-09-06T12:13:06.548Z",
|
||||
"base_sha": "4d302109cee0758a132ff150935630b50114cc05",
|
||||
"verified_at": "2026-09-06T12:57:13.096Z",
|
||||
"result": "passed",
|
||||
"changed_paths": [],
|
||||
"content_sha256": {},
|
||||
"backend_content_sha256": {
|
||||
"internal/ai/tools/tools_query.go": "e9411606cdf96dec84882d0649ee69fb935b36086f0b5682d98fe3ee184f6841"
|
||||
"internal/ai/tools/data_types.go": "00200b1366ee4bf1e541e3acf6618f56993b05b05fa08122b91cd2db320969ec",
|
||||
"internal/ai/tools/tools_query.go": "69bd79091bc8892043b900ca8475d1ee9d25c9613cf0ca86e36e41a0e468e2f8",
|
||||
"internal/ai/chat/service.go": "7b4a7696eb0a3ca2a99427d98f991740ff362bfc0c738c36f45f80549d7c2744"
|
||||
},
|
||||
"binary_sha256": "552699cdf2e61a4ca1cea2ac5ef4e065735cbd1dbca01665456e184bd4fc3533",
|
||||
"binary_sha256": "bcaf748107211ee733a6dc0f4d17220d9b4d1ce1918c25bde27cf3d10c0d6379",
|
||||
"rendering_content_sha256": {
|
||||
"frontend-modern/src/components/AI/Chat/hooks/useChat.ts": "0b56b7a56e35d51ca96f0e126dd493b3164aa9e0ad4d8ae24bcf3af7a574b97c",
|
||||
"frontend-modern/src/components/AI/Chat/ChatMessages.tsx": "9672f7608d1e3a531c73cba20fd4a78752316783212afd0c292ddfd11d2bf371",
|
||||
@@ -32,13 +34,13 @@
|
||||
}
|
||||
],
|
||||
"states": [
|
||||
"Actual configuration tool results captured from the final Go regression run: available config without session context, unsupported adapter, actual missing inventory resource, explicit query denial, nil provider response, provider failure, incomplete placement and unavailable inventory. This backend-only change preserves the tool error bit and exact known resource identity.",
|
||||
"Eight captured results replayed through the current Assistant stream renderer and restored through controlled session responses after a full page reload. This is rendering and reload proof, not server persistence, native provider integration, model diagnosis or action qualification.",
|
||||
"The local Pro backend was rebuilt on pulse-dev with Go1.26.8. Binary and source hashes matched before and after Playwright. Non-GET provider and infrastructure requests were blocked. The route warning in this controlled browser does not qualify provider readiness."
|
||||
"Nine actual regression outputs: disconnected transport beside current workload metrics, connected transport under read-only control, distinct parent and guest connections, control-enabled transport, unqueried topology and an observed empty connection snapshot.",
|
||||
"Each captured input and output is preserved by the Assistant stream renderer and restored through controlled session responses after reload. Unobserved fields remain absent and observed false/zero remains explicit. This is rendering proof, not native integration or model diagnosis.",
|
||||
"Local Pro backend cross-built on pulse-dev with Go1.26.8. Source and binary hashes matched before and after Playwright. Browser interception blocks provider and infrastructure writes. Its selected-route warning does not qualify provider readiness."
|
||||
],
|
||||
"interactions": [
|
||||
"Tool details open/closed, hover, keyboard focus, Enter and Space, complete input/output comparison, deepest output scrolling and pixel inspection at desktop, intermediate and narrow widths. Successful reads show completed and unavailable or denied reads show failed.",
|
||||
"Escape, full reload, session picker selection, reopening each restored result and exact output comparison with preserved success/error state. No model call or infrastructure action was attempted.",
|
||||
"Private final-source evidence: /Volumes/Development/pulse/tmp/patrol-config-read-contract/browser, tool-evidence.json and runtime-binding.json."
|
||||
"Tool details open/closed, hover, keyboard focus, Enter and Space, exact complete input/output comparison, deepest output scrolling and pixel inspection at desktop, intermediate and narrow widths.",
|
||||
"Escape, full reload, session picker selection, reopening all nine restored tool records with exact output and completed-state assertions. Controlled session fixtures do not qualify server persistence.",
|
||||
"Private evidence: /Volumes/Development/pulse/tmp/patrol-command-context/browser, tool-evidence.json, runtime-binding.json and final.log."
|
||||
]
|
||||
}
|
||||
|
||||
+34
-34
@@ -1418,15 +1418,15 @@ func marshalAssistantInventoryTopologyContext(topology tools.TopologyResponse) (
|
||||
}
|
||||
for _, node := range topology.Proxmox.Nodes {
|
||||
nodeContext := assistantInventoryProxmoxNode{
|
||||
AnswerLabel: assistantInventoryNodeAnswerLabel(node.Name),
|
||||
Name: node.Name,
|
||||
Status: node.Status,
|
||||
AgentConnected: node.AgentConnected,
|
||||
CanExecute: node.CanExecute,
|
||||
VMCount: node.VMCount,
|
||||
ContainerCount: node.ContainerCount,
|
||||
VMs: make([]assistantInventoryWorkload, 0, len(node.VMs)),
|
||||
Containers: make([]assistantInventoryWorkload, 0, len(node.Containers)),
|
||||
AnswerLabel: assistantInventoryNodeAnswerLabel(node.Name),
|
||||
Name: node.Name,
|
||||
Status: node.Status,
|
||||
CommandAgentConnected: node.CommandAgentConnected,
|
||||
CanExecute: node.CanExecute,
|
||||
VMCount: node.VMCount,
|
||||
ContainerCount: node.ContainerCount,
|
||||
VMs: make([]assistantInventoryWorkload, 0, len(node.VMs)),
|
||||
Containers: make([]assistantInventoryWorkload, 0, len(node.Containers)),
|
||||
}
|
||||
for _, vm := range node.VMs {
|
||||
nodeContext.VMs = append(nodeContext.VMs, assistantInventoryWorkload{
|
||||
@@ -1452,14 +1452,14 @@ func marshalAssistantInventoryTopologyContext(topology tools.TopologyResponse) (
|
||||
}
|
||||
for _, host := range topology.Docker.Hosts {
|
||||
hostContext := assistantInventoryDockerHost{
|
||||
AnswerLabel: firstNonEmptyString(host.DisplayName, host.Hostname),
|
||||
Hostname: host.Hostname,
|
||||
DisplayName: host.DisplayName,
|
||||
AgentConnected: host.AgentConnected,
|
||||
CanExecute: host.CanExecute,
|
||||
ContainerCount: host.ContainerCount,
|
||||
RunningCount: host.RunningCount,
|
||||
Containers: make([]assistantInventoryAppContainer, 0, len(host.Containers)),
|
||||
AnswerLabel: firstNonEmptyString(host.DisplayName, host.Hostname),
|
||||
Hostname: host.Hostname,
|
||||
DisplayName: host.DisplayName,
|
||||
CommandAgentConnected: host.CommandAgentConnected,
|
||||
CanExecute: host.CanExecute,
|
||||
ContainerCount: host.ContainerCount,
|
||||
RunningCount: host.RunningCount,
|
||||
Containers: make([]assistantInventoryAppContainer, 0, len(host.Containers)),
|
||||
}
|
||||
for _, container := range host.Containers {
|
||||
hostContext.Containers = append(hostContext.Containers, assistantInventoryAppContainer{
|
||||
@@ -1547,15 +1547,15 @@ type assistantInventoryKubernetesTopology struct {
|
||||
}
|
||||
|
||||
type assistantInventoryProxmoxNode struct {
|
||||
AnswerLabel string `json:"answer_label"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
AgentConnected bool `json:"agent_connected,omitempty"`
|
||||
CanExecute bool `json:"can_execute,omitempty"`
|
||||
VMCount int `json:"vm_count"`
|
||||
ContainerCount int `json:"container_count"`
|
||||
VMs []assistantInventoryWorkload `json:"vms"`
|
||||
Containers []assistantInventoryWorkload `json:"containers"`
|
||||
AnswerLabel string `json:"answer_label"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
CommandAgentConnected *bool `json:"command_agent_connected,omitempty"`
|
||||
CanExecute *bool `json:"can_execute,omitempty"`
|
||||
VMCount int `json:"vm_count"`
|
||||
ContainerCount int `json:"container_count"`
|
||||
VMs []assistantInventoryWorkload `json:"vms"`
|
||||
Containers []assistantInventoryWorkload `json:"containers"`
|
||||
}
|
||||
|
||||
type assistantInventoryWorkload struct {
|
||||
@@ -1568,14 +1568,14 @@ type assistantInventoryWorkload struct {
|
||||
}
|
||||
|
||||
type assistantInventoryDockerHost struct {
|
||||
AnswerLabel string `json:"answer_label"`
|
||||
Hostname string `json:"hostname"`
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
AgentConnected bool `json:"agent_connected,omitempty"`
|
||||
CanExecute bool `json:"can_execute,omitempty"`
|
||||
ContainerCount int `json:"container_count"`
|
||||
RunningCount int `json:"running_count"`
|
||||
Containers []assistantInventoryAppContainer `json:"containers"`
|
||||
AnswerLabel string `json:"answer_label"`
|
||||
Hostname string `json:"hostname"`
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
CommandAgentConnected *bool `json:"command_agent_connected,omitempty"`
|
||||
CanExecute *bool `json:"can_execute,omitempty"`
|
||||
ContainerCount int `json:"container_count"`
|
||||
RunningCount int `json:"running_count"`
|
||||
Containers []assistantInventoryAppContainer `json:"containers"`
|
||||
}
|
||||
|
||||
type assistantInventoryAppContainer struct {
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package chat
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
||||
)
|
||||
|
||||
func TestAssistantInventoryDoesNotInventCommandConnectionObservations(t *testing.T) {
|
||||
registry := unifiedresources.NewRegistry(nil)
|
||||
registry.IngestSnapshot(models.StateSnapshot{
|
||||
Nodes: []models.Node{{ID: "node-one", Name: "node-one", Status: "online"}},
|
||||
DockerHosts: []models.DockerHost{{ID: "host-one", Hostname: "observed-host", Status: "online", Containers: []models.DockerContainer{{ID: "app-one", Name: "observed-app", State: "running"}}}},
|
||||
})
|
||||
raw, err := marshalAssistantInventoryTopologyContextFromReadState(registry)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, field := range []string{"agent_connected", "command_agent_connected", "can_execute", "nodes_with_agents", "docker_hosts_with_agents", "nodes_with_command_agents", "docker_hosts_with_command_agents"} {
|
||||
if strings.Contains(raw, `"`+field+`"`) {
|
||||
t.Fatalf("inventory seed invented %s without observing command connections: %s", field, raw)
|
||||
}
|
||||
}
|
||||
var decoded map[string]any
|
||||
if err := json.Unmarshal([]byte(raw), &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
host := decoded["docker"].(map[string]any)["hosts"].([]any)[0].(map[string]any)
|
||||
if host["hostname"] != "observed-host" || host["container_count"] != float64(1) {
|
||||
t.Fatalf("monitoring inventory was lost: %+v", host)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/agentexec"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
||||
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
||||
)
|
||||
|
||||
func commandEvidenceSnapshot() models.StateSnapshot {
|
||||
return models.StateSnapshot{
|
||||
Nodes: []models.Node{{ID: "node-one", Name: "node-one", Status: "online"}},
|
||||
VMs: []models.VM{{ID: "vm-one", VMID: 101, Name: "guest-one", Node: "node-one", Status: "running"}},
|
||||
DockerHosts: []models.DockerHost{{
|
||||
ID: "host-one", Hostname: "command-host", Status: "online", LastSeen: time.Now(),
|
||||
Containers: []models.DockerContainer{{ID: "container-one", Name: "observed-service", State: "running", CPUPercent: 12.5}},
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
func commandEvidenceJSON(t *testing.T, value any) map[string]any {
|
||||
t.Helper()
|
||||
raw, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var result map[string]any
|
||||
if err := json.Unmarshal(raw, &result); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func TestCommandConnectivityDoesNotReplaceMonitoringEvidence(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
connected, guestConnected, controlEnabled bool
|
||||
}{
|
||||
{name: "no_command_connection"},
|
||||
{name: "connected_read_only", connected: true},
|
||||
{name: "guest_connection_only", guestConnected: true},
|
||||
{name: "connected_control_enabled", connected: true, controlEnabled: true},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
server := &mockAgentServer{}
|
||||
if tc.connected {
|
||||
server.agents = []agentexec.ConnectedAgent{{Hostname: "command-host"}, {Hostname: "node-one"}}
|
||||
}
|
||||
if tc.guestConnected {
|
||||
server.agents = append(server.agents, agentexec.ConnectedAgent{Hostname: "guest-one"})
|
||||
}
|
||||
controlLevel := ControlLevelReadOnly
|
||||
if tc.controlEnabled {
|
||||
controlLevel = ControlLevelControlled
|
||||
}
|
||||
registry := unifiedresources.NewRegistry(nil)
|
||||
registry.IngestSnapshot(commandEvidenceSnapshot())
|
||||
executor := NewPulseToolExecutor(ExecutorConfig{ReadState: registry, UnifiedResourceProvider: ®istryUnifiedQueryProvider{registry}, AgentServer: server, ControlLevel: controlLevel})
|
||||
query := func(args map[string]interface{}) map[string]any {
|
||||
t.Helper()
|
||||
result, err := executor.executeQuery(context.Background(), args)
|
||||
if err != nil || result.IsError {
|
||||
t.Fatalf("query failed: %v %+v", err, result)
|
||||
}
|
||||
var decoded map[string]any
|
||||
if err := json.Unmarshal([]byte(result.Content[0].Text), &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
capture, _ := json.Marshal(map[string]any{"case": tc.name, "input": args, "output": decoded})
|
||||
t.Logf("COMMAND_EVIDENCE %s", capture)
|
||||
return decoded
|
||||
}
|
||||
list := query(map[string]interface{}{"action": "list", "type": "docker-hosts"})
|
||||
host := list["docker_hosts"].([]any)[0].(map[string]any)
|
||||
if host["command_agent_connected"] != tc.connected {
|
||||
t.Fatalf("connection must name command transport: %+v", host)
|
||||
}
|
||||
if _, exists := host["agent_connected"]; exists {
|
||||
t.Fatal("ambiguous connection field remains")
|
||||
}
|
||||
container := host["containers"].([]any)[0].(map[string]any)
|
||||
resource := query(map[string]interface{}{"action": "get", "resource_type": "app-container", "resource_id": container["id"]})
|
||||
if resource["status"] != "running" || resource["cpu"].(map[string]any)["percent"] != 12.5 {
|
||||
t.Fatalf("command state replaced monitored evidence: %+v", resource)
|
||||
}
|
||||
topology := query(map[string]interface{}{"action": "topology", "include": "all"})
|
||||
docker := topology["docker"].(map[string]any)["hosts"].([]any)[0].(map[string]any)
|
||||
if docker["command_agent_connected"] != tc.connected || docker["can_execute"] != (tc.connected && tc.controlEnabled) {
|
||||
t.Fatalf("transport/control hint changed: %+v", docker)
|
||||
}
|
||||
search := query(map[string]interface{}{"action": "search", "query": "guest-one"})
|
||||
guest := search["matches"].([]any)[0].(map[string]any)
|
||||
if guest["node_command_agent_connected"] != tc.connected {
|
||||
t.Fatalf("parent transport not identified: %+v", guest)
|
||||
}
|
||||
if guest["command_agent_connected"] != tc.guestConnected {
|
||||
t.Fatalf("parent connection became a direct guest connection: %+v", guest)
|
||||
}
|
||||
server.AssertNotCalled(t, "ExecuteCommand")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTopologyOmitsUnobservedCommandConnections(t *testing.T) {
|
||||
registry := unifiedresources.NewRegistry(nil)
|
||||
registry.IngestSnapshot(commandEvidenceSnapshot())
|
||||
for _, observed := range []bool{false, true} {
|
||||
options := TopologyBuildOptions{Include: "all", ControlEnabled: true}
|
||||
if observed {
|
||||
options.ConnectedAgentHostnames = map[string]bool{}
|
||||
}
|
||||
result := commandEvidenceJSON(t, BuildTopologyResponseFromReadState(registry, options))
|
||||
caseName := "unobserved_topology"
|
||||
if observed {
|
||||
caseName = "observed_empty_topology"
|
||||
}
|
||||
capture, err := json.Marshal(map[string]any{"case": caseName, "input": map[string]any{"action": "topology", "include": "all"}, "output": result})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Logf("COMMAND_EVIDENCE %s", capture)
|
||||
for _, group := range []struct{ family, collection string }{{"docker", "hosts"}, {"proxmox", "nodes"}} {
|
||||
item := result[group.family].(map[string]any)[group.collection].([]any)[0].(map[string]any)
|
||||
for _, field := range []string{"command_agent_connected", "can_execute"} {
|
||||
value, exists := item[field]
|
||||
if exists != observed || (exists && value != false) {
|
||||
t.Fatalf("observed=%t field=%s: %+v", observed, field, item)
|
||||
}
|
||||
}
|
||||
if _, exists := item["agent_connected"]; exists {
|
||||
t.Fatal("ambiguous connection field remains")
|
||||
}
|
||||
}
|
||||
for _, field := range []string{"nodes_with_command_agents", "docker_hosts_with_command_agents"} {
|
||||
value, exists := result["summary"].(map[string]any)[field]
|
||||
if exists != observed || (exists && value != float64(0)) {
|
||||
t.Fatalf("unobserved connections became a count: %+v", result["summary"])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -238,40 +238,40 @@ func (r ResourceSearchResponse) NormalizeCollections() ResourceSearchResponse {
|
||||
// ResourceMatch is a compact match result for pulse_search_resources
|
||||
type ResourceMatch struct {
|
||||
GovernedResourceMetadata
|
||||
Type string `json:"type"` // "agent", "node", "vm", "system-container", "app-container", "docker-host", "storage"
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Node string `json:"node,omitempty"` // Hypervisor node this resource is on
|
||||
NodeHasAgent bool `json:"node_has_agent,omitempty"` // True if the node has a connected agent
|
||||
Host string `json:"host,omitempty"` // Docker host for docker containers
|
||||
Platform string `json:"platform,omitempty"`
|
||||
VMID int `json:"vmid,omitempty"`
|
||||
Image string `json:"image,omitempty"`
|
||||
AgentConnected bool `json:"agent_connected,omitempty"` // True if this specific resource has a connected agent
|
||||
Type string `json:"type"` // "agent", "node", "vm", "system-container", "app-container", "docker-host", "storage"
|
||||
ID string `json:"id,omitempty"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status,omitempty"`
|
||||
Node string `json:"node,omitempty"` // Hypervisor node this resource is on
|
||||
NodeCommandAgentConnected *bool `json:"node_command_agent_connected,omitempty"` // Live command connection on the parent node, independent of telemetry collection
|
||||
Host string `json:"host,omitempty"` // Docker host for docker containers
|
||||
Platform string `json:"platform,omitempty"`
|
||||
VMID int `json:"vmid,omitempty"`
|
||||
Image string `json:"image,omitempty"`
|
||||
CommandAgentConnected *bool `json:"command_agent_connected,omitempty"` // Live command connection for this resource, independent of telemetry collection
|
||||
}
|
||||
|
||||
// SystemSummary is a summarized infrastructure system for list responses.
|
||||
type SystemSummary struct {
|
||||
GovernedResourceMetadata
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
Platform string `json:"platform,omitempty"`
|
||||
ChildCount int `json:"child_count,omitempty"`
|
||||
AgentConnected bool `json:"agent_connected,omitempty"`
|
||||
CPU float64 `json:"cpu_percent,omitempty"`
|
||||
Memory float64 `json:"memory_percent,omitempty"`
|
||||
Disk float64 `json:"disk_percent,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
Platform string `json:"platform,omitempty"`
|
||||
ChildCount int `json:"child_count,omitempty"`
|
||||
CommandAgentConnected *bool `json:"command_agent_connected,omitempty"`
|
||||
CPU float64 `json:"cpu_percent,omitempty"`
|
||||
Memory float64 `json:"memory_percent,omitempty"`
|
||||
Disk float64 `json:"disk_percent,omitempty"`
|
||||
}
|
||||
|
||||
// NodeSummary is a summarized node for list responses
|
||||
type NodeSummary struct {
|
||||
GovernedResourceMetadata
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
ID string `json:"id,omitempty"`
|
||||
AgentConnected bool `json:"agent_connected"` // True if an execution agent is connected for this node
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
ID string `json:"id,omitempty"`
|
||||
CommandAgentConnected *bool `json:"command_agent_connected,omitempty"` // True if an execution agent is connected for this node
|
||||
}
|
||||
|
||||
// VMSummary is a summarized VM for list responses
|
||||
@@ -299,12 +299,12 @@ type ContainerSummary struct {
|
||||
// DockerHostSummary is a summarized Docker host for list responses
|
||||
type DockerHostSummary struct {
|
||||
GovernedResourceMetadata
|
||||
ID string `json:"id"`
|
||||
Hostname string `json:"hostname"`
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
ContainerCount int `json:"container_count"`
|
||||
AgentConnected bool `json:"agent_connected"` // True if an execution agent is connected for this host
|
||||
Containers []DockerContainerSummary `json:"containers"`
|
||||
ID string `json:"id"`
|
||||
Hostname string `json:"hostname"`
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
ContainerCount int `json:"container_count"`
|
||||
CommandAgentConnected *bool `json:"command_agent_connected,omitempty"` // True if an execution agent is connected for this host
|
||||
Containers []DockerContainerSummary `json:"containers"`
|
||||
}
|
||||
|
||||
func (s DockerHostSummary) NormalizeCollections() DockerHostSummary {
|
||||
@@ -454,15 +454,15 @@ func (t ProxmoxTopology) NormalizeCollections() ProxmoxTopology {
|
||||
// ProxmoxNodeTopology represents a Proxmox node with its guests
|
||||
type ProxmoxNodeTopology struct {
|
||||
GovernedResourceMetadata
|
||||
Name string `json:"name"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Status string `json:"status"`
|
||||
AgentConnected bool `json:"agent_connected"`
|
||||
CanExecute bool `json:"can_execute"` // True if commands can be executed on this node
|
||||
VMs []TopologyVM `json:"vms"`
|
||||
Containers []TopologyContainer `json:"containers"`
|
||||
VMCount int `json:"vm_count"`
|
||||
ContainerCount int `json:"container_count"`
|
||||
Name string `json:"name"`
|
||||
ID string `json:"id,omitempty"`
|
||||
Status string `json:"status"`
|
||||
CommandAgentConnected *bool `json:"command_agent_connected,omitempty"`
|
||||
CanExecute *bool `json:"can_execute,omitempty"` // True if commands can be executed on this node
|
||||
VMs []TopologyVM `json:"vms"`
|
||||
Containers []TopologyContainer `json:"containers"`
|
||||
VMCount int `json:"vm_count"`
|
||||
ContainerCount int `json:"container_count"`
|
||||
}
|
||||
|
||||
func (t ProxmoxNodeTopology) NormalizeCollections() ProxmoxNodeTopology {
|
||||
@@ -538,15 +538,15 @@ func (t DockerTopology) NormalizeCollections() DockerTopology {
|
||||
// DockerHostTopology represents a Docker host with its containers
|
||||
type DockerHostTopology struct {
|
||||
GovernedResourceMetadata
|
||||
Hostname string `json:"hostname"`
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
AgentConnected bool `json:"agent_connected"`
|
||||
CanExecute bool `json:"can_execute"` // True if commands can be executed on this host
|
||||
Containers []DockerContainerSummary `json:"containers"`
|
||||
ContainerCount int `json:"container_count"`
|
||||
ReturnedCount int `json:"returned_container_count"`
|
||||
Truncated bool `json:"containers_truncated"`
|
||||
RunningCount int `json:"running_count"`
|
||||
Hostname string `json:"hostname"`
|
||||
DisplayName string `json:"display_name,omitempty"`
|
||||
CommandAgentConnected *bool `json:"command_agent_connected,omitempty"`
|
||||
CanExecute *bool `json:"can_execute,omitempty"` // True if commands can be executed on this host
|
||||
Containers []DockerContainerSummary `json:"containers"`
|
||||
ContainerCount int `json:"container_count"`
|
||||
ReturnedCount int `json:"returned_container_count"`
|
||||
Truncated bool `json:"containers_truncated"`
|
||||
RunningCount int `json:"running_count"`
|
||||
}
|
||||
|
||||
func (t DockerHostTopology) NormalizeCollections() DockerHostTopology {
|
||||
@@ -642,21 +642,21 @@ type KubernetesPodDetail struct {
|
||||
|
||||
// TopologySummary provides aggregate counts and status
|
||||
type TopologySummary struct {
|
||||
TotalNodes int `json:"total_nodes"`
|
||||
TotalVMs int `json:"total_vms"`
|
||||
TotalSystemContainers int `json:"total_system_containers"`
|
||||
TotalDockerHosts int `json:"total_docker_hosts"`
|
||||
TotalDockerContainers int `json:"total_docker_containers"`
|
||||
TotalK8sClusters int `json:"total_k8s_clusters"`
|
||||
TotalK8sNodes int `json:"total_k8s_nodes"`
|
||||
TotalK8sDeployments int `json:"total_k8s_deployments"`
|
||||
TotalK8sPods int `json:"total_k8s_pods"`
|
||||
NodesWithAgents int `json:"nodes_with_agents"`
|
||||
DockerHostsWithAgents int `json:"docker_hosts_with_agents"`
|
||||
RunningVMs int `json:"running_vms"`
|
||||
RunningContainers int `json:"running_containers"`
|
||||
RunningDocker int `json:"running_docker"`
|
||||
RunningK8sPods int `json:"running_k8s_pods"`
|
||||
TotalNodes int `json:"total_nodes"`
|
||||
TotalVMs int `json:"total_vms"`
|
||||
TotalSystemContainers int `json:"total_system_containers"`
|
||||
TotalDockerHosts int `json:"total_docker_hosts"`
|
||||
TotalDockerContainers int `json:"total_docker_containers"`
|
||||
TotalK8sClusters int `json:"total_k8s_clusters"`
|
||||
TotalK8sNodes int `json:"total_k8s_nodes"`
|
||||
TotalK8sDeployments int `json:"total_k8s_deployments"`
|
||||
TotalK8sPods int `json:"total_k8s_pods"`
|
||||
NodesWithCommandAgents *int `json:"nodes_with_command_agents,omitempty"`
|
||||
DockerHostsWithCommandAgents *int `json:"docker_hosts_with_command_agents,omitempty"`
|
||||
RunningVMs int `json:"running_vms"`
|
||||
RunningContainers int `json:"running_containers"`
|
||||
RunningDocker int `json:"running_docker"`
|
||||
RunningK8sPods int `json:"running_k8s_pods"`
|
||||
}
|
||||
|
||||
// ResourceResponse is returned by pulse_get_resource
|
||||
|
||||
@@ -2151,7 +2151,7 @@ func (e *PulseToolExecutor) registerQueryTools() {
|
||||
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. Health returns the connection overview by default, or the canonical resource projection when resource_id is provided.`,
|
||||
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. Health returns the connection overview by default, or the canonical resource projection when resource_id is provided. command_agent_connected describes live command transport, independently of monitoring collection or freshness. Missing connection fields were not observed. can_execute describes connected transport with control enabled, not approval for a particular operation.`,
|
||||
InputSchema: InputSchema{
|
||||
Type: "object",
|
||||
Properties: map[string]PropertySchema{
|
||||
@@ -2463,17 +2463,35 @@ func resourceHostCandidates(resource unifiedresources.Resource) []string {
|
||||
return candidates
|
||||
}
|
||||
|
||||
func resourceAgentConnected(resource unifiedresources.Resource, connected map[string]bool) bool {
|
||||
for _, candidate := range resourceHostCandidates(resource) {
|
||||
key := strings.TrimSpace(candidate)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if connected[key] {
|
||||
return true
|
||||
// commandConnectionObservation keeps an unqueried snapshot distinct from an
|
||||
// observed disconnected transport. It says nothing about telemetry freshness.
|
||||
func commandConnectionObservation(snapshot map[string]bool, value bool) *bool {
|
||||
if snapshot == nil {
|
||||
return nil
|
||||
}
|
||||
return &value
|
||||
}
|
||||
|
||||
func resourceCommandAgentConnected(resource unifiedresources.Resource, connected map[string]bool) *bool {
|
||||
// A guest's provider node/host identifies placement, not a command
|
||||
// connection inside the guest. Parent transport is projected separately.
|
||||
candidates := []string{resourceDisplayName(resource)}
|
||||
candidates = append(candidates, resource.Identity.Hostnames...)
|
||||
if resource.Agent != nil {
|
||||
candidates = append(candidates, resource.Agent.Hostname)
|
||||
}
|
||||
switch resource.Type {
|
||||
case unifiedresources.ResourceTypeVM, unifiedresources.ResourceTypeSystemContainer, unifiedresources.ResourceTypeAppContainer:
|
||||
// Only the guest's own identity can establish its direct connection.
|
||||
default:
|
||||
candidates = append(candidates, resourceHostCandidates(resource)...)
|
||||
}
|
||||
for _, candidate := range candidates {
|
||||
if key := strings.TrimSpace(candidate); key != "" && connected[key] {
|
||||
return commandConnectionObservation(connected, true)
|
||||
}
|
||||
}
|
||||
return false
|
||||
return commandConnectionObservation(connected, false)
|
||||
}
|
||||
|
||||
func appContainerProviderID(resource unifiedresources.Resource) string {
|
||||
@@ -2972,16 +2990,16 @@ func addCanonicalGuestSearchMatches(
|
||||
node := canonicalGuestTarget(resource)
|
||||
metadataCandidates := append([]string{resourceDisplayName(resource), resource.ID}, candidates...)
|
||||
addMatch(ResourceMatch{
|
||||
GovernedResourceMetadata: governance.Resolve(metadataCandidates...),
|
||||
Type: kind,
|
||||
ID: resource.ID,
|
||||
Name: resourceDisplayName(resource),
|
||||
Status: status,
|
||||
Node: node,
|
||||
NodeHasAgent: connectedAgentHostnames[node],
|
||||
Platform: canonicalResourcePlatform(resource),
|
||||
VMID: vmid,
|
||||
AgentConnected: resourceAgentConnected(resource, connectedAgentHostnames),
|
||||
GovernedResourceMetadata: governance.Resolve(metadataCandidates...),
|
||||
Type: kind,
|
||||
ID: resource.ID,
|
||||
Name: resourceDisplayName(resource),
|
||||
Status: status,
|
||||
Node: node,
|
||||
NodeCommandAgentConnected: commandConnectionObservation(connectedAgentHostnames, connectedAgentHostnames[node]),
|
||||
Platform: canonicalResourcePlatform(resource),
|
||||
VMID: vmid,
|
||||
CommandAgentConnected: resourceCommandAgentConnected(resource, connectedAgentHostnames),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3012,16 +3030,16 @@ func addGuestViewSearchMatches[V queryGuestView](
|
||||
continue
|
||||
}
|
||||
addMatch(ResourceMatch{
|
||||
GovernedResourceMetadata: governance.Resolve(g.Name(), g.ID(), vmidStr),
|
||||
Type: kind,
|
||||
ID: g.ID(),
|
||||
Name: g.Name(),
|
||||
Status: status,
|
||||
Node: g.Node(),
|
||||
NodeHasAgent: connectedAgentHostnames[g.Node()],
|
||||
Platform: "proxmox",
|
||||
VMID: g.VMID(),
|
||||
AgentConnected: connectedAgentHostnames[g.Name()],
|
||||
GovernedResourceMetadata: governance.Resolve(g.Name(), g.ID(), vmidStr),
|
||||
Type: kind,
|
||||
ID: g.ID(),
|
||||
Name: g.Name(),
|
||||
Status: status,
|
||||
Node: g.Node(),
|
||||
NodeCommandAgentConnected: commandConnectionObservation(connectedAgentHostnames, connectedAgentHostnames[g.Node()]),
|
||||
Platform: "proxmox",
|
||||
VMID: g.VMID(),
|
||||
CommandAgentConnected: commandConnectionObservation(connectedAgentHostnames, connectedAgentHostnames[g.Name()]),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3469,15 +3487,15 @@ func resolvedAppContainerRegistration(resource unifiedresources.Resource) (Resou
|
||||
|
||||
func canonicalSystemSummaryFromResource(resource unifiedresources.Resource, connected map[string]bool) SystemSummary {
|
||||
return SystemSummary{
|
||||
ID: strings.TrimSpace(resource.ID),
|
||||
Name: resourceDisplayName(resource),
|
||||
Status: string(resource.Status),
|
||||
Platform: canonicalResourcePlatform(resource),
|
||||
ChildCount: resource.ChildCount,
|
||||
AgentConnected: resourceAgentConnected(resource, connected),
|
||||
CPU: metricPercent(resourceMetric(resource, "cpu")),
|
||||
Memory: metricPercent(resourceMetric(resource, "memory")),
|
||||
Disk: metricPercent(resourceMetric(resource, "disk")),
|
||||
ID: strings.TrimSpace(resource.ID),
|
||||
Name: resourceDisplayName(resource),
|
||||
Status: string(resource.Status),
|
||||
Platform: canonicalResourcePlatform(resource),
|
||||
ChildCount: resource.ChildCount,
|
||||
CommandAgentConnected: resourceCommandAgentConnected(resource, connected),
|
||||
CPU: metricPercent(resourceMetric(resource, "cpu")),
|
||||
Memory: metricPercent(resourceMetric(resource, "memory")),
|
||||
Disk: metricPercent(resourceMetric(resource, "disk")),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3809,7 +3827,7 @@ func (e *PulseToolExecutor) executeListInfrastructure(_ context.Context, args ma
|
||||
GovernedResourceMetadata: governance.Resolve(node.Name(), node.ID()),
|
||||
Name: node.Name(),
|
||||
Status: string(node.Status()),
|
||||
AgentConnected: connectedAgentHostnames[node.Name()],
|
||||
CommandAgentConnected: commandConnectionObservation(connectedAgentHostnames, connectedAgentHostnames[node.Name()]),
|
||||
})
|
||||
count++
|
||||
}
|
||||
@@ -3993,7 +4011,7 @@ func (e *PulseToolExecutor) executeListInfrastructure(_ context.Context, args ma
|
||||
Hostname: hostname,
|
||||
DisplayName: displayName,
|
||||
ContainerCount: len(hostContainers),
|
||||
AgentConnected: connectedAgentHostnames[hostname] || connectedAgentHostnames[displayName],
|
||||
CommandAgentConnected: commandConnectionObservation(connectedAgentHostnames, connectedAgentHostnames[hostname] || connectedAgentHostnames[displayName]),
|
||||
}
|
||||
for _, container := range hostContainers {
|
||||
state := strings.TrimSpace(container.ContainerState())
|
||||
@@ -4248,7 +4266,7 @@ type TopologyBuildOptions struct {
|
||||
MaxK8sNodesPerCluster int
|
||||
MaxK8sDeploymentsPerCluster int
|
||||
MaxK8sPodsPerCluster int
|
||||
ConnectedAgentHostnames map[string]bool
|
||||
ConnectedAgentHostnames map[string]bool // nil means command connections were not observed
|
||||
ControlEnabled bool
|
||||
}
|
||||
|
||||
@@ -4266,9 +4284,6 @@ func BuildTopologyResponseFromReadState(rs unifiedresources.ReadState, options T
|
||||
includeDocker := include == "all" || include == "app-containers"
|
||||
includeKubernetes := include == "all" || include == "kubernetes"
|
||||
connectedAgentHostnames := options.ConnectedAgentHostnames
|
||||
if connectedAgentHostnames == nil {
|
||||
connectedAgentHostnames = map[string]bool{}
|
||||
}
|
||||
governance := newGovernedQueryMetadataResolver(rs)
|
||||
|
||||
summary := TopologySummary{
|
||||
@@ -4283,12 +4298,17 @@ func BuildTopologyResponseFromReadState(rs unifiedresources.ReadState, options T
|
||||
TotalK8sPods: len(rs.Pods()),
|
||||
}
|
||||
|
||||
if connectedAgentHostnames != nil {
|
||||
summary.NodesWithCommandAgents = new(int)
|
||||
summary.DockerHostsWithCommandAgents = new(int)
|
||||
}
|
||||
|
||||
for _, node := range rs.Nodes() {
|
||||
if node == nil {
|
||||
continue
|
||||
}
|
||||
if connectedAgentHostnames[node.Name()] {
|
||||
summary.NodesWithAgents++
|
||||
(*summary.NodesWithCommandAgents)++
|
||||
}
|
||||
}
|
||||
for _, host := range rs.DockerHosts() {
|
||||
@@ -4298,7 +4318,7 @@ func BuildTopologyResponseFromReadState(rs unifiedresources.ReadState, options T
|
||||
hostname := strings.TrimSpace(host.Hostname())
|
||||
displayName := strings.TrimSpace(host.Name())
|
||||
if connectedAgentHostnames[hostname] || connectedAgentHostnames[displayName] {
|
||||
summary.DockerHostsWithAgents++
|
||||
(*summary.DockerHostsWithCommandAgents)++
|
||||
}
|
||||
}
|
||||
for _, pod := range rs.Pods() {
|
||||
@@ -4325,8 +4345,8 @@ func BuildTopologyResponseFromReadState(rs unifiedresources.ReadState, options T
|
||||
GovernedResourceMetadata: governance.Resolve(node.Name(), node.ID()),
|
||||
Name: name,
|
||||
Status: string(node.Status()),
|
||||
AgentConnected: hasAgent,
|
||||
CanExecute: hasAgent && options.ControlEnabled,
|
||||
CommandAgentConnected: commandConnectionObservation(connectedAgentHostnames, hasAgent),
|
||||
CanExecute: commandConnectionObservation(connectedAgentHostnames, hasAgent && options.ControlEnabled),
|
||||
VMs: []TopologyVM{},
|
||||
Containers: []TopologyContainer{},
|
||||
}
|
||||
@@ -4348,8 +4368,8 @@ func BuildTopologyResponseFromReadState(rs unifiedresources.ReadState, options T
|
||||
GovernedResourceMetadata: governance.Resolve(name),
|
||||
Name: name,
|
||||
Status: status,
|
||||
AgentConnected: hasAgent,
|
||||
CanExecute: hasAgent && options.ControlEnabled,
|
||||
CommandAgentConnected: commandConnectionObservation(connectedAgentHostnames, hasAgent),
|
||||
CanExecute: commandConnectionObservation(connectedAgentHostnames, hasAgent && options.ControlEnabled),
|
||||
VMs: []TopologyVM{},
|
||||
Containers: []TopologyContainer{},
|
||||
}
|
||||
@@ -4489,8 +4509,8 @@ func BuildTopologyResponseFromReadState(rs unifiedresources.ReadState, options T
|
||||
GovernedResourceMetadata: governance.Resolve(host.Hostname(), host.Name(), host.HostSourceID(), host.ID()),
|
||||
Hostname: hostname,
|
||||
DisplayName: displayName,
|
||||
AgentConnected: hasAgent,
|
||||
CanExecute: hasAgent && options.ControlEnabled,
|
||||
CommandAgentConnected: commandConnectionObservation(connectedAgentHostnames, hasAgent),
|
||||
CanExecute: commandConnectionObservation(connectedAgentHostnames, hasAgent && options.ControlEnabled),
|
||||
Containers: containers,
|
||||
ContainerCount: len(hostContainers),
|
||||
ReturnedCount: len(containers),
|
||||
@@ -5721,7 +5741,7 @@ func (e *PulseToolExecutor) executeSearchResources(_ context.Context, args map[s
|
||||
Status: status,
|
||||
Host: canonicalAgentHost(resource),
|
||||
Platform: canonicalResourcePlatform(resource),
|
||||
AgentConnected: resourceAgentConnected(resource, connectedAgentHostnames),
|
||||
CommandAgentConnected: resourceCommandAgentConnected(resource, connectedAgentHostnames),
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -5740,7 +5760,7 @@ func (e *PulseToolExecutor) executeSearchResources(_ context.Context, args map[s
|
||||
Type: "node",
|
||||
Name: node.Name(),
|
||||
Status: status,
|
||||
AgentConnected: connectedAgentHostnames[node.Name()],
|
||||
CommandAgentConnected: commandConnectionObservation(connectedAgentHostnames, connectedAgentHostnames[node.Name()]),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user