Commit Graph

4282 Commits

Author SHA1 Message Date
rcourtman 0027c82f84 Record rc.5 release convergence evidence 2026-08-21 17:56:23 +01:00
rcourtman 8869753b1e Require tailnet access for every paid runtime proof 2026-08-21 17:52:19 +01:00
rcourtman b16b8e5242 Bind Helm convergence to the release repository 2026-08-21 17:46:55 +01:00
rcourtman 1ef8797d28 Repair release activation recovery contracts 2026-08-21 17:43:01 +01:00
rcourtman 1327dddad5 Compress release backend test selectors 2026-08-21 17:04:18 +01:00
rcourtman 418402bf9e Use complete PVE release worktree 2026-08-21 16:19:11 +01:00
rcourtman d45ecd9a24 Include release helpers in PVE checkout 2026-08-21 16:14:21 +01:00
rcourtman d6591da900 Support PVE sparse release checkout 2026-08-21 16:11:17 +01:00
rcourtman d9e634a4eb Run release preparation on PVE 2026-08-21 16:07:33 +01:00
rcourtman b261e42901 Record rc.5 acceleration qualification 2026-08-21 15:50:06 +01:00
rcourtman ae27ad7511 Bound release API test batch arguments 2026-08-21 15:47:12 +01:00
rcourtman 4217f72bdc Prepare v6.3.0-rc.5 release 2026-08-21 15:14:23 +01:00
rcourtman d505fce29d Record API decomposition qualification 2026-08-21 14:56:11 +01:00
rcourtman c68d5dd3d8 Extract configuration API runtime package 2026-08-21 14:56:07 +01:00
rcourtman 58bf77c1ae Extract alert delivery API package 2026-08-21 14:56:04 +01:00
rcourtman f76a8279a0 Parallelize release archive validation 2026-08-21 14:52:06 +01:00
rcourtman 74bd06a953 Promote qualified release payloads directly 2026-08-21 14:47:52 +01:00
rcourtman 962df694e9 Accept architecture-bound server signatures 2026-08-21 14:31:42 +01:00
rcourtman c661bdab57 Overlap inert release staging and qualification 2026-08-21 14:25:42 +01:00
rcourtman ae317c96bb Publish images from exact candidate payloads 2026-08-21 14:17:12 +01:00
rcourtman 97cf30aed6 Qualify containers in candidate workflow 2026-08-21 14:03:25 +01:00
rcourtman 51f4c64322 Qualify exact candidate containers on PVE 2026-08-21 13:56:52 +01:00
rcourtman c6bf50b455 Stage private release assets during qualification 2026-08-21 13:30:24 +01:00
rcourtman dcc67662b6 Stage release archives directly in parallel 2026-08-21 13:23:44 +01:00
rcourtman a5893e916c Use persistent caches on PVE release runners 2026-08-21 13:14:33 +01:00
rcourtman 312aa2c7e1 Govern private PVE release compilation 2026-08-21 13:09:40 +01:00
rcourtman d44c15a4fd Compile release payloads in parallel on PVE 2026-08-21 12:59:32 +01:00
rcourtman 0293f67635 Preserve API test order in release shards 2026-08-21 12:46:50 +01:00
rcourtman 9dac68fd62 Parallelize release qualification on PVE runners 2026-08-21 12:24:47 +01:00
rcourtman 3c97e94094 Restore the rc.4 release-note contract
The publish-body condensation removed exact operator-facing statements pinned by the prerelease packet test. Restore those statements within the three-highlight limit, record the packet contract, and strengthen the proof to require the complete publish-safe sentences.
2026-08-21 09:36:04 +01:00
rcourtman 547fa77892 Describe autonomous support email in the AI transparency statement 2026-08-21 09:04:23 +01:00
rcourtman ae5df19b62 Give release backend tests hosted-runner headroom
The internal/api race suite now routinely exceeds the old 20-minute package timeout on hosted runners while passing. Set a governed 30-minute package timeout and 40-minute release job ceiling, pin the relationship with contract tests, and refresh the rc.4 packet with the fixes landed since preparation.
2026-08-21 08:49:05 +01:00
rcourtman 5c13befcac Run the QNAP agent from the data volume instead of the RAM-backed root
The installer staged the download in /tmp and installed the runtime
binary to /usr/local/bin, both on the small RAM-backed QTS/QuTS hero
root, and the boot wrapper copied 34MiB back onto that root at every
boot. Roots without ~50MiB of headroom could not install at all, and
setting TMPDIR only moved the staging half of the requirement.

QNAP's own QPKG packages execute from the data volume, so do the same:
relocate the install dir to the data volume's state dir before the
preflight and download, default TMPDIR there too, skip the boot-time
self-copy when the stored and runtime binaries are one file, and remove
a pre-relocation runtime copy from /usr/local/bin to give that space
back. Split layouts with an operator-supplied state dir keep the copy
semantics. The rendered wrapper is exercised in both layouts by the
installer tests.

Refs #1617

Contract-Neutral: Refs #1617: QNAP installer layout fix with its deployment-installability contract clause staged in this commit; residual proof policies for unrelated boundaries do not apply to this shell-only change
2026-08-21 06:35:52 +01:00
rcourtman 7da385cf28 Prepare v6.3.0-rc.4 release 2026-08-21 00:00:37 +01:00
rcourtman 4c7b1a2434 Fix Docker-in-LXC probe storm against slow Proxmox hosts
The guest Docker socket probe hung minipc hard enough to need a power
cycle (2026-08-20): ~100 orphaned pct exec children, load 133, sshd and
pveproxy starved. Three bugs chained, each fixed here:

1. Dispatcher re-issued a probe while the previous one was still
   executing. The poll cycle's enrichment context had expired, so
   ExecuteCommand dispatched, returned the context error 50ms later,
   and the next 3s cycle sent the identical command again — unbounded
   concurrency against a host that was slow to begin with. The
   monitoring dispatcher now takes a per-guest in-flight claim before
   dispatching probe or inventory commands (completed probes release
   it; abandoned ones hold it for a 2-minute window), and both dispatch
   paths bail out under a dead context.

2. The host agent never got the July process-leak fix: 45480a5cc
   landed only on pulse/v6-release, so main-line agents killed just the
   direct shell on timeout, orphaning pct exec → lxc-attach children
   and blocking Wait on their inherited pipes (10s timeouts reported as
   300s+ durations). Port it: run each command in its own process
   group, SIGKILL the group on cancel, bound Wait with WaitDelay, and
   treat ErrWaitDelay after a clean exit as success.

3. Server-side abandonment never reached the agent. ExecuteCommand and
   ReadFile now refuse to dispatch under an already-expired context,
   and send a best-effort cancel_command when they stop waiting; the
   agent cancels the in-flight execution (killing its process group)
   and reports "command canceled". Older agents ignore the unknown
   message type.

Also add a per-node circuit breaker: three consecutive command failures
on one node suspend all Docker probe/inventory dispatch to it on the
existing 1m→30m backoff schedule, so a host-level stall (NFS flapping)
stops the probing entirely instead of failing guest by guest.

Regression tests simulate the storm without hardware: a never-returning
executor is not re-issued across poll cycles, an expired context
dispatches nothing and records no failure, abandoned probes hold their
claim, the breaker blocks new guests on a failing node, and the agent
kills the whole process group on timeout and on server-issued cancel.

Contract-Neutral: monitor.go delta is three private struct fields holding Docker probe dispatch state; host-agent deletion/re-enrollment lifecycle untouched — contracts and all other proofs are staged
2026-08-20 23:33:36 +01:00
rcourtman df1290066e Relax NoNewPrivileges only when a least-privilege sudo grant is active
Live proof on a real systemd host (Debian 12 container, full
install/report/update/uninstall cycle) caught what no unit test did:
NoNewPrivileges=true blocks sudo outright, so the --grant-smart and
--grant-pct helpers failed inside the service and SMART/pct silently
disappeared while the install reported success. A unit with an active
grant now sets NoNewPrivileges=false, keeping the rest of the hardening;
a grantless least-privilege install keeps NNP enabled. Docs and contracts
state the trade-off: each grant is a scoped, auditable widening.

Also proven in the same live cycle: the flag-conflict refusals, user and
sudoers provisioning, wrapper execution as the service user, the healthy
privilege report ({runningAsRoot:false, serviceUser:pulse-agent,
smartctlHelper:true}), --update preserving the profile without repeated
flags, and uninstall removing the sudoers file and helpers.
2026-08-20 21:36:34 +01:00
rcourtman 4686efd8c8 Add a supported least-privilege agent install profile
The unified agent's Linux installer only offered the root profile, and the
docs called non-root unsupported. That default is the most-cited reason
security-conscious evaluators reject Pulse without trying it. install.sh
gains --least-privilege: the service runs as a dedicated nologin
pulse-agent system user with every existing hardening directive, no
LXC-attach ambient capabilities, docker-group membership for socket reads,
and refusal (not silent root fallback) on appliance platforms, non-systemd
init systems, and --enable-commands. Optional --grant-smart and --grant-pct
restore the only two collectors that need elevation through
visudo-validated exact-command sudoers rules and root-owned wrappers the
agent reaches via new absolute-path-only PULSE_SMARTCTL_PATH /
PULSE_PCT_PATH overrides; the pct grant covers pct list and pct df only
and can never widen into pct exec. --update preserves the profile and its
grants by reading the installed unit.

The agent now authors a privilege block in its report (effective root,
service user, active helpers), carried through models into the fleet
doctor as a descriptive field: Agent Doctor shows the profile and its
helpers instead of presenting intentionally absent collectors as a fault,
and a least-privilege agent can never be marked unhealthy on that evidence
alone.
2026-08-20 20:49:44 +01:00
rcourtman 74873e2b55 Add a user-facing alert delivery log and honest test-send results
The delivery health verdict said when something was wrong; nothing showed
what actually fired and where it went. Expose the queue's retained
per-attempt audit rows as GET /api/notifications/delivery-log (newest
first, retention-labeled, webhook secrets redacted from error text) and
render them as a Recent delivery activity card on the alert destinations
tab, with outcome badges, destination names, and failure classes. Audit
rows now persist the normalized destination identity in a destination_id
column; older rows fall back to their operational links.

Test sends bypass both the queue and the activation gate, which is exactly
how installs came to believe delivery worked while every real alert was
suppressed (483 installs in the 08-18 telemetry read). Successful test
responses now carry deliveryPaused: true whenever the manager is gated
off, and the destinations UI warns instead of celebrating.
2026-08-20 17:35:13 +01:00
rcourtman fd35c54773 Add canonical infrastructure search completion 2026-08-20 17:22:37 +01:00
rcourtman 3290dd8d17 docs(governance): record the owner-approved business tier launch
The dormant self-hosted business tier opens for public checkout under the
dials the owner approved on 2026-08-20: \$399/year annual-only, unlimited
seats, 365-day history, next-business-day support target, Pro-identical
features per cloud-paid Extension Point 26. Newly issued Pro
subscriptions carry a 3-seat max_users limit on new Stripe price ids;
previously issued licenses keep their unlimited posture because their
plan entries are untouched. The 2026-08-08 opt-in presentation posture is
unchanged: Business appears only through the public pricing-model payload
and checkout, and no proactive in-product surface returns.

Adds the decision record, resolves the decision in status.json, lifts the
EP26 dormancy sentence, and extends the subsystem_lookup decision-id pin.
License-server and landing changes land separately in pulse-pro.
2026-08-20 15:06:25 +01:00
rcourtman a4d37447a0 Give the Docker-in-LXC opt-in an admin Settings toggle
Discovering Docker inside Proxmox LXC guests previously required an
undocumented environment variable plus a hand-edited systemd unit,
which is how the surface kept shipping invisible. The opt-in is now a
persisted system setting with an admin-only toggle in Settings →
System → General, presented as its own "Docker in Proxmox LXCs" card
under the existing Docker/Podman updates card, with the requirements
(node agent with command execution) and data-collection bounds stated
inline.

The setting rides the admin-gated system settings endpoint
(RequireAdmin + settings:write, matching every other server-wide
toggle). Flipping it persists first, then applies to the runtime
config and fires a reconfigure hook so the router rebuilds the
monitor's Docker checker and inventory collector immediately — no
restart. PULSE_ENABLE_PROXMOX_GUEST_DOCKER_INVENTORY remains
authoritative: when set it locks the toggle (EnvironmentLockBadge in
the UI, HTTP 400 from the API), and an unrelated settings save can
never clobber the runtime value. The opt-in guardrail test now pins
the settings path, the env lock, and the persisted field.

Installer and node setup-guide callouts now point at the toggle first
with the environment variable as the locking override, and the docs,
their public mirrors, and the agent-lifecycle subsystem contract say
the same. New i18n strings shipped for en/de/es.

Verified live in mock mode: toggle off/on from the UI drives the
collector teardown/setup log lines, the value survives a server
restart, and all three touched surfaces render at desktop and mobile
widths (receipt in frontend-modern/browser-verification.json).
2026-08-20 14:27:12 +01:00
rcourtman 2483dbed9c Surface Docker-in-LXC probe failures and stop the probe churn
The pct exec socket probe that discovers Docker inside LXC guests
failed invisibly: errors logged at debug only, and a failed probe never
set DockerCheckedAt, so the guest stayed in first-check state and was
re-probed on every poll forever. On a node where lxc-attach could not
enter unprivileged guests that meant ~49 pct execs per minute of
steady-state churn, with nothing at default log level to say why the
Proxmox page showed no Docker.

Track consecutive probe failures per guest in the monitor. The first
failure of a streak, and the point where the backoff reaches its cap,
log at warn with the guest, node, and error; repeats stay at debug.
Failed probes retry with exponential backoff from one minute to a
thirty-minute ceiling, a success clears the streak with an info log,
and reconfiguring the checker resets all streaks so command execution
enabled from the UI at runtime still retries immediately. Entries not
refreshed for a day belong to deleted guests and are pruned.

PULSE_PROXMOX_GUEST_DOCKER_INVENTORY_VMIDS now gates the socket probe
as well as inventory collection. It previously only limited inventory,
so opting into specific guests still probed every running LXC on every
cycle; now guests outside the allowlist are never pct exec'd at all.

Subsystem contract and configuration docs updated to match.
2026-08-20 11:40:16 +01:00
rcourtman c2f6848006 Keep Docker-in-LXC working when commands are enabled after install
lxc-attach into an unprivileged guest writes /proc/<pid>/uid_map, which
needs CAP_SETUID in the parent user namespace. NoNewPrivileges drops
CAP_SETUID from the effective set and also stops lxc-attach falling back
to the setuid newuidmap/newgidmap helpers, so the socket probe dies with
"write_id_mapping: 61 Operation not permitted".

install.sh already relaxed NoNewPrivileges for this, but only when the
agent was installed with --enable-commands. Command execution is also
togglable from the server afterwards: applyRemoteConfig starts the
command client without rewriting the unit. An agent installed without
the flag and switched on later therefore ends up able to run commands
and unable to attach to unprivileged guests, so Docker inside every
unprivileged LXC disappears from the Proxmox page. The probe failure is
logged at debug level and retried on every poll, so the surface looks
empty rather than broken while the agent re-probes the whole guest list.

Grant CAP_SETUID/CAP_SETGID to any PVE agent rather than gating on the
install-time flag, so the later toggle lands on a unit that can attach.
Ambient capabilities restore exactly the privilege lxc-attach needs and
leave the rest of the sandbox intact; the existing install-time
relaxation is unchanged.

Verified on a live PVE node. With the hardened unit the probe succeeded
only on the three privileged guests and failed on every unprivileged
one. After the ambient grant CapEff regained CAP_SETUID and both
unprivileged Docker guests were discovered, taking that node from one
Docker LXC to three.
2026-08-20 11:05:39 +01:00
rcourtman d3a1fc07b8 Define Pulse Intelligence install quality 2026-08-20 09:14:29 +01:00
rcourtman 8f8c2bda1a fix(doctor): stop reporting a live credential as a credential outage
An agent whose token authenticated seconds ago cannot be in a
credential outage, yet the fleet doctor flagged it critical with
"missing or revoked" whenever the token id missed the server's token
registry view, and the offered repair-authentication loop cannot fix a
stale server-side registry view. When the subject's own row shows the
judged token authenticating within the freshness window, emit a
warning-severity registry-stale verdict naming the id and the last
authentication instead. Genuinely stale rows keep the critical verdict.

Refs #1730
2026-08-20 06:39:15 +01:00
rcourtman 6399e83f6d Remove FilterBar saved views
Saved views stored a named URL query string in localStorage under
`pulse:filterbar:saved-views:<key>` and offered it back from a "Saved"
control on eight surfaces. A saved view was only ever the page's URL:
`saveCurrent` snapshotted `window.location.search` and `applyView`
navigated back to it.

The browser already does that, better. A bookmark syncs across devices,
survives clearing site data, is searchable, can be foldered, and can be
shared with someone else. The localStorage copy did none of those, so a
"saved" view silently vanished on a new browser or a cleared cache.

There is also no evidence anyone used it. It shipped 2026-05-01 in
v6.0.5 and no Pulse issue, comment, or discussion has mentioned it since.
#1510 is the telling case: filed two months after it shipped, by a user
who wanted their Proxmox status filter to survive a tab switch, which is
exactly the job this feature claimed. They did not reach for it, and the
fix that satisfied them was sticky route state (d20c556db).

Removed the hook, the menu, the `savedViewsKey` prop, and the per-surface
keys. URL-backed filters and sticky route state are untouched, so a
filtered page is still a shareable, bookmarkable link. On mobile, a
surface with no other auxiliary control no longer renders an action row
that existed only to hold the Saved button.

Subsystem contracts keep the URL-ownership rule this feature depended on
and now state it as the primary contract, with a note not to reintroduce
an in-app view library. A guardrail asserts the FilterBar barrel does not
re-export SavedViews.

User-visible removal: needs a release-note line at the next cut.
2026-08-19 21:01:59 +01:00
rcourtman 547afd2d17 Fix wrapped filter action alignment 2026-08-19 20:32:27 +01:00
rcourtman 202f3b2f4c Fix Proxmox LXC workload columns 2026-08-19 20:22:55 +01:00
rcourtman e2e8b72baa fix(frontend): keep Proxmox workload controls in view 2026-08-19 20:07:48 +01:00
rcourtman 54f623cffb fix(ui): measure the mobile nav instead of guessing its height
The Assistant overlay and its backdrop reserved a hardcoded
calc(5rem + env(safe-area-inset-bottom)) for a bar that measures 45px. At
375x812 the backdrop ended at y=732 while the bar starts at 767, leaving a
35px band that was neither dimmed nor click-blocked: page content showed
through and stayed interactive outside an open modal.

The bar's height is content-driven and already includes the safe-area inset
via pb-safe, so no caller can hardcode it correctly. Five sites had tried:
the Assistant panel, its backdrop, the star banner, and both halves of the
.filter-bottom-nav-aware-panel rule, which also carried a derived 6rem.

MobileNavBar now publishes its measured height as --pulse-mobile-nav-height
and those five sites read it. Publishing happens in onMount rather than the
ref, because Solid runs refs before the node is in the document where
offsetHeight is 0; reading after mount forces layout and gets the real value
on first paint. A ResizeObserver covers content-driven changes and a resize
listener covers the xl breakpoint, where the bar swaps between its height and
0 by display alone.

The declared :root value is only a pre-measurement fallback and deliberately
under-estimates. Over-reserving is what produces the un-dimmed band; reserving
slightly less is harmless because the opaque bar paints over it.

Verified at 375x812 and 1280x800: backdrop and panel now both end at 767
against a bar top of 767, gap 0; the point 5px above the bar hit-tests to the
Assistant panel rather than page content; the filter panel resolves to 45px on
mobile; and the star banner's md:bottom-4 still wins at 16px.

Adds the regression proofs the shape guard asks for, including an
architecture test that fails if any runtime source reintroduces a literal
bar height, and records the ownership boundary in the frontend-primitives
and ai-runtime contracts.
2026-08-19 19:57:18 +01:00