Commit Graph

126 Commits

Author SHA1 Message Date
rcourtman 72c409743d Warn when two machines share one Docker agent identity
Cloned VMs that keep the same /etc/machine-id collapse into a single
Docker host in Pulse, with each clone's report silently overwriting the
other (#1584). The server now tracks per-identity hostname and machine
ID observations and flags a conflict when a value flaps back to one
already seen inside a 15-minute window, a signature a one-time rename
never produces. The conflict rides the DockerHost model through unified
resources, and the Docker page shows a warning naming the flapping
hostnames with the machine-id remedy. The warning self-clears once one
clone stops reporting for the window.
2026-07-16 20:55:09 +01:00
rcourtman e3382c8bcb Use authoritative Docker OOM evidence 2026-07-15 17:55:26 +01:00
rcourtman 71a3b6ebcd Restore release-blocking backend contracts 2026-07-13 21:51:33 +01:00
rcourtman 4aac79dc72 Add durable agent operation receipts 2026-07-12 04:16:22 +01:00
rcourtman e77ab9518d Add safe APT workflow foundations 2026-07-12 02:53:01 +01:00
rcourtman 82c7c52727 Expose server-authored action policy provenance 2026-07-12 00:18:43 +01:00
rcourtman b7b897651b Add canonical action result truth 2026-07-11 21:27:00 +01:00
rcourtman cd7886aa44 Enforce server-owned action approval authority 2026-07-11 19:49:17 +01:00
rcourtman 71ea14e4dd Add durable action dispatch continuity 2026-07-11 16:08:44 +01:00
rcourtman c74c88fb86 Fix dispatch-time policy revocation 2026-07-11 14:29:26 +01:00
rcourtman e65bef8e84 Fix atomic action lifecycle replay 2026-07-11 14:02:32 +01:00
rcourtman eb9954618a Add governed storage pressure cleanup 2026-07-11 10:11:35 +01:00
rcourtman 1312da3acb Add governed host update autonomy 2026-07-11 01:25:14 +01:00
rcourtman 94ccc7a0c2 Close the 2b residuals on the typed Patrol proposal seam
Proposals now require their full correlation identity before anything
persists: a Submit without a finding ID or investigation ID is refused,
so a planned action can never lose the deterministic link back to its
Patrol finding. Pinned in the plan-only broker contract test.

The persisted-state transition callback becomes org-scoped
(OnActionTransition func(orgID, record)) and is wired through
ResourceHandlers.SetActionTransitionPublisher into the shared lifecycle
service, so a multi-tenant Patrol reconciler can key per-tenant stores
and can never apply a transition to the wrong tenant. Publication still
strictly follows persistence; code-standards pins guard the org-scoped
signature and wiring.

pkg/aicontracts/action_broker.go gains machine ownership: a shared
ai-runtime/api-contracts registry boundary (owned_files plus a sorted
shared_ownerships entry) with path policies proving through
pkg/aicontracts/contracts_test.go, and Shared Boundaries entries
inserted in canonical sorted order in both contracts.

Slice 2b-1 of the typed-lifecycle ratchet; the Patrol reconciler itself
lands with the orchestrator wiring now that ai_handlers.go is free.
2026-07-10 12:43:17 +01:00
rcourtman f356994869 Extract the transport-independent action lifecycle service
Planning, approval decisions, and execution for typed resource actions
move out of the HTTP handlers in internal/api/actions.go into a new
internal/actionlifecycle.Service owned by api-contracts. The REST
handlers become thin decode/actor/error-mapping adapters over the one
shared service, and ResourceHandlers.ActionLifecycle() exposes the same
service for in-process consumers, so a future Patrol action broker
inherits identical resource lookup, availability checks, plan hashing,
audit persistence, remediation locks, plan-drift revalidation,
execution, and terminal publication instead of loopback HTTP or a
parallel lifecycle.

Behavior is preserved: same status codes, error codes, and audit/
lifecycle persistence ordering, backed by the existing api contract
tests plus new fail-closed proofs for the service itself (unknown
resource/capability, availability refusal, unapproved execution,
remediation lock, plan drift, missing executor, missing store).

Contract text in api-contracts, agent-lifecycle, and storage-recovery
now names the service alongside actions.go and planner.go; the
subsystem registry owns internal/actionlifecycle/ under api-contracts
with a dedicated path policy; the code-standards and contract source
pins follow the moved invariants; and the subsystem_lookup line-number
pin shifts with the api-contracts canonical-files list insertion.

This is the first slice of making the typed action lifecycle the only
autonomous execution route for Patrol, Assistant, and MCP.
2026-07-10 11:06:55 +01:00
rcourtman 0fa841f66f Improve monitor-first attention states 2026-07-09 21:29:45 +01:00
rcourtman 74131e56e3 Fix no-op relationship_change spam from registry rebuilds
Every state update rebuilds the resource registry from scratch, which
reconstructs all relationship edges with fresh ObservedAt/LastSeenAt
stamps and fresh metadata maps. recordRegistryChanges compared the old
and new slices with reflect.DeepEqual, so every relationship-bearing
resource emitted a relationship_change row on every rebuild cycle even
when nothing changed. On the public demo this wrote roughly 450k rows
per day (1.2M of 1.6M rows were literal from==to no-ops), grew
unified_resources.db to 1.6GB in three days, starved the store's single
connection until retention pruning failed with SQLITE_BUSY, and drove
the droplet into the swap-thrash outage on 2026-07-08. Same mechanism
as issue #1496.

Compare relationship sets by edge identity instead: canonical source,
canonical target, type, and active state, order-insensitive. Volatile
provenance fields no longer count as change.

Also cap resource_changes at 200k rows during retention pruning so a
pathological writer can never grow the table unbounded inside the
30-day retention window, and record both invariants in the
unified-resources subsystem contract.
2026-07-08 08:10:15 +01:00
rcourtman b1fdefdd50 Normalize Docker container CPU capacity
Refs #1293
2026-07-03 11:51:08 +01:00
rcourtman 7c75b13d2c Carry host power sensor readings 2026-06-30 10:02:49 +01:00
rcourtman d393ccf310 Add typed NVIDIA GPU stats 2026-06-30 09:43:33 +01:00
rcourtman ccf90bb26d fix: resolve 4 pre-existing test failures blocking clean CI
1. Regenerate pulse-mcp README from canonical manifest (doc drift)
2. Add 31 missing Pulse Intelligence telemetry fields to both PRIVACY.md
   copies to match current Ping struct JSON tags
3. Rebuild portal frontend bundle to update build_manifest.json hash
4. Update action execution contract test to match current code structure
   (handler wrapped with withExternalAgentCapabilityActivity, error codes
   referenced via agentcapabilities constants instead of literal strings)

Full Go test suite now passes clean: 126 packages, 0 failures.
2026-06-26 13:30:00 +01:00
rcourtman 62c2b765d0 feat: attach availability checks as facet on known resource row
Agentless availability checks (ICMP/TCP/HTTP) were always minting
standalone network-endpoint resources, leaving them disconnected from
the known Proxmox/Docker guest they actually monitor. This made
availability evidence invisible on the platform resource row where the
user expects it, per the performance-and-scalability bounded-row
contract.

Backend (unified-resources ingest):
- Add LinkedResourceID field to AvailabilityData and AvailabilityTarget
- resolveAvailabilityLink: explicit link first, then exact-IP unambiguous
  correlation; skip hostname-only (lossy); guard against overwriting a
  different target's facet
- Unlinked/unmatched probes still mint network-endpoint (fallback)

Frontend:
- Relax getAvailabilityProbePresentation for any resource with availability
- Add compact protocol badge to UnifiedResourceHostTableCard name cell
- Add optional 'Link to resource' field to availability target form
- Add linkedResourceId to frontend types

Contracts: api-contracts, unified-resources, monitoring,
performance-and-scalability, storage-recovery.

Governance: coverage_gap + candidate_lane in status.json.
2026-06-26 12:16:11 +01:00
rcourtman 6a162a1736 Add macOS thermal state reporting
Report Darwin pmset pressure separately from Celsius readings and carry it through host sensor state and resource details.
2026-06-14 11:07:47 +01:00
rcourtman 2a33c3a09a Add Docker action readiness reasons
Refs #1034
2026-06-12 23:58:24 +01:00
rcourtman 53a05ccb7c Fail closed Docker and Podman actions without agents
Refs #1034
2026-06-12 22:42:16 +01:00
rcourtman 6725d6a784 Add governed Docker and Podman lifecycle actions
Refs #1034
2026-06-12 21:17:58 +01:00
rcourtman b53e1a3e0e storage(pools): carry the full ZFS pool payload to the storage surface
The unified-resource adapter flattened models.ZFSPool to four scalars
(state + R/W/C totals), so scan status and the per-device report never
reached the frontend. The storage detail's ZFS card, the usage bar's
scrub/resilver overlay and ZFS tooltip, and ZFS-device-to-disk matching
were all dead code on the unified path: a degraded pool rendered as a
one-word State cell with no way to see which device failed or that a
resilver was running. Found by the v5-parity audit of the pools table
(same pipeline disease as #1471 on the disks table).

- Add StorageMeta.ZFSPool and populate it in resourceFromStorage
  (normalized copy, not aliased).
- Serialize it in monitorStoragePlatformData so the /api/state path
  carries it alongside the existing flat scalars.
- Map it through normalizeStorageMeta and the storage record adapter
  (meta first, flat platformData fallback).
- Render the per-device report (name, vdev type, state, R/W/C errors,
  message) in the detail's ZFS Pool card - the v6 home for what v5
  showed via the health-map dots and the degraded-pool warning row.
- Pin the end-to-end path in code_standards_test and update the
  unified-resources, monitoring, storage-recovery, and
  frontend-primitives contracts.

Verified live in mock mode: degraded pool now shows the resilver scan,
pool error totals, per-device states, and the bar pulse overlay.
2026-06-11 16:48:57 +01:00
rcourtman a43f7cbe7f Add discovery readiness to Assistant context 2026-06-04 21:56:36 +01:00
rcourtman 0d0eb4bf11 Stabilize v6 release dry-run backend gate 2026-06-03 18:12:42 +01:00
rcourtman 3e61849242 Drop standalonePageModel snippet from agentless contract test; bump test timeout to 20m
TestAgentlessAvailabilityTargetKindStaysCanonical was pinning the
former agentless-machine classification in
frontend-modern/src/features/standalone/standalonePageModel.ts
(resource.availability?.targetKind,
availabilityTargetKindFor(resource) === 'machine'). Commit 1e16cf34f
intentionally narrowed the Machines surface to Pulse Agent resources
only, removing that classification, but did not update the test. The
server-side contract for availability targetKind across
config/availability.go, monitoring/availability_poller.go, types.go,
and frontend-modern/src/api/availabilityTargets.ts is preserved and
still pinned by the same test for any future consumer.

Makefile go test timeout bumped from 10m to 20m. The rc.5 backend
test run cleared 10m with slack; the rc.6 backend test run hit 13m
in internal/api before the binary panic-killed itself. 20m gives
headroom without hiding regressions for the rc.6 release path while
the package-size growth is tracked separately.
2026-05-27 17:17:13 +01:00
rcourtman bfafe559f6 Classify agentless machine availability targets 2026-05-25 21:38:43 +01:00
rcourtman 5016cbc2ba Add vSphere network inventory
Project vCenter network inventory through canonical resources and add the vSphere Networks table backed by vCenter network topology. Align resource presentation coalescing so state and resource APIs share the same host contract.
2026-05-22 20:26:56 +01:00
rcourtman 6c7d64ae43 Carry vSphere cluster services
Project vCenter cluster HA and DRS service state through the VMware resource facet so existing hosts and VMs expose cluster posture as read-only topology context.
2026-05-22 19:18:46 +01:00
rcourtman 9f98f8fcb9 Carry vSphere VM hardware config
Project vCenter VM hardware, CPU, memory, and boot configuration through the VMware resource facet and shared vSphere details so operators can inspect virtual hardware posture as read-only monitoring context.
2026-05-22 18:54:36 +01:00
rcourtman add3984f25 Carry vSphere VMware Tools status
Project vCenter VMware Tools runtime facts through the VMware resource facet and vSphere VM surface so operators can see Tools run state, version posture, upgrade policy, install attempts, and guest reboot requests as read-only monitoring context.
2026-05-22 18:22:49 +01:00
rcourtman 10c3c66f92 Carry vSphere VM virtual disks
Project vCenter VM hardware disk facts through the VMware resource facet and vSphere VM surface so operators can see virtual disk backing, capacity, datastore, and bus placement as read-only monitoring context.
2026-05-22 16:45:08 +01:00
rcourtman 6a7936ba4e Carry vSphere VM network adapters
Project vCenter VM hardware Ethernet adapter facts through the VMware resource facet and vSphere VM surface so operators can see vNIC backing network, MAC address, connection state, and adapter flags as read-only monitoring context.
2026-05-22 15:40:25 +01:00
rcourtman 3cd1517883 Surface vSphere activity timeline
Add a global resource timeline endpoint for provider activity and wire vSphere Activity to VMware timeline changes. Seed mock VMware activity through the same supplemental-change path and keep the relevant resource contract tests current.
2026-05-22 08:54:43 +01:00
rcourtman 0c9ea7504e Normalize inactive Docker Swarm evidence 2026-05-17 18:45:24 +01:00
rcourtman d3670d7882 Redact legacy action audit verification details 2026-05-13 21:50:56 +01:00
rcourtman 8b0f3564f6 Fail closed on stale API action plans 2026-05-12 17:32:11 +01:00
rcourtman 744166731e Merge duplicate map keys in TestActionExecutionContractStaysAPIOwned
Pre-existing bug exposed by slice 33: the contract test had two
filepath.Join(".", "actions.go") entries in its requiredSnippets map.
Go silently drops duplicate map keys (last entry wins), so the first
list — including the canonical ErrActionPlanDrift pin and the new
ErrResourceRemediationLocked pin from slice 33 — was never actually
running. The test reported PASS while pinning nothing it claimed to.

Merges both lists into a single actions.go entry so every snippet
runs. Also corrects ErrActionPlanDrift's expected alignment to the
single-space form gofmt produced when the var block grew the new
sentinel.
2026-05-09 15:08:51 +01:00
rcourtman e1103bf119 Refuse action dispatch when resource is operator-locked against remediation
Third compounding slice on per-resource operator state, completing the
suppression triangle: maintenance window (slice 31), intentionally
offline (slice 32), and now NeverAutoRemediate at the action-broker
boundary. When the operator has set NeverAutoRemediate=true on a
resource, executeCommandWithAudit refuses the dispatch even with a
valid approval and matching plan hash — per-resource operator intent
outranks per-action approval.

The check sits next to the drift refusal in the same audit hot path:
fail-open on store-lookup error (logged but doesn't block), persist a
Failed audit record with `resource_remediation_locked:` prefix on the
ErrorMessage so audit-UI filters and alert rules can branch on the
stable token. Returns ErrResourceRemediationLocked sentinel for
caller-side branching.

Reuses the existing actionAuditStore handle (which is the unified
ResourceStore from slice 29), so no new provider plumbing needed —
the broker already has the data path it needs.
2026-05-09 15:06:13 +01:00
rcourtman a597321801 Run class-derived verification check after successful dispatch
Closes the read-after-write piece of the user's act-with-control loop:
"Act with control: ask for approval, execute through Pulse tools, then
verify." Until now Pulse authored verification narrative copy at
approval time but never actually executed the check.

Adds ActionVerificationResult to internal/unifiedresources/actions.go
and embeds it as ExecutionResult.Verification so the existing
result_json column persists the outcome without a schema migration.
The struct records: did the check run, what command did Pulse send,
what came back, did it succeed, when, and any failure note.

Adds VerificationCommandForCommand in tools_control.go that derives
the per-class read-after-write check used by the broker:
- service-restart / service-start / service-reload / service-stop
  → systemctl is-active <unit>

Container classes (container-restart, container-stop) are
intentionally deferred to pulse_docker's existing tool-level
docker inspect verification — adding a broker-level dispatch would
double-run the same check.

executeCommandWithAudit now runs the derived verification command via
the same agent path immediately after a successful dispatch, captures
output and exit code, and writes the result onto
ExecutionResult.Verification before recordActionExecutionResult
persists. If the check returns non-zero, Verification.Success=false
with a Note explaining the exit code so the audit history honestly
shows that Pulse ran the action but couldn't confirm it took.

Verification is best-effort and class-scoped: unknown command shapes
leave Verification nil rather than fabricating a verified=true entry,
matching the no-fabrication boundary from Impact / preflight authoring.

Tests cover:
- VerificationCommandForCommand derives the right check per class,
  shell-escapes single quotes in unit names, and returns "", false
  for docker (deferred) and unknown commands.
- TestExecuteCommandWithAuditRunsClassDerivedVerificationAfterDispatch
- TestExecuteCommandWithAuditMarksVerificationFailedWhenReadbackDoesNotConfirm
- TestExecuteCommandWithAuditSkipsVerificationForUnclassifiedCommands

Updates ai-runtime contract with the read-after-write rule and
container-class deferral. Adds Completion Obligation #21 to
unified-resources pinning ExecutionResult.Verification shape and the
no-fabrication boundary. Extends TestActionExecutionContractStaysAPIOwned
to pin ActionVerificationResult struct and the Verification field on
ExecutionResult.
2026-05-09 09:48:40 +01:00
rcourtman 643d5f2992 Redact known secret shapes from action audit log persistence
The action audit log is plaintext SQL. Operators sometimes paste
secrets into a natural-language `reason` field ("rotate the key
sk-abc123 because it leaked"), and command output sometimes echoes
tokens. Both flows previously persisted unredacted, leaving the
audit history as a leaked-credential surface.

Adds RedactAuditText / RedactAuditRecord in a new
internal/unifiedresources/audit_redaction.go with a curated regex set
that targets the credential shapes most likely to be real secrets:
- URL with embedded basic-auth credentials (https://user:pass@host)
- Authorization: Bearer <token> and x-api-key: <token> headers
- Query-string secret params (?api_key=, &token=, &access_token=)
- JSON-style secret fields ("api_key": "...", "password": "...")
- Env-style or CLI-style secret assignments (PASSWORD=, api_key=)
- OpenAI/Anthropic-style API keys (sk-...)

The set is intentionally narrower than the patrol-failure redactor in
internal/ai/patrol_runtime_failure.go: it does NOT strip arbitrary
URLs, because operators legitimately reference runbooks, ticket
links, and GitHub issues in audit reasons. Only patterns very likely
to be real secrets are touched.

Wired at the persistence boundary (top of each store method) so all
record paths get redaction uniformly:
- SQLiteResourceStore.RecordActionAudit
- SQLiteResourceStore.RecordActionExecutionStart
- SQLiteResourceStore.RecordActionExecutionResult
- MemoryStore.RecordActionAudit
- MemoryStore.RecordActionExecutionStart
- MemoryStore.RecordActionExecutionResult

Plan, Approvals, and identity fields are left alone — they are
produced by Pulse, not operators or external command output, so they
do not need redaction (and changing them would break PlanHash drift
detection).

Verification artifacts:
- audit_redaction_test.go: pattern-by-pattern coverage plus public-URL
  passthrough and empty-string passthrough; full RedactAuditRecord
  shape test (Reason + Params string values + Result output redacted;
  non-string Params and Plan untouched).
- code_standards_test.go: pins RedactAuditRecord call sites in
  store.go and helper signatures in audit_redaction.go so future
  refactors cannot silently bypass redaction.
- registry_test.go: cross-cutting integration test that exercises the
  registry-store boundary (MemoryStore.RecordActionAudit) and asserts
  redaction is applied while Plan fields stay untouched.

Adds new Completion Obligation #20 to unified-resources contract
pinning the redaction-at-persistence-boundary rule and the
no-touch-Plan-fields invariant.
2026-05-08 22:34:57 +01:00
rcourtman 22ce58cb9b Persist a refused audit record when plan drift is caught
Slice 13 added the drift refusal path but only logged at WARN level —
the audit history showed nothing, so an operator reviewing the action
trail could not see "Pulse caught this drift attempt." Now the drift
branch writes a Failed audit record with Result.ErrorMessage prefixed
"plan_drift:" and dispatches a Failed lifecycle event before returning
ErrActionPlanDrift.

The record carries the same Request, Plan, and Approvals snapshots that
a normal audit record would, so the operator-facing audit row shows
exactly what was attempted and what was approved. The "plan_drift:"
prefix is a stable token for downstream surfaces (audit UI filters,
alert rules) to distinguish drift refusals from generic execution
failures.

Extends TestExecuteCommandWithAuditRefusesPayloadDriftAgainstApprovedPlan
to assert the audit record exists with State=Failed and the plan_drift
error message after refusal. Updates the code-standards snippet check
for ErrActionPlanDrift to match gofmt's actual alignment in actions.go.
ai-runtime contract pinned with the audit-record-on-drift rule.
2026-05-08 22:20:01 +01:00
rcourtman a9e652e05b Refuse action execution when approved plan hash drifts from payload
PlanHash existed on ActionPlan as the contract for "the operator
approved exactly this (command, target, reason) combination" but the
broker never validated it: at execute time the freshly-recomputed hash
was overwritten by the approved plan's hash via mergeApprovedActionPlan
without comparison. A drifted payload (LLM re-emits with different
command, agent ID changes between approval and execute, malicious
injection) would run under a stale approval.

Fix: at the dispatch boundary in executeCommandWithAudit, recompute the
approval-equivalent hash from the actual payload using approvalPlanHash
(same function used at approval-creation time, so direct comparison is
meaningful), compare to plan.PlanHash, and refuse with a new
ErrActionPlanDrift error when they differ. The drift refusal also logs
at WARN level with action_id, approval_id, and both hashes so audit
review can see when drift was caught.

When approvedHash is empty (older approval records or contract paths
that did not author one), validation is skipped and existing behavior
is preserved.

Adds two tests:
- TestExecuteCommandWithAuditRefusesPayloadDriftAgainstApprovedPlan:
  approval is for "systemctl restart workload"; payload at execute
  time is "rm -rf /var/log/pulse"; expects ErrActionPlanDrift and no
  agent dispatch.
- TestExecuteCommandWithAuditAllowsMatchingPlanHash: same payload
  matches the approved hash; expects normal dispatch.

Updates two pre-existing tests that used stub PlanHash strings:
- TestExecuteCommandWithDeniedApprovalDoesNotDispatch: was testing the
  denial path with PlanHash:"sha256:test". Now uses the real approval-
  equivalent hash so denial fires (not drift) and the test still
  isolates the denial behavior.
- ControlledConsumesApprovedCommandWithResolvedRoutingTarget: same fix
  pattern.

Extends TestActionExecutionContractStaysAPIOwned in code_standards
test to pin ErrActionPlanDrift's existence in actions.go so future
refactors cannot silently downgrade drift into a generic error kind.

Native action path (executeNativeActionWithAudit) is left for a
follow-up: it has the same drift gap but uses actionPlanHashForParams
which is shaped differently from approvalPlanHash, so a sound fix
needs a coherent canonical hash function rather than just adding the
check. Contracts pinned in ai-runtime and unified-resources (new
Completion Obligation #20).
2026-05-08 22:11:31 +01:00
rcourtman 39a5be31a0 Sanitize Assistant briefing handoffs 2026-05-06 21:01:55 +01:00
rcourtman d7225a45a0 Fix Proxmox guest memory fallbacks
Also fixes Ceph pool threshold resource identity.

Refs #1341
2026-05-05 14:59:29 +01:00
rcourtman 82a2494ffa Add action execution safety contract 2026-05-04 23:19:58 +01:00