Commit Graph

8141 Commits

Author SHA1 Message Date
rcourtman ee9d4b1071 Reserve Patrol reporting turn for confirmed symptoms 2026-07-14 18:10:00 +01:00
rcourtman e0edc520b7 Release Stripe proof audit work claim 2026-07-14 18:07:53 +01:00
rcourtman f6a289f72d Make Patrol report confirmed health failures 2026-07-14 18:04:09 +01:00
rcourtman 7f2c0380c6 Claim read-only Stripe proof audit 2026-07-14 17:59:51 +01:00
rcourtman dc416dd9be Claim Patrol Watch evidence contract work 2026-07-14 17:49:52 +01:00
rcourtman 1f9410a10b Release commercial coherence work claim 2026-07-14 17:39:49 +01:00
rcourtman f63a58b9a8 Release Patrol provider resilience work claim 2026-07-14 17:36:58 +01:00
rcourtman 728d09769c Preserve qualification artifacts for bounded transcripts 2026-07-14 17:31:05 +01:00
rcourtman f1adb00d90 Govern license runtime commercial config 2026-07-14 17:29:05 +01:00
rcourtman 7deaf60b37 Harden Patrol provider streams and runtime scoring 2026-07-14 17:20:56 +01:00
rcourtman 206b68cc94 Clarify commercial offer boundaries 2026-07-14 17:18:14 +01:00
rcourtman a8341b013f Open the shipped security guide outside the SPA router
The Security Overview configure-https action linked /docs/SECURITY.md
with external: false, so Solid Router intercepted the same-origin
anchor click and rendered the SPA NotFound page instead of letting the
browser fetch the embedded doc. Mark the link external like every
other shipped-doc consumer.

Refs discussion #1505
2026-07-14 17:13:50 +01:00
rcourtman 085f5abe0d Claim Patrol provider stream resilience work 2026-07-14 17:11:45 +01:00
rcourtman 8f31fec341 Release Patrol headless projection work claim 2026-07-14 17:08:43 +01:00
rcourtman e56561b76a Refresh canonical resources after headless agent reports 2026-07-14 17:03:14 +01:00
rcourtman a05e905381 Govern commercial transition convergence 2026-07-14 16:59:28 +01:00
rcourtman e98eaebf43 Claim commercial transition coherence work 2026-07-14 16:47:56 +01:00
rcourtman 630481aeca Release Patrol qualification evidence work claim 2026-07-14 16:45:33 +01:00
rcourtman 93fff4f1d8 Add an explicit operator override for plaintext HTTP to non-local Pulse hosts
Fleets on networks numbered from nominally public IP space (issue
#1522: an AD estate on 192.20.0.0/16) cannot pass the agent's
local-network plaintext heuristic, and the only workaround was pointing
a .internal DNS alias at the server, which bypasses the same control
less visibly than a flag would. --allow-plaintext-http
(PULSE_AGENT_ALLOW_PLAINTEXT_HTTP) records process-wide consent once at
agent startup before any module validates a URL, covers every agent
transport including the websocket command channel, warns at startup
that the API token travels in cleartext, defaults closed, and is never
emitted by generated install commands or settable by the server.
2026-07-14 16:42:02 +01:00
rcourtman 84eff17578 Preserve Patrol evidence on provider errors 2026-07-14 16:34:19 +01:00
rcourtman 8e417010eb Release commercial invalidation work claim 2026-07-14 16:20:23 +01:00
rcourtman ae4162f8f2 Enforce installation-scoped license invalidation 2026-07-14 16:18:21 +01:00
rcourtman 623000b933 Release Patrol runtime work claim 2026-07-14 15:37:36 +01:00
rcourtman 3f45953866 Complete Patrol autonomous qualification loop 2026-07-14 15:35:48 +01:00
rcourtman e18b28a780 test(coverage): unifiedresources adapters and agentexec server-receipt branches
Add purely-additive branch-coverage tests for two 0%-covered backend surfaces
from the recent feature drop:
- internal/unifiedresources: unraidDiskMountTotal (role/mount-path resolution,
  trim-normalized mountpoint match, zero-total skip) and dockerStorageUsageMeta
  (four-operand nil guard, full field copy).
- internal/agentexec: Server.matchesPendingDockerUpdateOperation (identity /
  subject-id normalization / absent-key arms) and AgentOperationReceiptVersion
  (nil-receiver, not-found, stored-version arms).
No source changed.
2026-07-14 14:56:47 +01:00
rcourtman 4dcc18fbd4 Match nvme-eui zpool member references to physical disks
A pool built from /dev/disk/by-id/nvme-eui.<hex> references (the
installer's device naming when identical NVMe models share a box)
produced no serial key in the disk-to-pool matcher, and the WWN lookup
never stripped the eui. prefix smartctl reports, so the disk fell back
to the generic Proxmox usage string and showed 'ZFS' instead of its
pool name while an identically-built node showed 'local-zfs' (issue
#1540).
2026-07-14 14:48:30 +01:00
rcourtman d7a73ee521 test(coverage): patrol investigation context model branch coverage
Add a purely-additive branch-coverage suite for the residual uncovered arms of
patrolInvestigationContextModel builders (assistant briefing/handoff inputs,
configuration-failure handoff secret redaction, investigation context summary
numeric clamping, proposed-fix keep/drop gating, approval-policy checks). The
existing dev test is left untouched; this exercises previously-unexecuted
exports and branch arms only. No source changed.
2026-07-14 14:48:01 +01:00
rcourtman f5aeac590b Survive NAS installer environments without od and with Synology systemd
Two agent-install failures from stock NAS shells: QNAP ships no od, so
the ELF header sniff read empty and rejected a valid download (issue
#1572); Synology DSM 7's patched systemd cannot apply the unit's
sandbox directives and killed the service with
status=227/NO_NEW_PRIVILEGES before exec (issue #1578). The header
sniff now falls back od -> hexdump -> xxd and skips with a warning when
none exist (checksum verification still guards integrity), and the
systemd unit omits the sandbox hardening block on DSM.
2026-07-14 14:47:45 +01:00
rcourtman e1985528d9 test(coverage): agentexec codecs and licensing service branch coverage
Add purely-additive branch-coverage tests for freshly-landed, previously-
untested backend surface:
- internal/agentexec: docker container update codec (decode/validate/bind/
  digest/result-cross-validation), residual host apt codec validation arms
  (host update + storage cleanup decode/bind/result/operation-query identity),
  and NormalizeDeployMaxParallel clamping.
- pkg/licensing: service helpers (unionFeatures, safeIntFromInt64,
  remainingDaysCeil, ensureGracePeriodEnd, Set/Get Evaluator+StateMachine,
  IsValid, CurrentUnsafeForTesting, dev-mode) and activation GrantEnvelope
  ParseExpiresAt + uncapped-core-monitoring tier classification.
No source changed.
2026-07-14 14:45:55 +01:00
rcourtman f50bcce2dc Govern Relay commercial invalidation 2026-07-14 14:38:58 +01:00
rcourtman acd5637485 Drive executing-action restart recovery at startup and agent registration
RecoverExecutingActions existed with full test coverage but had no
production caller, so any typed action mid-dispatch across a server
restart (container update, start/stop/restart, host update, storage
cleanup) stayed in the executing state forever and sat in the Actions
inbox as live work, even after the agent persisted its terminal durable
receipt. Reproduced live on the dev instance with a Docker container
update (act_bf77dfe860ad3d8e4e0a91dc8eb83b44).

The router now runs a bounded, serialized recovery pass per organization
from a startup background worker, and again whenever an agent
(re)registers on the agentexec command server via a new registration
notifier, because a receipt-pending attempt can only be reconciled while
the owning agent is connected. Both triggers reuse the existing
query-only reconciliation semantics; nothing gains a resend authority.

Task 07 owns this residual; the api-contracts and agent-lifecycle
subsystem contracts now record the production trigger. The
rg-07-durable-delivery gate suite stays green, and a new router-level
test pins that a receipt-pending executing action completes from the
agent receipt without a second dispatch.
2026-07-14 14:19:19 +01:00
rcourtman 1a2215c9b4 Refresh the action-plan contract snapshot for identity-hashed resource versions
098ba4eaa changed plan resource versions to hash relationship identity
instead of observation stamps, and updated the planner unit tests, but
missed the pinned API contract snapshot: its fixture carries a vm-to-node
relationship, so the resourceVersion (and the actionId, decisionId, and
planHash derived from it) legitimately changed. TestContract_ActionPlanJSONSnapshot
has been red on main since that commit; this re-pins the snapshot to the
deterministic values the new hashing produces.
2026-07-14 14:15:52 +01:00
rcourtman 68dd64e768 Drive the container Update button through the governed action review flow
The Update button on the Docker containers table and workloads rows now
plans an audited update action and opens the same governed review
dialog the lifecycle controls use (approve, run, per-action audit),
instead of calling the retired direct-update endpoint. The host
drawer's bulk update-all reports honestly that updates run as reviewed
per-container actions until a reviewed bulk flow exists, and the dead
updateDockerContainer/updateAllDockerContainers API clients are
removed. Update state still tracks through the containerUpdates store,
flipping to success when the refreshed report clears updateAvailable.
2026-07-14 12:23:43 +01:00
rcourtman 098ba4eaa9 Hash relationship identity, not observation stamps, into plan resource versions
Docker adapters restamp relationship ObservedAt/LastSeenAt on every
~15s report, and the action planner folded those stamps into the plan's
resource version, so any reviewed action against a relationship-bearing
container (start, stop, restart, and the restored update) drifted to a
409 action_plan_drift before a human could read the review dialog and
click approve. Relationship edges now count by identity (source,
target, type, active, discoverer, metadata), the same
identity-versus-timestamp boundary change emission drew for issue
#1496. Found live: the UI update journey failed with plan drift on
every attempt slower than one report cycle.
2026-07-14 12:22:02 +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 8f475cbf58 Fold runtime-key Docker URL metadata into the stable guest key
URLs saved through the resource drawer historically landed in the docker
store under the runtime container key, which any stable record (including
an intentionally empty cleared one) outranks in the unified customUrl
projection, and which orphans on container recreation. On report ingest,
copy those records into the stable app-container guest key when it is
missing (cleared links stay cleared), healing saves stranded before the
drawer moved to the stable identity. Also read the runtime key before its
copy-if-missing container-name snapshot so the freshest write wins among
the docker-store fallbacks.

Refs #1556
2026-07-14 12:00:57 +01:00
rcourtman 7a1e41bcf9 Let flowing AI streams outlive the configured request timeout
Thinking models served through OpenAI-compatible endpoints (qwen3 via
Ollama >=0.31, DeepSeek) spend most of a turn streaming per-token
reasoning deltas before any content. The stream reader wrapped the whole
turn in a wall-clock deadline at the configured request timeout, so a
live, visibly-thinking stream was killed mid-thought with 'AI response
timed out before completion' (#1576, second symptom).

The timeout's job is stall detection, not turn budgeting. It now bounds
only how long Pulse waits for the stream to start: the response-header
wait (Ollama holds headers while a cold model loads) and the first-chunk
wait both honor the configured timeout, completing what 1c0648451
started. Once deltas flow, the 12s inter-chunk stall bound and caller
cancellation are the only limits, matching the native Ollama provider's
long-standing design.

Verified live against Ollama 0.31.1 / qwen3:8b: with a 15s configured
timeout, a turn streaming 10k chars of reasoning over 4m17s completes
and delivers the answer; previously it died at the timeout.
2026-07-14 11:57:26 +01:00
rcourtman 99c2ef22a6 Govern commercial offer and lifecycle 2026-07-14 11:54:22 +01:00
rcourtman 5d4f51c027 Key TrueNAS systems by configured connection, not reported hostname
Two TrueNAS systems that report the same hostname collapsed into one
flapping resource (#1573, #1575): systemSourceID keyed the system by the
snapshot-reported hostname, every child pool/dataset/app/VM/share/disk
was scoped under it, and the client minted the system's machine key from
the DMI serial with a hostname fallback, so serial-less systems sharing
a hostname (and DR clones sharing a serial) also fully merged in the
identity matcher.

The system source ID now scopes to the connection ID the poller passes
through NewLiveProviderForConnection; the hostname arm survives only for
fixture snapshots, which carry no connection. The ingest identity drops
the machine key entirely (DR clones share DMI serials, and vendor
placeholder serials collide across unrelated machines), the client no
longer falls back to the hostname for MachineID, and ingest skips
identity-pin completion for SourceTrueNAS so a stale pre-fix pin or a
same-named agent host's pin cannot lend the system a machine key and
re-merge what connection scoping keeps apart. Agent.AgentID and the
native metric history keys follow the source ID minus its system:
prefix, so BuildMetricsTarget keeps resolving one series.

Rows minted under the retired hostname-keyed derivation re-key once via
record-declared succession: records name their old canonical IDs in
IngestRecord.SupersededCanonicalIDs and IngestRecords applies the
existing ApplyCanonicalIDSuccessions semantics (operator state and
action audits re-key, the superseded pin drops, never while the old ID
still belongs to a live resource, journal rows are never rewritten).
Alert identities and persisted metric series under old child IDs are
not re-keyed: active alerts re-arm under the new IDs and TrueNAS host
charts are backed by native read-through history.
2026-07-14 11:51:55 +01:00
rcourtman 52469f5cbc Surface native Ollama thinking tokens instead of silently dropping them
Ollama's /api/chat returns reasoning in message.thinking, but the native
provider had no field for it: thinking models like qwen3 (the quickstart
default) showed dead air in the Assistant drawer for the whole reasoning
phase. Decode the field, stream it as thinking events (matching the
OpenAI provider's reasoning handling), carry it on the non-streaming
response as ReasoningContent, round-trip prior-turn reasoning on
assistant history messages, and make SupportsThinking tell the truth.

Verified live against Ollama 0.31.1 with qwen3:8b: first stream event is
now thinking (1137 thinking chunks before content), and non-streaming
Chat returns the reasoning text.
2026-07-14 11:50:01 +01:00
rcourtman 848b4d5038 Preserve customer data across plan downgrades 2026-07-14 11:47:25 +01:00
rcourtman caa822ed92 Keep the drawer Access section in one place with an always-visible open link
In row drawers the Access disclosure sat collapsed at the very bottom, but
expanding it re-rendered the section at the top — clicking "Show access"
teleported the content away from the user, and reaching a container's web
interface meant scrolling the whole drawer down and back up (#1556).
Promote the section whenever it exists so it renders in one stable spot at
the top, and surface the saved URL as a clickable link in the disclosure
header so opening the web interface requires no expansion at all.

Refs #1556
2026-07-14 10:51:13 +01:00
rcourtman 2c6f96513b Key the resource drawer's container web-interface URL by stable identity
A URL saved from the Docker tab's container drawer went to the docker
store under the runtime container key — the lowest-priority key in the
unified customUrl projection, and one that goes stale the moment the
container is recreated (e.g. by an update). Any stable record, including
an intentionally empty cleared one, masked it forever, so the saved URL
never turned the container name into a table link even after a refresh
(#1556). The Workloads drawer was already fixed to use the stable
host-plus-container-name guest key; align the resource drawer on the
same identity.

Refs #1556
2026-07-14 10:46:07 +01:00
rcourtman 18b5b1dd58 Make web-interface name links look like links everywhere
The Proxmox nodes table styled its PVE UI link in the plain text color, so
a working link was indistinguishable from a label (#1556). The workloads
node group headers and both alerts-surface name links had the same drift.
Extract the canonical blue link classes into WEB_INTERFACE_LINK_COLOR_CLASS
on WebInterfaceNameLink and compose it at every call site.

Refs #1556
2026-07-14 10:45:13 +01:00
rcourtman 12e6b74ae2 Document the -1 disk usage sentinel in the API reference
Guest disk percentages report -1 when a VM is stopped or its guest
agent is unavailable (issue #1569); consumers were treating it as a
real percentage.
2026-07-14 09:46:12 +01:00
rcourtman 1c06484512 Honor the configured AI timeout while waiting for the first stream chunk
The OpenAI-compatible stream reader bounded every chunk wait at 12s.
Local backends (LM Studio, llama.cpp) legitimately spend minutes on
prompt processing before the first SSE chunk, so raising the provider
timeout in Settings changed nothing and Pulse dropped the stream with
'AI response timed out' (discussion #1571). The wait for first bytes now
uses the configured request timeout; the 12s bound still applies to
inter-chunk gaps once the stream is flowing.
2026-07-14 09:44:22 +01:00
rcourtman b3ae6f72b6 Recognize OIDC sessions that carry no refresh token
A provider that issues no refresh token (offline_access not requested,
e.g. default Authelia) left session.OIDCIssuer unset, so
/api/security/status reported an empty ssoSessionUsername and
X-Auth-Method fell back to plain session. The frontend then skipped the
SSO fast path, and the pre-auth bootstrap short-circuit pinned the user
to the login page after ?oidc=success before the /api/state probe could
run (issue #1574). Stamp issuer/client on every OIDC session (refresh
stays gated on the token being present) and let a completed SSO
callback bypass the bootstrap short-circuit.
2026-07-14 09:42:27 +01:00
rcourtman 58d533a524 Stop emitting no-op docker.updateStatus change rows
dockerUpdateStatusChanged compared the whole DockerUpdateStatusMeta with
reflect.DeepEqual, so the LastChecked stamp refreshed by every periodic
update check emitted a resource_changes row per container per cycle and
flooded unified_resources.db (discussion #1577, same class as #1496).
Only availability, digests, and error text count as change now.
2026-07-14 09:38:13 +01:00
rcourtman d746731c75 Dispatch relay proxy requests to the local API in-process
The relay client's HTTP proxy dialed http://127.0.0.1:<FrontendPort> for
every proxied mobile request. With HTTPS_ENABLED the main listener serves
TLS on that port, so Go answered each plaintext dial with a bare
"Client sent an HTTP request to an HTTPS server" 400 - breaking Remote
Access backlog sync (alerts/approvals) on every HTTPS-enabled instance.
A non-loopback BIND_ADDRESS broke the same dial outright.

Route proxied requests through the router's own handler chain in-process
instead, via a streaming-capable RoundTripper (pipe-backed, SSE flush,
panic recovery, loopback RemoteAddr for address-keyed middleware). The
listener's scheme and bind address no longer matter, and the request
traverses exactly the middleware the real listener serves.

Reported by Johannes Strasser (Remote Access thread, 2026-07-14).
2026-07-14 09:26:41 +01:00
rcourtman 1054f28fe8 test(coverage): action audit canonicalization branch coverage
Cover the uncovered branches of migrateActionAuditCanonicalization (store.go):
idempotent no-op skip + len(updates)==0 early return, terminal-state exclusion
(expired not in the WHERE clause), mixed-batch skip-vs-update partition, and the
un-normalizable strict-error WARN branch (with an in-test honesty guard that
fails if the fixture stops reaching the strict path). The shipped repro test
covers only the happy path. New _test.go only; no source or existing tests
touched. Adds 4 tests; go test/vet/gofmt green.
2026-07-14 05:55:50 +01:00