From bcea56db970cd59f2f703584dc3ad2c88bc40910 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Tue, 18 Aug 2026 12:44:20 +0100 Subject: [PATCH] fix(patrol): clarify decisions and action origins --- .../v6/internal/subsystems/ai-runtime.md | 7 +- .../v6/internal/subsystems/api-contracts.md | 5 +- .../subsystems/frontend-primitives.md | 13 +++- .../subsystems/patrol-intelligence.md | 19 ++++-- .../internal/subsystems/unified-resources.md | 10 ++- frontend-modern/browser-verification.json | 52 ++++++++------- .../features/actions/ActionDecisionPacket.tsx | 10 +++ .../__tests__/ActionDecisionPacket.test.tsx | 8 +++ .../features/actions/actionPresentation.ts | 19 ++++++ .../patrol/PatrolAttentionWorkbench.tsx | 31 ++++++--- .../patrol/PatrolIntelligenceSurface.tsx | 6 +- .../PatrolAttentionWorkbench.test.tsx | 33 +++++++++- frontend-modern/src/pages/Actions.tsx | 9 +++ .../pages/__tests__/AIIntelligence.test.tsx | 4 +- ...patrolSummaryPresentation.coverage.test.ts | 4 +- .../patrolSummaryPresentation.test.ts | 4 +- .../src/utils/patrolSummaryPresentation.ts | 3 +- internal/ai/attention.go | 64 ++++++++++++++++++- internal/ai/attention_test.go | 60 +++++++++++++++++ internal/unifiedresources/incident_actions.go | 18 +++++- internal/unifiedresources/registry_test.go | 22 +++++++ 21 files changed, 342 insertions(+), 59 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/ai-runtime.md b/docs/release-control/v6/internal/subsystems/ai-runtime.md index 5c1dd843c..26aa45ca0 100644 --- a/docs/release-control/v6/internal/subsystems/ai-runtime.md +++ b/docs/release-control/v6/internal/subsystems/ai-runtime.md @@ -4223,7 +4223,12 @@ projection over alert-owned operational records, transition timelines, evidence envelopes, and recovery-owned protection postures. It may order, filter, paginate, summarize, and shape selected Assistant context, but it must not create a second writable lifecycle or infer a healthy state when lifecycle -coverage is unavailable. `internal/ai/patrol_metrics.go` exports only +coverage is unavailable. The projection also owns customer-facing attention +titles: provider alarm messages yield their bounded alarm subject, generic +resource/storage incident types become plain issue language, and known +acronyms such as CPU, ZFS, and I/O retain their canonical casing. Clients must +not receive machine-title-cased placeholders such as `Resource Incident` and +repair them independently. `internal/ai/patrol_metrics.go` exports only low-cardinality lifecycle-state counts, queue age, acknowledgement time, and calm-evaluation age; raw resource IDs are forbidden as metric labels. diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index 44429a2f5..a4ff1cf7d 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -3924,7 +3924,10 @@ next selection from the refreshed typed membership. It must not optimistically author lifecycle state, keep acknowledged/suppressed records in the active inbox, or turn review progress into a second queue API. Newly returned active items may be appended to the in-progress review session without changing their -server-authored identity or trust fields. +server-authored identity or trust fields. Visible queue position and calm-state +evaluation age must remain reactive to those refreshed typed projections; a +client must not capture either accessor as mount-time copy and then display a +stale position or evaluation timestamp. Lifecycle read failure returns a typed unavailable response and never a synthetic zero or calm state. Protection lookup failure may return lifecycle diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 237ab5456..320512976 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -2653,7 +2653,13 @@ announces the remaining count and advances to the next current item, and the final settled item returns to the calm inbox. Lifecycle copy must explain that review removes one occurrence from today's inbox while suppression is bounded; generic backend verbs must not leave the user guessing whether the alert was -resolved, hidden permanently, or merely recorded as seen. +resolved, hidden permanently, or merely recorded as seen. Both the position +label and calm evaluation age are reactive text nodes so detail navigation and +refresh cannot leave their first-render values frozen. The selected metadata +keeps latest-observation age visible, action verification copy stays generic +across capabilities, and the list itself is not a broad live region; only +errors, calm/empty transitions, and explicit completion announcements receive +scoped assistive announcement semantics. The selected attention detail may compose the shared Actions review for an eligible backend-authored offer. The detail owns only the expected @@ -5772,7 +5778,10 @@ universal audit records originating from Patrol, Assistant, MCP, or manual controls. While that route is open, desktop and mobile navigation select Actions and the browser title identifies `Actions`. The first-class navigation entry composes the existing shared review dialog, route, API client, and durable -action identity rather than creating another action client. +action identity rather than creating another action client. When the trusted +audit origin is present, both the queue row and decision packet show bounded +product attribution such as `From Patrol`; unknown first-party surfaces fall +back to `From Pulse`, and absent origin remains absent rather than guessed. ### Protection posture presentation boundary diff --git a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md index 3109d2080..e78fb6fee 100644 --- a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md +++ b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md @@ -1246,7 +1246,9 @@ keeps the canonical resource identifier behind a copy affordance, suppresses impact text that merely repeats the summary, and groups evidence, protection, and lifecycle history behind one deliberate disclosure. Occurrence controls remain visible, while durable threshold and finding-management guidance is -secondary disclosure. This is progressive presentation of the same typed +secondary disclosure. Selected metadata includes the latest observation age, +and action-result wording remains capability-neutral rather than assuming every +offer is a restart or container operation. This is progressive presentation of the same typed record, not a loss of evidence or lifecycle truth. Plan-locked Watch-only installs do not render an empty mode disclosure after @@ -1256,13 +1258,17 @@ choice controls. The empty Protection mode offers bounded example outcomes that prefill the existing objective form without creating an objective until the operator reviews and submits it. Activity keeps Actions and Patrol records as separate subordinate handoffs, but the Patrol-record handoff does not expose -the legacy findings count beside the canonical decision count. +the legacy findings count beside the canonical decision count. Its supporting +copy explicitly distinguishes raw finding/run history from Inbox decisions, and +its collapsed container must not leave a decorative border artifact behind. Selecting the first item starts a bounded review session over the loaded decision order. The selected context shows its current position and exposes previous/next navigation on desktop and mobile; deterministic identity breaks otherwise equal priority ties so polling cannot reshuffle equal decisions -arbitrarily. The session retains its review order while fresh list responses +arbitrarily. Position copy is a live projection of the selected identity and +loaded queue length, not a one-time string captured when detail first mounts. +The session retains its review order while fresh list responses remove settled items and append genuinely new work. Acknowledging an occurrence is presented as `Mark reviewed`: on success the item leaves the active inbox, Patrol announces the remaining decision count, and advances to the next current @@ -2238,11 +2244,12 @@ signal from the most recent completed run via `PatrolRecencyPresentation.resourcesChecked`, populated by `getPatrolRecencyPresentation` from `PatrolRunRecord.resources_checked`. The render reads "Last full -patrol: 3m ago — verified 47 resources" so operators see both +patrol: 3m ago — checked 47 resources" so operators see both temporal recency and coverage in one line. The field stays optional (omitted when zero) so a degenerate run that completed without -checking any resources does not render a misleading "verified 0 -resources" line. +checking any resources does not render a misleading "checked 0 +resources" line. Coverage never uses `verified`, which is reserved for +confirmed action outcomes rather than inventory traversal. The expanded finding card must also expose a "Copy summary" action that produces a paste-ready Markdown summary of the finding (severity, title, resource header, description, impact, recommendation, diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index 309644ed7..e698bdf5f 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -1695,7 +1695,10 @@ served clones. Proof: `TestClonedResourcesPreservePlatformAdmission` and `migrateActionAuditsSchema`) and round-tripped through `scanActionAuditRecord`. Origin identifies which internal surface proposed the action (e.g. Patrol) so decisions and terminal outcomes - can be reconciled back onto that surface's records. It is set only by + can be reconciled back onto that surface's records. The Actions queue and + decision packet render that trusted surface as bounded product copy such as + `From Patrol`; they do not expose an internal surface token or invent a + return link when origin metadata lacks a canonical route. It is set only by in-process planning callers through the action lifecycle service's plan options; the public `POST /api/actions/plan` body must never be able to claim a first-party origin. `NormalizeActionOrigin` trims @@ -1720,6 +1723,11 @@ served clones. Proof: `TestClonedResourcesPreservePlatformAdmission` and `TestSQLiteStoreActionAuditOriginRoundTrip`, `TestSQLiteActionAuditOriginOperationalRecordReader`, and the matching memory/batch reader tests in `internal/unifiedresources/store_test.go`. + Incident next-step fallback follows the same canonical resource boundary: + storage guidance is reserved for storage, physical-disk, network-share, + PBS, and Ceph resources. Network and every other non-storage type receive + neutral resource-health guidance rather than being misclassified as + storage merely because they are not a host or VM. 27. Keep API-added TrueNAS systems keyed by the configured connection, never by snapshot-reported identity. `systemSourceID` in `internal/truenas/provider.go` scopes the system source ID (and every diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index dd2cfefe4..191c668f8 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,25 +1,29 @@ { "version": 1, - "base_sha": "95c253c4860d704dfdc8b3d743b482182d749fff", - "verified_at": "2026-08-18T10:49:01Z", + "base_sha": "ac4116e09b85055e281aee3895ba65012e747215", + "verified_at": "2026-08-18T11:35:39Z", "result": "passed", "changed_paths": [ + "frontend-modern/src/features/actions/ActionDecisionPacket.tsx", + "frontend-modern/src/features/actions/actionPresentation.ts", "frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx", - "frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx", "frontend-modern/src/features/patrol/PatrolIntelligenceSurface.tsx", - "frontend-modern/src/features/patrol/PatrolObjectivesPanel.tsx" + "frontend-modern/src/pages/Actions.tsx", + "frontend-modern/src/utils/patrolSummaryPresentation.ts" ], "content_sha256": { - "frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx": "42d0e595053546270727bcfe57de37b1f9c6e4c721a2bbc6175335136a4e03b2", - "frontend-modern/src/features/patrol/PatrolIntelligenceHeader.tsx": "eeaecb901bac67f4b2ba2c442a5f669be50ee2a1bb01928340eef4d83da5532d", - "frontend-modern/src/features/patrol/PatrolIntelligenceSurface.tsx": "90a0e1b7b7b75567a6c026d4d6c115b82d06c1f86412f971c0c8989a60562471", - "frontend-modern/src/features/patrol/PatrolObjectivesPanel.tsx": "fc03c1905afae9aaa684aecd759cd6e409eee5fb987abcdee5e2c83d850352d1" + "frontend-modern/src/features/actions/ActionDecisionPacket.tsx": "4bd177108ab987198c977d1c763a0ca21293d665b71520364eb168a312ac0ac0", + "frontend-modern/src/features/actions/actionPresentation.ts": "924734c195777644da82dfe4190eaa18ba4491d75ad67204b3bb39bd9f3eb1c8", + "frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx": "8b28588b1117a87a7c22944b29446a9b6944abf402b5ea162bbe88635e8a8f35", + "frontend-modern/src/features/patrol/PatrolIntelligenceSurface.tsx": "edb3703a3f28c562fa16b8762c8907651504d8aff0ae006a130c2d3a3afe98be", + "frontend-modern/src/pages/Actions.tsx": "7649d29a5a32650265fd745c07eab2e5c42f397a2e0a6da3b5563170daade9dd", + "frontend-modern/src/utils/patrolSummaryPresentation.ts": "22416045104e977f1a8ec7995a32a5c5943ae98a02d5bbae5aed0bab29ed2330" }, - "routes": ["/patrol"], + "routes": ["/patrol", "/actions"], "viewports": [ { - "width": 1440, - "height": 900 + "width": 1280, + "height": 720 }, { "width": 390, @@ -27,20 +31,20 @@ } ], "states": [ - "Inbox default queue with resource-first decision titles and no selected detail", - "Inbox selected decision with compact trust metadata and evidence collapsed", - "Inbox selected decision with evidence, protection, and timeline expanded", - "Protection empty state with starter outcomes", - "Protection create-objective dialog prefilled from a starter outcome", - "Activity with verified outcomes, Actions handoff, and Patrol records disclosure", - "Watch-only header with its mode explanation and no empty automation disclosure" + "Patrol Inbox default queue with canonical checked-resource recency and human-readable incident titles", + "Patrol selected decision at the first and second live queue positions on desktop and mobile", + "Patrol selected detail with latest-observation age, lifecycle controls, and evidence disclosure collapsed", + "Activity with verified outcomes, separate Actions and Patrol-record handoffs, and no collapsed border artifact", + "Expanded Patrol records with explicit raw-record versus Inbox-decision relationship copy", + "Actions Open queue with Patrol origin badges and the route selected in desktop and mobile navigation", + "Actions governed review with Patrol origin repeated in the decision packet" ], "interactions": [ - "Used the live in-app Browser to switch between Inbox, Protection, and Activity", - "Selected the highest-priority decision and confirmed Assistant, Copy ID, Open resource, lifecycle controls, and progressive disclosure", - "Expanded Evidence and history and confirmed the latest evidence, older-observation reveal, protection posture, and timeline", - "Used a Protection starter outcome and confirmed the objective dialog opened with the chosen brief prefilled", - "Used Claude Code Playwright verification at desktop and phone widths to inspect layout, focus visibility, copy, console output, and dialog behaviour", - "Confirmed there are no user-visible semicolons or em dashes and no console errors at either viewport" + "Used the live in-app Browser to start review and move from Decision 1 to Decision 2 at desktop and 390-pixel phone widths", + "Confirmed previous and next controls, resource-first titles, latest-observation age, and neutral action verification framing", + "Opened Activity, inspected the collapsed handoffs, expanded Patrol records, and confirmed decision/history separation", + "Opened Actions from Patrol, inspected From Patrol badges, and opened a governed action review to confirm its Origin field", + "Verified the mobile document and body widths equal the 390-pixel viewport with no horizontal overflow", + "Confirmed the final stable Patrol and Actions pass produced no new console warnings or errors" ] } diff --git a/frontend-modern/src/features/actions/ActionDecisionPacket.tsx b/frontend-modern/src/features/actions/ActionDecisionPacket.tsx index c0c7c5d0f..55fc51c17 100644 --- a/frontend-modern/src/features/actions/ActionDecisionPacket.tsx +++ b/frontend-modern/src/features/actions/ActionDecisionPacket.tsx @@ -6,6 +6,7 @@ import { formatEvidenceClass, formatPolicyAuthority, formatPolicyReason, + getActionOriginLabel, getActionResourcePresentation, verificationTruthLabel, } from './actionPresentation'; @@ -19,6 +20,7 @@ export const ActionDecisionPacket: Component<{ const result = () => props.audit.result?.actionResultV2; const apt = () => getAPTActionPresentation(props.audit); const firstEvidence = () => result()?.verification.evidence?.[0]; + const originLabel = () => getActionOriginLabel(props.audit.origin); const resource = createMemo(() => getActionResourcePresentation(props.audit.request.resourceId, props.audit.resource), ); @@ -57,6 +59,14 @@ export const ActionDecisionPacket: Component<{
Reason
{props.audit.request.reason}
+ + {(label) => ( +
+
Origin
+
{label()}
+
+ )} +
Current state
diff --git a/frontend-modern/src/features/actions/__tests__/ActionDecisionPacket.test.tsx b/frontend-modern/src/features/actions/__tests__/ActionDecisionPacket.test.tsx index 8e5766389..7494cc896 100644 --- a/frontend-modern/src/features/actions/__tests__/ActionDecisionPacket.test.tsx +++ b/frontend-modern/src/features/actions/__tests__/ActionDecisionPacket.test.tsx @@ -19,6 +19,12 @@ const audit: ActionAuditRecord = { requestedBy: 'ui:docker-page', }, resource: { id: 'docker:container:edge', name: 'Edge proxy', type: 'app-container' }, + origin: { + surface: 'operational_trust_attention', + findingId: 'finding-1', + investigationId: 'investigation-1', + proposalId: 'proposal-1', + }, plan: { actionId: 'action-1', requestId: 'request-1', @@ -85,6 +91,8 @@ describe('ActionDecisionPacket', () => { render(() => ); expect(screen.getByText('Edge proxy')).toBeInTheDocument(); expect(screen.getByText('docker:container:edge')).toBeInTheDocument(); + expect(screen.getByText('Origin')).toBeInTheDocument(); + expect(screen.getByText('From Patrol')).toBeInTheDocument(); expect(screen.getByText('Policy evidence')).toBeInTheDocument(); expect(screen.getByText(/2 authorities checked at planning/)).toBeInTheDocument(); expect(screen.getByText('Capability safety policy')).not.toBeVisible(); diff --git a/frontend-modern/src/features/actions/actionPresentation.ts b/frontend-modern/src/features/actions/actionPresentation.ts index 0994f2c2f..91328f24c 100644 --- a/frontend-modern/src/features/actions/actionPresentation.ts +++ b/frontend-modern/src/features/actions/actionPresentation.ts @@ -1,4 +1,5 @@ import type { + ActionAuditOrigin, ActionAuditRecord, ActionResourceReference, ActionAuditState, @@ -20,6 +21,24 @@ export const formatActionName = (value: string): string => .trim() .replace(/\b\w/g, (letter) => letter.toUpperCase()); +export const getActionOriginLabel = (origin?: ActionAuditOrigin): string | undefined => { + const surface = origin?.surface.trim().toLowerCase(); + if (!surface) return undefined; + if ( + surface === 'operational_trust_attention' || + surface === 'patrol_control' || + surface === 'pulse_patrol' || + surface === 'patrol' + ) { + return 'From Patrol'; + } + if (surface === 'pulse_assistant' || surface === 'assistant' || surface === 'chat') { + return 'From Assistant'; + } + if (surface === 'mcp' || surface === 'pulse_mcp') return 'From MCP'; + return 'From Pulse'; +}; + export interface ActionInboxStatePresentation { accentClass: string; label: string; diff --git a/frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx b/frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx index 2cf06699d..b31f13c15 100644 --- a/frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx +++ b/frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx @@ -517,10 +517,13 @@ function AttentionList(props: { }; return ( -
+
{(message) => ( -
+