Commit Graph

138 Commits

Author SHA1 Message Date
rcourtman 3a4a3fd62b Preserve native filesystem evidence and Patrol action history
Expose confined, identity-bound filesystem observations through the shared
resource pipeline so investigations can distinguish an exhausted container
mount from unrelated host capacity. Keep unavailable measurements explicit.

Isolate alert-history reads from durable writes and reuse one chronological
fold across polling. Catch up through bounded durable event IDs so simultaneous
readers do not replay every retained snapshot. Retain expired actions when
investigation outcomes move back to needs attention, and keep attached
Assistant context focused.

Record live storage diagnosis, healthy and dependency controls, approved and
rejected Docker outcomes, source-bound browser proof and exact test limits.
Missing-access continuity, VM dispatch completion and remaining Assistant
orchestration defects stay open in the redesign plan.
2026-09-07 09:45:31 +01:00
rcourtman b0b39f00dc Qualify Docker storage collection against a live fault
Exercise the production collector through healthy, full and recovered
storage states using the existing bounded disposable lab. Preserve exact
mount configuration while keeping collector proof separate from model
diagnosis and installed-agent qualification.
2026-09-06 18:00:24 +01:00
rcourtman 6e18777d30 Preserve tmpfs mount evidence through collection and queries
Docker can report tmpfs mounts only in HostConfig.Tmpfs. Preserve those
entries in shared inventory and retain type, options and canonical write
access in diagnostic queries. Configured size is not measured free space.

Record the failed ordinary storage diagnosis and independently verified
recovery without claiming autonomous or installed-collector qualification.
2026-09-06 14:50:11 +01:00
rcourtman f48c806718 Preserve observed Docker storage evidence
Missing block I/O and container image sizes could become false evidence
for diagnosis. Preserve per-direction counter presence and measured zero
through collection, resource conversion and browser rendering. Separate
new observed history from ambiguous retained disk series without deleting
old rows or changing public metric names.

Keep partial host rates distinct and persist a newly enabled Disk I/O
column across the first preference reload.
2026-09-06 12:45:40 +01:00
pulse-triage[bot] 6d7854724e test(dockeragent): protect CPU baseline routing across modules
Issue #1894 exposed CPU sampling interference between host and Docker reporting loops. Existing tests cover independent collectors and host routing but do not exercise Docker's production dependency entry points.

Interleave the real host, Docker and package-level collectors over synthetic procfs counters. Alternate Docker's include and no-include paths and require each loop to retain a full-interval 5 percent baseline. Production behaviour is unchanged.

Validation: focused test passed; reverting either Docker route to the package-level collector made it fail. After restoration, three focused CPU tests passed with -race -count=3 across dockeragent, hostagent and hostmetrics. git diff --check passed.

Change-source: pulse-maintainer
2026-09-05 06:03:18 +01:00
pulse-triage[bot] 0f972f42f2 Isolate host and Docker CPU sampling baselines (#1894)
Near-synchronous host and Docker reports consumed a shared CPU baseline, measuring collection bursts rather than each module's reporting interval. Retain a collector per host collector and a separate Docker module collector while preserving the package-level convenience API and disk filters.

Add an interleaved-counter regression covering both collection entry points. It fails when routed through the shared baseline and passes with isolated state. All hostmetrics, hostagent and dockeragent tests pass, as do the focused CPU regression tests under the race detector.

Change-source: pulse-maintainer
2026-09-04 22:29:32 +01:00
rcourtman d99e3cc715 Log rootless admission fallback reasons 2026-09-01 18:39:23 +01:00
pulse-triage[bot] 696cb39c08 Bound container stats response bodies
Change-source: pulse-maintainer
2026-09-01 08:47:51 +01:00
rcourtman 2aece859a5 Harden safe rootless runtime recovery 2026-08-31 22:17:07 +01:00
rcourtman cf021bc9ce Surface typed helper container degradation 2026-08-31 15:53:50 +01:00
pulse-triage[bot] e094a55b45 Integrate typed helper container summaries 2026-08-30 21:29:11 +01:00
Pulse Test d607d5cf46 Separate agent remediation runtime 2026-08-29 23:48:28 +01:00
pulse-triage[bot] 7af19a973e Bound stopped-container Docker inspection cadence
Cache unchanged inactive container detail between live telemetry cycles so NAS hosts with large historical inventories do not re-inspect every stopped container every 30 seconds. Keep running state live, fail open to fresh inspection when lifecycle evidence is incomplete, and clear daemon-scoped caches on reconnect.

Refs #1729.

Contract-Neutral: Bounds internal daemon call cadence without changing report or operator contracts
2026-08-28 19:05:40 +01:00
rcourtman a9076de15a fix(patrol): require Docker health recovery 2026-08-27 20:43:54 +01:00
pulse-triage[bot] 735acbb05a fix(dockeragent): bound image storage computation
Keep live image identity fresh without recomputing shared layer sizes on every report. Reuse the throttled storage snapshot and qualify the reported Synology inventory shape.

Change-source: pulse-maintainer
2026-08-27 16:33:14 +01:00
rcourtman f675b5a4d8 Fix false self-container update reports 2026-08-25 18:37:57 +01:00
rcourtman ddf081a55a Bound Docker storage inventory collection 2026-08-23 09:34:40 +01:00
rcourtman ebb08e0de2 Make native-agent test fixtures path-portable on Windows
The dockeragent credential-store tests keyed their fake filesystem and
path-bearing env seams on POSIX literals, while dockerConfigCredentials
builds candidate config paths with filepath.Join. On Windows every lookup
therefore missed the fixture and the nine credential-expecting
TestDockerConfigCredentials_* cases failed with a nil credential; the
hostagent PULSE_PCT_PATH override test likewise asserted a POSIX path that
is not absolute on Windows. Map fixture keys and env values through
filepath.FromSlash and anchor the override on t.TempDir().

Taken from PR #1755 (ff96aed8c) so the correction lands on main instead of
a parked agent branch. The unified-agent-native Windows x64 job has been
red on every push run since these tests landed on 2026-08-13; the
intervening green runs were pull_request events exercising that PR's own
corrected fixtures.

Contract-Neutral: test fixtures only, no production runtime change.
2026-08-21 22:36:52 +01:00
rcourtman d08ca46194 Expose Docker health-check dependencies to Patrol 2026-08-16 16:58:44 +01:00
rcourtman 48fac73ff0 Rework Patrol around outcome-driven autonomous operations 2026-08-14 12:52:51 +01:00
rcourtman e31fc37983 Gate Patrol actions on agent preflight 2026-08-14 01:12:49 +01:00
rcourtman 3355f7a671 Read host Docker credentials for private registry update checks
Container update detection only ever negotiated anonymous pull tokens, so
containers from registries that reject anonymous digest HEADs pinned a
permanent "authentication required" badge (#1706). The agent already runs
on the Docker host, so the checker now resolves the same credential store
docker pull uses - config.json auths entries, credsStore/credHelpers
credential helpers (docker-credential-<name> get), and Podman's auth.json -
and presents the stored login: Basic auth on Bearer token negotiation and
on the hardcoded Docker Hub / ghcr.io token endpoints, direct answers to
Basic challenges, and the refresh-token grant for identity-token logins
such as Azure ACR.

Credentials never leave the host: they are only presented to the registry
or its token endpoint, helper output stays out of reported check errors,
and lookups are cached in memory for five minutes. Helper names are
validated before exec, and a stale login falls back to the anonymous path
so checks that used to work keep working. Set
PULSE_DISABLE_REGISTRY_CREDENTIALS=true (--disable-registry-credentials)
to keep detection anonymous-only. The agent-lifecycle and security-privacy
subsystem contracts pin the host-local credential boundary.
2026-08-13 10:06:26 +01:00
courtmanr@gmail.com 1549216b6e Handle registry responses without digest headers 2026-08-11 16:39:36 +01:00
courtmanr@gmail.com 791a2f86bf Route Pulse images through product update checks 2026-08-11 16:39:12 +01:00
courtmanr@gmail.com 4dac4dd163 Allow agents to include filtered disk mounts 2026-08-11 16:37:37 +01:00
Richard Courtman e84042a4d9 Fix multi-architecture update digest reporting
Addresses #1689.
2026-08-06 23:31:34 +01:00
rcourtman 022316fca6 Name both digests when a container update preflight refuses
The digest-drift refusal only said the digest no longer matched, which
left no way to tell a genuine plan/image drift from a comparison bug
(#1666 shipped refusals on every classic-overlay2 host for exactly that
reason, and the report had to correlate image stores to get close).
Include the planned digest plus the local image id and repo digest in
the refusal so the action record itself carries the evidence.

Contract-Neutral: agent preflight refusal message detail only; no payload or schema change, error stays within the existing 1024-byte bounded field
2026-08-02 12:31:53 +01:00
rcourtman fcb7c068a1 Back off container update checks for an hour after a rate limit
The registry checker cached rate-limit refusals on the 15 minute
transient-error TTL, and the per-container check runs every collection
cycle, so once a strict registry refused a lookup the agent retried
every 15 minutes. Each refused HEAD still counts against the registry's
allowance, which can hold the limit tripped indefinitely. A design
partner saw exactly this on docker.n8n.io, where the rate limited badge
never cleared.

Cache rate-limited lookups for an hour instead so the allowance can
recover; other transient errors keep the short TTL.

PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT: behavioral backoff fix; no payload or contract delta

Contract-Neutral: behavioral backoff fix; no payload or contract delta
2026-08-01 21:33:00 +01:00
rcourtman 5e1a2a70f4 Skip container update checks against the entitled Pro registry
The Docker agent's registry checker HEADs manifests anonymously. The
Pulse Pro image lives on license.pulserelay.pro, whose token endpoint
requires a license credential the agent does not hold, so the check can
never succeed and every Pro Docker deployment pinned a permanent
"authentication required" badge on its own Pulse container. That
container updates through the broker's digest-pinned commands, not the
generic checker.

Report nothing for that registry instead, the same way a disabled
checker does, so no badge renders. Reported by a design partner on
6.2.0-rc.4.

PULSE_ALLOW_CONTRACT_NEUTRAL_COMMIT: behavioral fix suppressing a structurally impossible check; no payload or contract delta

Contract-Neutral: behavioral fix suppressing a structurally impossible check; no payload or contract delta
2026-08-01 21:23:03 +01:00
courtmanr@gmail.com ad054defaf fix outstanding integration and agent regressions 2026-07-29 22:10:04 +01:00
courtmanr@gmail.com e734bd0097 Fix transient rootless Podman socket outranking rootful Docker (#1647)
The installer's discover_rootless_container_runtime only globbed
/run/user/* sockets and never consulted the system Docker daemon, so a
socket-activated rootless Podman API socket (alive only for root's login
session on Debian/OMV) won discovery over a healthy rootful Docker and
pinned PULSE_DOCKER_RUNTIME=podman plus CONTAINER_HOST/PODMAN_HOST/
XDG_RUNTIME_DIR into the agent unit. The env-application block also ran
for explicit --enable-docker installs. Rootless discovery now defers to
system_docker_runtime_is_active (docker info with DOCKER_HOST stripped,
or a live /var/run/docker.sock probe) before touching rootless sockets.

On the agent, detectRuntime short-circuited to podman whenever the
preference said podman, mislabeling connections that actually fell
through to the Docker socket and disabling Swarm collection. The
preference is now an ordering hint: a podman-preferred connection landing
on a docker endpoint reports docker, while unlabeled endpoints with no
runtime signals still honor the pin. When the bound socket disappears
mid-run the agent re-runs runtime discovery after three consecutive
daemon-unavailable collects, swapping the connection behind a
swappableDockerClient so concurrent goroutines keep a stable handle.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 22:49:48 +01:00
courtmanr@gmail.com ad97ae890d Backfill agent-lifecycle governance for the marshal-hook race fix
68e557e9f moved the Docker agent's JSON-marshal hook from a package global
to a per-Agent field, which touches internal/dockeragent/agent.go and
internal/dockeragent/container_update.go. It meant to take the
Contract-Neutral bypass, but a blank line separated the trailer from
Co-Authored-By, so git's trailer parser dropped it, CI ran the completion
guard without a reason, and Canonical Governance went red on three counts:
missing contract docs/release-control/v6/internal/subsystems/agent-lifecycle.md,
missing verification artifact for "agent runtime transport trust proof", and
missing verification artifact for "Docker container recreate, rollback,
durable result, and live network-mode proof". The sibling timer-hook commit
8e5ef365d has the same defect, so this backfill covers both seams.

Document the seams in the agent-lifecycle contract's Current State: both
newTimerFn and jsonMarshalFn are unexported Agent fields reached through the
newTimer and jsonMarshal methods, nil falls back to time.NewTimer and
json.Marshal, no package-level hook global remains in internal/dockeragent,
and injection happens at construction only so the fields need no mutex. The
seams stay internal — no enrollment, transport trust, command admission,
acknowledgement, or recreate/rollback semantics move, and they must not be
promoted into NewAgent options or any server-facing surface.

Pin that shape in the two registered verification artifacts.
agent_internal_test.go asserts the field names, types, and unexported-ness,
parses every non-test source in the package to fail if either hook returns as
a package-level var, proves the nil defaults run the standard library, drives
two Agents with different hooks concurrently so the isolation is checked under
-race, and proves sendCommandAck marshals through the receiver's own seam.
container_update_test.go proves decodeUpdateContainerPayload routes through
a.jsonMarshal, that an injected failure never reaches a sibling Agent, and
pins the method form at compile time.

Guard dry-run over the staged set passes with no Contract-Neutral bypass, and
replaying 68e557e9f's file list plus these three files is green, so the
original commit would have passed had it carried them.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 22:33:55 +01:00
courtmanr@gmail.com 68e557e9f0 Fix Docker agent test JSON-marshal race by making the hook per-Agent
Same class of race fixed for newTimerFn in 8e5ef365d: tests swapped the
package-level jsonMarshalFn hook while async goroutines leaked from
earlier tests (sendCommandAck ack retries via runAsync) could still be
reading it, tripping the race detector. Replace the global with a
per-Agent jsonMarshalFn seam (nil defaults to json.Marshal), make the
decode payload helpers Agent methods so they use it, and inject the
failing marshaller into the tests that previously swapped the global.
Verified with go test ./internal/dockeragent/ -race -count=20.

Contract-Neutral: test seam refactor to fix data race, no public contract delta

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 17:16:06 +01:00
courtmanr@gmail.com 8e5ef365d4 Fix Docker agent test timer race by making the timer hook per-Agent
Tests swapped the package-level newTimerFn hook while async goroutines
leaked from earlier tests (backup-cleanup and stop-command paths) were
still reading it in waitForAsyncDelay, tripping the race detector on CI.
Replace the global with a per-Agent newTimerFn seam (nil defaults to
time.NewTimer), make waitForContextDelay an Agent method, and inject the
immediate timer into the tests that previously swapped the global.
Verified with go test ./internal/dockeragent/ -race -count=20.

Contract-Neutral: test seam refactor to fix data race, no public contract delta

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-28 16:27:30 +01:00
courtmanr@gmail.com f7ff8d7452 Fix Docker agent test timer race 2026-07-26 21:32:35 +01:00
courtmanr@gmail.com e405270e89 Prepare v6.1.2 stable patch release 2026-07-26 20:38:42 +01:00
rcourtman 0c17922055 Fix Docker report size contract drift 2026-07-23 23:57:42 +01:00
rcourtman 50bac2ccf1 Ensure Docker lifecycle lab removes fixtures 2026-07-23 22:06:57 +01:00
rcourtman 8580b48abc Fix Docker container update recreation lifecycle 2026-07-23 21:39:35 +01:00
rcourtman 2b7d202d8d Fix bounded Docker update checks
Keep manual check commands active until registry collection completes. Deduplicate replayed and concurrent commands, bound collection and acknowledgement retries, surface registry result counts, and prove timeout, rate-limit, and replay behavior.
2026-07-23 21:38:20 +01:00
rcourtman 133a47d284 Cover agent policy, alias normalization and agent report helpers
Eight new branch-coverage tests taking thirty-one previously unreached
functions from zero to covered, with no source or existing test touched.

internal/kubernetesagent: twenty-one pure report helpers, including the pointer
converters proved non-aliasing in both directions, the ingress host and address
collectors across their trim, dedupe and insertion-order arms, the endpoint
slice readiness count where a nil Ready field counts as ready, and the target
role predicate.

internal/agentexec: the sudo long-option value gate over the real option list
including the inline equals form, and the approval grant verification error
unwrapped through errors.Is.

internal/alerts: the alert config alias normalization across the nil config
guard, the empty threshold early return, the blank type-key continue arm and
the legacy-delete versus supported-keep split, asserting both maps stay
independent.

internal/alerts/specs: the resource incident rollup evidence validation, each
failure arm asserted on its concrete error and the check order pinned when
several fields are invalid at once.

internal/cloudcp/docker: the not-found predicate through a wrapped error, the
route host label precedence, and the Traefik host rule parser across quoting
styles, combined matchers, multiple host clauses and malformed input.

internal/cloudcp/portal: the anonymous bootstrap builder, asserting no tenant
or user identity field is ever populated on the anonymous result.

internal/config: the legacy OIDC environment provider, including the arm where
an already-configured provider is present and the redirect derivation from a
public URL with a trailing slash.

internal/dockeragent: the update-all payload decode across wrong-typed and
missing fields, and the docker filter conversion.

Contract-Neutral: test-only branch coverage, no contract surface touched
2026-07-23 06:44:33 +01:00
rcourtman 52ed884640 Prepare v6.1.0-rc.4 release 2026-07-19 15:52:42 +01:00
rcourtman 4b066a0f1e Add report-only Unified Agent observer destinations 2026-07-19 15:30:34 +01:00
rcourtman a0f75b1bb2 fix(dockeragent): bound the collect cycle with a watchdog and pin hung-daemon deadline behavior
Follow-up to the 2026-07-17 live docker-update exercise where a DiskUsage
roundtrip against a colima daemon parked 6+ minutes even though
dockerCallWithRetry wraps every call in a 20s context.WithTimeout.

Investigation result: context deadline propagation through moby client
v0.5.0 (request.go, API-version negotiation) and the otelhttp transport
wrapper is intact. Reproducing with a deliberately hung unix-socket
daemon aborts DiskUsage at the deadline in both hang shapes (pre-header
and mid-body), so there is no client-library bug to fix or file
upstream; the production stall's root cause remains environmental
(deadline timer never fired process-side).

Containment and diagnosis:
- buildReport now runs under dockerCollectCycleTimeout (5m) so a wedged
  cycle can never stall the module indefinitely, plus an independent
  watchdog timer that logs an error with a full goroutine dump if the
  cycle outlives even that deadline - capturing exactly the evidence
  that was missing from the original incident.
- hung_daemon_deadline_test.go pins that a context deadline aborts the
  real moby client against a hung unix-socket daemon (pre-header and
  mid-body stalls), guarding future moby/otelhttp upgrades.

The incident note referenced a dockerCollectCycleTimeout watchdog as
already added; it did not exist on any branch - this commit is that
containment, landed for real.

Contract-Neutral: dockeragent collect-cycle watchdog containment: timeout plumbing only, no collection-semantics or contract-surface delta
2026-07-17 23:37:41 +01:00
rcourtman fda64e0318 Negotiate registry pull tokens from WWW-Authenticate challenges
Update checks against registries without a hardcoded token endpoint
(lscr.io and other spec-compliant registries) failed with a blanket
"Check failed" because the manifest HEAD went out anonymously and the
401 was terminal. Parse the Bearer challenge on the 401, fetch a pull
token from the advertised realm, and retry once. Token endpoints that
answer with access_token instead of token are now accepted too.

Fixes #1583.
2026-07-16 19:50:04 +01:00
rcourtman e3382c8bcb Use authoritative Docker OOM evidence 2026-07-15 17:55:26 +01:00
rcourtman 3c778e2b26 Restore one-click Docker container updates through the typed action plane
v6.1.0-rc.1 retired the legacy update endpoints before a replacement
existed, so the UI's Update button failed with an internal-jargon 410
(issue #1564). This lands the replacement end to end: update_container
is a typed agentexec operation with its own strict codec, durable
receipts, and a request digest bound to the image digest the plan
observed; the unified agent bridges execution to the Docker module's
existing pull/backup/recreate/verify/rollback implementation (which now
reports rollback attempt and outcome); and the container action
executor plans, dispatches, and reconciles the operation with declared
backup/rollback compensation truth. Containers advertise an
admin-approval update capability while an image update with a stated
current digest is detected. The legacy endpoints stay retired but
return actionable copy.

Proven live against a Colima daemon: single-container update, the
issue-1564 shared-network-namespace update, and the full UI journey
(Update button, governed review, approve, run) all completed with the
namespace preserved and the backup retained.
2026-07-14 12:19:04 +01:00
rcourtman 10b73e1964 fix(docker-agent): preserve shared network namespaces through container updates
One-click updates recreated containers from the raw inspected config, so
Docker rejected the create for any container using network_mode:
container:<id> (compose service:<x>) or host with "conflicting options:
hostname and the network mode", leaving the workload stopped under its
_pulse_backup_ name until the rollback rename restored it. Docker fills
Config.Hostname with the namespace owner's ID on such containers, so the
verbatim copy always tripped the daemon validation (verified against a
real Docker 29.5.2 daemon: verbatim create 409s, sanitized create
succeeds and the replacement starts in the shared namespace).

Strip the namespace-owned settings before ContainerCreate: hostname and
domainname for container:/host modes, plus exposed/published ports,
links, DNS options, and extra hosts for container: mode, all of which
the daemon derives from the owning namespace. Bridge and user-defined
networks are untouched.

Refs #1564
2026-07-12 19:48:18 +01:00
rcourtman 1894d2e665 Make native agent tests Windows-safe 2026-07-09 23:51:50 +01:00
rcourtman 255c7c23d4 Modernize Unified Agent lifecycle and platform support 2026-07-09 23:20:35 +01:00