Two mandatory round-trips stood between an interested MSP and their first
screen, and neither was technical.
setup.sh required four image digests shipped as literal <pin>
placeholders, so the only way to get them was to ask. All four images are
publicly readable, so there was never anything to hand out. setup.sh now
resolves each blank pin to an immutable digest from its published tag via
buildx imagetools and writes it back to .env; hand-set values are left
alone.
setup.sh then died outright without a licence file, so nobody could start
the stack, create a workspace, or see the portal until a human minted a
licence for them. The control plane already ran unlicensed via
ProviderMSPPlanSourceEnvFallback; only the installer refused. A licence
path that is set but missing is still a hard failure, since that is a
misconfiguration rather than a choice.
Unlicensed now means evaluation rather than the cheapest paid tier. The
env fallback defaulted to msp_starter, handing every unlicensed
deployment the full 5-client Starter allowance and leaving no boundary
between evaluating and buying. Adds msp_eval at 2 workspaces: same
capabilities, smaller cap, not purchasable, not on the public ladder.
An isolation guarantee is the one claim a provider cannot evaluate from a
screenshot, and both MSP leads this year went quiet at exactly this step.
Contracts: cloud-paid records the unlicensed plan rule and the
strictly-below-paid invariant; deployment-installability records
credential-free, correspondence-free installability.
Verification: TestMSPEvalCapStaysBelowCheapestPaidTier,
TestCanonicalizePlanVersion_MSPEval,
TestProviderMSPSetupScriptSupportsUnlicensedEvaluation. The last was
negative-tested by reintroducing a <pin> placeholder and confirming it
fails. ensure_image_pins exercised against the live registries.
licensing, cloudcp, control-plane and installtests all green.
The existing preview scenarios exercise edge cases and are named for it
(MSP Test Workspace A/B/C), and none of them set the alert counts, so
the Alerts column renders 'unknown' and the fleet rollup is invisible.
That is fine for a dev harness and useless for showing anyone what the
provider portal does.
Adds a 'demo' scenario: six settled clients with invented names on
reserved example domains, one critical and one warning, so the portal
surfaces a real 'Next: Review Hilltop Care' and the fleet summary reads
the way it does on a normal morning. This is what the pulserelay.pro MSP
walkthrough is recorded against.
Preview harness only, no production path touched. Portal suite green,
103 tests.
The FilterBar chunk grew past its budget as the saved-views and View
controls work landed, so the bundle-size gate held Build and Test red once
the lint and docs-sync failures ahead of it were cleared. The growth is
accounted for by that merged work rather than accidental bloat, and total
JS and CSS both stayed within budget throughout.
Regenerated with vite build plus check-bundle-size.mjs --update-baseline.
Contract-Neutral: build budget baseline, no contract delta
AGENT_SUBSTRATE_DOC_URL resolves to /docs/AGENT_SUBSTRATE.md and is
rendered as a link in AgentIntegrationsPanel, but the file was never
copied into frontend-modern/public/docs, so following it returned a 404.
Ship the doc and cover it in the sync test alongside the others.
17f113708 added the AI-Assisted Development entry to docs/README.md but
left the shipped copy under frontend-modern/public/docs untouched, so the
docs-sync test failed and Build and Test stayed red once the lint error in
front of it was cleared.
Copy the README across and ship AI_TRANSPARENCY.md too, otherwise the new
entry resolves to a missing /docs/AI_TRANSPARENCY.md in the in-app viewer.
Add the pair to the sync test so the two cannot drift again.
Contract-Neutral: shipped documentation sync, no contract delta
Reinstalling after removing /etc/pulse reaches setup_auto_updates before
setup_directories has recreated the config directory. The system.json
write then failed with "No such file or directory" while the run still
printed that automatic updates were enabled, so the installer reported a
state it had not reached.
mkdir -p the config directory first, and fall back to disabling
auto-updates when it cannot be created.
Contract-Neutral: installer behavioural fix, no contract delta
Refs #1663
collectDiskIO hand-rolled a partial exclusion list (loop, ram, dm-) while
the two sibling paths, agent SMART collection and the server-side resource
registry, both use fsfilters.IsVirtualBlockDevice. ZFS zvols were therefore
collected as if they were physical disks: a Proxmox host with ZFS-backed
guest storage exposes one zd<N> device per zvol, so a few hundred guests
produced a few hundred phantom disks in every agent report, driving
sustained metrics and resource_changes growth.
Route the filter through the canonical helper so zd, zram, nbd, rbd, drbd,
md, pmem, vd and xvd are all excluded. md and dm- aggregates additionally
restated the I/O of their own physical members, so counting them alongside
those members double counted host disk I/O.
Refs #1671
The threshold platform tab builder destructured its map callback argument,
which eslint's solid/no-destructure flags because the callback returns JSX.
Build and Test has been red on main since c35ab1ad1.
Use property access on the definition object instead.
Contract-Neutral: presentation-only lint fix, no contract delta
golangci-lint had accumulated 12 findings since 5abb2d8f4. All fixed with
real dedup (no nolint suppressions) and the repo's existing errcheck idioms:
- dupl internal/monitoring: docker/host identity-conflict trackers were
structural clones; extracted a shared identityFlapTracker core with a
domain-neutral identityConflict result. Per-domain files now hold only
the window const and the model translation. Tracker-behavior tests
consolidated into identity_flap_tracker_test.go; Monitor-level
translation and Apply*Report integration tests remain per domain.
- dupl internal/api/router.go: VM/container workload chart loops shared a
16-line live-fallback block; extracted guestChartSeriesWithLiveFallback
over a guestLiveMetricsView interface both views satisfy.
- dupl internal/storagehealth/risk.go: SMART attribute copying extracted
into applySMARTAttributes shared by both assessors (same
*models.SMARTAttributes type on both inputs).
- errcheck pkg/audit/sqlite_logger.go: three defer tx.Rollback() sites
now use the repo-wide defer func() { _ = tx.Rollback() }() idiom.
- errcheck telemetry/notifications tests: send() errors now fail the
test; queue.Stop() uses the package's _ = idiom.
Full test suites pass for all six touched packages.
Contract-Neutral: lint-hygiene restoration: dupl dedup (identical logic extracted to shared helpers) and errcheck idiom fixes; no public-contract or behavioral delta
The resource_metadata table (canonical_id-keyed custom_url/custom_name/
notes/tags) had no readers or writers anywhere in pulse or
pulse-enterprise; guest metadata is owned by the JSON-file store in
internal/config/guest_metadata.go keyed by instance:node:vmid. Stop
declaring the dead table and its index in fresh schemas. Deployed
databases are left alone deliberately — no DROP TABLE migration, since
a stale unused table is harmless. store_test.go now fails if a fresh
store's sqlite_master reintroduces resource_metadata.
The findings runtime keys resources by whatever ID the producer used:
unified-derived findings carry the hashed canonical resource ID, but
Patrol guest inventory rows carry the node-scoped Proxmox source ID
(instance:node:vmid). The operator-state provider closure looked the
reference up in the unified store directly, which keys by canonical ID
only, so maintenance windows, intentionally-offline intent, and
criticality silently never reached guest findings - Patrol could
auto-propose fixes for a guest the operator had flagged as in
maintenance, and the suppression auto-ack never fired for guests.
A reference that misses the store now resolves through the registry
(GetByReference, which after the node-independent guest identity change
also covers retired canonical-ID eras and node-scoped guest source
references) and retries under the canonical ID. Direct canonical hits
stay as cheap as before; the resolution hop only runs on a miss.
Contract-Neutral: operator-state provider ref-resolution fix: router.go closure bug fix with api-contracts delta staged; dependent subsystem contracts (agent-lifecycle, performance-and-scalability, security-privacy, storage-recovery) have no behavioral delta
A guest's unified canonical ID hashed its node-scoped source ID
(instance:node:vmid), so a live migration to another cluster node
re-minted the resource and orphaned every operator-owned row keyed by
the old ID: explicit availability check links (fail-closed by design,
the reported symptom in #1669), alert overrides, operator state, action
audits, manual links, and recovery subjects. VMIDs are unique within a
cluster, so guests now derive their canonical ID from instance+VMID
("proxmox-guest:<instance>:<vmid>") and keep it across migrations. The
guest-metadata half of #1669 was fixed separately at the metadata-store
layer.
Existing installs converge through record-declared succession: ingest
declares the retired node-scoped IDs superseded for every node the
instance currently knows (current names plus native aliases), so rows
orphaned by pre-upgrade migrations also re-key. Successions are now
recorded durably in a canonical_id_successions table, which memoizes
the re-key (steady-state rebuilds re-declare the same eras every tick
without touching SQL) and lets change-journal reads merge retired guest
eras the way pin EraIDs do for hosts. The succession re-key also covers
manual link/exclusion rows.
Availability links resolve retired canonical IDs and old-node source
triples through a registry superseded index plus guest-triple parsing
(persistence keys only, ambiguity fails closed), and the stored
LinkedResourceID re-homes to the current canonical ID on the
alert-migration cadence. Recovery subjects converge on the same
derivation via CanonicalSubjectResourceID, the mapper's registry-miss
fallback, node-independent external guest keys, and the store's startup
backfill, which also sweeps posture rows stranded under retired subject
keys. Metrics history and frontend row identity key off the node-scoped
source ID and are deliberately unchanged.
Public declaration of how AI tools are used to build and maintain
Pulse, linked from the README documentation list and the FAQ.
Automated triage replies now carry a disclosure footer per the
policy this document sets.
Every unattended update created another full config snapshot under
config-backups (or next to the config dir) and nothing ever pruned old
ones, so small root filesystems filled up within days (#1646, reported
on the hardened-unit fallback path where snapshots land under the
install dir). backup_existing now keeps the five newest snapshots and
removes the rest after each successful copy.
Contract-Neutral: installer config backup rotation; shell-only fix, no runtime contract
TrueNAS reserves most RAM for the ZFS ARC and the kernel's MemAvailable
does not count ARC even though it shrinks under memory pressure, so
memory derived from total minus available read ~95% used on every ZFS
system and masked the real pressure (#1668). The realtime telemetry
Pulse already subscribes to carries arc_size, so parse it, subtract the
ARC from effective used the same way node accounting treats buffers and
page cache, and surface it through the existing agent memory cache
field. The TrueNAS systems table stacks the ARC as a reclaimable cache
segment with a Used with ARC cache tooltip row, and the reporting
history query now fetches the arcsize graph so the memory history
subtracts the ARC per point instead of contradicting the current value.
Contract-Neutral: TrueNAS memory accounting treats ZFS ARC as reclaimable cache; existing cache field, no schema change
Guest metadata (web interface URL, tags, notes) is keyed by the
node-scoped guest ID, so a VM or container migrating to another node in
the same cluster orphaned everything attached to it (#1669). VMIDs are
unique within a Proxmox cluster, so when the exact ID and the legacy
formats all miss, an entry for the same instance and VMID under a
different node is the same guest and migrates to the new node-scoped ID.
The explicit availability check link stores the hashed canonical
resource ID, which also embeds the node; that half needs a
node-independent canonical guest identity and is tracked separately.
Contract-Neutral: guest metadata follows node migration; behavioral bug fix, no wire contract or payload change
A hostname change on the same machine and token forked the host module
onto a suffixed identity to stay safe against cloned VMs sharing a
machine ID (#1584), but the fork never healed, so one machine stayed
split into two agent identities forever. In unified installs the
workload modules keep reporting under the base agent ID, so Agent
Doctor flagged both halves with no way to merge them (#1667, Home
Assistant add-on with agent_hostname set after first boot).
Once the pre-rename record has stopped reporting for three health
windows the collision is proven to be a rename, not a clone: the stale
record and the forked identity are superseded and the binding returns
to the base identity. A rename discovered while the old record is
already stale now adopts the base identity without forking at all.
Live clones keep reporting under the old hostname, so they never
match the heal condition and stay forked.
Contract-Neutral: host agent identity rename heal; behavioral bug fix, no wire contract or payload change
A node joining a PVE cluster after setup could never be trusted when the
primary was fingerprint-pinned (#1664). Two independent gaps: cluster
discovery validation only relaxed TLS when client construction failed,
but a pinned-fingerprint mismatch surfaces from the first API call, so
the member was judged not-a-Proxmox-node and its captured fingerprint
discarded; and the cluster client's TOFU refresh only ran for endpoints
that already had a per-endpoint fingerprint, handling rotation but never
first trust. Validation now retries with the member's own captured
fingerprint before rejecting it, TOFU capture runs on first use, and
discovery failures now distinguish DNS, refused, timeout, and TLS
causes in the endpoint error instead of one generic message.
Contract-Neutral: cluster discovery TLS validation bug fix; no wire contract or payload change
install.ps1 set ServicePointManager's certificate callback to a
PowerShell scriptblock. ServicePointManager can invoke that callback on
a worker thread that has no runspace, where a scriptblock delegate
fails closed, so agent downloads died with a TLS error even though the
certificate policy would have accepted them (hit by an rc.6 tester with
Skip TLS verification enabled). The copied install command already
migrated to a compiled validator for exactly this reason in rc.6; the
downloaded script now does the same. Validation policy is unchanged,
with fingerprint pinning first, then insecure, then clean chains, then
the custom CA. Verified with pwsh covering all four policy branches
plus the callback running on a runspace-free worker thread.
Contract-Neutral: installer TLS callback implementation moved to a compiled type; same validation policy, no payload or endpoint change
Console hosts execute pasted input line by line, so the literal
newlines inside the embedded certificate-validator C# broke the copied
command in both Windows PowerShell 5.1 and PowerShell 7 (reported by an
rc.6 tester who had to join it manually before it would run). C# is
whitespace insensitive and the source carries no line comments, so the
type definition now collapses onto the command's single line, and a
test pins every command variant to stay newline free.
Contract-Neutral: copied command formatting only; same PowerShell semantics, no payload or endpoint change
The agent intentionally reports the digest-pinned image sentinel for
image@sha256 references, where there is no tag to resolve against the
registry. Every surface funneled that through the error branch and
rendered a danger-toned Check failed badge for a state that is not a
failure (raised in #1666). The containers table, the images table, and
the container drawer now render a neutral Pinned state with a tooltip
saying why checks do not apply.
Mock containers also gain update check states (current, update
available, digest-pinned) so these badges are exercisable in mock mode
and on the demo, which previously never populated updateStatus at all.
Contract-Neutral: frontend presentation of the existing digest-pinned sentinel plus mock fixture variety; no wire contract change