From 3630ab186735ec030504c0c4884d03ae9b1d1338 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Tue, 2 Jun 2026 13:14:16 +0100 Subject: [PATCH] Verify provider MSP agent report ingest --- cmd/pulse-control-plane/provider_msp_proof.go | 145 +++++++++++++++++- .../provider_msp_proof_test.go | 13 +- .../subsystems/deployment-installability.md | 5 + 3 files changed, 160 insertions(+), 3 deletions(-) diff --git a/cmd/pulse-control-plane/provider_msp_proof.go b/cmd/pulse-control-plane/provider_msp_proof.go index e1daf0159..8684602c5 100644 --- a/cmd/pulse-control-plane/provider_msp_proof.go +++ b/cmd/pulse-control-plane/provider_msp_proof.go @@ -12,6 +12,7 @@ import ( "path/filepath" "regexp" "strings" + "time" "github.com/rcourtman/pulse-go-rewrite/internal/api" "github.com/rcourtman/pulse-go-rewrite/internal/cloudcp" @@ -23,6 +24,8 @@ import ( "github.com/rcourtman/pulse-go-rewrite/internal/cloudcp/registry" cpstripe "github.com/rcourtman/pulse-go-rewrite/internal/cloudcp/stripe" runtimeconfig "github.com/rcourtman/pulse-go-rewrite/internal/config" + "github.com/rcourtman/pulse-go-rewrite/internal/monitoring" + agentshost "github.com/rcourtman/pulse-go-rewrite/pkg/agents/host" "github.com/spf13/cobra" ) @@ -61,6 +64,9 @@ type providerMSPProofWorkspace struct { InstallCommandGenerated bool AgentTokenAuthVerified bool SetupFactsTokenUseVisible bool + AgentReportIngestVerified bool + AgentReportAgentID string + AgentReportHostname string HandoffExchangeVerified bool HandoffTargetPath string } @@ -236,6 +242,7 @@ func (rt *providerMSPProofRuntime) runProviderMSPProof(ctx context.Context, opts HandoffExchangeVerified: true, InstallTokenBoundaryOK: true, SetupFactsTokenUseVisible: true, + AgentReportIngestVerified: true, Cleanup: opts.Cleanup, } @@ -261,6 +268,9 @@ func (rt *providerMSPProofRuntime) runProviderMSPProof(ctx context.Context, opts if !workspace.SetupFactsTokenUseVisible { report.SetupFactsTokenUseVisible = false } + if !workspace.AgentReportIngestVerified { + report.AgentReportIngestVerified = false + } report.Workspaces = append(report.Workspaces, workspace) } @@ -403,6 +413,11 @@ func (rt *providerMSPProofRuntime) proveProviderMSPWorkspace(ctx context.Context facts.AgentTokenCount != nil && *facts.AgentTokenCount > 0 && facts.LastAgentSeenAt != nil + agentReport, err := rt.verifyProviderMSPProofAgentReportIngest(ctx, tenant, tenantDataDir, install.Token, install.TokenID) + if err != nil { + return providerMSPProofWorkspace{}, err + } + exchangedTargetPath, err := rt.verifyProviderMSPProofHandoff(ctx, tenant, ownerUserID, targetPath) if err != nil { return providerMSPProofWorkspace{}, err @@ -421,11 +436,136 @@ func (rt *providerMSPProofRuntime) proveProviderMSPWorkspace(ctx context.Context InstallCommandGenerated: true, AgentTokenAuthVerified: tokenAuthVerified, SetupFactsTokenUseVisible: setupFactsVisible, + AgentReportIngestVerified: agentReport.Verified, + AgentReportAgentID: agentReport.AgentID, + AgentReportHostname: agentReport.Hostname, HandoffExchangeVerified: exchangedTargetPath == targetPath, HandoffTargetPath: exchangedTargetPath, }, nil } +type providerMSPProofAgentReportIngest struct { + Verified bool + AgentID string + Hostname string +} + +func (rt *providerMSPProofRuntime) verifyProviderMSPProofAgentReportIngest(ctx context.Context, tenant *registry.Tenant, tenantDataDir, rawToken, tokenID string) (providerMSPProofAgentReportIngest, error) { + if tenant == nil { + return providerMSPProofAgentReportIngest{}, fmt.Errorf("tenant is required") + } + tenantID := strings.TrimSpace(tenant.ID) + if tenantID == "" { + return providerMSPProofAgentReportIngest{}, fmt.Errorf("tenant id is required") + } + + tokens, err := configPersistenceForProviderMSPProof(tenantDataDir).LoadAPITokens() + if err != nil { + return providerMSPProofAgentReportIngest{}, fmt.Errorf("load tenant api tokens before report ingest: %w", err) + } + tenantCfg := &runtimeconfig.Config{ + DataPath: tenantDataDir, + ConfigPath: tenantDataDir, + PublicURL: rt.providerMSPProofTenantPublicURL(tenantID), + APITokens: tokens, + } + tenantPersistence := runtimeconfig.NewMultiTenantPersistence(tenantDataDir) + if !tenantPersistence.OrgExists(tenantID) { + return providerMSPProofAgentReportIngest{}, fmt.Errorf("tenant %s organization metadata is missing from runtime data dir", tenantID) + } + + tenantMonitor := monitoring.NewMultiTenantMonitor(tenantCfg, tenantPersistence, nil) + defer tenantMonitor.Stop() + + handler := api.NewUnifiedAgentHandlers(tenantMonitor, nil, nil) + report := agentshost.Report{ + Agent: agentshost.AgentInfo{ + ID: "provider-msp-proof-agent", + Version: "6.0.0-provider-msp-proof", + Type: "unified", + IntervalSeconds: 30, + }, + Host: agentshost.HostInfo{ + ID: "provider-msp-proof-machine", + MachineID: "provider-msp-proof-machine", + Hostname: "pve1", + DisplayName: "pve1", + Platform: "linux", + OSName: "Debian", + OSVersion: "12", + Architecture: "amd64", + CPUCount: 4, + UptimeSeconds: 3600, + KernelVersion: "6.1.0-provider-proof", + }, + Metrics: agentshost.Metrics{ + CPUUsagePercent: 12.5, + Memory: agentshost.MemoryMetric{ + TotalBytes: 8 * 1024 * 1024 * 1024, + UsedBytes: 2 * 1024 * 1024 * 1024, + FreeBytes: 6 * 1024 * 1024 * 1024, + Usage: 25, + }, + }, + Timestamp: time.Now().UTC(), + SequenceID: "provider-msp-proof-report", + } + body, err := json.Marshal(report) + if err != nil { + return providerMSPProofAgentReportIngest{}, fmt.Errorf("marshal provider MSP proof agent report: %w", err) + } + + req := httptest.NewRequest(http.MethodPost, "/api/agents/agent/report", bytes.NewReader(body)) + req = req.WithContext(context.WithValue(ctx, api.OrgIDContextKey, tenantID)) + req.Header.Set("Authorization", "Bearer "+rawToken) + rec := httptest.NewRecorder() + api.RequireAuth(tenantCfg, api.RequireScope(runtimeconfig.ScopeAgentReport, handler.HandleReport))(rec, req) + if rec.Code != http.StatusOK { + return providerMSPProofAgentReportIngest{}, fmt.Errorf("tenant %s agent report status=%d body=%s", tenantID, rec.Code, strings.TrimSpace(rec.Body.String())) + } + + var payload struct { + AgentID string `json:"agentId"` + } + if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil { + return providerMSPProofAgentReportIngest{}, fmt.Errorf("decode provider MSP proof agent report response: %w", err) + } + if strings.TrimSpace(payload.AgentID) == "" { + return providerMSPProofAgentReportIngest{}, fmt.Errorf("tenant %s agent report response omitted agent id", tenantID) + } + + monitor, ok := tenantMonitor.PeekMonitor(tenantID) + if !ok || monitor == nil { + return providerMSPProofAgentReportIngest{}, fmt.Errorf("tenant %s monitor was not initialized by agent report", tenantID) + } + hosts := monitor.GetLiveHostsSnapshot() + if len(hosts) != 1 { + return providerMSPProofAgentReportIngest{}, fmt.Errorf("tenant %s host count after agent report = %d, want 1", tenantID, len(hosts)) + } + host := hosts[0] + if host.ID != payload.AgentID { + return providerMSPProofAgentReportIngest{}, fmt.Errorf("tenant %s ingested host id=%q, response agent id=%q", tenantID, host.ID, payload.AgentID) + } + if host.Hostname != "pve1" { + return providerMSPProofAgentReportIngest{}, fmt.Errorf("tenant %s ingested hostname=%q, want pve1", tenantID, host.Hostname) + } + if strings.TrimSpace(tokenID) != "" && strings.TrimSpace(host.TokenID) != strings.TrimSpace(tokenID) { + return providerMSPProofAgentReportIngest{}, fmt.Errorf("tenant %s ingested token id=%q, want %q", tenantID, host.TokenID, tokenID) + } + + if defaultMonitor, ok := tenantMonitor.PeekMonitor("default"); ok && defaultMonitor != nil { + if defaultHosts := defaultMonitor.GetLiveHostsSnapshot(); len(defaultHosts) != 0 { + return providerMSPProofAgentReportIngest{}, fmt.Errorf("tenant %s report leaked into default runtime: %#v", tenantID, defaultHosts) + } + } + + return providerMSPProofAgentReportIngest{ + Verified: true, + AgentID: payload.AgentID, + Hostname: host.Hostname, + }, nil +} + func configPersistenceForProviderMSPProof(tenantDataDir string) *runtimeconfig.ConfigPersistence { return runtimeconfig.NewConfigPersistence(tenantDataDir) } @@ -662,7 +802,7 @@ func printProviderMSPProofReport(report *providerMSPProofReport) { fmt.Printf("agent_report_ingest_verified=%t\n", report.AgentReportIngestVerified) fmt.Printf("cleanup=%t\n", report.Cleanup) for _, workspace := range report.Workspaces { - fmt.Printf("workspace=%s display_name=%q state=%s plan_version=%s container_id=%s public_url=%s install_type=%s install_token_id=%s install_command_generated=%t agent_token_auth_verified=%t setup_facts_token_use_visible=%t handoff_exchange_verified=%t handoff_target_path=%s\n", + fmt.Printf("workspace=%s display_name=%q state=%s plan_version=%s container_id=%s public_url=%s install_type=%s install_token_id=%s install_command_generated=%t agent_token_auth_verified=%t setup_facts_token_use_visible=%t agent_report_ingest_verified=%t agent_report_agent_id=%s agent_report_hostname=%s handoff_exchange_verified=%t handoff_target_path=%s\n", workspace.TenantID, workspace.DisplayName, workspace.State, @@ -674,6 +814,9 @@ func printProviderMSPProofReport(report *providerMSPProofReport) { workspace.InstallCommandGenerated, workspace.AgentTokenAuthVerified, workspace.SetupFactsTokenUseVisible, + workspace.AgentReportIngestVerified, + workspace.AgentReportAgentID, + workspace.AgentReportHostname, workspace.HandoffExchangeVerified, workspace.HandoffTargetPath, ) diff --git a/cmd/pulse-control-plane/provider_msp_proof_test.go b/cmd/pulse-control-plane/provider_msp_proof_test.go index 93eb26a7d..6de20ba93 100644 --- a/cmd/pulse-control-plane/provider_msp_proof_test.go +++ b/cmd/pulse-control-plane/provider_msp_proof_test.go @@ -86,8 +86,8 @@ func TestProviderMSPProofExercisesWorkspaceInstallHandoffAndIsolation(t *testing if !report.SetupFactsTokenUseVisible { t.Fatal("setup facts did not see hosted root token use") } - if report.AgentReportIngestVerified { - t.Fatal("agent report ingest should remain explicitly unverified in this control-plane proof") + if !report.AgentReportIngestVerified { + t.Fatal("agent report ingest was not verified") } seenTenants := map[string]struct{}{} @@ -114,6 +114,15 @@ func TestProviderMSPProofExercisesWorkspaceInstallHandoffAndIsolation(t *testing if !workspace.SetupFactsTokenUseVisible { t.Fatalf("setup facts token use not visible for %s", workspace.TenantID) } + if !workspace.AgentReportIngestVerified { + t.Fatalf("agent report ingest not verified for %s", workspace.TenantID) + } + if workspace.AgentReportAgentID == "" { + t.Fatalf("agent report id missing for %s", workspace.TenantID) + } + if workspace.AgentReportHostname != "pve1" { + t.Fatalf("AgentReportHostname = %q, want pve1", workspace.AgentReportHostname) + } if !workspace.HandoffExchangeVerified || workspace.HandoffTargetPath != "/settings/infrastructure?add=linux-host" { t.Fatalf("handoff exchange proof mismatch: %#v", workspace) } diff --git a/docs/release-control/v6/internal/subsystems/deployment-installability.md b/docs/release-control/v6/internal/subsystems/deployment-installability.md index cc880046c..1bcaa5c81 100644 --- a/docs/release-control/v6/internal/subsystems/deployment-installability.md +++ b/docs/release-control/v6/internal/subsystems/deployment-installability.md @@ -123,6 +123,11 @@ surfaces. the configured tenant runtime image, configured Docker network, Docker daemon reachability, and storage-admission guardrails before the provider treats a fresh install as ready for client onboarding. + `pulse-control-plane provider-msp proof` must exercise the first-client + onboarding path through workspace creation, client-bound install token + generation, tenant-local unified-agent report ingest, handoff exchange, and + duplicate-hostname isolation before provider-hosted MSP installability is + treated as proven. 5. `internal/cloudcp/tenant_runtime_rollout.go` shared with `cloud-paid`: hosted tenant runtime rollout is both a Pulse Cloud runtime contract boundary and a deployment-installability release-rollout boundary. 6. `scripts/install.ps1` shared with `agent-lifecycle`: the Windows installer is both a deployment installability entry point and a canonical agent lifecycle runtime continuity boundary. It must expose a non-mutating preflight for the exact Windows agent