Nothing in Pulse showed a paying customer what Patrol had done for them: about 164 runs a month per install, findings raised, investigations and fixes, and none of it summarised. GET /api/ai/patrol/digest rolls the last N days (default 7, max 30) up from records Pulse already keeps: run history, the findings store, Patrol-origin action audits, and the usage cost store. It adds no telemetry and persists nothing. The payload reports when the bounded run history no longer covers the window and when model pricing is unknown, rather than quietly under-counting. This is the first slice of the "Patrol weekly digest" named bet in the pulse-pro demand ledger; the in-app "This week" card follows once its browser pass is recorded. docs/PATROL_WEEKLY_DIGEST.md holds the design note and the honest limits of each line. status.json registers the patrol-value-visibility coverage gap, the candidate lane, and its work claim. It also drops the second, identical copy of the ai-provider-guided-setup coverage gap that landed with #1853; the duplicate id fails the status audit on main for every pull request.
524 KiB
Agent Lifecycle Contract
Contract Metadata
{
"subsystem_id": "agent-lifecycle",
"lane": "L16",
"contract_file": "docs/release-control/v6/internal/subsystems/agent-lifecycle.md",
"status_file": "docs/release-control/v6/internal/status.json",
"registry_file": "docs/release-control/v6/internal/subsystems/registry.json",
"dependency_subsystem_ids": ["api-contracts"]
}
Purpose
models.PBSBackup may carry monitoring-owned write-activity evidence for an
incomplete PBS snapshot. inProgress means the snapshot has no completed
manifest; writeActivityObserved and writeActive distinguish a successfully
observed live PBS writer from absent task visibility. These shared model fields
grant no agent enrollment, identity, profile, reporting, command, update, or
removal authority, and an absent observation must not be interpreted as an
agent lifecycle fact.
Agent-profile assignment presents one actionable identity per connected host. When a PBS or PMG provider resource projects an agent ID that is not an active host-telemetry surface control, the provider projection is excluded so the same physical server cannot appear twice or receive conflicting assignments.
First-run security setup persists the canonical authentication environment file before activating runtime credentials for every deployment shape. A systemd override may additionally bind service startup, but agent and server lifecycle must not depend on successful service-name detection to retain authentication.
API-token names are operator-facing metadata, not agent identity. Renaming an existing token must preserve its ID, secret hash, scopes, expiry, and connected agent sessions, so lifecycle continuity never depends on a display label.
Own unified agent installation, registration, update continuity, profile
management, and fleet control surfaces. Pulse v6 has one host-installed
infrastructure agent binary, pulse-agent; host, Docker / Podman,
Kubernetes, Proxmox-local, and other node-local telemetry are modules inside
that binary, not separate customer-facing agent products.
On supported Linux systemd hosts, the opt-in safe runtime is a root-owned,
unprivileged monitoring collector plus the no-network typed helper, with
remediation installed only as the separate root-owned pulse-agent-runner.
The runner has its own host-bound credential and state, registers with the
explicit action-runner role, and admits only protocol-v1 typed host-update,
host-storage-cleanup, Proxmox guest lifecycle, and container lifecycle/update
operations. Generic shell, exec, unrestricted read_file, and deploy requests
are forbidden. Removing or disabling the runner leaves collector monitoring
and the helper unchanged.
docs/AGENT_SECURITY.md owns the canonical safe-profile platform,
privilege, telemetry, and action qualification matrix, and its shipped frontend
copy must remain byte-identical. Each unqualified or unavailable row must name
the fail-closed behavior, residual owner, and concrete removal condition.
Implemented or mocked behavior is never promoted to live-provider
qualification, and the safe profile cannot become the general default while a
matrix row required by that default remains unqualified.
Safe-profile collector credentials always carry agent:report and the
host-bound agent:config:read capability. Host collectors may additionally
carry only the provider-report capabilities they actively need:
docker:report and kubernetes:report. No collector credential may carry
agent:exec, agent:manage, an operator scope, an action scope, wildcard, or
any future scope until that scope is deliberately added to this allowlist.
Issuance, persisted-token loading, request admission, and Agent Doctor consume
the same role policy. Migration to that profile must durably reduce the exact
collector token to this allowlist before local privilege separation, close
only the matching live collector session, and fail closed if persistence does
not complete. Once the remote authority reduction succeeds, every local
rollback path must retain a monitoring-only collector service rather than
restoring --enable-commands or otherwise recreating command authority.
When that helper socket is configured, collector startup must complete a
versioned helper.health request before the runtime may become ready. A
listening or active systemd socket alone is not helper health. Once the helper
boundary is selected, SMART, Proxmox LXC filesystem, and rootful Docker/Podman
container inventory collection fail closed to an omitted/degraded snapshot
when the helper fails; the collector must not silently retry the privileged
read locally or adopt a rootful container-runtime socket.
Direct rootless runtime admission is a separate exact path. Installer
discovery runs only after the dedicated collector UID exists, accepts one
readable/writable socket owned by that UID across Docker and Podman, and treats
multiple usable sockets as ambiguous. The collector independently reconstructs
that single live endpoint before its first daemon API request and on reconnect;
rootful-path, remote, cross-user, missing, and ambiguous endpoints are rejected
without a daemon request. The selected endpoint's daemon must then attest rootless mode through its
information response. Exact same-UID environment pins recovered from an
installer-owned service unit may resolve automatic Docker/Podman ambiguity and
survive socket downtime; conflicting pins fail closed. The collector retries
direct admission while using helper summary inventory and falls back to a
complete helper snapshot after repeated direct-runtime loss, without exposing
container actions in either safe-profile mode. Static profile gates also reject
legacy Docker report-response update commands and suppress registry scans and
orphan-backup cleanup in the collector. Before initial helper fallback, local
runtime diagnostics record the exact direct-admission refusal so ambiguity and
other boundary failures remain distinguishable without weakening the fallback.
The collector reports that runtime truth as the shared
typed-privilege-helper module. SMART, Proxmox, and container.inventory
operation failures remain independently active until that exact operation
succeeds again, so one healthy helper call cannot hide another degraded
capability. A successful complete typed-helper container inventory, or a
collector-owned rootless runtime that makes helper fallback inapplicable,
clears only container.inventory. The module carries only a stable classified
current error and returns to running after complete recovery; raw helper
messages, payloads, credentials, and caller-selected paths never enter the
status or persisted report.
Docker-only collectors carry the same module on an explicit status-only Docker
report when helper inventory fails. That report marks inventory incomplete so
the server preserves the last complete container snapshot; it must never turn
an omitted privileged read into an authoritative empty inventory. The next
complete helper report replaces inventory and clears only the recovered helper
operation. Incomplete status reports are current-state evidence and are not
buffered for later replay after recovery.
The schema-v7 release qualification contract exercises this Docker boundary on
a live nested rootful daemon. It correlates a non-empty legacy inventory with
the summary-only typed-helper inventory, proves the collector loses direct
socket authority, replaces the helper process without changing the canonical
summary, and observes incomplete/degraded then complete/running reports across
forced helper loss and recovery. This is a bounded Docker summary-inventory
claim only; it grants no qualification to unsupported metrics, update checks,
container actions, Podman, rootless runtimes, or appliance-specific behavior.
An absent Proxmox inventory is a failure only when Proxmox mode is configured
or the unprivileged collector can discover the local pct binary. Ordinary
Linux hosts without that provider remain healthy rather than reporting a
permanent helper warning.
The Proxmox provider also distinguishes complete collection from a partial
result internally. If listing or any running-container filesystem query fails,
protocol v1 returns a stable typed provider failure and omits the affected
snapshot instead of serializing partial inventory as healthy.
Helper-backed collector updates are a durable pending transaction rather than
a successful binary swap. The helper records activation intent before the
root-owned replacement, retains the last-known-good digest, and places the new
binary under a bounded rollback deadline. The replacement process must load
the private pending handoff, pass local readiness, and deliver a newly
collected authoritative primary report before it may issue the identity-bound
typed commit. Buffered or observer reports do not satisfy that floor. An
expired, interrupted, invalid, or uncommitted activation rolls back during
helper recovery or through the deadline watchdog; commit and rollback remove
their fixed staging/quarantine artifacts durably. The first accepted report
retains the previous-version update evidence from that handoff.
Deadline recovery retains the exact pending identity and retries transient
state-read, last-known-good restore, cleanup, and persistence failures with a
nonzero bounded backoff until the durable state becomes terminal. Each failed
attempt is emitted to the helper log with the activation identity and retry
delay; a failed attempt never silently disarms automatic rollback.
Before staging or activation, the helper must establish that the signed target
is the Pulse agent Go command, that its declared and operational version equals
the requested target, and that the target advances the installed version.
Every collector-owned quarantine artifact is opened exactly once with
no-follow, close-on-exec, and nonblocking semantics. The helper derives file
type, ownership, mode, and bounded size only from that descriptor, rejects
FIFO, device, socket, symlink, oversized, and size-unstable inputs without
mutating staging or activation state, and releases update admission so a
subsequent valid stage can proceed.
Commit is additionally bound to the activating process ID and to the digest of
that process's /proc/<pid>/exe, preventing an unrelated collector process
from finalizing a pending replacement.
The runner's server transport currently reuses the combined agent command
WebSocket envelope as a migration boundary. That compatibility path may carry
only the closed typed action set after runtime-role, organization, canonical
host, token, capability, target, digest, deadline, replay, cancellation, and
receipt validation. It must be removed once supported command-capable installs
have an action-runner migration path, live action-runner session parity is
qualified, and no supported client depends on collector command delivery.
Until those criteria are met, the combined collector command path is explicitly
legacy/full-trust compatibility, not part of the safe profile.
Because the typed runner performs real package, storage, guest, and container
mutations, its systemd service cannot use ProtectSystem=strict; the runner
keeps the remaining identity, network, filesystem, capability, protocol, and
receipt restrictions, while the collector and helper remain separately
hardened. Qualification must prove an allowed mutation completes as well as
proving generic command denial.
The runner service persists the same normalized canonical hostname used when
its credential was issued; it must not substitute the machine's incidental OS
hostname when the collector was enrolled under an override. When enrollment
already supplied the canonical agent ID, the installer also persists that
non-secret ID directly in the root-owned runner environment so runner
registration does not wait for the collector's first-report identity file;
the private identity file remains the compatibility fallback for installs
whose ID is generated later. That direct binding is authoritative for runner
startup, activation health, and self-revocation even if a stale collector
identity file is present. Before any runner restart or activation attempt, the
installer replaces both the root-only bearer file and the root-owned
environment through same-filesystem temporary files, synchronizes each file,
renames it atomically, and synchronizes the containing directory. An existing
unit is disabled and runtime-masked before either live file changes, while its
already-running predecessor may continue with its in-memory credential until
the final restart. The mask prevents Restart=on-failure from reopening mixed
on-disk state during that interval; disabling preserves the same fail-closed
posture across a reboot, where the runtime mask no longer exists.
A staging, identity, or sync failure cannot truncate a live file or make mixed
on-disk authority state bootable. Readiness failure re-establishes the
stop/disable/runtime-mask fence and requires an authoritative inactive-state
check before classification or rewrite. Stop failure or a still-active process
retains every backup and never restores predecessor files beneath that process.
Indeterminate activation recovery may unmask and re-enable the runner only
after rebuilding and durably committing both files; otherwise it leaves the
unit masked, disabled, and backed up. Before that recovery path restarts a
retained replacement, it must reload and revalidate the effective FragmentPath,
drop-in absence, executable, identity, environment file, address families, and
hardening properties. A failed effective-target check retains the exact new
credential and runtime but re-establishes the disabled runtime mask instead of
starting the rejected root unit. Re-fencing is itself fail-closed: disable,
runtime-mask, daemon-reload, and inactive-state checks must all succeed before
the installer may claim the rejected unit is fenced or discard predecessor
repair artifacts.
Issuance and every runner boundary use the same
bounded action-identity vocabulary. Credential
rotation is a prepare/commit transaction. Issuance stores a ten-minute pending
replacement beside the active predecessor; the pending transport may register
but is staged in a separate bounded session slot and is not dispatchable.
Pending reconnect or flood traffic may replace only that pending slot; the
active predecessor remains connected and dispatchable until activation begins.
Activation then installs a reversible per-host session fence before changing
credential memory or durable storage, so neither new dispatch nor an inbound
result from either transport is authoritative during persistence. A failed
save removes the fence only after restoring the predecessor inventory; a
successful save atomically promotes the exact prepared transport before the
fence is removed. If exact promotion cannot commit, compensating persistence
must restore the predecessor inventory before its authority is unfenced, while
an indeterminate compensating save removes and closes both transports.
The runner
may exchange liveness ping/pong traffic while prepared, but pending map
membership never authorizes inbound results, operation-query responses, or
deployment progress. Every authority-bearing inbound delivery is bound to
pointer-identical membership in the active session map while the server lock
also protects the pending request/subscription lookup and channel send. Typed
requests, durable-operation queries, and their result channels additionally
carry an immutable per-WebSocket authority generation, so promotion cannot
transfer predecessor work to the replacement even though both transports have
the same canonical host identity. This same rule rejects a displaced
predecessor immediately after the atomic map swap, even before its deferred
socket cleanup completes; immutable admission metadata is not used as live
transport authority.
The runner
first durably replaces a private pending
health marker carrying the current installer-generated activation nonce, then
calls the authenticated activation method. Activation atomically removes the
exact predecessor set, removes the replacement expiry/pending state, and
promotes the exact registered pending session while the token inventory lock
serializes both decisions. The server fences the session maps before persisting
the activated inventory, completes the bounded session-map swap before
unfencing, and closes the displaced predecessor transport
only after both locks are released. If the exact pending transport vanished or
was replaced, the server durably restores the prior inventory and returns
conflict; if that compensating persistence fails, memory remains aligned with
the last known durable activated inventory and the response is indeterminate,
never a false success. Only after a successful response may the runner replace the
marker with activated state. Failure before that commit leaves the predecessor
valid, while initial persistence failure or a successful compensating save
restores both records. Installer rollback is
authorized only by a bodyless, self-only cancellation that durably removes the
exact still-pending replacement under the same inventory lock as activation. A
committed replacement returns conflict; persistence or admission-tombstone
uncertainty is never rollback authority. The installer removes
the prior marker before restart, ignores wall-clock mtime as authority, and
accepts only an activated marker whose nonce and canonical agent ID match the
current attempt; rollback never restores a prior marker. If readiness fails,
the installer stops the replacement and restores the predecessor only after
that atomic cancellation succeeds. Activation conflict, transport/TLS failure,
persistence failure, or indeterminate cancellation retains the replacement
with an explicit
repair-required result, because restoring the potentially revoked predecessor
would create a credential/runtime split. Before restart or predecessor-backup
removal, that path atomically and durably rewrites the exact requested
replacement bearer to the root-owned token file; failure leaves the runner
stopped, does not restore the predecessor, and requires re-enrollment. An
already-started
host mutation remains governed by its typed receipt and best-effort
cancellation semantics rather than being described as rolled back. Runner
uninstall stops and disables the runner, then requires an authenticated durable
credential self-revoke before deleting any local artifact. The delete route may
remove only the caller's exact host-bound action-runner record. An unreachable
or untrusted server, missing credential, or persistence failure retains all
root-only recovery material for retry or manual server-side revocation.
Runner WS registration and activation, cancellation, and self-revocation HTTP
calls require HTTPS/WSS with normal trust, a configured CA, or an enforced
certificate-DER fingerprint. Plaintext HTTP/WS accepts only exact localhost,
127/8, or ::1 literals (not *.localhost) and never
inherits generic installer insecure or curl -k behavior. Bearer-bearing
activation, cancellation, and self-revocation clients connect directly and do
not inherit ambient HTTP proxy variables.
Runner readiness is exposed through a bounded, secret-free health marker that
is replaced atomically only after its contents reach stable storage. The
marker distinguishes registered/pending from activated and carries only the
per-attempt nonce, never the bearer credential. POSIX
targets must sync the containing directory after rename; Windows targets must
use a write-through replacement rather than attempting to flush the read-only
directory handle returned by the standard library. The marker and helper-update
handoff must also enforce platform-native privacy: owner-only modes on POSIX,
and a protected DACL limited to the owning identity, LocalSystem, and
Administrators on Windows. Unix-looking mode values are not Windows access
control.
Fresh installs carry an explicit local command-authority profile. The closed
values are monitoring-only, command-capable, and legacy. A
monitoring-only service may accept remote configuration that keeps commands
off, but remote configuration must never promote it to command-capable or
advance the accepted configuration fingerprint for that rejected promotion.
An explicit command-enabled install is command-capable; an already-installed
service with no marker remains legacy during the compatibility window so an
upgrade does not silently revoke an existing operator choice. Installers must
persist the marker in the service command line and recover it during updates.
The reported privilege projection carries this local ceiling independently of
the current commandsEnabled state and the server-issued credential scope.
The host telemetry module owns lossless physical-disk report assembly. Linux
smartctl permission/open failures are not standby; only the explicit
-n standby,3 exit contract may mark a device sleeping. Identity-only rows
must retain sysfs model, serial, WWID, size, and transport when available,
smartctl retry modes merge rather than replace evidence, and partial NVMe JSON
must preserve field presence so omitted health counters are not fabricated as
zero. On Unraid, native disks.ini spin state may suppress SMART commands
entirely and produce an identity-only standby row without touching the device.
Unsupported SMART commands, permission failures, and timeouts must never alter
native array membership or turn a present disk into a missing disk. Direct
SATA, SAS, USB-bridge, NVMe, and controller-member inventory must all survive
one compressed unified-agent report without a collector-side suffix cap.
Appliance-vendored smartctl builds are part of that compatibility boundary.
PULSE_SMARTCTL_PATH may select the installed binary explicitly; rejection of
the JSON output option must retry the same probe in parseable text mode; DSM
/dev/sataN devices must receive a SAT retry; and STANDBY (OS) remains
standby evidence. Enumeration and each device probe have independent bounded
deadlines so one slow disk cannot consume the complete SMART pass, while
command errors retain bounded stderr for compatibility diagnosis.
On a PVE node, the Unified Agent may also report the mounted filesystem
capacity of running LXCs. The collector uses the fixed local pct list command
to admit only node-local running guests, reads each admitted guest's main
/etc/pve/lxc/<vmid>.conf mount declarations, resolves its init PID through
the fixed lxc-info -n <vmid> -p shape, and probes usage through the Linux
/proc/<pid>/root namespace. A configured path that resolves to the same
device as its parent is not a live mount and must be omitted instead of
borrowing the parent's capacity. Least-privilege installs that cannot read the
config, resolve the PID, or traverse /proc may fall back to the fixed
pct df <vmid> query for that container only. The complete pass has a bounded
collection window, every container has an independent deadline, command output
has byte ceilings, guest and disk counts are capped, unvalidated VMIDs never
become arguments, and stopped guests are never queried. Linux namespace paths
remain slash-canonical in every native build so cross-platform verification
cannot accidentally test only the fallback. The report carries VMID and exact
container name so monitoring can reject stale migration or rename
correlations.
internal/monitoring/monitor.go also serializes shared unified-resource
websocket payloads. Carrying plural availability facets through that serializer
is an adjacent monitoring/API projection and does not change agent enrollment,
report admission, removal, update, profile, or command authority.
The serializer's broadcast slimming is likewise an adjacent projection with an
agent-identity obligation: superseded canonical ids produced by re-enrollment
or identity re-derivation must remain resolvable to the client through
canonicalIdentity.supersededIds even though they are no longer duplicated
into the broadcast alias list, so alert overrides and identity consumers keep
following a forked or re-enrolled host across its retired spellings.
That shared monitor constructor may also copy the persisted alert schedule's
initial notification target into the notification manager. This adjacent
alerts/notifications wiring grants no agent enrollment, reporting, removal,
profile, update, probe-assignment, or command authority and must not mutate
host-agent state.
The same adjacent boundary installs external alert callbacks before the
monitor is exposed to report handlers. This guarantees that the first accepted
agent observation can reach canonical notification delivery without granting
the callback path enrollment, identity, profile, update, or command authority.
The shared internal/api/ai_handlers.go route may also reopen a dismissed
Patrol finding and mirror that state into the unified findings store. This is
AI finding-state management only; it grants no agent enrollment, report,
profile, update, removal, command, or fleet-control authority.
The shared Connections aggregation may carry a JSON-excluded PBS/PMG
monitor-resource identity into alert evaluation so the owning resource's
availability policy is applied. That alert-only routing identity must not
become an agent enrollment, host continuity, command-channel, or fleet
identity, and it must not alter the public Connections payload.
The JSON-excluded Proxmox VM/LXC I/O-rate validity fields carried by
internal/models/models.go are likewise monitoring-owned sidecar evidence.
They distinguish a valid idle interval from an unknown rate for history,
alerts, and unified-resource projection; they do not describe agent report
admission, heartbeat, enrollment, command reachability, or fleet liveness.
Docker / Podman report sizing is a two-dimensional transport contract:
pkg/agents/docker/report_limits.go owns the inclusive encoded HTTP-body and
decoded JSON byte ceilings, derives the 80% early-warning boundaries, and
generates the operator-facing limit description consumed by server ingress
and every supported pulse-agent release target built from this source.
Docker health-check dependency evidence follows the same report boundary.
The agent may derive a bounded list of normalized URL hostnames from the
runtime-authored health-check test, but raw health-check commands, URL paths,
queries, credentials, and environment expansion must never enter the report.
Those secret-free targets are observational metadata only: they do not grant
command authority or change container lifecycle admission.
An agent's lifecycle operations are scoped to that agent. Installing,
upgrading, restarting, or uninstalling one agent must never terminate a
co-installed agent that happens to share a binary-name prefix, which is how a
host runs a development agent beside a production one. Process matching by
whole command line is prefix matching unless the pattern bounds its far end, so
every such pattern must be terminated (pulse-agent must not match
pulse-agent-prod); where the intent is genuinely path-agnostic, match the
process name exactly instead. This binds the generated NAS wrapper scripts as
well as the installer, and most sharply there, because restarting through the
wrapper is the documented runbook step and would otherwise repeat the
collateral kill on every use.
Exactly one supervisor may own an agent at a time. An install that starts a watchdog must first stop any watchdog already supervising that same agent, and must stop it before stopping the agent itself, because a watchdog left running respawns the agent mid-install with the previous binary and arguments. Two surviving supervisors race to own one agent id, which presents as an agent that restarts on its own or reverts to superseded arguments rather than as an install failure. The supervisor match is subject to the same boundary rule as the binary match, so stopping one agent's supervisor never stops a co-installed agent's. Removing an agent inherits the same ordering: a supervisor left looping while its agent is torn down restarts the agent that was just stopped, so uninstall stops the supervisor first as well. Because removal must also reach a supervisor that a superseded install left behind, its match may be looser about location than an install's, but never about the far end of the name: a backup copy of a wrapper script, or an editor holding one open, is not a supervisor and must never be a termination target.
An agent and the wrapper that starts it are one unit, so a server must never
hand out an agent older than the wrapper template it will be launched by. The
installer renders that wrapper from the server's current template, and an agent
predating a flag the template now passes exits on startup and crash-loops under
its supervisor, which reads as a broken host rather than a version mismatch.
The download path therefore refuses an agent binary that does not carry the
server's own agent version, and resolves that expected version from the same
source the agent build stamps in rather than from a compiled-in build
placeholder, because development builds carry placeholders no version parser
accepts and those are precisely the builds whose local artifacts go stale.
One definition of "same agent version" governs both ends of that exchange. It
is the release identity: a leading v and semver build metadata are not part
of it, the prerelease suffix is. The server applies it when deciding whether a
local artifact is fresh enough to serve, and the installer applies it when
reporting whether the agent it downloaded matches the server that served it.
Letting the two ends disagree costs more than a redundant check, because the
installer's report is the only signal a human sees: when it compared raw
strings it contradicted a correct server on every development install, and a
contradiction that appears every time is read as noise rather than as the one
warning that mattered.
Mock mode is a clean room on the report-admission boundary. Mock mode already
suspends pull-based PVE/PBS/PMG collection by never building those clients, and
push-based agent reports are held to the same rule: ApplyHostReport,
ApplyDockerReport, and ApplyKubernetesReport acknowledge a report with the
reporting agent's own identity and discard it without touching monitor state.
Ingesting it would land a real machine in state, where it raises alerts,
persists host continuity, records metrics, and feeds the online/offline sweep
beside authored fixtures. Persisted host continuity is suppressed for the same
reason, because those entries outlive the mock toggle and every consumer injects
them after the read path has already substituted the mock snapshot. The
acknowledgement stays a success rather than a rejection so a real agent does not
read a demo server as an outage and retry-storm it. Tests that assert report
admission or tenant isolation must therefore not enable mock mode, or the
assertion passes without exercising the boundary.
Physical-disk evidence collected by a host agent must survive projection back
into monitoring's models. Absent evidence has to carry its declared sentinel
rather than a zero value that reads as a real measurement: an absent
physical-disk view projects Wearout as unifiedresources.WearoutUnreported,
never 0, which would announce a spent disk the agent never reported.
An enabled availability target assigned to a host agent creates an
agent-lifecycle lease for that exact target/agent pairing. First assignment
and reassignment start a fresh reporting grace window; a result from the
previous agent must not satisfy the new lease. Once the host heartbeat itself
is unhealthy, the canonical host-offline lifecycle owns the outage so the
availability-probe lifecycle cannot create a duplicate whole-agent incident.
The adjacent /api/connections availability row uses monitoring's
server-receipt freshness reference for that lease while preserving the
agent-authored check time as observation metadata. That receipt timestamp is
probe-result liveness only: lifecycle consumers must not reinterpret it as a
host heartbeat, enrollment renewal, command-channel readiness, or fleet-update
observation.
An HTTPS probe result may carry a secret-free certificate observation alongside
the outcome. The host agent must use the shared availability probe and report
shape so local and remote checks derive identical trust vocabulary. Certificate
identity, validity, and trust are observation payloads only. They do not renew
the target lease, agent enrollment, host heartbeat, or command readiness beyond
the accepted report's existing server-receipt evidence.
An assigned HTTP/S application-response contract travels to the owning agent
only through its authenticated remote-config assignment. The agent may receive
the encrypted-at-rest request body, Basic password, bearer token, and operator
header values because it must execute the check, but it must never copy those
values, response bytes, or arbitrary remote content into logs, module status,
the host report, telemetry, or Patrol context. The shared probe returns only
the overall outcome, transport outcome, typed application outcome, HTTP status,
bounded failure code/message, latency, and optional certificate observation.
Local and assigned-agent execution must therefore use the same method, status,
body-size, assertion, redirect, and outbound-URL limits, while the report
ingestion boundary continues to reject stale revisions and non-owning agents.
Explicit disk inclusion is an agent lifecycle input. The CLI, persisted service arguments, unified host agent, and Docker/Podman agent must carry the same include list to collection. Includes may opt a matching filesystem back in after Pulse's automatic pseudo-filesystem filtering, while an explicit operator exclusion remains authoritative.
Removing a unified host agent is also an ownership cleanup boundary for the Docker/Podman module attached to that host identity. The removal must delete the associated runtime report surfaces and clear their active alerts instead of leaving container alerts or inventory detached from a removed agent.
Notification terminal-failure retry and dismissal are delivery-queue operator actions only. They do not renew agent enrollment, heartbeat, command-channel readiness, update observation, or any agent-lifecycle lease.
Proxmox node and backup-server setup may request an explicitly insecure Unified Agent installer command. The choice must remain off by default, be visible at the point where the command is generated, and affect both the installer download and the agent's subsequent Pulse TLS connection.
Canonical Files
internal/api/agent_install_command_shared.gointernal/api/configapi/config_setup_handlers.go2a.internal/api/configapi/setup_script_render.gointernal/api/unified_agent.gointernal/agentupdate/update.gointernal/hostagent/agent.go5a.internal/dockeragent/agent.go5b.internal/dockeragent/container_update.go5c.internal/dockeragent/container_update_typed.go5d.internal/kubernetesagent/agent.go5e.internal/agentexec/verifier_postconditions.go5f.pkg/agents/docker/report_limits.go5g.internal/hostagent/xcpng.go5h.internal/hostagent/proxmox_lxc_filesystems.go5i.internal/agenthelper/5j.internal/actionrunner/5k.internal/dockeragent/action_runtime.go5l.internal/collectorlifecycle/cmd/pulse-agent/main.go6a.cmd/pulse-agent-helper/main.go6b.cmd/pulse-agent-runner/main.go6c.cmd/pulse-agent/collector_lifecycle.goscripts/install.shscripts/install.ps18a..github/workflows/unified-agent-native.yml8b.scripts/installtests/windows_agent_lifecycle.ps18c.scripts/installtests/windowslifecycleserver/main.gofrontend-modern/src/api/agentProfiles.tsfrontend-modern/src/components/Settings/AgentProfilesPanel.tsxfrontend-modern/src/components/Settings/agentProfileSettings.tsfrontend-modern/src/components/Settings/useAgentProfilesPanelState.tsfrontend-modern/src/components/Settings/infrastructureOperationsModel.tsxfrontend-modern/src/components/Settings/connectionsTableModel.tsfrontend-modern/src/components/Settings/useConnectionsLedger.tsfrontend-modern/src/components/Settings/useConnectionRowActions.tsfrontend-modern/src/components/Settings/ConnectionEditor/ConnectionEditor.tsxfrontend-modern/src/components/Settings/ConnectionEditor/AddressProbeStep.tsxfrontend-modern/src/components/Settings/ConnectionEditor/useConnectionEditor.tsfrontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/NodeCredentialSlot.tsxfrontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/TrueNASCredentialSlot.tsxfrontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/VMwareCredentialSlot.tsx22a.frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/AvailabilityTargetSlot.tsxfrontend-modern/src/components/Settings/InfrastructureWorkspace.tsx23a.frontend-modern/src/components/Settings/InfrastructureAgentDoctorPage.tsx23b.frontend-modern/src/components/Settings/useAgentFleetDiagnostics.tsfrontend-modern/src/components/Settings/InfrastructureSourceManager.tsxfrontend-modern/src/components/Settings/InfrastructureSourcePicker.tsxfrontend-modern/src/components/Settings/InfrastructureDiscoverySettingsDialog.tsxfrontend-modern/src/components/Settings/DiscoverySettingsForm.tsxfrontend-modern/src/components/Settings/discoverySettingsModel.tsfrontend-modern/src/components/Settings/infrastructureWorkspaceModel.ts29a.frontend-modern/src/components/Settings/infrastructureAgentUpdateCommandsModel.tsfrontend-modern/src/components/Settings/proxmoxSettingsModel.tsfrontend-modern/src/components/Settings/useInfrastructureOperationsState.tsxfrontend-modern/src/components/Settings/useInfrastructureSettingsState.tsfrontend-modern/src/components/Settings/NodeModalAuthenticationSection.tsxfrontend-modern/src/components/Settings/NodeModalBasicInfoSection.tsxfrontend-modern/src/components/Settings/nodeModalModel.tsfrontend-modern/src/components/Settings/NodeModalMonitoringSection.tsxfrontend-modern/src/components/Settings/NodeModalSetupGuideSection.tsxfrontend-modern/src/components/Settings/NodeModalStatusFooter.tsxfrontend-modern/src/components/Settings/useNodeModalState.tsfrontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsxfrontend-modern/src/utils/agentProfilesPresentation.tsfrontend-modern/src/utils/agentInstallCommand.ts42a.frontend-modern/src/utils/agentVersion.tsfrontend-modern/src/utils/infrastructureOnboardingPresentation.tsfrontend-modern/src/api/nodes.tsfrontend-modern/src/components/Settings/InfrastructureInstallerSection.tsxfrontend-modern/src/components/Settings/useInfrastructureInstallState.tsxfrontend-modern/src/components/Settings/infrastructureSettingsModel.tsfrontend-modern/src/components/Settings/useInfrastructureConfiguredNodesState.tsfrontend-modern/src/components/Settings/useInfrastructureDiscoveryRuntimeState.tsfrontend-modern/src/utils/infrastructureSettingsPresentation.tsfrontend-modern/src/utils/agentCapabilityPresentation.tsfrontend-modern/src/utils/agentProfileSuggestionPresentation.tsfrontend-modern/src/utils/unifiedAgentInventoryPresentation.tsfrontend-modern/src/utils/unifiedAgentStatusPresentation.tsfrontend-modern/src/utils/nodeModalPresentation.tsfrontend-modern/src/components/Settings/useTrueNASSettingsPanelState.tsfrontend-modern/src/components/Settings/useVMwareSettingsPanelState.tsfrontend-modern/src/components/Settings/MonitoredSystemImpactPreview.tsxinternal/hostagent/proxmox_setup.gointernal/remoteconfig/client.gointernal/agenttls/config.gointernal/api/agent_exec_token_binding.gointernal/securityutil/httpurl.gopkg/securityutil/httpurl.go
Shared Boundaries
internal/models/models.go and internal/monitoring/monitor.go also carry
monitoring-owned Proxmox cluster node identity and membership-confirmation
state. Provider instances with equal cluster/member display names must not
collapse through a shared endpoint or linked-agent alias, but this scoping does
not change Unified Agent enrollment, token binding, report admission,
tombstones, re-enrollment, or command authority. A linked agent remains
supplemental evidence on the retained canonical Proxmox member; its presence
does not decide provider membership or authorize removal.
That separation also applies to configured cluster-node display overrides.
Infrastructure lifecycle rows may present the connection-owned override and
the current native Proxmox name together, but row identity, linked-agent
attachment, update eligibility, uninstall targeting, enrollment continuity,
and removal tombstones remain keyed by immutable member and agent identities.
Clearing or changing a display override must not detach, re-enroll, remove, or
retarget an agent.
The same shared model file may carry monitoring-owned native storage-health evidence such as structured ZFS scans, vdev members, and Ceph health checks. Those additive read-state fields do not change Unified Agent enrollment, transport admission, token binding, command authority, deletion tombstones, or re-enrollment continuity. Unknown or absent provider storage evidence must not be reinterpreted as an agent lifecycle transition.
Docker / Podman report transport is shared with api-contracts.
internal/dockeragent/agent.go measures the gzip-encoded HTTP entity and the
decoded JSON before attempting any destination, while
internal/api/docker_agents.go enforces the same
pkg/agents/docker/report_limits.go contract. Exactly 2,097,152 encoded bytes
and 10,485,760 decoded bytes are accepted; only the first byte above either
ceiling is rejected. Warning copy must derive its limit wording from that
contract and retain exact structured byte fields, so Docker, Podman, native,
containerized, updated, and restarted Unified Agent runtimes cannot silently
return to a stale hard-coded threshold.
Docker container update recreation under internal/dockeragent/ is lifecycle
mutation authority, not monitoring collection. The module must translate
daemon inspect output into a desired create plan before the original container
is renamed: preserve operator configuration such as labels, mounts, binds,
restart policy, explicit hostname, network aliases, IPAM, driver options,
gateway priority, and network mode; remove daemon-generated observations and
fields that conflict with host or shared container:<id> namespaces.
network_mode: service:<name> reaches this boundary as the daemon-resolved
container:<id> mode and follows the same rule. The create-time network must
be deterministic, every additional attachment must be restored before start,
and any create, connect, start, or verification failure after backup creation
must report compensation truth and attempt rollback rather than declaring a
partially connected replacement successful. The typed host-agent command path
owns durable terminal receipts for that mutation: reconnect or server recovery
may replay a persisted result, but may not execute the Docker mutation twice.
The typed update preflight must accept the planned image only when the daemon's
resolved image proves either the expected local content ID or the expected
registry RepoDigest. A tag that resolves to neither digest fails before the
rename boundary; comparing only a registry digest to a daemon-local image ID is
not sufficient proof and must not reject an otherwise matching pull.
Automatic Patrol action admission wired through internal/api/ is API/action-
lifecycle authority, not agent enrollment or command authority. Agent command
transport may run only after the lifecycle-owned policy lease has been
revalidated and the policy approval plus executing transition have committed
atomically; a policy-approved intermediate state is never reusable by an agent.
Human approvals remain separate lifecycle evidence and are not invalidated by
later automatic-policy revocation. Emergency stop blocks admission before
executing; cancellation after that boundary is best effort and is not
rollback proof.
Governed action admission also treats requester and decision identity as
server-owned lifecycle authority. The authenticated session or owner-bound API
token supplies the immutable ActionActor, and the captured
ApprovalRequirement is bound into the request, plan identity, and plan hash.
The plan also carries the unified-resource-owned, versioned
ActionPolicyDecisionProvenance. It binds the bounded policy authorities
actually consulted, their source revisions and scope, the resulting approval
requirement, and stable reason codes into the plan hash and immutable audit.
This is descriptive plan-time evidence for pending-action consumers, not a
decision or dispatch token. Lifecycle freshness may detect provenance drift,
but execution still requires current actor/RBAC/scope checks and the Task 04
authorization lease. Legacy rows without provenance remain explicit
legacy_unknown and cannot resume nonterminal authority by implication.
Each accepted approval increments a durable decision revision through an exact
prior-prefix CAS, appends one typed decision fact, and atomically appends the
approved or rejected transition only when state changes. Exact replay appends
no event, while conflicting replay and legacy nonterminal records without actor
or requirement bindings fail closed as replan-required. MFA remains unavailable
and fail-closed unless the lifecycle boundary verifies action-bound
cryptographic step-up evidence through its configured verifier; session,
API-token, relay, and local-biometric labels alone are never MFA proof.
Assistant transport scopes do not grant agent command authority. ai:chat
and relay:mobile:access remain conversation/read/session scopes; an
interactive infrastructure invocation must carry server-bound ai:execute
authority and still satisfy the separate agent execution/token-binding and
control-level gates. Unknown or absent request scope never falls back to agent
execution permission.
Commercial v5-to-v6 migration retry state may pass through internal/api/
handlers that agent-lifecycle also references, but the ownership remains
API/cloud-paid. Agent lifecycle surfaces may observe paid-migration posture for
upgrade continuity, but they must not reinterpret commercial_migration
reasons, reset first_failed_at, change the license-server retry cadence, or
turn blocked license egress into an agent update or enrollment state.
/api/connections command-policy comparison is lifecycle-adjacent fleet
truth. The desired side is the effective runtime config served to the agent
after token scope and binding checks, not the unsanitized profile desire. If a
profile enables commands but the agent runtime token cannot execute them,
lifecycle surfaces must see desired-disabled/applied-disabled as in sync
rather than a command-policy rollout failure. A host whose recorded token
binding no longer resolves to a live API token is the same case; when API
tokens exist, desired command policy fails closed to disabled for that host
instead of reporting drift against a token that cannot execute.
Install tokens for the generic host agent flow are minted server-side through
the agent install command endpoint. The server decides the token's scopes from
the operator's command-execution choice at mint time, because scopes are never
upgraded implicitly by an install or repair flow, and stamps the install-type
and issuance metadata that make the token eligible for one first-use
command-channel binding. The operator-owned
PATCH /api/security/tokens/<id> scope editor is a separate API/security
mutation: it may deliberately change an installed agent token's live
authority, but agent lifecycle code must not call it to auto-upgrade, repair,
or reconcile command permissions. Frontend install surfaces may choose whether
commands are requested for a newly minted credential but must not compose
install-token scope lists themselves.
The same server-issued metadata is the only token evidence that may authorize a host-identity handoff during re-enrollment. A final report from the retiring agent may arrive after the replacement token was minted, so the token-creation boundary includes at most one health window of that existing agent. Reuse of the existing identity additionally requires one unambiguous record with the same machine ID and equivalent hostname; differing non-empty report IPs or an active identity conflict veto the handoff. An arbitrary API token, multiple matching identities, or an agent that remains live beyond that window must retain the clone-safe fork. Once accepted, bindings for superseded tokens are retired so a late old process cannot overwrite the replacement identity.
PVE node setup shared boundaries that render or copy PulseMonitor
permissions must treat VM.GuestAgent.Audit plus VM.GuestAgent.FileRead as
the PVE 9+ primary contract, with VM.Monitor retained only as the legacy PVE
8 fallback when guest-agent privileges are unavailable.
internal/dockeragent/ is lifecycle-adjacent for agent binary/update trust
and owns the Docker / Podman collection module used by pulse-agent, but
Docker runtime capability truth is monitoring-owned. Lifecycle consumers must
not reinterpret standalone Swarm.LocalNodeState=inactive metadata as agent
enrollment, install, command, or fleet-control authority.
The Docker / Podman module reports runtime-native per-core container CPU
percent because that is the collector's source evidence. Lifecycle surfaces may
transport that report, but must not reinterpret it as host-capacity utilization;
monitoring and unified resources own the normalized CPU contract used by
history, alerts, and canonical app-container metrics.
The same module authors the container OOM evidence boundary from Docker inspect.
Current agents must serialize a non-null OOMKilled boolean for inspected
containers, preserving explicit false; absence is reserved for older or
reduced-fidelity report producers. Lifecycle transport must not synthesize OOM
state from exit code 137 or collapse absent and false, because monitoring and
alerts depend on that distinction to fail closed.
Inside-guest Docker / Podman visibility is also a privacy boundary: full
Docker / Podman inventory may come from a guest-local agent or another explicit
guest reporting path. LXC Docker inventory may also come from the Proxmox host
agent, but only through the explicit, server-side opt-in inventory mode that
uses minimal Docker summary commands rather than unbounded guest scraping. A
local --enable-docker=false or PULSE_ENABLE_DOCKER=false remains a hard
Unified Agent opt-out that auto-detection and remote profile settings cannot
reverse.
Proxmox workload actions may use the parent node pulse-agent only when the
shared /api/resources payload carries a backend-authored Proxmox workload
target linked to that node agent. Suppressing an inside-guest install cue for
that case means governed actions have a node-agent path; it must not be
presented as proof that a guest-local agent is installed.
Candidate-sourced PVE/PBS/PMG onboarding remains lifecycle-owned even when the
candidate starts in Discovery or a platform probe: the settings connection flow
may preview the monitored-system impact and credential path, but setup
commands, downloaded scripts, manual token command copies, and manual saves
must stay blocked until the operator explicitly approves the candidate import
plan. Type-first manual adds without a candidate must stay on the normal
credential-entry path and must not synthesize a discovery import plan.
Agent-facing operations-loop status wiring in internal/api/router.go and
internal/api/agent_resource_context.go is lifecycle-adjacent only because it
shares agent route infrastructure. Other handlers in internal/api/ such as
the AI settings handler (ai_handlers.go) carry AI provider configuration
(for example per-provider base URL overrides) that is ai-runtime config-surface
and is not agent enrollment, liveness, or lifecycle state. The Pro update
credential wiring in internal/api/router.go (feeding the activation's
installation token and instance fingerprint to the server updater's
download-broker path) is likewise server self-update plumbing: agent
enrollment, agent update liveness, and fleet-control semantics must not key
off it. The same boundary covers the server updater's deployment adapters
behind GET /api/updates/plan in internal/api/updates.go: they are plan
providers only, with the real apply in the internal/updates/manager.go
pipeline, and lifecycle surfaces must not read them as an agent-side apply,
update, or rollback transport. The server updater's own downgrade guard and
POST /api/updates/rollback backup-restore endpoint are equally server
self-update plumbing: they roll the Pulse server binary and its local
backups, never agent binaries, and agent lifecycle surfaces must not key
enrollment, update liveness, or fleet-control semantics off them.
The authenticated GET /api/updates/release-notes projection follows the same
server-only boundary: it reads the exact published notes for the running Pulse
server release and must not be used as agent version, enrollment, update
availability, or fleet rollout evidence.
The alerts-owned POST /api/alerts/snooze and
POST /api/alerts/unsnooze handlers share internal/api/ routing only. They
pause and resume notification and escalation policy for one existing incident;
they grant no agent enrollment, liveness, command, update, removal, or
fleet-control authority, and agent lifecycle state must not key off them.
Workflow starter counts on that endpoint,
contextual Assistant/external-agent collaboration counts inside the Assistant
step, the content-free Patrol control starter split, and Patrol control
completed-loop, resolved-loop, or patrolControlValueState proof mirrored to
patrolAutonomyValueState compatibility fields are
API-contract telemetry/orientation fields, not agent enrollment, liveness,
setup completion, install, update, command, recovery, or fleet-control
lifecycle authority. Native pulse_patrol starts, current paid
patrol_control starts, legacy patrol_autonomy starts, and legacy
pulse_pro_activation starts all contribute to Patrol control starter orientation;
proActivationOperationsLoopStarterCount remains a legacy entry-point counter
for continuity. Router wiring that passes monitor-owned Proxmox/PBS/PMG source
freshness thresholds into unified-resource adapters is likewise adjacent API
plumbing: lifecycle surfaces may consume the resulting resource status, but
must not treat a between-poll stale/fresh decision as agent enrollment,
installer success, update liveness, command reachability, or fleet-control
authority.
for the old alias only, while the legacy completed/resolved/value
proActivation* fields are compatibility aliases for that same first-party
Patrol control proof while older commercial and telemetry consumers migrate.
Lifecycle surfaces must
not treat verified,
legacy-compatible verified_needs_mcp, or governed_decision_recorded as
proof that an agent install, update, registration, profile rollout, or fleet
command completed. External-agent/MCP readiness is optional collaboration
setup for agents outside the app, not a first-party Patrol control completion
gate and not a lifecycle setup stage.
Server update funnel telemetry is lifecycle-adjacent only. The updater's local
history may feed outbound usage 30-day attempt, success, failure, and coarse failure
category counters, but those counters are adoption analytics, not proof that a
particular agent update, profile rollout, host command, registration, or fleet
operation succeeded. Lifecycle surfaces must keep reading update readiness and
continuity from the updater, installer, connection ledger, and agent runtime
state instead of inferring it from outbound usage telemetry.
Scheduled-report route and background-worker wiring in internal/api/router.go
and the reporting handlers is API/reporting ownership, not agent lifecycle.
The scheduler may enumerate tenant organization IDs so each workspace can run
its own reports, but that enumeration is not agent enrollment, install,
update, profile rollout, command reachability, or fleet-control authority.
frontend-modern/src/api/agentProfiles.tsshared withapi-contracts: the agent profiles frontend client is both an agent lifecycle control surface and a canonical API payload contract boundary.frontend-modern/src/api/nodes.tsshared withapi-contracts: the shared Proxmox node client is both an agent lifecycle setup/install control surface and a canonical API payload contract boundary.frontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/NodeCredentialSlot.tsxshared withapi-contracts: the inline node credential slot is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary.frontend-modern/src/components/Settings/infrastructureOperationsModel.tsxshared withapi-contracts: the pure infrastructure operations inventory/install model is both an agent fleet lifecycle control surface and an API token, lookup, assignment, and reporting/install contract boundary.frontend-modern/src/components/Settings/MonitoredSystemImpactPreview.tsxshared withcloud-paid: the monitored-system impact preview is both a platform-connections lifecycle surface and a canonical cloud-paid monitored-system presentation boundary.frontend-modern/src/components/Settings/NodeModalAuthenticationSection.tsxshared withapi-contracts: the node setup authentication section is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary.frontend-modern/src/components/Settings/NodeModalBasicInfoSection.tsxshared withapi-contracts: the node setup basic-info section is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary.frontend-modern/src/components/Settings/nodeModalModel.tsshared withapi-contracts: the pure node setup modal model is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary.frontend-modern/src/components/Settings/NodeModalMonitoringSection.tsxshared withapi-contracts: the node setup monitoring section is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary.frontend-modern/src/components/Settings/NodeModalSetupGuideSection.tsxshared withapi-contracts: the node setup guide section is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary.frontend-modern/src/components/Settings/NodeModalStatusFooter.tsxshared withapi-contracts: the node setup status/footer section is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary.frontend-modern/src/components/Settings/useInfrastructureConfiguredNodesState.tsshared withapi-contracts: the direct-node infrastructure settings state hook is both an agent lifecycle control surface and a shared Proxmox node API contract boundary. Because the Settings shell retains this hook on every settings route, its unified-resource dependency must remain bounded totype=agentand gated by infrastructure-read authority. Estate-wide node and guest license counts come from the list response's canonical aggregations; this lifecycle state must not hydrate every workload merely to count them.frontend-modern/src/components/Settings/useInfrastructureDiscoveryRuntimeState.tsshared withapi-contracts: the infrastructure discovery runtime state hook is both an agent lifecycle control surface and a shared discovery/settings API contract boundary.frontend-modern/src/components/Settings/useInfrastructureInstallState.tsxshared withapi-contracts: the infrastructure install state hook is both an agent fleet lifecycle control surface and an API token, lookup, and install transport contract boundary.frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsxshared withapi-contracts: the shared infrastructure operations state hook is both an agent fleet lifecycle control surface and an API token, lookup, assignment, and reporting/install contract boundary.frontend-modern/src/components/Settings/useNodeModalState.tsshared withapi-contracts: the node setup modal state hook is both an agent lifecycle control surface and a shared API-backed install/setup contract boundary.frontend-modern/src/utils/agentInstallCommand.tsshared withapi-contracts: the shared frontend install-command helper is both an agent lifecycle control surface and a canonical API/install transport contract boundary. Operator-facing agent install commands must preflight the selected OS/architecture download before they mutate the target host, and generated commands must pass enrollment secrets through short-lived token files rather than long-lived service/environment arguments. Windows, macOS, and Linux commands must keep custom CA, insecure/plain-HTTP, and optional-auth behavior aligned so the Machines onboarding path does not diverge by OS.frontend-modern/src/utils/infrastructureSettingsPresentation.tsshared withapi-contracts: the infrastructure settings presentation helper is both an agent lifecycle control surface and an API-backed direct-node/discovery settings boundary.internal/api/agent_ingest.goshared withapi-contracts: Unified Agent report admission, removal, remote-config identity binding, and re-enrollment responses are both an agent lifecycle authority and a canonical authenticated API contract boundary.internal/api/agent_install_command_shared.goshared withapi-contracts: agent install command assembly is both an agent lifecycle control surface and a canonical API payload contract boundary. 20a.internal/api/cloud_agent_install_command.goshared withapi-contracts,cloud-paid: hosted tenant agent install command generation is both an agent lifecycle enrollment surface and a provider-hosted tenant boundary. The hosted PVE/PBS install command path must stay on the same token-file command transport as the normal lifecycle install helpers, while failing closed unless hosted mode is active, the target org exists, and the mintedagent:reporttoken remains scoped to that tenant workspace. Provider-hosted MSP report branding must not alter agent enrollment or ingest authority. Agents continue to report into one tenant Pulse runtime with tenant-local tokens; branding is report rendering configuration inside that runtime, not a control-plane token, agent profile, or cross-client ingest path.internal/api/agentbinding/policy.goshared withapi-contracts,security-privacy: install-token command-channel binding is simultaneously an agent lifecycle admission policy, a canonical API identity contract, and a security boundary.internal/api/agenttokens/install.goshared withapi-contracts,security-privacy: agent install-token issuance and persistence are simultaneously an agent lifecycle authority, a canonical API token contract, and a security boundary.internal/api/configapi/config_setup_handlers.goshared withapi-contracts: auto-register and setup handlers are both an agent lifecycle control surface and a canonical API payload contract boundary. Assisted-setup naming is lifecycle-owned bootstrap state: the connection name typed in the add dialog travels on the one-time setup token (SetupTokenRecord.DesiredName) rather than through the node-side script, and the setup-token-authenticated auto-register completion names a newly created connection from that carried value, falling back to the node's self-reported hostname only when no name was typed. Dedup and cluster-member adoption identity remain hostname/candidate based so the carried name cannot fork or rotate an existing registration.internal/api/configapi/setup_script_render.goshared withapi-contracts,storage-recovery: the generated Proxmox setup-script is a shared boundary across agent lifecycle (forced-command keys, install/uninstall edits), API contracts (rendered token shape and encoded rerun URL), and storage/recovery (backup visibility grants, Pulse-managed temperature SSH keys, and SMART disk-temperature collection). PBS setup-script auto-registration remains lifecycle-owned bootstrap transport: rendered scripts must post registration payloads to the canonical Pulse base URL plus/api/auto-register, not to the script download artifact URL, so install-time registration keeps one API root regardless of whether the script came from/api/setup-scriptor/api/setup-script-url. Generated PVE setup scripts must prove a freshly created monitoring token after ACL application and before/api/auto-registerby calling the local Proxmox/api2/json/nodesendpoint with the exactPVEAPITokenheader that would be sent to Pulse. A failed smoke check may leave manual token details for the operator, but it must not POST an unproven token as a completed lifecycle registration.internal/api/unified_agent.goshared withapi-contracts: unified agent download and installer handlers are both an agent lifecycle control surface and a canonical API payload contract boundary.internal/config/host_continuity.goshared withmonitoring: the durable host identity, report-order watermark, and removal tombstone journal is jointly owned by agent lifecycle admission and monitoring report continuity.internal/kubernetesagent/agent.goshared withmonitoring: the Kubernetes native agent runtime is both a monitoring inventory source and an agent lifecycle Pulse control-plane transport client.internal/models/models.goshared withmonitoring: removed host-agent identity aliases and tombstone state are both agent lifecycle authority and monitoring runtime report state. The PVE-side PBS guest-backup confirmation evidence the same file carries is monitoring-owned attribution state for Proxmox backup freshness: it is internal-only, never serialized into state payloads or snapshots, and does not describe agent enrollment, report admission, tombstones, re-enrollment, or command authority. The evidence is keyed by the PVE storage it was listed from and is readable per connection so monitoring can carry it forward across a failed storage query; that accessor is likewise monitoring-internal and never becomes an agent- or API-facing surface. The optionalmodels.Alert.LastSeenfield is likewise monitoring-owned lifecycle presentation evidence. It keeps mock active-alert and history payloads aligned with the canonical alert contract, but it cannot enroll, identify, link, command, remove, or otherwise grant authority to an agent.internal/monitoring/monitor.goshared withmonitoring: monitor construction owns both monitoring runtime initialization and fail-closed agent lifecycle journal hydration before report admission.internal/monitoring/monitor_agents.goshared withmonitoring: server-side Unified Agent report, removal, token binding, tombstone expiry, and re-enrollment semantics are jointly owned by agent lifecycle authority and monitoring ingest.pkg/agents/host/report.goshared withmonitoring: the Unified Agent host report is both an agent lifecycle authored-state contract and a monitoring ingest contract for host maintenance posture. Every current agent process authors a process-unique report stream and a monotonic sequence within that stream. Reconnect delivery must preserve collection order by draining each destination's persisted FIFO before sending a newly collected report, so buffered running-task snapshots cannot overwrite a later cancellation or completion. Authentication/scope rejection may discard an undeliverable FIFO; transient transport failures retain it. Unraid collection treats positivemdResyncPos/mdResyncas authoritative active-operation evidence and emits zero progress whenever no active sync action exists, even if terminal percentage fields linger.scripts/install.ps1shared withdeployment-installability: the Windows installer is both a deployment installability entry point and a canonical agent lifecycle runtime continuity boundary. The Windows installer must support a non-mutating download preflight that can run before Administrator-only install work, must accept token-file enrollment input, and must persist plain-HTTP/insecure runtime continuity consistently with the Unix installer. The installed Windows service must also expose the same local health/readiness server as foregroundpulse-agentruns so installer "healthy" verification and post-install smoke checks prove a live agent runtime, not merely a running service wrapper. The service must pass the installer-owned ProgramData log path to the agent's canonical rotating file sink, and install success requires both/readyzand a non-empty log file. SCM recovery must be configured as a required lifecycle contract, including non-crash failures, rather than a best-effort warning. Native Windows proof must exercise preflight, install, version replacement, logged readiness, forced-process recovery, service restart or OS reboot persistence, and complete uninstall cleanup through the reusable lifecycle harness underscripts/installtests/.scripts/install.shshared withdeployment-installability: the shell installer is both a deployment installability entry point and a canonical agent lifecycle runtime continuity boundary.--state-diris the canonical runtime-state boundary, not an authority for later root lifecycle work. The resolved directory owns the bootstrap and enrollment runtime token, server-acknowledgedagent-id, buffered state, and command receipts. On a least-privilege install, root-ownedconnection.env, the saved offline installer, and its SHA-256 integrity record live separately under the installer lifecycle directory; the collector-owned runtime directory must not contain executable or root-trusted recovery artifacts. Generated service definitions must carry the canonical runtime directory and token file through install, process restart, server restart, update, and re-enrollment. Explicit state wins over protected lifecycle recovery, which wins over discovered service state and platform defaults; a custom instance must never borrow token or identity files from the default instance. Recursive uninstall cleanup is authorized only by an exact explicit/default/platform path or a protected lifecycle record, never by process arguments or a collector-writable file. A changed bootstrap token may clear the old enrollment runtime token to express re-enrollment, while an unchanged token and tokenless update must preserve it. Default platform paths remain valid migration inputs for installations created before this contract. Legacy update recovery is cross-platform lifecycle continuity. Linux may read procfs or a systemd unit, while FreeBSD and pfSense must recover the same URL, token, feature, identity, and trust arguments from the live process viaps/procstator from the installed rc.d service script. Successful recovery always migrates the raw legacy token into the v6 installer-owned token file; absence of complete local URL and token state remains a fail-closed update, never a fresh enrollment. Disk exclusion is lifecycle state rather than an install-only hint.--disk-excludeaccepts device names, device paths, and mount-point patterns; repeated values must retain their order across live-process, systemd, and rc.d recovery, service rendering, restart, and update. The recovered exclusions must apply before filesystem-usage, disk-I/O, and SMART collection and must also suppress linked Proxmox disk health and wear alerts for excluded devices. FreeBSD-family uninstall must stop the installer-owned rc.d supervisor before removing the agent binary so daemon(8) cannot restart a deleted child. It must also remove the rc.d script, pfSense boot wrapper,pulse_agent_enablerc.conf state, supervisor and child PID files, runtime token/state directory, and residual agent process before reporting success.scripts/installtests/install_sh_test.goowns the static teardown contract, and native FreeBSD rehearsal must prove clean install, update, reboot persistence, and complete uninstall.
Server update planning is part of the same lifecycle contract. The System
Updates plan must surface a structured upgrade-readiness verdict before an
operator installs a v6 update, and the backend apply route must enforce a
blocked verdict before starting the updater: server update path, registered
agent continuity, v5 agent migration transport security, and agent reporting
token scope must be derived from the existing updater, connections ledger, and
API token state rather than from docs-only guidance or a parallel migration
registry. v5 or legacy agents that are still reporting must be treated as
compatible with the v6 reporting boundary, while still surfacing that their
first automatic hop uses the v5 updater before v6 signature and downloaded
binary self-test protections apply. Stale agents, missing reported versions,
missing agent reporting scope, or expired agent tokens must be called out
before the update starts. Root install.sh v5-to-v6 upgrades must also inspect
local token metadata before binary replacement and warn when the shell path
cannot prove agent-token continuity.
Stale-agent platform notices and the Infrastructure agentUpdates workspace
route are part of that same lifecycle contract. Platform pages may link to the
central Infrastructure update dialog with a scoped list of affected agent
connection IDs, but command rendering stays lifecycle-owned in Settings.
currentAgentTargetVersion() is the canonical agent update target and must be
projected to browser clients as /api/version.agentUpdateTargetVersion;
platform pages and Settings must not compare agent binaries against the app
build version when the server would tell agents dev. For Unix-like agents
that Pulse already sees, the copied stale-agent update command must use
scripts/install.sh --update and recover URL, token, identity, custom CA, and
insecure transport from installer-owned saved state instead of asking the
operator to mint a fresh install token or exposing agent IDs in the copied
command. If saved state is missing and the Unix shell installer falls back to
legacy running-process or service-unit recovery, recovered connection state is
usable only when both the control-plane URL and token are present. A URL-only
process or unit may seed later recovery attempts, but it must not be logged or
treated as recovered update state. That fallback must still run when an
operator supplies the update URL explicitly but token, identity, feature-flag,
or trust continuity remains recoverable only from a legacy v5 process or
service. Legacy v5.1.x Linux services that omitted --token and
--token-file because the Go agent read /var/lib/pulse-agent/token
implicitly may complete recovery from that installer state-dir/default token
file only after a running process, service unit, or saved state supplies local
agent connection context; the token file alone must not hide a missing
control-plane URL. Because v5.1.x agents were launched by Go flag parsing,
that legacy fallback must treat single-dash and double-dash agent flag
spellings as the same recovered state while preserving the same fail-closed
URL-plus-token threshold. Windows stale-agent update commands remain on the
existing token-gated install transport until the Windows installer owns an
equivalent saved-state update mode.
The current Unified Agent updater is the sole automatic binary-update
authority for Linux, Windows, and Docker-enabled pulse-agent installations.
It checks once after the five-second startup delay and then hourly, compares
normalized semantic versions across RC and stable boundaries, and refuses
downgrades. Each version check carries the running agent version plus a
request-unique non-secret cache key; each binary download carries the exact
server target version. The server version response and binary response are
non-cacheable, so a reverse proxy may not reconcile a fresh target with an
older cached artifact. Offline, authentication, checksum, signature,
self-test, or pre-replacement failures retain the running binary and retry on
a later check. The legacy updater in internal/dockeragent/self_update.go is
not a second v6 authority: Docker and Podman are modules inside the unified
agent and set AgentType=unified, which bypasses that compatibility path.
The report contract separates runtime family from OS identity. Newly built
agents derive Host.Platform from their compiled GOOS and keep the
distribution or appliance caption in Host.OSName; a Mageia agent therefore
reports linux plus Mageia, not mageia as an unclassified runtime. Server
ingest and connection-ledger projection must remain compatible with older
agents whose gopsutil platform field contains a Linux distribution identifier.
That compatibility is expressed once through
platformsupport.ResolveAgentRuntimePlatform: Pulse's published Windows,
macOS, and FreeBSD families resolve explicitly, known unsupported OS families
fail closed, and the remaining non-empty legacy distribution identifiers
resolve to Linux without a distro allowlist. Agent Doctor's browser fallback
mirrors that bounded release-family rule so a stale server payload cannot
regress long-tail Linux distributions back to an unavailable manual command.
Agent Fleet Doctor diagnostics extend that same read-only lifecycle triage
surface: GET /api/agents/diagnostics may explain stale versions, missing
reports, profile deployment drift, expected Docker/Kubernetes telemetry gaps,
identity splits, updater/module failures, and removed-agent blocks. The
diagnostic target is the canonical agent-update target, not the Pulse server
build string, so development builds and separately versioned agent artifacts
do not create false drift. The Infrastructure workspace polls this read model
only while Agent Doctor is open, enriches canonical connection-ledger rows by
stable connectionId, and retains ledger-only fallback rows when structured
evidence is absent.
Secure-runtime posture on that read model is factual and split by authority
source. The collector-authored report supplies effective-root state, service
user, local commandAuthority, and configured typed/SMART/pct helpers. The
server adds only evidence it can independently observe: whether the reporting
token is present and carries agent:exec, whether the current organization has
an active action-runner credential bound to the canonical agent ID, and whether
the organization-scoped live-session registry contains that agent under the
closed action-runner role and typed_actions.v1 capability. Connected
runner version, connection time, binding version, receipt protocol, preflight
protocol, and Docker-observation protocol remain separate optional facts.
Absence is not projected as safe, and neither collector liveness nor a healthy
fleet verdict may stand in for action-session readiness.
Agent Doctor may issue or rotate an action-runner credential only after the
selected diagnostic proves a non-root, monitoring-only collector with a known
non-exec credential and typed helper, resolves to a standard-looking Linux
systemd installation, and has one complete canonical agent ID/hostname pair.
The operator-only endpoint re-resolves that pair against exactly one live,
non-conflicted, non-integration host in the request tenant. The resulting
credential has only agent:exec, the explicit action-runner role,
typed_actions.v1, binding version 1, organization ID, canonical agent ID, and
normalized hostname. Rotation replaces only the earlier action-runner record
for that organization/agent binding; the token inventory is saved atomically,
and a failed save restores the complete previous inventory before the secret
can be returned.
The raw secret is a one-time UI reveal, not a diagnostic field. The page keeps
it in component memory only, permits at most one successful issuance for that
target in the page session, and clears it explicitly or on navigation. The
host handoff prompts through /dev/tty, writes
/etc/pulse-agent-runner/token under a root-owned 0700 directory with file
mode 0600, and puts only --action-token-file plus canonical identity in the
installer command. Secrets must not enter URLs, browser storage, diagnostics,
command arguments, or logs. A successful rotation invalidates the prior
credential immediately; the UI therefore blocks enrollment while a compatible
runner session is already connected and presents rotation only for an issued
but disconnected binding.
Repair entries remain handoffs to existing lifecycle operations:
copy_upgrade_command renders a local operator command and
allow_reenroll invokes the existing removed-agent flow. Agent Doctor exposes
that supported action for removed host, Docker / Podman, and Kubernetes
diagnostics, disables the control while the request is pending, reports the
result, and refreshes diagnostics after success. The connection ID's typed
prefix selects the matching lifecycle endpoint; an absent ID or unknown type
must fail closed without issuing a request. They never enqueue a remote command
or create an action plan. A stale agent with an unknown or
explicitly unsupported runtime family, or with FreeBSD/pfSense installer state
that the server cannot verify, must receive an unsupported handoff rather than
a guessed command. A non-empty legacy Linux distribution identifier is not
unknown: it resolves through the published agent runtime-family contract.
When automatic update is enabled and checking or applying, Agent
Doctor waits and reports that state instead of prematurely offering a manual
installer path. The agentDoctor route key is canonical; the older
agentUpdates deep link remains a read-side compatibility alias only. Agent
Doctor must not replace the canonical /api/connections fleet projection used
by Infrastructure.
Authentication repair is a distinct lifecycle handoff, not a successful
upgrade. When a reported host still names a token record that is absent from
the server inventory, has expired, or lacks agent:exec while that host reports
Pulse command execution enabled, Agent Doctor must raise a critical typed
credential reason (agent_credential_missing, agent_credential_expired, or
agent_exec_scope_missing) and may offer repair_authentication only for a
recognized, unambiguous local runtime. A disconnected command channel alone is
not proof of a scope defect. The copied Unix repair command mints a fresh scoped
agent credential—including command scope when the affected host reports it
enabled—and transports it through an ephemeral --token-file; an
ordinary version-only Unix update stays tokenless and recovers the installed
credential from installer-owned state. If two different host agent IDs report
the same non-empty machine ID and equivalent hostname, both installations stay
visible as distinct fleet rows with peer identity/version evidence. Pulse must
not collapse them into one apparent agent or choose one generic local service:
authentication and upgrade handoffs fail closed until the duplicate
installation is resolved or an installation-specific path exists. Finally,
local process health cannot override server authentication: a definitive
registration 401 or missing-scope 403 makes scripts/install.sh complete with
the non-zero auth_rejected result instead of printing installation or upgrade
success. An ownership-mismatch 403 is not definitive during first-use binding;
the installer must prefer the persisted agent ID and continue its bounded
verification window rather than mislabeling a fresh token.
Agent lifecycle and fleet-operation surfaces may consume
POST /api/actions/plan for resource capability planning, but the action plan
contract remains API-owned through internal/api/actions.go,
internal/actionlifecycle/service.go, and
internal/actionplanner/planner.go. Agent lifecycle work must not define a
parallel approval policy, blast-radius model, stale-plan hash, or execution
contract for those resource actions. Successful action plans also belong to
the API-owned action-audit trail before any lifecycle surface consumes them:
approval-required plans must be visible as pending_approval with initial
lifecycle evidence, and retry/idempotency handling must not create duplicate
lifecycle events. Approval or rejection decisions for those plans must flow
through POST /api/actions/{id}/decision, which records API-owned audit and
lifecycle evidence only; lifecycle surfaces must not treat approval as
implicit command execution or define a parallel execution handoff. The
Patrol proposal broker (internal/api/patrol_action_broker.go) rides that
same API-owned lifecycle; agent lifecycle surfaces must not consume
Patrol-origin action audits as an agent command grant or lifecycle
execution shortcut, and must not subscribe lifecycle side effects to the
API-owned org-scoped action-transition hook. The investigation
broker may authorize eligible actions under core-owned tenant/resource policy,
but that policy decision does not grant agent lifecycle code a parallel
authorization primitive: all execution still enters through the shared action
lifecycle and its agent executor.
When runtime mock mode projects representative action audits through those
same read routes, the response is explicitly readOnly and the rows remain
inspection fixtures only. Agent lifecycle surfaces must remove decision and
execution affordances, must not treat a mock approval as an agent command
grant, and must not infer lifecycle reachability from fixture state.
Those list/detail reads may also attach the canonical unified-resource name and
contract type as a sibling resource presentation object. Agent lifecycle
consumers must treat that object as display metadata only: the exact
request.resourceId remains the command target, and resource display changes
must not alter the action ID, plan hash, approval binding, dispatch identity, or
durable receipt reconciliation.
The investigation
continuity reconciler in internal/api/patrol_action_reconciliation.go is
also API-owned: callbacks only wake an authoritative action-audit re-read,
and missed callbacks recover during investigation reads. Agent lifecycle
surfaces may observe the resulting audit/lifecycle evidence but must not use
callback payloads as command grants or maintain a parallel action state.
The investigation
orchestrator adapter wired here exposes only investigation-specific
execution and listing with no autonomy or command-execution surface, so
agent lifecycle surfaces gain no command-dispatch path through Patrol
investigations. Assistant
handoffs that recover a live Patrol approval by finding ID are still AI/runtime
review context only; agent lifecycle surfaces must not treat that recovered
approval reference as an agent command grant or host-execution shortcut.
Assistant session-history reads from the shared AI endpoints are likewise
API-owned browser-safe projections: lifecycle surfaces, MCP adapters, and
agents must not depend on reasoning_content, raw pulse_* / patrol_*
tool-call prose, token accounting text, or assistant transcript prose as an
agent command grant, install-token disclosure, or host-execution shortcut.
Searchable Assistant session-list queries on GET /api/ai/sessions are the
same browser-safe history navigation projection; lifecycle surfaces may not
reinterpret search matches, handoff summaries, or message counts as agent
capability state, command authority, enrollment evidence, or install-token
availability. When a planned resource capability is actually executed from an
agent-lifecycle surface, that handoff must route through
POST /api/actions/{id}/execute so
the API-owned action audit records executing before dispatch and the
terminal execution result afterward. Dry-run-only plans remain planning evidence
only; lifecycle surfaces must not present them as executable, dispatch them
through agent-local command paths, or bypass the API fail-closed execution gate.
Docker / Podman container start, stop, restart, and image-update affordances
follow that same boundary: lifecycle UI may consume unified-resource
capabilities for enabled or disabled presentation, but execution must stay
inside the API-owned action executor wired from internal/api/router.go and
must not shell out, SSH, or call Docker / Podman from lifecycle-local code.
That API-owned executor may resolve the command WebSocket by Docker source ID
or canonical Docker host name and may mark its vetted container lifecycle
dispatch as a trusted agent command after the API action has entered
execution; lifecycle surfaces still consume only the resource payload, action
readiness, and action-audit result rather than issuing or approving
command-agent grants themselves. Because the agent and Pulse server have
independent clocks, the API-owned Docker result projector validates raw
lifecycle/update chronology within its bounded skew window and then binds a
slightly future valid observation to the server receipt boundary for canonical
evidence. Agent lifecycle consumers must not reject that bounded case, widen
the skew window, or clamp stale/excessively future evidence into validity.
Inside a containerized Unified Agent, the closed start/stop/restart command
must inspect and mutate through the Docker / Podman module's already-connected
daemon API; it must not depend on a second docker or podman executable being
present in the agent image. The host-command bridge exposes only exact-container
inspect plus the allowlisted start, stop, and restart verbs. Execution remains
read-before-mutate-read, and a mutation request is sent exactly once: an
ambiguous daemon transport failure must surface for receipt reconciliation
rather than being retried and possibly applying the operation twice. Native
host agents may retain the equivalent CLI implementation as a compatibility
fallback when no module bridge is configured.
Docker / Podman container image updates are a fourth typed operation on that
same closed channel, not an extension of the raw command path and not a
revival of the retired queued update_container transport.
docker_container_update has its own strict codec in
internal/agentexec/docker_update_codec.go: the payload carries only the
immutable container id, the runtime, and the exact image reference the plan
was bound to (part of the request digest; no command text, flags, or
user-controlled names), and the result reports phase, replacement-container
identity, image digests, backup identity, and rollback attempt and outcome as
facts, never canonical action truth. The unified agent admits the operation
through the same durable operation-receipt store and bridges execution to the
Docker module's own pull/backup/recreate/verify/rollback implementation via
internal/hostagent/docker_update.go; a missing Docker module, a runtime
mismatch, or image drift since planning refuses before any mutation. The
module attests the first mutating step (the backup rename), so failures before
it report an unmutated tree, and restore attempts with their outcome ride the
result so the server-side executor reports declared compensation truth instead
of guessing.
Agent (re)registration on the internal/agentexec command server is also the
durable-dispatch recovery trigger: after a successful registration the server
fires a router-installed notifier that re-drives the API-owned
executing-action recovery pass, because a receipt-pending dispatch attempt
can only be reconciled against the agent's durable operation receipt while
that agent is connected. The notifier is observation-only wiring; it grants
no command authority, fires only after token validation admits the agent, and
a rejected or replaced registration must not synthesize receipts or resend
transport.
Host OS package updates follow a stricter adjacent boundary. The Unified Agent
may report a bounded, read-only APT upgrade simulation through
pkg/agents/host/report.go, but installation authority is available only via
the closed host_update WebSocket operation owned by internal/agentexec and
internal/hostagent/package_updates.go. That envelope carries an action id and
the exact install_os_updates operation only: it has no command, package-name,
reboot, removal, or arbitrary-argument field. The agent owns metadata refresh,
preflight simulation, noninteractive apt-get upgrade --no-remove execution,
and the second simulation used as read-after-write evidence. The request carries
the agent-authored expected inventory fingerprint; if metadata refresh changes
that inventory, the agent updates its observed state and refuses installation
so Pulse must re-plan against the widened or changed set. A reboot-required
marker is reported as state, never acted upon by this capability. Generic
agent command execution, Patrol prose, lifecycle UI, and external agents must
not reconstruct or bypass this typed operation.
Every fixed-catalog package and Proxmox guest invocation runs inside a
cryptographically named transient systemd service. PID 1 starts a hidden
runner launcher that revalidates the closed tool/argument/environment catalog,
mirrors the runner sandbox, caps output, and records direct-process and cgroup
inspection state through a root-private atomic sideband. KillMode=control-group
and bounded TERM-to-KILL escalation own cancellation; disappearance of the
exact transient unit, never a PID/PGID observation or pipe close, is the sole
authority to release the shared typed-action gate and package-manager lease.
The runner reconciles stale private units and executes a harmless containment
probe before registering. Missing systemd features, manager-query ambiguity,
launcher inspection failure, or a direct process exiting with descendants is
indeterminate and fail-closed; failed adopted workers remain indeterminate even
after they exit. Package interruption sets recovery-required,
and indeterminate Proxmox dispatch requires target-state recovery inspection.
The package unit alone permits setuid/setgid file creation and kernel-package
payload writes because apt/dpkg legitimately require them; every other runner
hardening property and the exact package command catalog remain enforced.
Contained Proxmox stop/shutdown units alone add AF_NETLINK to their
address-family allowlist because PVE teardown helpers can configure bridged
networking through route netlink; status and QEMU reboot retain the narrower
family set. QEMU/container start and container reboot use a distinct fixed
proxmox-handoff catalog: PID 1 launches the same hidden validator in an exact
transient unit, but the unit omits sandbox settings that would otherwise be
inherited by PVE-managed VM/container processes after they move to native PVE
scopes. The handoff launcher deliberately does not become a child subreaper;
the exact transient unit must disappear before return, and the lifecycle may
report success only after a separate contained status read proves the requested
VMID state. Cancellation after a provider handoff remains indeterminate and
requires recovery inspection. Package and containment-probe units never gain
AF_NETLINK or the handoff relaxation. This provider-aware path remains
unqualified until disposable-PVE proof establishes the native scope handoff,
bridged networking, postcondition, and cancellation behavior.
scripts/run-native-pve-action-qualification.sh is the fail-closed entrypoint
for the live provider slice. It requires a clean exact source revision, pinned
SSH host identity, a dedicated PVE node with no installed or loaded Pulse
runner, distinct VM and container IDs, and a confirmation bound to the machine
ID, PVE node, source revision, and both guest IDs. The Linux test then rejects
templates, HA-managed guests, non-local owners, running initial state, missing
pulse-disposable tags, absent bridged interfaces, and config-identity drift.
It runs the candidate runner as the real hidden launcher behind a temporary
runtime-only anchor and executes the test inside a root-owned systemd
supervisor. The supervisor's independent ExecStopPost cleanup revalidates the
durable original identities, stops exact qualification action units and any
still-running original guests, and removes only the matching anchor invocation
after normal exit, timeout, or process termination. The harness revalidates the
exact PVE config digest before every mutation and cleanup and records artifact
hashes/build metadata, bound production results, per-operation transient-unit
and sideband disappearance, VMID-specific native PVE cgroups, per-NIC live
bridge attachment paths, final states, and any emergency cleanup in
root-private structured receipts. Emergency cleanup, missing cleanup evidence,
or identity ambiguity fails the run; an identity mismatch never authorizes a
cleanup mutation against a replacement guest. The reboot observation proves
only that the fixed provider command returned and the production running
postcondition passed, not an independent in-guest reboot. A checked-in harness,
skipped local test, or unattested receipt is not provider qualification.
Cancellation after provider handoff, hook inheritance, WSS/API authorization,
durable reconnect/replay, and exact release-artifact reproduction remain
separate required evidence and must not be inferred from this lifecycle pass.
Docker/Podman lifecycle remains on the already-connected daemon API operator;
the action runner does not fall back to a killable external runtime CLI.
Host storage-pressure cleanup is a second closed agent operation. The report
may expose only the bounded apt-package-cache provider, its reclaimable byte
count, freshness, and a SHA-256 fingerprint; cache entry names and paths remain
agent-local. Execution crosses host_storage_cleanup with the exact
clean_package_cache operation and expected fingerprint only. The agent owns
the fixed /var/cache/apt/archives scan, bounded entry/byte limits, and sole
apt-get clean command. Fingerprint drift, unsupported providers, empty cache,
inspection failure, command failure, and unconfirmed reclaimed bytes all fail
closed. The envelope has no command, path, package selector, arbitrary
argument, installed-package removal, or reboot authority.
Before a concrete Docker lifecycle/update, host update, or package-cache
cleanup action is persisted for approval, the current Unified Agent may be
asked to evaluate the exact already-bound operation through the versioned
action_preflight protocol. That request is read-only, carries exactly one
closed typed envelope and its action-bound request digest, and never enters the
durable operation-receipt store. The result is limited to feasibility, the
same bounded refusal-code vocabulary, the exact operation binding, and a fresh
agent timestamp. It cannot approve, admit, reserve, execute, or verify a
mutation. Current agents advertise protocol version 1 at registration; a
server with the transport but an older connected agent fails concrete
feasibility closed, while internal executors that predate the optional
transport retain their compatibility path. An agent older than the bounded
refusal-code vocabulary reports pre-mutation refusals with no reason code at
all. The API layer records those under the preflight_refused legacy aggregate
and aggregate telemetry counts them as uncoded rather than other, so the
share of the fleet still awaiting an agent capable of typed refusals stays
measurable and is never mistaken for a categorization failure. Dispatch still repeats all local
preconditions after durable admission because preflight evidence is not a
lease and target state may race after approval.
The aggregate telemetry assembled through the shared internal/api/ router
also exports the Patrol blocked cause enum at schema v10. That value is
derived from Patrol runtime state on the server, never from agent transport,
agent refusal traffic, or agent identity, and it adds no agent-visible
surface: agent refusal reporting, its typed vocabulary, and the uncoded
partition above are unchanged by it.
Proxmox VM and LXC lifecycle affordances follow the same adjacent boundary:
lifecycle and fleet surfaces may consume backend-advertised start,
shutdown, reboot, and stop capabilities and typed actionReadiness, but
the only execution path is the API-owned action executor that resolves a
connected Proxmox node command agent and records action audit plus verification.
Lifecycle surfaces must not run qm / pct, SSH to a node, call Proxmox
mutation APIs, or substitute a guest-local agent to perform VM/LXC lifecycle
control.
The executing node agent's status read remains agent-attested. The closed
postcondition registry in internal/agentexec/verifier_postconditions.go
defines start, shutdown, stop, and reboot checks for both qm and pct, while
the API-owned verifier supplies provider observations. Reboot is not confirmed
from status alone: it requires a distinct Proxmox API observation whose uptime
reset proves that the guest actually restarted.
Disconnected command-agent state is also API-owned readiness: lifecycle
surfaces may reflect missing backend-advertised capabilities, but must not
reconnect, substitute, or directly address an agent to make a stale container
action executable. Backend resource payloads and plan-action readiness are the
only supported lifecycle signal for that state. When a resource payload carries
typed actionReadiness, lifecycle surfaces may display the reason but must not
treat it as reconnect authority or an alternate command grant. The optional
detail such an entry may carry (naming the missed enrollment-token or
session lookup) and the server-side warn log every availability refusal now
writes are the same API-owned diagnosis surface: they add no agent-visible
transport, and no surface gains reconnect, rebind, or command authority from
reading them.
Assistant session rename through PATCH /api/ai/sessions/{id} follows that
same browser-safe history boundary. Lifecycle surfaces, MCP adapters, and
agents may display the updated title as human navigation metadata, but they
must not treat a renamed title as command authorization, host identity,
enrollment state, capability evidence, or install-token disclosure.
Assistant turn undo through POST /api/ai/sessions/{id}/undo (including the
optional expected-prompt guard body used by retry/regenerate) is the same
kind of conversation-history mutation: it rewrites chat transcript state
only. Lifecycle surfaces and agents must not interpret an undone or
regenerated Assistant turn as reverting any lifecycle action, approval, or
agent command the original turn produced; governed action history remains
the only revert authority for infrastructure changes.
Mid-turn steering through POST /api/ai/sessions/{id}/steer is likewise
conversation input only: it adds a user message to a running Assistant loop
at a turn boundary. A steering message grants no agent command authority,
cannot approve, deny, or bypass a pending approval, cannot escalate the
running turn's control level or autonomous mode, and must not be
interpreted by lifecycle surfaces as operator authorization for any action
the steered turn subsequently proposes; those proposals still route through
the governed approval and action lifecycle unchanged.
The native Assistant surface-tool inventory at
GET /api/ai/assistant/surface-tools is also AI-runtime/API-contract metadata:
lifecycle surfaces may display which Assistant tools are available, but must
not reinterpret that list as agent enrollment state, command authority, install
token scope, or proof that a host can execute lifecycle actions.
Agent lifecycle consumers of /api/agent/events and
/api/agent/resource-context/{id} must also honor the shared API command
payload boundary: API tokens with monitoring/read scope receive
commandRedacted:true instead of raw approval, action, or verification command
text unless they also hold action execution scope. Lifecycle UI and agents may
use those redacted events as doorbells or status summaries, but they must fetch
governed detail through the approval/action surfaces and must not treat a
monitoring-readable event stream as command disclosure or execution authority.
Lifecycle surfaces that inspect agent event kinds must consume the shared
internal/agentcapabilities event vocabulary. Local lifecycle, MCP, or probe
copies of finding.created, approval.pending, action.completed,
stream.connected, or heartbeat, or local SSE parser semantics for those
events, are contract drift because event naming and stream framing are
API/AI-owned, not lifecycle-owned.
Lifecycle consumers of agent-surface failures must also consume the shared
internal/agentcapabilities error envelope; lifecycle work may branch on the
stable error code but must not define a local failure envelope or reinterpret
agent errors as install, enrollment, command, or update authority. Branchable
agent-surface codes exposed through shared internal/api/ routes must be
referenced from agentcapabilities.AgentErrCode* constants so lifecycle UI,
Assistant handoffs, MCP adapters, and manifest declarations do not drift into
separate string vocabularies.
The resource-context endpoint's additive context sections are the same
read-only boundary: lifecycle consumers, MCP adapters, and external agents may
use their bounded facts, provenance, freshness, and explicit redaction metadata
to understand a resource, but those sections must not be expanded into
provider-local config, raw discovery command output, environment values,
secret-like metadata, install-token scope, or command authority.
Discovery context exposed by /api/discovery is similarly API-owned
observability context, not lifecycle command authority. Lifecycle and
infrastructure setup surfaces may display service versions, config paths,
ports, bind mounts, suggested URLs, and mock-mode examples from the Discovery
API, but they must not widen install tokens, agent profiles, or command grants
because a Discovery record exists. Mock-mode Discovery records are demo
payloads for the same API contract and must not become a lifecycle-local
fixture or an implicit permission to run commands.
Agent-facing provisioning capabilities declared by
/api/agent/capabilities are also API-owned projections over the canonical
node lifecycle and discovery APIs. Lifecycle and infrastructure onboarding
surfaces may use discover_lan, list_nodes, add_node, update_node,
remove_node, test_node_credentials, test_node_connection, and
refresh_node_cluster_membership to orchestrate source onboarding, but those
tools must preserve settings-scope auth, typed manifest schemas, and redacted
source reads. The manifest does not turn discovered hosts into command-agent
authority, install-token scope, fleet-command grants, or permission to bypass
human approval for multi-source imports.
Native provider resource types exposed through shared internal/api/ resource
handlers are the same kind of read-only context for lifecycle surfaces. A
TrueNAS network-share resource may appear in resource pickers, connection
impact previews, or cross-organization sharing only through the API and
unified-resource contracts; lifecycle code must not treat that share as an
agent install target, a command-agent authority, or a reason to widen setup
tokens.
Source-specific backup artifact routes under shared internal/api/, including
/api/backups/pve and /api/backups/pbs, are also read-only monitoring and
storage/recovery evidence for lifecycle-adjacent surfaces. Lifecycle code may
display those backup facts as connection or workload context, but it must not
treat a backup artifact, datastore, namespace, owner, or verification record as
an agent enrollment candidate, install target, setup-token scope, credential
grant, or fleet-command authority.
Docker / Podman native inventory and Kubernetes native API inventory follow
that same lifecycle boundary. Images, volumes, networks, Swarm tasks,
Swarm nodes, Swarm secrets, Swarm configs, engine storage-usage buckets,
Kubernetes Services, controllers, EndpointSlices, NetworkPolicies, PV/PVC
records, StorageClasses, ConfigMaps, Secrets, ServiceAccounts, Roles,
ClusterRoles, RoleBindings, ClusterRoleBindings, ResourceQuotas,
LimitRanges, PodDisruptionBudgets, HorizontalPodAutoscalers, ingresses,
namespaces, and events may appear as API/unified-resource evidence, but
lifecycle surfaces must not treat those child inventory rows as agent
enrollment candidates, install targets, fleet command authorities, setup-token
scopes, credential grants, or command material. RBAC inventory in particular
reports summary counts only and never carries credentials, individual subject
identities, or full policy rules into the lifecycle surface. Podman libpod pod records are
not part of that lifecycle-visible inventory until a libpod-native collector
owns them explicitly; Docker-compatible Podman container labels remain runtime
metadata only.
VMware vSphere network resources follow that same lifecycle boundary. A
vCenter connection may project networks into platform tables, resource
pickers, Assistant context, or monitored-system previews through the shared
resource contract, but those rows are provider inventory facts only. Lifecycle
surfaces must not interpret a vSphere network as a host enrollment candidate,
Pulse Agent install target, fleet command authority, or setup-token scope. When
host-shaped records are coalesced for presentation, lifecycle consumers must
use the API/unified-resource presentation result and must not create their own
merge that bypasses registry-owned report exclusions.
The node setup modal boundary must keep guided setup and manual credential
submission separate. For new PVE/PBS setup, API Inventory and Host Telemetry
Agent setup modes are command-driven auto-registration paths; Token ID/Value
fields, Test Connection, and Add Node submission belong only to Manual Token
Setup or existing-node edit flows.
The setup guide must also present the source strategy at action time: API
Inventory is the recommended least-privilege API path, Host Telemetry Agent is
the optional full-host-telemetry root-agent path, and Manual Token Setup is an
advanced manual API-token escape hatch.
For PVE, that same setup guide must keep Docker inside Proxmox LXCs attached
to the Host Telemetry Agent path, not the default API Inventory path: API
Inventory alone cannot run host-side guest Docker inventory, while the PVE Host
Telemetry Agent command must explicitly request Pulse command execution and
explain the separate server-side opt-in: the admin-only Discover Docker in LXC
guests toggle in Settings → System → General, or the
PULSE_ENABLE_PROXMOX_GUEST_DOCKER_INVENTORY environment variable, which
overrides and locks the toggle when set.
For existing Proxmox API sources, that same setup guide must expose the
non-destructive Audit/Repair rerun path before asking operators to rotate
credentials. Audit/Repair is the first lifecycle response to suspected setup
drift: it checks the Pulse-managed user, current token presence/expiry,
expected ACLs, and older Pulse-managed tokens without replacing the stored API
secret. Full Install/Configure remains the explicit token-rotation path when
the Proxmox-side token value is gone or no longer matches Pulse.
That shared monitored-system impact preview boundary also owns the disabled
platform-connection lifecycle state. Once a TrueNAS or VMware setup form marks
the connection disabled, lifecycle surfaces must treat a canonical zero-delta
or removal-only preview as a valid save path instead of holding the dialog in
an add-only posture.
Agentless availability targets no longer belong to the infrastructure
source-management surface. MQTT, HTTP/S, ping, and TCP checks are monitoring
availability resources owned by the Settings Monitoring availability panel and
its model, while infrastructure remains limited to platform API connections,
agent-backed systems, discovery, and install lifecycle. Lifecycle UI may
observe availability rows only through shared API/resource facts; it must not
offer them from Add infrastructure, Manage, or the host connections ledger, and
must not ask them for SSH, setup tokens, auto-registration, agent profiles, or
install commands. Their managed actions remain availability-resource actions:
pause, test, edit, and remove.
An availability target classified as machine is still agentless unless a
Pulse Agent actually registers for that host. That classification exists so
servers, desktops, laptops, and comparable computers such as a Mac mini can
use machine-specific reachability copy in Availability checks; it must not
create Standalone Machines membership, lifecycle authority, install state,
agent profile membership, or host-command capability. A targetKind=machine
add-dialog handoff is only a preselected availability-resource creation path,
not an agent install or lifecycle onboarding state.
The lifecycle-owned onboarding presentation helper must consume the governed
platform support manifest for readiness stage, primary mode, canonical
projections, and support-floor posture.
frontend-modern/src/utils/infrastructureOnboardingPresentation.ts may adapt
those facts into source-strategy copy, but it must not turn an admitted
first-lab-ready platform such as VMware into a product-level supported
claim, invent platform-local projections, or classify assistant control beyond
the manifest's support-floor row. Admitted API platforms may remain available
inside the add-flow source picker and source-manager summaries, but their
operator-facing badge must be preview/admission language and empty-state copy
must keep them separate from the supported-source list until the governed live
proof promotes the platform.
That same lifecycle-owned helper must keep first-class platform APIs separate
from governed Pulse Agent host profiles. Host/appliance compatibility such as
Unraid is presented as an agent install/profile path sourced from the manifest
agent_host_profiles section, not as PLATFORM_TYPE_KEYS membership or a
peer API-backed platform.
The add-infrastructure picker must preserve that boundary while presenting a
plain operator-facing system finder: users choose recognizable infrastructure
systems such as Unraid, TrueNAS, Proxmox, Docker, Kubernetes, or standalone
hosts, and the helper maps those choices into API-backed or agent-backed routes
only after selection. The first picker surface must support aliases for common
operator terms and must not make API/agent taxonomy the primary choice model.
Agentless MQTT, HTTP/S, ping, and TCP probes are not infrastructure choices;
their add flow belongs to the Settings Monitoring availability owner.
Agent-backed typed routes must keep that context in the installer: Unraid,
Docker, Kubernetes, and generic host choices may share the same unified agent
installer, but their visible title, recommendation copy, preferred install
profile, and first command section must match the selected system instead of
falling back to an all-platform generic agent screen.
The generic host installer route is ?add=linux-host: setup completion,
Proxmox "Install agent" shortcuts, detect-flow agent handoffs, and first-run
test helpers must target that typed route so the operator sees the standalone
Linux/macOS/Windows host context. The older ?add=agent value may remain a
compatibility input, but it is not the canonical destination for new handoffs.
Agent runtime normalization must use that same governed host-profile manifest:
profile identity tokens and runtime platform fallback values such as
unraid -> linux are generated into the runtime resolver instead of being
redeclared in host-agent or settings-table branches. Host-profile detection is
an identity fact and must not depend on optional storage probes succeeding; an
Unraid host still reports the governed unraid profile and linux runtime
platform when mdcmd or array-topology collection is unavailable.
Unraid array collection belongs to that same runtime-normalized agent path:
internal/hostagent/unraid.go must treat empty DISK_NP/DISK_NP_DSBL
slots with no device, id, filesystem, or size as unassigned topology
placeholders rather than failed disks, even when Unraid gives those slots
topology labels such as disk6 or parity2. Transport-only native identity
sentinels such as ata-_ are empty-slot metadata, not assignment evidence;
the collector must discard them before native-inventory merge and report
projection. Conversely, provider status DISK_NP_MISSING is explicit evidence
of an assigned-but-absent member and must remain reportable even when its
identity fields are unavailable. Assigned disks may use
diskId/rdevId as the serial fallback when Unraid does not expose a separate
serial field, so monitoring receives stable disk identity without inventing
host-profile or platform state from optional storage probe success.
Monitoring ingest must apply the same evidence filter to reports from deployed
older agents, then make the remaining structured member and parity statuses
authoritative over stale aggregate failure and protection counters. Focused
provider-boundary proof lives in internal/hostagent/unraid_test.go,
internal/unraid/status_test.go, and
internal/monitoring/monitor_host_agents_test.go.
That Unraid runtime path must also prefer native appliance topology over
generic block-device inference. The Unified Agent should best-effort merge
/var/local/emhttp/disks.ini into the mdcmd status view and carry disk
device, model, transport, filesystem, size, used/free capacity, temperature,
spin state, read/write counters, and error counters in the report contract.
The runtime must read that native membership before optional SMART work and
preserve it when mdcmd status times out or is unavailable. When structured
native per-disk states exist, their disabled/invalid/missing counts override
stale aggregate mdcmd counters while assigned DISK_NP evidence still remains
a genuine missing disk. Failure to read the native file must degrade to the
existing mdcmd view without blocking host reporting. SMART collection uses an
independent deadline, receives native transport/spin hints, and is supplemental
hardware telemetry rather than the owner of Unraid storage topology.
First-class platform hosts that also run the Pulse Agent must keep the same
operator-facing system identity split: a Proxmox VE node may report a Debian
runtime platform underneath, but the host-agent OS identity and infrastructure
System badge must resolve and present Proxmox VE / PVE from PVE runtime
evidence instead of exposing the Debian base distro as the primary system label.
The agent-side PVE version probe must prefer fast package metadata such as the
installed pve-manager package before falling back to pveversion, and that
fallback budget must tolerate slower ARM/Pi PVE installs so platform identity
does not lose the runtime version on small lab nodes.
The lifecycle-owned infrastructure source manager also owns platform/system
grouping as source-management content, but not its table band presentation:
frontend-modern/src/components/Settings/InfrastructureSourceManager.tsx must
route table-level product/system group rows through the shared grouped table row
helper instead of carrying lifecycle-local background or hover classes. Its
desktop source-management table also inherits the surrounding settings panel
frame and the shared Table shell from frontend-primitives; lifecycle work
must not restore a local overflow-x-auto wrapper or a page-local table card
around that table.
The same source-manager landing surface must keep onboarding primary and
compact: Add infrastructure stays in the Connected systems header, connection
posture is one short actionable line, and detailed governance/fleet state stays
in the systems table rows or deeper fleet surfaces instead of expanding into
duplicate explanatory bands above the table. A coverage gap that has an
immediate lifecycle action, such as missing Proxmox host telemetry, must name
that gap and expose the install action rather than appearing only as a count.
Manual discovery remains observable because scans are an operator command, but
it is optional setup rather than the primary connected-systems job.
InfrastructureSourceManager.tsx must keep one explicit Discover Proxmox
systems band after the configured-systems ledger that shows the current
enabled/scanning/error/result state, scan scope, last scan metadata, and review
action for discovered candidates before anything is added. When discovery is
off, the landing action must configure discovery instead of presenting a
disabled Run command.
Its copy must describe only the platform APIs represented by the LAN discovery
candidate model, and it must not imply TrueNAS, VMware, Docker, Kubernetes, or
agent command discovery when this source-manager path cannot surface those
candidates.
The DiscoverySettingsForm.tsx scan-scope selector must make automatic versus
custom subnet selection a full-row operator control, not a narrow or hidden
input target; common subnet chips and the custom subnet field must share the
same environment-override and in-flight-save lock semantics as the scan-scope
selector.
The discovery settings safety notice may use the frontend-primitives-owned
CalloutCard shell, but the lifecycle contract still owns the operator-facing
scan-scope semantics: environment overrides, immediate persistence, automatic
interface scan breadth, and custom-subnet guidance must remain visible before
the selector rather than becoming feature-local styling copy.
Adjacent Assistant resource-context handling in internal/api/ai_handler.go
may reference agent-backed resources only as selected-resource, model-only
context. It must not mutate agent lifecycle state, start agent discovery, or
grant agent command authority unless the request flows through the governed
agent/action execution contract.
Reported agent-backed host profiles must be visible at the source-manager
grouping level without changing the canonical connection type: an Unraid agent
row stays agent internally, but the connected-systems table groups it under
the governed Unraid host-profile family and routes that group's add action
back to the typed Unraid onboarding flow. Discovered source candidates remain
attached to the base source group until a configured agent reports a governed
host profile; discovery must not be visually inherited by a profiled group.
The source-management ledger must keep source-level health and per-member
agent posture separate. Cluster parent rows may summarize the primary source
and their rendered members, but attached-agent fleet warnings that are already
represented by member rows must not be duplicated onto the cluster parent.
Passive agent config or rollout handshakes whose only cause is a missing
comparable applied agent configuration fingerprint are not operator-actionable
setup attention and must not be counted as a visible infrastructure problem
unless a row or member still renders them as an actionable highlight.
The API-owned distinction is important: an unmanaged default agent config is
not a rollout at all, while a managed desired config that lacks an applied
fingerprint is rollout attention until the agent report proves convergence.
Configured-node settings tables follow the same boundary:
frontend-modern/src/components/Settings/ConfiguredNodeTables.tsx may own node,
credential, capability, status, and action cells, but table scroll framing must
stay on the shared Table primitive. If a configured-node table needs bounded
vertical height, apply it through Table wrapperClass instead of an outer
lifecycle-local scroll container.
Agent profile management tables follow that same presentation boundary:
frontend-modern/src/components/Settings/AgentProfilesPanel.tsx may own agent
profile and assignment columns, but embedded table framing must route through
PulseDataGrid's shared frame variants instead of lifecycle-local
overflow-x-auto or side-border wrappers.
Agent profile prompt and description textareas follow the same shared primitive
boundary. AgentProfilesPanel.tsx and SuggestProfileModal.tsx may own agent
profile lifecycle behavior and API mutations, but multi-line form controls in
those surfaces must compose FormTextarea instead of local raw <textarea>
wrappers.
Agent profile read-only state indicators follow the same presentation split:
AgentProfilesPanel.tsx may own lifecycle status labels and assignment state,
but badge tone, optional dot wiring, sizing, and shell classes must compose the
frontend-primitives-owned StatusIndicatorBadge instead of calling status tone
helpers or recreating badge spans inline.
Agent profile commercial gates follow the same primitive split:
AgentProfilesPanel.tsx and useAgentProfilesPanelState.ts may own lifecycle
entitlement checks, upgrade destinations, and label selection, but the
button-styled upgrade action must compose frontend-primitives'
UpgradeButtonLink instead of exporting or applying a lifecycle-local CTA
class helper.
Agent profile loading indicators follow that same split:
AgentProfilesPanel.tsx may own license-checking, profile-loading, and empty
profile semantics, but spinner shell, size, tone, and accessible status
behavior must compose frontend-primitives' LoadingSpinner instead of
lifecycle-local animate-spin border classes.
Agent profile empty states follow that same split:
AgentProfilesPanel.tsx may own profile and assignment empty-state copy and
lifecycle conditions, but compact empty-state spacing, icon treatment, and text
hierarchy must compose frontend-primitives' EmptyState variant="panel"
instead of lifecycle-local centered icon/text shells.
-
scripts/run-secure-runtime-rootful-qualification.shshared withdeployment-installability: the rootful runtime qualification wrapper is both an agent lifecycle typed-helper provider-boundary proof entry point and a deployment installability proof harness. -
scripts/run-secure-runtime-rootless-qualification.shshared withdeployment-installability: the rootless runtime qualification wrapper is both an agent lifecycle provider-boundary proof entry point and a deployment installability proof harness.
Extension Points
Safe-profile installation and migration invoke the private lifecycle commands
in cmd/pulse-agent/collector_lifecycle.go, backed by
internal/collectorlifecycle/, for collector authority reduction and
authoritative registration proof. These commands are installer-only lifecycle
boundaries: they read the bearer from one bounded, no-follow descriptor owned
by root or the configured collector UID; use system/custom CA trust or exact
DER pinning; reject redirects and ambient proxies; and permit plaintext only
for literal loopback hosts. Any new bearer-bearing collector transition must
extend this client and its installer negative tests instead of adding a curl
or generic insecure-transport path.
The authenticated runtime-display projection under shared internal/api/ may
carry the effective global disableDockerUpdateActions boolean so non-admin
viewers render container updates read-only. This is API/settings presentation
only: it neither disables image-update detection nor changes agent update,
registration, profile, command, enrollment, or fleet-control authority.
The Settings workspace mount in
frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts
still gates node, discovery, TrueNAS, and VMware loading on
infrastructureRead, but it must run the system-settings state initialization
for sessions without that capability too — that path degrades to the
viewer-safe runtime-display projection and carries no infrastructure,
connection, or agent-lifecycle reads, so skipping it only breaks read-only
presentation (the issue #1601 cadence misreport) without protecting anything.
Manual scoped Patrol work that reaches internal/api/ai_handlers.go (such as an
alert-initiated targeted Patrol check via POST /api/ai/patrol/run) is
investigation-only over agent-reporting resources: it must not alter agent
lifecycle, registration, token binding, reporting contracts, or install-command
identity. The scoped Patrol request carries resource identity only and reuses
the existing Patrol scoped engine, so it adds no agent lifecycle control
surface and no new internal/api/ lifecycle handler. AI settings projection
changes in internal/api/ai_handlers.go, such as the registry's suggested
Patrol quickstart model metadata on /api/settings/ai, are likewise
ai-runtime configuration surface: they must not become an agent lifecycle
install-guidance or discovery channel, and agent setup surfaces must not
consume them as lifecycle state.
Local subscription-agent settings in internal/api/ai_handlers.go follow the
same boundary. Opting into a locally authenticated Codex or Claude CLI selects
an ai-runtime transport only: it does not install, register, update, authorize,
or grant scopes to a Pulse agent. Pulse agent tokens and lifecycle credentials
must not be projected into the provider child process. Any future design that
distributes subscription CLI state or authentication through an agent requires
a separate agent-lifecycle contract change and verification.
Patrol investigation evidence copied by
internal/api/patrol_action_broker.go into an action's broker-owned origin is
API/AI audit provenance only. Tool-use evidence IDs do not identify an agent,
grant an agent scope, authorize a command channel, or replace live agent
readiness and dispatch admission. Any agent-mediated action still crosses the
existing lifecycle identity, authorization, and receipt boundaries.
First-run security discovery under shared internal/api/ is likewise an
adjacent API/security boundary, not an agent-lifecycle discovery surface.
Unauthenticated /api/security/status responses must stay on the public tier
and must not disclose bootstrap-token paths, container/runtime identity,
credential state, token diagnostics, or agent URL configuration. Agent setup
may consume those details only after the caller reaches the authenticated or
privileged security-status tier; it must not infer lifecycle readiness from
the intentionally sparse public response.
The authenticated POST /api/usage/workload-history intake is adjacent
product-adoption telemetry only. It accepts a closed content-free activity and
updates a bounded aggregate tally; it does not identify, register, authorize,
configure, update, or report a Pulse agent and must not consume agent tokens,
agent inventory, registration state, or command-channel readiness.
- Add or change install-command generation, canonical /api/auto-register behavior, or installer download behavior through the owned
internal/api/files above. Canonical/api/auto-registerauth is split by intent: when the setup-token bootstrap path succeeds, lifecycle clients must complete registration with the one-timeauthTokenonly and must not also send the long-livedX-API-Tokenheader. The header-backedagent:reportfallback exists only for update-only re-registration when setup-token fetch is unavailable. Canonical auto-register WebSocket semantics must distinguish first-time node creation from idempotent existing-node refreshes: only newly created PVE/PBS nodes may emitnode_auto_registered, while matched existing nodes that rotate or refresh credentials must emit a non-toast configuration refresh such asnodes_changed. Browser consumers must treatnode_auto_registeredas a real-time, timestamped lifecycle event rather than durable infrastructure state: fresh first-time events may show one success toast, but replayed, stale, or duplicate events must refresh configuration silently so an old registration cannot present as a newly connected node. The event payload must preserve the canonical registrationsource; script-initiated first-time registrations are the only source that may display the operator-facing success toast, while background agent registrations must update the ledger without claiming the operator just added that node. Lifecycle-adjacent handlers that refresh browser state after agent reports or registration changes may emit canonical lifecycle events when the event contract requires it, but routine state refresh is an API/monitoring-owned current-state invalidation signal. Those handlers must not build or retain full frontend state in the lifecycle boundary, and lifecycle consumers must not treat a coalesced websocket state refresh as install, registration, command, or update proof. Sharedinternal/api/session and auth changes consumed by lifecycle routes must preserve durable principal IDs as the authorization key. Agent lifecycle surfaces may display contact email when supplied by the shared auth boundary, but they must not reinterpret SSO or Stripe email as the canonical user identifier for setup, install, or fleet-management actions. Mobile onboarding payload sanitization ininternal/api/onboarding_handlers.goremains API/relay-mobile ownership: omitting a non-HTTPSinstance_urlfrom a QR/deep-link is a Pulse web handoff decision only and must not change install-command URLs, auto-register behavior, agent connection URLs, or lifecycle admission. Approved-action tool invocation parsing ininternal/api/router_routes_ai_relay.gois not an agent-lifecycle route grammar: lifecycle-adjacent setup or repair flows that execute governed Pulse tools must inheritinternal/agentcapabilities.ParseTextToolInvocationthrough the API relay instead of duplicatingpulse_*command-string parsing in lifecycle helpers. Approved-action replay through that same relay must also use the sharedinternal/agentcapabilitiesapproved-action argument helper, not lifecycle-local approval argument keys or pass-through maps. Native Assistant workflow-prompt rendering throughPOST /api/ai/workflow-prompts/renderis likewise AI-runtime/API-contract ownership even though it is wired through sharedinternal/api/files: lifecycle surfaces may benefit from the resulting Assistant prompt text, but they must not treat manifestworkflowPrompts, rendered prompt bodies, or Assistant starter availability as install, enrollment, or fleet-control route grammar. AI provider registry and/api/settings/aicredential-shape changes ininternal/api/ai_handlers.goare likewise adjacent AI-runtime/API-contract ownership. Agent lifecycle may depend on the shared authenticated API substrate staying intact, but provider ids, provider default endpoints, model routes, API-key fields, and provider configured state are not agent install, enrollment, reporting, or fleet-control lifecycle inputs. The adjacent workflow-starter activity markers onPOST /api/ai/workflow-prompts/activityandPOST /api/agent/workflow-prompt-activityfollow that same split: lifecycle surfaces may use the aggregate activation reports, but the content-free prompt-name/surface/timestamp marker is not agent enrollment, install readiness, fleet-control liveness, command authority, or agent update state. The first-partypulse_patrolstarter surface means a user entered the Pulse Intelligence operations-loop journey from app UX; it does not imply a host agent is installed, healthy, or authorized to run commands. The first-partypatrol_controlstarter surface and legacypatrol_autonomy/pulse_pro_activationaliases are paid Patrol control journey markers only; lifecycle code must not treat any of them as setup, install, fleet-health, or command-agent state. The adjacentGET /api/agent/patrol-control/statusprojection ininternal/api/agent_resource_context.gomay expose operator-readable Patrol status copy for native and external-agent orchestration, but lifecycle surfaces must not reinterpret that copy, its next action, or its completed/resolved Patrol outcome counts as install readiness, enrollment health, command authorization, or agent update state. Hosted Pulse Account may deep-link an MSP operator into a tenant workspace's agent-install surface through a signed local handoff target such as/settings/infrastructure?add=linux-host, but install-token generation, installer command construction, and first-agent onboarding remain agent-lifecycle/API-owned inside the tenant runtime. Control-plane handoff code must not mint lifecycle tokens or synthesize install commands for the tenant; it may only establish the tenant session and local destination. Availability-target API changes are adjacent but not lifecycle-owned: agentless MQTT, HTTP/S, ping, and TCP targets may appear in sharedinternal/api/handlers and resource projections, but their settings owner isfrontend-modern/src/components/Settings/AvailabilitySettingsPanel.tsxwithfrontend-modern/src/components/Settings/availabilitySettingsModel.ts. They must remain settings/API availability resources and must not create install commands, agent tokens, host uninstall actions, stop-monitoring lifecycle actions, or infrastructure add-flow states. Themachinetarget-kind label may make an agentless server, desktop, laptop, or Mac mini visible in Standalone Machines, but lifecycle code must still treat it as reachability evidence rather than an enrolled host. Global resource timeline API changes are likewise adjacent when they touch sharedinternal/api/route wiring:/api/resources/timelinemay expose monitoring-read provider activity for platform pages, but it must not create setup-token authority, install-command state, fleet command execution, or agent lifecycle enrollment semantics. Patrol autonomy API changes are likewise adjacent when they touch sharedinternal/api/route wiring: monitor-mode configuration and remediation entitlement payloads remain AI runtime/API-contract owned and must not create agent lifecycle authority, install-token scope, or fleet command semantics. Clearing stale Patrol full-mode unlock state during monitor-only saves is also an AI runtime entitlement clamp, not a lifecycle enrollment or command authority signal. Patrol readiness and settings-save payload changes on those shared handlers are also adjacent only: structured provider/model/tool causes may be exposed to Patrol and Assistant, but they do not grant agent install, enrollment, or fleet command authority. Assistant runtime identity strings exposed by those handlers must name the first-party surface as Pulse Assistant rather than the legacy genericPulse AI, and lifecycle consumers must not treat that label as agent registration, install progress, or command-agent readiness state. Diagnostics frominternal/api/diagnostics.gofollow the same adjacent boundary: lifecycle consumers may readassistantRuntimeConnected, but they must not reinterpret it as MCP transport state, agent registration, install progress, or command-agent readiness. Profile-suggestion availability errors consumed throughfrontend-modern/src/api/agentProfiles.tsare shared Pulse Intelligence availability guidance, not lifecycle registration state; the client must point operators at Assistant & Patrol settings instead of revivingPulse Assistant settingscopy. Hosted handoff subjects consumed through the shared API auth boundary must already be stable, non-email principals; lifecycle-adjacent routes must not recover authority from a blank handoff subject by falling back to contact email. The same rule applies to hosted public magic-link sessions consumed by lifecycle-adjacent routes: shared auth must mint a browser session only for a stored organization principal, not for a contact email on a blank owner/member row. Checkout webhook magic-link delivery follows that same shared-auth dependency: lifecycle-adjacent consumers may observe billing activation after server-owned org linkage, but they must not treat Stripe contact email as fleet authority unless the API-owned organization resolver maps it to a stored owner/member principal first. Runtime org authorization consumed by lifecycle-adjacent setup and fleet routes must use strictOwnerUserID/memberUserIDmembership checks; the API-owned email-aware organization helpers are migration/delivery helpers only and must not let contact email become setup, install, or fleet authority after a stable principal exists. API-token owner metadata follows the same rule: lifecycle-adjacent setup or mobile-pairing token flows may consume the shared token helper, but they must not pass extension metadata that authors or overwritesowner_user_id. Agent install command and deploy bootstrap/enrollment tokens must derive owner identity from the API-authenticated caller through the shared server-side token owner helper; bootstrap deploy metadata may bind cluster, job, target, and expected node, but not the human owner. Command-agent WebSocket identity follows the same lifecycle/auth split: install-command tokens that enable command execution may be minted before the final agent ID is known, but only Pulse-minted PVE/PBS or host install-command tokens may bind on first/api/agent/wsregistration. That first-use bind must persist the registering agent ID and hostname and become authoritative for later command registration attempts; generic unboundagent:exectokens are not lifecycle credentials and must fail closed. A pre-v6.1.1 hostname-bound record carrying a server-synthesized ID may migrate to the registering runtime ID exactly once; the versioned result must then require both fields. The live registry keys the session by organization plus agent ID, retains only non-secret token identity, isolates equal IDs across organizations, rejects same-organization hostname collisions, and revalidates token existence, expiry, scope, organization, and binding before reporting connectivity or dispatching work. Reconnect replacement and stale socket cleanup must use pointer identity so an old reader cannot remove the newly admitted session. The canonical durable-principal vocabulary for those shared auth routes is recorded indocs/release-control/v6/internal/IDENTITY_INVARIANTS.md; agent lifecycle work may consume that identity but must not define a parallel email-keyed actor model for fleet operations. SSO-authenticated lifecycle callers must already arrive with the API-owned provider-scoped principal; lifecycle code must not recover authority from SSO email or display claims. That same lifecycle-owned setup path also owns script teardown behavior: rerunning the governed setup script in remove mode must call canonical/api/auto-unregisterwithsource:"script"before local credentials are deleted, and the short recent-setup-token grace window must remain valid for that follow-up uninstall so operators can immediately back out a reviewed script-managed Proxmox setup without leaving a stale node record in Pulse. That same lifecycle-owned setup path also owns Proxmoxauthorized_keyssymlink preservation for temperature-monitoring SSH keys: generated PVE setup scripts must resolve the real authorized-keys target before filtering Pulse-managed# pulse-lines during install or removal. That same sharedinternal/api/adjacency does not transfer ownership of AI provider setup or mobile Patrol-provider bridging:internal/api/ai_handlers.goandinternal/api/chat_service_adapter.gomay be invoked by lifecycle-adjacent surfaces, but AI runtime auth and provider selection remainai-runtimeplusapi-contractsconcerns rather than install-token or lifecycle credential state. Legacy Anthropic OAuth fields exposed by AI settings are cleanup-only compatibility state: lifecycle-adjacent setup, install, profile, and relay flows must not treat stored OAuth tokens orauth_method=oauthas an agent enrollment, provider bootstrap, or AI provider-readiness signal. API-facing Assistant chat tool calls projected throughinternal/api/chat_service_adapter.gomust stay on the sharedagentcapabilitiesprovider-call shape; lifecycle consumers must not treat Assistant transcript tool-call IDs, inputs, output, success flags, or provider continuation metadata as enrollment, assignment, installer, or fleet-command evidence. Lifecycle flows must not recreate the retired Patrol quickstart bootstrap path, mint server-issued hosted-model tokens, or derive AI provider state from installation identity. Per-request/api/ai/chatexecution-mode overrides follow that same boundary: lifecycle-adjacent consumers may rely on Assistant approval semantics, but scopedautonomous_mode:falsechat requests must not be reinterpreted as agent registration, assignment, installer, or connection lifecycle state. Patrol finding handoffs that force approval-required mode from a non-emptyfinding_idare likewise AI/runtime governance, not an agent command grant or lifecycle authorization primitive. Patrol queued-fix approvals may now seed the shared action-audit store with planned and pending lifecycle evidence, but lifecycle surfaces must treat those records and their persisted requester identity as action-governance context only. Backend-refreshed Assistant handoffs may also carry that requester identity as provenance, but it still does not create install, enrollment, assignment, or fleet-command authority. Lifecycle flows must not reintroduce anonymous bootstrap identity, tenant-local commercial-owner surrogates, or fake activation records when they traverse those shared handlers. They also must not infer tenant creation, email issuance, or public-route availability from/api/public/signupresponse codes or payload fields just because that commercial route lives under the sharedinternal/api/tree. That same retired quickstart boundary is vendor-neutral at the lifecycle edge too: lifecycle-adjacent consumers may observe oldquickstart:*values only as compatibility data being cleared by shared settings helpers. They must not bake vendor model IDs or provider-model fallback rules into install or activation flows just because those routes share the backend API tree. Lifecycle-adjacent resource reads that traverseinternal/api/resourceapi/resources.gomust also preserve the canonical unified-resourcename -> type -> idorder instead of inheriting map order or page-local re-sorts, so install and runtime hydration do not present one resource ordering at first load and a different ordering after the first live refresh. Those same resource-list reads must preserve browser-encoded comma-separated/api/resourcestype filters. Lifecycle-adjacent consumers that request agent or Docker host resource rows through standard query builders must receive the same canonical resource set as literal comma filters instead of treating%2Cas part of a resource type token. Those lifecycle-adjacent reads may observe resource APIpolicyPostureaggregation as read-only data-governance context, but they must not reinterpret sensitivity, routing, or redaction counts as install capacity, registration eligibility, or agent assignment state. Relay mobile credential issuance is not an agent bootstrap or lifecycle repair path just because it lives under sharedinternal/api/routing.POST /api/security/tokens/relay-mobilemust remain API/security owned, must require the paidrelayentitlement before token minting, and must not be reused by installer, auto-registration, assignment, or repair flows as an alternate setup credential. When lifecycle surfaces also hydrate from/api/state, that first-session snapshot must carry the same canonical resource types and display names as/api/resourcesinstead of briefly showing legacy host aliases before the first websocket-backed refresh lands. Chart-adjacent lifecycle reads in sharedinternal/api/chartapi/service.gomust obey that same mock-aware unified snapshot boundary: demo/api/chartsand/api/charts/infrastructurepayloads may not bypassGetUnifiedReadStateOrSnapshot()and silently drop VMware-backed host rows that the canonical mock estate already published through/api/stateand/api/resources. Persisted legacy hosted quickstart model IDs are therefore not lifecycle truth either: when shared settings helpers load or save historical quickstart values, they must clear them before adjacent install or activation flows read the payload. Tenant-local lifecycle routes may reuse shared installation activation or effective entitlement billing state for their own governed purposes, but they must not fork per-org activation caches, alternate installation-token stores, synthetic entitlement mirrors, or competing AI-provider identity. That same shared AI/runtime boundary owns Patrol execution identity: lifecycle-adjacent flows may trigger or observe Patrol runs throughinternal/api/chat_service_adapter.go, but they must preserve the stable execution identifier that describes one higher-level Patrol run across agentic turns. - Add or change update continuity and persisted-version handoff through
internal/agentupdate/. - Add or change runtime-side Unified Agent startup, first-report assembly, and enroll/runtime continuity through
internal/hostagent/. Local real-lab journey harnesses underscripts/intelligence_lab/belong to this lifecycle boundary. They must be explicitly authorized, use ignored artifacts, bind every mutation to an exact disposable run label, preserve pre-existing resources, redact bounded evidence, prove idempotent cleanup, and distinguish lab compensation from product rollback. The artifact-backed current-build browser contract for the Docker restart journey istests/integration/tests/84-docker-restart-real-lab-artifact.spec.ts. It may render only the strict redacted projection produced by the matching managed-runtime run and must reject missing or mismatched proposal, action, attempt, receipt, finding, and evidence identities. Unified Agent registration separately advertises the versioned, read-only Docker observation protocol.docker_container_observeis not an action, preflight grant, or durable operation: it accepts only an action-bound digest, runtime enum, and immutable container ID, invokes the fixed local Docker/Podman lifecycle inspector, and returns either one bounded snapshot or a typed inconclusive reason. It must never enter the operation receipt store, execute a mutation, accept command text, or infer action truth. The server validates freshness and correlation and owns all evidence-class and verification projection. Agents advertising a protocol version below 2 fail closed without receiving an unsupported request. Observation protocol version 2 also requires an explicit bounded daemon health state (none,starting,healthy, orunhealthy) in every successful snapshot.nonemeans the daemon confirms that no health check exists; omission is unknown, never an alias fornone. Typed start and restart execution waits within the existing action timeout while a running container remainsstartingorunhealthy, performs no second mutation, and completes only after the daemon reportshealthyor explicitnone. Timeout, persistent unhealthy state, unsupported health vocabulary, or a missing health fact remains contradicted or inconclusive rather than manufacturing a successful postcondition from running state alone. RG-06 has a separate mutation harness atscripts/intelligence_lab/patrol_autonomy_colima.pyandinternal/api/patrol_autonomy_colima_real_lab_test.go. It must run from a clean archive of the audited SHA, build the pinnedcmd/pulse-agent, and execute both the agent and certification test from that same extracted source directory with an artifact SHA binding. It must refuse to pull or mutate a non-disposable image. The disposable Debian agent creates bounded APT cache pressure, reports through the production WebSocket, and is converted withunifiedresources.HostIngestRecord; the productionclean_package_cachecapability andhostStorageCleanupActionExecutortherefore remain the sole eligibility and execution path. The proof binds every action to the persisted current-version human acknowledgement and activation, records server actor/org/resource/ capability and policy-plan bindings, proves one command, one transport attempt, one receipt, independent cache before/after readback, terminal ActionResultV2 truth, and finding reconciliation. Each revoked, downgraded, emergency-stop, stale-resource, and Never barrier first creates a valid finding/investigation and measures cache/daemon state, agent command count, transport attempt/receipt, audit/event state, authority/config digest, and finding resolution before and after, including cache bytes/fingerprints and container identity/running/start state. Every barrier must produce the expected canonical refusal reason and never reach completed state. Negative triple-zero means measured unauthorized mutation, transport dispatch, and authority writes are all zero; expected lifecycle refusal event deltas are recorded separately and do not get mislabelled as zero activity. The one authorized dispatch is recorded separately and never disguised as triple-zero evidence. Proxmox host-agent setup must treat localproxmox-registeredmarkers as a cache, not authority: before skipping token setup or node repair,internal/hostagent/proxmox_setup.gomust revalidate the current type and candidate hosts against Pulse through the canonical auto-register contract. Runtime-side PVE token setup must also keep the same permission shape as the generated setup script: Pulse-managed PVE monitor tokens are privilege-separated, andPVEAuditor, optionalPulseMonitor, plus/storagePVEDatastoreAdmingrants must be mirrored to bothpulse-monitor@pveand the concretepulse-monitor@pve!pulse-*token id. Runtime startup must preserve the token-optional path for already-installed non-enrollment agents; only enrollment mode may fail CLI configuration loading solely because no API token was supplied. That runtime-side ownership includes local disk telemetry collection ininternal/hostagent/smartctl.go. Linux SMART discovery must prefer non-openingsmartctl --scantyped targets before generic block-device fallback so controller-backed disks keep their canonical SMART and wearout coverage without opening every enumerated disk during discovery. Direct Linux SATA/SAT-style block devices that return health but no temperature through smartctl auto-detection may retry explicit-d satbefore settling on a no-temperature result. The runtime must not infer-d scsifrom ansdXbasename: direct ATA may use SAT, direct SAS may use SCSI only from native/sysfs/typed-scan evidence, USB bridges keep their explicit scan mode, and multiplexed HBA members keep their exact controller target without an untyped fallback. Those retries must accumulate model, serial, WWN, health, temperature, and SMART attributes across attempts. A later healthy result must not erase an earlier explicit failure, and an omitted NVMe counter must stay absent while a reported zero remains present through report serialization. Linux/devand/sys/blockidentity parsing must use slash-based device semantics independent of the Go build host, so the Windows native CI lane proves the same SSD standby-guard decisions as Linux instead of interpreting Unix device paths with Windows path separators. FreeBSD SMART probing must retry through the canonical typed and untyped device modes and the SCT temperature status path before settling on standby or no-data results, and partial or plain-text smartctl output must still preserve model, serial, health, and temperature data through the same host-agent runtime boundary instead of leaving monitoring to guess. Vendor smartctl compatibility is proved ininternal/hostagent/smartctl_standby_guard_test.go: a JSON-option rejection retries in text mode, DSM/dev/sataNgains a SAT attempt, an explicit binary path bypasses PATH lookup,STANDBY (OS)is recognized, and stderr survives command failure wrapping. macOS thermal telemetry belongs in the same host-agent sensor contract, but it must report Darwinpmsetthermal and performance pressure assensors.thermalStateinstead of inventing Celsius readings from unavailable Apple silicon sensor values. Linux and Windows NVIDIA GPU telemetry belongs in that same host-agent sensor contract: the runtime may use a boundednvidia-smiquery for direct GPU temperature, utilization, and VRAM readings. Linux may supplementlm-sensorswith that query or use it as a best-effort fallback whenlm-sensorsis unavailable; Windows may use it as its supported direct-GPU provider without implying support for CPU or motherboard temperatures. Windows physical-disk temperature is a separate best-effort native source:internal/hostagent/windows_storage_sensors.gomay issue one fixed, non-interactive, five-second Windows Storage-module query for at most 128 physical disks and map only device-reported reliability temperatures fromGet-StorageReliabilityCounterinto the existingsensors.smartphysical disk contract. Its JSON output, device identities, model labels, and temperatures must be bounded and validated; absent cmdlets, missing counters, malformed output, and unsupported devices omit that telemetry without failing the host report. This path must not use ACPI thermal zones, infer disk health, execute server-authored PowerShell, or imply support for CPU or motherboard temperatures. Windows CPU and motherboard Celsius readings may come only from the explicitly qualified, driver-backed LibreHardwareMonitor path ininternal/hostagent/windows_librehardwaremonitor.go. Production collection must issue one unauthenticated two-second GET to the fixed loopbackhttp://127.0.0.1:8085/data.jsonendpoint, bypass proxies, reject redirects and non-success responses, and cap the body at one MiB. Parsing must bound node count, tree depth, sensor count, and key length; accept only finite direct Celsius values in the supported range from CPU or motherboard hardware identifiers; and ignore GPU, storage, unknown, malformed, or unclassified nodes. An absent, stopped, authenticated, or incompatible helper must omit these readings without failing the host report. The agent must not poll a server-authored address, accept ACPI thermal zones as a substitute, expose LibreHardwareMonitor lifecycle or hardware-control authority, or displace the native Windows Storage and NVIDIA providers. The report must keep typed GPU readings insensors.gpuwhile mapping only directtemperature.gpureadings into existingsensors.temperatureCelsiusgpu_nvidia_<index>keys for compatibility. Linux libvirt inventory belongs to the host-agent report as a read-only, auto-detected child workload source.internal/hostagent/libvirt.gomay locate only the localvirshexecutable and use its global--readonlymode withlist --all --namefollowed by onedomstats --raw --nowaitquery over at most 128 validated domain names. The collector has one eight-second deadline, bounded list/stat output, fixed state/CPU/balloon/ interface/block field selection, and no server-authored URI, domain, or command input. Missing clients, inaccessible sockets, unsupported drivers, partial statistics, or parse failures omit the inventory without failing host telemetry. Bounded command output is an optional collector capability rather than a newSystemCollectorinterface requirement, preserving compatibility with existing injected collectors; the production collector enforces the cap during execution and fallback collectors are rejected after the same size limit. Authenticated ingest derives CPU and I/O rates only after two accepted samples and projects the domains as provider-neutraltechnology=libvirtVM resources parented to the reporting host. It preserves the last successful collection timestamp across a failed query so the canonical source-staleness policy degrades the rows instead of making every guest disappear, while a successful empty inventory removes them. This path does not grant lifecycle, console, XML, snapshot, migration, or storage mutation authority. XCP-ng inventory follows the same observation-only boundary. On Linux,internal/hostagent/xcpng.gomay auto-detect only the localxeclient and issue fixed, boundedpool-list,host-list, andvm-listrequests under one eight-second deadline. The report may carry one pool identity, the reporting host's XCP UUID, and at most 1024 non-template, non-control-domain VMs with UUID, power state, vCPU, configured/actual memory, and resident host UUID. No server-authored xe argument, credential, lifecycle call, or Xen Orchestra authority enters this path. Missing or invalid output omits the new sample without failing normal host telemetry. Authenticated server ingest may reduce those typed samples into bounded host-level GPU utilization, VRAM pressure, and GPU temperature history on the existing agent identity, but that monitoring projection does not change the report or command protocol. Neither side may infer lifecycle health, command authority, enrollment state, or GPU workload/process inventory fromnvidia-smioutput. Site-defined numeric sensors belong in the typedsensors.customreport contract rather than the temperature-orientedsensors.additionalmap. Their execution authority is local-only: the agent may load a private, versioned YAML file named by--custom-sensors-fileorPULSE_CUSTOM_SENSORS_FILE, but server profiles, remote configuration, and command transport must not author executable paths or arguments. The runtime must reject shell strings and relative, symlinked, or insecure executable/config paths; enforce bounded sensor count, intervals, timeouts, output, and concurrency; revalidate executables before use; and accept only one finite numeric value per execution. Typed identity, unit, status, observation time, stale last-good value, and bounded error state must survive report ingest, model/front-end conversion, unified-resource cloning, and alert cleanup. Threshold evaluation remains agent-authored, while the server maps warning, critical, and opted-in error states into canonical alerts. Enabling custom sensors must not implyagent:exec, AI command authority, or any general remote-shell capability. Runtime RAID collection uses/proc/mdstatas the canonical discovery baseline for Linux md arrays.mdadm --detailmay enrich level, state, member, UUID, and rebuild fields when available, but missing or failing mdadm detail probes must not hide a kernel-reported md array from the unified agent report. Vendor fixed-width role maps must not be treated as missing members: a QNAP system RAID1 array that reports[24/2]while both active members occupy roles 24 and 25 has two configured members, not 22 failed disks; ordinary in-range degraded maps such as[4/3] [UUU_]retain their missing-member semantics (#1688). Server-side lifecycle admission must preserve that same continuity across Pulse restart and upgrade. When a standalone host comes back with the same durable machine/report identity and token continuity, the shared admission path must treat it as the existing system until live inventory rebuild catches up, rather than as a fresh enrolment competing for another monitored-system slot. - Keep shared agent-side TLS identity fail-closed across
cmd/pulse-agent/main.go,internal/hostagent/,internal/agentupdate/,internal/remoteconfig/client.go, andinternal/agenttls/config.go. Self-signed deployments may use a canonical pinned Pulse server certificate fingerprint, but lifecycle transport must route that pin through reporting, enrollment, command websocket, remote-config, and self-update clients instead of wideningPULSE_INSECURE_SKIP_VERIFYinto a blanket MITM carve-out. A configured custom CA bundle is part of that same trust boundary: if the bundle is unreadable or invalid, lifecycle transport must refuse the connection path rather than silently downgrading back to system roots. - Keep release-grade updater trust fail-closed across
internal/agentupdate/,internal/dockeragent/, and the sharedinternal/api/unified_agent.godownload helpers. When release builds embed trusted update signing keys, published agent binaries and installer assets must carry detached.sigplus.sshsigsidecars; updater/runtime paths must requireX-Signature-Ed25519in addition toX-Checksum-Sha256, and installer-owned download flows must require the matching base64-encodedX-Signature-SSHSIG, instead of silently downgrading to checksum-only trust. - Keep shared
internal/api/helper edits isolated from agent lifecycle semantics: Patrol-specific status transport or alert-trigger wiring changes in shared handlers must not bleed into auto-register, installer, or fleet-control behavior unless this contract moves in the same slice. The same isolation rule applies to retained Patrol objective routes underinternal/api/: objective briefs, resource scope, optimistic revisions, derived observer coverage, and observer-health state are AI/runtime plus API-contract facts. Agent lifecycle surfaces must not reinterpret an objective or observer record as agent enrollment, install state, command connectivity, profile assignment, update readiness, setup-token scope, or infrastructure mutation authority. Commercial migration startup behavior ininternal/api/licensing_handlers.goandinternal/api/licensing_bridge.goremains a cloud-paid/API concern. Suppressing synthetic mock-license exchange must not change agent enrollment, reporting credentials, install commands, update continuity, or fleet-control authority. Audit-log list, filter, pagination, and storage-error contract changes ininternal/api/activity_audit_handlers.goremain security/API behavior. They must not change agent enrollment, reporting, command transport, lifecycle event creation, or fleet-control authority merely because the handler shares theinternal/api/package. SSO browser-session display labels in shared auth/session helpers are likewise API/security presentation state, not lifecycle identity. Agent enrollment, installer reruns, update recovery, token binding, and fleet control must continue to derive authority from agent tokens, setup tokens, machine/report identity, and configured lifecycle state rather than fromssoSessionDisplayNameor other mutable SSO claim labels. The same isolation rule applies to AI settings payload work ininternal/api/ai_handlers.go: provider auth fields, masked-secret echoes, provider-test model selection, and legacy Anthropic OAuth cleanup fields remain AI/runtime plus API-contract ownership and must not be reinterpreted as lifecycle setup, provider activation, or registration semantics just because they share backend helper layers. Patrol readiness labels on the same settings payload, including the user-facing Patrol control label for the stableconfigurationcheck ID, remain AI/runtime plus API-contract wording and must not be treated as agent lifecycle configuration, install readiness, registration state, or fleet control setup. Governed AI action-target normalization ininternal/api/ai_handlers.goandinternal/api/ai_resource_types.gofollows that same split: Assistant approval routing may map resource types to action targets, but lifecycle setup, enrollment, installer, and command websocket target semantics remain owned by the lifecycle contracts above. The same isolation rule applies to Pulse Assistant chat SSE progress ininternal/api/ai_handler.go: neutralworkflow_statetransport liveness such asstream_idle, provider startup, retry, fallback, and model-thinking status remains AI/runtime plus API-contract ownership and must not be reinterpreted as agent enrollment, install progress, command websocket liveness, or fleet-control freshness. Native Assistant workflow-prompt rendering throughPOST /api/ai/workflow-prompts/renderfollows the same split: manifest-owned prompt names, argument validation, and rendered starter text remain AI/runtime plus API-contract ownership and must not become lifecycle setup, enrollment, installer, or fleet-control state just because the route lives ininternal/api/ai_handler.go. Workflow-starter activity telemetry recorded after successful Assistant prompt rendering, first-party Patrol activation handoff, Pro activation handoff, or Pulse MCP prompt rendering follows the same ownership split. The marker may contribute to Pulse Intelligence activation reporting, but lifecycle code must not treat it as proof of installed agents, reported hosts, command connectivity, stale-agent freshness, or fleet-operation capability. The same isolation rule applies to report branding validation and rendering request assembly ininternal/api/system_settings.goandinternal/api/metrics_reporting_handlers.go: lifecycle-owned install, enrollment, and reporting freshness flows may coexist with generated reports, but workspace logo material remains API/security/reporting ownership and must not become agent credential, install-token, or fleet lifecycle state. The same isolation rule applies to Patrol investigation-record propagation through shared AI intelligence handlers andinternal/api/router.go: lifecycle surfaces may observe the resulting resource context, but they must not reinterpretinvestigation_recordas agent enrollment, installer, command policy, or fleet-control authority. The same isolation rule applies to operator-facing finding fields likeimpactandrecommendationpropagated through that router conversion: lifecycle surfaces may render them in finding context but must not treat them as enrollment, installer, or fleet-control authority for the named resource. The same scope applies to theprevious_resolved_fix_summaryoperational-memory field carried on findings across regressions: lifecycle surfaces may render it in finding context but must not replay the recorded fix description as an installer command, enrollment action, or fleet-control instruction. The same scope applies to thetrustblock on the patrol-status response (FindingsTrustSummary): lifecycle surfaces may read it as adjacent operator context but must not derive enrollment, installer, or fleet-control authority from any of its counters. Assistant chat enrichment fromfinding_idfollows the same rule: lifecycle-owned command execution and agent auto-approval policy stay canonical in the agent/runtime owners, not in Patrol investigation-record prompt text. Model-only Assistant handoff context for a Patrol finding, including same-session metadata retained for follow-up turns and any resolved-resource scope hydrated from that finding, is also not agent lifecycle state and must not be used as enrollment evidence, command-websocket identity, or installer authority. If Assistant stores the originating finding ID to refresh the current unified finding and investigation record on follow-up turns, that stored reference remains an AI/runtime context selector and still cannot become agent enrollment, lifecycle, or command-websocket authority. Clearing that stored handoff when the finding no longer resolves is also AI/runtime invalidation, not an agent lifecycle transition. Unified finding lifecycle facts, latest lifecycle event briefing lines, and detailed lifecycle context included in Assistant handoff context remain Patrol/AI review metadata and must not become agent lifecycle state or fleet-control authority. Primary finding recency, evidence, verification, and governed action artifact facts in the finding briefing and related root-cause or correlated finding summaries resolved for the same Assistant handoff follow that same isolation rule, including any recency or latest lifecycle facts attached to those related summaries: they may explain Patrol context to the model and seed model-only handoff resources, but they are not enrollment evidence, command-websocket identity, agent command grants, or auto-approval policy. Structured Assistant handoff action references from the same Patrol finding remain AI/runtime review metadata only; lifecycle code must not treat approval IDs, action IDs, fix IDs, action lifecycle state, risk, or target-resource labels from that handoff as agent command grants, enrollment proof, or auto-approval policy. Any approval or action-audit status snapshot refreshed for Assistant review remains read-only AI/runtime context and must not become command-websocket authority or lifecycle execution state. Patrol runhandoff_metadataretained for saved Assistant sessions follows the same isolation rule: a run ID, run type/status, runtime-failure flag, or scoped resource label may restore Assistant presentation, but it must not become agent enrollment evidence, installer state, command-websocket identity, or fleet-control authority. Assistant finding-briefing action metadata derived from those same recovered handoff action references stays under that review-only boundary; lifecycle surfaces must not reinterpret clearer approval metadata as install, enrollment, or command-execution authority. The same isolation rule applies to CSRF token-store behavior ininternal/api/csrf_store.go: lifecycle-adjacent browser flows may rely on the shared API/security layer to keep parallel replacement-token retries valid for one authenticated session, but retained CSRF hashes are not install tokens, setup-token state, enrollment authority, or agent credential continuity. The same isolation rule applies to the Pulse Intelligence agent capability manifest ininternal/agentcapabilities/manifest.go: lifecycle surfaces may consume infrastructure setup routes advertised there, but manifest governance fields, MCP tool projection, shared external-tool projection helpers, shared schema-envelope helpers, and external-agent typed argument schemas remain API/AI-owned contract state rather than agent enrollment or installer semantics. Native Pulse Assistant provider seams and native tool-adapter names in sharedinternal/api/ai_handler.go,internal/api/ai_handlers.go,internal/api/agent_profiles_tools.go,internal/api/router.go, andinternal/api/router_routes_ai_relay.gofollow that same isolation rule:MCPremains an external protocol, manifest, and wire-schema term, while the in-app Assistant tool family is AI/runtime plus API-contract state. Lifecycle consumers must not infer enrollment, installer, command-websocket, or fleet-control authority from those native Assistant tool names, and must not reintroduce MCP-named native adapters to express lifecycle ownership. Pulse Intelligence operations-loop external-agent readiness ininternal/api/agent_resource_context.gofollows that same shared-helper split: the Pulse MCP token/scope check is AI/runtime plus API-contract ownership and must not become agent enrollment, installer, fleet-control, or command-websocket readiness. Patrol control completed/resolved proof andpatrolControlValueStatefollow the sharedinternal/telemetrycount-only classifier even when they are surfaced byinternal/api/agent_resource_context.go; legacypatrolAutonomy*fields mirror those values only for compatibility, and lifecycle surfaces may observe them as operations-orientation evidence only.proActivationOperationsLoopStarterCountis only legacy entry-point orientation, and the legacy completed/resolved/valueproActivation*status fields mirror Patrol control values as compatibility aliases, not as a second lifecycle signal. MCP readiness remains a separate external-agent handoff signal and is not a Patrol control proof input, agent install, registration, update, profile, command, or fleet-control completion. Approved Assistant tool execution exposed through the same sharedinternal/api/router_routes_ai_relay.goextension point follows that native naming rule:AssistantToolExecutor/ApprovedAssistantToolExecutoris the only current cross-repo fix-execution dependency. MCP remains an external protocol adapter term and must not reappear as a lifecycle command ownership or approved-fix execution dependency. The same shared-helper rule now covers SSO outbound discovery and metadata fetches plus credential-file loads ininternal/api/sso_outbound.go,internal/api/saml_service.go, andinternal/api/oidc_service.go: lifecycle-adjacent setup or auth work may depend on that shared trust boundary, but it must not fork a second HTTP client, redirect policy, file-read rule, or SSO entitlement interpretation inside lifecycle-local flows. SAML and multi-provider SSO availability remains API/security-owned Community-tier behavior, not an agent lifecycle setup gate. The same shared-helper rule also covers organization membership and cross-organization sharing transport ininternal/api/org_handlers.goplus adjacent route wiring. Lifecycle surfaces may coexist on the same settings shell, but they must not reinterpret pending-versus-accepted org-share state as install authority, monitored-system admission truth, or enroll-time access control; target-org approval remains organization-settings plus security/privacy ownership even when the implementation moves through the shared backend API tree. - Keep legacy Unified Agent compatibility names explicitly secondary when touching shared
internal/api/runtime helpers: the legacy host-route family andhost-agent:*scope names may remain as ingress or migration aliases, but they must not retake primary ownership in router state, live runtime scope checks, handler commentary, or operator-facing guidance. - Add or change the unified agent CLI entrypoint, version/help exit semantics, or startup argument/error routing through
cmd/pulse-agent/main.go. The CLI entrypoint owns propagation of persistence context into runtime-owned helpers. When installer-selected state roots differ from the default,cmd/pulse-agent/main.gomust pass that exactStateDirthrough both the host-agent runtime and updater startup paths instead of letting one path silently fall back to/var/lib/pulse-agent. Pending helper-update startup must bind the executing collector through the bounded/proc/self/exeSHA-256 digest before acting on the durable handoff. A process running the pending active digest may continue toward report-gated commit. A process already running the recorded rollback digest must acknowledge the helper's idempotent terminal rollback and clear the handoff immediately instead of retrying commit until the old deadline. A digest matching neither identity fails closed without committing, rolling back, or deleting the recovery handoff. The same runtime-owned boundary also owns Pulse control-plane URL validation for agent startup, remote config, updater continuity, and command transport. Public control-plane hostnames remain HTTPS/WSS, but self-hosted local control planes may use plain HTTP/WS when the host is loopback, a private, link-local, or carrier-grade NAT IP, a single-label LAN name, or a local DNS suffix such as.local,.lan,.home,.home.arpa, or.internal; installer-persisted local HTTP URLs must not be accepted by one runtime path and rejected by another. Plaintext to a host that does not look local is available only as an explicit operator override: the--allow-plaintext-httpflag (PULSE_AGENT_ALLOW_PLAINTEXT_HTTP) records process-wide consent throughsecurityutil.SetOperatorPlaintextHTTPConsentbefore any module validates a URL, applies uniformly to every agent transport (HTTP and WS), warns at startup that the API token travels in cleartext, defaults closed, and is never emitted by generated install commands. It exists for self-hosted networks numbered from nominally public IP space; the Pulse server never sets it. Bearer-bearing collector HTTP requests never inherit an ambient proxy for a plaintext destination, including an explicitly allowed local/private or operator-consented URL. They connect directly to the selected destination soHTTP_PROXYcannot silently receive the cleartext credential. HTTPS destinations may continue to use the operator's proxy environment because the bearer stays inside the authenticated TLS tunnel; redirects remain forbidden in both cases. The unified agent CLI copy follows the same command-execution vocabulary as the install surface.cmd/pulse-agent/main.gomay keep the--enable-commandsflag name for compatibility, but the help text and inline comments must describe command execution as Pulse command execution for Patrol actions and governed Proxmox LXC Docker inventory rather than reviving AI auto-fix language. The--disk-excludehelp text must describe device names or paths and mount-point patterns, and the repeatable flag must append values rather than replacing exclusions already supplied by the environment. The unified agent CLI copy also owns operator-facing Docker / Podman runtime labels.cmd/pulse-agent/main.gomay keep the historical--enable-dockerand--docker-runtimeflag names for compatibility, but help text and inline comments must describe the module and runtime as Docker / Podman rather than exposing the generic container-runtime family label. The--enable-dockerhelp text must use the operator-facing "Enable Docker / Podman Agent module" wording instead of leaking the implementation-level collection-module name. The CLI entrypoint also owns the local Docker / Podman privacy opt-out: when--enable-docker=falseorPULSE_ENABLE_DOCKER=falseis set on the host, auto-detection and remote config must not start the Docker / Podman module. - Add or change installer flags, persisted service arguments, or upgrade-safe re-entry behavior through
scripts/install.shandscripts/install.ps1. On Unix systemd hosts,scripts/install.shmust keep the default agent unit hardened, but command-enabled Proxmox VE agents selected with--enable-commands --enable-proxmoxand typepveorallmust explicitly allow the minimumpct exec/lxc-attachservice posture by settingNoNewPrivileges=falseandRestrictSUIDSGID=false. That exception is limited to the PVE command-agent path; it must not become the default for Docker / Podman, host-only, PBS-only, or ordinary non-command agents. The narrowerAmbientCapabilities=CAP_SETUID CAP_SETGIDgrant is not bound to the install-time flag and must be written for every--enable-proxmoxagent of typepveorall, with or without--enable-commands. Command execution is also enabled from the server at runtime, whereapplyRemoteConfigstarts the command client without regenerating the unit, so an agent provisioned only for the install-time case would accept commands it cannot use:lxc-attachneedsCAP_SETUIDto write/proc/<pid>/uid_mapfor unprivileged guests, and without it the Docker socket probe fails withwrite_id_mapping: 61 Operation not permittedfor every unprivileged LXC. The monitoring server must surface that failure mode instead of hiding it: the first probe failure of a streak, and the point where the retry backoff reaches its cap, log at warn with the guest, node, and error, and failed probes retry with exponential backoff (one minute doubling to a thirty-minute ceiling) instead of re-runningpct execon every poll. Reconfiguring the checker resets the backoff so command execution enabled at runtime retries immediately. A warn log is diagnosis, not discovery, so the capability must still be provisioned up front rather than diagnosed later. ThePULSE_PROXMOX_GUEST_DOCKER_INVENTORY_VMIDSallowlist must gate the socket probe as well as inventory collection, so opting into specific guests never leaves every other guest probed each cycle. Persistence-sensitive NAS targets must keep one canonical continuity model here: installer-owned bootstraps may use flash-backed or immutable-root launch hooks only as thin trampolines, while the durable wrapper, state, and reboot-surviving binary copy stay in the governed persistent state directory that updater continuity also refreshes. Unix--updatere-entry must also preserve lifecycle identity for legacy v5.1.x agents that do not yet have v6connection.envstate. When a runningpulse-agentprocess, its systemd unit, or its FreeBSD rc.d service script already carries the Pulse URL, token, feature flags, agent id, hostname, or trust posture, the shell installer may recover those values for the update handoff, but the resulting v6 service must be rendered through the shared exec-argument builder and migrate the token into the installer-owned--token-filepath. Approval-gated command execution must expose stable rejection reasons for invalid approval grants so fleet operators can distinguish missing, expired, mismatched, and signature-invalid grants through agent metrics. Interactive action clients may additionally present the reviewedplanHashon decision and execution requests. The sharedinternal/api/actions.goboundary must reject a mismatch before it records approval state or dispatches to an agent; lifecycle consumers must never reinterpret a stale client plan as command authority merely because the action id is still valid. The server must not mint that grant from a nonempty approval id alone.internal/agentexecaccepts approval-gated arbitrary commands only with a non-serializable server-owned authorization context and a verifier that consumes an org/action/command/target-bound approval before signing. A missing, wrong-org, wrong-action, expired, or already-consumed approval stops before grant minting and before any WebSocket command frame. - Preserve canonical token-lifecycle reads in shared
internal/api/auth/security helpers so lifecycle-adjacent setup and install flows do not revoke a displayed relay pairing token afterlastUsedAtproves that an already paired device is actively depending on that credential. - Preserve backend-owned Pulse Mobile relay runtime credential minting in those same shared
internal/api/auth/security helpers so lifecycle-adjacent setup and install flows reuse the canonical mobile token route instead of reintroducing wildcard or browser-authored runtime token bundles. - Preserve the dedicated backend-owned
relay:mobile:accesscapability and its governed backward-compatible route inventory plus the shared helper call sites around it, so lifecycle-adjacent setup and install flows do not widen the mobile device credential back into general AI chat/execute scope ownership. Shared proxy-auth admin evaluation ininternal/api/auth.goremains an API/security trust boundary, not an agent lifecycle enrollment shortcut: lifecycle-adjacent setup and install surfaces may rely on the authenticated user identity passed through that helper, but they must not treat a missing configured role header as administrator proof. - Preserve shipped security-doc guidance in shared lifecycle setup helpers so
internal/api/configapi/config_setup_handlers.goand adjacent install/setup runtime paths point operators at the running build's local security documentation route rather than GitHubmainlinks. - Keep shared
internal/api/chartapi/service.goworkload-chart downsampling presentation-only: when that service caps mixed-cadence workload history into equal-time buckets for operator-facing cards, lifecycle-adjacent setup and fleet surfaces must not reuse the shaped chart samples as heartbeat, enrollment, or last-seen authority. That same presentation-only boundary must preserve canonical millisecond timestamps when it serializes chart points, so lifecycle-adjacent first-host and fleet surfaces do not misread rounded chart samples as duplicate or restarted heartbeat evidence. The chart service's shared payload cache may sweep expired query variants and evict payloads at its process-local retention bounds, but cache residency, eviction, or an oversized response bypassing retention must not become agent connectedness, heartbeat, enrollment, or fleet freshness evidence. The same rule now applies to storage summary interaction. Shared sticky-card or row-hover focus behavior on infrastructure, workloads, and storage may reuse the canonical chart transport, but lifecycle-adjacent install, enrollment, and fleet surfaces must not treat highlighted summary series or sticky-shell state as agent freshness or setup progress. The same rule now applies to infrastructure-summary metric filters. Shared infrastructure and other route-owned consumers may narrow the canonical/api/charts/infrastructurepayload with ametricsquery for presentation hot paths, but lifecycle surfaces must not reinterpret omitted disk or network series as missing lifecycle telemetry, missing agent capabilities, or reduced fleet freshness truth. The same rule now applies to retired compact dashboard summary payloads. Sharedinternal/api/resourceapi/resources.goroutes must not restore/api/resources/dashboard-summaryas a compatibility read; lifecycle surfaces must continue to use install inventory, enrollment proof, and fleet freshness truth from their owning contracts. Those routes may now seed the availability provider from canonical pluralavailabilityChecksfacets so attached checks survive startup rehydration. Lifecycle surfaces must not reinterpret an attached check, its compatibilityavailabilitysummary, or its evidence freshness as agent enrollment, heartbeat, command reachability, or fleet liveness. The same presentation-only boundary now covers compact storage summary chart reads as well. Shared/api/charts/storage-summarytransport may request only the canonicalusedandavailstorage series needed for the dashboard capacity sparkline, and lifecycle surfaces must not reinterpret the omittedusageortotalseries as missing lifecycle telemetry or enrollment-state evidence. Dashboard storage trend consumers on that shared router boundary must now reuse the single/api/storage-chartssummary response instead of fanning out per-pool/api/metrics-store/historyreads, and lifecycle surfaces still must treat that batched storage summary transport as presentation context only rather than install, enrollment, or freshness truth. - Keep install-script serving fallback-free, and keep the lifecycle
agent-download fallback pinned to published release lineage. The served
install-script endpoints (/install.sh, /install.ps1) have no GitHub fallback:
internal/api/unified_agent.goserves the locally bundled AGENT installer (present in every release tarball and Docker image) or fails closed, so first-host install, repair, and fleet continuity never receive the top-level GitHub install.sh SERVER installer in place of the agent installer (issue #1470). The agent-BINARY download fallback that remains may only treat stable tags and explicit RC prerelease tags as release assets; working-line dev prereleases and build-metadata versions must fail closed so install/repair do not depend on unpublished or branch-local URLs. - Keep self-hosted purchase handoff state on the adjacent commercial/auth
boundary. When shared
internal/api/router.go,internal/api/router_routes_cloud.go,internal/api/licensing_handlers.go, orinternal/api/demo_mode_commercial.goevolve public/auth/license-purchase-startor/auth/license-purchase-activate, lifecycle-adjacent setup and fleet surfaces may rely on that public-route wiring but must not reinterpret the commercial-ownedportal_handoff_id, server-resolved checkout intent, purchase-return tokens, activation-bridge form state, owned billing purchase-arrival states, or demo-hidden commercial route policy as installer credentials, registration state, or fleet enrollment authority. The adjacent licensing boundary also owns public-vs-Pro runtime build attribution for activated installs; lifecycle and fleet surfaces may read the resulting entitlement/runtime-capability posture, but must not infer paid runtime status from agent install commands, public image tags, or local lifecycle inventory. The same adjacent commercial boundary also owns purchase-start unavailability recovery: lifecycle-adjacent surfaces may coexist with that shared browser route, but they must not strand install or fleet flows on a raw Pulse Account error tab, reinterpretpurchase=unavailableas lifecycle repair state, or bypass the shared secure callback policy that limits self-hosted commercial return URLs to HTTPS instance origins or direct-loopback HTTP and keeps hosted commercial follow-up fetches on the restricted outbound client. bypass the owned billing retry/recovery path. That same adjacent commercial/auth boundary also owns the canonical self-hosted purchase intent label: lifecycle-adjacent setup and install flows may observeself_hosted_plan, but they must not keep emitting or inferring legacymax_monitored_systemsintent/query values once the uncapped self-hosted model is canonical. The same adjacent commercial boundary treats migrated-v5 monitored-system grandfathering as retired compatibility metadata. Lifecycle surfaces may react to active license or entitlement payloads, but they must not cache their own pre-activation host counts, synthesize a grandfather floor, restart capacity reconciliation from billing reads, or reinterpret continuity payloads as install eligibility, fleet enrollment evidence, or0 / limitmonitored-system state. That same adjacent commercial boundary also owns authenticated install-version attribution for migrated installs: lifecycle surfaces may observe versioned commercial status, but they must not treat activation/exchange/refresh version fields as installer enrollment state, invent a second fleet-version cache, or backfill install lineage from local host inventory when the shared licensing runtime already sends the canonical process version. That same adjacent commercial boundary also owns internal demo-fixture grants: lifecycle surfaces may observe that a governed demo runtime is fixture-backed, but they must not mint, echo, or infer the internaldemo_fixturescapability through install/setup payloads or installer heuristics. The same lifecycle-adjacent platform-connections boundary also assumes direct TrueNAS and VMware connection writes fail closed when canonical monitored-system grouping is unavailable. Sharedinternal/api/preview helpers may returnmonitored_system_usage_unavailablebefore save, and VMware must not collect external vCenter inventory before that canonical grouping view is safe, so fleet/setup surfaces do not fork monitored-system identity through direct API writes. The same lifecycle-adjacent platform-connections boundary now also owns the unified connections ledger (GET /api/connections) and address probe (POST /api/connections/probe). Lifecycle surfaces may observe agentHost.LastSeen-backed rows on that ledger, but must not reinterpret derivedstate(active/paused/unauthorized/unreachable/ stale/pending) as install authority or treat the probe response as enrollment state. Metadata, link-local, multicast, and unspecified probe destinations must fail closed before any outbound dial, and lifecycle surfaces must surface that canonical rejection instead of retrying through lane-local probe helpers. Ledger writes still flow through the per-type config endpoints that own admission checks, and theDisabledflag on PVE/PBS/PMG surfaced by that ledger must remain a pause-only signal rather than an installer pre-flight gate. The unified add surface is the governed modal flow mounted byInfrastructureWorkspace.tsx: the landing page owns the persistent instance list, the picker dialog owns source-type selection, andfrontend-modern/src/components/Settings/ConnectionEditor/ConnectionEditor.tsxowns detect-driven credential handoff plus type-specific form bodies. That same landing now owns explicit discovery review for API-backed Proxmox-family systems:InfrastructureSourceManager.tsxmay surface discovered VE / PBS / PMG candidates under their matching platform groups, andInfrastructureWorkspace.tsxmay route a candidate'sReviewaction into the same typed add dialog with canonical prefills. Lifecycle flows must not fork a second discovery-specific credential wizard or treat discovery results as already-enrolled systems before the operator saves the governed add form. That same landing-owned shell now keeps discovery compact and secondary: the persistent page may expose only a concise discovery status line after the systems ledger plusRun discovery,Settings, orConfigure discoveryactions appropriate to the current state. New-source admission stays on the header and per-platform table actions instead of competing with discovery at the top of the page. Command-backed discovery sweeps and forced single-resource refreshes remain API/AI-owned admin operations: lifecycle surfaces may expose the controls, but route-level authority must requiresettings:writeplus the Discovery enablement gate, notmonitoring:write, enrollment state, or any fleet lifecycle shortcut. Forced workload discovery also remains API/service-owned for endpoint identity: lifecycle surfaces must not fill a Proxmox parent node or agent name into workload trigger hostnames when the operator did not provide one, because the discovery service resolves VM and system-container names from canonical state before suggesting URLs or classifying known services. The editor's probe step calls the aggregator probe endpoint and dispatches the detected or manually-selected type into a credential slot; it must not bypass the probe endpoint or fabricate probe candidates, and the agent credential slot must continue to reachInfrastructureInstallerSection.tsxso install handoffs remain on the canonical unified-agent install path. Those governed add/edit dialogs must also keep their form body scrollable inside the modal:InfrastructureWorkspace.tsxandConnectionEditor.tsxkeep the content shell onmin-h-0flex columns so long lifecycle forms do not clip lower fields behind the dialog boundary. For PVE, PBS, and PMG, the credential slot isfrontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/NodeCredentialSlot.tsx, which reuses the existingNodeModalBasicInfoSection,NodeModalAuthenticationSection,NodeModalMonitoringSection, andNodeModalStatusFooterprimitives inline under the editor — dropping the Dialog wrapper and the surrounding discovery/configured-nodes workspace. The inline credential slot must keep the visible setup sequence asEndpoint,Authentication, andCoveragebefore the PVE/PBS/PMG setup forms so lifecycle actions keep a stable operator model inside the unified editor. For TrueNAS and VMware, the credential slots arefrontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/TrueNASCredentialSlot.tsxandfrontend-modern/src/components/Settings/ConnectionEditor/CredentialSlots/VMwareCredentialSlot.tsx; they extract the inner form bodies from the per-type panels and render them inline under the editor while still driving the existingTrueNASSettingsPanelStateandVMwareSettingsPanelStateAPIs for save, test, preview, and impact-preview behavior. Feature-disabled, delete-error, and probe guidance notices in the connection editor may compose frontend-primitives'CalloutCard; agent-lifecycle owns the selected-source lifecycle semantics and form handoff, not colored local notice shells. The add flow must not reintroduce the full per-type workspace (Proxmox discovery card, configured nodes table, node-modal stack; TrueNAS/VMware connection list with headers and row actions) into the credential slot, because that previously showed the ledger-of-other-systems in the middle of entering one system's credentials. The configured systems ledger reads exclusively from the unified aggregator: theInfrastructureSourceManagerrows are produced byfrontend-modern/src/components/Settings/useConnectionsLedger.ts, which pollsGET /api/connectionsand maps the aggregator-derived state (active/paused/unauthorized/unreachable/stale/pending) and active scope keys into the source-manager display.InfrastructureWorkspacemust not reconstruct per-type health, scope, or last-seen columns from any retired reporting-local state for configured connection rows; the aggregator is the only configured-connections source of truth. That same aggregator-authored connection/member payload also owns discovery reconciliation for already represented hosts: when a platform row, its attached agent augmentation, or one of its child members already carries canonical host aliases, the settings shell must suppress any duplicate discovery candidate for that same platform instead of asking the operator to review the same machine twice under a hostname row and an IP-only candidate row. Per-row Manage actions live directly on theInfrastructureSourceManagerrow and open the governed edit/detail flow;frontend-modern/src/components/Settings/useConnectionRowActions.tsowns the pause/remove API dispatch, two-click remove confirm, and per-id action error presentation inside that flow. Last-error detail is rendered inline on the row whenconnection.lastErroris non-null, not hidden behind a click-through. Remove-confirm on an agent detail flow reveals the Linux + Windows uninstall commands so the operator can copy and run them before the final confirm; that expansion replaces the legacyInfrastructureActiveRowDetailssurface-breakdown drawer. The ledger must never reintroduce a separate detail page orDialogdrawer for viewing a connection's aggregator fields — everything is on the row. The TrueNAS and VMware credential slots carry per-surface Monitor* scope the same way the PVE/PBS/PMG credential slot already does:TrueNASSettingsPanelState/VMwareSettingsPanelStateread and write positiveMonitorDatasets/MonitorPools/MonitorReplication(TrueNAS) andMonitorVMs/MonitorHosts/MonitorDatastores(VMware) booleans, and the credential slot renders a "Collection scope" checkbox cluster backed by those fields. The unified aggregator must project those flags into the connection row'sscopemap and keepcapabilities.supportsScope: truefor TrueNAS/VMware — reintroducing the per-type Stop-this-surface dialog or hard-coding scope to a fixed all-true map is forbidden.
Agent ownership and platform admission
Agent-typed resources are not automatically Pulse-managed hosts. A host that
reports through the agent source while carrying provider-owner platform
evidence (proxmox-pve, proxmox-pbs, proxmox-pmg, kubernetes,
truenas, vmware-vsphere) belongs to that provider's platform page and MUST
NOT admit the standalone platform page on its own. The canonical predicate is
unifiedresources.IsPulseAgentPlatformResource; new agent surfaces MUST use it
rather than testing type == "agent" or counting the agent source.
Recovery list pagination meta adjacency
Recovery list pagination-meta normalization in
internal/api/recovery_handlers.go (the clamped effective limit echoed in
meta, totalPages computed from that effective page size) is an
API-contracts and storage-recovery transport concern that lives under the
shared internal/api/ extension boundary. The clamp bounds are the exported
DefaultListPageLimit / MaxListPageLimit constants in
internal/recovery/model, which are likewise recovery-transport vocabulary
and not lifecycle surface. No agent lifecycle surface consumes the
/api/recovery/points or /api/recovery/rollups list meta; lifecycle code
MUST NOT treat that pagination meta as an agent enrollment, admission, or
report-ingestion contract.
The node connection test handlers under internal/api/configapi/ record a
content-free outcome tally for telemetry. This adds no agent lifecycle surface:
it does not touch agent registration, capability negotiation, command dispatch,
token rotation, or removal state, and it records nothing about agents. Node
connection tests target Proxmox, PBS, and PMG endpoints directly and never
traverse an agent.
Forbidden Paths
- New install or update continuity behavior hidden only inside broad monitoring ownership.
- Agent profile or fleet-control behavior implemented outside the canonical agent settings/profile surfaces.
- Installer or update flows that depend on branch-tip, dev-only, or non-release asset behavior for supported RC/stable paths.
- Lifecycle setup, install, or fleet surfaces that invoke retired self-hosted trial acquisition;
POST /api/license/trial/startand the retired/auth/trial-activatecallback must stay closed on the ordinary self-hosted router rather than reappearing as lifecycle-local CTAs or retry paths.
Completion Obligations
Command-capable agent completion must prove more than fresh telemetry. The dedicated agent listener must admit the full bootstrap/report/WebSocket lifecycle, command sessions must be keyed by organization plus canonical bound identity, and every dispatch must revalidate the non-secret token admission. Reconnect replacement may replace only that exact tenant identity; duplicate hostnames, stale sockets, revoked or rebound tokens, and ambiguous hostname or token resolution fail closed. Fleet/Doctor projections must keep adapter health separate from command-channel admission and report an enabled-but-disconnected command runtime as a blocking condition.
Any Docker / Podman report-size change must update the shared contract, agent diagnostic proof, API encoded/decoded boundary proof, and the API-contract dependency in one slice. A handler-local literal, agent-local threshold, or freehand limit string is incomplete even if one side's tests pass.
The shared router's canonical RBAC initialization is an adjacent security/API-contract boundary. It must remain a one-time startup and shutdown dependency and must not alter agent registration, install tokens, profiles, command transport, update policy, or fleet lifecycle authority.
Construction-time callbacks added to the shared monitor for monitoring-owned state, including notification-delivery reconciliation, grant no agent enrollment, report, command, or lifecycle authority. They must remain separate from the fail-closed host continuity hydration that agent lifecycle owns.
Shared browser-session authorization for the action plan, decision, and execute
lifecycle must require a configured local administrator or an effective RBAC
admin grant on *. SSO authentication and the sso: session-user prefix are
identity evidence only; agent lifecycle must never reinterpret them as command
authority when no local administrator is configured. Keep
TestActionRoutesRequireExplicitSSOAdminOnSSOOnlyInstance proving that an
unassigned SSO session is refused across all three action stages while an
explicitly mapped administrator reaches the lifecycle handlers.
-
Update this contract when agent lifecycle ownership changes. Routes added under the shared
internal/api/extension point that are clearly outside lifecycle ownership (for examplePOST /api/ai/patrol/preflight, thepatrol_preflightsnapshot field added to/api/settings/ai, the auto-trigger preflight dispatch on settings save, the startup-seed dispatch inNewAISettingsHandler, and the cached-preflight integration into the Patroltoolsreadiness check — all owned by ai-runtime) do not extend this subsystem's contract; they live in their owning subsystem. Canonical scoped Patrol resolution onPOST /api/ai/patrol/runand structuredpatrol_assess_findinglifecycle outcomes are likewise adjacent AI/API contracts: they may consume agent-reported identities and evidence, but they do not change agent registration, install, token, profile, command transport, update, or fleet-lifecycle authority. -
Keep shared API proof routing aligned whenever install, register, or profile payloads change.
-
Update runtime and settings tests in the same slice when lifecycle behavior changes. Shell installer lifecycle changes must keep
scripts/installtests/install_sh_test.gocovering explicit flags, persisted connection state, legacy running-process/service recovery, legacy single-dash v5 agent flag recovery, repeated disk-exclusion recovery, and secure token-file service argument rendering for update re-entry. Host metrics tests must prove excluded FreeBSDfdescfsmounts are filtered before filesystem usage is attempted. -
Keep host-agent test hooks, command-client factories, and timing overrides instance-scoped under
internal/hostagent/agent.go; lifecycle-owned registration and update paths must not depend on package-global mutable test seams that can leak between concurrent agent sessions or tests. -
Preserve canonical /api/auto-register node identity continuity when canonical hosts shift between hostname and IP forms for the same node, and keep runtime-side Proxmox setup host ordering aligned with the contract: explicit report-IP override, route-aware local IP, resolvable hostname fallback, then heuristic local IP fallback. Continuity matches inferred from resolved addresses, a changed-address node-name/token pair, or cluster-member identity must fail safe when both sides carry different TOFU TLS fingerprints. That contradiction identifies distinct PVE or PBS machines and must preserve separate connections even when MSP-managed sites reuse node names, token names, or private address ranges. An exact stored endpoint remains authoritative so a certificate rotation at the same URL can refresh its pin in place. That same lifecycle-adjacent shared-API boundary now covers relay and command-target hostname resolution too. When lifecycle flows reuse
internal/api/router_routes_ai_relay.go,internal/agentexec/server.go, or other shared agent-target helpers, they may treat a short hostname as equivalent to the same agent's FQDN, but they must not widen that fallback into a short-name collapse that would make two different FQDNs appear to be the same lifecycle target. -
Keep Proxmox registration continuity self-healing: stale local registration markers must be verified against Pulse before the host agent skips setup, and a missing matching node on the Pulse side must drive canonical re-registration instead of asking operators to delete marker files manually. Runtime-side Proxmox setup must keep Pulse-managed Proxmox API tokens scoped to the concrete node before falling back to the Pulse URL. PVE API tokens are cluster-wide, so two cluster members that report to the same Pulse server must not share one token name or let a later node install rotate the token stored by an earlier node. Reusable Infrastructure install tokens minted by the browser setup handoff are reporting and config-read credentials, with Docker and Kubernetes reporting scope when needed. They must not include
agent:exec; command execution is granted only by backend-issued host-bound runtime tokens or by first-use Proxmox install-command tokens carrying the governed install metadata that the command websocket can bind to one agent identity. The browser must treat the chosen command-execution option and the minted token as one atomic copy state. Changing that option after minting locks all copyable commands, requests the matching replacement scope, and unlocks only after the replacement arrives; failure restores the complete previous option/token pair. It must never render--enable-commandswith the prior non-exec token, even briefly, and must not accept another option change while a token request is in flight. -
Keep first-session lifecycle handoff explicit: the live setup completion surface in
frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsxmust route the primary CTA into/settings/infrastructure?add=pick, frame that route as source strategy selection, and present platform API inventory plus Pulse Agent telemetry as peer choices for Proxmox, TrueNAS, VMware, standalone hosts, and future provider integrations rather than leaving post-setup next actions implicit. A direct Pulse Agent handoff may remain as a secondary control for operators who already know the first source is agent-managed, but the primary first-run path is the unified source picker. Once the completion surface observes connected systems, that same handoff model must derive its follow-up actions from the canonical connected-system path classification rather than a raw connected-agent count. API-backed first-session states must keepAdd infrastructurevisible for both API-backed and agent-managed next systems instead of reviving separatePlatform connectionsandInfrastructure Installbranches. The completion panel, infrastructure installer, install state, and agent-profile settings surfaces must also stay free of local browser commercial or onboarding metrics wrappers. Lifecycle surfaces may navigate to canonical destinations, but Pulse Account and server-owned reporting routes own commercial event capture. API-backed versus agent-managed classification must come from the governed onboarding paths indocs/release-control/v6/internal/PLATFORM_SUPPORT_MANIFEST.jsonthrough the shared frontend manifest helper, not from a Setup Wizard-local platform allowlist. When preview-only browser proof needs a deterministic connected snapshot,frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsxmay accept a preview-provided connected-resource override, but the live first-session runtime path must keep/api/statepolling as the sole source of connected-system truth when no override is supplied. Localized setup-completion copy may route throughfrontend-modern/src/i18n/messages.ts, but the lifecycle contract remains unchanged: add-infrastructure and agent-install destinations, API token meaning, generated credential values, install/source product identifiers, and reported resource names must stay stable while only surrounding operator-facing guidance translates. 7a. Keep lifecycle-neutral sharedinternal/api/changes from altering agent setup, registration, install, or profile payloads by accident. AI runtime or entitlement work that touches shared router or handler wiring must keep lifecycle public routes, setup-token validation, and agent profile payloads unchanged unless the lifecycle contract and its proofs are updated in the same slice. -
Keep
frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsxoriented around the first monitored host. Install-token generation, governed command copy, and install instructions belong to the canonical lifecycle path; transport details, trust overrides, profile tuning, and adjacent alternatives must remain secondary to that first-host onboarding narrative, including an explicit advanced-options disclosure so first-time operators see token generation, command copy, and status confirmation before non-default connection controls. Setup handoff and connected-system completion actions in that installer surface must keep their lifecycle meaning, destination, and token/download behavior here, while composing the frontend-primitivesButtonvariants for success, outlined success, and ghost success chrome instead of carrying lifecycle-local emerald button shells. -
Keep
frontend-modern/src/components/Settings/InfrastructureWorkspace.tsxandfrontend-modern/src/components/Settings/infrastructureWorkspaceModel.tsaligned with that same lifecycle path. The bare/settings/infrastructureroute must render one Connections and inventory ledger that lists top-level infrastructure only — active or ignored infrastructure roots plus saved Proxmox VE, PBS, PMG, TrueNAS, VMware, and agent-managed entries — as sibling rows sharing one system/coverage/collection/status/last-activity workspace model, so operators can read infrastructure state in one scan instead of hopping between install, reporting, and provider shells. Guest-linked agent rows still belong to the reporting inventory and inline lifecycle detail, but they must not appear as peer connection rows on that top ledger. Adding a new system must stay a single entry point on that ledger: oneAdd infrastructureentry point that opens the source picker, keepsInstall on a hostexplicit only after the operator chooses Pulse Agent, and opens the saved-connection create flow for API-backed platforms on the same page./settings/infrastructure/install,/settings/infrastructure/platforms, and/settings/infrastructure/operationsare retired settings aliases and must fail route eligibility instead of resolving to section focus or rendering separate page shells. The canonical add flow is/settings/infrastructure?add=<step>. Read-only sessions must redirect the canonical Infrastructure workspace back to/settings/infrastructure, suppress the add-system entry point, and hide configuration-only sections so presentation-policy restrictions still hold. That top ledger must also stay readable inside the governed settings shell at ordinary desktop widths:InfrastructureSourceManager.tsxmust keep the compact row/card presentation readable without forcing horizontal scrolling just to reach primary controls. Dedicated collection and last-activity columns may return only once the workspace has enough width to show the full ledger without clipping headers or row actions. -
Keep post-install lifecycle completion explicit inside
frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsxandfrontend-modern/src/components/Settings/useInfrastructureInstallState.tsx. -
Keep the dev first-session proof deterministic on the real wizard path:
tests/integration/tests/helpers.tsandtests/integration/tests/11-first-session.spec.tsmust refresh first-run state through/api/security/dev/reset-first-run, then prove the canonicalAdd infrastructurehandoff and the explicitInstall Pulse Agentsecondary handoff against the live setup wizard instead of relying on stale bootstrap tokens, dashboard fallbacks, or preview-only coverage. The primary handoff must land on the shared infrastructure onboarding contract at/settings/infrastructure?add=pickand normalize back to/settings/infrastructureinstead of reviving a separate platform-management shell. The secondary agent handoff must land on/settings/infrastructure?add=agent. When the first host reports successfully, the install workflow must treat that as a completion handoff with direct navigation into/settings/infrastructureand the first visible platform/runtime page instead of leaving operators on a generic lookup result or reviving the removed/infrastructureroute. When the workspace starts from zero active connected infrastructure and install commands are available, the same lifecycle path must auto-watch the canonical/api/stateprojection for the first reporting host rather than requiring a brand-new operator to know and type a hostname or agent ID just to see the first success handoff. When that workspace is entered through first-run setup handoff, the same lifecycle path must also auto-create the scoped first-host install token so the operator lands on ready-to-copy commands instead of being asked to perform a second manual token-generation step immediately after securing the server. The install-state hook may skip UI state updates after the component is disposed, but cleanup must not return fromfinallyor mask lifecycle token creation failures. Any first-run credentials download generated from that same handoff must describe the prepared first-host token path consistently instead of telling the operator to generate another install token manually. -
Keep
frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsxordered around the actual first-run operator sequence: credentials that must be saved now should be visible before the operator leaves the screen, and the completion surface should present one canonical primary next-step path into Add infrastructure instead of repeating competing install or dashboard CTAs across multiple sections. Once the first monitored system is already connected, that same surface must pivot its primary CTA and headline to/settings/infrastructureso the operator returns to the shared infrastructure workspace rather than being told to connect the first source again or being sent through a generic home/dashboard completion. While the first source is still pending, that same completion narrative must describe Add infrastructure as the place where the operator chooses platform API inventory, Pulse Agent telemetry, or both. If the operator selects the direct agent path from that completion surface, the agent install body may prepare the first-host scoped install token from setup handoff, and when it names the shared settings workspace for follow-up lifecycle control it must use the canonicalInfrastructurelabel instead of reviving the retiredInfrastructure Operationswording. not as a second manual token-generation task the operator still needs to figure out. -
Keep API-backed platform onboarding explicit across
frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx,frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx,frontend-modern/src/utils/agentInstallCommand.ts,frontend-modern/src/components/Settings/useInfrastructureInstallState.tsx,frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx, andfrontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx. Machines/host onboarding means a Pulse Agent install with full host telemetry, while agentless machine reachability remains Availability-owned. Unix-family copied installer commands must run the shared installer preflight before privilege escalation, verify the exact agent binary artifact, and pass token-bearing installs through ephemeral--token-filetransport instead of raw service arguments. TrueNAS must be presented as an API-backed source flow through Add infrastructure first, not as a dedicated Unified Agent install profile. The agent install path may remain available for optional later agent augmentation on TrueNAS, but first-run copy, alternative CTAs, and install-profile lists must not imply that an agent install is the required bootstrap for TrueNAS support in Pulse. -
Keep first-session and lifecycle-adjacent frontend resource handling on the canonical unified-resource boundary. Top-level TrueNAS appliances may reach setup-completion or infrastructure lifecycle surfaces only as canonical
agentresources withplatformType: 'truenas'; any legacy rawresource.type === 'truenas'compatibility collapse belongs in the shared frontend resource adapters, not in setup or lifecycle-local UI branching. -
Keep lifecycle-adjacent AI transport compatibility on the shared
internal/api/boundary. If chat mention parsing, alert investigation targets, or adjacent Assistant resource transport still accept a legacy top-leveltruenastype, that value must collapse immediately to the canonicalagenthost type before lifecycle surfaces, setup handoffs, or operator-visible route state consume it. -
Keep onboarding ownership aligned with
docs/release-control/v6/internal/PLATFORM_SUPPORT_MODEL.md: agent-backed first-class platforms belong to the install/reporting lifecycle path, API-backed first-class platforms belong to the Add infrastructure API source flow, and any later unified-agent augmentation on an API-backed platform must remain an optional secondary path instead of silently becoming the required bootstrap. -
Preserve the on-agent command-policy approval gate in
internal/hostagent/commands.goandinternal/agentexec/server.go. Both ends may honor aTrustedflag onExecuteCommandPayloadto bypass thePolicyRequireApprovalbranch, but only when the payload is constructed by a vetted Pulse-internal call site that ships its own hardcoded command catalog (today only the servicediscovery deep scanner viainternal/ai/discovery_adapter.go).PolicyBlockmust still apply to trusted payloads, andTrustedmust never be set from a deserialised HTTP body, a user-supplied command string, an AI tool call, or a governed approval consumer — those callers must continue to carry anApprovalIDand approval grant. -
Keep plaintext agent transport validation resolver-aware but fail-closed. When agent reporting, remote config, update, Docker, Kubernetes, or command-channel clients allow self-hosted
http://orws://, dotted local DNS names must resolve only to loopback, private/link-local, or carrier-grade NAT addresses. Any unresolved, public, or mixed public/local answer must keep requiring HTTPS/WSS. -
Keep the Patrol model-readiness advisor outside the agent command lifecycle.
POST /api/ai/patrol/readinessmay exercise only synthetic provider tools and in-memory tool results under settings-write authority; it must not requestagent:exec, dispatch an agent command, inspect agent inventory, or turn a successful model probe into command authority. The advisor's keepalive-streaming response transport changes request pacing only: keepalive bytes, the up-front status commit, interrupted-run classification, panic recovery on the evaluation goroutine, and preserved partial probe evidence grant no agent capability and must not be read as agent lifecycle signals. A readiness result carrying theinterruptedorinternal_errorcause is an absence of evidence, so it must never be treated as an agent-readiness verdict in either direction. The readiness checks derived from that snapshot follow the same rule: a passing tool-protocol dimension on an evaluation that never completed is not a readiness verdict, and no readiness check may promote a synthetic probe dimension into agent command authority. -
Keep Docker container-update proof on the production recreate path. Unit coverage must include standalone and Compose-shaped host, shared-service, shared-container, bridge, and custom-network plans, generated and explicit hostnames, labels, dependencies, mounts, volumes, secondary-network rollback, progress errors, and durable reconnect replay. The opt-in live Docker Compose proof must exercise those network modes against a real daemon; a fake-only sanitizer test is not sufficient lifecycle evidence.
-
Keep Proxmox runtime health and bootstrap authority separate. Agent-token startup and periodic registration checks must call
/api/auto-registerdirectly;/api/setup-script-urlremainssettings:write. Ordinary agent tokens stay update-only, while a server-minted install token may consume one initial registration per canonical Proxmox type after durable first-host binding and serialized completion: a PVE/PBS-typed token for its declared type, and a generichostinstall token for each detected canonical type (#1644). A host running both PVE and PBS is an officially supported deployment, so one install token bootstraps one PVE source and one PBS source; a second create of a type already consumed still fails closed. A denied registration grant must fail loudly on the agent side through an error-level log, a returned setup error, and an installer-readableproxmox-<type>-registration-blockedstate marker. One product's refusal must not erase another's success:RunAllstill attempts and returns the remaining products, publishes the detected products in aproxmox-detected-typesstate marker, and errors only when every detected product failed, so the installer reports an outcome per product instead of one verdict for the whole host. -
A rejected legacy RBAC import must not destroy the store. The import is transactional and leaves the legacy files in place, so a failure leaves the database un-migrated rather than half-migrated, which denies access rather than granting it. Failing manager construction instead takes the whole org's RBAC surface offline including
ResetAdminRole, the operator's only way back, on a conflict an ordinary v5 upgrade can produce. The store therefore stays live and records the failure onSQLiteManager.MigrationError. Management routes still fail closed withrbac_store_unavailablethrough the handler manager accessors; recovery reaches the provider directly and is deliberately exempt. Regression coverage:TestAdminRecoverySurvivesFailedLegacyRBACMigrationininternal/api/contract_test.go,TestRBACHandlersReportLegacyMigrationFailureininternal/api/rbac_handlers_test.go, and the rejection cases inpkg/auth/sqlite_manager_test.go. -
Linux SMART transport classification tests vendor before bus address. Vendor
ATAis the SCSI layer's marker for a device reached through a SAT translation layer, so it is the more specific signal; a genuine SAS disk reports its own vendor and neverATA. SATA disks behind an LSI/mpt3sas HBA (the common Unraid and TrueNAS layout) exposesas_addresson their scsi_device while reporting vendorATA, so testingsas_addressfirst classified them as SAS, which drops the-d satprobe hint and steers them back to the-d scsiprobe the discovery work exists to avoid. Regression coverage:TestSATABehindSASControllerClassifiesAsSATAininternal/hostagent/issue1595_sas_collection_test.go. -
smartctl --scandevice types are hints, not ground truth, and discovery fixtures must come from the command actually being run. The non-opening scan cannot interrogate a device, so on a real host every libata disk is reported as-d scsi(captured from Unraid, smartctl 7.5, 2026-07-24), never-d sat. Probe selection therefore has to reject a scan type that contradicts the sysfs transport evidence and still reach-d sat, or every SATA array member is probed as SCSI. Fixtures relabelled from--scan-opento--scandescribe a scan shape that never occurs and cannot prove this. Regression coverage:TestScanReportedSCSITypeStillProbesSATADisksAsSATininternal/hostagent/issue1595_sas_collection_test.goandTestParseSmartctlScanTargetsRealScanOutputininternal/hostagent/smartctl_discovery_test.go.
Current State
Privileged helper framing and identities are architecture-safe
The no-network helper rejects a framed allocation size that cannot include its
four-byte protocol header before calling make, even if a future payload limit
is raised beyond today's bounded request and response ceilings. Resolved helper
UIDs must fit the protocol's unsigned 32-bit identity, and resolved socket GIDs
must fit the target architecture's int before reaching os.Chown; this keeps
the Linux 386 release target fail-closed instead of relying on a narrowing
conversion. TestCheckedFrameSizeRejectsInvalidAllocationSizes and
TestResolveIdentityRejectsOutOfRangeNumericValues pin those boundaries.
Linked-agent interface precedence remains singular
A Proxmox node may now carry PVE API interface inventory alongside an optional
linked Unified Agent. This does not create a second agent identity or lifecycle
channel: the linked agent remains authoritative for richer live interface
telemetry, and the shared drawer presents that view once instead of rendering
both source inventories. internal/models/metrics_types_test.go pins the node
report collection shape used at this merge boundary.
Docker update preflight and execution share one late-bound capability
The unified agent's late-bound Docker updater now carries the read-only typed
container-update preflight and the corresponding execution method in one
required interface. The bridge delegates both methods to the running Docker /
Podman module and fails closed while that module is unavailable. This prevents
the command client from advertising a false agent_capability_unavailable
refusal merely because the process-lifetime bridge omitted the preflight
method, and makes future bridge drift a compile-time failure. The bridge proof
in cmd/pulse-agent/main_test.go covers unavailable, invalid-candidate, and
connected-module states for both preflight and execution.
Monitoring task limits do not change lifecycle authority
The monitoring-owned process-wide scheduled-task limiter may bound host and
availability-agent poll execution across tenants. It does not change lifecycle
journal hydration, report admission, token binding, removal tombstones, or
re-enrollment authority in internal/monitoring/monitor.go.
Retained Patrol objectives do not expand agent lifecycle authority
The shared internal/api retained-objective endpoints can queue read-only
Patrol coverage planning, but they cannot enroll, upgrade, restart, revoke, or
otherwise mutate an agent. Metric and application observers consume canonical
reported/discovered evidence; any later repair still crosses the existing
agent capability, preflight, approval, dispatch, and verification boundaries.
Local command and REST custom metrics are a bounded reporting module
The host agent now accepts an optional private version-1 YAML file through
--custom-sensors-file / PULSE_CUSTOM_SENSORS_FILE. Each of at most 32
definitions names exactly one absolute local executable or HTTP(S) REST URL,
plus collection interval, timeout, optional group/subgroup, numeric, boolean,
or timestamp kind, source-freshness limit, and upper/lower warning and critical
thresholds. Executables receive no arguments; REST collection uses bounded GET
requests without redirects and accepts one scalar or a bounded JSON
value/observedAt object. Boolean values normalize to 1/0 and timestamp
values to event age in seconds before threshold evaluation. Collection remains
bounded to four concurrent executions inside the existing ten-second host
collection context; last-good values survive failures as stale evidence with
their original observation and event times. The resulting typed
sensors.custom array is reporting data only and grants no command, enrollment,
lifecycle, remote-shell, or AI authority. Agent initialization fails closed on
invalid configuration, and Windows service startup carries the same optional
file path. TestLoadConfig, TestCustomSensorKindsAndHTTPFreshness, and
TestCustomSensorMetricJSONRoundTrip pin CLI plumbing, runtime semantics, and
the additive report wire shape.
Legacy rootful precedence and safe rootless admission are explicit
Installer container-runtime discovery
(discover_rootless_container_runtime in scripts/install.sh) consults a
working system Docker first: when system_docker_runtime_is_active confirms a
rootful daemon (a docker info success with DOCKER_HOST/CONTAINER_HOST
stripped, or a live /var/run/docker.sock probe), rootless socket discovery
returns nothing and no PULSE_DOCKER_RUNTIME=podman /
CONTAINER_HOST/PODMAN_HOST/XDG_RUNTIME_DIR pin is written into the agent
service environment. This holds for auto-detection and for explicit
--enable-docker alike (#1647) — previously a transient rootless Podman API
socket (socket-activated for root's login session on Debian/OMV) outranked a
healthy rootful Docker and pinned the agent to a socket that vanished with the
session.
That precedence applies to the legacy/root service only. A typed-helper safe
profile cannot use the rootful socket directly, so it defers rootless discovery
until pulse-agent has been provisioned, scans Docker and Podman as one
candidate set, and persists an endpoint only when exactly one socket is owned
and readable/writable by that collector UID. A socket owned by root or another
login user is ignored; two usable sockets are an explicit ambiguity. The agent
repeats the same exact-one admission on startup and reconnect, including the
standard /run/user/<uid>/docker.sock and
/run/user/<uid>/podman/podman.sock paths, before any daemon information probe.
The information response must independently attest rootless mode; an owned
socket path proxying a rootful daemon is closed and rejected. A root-owned unit
that is not group/world-writable can preserve its exact standard-path pin
through an update even while the socket is offline.
On the agent side (internal/dockeragent/agent.go), the podman runtime
preference is an ordering hint, not an identity override: detectRuntime no
longer short-circuits to podman on preference alone, so a podman-preferred
connection that falls through to a docker-named endpoint reports docker and
keeps Swarm collection enabled, while an unlabeled endpoint with no runtime
signals still honors the preference. connectRuntime accepts that fallback
(only an explicit docker preference still hard-rejects a podman endpoint). When
the bound socket disappears mid-run, the agent counts consecutive
daemon-unavailable collect cycles and after runtimeReconnectFailureThreshold
re-runs runtime discovery against the original preference, swapping the
connection through swappableDockerClient
(internal/dockeragent/runtime_reconnect.go) so concurrent cleanup and
container-update goroutines keep a stable handle, and refreshing the reported
runtime, daemon host, and Swarm capability from the new connection.
Shared system-settings boundary dropped dead auto-update schedule fields
The shared internal/api system-settings surface this subsystem consumes
(internal/api/system_settings.go, internal/api/configapi/config_system_handlers.go)
removed the never-consumed autoUpdateCheckInterval / autoUpdateTime
fields. No agent-lifecycle behavior keyed off them — agent update targeting
and command admission are unaffected — and the extension-point expectations
on the system-settings boundary are otherwise unchanged.
Shared system-settings boundary gained an SSH backoff reset side effect
The shared internal/api system-settings surface this subsystem consumes
(internal/api/system_settings.go) now clears the temperature collector's
SSH failure backoff on every live tenant monitor after a successful save
(#1638). That backoff belongs to monitor-owned direct SSH temperature
collection, not to the agent command transport: no agent registration, token,
command admission, update, or fleet-lifecycle authority is touched, and the
extension-point expectations on the system-settings boundary are otherwise
unchanged.
Monitor housekeeping prunes only live memory-evidence caches
The shared monitor housekeeping pass in internal/monitoring/monitor_agents.go
(cleanupRRDCache) prunes exactly two short-lived memory-evidence caches: the
node RRD cache and the guest-agent meminfo cache keyed by
(instance, node, vmid). The former per-VM guest RRD memory cache was removed
along with GuestRRDPoint's fictional memused/memavailable columns —
recorded PVE 8/9 guest rrddata responses never carry them (#1634) — so
monitor construction in internal/monitoring/monitor.go no longer initializes
a guest RRD cache and housekeeping must not regrow one. Stale guest-agent
entries age out under vmAgentMemCleanupMaxAge so removed or re-enrolled
guests cannot replay old agent memory evidence.
Command-channel token binding is one decision consumed by every surface
Agent exec token binding is evaluated by a single decision function
(evaluateAgentExecBinding in internal/api/agent_exec_token_binding.go)
that both command-channel admission (admitAgentExecToken) and the agent
config gate (commandConfigAllowedForToken) consume. The two surfaces may
never diverge: an agent is told commands are enabled only when its channel
registration would be admitted, because a divergent gate leaves the host
reporting CommandsEnabled=true against a channel that is always rejected — the
permanent "Remote control blocked" state shipped in v6.1.2. The immutable
machine-derived agent ID is the primary binding identity: an exact ID match
re-binds a drifted (renamed) bound_hostname in place rather than stranding
the host, while a hostname match alone never satisfies a version-2 identity
binding. Hostname comparison uses the system-wide equivalence rule
(unifiedresources.HostnamesEquivalent, plus case-insensitive exact match
for IP literals), so short-name vs fully-qualified drift does not break
admission. Legacy pre-v6.1.1 hostname-bound records still migrate exactly
once on hostname match.
Install-token command intent converges once on the accepted host
The generic host and PVE/PBS install-command endpoints stamp every newly
minted token with the operator's explicit enabled/disabled command-policy
intent. On the first report accepted for that token,
reconcileInstallTokenCommandPolicy re-reads the live token record, requires
the same evaluateAgentExecBinding identity decision used by the command
channel, projects enabled intent only while agent:exec is still present, and
persists the effective choice through Monitor.UpdateHostAgentConfig before
the report acknowledgement is built. This closes the #1728 reinstall failure:
a stable machine ID may retain an older commandsEnabled=false override, but
a fresh commands-enabled install no longer receives the stale override that
would stop its command client one second after startup.
The token records the accepted host ID after the projection. That marker makes
install intent one-shot, so a later server-side command-policy change remains
authoritative instead of being undone on every report. Hand-created API tokens,
legacy records without explicit intent metadata, and tokens whose shared
binding decision rejects the reporting identity never enter this convergence
path. TestHostAgentFreshInstallTokenReplacesStaleDisabledCommandPolicyOnce
pins stale-policy repair and subsequent-admin-policy preservation;
TestHandleAgentInstallCommand_HostWithCommands and
TestHandleAgentInstallCommand_HostWithoutCommands pin the mint-time intent.
PBS connection composition does not create agent lifecycle evidence
The shared API connections ledger and diagnostics now project PBS health from
the monitoring scheduler's completed poll result and keep immediate diagnostics
probes separate. This does not enroll a PBS host as a Pulse Agent, extend an
agent heartbeat, prove command reachability, or create update/repair authority.
Agent lifecycle consumers continue to use agent-source identity and report
freshness only. When normalized host identity uniquely proves that a PBS API
source and a Pulse Agent describe the same machine, the grouped systems
payload composes them into one source row with API + Agent coverage; provider
health and agent lifecycle facts remain independently authored inside that row.
That host identity includes the hostname the PBS node reports about itself
(models.PBSInstance.NodeName, captured by the PBS poll from GET /nodes),
so an IP-or-alias-configured PBS connection still reconciles with the agent's
reported hostname. The reported node name is identity evidence only: it never
enrolls the machine, extends heartbeats, or substitutes for agent-source
identity in lifecycle decisions.
Docker and Podman report sizes share one exact-byte contract
The Unified Agent now measures the encoded gzip body and decoded report JSON
against the same constants used by server ingress. The 80% warning boundaries
are derived with integer ceiling semantics, the old 400 KiB/512 KB warning is
retired, and diagnostics identify both byte counts and both ceilings before a
send is attempted. All supported native release binaries and the
container-agent entrypoint compile the same internal/dockeragent module; an
installed older agent adopts this behavior only after the normal agent update
and process/service restart loads a build containing the shared contract.
Docker updates recreate desired configuration and replay terminal truth
The Docker module now builds an immutable recreate plan from inspect output before pulling or mutating. Generated container-ID hostnames and operational endpoint fields are not injected as explicit create configuration; explicit hostnames and configurable endpoint fields remain intact. Host and container/service namespace modes receive no conflicting endpoint or hostname configuration. Multi-network replacements connect the primary network deterministically and restore every remaining network before start; a failed attachment removes the replacement, restores the backup name, restarts the original when required, and returns rollback facts through the typed action result. The host agent persists that terminal result before delivery and replays it after reconnect without rerunning Docker. Agent Doctor hides the manual host-local update command only for a timestamped update attempt inside the bounded in-flight window; a missing timestamp or a stale attempt restores the manual recovery handoff.
Docker agent test seams are per-Agent fields, never package globals
The Docker / Podman module owns two indirection seams that exist only so tests
can drive timing and encoding failures: the timer constructor used by
waitForAsyncDelay and the JSON marshaller used by command acknowledgement
delivery and by update-command payload decoding. Both are unexported fields on
the Agent struct — newTimerFn func(time.Duration) *time.Timer and
jsonMarshalFn func(any) ([]byte, error) — reached through the newTimer and
jsonMarshal methods, and both fall back to the standard library
(time.NewTimer, json.Marshal) when the field is nil. No package-level
newTimerFn or jsonMarshalFn variable exists in internal/dockeragent.
Ownership is per-Agent because the seams are read from goroutines the agent
itself started. runAsync work such as command-acknowledgement retry outlives
the test that scheduled it, so a package global swapped by a later test was a
genuine data race between that swap and the leaked goroutine's read, not merely
a test-isolation smell. Making the seam a field the goroutine's own Agent
already carries removes the shared location: an injected hook can only ever be
observed by the Agent it was constructed with, and two Agents running
concurrently with different hooks stay independent. The injection point is
construction only — nothing reassigns either field after an Agent is in use, so
the fields need no mutex and take part in no locking order.
This is an internal test-seam shape with no public contract delta. Agent
enrollment, transport trust, command admission, acknowledgement semantics, and
the Docker recreate/rollback result contract are all unchanged by it; a
production Agent leaves both fields nil and runs the standard-library
implementations. The seams remain unexported and must not be promoted into
NewAgent options, agent profiles, or any server-facing configuration surface.
internal/dockeragent/agent_internal_test.go proves the field shape, the nil
defaults, the absence of package-level hook globals, and per-Agent isolation
under the race detector;
internal/dockeragent/container_update_test.go proves the update-command
decode path routes through the receiver's own marshaller.
vSphere system members carry no agent semantics
ESXi host rows that now compose under their owning vCenter connection in the
grouped systems payload are API-side composition only: they carry no
agentConnectionId, never claim the primary marker, and present the
"vSphere host" member subtitle rather than cluster-node wording. Agent
lifecycle surfaces must not treat these members as enrollable, updatable, or
doctorable agents — the Pulse Agent fleet remains exactly the set of
SourceAgent-backed connections and structured fleet diagnostics.
Provider route scoping does not change that separation. Compact resource-list type facets and route-specific Docker, Kubernetes, TrueNAS, or VMware inventory queries are read-side presentation evidence only. A provider-owned canonical agent row may remain in the active route query so the page can render its system or update posture, but neither the row nor its facet count creates enrollment, token binding, command, update, removal, or re-enrollment authority.
Agent Doctor covers exactly the Pulse Agent fleet
The connections ledger no longer fabricates agent-type rows for
integration-monitored machines at all (buildConnections skips hosts with a
non-empty IntegrationSource), so vSphere ESXi hosts and TrueNAS boxes no
longer appear under "Pulse Agent hosts" in Infrastructure settings — their
owning platform connection is the source representation, and per-machine
visibility stays on the platform pages and Machines. Agent Doctor target
collection (collectInfrastructureAgentDoctorTargets) additionally excludes
any ledger agent connection that carries integrationSource as
defense-in-depth — integration-monitored machines have no Pulse Agent to
diagnose and previously rendered as permanent "Unknown / no structured
reason" rows. In the other direction,
structured fleet diagnostics that have no ledger binding (agents reporting
only workload telemetry, e.g. Docker-only or Kubernetes-only agents) are now
appended as diagnostics-only targets, honoring the scoped-agent filter, so a
critical workload-only agent can no longer vanish from the fleet view.
Telemetry liveness and an applied commandsEnabled report are not command
connectivity. The connections ledger must consult the tenant- and
token-scoped live session registry: an enabled report without an admitted
socket projects remoteControl="disconnected" and a blocked command policy,
while a deliberately disabled command policy remains disabled rather than
becoming a transport alarm. Agent Doctor merges that ledger fact with
structured diagnostics, so a diagnostic snapshot marked healthy cannot hide
the critical command_channel_disconnected reason.
Diagnostics-only rows render the diagnostic's status, reasons, and evidence
but offer no host-local update command (there is no ledger connection to
derive an update from). Removed rows are the deliberate exception in the
uninstall direction: a removed agent's one remaining next step is host-side
cleanup, so a removed target resolves its command platform from the retained
diagnostic identity through the strict platform resolver and its row
expansion hands off the host-local uninstall command
(getInfrastructureAgentDoctorUninstallHandoff) — the single matching
platform command when the identity resolves, or both explicitly labeled
Linux-family and Windows commands when it does not, never one guessed
executable. The handoff carries the diagnostic's agent id and hostname as
uninstall identity flags, and its copy states the command runs on the
affected host itself, not from Pulse. Page presentation follows the same
honesty rule: the
"Target" column renders a version only when one is actually published, the
host-local command explainer renders only when at least one row offers a
command, and the summary chips list only non-zero status counts. The summary
chips double as status filters over the fleet table, and a filter whose
status empties out clears itself rather than dead-ending the table. The page
also offers plain-text diagnostic reports (fleet-level over the currently
visible rows and per-agent from a row expansion) via
formatInfrastructureAgentDoctorReport; reports carry status, versions,
last-seen, reasons, identity evidence, and non-command repair actions, and
must never embed host-local update or uninstall commands because those can
carry install tokens.
Manual Docker update checks are bounded, replay-safe commands
The Docker / Podman module treats check_updates as one bounded agent
operation rather than as permission to start a registry scan on every report
poll. The monitoring-owned server queue dispatches a command only on its
queued to dispatched transition; after receipt, the module keeps one active
manual check plus a bounded ten-minute cache of terminal command IDs. A replay
of the same ID may restore its current acknowledgement but must not clear the
registry cache or collect again, and a different command arriving while the
first is active must fail without starting another scan.
The first receipt clears registry result/error caches once, reports
in_progress, and runs an immediate collection through the same collection
mutex used by automatic reports. That shared mutex, not a scheduling sleep,
serializes manual and automatic work. The manual operation inherits the
five-minute whole-cycle ceiling and registry request cancellation from the
Docker collector. Only after collection finishes may it report completed
with checked, update, skipped, registry-error, and rate-limit counts; a fatal
collection error or deadline reports failed. Terminal acknowledgement
delivery has a small bounded retry budget, while acknowledgement failure never
feeds the enclosing report back into command execution. The API and monitoring
contracts continue to own host command admission, command TTL expiry, and UI
in-flight projection.
Registry update checks may use host Docker credentials, host-locally only
Automatic and manual Docker / Podman update detection may authenticate to a
registry with the host's own Docker credential store — config.json auths
entries, configured credsStore/credHelpers credential helpers, and
Podman's auth.json, honoring the REGISTRY_AUTH_FILE and DOCKER_CONFIG
overrides — so containers from private registries get real digest checks
instead of a permanent "authentication required" failure. Resolved
credentials are presented only to the registry itself or the token endpoint
it names: Basic auth on Bearer token negotiation (including the hardcoded
Docker Hub and ghcr.io endpoints), a direct Basic answer to a Basic
challenge, and the OAuth refresh-token grant for identity-token logins.
Credentials must never be reported to the Pulse server, logged, or embedded
in check errors, and credential helper output must stay out of returned
error surfaces. Helper names are validated against a strict pattern before
the agent executes docker-credential-<name> get, helper execution is
time-bounded with capped output, and lookups are cached in memory for five
minutes. A stale or rejected login falls back to the anonymous path so
checks that used to work anonymously keep working, and
--disable-registry-credentials / PULSE_DISABLE_REGISTRY_CREDENTIALS
keeps detection anonymous-only without reading the store or executing
helpers.
Governed action readiness remains outside agent lifecycle authority
The canonical Actions lifecycle may ask an executor-owned
AvailabilityChecker whether an already-planned capability is still reachable
before a human approval is persisted and again immediately before human or
automatic policy dispatch admission. Agent
connectivity and command-agent loss are read-only readiness evidence at this
boundary: at approval it prevents the decision record and returns the exact
bounded reconnect reason; at dispatch an explicit unavailable result produces
the stable action_execution_unavailable refusal, a terminal failed action
audit and lifecycle event, and the normal action-completed publication without
creating a dispatch attempt or issuing an agent command. The check does not enroll,
reconnect, reconfigure, update, or otherwise mutate an agent, and it cannot
replace canonical planning, approval, policy authorization, dispatch receipt,
or verification.
Shared internal/api/ai_handlers.go now projects separate Patrol investigation
evidence-call and model-response budgets/counters. Those fields remain adjacent
AI-runtime/API evidence only: agent enrollment, dispatch attempts, liveness,
and command-delivery accounting must not infer agent lifecycle progress or
capacity from investigation call counts.
Canonical mutation-plane dependency
Agent deployment remains an explicit Pulse-administrative exception in the closed mutation registry. Agent command transports do not originate authority: typed host update/storage cleanup and executor-owned resource commands may run only after committed action-lifecycle authority, while raw model command and unowned delivery paths remain denied.
Agent dispatch begins only after the canonical action store wins and commits
the transition to executing. Concurrent replay, another SQLite connection,
or a process restart cannot obtain a second executor admission for that action.
This contract intentionally does not claim exactly-once infrastructure effects
after a crash; durable attempt recovery and downstream effect reconciliation
remain the action-continuity layer's responsibility.
Deploy fan-out concurrency is one shared protocol contract in
internal/agentexec: server request normalization and host-agent semaphore
allocation both cap max_parallel at the same bound, including payloads that
bypass the normal API producer. Shared local-redirect validation in
pkg/securityutil/httpurl.go likewise rejects scheme-relative and backslash
authority forms before agent-adjacent handoff or proof flows consume them.
The host-agent semaphore materializes only literal capacities from one through
the shared maximum after normalization, so network-derived payload values never
reach the channel allocation site while the requested concurrency semantics are
preserved.
Denied Patrol investigation-fix approvals passing through shared
internal/api/ handlers are adjacent AI-runtime/action-governance state only.
The fix_rejected finding outcome records that an operator declined a proposed
Patrol fix before execution; it must not be reinterpreted as agent enrollment,
install progress, fleet liveness, or an agent-command failure.
Patrol finding lifecycle payloads exposed through shared AI handlers, including operator resolution-note fields, remain AI-runtime/API-contract vocabulary: adjacent fleet and install surfaces may render the lifecycle state when another surface supplies it, but they must not reinterpret it as install progress, agent approval state, topology truth, or lifecycle-owned remediation policy.
Default-org token scoping and notification-settings fan-out on shared
internal/api/ handlers are likewise adjacent only: org-bound token denial
for the default org and instance-wide webhook allowlist propagation are
api-contract/security owned and change no install, enrollment, setup-token,
or fleet command authority. Agent tokens keep routing agents into their bound
org exactly as before.
Notification webhook management changes on shared internal/api/ handlers are
likewise adjacent only: the webhook signingSecret payload field, its masked
list representation, and masked-echo preservation on update are
notifications/API-contract owned and grant no agent install, enrollment,
setup-token, or fleet command authority.
Notification queue-health changes on that same shared boundary are also
adjacent only. Retained failed/dead-letter counts, fail-closed unavailable
state, and retention metadata are notifications/API-contract delivery evidence;
they do not describe agent liveness, enrollment, install readiness, command
transport, or fleet health.
SSO provider-detail payload changes on shared internal/api/identity_sso_handlers.go
are API-contract/security-settings owned. Nested OIDC/SAML edit fields,
restriction lists, role mappings, and masked secret-presence markers create no
agent install, enrollment, setup-token, command, fleet liveness, or agent
profile semantics.
Alert delivery diagnosis on shared internal/api/alerting/alerts.go is likewise
adjacent only: /api/alerts/delivery-diagnosis is alerts/API-contract owned
read-only notification-policy evidence and must not be interpreted as agent
enrollment, agent liveness, install progress, setup-token authority, or fleet
repair eligibility.
Its identifier-free bulk form merely projects the same evidence for every
active alert in one read pass. Array membership, resource identity, delivery
reason, and missing diagnosis data grant no agent command or continuity
authority and must not be folded into agent lifecycle state.
The adjacent /api/alerts/events route and its SQLite source are likewise
alerts/API-contract evidence only. Alert resource fields, suppression reasons,
and lifecycle event membership grant no enrollment, heartbeat, command,
installation, continuity, or repair authority, and a missing event must not be
reinterpreted as agent liveness evidence.
The router projection-builder (internal/api/router.go) that wires
the operator-state provider into the findings runtime now also
populates NeverAutoRemediate on the projection. The investigation
runtime reads the same projection to attach OperatorContext to
findings handed to the orchestrator, so investigation reasoning sees
the same lock-against-remediation flag that the action broker
enforces downstream — no possible drift between "what Patrol
proposes" and "what the broker accepts."
The same router wiring owns the alert-bridge patrol-trigger callback. It now
receives the full alert payload as a struct and consults the operator's
per-rule trigger policy before queuing a scoped patrol: an alert_fired event
that fails AIConfig.AlertTriggersInvestigation (below the minimum-severity
floor or outside the alert-type allowlist) is logged and dropped without
entering the trigger manager, so alert-driven investigation lifecycle stays
bounded to the alerts the operator opted into.
/api/agent/events is the SSE stream agents subscribe to for
real-time notifications: finding.created when a new finding is
raised, approval.pending when a remediation request enters
StatusPending and waits on operator decision, action.completed
when an action audit reaches a terminal state (Completed,
runtime-Failed, or refused-before-dispatch with stable
plan_drift: / resource_remediation_locked: error-token
prefixes preserved verbatim; successful dispatches carry a
verification block — the agent-stable projection of the
broker's read-after-write probe, with verification command/note
details redacted to stable markers at the action-audit readback
boundary — so agents close the "did it actually work?" loop without
polling /api/actions/{id}),
and a 15-second heartbeat keepalive.
The broadcaster drops real published events for slow subscribers
rather than blocking publishers, so the patrol-finding runtime, the
approval store's post-create callback, and the executor's
post-completion callback can publish without ever stalling on
consumer slowness. API-owned action execution uses the same shared
terminal publisher, including stale-plan action_plan_drift
refusals that fail the audit with a plan_drift: result before any
executor is called. Heartbeats are stream-local keepalives written
to each connected response; one subscriber's heartbeat ticker must
not publish heartbeat events to every other subscriber.
The agent runtime keeps the broadcaster wired across restarts, and
the capabilities manifest declares the stream under
subscribe_events so the surface stays self-describing.
Shared internal/api/router.go metrics-history reads may now expose
host-agent CPU temperature as an agent chart metric for Proxmox node
drawers, but lifecycle surfaces must continue treating that data as
presentation telemetry only. Temperature history is not heartbeat authority,
agent enrollment state, or freshness proof; those remain owned by the
agent-lifecycle event, token, and last-seen contracts.
/api/agent/capabilities is the discovery document any external
agent reads to learn what Pulse exposes. The manifest is
unauthenticated and cacheable; the underlying capabilities keep
their own auth scopes. Adding a capability is a deliberate "this
is part of the agent surface" commitment so the agent contract
stays curated, not auto-derived from every internal endpoint.
The manifest's action mode and approval policy metadata are API/AI-owned
governance posture; lifecycle setup, enrollment, and fleet-control flows may
surface that metadata to agents but must not reinterpret it as install,
registration, heartbeat, update, or agent authority.
The same adjacency applies to manifest inputSchema metadata: typed tool
arguments help external agents call API-owned actions, but do not create a
lifecycle-owned setup command path, operator-state write contract, or
agent-control argument registry.
/api/agent/resource-context/{id} is the agent-paradigm substrate
endpoint: any agent (in-process Patrol/Assistant or external) reads
the full situated picture of a resource — identity, operator state,
active findings, pending approvals scoped to the resource, recent
actions including refused dispatches with their stable token
prefixes preserved — in one call. The endpoint is read-only under
monitoring:read. The active-findings section flows through an
AgentFindingsProvider adapter wired in router.go from the
patrol service so the api package stays free of an internal/ai
import; the parallel AgentApprovalsProvider adapter resolves the
canonical approval store at request time, filters full per-resource
summaries by canonical resource id and org, and exposes a separate
resource-keyed count projection for fleet reads. The agent runtime
keeps both providers wired across restarts. The companion
/api/agent/resource-capabilities/{id} endpoint (same
monitoring:read scope) returns the structured governed
capability list and parameter schemas for the same resource, so an
agent can populate plan_action inputs from the canonical
Resource.Capabilities source rather than the prose summary in
the context bundle.
/api/agent/fleet-context is the companion triage view: one read
returns a thin per-resource rollup across every resource visible
to the org — identity, operator flags
(intentionallyOffline, neverAutoRemediate,
maintenanceWindowActive), per-severity finding counts, and
pending-approval count. Same auth scope (monitoring:read) and
same provider wiring as the per-resource bundle; the fleet sweep
walks the registry once and reuses the in-memory findings index,
one bounded approval-store scan grouped by canonical resource id,
and a per-resource operator-state SQLite point lookup. Agents pick
"where do I focus?" from the fleet view and then drill into the
per-resource bundle for depth. Optional additive filter query params
(hasFindings, severity, technology, resourceType) narrow the sweep to
a relevant subset; they introduce no new lifecycle state, operator
intent, or persistence — they are a read-only projection over the
same in-memory registry walk.
/api/agent/capabilities is registered in the router's
publicPaths list so the global auth middleware does not gate
the discovery manifest. The manifest is unauthenticated by
design: an agent that does not yet have a token must still be
able to introspect Pulse to learn how to ask for one. The
underlying capabilities the manifest declares keep their own auth
scopes — the public posture applies only to discovery itself.
The action governance loop (/api/actions/plan,
/api/actions/{id}/decision, /api/actions/{id}/execute) joined
the agent surface via the manifest under a new action category,
gated by the ai:execute scope (distinct from monitoring:write
because action governance is the governed-execution dimension).
The handlers were migrated from the platform-wide APIError
envelope to the agent-stable {"error", "message", "details"?}
shape so the substrate keeps a single envelope contract across
read, write, and action capabilities.
The governed Patrol finding lifecycle tools advertised in the same
manifest (acknowledge_finding, snooze_finding, dismiss_finding,
resolve_finding) also return that agent-stable envelope for branchable
failures and declare their closed error-code set in the manifest. That is
an API/AI-runtime contract only: it does not create agent lifecycle state,
registration state, install state, or any new durable recovery artifact.
Docker / Podman lifecycle execution extends that same action category only:
the manifest advertises the execute_action substrate, while per-resource
container capabilities, policy checks, action audit records, and terminal
verification remain API-owned facts rather than agent lifecycle state.
The findings runtime now consumes operator-set per-resource state
through a provider adapter wired in internal/api/router.go at
startup. The adapter returns a ResourceOperatorStateProjection
covering every signal in one call (active maintenance window plus
the indefinite IntentionallyOffline flag), so adding new signals
later does not multiply round-trips per finding. When a resource is
in an operator-set maintenance window OR is marked intentionally
offline, new findings against it are auto-acknowledged at creation
time so the operator's commitment is honored without flooding
notifications. The agent runtime keeps the operator commitment honored across
restarts because the underlying state lives in the durable
resource_operator_state SQLite table.
The /api/resources/{id}/operator-state GET / PUT / DELETE handlers in
internal/api/resources_operator_state.go are the canonical operator
surface for setting per-resource intent (intentionally offline, never
auto-remediate, mutually exclusive one-shot or timezone-aware recurring
maintenance with explicit resource/descendant scope, criticality hint). The route lives
on the same monitoring router (router_routes_monitoring.go) as the
rest of /api/resources/{id}/...; method-keyed scope dispatch means GET
runs under monitoring:read while PUT and DELETE require
monitoring:write because they modulate Patrol's behavior on findings
against the resource. The agent runtime must surface the same
operator-set state across restarts — persistence is in the
resource_operator_state SQLite table managed by the unified-resources
store from slice 29 — so either maintenance schedule form or never-auto-remediate
flag set before a process restart is honored after the agent reloads.
Patrol-finding to unified-finding mirroring in internal/api/router.go
also keeps the will_fix_later wake-up deadline (Finding.RemindAt)
intact across restarts. Both the live wire-up callback and the
persistence-recovery resync must copy f.RemindAt onto the unified
finding so the operator's commitment survives a reboot or process
restart instead of silently lapsing into the canonical findings store
without being mirrored on the API surface.
The same router boundary now also threads Finding.AutoResolved
through to UnifiedFinding.AutoResolved so an operator who manually
clicked Mark resolved before a restart keeps that attribution after
the agent reloads findings from disk. Without that mirror the
recovery sync would flatten every prior closure into Pulse's
auto-detection bucket and erase the operator-vs-Pulse distinction
across process restarts.
Linux agent privilege hardening is now part of the installer/runtime contract.
The supported full-telemetry systemd agent may still run as root, but
cmd/pulse-agent/main.go must bind health/metrics to loopback by default,
scripts/install.sh must preserve explicit health-address disable/open choices
in the rendered service, and generated systemd units must keep conservative
sandboxing in place unless a future telemetry requirement records a narrower
exception.
New Proxmox VE and Proxmox Backup Server setup must default to the API
Inventory path: the UI may recommend a root Pulse Agent only as the Host
Telemetry Agent path for temperatures, SMART, local storage detail,
agent-driven operations, or other node-local telemetry the Proxmox API cannot
provide.
Generated API Inventory scripts remain a one-time privileged setup action, but
their steady-state credential must be a narrowly scoped Proxmox API token: PVE
setup must use a privilege-separated token and mirror generated ACLs onto both
the service user and token, while PBS setup must keep Audit grants on both
the service user and token.
Generated TrueNAS CORE rc.d service scripts must give /usr/sbin/daemon -r a
supervisor pidfile with -P, keep the child pid in a separate diagnostic
pidfile, and stop legacy child-pidfile installs by resolving the child back to
its daemon supervisor before replacing or restarting the agent binary.
Deploy selection and retry no longer carry monitored-system capacity feedback. Lifecycle-owned deploy surfaces must not revive license-slot, workspace-slot, plan-upgrade, or monitored-system capacity wording in user-facing confirmation, preflight, retry, and status labels.
The infrastructure workspace collapsed to a single /settings/infrastructure
route. buildInfrastructureWorkspacePath() always returns the base path;
add flow state is carried only by the add query parameter on that base path.
SetupCompletionPanel.tsx uses the shared infrastructure path builders for all
install and platform CTAs.
frontend-modern/src/utils/infrastructureSettingsPresentation.ts owns the
customer-facing Settings Infrastructure target label and onboarding source
strategy copy. Lifecycle and setup guidance must point operators to
Settings → Infrastructure and must not revive removed subpaths such as
Settings → Infrastructure → Proxmox.
The shared monitored-system impact preview now formats save-impact
summaries through frontend-modern/src/utils/monitoredSystemPresentation.ts
so infrastructure setup screens describe count impact and grouping changes
without raw slash-quota rendering.
This subsystem now sits under the dedicated agent lifecycle and fleet
operations lane so install, registration, update continuity, profile
management, and fleet safety stop hiding inside architecture, migration, or
monitoring work.
Lifecycle-owned connected-infrastructure and reporting browsers now also keep
governed platform rows on canonical local operator identity while tolerating
optional optimistic hostnames. Shared row models may fall back to the row name
when staging a removal state, but they must not resurrect legacy
policy.display shims or require platform-managed surfaces to synthesize a
second hostname contract.
That same adjacent internal/api/ router boundary now also keeps usage-data
controls out of lifecycle truth. Agent install, reporting, and setup surfaces
must not depend on /api/upgrade-metrics/*, telemetry preview routes, or
local-only upgrade-event state under shared licensing/auth routing. The normal
customer product router must keep those retired commercial analytics routes
absent, and lifecycle code must not reinterpret telemetry preview payloads or
published-release classification fields as enrollment evidence, agent
freshness, or setup progress truth.
That same adjacent internal/api/ boundary now also keeps public demos from
leaking commercial state through lifecycle-adjacent surfaces. Agent install,
reporting, and setup flows may share backend helpers with billing or license
transport, but DEMO_MODE must continue to 404 commercial read surfaces
instead of teaching lifecycle or mock-mode paths to bypass licensing. Public
demo readiness therefore comes from hiding commercial presentation on the
shared API boundary, not from introducing a second fake-entitlement path into
lifecycle-owned install or reporting flows. Browser-facing lifecycle surfaces
must also treat /api/security/status as the canonical public-demo bootstrap
contract. The backend source-of-truth fact remains
sessionCapabilities.demoMode, but lifecycle surfaces must consume the shared
resolved presentationPolicy instead of inferring demo posture from headers,
/api/health, or hostname heuristics.
That same shared API boundary now owns the hidden-versus-runtime-only split as
well: lifecycle-adjacent flows may inherit non-commercial
/api/license/runtime-capabilities reads when demo-visible product behavior
needs them, but /api/license/commercial-posture,
/api/license/entitlements, and /auth/license-purchase-start stay hidden in
public demo mode and those lifecycle flows must not depend on licensed
identity, plan labels, upgrade reasons, checkout handoff state, or observed
usage counts surviving the public-demo contract.
That same demo-safe runtime contract keeps monitored-system capacity posture
out of public-preview runtime capabilities. Lifecycle-adjacent install or
reporting surfaces may still depend on demo-safe capability flags, but they
must not expect monitored_system_capacity, admission-freeze copy, or
observed plan overage posture to exist.
That shared runtime-capabilities exception may also carry paid-runtime block
records for private Pro runtime hooks. Lifecycle-adjacent flows must treat
those records as executable-runtime identity, not as agent enrollment,
installer success, or lifecycle capability evidence, and must leave private
runtime download guidance to the licensing/settings surfaces that own it.
The same presentation-policy split governs paid lifecycle extensions in
ordinary self-hosted v6 installs. presentationPolicy.hideUpgrade defaults
true for free self-hosted sessions, and demo mode or white-label runtimes also
force the stronger commercial suppression boundary. Default Infrastructure
navigation must not advertise agent-profile upgrades, trial prompts, or paid
helper links. Agent lifecycle flows themselves carry no commercial content.
This presentation posture does not alter agent auth, registration, or the
security-status auth snapshot.
The normal Infrastructure installer also follows that contract. Agent-command
execution controls may describe the runtime trust and command-execution effect,
but their default labels and tooltips must not mention Pro requirements or paid
upgrade posture while they are part of the ordinary host-install workflow.
That same demo-hidden API boundary also keeps runtime-admin operations out of
public lifecycle flows: /api/diagnostics,
/api/diagnostics/docker/prepare-token, and /api/logs/* must return 404
in demo mode instead of exposing runtime bundles, log streams, or diagnostics
payloads through a nominally read-only preview account. GET and HEAD reads
for /api/admin/users and manual discovery at /api/discover are part of
that same hidden boundary; lifecycle-adjacent UI must not rely on those routes
remaining discoverable in public demo mode.
Lifecycle-owned browser shells must also defer any commercial helper reads
until that presentation policy resolves so demo suppression stays fail-closed
during first render instead of racing hidden commercial endpoints from shared
setup or install surfaces.
The governed exception is
frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx: because
that first-run completion surface renders before the authenticated shell has
mounted frontend-modern/src/useAppRuntimeState.ts, it may issue the local
commercial posture bootstrap needed for trial and upgrade posture, and it may
force-refresh that posture after a successful trial start. Other
lifecycle-adjacent authenticated-shell surfaces such as
frontend-modern/src/components/Settings/useNodeModalState.ts and
frontend-modern/src/components/Settings/useAgentProfilesPanelState.ts must
consume the shared posture owner instead of reintroducing their own mount-time
commercial reads.
Even on that governed first-run exception, render-time commercial gating must
not revive trial-status selectors or raw commercial-posture reads inside
SetupCompletionPanel.tsx; first-run can link to explicit plan or support
handoff only where presentation policy allows.
That same shared boundary now also owns the one-time checkout-return lookup:
lifecycle-adjacent surfaces may initiate billing or account handoff through
shared public routes, but they must never persist, derive, or replay the
server-owned portal checkout state or owned billing purchase-arrival state as
lifecycle state.
Lifecycle-adjacent storage and fleet surfaces now also depend on one governed
physical-disk history transport. When agent-backed disk telemetry is rendered
through shared drawers or lifecycle-adjacent resource context, those reads
must flow through the canonical /api/metrics-store/history boundary and the
disk MetricsTarget.ResourceID that monitoring projects for the resource,
rather than reviving a browser-local collector or a lifecycle-only
agent/device identity.
That shared metrics-history boundary may enforce commercial history windows
such as Relay 14-day and Pro 90-day retention for operator charts, but lifecycle
surfaces must treat those windows as presentation entitlements only. Agent
registration, heartbeat, installer status, and fleet freshness must not infer
lifecycle truth from whether a longer chart range is enabled or denied.
That same adjacent API boundary now also owns internal demo-fixture runtime
gating. Lifecycle-adjacent install, reporting, and demo-facing flows may
share mock-mode handlers in dev and test, but release builds must authorize
runtime mock rewiring only through the internal demo_fixtures entitlement,
and browser-facing lifecycle surfaces must not infer or persist that internal
grant from public runtime-capabilities or presentation-policy payloads.
Shared workload-chart reads that lifecycle surfaces reuse must stay
presentation-only on that same boundary:
internal/api/chartapi/service.go may batch those reads in parallel, but it
must request only the canonical rendered metric set for workload cards instead
of widening the hot path back to fetch-all metrics on behalf of install or
reporting callers.
The same presentation-only rule applies when shared infrastructure-summary or
workloads-summary chart routes serve a short cached response for repeated
org/range/scope requests: lifecycle-adjacent surfaces may render those charts
as operator context, but agent registration, heartbeat, installer status,
profile assignment, reporting freshness, and fleet-control readiness must not
derive authority from the cached chart payload or its timestamp.
That shared internal/api/ dependency now also assumes hosted tenant AI and
relay bootstrap reads use one effective hosted billing lease before
lifecycle-adjacent flows inspect runtime readiness, so install and setup
surfaces do not observe a tenant-org Pulse Assistant state that disagrees
with the machine-owned hosted entitlement already backing the same instance.
That same shared internal/api/ dependency now also assumes AI settings stay
vendor-neutral on that boundary. Lifecycle-adjacent setup and infrastructure
surfaces may depend on the shared AI settings transport being available, but
they must not revive host-install or first-run branches that guess provider
model defaults once the backend owns BYOK model resolution from live provider
catalogs.
That same shared dependency now also assumes settings-driven AI enablement can
cold-start the direct Assistant runtime and approval persistence without a
prior chat session. Lifecycle-adjacent mobile pairing and setup flows depend
on /api/ai/approvals becoming ready from the first governed settings save,
not only after some earlier process-start or chat-start side effect has
already initialized the approval store.
That same shared dependency now also assumes mock-mode Assistant runtime
startup is AI/runtime-only. When mock mode is active, internal/api/ai_handler.go
may enable the Assistant runtime in memory so /api/ai/chat can exercise the
typed mock stream without configured providers, but that effective config must
not become agent enrollment, install readiness, command approval authority, or
persisted lifecycle setup state.
That same shared internal/api/ai_handler.go dependency now also assumes
Assistant finding handoffs may include a model-only factual finding briefing
from Patrol's unified finding and structured investigation record. Lifecycle-adjacent
setup, install, and fleet surfaces may treat that briefing as adjacent
AI/runtime guidance only; it must not become agent readiness, install status,
or lifecycle execution authority. When that briefing includes recovered
approval lifecycle metadata, action plan identity, approval policy, or dry-run
posture, those fields remain Assistant review context and must not be treated
as an agent command grant, enrollment state, update readiness, or fleet-control
decision.
Patrol run handoffs through that same shared handler and internal/api/router.go
remain metadata-driven Assistant review context only. The router may expose a
run-history lookup so AI runtime can rebuild model-only run context, but the
resulting scoped resources, runtime failure summaries, and follow-up session
metadata must not become agent command authorization, reachability proof,
install state, profile assignment, or fleet lifecycle evidence.
Assistant session undo/redo through POST /api/ai/sessions/{id}/undo and
POST /api/ai/sessions/{id}/redo follows the same adjacent AI/runtime boundary.
The restored prompt, redo availability flag, and restored message count are
conversation repair metadata only; lifecycle-adjacent surfaces must not treat
them as agent enrollment state, command authorization, update readiness, or
fleet-control evidence.
Legacy OpenCode-style Assistant file-change routes under
/api/ai/sessions/{id}/diff, /revert, and /unrevert are not
agent-lifecycle operations. If those routes are called directly, the API must
fail them as unsupported rather than presenting file diffs or reverts as
agent command rollback, enrollment repair, update rollback, or fleet-control
authority.
That same shared dependency now also assumes hosted cloud handoff authorizes
tenant org access before browser lifecycle continues. Lifecycle-adjacent opens
into hosted workspaces may depend on internal/api/cloud_handoff_handlers.go,
but the canonical contract is that a successful handoff exchange may continue
only when the handed-off account already has server-owned tenant membership.
The exchange path must derive the effective role from the existing owner/member
record, reject any handoff claim that would upgrade that stored role, and fail
closed when the tenant org still has a blank OwnerUserID instead of letting
the first owner-shaped token claim the tenant during browser session minting.
Lifecycle-owned paywalls now also follow the shared commercial navigation
contract. frontend-modern/src/components/Settings/AgentProfilesPanel.tsx and
frontend-modern/src/components/Settings/useAgentProfilesPanelState.ts may
request the canonical commercial destination from the shared license boundary,
but they must leave internal-versus-external navigation semantics to
frontend-primitives instead of hardcoding pricing URLs or tab-open behavior
inside lifecycle-owned settings surfaces.
That same lifecycle-owned settings surface must also keep assistant
availability as an app-shell fact instead of an AI-runtime fetch. Agent
Profiles may read the shared browser bootstrap availability state to decide
whether assistant affordances render, but they must not mount
frontend-modern/src/stores/aiRuntimeState.ts or call /api/settings/ai
just to decide whether to show assistant-adjacent UI.
That same platform-connections ownership now also includes mock-runtime
continuity for API-backed platforms. When /api/system/mock-mode flips a
running server between real and mock data, the canonical TrueNAS and VMware
settings routes must keep surfacing through the same Add infrastructure source
picker and handoff URLs instead of depending on process-start-only wiring or a
mock-only alternate shell.
That same lifecycle-owned mock path now also requires one shared fixture owner
for API-backed platform onboarding. TrueNAS and VMware connection-list payloads
shown in Add infrastructure must be assembled from the canonical
internal/mock/ platform fixture layer, so settings handoff metadata cannot
drift from the runtime mock inventory and shared storage/recovery context.
That same lifecycle-adjacent mock path must stay graph-first at the shared
internal/api/ boundary. When lifecycle-adjacent handlers depend on mock
platform inventory or recovery context, they must consume
internal/mock/fixture_graph.go and its graph-owned projections instead of
reintroducing snapshot-only or platform-only helper exports.
Lifecycle-adjacent summary chart consumers may still depend on shared
internal/api/chartapi/service.go transport, but any synthetic mock series on
that path must resolve through canonical resourceType and resourceID
identities rather than lifecycle-local seed prefixes, so platform handoff
surfaces do not see a different recent tail than the runtime mock inventory
they describe.
When those lifecycle-adjacent surfaces call /api/charts/infrastructure, the
shared metrics filter contract must stay authoritative through the backend
batch loader as well, so quickstart or install readouts that only render CPU
and memory do not silently pay for disk/network guest fan-out.
That same hosted continuity contract also applies to the older direct tenant
magic-link path. Lifecycle-adjacent control-plane redirects through
/auth/cloud-handoff must preserve canonical account/user/role identity in the
handoff token long enough for the tenant runtime to validate the existing org
membership and derive the bounded effective role before it lands in protected
hosted routes. Direct opens must fail closed on missing membership, blank-owner
orgs, or owner/admin role escalation attempts instead of diverging from the
newer portal exchange path by repairing org metadata on arrival.
Lifecycle-adjacent entry surfaces must also treat public hosted signup as a
server-side identity bootstrap: the signup response cannot expose or define the
owner principal, and follow-on lifecycle access must rely on the stored
organization membership reached through magic-link verification.
That same shared internal/api/ organization boundary also now assumes
self-hosted org membership is consent-backed rather than manager-written for a
target user ID. Lifecycle-adjacent setup, install, or hosted-entry surfaces
may call /api/orgs/{id}/members, but a new user must land in a pending
invitation record and become a real member only after the invited account
accepts through the canonical invitation routes. Owner transfer remains an
existing-member operation on that same boundary; lifecycle-adjacent flows may
not treat an unaccepted invitation or arbitrary userId string as a
member-shaped owner target, and they may not complete owner transfer through a
stale ambient browser cookie. The acting owner must re-enter through a fresh
browser session on the shared auth boundary before lifecycle-adjacent surfaces
can permanently reassign org ownership.
That same shared internal/api/ dependency also assumes telemetry
transparency remains explicitly system-settings-owned. When lifecycle-adjacent
setup or router work touches shared internal/api/ files, telemetry preview
and install-ID reset routes must keep reusing the canonical system-settings
trust boundary and server-owned telemetry runtime instead of borrowing agent
lifecycle proof or state ownership just because the same router surface moved.
Content-free Pulse Intelligence telemetry rollups under shared internal/api/
also remain system-settings, API-contract, and security/privacy ownership:
lifecycle-adjacent surfaces may observe that action-plan, approval,
approved-action-decision, rejected-action-decision, first-party workflow
starter, Pro activation starter, and external-agent usage is summarized, but
they must not reinterpret those pseudonymous install-level counters as agent enrollment, install
success, recovery scope, or lifecycle state.
External-agent activity may be counted when a narrow API token satisfies the
specific manifest capability scope being called, including read-only
monitoring:read context usage. That signal remains API-owned collaboration
telemetry, not evidence that a command agent installed, checked in, or applied
fleet state.
External-agent/MCP readiness may also be true for a non-expired token that
covers any Pulse MCP-published capability scope, but that readiness remains a
collaboration-surface setup signal. Agent lifecycle code must not reinterpret
it as host-agent enrollment, command-agent reachability, or applied fleet
configuration. The manifest shape alone may prove that Pulse can publish the
external-agent contract, but it must not satisfy externalAgentReady on
/api/agent/patrol-control/status without a current token that covers at
least one Pulse MCP surface capability. The legacy
/api/agent/operations-loop/status URL is only a compatibility alias. Missing
external-agent readiness must not downgrade or block first-party Patrol control
completed/resolved proof; the legacy verified_needs_mcp state is retained
only as compatibility input and must not become new lifecycle or activation
output.
The Patrol-control status projection at
GET /api/agent/patrol-control/status follows the same ownership split. Its
stage labels, next action, Patrol issue evidence count, pending approval count,
contextual collaboration count, governed action count, verified outcome count,
Patrol control starter/completed/resolved proof exposed first through
patrolControl* fields and mirrored through patrolAutonomy* compatibility
fields,
proActivationOperationsLoopStarterCount as legacy entry-point orientation,
legacy completed/resolved/value proActivation* aliases, and optional
external-agent readiness
are content-free Pulse Intelligence collaboration signals; lifecycle code must
not reinterpret them as installed-agent proof, command-agent heartbeat,
installer progress, profile convergence, or fleet-control authority.
When that projection lets an aggregate active Patrol finding or pending approval
outrank older completed/resolved loop proof, the result is still operator
orientation only; it is not host-agent liveness, install health, or command
authorization evidence.
The same route may use action lifecycle events to notice recent loop activity,
but its governance and verification counts remain action-governance signals:
governedActionCount requires approved or rejected governed-action evidence,
approvedDecisionCount and rejectedDecisionCount expose that split without
identifiers, and verifiedOutcomeCount requires an approved governed action
with verified post-action evidence. Agent lifecycle surfaces must not satisfy
those stages from generic executing, completed, or failed command-agent
state, and must not reinterpret a rejected-only no-execution terminal decision
as command-agent success or failure.
The same verified-outcome predicate backs outbound Pulse Intelligence
approved-success telemetry: a completed action result is not lifecycle proof
unless the approved action also has VerificationOutcome.Status=verified or a
canonical verification result that ran and succeeded. Agent lifecycle surfaces
must not count host-agent command completion, installer success, or generic
execution success as Patrol control resolved-loop proof.
The route's four-step operator rollup follows the same boundary: governance
step counts may represent pending approvals before a decision or
approved/rejected decision evidence after one exists, while verification step
counts may represent verified outcomes or terminal rejected decisions. Optional
MCP readiness stays in externalAgentReady, not in the operator step list.
Agent lifecycle surfaces must not read those step counts as host-agent
heartbeat, command-agent install state, or fleet-control authority.
Approved action decision telemetry may use action lifecycle events or approved
approval records as its API-owned proof source, but the exported value remains
a pseudonymous approve/reject journey counter. Agent-lifecycle surfaces must not
read that rollup as command dispatch, command-agent reachability, profile
convergence, install success, or fleet lifecycle state.
Approved execution attempt telemetry may use action lifecycle events as its
API-owned proof source, but the exported value remains a pseudonymous operations
loop counter. Agent-lifecycle surfaces must not treat that rollup as command
agent heartbeat, installation proof, profile convergence, or fleet lifecycle
state.
Approved action success telemetry may also derive from the same governed action
audit stream, but only as a content-free count of approved actions that reached
completed state with a successful result. The existing approved execution
counter remains attempt-based for compatibility; the success counter must not
export resource identifiers, actor identifiers, command text, command output,
verification details, or host-agent state.
The external-agent recent-use bit is backed by content-free authenticated
agent/MCP route activity for manifest-capable API tokens; it is not an agent
lifecycle heartbeat, install proof, or generic token-last-used proxy.
The MCP adapter recent-use bit is narrower adapter-origin telemetry for
pulse-mcp requests on that same shared agent surface. It may distinguish MCP
adapter collaboration from direct external-agent API collaboration, but agent
lifecycle surfaces must not treat it as evidence that pulse-agent installed,
checked in, accepted remote config, or executed a command.
That same shared internal/api/ai_handlers.go dependency also now assumes
Patrol-specific settings and status expansions stay Patrol-owned. When shared
AI handlers add split scoped-trigger fields, recency labels, or trigger-state
transport for Patrol, lifecycle-adjacent setup and fleet surfaces must treat
those payloads as Patrol-only runtime context and must not reinterpret them as
agent install readiness, enrollment health, or fleet-control state.
Provider preflight diagnostics on the same handler remain AI runtime readiness
context as well: lifecycle-adjacent setup and fleet surfaces may not treat
provider_auth, provider_connection, model-selection, or provider-settings
recommendations as agent registration, updater trust, or fleet-control health.
That same shared AI handler dependency also assumes direct alert-investigation
execution mode is AI/API-owned. Request-scoped AutonomousMode:false and
RequireCommandApproval:true on /api/ai/investigate-alert are Assistant
action-governance facts, not agent install readiness, command reachability, or
fleet-control capability signals.
Visible stream_idle workflow progress on that same legacy Assistant SSE
route, and on /api/ai/execute/stream, is likewise Assistant/API transport
liveness only. It must not be reused as agent heartbeat, enrollment progress,
installer status, command websocket liveness, or fleet freshness evidence.
The model-boundary sanitizer that governs how much Assistant infrastructure
context reaches cloud models (credentials and local-only resources always
withheld) is an AI-runtime privacy concern; it is not agent enrollment config,
installer readiness, command reachability, or any fleet-control capability signal.
Patrol finding chat handoffs follow the same ownership split: when
/api/ai/chat resolves a finding_id into model-only Patrol briefing,
resource, or action context, the backend-enforced autonomous_mode:false
clamp is Assistant action-governance, not agent readiness, fleet command
reachability, or enrollment health.
If the same request also carries recognized Patrol product handoff context,
resources, or action references, the API handler may merge only same-finding
secondary model context after backend-refreshed finding context; those merged
references still must not become agent enrollment, install, readiness, or
fleet-command authority.
Scoped Assistant handoffs from alert or incident surfaces may also send
bounded handoff_context, handoff_resources, and Patrol assessment
handoff_actions through /api/ai/chat without a finding_id;
lifecycle-adjacent surfaces may treat those references as AI/runtime
explanation context only, and must not reinterpret them as agent readiness,
install status, command reachability, enrollment health, fleet execution
authority, or agent command grants.
Those handoffs must remain context-only for the configured model. Lifecycle
code must not convert alert, incident, or Patrol Assistant handoffs into
pre-filled prompts, suggested prompt chips, required tool routes, or agent
lifecycle next-step instructions.
That same shared internal/api/ dependency also now assumes SSO test and
metadata-preview routes fail closed on validated outbound URL handling.
Lifecycle-adjacent setup and hosted bootstrap surfaces may depend on those
shared helpers, but they must not reintroduce raw URL concatenation,
userinfo-bearing fetch targets, or origin-root OIDC discovery drift when
operators validate identity configuration.
That same lifecycle-adjacent identity validation path now also assumes the
manual SAML test payload preserves the optional idpSloUrl alongside
idpSsoUrl on the shared API contract, so operators validating hosted
identity before first-user or first-host handoff do not silently lose logout
endpoint validation when they choose manual SAML entry instead of metadata
import.
That same shared internal/api/ dependency also now assumes post-auth browser
handoff stays on one canonical local redirect builder. Lifecycle-adjacent setup
and hosted bootstrap surfaces may depend on shared OIDC/SAML callbacks, but
they must not reintroduce per-handler returnTo shaping that can bypass the
governed local-path validation before success or error query markers are added.
That same shared auth dependency also assumes release builds do not revive the
development-only admin bypass path. Lifecycle-adjacent setup or first-session
flows may still run in non-release developer mode with ALLOW_ADMIN_BYPASS,
but release binaries must compile that env override out instead of carrying a
runtime branch that can be reopened after deployment.
That same shared auth dependency now also assumes direct auth probes fail
explicitly without overwriting narrower route-owned failures. Lifecycle-adjacent
setup-token, recovery, and install helpers may depend on shared auth wrappers,
but missing setup-token or API-token-only failures must preserve their specific
response instead of being flattened into a second generic auth body.
Agent lifecycle owns the install/register/update continuity surfaces, but it
does not own unified-resource history or control-plane timeline persistence.
Those runtime changes now travel through the shared API and unified-resource
contracts, which keeps fleet bootstrap and identity continuity separate from
resource-change recording and historical inspection.
The shared API runtime now also exposes unified-resource action, lifecycle,
and export audit reads alongside the enterprise audit surface. That read path
belongs to the API and unified-resource contracts, not to lifecycle ownership,
so the agent-install and registration lane stays focused on fleet continuity
instead of adopting execution-history persistence as a side effect.
Agent-backed action transports may receive a mutation only after the canonical
lifecycle commits a durable dispatch attempt and crosses its one-shot pre-send
boundary. The transport request_id is that attempt identity while the action
or approval field remains the canonical action identity. Timeouts and late
agent responses do not authorize lifecycle-local retry: restart recovery must
reconcile the persisted attempt without resending, and transport receipt state
must not be interpreted as Task 10 execution or verification truth.
That shared audit-read path also now requires the dedicated audit:read
token scope instead of inheriting broader settings:read access, so
lifecycle-adjacent install and registration surfaces cannot regain enterprise
audit history just by holding general settings visibility.
The connected-infrastructure reporting workspace also now treats API-backed
platform surfaces as platform-connection-managed capabilities, not host-managed
agent extensions. frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx,
frontend-modern/src/components/Settings/useConnectionsLedger.ts,
frontend-modern/src/components/Settings/useConnectionRowActions.ts, and
frontend-modern/src/components/Settings/InfrastructureSourceManager.tsx must keep
Proxmox, PBS, PMG, and TrueNAS on the shared Infrastructure API-backed path,
while only machine-installed agent, Docker, and Kubernetes surfaces
participate in host stop-monitoring scope, uninstall commands, and upgrade
actions. That same lifecycle-owned reporting contract now also owns guest-link
truth for agent rows: when a host agent is actually attached to a VM or system
container, the shared connected-infrastructure payload must preserve that
linked guest identity so the top Connections and inventory ledger can stay
scoped to top-level infrastructure instead of rendering guest-backed agents as
peer infrastructure roots.
Those unified audit list endpoints also clamp oversized limit requests to
the governed maximum, so audit history stays bounded even when callers ask
for arbitrarily large pages.
The adjacent enterprise audit-log read path now also preserves structured
store-failure codes (audit_store_busy, audit_store_unavailable) instead of
generic 500s; lifecycle surfaces may share that API layer, but they do not own
or reinterpret audit-store health.
That same shared internal/api/ dependency also now assumes hosted runtime
websocket upgrades trust the cloud proxy only through explicit tenant
PULSE_TRUSTED_PROXY_CIDRS wiring, so first-session handoff and agent-facing
live activity surfaces do not degrade into reconnect loops when a hosted
workspace is opened through the control plane. That proxy-trust boundary must
also reject wildcard trust ranges such as 0.0.0.0/0 or ::/0 at startup,
and agent-adjacent forwarded-header reads must fail closed if invalid wildcard
proxy trust configuration is present.
That same lifecycle-owned command websocket now derives an explicit
same-origin HTTP Origin header for /api/agent/ws from the canonical Pulse
base URL through internal/securityutil/websocket_origin.go, and the agent
receiver must reject missing or cross-host origins before registration.
Runtime command sockets therefore stay on the same fail-closed host/proxy
continuity contract as the browser websocket path instead of accepting
originless upgrades. That same receiver-owned admission path must also cap
concurrent websocket connections per client IP before upgrade so one source
cannot hold unbounded agent command sockets open.
That same shared helper layer also now assumes the Pulse Mobile relay runtime
credential reaches only the explicit backend-owned route inventory, so
lifecycle-adjacent setup and install flows cannot accidentally widen the
paired-device credential just by touching neighboring internal/api/ routes.
The same shared API runtime now also exposes dedicated unified-resource
timeline reads through internal/api/resourceapi/resources.go plus the bundled facet
history read used by the drawer, but those query surfaces remain owned by the
API and unified-resource contracts rather than by lifecycle continuity.
Those timeline reads also accept governed filters for change kind, source
type, and source adapter, and the underlying store owns the filtered counts so
agent lifecycle routing still stays on canonical fleet-continuity ownership
instead of re-deriving resource history locally.
Those dedicated resource timeline and facet reads are also relationship-aware
at the API boundary: lifecycle-adjacent fleet views may consume the direct plus
relatedResources history returned by internal/api/resourceapi/resources.go, but they
must not rebuild cross-resource timeline joins inside lifecycle-owned routes or
change the direct-only store default used by other callers.
The bundled facet read may also expose the selected resource's canonical
capabilities and relationships for shared drawers, but lifecycle-adjacent
surfaces must treat those fields as API/unified-resource facts rather than
agent-lifecycle-owned install, approval, or topology state.
Agent-host, Kubernetes, and runtime parentage exposed through ParentID must
therefore enter shared drawers as facet relationships from
internal/api/resourceapi/resources.go; lifecycle surfaces must not rederive those edges
from agent install state, cluster names, or local fleet table grouping.
That same shared internal/api/ boundary now also exposes a dedicated VM
inventory export route for reporting. Fleet and install surfaces may coexist
with that export, but internal/api/reporting_inventory_handlers.go and
internal/api/router_routes_licensing.go remain API-owned reporting transport,
not lifecycle-owned inventory or install behavior.
That adjacent reporting transport now also includes a reporting catalog route
whose nested VM inventory definition owns panel copy, performance report
options, export title, column schema, and filename prefixes. Lifecycle-
adjacent install and fleet surfaces may read those facts, but they must not
redefine reporting or inventory schema locally.
That catalog route is intentionally metadata-readable without the
advanced_reporting feature gate so locked admin reporting shells can stay on
the same API-owned definition before upsell; lifecycle-adjacent surfaces must
not treat that metadata visibility as permission to execute paid report/export
routes.
That same API-owned performance-report definition also governs transport-side
validation and attachment naming. Lifecycle-adjacent fleet surfaces may depend
on those downloads, but they must treat allowed formats, multi-resource caps,
optional metric/title support, default fallback range windows, attachment
filename stems, and invalid-format validation copy as API-owned reporting
contract rather than mirroring local constants.
That adjacent export contract now also carries canonical Proxmox pool
membership for each VM row. Lifecycle-adjacent install and fleet surfaces may
reuse those current-state facts, but they must still treat the pool column as
API-owned reporting data rather than introducing lifecycle-local guest
inventory assembly.
The same API serializer now also refreshes canonical identity and policy
metadata through the shared unified-resource helper before it returns
resource payloads, so lifecycle-adjacent links keep the same canonical
metadata pass as the rest of the resource API instead of composing local
attach wrappers.
That same shared internal/api/ dependency now also keeps Patrol runtime
availability explicit as API-owned state. Lifecycle-adjacent setup and install
flows may touch the shared AI handler layer, but they must not collapse a
blocked Patrol runtime back into generic healthy status just because the last
successful summary snapshot was green.
Invalid sourceAdapter values are rejected at the API boundary, so the fleet
lane continues to consume only the canonical adapter set rather than
introducing a broader compatibility escape hatch.
That same API boundary now routes the kind, sourceType, and
sourceAdapter query values through the shared unified-resource change
filter parser, so the lifecycle lane keeps the transport contract aligned
with the canonical resource-history model instead of rebuilding filter
normalization locally.
That same shared internal/api/ boundary now also keeps recovery payload
platform vocabulary canonical at the transport edge. Lifecycle-adjacent
surfaces that deep-link into recovery may still depend on those handlers, but
they must treat response platform / platforms as API-owned fields and use
legacy provider aliases only as compatibility fallback rather than reviving
provider-shaped transport assumptions in fleet flows.
That same lifecycle boundary also relies on canonical Kubernetes pod metrics
targets. Pod-facing drawers may expose MetricsTarget.ResourceID only as the
history lookup coordinate, but they must keep the prefixed
k8s:<cluster>:pod:<uid> contract and let metrics-history handlers
canonicalize any legacy bare pod ID back onto that key, otherwise pod detail
history and workload summary cards split onto different timelines.
The router now wires the tenant resource state provider during initial setup
when a multi-tenant monitor is present, so tenant-scoped fleet pages do not
trip a missing-provider 500 before the monitor has finished initializing.
The dedicated profile client now also routes list, schema, and validation
parsing through shared response helpers in frontend-modern/src/api/agentProfiles.ts,
so profile transport stays aligned with the governed API contract instead of
reintroducing local array or JSON parsing rules.
That same lifecycle-owned install/profile surface now also keeps trial-start
CTA orchestration out of ordinary authenticated self-hosted feature gates.
Agent profile paywalls may describe the entitled lifecycle capability and link
to neutral plan review, but they must not present direct trial-start copy inside
the normal Settings flow or open-code startProTrial() branches in each
lifecycle surface. Any paid handoff belongs to explicit plan, hosted,
activation, recovery, or support contexts governed by presentation policy.
The owned backend API surfaces must preserve the exact-release installer
fallback, canonical /api/auto-register behavior, and hosted org install-command
contracts instead of leaving those guarantees implied by generic API ownership.
Those shared auth/security helpers now also own the dedicated
relay:mobile:access capability that backs Pulse Mobile pairing. Lifecycle-
adjacent setup and install flows may depend on that helper layer, but they may
only consume the server-owned minting route and the governed compatibility
gates for the mobile runtime endpoints. They must not recreate broader
AI-scoped mobile credentials or invent route-local scope exceptions.
The governed backward-compatible route inventory behind that capability now
includes the Patrol attention read and lifecycle-mutation routes, which
supersede the legacy patrol findings routes the paired device already
consumed. That inventory growth keeps an already-paired phone's credential
working across server upgrades; it does not widen the mobile credential into
general AI chat/execute scope ownership, and lifecycle-adjacent flows must
not read it as license to mint broader-scoped device tokens.
That same lifecycle-adjacent setup path now also depends on the hosted relay
runtime helper inside internal/api/. Hosted Pulse Cloud tenants must not
require an operator to visit Settings and manually PUT /api/settings/relay
before Pulse Mobile pairing becomes possible. When hosted entitlements grant
relay, the shared backend helper must auto-bootstrap the canonical relay
runtime state that onboarding and relay-status reads consume, while still
preserving explicit operator-owned disablement when a real relay config was
already written.
That same hosted setup boundary also depends on tenant browser sessions staying
canonical after cloud handoff. Lifecycle-adjacent mobile pairing and hosted
admin setup routes may run without local credentials configured, but shared
internal/api/auth.go helpers must still honor a valid hosted pulse_session
before any API-only token fallback or optional-auth anonymous fallback so
operators can mint relay-mobile credentials and continue onboarding from the
hosted runtime itself even after that tenant has already minted managed API
tokens.
That same lifecycle-adjacent hosted setup path now depends on AI bootstrap
staying explicitly BYOK/local before the first settings write. Hosted
operators may land in Chat, Patrol-backed setup hints, or AI-dependent
remediation surfaces before anyone has visited AI Settings, but the shared
internal/api/ hosted-AI helper must return the same unconfigured
provider-setup state as self-hosted unless an explicit AI config exists. It
must not fabricate installation activation, quickstart credits, or a
quickstart-backed ai.enc from billing state. Historical quickstart grant
fields may stay parseable in billing state for old files, but hosted setup and
pairing must not treat them as active AI inventory while entitlement refresh
rewrites lease-backed plan and capability data.
That same shared entitlement refresh path must also keep hosted effective-org
ownership canonical for lifecycle-adjacent routes: when pairing or relay-mobile
bootstrap arrives scoped to a tenant org with no org-local lease, the refresh
must target the instance-level default billing lease and evaluator instead
of persisting a second empty tenant copy. Otherwise hosted pairing falls back
to free-tier behavior even though the machine already carries the paid hosted
lease.
The same setup boundary also depends on canonical org-management privilege
surviving the next step: once the request is scoped to a hosted tenant org,
shared internal/api/security_setup_fix.go helpers must allow that org's
owner/admin membership to exercise settings-bound pairing routes instead of
requiring a separate configured local admin username that does not exist on
hosted tenants.
The same setup boundary also owns the dedicated relay-mobile bootstrap read:
once the backend mints the server-owned Pulse Mobile credential, the QR,
deep-link, and validation reads in internal/api/router_routes_ai_relay.go
must accept that relay:mobile:access scope directly instead of demanding the
broader settings-read privilege that the pairing token was never meant to
carry.
That same bootstrap dependency assumes the API-owned onboarding readiness
gate remains authoritative. Lifecycle-adjacent setup must surface the backend
409 onboarding_not_ready diagnostics when relay registration, Remote Access,
or the dedicated mobile credential is incomplete, rather than generating a
partial QR/deep-link payload or retrying through broader settings-read
credentials.
That same adjacent internal/api/ reporting surface also keeps lifecycle-
adjacent automation on the canonical time-window transport contract. Any setup,
handoff, or scheduled lifecycle flow that triggers performance reports must
treat reporting start/end values as optional RFC3339 fields owned by the
API contract, with malformed or inverted ranges rejected as
400 invalid_time_range rather than silently drifting to a fallback window.
Those same lifecycle-triggered reporting calls must also stay inside the
API-owned metricType/title limits and the strict multi-report JSON body
rules instead of assuming the backend will coerce malformed payloads into a
best-effort report.
When those lifecycle-adjacent calls fail validation, adjacent automation should
rely on the API-owned error codes rather than message-text heuristics, because
the backend contract owns the reporting validation classification.
The API-backed platform onboarding surface now lives inside the shared
Infrastructure workspace. InfrastructureSourceManager.tsx,
connectionsTableModel.ts, InfrastructureWorkspace.tsx,
infrastructureWorkspaceModel.ts, InfrastructureInstallerSection.tsx,
useInfrastructureSettingsState.ts, useTrueNASSettingsPanelState.ts,
useVMwareSettingsPanelState.ts,
proxmoxSettingsModel.ts, useInfrastructureConfiguredNodesState.ts, and
useInfrastructureDiscoveryRuntimeState.ts own the fallback
install/direct/reporting operator flow, with InfrastructureSourceManager.tsx
plus connectionsTableModel.ts as the canonical top-level infrastructure
ledger and the governed add/edit modals as the API-backed add/edit surface.
Operator-facing setup copy should use Add infrastructure and source-strategy
language for the shared Infrastructure onboarding path
(/settings/infrastructure?add=pick) rather than reviving the standalone
PlatformConnectionsWorkspace.tsx shell or the old Platform connections
label.
That infrastructure destination now has one canonical mental model:
configured infrastructure sources stay visible on the landing page as the
primary objects the operator manages. The landing table is instance-first, not
type-first: existing connections or agent-backed hosts render inside one
platform-banded systems ledger, each platform section owns its own Add
action, and the page does not fork back into a second monitored-systems ledger
below. The default ledger prioritizes system identity, collection coverage,
health/last activity, and actions; raw management addresses remain in the
governed detail flow or an explicitly expanded cluster-member row. Cluster
members are collapsed under an accurate node count by default so member rows
cannot make the top-level connected-system count appear contradictory.
Adding infrastructure therefore happens in two governed steps. The
?add=pick modal owns grouped source-type selection and may offer
Detect API platform as a secondary utility. The ?add=detect modal owns
probe-driven handoff into ConnectionEditor.tsx; its outer shell title and
description must also use API-platform / management-API-endpoint language so
operators do not mistake the probe for Unraid, Docker, or generic Pulse Agent
host detection. Typed add routes jump
straight into the matching credential or install body. The shared Settings
sidebar still owns only the top-level Infrastructure destination; movement
between landing, picker, detect flow, add form, and edit form belongs to
explicit actions inside InfrastructureWorkspace.tsx, not extra sidebar
entries or body-replacing workspace subtabs.
That same landing/table contract now also owns collection-method phrasing.
connectionsTableModel.ts, useConnectionsLedger.ts, and
InfrastructureSourceManager.tsx must preserve the same plain-language
collection identity (via platform API, via Pulse Agent, or via platform API and Pulse Agent) from the shared ledger contract. The compact landing may
render its API / Agent / API + Agent badge beside the system name, with the full
phrase available through accessible metadata and the detail flow, instead of
spending a separate table column on collection method.
Source badge class selection may use semantic gray treatment for API-only rows
and typed non-gray tones for agent, probe, or combined sources, but the source
identity remains the API/Agent/Probe label and subtitle from the shared ledger
contract rather than a color-only cue.
That same lifecycle-owned platform onboarding boundary must keep API-backed
provider state operationally useful, not CRUD-only.
useTrueNASSettingsPanelState.ts and useVMwareSettingsPanelState.ts must
surface shared runtime health, poll cadence, discovered contribution summary,
and canonical infrastructure / workloads / storage / recovery handoffs coming
from the saved-connection APIs instead of falling back to provider-local
inference or agent-first setup guidance. Saved-connection retests must use the
server-owned test routes, must allow masked-secret continuity on edit, and
must refresh the shared connection-summary state after a save or retest
completes. A TrueNAS edit always sends the fingerprint field: a non-empty value
updates the TLS pin and an explicit empty string clears a previously persisted
pin. Omitting the field when the operator clears it would silently retain stale
trust state and is outside the saved-connection lifecycle contract.
frontend-modern/src/utils/clusterEndpointPresentation.ts and
frontend-modern/src/utils/proxmoxSettingsPresentation.ts remain part of that
same governed lifecycle surface, so endpoint reachability state,
discovery-prefill defaults, and variant copy do not drift into card-local
strings or prefill assembly.
That same platform-onboarding boundary also defines the agent-optional rule
for API-backed platforms. TrueNAS and VMware may surface Assistant control,
diagnostics, configuration reads, and runtime insight through the backend-owned
connection and polling path, but adjacent lifecycle flows must not start
treating a unified-agent install as the required bootstrap for provider-backed
operations.
That same boundary also defines the only acceptable VMware phase-1 path:
vCenter under the shared Infrastructure onboarding flow. Lifecycle-adjacent
flows must not invent a VMware-only setup shell, direct-ESXi branch, or
agent-first bootstrap story just because the runtime now has a live VMware
connection panel and poller.
That same platform-onboarding boundary also owns demo/mock continuity for API-
backed settings surfaces. When /api/system/mock-mode is enabled, provider
fixtures and their downstream infrastructure/workloads/storage/recovery
handoffs must still read from internal/mock/fixture_graph.go, so
operator-facing demos stay coherent across those adjacent product surfaces
without a restart.
That same lifecycle-owned platform-onboarding boundary also owns configured
Proxmox, PBS, and PMG replacement continuity. Node update handlers must pass
the current platform surface into monitored-system admission through the shared
structured replacement selector so host or name edits preserve the intended
slot without reintroducing lifecycle-local matcher closures or empty-estate
fallbacks.
That same shared router boundary must treat infrastructure summary chart
normalization as summary-only presentation transport: long-range chart bucket
shaping may improve operator-facing summary readability, but it must not be
reused as lifecycle freshness, heartbeat, or enrollment-state authority.
That same shared chart boundary may resolve provider-backed workload history
through unified metrics targets, but emitted workload IDs must stay on the
canonical /workloads row contract so lifecycle settings, reporting, and
handoff surfaces never depend on provider-native metric keys.
That same lifecycle-owned settings slice now also owns the shared VMware
handoff framing. InfrastructureWorkspace.tsx,
useInfrastructureSettingsState.ts, and
useSettingsInfrastructurePanelProps.ts must surface VMware availability and
connection counts from the same shared infrastructure settings state that owns
the inline VMware credential flow itself, rather than letting adjacent setup
surfaces grow a second VMware availability fetch or a VMware-only handoff
path.
That same infrastructure workspace boundary now also owns the first-run
handoff copy for new operators. InfrastructureWorkspace.tsx must keep
platform API inventory and Pulse Agent telemetry explicit in the shared
workspace instead of leaving first-session guidance implicit in generic
settings-shell prose or retreating to one provider's name or one onboarding
mode as the primary story.
That same first-run infrastructure handoff now also owns the instance-first
add flow. InfrastructureSourcePicker.tsx must present grouped source types
only after the operator deliberately clicks Add infrastructure, and
ConnectionEditor.tsx must stay focused on detect-driven handoff and the
selected type's form body instead of reviving a second top-level catalog.
The default picker must present the generic Linux, macOS, and Windows host
journey once through the primary Install Pulse Agent path instead of
duplicating it as a specific-source card. The canonical host catalog item must
remain searchable by platform names and aliases so deliberate catalog lookup
still reaches the same linux-host installer route with full platform
coverage.
Product grouping belongs to the governed platform-support presentation helper,
not lane-local card lists. Detect utility copy must stay provider-neutral and
operationally plain, and returning from a chosen credential slot to detect
must reset probe input and result state rather than reopening the editor with
stale no-match or detected-product state already rendered. Render-order proof
for that landing belongs to DOM-backed settings tests, not raw source-string
position checks, so lifecycle ownership continues to guard the operator-visible
order after reasonable component extraction or copy refactors.
When that infrastructure workspace needs to redirect operators to the plan-
owned self-hosted commercial surface for billing, license status, or paid
feature activation, it must
consume the settings-owned referral copy from
frontend-modern/src/components/Settings/selfHostedBillingPresentation.ts
instead of carrying workspace-local commercial guidance or reaching back into
generic commercial presentation helpers from the hosted infrastructure route.
Shared licensing routes under internal/api/ must not retain normal-product
upgrade-metrics route names for compatibility. Lifecycle-adjacent settings
and install flows must treat that route family as retired local commercial
analytics, not as a reason to reintroduce default self-hosted upgrade prompts
or local handoff event capture.
That canonical /api/auto-register behavior now also includes hostname/IP continuity:
reruns that arrive through a different canonical host form must reuse the same
Pulse-managed node record and token instead of forking duplicate fleet entries.
That same lifecycle contract also governs the runtime-side Proxmox setup host
selection in internal/hostagent/proxmox_setup.go: an explicit report-IP
override remains the highest-priority operator-provided endpoint, and otherwise
the route-aware local IP used to reach Pulse is the preferred registration
host. A resolvable system hostname remains an ordered fallback candidate, not
the primary generated host, so Pulse can still select it when the server-side
fingerprint probe proves that hostname is the correct reachable endpoint. The
candidate list plus hostname/IP continuity is the canonical place for DNS
continuity; runtime setup must not prefer a short hostname over the
route-aware interface merely because local resolution succeeds. Heuristic local
IP selection is only a fallback after the explicit override, route-aware IP,
and resolvable-hostname candidate paths.
That same Proxmox registration boundary must now also let Pulse choose from the
agent's ordered candidate host list instead of blindly persisting the agent's
first preference. Unified Agent setup must send canonical candidateHosts
alongside the preferred host, and /api/auto-register must store the first
candidate that Pulse can actually reach for fingerprint capture from its own
network view so mixed-DNS and split-network installs do not register a host the
server itself cannot use afterward. That same selection path must only persist
VerifySSL=true when Pulse actually captured a certificate fingerprint for the
selected host; if every candidate fingerprint probe fails, registration must
fall back to the preferred normalized host with strict TLS disabled instead of
pretending public-CA verification is now safe for a self-signed Proxmox node.
Cluster-member Proxmox agents that match a non-primary endpoint of an existing
cluster source are already registered for lifecycle purposes, even when the
cluster source's primary connection is currently disconnected. Only the primary
configured endpoint may drive disconnected-source repair and token rotation; a
covered member endpoint must not rotate the cluster token just because it can
reach /api/auto-register.
A covered member's re-registration must still land its address: canonical
auto-register matches the agent against cluster member endpoints (address
identity against the candidate list first, then an unambiguous corosync
node-name match) instead of creating a standalone instance for consolidation
to fold back in and silently discard. The Pulse-verified selected host is
adopted as that member's ClusterEndpoints[n].IPOverride (the durable field
re-discovery preserves and polling prefers) together with the certificate
fingerprint captured from that address; an admin-managed override absent from
the agent's candidate list is preserved, mirroring the top-level stored-host
preservation rule. The only credential writes a member match may perform are
a same-token-identity secret refresh (the agent rotates its own token in
place on reinstall, so the stored secret is already invalid) and full token
promotion onto a cluster source that has no credentials at all; a member's
distinct per-node token must never replace working cluster credentials.
That same canonical behavior also includes one auth transport for Proxmox
completion: runtime-side Unified Agent and script callers must send /api/auto-register
authentication through a one-time setup token in the request-body
authToken field instead of keeping either a header-auth compatibility path
or a long-lived admin-token completion path alive.
That same first-session lifecycle boundary also owns bootstrap-token
recovery: the supported operator path is pulse bootstrap-token, and the
runtime may not keep .bootstrap_token as an unstructured plaintext secret
file after startup. Canonical persistence must encrypt the bootstrap token at
rest and rewrite any legacy plaintext bootstrap-token file immediately into
the encrypted canonical format on load. Automatic startup logs may surface the
token file path for local recovery, but they must never print the bootstrap
token value itself into stdout, systemd journal, Docker logs, or Kubernetes
pod logs. The validation endpoint for that same bootstrap token must also
rate-limit per client and return an explicit Retry-After backoff instead of
offering an unbounded brute-force surface during first-run setup.
That same deploy/install runtime boundary also owns peer-node SSH trust.
internal/hostagent/commands_deploy.go must resolve and persist peer host
keys through the managed ssh_known_hosts store before any automated deploy
fan-out writes a bootstrap token or runs the installer on a remote node, keep
StrictHostKeyChecking=yes, and fail closed on key mismatch or missing-host-
key state instead of downgrading to unauthenticated SSH during install.
That same boundary also owns least-privilege peer deploy execution: when
operators configure a non-root SSH user for deploy fan-out, privileged token
write and install steps must escalate through non-interactive sudo on the
remote node instead of hard-coding root@ for every SSH hop or silently
falling back to a second unaudited privilege path.
That same transport boundary also keeps plaintext Pulse URLs local/private.
pkg/securityutil/httpurl.go, surfaced internally through
internal/securityutil/httpurl.go, owns the canonical Pulse transport
normalization used by internal/hostagent/agent.go,
internal/hostagent/commands.go, internal/agentupdate/update.go,
internal/dockeragent/agent.go, internal/kubernetesagent/agent.go, and
internal/remoteconfig/client.go. Those runtime clients may keep self-hosted
http:// or ws:// only for loopback, private/link-local IP,
carrier-grade NAT 100.64.0.0/10, single-label, or local DNS Pulse origins;
public remote Pulse URLs must still use HTTPS/WSS.
For dotted local DNS Pulse origins, the validator must either recognize the
name as an operator-local namespace or resolve it and require every returned
address to remain loopback, private/link-local, or carrier-grade NAT; mixed
public and local resolution must fail closed to HTTPS/WSS.
InsecureSkipVerify may relax certificate verification on TLS transport; it
must not reopen public plaintext HTTP for updater, websocket, reporting, or
remote-config paths.
That same first-run lifecycle boundary also keeps unauthenticated setup local.
Lifecycle-adjacent quick setup or recovery entrypoints may exist before an
operator has configured auth, but they must stay direct-loopback only and any
recovery token/session path must stay bound to the generating localhost client
instead of reopening auth for all loopback callers.
That same shared internal/api/ lifecycle boundary also assumes tenant-scoped
resource helpers stay on canonical unified-resource seeds: adjacent fleet and
install surfaces may not revive raw tenant StateSnapshot fallback through
shared API resource wiring once UnifiedResourceSnapshotForTenant exists.
That same shared internal/api/ dependency now also includes the monitored-system
ledger support read: lifecycle-adjacent inventory and billing surfaces may
show the counted monitored systems coming from agent-backed infrastructure, but
the shared API helper must expose the canonical unified-resource grouping
explanation instead of rebuilding count reasons from install or registration
state.
That shared ledger read must also preserve canonical grouped system status,
including warning, so lifecycle-adjacent operator surfaces do not mislabel
live agent-backed infrastructure as Unknown when the unified-resource layer
already resolved a governed degraded state.
That same ledger read now also carries backend-owned status explanation copy,
and lifecycle-adjacent details must render it beside the counting rationale so
operators can interpret warning, offline, and unknown states without inventing
local status semantics.
Those status details are now structured as well: lifecycle-adjacent consumers
must preserve the canonical reason list from the ledger read so operators can
see which grouped source or surface degraded and its canonical reported_at
timestamp,
instead of only seeing a generic warning/offline paragraph.
That same ledger read also treats the canonical latest_included_signal
object as the freshest included grouped observation. Lifecycle-adjacent
consumers must not label it with generic single-source health wording, and
should use the canonical object when they need attribution for which grouped
surface reported most recently. Retired flat alias fields must not reappear as
parallel lifecycle signal inputs or contract language.
Lifecycle-adjacent workspace copy must also keep the same commercial framing:
infrastructure operations may point operators to Plans for billing, but it
must describe that boundary in license-status and unlocked-capability terms
rather than reviving monitored-system plan limits, legacy agent-allocation
language, or treating the entire destination as the Pulse Pro tier page.
That same direct-workspace boundary now also owns the shared customer-facing
error copy for discovery and configured-node actions through
frontend-modern/src/utils/infrastructureSettingsPresentation.ts, so direct
Proxmox settings mutations do not drift back to inline toast text inside the
runtime hooks.
That same fleet lifecycle boundary now also owns the shared capability,
status, and inventory presentation helpers that those settings surfaces reuse.
frontend-modern/src/utils/agentCapabilityPresentation.ts,
frontend-modern/src/utils/agentProfileSuggestionPresentation.ts,
frontend-modern/src/utils/configuredNodeCapabilityPresentation.ts,
frontend-modern/src/utils/configuredNodeStatusPresentation.ts,
frontend-modern/src/utils/unifiedAgentInventoryPresentation.ts, and
frontend-modern/src/utils/unifiedAgentStatusPresentation.ts are the
canonical owners for agent capability badges, profile suggestion formatting,
configured-node capability/status badges, monitoring-stopped inventory copy,
and unified-agent status labels. Lifecycle-adjacent settings and inventory
surfaces should extend those helpers instead of reintroducing inline fleet
semantics in panels, workspace models, or reporting hooks.
That same boundary now also assumes canonical resource payloads preserve
shared facet totals through facetCounts, so the resource list and detail
surfaces can keep row summaries aligned without re-inferring totals from
consumer-local slices.
That same shared facet bundle now also carries grouped recentChangeKinds
counts by canonical change kind, so the lifecycle-adjacent detail surfaces can
report restart, anomaly, and other timeline distribution without rebuilding
timeline math in the browser.
That same shared facet bundle now also carries grouped
recentChangeSourceTypes counts by canonical source type, so the
lifecycle-adjacent detail surfaces can distinguish platform events, pulse
diffs, heuristics, user actions, and agent actions without re-inferencing the
provenance mix in the browser.
That same shared facet bundle now also carries grouped
recentChangeSourceAdapters counts by canonical source adapter, so the
lifecycle-adjacent detail surfaces can distinguish Docker, Proxmox, TrueNAS,
and ops-helper provenance without re-inferencing the integration mix in the
browser.
Timeline entries surfaced through that same boundary also preserve
relatedResources correlation context for non-relationship changes, so adjacent
fleet and install surfaces can link the affected neighbors without trying to
reconstruct correlation context from the raw resource payload alone.
That same shared internal/api/ boundary now also assumes tenant AI services
stay on canonical Patrol runtime wiring: adjacent fleet and install surfaces
must not revive tenant snapshot-provider bridges through shared AI handler
setup once Patrol can initialize from tenant ReadState and unified-resource
providers directly.
That same boundary now also assumes the Patrol-backed recent-changes API
surface reads through the canonical intelligence facade first, so adjacent
fleet and install surfaces do not bypass the shared unified timeline through
the old detector-only handler path.
The Patrol-backed correlation API surface must follow the same canonical
intelligence-facade path, so fleet and install surfaces do not need to know
about the detector directly when they render learned relationship context.
That same canonical /api/auto-register response must stay on one completion
truth: caller-supplied Proxmox credentials complete registration with a
direct-use action, and the runtime no longer preserves a dead pending-secret
placeholder state. That same response
must also stay truthful
about lifecycle state: it may not claim the node is already registered
successfully while local token creation is still outstanding.
That same first-hop lifecycle boundary must validate that response shape
instead of trusting HTTP success alone: runtime-side Unified Agent and installer callers must
require the canonical status="success" plus action="use_token" response
contract before treating registration as complete.
That same canonical response contract must also carry the runtime-owned
identity truth back to those callers: type, source, normalized host, and
matching nodeId/nodeName must describe the resolved stored node record, and
installer/runtime-side Unified Agent success reporting must use that returned canonical node
identity instead of the caller's pre-registration serverName.
The canonical /api/auto-register response must preserve canonical
node identity: nodeId must carry the resolved stored node name rather than
the raw host URL or requested serverName, so every live registration caller
stays aligned with saved fleet state.
That same /api/auto-register boundary must also preserve canonical
live-event identity: the node_auto_registered WebSocket payload must emit the
normalized stored host plus the resolved stored node name in name, nodeId,
and nodeName, rather than broadcasting raw request fields that can drift
from the saved node record.
That same runtime-side Unified Agent boundary also owns one canonical ingest
name through internal/api/agent_ingest.go and internal/api/router*.go:
the primary runtime surface is the Unified Agent report/config boundary, while
the /api/agents/host/* routes remain compatibility aliases only and may not
re-emerge as the primary lifecycle concept in router state, handlers, or
proofs.
The remote-config side of that same Unified Agent boundary now also carries a
backend-owned desired-config fingerprint. Monitor.GetHostAgentConfig must
compute the metadata after profile settings and command enablement decisions
have been merged. /api/agents/agent/{id}/config must keep the existing
config signature backward-compatible with installed agents by signing the
legacy canonical payload shape only; newer clients validate desiredConfig by
recomputing it from the signed command decision and signed settings payload,
restricted to the agent-applied settings key schema. Current Unified Agent
reports carry the comparable applied fingerprint, and /api/connections owns
managed-config convergence by comparing those fingerprints. Agent Doctor must
not demand the separate legacy profile-deployment acknowledgement that current
agents do not emit or label its absence as deployed version zero; it may still
surface explicit persisted failed, pending, or version-drift records. The empty
default desired config is different: it may be signed and served for
compatibility, but without a managed command decision or agent-applied setting
it is not a rollout and must project as configDrift: not-applicable with a
current applied rollout.
Command policy follows the same lifecycle truth boundary: the server desired
command setting is not applied runtime truth until a current agent report
confirms it. /api/connections must expose desired command policy, applied
agent truth, effective enforcement, and bounded reason separately so lifecycle
surfaces can show desired-disabled/applied-enabled and
desired-enabled/applied-disabled as drift or attention, and no-report cases as
pending or unknown rather than in-sync.
That same canonical /api/auto-register path must also complete the live
post-registration contract after persistence: it must trigger discovery refresh
and emit the canonical node_auto_registered WebSocket payload instead of
stopping at a backend-only save/response path.
That same post-registration discovery update must keep structured error
ownership in discovery runtime state: lifecycle handlers may broadcast the
deprecated string errors list only as a compatibility field derived from
canonical structured_errors, not as a second live discovery owner path.
That same canonical /api/auto-register path must also accept caller-supplied Proxmox token
completion for confirmed runtime-side Unified Agent or script flows, so live registration
surfaces stay on one governed completion contract instead of inventing a
second explicit-token endpoint outside /api/auto-register.
On the PVE side, only tokens that previously came back through a completed
source="agent" or source="script" auto-register flow count as reusable
confirmed credentials, so interrupted runs cannot harden a false use_token
state from any non-canonical token placeholder.
The canonical setup-script path must stamp that same source="script" marker
on /api/auto-register payloads, and canonical registration callers must send
that source explicitly, so confirmed script-created tokens stay distinguishable
from agent-created tokens across later canonical reruns.
That same canonical request contract must also reject any non-canonical source
marker: /api/auto-register accepts only source="agent" and
source="script" so v6 does not preserve arbitrary caller labels as a hidden
compatibility surface.
That same canonical request contract must also reject any non-canonical node
type: /api/auto-register accepts only type="pve" and type="pbs" so
unsupported runtime labels cannot slip through as fake successful fleet
registrations.
That same canonical request contract must also reject non-canonical token
identities: /api/auto-register accepts only Pulse-managed
pulse-monitor@{pve|pbs}!pulse-... token ids, so v6 does not preserve
arbitrary, cross-type, or non-Pulse-managed token labels as successful
registration state.
That same canonical token identity must also stay deterministic across live
callers: install.sh, generated setup scripts, and runtime-side Unified Agent-driven Proxmox
registration must all create the same Pulse-managed pulse-<canonical-scope-slug>
token name for a given Pulse endpoint instead of letting one caller drift into
timestamp-suffixed or rerun-local token identities.
The corresponding node setup modal owner is now an explicit shell-plus-sections
surface:
ConnectionEditor/CredentialSlots/NodeCredentialSlot.tsx composes
NodeModalBasicInfoSection.tsx, NodeModalClusterMembersSection.tsx,
NodeModalAuthenticationSection.tsx, NodeModalMonitoringSection.tsx,
NodeModalStatusFooter.tsx, nodeModalModel.ts, and useNodeModalState.ts.
The cluster members section is the canonical manual override surface for
per-member connection addresses on an existing PVE cluster: it writes only
ClusterEndpoints[n].IPOverride through the write-only
clusterEndpointOverrides node update payload built by
buildClusterEndpointOverridesPayload in nodeModalModel.ts (changed
members only; blank clears), because discovered member Host and IP are
rebuilt on every cluster re-discovery while IPOverride is preserved and
preferred at poll time. Server-side agent re-registration adoption remains
the automatic path for agent-managed members; this editor surface is the
lifecycle path for members without an agent. The configured-nodes cache in
useInfrastructureConfiguredNodesState.ts must mirror saved overrides onto
its cached clusterEndpoints rather than spreading the write-only payload
field onto node config state.
That same node setup owner also includes
frontend-modern/src/utils/nodeModalPresentation.ts, which now owns the
canonical node-type defaults, endpoint/auth placeholders, monitoring coverage
copy, and test-result styling for PVE, PBS, and PMG setup.
For PBS connections, the monitoring section also owns the datastore collection
scope editor beside the datastore and backup monitoring toggles. It must
round-trip the stored excludeDatastores list, accept comma-separated exact or
wildcard patterns, and submit the canonical collection without changing agent
identity, credentials, registration, or connection lifecycle.
That presentation layer remains presentation-only for those API-managed
Proxmox, PBS, and PMG connections. Lifecycle guidance in that settings surface
may explain monitored-system grouping, but monitored-system volume caps are
retired and must not reappear as a modal-local rule or exemption path.
That same deterministic token-identity contract also applies to backend-owned
turnkey Proxmox token creation: generated setup scripts and the password-based
PBS add-node path must derive Pulse-managed token names from the canonical
Pulse endpoint itself rather than request-local Host fallbacks, so loopback
or proxy-facing admin requests cannot fork monitor-token identity for the same
Pulse instance.
That same generated setup-script path must now complete registration through
the canonical /api/auto-register contract itself: locally created Proxmox tokens must
be submitted directly on the canonical contract instead of diverging into a
second registration shape.
That same setup bootstrap surface must also keep canonical request handling
aligned across /api/setup-script-url and /api/setup-script: unsupported
node types may not drift into implicit PBS script generation, and the direct
setup-script route must normalize the supplied host before emitting script text
or rerun URLs so the bootstrap artifact and downloaded script stay on the same
node identity.
That same setup bootstrap surface must also stay owned by one backend bootstrap
artifact builder: /api/setup-script-url response fields, setup-token hinting,
download URLs, script filenames, and the generated script's rerun command must
all derive from the same canonical bootstrap contract instead of being rebuilt
as separate handler-local shell snippets.
That same canonical request contract must also keep one-time setup-token auth
on a single field: /api/auto-register accepts authToken as the governed
request payload key and may not preserve a parallel setupCode alias.
That same governed runtime path must also keep its active auth terminology on
setup tokens instead of setup-code residue: config_setup_handlers.go,
config_handlers.go, and their direct proofs must model the one-time
credential as a setup token in runtime names, logs, and auth failure text.
That same auth failure contract must also fail specifically on the canonical
setup-token requirement: missing authToken input on /api/auto-register may
not collapse back to a generic authentication message once the route is
governed as setup-token-only.
Shared router auth bypass for that setup-token route remains a handler-ownership
mechanism only: lifecycle-adjacent routes may bypass global auth so they can
return their route-specific setup-token failure, not so unauthenticated callers
can fall through to a successful lifecycle mutation.
That same canonical request contract must also keep field-validation failures
specific: mismatched tokenId/tokenValue input may not collapse into
generic missing-field output, and other missing canonical fields must return
explicit Missing required canonical auto-register fields: ... guidance.
That same owned setup and auto-register boundary participates in the canonical
monitored-system grouping model without commercial volume admission. A new
/api/auto-register completion may project whether it dedupes onto an
existing top-level monitored system or creates a new one, but the lifecycle
surface must not block API-backed monitoring on a self-hosted or hosted
monitored-system cap.
That grouping projection must come from the same canonical prospective
monitored-system projection the runtime uses for final grouped counting.
Auto-register may preview its own candidate, but it must not keep a
lifecycle-local counter, drift on source priority, or treat missing grouping
usage as a commercial admission state.
The retired private monitored-system admission policy hook must not return as
a lifecycle-local branch or exemption rule.
When lifecycle-adjacent setup or support surfaces need to explain why a
candidate would count or dedupe, they must consume the shared monitored-system
ledger preview contract rather than rebuilding a second preview model from
setup-local transport fields. frontend-modern/src/components/Settings/MonitoredSystemImpactPreview.tsx
is the shared shell for that explanation inside platform-connections settings,
so provider-specific panels must not fork their own monitored-system preview
copy or inline projected-usage rendering.
That shared shell must use neutral count-impact language for ordinary platform
connection previews. Previews must not describe "capacity", finite policy
failures, or raw current / limit quota math as the operator-facing mental
model for monitoring.
That same grouping readiness boundary assumes settled canonical usage, not the
first non-nil monitor view. Lifecycle-owned setup or first-host surfaces may
not display counted-system totals as final against a provider-owned
supplemental platform such as TrueNAS or VMware until the monitor has both
seen an initial baseline for every active connection and rebuilt the canonical
store at or after that provider watermark.
That same lifecycle-owned preview surface must keep provider save actions
gated on a successful monitored-system grouping preview. TrueNAS and VMware
settings may not create or update a connection while the preview is missing,
loading, unavailable, or errored, and save-time backend races must reuse the
same canonical unavailable presentation state instead of falling back to
provider-local billing messages.
That same lifecycle-adjacent request contract now also assumes canonical
enablement defaults. New platform-connection preview/test/add payloads must
inherit the provider default enabled=true when the field is omitted, while
saved-connection preview/test/update payloads must preserve stored enablement
unless the caller explicitly changes it, so setup surfaces do not accidentally
preview an unchanged active connection as inactive just because JSON omitted a
bool field.
The VMware credential editor must also distinguish required-floor failure from
optional enrichment degradation. When the canonical connection-test response
is successful but degraded, the editor must keep the exact upstream
diagnostic visible in a limited-data warning and identify the negotiated VI
JSON release when available; it must not show the test as fully green or route
the warning through the hard-failure presentation.
That same validation contract must stay coherent across the public
/api/auto-register route and the direct canonical handler path used by the
same runtime surface, so Unified Agent/setup entry points do not inherit divergent
messages for the same missing-field or token-pair failures.
That same canonical caller contract must also require explicit node identity
input from live callers: /api/auto-register may not synthesize serverName
from host once installer, setup-script, and runtime-side Unified Agent callers all send the
canonical field directly.
That same canonical runtime path must also keep overlap and rerun continuity
wording on the canonical /api/auto-register contract itself: active runtime
messages and helpers may not preserve the deleted "secure auto-register" split
when describing host-identity, DHCP-continuity, or in-place token-update
matches.
That same canonical runtime path must keep token-completion validation wording
on the canonical contract too: incomplete tokenId/tokenValue payloads may
not preserve deleted "secure token completion" wording in live handler
messages.
That same migration rule also applies to scripts/install.sh: installer-owned
Proxmox auto-registration must keep local token creation in the installer, but
submit the resulting token completion through the canonical /api/auto-register
contract directly as the one supported completion path.
That same shared scripts/install.sh boundary must also keep one canonical
runtime-argument builder for the service and wrapper launch flags it persists.
Token-bearing installs, token-file systemd installs, and wrapper-script
launches may not each rebuild their own shell fragment for --url, --token,
feature toggles, identity flags, or disk-exclude transport; they must all
derive from the same installer-owned argument item list so lifecycle state does
not drift by install path.
That same install/setup boundary must also keep setup bootstrap metadata on one
backend-owned artifact model. Proxmox setup-script downloads, rerun guidance,
and /api/setup-script-url responses may not each carry mirrored local struct
definitions for the same bootstrap fields.
That same lifecycle shell transport must also keep one shared render owner for
generated PVE and PBS setup scripts: the handler may validate inputs and choose
the artifact, but the shell body itself must come from shared backend render
helpers rather than an inline handler-local template engine.
Those install and setup-command paths now also preserve the configured
canonical PublicURL end to end when the admin session originates from the
local frontend loopback, including the configured HTTPS scheme and path, so
generated commands do not silently downgrade agent reachability to http://.
That same backend install-command boundary must also normalize trailing slashes
on canonical base URLs before composing installer asset paths or response
snippets, so /api/agent-install-command and the container-runtime migration
token path cannot drift onto //install.sh or slash-suffixed PULSE_URL
values when PublicURL or AgentConnectURL is configured with a trailing /.
That shared frontend install-command helper must also stay under explicit proof
routing on both sides instead of relying only on downstream consumer coverage:
changes in frontend-modern/src/utils/agentInstallCommand.ts must continue to
carry the direct frontend-install-command-helper lifecycle proof together
with the API-contract helper proof.
That same shared diagnostics dependency must also preserve canonical
fallback-reason continuity at the API boundary: when
internal/api/diagnostics.go serializes monitoring memory-source breakdowns
for lifecycle-adjacent diagnostics surfaces, legacy aliases and empty
fallback-reason fields must still normalize onto the governed canonical reason
contract instead of depending on monitor-owned snapshot accessors to have run
first.
Unified Linux agents carry memory evidence across this lifecycle boundary, but
they do not own memory-source inference. A report with only total and free
bytes must remain explicitly usage-unavailable because Linux MemFree excludes
reclaimable page cache; a complete used/cache/free split or an explicit
cache-aware usage remains trusted. Lifecycle transport, reconnect, and JSON
reload must preserve UsageUnavailable, including known total capacity,
without turning it into zero usage or an enrollment/freshness failure.
That same shared diagnostics dependency now also assumes local commercial and
onboarding analytics stay out of user diagnostics entirely: lifecycle-adjacent
admin surfaces may consume operational diagnostics, but they must not restore
self-hosted upgrade-metric summaries or infrastructure-onboarding analytics to
internal/api/diagnostics.go or the settings diagnostics panel. The retired
local commercial metrics reporting routes must stay absent from the normal
product API and must not become lifecycle setup, install, or fleet-progress
signals.
Lifecycle-adjacent Docker and Podman module diagnostics are part of that same
shared backend dependency. When internal/api/diagnostics.go emits module
health notes for Docker and Podman, the copy must keep Infrastructure as the
operator recovery surface and must not send users back to retired
agent-management routes.
Lifecycle-adjacent Docker / Podman management responses are part of that same
shared backend dependency. When internal/api/docker_agents.go,
internal/api/docker_metadata.go, or frontend-modern/src/api/monitoring.ts
surface host removal, hide/unhide, pending uninstall, display-name, or metadata
errors, the operator-facing copy must describe Docker / Podman modules or hosts
rather than reviving generic container-runtime labels or a separate Docker
product identity.
That same shared internal/api/ dependency now also assumes auth persistence
compatibility is handled as an explicit migration/import boundary: legacy
raw-token sessions.json and csrf_tokens.json files may load for upgrade
continuity, but session_store.go and csrf_store.go must immediately
rewrite hashed canonical persistence during load instead of leaving raw-token
files on the primary runtime path until a later save side effect happens to
run.
That same shared internal/api/ dependency also assumes ordinary self-hosted
commercial-trial acquisition is retired: lifecycle-adjacent setup, install, and
fleet surfaces must not expose direct trial CTAs or depend on
POST /api/license/trial/start, and the normal router must fail that path as
404 without mutating entitlements. The retired /auth/trial-activate
self-hosted callback must also stay absent from lifecycle retry and backoff
behavior. Lifecycle-adjacent setup and install surfaces must also treat
trial_eligible and trial_eligibility_reason as retired compatibility
fields, not as prompt state or setup transport state.
Legacy-named hosted entitlement verifier plumbing under shared internal/api/
is boundary-only commercial compatibility, not lifecycle setup state:
agent-lifecycle surfaces may consume the resolved entitlement outcome, but
must not treat TrialActivation* names or the retained
PULSE_TRIAL_ACTIVATION_PUBLIC_KEY literal as permission to recreate trial
acquisition, setup retry, or install-progress prompts.
That same shared internal/api/ dependency also assumes session-carried OIDC
refresh tokens stay fail-closed at rest: session_store.go may only persist
or recover those tokens through encrypted-at-rest session payloads, and any
missing-crypto or invalid-ciphertext path must drop the refresh token instead
of leaving plaintext-at-rest session state on the lifecycle runtime path.
That same shared internal/api/ dependency also assumes notification test
handlers stay decode-and-delegate only: internal/api/alerting/notifications.go may
surface adjacent operator test actions, but service-template selection and
generic webhook-test payload fallback must remain notifications-owned instead
of becoming a second API-layer owner under the shared helper surface.
That same shared API boundary also assumes legacy service-specific webhook
aliases are rewritten at ingress only: internal/api/alerting/notifications.go may
accept compatibility keys like Pushover app_token / user_token, but it
must return and forward only canonical token / user fields so agent-
adjacent shared internal/api/ surfaces do not inherit a second live alias
contract.
That notification boundary also carries resource-tag routing configuration.
internal/api/alerting/notifications.go may preserve and normalize tagFilter plus
tagFilterMode for email and webhook destinations, but those fields remain
notifications/alerts-owned delivery policy. They do not change agent
enrollment, command targeting, lifecycle authority, or host identity.
That same shared internal/api/ dependency now also assumes recovery-token
persistence follows the same rule: raw recovery secrets may be minted for
immediate operator use, but recovery_tokens.go must persist only token hashes
and treat any legacy plaintext-token file as a one-time migration input that
is rewritten immediately into hashed canonical persistence on load.
That same shared internal/api/ dependency now also assumes those auth stores
stay owned by the configured router data path: session, CSRF, and
recovery-token runtime state may not silently bind themselves to hidden
/etc/pulse fallback initialization or retain old-path state after a
reconfiguration.
That same shared internal/api/ dependency also assumes those auth stores
tear down synchronously when lifecycle-adjacent routers or hosted runtimes are
reconfigured: session and CSRF workers may not rely on best-effort background
signals that can wedge teardown, block temp-path cleanup, or leave first-
session and hosted handoff validation hanging behind a stale auth worker, and
each router must retain the exact session, CSRF, and recovery-token workers it
initialized so later global rebinds cannot orphan a live test or hosted-runtime
data path.
That same path-ownership rule also applies to bootstrap-token recovery and
adjacent hosted billing side effects that share the internal/api/ boundary:
CLI/bootstrap retrieval, webhook dedupe state, and customer-index persistence
must all route through the shared runtime data-dir helper instead of carrying
private /etc/pulse fallbacks in neighboring entry points.
That same shared internal/api/ boundary also assumes manual auth env writes
and first-session status reads resolve the .env path through the shared
auth-path helper, so lifecycle-adjacent setup and password flows do not each
reconstruct their own /etc/pulse/.env fallback logic.
That same shared internal/api/ dependency also assumes config import reloads
fail closed without panicking when optional runtime managers are absent.
Lifecycle-adjacent setup, install, and restore flows may invoke the shared
config-import path before every notification or monitoring manager is wired,
but internal/api/configapi/config_export_import_handlers.go must still rebind the
imported configuration without turning missing optional managers into a fatal
reload path.
The same proof boundary also owns deterministic first-run re-entry for the
managed local backend: integration helpers may use the seeded runtime-state
primary API token to call the dev-only /api/security/dev/reset-first-run
route, but they may not recreate auth teardown by deleting files or rebuilding
bootstrap state outside the canonical backend path.
That same shared internal/api/ boundary also assumes generated developer
warnings do not mis-teach the local runtime split: the embedded frontend notice
under internal/api/DO_NOT_EDIT_FRONTEND_HERE.md may point operators to the
shared backend on :7655 when explaining the proxy relationship, but it must
keep the hot-reload browser entrypoint on http://127.0.0.1:5173 so lifecycle-
adjacent setup and install guidance does not regress to the backend port.
Those same lifecycle-adjacent setup and password flows must now also route
.env writes through the shared writable auth-env helper instead of
re-implementing config-path writes plus data-path fallback ordering inline.
The same agent-lifecycle boundary now also fails closed on profile assignment:
assigning an agent to a non-existent profile must return a not-found contract
instead of persisting an orphan profile reference through the API.
That same missing-profile assignment contract must survive the shared frontend
control surface: frontend-modern/src/api/agentProfiles.ts must preserve the
canonical missing-profile message for assignment 404s, and
AgentProfilesPanel.tsx and InfrastructureInstallerSection.tsx must resync profile state after
that rejection instead of flattening it into a generic assignment failure while
leaving stale profile options visible.
That same shared profile-management boundary must also fail closed on malformed
list payloads: frontend-modern/src/api/agentProfiles.ts may not silently
reinterpret non-array profile or assignment responses as an empty state, and
useAgentProfilesPanelState.ts / InfrastructureInstallerSection.tsx must surface that load
failure instead of pretending no profiles exist.
That same shared profile-management boundary must also fail closed on malformed
profile-object, suggestion, schema, and validation payloads: the shared
agentProfiles client may not trust partial profile objects, malformed schema
definitions, or malformed validation/suggestion bodies, and the profile editor
plus suggestion modal must surface those canonical contract failures instead of
flattening them into generic save/delete/schema/validation fallback copy.
That same frontend profile-management boundary now keeps its render shell and
runtime owner separate: AgentProfilesPanel.tsx is the surface shell, while
useAgentProfilesPanelState.ts owns license gating, AI availability, profile
load/save mutations, assignment resync, and modal form lifecycle so the panel
does not carry a second inline controller.
That same connected profile-assignment surface must also preserve canonical
local operator identity for monitored systems. When governed resources such as
PBS or PMG appear in the assignment list, the panel must keep the local
instance label for ordering and row display instead of substituting governed
summary text, so profile assignment remains instance-specific.
Canonical Proxmox auto-register must also preserve the legacy DHCP continuity
contract: when a node reruns registration from a new IP but presents the
same canonical node name and deterministic Pulse-managed token identity, Pulse
must update the existing node in place instead of duplicating it as a second
inventory record.
That same profile-management UI boundary must also stay on the direct
agent-profiles-surface proof path, rather than relying only on the shared
API client coverage to catch lifecycle drift in AgentProfilesPanel.tsx.
That same profile-management presentation helper must also stay on that direct
agent-profiles-surface proof path, rather than relying only on panel-level
tests to catch lifecycle drift in
frontend-modern/src/utils/agentProfilesPresentation.ts.
Shared internal/api/ recovery transport helpers now also preserve normalized
filter coherence across rollup, point-history, series, and facet views so
agent-adjacent protected-resource drill-downs do not fork between protected
items and history slices under the same active recovery filter set.
That same shared internal/api/ recovery boundary must also preserve the
canonical provider-neutral itemType filter and display contract. When
agent-adjacent recovery data originates from Proxmox, Kubernetes, TrueNAS, or
other platform-native subjects, the shared transport layer must normalize
those source-specific labels onto the governed recovery item vocabulary before
the UI route/filter state sees them, so lifecycle-adjacent drill-downs remain
coherent across platforms instead of reintroducing Proxmox-native subject
types as the de facto recovery model.
That same shared recovery boundary now also treats platform as the canonical
operator-facing filter query for lifecycle-adjacent drill-down links. Any
legacy provider alias support must remain compatibility-only input behind
the shared API/router layer rather than becoming the route shape lifecycle
surfaces copy back out to operators.
That same lifecycle-adjacent recovery drill-down boundary must also stay on
canonical itemResourceId filter and payload vocabulary. When lifecycle
surfaces deep-link into shared recovery handlers or consume recovery payloads,
they should treat legacy subjectResourceId only as an API-layer compatibility
alias rather than reviving it as the route or runtime model they expose.
That same lifecycle-adjacent recovery drill-down boundary must also stay on
canonical itemRef payload vocabulary. When lifecycle surfaces consume shared
recovery point or rollup payloads, they should treat legacy subjectRef only
as an API-layer compatibility alias rather than reviving it as the runtime
item-reference model they expose back out to operators.
The updater/runtime surfaces must preserve the one-shot updated_from
continuity handoff and the non-TLS continuity path for supported self-hosted
installs, so upgrade-safe agent behavior does not drift between install,
restart, and reconnect paths.
The v5-to-v6 automatic migration boundary must name its first-hop trust model
explicitly: an already-installed v5 pulse-agent may use its v5 updater to
reach v6, but release readiness and operator docs must warn that this hop
depends on HTTPS or trusted local-network transport until the v6 signature and
downloaded-binary self-test protections are installed. High-assurance
environments must be pointed at signed v6 installer reinstall instead of a
plain-HTTP first hop.
That same shared agent transport boundary must not force operators to choose
between public-CA trust and blanket TLS disablement. cmd/pulse-agent/main.go,
internal/hostagent/, internal/agentupdate/, and adjacent remote-config
transport must accept a canonical pinned Pulse server certificate fingerprint
for self-signed deployments, and that pin must flow through reporting,
enrollment, command websocket, remote-config, and self-update transport
instead of widening PULSE_INSECURE_SKIP_VERIFY into an all-path MITM
carve-out.
Release-grade updater continuity must also stay fail-closed on signed assets.
When release builds embed trusted update signing keys through
internal/updatesignature, internal/agentupdate/ and
internal/dockeragent/ must require both X-Checksum-Sha256 and
X-Signature-Ed25519, while installer-owned download flows must also require
the matching base64-encoded X-Signature-SSHSIG, and
internal/api/unified_agent.go must only serve published release installers
and agent binaries from local or proxied assets that carry the matching
detached signature sidecars.
Target-version cache keys do not relax that trust boundary. A cache-keyed
download must still pass the exact checksum, embedded-key Ed25519 signature,
platform magic, downloaded-binary self-test, same-filesystem atomic
replacement, and backup restoration rules before restart. The cache key is
version selection metadata only and must never be treated as artifact
integrity or downgrade authorization.
That same self-update pre-flight must keep the live agent token out of process
argv. internal/agentupdate/update.go and legacy
internal/dockeragent/self_update.go may pass a short-lived 0600 token file
into cmd/pulse-agent/main.go --self-test --token-file, but they must not
revive --token <secret> argument passing that exposes the runtime
credential through /proc/*/cmdline.
That same unified-agent runtime boundary also owns vendor-aware host identity.
When gopsutil reports generic Linux platform fields on NAS appliances,
internal/hostagent/ must prefer canonical platform files such as Synology DSM
or QNAP QTS/QuTS version manifests before the first report is built, so
downstream monitoring and alerting do not depend on hostname or display-name
heuristics to infer the real vendor OS.
Proxmox VE host identity follows the same vendor-aware rule: when /etc/pve,
pveversion, or Proxmox package metadata proves the host is PVE,
internal/hostagent/ must report Proxmox VE plus the PVE product version
when available, while preserving the underlying Linux runtime platform for
runtime/platform-normalization decisions.
That same runtime continuity must stay on direct lifecycle proof routes too:
changes under internal/hostagent/ must continue to carry the explicit
unified-agent-runtime proof, and changes under internal/agentupdate/ must
continue to carry the explicit agent-update-runtime proof, instead of
relying on broad owned-prefix coverage to catch lifecycle regressions in the
Unified Agent runtime and updater boundaries.
The settings/profile surfaces must keep unified v6 agent identity and profile
assignment behavior canonical, rather than falling back to host-era or
module-local assumptions. That includes copied shell install and upgrade
commands in the unified settings surface: privilege-escalation wrappers must
preserve the full installer argument list exactly, so selecting target profile,
token, and command-execution flags cannot be dropped at the last clipboard hop.
That same target-profile continuity must hold for PowerShell transport as well:
when the selected profile enables Proxmox mode, copied Windows install commands
must preserve both PULSE_ENABLE_PROXMOX and PULSE_PROXMOX_TYPE, and
scripts/install.ps1 must persist those flags into the managed service
arguments instead of silently collapsing back to generic host monitoring.
The same lifecycle ownership now also covers manual node setup command
presentation in the extracted node setup surface
(ConnectionEditor/CredentialSlots/NodeCredentialSlot.tsx,
NodeModalSetupGuideSection.tsx, nodeModalModel.ts, and
useNodeModalState.ts): the copied PVE permission snippet must stay
aligned with the canonical backend setup script, including comma-joined
privilege transport and non-destructive PulseMonitor role updates, instead
of shipping a stale local fork. PVE 9+ guest-agent privileges
VM.GuestAgent.Audit and VM.GuestAgent.FileRead are the primary setup
contract; legacy VM.Monitor may appear only as the PVE 8 fallback when the
guest-agent privilege probe is unavailable.
That same node setup modal owner must also route Proxmox agent-install command
generation through the canonical NodesAPI.getAgentInstallCommand client for
both PVE and PBS, instead of mixing client-mediated and ad hoc raw POST
transport for the same backend lifecycle command surface. That same settings
surface must consume the shared validated response uniformly for both node
types, surfacing canonical install-command errors inline instead of collapsing
one pane back to generic notification-only failure.
That same node setup modal owner must also route Proxmox quick-setup command
generation and manual setup-script download through canonical NodesAPI
helpers for both PVE and PBS, preserving the shared setup-token and expiry
contract instead of letting one node type drift onto a raw fetch-only path.
That same node setup modal owner must also stay on the direct
node-setup-settings-surface proof path across
ConnectionEditor/CredentialSlots/NodeCredentialSlot.tsx,
NodeModalAuthenticationSection.tsx, NodeModalBasicInfoSection.tsx,
NodeModalMonitoringSection.tsx, NodeModalSetupGuideSection.tsx,
NodeModalStatusFooter.tsx, nodeModalModel.ts, and useNodeModalState.ts,
rather than relying only on broad lane ownership or downstream command tests
to catch lifecycle drift in the settings surface.
Candidate-sourced PVE/PBS/PMG node setup is part of that same node setup
owner. When a connection originates from Discovery or the API platform probe,
InfrastructureWorkspace.tsx must pass the candidate into
NodeCredentialSlot.tsx, infrastructureImportPlanModel.ts must derive the
endpoint, identity, credential path, collection scope, monitored-system preview
request, and reset signature, and the setup surface must require explicit
operator approval before any assisted setup command, downloaded setup script,
manual token command copy, or manual save can proceed. Direct type-first adds
with no candidate remain normal credential entry and must not fabricate an
import plan.
That same Proxmox lifecycle transport now explicitly includes the shared
frontend-modern/src/api/nodes.ts client boundary itself: changes to setup
command or install-command request transport must carry both lifecycle proof
and the shared API contract instead of staying implicit behind downstream
consumer tests alone.
That same lifecycle ownership also covers the setup completion preview's copied Unix
install handoff in SetupCompletionPanel: the first-session install snippet must use the
same shell-safe URL/token quoting, curl -fsSL failure behavior, and
root-or-sudo privilege wrapper contract as the governed unified install
surface instead of carrying a stale inline transport variant.
That same setup-completion install transport must also preserve the canonical
plain-HTTP continuity rule: when the configured Pulse URL is http://, the
copied Unix install command must carry --insecure through the shared host
install command builder instead of bypassing the lifecycle transport contract
with local inline shell assembly.
That same Unix install-command contract also governs backend-generated Proxmox
install transport in internal/api/agent_install_command_shared.go: the
canonical /api/agent-install-command and hosted Proxmox install-command
surfaces must emit the same root-or-sudo privilege wrapper already required by
the shared frontend Unix builder, instead of returning a raw | bash -s --
pipeline that drifts from the lane's governed install shape.
The same lifecycle shell-transport contract also applies to the diagnostics
container-runtime migration install command in internal/api/router.go: that
response defaults to the native Unified Agent's host plus Docker modules,
emits the canonical --enable-host flag, and uses a token scoped for host
report/configuration continuity as well as Docker reports. An explicit
enableHost:false request keeps the intentional workload-only path, the
canonical --enable-host=false flag, and Docker-report-only token scope. Both
modes must retain the governed root-or-sudo wrapper rather than falling back
to the stale --disable-host alias or a raw curl | sudo bash pipe that
drifts from the managed install surface.
The Settings Docker / Podman install profile and the legacy
scripts/install-container-agent.sh compatibility wrapper follow that same
default: selecting Docker monitoring enables the Docker module without
silently disabling the host module. Workload-only operation remains an
explicit --enable-host=false choice through the canonical installer.
That same diagnostics migration command must stay on the shared backend
install-command helper path in internal/api/agent_install_command_shared.go,
rather than rebuilding a local shell formatter in router.go, so optional
token omission, plain-HTTP --insecure, trailing-slash normalization, and the
governed privilege wrapper stay aligned with the rest of the lifecycle install
surface.
That same lifecycle shell transport also governs the quick setup command
returned by /api/setup-script-url: config_setup_handlers.go must emit a
shell-quoted curl -fsSL fetch for the generated setup script, and the
token-bearing and tokenless variants must come through a shared helper instead
of open-coding a stale curl -sSL pipeline in the handler.
That same bootstrap route must also stay on one canonical request shape:
/api/setup-script-url accepts a single JSON object with only the supported
request fields, and the handler must fail closed on unknown fields or trailing
JSON instead of tolerating typo-compatible or concatenated payloads.
That same request contract also keeps backup-permission semantics explicit:
backup_perms / backupPerms is a PVE-only bootstrap option, and both
/api/setup-script and /api/setup-script-url must reject it for PBS instead
of quietly carrying a no-op flag through the canonical setup surface.
That same bootstrap request boundary must stay canonical on host identity too:
/api/setup-script no longer generates placeholder-host scripts for later
repair, and both setup routes must reject missing host input instead of
minting artifacts that can only fail closed after download.
That same request boundary must stay canonical on Pulse identity too:
/api/setup-script no longer reconstructs pulse_url from the request-local
origin, and both setup routes must require the explicit canonical Pulse URL
that the rest of the bootstrap envelope already carries through url,
command*, and downstream auto-register state.
That same bootstrap boundary must now also stay canonical on identity: the
request must carry a supported type and non-empty host, the backend must
normalize that host before minting the one-time setup token, and both
installer-owned and runtime-side Unified Agent callers must validate the returned
bootstrap type, normalized host, and live expires metadata before they
trust the returned setupToken. That consumer-side validation must fail closed
on already-expired bootstrap responses rather than treating any non-empty
expires field as usable. That same /api/setup-script-url request boundary
must also stay truthful about auth: setup tokens only bootstrap the later
/api/setup-script and /api/auto-register flows, while the setup-script-url
request itself remains a normal authenticated request once Pulse auth exists.
Those same installer-owned and runtime-side Unified Agent callers must also require the
full canonical bootstrap artifact, including token-bearing downloadURL and
masked tokenHint, so they do not keep accepting an older reduced setup-token
response shape after the runtime and shared settings client have moved to the
full envelope.
The shared settings/frontend consumer in frontend-modern/src/api/nodes.ts
must stay on that same canonical bootstrap contract too, normalizing and
validating the returned setup-script-url identity fields instead of exposing a
raw JSON passthrough to NodeModal and related quick-setup surfaces. That
shared frontend consumer must also reject already-expired setup-script-url
responses instead of treating any positive expires value as sufficient, and
it must validate the returned setupToken without retaining that raw secret
beyond the shared client boundary.
The extracted node setup modal owner must then consume that canonicalized
response directly,
including copying the token-bearing commandWithEnv field while rendering the
non-executable credentialed-command readiness state instead of rendering the
tokenless commandWithoutEnv shell command as if it were runnable or
re-interpreting the bootstrap payload through local nullable fallbacks.
Operator-facing quick-setup display must also stay on the runtime-owned token
boundary: the shared frontend client must require masked tokenHint, and the
extracted node setup modal owner must render that hint rather than the full returned
setupToken once the bootstrap artifact itself already carries the live
secret. That non-executable readiness contract applies to both the PVE and PBS
quick-setup panes; the settings surface may not render either the token-bearing
command or a tokenless lookalike command after bootstrap generation. Operator
guidance on those panes must stay
truthful too: once the visible UI only shows a masked hint, copy-success text
may not instruct the operator to paste a token "shown below" and must instead
state that the copied command already embeds the one-time setup token. The same settings quick-setup surface must also trim and validate the Endpoint URL
before manual setup-script download, so download and copy paths stay on the
same canonical host-input contract. That same manual download path must also
stay on one shell-script artifact contract: /api/setup-script responses must
ship with canonical text/x-shellscript attachment headers and deterministic
pulse-setup-*.sh filenames, while frontend-modern/src/api/nodes.ts and the
extracted node setup modal owner must validate and use the returned content type and filename
instead of inventing local text/plain download metadata.
Manual download must also stay non-interactive without re-exposing raw setup
tokens in UI state: /api/setup-script-url must return a dedicated
token-bearing downloadURL, and the shared frontend client plus the extracted node setup modal owner
must use that runtime-owned download artifact instead of fetching the plain
script url and then relying on a separately displayed token value.
That same settings quick-setup surface must also treat /api/setup-script-url
as one canonical bootstrap artifact per active host/type/mode: copy and manual
download must reuse the returned url, downloadURL, scriptFileName,
commandWithEnv, tokenHint, and expires until the artifact expires or the
operator changes the endpoint, instead of re-fetching and rebuilding a second
local download path or caching the raw setup token past the shared frontend
client.
That same public/operator guidance must also describe that canonical bootstrap
artifact truthfully: API docs and Proxmox/PBS setup guides may not fall back to
stale raw curl -sSL ... | bash examples or omit the returned bootstrap
artifact fields once the runtime and settings surfaces are contractually using
url, scriptFileName, command*, setupToken, and expires.
That same bootstrap response boundary must also own the setup-script filename
before download happens: /api/setup-script-url must return the canonical
scriptFileName, and the settings quick-setup surface must use that runtime
metadata for operator guidance instead of hardcoded PVE/PBS script names that
can drift from the downloaded artifact.
That same setup-token bootstrap response must also stay coherent for the
non-frontend consumers: the runtime-side Unified Agent and installer Proxmox registration must
reject missing or mismatched canonical url, scriptFileName, command,
commandWithEnv, and commandWithoutEnv fields instead of consuming
/api/setup-script-url as a token-only response.
That same quick-setup transport must also preserve the governed root-or-sudo
continuity used by the install surface: /api/setup-script-url commands must
execute bash directly when already root and fall back to sudo otherwise,
including preserving PULSE_SETUP_TOKEN through the sudo path instead of
assuming operators are already in a root shell.
That same transport rule also applies to the generated PVE and PBS setup
scripts themselves: operator-facing retry and off-host rerun guidance printed
by HandleSetupScript must advertise the same fail-fast curl -fsSL fetch
shape instead of drifting back to stale curl -sSL examples inside the script
body.
That embedded guidance must preserve the same root-or-sudo continuity too, so
the script body does not hand operators a direct-root-only retry command after
the API response itself already supports both execution paths.
That same retry guidance must also preserve PULSE_SETUP_TOKEN continuity
through both the direct-root and sudo paths, so reruns from the generated PVE
and PBS setup scripts stay on the same non-interactive setup-token contract
instead of silently falling back to an interactive prompt.
That same rerun-token contract must also hydrate PULSE_SETUP_TOKEN from any
embedded setup token before the script prints rerun guidance, so generated
PVE/PBS scripts issued with canonical setup_token transport do not drop back
to prompt mode on the next hop.
That same setup-script bootstrap boundary must keep one token name end to end:
/api/setup-script accepts only the canonical setup_token query when a token
is embedded into the script payload, and the rendered PVE/PBS script body uses
only PULSE_SETUP_TOKEN instead of lane-local alias variables.
The same generated PVE setup-script boundary must also preserve cleanup
continuity for discovered legacy tokens: when the script offers to remove old
Pulse tokens from the same server scope, it must iterate the actual discovered
pve and pam token lists instead of falling through an undefined placeholder
loop variable that turns cleanup into a no-op. That discovery path must also
reuse the canonical Pulse-managed token prefix for the active Pulse URL, while
still matching legacy timestamp-suffixed variants, instead of rebuilding a
lane-local IP-pattern guess that drifts from buildPulseMonitorTokenName.
The generated PBS setup-script boundary must preserve that same cleanup
discovery contract instead of keeping a separate IP-pattern matcher for old
token cleanup.
The same generated PVE setup-script boundary must also preserve
Proxmox-managed /root/.ssh/authorized_keys symlinks when lifecycle setup or
removal touches Pulse-managed temperature-monitoring SSH keys: scripts must
resolve the real authorized-keys target before filtering # pulse- entries and
must use that resolved path for both install and uninstall edits.
That same generated PVE setup-script boundary also owns the forced-command
shape for temperature-monitoring SSH keys: new keys must execute the
Pulse-owned /usr/local/sbin/pulse-sensors wrapper, not raw sensors -j, so
lifecycle setup can collect CPU sensor data and SMART disk temperatures through
one bounded JSON payload while preserving old forced-key compatibility in the
runtime collector. That wrapper must mirror the host-agent Linux SMART probe
fallback for direct SATA/SAT-style disks by retrying explicit -d sat and
-d scsi before reporting an active disk with no temperature.
That same generated setup-script boundary must also use exact token-name
matching when it decides whether to rotate an existing Pulse-managed token, so
reruns do not treat partial-name collisions as the canonical managed token.
The generated PBS setup-script branch must also keep token-copy guidance
truthful: it may only print the one-time token-copy banner after token creation
has actually succeeded, not ahead of a failure path that produced no token.
That same generated PBS setup-script branch must also keep auto-register
attempt guidance truthful: it may only print the attempt banner on the real
request path, after token-availability and setup-token gating are resolved,
rather than before a skip branch that never sends a registration request.
That same rerun path must also preserve the backend-owned encoded setup-script
request URL: embedded SETUP_SCRIPT_URL values in generated setup scripts must
keep the canonical host, pulse_url, and backup_perms query contract
instead of rebuilding a lossy raw query string inside the shell.
That same off-host fallback path must not invent a second manual token-creation
workflow either: when the script is run outside a Proxmox host, it must direct
the operator back to rerun on the host through the canonical generated command
instead of teaching a separate pveum + Pulse Settings flow that can drift
from the backend-owned lifecycle contract.
That same runtime boundary must also preserve canonical privilege guidance when
the script is launched directly: generated setup scripts may not fall back to
the stale "Please run this script as root" wording, and must instead use the
same root requirement language already carried by the governed retry wrapper.
That same manual-follow-up surface must also preserve one canonical token
placeholder contract across its adjacent branches: generated PVE and PBS setup
scripts may not drift between "[See above]", "Check the output above...", and
other local variants when the token value is only available in prior output.
That same completion boundary must also preserve one canonical success message
across generated PVE and PBS setup scripts, so identical successful
auto-register outcomes do not surface different node-type-specific wording for
the same finished lifecycle state.
That same auto-register boundary must also fail closed when token extraction
fails after token creation: generated PVE and PBS setup scripts may not
continue into prompt or request assembly with an empty token value, and must
instead stop on the canonical "token value unavailable" branch before any
registration attempt is formed.
That same PBS auto-register path must also report skipped states truthfully:
when setup-token input is absent or token extraction never produced a usable
secret, the script may not relabel that skip as a failed request before
success confirmation.
The generated PVE and PBS setup scripts must also fail closed on
auto-register success detection: their runtime branch may only treat a
response as successful when it contains an explicit success:true signal,
rather than any broad success substring match that could misclassify
success:false payloads as a completed registration.
That same auto-register path must also fail closed on HTTP and transport
errors: the generated scripts must use fail-fast curl -fsS transport and
gate success parsing on a successful curl exit code instead of interpreting
arbitrary error output as a registrable response body.
That same generated setup-script boundary must also preserve setup-token
messaging continuity: when auto-register authentication fails, operator
guidance must point back to the one-time Pulse setup token flow rather than
telling the user to provide or validate an API token that this script path no
longer uses.
That same auth-failure guidance must also stay truthful once the generated
setup script has already sent a registration request: it may not branch back
into a missing-token explanation after the request path proves a setup token
was present, and must instead direct the operator to mint a fresh setup token
from Pulse Settings → Nodes and rerun. That same auth-failure state must also
block the later manual-details footer, so the script does not immediately
contradict itself by offering manual completion with the current token details.
That same completion boundary must also preserve outcome truth: generated PVE
and PBS setup scripts may only claim successful Pulse registration when
auto-register actually succeeded, and must otherwise present the result as
token setup plus manual registration follow-up instead of announcing a false
successful onboarding state.
That same manual-follow-up path must also stay on the canonical node-add
contract: generated setup scripts may not redirect operators onto a stale
secondary registration-token rerun flow, and must instead point them to finish
registration with the emitted token details in Pulse Settings → Nodes.
That same manual-follow-up path must also keep its failure summary on that
canonical node-add contract: generated setup scripts may not fall back to
vague "manual configuration may be needed" copy when the emitted token details
already define the exact Pulse Settings → Nodes completion path.
That same manual-follow-up path must also preserve the canonical node host
identity already in scope for the script, rather than falling back to a stale
placeholder host string that forces the operator to reconstruct the node
address by hand.
That same host continuity rule applies to PBS as well: generated setup scripts
may not replace the requested canonical PBS host with runtime-local interface
discovery in the manual-add footer, because DHCP or multi-NIC nodes can make
that fallback diverge from the host the operator actually intended to register.
That same PBS host continuity must survive auth-skip and token-skip fallback
branches too: the generated script must bind the canonical PBS host before any
auto-register gating that can short-circuit into manual completion, so the
manual footer never emits a blank or lost host URL when setup-token input is
missing.
That same fail-closed host rule must also apply when the script never received
any canonical host at all: generated PVE and PBS scripts may not fall back to
placeholder host values in manual completion and must instead direct the
operator to regenerate the script with a valid host URL.
That same PBS host binding must exist before token-creation failure fallback as
well, so the final manual footer still preserves the canonical requested host
even when the script cannot mint a usable PBS token and never reaches the
auto-register branch at all.
Generated PVE and PBS setup scripts must also fail closed on token-creation
failure truth: if Proxmox token minting fails, the script may not continue into
fake manual token details or claim token setup completed. It must skip
auto-register, surface the token-creation failure explicitly, and direct the
operator to rerun after fixing the node-local token error.
That same failure-truth contract also applies to token extraction errors after
creation output is returned: generated setup scripts may not tell operators
that manual registration might still work from that broken output. They must
keep the flow on rerun-after-fix guidance until a usable token value actually
exists, and the final completion footer/manual-details branch must key off that
usable-token state rather than raw token-create success.
That same manual-follow-up path must also preserve canonical Settings-surface
language across both PVE and PBS setup scripts, so the operator is always
directed back to Pulse Settings → Nodes with the emitted token details instead
of drifting onto lane-local wording for one node type.
That same canonical path must also hold inside the immediate auto-register
failure branch itself, so generated scripts do not fall back to a shorter
"Pulse Settings" variant before the final manual-completion footer repeats the
correct Settings → Nodes destination or diverge into a separate numbered
manual-setup detour instead of reusing the same "use the token details below"
completion contract. That includes transport/request failures before the
backend ever returns a response body, not just explicit error payloads.
That same SetupCompletionPanel transport must also preserve the governed self-signed
and private-CA continuity controls used by the shared lifecycle command
surface: the first-session setup-completion install handoff must pass explicit
--insecure and --cacert choices through the shared Unix install builder so
the very first installer fetch and the installer runtime stay aligned with the
same transport contract as InfrastructureInstallerSection.tsx and
useInfrastructureOperationsState.tsx. In explicit insecure mode, that
means the outer curl fetch must widen to -kfsSL instead of preserving
strict TLS until install.sh starts.
That same first-session install surface must also preserve canonical
agent-to-Pulse addressing, not just browser-local origin: SetupCompletionPanel must
default to the governed security status agentUrl when available and allow an
operator override for agent connectivity, so setup-completion commands do not
silently hand out loopback or wrong-origin install transport.
That same first-session surface must also preserve Windows install parity:
SetupCompletionPanel may not stop at Unix-only shell transport while claiming Windows
coverage. Its PowerShell install command must route through the shared
transport helper so URL, token, insecure-TLS, and custom-CA behavior stay
aligned with InfrastructureInstallerSection.tsx and
useInfrastructureOperationsState.tsx.
That same first-session setup-completion surface also owns the operator's v6
mental model for Unified Agent onboarding: SetupCompletionPanel must teach that one
Unified Agent install creates one canonical Pulse system resource first, then
layers workload discovery and API-linked platform context onto that same
inventory. It may not present Docker, Kubernetes, Proxmox, or TrueNAS as
competing primary onboarding paths, nor fall back to logo-led feature
brochure copy that obscures the unified-resource contract the wizard is
supposed to introduce.
That same onboarding/install guidance must also preserve the simple fleet
mental model for clustered and API-backed systems: platform connections own
cluster or appliance inventory, while Pulse Agent remains the low-overhead
per-machine install path for full node-local telemetry. Settings and first-run
install copy may recommend installing the agent on every machine that needs
temperatures, SMART disk data, services, Docker, or Kubernetes telemetry, but
they may not imply that API-backed cluster visibility or best-effort peer
augmentation is equivalent to a local agent install on that machine.
Docker onboarding copy must also distinguish the standalone Docker / Podman
install path from Docker inside Proxmox LXCs: standalone runtimes use the
Docker runtime profile on that host, while Proxmox LXC Docker inventory uses a
Proxmox VE node agent with command execution plus an explicit server-side
opt-in (the admin-only Settings toggle, or
PULSE_ENABLE_PROXMOX_GUEST_DOCKER_INVENTORY=true which locks the toggle),
and the copy may not imply
that installing an agent inside every LXC is required. The Docker empty state,
infrastructure installer, and Proxmox node setup guide must all preserve that
same distinction.
That same connected-systems summary must preserve canonical local operator
identity for newly connected infrastructure. When governed resources such as
PBS or PMG appear in the setup-completion poll, the surface must show their
local instance labels instead of replacing those identities with governed
summary text, so the operator can tell which system actually connected.
That same first-session setup-completion surface must also honor the lane's
optional-auth install contract: when Pulse does not require API tokens, the
wizard may switch to tokenless install commands only after an explicit operator
confirmation, but it must preserve the generated token by default and keep that
explicit token path available instead of collapsing onboarding into tokenless-only
transport. Once the operator has explicitly chosen tokenless mode, repeated
wizard copies must preserve that tokenless choice instead of silently rotating
back onto token-auth transport after the first clipboard action. That same
tokenless choice must also survive the wizard's background "agent connected"
token-rotation path: new agent arrivals may not regenerate a token or flip the
surface back to token-auth mode while explicit tokenless onboarding remains the
active contract.
That same wizard boundary must also keep its credentials drawer and exported
credentials file aligned with the current rotated install token, rather than
continuing to display or download the stale bootstrap token after the install
command surface has already moved on. It must do that without erasing the
stable bootstrap admin API credential: the wizard needs to preserve both the
admin token and the current rotated install token as separate operator-visible
surfaces instead of collapsing them into one mutable credential slot. The saved
credentials handoff must also preserve the current agent-install URL and the
matching install command shape for both Unix and Windows onboarding, so
exported first-session material cannot drift back to browser-local login
context or Unix-only transport while the live wizard command surface is using
a governed or operator-overridden agent endpoint. When the operator explicitly
confirms tokenless optional-auth mode, those same credential surfaces must stop
claiming a current install token and instead present tokenless install mode as
the active onboarding contract. The primary install guidance text in the wizard
must switch with that mode as well: tokenless onboarding may not keep
advertising automatic token rotation after each copy once the active transport
is explicitly tokenless.
The same first-session contract now also owns the landing handoff after secure
setup: RC-proof and helpers must treat direct navigation into
/settings/infrastructure?add=pick as the canonical completion path, rather
than assuming an agent-only install landing or the legacy dashboard-only
landing still defines successful wizard completion.
For already-connected completion states, the Open Infrastructure action must
return through the shared infrastructure workspace route
/settings/infrastructure, not the root app shell or a dashboard alias. The
source-picker and agent-install actions remain the explicit first-source
handoffs at /settings/infrastructure?add=pick and
/settings/infrastructure?add=agent, so setup-completion tests must assert the
route helpers rather than allowing a generic home-route completion.
That same SetupCompletionPanel boundary must also stay on the direct
setup-completion-source-picker-surface proof path, rather than relying only
on shared helper coverage or downstream install tests to catch lifecycle drift
in the setup completion surface.
That same first-session browser proof must also exercise the explicit
Install Pulse Agent secondary action through the real setup wizard flow,
rather than relying only on the preview route or prose-level assertions to
represent the agent-managed alternative.
The same ownership also covers manual install fallback in the infrastructure
settings surface: active and ignored Connected infrastructure rows must now
come from the backend-owned connectedInfrastructure projection instead of a
frontend-local merge of raw unified-resource facets and removed runtime arrays,
and v6 clients no longer treat those removed runtime arrays as a parallel
settings contract, so lifecycle scope and reconnect behavior stay canonical
across host, Docker, and Kubernetes reporting.
deploy results surface: ResultsStep must request the canonical backend
install command from /api/agent-install-command for failed deploy targets
instead of rebuilding a local shell snippet that can drift from the governed
installer contract. That fallback surface must consume the shared validated
NodesAPI.getAgentInstallCommand response, so malformed backend payloads fail
closed and the raw backend install token stays inside the shared client
boundary rather than leaking into deploy UI state.
Deploy wizard target tables are lifecycle-owned presentation surfaces:
CandidatesStep, ConfirmStep, PreflightStep, DeployingStep, and
ResultsStep must use the shared frontend Table primitive for scroll and
table semantics instead of raw table markup or step-local scroll frames.
Deploy selection and retry UI must not consume retired monitored-system
capacity boundaries. Lifecycle UI must avoid workspace-capacity, legacy
license-slot, and plan-upgrade language in deploy confirmation, preflight, or
retry surfaces.
That same deploy wizard boundary must also stay on the direct
deploy-fallback-install-surface proof path, rather than relying only on the
shared install helper or downstream deploy tests to catch lifecycle drift in
the infrastructure fallback surface.
The same Windows install, upgrade, and uninstall copies must also preserve
operator-selected transport and capability toggles: if the settings surface
enables insecure TLS mode or Pulse command execution, the PowerShell path must
carry PULSE_INSECURE_SKIP_VERIFY and PULSE_ENABLE_COMMANDS through to the
installer where those settings apply, so Windows agents do not diverge from the
governed shell transport.
That same command-enabled lifecycle path must still enforce the shared command
policy on the agent itself. internal/hostagent/commands.go may accept the
installed command-execution capability, but it must re-evaluate
internal/agentexec/policy.go immediately before sh -c, reject
PolicyBlock commands regardless of caller, and require a consumed approval
identifier before executing any PolicyRequireApproval command so a missed
control-plane gate cannot silently turn into host-level RCE. The single
exception is commands marked Trusted in the wire payload: those originate
from a vetted Pulse-internal subsystem whose command catalog is hardcoded
in Pulse source (today only the servicediscovery deep scanner, which wraps
read-only inspections in docker exec). PolicyBlock still applies to
trusted payloads. The Trusted field must never be set by a code path that
deserialises user input or accepts a caller-supplied command string.
That same copied install transport must also normalize canonical base URLs
before composing installer asset paths: when operators enter a trailing-slash
Pulse URL, shell and PowerShell install commands must trim it before appending
/install.sh or /install.ps1 so lifecycle transport does not drift onto
double-slash asset paths.
That same shared install-command transport must also fail closed on blank local
overrides: whitespace-only custom Pulse endpoint input in InfrastructureInstallerSection.tsx
or SetupCompletionPanel.tsx may not override the canonical backend-governed
endpoint, and shared command builders must reject blank endpoint URLs instead
of composing /install.sh or /install.ps1 from an empty base.
That same copied upgrade boundary must preserve canonical runtime identity when
inventory already knows it: shell upgrade payloads must carry --agent-id and
--hostname, and PowerShell upgrade payloads must carry PULSE_AGENT_ID and
PULSE_HOSTNAME, so rerunning an upgrade does not silently collapse back to
local-machine identity.
Copied per-agent uninstall commands must also preserve the selected agent's
canonical identity instead of relying on local fallback discovery alone: when
inventory already knows the agent ID for the chosen row, the shell and
PowerShell uninstall payloads must carry that ID through to the installer so
managed removal deregisters the intended agent record even if local state or
hostname lookup is stale.
That same uninstall continuity must preserve canonical hostname fallback too:
copied shell uninstall payloads must carry --hostname, copied PowerShell
uninstall payloads must carry PULSE_HOSTNAME, and both installer runtimes
must prefer that explicit hostname during lookup fallback before querying local
machine identity. That fallback must also fail closed when hostname matches are
ambiguous: hostname matches may resolve only when they identify one and only
one agent, and display-name or short-hostname fallbacks must return not found
otherwise.
That governed hostname lookup fallback must also normalize query transport:
both installer runtimes must percent-encode the resolved hostname before
calling /api/agents/agent/lookup, so canonical identity recovery does not
drift on hostnames that contain spaces or other query-significant characters.
That same copied uninstall transport must also fail closed under required auth:
when Pulse requires API tokens, shell and PowerShell uninstall commands must
carry the same resolved token contract as install and upgrade instead of
silently degrading to tokenless deregistration transport.
That same copied Unix lifecycle transport must also preserve shell-safe
canonical identity: shell uninstall and upgrade commands must quote the
selected URL, token, agent ID, and hostname as command arguments instead of
interpolating raw inventory values into the shell line.
That same copied Windows lifecycle transport must also preserve
PowerShell-safe canonical identity: uninstall and upgrade commands must escape
selected URL, token, agent ID, and hostname values before placing them into
PowerShell env assignments or command text.
The same transport rule applies to copied install commands: shell install
payloads must quote canonical URL/token transport, and PowerShell install
payloads must escape URL/token values before they enter env assignments or
irm command text. The same Windows upgrade boundary must quote the resolved
PowerShell script URL as well, so canonical URLs with spaces or other
PowerShell-significant characters do not break copied upgrade transport after
the env assignments have already been escaped.
That same copied lifecycle transport must also preserve explicit custom CA
trust whenever the operator provides it: shell install, upgrade, and uninstall
commands must pass --cacert to both the outer installer download and the
installer runtime, while Windows install, upgrade, and uninstall commands must
emit PULSE_CACERT and fetch install.ps1 through a transport-aware
PowerShell bootstrap that honors insecure-TLS or custom-CA settings on the
first script download instead of only after the installer has already started.
That bootstrap parity must match the installer's accepted trust formats too:
Windows copied commands must treat PULSE_CACERT as the same PEM/CRT/CER
certificate input that scripts/install.ps1 accepts, rather than narrowing
the first-hop bootstrap to constructor-only certificate formats.
That same unified settings shell install and upgrade transport must also
preserve plain-HTTP continuity automatically: when the selected Pulse URL uses
http://, copied Unix commands must append --insecure even without the
manual TLS-skip toggle, while only the explicit TLS-skip toggle may widen curl
itself to -k.
That same unified settings installer surface must not drift between preview and
clipboard transport: the rendered Linux/macOS/BSD and Windows install snippets
must already include the active token choice, custom-CA trust, insecure/plain-
HTTP handling, install-profile flags, and command-execution mode instead of
displaying one command and mutating it only during copy.
Windows first-hop TLS overrides must remain executable on the oldest supported
Windows PowerShell runtime. Generated install commands and inline
install.ps1 bootstraps use a compiled .NET
RemoteCertificateValidationCallback, never a PowerShell scriptblock delegate
that can be invoked on a worker thread without a runspace. Their custom-CA
loader accepts PEM and DER without relying on X509Certificate2.CreateFromPem,
which Windows PowerShell 5.1 does not provide. Native Windows verification and
the release workflow both execute the generated insecure and PEM custom-CA
commands against a self-signed HTTPS installer fixture, including the full
preflight-to-install handoff.
For Unix-family host installs, that same seamless installer contract requires
the copied command to fetch the shared installer into an ephemeral directory,
run install.sh --preflight-only before privilege escalation, and fail before
sudo if the selected Pulse URL or exact /download/pulse-agent?arch=...
artifact is unavailable or missing checksum metadata. Preflight must follow
redirects to the final Pulse download response before reading that metadata so
reverse-proxied and canonicalized public URLs do not fail on an intermediate
redirect that never served an agent binary. Token-bearing copied
commands must pass the credential to the installer through an ephemeral
--token-file and clean it up with the downloaded installer script, so the
installed service never receives a raw --token argument.
That same Windows install boundary must preserve the canonical server URL even
for the interactive PowerShell snippet: copied commands that still prompt for a
token must export PULSE_URL before invoking install.ps1, so the selected
agent-to-Pulse address cannot drift back to a default prompt target.
When the operator has already generated or selected a token, that same
interactive Windows install snippet must preserve the selected token in copied
transport as well, rather than silently dropping back to a second manual token
prompt while every other lifecycle command stays bound to the chosen credential.
The inverse must also hold: token-required instances without a selected token
must keep that interactive Windows snippet prompt-driven instead of exporting a
placeholder PULSE_TOKEN value into copied transport.
That same rule applies to copied Windows uninstall transport: after PULSE_URL
is escaped into env assignments, the uninstall path must still quote the
resolved install.ps1 URL so canonical URLs with spaces remain valid
PowerShell transport during deregistration and removal.
For the shell installer, that continuity must also survive beyond the original
clipboard command: when install or upgrade runs with explicit --agent-id or
--hostname, scripts/install.sh must persist those values into its saved
connection state and recover them during later offline uninstall instead of
dropping back to ambient local discovery.
That same lifecycle-owned connection.env contract must also stay on one
installer-owned helper path: scripts/install.sh may not write the state file
one way and then recover it through a separate field-by-field inline parser,
because lifecycle ownership requires one canonical reader/writer for persisted
install identity and trust metadata.
On least-privilege profiles, that canonical path is a root-owned private file
under the installer lifecycle directory, alongside a root-owned mode-0700
offline installer and mode-0600 checksum record. Recovery must reject a
symlink, a file not owned by the lifecycle process, an over-permissive file, or
a group/other-writable parent. Executing the saved installer must verify its
checksum before accepting any persisted lifecycle state. Runtime agent-id
recovery must use the descriptor-bound, no-follow, bounded lifecycle reader so
collector-controlled symlinks, FIFOs, devices, and oversized files cannot make
the root installer disclose a file or block. Safe-profile migration rollback
must snapshot and restore all three root lifecycle artifacts with the local
profile transaction.
When an install was streamed on stdin, the offline copy may be persisted only
after the installed root-owned lifecycle binary downloads it over the canonical
system-CA/custom-CA/exact-leaf-pin transport with proxies and redirects denied,
and the shell installer verifies the returned SSH signature against its
embedded release key. Generic insecure curl plus a locally generated checksum
is not source authentication; when authenticated bytes are unavailable the
installer omits the offline copy and tells the operator to fetch a fresh one.
Collector token and identity recovery from mutable state must use the bounded
descriptor reader through a root-trusted lifecycle binary. A legacy
least-privilege binary that is collector-owned is never executed as root; an
early tokenless migration without a trusted reader fails closed and requires a
fresh scoped credential rather than falling back to cat.
Generated Unix token-file commands must create their bootstrap credential only
after root or sudo elevation, inside a root-owned mode-0700 directory with a
mode-0600 token file. Frontend host commands and backend Proxmox commands must
preserve that shape and remove the complete bootstrap directory on every exit;
an invoking-user-owned mktemp file is not a trusted input to the root
installer on a fresh host.
When persisted state is absent or partial during update, legacy running-process
or service-unit recovery is a fallback into that same lifecycle continuity
model, not a separate source of truth: it may only seed the installer-owned
state for the upgrade and must not keep raw token arguments in the installed v6
service command. It may report recovery success only after URL and token are
both available, even when those values were assembled from different legacy
sources; partial URL-only recovery must fall through to the explicit
missing-state error so operators are not told the update state was recovered
when the installer still cannot reconnect. That fallback must also run after
saved-state parsing when explicit update arguments supplied the URL but legacy
v5 process or service state is still the only source for the token, identity,
feature flags, or trust posture.
That same lifecycle ownership must cover service control too: the installer may
still choose different platform adapters, but stop/restart semantics for the
managed agent must route through shared installer helpers instead of being
re-authored in each upgrade, systemd, OpenRC, SysV, or FreeBSD branch.
That same rule applies to teardown: uninstall and reinstall cleanup may not
rebuild disable/remove flows inline per platform. Shared installer helpers
must own service stop/disable/remove semantics for systemd, OpenRC, SysV, and
service-command runtimes so lifecycle cleanup stays canonical.
The shared state-directory remover must additionally require the requested
path to match the exact removal authority established by an explicit option,
platform selection, or protected lifecycle record. Indeterminate or untrusted
recovery retains the runtime directory for manual repair rather than invoking
recursive removal on an attacker-selected path.
When a local collector credential exists, uninstall must first resolve and
durably remove the exact bearer-bound server record through the same
CA/fingerprint, no-proxy, redirect-denying lifecycle client. TLS failure,
invalid confirmation, or server unavailability retains the local service,
credential, and recovery state; curl -k lookup/unregister is never rollback
or deletion authority.
Server confirmation requires the exact host/token transaction to persist both
the host-removal tombstone and any dedicated token revocation before live
teardown. Either persistence failure returns non-success with the live host and
retry bearer retained; after success, restart must preserve the removed host
state and reject the old bearer.
After that confirmation, full shell uninstall must remove the fixed root-owned
privileged-helper activation/staging state as well as collector state, units,
binaries, sockets, and credentials. Recursive helper-state deletion is allowed
only when the requested path exactly matches the installer-established helper
lifecycle authority; an indeterminate path is retained for repair.
The server also refuses teardown-authorizing success when a legacy collector
token still belongs to another live host; that credential must first be split
or rotated. A crash or response loss after both durable writes is fail-closed
in the other direction: the server remains removed and the old bearer remains
rejected, while the installer keeps local service and recovery state because
it never observed confirmation. Recovery is therefore an operator-verified
local-only cleanup: confirm the exact agent ID is absent through an
administrator session, stop the retained service, quarantine its collector
credential files out of the installer discovery paths, and rerun the protected
saved installer for local removal. A rejected old bearer alone is never
automatic deletion authority.
The same lifecycle rule applies to TrueNAS bootstrap too: boot-time recovery
for SCALE and CORE may only vary at the service-manager adapter, while binary
sync, service-link recreation, and startup sequencing stay on one
installer-owned renderer instead of drifting across separate embedded scripts.
That same lifecycle ownership must also cover service definition rendering:
systemd and FreeBSD rc.d files may not preserve parallel heredoc definitions
for the same agent runtime contract. Shared installer renderers must own the
common service shape, with platform branches only choosing the correct runtime
path, dependency targets, and logging adapter.
That same lifecycle rule also applies to installer completion: success,
unhealthy, and upgrade result handling may not drift by platform branch.
Shared installer helpers must own the save-state handoff, health verification,
canonical completion json_event output, and uninstall guidance instead of
letting each service-manager branch narrate those outcomes separately.
The same lifecycle rule applies to FreeBSD enablement too: direct rc.d install
and TrueNAS CORE boot recovery may not mutate pulse_agent_enable through
separate inline snippets. A shared installer-owned rc.conf enablement helper
must own that contract so lifecycle recovery and direct installs do not drift,
and that helper must execute the shared snippet in-process instead of defining
it in a discarded subshell.
The same rule applies to SysV registration: direct install may not keep its own
inline update-rc.d / chkconfig / manual symlink block while teardown owns a
separate canonical removal path. Shared installer helpers must own SysV
enablement and disablement semantics as one lifecycle contract.
The same durability rule applies to scripts/install.ps1: when Windows install
or upgrade runs with explicit agent or hostname identity, the installer must
persist that connection state under ProgramData and recover it during later
uninstall before falling back to machine-local discovery.
That Windows installer-owned continuity state is only valid for the currently
installed agent. After a successful uninstall, scripts/install.ps1 must clear
its ProgramData state so later reruns cannot inherit stale identity or transport
context from a removed node.
That persisted installer-owned state must also retain self-signed transport
intent: when install or upgrade ran in insecure TLS mode, later offline
uninstall must recover that mode from saved state instead of silently
reverting to strict certificate validation.
For the shell installer, the same offline transport continuity also applies to
custom CA trust: when install or upgrade ran with --cacert, later offline
uninstall must recover that saved CA bundle path before reaching for governed
lookup or deregistration transport.
The Windows installer must now preserve the same installer-owned custom CA
transport continuity for its own network calls: when install or upgrade ran
with PULSE_CACERT, later offline uninstall must recover that saved CA
certificate path before governed lookup or deregistration falls back to
strict default trust.
That same Windows custom-CA continuity must also reach the long-lived unified
agent runtime: scripts/install.ps1 must persist --cacert into the managed
service arguments, and pulse-agent must apply that bundle to updater,
remote-config, Unified Agent report, and command-channel HTTPS transport instead
of limiting PULSE_CACERT to installer-owned download and uninstall traffic.
That saved shell uninstall recovery must not depend only on a missing URL or
token. When the operator reruns uninstall with only partial CLI context, the
installer must still reload any missing persisted agent, hostname, insecure-TLS,
or custom-CA continuity before governed lookup or deregistration falls back to
ambient local state.
That same insecure-TLS continuity must hold during the Windows installer's own
network traffic, not only in the persisted service args: when the operator
selects insecure mode, scripts/install.ps1 must also relax certificate
validation for its binary download and uninstall deregistration requests so
PowerShell transport can reach self-signed Pulse instances end to end.
The same copied install and upgrade commands must also fail closed on
token-optional Pulse instances: when the server does not require API tokens,
the command builder must omit token arguments entirely instead of serializing a
fake sentinel token value into shell or PowerShell install transport.
That token-optional settings path must still preserve explicit governed token
selection when the operator generates one anyway: optional auth widens the
contract to allow tokenless transport, but it must not erase or suppress a real
selected token and force copied install commands back to tokenless-only mode.
The installer scripts themselves must honor that same optional-auth contract:
scripts/install.sh and scripts/install.ps1 must accept a missing token and
persist service arguments without --token on token-optional Pulse instances,
instead of advertising a no-token flow in settings while the installer still
fails validation at runtime.
That same optional-auth install contract also applies to backend-generated
Proxmox install commands in internal/api/configapi/config_setup_handlers.go and
internal/api/agent_install_command_shared.go: when Pulse auth is not
configured, the canonical agent-install-command API must return tokenless
install transport and must not persist a new API token record just because an
operator opened a backend-driven install surface.
That same backend-owned setup/install boundary also owns shipped security-doc
guidance in runtime responses and logs: internal/api/configapi/config_setup_handlers.go
and adjacent lifecycle setup helpers must not point operators at GitHub
main for security instructions that the running build already serves
locally, and should use the shipped /docs/SECURITY.md path instead.
The same optional-auth continuity must hold after install as well: Unified Agent
runtime startup may not reject a blank token unless enrollment is explicitly
enabled, agent report transport must omit auth headers when no token is
configured, and Proxmox auto-register flows must still complete without
serializing an empty token header on token-optional Pulse instances.
That same runtime-side reporting boundary must keep its product terminology
canonical in active comments and operator-facing logs: internal/hostagent/
may remain a package-location fact, but successful and failed report transport
must describe the runtime as the Unified Agent rather than reintroducing
"host agent" wording into v6 operator guidance.
That same post-install optional-auth contract must also hold during managed
removal: uninstall and deregistration flows must still notify Pulse with the
canonical agent-uninstall payload when URL and agent identity are known, and
must only attach API-token headers when a real token exists instead of
silently skipping deregistration on token-optional installs.
The same settings/profile boundary must also preserve assigned-profile
continuity when a referenced profile is no longer present in the fetched
profile list: assignment controls must keep the missing profile visible as the
current state instead of collapsing the agent back to a false default-looking
selection.
That same uninstall-command boundary must also preserve platform-canonical
transport in copied utility actions: Windows agents must receive the
PowerShell uninstall flow, and copied uninstall payloads must never substitute
an API token record ID where the runtime expects the real token secret for
server-side deregistration.
The same rule applies to Unix shell uninstall commands in the shared fleet
settings surface: copied uninstall payloads may include only a real token
secret when one is available, and must never fall back to token record IDs or
other settings-only identifiers that the installer runtime cannot authenticate.
Token-optional Windows uninstall commands must also preserve the canonical
server URL in PULSE_URL; otherwise the PowerShell installer can remove the
service locally while losing the deregistration path back to Pulse.
Shared internal/api/ resource helpers now also expose governed
policy-aware resource metadata. Agent lifecycle and fleet-control surfaces may
consume canonical policy and aiSafeSummary fields from unified resource
payloads when they need resource context, but they must not fork their own
sensitivity-classification or local-vs-cloud routing heuristics on the same
runtime boundary. The same shared resource boundary now also owns the bundled
facet history read path for timeline data, so fleet lifecycle surfaces that
open resource drawers must continue to consume the backend bundle instead of
reassembling a local multi-call summary.
That same shared internal/api/ extension-point boundary now also assumes
canonical security-token lifecycle reads. Lifecycle-adjacent setup and install
flows may inspect token metadata through the shared auth/security routes, but
they must not assume a displayed relay pairing token is disposable once
lastUsedAt is set. Shared helper changes that refresh, hide, or replace a
pairing credential must preserve used-token continuity instead of deleting a
credential that an already paired device still depends on.
That same shared internal/api/ boundary now also owns agent-derived
physical-disk history transport. Lifecycle-adjacent storage drawers and fleet
resource surfaces may show host SMART-backed disk telemetry through the shared
/api/metrics-store/history route, but they must read the canonical disk
metrics target that monitoring projects for the resource instead of reviving a
browser-local disk collector, agent/device concatenation scheme, or other
surface-local history identity.
The browser-side runtime boundary is now explicit too. Lifecycle-owned
settings hooks such as
frontend-modern/src/components/Settings/useAgentProfilesPanelState.ts and
frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx
may read websocket state only through
frontend-modern/src/contexts/appRuntime.ts. They must not import @/App or
recreate app-shell providers, because frontend-modern/src/App.tsx owns
provider placement while lifecycle hooks must stay lazy-load safe and
shell-independent.
That same adjacent internal/api/ boundary now also keeps public demos from
leaking commercial state through lifecycle-adjacent surfaces. Agent install,
reporting, and setup flows may share backend helpers with billing or license
transport, but DEMO_MODE must continue to 404 commercial read surfaces
instead of teaching lifecycle or mock-mode paths to bypass licensing. Public
demo readiness therefore comes from hiding commercial presentation on the
shared API boundary, not from introducing a second fake-entitlement path into
lifecycle-owned install or reporting flows. Browser-facing lifecycle surfaces
must also treat /api/security/status.sessionCapabilities.demoMode as the
canonical public-demo bootstrap signal instead of inferring demo posture from
headers, /api/health, or hostname heuristics.
Lifecycle-owned connected-infrastructure and reporting surfaces must also keep
governed platform rows on canonical local operator identity while tolerating
optional optimistic hostnames. Shared row models may fall back to the row name
when staging a removal state, but they must not resurrect legacy
policy.display shims or require platform-managed surfaces to synthesize a
second hostname contract.
That same lifecycle boundary also owns host-agent runtime test seams. Shared
agent lifecycle code such as internal/hostagent/agent.go must keep mutable
test hooks, command-client factories, and timing overrides on the per-config or
per-agent instance instead of package-global variables, so concurrent
lifecycle-owned update and registration paths cannot leak one test or runtime
override into another agent session.
The infrastructure workspace now uses a single flat route
(/settings/infrastructure) instead of the former three-sub-route layout
(/install, /platforms, /connections). Panel state — which add/edit
modal is open and which add step is active — is managed through the governed
InfrastructurePanelStep query contract rather than URL segments.
frontend-modern/src/components/Settings/infrastructureWorkspaceModel.ts
exposes buildInfrastructureWorkspacePath() and
buildInfrastructureOnboardingPath() as the sole path-building contract for
lifecycle-adjacent install and setup surfaces. Callers that formerly passed
'platforms' or 'install' to buildInfrastructureWorkspacePath must use the
no-argument form or the query-backed onboarding builder; removed subpaths must
not remain as compatibility hrefs.
frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx now uses
shared infrastructure path-builder constants for workspace, add-infrastructure,
and agent-install CTAs, replacing the former pair of
INFRASTRUCTURE_INSTALL_PATH and INFRASTRUCTURE_PLATFORMS_PATH constants.
Installer-owned runtime continuity on persistence-sensitive NAS platforms is
also explicit again: scripts/install.sh now owns the QNAP bootstrap contract
that waits for the persistent data volume before launching the stored wrapper,
and internal/agentupdate/update.go keeps the persisted QNAP binary copy in
sync on self-update so reboot does not roll the runtime back to an older
binary.
That appliance lifecycle must remain bounded by available storage. Before
downloading or replacing an agent, scripts/install.sh checks the effective
temporary and installation filesystems for enough free space, coalesces the
check when both paths share a filesystem, honors an operator-selected
TMPDIR, and exposes the same check through --preflight-only. QNAP and
Unraid watchdogs must route agent output through the agent's rotating
--log-file writer instead of an unbounded shell append; their own watchdog
messages must also be size-bounded so either logging path cannot exhaust a
RAM-backed appliance root. The QNAP wrapper is also a singleton lifecycle
owner: concurrent starts from flash-backed autorun.sh and an install or
upgrade must elect exactly one watchdog before terminating or launching an
agent. The elected watchdog records both supervisor and child PIDs, removes
only lock state it owns, and terminates its child on shutdown so a second
watchdog or orphan agent cannot accumulate appliance CPU.
Unified Agent lifecycle truth is agent-authored. The host report contract now
carries the non-secret applied managed-config fingerprint, self-updater state
and timestamps, last successful source version, and per-module initialization
state. /api/connections may compare the reported applied fingerprint with the
canonical desired fingerprint and may expose updater or module failures, but it
must not synthesize those facts from server version comparison or from the
mere presence of a process. Successful update evidence observed on the first
post-restart report remains retained across later reports so a one-shot update
marker cannot disappear before an operator sees it.
The runtime readiness boundary is module-aware. /healthz remains process
liveness, while /readyz, /status, and the module readiness Prometheus
gauges identify enabled modules that are starting, retrying, or running. An
enabled Docker or Kubernetes module that is still retrying initialization may
not be laundered into an overall ready state.
Installer-owned connection continuity includes certificate pinning. Unix and
Windows installers must validate a supplied SHA-256 leaf-certificate
fingerprint, persist it in connection.env, recover it during later lifecycle
operations, and pass it to the long-lived service as
--server-fingerprint. A certificate pin is distinct from blanket insecure
TLS mode even when installer download tooling must temporarily bypass chain
validation after the explicit pin check.
Native support evidence is governed by
docs/release-control/v6/internal/UNIFIED_AGENT_PLATFORM_SUPPORT.md and
.github/workflows/unified-agent-native.yml. Cross-compilation and archive
presence are build evidence only; platform support claims must name native CI,
native installed-lifecycle, appliance-lab, and platform code-signing evidence
separately. The native Windows binary self-test must use runner-temporary state
and logging paths so it cannot create the machine-global ProgramData\Pulse
directory before the same job exercises the clean install, update, service
recovery, and uninstall lifecycle. The dedicated ephemeral Windows job must
also clear test-only ProgramData\Pulse state immediately before that proof;
the lifecycle harness still refuses to proceed when the PulseAgent service is
already installed.
The Windows native lifecycle job's generated install-command proof consumes
the governed frontend toolchain and must therefore select Node.js 24, in
parity with the release and integration workflows. That build prerequisite
does not widen agent runtime or installer authority, but drift to another Node
major invalidates the native lifecycle job as release evidence.
Platform-native lifecycle proof must exercise the platform's real filesystem
boundary rather than importing Unix test assumptions. Collector bearer files
use owner and mode checks on Unix and protected owner/DACL checks on Windows;
test credentials must be hardened through that same platform abstraction.
Privilege-helper test paths must be absolute under the executing platform, and
download fixtures must derive the release asset OS and architecture from the
native runner. A fixture that silently requests amd64 on arm64, or a Windows
test rejected only because /run/... is not a Windows absolute path, is not
valid lifecycle evidence.
Normal release promotion requires platform-native identity for desktop agent
binaries. macOS agents must be Developer ID signed, submitted successfully to
Apple notarization, and accepted by spctl; Windows agents must carry a
verified Authenticode signature. Those native bytes must replace cross-built
desktop binaries before release packaging, Pulse checksum/signature creation,
SBOM generation, and immutable candidate manifest creation. Missing signing
credentials or failed native verification is a release failure, not a warning
or an unsigned fallback.
Setup completion preview actions are part of the lifecycle handoff contract. The preview copy affordances must use the shared accessible action-button primitive, carry localized names for the exact value being copied, and retain a 40-pixel mobile touch floor. The setup surface must not reintroduce raw, unlabelled icon buttons or a phone-only copy-control fork.
The setup completion surface also owns the Pro activation handoff. When the
compiled Pro edition reports no valid license (both read with the setup
token from /api/license/runtime-capabilities and /api/license/status,
gated by shouldShowSetupProActivationPointer requiring an explicitly
invalid license, never a failed or missing probe), the completion panel
renders a factual activation card whose action targets the direct
/settings/pulse-intelligence/billing/plan route. That direct-route target
is deliberate: it must not depend on any navigation entry being visible, and
the pointer must never appear on community builds, licensed installs, or
probe failures — the community self-hosted opt-in commercial posture is
unchanged. On compiled Pro-edition binaries the served presentation policy
now also exposes the Plans & Billing navigation entry before activation
(edition-derived commercial context in the security-status contract), so the
first-run pointer and the visible nav entry together close the activation
handoff; the pointer keeps its own explicit license gate rather than
inheriting the nav policy.
Agent-facing action completion events and resource-context history now expose
the canonical ActionResultV2 alongside bounded legacy projections. Consumers
must branch on execution and verification separately: a completed transport is
not a verified postcondition, and an independently verified label is valid only
when the canonical evidence identifies a distinct trust domain. Agent-side
producer migration and UI wording are downstream governed work, not permission
to add an agent-local truth model.
Autopilot activation is not action approval
The server-owned Patrol Autopilot acknowledgement authorizes only the effective tenant Patrol mode. It does not approve an action, weaken capability policy, satisfy an MFA floor, manufacture dispatch authority, or alter agent receipt and result semantics. Every resulting action still enters the canonical Task 08 actor/RBAC/approval boundary, Task 07 dispatch contract, and Task 10 two-axis result contract. API tokens are ineligible for the human Autopilot acknowledgement even where an owner-bound token remains compatible with an authenticated action capability. Consuming that acknowledgement at the API gate changes only the effective tenant Patrol mode: omitted investigation budget and timeout controls remain unchanged and cannot be synthesized as zero-valued lifecycle authority.
Task 07 generic durable agent-operation receipts and Task 09 APT adapter
Host update and package-cache cleanup use one strict server/agent codec; unknown fields, trailing JSON, malformed identity, and open command, path, package, removal, or reboot authority are rejected. Pending delivery binds the authenticated agent, dispatch request, action, and operation. All APT inspection, refresh, update, and cleanup paths share one composition-root- injected lease; nil authority fails closed. Metadata refresh is the first possible external effect.
The agent-side authority is the generic SQLite operation-receipt store in
internal/operationreceipt/, not an APT-specific journal. Before any typed
mutation, it durably binds the canonical dispatch attempt, action, operation
kind/version, request digest, and authenticated agent identity. Admission and
start are atomic keyed transitions; a process reopen conservatively converts
accepted or started work to interrupted_unknown, which is queryable but never
automatically executable. Strict versioned terminal envelopes remain exactly
replayable while recent, then compact to immutable identity-and-digest
replay-denial tombstones. Replayable payload bytes are TTL/byte bounded;
tombstone metadata grows monotonically by design so no prior attempt can become
new authority. Disk exhaustion fails the current admission before mutation and
recovers when storage capacity returns; operator disk monitoring remains
required and total tombstone disk use is not described as bounded.
The server derives operation version and request digest after canonical request
fields are fixed, persists that binding on the action dispatch attempt, and
queries the authenticated agent by the exact binding after callback loss or
restart. not_found, interrupted, tombstoned, malformed, late, duplicate,
wrong-agent, and mismatched responses never authorize resend, and none of them
can ever produce success or agent-attested verification. They are not, however,
an open-ended wait. Because a reopen converts accepted or started work to
interrupted_unknown and nothing can move an interrupted or tombstoned receipt
back to terminal, an identity-correlated interrupted or tombstoned answer is
proof that this agent will never complete the operation, and the API-owned
lifecycle settles the action as inconclusive on the spot. A not_found answer,
or a receipt still merely accepted or started, keeps the attempt receipt-pending
until the API-owned bounded reconciliation window elapses and is only then
settled the same inconclusive way. The agent still authors no execution,
verification, evidence, or compensation truth in any of these paths: it reports
receipt state, and the server alone decides that the absence of evidence is now
durable.
Terminal payload kind/version and strict adapter codecs are revalidated on
write, read, query, and reopen. APT update and cleanup are adapters over this
generic owner, persist only bounded sanitized typed results, and still share
the package-manager lease. Raw commands, paths, package selectors, stderr,
secrets, and unbounded output are outside the receipt schema.
Task 09 now consumes that continuity end to end. A delayed terminal receipt is
validated against its immutable identity/digest and the agent-attested durable
terminal boundary, not the later query time. The server retains the later
query receipt separately. Impossible observation/terminal chronology, a
future terminal timestamp, stale-at-completion evidence, an evidence-bearing
before-state mismatch, or a hostile envelope still fails closed. A legitimate
pre-mutation inventory or cache-fingerprint drift result remains terminal and
inconclusive: its newly observed Before value is not substituted for the
originally authorized expected value when validating the admitted request
digest. The generic receipt store continues to bind completion and replay to
the exact admitted attempt/action/operation/digest/agent identity, while the
drift observation records why mutation was refused and replan is required.
Typed APT, storage-cleanup, Docker, and Proxmox result payloads carry an optional bounded
reason_code for pre-mutation refusal. Codes distinguish invalid contracts,
missing agent capabilities, target drift, unavailable inspection, package
manager contention or health, and bounded preflight failure without exposing
stderr, commands, paths, package names, or provider text. Older agents may omit
the field and remain valid; the server projects that legacy absence to
preflight_refused. A refusal code cannot accompany a started mutation or a
successful result. An already-empty package cache is a verified no-op success,
because the requested postcondition is already satisfied.
Callback loss and a reopened server store reconcile APT, Docker, and Proxmox
actions by exact identity query only; the original typed dispatch is never
resent. Proxmox query validation recognizes every closed lifecycle verb,
revalidates the terminal envelope and chronology, and rejects mismatched
action, digest, or agent identity. Legacy APT v1 terminal
payloads that predate additive package-manager health facts remain
structurally valid receipts, but their verified claim is projected as health-
unknown/inconclusive rather than confirmed.
For host updates, MutationStarted begins immediately before the fixed install
command, not during metadata refresh or simulation. A fixed read-only
dpkg --audit health check under the same shared lease supplies explicit
health_checked, package_manager_healthy, and recovery_required facts.
Refresh failure, refreshed inventory drift, zero-pending state, and pre-install
health refusal therefore cannot claim partial installation or action-caused
recovery. Install/verify failures preserve phase, remaining count, tri-state
health, and recovery posture through the existing ActionResultV2 summaries.
Durable receipt protocol support is explicit agent-reported and server-observed capability metadata. Missing, legacy, or future versions stay connected for monitoring but advertise no APT mutation capability, emit no actionable APT finding, and fail dispatch readiness. Product version strings never imply this authority. The raw protocol integer stays internal to ingest, registry, and live dispatch checks; customer/frontend resource JSON consumes only derived capabilities and readiness. The fake-only code/test floor for claims 16 and 17 is implemented; both claims and workflow scorecards remain below operational completion until browser proof, disposable Debian/Ubuntu lab proof, and Task 12 certification are complete.
RG-06 and RG-09 keep the executing agent's fresh typed readback classified as
agent_attested. A separate Colima control-plane observation proves the lab
fixture changed, but it does not enter the authenticated product action result
and therefore cannot upgrade the product finding to fix_verified. Until a
distinct-trust-domain observation is ingested into ActionResultV2, those
findings remain fix_verification_unknown and unresolved. Proxmox VM/LXC
lifecycle is now the first production path to ingest a server-side provider
observation as independent evidence; the Docker restart lab journey remains a
positive control but does not imply that agent-reported Docker inventory is an
independent production observer.
The API router's tenant-scoped commercial resolver for report scheduling is an
adjacent entitlement boundary, not agent authority. It may stop a background
report before generation when the current license lacks advanced_reporting,
but it must not mint agent credentials, dispatch agent work, reinterpret an
agent receipt, or expand any lifecycle capability.
The adjacent shared authentication boundary must also preserve agent liveness
while an operator or qualification client uses local Basic authentication.
Agent API-token validation may update the token usage record under the global
configuration write lock; a simultaneous local credential check must not take
a recursive read lock while already inside checkAuth's read-locked decision.
The server must continue accepting reports and command registrations under
that mixed-auth load rather than allowing a queued writer to freeze the API.
Once that local credential check succeeds, the verified operator identity must
travel in the request context into action-governance authorization. A response
header alone is not identity. This lets a Basic-authenticated qualification
operator approve or reject an agent action while preserving the agent token's
separate reporting and command authority; invalid credentials must never reach
the action handler or acquire an operator principal.
Multi-destination reporting authority
The Unified Agent may fan one collected host, Docker/Podman, and Kubernetes snapshot out to one primary Pulse destination and zero or more report-only observers. Exactly one destination is authoritative. Only that primary may provide remote configuration, commands, enrollment, update selection, or canonical agent identity. Observer response bodies cannot enter any control path. Delivery queues, authentication failures, and persisted host-report buffers are destination-scoped; failure of one destination cannot block or replay reports to another.
Assigned availability-probe observations use the same primary-report delivery
boundary. Concurrent probe completions must allocate their monotonic sequence
and append to the pending queue in one ordered critical section; the queue must
never contain a newer sequence before an older one. A report snapshot records
the last included sequence as its delivery high-water mark. A successful
primary acknowledgement removes only observations at or below that mark, while
observations completed after the snapshot remain queued for the next report;
failed or buffered delivery retains the whole unacknowledged batch. This keeps
multi-target probe concurrency from stranding already-delivered observations
or discarding observations that the primary never received. The scheduler and
concurrent-enqueue coverage in internal/hostagent/availability_test.go pins
the ordering and acknowledgement contract.
Every assigned availability result also carries the server-issued target configuration revision and a stable observation ID. The agent allocates that ID once when the scheduled execution completes and preserves it across report buffering or retry, so an acknowledged report retry cannot create a second history observation. The server rejects results for an obsolete revision, uses receipt time as the coverage timeline, and retains the agent-authored check time only as evidence metadata. Legacy agents that omit the additive ID remain ingestible through a deterministic server fallback, but they do not gain authority to author revision boundaries, target identity, retention, or the service-assurance timeline.
The server may assign one target to several agent-backed observation locations,
but each agent receives and reports only the targets whose canonical
agent:<agent-id> location it owns. Location membership is server-authored
configuration, not agent enrollment identity: it does not grant control-plane
authority, create another agent resource, or let one reporter stand in for a
different site. Reassignment increments the target configuration revision, so
late results from the previous location set are rejected. Local Pulse execution
can coexist with assigned agent execution for the same logical target, while
license lapse removes every remote location from the effective assignment and
leaves the local path as the fail-safe execution boundary.
Observer configuration is explicit, versioned, and file-backed. It contains no raw token values and resolves each token from a separate private absolute-path file. Proxmox registration is also destination-scoped: the primary retains its legacy token name for upgrade continuity, observers use distinct token names and state markers, and every setup path must obtain a successful registration state response before any create, delete, or rotation command is executed. Plaintext transport consent is destination-scoped too. The primary keeps the existing local-network compatibility and explicit process-level override, but an observer must opt in independently for every non-loopback HTTP URL; primary consent cannot silently widen an observer. That policy must survive the shared config loader, host/Docker/Kubernetes target normalization, and observer Proxmox registration path. Unix and Windows service installation must preserve the absolute observer-config path across an in-place update. Cross-platform config proof must serialize observer JSON through the canonical encoder so Windows absolute token-file paths remain valid JSON rather than being interpreted as escape sequences.
The adjacent recovery handlers under internal/api/ do not widen this agent
lifecycle boundary. Protection posture is a read-only monitoring:read
projection over recovery points and provider collection evidence. It does not
register agents, issue or rotate credentials, interpret observer responses,
or grant backup, restore, command, or remote-configuration authority.
The adjacent Patrol attention read handlers and shared router registration do
not widen agent authority. Reads consume the canonical alert lifecycle under
monitoring:read; they do not accept agent reports, mint credentials, or
reinterpret an observer response as configuration. Phase 5 may plan one
evidence-gated Docker restart only after current plan/approve/execute authority,
declared capability, and executor readiness checks. That attention handler
binds an internal origin and enters the existing action lifecycle; it does not
deliver a command itself or add an agent wire shape. Actual dispatch retains
the canonical action executor, exact agent/resource binding, command-enabled
token policy, durable attempt/receipt, timeout, and restart-reconciliation
boundaries.
Operational Trust lifecycle mutation boundary
The attention evidence and lifecycle mutation handlers under internal/api/
remain resource-monitoring consumers. Acknowledge, unacknowledge, suppress, and
unsuppress call the alerts-owned operational lifecycle and persist through the
active-alert store; they do not mint agent credentials, alter observer config,
or dispatch commands. Evidence reads require monitoring:read, lifecycle
mutations require monitoring:write, and slash-containing canonical record IDs
are parsed as opaque identities. Governed action planning still enters the
existing action lifecycle and entitlement boundary before any agent executor
is considered.
Alert intent remains outside agent authority
Alert-intent policy reads and writes, UDP availability probes, operator-state lookups, and backup-task context do not widen the agent lifecycle. The server evaluates these read-state inputs without issuing an agent command, accepting a new report shape, rotating credentials, or changing remote configuration. Neither an expected-transient decision nor an indeterminate UDP outcome may be translated into an agent action. Any later customer-infrastructure mutation still requires the canonical Actions planner, approval, executor, receipt, audit, and verification boundaries. Powered-off tolerance is evaluated from the Pulse server's monotonic elapsed time and the existing VM/LXC observation stream. It does not accept agent time, poll count, a new report field, or remote configuration authority, and a guest resource-type default cannot change node or host-agent connectivity confirmations.
Server-side host deletion and re-enrollment authority
Agent lifecycle owns the complete server-side host deletion transition through
internal/config/host_continuity.go, internal/monitoring/monitor.go,
internal/monitoring/monitor_agents.go, internal/models/models.go, and
internal/api/agent_ingest.go. Removing a host must atomically convert its
active continuity record into a durable removal tombstone before token
revocation, live-state deletion, alert cleanup, resource unlinking, or remote
configuration cleanup can be reported as successful. A journal write failure
restores the live host and returns a server error. An unreadable journal blocks
monitor startup rather than starting with an empty deny set.
The tombstone retains the canonical host ID, machine ID, agent-reported ID, report-host ID, hostname, platform, token ID, and removal time. Monitor construction hydrates non-expired tombstones before report admission; active continuity, licensing, inventory, and remote-config lookups exclude them. Tombstones expire durably after 24 hours. If expiry cannot be persisted, the server keeps the deny boundary in memory and on the next restart.
A report may clear a tombstone automatically only when it presents a different
token created after removal and its canonical or retained alias, machine ID,
and hostname still identify the removed machine. A fresh token for a cloned
machine, different hostname, or conflicting machine ID must not clear the
record. Successful re-enrollment preserves the original canonical host ID and
retains the detached old token in the identity's deny lineage. This lets a
shared old token continue serving an unrelated active agent without allowing
that credential to recreate or remote-configure the removed host. Revoked,
expired, missing-scope, and pre-removal credentials remain denied. The
operator-only allow-reenroll action is the explicit override that may clear
both the tombstone and detached-token lineage.
A fresh installation can also arrive while an older same-machine host record
is merely stale rather than tombstoned. When the presenting token's CreatedAt
is newer than that record's last observation and machine ID plus normalized
hostname identify the same physical host, report admission treats the event as
explicit re-enrollment: it preserves the established canonical host ID,
rebinds the new token, removes older duplicate generations and their token
bindings, and therefore preserves physical-disk resource continuity. A
pre-existing generation observed at or after the new token was created is live
and must not be superseded. The focused proof lives in
internal/monitoring/monitor_host_agents_test.go.
Report ingestion holds a shared lifecycle lock for the full state transition;
deletion, manual allowance, and tombstone expiry hold the exclusive side.
Reports that began before deletion complete first, then deletion removes their
result. Reports arriving after deletion see the tombstone. A completed delete
therefore cannot be undone by an in-flight report. Before that delete returns,
the same transition must republish the canonical unified-resource generation
so an immediate /api/state read cannot retain the removed host behind the
read-path freshness window. Unrelated and duplicate-machine agents remain
concurrent and independently bound by token plus hostname. The focused
read-after-delete proof lives in
internal/monitoring/monitor_host_agent_removal_lifecycle_test.go.
The server transition does not introduce a second installer lifecycle.
Generated host install commands still mint the canonical scoped token, and the
existing Unix systemd/state-directory and Windows ProgramData/service
installers remain idempotent across rerun, process restart, and service
restart. internal/api/host_agent_removal_lifecycle_integration_test.go,
internal/config/host_continuity_test.go,
internal/monitoring/monitor_host_agent_removal_lifecycle_test.go, and the
existing scripts/installtests/agent_state_dir_lifecycle_test.go,
install_sh_test.go, and install_ps1_test.go are the production-path proofs.
Config reload must also hand the replacement monitor's tenant-scoped guest, Docker, and host metadata stores to shared API/config persistence consumers. That handoff is lifecycle continuity only: it prevents post-reload agent reports and resource projection from observing an orphaned metadata cache, and does not add a report shape, credential, command, or remote-configuration authority. Web-interface URL continuity shares that handoff but does not widen lifecycle ownership. Docker/Podman display-name updates must merge with the runtime host metadata record so they cannot discard its URL or notes, and monitoring projection may follow stable source identities across reconnect, rename, recreation, or canonical alias migration. None of those navigation-metadata operations may reverse a removal tombstone, re-enroll an agent, change report identity, or authorize commands.
Proxmox install bootstrap and runtime-health authority
The Proxmox Unified Agent has one canonical runtime registration surface:
/api/auto-register. When the agent already has an API token, startup checks,
five-minute health checks, token repair, re-registration, and URL-candidate
changes must authenticate directly to that endpoint. They must not probe
/api/setup-script-url to discover whether the runtime token can mint a setup
artifact. That endpoint remains an operator control-plane surface protected by
settings:write; an expected denial there is not part of a healthy runtime
control flow.
Registration checks return separate registered, sourceExists, and
canRegister facts. A disconnected configured source can therefore be repaired,
while a removed source paired with an ordinary agent token leaves local
credentials untouched and directs the operator to run a fresh install command.
Agents treat the two capability fields as permissive only when talking to an
older server that omits them during a rolling update.
Ordinary agent:report tokens remain limited to registration checks and token
updates for existing PVE/PBS sources. The PVE/PBS token minted by the
settings-authorized agent-install command carries a narrower bootstrap
exception: its server-owned install_type and issued_via metadata may create
one source of the declared type. The server binds that grant to the first
fully validated presenting hostname before local Proxmox credentials are
mutated, serializes checks with completion, persists the completed type,
hostname, selected host, node, and time, and then permanently returns the token
to update-only behavior for that type. A malformed request, wrong type,
different hostname, reused grant, rejected/rotated token, or arbitrary agent
token must fail closed with visible authorization diagnostics.
A positive registration preflight for an already-existing source consumes the
fresh-install grant without rotating local credentials; a negative preflight
keeps it available for the immediately following initial completion.
The bootstrap exception covers both install-token shapes the settings-write
control plane actually mints. A PVE/PBS-typed agent-install token stays pinned
to its declared type. A generic host install token minted by the Settings →
Infrastructure installer additionally holds the same bounded grant for each
canonical Proxmox type, because the unified installer auto-detects PVE/PBS on
the target machine and the agent presents the detected type at registration
(#1644). The host-token grant keeps every existing bound: settings-authorized
mint via issued_via, first-presenting-hostname binding before local Proxmox
credentials are mutated, serialized completion, and one-shot consumption — and
non-Proxmox request types never hold a grant.
Consumption is recorded per canonical type, not per token. A host with both PVE
and PBS installed is an officially supported deployment, and the agent's
RunAll registers each product in turn from the one install token; recording a
single consumption for the whole token refused the second product and made the
installer print a failure banner over a successful first registration (#1644).
One install token therefore bootstraps one PVE source and one PBS source, each
still one-shot: a second create of an already-consumed type fails closed with
the same 403. The bounds that are not per type stay singular — the 24-hour
mint-age clock and the first-use bound_hostname are shared, so whichever type
registers first pins the hostname for both and the second type cannot be
redirected at a different machine.
The consumed types are recorded in proxmox_registration_consumed_types, and
its presence is what distinguishes a per-type record from a token spent under
the previous per-token semantics. A record carrying only
proxmox_registration_completed=true predates the per-type ledger and means the
whole token was spent, so on upgrade it must be read as every canonical type
consumed; reading it as "one type spent, the other free" would hand a fresh
create to every already-used install token in the field. The unsuffixed
completion block continues to describe the most recent completion so an older
Pulse binary reading the same token store also fails closed.
The grant is time-bounded independently of the token that carries it. Install
tokens are minted without an expiry because the agent reports with them for the
life of the install, so the bootstrap grant carries its own 24-hour clock from
mint (install_issued_at, falling back to the token's creation time; a record
with neither fails the grant closed). Past that window the token keeps
reporting and stays update-only, the denial takes the same 403 path as any
other ungranted token, and the server logs the expiry distinctly so an operator
running a stale install command sees why source creation was refused.
Grant consumption and source creation must fail together. The one-shot grant is consumed and durably persisted BEFORE the created source is written, and a failed source save rolls the consumption back. The opposite order leaves a persistently failing token store able to write a source while the grant stays live, which is a repeatable create-a-source primitive under one install token. Either the source is persisted and the grant is spent, or neither happened and the install can be retried. The rollback is scoped to the type being consumed: on a combined host a failed PBS save restores the PBS grant and must not resurrect the PVE grant that already produced a persisted source.
A host install token that has already auto-registered its Proxmox source
carries a bound_hostname written by the registration bootstrap, with no
bound_agent_id and no exec binding version. That is still a clean first use
of the command channel: the first command enrollment whose hostname matches the
bound one binds the fresh runtime agent ID and the current binding version,
rather than being admitted by the legacy pre-v6.1.1 identity-migration branch.
The registration-bound hostname is authoritative and is not rewritten by an
equivalent spelling the agent reports, because the still-unconsumed install
grant compares against it.
Command-capable Docker / Podman resources may retain a last-seen telemetry token after their enrollment token rotates. Action dispatch therefore tries the report token first, then permits recovery only through one live admission in the same tenant whose agent ID and canonical hostname both match the resource. Independent ID-only or hostname-only fallback remains forbidden for token-named resources, preventing a stale report from crossing identities while restoring start, stop, restart, and update control for the same agent (#1728).
A blocked registration is not a silent skip. When the pre-registration check
reports canRegister=false, the agent must log at error level, return a setup
error to its caller, and record the operator-facing reason in a
proxmox-<type>-registration-blocked marker in its state directory; the
marker is cleared once a later run finds the source registered or proceeds to
register. The shell installer reads that marker after agent health
verification and surfaces the denial with remediation in its own output, so
the failure is visible at install time instead of buried in the agent journal.
With per-type grants that path is reserved for genuine refusals: the second
product of a combined host is no longer denied for having spent the token's
only grant.
The installer's verdict is per product, not per host. RunAll publishes the
products it detected in a proxmox-detected-types state marker, and the
installer waits for an outcome from each of those products before reporting,
then prints a success or a denial line for each one. Without that list a
combined host reported whichever marker appeared first, which produced either a
premature success or an error banner that buried a completed registration.
Agents that predate the marker keep the previous first-outcome-wins timing, so
a PVE-only host never waits out the timeout for a PBS outcome that will not
arrive.
Token-optional installations retain the setup-token bootstrap path. Ordinary
hosts never enter this Proxmox registration loop, while PVE, PBS, mixed-product
hosts, restarts, and hosted-tenant install tokens retain the same authority
split. internal/hostagent/proxmox_setup_test.go,
internal/api/configapi/config_handlers_auto_register_test.go,
internal/api/configapi/proxmox_install_registration_test.go, and
internal/api/configapi/issue1644_host_install_token_proxmox_test.go prove the recurring
health, first-install, type/host binding, one-time consumption, rejection,
host-token bootstrap, and concurrent-completion contracts.
Shared internal/api request-origin resolution is lifecycle credential targeting
The shared internal/api boundary this subsystem consumes now routes
requestOriginBaseURL, in-memory public-URL capture,
Router.resolvePublicURL, config-owned PVE/PBS install commands, and
setup-script artifacts through one resolveRequestOrigin trust boundary and
one resolveConfiguredPublicBaseURL precedence function in
internal/api/router.go. Forwarded host, scheme, and port are admitted only
from an immediate peer in PULSE_TRUSTED_PROXY_CIDRS; direct and forwarded
hosts must pass strict authority validation, including bounded ports and valid
IPv4/IPv6 forms, before they can become an absolute URL. The prior
resolveLoopbackAwarePublicBaseURL implementation is retired so lifecycle
code cannot drift back to raw request authority or untrusted forwarded scheme
handling.
The earlier governance claim that this derivation touched no token issuance,
update targeting, or fleet lifecycle was false once live request origin began
outranking an auto-detected public URL. The resolved value targets
token-bearing config PVE/PBS install commands, setup-script artifacts,
diagnostics install commands, and cluster deployment payloads, and it also
feeds hosted install, onboarding, and agent URL projections.
Therefore lifecycle consumers must treat it as an API/security credential-
target boundary: explicit AgentConnectURL or operator PublicURL remains
authoritative, only validated live origin may replace an auto-detected guess,
invalid request evidence falls back safely, and hosted mode never guesses.
It does not itself grant registration or command authority, but raw Host or
forwarded headers must never bypass it before credentials are placed into an
install or deploy transport. TestContract_RequestOriginCannotRetargetTokenBearingCommands
is the routed lifecycle/API proof and invokes the actual
POST /api/agent-install-command PVE/PBS handler so every asserted target is
paired with the fresh token returned by that endpoint.
Hosted lifecycle callers now receive the runtime mode from
Router.hostedMode through ConfigHandlers; they no longer call the shared
resolver with a self-hosted constant. In hosted mode, a non-empty
AgentConnectURL remains authoritative and an explicit PublicURL remains
the fallback, but the selected value must pass the canonical Pulse HTTP base
URL validator. An invalid authoritative value, an auto-detected PublicURL,
or missing configuration fails closed without consulting direct or forwarded
request authority. Config-owned PVE/PBS install commands and setup-script
bootstrap artifacts resolve that target before creating an API or setup token,
the diagnostics Docker/Podman migration route resolves it before creating or
persisting its lifecycle token, and password-driven PBS setup resolves it
before contacting PBS to derive or create the managed token label.
TestContract_HostedInstallerOriginsFailClosedAtRouter
exercises those registered routes for PVE and PBS with hostile direct Host,
untrusted and trusted forwarded headers, missing/invalid configuration,
configured URL success, rendered setup-script delivery, and zero token/state
mutation on failure. The diagnostics-specific
TestContract_HostedDiagnosticsDockerPrepareTokenValidatesOriginBeforeMutation
adds missing, auto-detected-only, invalid, invalid-high-precedence,
PublicURL, and AgentConnectURL router proof with exact in-memory and
persisted token-state checks. The self-hosted precedence and validated
live-origin fallback remain covered by that diagnostics proof and
TestContract_RequestOriginCannotRetargetTokenBearingCommands.
The adjacent hosted returning-user and Stripe checkout magic-link callers now
resolve the same canonical external URL before persisting their one-time token.
Unavailable configuration leaves both in-memory and SQLite magic-link state
empty; the public caller retains its enumeration-resistant generic response
and the Stripe caller retains checkout/event idempotency. These credentials
grant browser-session entry only after the existing stable organization-
principal verification and do not become agent enrollment, setup, install,
profile, update, or fleet authority. Proof lives in
TestContract_HostedMagicLinkRequestValidatesOriginBeforeMutation and
TestStripeWebhook_CheckoutMagicLinkValidatesOriginBeforeMutation.
Runtime branding stays outside agent lifecycle authority
The authenticated GET /api/runtime/branding presentation endpoint is an
application-shell read, not an agent registration, configuration, update, or
command route. It may read the active white_label entitlement and the
tenant-local reportBranding settings record, but it must not enumerate
agents, resolve install tokens, mutate agent state, or reuse agent lifecycle
authority. Agent setup and reporting remain independent of whether an
operator has configured a custom application name or logo.
ZFS dataset inventory is read-only host evidence
The Unified Agent host report may attach a bounded zfsDatasets collection to
each detected ZFS pool disk. The collector executes only the fixed read-only
zfs list projection for pools it already discovered, admits only filesystem
and volume rows beneath those pools, and bounds command output, row counts, and
text fields. Dataset collection does not broaden registration, token,
configuration, update, or command authority. Servers validate and copy the
optional evidence during normal authenticated host-report ingest; agents that
do not send it remain wire-compatible.
Windows CPU and motherboard temperatures require a local qualified provider
Windows Unified Agents now supplement native Storage reliability counters and
direct NVIDIA telemetry with CPU and motherboard Celsius readings from a
running local LibreHardwareMonitor instance. The integration is optional and
read-only: it polls only the fixed loopback data.json endpoint, admits only
bounded and classified temperature nodes, and leaves the host report intact
when the helper is absent or incompatible. It does not treat Windows ACPI
thermal zones as hardware sensor evidence, accept remote provider locations,
or grant Pulse hardware-control authority.
Agent profile adoption is reported as a count only
The usage telemetry snapshot reads LoadAgentProfiles and reports
agent_profiles, the number of configured profiles, in the outbound payload.
Profile names, targets, assignment mappings, and profile contents are not sent.
The read happens in applyLicensedFeatureConfigSnapshot
(pkg/server/telemetry_licensed_features.go) and is pinned by
TestApplyLicensedFeatureConfigSnapshot_CountsScheduledReportingAndProfiles,
which also asserts an unconfigured install reports zero rather than omitting
the field.
Licence-gated audit read surfaces record a content-free usage marker
The eight audit read and export routes registered in
internal/api/router_routes_licensing.go wrap their handlers with
Router.withAuditReadActivity inside RequireLicenseFeature, so only requests
that cleared the licence gate record a marker. The marker is a timestamp plus a
coarse activity class (list, export, verify, summary); the handler runs
unchanged whether or not recording succeeds, and a router without persistence
serves the request rather than failing it
(TestWithAuditReadActivity_NilPersistenceIsSafe).
Monitor shutdown drains queued guest metadata writes
Monitor.Stop waits for in-flight GuestMetadataStore writes before closing
the metrics store, so a tenant monitor that has been stopped is guaranteed not
to write into its data directory afterwards. Tenant offboarding and any caller
that removes a tenant directory can rely on Stop having quiesced disk writes,
rather than racing a detached goroutine.
Per-tenant resource stores are released on offboarding and shutdown
ResourceHandlers.getStore opens a SQLite handle per org and caches it for the
process lifetime. CloseTenantStore releases and evicts one org's handle and is
called from Router.CleanupTenant alongside the other per-tenant teardown;
CloseStores, exposed as Router.ShutdownResourceStores, releases all of them.
Without this an offboarded tenant kept its file descriptors and its
unified_resources.db-wal/-shm files alive and its directory could not be
fully removed. Closed stores are evicted from the cache so a later request opens
a fresh handle rather than using a closed one
(TestResourceHandlers_CloseTenantStoreReleasesTheHandle), and both entry points
are idempotent and nil-safe
(TestResourceHandlers_CloseIsIdempotentAndNilSafe).
Server upgrades nudge agent self-update within one report cycle
The unified-agent report ack now carries the server's version
(serverVersion), and the host module surfaces it through
hostagent.Config.OnServerVersion, which cmd/pulse-agent wires to
agentupdate.Updater.NudgeVersion via wireUpdaterHooks
(TestWireUpdaterHooksNudgesUpdaterOnNewerAckVersion). A nudge wakes the
update loop immediately, including during the initial-check delay, so after a
server upgrade agents converge within one report interval instead of waiting
out the hourly check (TestRunLoopRunsCheckOnNudge). Nudges queue only for
versions strictly newer than the running agent, at most once per distinct
server version, and never on disabled or development-mode updaters
(TestNudgeVersionQueuesOnlyForNewerVersions,
TestNudgeVersionComparesPrereleaseIdentifiers,
TestNudgeVersionNudgesEachDistinctVersionOnce,
TestNudgeVersionRespectsDisabledAndDevelopmentGates); the hourly loop stays
the retry path when a nudged check fails. Only the authoritative destination's
ack invokes the hook — observer acks never steer the updater
(TestAgentSendReport_SurfacesAckServerVersion,
TestAgentSendReport_SkipsCallbackWithoutAckServerVersion,
TestAgentSendReport_ObserverAckNeverInvokesCallback).
agentupdate.Config.InitialCheckDelay overrides the five-second initial check
delay; zero keeps the default.
Purchase-start attribution does not touch agent routes
The source attribution parameter added to
internal/api/licensing_handlers.go (an extension point of this subsystem's
internal/api/ surface) is confined to the self-hosted purchase handoff:
it is read from the purchase-start query, validated, and passed to the
license-server portal handoff. No agent enrollment, report, ack, or update
route reads or emits it, and the agent-facing payload shapes are unchanged.
Generic host onboarding preserves API-first Proxmox setup
frontend-modern/src/components/Settings/InfrastructureInstallerSection.tsx
now makes the least-privilege Proxmox choice visible before an operator copies
a host-agent command. The generic host installer remains canonical for
host-local, Docker, and Kubernetes telemetry, but it routes PVE and PBS users
to the existing platform-connection flows first and states that normal
Proxmox inventory and metrics do not require a root agent. The root agent stays
an explicit augmentation for SMART, temperatures, and host-local storage
detail. ConnectionEditor also reconciles its selected credential slot when a
direct onboarding route changes in place, so following the PVE or PBS link from
the already-mounted agent dialog replaces the agent installer with the matching
API credential form instead of changing only the dialog title. This adds no
enrollment, token, command, report, discovery, or update authority and does not
replace the existing PVE or PBS setup handlers. Source guards in
InfrastructureOperationsModel.test.tsx and ConnectionEditor.test.tsx pin
the API-first copy, both canonical onboarding routes, and the reactive
credential-slot handoff.
Platform connection panel state waits on the infrastructure capability
Settings.tsx constructs useInfrastructureSettingsState for every settings
tab, not just Infrastructure, so its bootstrap and pollers ran on whatever
settings page a session happened to open. All of the endpoints involved are
RequireAdmin, which made this the largest repeating source of
Non-admin user attempted to access admin endpoint warn lines on an idle
instance.
useInfrastructureSettingsState, useInfrastructureDiscoveryRuntimeState,
useTrueNASSettingsPanelState and useVMwareSettingsPanelState now take the
session's infrastructureRead capability (see the api-contracts entry for the
served field) and hold their reads until it is granted:
- the discovery hook skips
loadDiscoveredNodesoutright and only arms its 30s/api/discoverinterval once the capability reads true — the accessor is read inside the effect so the interval still arms for an admin as soon as the status resolves; - the infrastructure bootstrap still awaits
loadSecurityStatus(which is what resolves the capability, and is readable by any session) but returns before the node, discovery and system-settings loads when it is withheld; - the TrueNAS and VMware panel loads moved from
onMountto a once-only effect. AnonMountcheck would sample the capability before the status request resolves and withhold the load from admins too.
canLoad is optional on the two panel hooks so tests and stories that construct
them directly keep the eager load.
Pinned by the capability cases in useTrueNASSettingsPanelState.test.tsx and
useVMwareSettingsPanelState.test.tsx (skipped when withheld, loads once when
the accessor flips) and by the discovery source pin in
InfrastructureOperationsModel.test.tsx.
internal/api/ is a canonical reference in this contract's Extension Points,
so this records the additive sibling added alongside that capability:
settingsCapabilities.systemSettingsRead, describing the same
canAccessAdminSurface(config.ScopeSettingsRead) gate for the admin-only
System tabs. It changes no lifecycle route, payload, or agent-facing shape, and
the infrastructure hooks above continue to read infrastructureRead rather
than the new field — the two are siblings precisely so neither page's gate can
be moved by a change aimed at the other. The api-contracts entry holds the
authoritative description and its proof.
The same security-status payload now includes the additive
availabilityRead, pulseIntelligenceRead, diagnosticsRead,
systemLogsRead, and reportingRead fields. This is recorded because
internal/api/ is an Extension Point here; none of the fields changes an agent
route, enrollment contract, report payload, capability manifest, or lifecycle
decision. Agent-facing code must not infer any of these browser Settings grants
from agent credentials. The api-contracts contract owns their predicates and
proof.
API-layer refusal logging moved to debug
internal/api/ is a canonical reference in this contract's Extension Points, so
this records the change made in internal/api/auth.go,
internal/api/middleware.go and internal/api/auth_denial_signal.go: routine
authorization refusals log at debug, and only an abnormal rate from one caller
warns. Agent registration, capability, and lifecycle routes keep their existing
status codes and payloads unchanged — an agent presenting insufficient
credentials is refused exactly as before, it simply no longer emits a warn line
per attempt. The security-privacy and api-contracts entries hold the
authoritative description and proof.
Authenticated runtime display projection
internal/api/runtime_display.go adds GET /api/runtime/display under
RequireAuth plus monitoring:read. This is an API-contract change only: it
does not alter agent enrollment, reports, commands, identity, or lifecycle
state. Monitoring-capable agents may read the same presentation-only defaults
as other authenticated clients, but the response contains no connection,
credential, or agent identity data. api-contracts owns the authoritative
payload and authorization proof.
Viewer inventory health excludes agent lifecycle identity
The authenticated GET /api/runtime/inventory-sources monitoring projection
derives cached source health through the shared connection aggregator but is not
an agent enumeration, enrollment, update, or command route. Its base wire type
carries only source type, operator-facing name, normalized lifecycle
state, and workload-only surfaces. An optional VMware completeness summary
may carry only degraded state, aggregate count, and bounded stage/category/
occurrence tuples for otherwise-successful inventory reads.
It has no stable connection or agent ID, token binding, address, report IP, hostname identity object, operating-system, kernel or architecture fact, agent version, update or module state, command-session state, commands-enabled value, fleet policy, credential-health object, raw enrichment error, or affected entity identity. The handler deliberately skips the command-session enrichment used by the administrative ledger. Agent reporting, registration, update, and command authority remain unchanged, and the fully-populated projection test fails if any of those lifecycle facts cross the viewer boundary.
RBAC user deprovisioning does not alter agent identity
internal/api/ is a canonical reference in this contract's Extension Points,
so this records the additive RBAC user-removal route and SSO presentation
metadata. Human browser principals remain separate from enrolled agent IDs,
agent tokens, command sessions, and report identity. Deprovisioning a human
principal revokes that principal's browser sessions only and does not delete,
reassign, or synthesize agent lifecycle state. The api-contracts and
security-privacy contracts own the authoritative behavior and proof.
Resource monitoring policy does not mutate agent lifecycle
The shared internal/api/ router now reconciles alert attention after a
resource operator-state mutation, but it does not unregister, delete, retoken,
or reassign an agent. Monitoring and lifecycle policy is keyed through the
tenant's canonical resource registry and is separate from enrolled agent
identity. Agent context may report monitoringMode and lifecycleState so an
operator or external agent can explain current attention behavior, without
changing registration or command authority. Retiring an agent-backed resource
blocks automated remediation through the shared action policy while preserving
its enrollment and history.
Configuration transfer authority does not derive from agent membership
The shared internal/api/ configuration export/import routes use browser,
proxy, Basic, or API-token administration authority for the resolved
organization before entering persistence. Agent registration, report, setup,
and command credentials do not become configuration-transfer authority merely
because they authenticate another lifecycle route. An API token must retain
organization binding and carry settings:read for export or settings:write
for import; tenant browser membership without management authority is denied.
This boundary neither enrolls nor mutates agent identity, tokens, command
sessions, fleet policy, or update state.
Patrol follow-up manifests do not confer agent authority
internal/api/chat_service_adapter.go now forwards a structured tool-name
allowlist and successful finding-report cap for bounded Patrol continuations.
These are AI-runtime metadata that can only reduce the already projected
detection manifest and model-owned Pulse-state writes. They do not
enroll an agent, select an agent identity, enable commands, grant a command
session, alter fleet policy, or bypass agent-side preflight and action
admission. Agent lifecycle and command authority remain unchanged.
Exact scoped Watch and investigation requests may also carry a core-resolved canonical resource type through the shared AI handler boundary. The type only selects a smaller read-only provider-tool projection after the Patrol profile has been applied; it cannot select an agent, add an agent-routed tool, establish a command session, or bypass capability, feasibility, policy, approval, dispatch, receipt, and verification gates. Unknown types retain the existing governed Patrol profile rather than guessing at agent authority.
Notification delivery-log routes do not touch agent lifecycle
The shared internal/api/ surface now includes the notifications-owned
read-only GET /api/notifications/delivery-log route and a deliveryPaused
field on successful test-send responses. Both are notification delivery
evidence only: they read retained notification audit rows and the manager's
enabled gate, and they do not enroll or select an agent, open or alter a
command session, change fleet policy or update state, or add any
agent-routed capability. Agent lifecycle obligations over internal/api/
are unchanged by this surface.
The delivery-log read may span the notifications-owned 30-day dead-letter
window while naming the shorter seven-day completed-attempt retention in its
metadata. That wider evidence window and the Alerts navigation into it do not
inspect removed-host clients, agent enrollment, or agent transport state and
must not be presented as agent/server lifecycle diagnosis.
Least-privilege agent profile
scripts/install.sh --least-privilege is a supported Linux/systemd install
profile that runs the unified agent as the dedicated nologin pulse-agent
system user instead of root. The profile is explicit at every boundary: it is
refused on appliance platforms and non-systemd init systems rather than
silently falling back to root, it is mutually exclusive with
--enable-commands (governed command execution stays a root-profile
capability), it never receives the LXC-attach ambient capability grant, and
--update preserves an existing profile and its grants by reading the
installed unit rather than requiring the flags to be repeated. Optional
--grant-smart and --grant-pct restore exactly the two collectors that
need elevation through visudo-validated, exact-command sudoers rules and
root-owned wrapper helpers the agent reaches only via the 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. An active
grant sets NoNewPrivileges=false because NNP blocks sudo outright (proven
on a live systemd host); a grantless profile keeps NoNewPrivileges=true.
The agent
authors a privilege block in its report (pkg/agents/host/report.go
PrivilegeStatus: effective root, service user, active helpers) so the
server can present the profile descriptively. Uninstall removes the sudoers
file and helpers. scripts/installtests/install_sh_test.go
(TestInstallSHLeastPrivilegeProfile) pins the profile's invariants.
Typed local privilege helper boundary
cmd/pulse-agent-helper/main.go and internal/agenthelper/ own the Linux
collector's replacement for command-shaped sudo wrappers. The helper is a
standalone root process with no Pulse URL, API credential, outbound transport,
or generic command/path operation. It accepts exactly one protocol-v1 request
per local Unix-socket connection, uses a four-byte big-endian JSON frame with a
64 KiB request ceiling and 10 MiB response ceiling, rejects unknown fields,
trailing JSON, unsupported versions, and expired deadlines, and returns typed
errors correlated by request ID.
Admission is local and fail closed: Linux peer credentials must resolve to the
configured collector UID before dispatch. The registry exposes only named,
versioned operations whose providers own all executable paths and arguments.
The first collection operations are smart.snapshot.v1,
proxmox.lxc_filesystems.v1, and the bounded fixed-endpoint
container.inventory.v1; callers cannot supply a binary path, arbitrary
filesystem path, daemon endpoint, environment, or command arguments. The
collector uses this operation only as a summary-only reporting fallback when
direct runtime admission fails; it does not expose update or lifecycle methods,
and reports preserve that reduction as collectionMode: typed-helper-summary.
Under the helper profile, direct runtime candidates are admitted by endpoint
type, collector UID ownership, rootless runtime path, and an exact-one live
candidate rule before the first daemon API request. Rejected rootful, remote,
cross-user, missing, conflicting-pin, and ambiguous Docker/Podman candidates
are never probed. A candidate that passes the filesystem boundary must also
attest rootless daemon mode. The same admission is rebuilt during daemon
reconnect; the collector can promote from helper summary to direct rootless
monitoring or fall back after repeated direct loss, and the safe collector never
exports container action authority in either mode.
The agent_update.activate.v1 and agent_update.rollback.v1 families accept only
fixed-root, regular, owned, digest-bound update artifacts and produce durable
activation identity around an atomic swap. Their collector staging,
restart/health, and live rollback integration remains qualification work, so
the safe collector must not claim automatic update parity yet. A health/capabilities
operation reports protocol and operation availability without widening the
allow-list. Provider execution inherits a bounded request context. Audit hooks
receive operation, request ID, peer identity, duration, outcome, and response
size metadata only, never collected payloads or secrets.
The packaged service listens on /run/pulse-agent/helper.sock with
root:pulse-agent ownership and mode 0660. Its systemd sandbox is root-owned,
networkless (PrivateNetwork=true, RestrictAddressFamilies=AF_UNIX), and
retains NoNewPrivileges=true, ProtectSystem=strict, and ProtectHome=true.
It must not use PrivateDevices=true, because SMART collection requires access
to host block devices. Helper absence, incompatibility, or an unavailable
operation degrades only the affected telemetry and must never reactivate a sudo
wrapper, Docker-group membership, ambient capability, or root collector.
The collector projects post-start operation failure through the canonical
typed-privilege-helper module status. Agent Doctor maps that module to the
stable agent_privilege_helper_degraded reason and explains that affected
telemetry was omitted without widening collector privilege. The status is an
observability result only; it never authorizes a fallback or helper repair.
The rate-limited public /download/pulse-agent-helper?arch=linux-* endpoint is
the only server delivery path: it rejects non-Linux targets, serves the exact
separate helper artifact with checksum plus detached signatures, and requires a
published signed release asset when no local binary exists.
Safe collector profile migration is an explicit transaction
scripts/install.sh --safe-profile-inspect is a standalone no-write action.
It reports platform support, detected current profile, unit user and groups,
ambient capabilities, collector-binary owner/mode, host/Docker/Kubernetes/
Proxmox provider flags, typed-helper and collector-command state, independent
runner presence, and the calculated typed-helper target plus Docker/action
degradation. Those host-local facts are broader than the remote Agent Doctor
projection and must not be guessed from it.
--safe-profile-apply is supported only for an existing collector on reviewed
standard Linux systemd hosts. It snapshots exact collector and helper binaries,
units, legacy sudoers/wrappers, service enable/active state, Docker-group
membership, credential-directory metadata, monitoring token files, agent ID,
and connection state before mutation. It then installs root-owned collector
and helper artifacts, lowers the collector to monitoring-only, removes legacy
sudo, Docker-group, and ambient authority, and commits an atomic current/prior
profile record only after collector health, helper-socket health, and declared
server registration succeed. Any pre-commit failure restores the snapshot;
--safe-profile-rollback restores the retained committed snapshot explicitly.
Rollback may restore only privilege recorded in that explicit prior profile and
must never invent sudo, Docker-group, ambient, root, or command authority. The
action runner is independently installed and is never snapshotted, stopped,
rewritten, or restored by this transaction. Ordinary --update preserves the
installed profile and cannot trigger migration.
This remains candidate-lane evidence, not a product-wide default flip. The secure collector profile stays explicit until representative real Linux hosts qualify migration and failure rollback, typed-helper update activation, provider parity and documented degradation, and separately credentialed live action sessions. Unsupported platforms continue to fail closed on migration.
Rootless container qualification is a separate immutable packet
scripts/run-secure-runtime-rootless-qualification.sh is the opt-in entrypoint
for collector-owned rootless Docker and Podman proof. It does not extend or
reinterpret the schema-v7 systemd receipt. The wrapper confines destructive
runtime setup to disposable nested Ubuntu/systemd hosts with no mounted host
daemon socket. Qualification artifacts stay out of image layers: the image
must pre-create their root-owned, mode-0700 destination before the wrapper
injects the exact packet into a stopped disposable container. The wrapper
uses the supported slirp4netns rootless Docker driver only inside that outer
--network none boundary; selecting the unsupported host RootlessKit driver
or granting an outer default route is invalid qualification. The outer host
uses a private cgroup namespace with no host cgroup bind mount. Each dedicated
runtime identity must have an active delegated systemd user manager before its
daemon starts, and cleanup must stop that manager and remove its linger state.
Before each disposable host's first systemd boot, the wrapper installs a fresh
valid machine ID into that stopped container. The combined receipt validator
rejects a Docker and Podman pair that reports the same machine identity; a
shared image fallback identity cannot stand in for two independent hosts.
The in-container control-plane fixture must implement the same authenticated
collector-uninstall response contract as the production lifecycle client. A
teardown succeeds only after the fixture records the exact registered binding
as removed, rejects that bearer thereafter, and returns the matching agent ID;
the following legacy-migration phase uses a distinct replacement enrollment
credential rather than resurrecting the removed one. Final cleanup must commit
and verify the replacement binding's removal too, then remove the fixed
root-owned helper and safe-profile transaction state boundaries through exact
path-authorized deletion.
The wrapper exercises each runtime in an isolated state root and emits the
standalone secure-runtime-rootless-v1 receipt only after exact socket
ownership, daemon rootless attestation, installer pinning, direct telemetry,
same-family typed-helper fallback, recovery without collector restart,
ambiguity refusal, authority isolation, and cleanup are recorded.
Receipt output uses a dedicated root-owned, mode-0700 /opt/pulse/result
boundary instead of volatile /run state, so systemd user-manager teardown
cannot erase a passing result before the wrapper retains it.
Recovery qualification must wait for the complete stable telemetry digest,
including stats and secondary inventory, rather than accepting an earlier
semantic-only report and racing the evidence assertion.
Cleanup removes the exact fixture containers through both still-live runtime
APIs before stopping their services or deleting storage, so Podman namespace
and shared-memory mounts cannot outlive the state they protect. After both
runtime services stop, Podman cleanup resets each rootless and rootful storage
identity through the local runtime CLI before the delegated user manager is
stopped. Cleanup then waits until the kernel mount table contains no mount at
or below any dedicated runtime state root before deleting that state; an empty
runtime inventory or a successful systemd stop alone is not mount-release
evidence.
The wrapper must compile every Go artifact with mandatory VCS stamping so a
missing revision or unavailable clean-worktree proof fails before live evidence
can be emitted; the receipt and validator retain the exact artifact hashes and
source commit.
Any failed runtime test must retain the disposable host's systemd journal and
container log before strict nonce-bound cleanup; a missing receipt never
silently substitutes for causal failure evidence.
scripts/release_control/secure_runtime_rootless_attestation_v1.py validates
the secret-free receipt against the governed rootless source manifest and the
exact qualification artifact. It treats rootless and rootful helper
inventories as separate daemon identities, requires direct recovery to restore
the original rootless identity and rich telemetry, and understands that a
collector restart creates a new report stream while daemon restart preserves
the durable daemon identity. Dual-socket ambiguity evidence validates Docker
and Podman against their respective recorded socket ownership and modes; it
must not project the selected runtime's permissions onto the other socket.
Its classification is local, opt-in,
artifact-bound self-attestation only. The exact packet at source commit
60041ad9e60c282c892f944e04f777b874991a5d passed all eleven canonical
scenarios for Docker and all eleven for Podman on distinct disposable hosts.
Its secret-free receipt SHA-256 is
7a116d63ab0cd1560482165055f0a8c9158ce0a8333a27bd9e8256582a52dfb5 and
its attestation SHA-256 is
566279ecd7d7dfa89c92f24243e9fcd5ae3e295d4284bd559d4be42f1e83b3b5.
The validator matched 418 governed source hashes, exact artifacts, the two
runtime-specific socket profiles, distinct host and daemon identities, and all
twenty-two scenario records. The sanitized evidence record is
docs/release-control/v6/internal/records/secure-runtime-rootless-v1-local-qualification-2026-09-01.md.
This qualifies only the exercised local rootless monitoring, migration,
fallback, recovery, ambiguity, parity, authority, and cleanup paths. It is not
published-release provenance, default-profile authorization, rootless
action/update qualification, or independent security review and cannot change
the product default.
Rootful container qualification has its own typed-helper packet
scripts/run-secure-runtime-rootful-qualification.sh is the destructive,
explicit entrypoint for real rootful Docker and Podman proof on two distinct
disposable Ubuntu/systemd hosts. It never mounts a host daemon socket and runs
the outer containers without a default route. The host image masks Docker and
Podman's distro-managed service, socket, auto-update, transient-cleanup, and
restart units plus the distro containerd service before first boot; the packet
starts only its explicit runtime-specific daemon units, requires systemd's
exact running state with no failed units before and after each runtime packet,
and proves the explicit Docker daemon's child containerd exits with that
daemon. The packet binds exact clean Go
artifacts, every compiled installtests package input, the canonical remote-main
commit, the immutable Ubuntu base-image digest, and governed source hashes,
then records fresh install, legacy migration with authority reduction,
collector and helper restart continuity, helper loss without an authoritative
empty replacement, exact recovery, bounded helper-operation failure through a
root-owned collector-executable copy of the qualification binary, ordinary
collector-update preservation, authority isolation, and cleanup for both
runtimes. Rootful telemetry is
intentionally summary-only: the collector remains unable to open the
root-owned daemon socket and gains neither container actions nor updates.
scripts/release_control/secure_runtime_rootful_attestation_v1.py accepts only
the exact Docker-then-Podman ten-scenario contract, distinct host and daemon
identities, canonical rootful socket paths, stable semantic inventory, causal
report streams, bounded failure and recovery, and immutable artifact/source
bindings. Adding this packet does not itself qualify the runtime. Only a
retained passing receipt and attestation may support a local opt-in claim, and
neither can establish published-release provenance, default-profile
authorization, appliance support, or independent security review.
Command and durable typed dispatch are context-honest and canceled when abandoned
The agent command transport now refuses to dispatch work its caller has already stopped waiting for, and propagates abandonment to the agent (minipc probe-storm incident, 2026-08-20: probes dispatched under an expired poll context were re-issued every cycle while every previous copy kept running on the Proxmox host). Three coupled guarantees:
agentexec.Server.ExecuteCommand,ReadFile, and every durable typed mutation dispatcher fail with anot dispatchederror when the caller's context is already expired — nothing crosses the WebSocket, so a caller polling on a dead deadline cannot leave the agent executing commands nobody awaits.- When the server stops waiting for a dispatched request (its own
operation timeout or caller-context cancellation), it sends exactly one
best-effort server→agent
cancel_commandmessage (MsgTypeCancelCmd, payloadCancelCommandPayload{request_id}). Agents that predate the message ignore the unknown type and fall back to their own per-command timeout; the protocol change is additive. Before launching a handler, the runner records the request in a bounded, connection-generation-scoped state table. Cancellation or connection teardown before handler registration leaves a tombstone that registration consumes atomically, so provider handoff cannot start after abandonment. A replay of a request ID that arrives while the previous handler still owns its slot waits for that slot to be released and then runs, so it answers from the durable receipt; it is never dropped as a duplicate, because the server replays exact request IDs to recover receipts and a dropped replay would leave the server waiting out the full timeout. - The unified agent's command client tracks in-flight
execute_command/read_fileexecutions and durable host update, storage-cleanup, Proxmox guest lifecycle, and container lifecycle/update operations by request ID and, oncancel_command, cancels that execution's context. Command execution runs each command in its own process group (Setpgid; SIGKILL of the group viacmd.Cancel,taskkill /Ton Windows), boundsWaitwith a 5sWaitDelayso orphan-held pipes cannot hang it, treatsexec.ErrWaitDelayafter a clean exit as success, and reports cancellation as a distinctcommand canceledfailure. This ports thepulse/v6-releaseprocess-leak fix (45480a5cc) to main, which had never received it, and extends it with server-driven cancellation. A typed mutation that reached provider handoff remains explicitly indeterminate; cancellation never claims rollback or no effect. The runner commits its bounded terminal receipt after cancellation when its durable store remains healthy even when the original waiter has gone, and reconnect/replay returns that exact receipt without a second mutation.
Proofs: internal/agentexec/server_websocket_test.go
(TestExecuteCommand_ExpiredContextNeverDispatches,
TestExecuteCommand_AbandonedCommandSendsCancel),
internal/agentexec/server_websocket_test.go
(TestTypedOperations_AbandonedDispatchSendsExactlyOneCancel,
TestTypedOperation_TimeoutSendsCancelAndExpiredContextNeverDispatches),
internal/hostagent/operation_receipt_websocket_integration_test.go
(TestRealServerActionRunnerCancellationPersistsAndReplaysProxmoxReceiptAfterReconnect),
internal/hostagent/command_client_test.go
(TestCommandClient_handleCancelCommand_CancelsRegisteredRequest,
TestCommandClient_handleCancelCommand_UnknownRequestIsNoOp,
TestCommandClient_CancellationBeforeRegistrationIsConsumedAndConnectionScoped,
TestCommandClient_StaleCleanupCannotEraseReusedRequestCancellation,
TestCommandClient_ReplayedRequestWaitsForInFlightHandlerInsteadOfDropping),
internal/hostagent/proxmox_guest_lifecycle_test.go
(TestProxmoxGuestLifecycleCancellationBeforeHandlerRegistrationSkipsProviderAndPersistsReceipt), and
internal/hostagent/commands_execute_unix_test.go (timeout and cancel
kill the whole process group; WaitDelay unblocks inherited pipes).
Docker report cadence does not imply full storage-scan cadence
The unified agent's Docker module continues to send live host and container telemetry at its configured interval, while the daemon-wide verbose storage inventory is cached for 15 minutes. A storage refresh is single-attempt and retains the last successful aggregate after failure, so a slow appliance daemon cannot be held busy by an immediate retry followed by another scan on the next report tick. Live image-list calls do not request Docker's optional shared-size computation; they keep image identity fresh while projecting size and container-count detail from that cached storage snapshot. This changes neither agent enrollment nor report authority; it bounds the collection work attached to that lifecycle (#1729).
Host removal blocks clear from every store on re-enroll
A host-agent removal block may live in the durable continuity store, the legacy in-memory map, or persisted monitor state written by releases that predate the durable store. The re-enroll transition honors whichever store still holds the block. A state-only block with a durable store attached is cleared rather than read as already-consumed, so a fresh token minted after removal always re-enrolls the host instead of rejecting its reports indefinitely with 400s that no reinstall can escape (#1772).
Remote configuration refreshes are transition-quiet
The Unified Agent polls its authoritative remote configuration every minute, but an unchanged response is reconciliation rather than an operator event. Repeated values for the host report interval, report-IP override, Ceph collection setting, availability assignments, and command-execution state must not reset schedules, restart command clients, or emit info-level journal entries. Actual state transitions and malformed or failed refreshes remain visible at their existing levels. This keeps managed agents quiet during healthy steady-state operation without hiding configuration changes or faults.
Agent token issuance and revocation remain continuous across persistence failure
An operator-created API token carrying agent authority, including the shared
host and Proxmox install tokens issued by internal/api/agenttokens, becomes
live and is returned only after its expanded inventory commits. A failed
creation write restores every prior token and the primary-token projection,
so the runtime does not retain an undisclosed install credential or evict an
older working credential from its sorted inventory. The install-token proof is
TestIssueAndPersistRollsBackCompleteInventoryWhenPersistenceFails in
internal/api/agenttokens/install_test.go.
The shared API-token deletion boundary may mark an agent credential revoked
only after the reduced token inventory is durably committed. If persistence
fails, the complete prior inventory and primary-token projection are restored
and the request fails, so connected agents do not become unauthenticated only
in the live process or regain a supposedly revoked credential after restart.
The lifecycle proofs cover failed creation, delete one exact token from a
three-token inventory, and force failed persistence commits in
internal/api/security_tokens_lifecycle_test.go.
Agent-owned removal follows the same revocation boundary for host agents,
Docker hosts, and Kubernetes clusters. The resource tombstone may still
commit when credential persistence is unavailable, but token removal occurs
under the shared configuration lock and rolls the complete inventory back if
its write fails. The credential therefore remains consistently active rather
than appearing revoked until restart silently restores it. Shared success and
forced-write-failure proofs live in
internal/monitoring/monitor_host_agent_removal_lifecycle_test.go.
External watchdog wiring does not widen agent lifecycle authority
internal/monitoring/monitor.go now starts and stops the external dead-man
worker beside the canonical monitor loop. That adjacency grants the watchdog
no enrollment, report admission, token, profile, update, command, tombstone,
or re-enrollment authority. Its liveness marker observes only whether the
monitor select loop is progressing; agent report success or failure cannot
independently assert that Pulse is healthy. Agent-lifecycle behavior and proof
routes remain unchanged.
Deploy enrollment swaps credentials as one durable transition
A deploy bootstrap token remains the live credential until Pulse can durably
replace it with the long-lived, host-bound runtime token. Enrollment prepares
the runtime credential first, then removes the single-use bootstrap token and
adds the runtime token under one configuration lock and one persistence write.
Concurrent reuse loses the locked removal and returns 409; persistence
failure restores the complete prior token inventory and primary-token
projection, returns 500, discloses no runtime credential, and leaves the
target enrolling so the same bootstrap credential can be retried safely.
Bootstrap minting follows the same commit boundary: a token is returned only
after its updated inventory is durably stored, and a failed write restores the
prior live inventory. TestHandleEnroll_Success,
TestHandleEnroll_RollsBackBootstrapConsumptionWhenPersistenceFails, and
TestMintBootstrapTokenForTarget_RollsBackWhenPersistenceFails in
internal/api/deploy_handlers_test.go pin the durable replacement and rollback
paths.
Alert push routing does not create an agent lifecycle channel
The shared API router may classify an external-probe outage for specialized
mobile copy and may publish an ordinary dispatched incident through Relay, but
that path carries only the canonical alert identity, severity, and private
operator-facing copy. It cannot enroll, assign, link, update, command, revoke,
remove, or re-enroll an agent. External-probe assignment remains an
alerts-owned classification input rather than a second agent identity, and a
mobile notification action resolves to the existing alert detail route rather
than an agent lifecycle operation. The ordinary-alert and probe-specific
payload proofs in internal/api/alerting/external_probe_notifications_test.go
pin this boundary.
Destination configuration failure cannot alter agent authority
Notification destination API writes persist their complete configuration
before publishing it to the live manager and return 500 without runtime
mutation when persistence fails. This ordering may retain or reject email,
webhook, or Apprise delivery state, but it cannot enroll, revoke, command,
relink, or otherwise mutate an agent. The serialized write boundary in
internal/api/alerting/notifications.go therefore prevents an uncommitted
notification change from being mistaken for an agent lifecycle transition;
its failure coverage lives in internal/api/alerting/notifications_test.go.
Mock alert timelines remain outside agent lifecycle authority
The shared internal/api/alerting/alerts.go boundary may resolve fixture-owned
alert incidents and notes from internal/mock/fixture_graph.go while mock mode
is active. Those records are alert-history presentation state only: creating,
listing, or annotating a mock incident cannot enroll, identify, link, command,
update, revoke, remove, or re-enroll an agent. The graph must not synthesize an
agent report or mutate the live agent inventory to make a mock timeline
complete. internal/api/alerting/alerts_test.go and
internal/mock/alert_incidents_test.go pin the transport and fixture sides of
this separation.
First agent reports cannot outrun alert delivery wiring
The monitoring constructor installs external alert callbacks before it returns
the monitor to API routing. An agent report accepted immediately after process
startup can therefore create a canonical alert and reach notification and push
delivery even if the long-running Monitor.Start goroutine has not yet begun.
Start may replace those single callback slots with runtime-specific WebSocket
context and add lifecycle projection replay, but it cannot be the first owner
of outbound alert wiring. This ordering changes no enrollment or token
authority; it only prevents the first accepted agent observation from losing
its alert consequence. TestNewMonitorRoutesStartupCustomSensorWarningBeforeStart
in internal/monitoring/monitor_host_agents_test.go pins the pre-Start
warning path.
Alert correlation consumes links without gaining lifecycle authority
Monitoring may read the canonical reciprocal Host.LinkedNodeID and
Node.LinkedAgentID relationship to attach alerts-owned shared-system context
to a host connectivity signal. Both sides must match exactly and the node must
identify one non-empty PVE instance; incomplete or conflicting evidence yields
no correlation. This adapter cannot create, heal, persist, remove, or otherwise
reinterpret an agent link, and it cannot merge the host detector lifecycle
with the PVE connection or node lifecycle.
The correlation contains no credential, enrollment, command, token, or
authority state. Agent lifecycle remains the sole owner of the relationship;
alerts receives only a bounded presentation identity after that relationship
has already been established. The reciprocal and fail-open cases are pinned in
internal/monitoring/monitor_host_agents_test.go.
Proxmox package evidence does not create agent authority
The PVE API poll may publish a node's bounded package-update evidence through
the shared node model whether or not a Unified Agent is linked to that node.
pendingUpdates, its last successful check time, and the fixed evidence status
and reason describe only the Proxmox package source. They cannot enroll, link,
command, update, revoke, remove, or re-enroll an agent, and a linked agent does
not become an alternative authority for the package count.
The node drawer may use Node.LinkedAgentID to label Pulse coverage, but that
presentation does not combine agent lifecycle with package evidence. The
polling states are pinned independently in
internal/monitoring/node_pending_updates_evidence_test.go.
Native agent lifecycle proof preserves protected checkout semantics
The native Linux and Windows agent lifecycle jobs obtain their source through
the reviewed immutable actions/checkout v7.0.1 pin. That baseline refuses
fork pull-request checkout on privileged events unless a workflow explicitly
opts out; Pulse prohibits that opt-out and the pull_request_target trigger.
A dependency refresh must update the central workflow-trust allowlist and the
native lifecycle workflow proof together, so lifecycle qualification cannot
silently execute under a weaker checkout boundary.
scripts/check_workflow_trust.py, scripts/tests/test_workflow_trust.py, and
scripts/installtests/install_ps1_test.go pin that relationship.
Patrol cost preview and model guidance reads are not agent lifecycle authority
The read-only GET /api/ai/patrol/cost-preview and GET /api/ai/patrol/model-guidance routes under internal/api/ price the Patrol
schedule and mark Patrol model choices from Pulse's price table, the
install's Patrol run history, and the cached readiness pass. They read no
agent identity, enrolment, or fleet state, issue no agent commands, and do
not change the shared agent-install or setup-script boundaries; the
agent-lifecycle contract keeps its authority over every agent surface.
Patrol digest does not touch agent authority
The Patrol digest route in internal/api/router_routes_ai_relay.go and its
handler internal/api/ai_patrol_digest_handler.go are read-only. They issue no
agent commands, create no action plans, and do not alter capability, token, or
binding state. Agent-executed Patrol actions appear in the digest only as
counts projected from existing action audit records.