Fix tenant-scoped service discovery execution

This commit is contained in:
rcourtman 2026-08-10 19:53:08 +01:00
parent b2dd6abfd8
commit dfcfe3fbd1
4 changed files with 72 additions and 7 deletions

View file

@ -5198,6 +5198,14 @@ single-resource discovery, and `pulse_discovery` refreshes before any
`discovery_interval_hours: 0` is the only manual-command-scan mode: recurring
scans stay stopped, but explicit admin-triggered refreshes may use the
hardcoded trusted catalog.
Each per-organization AI service now passes its organization-pinned
`AgentServer` interface directly into `internal/ai/discovery_adapter.go`.
Deep discovery does not recover the concrete global `agentexec.Server` or
silently drop command execution when the runtime supplies an
`agentexec.OrganizationServer`; connected-agent lookup and command dispatch
therefore remain on the same tenant-scoped view. The interface-backed service
regression in `internal/ai/service_test.go` requires a real deep-discovery run
to dispatch the trusted probe catalog and retain its command evidence.
The value boundary for keeping Discovery is observed workload context:
Assistant and Patrol may consume normalized service name, version, endpoint,
port, config path, data path, log path, bind-mount, confidence, and user-note

View file

@ -15,13 +15,14 @@ const (
minDiscoveryCommandTimeoutSeconds = 1
)
// discoveryCommandAdapter adapts agentexec.Server to servicediscovery.CommandExecutor
// discoveryCommandAdapter adapts the AI service's organization-scoped agent
// server view to servicediscovery.CommandExecutor.
type discoveryCommandAdapter struct {
server *agentexec.Server
server AgentServer
}
// newDiscoveryCommandAdapter creates a new adapter
func newDiscoveryCommandAdapter(server *agentexec.Server) *discoveryCommandAdapter {
func newDiscoveryCommandAdapter(server AgentServer) *discoveryCommandAdapter {
return &discoveryCommandAdapter{server: server}
}
@ -97,7 +98,12 @@ func (a *discoveryCommandAdapter) IsAgentConnected(agentID string) bool {
if a.server == nil {
return false
}
return a.server.IsAgentConnected(agentID)
for _, agent := range a.server.GetConnectedAgents() {
if agent.AgentID == agentID {
return true
}
}
return false
}
func nonNilContext(ctx context.Context) context.Context {

View file

@ -608,10 +608,10 @@ func (s *Service) initDiscoveryServiceLocked() {
return
}
// Create command executor adapter (wraps agentexec.Server)
// Create a command executor adapter over the organization-scoped server view.
var cmdExecutor servicediscovery.CommandExecutor
if agentSrv, ok := s.agentServer.(*agentexec.Server); ok {
cmdExecutor = newDiscoveryCommandAdapter(agentSrv)
if s.agentServer != nil {
cmdExecutor = newDiscoveryCommandAdapter(s.agentServer)
}
// Create deep scanner

View file

@ -6,13 +6,16 @@ import (
"errors"
"os"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/agentcapabilities"
"github.com/rcourtman/pulse-go-rewrite/internal/agentexec"
"github.com/rcourtman/pulse-go-rewrite/internal/ai/providers"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
"github.com/rcourtman/pulse-go-rewrite/internal/servicediscovery"
unifiedresources "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
"github.com/rcourtman/pulse-go-rewrite/pkg/aicontracts"
)
@ -353,6 +356,54 @@ func TestService_LoadConfig_SyncsInfraDiscoveryLifecycle(t *testing.T) {
}
}
func TestService_DeepDiscoveryUsesInterfaceBackedAgentServer(t *testing.T) {
var commandCalls atomic.Int32
agentServer := &mockAgentServer{
agents: []agentexec.ConnectedAgent{{
AgentID: "agent-1",
Hostname: "home-assistant",
Platform: "linux",
}},
executeFunc: func(_ context.Context, _ string, cmd agentexec.ExecuteCommandPayload) (*agentexec.CommandResultPayload, error) {
commandCalls.Add(1)
return &agentexec.CommandResultPayload{
RequestID: cmd.RequestID,
Success: true,
Stdout: "probe output",
}, nil
},
}
svc := NewService(config.NewConfigPersistence(t.TempDir()), agentServer)
defer svc.Stop()
registry := unifiedresources.NewRegistry(nil)
registry.IngestSnapshot(models.StateSnapshot{})
svc.SetReadState(registry)
discoveryService := svc.GetDiscoveryService()
if discoveryService == nil {
t.Fatal("expected deep discovery service to be initialized")
}
discoveryService.SetCommandScanningEnabled(true)
result, err := discoveryService.DiscoverResource(context.Background(), servicediscovery.DiscoveryRequest{
ResourceType: servicediscovery.ResourceTypeAgent,
ResourceID: "agent-1",
TargetID: "agent-1",
Hostname: "home-assistant",
Force: true,
})
if err != nil {
t.Fatalf("DiscoverResource() error = %v", err)
}
if commandCalls.Load() == 0 {
t.Fatal("expected deep discovery to dispatch commands through the interface-backed agent server")
}
if len(result.RawCommandOutput) == 0 {
t.Fatal("expected deep discovery to retain command evidence")
}
}
func TestService_GetCostSummary_NoStore(t *testing.T) {
svc := NewService(nil, nil)
svc.costStore = nil