fix(config): add PULSE_AGENT_CONNECT_URL and improve Docker detection
- Add AgentConnectURL config option to override public URL for agents - Improve install.sh to diagnose docker detection failures - Update router to prioritize AgentConnectURL for agent install commands
This commit is contained in:
parent
a5a45d1967
commit
c8634cebff
3 changed files with 69 additions and 14 deletions
|
|
@ -29,6 +29,7 @@ import (
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/agentexec"
|
"github.com/rcourtman/pulse-go-rewrite/internal/agentexec"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/ai"
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/alerts"
|
"github.com/rcourtman/pulse-go-rewrite/internal/alerts"
|
||||||
|
"github.com/rcourtman/pulse-go-rewrite/internal/license"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/auth"
|
"github.com/rcourtman/pulse-go-rewrite/internal/auth"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
||||||
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
||||||
|
|
@ -57,6 +58,7 @@ type Router struct {
|
||||||
systemSettingsHandler *SystemSettingsHandler
|
systemSettingsHandler *SystemSettingsHandler
|
||||||
aiSettingsHandler *AISettingsHandler
|
aiSettingsHandler *AISettingsHandler
|
||||||
resourceHandlers *ResourceHandlers
|
resourceHandlers *ResourceHandlers
|
||||||
|
licenseHandlers *LicenseHandlers
|
||||||
agentExecServer *agentexec.Server
|
agentExecServer *agentexec.Server
|
||||||
wsHub *websocket.Hub
|
wsHub *websocket.Hub
|
||||||
reloadFunc func() error
|
reloadFunc func() error
|
||||||
|
|
@ -193,6 +195,7 @@ func (r *Router) setupRoutes() {
|
||||||
r.hostAgentHandlers = NewHostAgentHandlers(r.monitor, r.wsHub)
|
r.hostAgentHandlers = NewHostAgentHandlers(r.monitor, r.wsHub)
|
||||||
r.temperatureProxyHandlers = NewTemperatureProxyHandlers(r.config, r.persistence, r.reloadFunc)
|
r.temperatureProxyHandlers = NewTemperatureProxyHandlers(r.config, r.persistence, r.reloadFunc)
|
||||||
r.resourceHandlers = NewResourceHandlers()
|
r.resourceHandlers = NewResourceHandlers()
|
||||||
|
r.licenseHandlers = NewLicenseHandlers(r.config.DataPath)
|
||||||
|
|
||||||
// API routes
|
// API routes
|
||||||
r.mux.HandleFunc("/api/health", r.handleHealth)
|
r.mux.HandleFunc("/api/health", r.handleHealth)
|
||||||
|
|
@ -419,6 +422,11 @@ func (r *Router) setupRoutes() {
|
||||||
|
|
||||||
// Registration token routes removed - feature deprecated
|
// Registration token routes removed - feature deprecated
|
||||||
|
|
||||||
|
// License routes (Pulse Pro)
|
||||||
|
r.mux.HandleFunc("/api/license/status", RequireAdmin(r.config, r.licenseHandlers.HandleLicenseStatus))
|
||||||
|
r.mux.HandleFunc("/api/license/activate", RequireAdmin(r.config, RequireScope(config.ScopeSettingsWrite, r.licenseHandlers.HandleActivateLicense)))
|
||||||
|
r.mux.HandleFunc("/api/license/clear", RequireAdmin(r.config, RequireScope(config.ScopeSettingsWrite, r.licenseHandlers.HandleClearLicense)))
|
||||||
|
|
||||||
// Security routes
|
// Security routes
|
||||||
r.mux.HandleFunc("/api/security/change-password", r.handleChangePassword)
|
r.mux.HandleFunc("/api/security/change-password", r.handleChangePassword)
|
||||||
r.mux.HandleFunc("/api/logout", r.handleLogout)
|
r.mux.HandleFunc("/api/logout", r.handleLogout)
|
||||||
|
|
@ -539,6 +547,10 @@ func (r *Router) setupRoutes() {
|
||||||
(r.config.OIDC != nil && r.config.OIDC.Enabled) ||
|
(r.config.OIDC != nil && r.config.OIDC.Enabled) ||
|
||||||
r.config.ProxyAuthSecret != ""
|
r.config.ProxyAuthSecret != ""
|
||||||
|
|
||||||
|
// Resolve the public URL for agent install commands
|
||||||
|
// If PULSE_PUBLIC_URL is configured, use that; otherwise derive from request
|
||||||
|
agentURL := r.resolvePublicURL(req)
|
||||||
|
|
||||||
status := map[string]interface{}{
|
status := map[string]interface{}{
|
||||||
"apiTokenConfigured": r.config.HasAPITokens(),
|
"apiTokenConfigured": r.config.HasAPITokens(),
|
||||||
"apiTokenHint": apiTokenHint,
|
"apiTokenHint": apiTokenHint,
|
||||||
|
|
@ -562,6 +574,7 @@ func (r *Router) setupRoutes() {
|
||||||
"authLastModified": "",
|
"authLastModified": "",
|
||||||
"oidcUsername": oidcUsername,
|
"oidcUsername": oidcUsername,
|
||||||
"hideLocalLogin": r.config.HideLocalLogin,
|
"hideLocalLogin": r.config.HideLocalLogin,
|
||||||
|
"agentUrl": agentURL,
|
||||||
}
|
}
|
||||||
|
|
||||||
if isAuthenticated {
|
if isAuthenticated {
|
||||||
|
|
@ -1108,6 +1121,8 @@ func (r *Router) setupRoutes() {
|
||||||
hostMetadataHandler.Store(),
|
hostMetadataHandler.Store(),
|
||||||
)
|
)
|
||||||
r.aiSettingsHandler.SetMetadataProvider(metadataProvider)
|
r.aiSettingsHandler.SetMetadataProvider(metadataProvider)
|
||||||
|
// Wire license checker for Pro feature gating (AI Patrol, Alert Analysis, Auto-Fix)
|
||||||
|
r.aiSettingsHandler.SetLicenseChecker(r.licenseHandlers.Service())
|
||||||
r.mux.HandleFunc("/api/settings/ai", RequireAdmin(r.config, RequireScope(config.ScopeSettingsRead, r.aiSettingsHandler.HandleGetAISettings)))
|
r.mux.HandleFunc("/api/settings/ai", RequireAdmin(r.config, RequireScope(config.ScopeSettingsRead, r.aiSettingsHandler.HandleGetAISettings)))
|
||||||
r.mux.HandleFunc("/api/settings/ai/update", RequireAdmin(r.config, RequireScope(config.ScopeSettingsWrite, r.aiSettingsHandler.HandleUpdateAISettings)))
|
r.mux.HandleFunc("/api/settings/ai/update", RequireAdmin(r.config, RequireScope(config.ScopeSettingsWrite, r.aiSettingsHandler.HandleUpdateAISettings)))
|
||||||
r.mux.HandleFunc("/api/ai/test", RequireAdmin(r.config, RequireScope(config.ScopeSettingsWrite, r.aiSettingsHandler.HandleTestAIConnection)))
|
r.mux.HandleFunc("/api/ai/test", RequireAdmin(r.config, RequireScope(config.ScopeSettingsWrite, r.aiSettingsHandler.HandleTestAIConnection)))
|
||||||
|
|
@ -1115,7 +1130,7 @@ func (r *Router) setupRoutes() {
|
||||||
r.mux.HandleFunc("/api/ai/models", RequireAuth(r.config, r.aiSettingsHandler.HandleListModels))
|
r.mux.HandleFunc("/api/ai/models", RequireAuth(r.config, r.aiSettingsHandler.HandleListModels))
|
||||||
r.mux.HandleFunc("/api/ai/execute", RequireAuth(r.config, r.aiSettingsHandler.HandleExecute))
|
r.mux.HandleFunc("/api/ai/execute", RequireAuth(r.config, r.aiSettingsHandler.HandleExecute))
|
||||||
r.mux.HandleFunc("/api/ai/execute/stream", RequireAuth(r.config, r.aiSettingsHandler.HandleExecuteStream))
|
r.mux.HandleFunc("/api/ai/execute/stream", RequireAuth(r.config, r.aiSettingsHandler.HandleExecuteStream))
|
||||||
r.mux.HandleFunc("/api/ai/investigate-alert", RequireAuth(r.config, r.aiSettingsHandler.HandleInvestigateAlert))
|
r.mux.HandleFunc("/api/ai/investigate-alert", RequireAuth(r.config, RequireLicenseFeature(r.licenseHandlers.Service(), license.FeatureAIAlerts, r.aiSettingsHandler.HandleInvestigateAlert)))
|
||||||
r.mux.HandleFunc("/api/ai/run-command", RequireAuth(r.config, r.aiSettingsHandler.HandleRunCommand))
|
r.mux.HandleFunc("/api/ai/run-command", RequireAuth(r.config, r.aiSettingsHandler.HandleRunCommand))
|
||||||
r.mux.HandleFunc("/api/ai/knowledge", RequireAuth(r.config, r.aiSettingsHandler.HandleGetGuestKnowledge))
|
r.mux.HandleFunc("/api/ai/knowledge", RequireAuth(r.config, r.aiSettingsHandler.HandleGetGuestKnowledge))
|
||||||
r.mux.HandleFunc("/api/ai/knowledge/save", RequireAuth(r.config, r.aiSettingsHandler.HandleSaveGuestNote))
|
r.mux.HandleFunc("/api/ai/knowledge/save", RequireAuth(r.config, r.aiSettingsHandler.HandleSaveGuestNote))
|
||||||
|
|
@ -1135,30 +1150,54 @@ func (r *Router) setupRoutes() {
|
||||||
r.mux.HandleFunc("/api/ai/oauth/disconnect", RequireAdmin(r.config, r.aiSettingsHandler.HandleOAuthDisconnect))
|
r.mux.HandleFunc("/api/ai/oauth/disconnect", RequireAdmin(r.config, r.aiSettingsHandler.HandleOAuthDisconnect))
|
||||||
|
|
||||||
// AI Patrol routes for background monitoring
|
// AI Patrol routes for background monitoring
|
||||||
|
// Note: Status remains accessible so UI can show license/upgrade state
|
||||||
|
// Read endpoints (findings, history, runs) return empty data with license_required flag
|
||||||
|
// Mutation endpoints (run, acknowledge, dismiss, etc.) return 402 to prevent unauthorized actions
|
||||||
r.mux.HandleFunc("/api/ai/patrol/status", RequireAuth(r.config, r.aiSettingsHandler.HandleGetPatrolStatus))
|
r.mux.HandleFunc("/api/ai/patrol/status", RequireAuth(r.config, r.aiSettingsHandler.HandleGetPatrolStatus))
|
||||||
r.mux.HandleFunc("/api/ai/patrol/stream", RequireAuth(r.config, r.aiSettingsHandler.HandlePatrolStream))
|
r.mux.HandleFunc("/api/ai/patrol/stream", RequireAuth(r.config, RequireLicenseFeature(r.licenseHandlers.Service(), license.FeatureAIPatrol, r.aiSettingsHandler.HandlePatrolStream)))
|
||||||
r.mux.HandleFunc("/api/ai/patrol/findings", RequireAuth(r.config, r.aiSettingsHandler.HandleGetPatrolFindings))
|
r.mux.HandleFunc("/api/ai/patrol/findings", RequireAuth(r.config, LicenseGatedEmptyResponse(r.licenseHandlers.Service(), license.FeatureAIPatrol, r.aiSettingsHandler.HandleGetPatrolFindings)))
|
||||||
r.mux.HandleFunc("/api/ai/patrol/history", RequireAuth(r.config, r.aiSettingsHandler.HandleGetFindingsHistory))
|
r.mux.HandleFunc("/api/ai/patrol/history", RequireAuth(r.config, LicenseGatedEmptyResponse(r.licenseHandlers.Service(), license.FeatureAIPatrol, r.aiSettingsHandler.HandleGetFindingsHistory)))
|
||||||
r.mux.HandleFunc("/api/ai/patrol/run", RequireAdmin(r.config, r.aiSettingsHandler.HandleForcePatrol))
|
r.mux.HandleFunc("/api/ai/patrol/run", RequireAdmin(r.config, RequireLicenseFeature(r.licenseHandlers.Service(), license.FeatureAIPatrol, r.aiSettingsHandler.HandleForcePatrol)))
|
||||||
r.mux.HandleFunc("/api/ai/patrol/acknowledge", RequireAuth(r.config, r.aiSettingsHandler.HandleAcknowledgeFinding))
|
r.mux.HandleFunc("/api/ai/patrol/acknowledge", RequireAuth(r.config, RequireLicenseFeature(r.licenseHandlers.Service(), license.FeatureAIPatrol, r.aiSettingsHandler.HandleAcknowledgeFinding)))
|
||||||
r.mux.HandleFunc("/api/ai/patrol/dismiss", RequireAuth(r.config, r.aiSettingsHandler.HandleDismissFinding)) // Dismiss with reason (LLM memory)
|
r.mux.HandleFunc("/api/ai/patrol/dismiss", RequireAuth(r.config, RequireLicenseFeature(r.licenseHandlers.Service(), license.FeatureAIPatrol, r.aiSettingsHandler.HandleDismissFinding)))
|
||||||
r.mux.HandleFunc("/api/ai/patrol/suppress", RequireAuth(r.config, r.aiSettingsHandler.HandleSuppressFinding)) // Permanently suppress (LLM memory)
|
r.mux.HandleFunc("/api/ai/patrol/suppress", RequireAuth(r.config, RequireLicenseFeature(r.licenseHandlers.Service(), license.FeatureAIPatrol, r.aiSettingsHandler.HandleSuppressFinding)))
|
||||||
r.mux.HandleFunc("/api/ai/patrol/snooze", RequireAuth(r.config, r.aiSettingsHandler.HandleSnoozeFinding))
|
r.mux.HandleFunc("/api/ai/patrol/snooze", RequireAuth(r.config, RequireLicenseFeature(r.licenseHandlers.Service(), license.FeatureAIPatrol, r.aiSettingsHandler.HandleSnoozeFinding)))
|
||||||
r.mux.HandleFunc("/api/ai/patrol/resolve", RequireAuth(r.config, r.aiSettingsHandler.HandleResolveFinding))
|
r.mux.HandleFunc("/api/ai/patrol/resolve", RequireAuth(r.config, RequireLicenseFeature(r.licenseHandlers.Service(), license.FeatureAIPatrol, r.aiSettingsHandler.HandleResolveFinding)))
|
||||||
r.mux.HandleFunc("/api/ai/patrol/runs", RequireAuth(r.config, r.aiSettingsHandler.HandleGetPatrolRunHistory))
|
r.mux.HandleFunc("/api/ai/patrol/runs", RequireAuth(r.config, LicenseGatedEmptyResponse(r.licenseHandlers.Service(), license.FeatureAIPatrol, r.aiSettingsHandler.HandleGetPatrolRunHistory)))
|
||||||
// Suppression rules management
|
// Suppression rules management (also Pro-only since they control LLM behavior)
|
||||||
|
// GET returns empty array for unlicensed, POST returns 402
|
||||||
r.mux.HandleFunc("/api/ai/patrol/suppressions", RequireAuth(r.config, func(w http.ResponseWriter, req *http.Request) {
|
r.mux.HandleFunc("/api/ai/patrol/suppressions", RequireAuth(r.config, func(w http.ResponseWriter, req *http.Request) {
|
||||||
switch req.Method {
|
switch req.Method {
|
||||||
case http.MethodGet:
|
case http.MethodGet:
|
||||||
|
// GET: return empty array if unlicensed
|
||||||
|
if err := r.licenseHandlers.Service().RequireFeature(license.FeatureAIPatrol); err != nil {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Header().Set("X-License-Required", "true")
|
||||||
|
w.Header().Set("X-License-Feature", license.FeatureAIPatrol)
|
||||||
|
w.Write([]byte("[]"))
|
||||||
|
return
|
||||||
|
}
|
||||||
r.aiSettingsHandler.HandleGetSuppressionRules(w, req)
|
r.aiSettingsHandler.HandleGetSuppressionRules(w, req)
|
||||||
case http.MethodPost:
|
case http.MethodPost:
|
||||||
|
// POST: return 402 if unlicensed
|
||||||
|
if err := r.licenseHandlers.Service().RequireFeature(license.FeatureAIPatrol); err != nil {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(http.StatusPaymentRequired)
|
||||||
|
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||||
|
"error": "license_required",
|
||||||
|
"message": err.Error(),
|
||||||
|
"feature": license.FeatureAIPatrol,
|
||||||
|
"upgrade_url": "https://pulsemonitor.app/pro",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
r.aiSettingsHandler.HandleAddSuppressionRule(w, req)
|
r.aiSettingsHandler.HandleAddSuppressionRule(w, req)
|
||||||
default:
|
default:
|
||||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
r.mux.HandleFunc("/api/ai/patrol/suppressions/", RequireAuth(r.config, r.aiSettingsHandler.HandleDeleteSuppressionRule))
|
r.mux.HandleFunc("/api/ai/patrol/suppressions/", RequireAuth(r.config, RequireLicenseFeature(r.licenseHandlers.Service(), license.FeatureAIPatrol, r.aiSettingsHandler.HandleDeleteSuppressionRule)))
|
||||||
r.mux.HandleFunc("/api/ai/patrol/dismissed", RequireAuth(r.config, r.aiSettingsHandler.HandleGetDismissedFindings))
|
r.mux.HandleFunc("/api/ai/patrol/dismissed", RequireAuth(r.config, LicenseGatedEmptyResponse(r.licenseHandlers.Service(), license.FeatureAIPatrol, r.aiSettingsHandler.HandleGetDismissedFindings)))
|
||||||
|
|
||||||
// AI Intelligence endpoints - expose learned patterns, correlations, and predictions
|
// AI Intelligence endpoints - expose learned patterns, correlations, and predictions
|
||||||
r.mux.HandleFunc("/api/ai/intelligence/patterns", RequireAuth(r.config, r.aiSettingsHandler.HandleGetPatterns))
|
r.mux.HandleFunc("/api/ai/intelligence/patterns", RequireAuth(r.config, r.aiSettingsHandler.HandleGetPatterns))
|
||||||
|
|
@ -4952,6 +4991,10 @@ func (r *Router) handleHostProxyStatus(w http.ResponseWriter, req *http.Request)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Router) resolvePublicURL(req *http.Request) string {
|
func (r *Router) resolvePublicURL(req *http.Request) string {
|
||||||
|
if agentConnectURL := strings.TrimSpace(r.config.AgentConnectURL); agentConnectURL != "" {
|
||||||
|
return strings.TrimRight(agentConnectURL, "/")
|
||||||
|
}
|
||||||
|
|
||||||
if publicURL := strings.TrimSpace(r.config.PublicURL); publicURL != "" {
|
if publicURL := strings.TrimSpace(r.config.PublicURL); publicURL != "" {
|
||||||
return strings.TrimRight(publicURL, "/")
|
return strings.TrimRight(publicURL, "/")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -79,6 +79,7 @@ type Config struct {
|
||||||
DataPath string `envconfig:"DATA_PATH" default:"/var/lib/pulse"`
|
DataPath string `envconfig:"DATA_PATH" default:"/var/lib/pulse"`
|
||||||
AppRoot string `json:"-"` // Root directory of the application (where binary lives)
|
AppRoot string `json:"-"` // Root directory of the application (where binary lives)
|
||||||
PublicURL string `envconfig:"PULSE_PUBLIC_URL" default:""` // Full URL to access Pulse (e.g., http://192.168.1.100:7655)
|
PublicURL string `envconfig:"PULSE_PUBLIC_URL" default:""` // Full URL to access Pulse (e.g., http://192.168.1.100:7655)
|
||||||
|
AgentConnectURL string `envconfig:"PULSE_AGENT_CONNECT_URL" default:""` // Dedicated direct connect URL for agents (e.g. http://192.168.1.5:7655)
|
||||||
|
|
||||||
// Proxmox VE connections
|
// Proxmox VE connections
|
||||||
PVEInstances []PVEInstance
|
PVEInstances []PVEInstance
|
||||||
|
|
@ -582,6 +583,7 @@ func Load() (*Config, error) {
|
||||||
TemperatureMonitoringEnabled: true,
|
TemperatureMonitoringEnabled: true,
|
||||||
EnableSensorProxy: false,
|
EnableSensorProxy: false,
|
||||||
EnvOverrides: make(map[string]bool),
|
EnvOverrides: make(map[string]bool),
|
||||||
|
AgentConnectURL: "",
|
||||||
OIDC: NewOIDCConfig(),
|
OIDC: NewOIDCConfig(),
|
||||||
// Metrics retention defaults (tiered)
|
// Metrics retention defaults (tiered)
|
||||||
MetricsRetentionRawHours: 2, // 2 hours of raw ~5s data
|
MetricsRetentionRawHours: 2, // 2 hours of raw ~5s data
|
||||||
|
|
@ -1228,6 +1230,12 @@ func Load() (*Config, error) {
|
||||||
log.Debug().Str("key_file", tlsKeyFile).Msg("TLS key file from env var")
|
log.Debug().Str("key_file", tlsKeyFile).Msg("TLS key file from env var")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if agentConnectURL := utils.GetenvTrim("PULSE_AGENT_CONNECT_URL"); agentConnectURL != "" {
|
||||||
|
cfg.AgentConnectURL = agentConnectURL
|
||||||
|
cfg.EnvOverrides["PULSE_AGENT_CONNECT_URL"] = true
|
||||||
|
log.Info().Str("url", agentConnectURL).Msg("Using dedicated agent connect URL from environment")
|
||||||
|
}
|
||||||
|
|
||||||
// REMOVED: Update channel, auto-update, connection timeout, and allowed origins env vars
|
// REMOVED: Update channel, auto-update, connection timeout, and allowed origins env vars
|
||||||
// These settings now ONLY come from system.json to prevent confusion
|
// These settings now ONLY come from system.json to prevent confusion
|
||||||
// Only keeping essential deployment/infrastructure env vars
|
// Only keeping essential deployment/infrastructure env vars
|
||||||
|
|
|
||||||
|
|
@ -99,12 +99,16 @@ detect_docker() {
|
||||||
# Try to connect to Docker daemon
|
# Try to connect to Docker daemon
|
||||||
if docker info &>/dev/null 2>&1; then
|
if docker info &>/dev/null 2>&1; then
|
||||||
return 0
|
return 0
|
||||||
|
else
|
||||||
|
log_warn "Docker binary found ($(command -v docker)) but 'docker info' failed. Is the daemon running?"
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
# Also check for Podman (Docker-compatible)
|
# Also check for Podman (Docker-compatible)
|
||||||
if command -v podman &>/dev/null; then
|
if command -v podman &>/dev/null; then
|
||||||
if podman info &>/dev/null 2>&1; then
|
if podman info &>/dev/null 2>&1; then
|
||||||
return 0
|
return 0
|
||||||
|
else
|
||||||
|
log_warn "Podman binary found but 'podman info' failed."
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
return 1
|
return 1
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue