Adversarial review of 9db25ba60 found four residual defects in the
auto-update asset install, plus a doc line it left contradicting itself.
- install_auto_update_assets copied the bundled helper into the staged
mktemp file with an unchecked cp, and both call sites invoke the
function under `if !`, which suppresses errexit for its whole body. A
failed copy (ENOSPC, EIO) fell through to
configure_auto_update_script_repo, whose awk emits a lone GITHUB_REPO=
line for empty input, so a shebang-less one-line stub replaced the
working helper with a "script" that only ever exits 0 - silently
disabling unattended updates. Check the cp, and refuse the swap unless
the staged helper is non-empty and starts with #!.
- Both units were rendered with a bare truncating `cat > "$unit"` whose
status was never checked, and the function's last statement is
safe_systemctl daemon-reload, which returns 0 by design. A failing
write truncated a working unit and still reported success. Render each
unit to ${path}.tmp and commit it with a checked rename, so a failure
leaves the installed unit byte-identical.
- The widened ReadWritePaths could not reach deployed boxes: the unit
that grants the write access is itself the file that has to be
rewritten, and on an existing install the sandbox running the
installer excludes /etc/systemd/system and /usr/local/bin (EROFS). The
Go update pipeline cannot carry it either - pulse.service runs as
User=pulse with its own ProtectSystem=strict over the install and
config dirs only. So probe each destination directory up front and,
when one is blocked, re-exec this already-signature-verified installer
through systemd-run with a new internal --repair-auto-update-units
entry point: PID 1 forks the transient unit, so it starts in the host
mount namespace instead of inheriting the sandbox. The installer is
copied into the install dir first because the calling unit's
PrivateTmp=yes hides its /tmp copy from PID 1. The escape needs root
and systemd-run, and never recurses.
- Keep the ReadWritePaths entries as directory grants: every write now
commits with a rename from a sibling staging file, and rename needs
write access on the containing directory, so the file-level entries
systemd would otherwise accept cannot work. Document the tradeoff in
the unit and the subsystem contract instead.
The deployment-installability contract still claimed the update sandbox
leaves "only the install dir, config dir and /tmp" writable, which the
paragraph the same file gained in 9db25ba60 contradicts; the same stale
rationale had been copied into two test comments.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Settings > Infrastructure installer mints generic host install
tokens, but install.sh auto-detects Proxmox and the agent presents type
pve/pbs at /api/auto-register. The bootstrap grant required an exact
install_type match, so every generic install on a Proxmox node was
denied source creation and the denial was a single buried journal warn.
Four-part fix (#1644):
- server: extend the one-shot bootstrap grant to host-issued install
tokens presenting a canonical Proxmox type. Typed tokens stay pinned,
the grant keeps its settings-write mint requirement, first-hostname
binding, serialized completion, and single consumption across types.
- agent: a canRegister=false denial now logs at error level, returns a
setup error, and records the operator-facing reason in a
proxmox-<type>-registration-blocked state marker.
- installer: report the Proxmox registration outcome in install output
by reading the registered/blocked markers, and poll the server lookup
for a bounded retry window before warning that registration was not
confirmed (readyz flips before the first report cycle).
- setup script: the auto-register transport now captures the HTTP
status alongside the body (no -f), making the invalid-setup-token
branch reachable via 401/403 instead of a dead server-string grep,
and operator guidance names Settings -> Infrastructure instead of the
retired Nodes page (also updated in docs/PBS.md and the pinned
assertions in contract, setup-script, and repoctl docs tests).
Regression proof: internal/api/issue1644_host_install_token_proxmox_test.go
plus new install.sh proofs for the retry window and blocked-marker
surfacing, and the updated hostagent blocked-registration test.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The registry gained internal/monitoring/issue1638_dns_cache_test.go
(108aa4e20) and internal/api/issue1640_readiness_transport_test.go
(8d0d74e35) as registered verification files, but the expected
verification-requirement snapshots in canonical_completion_guard_test.py
were not updated alongside them, so every Canonical Governance run on
main has been failing its guard unit-test step since. Add both files to
the expected lists.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Residual auto-update defects found while triaging #1643 and #1637
(the primary regression was fixed in 806cbe83d):
- The generated pulse-update.timer carried both OnCalendar=daily and
OnCalendar=02:00, so with RandomizedDelaySec=4h every box attempted
two updates per day (00:00-04:00 and 02:00-06:00 windows). Keep the
single documented 02:00 schedule.
- The generated pulse-update.service sandbox (ProtectSystem=strict)
excluded the helper and unit directories from ReadWritePaths, so the
unattended path could never refresh /usr/local/bin/pulse-auto-update.sh
or rewrite the units - updater fixes only reached boxes via manual
installs. Grant the sandbox write access to both directories on
purpose.
- Because the unattended path replaces the helper bash is currently
executing, stage the new helper next to its destination and swap it
in with an atomic rename only after repo configuration succeeds. A
failed download or configure now leaves the previously working helper
in place instead of rm -f'ing it out from under the enabled timer's
ExecStart.
- Delete scripts/systemd/pulse-update.{service,timer}: orphaned
reference copies that had drifted from the units install.sh actually
generates and were referenced by nothing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
First candidate on the v6.2.0 line, following stable v6.1.2. Carries
the External Probes Pro feature, the three-part multi-site Proxmox
identity isolation work, cloned machine-id collapse detection,
unattended-update service recovery, the TrueNAS same-host redirect
follow, and verified telemetry outcomes. Version pins move to
6.2.0-rc.1 across the repo root, Docker bootstrap defaults, and Helm
metadata per the deployment-installability contract; stable install
pointers remain on v6.1.2 until governed promotion.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The generator emitted JSON-marshaled TypeScript (quoted keys, no trailing
commas) while the committed file is prettier-formatted, so every run dirtied
frontend-modern/src/utils/selfHostedFeatureCatalog.generated.ts with a
324-line formatting-only diff. The generator now runs the frontend's own
prettier on the file after writing it, so runs at a clean checkout leave the
tree clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two bugs from #1630 that combined to take installs down silently:
1. perform_update()'s install-failed rollback branch restored the backup
but never restarted pulse.service. Since the generated
pulse-update.service gates on ExecCondition=systemctl is-active,
every later timer run was then skipped and the install stayed down
until manual intervention. Restart is now guaranteed by a
service_was_active-guarded restart in that branch plus an
ensure_service_restarted RETURN trap so no exit path can miss it
(re-fix of #1323, originally c0b3a0e66, lost in 778a2577b and only
partially restored in 672e81985).
2. install.sh aborted under errexit when writing the /bin/update helper
on a read-only filesystem - after the new binary was installed and
the service stopped, landing in bug 1's no-restart branch. The stock
pulse-update.service uses ProtectSystem=strict, so /bin and
/usr/local/bin are read-only on stock unattended updates; transient
read-only remounts hit the same path. The helper write, PATH
appends, and the /usr/local/bin/pulse symlink are now idempotent and
non-fatal with a warning (install_binary_symlink).
Contract: deployment-installability now pins fail-closed service
availability for unattended updates and non-fatal writes outside the
hardened unit's writable set, with proofs in pulse_auto_update_test.go
and root_install_sh_test.go plus shell regression coverage in
scripts/tests/test-pulse-auto-update.sh (installer-exits-nonzero path)
and scripts/tests/test-install-update-resilience.sh (read-only helper
and symlink paths, verified under set -e, root-safe via ENOTDIR).
Fixes#1630
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The agent installer (scripts/install.sh) had no free-space preflight, so on
RAM-rooted appliances (QNAP QTS, Unraid) it ran all the way to the download
before dying with an unhelpful ENOSPC. Lift the server installer's disk
headroom check into the agent installer: verify temp and install-dir space
(including the shared-filesystem case) before downloading and in
--preflight-only mode, with a TMPDIR hint in the failure message.
The QNAP and Unraid watchdog loops also shell-appended agent stdout to
/var/log/pulse-agent.log with no rotation, which could fill the RAM root on
its own. Pass --log-file so the agent's rotating writer engages (QNAP: data
volume state dir; Unraid: /var/log/pulse-agent with size-capped rotation),
discard the now-duplicate stdout mirror, and keep the watchdogs' own messages
in a small self-trimming log.
Document the TMPDIR override for constrained roots in docs/UNIFIED_AGENT.md.
Fixes#1617 (space half; CPU half pending reporter diagnostics)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The alerts-frontend-surface accepted proof list is asserted verbatim by
canonical_completion_guard_test.py, so registering
useAlertOverridesState.test.tsx in registry.json without updating the pinned
expectation failed the guard unit tests.
Verified by running every step of the canonical-governance workflow locally
rather than only the guard I expected to trip: status, control-plane, registry
and contract audits, the Pulse Intelligence gate schema, active-target
automated and hybrid readiness proofs, and all twelve release-control unit
test modules.
An explicit --report-ip is the user naming the primary address on a
multi-NIC host, but identityFromHost appended it after the auto-detected
interface addresses while every consumer of ResourceIdentity.IPAddresses
treats the first entry as primary, so the override never changed what
the Machines table displayed. Prepend it instead.
The install script also rejected --report-ip as an unknown argument even
though the agent supports the flag, forcing hand edits to the service
unit that a later --update run would drop. Accept the flag, render it
into the service ExecStart, persist it in connection state, and
recognise it during saved-state and arg-stream recovery so updates
preserve it.
Refs #829
Contract-Neutral: behavioral fix: user-specified report-ip leads host identity addresses and the installer passes --report-ip through; no public contract change (#829)
Keep macOS notarization mandatory for every release candidate while requiring Windows Authenticode only for stable promotion, matching the publish workflow and RC4 release packet.
New scripts/release_control/format_staged_frontend.py mirrors the staged
Go formatter: formats staged frontend-modern/src {ts,tsx,css,json} blobs
through prettier --stdin-filepath, writes results back to the index
directly (no broad restaging), syncs the worktree only when it matches
the previously staged content, and iterates to a fixed point to absorb
prettier's occasional non-idempotence. Skips gracefully when prettier is
not installed (fresh clones, linked worktrees without node_modules).
Wired into .husky/pre-commit after the Go formatter, with unit tests in
the governance battery, a README note, and a .gitignore allowlist entry.
With the one-time sweep in the previous commits, prettier drift can no
longer re-accumulate and make format stays clean on a clean tree.
Use the installer-owned ProgramData directory consistently for token lookup, enrollment state, agent identity, receipts, and service runtime data. Pass that directory explicitly to the Windows service and retain the existing Linux default elsewhere.
Contract-Neutral: Windows agent state-path alignment preserves the existing installer and agent public contract
Contract-Neutral: Legacy Windows PowerShell TLS enum compatibility preserves the existing installer transport contract without changing its public interface