Commit graph

9042 commits

Author SHA1 Message Date
rcourtman
8e8edbf7de fix(governance): isolate release-control worktree roots 2026-08-08 05:12:07 +01:00
rcourtman
790a7d8ce1 chore(governance): release worktree helper claim 2026-08-08 04:08:44 +01:00
rcourtman
ebff6f9946 fix(governance): restore advertised worktree helpers 2026-08-08 04:08:35 +01:00
Richard Courtman
7fd33dd6fa test(governance): update backend proof snapshot 2026-08-08 03:49:55 +01:00
rcourtman
60c1c51eb9 fix(workloads): serve viewer-safe inventory health 2026-08-08 03:36:55 +01:00
rcourtman
d1f687c0ea fix(governance): enforce frontend dependency audits 2026-08-08 03:26:36 +01:00
rcourtman
d1fa7f38c7 security: narrow historical secret scan allowlist 2026-08-08 03:17:57 +01:00
rcourtman
633d3117f7 fix(governance): fail closed on unsupported commercial evidence 2026-08-08 03:11:12 +01:00
rcourtman
6ba85a1856 fix(frontend): update vulnerable sanitizer dependencies 2026-08-08 02:59:43 +01:00
rcourtman
d130d00867 fix(release): make customer promotion convergent 2026-08-08 02:36:28 +01:00
rcourtman
541c9be7fd fix(websocket): keep oversized recovery baseline-free 2026-08-08 01:35:49 +01:00
rcourtman
98ecfb3f10 fix(updates): gate update-status polling by route authority 2026-08-08 01:21:44 +01:00
rcourtman
7a0f410508 fix(settings): gate all admin-only panels 2026-08-08 01:19:44 +01:00
rcourtman
ff03168e5e test(api): synchronize auth denial log capture
Some checks failed
Build and Test / Backend tests (rest-1) (push) Blocked by required conditions
Build and Test / Backend tests (rest-0) (push) Blocked by required conditions
Build and Test / Secret Scan (push) Waiting to run
Build and Test / Detect changed areas (push) Waiting to run
Build and Test / Frontend (push) Blocked by required conditions
Build and Test / Benchmarks (push) Blocked by required conditions
Canonical Governance / governance (push) Waiting to run
Public docs / check (push) Waiting to run
Build and Test / Backend tests (api) (push) Blocked by required conditions
Build and Test / Script smoke tests & backend build (push) Blocked by required conditions
Core E2E Tests / Playwright Core E2E (shard 7/8) (push) Waiting to run
Unified Agent Native Verification / Linux x64 (push) Waiting to run
Unified Agent Native Verification / macOS ARM64 (push) Waiting to run
Unified Agent Native Verification / macOS Intel (push) Waiting to run
Unified Agent Native Verification / FreeBSD cross-build contract (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 3/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 4/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 5/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 6/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 8/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 1/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 2/8) (push) Waiting to run
Core E2E Tests / Agent registration lifecycle (push) Waiting to run
Core E2E Tests / E2E verdict (push) Blocked by required conditions
Unified Agent Native Verification / Linux ARM64 (push) Waiting to run
Unified Agent Native Verification / Windows x64 (push) Waiting to run
Patrol Qualification Regression / Catalog, scorer, and replay regression (push) Has been cancelled
2026-08-08 01:15:25 +01:00
rcourtman
6d8a509376 fix(settings): serve runtime display settings to viewers 2026-08-08 01:10:40 +01:00
rcourtman
fbee92614a fix(release): restore verifiable MSP evaluation delivery 2026-08-08 01:04:07 +01:00
rcourtman
b0759d20d7 fix(release): activate before mutable customer promotion 2026-08-08 00:58:18 +01:00
rcourtman
4739d9af07 fix(release): reject issue references in highlights 2026-08-08 00:54:38 +01:00
rcourtman
27b4d98bc0 fix(workloads): stop offering viewers an infrastructure page they cannot open
The "No workload inventory available" empty state told every session to
"Review source credentials, permissions, and collection status in Settings
→ Infrastructure" and rendered a button to /settings/infrastructure. Since
755a88878 gated that nav item on the infrastructureRead capability, a
non-admin viewer cannot open the page — and once inventory source health is
served at monitoring:read, a viewer with a broken source lands on this exact
state and is pointed at a door that is locked for them.

Gate the call to action on the destination's own capability. Reusing
infrastructureRead rather than a second predicate is what keeps the link and
the nav gate from drifting apart. Without it the copy now names the action a
viewer can actually take: contact an administrator.

The signal did not exist outside Settings — infrastructureRead only reached
useSettingsAccess's local fetch, which runs when Settings mounts and so is no
help to a page deciding whether to link there. Publish it from the
/api/security/status resolve that useAppRuntimeState already performs on
mount, alongside the sessionCapabilities sync that hangs off the same call.
No new request is added. Unresolved sessions keep the link, matching how
settingsNavVisibility treats an unresolved capability set, so an admin never
flickers through the restricted copy.

The banner itself, the admin path, and the monitoring:read inventory-sources
call are all unchanged. The surface's inline fallback now defers to the
shared presentation helper instead of duplicating the copy, so the gate
cannot be bypassed by whichever path renders.

Verified on a scratch instance against real proxy-auth sessions: the viewer
(detailLevel authenticated, infrastructureRead false) gets the reworded copy
and no link at 1280x800 and 375x812, while the admin (privileged) is
byte-identical to before. Because the empty state is only reachable by a
viewer once inventory health is served at monitoring:read, the live exercise
ran with that branch's route present.
2026-08-07 22:57:16 +01:00
rcourtman
6a958761e8 fix(settings): hide the admin-only System tabs from non-admin sessions
755a88878 gated Settings -> Infrastructure on infrastructureRead but left the
System group untouched. Network, Pulse server updates, and Recovery are the
same shape: pure instance administration - the public URL and CORS boundaries,
the server update channel, backup polling and configuration export/import -
behind routes that are all RequireAdmin + settings:read. A session without it
was still offered all three, could deep-link straight at them, and landed on
panels it can never populate.

Serve systemSettingsRead alongside infrastructureRead and gate the three tabs
on it. requiredCapability blocks the route as well as the nav item, so a deep
link now falls through to the capability-aware fallback that 755a88878 added,
landing on the first tab the session can actually reach.

The new field is a sibling of infrastructureRead, not a reuse of it. They
share the canAccessAdminSurface(config.ScopeSettingsRead) derivation today, but
infrastructureRead is named and documented for the data sources behind one
page. Borrowing it to gate a different page is the same defect as re-deriving
a capability: the two would drift the first time one surface's gate changed,
and the api-contracts rule on published capabilities exists to prevent exactly
that. Each field names the surface it gates and is pinned separately.

These hide rather than render an inline gate, matching Infrastructure rather
than the paid-feature items in the same catalog. A free install seeing a paid
tab can act on it by upgrading, so hiding it would cost discoverability. A
non-admin cannot grant themselves admin, so the tab is only noise.

system-general stays ungated on purpose: theme, language, and unit preferences
there are user-scoped, so gating it would take personal settings away from
every non-admin, and the panel is not empty for them.

Verified against real admin and non-admin sessions rather than a stubbed
capability - one binary built from this worktree behind two header-injecting
proxies differing only in the role header. The viewer sees none of the three
tabs and a deep link to /settings/system-network lands on
/settings/monitoring/availability; the admin keeps all three and the same deep
link resolves to the Network panel. Confirmed at 1280x720 and 375x812.
2026-08-07 22:18:17 +01:00
rcourtman
14a82e7684 fix(api): log routine authorization refusals at debug, warn on the rate
A refusal is the access control working, not a fault, but every one of them
warned twice: once in RequireAuth/RequireAdmin/RequirePermission, and again in
the middleware, which warned on every 4xx unconditionally. A correctly
configured instance therefore could not produce a quiet log, and #1601's rc.9
reporter read that stream as an RBAC regression.

Gating clients one at a time does not fix this. The previous commit stopped six
endpoints being polled by a non-admin UI, and /api/updates/status,
/api/updates/plan and /api/availability-targets still warned, as would every
caller nobody has audited yet.

Refusals now route through logAuthDenial, which records them at debug and counts
them per caller. Attribution prefers the authenticated username so a principal
stays tracked across rotating addresses, falling back to the client IP. Crossing
20 refusals in a minute emits exactly one warn for that window, which is the
shape that separates probing from a UI mounting a surface its session cannot
read; a closed window re-arms it. The tracked set is bounded with oldest-window
eviction so spoofed forwarded-for values cannot grow it. The middleware now
warns only on 5xx.

Enforcement is untouched: every route returns the same status to the same
callers, and the contract test pins that pairing so a future attempt to quiet
the log by relaxing enforcement fails rather than passes. Verified live on a
proxy-auth instance - /api/connections, /api/updates/status and
/api/system/settings still 403 for a viewer and 200 for an admin; 19 refusals
produce no warn, the 20th produces one, and 30 more produce none; an idle
non-admin browser session logged zero warn lines across 90 seconds.
2026-08-07 21:38:47 +01:00
rcourtman
75fcc92424 fix(websocket): recover state over REST when the snapshot exceeds the inbound guard
The client drops any inbound frame larger than
MAX_INBOUND_WEBSOCKET_MESSAGE_BYTES (8 MiB). Measured against a real
/api/state payload, resources average 2673 bytes each, so a full
initialState snapshot crosses the guard at 3121 resources and reaches
12.79 MiB at 5000. On those estates the snapshot was discarded and the UI
rendered empty.

Since 413ca4e7b the delta path noticed the missing baseline and asked the
server for a full snapshot via `requestData`, but that response is the same
oversized payload, so it was dropped too: a 30s retry loop that never
hydrated.

Recover over REST instead. /api/state serves the same
monitor.BuildFrontendState() value the socket sends as initialState
(prepareStateForBroadcast is a pass-through), over a transport with no
frame-size ceiling, so the snapshot arrives intact. Feeding it through the
normal message handler establishes the delta baseline a socket snapshot
would have, which is what keeps recovery from stalling again on the next
tick. Both triggers -- a frame dropped as oversized, and a delta arriving
with no baseline -- now share one throttled entry point, so a server that
repeatedly emits an undeliverable snapshot cannot turn into a REST hammer.

Compression was not an option here: permessage-deflate is already enabled on
the hub, and the browser decompresses before the guard runs, so the cap
applies to decompressed bytes either way. Raising the cap only moves the
cliff and inflates transient heap, since an 8 MiB payload already costs a
JSON.parse plus a structuredClone of the resources array.

Verified live against a backend whose real snapshot is ~741 KB, with the cap
temporarily lowered to 256 KiB to reproduce the condition: the guard trips,
REST recovery hydrates 265 resources across 48 types, and subsequent deltas
keep applying to that baseline. With the 8 MiB cap restored the same frame
hydrates over the socket with no REST fetch. Regression tests pin the byte
boundary in both directions, the REST-established baseline, and the absence
of the old requestData loop.

Contract-Neutral: client-side oversized-snapshot recovery on a websocket error path: no alert identity, evaluation, threshold, routing or ownership delta. Preserves the contract's active-alert-truth and bounded-recentlyResolved invariants (alert state now hydrates on estates where the dropped snapshot previously left it empty). No public contract surface added.
2026-08-07 20:50:32 +01:00
rcourtman
755a888787 fix(settings): stop non-admin sessions polling admin infrastructure endpoints
Settings -> Infrastructure reads /api/connections, /api/config/nodes,
/api/system/settings, /api/truenas/connections and /api/vmware/connections on
mount and then polls /api/connections every 15s and /api/discover every 30s.
Every one of those is RequireAdmin, so an authenticated non-admin rendered a
page where nothing loaded while each poll reprinted "Non-admin user attempted
to access admin endpoint" at warn level. Measured on a proxy-auth viewer
session: 6 denials/minute from this page alone on an idle tab.

Serve infrastructureRead alongside the other settings capabilities, derived
from the same canAccessAdminSurface(settings:read) expression the routes
enforce, and gate the nav item on it. Two follow-on fixes were needed because
the page is not the only mount point:

- DEFAULT_SETTINGS_TAB is infrastructure-systems, so the blocked-route
  fallback pointed straight back at the tab it had just refused. It now falls
  back to the first tab the session can actually reach.
- Settings.tsx constructs useInfrastructureSettingsState for every settings
  tab, so the discovery poller and the TrueNAS/VMware mount fetches ran no
  matter which tab was open. They now wait on the same capability. The
  TrueNAS/VMware loads moved from onMount to an effect so admins still load
  once the capability resolves, rather than sampling it before it exists.

Verified against a local instance behind a header-injecting proxy-auth shim:
viewer goes from 6 infrastructure denials/minute to 0, admin keeps both
pollers armed (/api/discover x4 and /api/connections x3 over 152s) with 0
denials. Re-checked at 1280x800 and 375x812 for both roles.
2026-08-07 20:22:04 +01:00
rcourtman
06d8bfe65e docs(commercial): stage the owed release-note copy for the surfaces revision
The 2026-08-07 commercial-surfaces revision owed a release-note entry, and
its instrumentation (schema-v8 business_estate, checkout source attribution)
has now landed too. v6.2.0-rc.9 was already tagged before any of it merged,
so the copy is staged in the record rather than written into a
version-numbered packet: picking the next version and generating its packet
is the governed release-preparation step, not this one.

The copy is written for someone who will notice paid-feature pages appearing
in a free install and want to know why, so it leads with what changed on
screen, states that every button on the business-estate card dismisses it
permanently, and confirms demo, kiosk, white-label, and MSP tenant runtimes
stay clean. It also declares the telemetry field and the checkout-surface
attribution plainly rather than leaving them for someone to find in a diff.

Every claim was checked against source: the thresholds against
internal/monitoring/business_estate.go, the dismissal behaviour against
BusinessEstateCard.tsx, and the opt-out and schema number against
internal/telemetry/telemetry.go.
2026-08-07 20:10:23 +01:00
rcourtman
26b0227dcd build(hooks): raise pre-commit timeout ceilings for contended machines
Three commits failed today on timeouts that had nothing to do with their
diffs. golangci-lint hit its 5m default while loading packages, and
TestStatusJSONLaneEvidenceReferencesAreStructured hit Go's 10m default after
9m11s with its goroutine parked in a syscall. Both ran at load average 185,
with four pre-commit hooks and two dozen eslint and vitest processes from
other agent sessions competing for the same box. Each failure cost a full
re-run to diagnose, and the same commits passed unchanged once the machine
went quiet.

The repoctl guardrail tests are I/O bound rather than CPU bound: they os.Stat
every evidence path status.json references, across repos, so they degrade
badly under contention while doing nothing wrong. golangci-lint already
exposed GOLANGCI_LINT_TIMEOUT; the go test line had no -timeout at all and
silently inherited the 10m default.

This raises the ceilings to 20m and 30m and adds PULSE_HOOK_GO_TEST_TIMEOUT
for the latter. No check is skipped and no rule is relaxed: on an idle
machine these stages finish in seconds to minutes and the change is inert.
The cost is that a genuinely hung test now takes longer to surface, which is
the better trade against false failures that read as real ones.
2026-08-07 20:07:18 +01:00
rcourtman
d5bc3e3862 feat(commercial): attribute in-app checkouts to the surface that started them
Stripe checkouts could not be told apart by origin: every session was
stamped checkout_origin pulserelay_landing, so an upgrade started from an
RBAC gate looked identical to one started from the website. Gate CTAs now
stamp a closed-vocabulary source token (gate-<feature>, estate-card, or the
plans-page default) onto the owned billing plan route; the plan page threads
it into the purchase-start handoff, and the license server persists it on the
checkout intent and stamps Stripe metadata checkout_source.

Attribution is authenticated-session-only by construction. The public
/pricing route and the public pricing URL never carry it, and
getSelfHostedPurchaseStartUrl scrubs the parameter from forwarded query
strings so a crafted website link cannot claim in-app origin. Server-side,
source is validated against the same kebab vocabulary, skip-listed from the
Pulse Account portal redirect exactly as feature already is, and travels
only inside the handoff body. It is request-scoped and persists nothing on
the install; the cancel return echoes it so a retry keeps its origin.

The handoff field is omitempty because the license server decodes strictly,
so source-less installs stay compatible with a server either side of the
field being added. That ordering is recorded in pulse-pro OPERATIONS.md.
2026-08-07 19:45:13 +01:00
rcourtman
5b07bdc3d8 feat(telemetry): add business_estate to the ping at schema v8
The 2026-08-07 telemetry read showed installs at or above 5 PVE nodes, 10
Docker hosts, or 3 VMware hosts convert to paid at ~8x the rate of smaller
estates. The ping now carries that classification as a server-derived
boolean so receiver-side cohort queries keep a stable column even if the
thresholds move later; it is derived in the pkg/server snapshot closure
from the same AggregateInstallSnapshotCounts values the payload already
sends, so no new information leaves the install.

The thresholds move to internal/monitoring/business_estate.go as the
single definition; the session-capability surface behind the in-product
business-estate card delegates to it, and dropping the now-unneeded direct
pkg/licensing import there restores TestPkgLicensingImportBoundary, which
f0e2243b4 had left red. All three payload surfaces (Ping struct, private
receiver, TelemetryPingPreview) move together per
check_telemetry_schema_parity.py, and both PRIVACY.md copies document the
field.

Verified live on an isolated worktree backend with 6 mock PVE nodes: the
Settings telemetry preview renders schema_version 8 with business_estate
true, and /api/security/status still reports
sessionCapabilities.businessEstate true through the delegated thresholds.
2026-08-07 19:44:52 +01:00
rcourtman
2aa43058b7 fix(alerts): send only the fields the Patrol run endpoint accepts
3f4595386 removed the context field from manualScopedPatrolRequest and
enabled DisallowUnknownFields on the run body, but the alert
"Have Patrol investigate" button still sent context, so every click
returned 400 invalid_patrol_scope (reported on the #1601 rc.9 retest).
Drop the field from the call site and from PatrolRunScope; the backend
already derives the scoped run's context from alert_type.

Refs #1601

Contract-Neutral: frontend stops sending a field the Patrol run endpoint already rejects; no request or response shape change server-side
2026-08-07 19:33:55 +01:00
rcourtman
6a36b5f8ef test(settings): re-pin security-webhooks nav lock for the revised commercial posture
The commercial-surfaces revision (f0e2243b4) made security-webhooks
relay-shaped: the catalog entry dropped hideWhenUnavailable so the tab
stays visible for free installs with a panel-owned inline gate, and the
tab already carries a tabFeatureRequirements entry, so
isSettingsNavItemLocked now honestly reports it locked. The
settingsNavigation.integration.test.tsx gatedTabs pin was re-pinned by
the revision; the branchcov2 never-locked pin was not, and has been
failing on main since.

Re-pin to the revision's intent rather than flipping the assertion:
give security-webhooks a dedicated locked pin mirroring system-relay's,
drop it from the never-locked list, and refresh the two comments that
still claimed security-roles and friends carry hideWhenUnavailable
(only the organization-* tabs still do; the security-roles assertion
now exercises the no-requirements branch, so it was swapped for
organization-access to keep the early-branch coverage honest).
2026-08-07 19:02:21 +01:00
rcourtman
af7f0cb686 docs: sync shipped MSP.md with the repo doc
The MSP evaluation download step (734024c46, 6b7644345) landed in
docs/MSP.md without updating the copy shipped under
frontend-modern/public/docs, so docsLinks.test.ts's shipped-docs sync
pin has been failing on main. Byte-sync the shipped copy.
2026-08-07 18:48:31 +01:00
rcourtman
413ca4e7b5 fix(websocket): apply resource deltas to the raw server baseline
The rc.9 delta stream (962d29780) diffs resources against the server's
per-client snapshot, but the client applied those patches to its
canonically merged store. On estates where the canonical coalesce folds
agent and runtime-platform hosts into one resource the two domains
disagree: a merge patch for a coalesced-away ID materialised as a
typeless stub resource, and a server-side removal of the merged
carrier's ID deleted both facets with nothing ever re-adding the
survivor. Resources then vanished progressively — platform tabs
included, since navigation visibility derives from state.resources.
Reported on the #1601 rc.9 retest for both admin and read-only users;
the 403s in that report are an unrelated non-admin update poll.

Keep a pristine copy of the server's resource array, apply deltas to
that raw baseline (the exact objects the server diffed against), then
derive the canonical view through the same merge the full-snapshot path
already uses. Delta application now converges to the same state a full
snapshot would produce, by construction. The raw baseline stays
isolated from the store: entries are structuredClone'd before the
canonical merge because the merge output shares nested references and
reconcile mutates adopted objects in place. A delta arriving before any
full snapshot (for example after an oversized initial payload was
dropped) now requests a full snapshot instead of patching nothing.

The flipped status assertion in websocket-unified.test.ts is
deliberate: the old delta path deleted keys that the canonical merge
preserves, so it disagreed with the full-snapshot path; the assertion
now pins delta/full parity.

Refs #1601

Contract-Neutral: client-side websocket delta application fix; wire protocol and payload shapes unchanged
2026-08-07 17:23:26 +01:00
rcourtman
0928071b9a fix(actions): gate approvals on live readiness 2026-08-07 17:03:10 +01:00
rcourtman
8fd43b307b fix(release): simplify in-app highlights
Some checks are pending
Build and Test / Script smoke tests & backend build (push) Blocked by required conditions
Build and Test / Backend tests (api) (push) Blocked by required conditions
Build and Test / Backend tests (rest-0) (push) Blocked by required conditions
Canonical Governance / governance (push) Waiting to run
Helm CI / Lint and Render Chart (push) Waiting to run
Public docs / check (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 3/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 4/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 6/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 7/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 8/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 1/8) (push) Waiting to run
Unified Agent Native Verification / Windows x64 (push) Waiting to run
Unified Agent Native Verification / macOS ARM64 (push) Waiting to run
Unified Agent Native Verification / macOS Intel (push) Waiting to run
Unified Agent Native Verification / FreeBSD cross-build contract (push) Waiting to run
Build and Test / Backend tests (rest-1) (push) Blocked by required conditions
Build and Test / Secret Scan (push) Waiting to run
Build and Test / Detect changed areas (push) Waiting to run
Build and Test / Frontend (push) Blocked by required conditions
Build and Test / Benchmarks (push) Blocked by required conditions
Core E2E Tests / Playwright Core E2E (shard 5/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 2/8) (push) Waiting to run
Core E2E Tests / Agent registration lifecycle (push) Waiting to run
Core E2E Tests / E2E verdict (push) Blocked by required conditions
Unified Agent Native Verification / Linux ARM64 (push) Waiting to run
Unified Agent Native Verification / Linux x64 (push) Waiting to run
2026-08-07 15:50:39 +01:00
rcourtman
f0e2243b44 feat(commercial): revise self-hosted commercial surfaces (supersede RA5 opt-in posture)
Free self-hosted sessions now see reactive commercial surfaces: paid-feature
nav items stay visible with panel-owned inline gates (Relay precedent extended
to reporting, roles, users, audit log, audit webhooks), gate CTAs render, and
Plans & Billing is discoverable and carries an MSP/provider section with the
free 60-day evaluation. Adds a one-shot business-estate card for authenticated
free sessions whose monitored estate crosses business-scale thresholds
(>=5 PVE nodes, >=10 Docker hosts, or >=3 VMware hosts); the flag rides
sessionCapabilities only, never the pre-auth presentation policy, so estate
size cannot leak to anonymous visitors. Demo mode and white-label runtimes
(including MSP tenant containers) keep full commercial suppression; the
multi_tenant organization nav items deliberately stay hidden.

Decision record supersedes the 2026-04-25 opt-in record:
docs/release-control/v6/internal/records/self-hosted-commercial-surfaces-revision-2026-08-07.md
RA5 summary, contract pins, nav integration pins, and the RA5 browser spec
are updated to the revised invariant in the same commit.
2026-08-07 13:47:46 +01:00
rcourtman
1019310adf fix(release-control): unblock RA10 documentation currentness proof
The public repository surface refresh (40d29ca44) hard-wrapped the
Settings → Infrastructure → Install on a host phrase mid-line and
dropped the v5-to-v6 agent upgrades mention from README.md, breaking
RA10's documentation-currentness-tests proof on main.

- documentation_currentness_test.py: match prose fragments against
  whitespace-normalized content, the way markdown renders it, so an
  editorial rewrap can no longer fail the guard. Structural fragments
  (table rows, gate headings) and all assertNotIn checks still match
  raw source.
- README.md: restore the v5-to-v6 agent upgrades mention the refresh
  dropped; the invariant is still live while v5 installs migrate.
2026-08-07 13:17:19 +01:00
Richard Courtman
5e9db16927 fix(release): stage artifacts before publication 2026-08-07 12:27:36 +01:00
rcourtman
d699dc66a0 feat(agent): converge agent self-update within one report cycle
The server now echoes its version on unified-agent report acks, and the
agent nudges its auto-updater the moment an ack carries a newer version.
After a server upgrade, agents converge within one report interval
instead of waiting out the hourly update check, so the "older Pulse
agent" notice self-resolves in seconds once upgraded agents report in.

The hourly loop stays as the retry and backstop path. Nudges dedupe per
server version, refuse downgrades, skip disabled and development-mode
updaters, and never fire from observer destination acks — only the
authoritative server may steer an agent's updater, and a nudged check
re-validates against the server and runs the existing checksum and
self-test pipeline before swapping binaries. Agents deployed before
this change still converge on their old hourly cadence once; every
upgrade after that lands within a report cycle.

Contract deltas recorded in agent-lifecycle and api-contracts, with
boundary notes in security-privacy (no update authority in the echo),
performance-and-scalability (no steady-state work), and
storage-recovery (nothing persisted).
2026-08-07 11:40:46 +01:00
rcourtman
dd008f1c2d fix(alerts): resolve intent overrides for agents merged with Proxmox nodes
A pulse-agent installed on a Proxmox node merges into one canonical
resource whose primary ID is node:{sourceID}, so the agent:{hostID}
reference the alert evaluator uses (hostResourceID) matched nothing in
the registry. Per-resource grace overrides and operator-state lookups
then silently resolved to factory policy: alerts fired with no grace
while the policy preview, which receives the canonical ID directly,
showed the override working. Index the prefixed agent ref as a
canonical identity alias so runtime resolution lands on the same
resource the override was saved against.

Refs #1497

Contract-Neutral: behavioral fix #1497: index agent: ref as canonical alias, no public contract shape change
2026-08-07 10:55:38 +01:00
rcourtman
4885c378aa Keep the operator .env out of the Traefik container and unhardcode the DNS-01 provider
Traefik took the whole operator .env via env_file, so the internet-facing
container's environment carried CP_ADMIN_KEY and the entitlement signing
private key, neither of which it needs. It now receives only ACME/DNS
material: the CF token by explicit passthrough plus an optional
dns-credentials.env that setup.sh creates 0600.

The same wiring removes the silent Cloudflare-only narrowing: the resolver
provider is now ACME_DNS_PROVIDER (default cloudflare) via Traefik's env
override, the CF token is required only for the default provider, and any
other Traefik dnsChallenge provider supplies its credential variables through
dns-credentials.env. Pinned by
TestProviderMSPTraefikEnvIsMinimalAndDNSProviderOverridable; contract delta
in deployment-installability.
2026-08-07 10:47:13 +01:00
rcourtman
734024c460 Add MSP and its free evaluation to the README editions list
The MSP page has led with the free two-client evaluation since 08-03,
but nothing outside pulserelay.pro mentioned MSP at all, so only
visitors who already found the site could discover it. The README is
the highest-traffic owned surface; give MSP the same one-bullet
treatment as the other editions, worded to match msp.html's own claims.
2026-08-07 10:36:16 +01:00
rcourtman
6b76443455 Give the MSP evaluation docs their missing download step
The Start free button on the MSP page lands on this section, which described
the self-issued licence but never said how to obtain the bundle. The natural
guess, the latest stable tarball, ships a setup.sh that predates the
evaluation flow and dies asking for a licence. Point evaluators at main and
state the host prerequisites, which were documented nowhere in this guide.
2026-08-07 10:10:51 +01:00
Richard Courtman
8a616b4b56 Prepare v6.2.0-rc.9 release 2026-08-07 10:03:16 +01:00
rcourtman
c9e66b2295 release: pin code-signing policy link to the release tag
Some checks are pending
Build and Test / Script smoke tests & backend build (push) Blocked by required conditions
Build and Test / Secret Scan (push) Waiting to run
Build and Test / Detect changed areas (push) Waiting to run
Build and Test / Frontend (push) Blocked by required conditions
Build and Test / Backend tests (api) (push) Blocked by required conditions
Build and Test / Backend tests (rest-0) (push) Blocked by required conditions
Build and Test / Backend tests (rest-1) (push) Blocked by required conditions
Build and Test / Benchmarks (push) Blocked by required conditions
Canonical Governance / governance (push) Waiting to run
The docs link drift guard (test_repo_docs_link_drift.py) fails CI because
render_release_body.py links docs/CODE_SIGNING_POLICY.md at blob/main.
Release bodies must reference the docs snapshot of the release they
describe, so the installation section now links blob/v{version}, matching
how the rest of the body pins to the version being cut. The unit test pin
follows. Also skip .claude/ in the drift guard so local runs on a shared
tree are not polluted by stale agent worktrees under .claude/worktrees
that CI never checks out.
2026-08-07 01:13:56 +01:00
Richard Courtman
99407ee74c Fix QNAP RAID role bitmap parsing
Some checks are pending
Build and Test / Backend tests (api) (push) Blocked by required conditions
Build and Test / Secret Scan (push) Waiting to run
Build and Test / Detect changed areas (push) Waiting to run
Build and Test / Frontend (push) Blocked by required conditions
Build and Test / Backend tests (rest-0) (push) Blocked by required conditions
Build and Test / Backend tests (rest-1) (push) Blocked by required conditions
Build and Test / Script smoke tests & backend build (push) Blocked by required conditions
Build and Test / Benchmarks (push) Blocked by required conditions
Canonical Governance / governance (push) Waiting to run
Helm CI / Lint and Render Chart (push) Waiting to run
Patrol Qualification Regression / Catalog, scorer, and replay regression (push) Waiting to run
Public docs / check (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 1/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 2/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 3/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 4/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 5/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 6/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 7/8) (push) Waiting to run
Core E2E Tests / Playwright Core E2E (shard 8/8) (push) Waiting to run
Core E2E Tests / Agent registration lifecycle (push) Waiting to run
Core E2E Tests / E2E verdict (push) Blocked by required conditions
Unified Agent Native Verification / Linux ARM64 (push) Waiting to run
Unified Agent Native Verification / Linux x64 (push) Waiting to run
Unified Agent Native Verification / Windows x64 (push) Waiting to run
Unified Agent Native Verification / macOS ARM64 (push) Waiting to run
Unified Agent Native Verification / macOS Intel (push) Waiting to run
Unified Agent Native Verification / FreeBSD cross-build contract (push) Waiting to run
Addresses #1688.
2026-08-06 23:39:15 +01:00
Richard Courtman
e84042a4d9 Fix multi-architecture update digest reporting
Addresses #1689.
2026-08-06 23:31:34 +01:00
Richard Courtman
844d5f3a86 Fix platform-scoped alert threshold hosts
Addresses #1593.
2026-08-06 23:24:25 +01:00
rcourtman
bb75580073 fix(ui): keep responsive filter panels reachable 2026-08-06 22:57:04 +01:00
rcourtman
b765197426 test(alerts): repin resource-incidents placement after a0113b964
a0113b964 moved AlertResourceIncidentsPanel out of HistoryTab and into the
row that asked for it, but left two source-boundary assertions pinning the
old page-level placement:

  frontendResourceTypeBoundaries.test.ts:2796
  Alerts.helpers.test.ts:429 and the exact-JSX pin at 434

The panel move is correct, so the assertions are what is stale. At page level
the panel opened thousands of pixels above a reader scrolled into the history
and the per-row Resource button read as inert (#1687); the fix mounts it under
the originating row, gated on that row owning the open request so exactly one
panel is open at a time.

Retarget both pins onto AlertHistoryTableAlertRow, where the panel now lives,
and turn the HistoryTab pin negative so a regression back to page-level
placement fails the boundary rather than passing it. The rowKey gate is pinned
alongside the mount because the gate, not the mount, is what makes the button
land in view. The phone card list keeps its behavioural coverage in
__tests__/AlertHistoryMobileList.test.tsx, so it is not duplicated here.

Verified: both boundary suites green (59 tests), full alerts feature suite
green (59 files, 960 tests), prettier clean.
2026-08-06 22:45:13 +01:00
rcourtman
aae8aef4ab fix(security): bound provider MSP restore writes
restoreProviderMSPArchiveFile copied each tar entry with an unbounded
io.Copy, so the only limit on what a restore wrote to disk was the size
the archive declared for itself. A gzip bomb, a PAX sparse entry claiming
a huge logical size, or a corrupt stream could fill the target volume.

Bound extraction the way readProviderMSPBackupManifestBytes already
bounds the manifest: a per-entry cap and a cumulative cap across the
whole restore, both enforced against the bytes actually copied rather
than the declared header size. That size comes from the archive, so it
is only good for an early reject, never as the bound. An entry that
overruns fails the restore and its partial file is removed, rather than
being silently truncated into a file that looks complete.

A restore that failed partway had already deleted whatever it replaced,
leaving a half-populated control plane that looks bootable and forcing
the retry to use replace. Roll the partial restore back to an empty
target instead, and say so in the error.

Follows the zip-slip fix in the same function (CodeQL alert 314).
2026-08-06 22:39:20 +01:00
rcourtman
a06205c9e5 test(ui): repin touch-target classes missed by 2d04ffaa3
SummaryRowActionButton disclosure buttons and the FilterBar inline select
were resized to 44px mobile touch targets (h-11/min-h-11 with sm:
compact overrides) in the mobile interaction sweep, but these two pins
still asserted the old desktop-only sizes.
2026-08-06 22:16:34 +01:00