Commit Graph

2185 Commits

Author SHA1 Message Date
pulse-triage[bot] 9ee4dab97b Reproduce destructive acceptance of incomplete configuration backups (#3240)
**Reproduce destructive acceptance of incomplete configuration backups**

Assert that authenticated null, incomplete and mistyped backup envelopes
leave persisted settings, credentials, metadata and transaction
artifacts unchanged. These controls are expected to fail before
validating the archive shape.

**Reject incomplete configuration backups before replacement**

Require the five sections emitted by every supported archive before
starting a transaction. Preserve null webhook inventories, deliberate
empty configurations, legacy optional fields and best-effort forward
compatibility. Exercise encrypted rejected imports, byte-identical
persistent state, unchanged runtime, failure audits and legacy recovery.

Contract-Neutral: Rejects incomplete non-backup payloads before the
existing replacement transaction; emitted archive formats, supported
complete legacy backups, API response shape and tenant authority remain
unchanged.

**Exercise backup validation without bypassing empty-node protection**

The initial control is already protected when saved Proxmox nodes block
an empty replacement. Model an installation with no Proxmox nodes, where
invalid envelopes can reach the settings and credential writes. Keep the
existing empty-node safeguard intact and use that legitimate empty
inventory for legacy compatibility controls.

---
Fleet candidate `20261025T050006Z-core-runtime` from the `core-runtime`
lane. Reviewed tip `38a8b01ca3cd72180dcfd2a680f08f7cb398e8ca` on `main`.

[How Pulse handles
triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
2026-10-25 06:25:00 +00:00
pulse-triage[bot] d051c39bca Keep unavailable VM inventory out of successful downloads (#3239)
**Keep unavailable VM inventory out of successful downloads**

Reject missing selected-organization inventory with non-cacheable
structured guidance, matching saved inventory reports. Preserve genuine
empty fleets and populated CSV exports, and cover the existing consuming
warning path.

Contract-Neutral: Restores the existing organization-bound inventory
contract: unavailable source data is not a successful empty fleet;
schemas, CSV columns, routes, authorization and scheduling are
unchanged.

**Format the inventory availability regression**

Apply the locked formatter to the new consumer-side regression; product
source and test assertions are unchanged.

**Keep the inventory CSV contract fixture available**

Use an available empty organization for the stable CSV-header contract.
An absent monitor is no longer a successful inventory source; header,
filename and column assertions are unchanged.

---
Fleet candidate `20261025T044640Z-web-product` from the `web-product`
lane. Reviewed tip `78397cf67eb78cc444e80efe5ad56e1b6d826e66` on `main`.

[How Pulse handles
triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
2026-10-25 05:53:53 +00:00
pulse-triage[bot] cc105e261b Reject incomplete configuration backups before replacement
Require the five sections emitted by every supported archive before starting a transaction. Preserve null webhook inventories, deliberate empty configurations, legacy optional fields and best-effort forward compatibility. Exercise encrypted rejected imports, byte-identical persistent state, unchanged runtime, failure audits and legacy recovery.

Contract-Neutral: Rejects incomplete non-backup payloads before the existing replacement transaction; emitted archive formats, supported complete legacy backups, API response shape and tenant authority remain unchanged.
Change-source: pulse-maintainer
2026-10-25 05:04:14 +00:00
pulse-triage[bot] 78397cf67e Keep the inventory CSV contract fixture available
Use an available empty organization for the stable CSV-header contract. An absent monitor is no longer a successful inventory source; header, filename and column assertions are unchanged.

Change-source: pulse-maintainer
2026-10-25 05:00:27 +00:00
pulse-triage[bot] c809d3c93b Reproduce tenant configuration import runtime contamination (#3238)
**Reproduce tenant configuration import runtime contamination**

Exercise a complete synthetic archive through the authorised router with
distinct default and tenant runtimes, persisted credentials and
consuming settings readback. Disabled nodes prevent monitored-workload
traffic. The new control is expected to fail before the scoped runtime
repair.

**Apply configuration imports only to the selected tenant runtime**

Reload non-default organisations through their own persisted runtime
rather than copying the default config and restarting shared monitoring.
Use the same scoped loader at cold start, retain process environment
locks and instance identity, and fail closed when a scoped apply is
unavailable. Keep the default instance reload and existing archive/auth
contracts.

Contract-Neutral: Restores the existing selected-organisation isolation
and recovery contract without changing archive formats, public API
fields, authorization or guest/lifecycle guarantees.

**Compare detached runtime snapshots consistently in import regression**

Config.DeepCopy normalises empty discovery slices, so compare detached
snapshots on both sides rather than mistaking nil-versus-empty clone
representation for a non-target runtime mutation. The separate parent
controls still reproduce default settings, paths and credentials in the
selected tenant and the shared reload call.

**Verify imported tenant connections through the consuming node API**

Use the actual organisation-scoped node route for access and
rendered-field readback, including stored-credential redaction and TLS
verification. Keep the separate instance-wide system-settings surface
unchanged; internal runtime settings are not a substitute for its public
route.

**Preserve instance-wide settings during tenant recovery**

The actual system-settings route deliberately owns instance-wide policy.
Keep those effective values and process locks in the tenant runtime;
restore only scoped nodes, tokens and storage ownership. This removes an
unnecessary settings overlay and keeps the real scoped node consumer as
the recovery proof.

Contract-Neutral: Preserves existing instance-wide settings ownership
while restoring the existing tenant resource, credential and storage
isolation contract; no public schema, authority or lifecycle delta.

**Cover scoped token read failure and distinct instance policy**

Pin a non-default instance cadence and origin, and reject an unreadable
tenant token inventory instead of borrowing the default credential set.

**Use detached snapshots for loader non-mutation assertion**

The token clone normalises legacy empty scope metadata, just as
discovery cloning normalises empty slices. Compare the default snapshots
on both sides, retaining explicit path, identity, policy, token and
alias checks.

**Revalidate tenant credentials against their live scoped runtime**

A valid tenant token passed auth-context extraction but failed protected
routes because checkAuth checked only the global inventory. Retain the
middleware-selected tenant inventory owner privately and revalidate the
actual presented token against its live cached config at route
enforcement. Keep global fallback, explicit-credential priority, tenant
binding and scope enforcement; reject unvalidated context claims,
revocation and stale runtime owners.

Contract-Neutral: Repairs the existing tenant-token authentication path
and credential precedence without changing public fields, supported
authorities, scope grants or organisation membership rules.

**Keep tenant authentication bound to the current runtime owner**

Use the configuration handler's locked monitor-manager reference for
auth-context extraction and route revalidation, rather than capturing
the manager that an instance reload replaces. Add synthetic
old/new-owner, between-check replacement and removed-reference controls
without exercising native reload or altering membership/scope rules.

Contract-Neutral: Maintains the existing current-owner credential
validation and tenant-isolation invariants across monitor replacement;
no public fields, scope grants, membership or reload policy change.

**Give invalid-bearer auth controls their own session stores**

The focused auth batch panicked before reaching request enforcement
because these two controls relied on another test to initialise the
global session store. Initialise real temporary persistence in each
fixture and own its shutdown, without weakening the invalid-credential
assertions or changing runtime bootstrap.

**Model resolved tenant context in route-auth unit controls**

The full consuming router import now passes. Direct auth controls must
also supply TenantMiddleware's resolved organisation before testing
route enforcement; missing context correctly denotes the default scope.
Keep the positive, revocation, replacement and forged-context assertions
without relaxing runtime validation.

**Cover concurrent tenant route authentication and token metadata**

Exercise four concurrent callers through route revalidation and
middleware with cached synthetic tenant owners. Preserve token identity,
organisation, owner, scope checks and live usage metadata, with a
global-inventory control and no monitored runtime.

**Serialize tenant API token validation and usage metadata**

Use the same exclusive config writer for tenant and global token
normalization and LastUsedAt updates. Clone the validated record before
releasing it, then recheck the cached tenant owner across the lock
transition. Keep live revocation, organisation, owner and scope
enforcement and explicit credential precedence.

Contract-Neutral: Restores the existing concurrency, live-owner and
tenant-isolation guarantees without changing public fields, token
authority, organisation membership, archive formats or reload policy.

**Keep tenant storage guard aligned with the scoped runtime loader**

The reviewed recovery candidate moves tenant setup into
loadTenantRuntimeConfig. The existing guard still demands an inline
MetricsDBPath clear, so containing rest-1 CI fails despite the loader
retaining the isolation boundary. Check the non-default loader wiring
and execute the real loader for two tenants with and without the process
override, preserving default paths and instance rollup policy without
starting a monitor or database.

Contract-Neutral: Test-only repair follows the existing scoped loader
and executes its unchanged storage isolation contract; no runtime or
public contract changes.

---
Fleet candidate `20261025T041413Z-core-runtime` from the `core-runtime`
lane. Reviewed tip `c5d38323f42c77c082f4566f6d7666a45c6d1b70` on `main`.

[How Pulse handles
triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
2026-10-25 04:57:33 +00:00
pulse-triage[bot] 58cadd5c5a Keep unavailable VM inventory out of successful downloads
Reject missing selected-organization inventory with non-cacheable structured guidance, matching saved inventory reports. Preserve genuine empty fleets and populated CSV exports, and cover the existing consuming warning path.

Contract-Neutral: Restores the existing organization-bound inventory contract: unavailable source data is not a successful empty fleet; schemas, CSV columns, routes, authorization and scheduling are unchanged.
Change-source: pulse-maintainer
2026-10-25 04:51:55 +00:00
pulse-triage[bot] f14f908387 Compose scoped configuration recovery with current main
Preserve reviewed scoped import and credential race repairs while addressing the returned backend guard failure on current main.

Change-source: pulse-maintainer
2026-10-25 04:15:21 +00:00
pulse-triage[bot] 477be1e7c3 Match CSV export safety assertions to canonical runtime identity
Use the real registry's canonical VM ID and normalized node name in the download/saved-schedule regression. The first executable control exposed incorrect fixture expectations; production CSV escaping is unchanged.

Change-source: pulse-maintainer
2026-10-25 03:57:06 +00:00
pulse-triage[bot] 63d973f40c Keep report CSV text from becoming spreadsheet formulas
Escape formula-like external text in single-resource, fleet and VM inventory CSV exports, including saved inventory reports. Preserve field boundaries, numeric measurements, ordinary text and the runtime inventory.

Contract-Neutral: Restores safe literal-text handling in existing report CSV cells; columns, numeric values, endpoints, authorization, archive formats and scheduling contracts are unchanged.
Change-source: pulse-maintainer
2026-10-25 03:45:08 +00:00
pulse-triage[bot] 11f8ea81c5 Return real not-found responses for missing frontend assets (#3231)
Keep SPA fallback for client routes, including extensionless
documentation viewers, but not missing document files, bundles or other
static resources. Missing resources are not cached, so a later
installation can supply them without a stale negative response.

Contract-Neutral: Correct static-resource failure responses at the
existing SPA boundary; no API, authentication, data schema or supported
client route changes.

---
Fleet candidate `20261025T021357Z-web-product` from the `web-product`
lane. Reviewed tip `d0c3a7fadf683cd0669025200a849611aa6068b7` on `main`.

[How Pulse handles
triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
2026-10-25 03:20:07 +00:00
pulse-triage[bot] 6e156073de Serialize tenant API token validation and usage metadata
Use the same exclusive config writer for tenant and global token normalization and LastUsedAt updates. Clone the validated record before releasing it, then recheck the cached tenant owner across the lock transition. Keep live revocation, organisation, owner and scope enforcement and explicit credential precedence.

Contract-Neutral: Restores the existing concurrency, live-owner and tenant-isolation guarantees without changing public fields, token authority, organisation membership, archive formats or reload policy.
Change-source: pulse-maintainer
2026-10-25 02:56:30 +00:00
pulse-triage[bot] a4646caec9 Cover concurrent tenant route authentication and token metadata
Exercise four concurrent callers through route revalidation and middleware with cached synthetic tenant owners. Preserve token identity, organisation, owner, scope checks and live usage metadata, with a global-inventory control and no monitored runtime.

Change-source: pulse-maintainer
2026-10-25 02:53:06 +00:00
pulse-triage[bot] d9f33a50e5 Compose reviewed scoped configuration recovery for concurrency repair
Change-source: pulse-maintainer
2026-10-25 02:50:55 +00:00
pulse-triage[bot] d0c3a7fadf Return real not-found responses for missing frontend assets
Keep SPA fallback for client routes, including extensionless documentation viewers, but not missing document files, bundles or other static resources. Missing resources are not cached, so a later installation can supply them without a stale negative response.

Contract-Neutral: Correct static-resource failure responses at the existing SPA boundary; no API, authentication, data schema or supported client route changes.
Change-source: pulse-maintainer
2026-10-25 02:18:32 +00:00
pulse-triage[bot] 13100030d1 Model resolved tenant context in route-auth unit controls
The full consuming router import now passes. Direct auth controls must also supply TenantMiddleware's resolved organisation before testing route enforcement; missing context correctly denotes the default scope. Keep the positive, revocation, replacement and forged-context assertions without relaxing runtime validation.

Change-source: pulse-maintainer
2026-10-25 02:11:44 +00:00
pulse-triage[bot] 7cbeb99c7d Give invalid-bearer auth controls their own session stores
The focused auth batch panicked before reaching request enforcement because these two controls relied on another test to initialise the global session store. Initialise real temporary persistence in each fixture and own its shutdown, without weakening the invalid-credential assertions or changing runtime bootstrap.

Change-source: pulse-maintainer
2026-10-25 01:54:34 +00:00
pulse-triage[bot] 3b6483fce6 Keep tenant authentication bound to the current runtime owner
Use the configuration handler's locked monitor-manager reference for auth-context extraction and route revalidation, rather than capturing the manager that an instance reload replaces. Add synthetic old/new-owner, between-check replacement and removed-reference controls without exercising native reload or altering membership/scope rules.

Contract-Neutral: Maintains the existing current-owner credential validation and tenant-isolation invariants across monitor replacement; no public fields, scope grants, membership or reload policy change.
Change-source: pulse-maintainer
2026-10-25 01:43:58 +00:00
pulse-triage[bot] 31db0c8f38 Revalidate tenant credentials against their live scoped runtime
A valid tenant token passed auth-context extraction but failed protected routes because checkAuth checked only the global inventory. Retain the middleware-selected tenant inventory owner privately and revalidate the actual presented token against its live cached config at route enforcement. Keep global fallback, explicit-credential priority, tenant binding and scope enforcement; reject unvalidated context claims, revocation and stale runtime owners.

Contract-Neutral: Repairs the existing tenant-token authentication path and credential precedence without changing public fields, supported authorities, scope grants or organisation membership rules.
Change-source: pulse-maintainer
2026-10-25 01:39:41 +00:00
pulse-triage[bot] 3f8d5cb9e8 Preserve instance-wide settings during tenant recovery
The actual system-settings route deliberately owns instance-wide policy. Keep those effective values and process locks in the tenant runtime; restore only scoped nodes, tokens and storage ownership. This removes an unnecessary settings overlay and keeps the real scoped node consumer as the recovery proof.

Contract-Neutral: Preserves existing instance-wide settings ownership while restoring the existing tenant resource, credential and storage isolation contract; no public schema, authority or lifecycle delta.
Change-source: pulse-maintainer
2026-10-25 01:06:18 +00:00
pulse-triage[bot] 211b8bb716 Verify imported tenant connections through the consuming node API
Use the actual organisation-scoped node route for access and rendered-field readback, including stored-credential redaction and TLS verification. Keep the separate instance-wide system-settings surface unchanged; internal runtime settings are not a substitute for its public route.

Change-source: pulse-maintainer
2026-10-25 01:03:50 +00:00
pulse-triage[bot] 336c7c68ae Compare detached runtime snapshots consistently in import regression
Config.DeepCopy normalises empty discovery slices, so compare detached snapshots on both sides rather than mistaking nil-versus-empty clone representation for a non-target runtime mutation. The separate parent controls still reproduce default settings, paths and credentials in the selected tenant and the shared reload call.

Change-source: pulse-maintainer
2026-10-25 01:59:20 +01:00
pulse-triage[bot] d141bd5404 Apply configuration imports only to the selected tenant runtime
Reload non-default organisations through their own persisted runtime rather than copying the default config and restarting shared monitoring. Use the same scoped loader at cold start, retain process environment locks and instance identity, and fail closed when a scoped apply is unavailable. Keep the default instance reload and existing archive/auth contracts.

Contract-Neutral: Restores the existing selected-organisation isolation and recovery contract without changing archive formats, public API fields, authorization or guest/lifecycle guarantees.
Change-source: pulse-maintainer
2026-10-25 01:55:50 +01:00
pulse-triage[bot] 49074d0087 Reproduce tenant configuration import runtime contamination
Exercise a complete synthetic archive through the authorised router with distinct default and tenant runtimes, persisted credentials and consuming settings readback. Disabled nodes prevent monitored-workload traffic. The new control is expected to fail before the scoped runtime repair.

Change-source: pulse-maintainer
2026-10-25 01:49:25 +01:00
rcourtman c0c54de765 fix(e2e): await authenticated bootstrap in cookie-only fixtures
Fresh password login must await the mounted workspace before a caller
opens an onboarding route. Own and revoke the bootstrap regression token
without replacing the primary credential, and bootstrap update API tests
once per worker when CI provides cookies alone.

Opt the synthetic shared-IP stack into a finite development update budget
without changing production, authentication or configuration limits.

Observe gated panels through actual gate readiness. Keep the fixed 24h
growth assertion on its native header and select the retained LXC drawer
with the fixture's canonical resource identity. Integrate the published
filesystem-observation and issue-topic repairs before qualification.
2026-10-24 23:50:14 +01:00
pulse-triage[bot] c93757fb4b Keep absent host disk readings out of live charts and history
Share canonical disk admission with host-agent recording and fallback, preserve measured zero and recorded observations, and check both History and summary routes.

Change-source: pulse-maintainer
Contract-Neutral: Restore existing canonical disk-presence semantics; no new API, authentication, identity, persistence schema or retention contract.
2026-10-24 19:59:18 +01:00
rcourtman bc5491b6d1 Withdraw cached live history when agent reporting stops
An open drawer could retain a current-only reply after its agent entered
the first-report wait. Withdraw live snapshots immediately and refresh on
reporting transitions while preserving real stored observations.

Integrate published main while retaining the webhook acknowledgement and
chart optimizations. Keep guest metric admission under provider ownership.

Refs #2980
2026-10-24 17:51:28 +01:00
rcourtman ddaedc8c78 Keep unreported chart fallbacks empty after lease expiry
An expired saved enrollment still has no current telemetry. Keep summary
charts from turning that deadline into zero-valued live samples, while
preserving real stored history. Cover both sides of the reporting lease
and align the remaining pending-label assertions with the shared wording.

Refs #2980
2026-10-24 17:23:25 +01:00
rcourtman 5f0037fc25 Show agents awaiting a report after server restart
Keep retained enrollments unknown while their accepted-report lease is
current, then show offline if no fresh report arrives. Preserve historical
timestamps, removals and report ordering without manufacturing telemetry.
Use the shared first-report state in Machines, resource details and Settings.

Refs #2980
2026-10-24 16:45:39 +01:00
pulse-triage[bot] 94a50e75b0 Acknowledge persisted webhook deletion to the destination editor
Return the explicit success boolean the existing editor requires after durable and live removal, while retaining the legacy status field. Pin the response contract so a successful deletion is not displayed as failed and does not encourage blind retries.

Change-source: pulse-maintainer
2026-10-24 16:21:39 +01:00
pulse-triage[bot] bf9f0ed982 Cap retained chart observations before allocating wire points
Share the existing time-nearest selection by original indices, so chart conversion materialises only selected observations. Preserve exact timestamps, values, tie breaks, degenerate spans and oldest-source metadata. Check the frozen former sampler across 1,440 cases and the exported fallback contract.

Change-source: pulse-maintainer
Contract-Neutral: Exact chart observations, wire schema, sampling policy and guest/storage lifecycle contracts are unchanged; only index selection and allocation order change.
2026-10-24 14:08:51 +01:00
pulse-triage[bot] fbe410e244 Compose chart SLO repair without dropping current administrative guarantees
Resolve adjacent contract append conflicts by keeping bounded hosted-agent classification in its chart section and preserving all newer SSO, RBAC and uninstall guarantees. Runtime, workflow and synthetic chart controls merge without conflict; no latency budget or customer journey changes.

Change-source: pulse-maintainer
2026-10-24 11:42:23 +01:00
pulse-triage[bot] 7c0fc7f79e Reject competing audit webhook destination fields (#3192)
**Reject competing audit webhook destination fields**

Parse one bounded update object without discarding repeated urls
members, including escaped and case-folded aliases. Preserve deliberate
clears, singleton aliases and unknown fields; reject ambiguous or
incomplete selections before validation and mutation.

**Bind audit guidance smoke checks to the unambiguous decoder**

The complete-selection admission moved out of the inline struct. Retain
the route/access and organisation assertions, and require decoding
before URL validation instead of the obsolete struct spelling.

Contract-Neutral: Align a static documentation check with the reviewed
decoder; runtime and published contracts are unchanged.

**Reject competing public SSO and recovery request fields**

Validate complete bounded admin objects before decoding or applying
them. Preserve singleton aliases, unknown fields, case-sensitive role
map keys, explicit access clears, saved secrets and existing tenant and
one-use token boundaries. Add public producer, persistence and API
contract controls.

**Preserve published audit smoke bindings in the SSO candidate**

Resolve the overlapping audit-doc test correction against published main
52457777c4 by retaining its stronger
bounded decoder and mutation-order assertions. No runtime or frontend
source changes; the reviewed SSO and recovery proof remains applicable.

Contract-Neutral: Integration-only static test reconciliation; API,
runtime, frontend and documented contracts are unchanged.

---
Fleet candidate `20261024T084021Z-core-runtime` from the `core-runtime`
lane. Reviewed tip `7a50adc5ac6924829c1e871c638db9b461b40011` on `main`.

[How Pulse handles
triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
2026-10-24 10:17:29 +00:00
pulse-triage[bot] b25c22c00f Keep bounded hosted provisioning overhead in chart SLO verdicts
Retained hosted samples include both an over-budget failure and an under-budget pass with one provisioning agent occupying about one logical CPU. Attribute only that bounded Actions overhead; retain raw CPU and absolute 90 ms latency, and keep other or unattributed CPU, guest, steal and swap invalidating. Exercise the real CLI and workflow shells with adverse controls; no timed workload or release acceptance is claimed.

Change-source: pulse-maintainer
2026-10-24 10:48:40 +01:00
pulse-triage[bot] 52457777c4 Reject competing audit webhook destination fields (#3190)
**Reject competing audit webhook destination fields**

Parse one bounded update object without discarding repeated urls
members, including escaped and case-folded aliases. Preserve deliberate
clears, singleton aliases and unknown fields; reject ambiguous or
incomplete selections before validation and mutation.

**Bind audit help checks to the bounded selection decoder**

Repair the containing CI failure from PR 3186 without restoring
last-member-wins parsing. Keep the explicit array, request bound, URL
validation before mutation and organisation-scoped replacement checks;
runtime selection semantics remain covered by the existing handler
tests.

Contract-Neutral: Static guidance/source-binding test repair only; the
composed audit runtime and documented API, access, storage and delivery
contracts are unchanged.

---
Fleet candidate `20261024T082008Z-delivery-trust` from the
`delivery-trust` lane. Reviewed tip
`58e46e21d7c9bd265e88485d26bfed38e03fb31e` on `main`.

[How Pulse handles
triage](https://github.com/rcourtman/Pulse/blob/main/docs/AI_TRANSPARENCY.md)
2026-10-24 09:34:36 +00:00
pulse-triage[bot] 3d05fd3cda Reject competing public SSO and recovery request fields
Validate complete bounded admin objects before decoding or applying them. Preserve singleton aliases, unknown fields, case-sensitive role map keys, explicit access clears, saved secrets and existing tenant and one-use token boundaries. Add public producer, persistence and API contract controls.

Change-source: pulse-maintainer
2026-10-24 09:48:15 +01:00
rcourtman 1120502a1d Name Patrol inbox alert subjects by their unified resource
The Patrol inbox passed each alert's own resource reference through as the
item subject: docker:<host>/<container> for a Docker container,
<instance>:<node>:<vmid> for a guest, agent:<host>/disk:<label> for a
filesystem. Patrol findings, protection postures and resource actions carry
the unified resource ID instead, so "Browse Patrol findings for this
resource" opened an empty list, postures never attached, and the alert
detail said Patrol had raised no finding while it had one on that exact
container.

The attention projection now resolves each record's subject through the
org registry's alert reference resolver and names the unified resource. The
operational record keeps the alert's reference, and a reference inventory
cannot bind stays as before. The resolver gains Docker container references
with a complete ID, the key RecordChange already joins their history to, so
container alerts resolve like every other kind and alert-triggered Patrol
scopes for them keep the triggering alert. It also reads source targets
from the registry's view cache rather than scanning every mapping per
reference, which a page of alerts would otherwise multiply.

The alert-mirror reconcile compared the same mismatched strings and never
folded a finding under its alert; it now matches either identity. A
filesystem, sensor or ZFS pool alert resolves only to the owner its siblings
share, so the resolver marks it as a component and the reconcile does not
match on that owner. The inbox links a finding to the exact alert it
restates by the stamped alert ID, not by resource and kind, which several
alerts on one host share. The restart offer checks evidence against the
record's own reference, since the item subject is now the unified container
its registry lookup needs.

When no finding restates the alert but Patrol has open findings on the same
resource, the detail names them instead of saying Patrol raised nothing,
and the browse button appears only when there is something to browse.
2026-10-24 09:46:33 +01:00
pulse-triage[bot] 3883796a9b Reject competing audit webhook destination fields
Parse one bounded update object without discarding repeated urls members, including escaped and case-folded aliases. Preserve deliberate clears, singleton aliases and unknown fields; reject ambiguous or incomplete selections before validation and mutation.

Change-source: pulse-maintainer
2026-10-24 08:04:34 +01:00
pulse-triage[bot] f98c1eae78 Compare tenant audit mutations with each request starting state
The second tenant control must preserve the first tenant deliberate clear, not require that the earlier update never happened. Capture both update counters before the rejected request and require them to stay unchanged. No production behaviour changes.

Change-source: pulse-maintainer
Contract-Neutral: Correct the synthetic tenant control baseline; production audit handlers and contracts are unchanged.
2026-10-24 05:30:25 +01:00
pulse-triage[bot] 433f617db2 Preserve explicit audit selections before reads and webhook updates
Reject damaged query encoding and repeated singleton selectors in the canonical list, export and summary handlers before reading events. Require an explicit webhook urls array so absent selections cannot silently disable forwarding; intentional empty arrays, URL validation and tenant boundaries remain intact. Add executable handler/exporter and mutation preservation controls and update the owning contracts.

Change-source: pulse-maintainer
2026-10-24 05:21:01 +01:00
pulse-triage[bot] 7e7cea87a6 Reconcile chart reporting with current Patrol contracts
Compose retained 8478eac3 additively on current main. Preserve both independently appended contract sections at each of the three documentation conflicts; workflow, registered executable controls and classifier stay unchanged.

Change-source: pulse-maintainer
2026-10-24 02:02:24 +01:00
pulse-triage[bot] 8478eac3ef Complete credited chart evidence reporting on current main
Isolate the retained PR3095 completion (7f4ec192 and 93d87118) without importing unrelated history. Keep the exact proposed Python classifier and maintained chart shells, real CLI/workflow controls and dependent contracts. Inconclusive capacity never establishes the 90 ms budget; child, cleanup and malformed-proof failures remain errors.

Source-PR: https://github.com/rcourtman/Pulse/pull/3095
Source-Commit: b31fabd778
Co-authored-by: rcourtman <8825017+rcourtman@users.noreply.github.com>
Change-source: pulse-maintainer
2026-10-24 00:26:11 +01:00
rcourtman a4bfca4d32 Serialize Patrol action projection and finalize hung runs
Three gaps remained in how Pulse projects a Patrol investigation's
action and closes its runs.

Projections of one investigation could interleave: the transition
callback, an investigation read and a run's close each read the action
audit and applied it, so one that read an older state could land after
one that read a newer state, and an approval could land between a
run's reconciliation and its finalization. All of them now read and
apply under the investigation's lock, and a close finalizes before it
releases it.

The projection replaced the whole session through Update, so it could
undo run progress the orchestrator saved in between. It now writes
only the changed action and outcome when the store offers SetAction
(aicontracts.InvestigationActionStore), and the reference it writes
carries the broker's canonical result and verification status and the
model's causal attribution, which the audit does not hold.

A run that hung past its limit without closing was recovered by the
stuck-run sweep but never finalized. The sweep now finalizes it with a
copy of its session marked failed, leaving the orchestrator's session
as it is. A later real close is finalized instead, and a refresh no
longer reopens a record that finalization closed.
2026-10-23 22:26:09 +01:00
rcourtman 45b5a8cde8 Finalize Patrol runs when they close, failed runs included
A Patrol run that planned a fix and then failed, normally on its final
model call, was never finalized. Core's run goroutine returned on the
error before it rebuilt the investigation record, published the
unified finding and sent the approval push, so the record kept showing
the run as running and nobody was asked to approve the queued fix.
Manual re-investigations never reached that goroutine, even when they
succeeded, and a run a restart interrupted was only marked closed.

Core now finalizes a run when its close lands on the finding or when
the stuck-run sweep recovers it with a closed session. It first
reconciles the run's action from the action audit, because the
orchestrator writes the action back as planned and an approval or
expiry during the final model call may have overtaken it. For the same
reason a close no longer moves a finding back to fix_queued from an
outcome the action reached during the run. Finalization skips a run
the finding no longer names and asks for approval once per action. An
action's outcome keeps its single push from transition reconciliation,
so a close no longer sends a second fix-completed notification.

The action inbox and decision-queue expiry sweeps also publish the
plans they expire, as a single action's expiry already was, so a
Patrol finding's queued fix moves to needs_attention when its plan
expires. Reconciliation follows refresh lineage to ignore a transition
for a plan that the investigation's current action replaced, so the
expiry of a refreshed plan cannot take the investigation back.
2026-10-23 21:15:25 +01:00
rcourtman a32356ae94 Describe the Assistant surface as Pulse Mobile chat
With the web chat window and its backend routes retired (#3126,
#3137), the pulse_assistant surface only serves Pulse Mobile's chat.
Its manifest description still called it the explanation and handoff
surface for Patrol findings, and that text is public: the docs/AI.md
overview, the MCP README surface block, and the External agents
settings panel all show it. It now names the Pulse Mobile role and says
the web chat is retired.

The browser's fallback affordances for an Assistant surface with none
declared still offered interactive questions. They now match the
backend default of tools only, so no client path advertises a question
tool nobody can answer.
2026-10-23 16:10:15 +01:00
rcourtman 070d2a0e53 Remove chat backend routes only the web chat used (#3137)
## Why

The in-app Assistant chat window was retired in #3126. Several backend
routes existed only for that window, and Pulse Mobile, the remaining
chat client, never called them. This removes them and the code that only
they reached.

## What changes

- **Routes removed:**
  - `GET /api/ai/assistant/surface-tools`
  - `POST /api/ai/workflow-prompts/render`
  - `POST /api/ai/question/{id}/answer`
- the `/summarize`, `/fork`, `/undo`, `/redo`, `/steer`, `/diff`,
`/revert` and `/unrevert` sub-resources under `/api/ai/sessions/{id}`.
These now return `404` after the existing `ai:chat` scope check.
- **Code removed behind them:**
  - the handlers and their `AIService` methods;
- the chat service's fork, undo, redo, steer, summarize, answer-question
and surface-tool methods;
- the session store's fork and turn-undo operations and the persisted
redo stack (`can_redo` leaves session summaries);
- session compaction, the agent loop's steering inbox and
question-answer wait, the `steered` message flag, and the `question` and
`steer_applied` stream events.
- **The question tool is no longer offered.** Interactive chat still
offered `pulse_question`, and Pulse Mobile has no way to answer it, so a
Mobile turn that called it waited until the request ended. Now:
  - no profile offers the tool;
- the agent loop answers any call with a refusal telling the model to
ask in its reply text, the same path Patrol runs already took;
  - the chat system prompt no longer mentions a clarification tool;
- the agent capabilities manifest's Pulse Assistant surface no longer
declares the `interactiveQuestions` affordance, so the prompt's
affordance text, the fallback governance names, and the generated
`docs/AI.md` and `cmd/pulse-mcp/README.md` surface blocks drop it. The
External agents settings panel stops showing an "Interactive questions"
affordance for Pulse Assistant. The shared `pulse_question` declaration
stays.
- **Docs and contracts:**
- `docs/API.md` lists the remaining routes under "Chat Sessions (Pulse
Mobile)", including the rename route Mobile already used.
- The ai-runtime and api-contracts rules for the removed workflows are
rewritten, and the dependent contracts carry dated notes.

## Unchanged

- `/api/ai/chat` and the session list, create, rename, delete, messages
and abort routes, which are the Pulse Mobile relay route list.
- `/api/ai/status`, which the dev health check uses.
- `/api/ai/workflow-prompts/activity`, which Patrol uses.
- Patrol investigations, provider routing, model-boundary sanitizing and
approvals.
- Old session files: they load as before and drop the redo stack and
`steered` flags on their next write.

## Not in this PR

- The Pulse Assistant surface description in the manifest, which still
calls it a handoff surface for Patrol findings. It now describes Pulse
Mobile chat and can be reworded with the surface contract when Mobile
retires.
- The browser's legacy fallback in
`frontend-modern/src/api/agentCapabilities.ts`. It still gives Pulse
Assistant `interactiveQuestions` when a manifest declares no affordances
at all. A same-version server always declares them, so that path is
unused; it changes with the surface description.
- The dead `highlight.js` chunk rule in `vite.config.ts`.

## Release note

> Removed API routes the retired in-app chat window used: Assistant
surface tools, workflow-prompt rendering, question answers, and the
session summarize, fork, undo, redo, steer, diff, revert and unrevert
routes. Pulse Mobile's chat is unaffected.
2026-10-23 14:39:19 +01:00
rcourtman dcbfd2d86d Remove chat backend routes only the web chat used
The in-app Assistant chat window was retired in #3126. Several backend
routes existed only for it, and Pulse Mobile, the remaining chat client,
never called them: assistant surface-tools, workflow-prompt render,
question answers, and the session summarize, fork, undo, redo, steer,
diff, revert and unrevert sub-resources. This removes them along with
the handlers, service and session-store methods, the persisted redo
stack, session compaction, mid-turn steering and the question-answer
wait behind them.

The question tool needed more than a route removal. Interactive chat
still offered pulse_question, and Pulse Mobile has no way to answer it,
so a Mobile turn that called it waited until the request ended. No
profile offers it now. The agentic loop answers any call with a refusal
that tells the model to ask in its reply text, the path Patrol runs
already took, and the chat prompt drops its clarification-tool advice.
The capabilities manifest's Assistant surface stops declaring
interactive questions, so the generated docs and fallback governance
names follow it.

/api/ai/chat and the session list, create, rename, delete, messages and
abort routes Pulse Mobile uses are unchanged, as is /api/ai/status. Old
session files load as before and drop the redo stack and steered flag
on their next write. docs/API.md lists the remaining chat routes, and
the subsystem contracts record the removal.
2026-10-23 14:07:20 +01:00
rcourtman d167d7f492 Count each finished Patrol investigation once in the weekly digest (#3134)
## Why

The Patrol weekly digest's **Investigated** line, on the Patrol page and
in the weekly email, was unreliable:

- It counted `investigation_outcome` lifecycle rows and read their
outcome from `metadata["outcome"]`. The only writer of those rows
(`updateInvestigationOutcomeLocked`) writes
`prev_outcome`/`next_outcome`, so every counted row was labelled
`unknown`.
- Those rows are later steps of an earlier run's fix (approved, run,
verified), not investigations. Meanwhile the `investigation_updated`
rows every run writes were ignored, apart from one `LastInvestigatedAt`
fallback count per finding.
- So two runs on one finding counted once, a fix approved after last
week's run counted as this week's investigation, and the email could
print "Investigated: 3" followed by "No issues needed a closer look."

## What changes

- `digestInvestigationRuns` counts one investigation per run that
finished in the window, from the rows the findings store already writes
for every run. That includes a run whose own fix was verified before it
closed, and a run whose status a regression cleared mid-run. A fix that
moves on afterwards relabels its run instead of counting again.
- A run that failed with no outcome is labelled `failed`, the bucket
name telemetry already uses. It sits after `timed_out` in
`PatrolDigestOutcomeOrder`.
- The summary-field fallback is removed. Every run since the v6 release
writes lifecycle rows, and the fallback could count one run in two
adjacent weeks.
- Email: `failed` reads "ended with an error". When the count is above
zero but no outcome in the breakdown has wording, the line shows the
count alone instead of "No issues needed a closer look." or the
watch-only sentence.
- `internal/ai/findings.go` is unchanged; the digest reads the rows the
store writes today.

Known limits: lifecycle rows carry no session ID, so a late end written
by an older run is read as the end of whichever run is open. That's the
store-side stale-write problem, which is being closed separately with
session checks. A run whose rows the 50-row lifecycle has already
dropped is not counted.

## Proof

- `internal/ai/patrol_digest_test.go` drives the store's own writers:
the orchestrator's start and end updates, `UpdateInvestigationOutcome`,
`ReconcileInvestigationOutcome`, `RecoverStuckInvestigation`, and
`Resolve` + `Add` for a mid-run regression. The scenario covers ten
finished runs plus one still running. The old code reports 8
investigations, 3 of them `unknown`.
- `internal/api/contract_test.go` follows the scheduled email's path:
store writers → `AISettingsHandler.BuildPatrolDigest` →
`renderPatrolDigestEmail`. The old code reports two runs as `1 unknown`.
- `internal/api/patrol_digest_email_test.go` checks the `failed` wording
and the non-zero-count guard.
- On pulse-dev, `go vet` passes for `./internal/ai/` and
`./internal/api/`, and the full `./internal/ai/` package and every
`PatrolDigest` test in `./internal/api/` pass. The new tests fail
against the previous digest and email (negative controls via
`-overlay`).

## Related

The Patrol page's This week card gets the same `failed` wording and the
same non-zero-count guard in #3132. Until that lands, a week with only
failed runs reads "No issues needed a closer look." on the card. That's
what those runs already showed as `unknown`, so it isn't a regression.
2026-10-23 13:16:09 +01:00
rcourtman b633e54105 Count each finished Patrol investigation once in the weekly digest
The weekly digest counted investigation_outcome lifecycle rows and read
their outcome from a metadata["outcome"] key the findings store never
writes, so every one of them showed as "unknown". Those rows are later
steps of an earlier run's fix (approved, run or verified), not new
investigations, while the investigation_updated rows every run writes
were ignored apart from one fallback count per finding. Two runs on one
finding counted once, a fix approved after last week's run counted as
this week's investigation, and the email could print "No issues needed
a closer look." under a non-zero count.

The digest now counts one investigation per run that finished in the
window, from the rows the store already writes for every run, including
a run whose own fix was verified before it closed. A fix that moves on
afterwards relabels its run instead of counting again. A run that failed
with no outcome is "failed", the bucket telemetry already uses; the
email words it "ended with an error" and no longer denies a non-zero
count. The summary-field fallback is removed: every run since the v6
release writes lifecycle rows, and the fallback could count one run in
two adjacent weeks.

The tests drive the findings store's own writers rather than hand-built
rows, which is how the wrong metadata key went unnoticed.
2026-10-23 12:04:23 +01:00
rcourtman a408736bc4 Keep stale investigation runs from overwriting newer finding state
The private investigation orchestrator writes a finding's investigation
fields through orchestratorFindingsAdapter, and every write was applied
unconditionally. A run still in flight when its finding was resolved
and detected again, or when the stuck-run sweep gave it up and a retry
started another run, overwrote the newer state with its own session,
status, outcome and attempts when it finished. A stale "resolved" made
the regressed occurrence terminal, so Patrol stopped investigating it.

Closing writes now go through FindingsStore.CompleteInvestigation,
which records them only while the finding still names the run: the
session must be the finding's current one and the run must not predate
the finding's latest regression. Admission goes through
StartInvestigation, which refuses a run whose session started before
that regression, so a delayed admission cannot install an earlier
occurrence's attempt count or replace a newer run.

Both compare a run's start only with an occurrence start this process
observed: a regression, or the finding's removal by ClearAll or
Cleanup, kept unpersisted with the clock's monotonic reading for a day.
A regression an earlier process recorded predates every run of this
one, so a clock set back across a restart cannot make every new run
look stale. Records and action outcomes keep comparing with
LastRegressionAt, since they also judge sessions of earlier processes.
The adapter takes each run's start from the orchestrator's session
store, so the aicontracts boundary and the enterprise orchestrator are
unchanged.
2026-10-23 11:51:53 +01:00
rcourtman 85fb199ff5 Merge main into feat/patrol-typed-conclusions
Main gained the Assistant retirement (#3126), which also appended notes
to the agent-lifecycle, ai-runtime, api-contracts and storage-recovery
contracts. Keep both: main's notes, then this change's.
2026-10-23 09:36:06 +01:00