Tighten v5-to-v6 upgrade safety, release installability, provider MSP mode handling, AI cost accounting, metrics flushing, and frontend guardrails for the v6.0.0 GA candidate.
Two follow-ups caught by independent re-review:
- #29 (8855b78c0): GetConnectionStatuses keys a name-less instance by host,
but the diagnostics lookup used 'pve-'/'pbs-'+Name only, so the
monitor-state merge was a silent no-op for unnamed instances. Fall back
to host to match.
- #22 (d9da9b18f, #1254): add a full-router test asserting an UNBOUND
agent:report token gets 404 (not 200) on config fetch — continuously
verifying the token-binding security boundary that the scope change
relies on.
#23 (a6b773a85) made Restart load the AI config once and pass it to
startWithConfig, so the start path no longer re-loads it. The test still
expected LoadAIConfig().Twice(); updated to .Once() to match the fixed
single-load behavior. (Caught by running the full internal/api suite,
which the per-fix targeted runs had missed.)
Back-port the core of v5 fix 1de1392c9 to v6 (minimal, not the full
dedup refactor). The model list API now sets ModelInfo.Provider (stamped
in prefixProviderModels and passed through HandleListModels), and the
frontend groupModelsByProvider prefers the server-supplied provider over
splitting the model id, so models with opaque ids (e.g. Ollama-hosted
llama3-8b) group under the correct provider. Adds a grouping regression
test.
Back-port v5 fix 716340dcd to v6. Extracts startWithConfig from Start so
Restart passes the config it already loaded down to the start path, instead
of Start re-reading loadAIConfig a second time. Avoids starting the AI
service with a stale/inconsistent config if it changed between Restart's
load and the start. Both Restart start paths now use startWithConfig.
Back-port v5 fix d310c257a to v6, adapted to v6's connection-status key
format. computeDiagnostics now merges a failed PVE/PBS diagnostics probe
with the monitor's live connection state: if the long-running poller still
reports the instance connected, a transient probe failure (network blip,
TLS re-check) no longer flips it to 'disconnected' in the UI. Uses v6's
'pve-<name>'/'pbs-<name>' status keys (v5 used a bare node name, which
would not match in v6). Adds a merge-logic regression test.
Back-port v5 fixes 5997fd81f and 0a7b93a84 to v6:
- GetWebhooks list response now includes the configured mention so the UI
shows it after reload instead of blanking it.
- sendResolvedWebhook now assigns data.Mention (v6 set it for grouped/
firing webhooks but dropped it on resolved), and the Discord/Slack/Teams/
Mattermost ResolvedPayloadTemplate strings gained {{if .Mention}} guards.
Without these, a configured @everyone/@channel was silently omitted from
resolved/cleared notifications. Adds list-API and per-service resolved
mention regression tests.
Back-port v5 fix 04a828264 to v6. canReadConfig now accepts
ScopeAgentReport (legacy host-agent:report) in addition to the config/
manage/settings scopes; resolveConfigAgent already restricts a
report-only token to the host it is bound to, so it can only read its
own config. Without this, a v5 agent upgraded to a v6 server while still
holding a report-only token gets HTTP 403 on config fetch and breaks.
Retargets TestHandleConfigMissingConfigScope to monitoring:read and adds
TestHandleConfigAllowsBoundAgentReportScope as a #1254 regression guard.
Dead-code sweep. Functions flagged unreachable by golang.org/x/tools/cmd/deadcode
and confirmed unused across pulse, pulse-enterprise, pulse-pro and pulse-mobile by
adversarial cross-repo verification. Cross-module reachability was checked
explicitly (only pkg/ exported symbols are importable by other modules; internal/
packages and _test.go files are not). go build, go vet and test-compile all pass.
Root fix for the recurring class behind issue #1470. The served /install.sh and
/install.ps1 endpoints existed to hand out the unified AGENT installer, but their
GitHub fallback fetched the top-level install.sh release asset, which since
49412357a is the SERVER installer. Prior commits made that fallback unreachable
in normal deployments (deploy the sidecars; serve the local script even when
unsigned), but the endpoint was still structurally capable of serving the wrong
script in the no-local-bundle case.
The agent installer is a per-build artifact bundled into every release tarball and
Docker image, not a release asset, so the endpoint has no business proxying a
release asset at all. Remove proxyInstallScriptFromGitHub and its
installScriptReleaseAssetURL wrapper. handleDownloadInstallScriptCommon now serves
the locally bundled agent installer (signed when sidecars are present, unsigned
otherwise) or fails closed with 503 when no bundled script exists. Serving the
SERVER installer at this endpoint is now structurally impossible, not merely
unreachable.
The shared version-pinning (releaseAssetTag/releaseAssetURL) and installScriptClient
remain for the agent-BINARY download proxy, which legitimately fetches published
release assets; its version-pinning stays covered by the agentBinaryReleaseAssetURL
contract tests.
Replace the obsolete install-script proxy tests with fail-closed assertions
(including a guard that the endpoint makes no outbound call), drop the four
installScriptReleaseAssetURL contract tests, and revise the four subsystem
contracts that pinned the install-script fallback transport (api-contracts items
8 and 27, agent-lifecycle item 14, storage-recovery item 14, plus the
deployment-installability note) to state that install scripts are served local or
fail closed with no GitHub fallback.
The "Install on Linux/Windows" wizard does `curl -fsSL <server>/install.sh |
bash -s -- --url ...` and never verifies the response signature headers (curl|bash
discards them). But for published releases handleDownloadInstallScriptCommon
proxied the top-level GitHub install.sh asset whenever the local agent installer
lacked its .sig/.sshsig sidecars, and since 49412357a that asset is the SERVER
installer, which rejects --url. Every install missing the sidecars served the
wrong script. The companion deploy_agent_scripts fix deploys the sidecars for new
installs, but existing boxes stay broken until they redeploy.
Serve the locally bundled agent installer when its signatures are absent instead
of proxying. An unsigned-but-correct local script beats a signed-but-wrong proxied
one when nothing verifies the headers, and this retroactively fixes already-deployed
boxes the moment they get the new binary. The proxy now runs only when no local
installer is bundled at all, so the endpoint can no longer hand the agent wizard a
server installer in any reachable deployment state. New installs still ship the
sidecars and are served signed.
Revise the install-script signature/fallback contract this changes, across the
three subsystems that pin it (api-contracts item 8, agent-lifecycle item 14,
storage-recovery item 14) plus the deployment-installability note, to state that
the served endpoint serves the agent installer with correctness outranking
signature presence. Add a handler guard asserting a published-release server with
a present-but-unsigned local installer serves it locally and does not proxy.
Previously an alert that triggered Patrol ran a broad health check that
explicitly ignored the threshold breach. Now an alert carries its real
payload (metric type, value, threshold, identifier, level, message) into
the patrol scope, and the alert_fired run is framed around root-causing
that specific breach instead of a general assessment.
Three coordinated changes:
- Carry the alert payload into PatrolScope.AlertContext through the alert
bridge (PatrolTriggerEvent), so the patrol prompt sees the breach
specifics rather than just an alert-type string.
- Frame alert_fired patrol runs around the breach: replace the
"ignore threshold breaches" instruction with a root-cause directive
targeting the alert's metric and threshold.
- Add per-rule control via AIConfig.AlertTriggersInvestigation: a master
enable, a minimum-severity floor (patrol_alert_trigger_min_severity,
default critical-only), and an optional alert-type allowlist
(patrol_alert_trigger_types). The router's bridge callback consults the
policy and drops non-qualifying alert_fired events before queuing a
scoped patrol. A config-panel selector persists the severity floor.
Adds config, handler, and frontend proof tests, and updates the affected
subsystem contracts.
Forward-port of the release/5.1 fix (2c46c6c2d).
UpdateAlertConfig used to log SaveAlertConfig failures and still tell
the client "saved successfully", leaving the in-memory state with the
new override but the on-disk file untouched. On the next config reload
or process restart, the override silently vanished and the user saw
their threshold "revert" with no surfaced error. Return HTTP 500 with
the persistence error so the frontend can show a real save-failed
toast instead of false confidence.
Forward-port of the release/5.1 fix (9ac7df976). buildAlertsDiagnostic
previously emitted only cooldown/grouping flags, so triaging support
cases like #1341 where a user suspects an override key mismatch
required asking them to paste alerts config from inside their
container. Add an Overrides slice that names each persisted key with
its thresholds and disabled flags. Sanitize mode in the frontend
redacts the keys to override-N while keeping thresholds visible.
The four ReadDeadline(time.Now().Add(2 * time.Second)) calls in
router_integration_test.go (lines 1496, 1543, 1573, 1682) were
producing 'read tcp: i/o timeout' failures in CI under -race while
passing locally. The 2-second window is enough to read the welcome
+ initialState messages on a quiet dev workstation but too tight
once the runner is loaded with cumulative test work and the race
detector overhead. rc.5 cleared the same tests in CI but recent
fixture-size growth (k8s clusters 1->3 in 7938f28de plus the SMART
disk-temperature mock data added in 23ea4e487) pushed the
end-to-end server-start-to-welcome-message latency past the 2s
budget. Bumping to 15s gives CI breathing room without affecting
local test duration (the deadline only takes effect when the read
is genuinely stuck).
handleWorkloadCharts: add a 3s TTL response cache + per-key singleflight
so repeated sparkline polls from dashboards share work. TestSLO_WorkloadCharts
p95 drops from 188ms (over the 90ms target) to 838µs because most polls
hit cache or coalesce. Cold-call cost is unchanged; the win is dedup of
the steady-state polling pattern.
Cache + singleflight state live on the Router struct, not as globals, so
tenants don't cross-contaminate and tests don't pollute each other. Same
treatment for stateComputeGroup added in the previous commit.
monitor_metrics.mergeMetricHistory: drop the defensive cloneMetricPointMap
of base and cloneMetricSeries of candidate inside the merge. Every caller
already passes a freshly-owned map (from filterChartMetricMap,
GetAllGuestMetrics, GetAllStorageMetrics, queryStore*), so the internal
clones were pure waste. Caller-side cloneMetricPointMap is removed in
the three sites where the input is owned. ~6% allocation reduction on
BenchmarkHandleWorkloadCharts_StoreBacked.
Contract-neutral: no endpoint, response body, header, or wire-format
change.
Under load, 5x concurrent /api/state degraded from 276ms (single) to ~4s
each (linear), because every caller serialized on the monitor lock to
rebuild and JSON-encode the full 1.6MB state. /api/diagnostics had the
same dogpile shape on cache miss, even though its 45s TTL cache was
working as designed.
Wrap both handlers in a per-tenant singleflight.Group so concurrent
callers share the work: 20x concurrent /api/state now completes in 358ms
wall (~14-50x improvement). Diagnostics warm-cache responses are now
sub-3ms; cold compute coalesces.
Also drop websocket Upgrader buffers from 4MB read/write to 64KB. gorilla
streams larger payloads across the buffer transparently, so the 4MB
allocation per connection was overhead that scaled badly with concurrent
clients (100 clients * 8MB = 800MB just in buffers).
Contract-neutral: no endpoint, response body, header, or wire-format
change.
Closes the only API-coverage gap from the Docker / Kubernetes IA
maturity review: Roles, ClusterRoles, RoleBindings, and
ClusterRoleBindings now flow from the Kubernetes agent through the
canonical resource registry into the Kubernetes platform-page
Configuration tab.
Agent: pkg/agents/kubernetes/report.go gains four new report struct
types that carry summary counts plus subject-kind sets; individual
subject names and full PolicyRule contents are deliberately omitted
so Pulse stays a "what permissions exist where" surface, not an RBAC
enumeration tool. internal/kubernetesagent/agent.go gains four
collectors that call rbacv1.RoleList/ClusterRoleList/etc. through the
existing runKubernetesCallWithRetry wrapper, matching the
ServiceAccount collector's RBAC-forbidden retry pattern.
Canonical: internal/models mirrors with NormalizeCollections coverage;
convert* funcs in internal/monitoring/kubernetes_agents.go translate
agent report -> model; ResourceTypeK8sRole / K8sClusterRole /
K8sRoleBinding / K8sClusterRoleBinding join the canonical type set;
registry ingest* + adapter resourceFrom* functions emit one Resource
per RBAC object with ruleCount / roleKind / roleName / subjectCount /
subjectKinds / aggregationLabels on the K8s meta; search mapping in
internal/api/resources.go and the privacy allow-list in
internal/api/org_handlers.go pick up the four new type tokens; the
K8s privacy category in unifiedresources/policy_metadata.go classifies
them like the rest of K8s.
Frontend: ResourceType union + ResourceKubernetesMeta carry the new
kinds and RBAC summary fields; KubernetesPageSurface query asks for
them; the page model buckets them into the Configuration group;
KubernetesConfigTable renders Role / ClusterRole rule counts and the
aggregated flag, plus RoleBinding / ClusterRoleBinding role refs and
"N subjects · Kind1, Kind2 +overflow" subject summaries.
Curated demo seeds per-namespace Roles + RoleBindings plus an
aggregated ClusterRole + ClusterRoleBinding for pulse-demo-monitoring
in each cluster so the Configuration tab renders 18 RBAC rows across
the three demo clusters.
Contracts updated for the canonical-shape guard: monitoring,
api-contracts, unified-resources, frontend-primitives,
organization-settings (canonical) plus agent-lifecycle and
storage-recovery (dependent via Extension Points). Verification
proofs extended: kubernetes_registry_test.go, kubernetes_agents_test.go,
agent_inventory_test.go (new TestCollectRBACInventoryReportsSummaryCountsOnly
that pins the subject-name-omission contract), demo_scenarios_test.go,
adapter_coverage_test.go, contract_test.go, org_handlers_test.go,
resourceIdentity.test.ts, reportingResourceTypes.test.ts,
KubernetesConfigTable.test.tsx, and the
subsystem_lookup_test.py line-anchor bumps that the contract edits
shifted (api-contracts 246 -> 253, organization-settings 92 -> 93).
Verified:
- go build ./internal/... ./cmd/... clean
- go test ./internal/unifiedresources/..., ./internal/mock/...,
./internal/kubernetesagent/..., ./internal/api/...,
the K8s subset of ./internal/monitoring/... all clean (three
pre-existing unrelated monitoring failures noted earlier remain
unchanged by this commit)
- npm run type-check, lint:eslint, lint:theme,
lint:canonical-platforms clean
- vitest: 70 K8s frontend tests pass including the new RBAC render
coverage in KubernetesConfigTable.test.tsx
- browser proof on /kubernetes/configuration: 36 config rows
including 18 RBAC rows across three clusters; ClusterRole
"pulse-demo-monitoring" shows "12 rules · Aggregated";
ClusterRoleBinding shows "3 subjects · Group, ServiceAccount +1"