From 83672a00a890d016713f7c3c240fff4cdd0772ba Mon Sep 17 00:00:00 2001
From: rcourtman
Date: Sun, 19 Jul 2026 07:00:34 +0100
Subject: [PATCH] Harden Operational Trust rollout contracts
---
docs/AI.md | 7 +
docs/API.md | 24 +-
docs/OPERATIONAL_TRUST.md | 202 +++++++++
docs/UPGRADE_v6.md | 31 ++
docs/monitoring/PROMETHEUS_METRICS.md | 25 ++
.../v6/internal/subsystems/agent-lifecycle.md | 12 +
.../v6/internal/subsystems/ai-runtime.md | 11 +
.../v6/internal/subsystems/alerts.md | 18 +
.../v6/internal/subsystems/api-contracts.md | 20 +
.../subsystems/frontend-primitives.md | 12 +
.../v6/internal/subsystems/notifications.md | 12 +
.../subsystems/patrol-intelligence.md | 12 +
.../subsystems/performance-and-scalability.md | 16 +
.../internal/subsystems/security-privacy.md | 12 +
.../internal/subsystems/storage-recovery.md | 11 +
.../internal/subsystems/unified-resources.md | 11 +
.../src/api/__tests__/patrolAttention.test.ts | 39 ++
frontend-modern/src/api/patrolAttention.ts | 66 +++
.../patrol/PatrolAttentionWorkbench.tsx | 236 ++++++++++-
.../PatrolAttentionWorkbench.test.tsx | 101 +++++
internal/actionlifecycle/service.go | 10 +
internal/ai/patrol_metrics.go | 9 +
internal/ai/patrol_metrics_test.go | 30 ++
internal/alerts/active_lifecycle.go | 274 ++++++++++++
internal/alerts/operational_contract.go | 62 ++-
internal/alerts/operational_contract_test.go | 35 ++
.../alerts/operational_state_writers_test.go | 400 ++++++++++++++++++
internal/api/attention_actions.go | 16 +
internal/api/attention_actions_test.go | 28 ++
internal/api/attention_evidence.go | 113 +++++
internal/api/attention_handlers.go | 26 +-
internal/api/attention_handlers_test.go | 196 +++++++++
internal/api/attention_mutations.go | 152 +++++++
internal/api/contract_test.go | 1 +
internal/api/router.go | 6 +
internal/notifications/queue.go | 28 +-
internal/notifications/queue_test.go | 24 +-
internal/operationaltrust/metrics.go | 266 ++++++++++++
internal/operationaltrust/metrics_test.go | 77 ++++
internal/recovery/posture.go | 2 +
internal/recovery/posture_test.go | 20 +
.../availability_link_test.go | 10 +
internal/unifiedresources/registry.go | 9 +
.../relationship_presentation_test.go | 47 ++
internal/unifiedresources/relationships.go | 69 ++-
.../ai_runtime_docs_policy_test.py | 8 +
...rational-trust-attention-workbench.spec.ts | 149 ++++++-
47 files changed, 2919 insertions(+), 26 deletions(-)
create mode 100644 docs/OPERATIONAL_TRUST.md
create mode 100644 internal/alerts/operational_state_writers_test.go
create mode 100644 internal/api/attention_evidence.go
create mode 100644 internal/api/attention_mutations.go
create mode 100644 internal/operationaltrust/metrics.go
create mode 100644 internal/operationaltrust/metrics_test.go
diff --git a/docs/AI.md b/docs/AI.md
index 0db5677fc..d11ab87ad 100644
--- a/docs/AI.md
+++ b/docs/AI.md
@@ -121,6 +121,13 @@ only inside selected context and receives policy-shaped summaries and
references. Assistant can explain the item, but it cannot create lifecycle
truth, invent action authority, or hide uncertainty.
+Open items can be acknowledged directly. Suppression is temporary and requires
+an operator reason plus a bounded expiry; it never resolves the detector's
+finding. Eligible Pulse Pro Docker health items can offer a governed restart.
+That journey uses the canonical action plan, approval, execution, audit, and
+verification APIs. A successful command does not resolve the attention item;
+fresh detector evidence must still confirm recovery.
+
### What Patrol Sees
Every patrol run passes the LLM comprehensive context about your environment:
diff --git a/docs/API.md b/docs/API.md
index 364eb0a26..22c47b542 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -1066,9 +1066,27 @@ Runs a focused investigation for an alert payload (used by the UI).
- Returns one attention item with its operational record, lifecycle
timeline, typed evidence, recommended next step, relationships, and
protection posture.
-- All three attention routes require `monitoring:read`. They are read-side
- projections only; alert lifecycle mutations remain owned by the canonical
- alert routes.
+- `GET /api/ai/patrol/attention/{id}/evidence/{evidenceId}`
+ - Returns one exact retained evidence envelope with its current freshness.
+ - Returns `410 attention_evidence_detail_expired` when the operational
+ record still links the ID but the bounded detail has expired.
+- `POST /api/ai/patrol/attention/{id}/acknowledge`
+- `POST /api/ai/patrol/attention/{id}/unacknowledge`
+- `POST /api/ai/patrol/attention/{id}/suppress`
+ - Body: `{ "reason": "...", "expiresAt": "" }`.
+ - The expiry must be in the future and no more than 30 days away.
+- `POST /api/ai/patrol/attention/{id}/unsuppress`
+- `POST /api/ai/patrol/attention/{id}/actions/restart/plan`
+ - Creates or replays the one server-owned Docker restart plan attached to
+ this operational record and its exact evidence IDs.
+ - Requires the canonical action authorization, an eligible server-side
+ offer, and the Pulse Pro `ai_autofix` entitlement. Clients cannot supply
+ command authority or override the target.
+- Attention reads require `monitoring:read`; lifecycle mutations require
+ `monitoring:write`. Action decision and execution use `/api/actions` and
+ retain their existing action-specific scopes.
+- Attention IDs and evidence IDs are opaque and can contain slashes. Clients
+ must path-escape each ID.
- `GET /api/ai/patrol/autonomy`
- `PUT /api/ai/patrol/autonomy`
- `GET /api/ai/patrol/status`
diff --git a/docs/OPERATIONAL_TRUST.md b/docs/OPERATIONAL_TRUST.md
new file mode 100644
index 000000000..af24e836e
--- /dev/null
+++ b/docs/OPERATIONAL_TRUST.md
@@ -0,0 +1,202 @@
+# Operational Trust
+
+Operational Trust is Pulse v6's shared model for answering five operator
+questions:
+
+1. What needs attention now?
+2. What evidence supports that conclusion?
+3. Is the affected resource protected by usable recovery history?
+4. What changed in the issue lifecycle?
+5. Can Pulse offer a narrow action, and did fresh evidence verify its result?
+
+Alerts, the Patrol attention queue, navigation counts, attached availability
+checks, notifications, protection posture, and governed actions project the
+same canonical records. None of those views owns a second writable lifecycle.
+
+## Lifecycle states
+
+| State | Operator meaning |
+| :--- | :--- |
+| `observing` | Evidence is being confirmed. This is not yet active work. |
+| `open` | Current evidence supports an operational issue. |
+| `acknowledged` | An operator has seen the issue. The issue is not resolved. |
+| `suppressed` | The issue is temporarily removed from active attention with an actor, reason, and bounded expiry. |
+| `resolving` | Recovery evidence exists, but the detector has not yet confirmed normal health. |
+| `resolved` | Fresh detector evidence confirms that the issue no longer applies. |
+| `stale` | The prior issue remains relevant, but its collection evidence is no longer current. |
+| `unknown` | Permissions, completeness, provider state, or identity prevent a stronger conclusion. |
+
+Missing observations never resolve an open record. Collector disconnects move
+existing work to `stale`; permission or provider uncertainty moves it to
+`unknown`. A successful action result also does not close a record. Only fresh
+detector evidence can confirm recovery.
+
+Acknowledgement is reversible and does not reduce the active issue truth.
+Suppression requires a non-empty reason and an expiry no more than 30 days in
+the future. The Patrol UI offers shorter 1-hour, 24-hour, and 7-day choices by
+default.
+
+## Evidence
+
+Every evidence envelope records:
+
+- a stable opaque evidence ID;
+- provider, collector, and optional provider instance;
+- one canonical resource ID or one unresolved provider-scoped reference;
+- observation, ingestion, and optional validity times;
+- completeness, confidence, and permission state;
+- an optional bounded payload reference and identity-correlation proof.
+
+`fresh`, `complete`, and `confirmed` are independent dimensions. Partial,
+denied, unavailable, stale, inferred, ambiguous, and unknown evidence is
+represented explicitly and never upgraded to healthy by a client.
+
+The attention detail response contains the retained envelopes needed for the
+normal operator journey. An authorized client can request one exact envelope
+through the evidence endpoint. If the record still links the evidence ID but
+its bounded detail has expired, the endpoint returns `410
+attention_evidence_detail_expired`; it does not pretend the evidence never
+existed.
+
+## Protection posture
+
+Protection posture is evaluated server-side per canonical subject resource:
+
+- `protected`: current usable protection satisfies policy;
+- `attention`: protection exists but freshness, verification, or provider
+ outcome needs attention;
+- `unprotected`: sufficient evidence confirms that required protection is
+ absent;
+- `unknown`: identity, history, permissions, or collection coverage cannot
+ support a stronger claim.
+
+The response explains the conclusion, preserves provider-specific state, and
+links the recovery and repository resources involved. Platform tables fetch
+posture in batches of at most 200 resource IDs. They must not issue one network
+request per row.
+
+## Availability
+
+An availability check attaches to an existing unified resource only when an
+explicit link resolves or identity correlation yields exactly one candidate.
+The relationship carries a stable relationship ID and the evidence ID that
+supports it. Ambiguous checks remain standalone. An attached check appears on
+the owning platform row and detail; it is not duplicated as another inventory
+resource.
+
+Availability success is time-bounded. A stale successful observation is
+`stale`, not healthy. A failure enters the same alert lifecycle and Patrol
+attention queue as other detectors.
+
+## Patrol workflow
+
+The normal operator path is:
+
+1. Open Patrol from the monitor shell.
+2. Review the urgency-ordered active queue.
+3. Select an item for impact, next step, resource, evidence, protection, and
+ lifecycle detail.
+4. Acknowledge it, or temporarily suppress it with a reason and expiry.
+5. If an eligible Pulse Pro action is offered, review the server-owned plan,
+ approve it, run it, and inspect execution and verification separately.
+
+A calm state appears only when the lifecycle evaluation succeeded, coverage is
+current, and no active item exists. A failed read or partial coverage never
+becomes a calm claim.
+
+The first governed action is a Docker container restart. It is offered only
+for a uniquely identified container with fresh confirmed unhealthy evidence,
+declared executor readiness, the required authorization scope, and the
+`ai_autofix` entitlement. The action framework owns plan hashing, approval,
+idempotent execution, durable audit, restart reconciliation, and verification.
+
+## API and authorization
+
+Read routes require `monitoring:read`:
+
+- `GET /api/ai/patrol/attention`
+- `GET /api/ai/patrol/attention/summary`
+- `GET /api/ai/patrol/attention/{id}`
+- `GET /api/ai/patrol/attention/{id}/evidence/{evidenceId}`
+
+Lifecycle mutations require `monitoring:write`:
+
+- `POST /api/ai/patrol/attention/{id}/acknowledge`
+- `POST /api/ai/patrol/attention/{id}/unacknowledge`
+- `POST /api/ai/patrol/attention/{id}/suppress`
+- `POST /api/ai/patrol/attention/{id}/unsuppress`
+
+Suppression body:
+
+```json
+{
+ "reason": "Planned host maintenance",
+ "expiresAt": "2026-07-20T08:00:00Z"
+}
+```
+
+Planning an offered restart requires the action scopes enforced by the
+canonical action API and an active `ai_autofix` entitlement:
+
+```text
+POST /api/ai/patrol/attention/{id}/actions/restart/plan
+```
+
+Action decision, execution, detail, and audit use `/api/actions`.
+
+All IDs are opaque. Clients must path-escape operational-record and evidence
+IDs because canonical IDs can contain `/`, `:`, and provider-specific
+segments.
+
+## Metrics
+
+The `/metrics` listener exposes Operational Trust counters and histograms under
+`pulse_operational_trust_*`. Labels use closed, low-cardinality vocabularies;
+resource IDs, evidence IDs, provider-instance names, actors, and destination
+IDs never appear as labels.
+
+Useful alerts include:
+
+- sustained growth in `active_count_mismatch_total`;
+- notification `failed` or `dead_letter` outcomes;
+- growing stale, unavailable, denied, or partial evidence observations;
+- identity `ambiguous` or `unresolved` outcomes;
+- action verification `contradicted`, `inconclusive`, or `timed_out` outcomes.
+
+## Upgrade and compatibility
+
+Operational Trust migrations are additive. Existing alert, notification,
+recovery, relationship, availability, and action records are normalized on
+read or migrated in their owning stores. Read-side compatibility fields remain
+supported where older clients need them, but new writes go only through the
+canonical lifecycle, recovery, unified-resource, notification, and action
+owners.
+
+The Pulse Mobile primary backlog uses `/api/ai/patrol/attention` and preserves
+operational record, evidence, and action-verification identity. Its old finding
+shape is now a local display adapter, not a writable source of truth.
+
+Before upgrading:
+
+1. back up the Pulse data directory;
+2. confirm that the v6 process can write its alert, notification, recovery, and
+ action database directories;
+3. confirm supported clients can read additive JSON fields;
+4. expose the metrics listener to a protected scraper if rollout telemetry is
+ required;
+5. verify Pulse Pro entitlement connectivity before relying on action offers.
+
+After upgrading:
+
+1. confirm the Patrol navigation count matches the active queue;
+2. inspect an active item through its deepest evidence and protection detail;
+3. acknowledge and unacknowledge a test item;
+4. verify a bounded suppression returns to active attention;
+5. confirm stale collection remains visible rather than resolving;
+6. exercise notification retry/dead-letter monitoring;
+7. if using Pulse Pro actions, complete a review/approve/run/verify journey.
+
+If a migration fails, stop the upgraded process, preserve the data directory
+and logs, and restore the prior release with the pre-upgrade data backup. Do
+not delete lifecycle, evidence, notification, recovery, or action records to
+force startup.
diff --git a/docs/UPGRADE_v6.md b/docs/UPGRADE_v6.md
index 74eee91e3..0981c6664 100644
--- a/docs/UPGRADE_v6.md
+++ b/docs/UPGRADE_v6.md
@@ -294,6 +294,37 @@ Practical recommendation:
- Before upgrading, keep console access available so you can retry activation from the v6 license panel if the exchange service is temporarily unavailable.
+## Operational Trust migration
+
+Pulse v6 consolidates alerts, Patrol attention, evidence, protection posture,
+attached availability checks, notifications, and governed action verification
+onto one Operational Trust lifecycle. See
+[`OPERATIONAL_TRUST.md`](OPERATIONAL_TRUST.md) for the operator contract and
+post-upgrade checks.
+
+The migrations are additive:
+
+- existing alert state is normalized into operational records and transitions;
+- notification delivery keeps exact operational-record and transition links;
+- recovery points and provider observations materialize provider-aware
+ protection posture;
+- unified-resource relationships and availability facets gain stable evidence
+ linkage;
+- action audit records preserve execution and verification as separate truth.
+
+Supported legacy JSON fields remain readable where older clients need them,
+but the primary v6 runtime has one writable owner for each domain. Pulse Mobile
+now reads the canonical Patrol attention queue. Operators should upgrade
+desktop and mobile clients together when they depend on acknowledgement,
+evidence, protection, or action-verification parity.
+
+Before the upgrade, back up the Pulse data directory and confirm the service
+account can write the alert, notification, recovery, and action stores. After
+startup, verify that the Patrol navigation count matches the active queue,
+inspect one evidence/protection drill-down, and confirm stale collection does
+not appear resolved. Pulse Pro users should also verify entitlement
+connectivity before relying on restart offers.
+
### Multi-Tenant (Opt-In)
Multi-tenant mode is opt-in and additionally license-gated:
diff --git a/docs/monitoring/PROMETHEUS_METRICS.md b/docs/monitoring/PROMETHEUS_METRICS.md
index b98a12955..51e348cb9 100644
--- a/docs/monitoring/PROMETHEUS_METRICS.md
+++ b/docs/monitoring/PROMETHEUS_METRICS.md
@@ -78,6 +78,31 @@ In Docker and Kubernetes you must expose `9091` explicitly if you want to scrape
| `pulse_alerts_rate_limited_total` | Counter | Alerts suppressed due to rate limiting. |
| `pulse_alert_duration_seconds` | Histogram | Time from alert fire to resolve (by `type`). |
+## Operational Trust
+
+Operational Trust metrics use closed low-cardinality labels. They never label
+series with resource, operational-record, evidence, actor, provider-instance,
+or notification-destination IDs.
+
+| Metric | Type | Description |
+| :--- | :--- | :--- |
+| `pulse_operational_trust_observation_to_open_seconds` | Histogram | Time from the first canonical observation to an open operational record. |
+| `pulse_operational_trust_open_to_notification_enqueue_seconds` | Histogram | Time from an open transition to durable notification enqueue. |
+| `pulse_operational_trust_evidence_observations_total` | Counter | Evidence observations by bounded `source` and state (`current`, `stale`, `unknown`, `partial`, `unavailable`, `partial_permission`, `denied`, or `other`). |
+| `pulse_operational_trust_identity_correlations_total` | Counter | Resource correlation outcomes (`attached`, `standalone`, `ambiguous`, `unresolved`, or `other`). |
+| `pulse_operational_trust_protection_posture_evaluations_total` | Counter | Protection posture evaluations by state. |
+| `pulse_operational_trust_protection_posture_evaluation_failures_total` | Counter | Posture evaluation failures by bounded reason. |
+| `pulse_operational_trust_notification_delivery_total` | Counter | Transition-linked delivery outcomes (`queued`, `retry`, `sent`, `failed`, `dead_letter`, `cancelled`, or `other`). |
+| `pulse_operational_trust_active_count_mismatch_total` | Counter | Detected disagreement between the canonical active records and attention projection. |
+| `pulse_operational_trust_action_offers_total` | Counter | Action-offer projections by eligibility. |
+| `pulse_operational_trust_action_verification_total` | Counter | Verification outcomes, including confirmed, contradicted, inconclusive, timed out, and not attempted. |
+
+Example rollout alerts:
+
+- `increase(pulse_operational_trust_active_count_mismatch_total[15m]) > 0`
+- `increase(pulse_operational_trust_notification_delivery_total{outcome="dead_letter"}[15m]) > 0`
+- `increase(pulse_operational_trust_action_verification_total{outcome=~"contradicted|timed_out"}[30m]) > 0`
+
## 🚨 Alerting Examples
- **High Error Rate**: `rate(pulse_http_request_errors_total[5m]) > 0.05`
- **Stale Node**: `pulse_monitor_node_poll_staleness_seconds > 300`
diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md
index ed243644a..fd4c092f2 100644
--- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md
+++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md
@@ -4660,3 +4660,15 @@ deliver a command itself or add an agent wire shape. Actual dispatch retains
the canonical action executor, exact agent/resource binding, command-enabled
token policy, durable attempt/receipt, timeout, and restart-reconciliation
boundaries.
+
+### Operational Trust lifecycle mutation boundary
+
+The attention evidence and lifecycle mutation handlers under `internal/api/`
+remain resource-monitoring consumers. Acknowledge, unacknowledge, suppress, and
+unsuppress call the alerts-owned operational lifecycle and persist through the
+active-alert store; they do not mint agent credentials, alter observer config,
+or dispatch commands. Evidence reads require `monitoring:read`, lifecycle
+mutations require `monitoring:write`, and slash-containing canonical record IDs
+are parsed as opaque identities. Governed action planning still enters the
+existing action lifecycle and entitlement boundary before any agent executor
+is considered.
diff --git a/docs/release-control/v6/internal/subsystems/ai-runtime.md b/docs/release-control/v6/internal/subsystems/ai-runtime.md
index fb96d6525..fe87c81e1 100644
--- a/docs/release-control/v6/internal/subsystems/ai-runtime.md
+++ b/docs/release-control/v6/internal/subsystems/ai-runtime.md
@@ -6635,3 +6635,14 @@ review. Verification presentation is derived only from the durable canonical
action state and `ActionResultV2`: confirmed, contradicted, inconclusive, and
not-attempted remain distinct. The projection has no authority to resolve the
operational record.
+
+### Operational Trust rollout telemetry and lifecycle controls
+
+Patrol exposes evidence-backed lifecycle controls only for the selected
+canonical attention item. Acknowledge is reversible. Suppression requires a
+reason and bounded expiry. Neither action changes detector truth, and stale or
+unknown collection state remains stronger than a retained legacy
+acknowledgement flag. `internal/ai/patrol_metrics.go` compares the canonical
+active summary with the projected state set and increments one unlabeled
+low-cardinality mismatch counter on contradiction. It never labels metrics with
+resource, record, evidence, actor, or provider-instance identity.
diff --git a/docs/release-control/v6/internal/subsystems/alerts.md b/docs/release-control/v6/internal/subsystems/alerts.md
index 0662a477a..89942ad1a 100644
--- a/docs/release-control/v6/internal/subsystems/alerts.md
+++ b/docs/release-control/v6/internal/subsystems/alerts.md
@@ -1137,3 +1137,21 @@ That same ownership also governs acknowledgement and manual-clear cleanup.
Clearing an alert through the canonical alerts runtime must remove both legacy
public-id tracking and canonical-state acknowledgement records so old aliases
cannot keep an alert acknowledged after the canonical alert has been removed.
+
+### Operational Trust writable lifecycle
+
+`internal/alerts/active_lifecycle.go` is the single writable owner for
+acknowledge, unacknowledge, suppress, unsuppress, collection-stale,
+collection-unknown, resolving, and collection-restored transitions. Every
+writer is idempotent under retry, retains new evidence without duplicating a
+same-state transition, persists through the active-alert store, and survives
+restart. Explicit stale, unknown, resolving, or suppressed state takes
+precedence over the legacy `Alert.Acknowledged` projection. The canonical
+record's `LastObservedAt` is the maximum of retained record, alert, and evidence
+timestamps so a fresh outage observation cannot be rolled back by an older
+legacy alert timestamp.
+
+Suppression is bounded and reasoned, leaves the default active queue, and
+remains inspectable. Expiry or explicit unsuppression returns the record to its
+detector-owned state; it never resolves it. Only fresh sufficient recovery
+evidence may enter resolving, and only detector recovery may resolve.
diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md
index 1952172b2..5a24d6298 100644
--- a/docs/release-control/v6/internal/subsystems/api-contracts.md
+++ b/docs/release-control/v6/internal/subsystems/api-contracts.md
@@ -7844,3 +7844,23 @@ plan is replayed rather than duplicated.
Decision, execution, receipt, reconciliation, result, and verification remain
on the existing `/api/actions` contract. Provider success does not mutate the
operational record; only fresh detector recovery evidence may resolve it.
+
+### Operational Trust evidence and lifecycle mutation transport
+
+The canonical selected-item transport now includes:
+
+1. `GET /api/ai/patrol/attention/{id}/evidence/{evidenceId}` under
+ `monitoring:read`; retained evidence returns its typed envelope and
+ freshness, while an expired referenced envelope returns typed `410 Gone`
+ instead of false absence or health.
+2. `POST /api/ai/patrol/attention/{id}/acknowledge` and
+ `/unacknowledge` under `monitoring:write`.
+3. `POST /api/ai/patrol/attention/{id}/suppress` with a non-empty reason and
+ bounded future `expiresAt`, and `/unsuppress`, under `monitoring:write`.
+
+Every mutation reloads the canonical active record, delegates to the
+alerts-owned lifecycle writer, and returns the refreshed shared detail
+projection. The browser cannot submit operational state, transition,
+evidence, actor, or subject identity. Action offers and planning additionally
+require the canonical `ai_autofix` entitlement and return the normal
+license-required `402` contract when it is absent.
diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
index 163950d04..7aef46344 100644
--- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md
+++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md
@@ -5101,3 +5101,15 @@ Protection table controls continue to compose shared filter, table, status,
counter, loading, error, disclosure, and inline-detail primitives. The bounded
batch hook retains fulfilled values during refresh and issues at most one
request per 200 resource rows, rather than placing a request under each row.
+
+### Operational Trust attention interaction boundary
+
+The Patrol selected-detail surface owns the smallest lifecycle interaction
+set: acknowledge, return to open, temporary suppression with a required reason
+and one of the bounded 1-hour, 24-hour, or 7-day durations, and return to
+active. Every mutation refreshes both the shared detail and list projection.
+The queue retains screen-reader names, keyboard activation, focus restoration
+after node replacement, reduced-motion behavior, and a no-overflow phone
+layout. Raw evidence history, lifecycle timeline, provider limitations, and
+action audit remain one disclosure or shared review deeper; no platform table
+or Assistant surface recreates these controls.
diff --git a/docs/release-control/v6/internal/subsystems/notifications.md b/docs/release-control/v6/internal/subsystems/notifications.md
index 48a294dbe..58b4f95d5 100644
--- a/docs/release-control/v6/internal/subsystems/notifications.md
+++ b/docs/release-control/v6/internal/subsystems/notifications.md
@@ -244,3 +244,15 @@ channel targeting.
`api-contracts`: they are the product-facing control surface for
notification-management transport, while canonical payload-shape governance
still remains explicit in the shared API contract boundary.
+
+### Operational Trust delivery observability
+
+Notification queue rows retain the exact operational record and lifecycle
+transition IDs through grouping, retry, restart, send, cancellation, and dead
+letter. Restart during a queued retry must reopen the same durable delivery
+with the same transition links; it must not synthesize a new operational
+transition. `internal/notifications/queue.go` records bounded queue,
+retry/sent/failed/dead-letter/cancelled outcomes and open-to-enqueue latency
+without destination, record, resource, or evidence labels. Delivery state
+remains notification truth only and cannot resolve or reopen the alert
+lifecycle.
diff --git a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md
index f941539ac..365248021 100644
--- a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md
+++ b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md
@@ -2109,3 +2109,15 @@ nor its action authorizes a reboot. Contradicted, inconclusive, or health-unknow
postconditions remain actionable; only canonical confirmed postcondition truth
may support resolution. Package-manager internals remain forensic detail rather
than the least-expert default.
+
+### Operational Trust attention queue completion
+
+Patrol is the named monitor-first attention destination over one canonical
+operational lifecycle. Active, open, acknowledged, suppressed, stale/unknown,
+resolved, unavailable-evaluation, and honest calm states share one summary and
+detail projection. The selected detail owns reversible acknowledgement and
+bounded reasoned suppression, while Assistant remains explanation-only selected
+context. Evidence, impact, relationships, protection posture, lifecycle
+timeline, action origin, and verification truth remain linked to the same
+record. Mobile consumes the same list/detail and acknowledgement contracts; it
+does not revive the retired primary findings feed.
diff --git a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md
index b43c6ade1..7179f7211 100644
--- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md
+++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md
@@ -1941,3 +1941,19 @@ the indexed `origin_json` operational-record expression; MemoryStore performs
one bounded pass. The summary path performs no registry, action-store,
executor-readiness, or action-origin work. Readiness checks remain local to
the already-bounded visible page and do not issue browser requests per row.
+
+### Operational Trust rollout bounds
+
+The completed Operational Trust path preserves these explicit bounds:
+
+1. active-count and summary evaluation use the in-memory canonical projection;
+2. attention pages are capped at 200 items and a 10,000-record projection has
+ a five-second shared-CI ceiling;
+3. recovery posture joins batch at 200 subjects and platform tables issue one
+ bounded request rather than one request per row;
+4. action enrichment occurs after pagination and uses one registry, authority,
+ and latest-origin batch;
+5. evidence and recovery retention remain store-owned, indexed, bounded, and
+ covered by query-plan and retention tests;
+6. Operational Trust telemetry uses bounded enumerated labels only and never
+ introduces resource-cardinality labels.
diff --git a/docs/release-control/v6/internal/subsystems/security-privacy.md b/docs/release-control/v6/internal/subsystems/security-privacy.md
index 3f16f7698..716359def 100644
--- a/docs/release-control/v6/internal/subsystems/security-privacy.md
+++ b/docs/release-control/v6/internal/subsystems/security-privacy.md
@@ -1444,3 +1444,15 @@ origin, target, evidence set, handler, actor, or parameters. The server binds
the exact operational record and policy-shaped evidence IDs internally.
Unauthorized, stale, partial, permission-limited, ambiguous, unsupported, or
executor-unready records return no offer and no cross-resource detail.
+
+### Operational Trust evidence and mutation authorization
+
+Evidence detail is tenant-scoped through the selected canonical record and
+requires `monitoring:read`; lifecycle mutation requires `monitoring:write`.
+Unknown record/evidence pairs do not permit cross-resource enumeration, and an
+expired referenced envelope returns typed expiry without disclosing another
+record. Acknowledge and suppression actors are server-derived. Suppression
+accepts only a non-empty reason and bounded future expiry. Action offer and
+planning require the `ai_autofix` entitlement in addition to current RBAC,
+evidence, resource-capability, executor-readiness, approval, and action-policy
+checks.
diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md
index 64f837911..6709e63f0 100644
--- a/docs/release-control/v6/internal/subsystems/storage-recovery.md
+++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md
@@ -2180,6 +2180,17 @@ while storage detail drawers and filter controls must route summary series IDs,
source tones, and disk metrics through the shared storage helpers instead of
reconstructing them from local table state.
+### Operational Trust posture rollout
+
+Protection posture remains a provider-aware, subject-linked read model over
+recovery points and provider collection evidence. Invalid policy falls back to
+the canonical bounded default and records a bounded failure reason; invalid,
+partial, permission-denied, failed, stale, missing, or unknown provider
+evidence cannot produce `protected`. Evaluations record only the four canonical
+posture states. Platform and Patrol consumers use bounded batch posture reads,
+retain evidence and repository links, and never infer protection from the
+existence of a row, snapshot, or provider connection.
+
The adjacent Operational Trust attention action transport does not turn
protection posture into mutation authority. The selected item may display
storage/recovery-owned posture beside a separately declared Docker restart
diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md
index 7ab89279c..8b156a357 100644
--- a/docs/release-control/v6/internal/subsystems/unified-resources.md
+++ b/docs/release-control/v6/internal/subsystems/unified-resources.md
@@ -1815,6 +1815,17 @@ through the canonical resource model, but unified-resource consumers must not
reintroduce removed workload aliases or feature-local resource-type shims just
to satisfy one table, drawer, or badge surface.
+### Operational Trust relationship identity
+
+Operational Trust relationships use a stable normalized relationship ID and
+may bind an exact evidence ID. Availability attachment, hosting, storage,
+protection, and membership relationships normalize through the unified
+registry; ambiguous identity remains unattached and emits only a bounded
+correlation outcome. Attached availability is a facet of the known resource
+and must not create a duplicate standalone resource or second primary
+presentation. Supported legacy relationship payloads may omit the additive ID,
+but all primary runtime projections author the canonical stable identity.
+
Patrol attention resource navigation carries the canonical subject resource ID
through shared route builders as an opaque query value. Attention deep links
carry the canonical operational-record ID separately. Neither link may derive
diff --git a/frontend-modern/src/api/__tests__/patrolAttention.test.ts b/frontend-modern/src/api/__tests__/patrolAttention.test.ts
index 1e4f017f6..fb5296015 100644
--- a/frontend-modern/src/api/__tests__/patrolAttention.test.ts
+++ b/frontend-modern/src/api/__tests__/patrolAttention.test.ts
@@ -5,10 +5,15 @@ vi.mock('@/utils/apiClient', () => ({
}));
import {
+ acknowledgePatrolAttention,
getPatrolAttention,
getPatrolAttentionDetail,
+ getPatrolAttentionEvidence,
getPatrolAttentionSummary,
planPatrolAttentionAction,
+ suppressPatrolAttention,
+ unacknowledgePatrolAttention,
+ unsuppressPatrolAttention,
} from '@/api/patrolAttention';
import { apiFetchJSON } from '@/utils/apiClient';
@@ -33,6 +38,40 @@ describe('Patrol attention API', () => {
await getPatrolAttentionDetail('record/one');
expect(fetchMock).toHaveBeenLastCalledWith('/api/ai/patrol/attention/record%2Fone');
+
+ await getPatrolAttentionEvidence('record/one', 'evidence/two');
+ expect(fetchMock).toHaveBeenLastCalledWith(
+ '/api/ai/patrol/attention/record%2Fone/evidence/evidence%2Ftwo',
+ );
+ });
+
+ it('uses canonical item-scoped lifecycle mutations with bounded suppression input', async () => {
+ await acknowledgePatrolAttention('record/one');
+ expect(fetchMock).toHaveBeenLastCalledWith(
+ '/api/ai/patrol/attention/record%2Fone/acknowledge',
+ { method: 'POST', body: '{}' },
+ );
+
+ await unacknowledgePatrolAttention('record/one');
+ expect(fetchMock).toHaveBeenLastCalledWith(
+ '/api/ai/patrol/attention/record%2Fone/unacknowledge',
+ { method: 'POST', body: '{}' },
+ );
+
+ await suppressPatrolAttention('record/one', 'Maintenance window', '2026-07-20T08:00:00Z');
+ expect(fetchMock).toHaveBeenLastCalledWith('/api/ai/patrol/attention/record%2Fone/suppress', {
+ method: 'POST',
+ body: JSON.stringify({
+ reason: 'Maintenance window',
+ expiresAt: '2026-07-20T08:00:00Z',
+ }),
+ });
+
+ await unsuppressPatrolAttention('record/one');
+ expect(fetchMock).toHaveBeenLastCalledWith('/api/ai/patrol/attention/record%2Fone/unsuppress', {
+ method: 'POST',
+ body: '{}',
+ });
});
it('plans the fixed attention capability without accepting public action authority', async () => {
diff --git a/frontend-modern/src/api/patrolAttention.ts b/frontend-modern/src/api/patrolAttention.ts
index b1c957a32..7ef8a416a 100644
--- a/frontend-modern/src/api/patrolAttention.ts
+++ b/frontend-modern/src/api/patrolAttention.ts
@@ -91,6 +91,16 @@ export interface AttentionListResponse {
};
}
+export interface AttentionEvidenceResponse {
+ evidence: EvidenceEnvelope;
+ freshness: EvidenceFreshness;
+ retained: boolean;
+}
+
+export interface AttentionMutationResponse {
+ success: boolean;
+}
+
export async function getPatrolAttention(
filter: AttentionFilter = 'active',
page = 1,
@@ -114,6 +124,62 @@ export async function getPatrolAttentionDetail(itemId: string): Promise {
+ return apiFetchJSON(
+ `/api/ai/patrol/attention/${encodeURIComponent(itemId)}/evidence/${encodeURIComponent(evidenceId)}`,
+ );
+}
+
+async function mutatePatrolAttention(
+ itemId: string,
+ mutation: 'acknowledge' | 'unacknowledge' | 'suppress' | 'unsuppress',
+ body = '{}',
+): Promise {
+ return apiFetchJSON(
+ `/api/ai/patrol/attention/${encodeURIComponent(itemId)}/${mutation}`,
+ {
+ method: 'POST',
+ body,
+ },
+ );
+}
+
+export async function acknowledgePatrolAttention(
+ itemId: string,
+): Promise {
+ return mutatePatrolAttention(itemId, 'acknowledge');
+}
+
+export async function unacknowledgePatrolAttention(
+ itemId: string,
+): Promise {
+ return mutatePatrolAttention(itemId, 'unacknowledge');
+}
+
+export async function suppressPatrolAttention(
+ itemId: string,
+ reason: string,
+ expiresAt: string,
+): Promise {
+ return mutatePatrolAttention(
+ itemId,
+ 'suppress',
+ JSON.stringify({
+ reason,
+ expiresAt,
+ }),
+ );
+}
+
+export async function unsuppressPatrolAttention(
+ itemId: string,
+): Promise {
+ return mutatePatrolAttention(itemId, 'unsuppress');
+}
+
export async function planPatrolAttentionAction(
itemId: string,
capability: string,
diff --git a/frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx b/frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx
index d3b1f2607..5c2f14efe 100644
--- a/frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx
+++ b/frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx
@@ -16,6 +16,7 @@ import ClockIcon from 'lucide-solid/icons/clock';
import ExternalLinkIcon from 'lucide-solid/icons/external-link';
import RefreshIcon from 'lucide-solid/icons/refresh-cw';
import RotateCwIcon from 'lucide-solid/icons/rotate-cw';
+import ShieldOffIcon from 'lucide-solid/icons/shield-off';
import SparklesIcon from 'lucide-solid/icons/sparkles';
import XIcon from 'lucide-solid/icons/x';
import type {
@@ -24,7 +25,13 @@ import type {
AttentionItem,
AttentionItemDetail,
} from '@/api/patrolAttention';
-import { planPatrolAttentionAction } from '@/api/patrolAttention';
+import {
+ acknowledgePatrolAttention,
+ planPatrolAttentionAction,
+ suppressPatrolAttention,
+ unacknowledgePatrolAttention,
+ unsuppressPatrolAttention,
+} from '@/api/patrolAttention';
import { ResourceActionsAPI } from '@/api/resourceActions';
import { Button, ButtonLink } from '@/components/shared/Button';
import { LoadingSpinner } from '@/components/shared/LoadingSpinner';
@@ -40,6 +47,7 @@ import {
} from '@/routing/resourceLinks';
import type { EvidenceEnvelope } from '@/types/operationalTrust';
import type { ActionDetailResponse } from '@/types/actionAudit';
+import { getAlertResourceIncidentAcknowledgedByLabel } from '@/utils/alertIncidentPresentation';
import { formatRelativeTime } from '@/utils/format';
const PRIMARY_EVIDENCE_LIMIT = 3;
@@ -59,6 +67,8 @@ export function PatrolAttentionWorkbench() {
const [actionDetail, setActionDetail] = createSignal(null);
const [actionBusy, setActionBusy] = createSignal(false);
const [actionError, setActionError] = createSignal('');
+ const [lifecycleBusy, setLifecycleBusy] = createSignal(false);
+ const [lifecycleError, setLifecycleError] = createSignal('');
const itemButtons = new Map();
let detailPanel: HTMLDivElement | undefined;
let actionTrigger: HTMLButtonElement | undefined;
@@ -149,6 +159,25 @@ export function PatrolAttentionWorkbench() {
patrolAttentionStore.load(patrolAttentionStore.filter()),
]);
};
+ const changeLifecycle = async (operation: () => Promise) => {
+ if (lifecycleBusy()) return;
+ setLifecycleBusy(true);
+ setLifecycleError('');
+ try {
+ await operation();
+ const selected = selectedItemId();
+ await Promise.all([
+ selected ? patrolAttentionStore.select(selected) : Promise.resolve(),
+ patrolAttentionStore.load(patrolAttentionStore.filter()),
+ ]);
+ } catch (cause) {
+ setLifecycleError(
+ cause instanceof Error ? cause.message : 'The lifecycle change could not be saved.',
+ );
+ } finally {
+ setLifecycleBusy(false);
+ }
+ };
onMount(() => {
void patrolAttentionStore.load('active');
@@ -271,6 +300,16 @@ export function PatrolAttentionWorkbench() {
actionBusy={actionBusy()}
actionError={actionError()}
onReviewAction={reviewAction}
+ lifecycleBusy={lifecycleBusy()}
+ lifecycleError={lifecycleError()}
+ onAcknowledge={(itemId) => changeLifecycle(() => acknowledgePatrolAttention(itemId))}
+ onUnacknowledge={(itemId) =>
+ changeLifecycle(() => unacknowledgePatrolAttention(itemId))
+ }
+ onSuppress={(itemId, reason, expiresAt) =>
+ changeLifecycle(() => suppressPatrolAttention(itemId, reason, expiresAt))
+ }
+ onUnsuppress={(itemId) => changeLifecycle(() => unsuppressPatrolAttention(itemId))}
/>
@@ -425,6 +464,12 @@ function AttentionDetail(props: {
offer: AttentionActionOffer,
trigger: HTMLButtonElement,
) => void;
+ lifecycleBusy: boolean;
+ lifecycleError: string;
+ onAcknowledge: (itemId: string) => Promise;
+ onUnacknowledge: (itemId: string) => Promise;
+ onSuppress: (itemId: string, reason: string, expiresAt: string) => Promise;
+ onUnsuppress: (itemId: string) => Promise;
}) {
const detail = () => props.detail;
const item = () => detail()?.item;
@@ -548,6 +593,16 @@ function AttentionDetail(props: {