Commit Graph

3377 Commits

Author SHA1 Message Date
rcourtman 5abbaba091 Release mobile compatibility work claim 2026-07-22 13:45:30 +01:00
rcourtman 9e39ffc920 Enforce Pulse Mobile compatibility from the core contract
Contract-Neutral: mobile compatibility generation does not change agent lifecycle or storage recovery contracts
2026-07-22 13:41:09 +01:00
rcourtman 3c8705da92 Let mobile relay tokens reach the Patrol attention routes
v6.1.0-rc.4 shipped the attention workbench gated on monitoring:read
only, so mobile relay tokens (which carry just relay:mobile-access) got
403 on alert sync, finding detail, and acknowledge the moment a server
upgraded to rc.4. The attention routes supersede the legacy patrol
findings routes, which accepted the mobile capability, so they join the
governed relay mobile runtime route inventory: reads accept
relay:mobile-access alongside monitoring:read (legacy ai:execute),
mutations alongside monitoring:write.
2026-07-22 12:31:44 +01:00
rcourtman fb3d60e475 Document What's New dialog in rc.5 2026-07-22 11:53:50 +01:00
rcourtman 67d7a1b454 Delegate candidate artifact read permission 2026-07-22 11:41:10 +01:00
rcourtman d3070ce1cb Prepare v6.1.0-rc.5 release 2026-07-22 11:28:45 +01:00
rcourtman 671b159711 Retain last-known platform on removed agents for scoped cleanup handoff
Removed-agent records kept no platform identity, so the fleet doctor
emitted an empty platform for removed rows and the Agent Doctor
uninstall handoff could never tighten to a single platform command.
Host and Docker removal now capture the agent's last-known reported
platform onto the removed record, and the fleet diagnostic resolves the
retained value through the same strict normalization as live subjects,
so /api/agents/diagnostics reports a platform for removed agents and
the handoff shipped in 4fc86ec18 scopes to one command automatically.
A retained value that does not normalize to a known platform still
yields an empty diagnostic platform rather than a guess, and removed
Kubernetes clusters retain nothing because the cluster report never
carries a platform. The field is additive and optional, so removed
lists serialized before it existed load unchanged. Monitoring contract
updated; proofs staged in agent_fleet_doctor_test.go and
monitor_host_agents_test.go.
2026-07-22 11:06:27 +01:00
rcourtman 4fc86ec187 Hand off host-local uninstall commands for removed agents in Agent Doctor
Removed targets previously dead-ended with no next step. Their row
expansion now offers the host-local uninstall command, resolved from the
retained diagnostic identity via the strict platform resolver, or both
labeled Linux-family and Windows commands when the platform is unknown
(the current wire reality, since removed records retain no platform).
The handoff carries the diagnostic's agent id and hostname as uninstall
identity flags and states the command runs on the affected host, not
from Pulse. The governed uninstall builders now accept a narrowed
AgentUninstallIdentity view so surfaces without a ledger-backed
inventory row reuse the same transport.
2026-07-22 10:56:04 +01:00
rcourtman c87ad1cf26 Add status filters and copyable diagnostic reports to Agent Doctor
The summary chips now double as status filters over the fleet table
(click to filter, click again or let the status empty out to clear),
so a large fleet collapses to the rows that need attention. The page
also offers plain-text diagnostic reports for pasting into a support
thread or GitHub issue: a fleet-level copy over the currently visible
rows and a per-agent copy inside each row expansion. Reports carry
status, versions, last-seen, diagnosis reasons, identity evidence, and
non-command repair actions, and deliberately never embed host-local
update commands since those can carry install tokens.
2026-07-22 10:37:11 +01:00
rcourtman c8b1e9d07b Re-home Agent Doctor from a settings modal to a routed page
Agent Doctor now lives at /settings/infrastructure/agent-doctor as a
full page in the settings shell instead of a max-w-lg dialog stacked
over the Infrastructure workspace, which was cramped on phones. The
per-agent card pile becomes one fleet table (agent, system, status,
reported and target versions, last seen) with per-row expansions
holding the diagnosis reasons, identity evidence, repair actions, and
the host-local update command. A lone scoped target auto-expands so
platform-page deep links land straight on the diagnosis.

Legacy ?agentDoctor=1 / ?agentUpdates=1 workspace links redirect onto
the route with their agents scope preserved, and closing the page is
now ordinary back navigation. The dialog component is renamed to
InfrastructureAgentDoctorPage; shared-template registry, the
agent-lifecycle subsystem inventory, and the frontend-primitives
settings deep-link clause follow the change.
2026-07-22 10:26:06 +01:00
rcourtman f1eac15833 Clear expired release work claim 2026-07-22 09:10:40 +01:00
rcourtman b8e78fa9c2 Persist Patrol proposal evidence in action origins 2026-07-22 09:10:40 +01:00
rcourtman 0928584625 Pro updater requests its channel from the dual-channel broker
The license-server broker now serves stable and rc manifest slots
(pulse-pro side). The Pro self-updater passes channel=rc for rc-channel
installs so they track the RC slot; stable installs keep the unchanged
default URL, which older brokers also understand. The client-side guard
that refuses a prerelease pin on the stable channel stays as the backstop
for a drifted or single-manifest broker. Contract updated under
deployment-installability; fixture now pins the channel query behavior.
2026-07-22 00:16:43 +01:00
rcourtman f16a7f4d7d Record commercial proof approval boundary 2026-07-21 23:57:45 +01:00
rcourtman dc25f20cfd Record executable commercial floor proof 2026-07-21 23:49:43 +01:00
rcourtman be0ef26d16 Supersede unsafe commercial rehearsal outline 2026-07-21 23:26:12 +01:00
rcourtman d6fa623035 Compose ESXi hosts as members of their owning vCenter connection
One vCenter connection spans many ESXi hosts, so the Infrastructure source
row now lists them the way Proxmox cluster rows list their nodes: an
expandable member list with per-host state, aliases, and last-seen. Members
are API-side composition only — no primary marker, no agent connection, and
the member subtitle reads 'vSphere host' instead of cluster-node wording.
TrueNAS keeps no member composition because that connection monitors exactly
one machine.

Mock mode now feeds its vCenter and TrueNAS fixtures into the connections
aggregator (only when no real instances are configured), so the mock ledger
shows the same platform source rows a real deployment would instead of
omitting vSphere and TrueNAS entirely.
2026-07-21 23:09:29 +01:00
rcourtman 278820283c Record commercial rehearsal CLI validation
Contract-Neutral: Evidence-only commercial rehearsal record; unrelated runtime files and shared index belong to another active task
2026-07-21 23:03:59 +01:00
rcourtman 77cfa9f1e4 Record fail-closed commercial event rehearsal 2026-07-21 22:53:52 +01:00
rcourtman 536a845c7a Record commercial reconciliation evidence 2026-07-21 22:39:42 +01:00
rcourtman 43bb95a406 Integrate SignPath Windows release signing 2026-07-21 22:34:00 +01:00
rcourtman 9e8bcd23fe Stop fabricating ledger agent rows for integration-monitored machines
vSphere ESXi hosts and TrueNAS boxes rendered as standalone rows under
'Pulse Agent hosts' in Infrastructure settings even though no Pulse Agent
runs on them. The rows were unmanageable duplicates of their owning platform
connection: they carry no credentials, pause, or remove semantics, and they
can never attach to their vSphere/TrueNAS system because attachment requires
a shared host while the machine hostname differs from the vCenter address.
They also inflated connected-system counts.

buildConnections now skips hosts with a non-empty IntegrationSource, so the
ledger and grouped systems represent those machines solely through their
owning platform connection. Per-machine visibility is unchanged on the
vSphere/TrueNAS platform pages and Machines, which read the unified fabric
directly. Connection.integrationSource stays declared as defense-in-depth
for agent-only client workflows. No alert impact: agent-type rows were
already dropped from alert snapshots.
2026-07-21 22:11:46 +01:00
rcourtman 7354d8d19f Keep integration-monitored machines out of Agent Doctor and surface workload-only agents
The connections ledger derives agent rows from the unified fabric, which
includes machines whose telemetry comes from platform integrations (vSphere
ESXi hosts, TrueNAS). Agent Doctor rendered every one as a permanent
'Unknown / no structured reason' row, while agents the ledger does not carry
(Docker-only, Kubernetes-only) were silently dropped from the fleet view.

- Expose HostView.IntegrationSource() (source-set based: only SourceAgent
  ingest counts, since integration providers fabricate an Agent payload) and
  plumb it through models.Host to the connections ledger as the optional
  integrationSource field.
- Agent Doctor skips integration-backed connections and appends
  diagnostics-only agents, honoring scope, so the doctor covers exactly the
  real Pulse Agent fleet.
- Update readiness agent checks no longer count integration-backed machines
  as registered agents.
- Humanize doctor copy: plain-language stale message with '10m 2s'-style
  durations, offline wording without enum leakage, no 'Supported target:
  Unknown' cell when no target is published, host-local command banner only
  when a command is actually offered, and a compact non-zero summary strip.

Contracts updated for unified-resources, monitoring, api-contracts,
agent-lifecycle, and dependent storage-recovery; verification via
views_test.go, monitor_host_agents_test.go, state_host_test.go,
contract_test.go, and the frontend connections API test.
2026-07-21 21:24:15 +01:00
rcourtman ea664843ba Align prerelease dry-run signing requirements
Keep macOS notarization mandatory for every release candidate while requiring Windows Authenticode only for stable promotion, matching the publish workflow and RC4 release packet.
2026-07-21 09:17:39 +01:00
rcourtman 9f9e02e811 Refuse dispatch when action readiness is lost 2026-07-20 21:44:59 +01:00
rcourtman ed84c92da3 Add Patrol model readiness advisor 2026-07-20 20:45:39 +01:00
rcourtman 057cf74629 Add alert intent policies and delivery receipts 2026-07-20 20:27:39 +01:00
rcourtman e23f19459d Add Agent Doctor fleet diagnostics workflow 2026-07-20 19:53:34 +01:00
rcourtman cee4f64614 Complete trust-gate governance contracts 2026-07-20 17:43:01 +01:00
rcourtman b6a74576bc Integrate trust-gate reliability fixes 2026-07-20 16:03:29 +01:00
rcourtman d89e3e3163 Add staged-file prettier formatting to pre-commit
New scripts/release_control/format_staged_frontend.py mirrors the staged
Go formatter: formats staged frontend-modern/src {ts,tsx,css,json} blobs
through prettier --stdin-filepath, writes results back to the index
directly (no broad restaging), syncs the worktree only when it matches
the previously staged content, and iterates to a fixed point to absorb
prettier's occasional non-idempotence. Skips gracefully when prettier is
not installed (fresh clones, linked worktrees without node_modules).
Wired into .husky/pre-commit after the Go formatter, with unit tests in
the governance battery, a README note, and a .gitignore allowlist entry.
With the one-time sweep in the previous commits, prettier drift can no
longer re-accumulate and make format stays clean on a clean tree.
2026-07-20 10:44:36 +01:00
rcourtman 99af2a0bf2 Accelerate mock history readiness 2026-07-20 04:21:26 +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 260073c27e Complete Operational Trust governance 2026-07-19 15:11:02 +01:00
rcourtman 83672a00a8 Harden Operational Trust rollout contracts 2026-07-19 15:11:02 +01:00
rcourtman cd75722da3 Build canonical Operational Trust action loop
Refs #1034
2026-07-19 15:11:01 +01:00
rcourtman 69533c7afb Build canonical availability resource facets 2026-07-19 15:11:01 +01:00
rcourtman 07a4d05ca3 Build canonical Patrol attention workbench 2026-07-19 15:11:01 +01:00
rcourtman 93dbdd91cf Build canonical protection posture 2026-07-19 15:10:38 +01:00
rcourtman cf0486492e Build operational trust lifecycle foundation 2026-07-19 15:10:38 +01:00
rcourtman c2df4ad277 Fix alert detection and notification state boundary 2026-07-19 15:10:09 +01:00
rcourtman 0893dc8a5d Close v6.1.0-rc.3 release claim 2026-07-18 18:37:23 +01:00
rcourtman b9eb7c3241 Prepare v6.1.0-rc.3 release 2026-07-18 16:56:00 +01:00
rcourtman 667cb086b0 Fail closed on unresolvable agent tokens in the connections command-policy view
The desired side of the /api/connections command-policy comparison is
contractually the effective config served to the agent after token scope
and binding checks, but a host whose recorded TokenID no longer resolved
to a live API token skipped sanitization entirely and kept the raw
profile desire. A stale binding, typical after a token was revoked and
the agent reinstalled, then presented as Command policy mismatch even
though the served runtime config could never enable commands and the
operator had nothing to fix (seen on issue #1564). When API tokens
exist and the host's token cannot be resolved, desired command policy
now fails closed to disabled.

The contract delta also records the install-token mint semantics that
landed with the Add Pulse Agent scope fix, closing the deferred
agent-lifecycle delta noted on that commit's contract-neutral trailer.

Contract-Neutral: fail-closed bugfix bringing the connections command-policy view into conformance with the existing agent-lifecycle contract clause; the staged agent-lifecycle delta records the semantics, api-contracts/storage-recovery have no real delta
2026-07-18 13:09:18 +01:00
rcourtman 41c0c219a2 fix(truenas): read the API shapes TrueNAS actually serves (#1573 residuals)
wilddev65's rc.2 retest confirmed the connection-keying fix split the two
same-serial CORE boxes but left four symptoms. All four trace to Pulse
reading fields the TrueNAS API has never served, which the pre-fix
identity flapping had masked behind bigger breakage:

- Datasets all showed Offline: pool.dataset.query returns no "mounted"
  field on any TrueNAS version (CORE 13 and SCALE both strip it from the
  property allowlist), and the parser read absence as unmounted. A listed
  dataset now defaults to mounted unless "locked" (encrypted, key
  unloaded) or an explicit mounted=false says otherwise.

- Disks all showed Attention/Unknown: disk.query carries no status or
  smart_status field either, so every disk landed on UNKNOWN health,
  which the storage table bucketed as Attention. Per-disk ZFS state
  (ONLINE/DEGRADED/FAULTED/...) and pool membership are now derived from
  the pool.query topology that both platforms attach unconditionally --
  disk.query's extra.pools cannot cross the REST bridge, whose query
  params turn into filters that silently match nothing. Disks therefore
  nest under their pools again, and a genuinely unreadable health renders
  as neutral Unknown instead of amber Attention.

- Disk temperatures were blank: disk.temperatures takes parameters, so
  REST v2.0 has only ever served it as POST with a body keyed by param
  name; the client's GET failed on every version, and CORE (no JSON-RPC
  endpoint) has no reporting fallback to rescue it. The ladder now tries
  native JSON-RPC reporting first and falls back to the POST form, and
  the provider surfaces API-reported disk temps as SMART sensor entries
  so the host Thermals card lists disks for API-backed systems.

- The storage table ordered identically-named pools (a DR pair both
  named "tank") by map iteration order, flipping between refreshes;
  ordering now tie-breaks on system name then id.

Verified end-to-end against two stub TrueNAS CORE 13 REST servers
reporting the same placeholder DMI serial: storage tab nests both
systems' pools/datasets/disks correctly with temperatures and Healthy
states, and the system drawer Thermals card lists per-disk readings.
2026-07-18 01:01:56 +01:00
rcourtman 952fccf8df feat(ai): enforce look-before-asking — refuse first-action elicitations in the agentic loop
Completes the first-session elicitation fix that a3f8b18bf started at
the tool layer. The second observed failure mode ("Are there any alerts
I should look at?" on Ollama qwen3:8b) had the model calling
pulse_question BEFORE any tool call, so no tool error copy and no tool
description can reach it — the resolve-before-asking prompt policy
(945ed2198) needs runtime teeth for small local models.

The agentic loop now refuses an interactive pulse_question issued
before the run has attempted any real tool call: the model gets an
error tool result steering it to read-only enumeration
(pulse_summarize {"action":"fleet"} and the alert tools need no
parameters), no question card or clarify event reaches the stream,
sibling tool calls from the same provider turn keep processing instead
of tripping the interactive-set skip path, any queued tool attempt
satisfies the gate for the rest of the run, and it fails open after 2
refusals so an unanswerable prompt cannot livelock. Non-interactive
profiles keep their existing separate block. The system prompt
discloses the gate; question-flow tests now open with a look turn the
way real runs must; a corpus scenario pins the stream promise (a
natural first question produces an answer, never a clarification card).

Live-verified on a real Ollama qwen3:8b scratch instance (real local
agent, not mock): both natural openers stream tool-backed answers with
zero question events. Full ./internal/ai/... suite green. ai-runtime
contract updated in-commit.
2026-07-18 00:18:35 +01:00
rcourtman a3f8b18bfb feat(ai): make pulse_summarize self-target and forbid identifier elicitation
A fresh install's natural first question ("how is my machine doing?",
observed live with Ollama qwen3:8b) made the model call pulse_summarize
action=fleet without resource_ids; the tool errored and the agentic loop
surfaced a structured question asking a first-run user for "the
comma-separated list of resource IDs" — pure jargon, funnel-killing
(GitHub discussion #1042). Two root fixes:

pulse_summarize self-targets. action=fleet with resource_ids omitted now
enumerates the known fleet from the executor's unified resource provider
(infrastructure parents first, then guests, then storage; deduped;
bounded at the existing 50-resource cap with a truncation note). Both
modes resolve what models actually pass — canonical unified IDs and
unambiguous names — onto the reporting request shape the way the API
path's resolveReportSubject does: the canonical ID stays ResourceID
(findings/recovery keying) and the resolved metrics target rides
MetricsResourceID, so store queries find data instead of silently
returning zero points. Reporting types are classified from the unified
resource (agent-backed hosts "agent", pure Proxmox nodes "node" — the
documented target-type exception — Docker hosts "docker-host");
resource_type becomes an optional filter/default. Remaining error paths
instruct the model to enumerate or retry and forbid asking the operator
for resource IDs.

Ask-user policy covers tool-argument recovery. The system prompt's
resolve-before-asking section (945e2198's target policy) now extends to
failed tool calls: missing/invalid arguments are self-recovered
(enumerate, retry), and internal identifiers are never valid questions.
The pulse_question description carries the same prohibition so the
structured clarification surface cannot become an identifier elicitation
channel on small local models.

ai-runtime and api-contracts contracts updated in-commit (the
subsystem_lookup line pin follows the api-contracts insertion); full
./internal/ai/... and ./internal/agentcapabilities/... suites green.
2026-07-17 23:55:38 +01:00
rcourtman b5d852a279 fix(notifications): webhook dialer tries every permitted resolved IP instead of pinning the first
Same defect class as 64fb3d198 (pkg/securityutil): the SSRF-pinned webhook
dialer resolved the host and dialed only the first permitted IP, so a host
resolving to ::1 ahead of 127.0.0.1 while the receiver listens on one
loopback family — or a multi-A-record host with a dead leading address —
got a hard connection failure even though curl works. The dialer now tries
each permitted resolved IP in resolution order; every candidate still
passes the private-IP/allowlist validation, so rebinding protection is
unchanged.

Evaluated migrating onto securityutil.NewRestrictedOutboundHTTPClient and
rejected a straight swap: the webhook client's isPrivateIP blocks a wider
range set (CGNAT, benchmarking, TEST-NET), its redirect policy allows
re-validated cross-origin hops where securityutil enforces same-origin,
Proxy is deliberately nil, and the private-IP allowlist is a live per-CIDR
runtime hook — a swap would loosen the SSRF policy and change behavior.

Adds a resolver test seam on NotificationManager and a regression test
pinning the ::1-first fallback; notifications contract records the
multi-IP dial obligation.

Closes chip task_5e91afd9.
2026-07-17 23:49:02 +01:00
rcourtman 3ffd622de0 feat(settings): make the external-agent (MCP) connector setup findable in sidebar search
Production telemetry showed 1,437 installs on the external-agent surface but
only 2 on the Pulse MCP adapter. Live verification proved the pulse_mcp
telemetry surface attribution is honest (adapter tools/call records
surface=pulse_mcp; header-less calls record agent_api), so the gap is real -
but the funnel comparison is also misleading: agent_api counts any scoped
API-token REST consumer (scripts, dashboards polling /api/config/nodes or
/api/ai/patrol/findings), not BYO AI agents. Recorded that interpretation
constraint on PulseIntelligenceExternalAgentEvidence.

The genuine product gap: the in-app connector setup (Settings -> Pulse
Intelligence -> Assistant -> External agents) was invisible to sidebar search.
Searching 'mcp', 'claude', 'opencode', or 'connector' returned 'No settings
found' because search matched only nav labels and header descriptions.

- Add search-only keywords to SettingsNavItem and match them in
  filteredTabGroups; the Assistant item now carries mcp / model context
  protocol / external agent(s) / claude / opencode / connector / pulse-mcp.
- Name external agent (MCP) connectors in the Assistant header description
  (EN/DE/ES kept in sync), so the page header states what the page contains.
- Pin the search behavior with parameterized useSettingsAccess tests plus a
  settingsArchitecture source proof; record the keyword channel in the
  frontend-primitives contract (obligation 41) and the product-copy allowance
  in the security-privacy i18n extension point.
2026-07-17 23:43:05 +01:00