fix(actions): link reviews back to Patrol records

This commit is contained in:
rcourtman
2026-08-18 13:23:02 +01:00
parent bcea56db97
commit 7cb3fc2e6b
10 changed files with 146 additions and 42 deletions
@@ -2292,6 +2292,11 @@ a new API state machine, queue contract, or verification-accounting field.
the lifecycle normalizes and de-duplicates them before persistence. Model
output cannot mint this audit provenance, and the IDs grant no planning,
approval, or execution authority.
The frontend action-audit contract projects every broker-owned origin
correlation field, including `operationalRecordId` and `evidenceIds`, without
treating either as operator-authored data. Consumers may build an exact
Patrol return route from `operationalRecordId` only for a recognized Patrol
surface; absence of that field remains an unavailable return route.
Authority proof: `TestContract_PatrolActionBrokerKeepsPolicyExecutionCoreOwned` in
`internal/api/contract_test.go`.
The proposal enters that broker from the investigation-only
@@ -5781,7 +5781,13 @@ entry composes the existing shared review dialog, route, API client, and durable
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.
back to `From Pulse`, and absent origin remains absent rather than guessed. The
shared action review also exposes `Open Patrol record` only when a Patrol origin
carries its canonical `operationalRecordId`; the link targets the existing
route-backed Patrol attention selection and never derives identity from display
copy, resource IDs, finding IDs, or action reasons. Older correlated Patrol
actions may expose `Open Patrol` to the Patrol home, but never label that
fallback as a record-specific return.
### Protection posture presentation boundary
@@ -1697,9 +1697,12 @@ served clones. Proof: `TestClonedResourcesPreservePlatformAdmission` and
proposed the action (e.g. Patrol) so decisions and terminal outcomes
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
`From Patrol`. A Patrol action carrying the trusted `operationalRecordId`
exposes an exact return link through the canonical route-backed attention
selection. Older Patrol actions without that identifier may return to the
Patrol home but do not claim an exact record link. Internal surface tokens
remain hidden. Origin 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
fields, sorts and deduplicates evidence IDs, and collapses an all-empty
+18 -27
View File
@@ -1,25 +1,19 @@
{
"version": 1,
"base_sha": "ac4116e09b85055e281aee3895ba65012e747215",
"verified_at": "2026-08-18T11:35:39Z",
"base_sha": "bcea56db970cd59f2f703584dc3ad2c88bc40910",
"verified_at": "2026-08-18T12:14:30Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/features/actions/ActionDecisionPacket.tsx",
"frontend-modern/src/features/actions/ActionReviewDialog.tsx",
"frontend-modern/src/features/actions/actionPresentation.ts",
"frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx",
"frontend-modern/src/features/patrol/PatrolIntelligenceSurface.tsx",
"frontend-modern/src/pages/Actions.tsx",
"frontend-modern/src/utils/patrolSummaryPresentation.ts"
"frontend-modern/src/types/actionAudit.ts"
],
"content_sha256": {
"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"
"frontend-modern/src/features/actions/ActionReviewDialog.tsx": "5e489d440ce9b1e9f106a543a79b09d0b4d73aed65c9757688efa39b2d6fa833",
"frontend-modern/src/features/actions/actionPresentation.ts": "776904de782fb0e208f28de7eb6e7d8ea9ac8746c3b2fa1cd7ee9b6ff1bcb985",
"frontend-modern/src/types/actionAudit.ts": "435934beca93e8683ec5177e051b8a8d06f2d3eb4fb09a082e4ead30282bbc32"
},
"routes": ["/patrol", "/actions"],
"routes": ["/actions", "/patrol"],
"viewports": [
{
"width": 1280,
@@ -31,20 +25,17 @@
}
],
"states": [
"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"
"Actions Open queue with Patrol-origin records and a selected governed review",
"Older Patrol action review with an honest Open Patrol fallback and no record-specific claim",
"Patrol Inbox opened with a canonical encoded attention record selected in the detail workspace",
"Mobile action review with the Patrol return control visible above the decision packet",
"Mobile Patrol exact-record selection with the intended decision detail visible"
],
"interactions": [
"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"
"Opened a Patrol-origin action from the live Actions queue and used Open Patrol to return to the Patrol Inbox",
"Navigated through the canonical encoded attention route and confirmed it selected the exact operational record",
"Repeated the selected action review and exact Patrol selection at a 390 by 844 viewport",
"Verified the mobile dialog remained within the viewport and document width equalled the 390-pixel viewport",
"Confirmed the final browser diagnostics contained no warning or error entries"
]
}
@@ -195,6 +195,12 @@ describe('ActionAuditAPI', () => {
name: 'Edge proxy',
type: 'app-container',
},
origin: {
surface: 'operational_trust_attention',
findingId: 'finding-1',
operationalRecordId: 'record/one',
evidenceIds: ['evidence-1'],
},
plan: {
actionId: 'action/one',
requestId: 'request-1',
@@ -252,6 +258,12 @@ describe('ActionAuditAPI', () => {
name: 'Edge proxy',
type: 'app-container',
});
expect(detail.audit.origin).toEqual({
surface: 'operational_trust_attention',
findingId: 'finding-1',
operationalRecordId: 'record/one',
evidenceIds: ['evidence-1'],
});
expect(detail.audit.request).not.toHaveProperty('resourceName');
expect(detail.readOnly).toBe(true);
expect(detail.audit.result?.actionResultV2).toMatchObject({
@@ -1,13 +1,18 @@
import { Show, createEffect, createMemo, createSignal, onCleanup, type Component } from 'solid-js';
import XIcon from 'lucide-solid/icons/x';
import ArrowUpRightIcon from 'lucide-solid/icons/arrow-up-right';
import { ResourceActionsAPI } from '@/api/resourceActions';
import { Button } from '@/components/shared/Button';
import { Button, ButtonLink } from '@/components/shared/Button';
import { Dialog } from '@/components/shared/Dialog';
import { notificationStore } from '@/stores/notifications';
import { presentationPolicyIsReadOnly } from '@/stores/sessionPresentationPolicy';
import type { ActionDetailResponse } from '@/types/actionAudit';
import { ActionDecisionPacket } from './ActionDecisionPacket';
import { formatActionName, getActionResourcePresentation } from './actionPresentation';
import {
formatActionName,
getActionOriginDestination,
getActionResourcePresentation,
} from './actionPresentation';
import { getAPTActionPresentation } from './aptActionPresentation';
export const ActionReviewDialog: Component<{
@@ -19,6 +24,7 @@ export const ActionReviewDialog: Component<{
const [error, setError] = createSignal('');
const [clock, setClock] = createSignal(Date.now());
const audit = () => props.detail?.audit;
const originDestination = () => getActionOriginDestination(audit()?.origin);
const resource = createMemo(() => {
const record = audit();
return record
@@ -245,6 +251,19 @@ export const ActionReviewDialog: Component<{
{resource().label}
<Show when={resource().detail}> · {resource().detail}</Show>
</p>
<Show when={originDestination()}>
{(destination) => (
<ButtonLink
href={destination().href}
variant="ghost"
size="xs"
class="mt-2 -ml-2.5 gap-1.5"
>
{destination().exact ? 'Open Patrol record' : 'Open Patrol'}
<ArrowUpRightIcon class="h-3.5 w-3.5" aria-hidden="true" />
</ButtonLink>
)}
</Show>
</div>
<Button
variant="ghost"
@@ -24,6 +24,8 @@ const audit: ActionAuditRecord = {
findingId: 'finding-1',
investigationId: 'investigation-1',
proposalId: 'proposal-1',
operationalRecordId: 'record/one',
evidenceIds: ['evidence-1'],
},
plan: {
actionId: 'action-1',
@@ -1,4 +1,5 @@
import { cleanup, fireEvent, render, screen, waitFor } from '@solidjs/testing-library';
import { Route, Router } from '@solidjs/router';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { ResourceActionsAPI } from '@/api/resourceActions';
import { syncSessionPresentationPolicy } from '@/stores/sessionPresentationPolicy';
@@ -114,6 +115,51 @@ const detail = (audit: ActionAuditRecord): ActionDetailResponse => ({
});
describe('ActionReviewDialog trust gates', () => {
it('links a trusted Patrol action back to its exact operational record', () => {
const audit = makeAudit('resolved', '2099-01-01T00:00:00Z');
audit.origin = {
surface: 'operational_trust_attention',
operationalRecordId: 'record/one',
};
render(() => (
<Router>
<Route
path="*"
component={() => <ActionReviewDialog detail={detail(audit)} onClose={vi.fn()} />}
/>
</Router>
));
expect(screen.getByRole('link', { name: 'Open Patrol record' })).toHaveAttribute(
'href',
'/patrol?attention=record%2Fone',
);
});
it('returns older Patrol actions to Patrol without claiming an exact record link', () => {
const audit = makeAudit('resolved', '2099-01-01T00:00:00Z');
audit.origin = { surface: 'operational_trust_attention' };
render(() => (
<Router>
<Route
path="*"
component={() => <ActionReviewDialog detail={detail(audit)} onClose={vi.fn()} />}
/>
</Router>
));
expect(screen.getByRole('link', { name: 'Open Patrol' })).toHaveAttribute('href', '/patrol');
expect(screen.queryByRole('link', { name: 'Open Patrol record' })).not.toBeInTheDocument();
});
it('does not treat another origin surface as Patrol even when it carries a record id', () => {
const audit = makeAudit('resolved', '2099-01-01T00:00:00Z');
audit.origin = {
surface: 'pulse_assistant',
operationalRecordId: 'record/one',
};
render(() => <ActionReviewDialog detail={detail(audit)} onClose={vi.fn()} />);
expect(screen.queryByRole('link', { name: 'Open Patrol record' })).not.toBeInTheDocument();
});
it('offers no approve or run control for legacy provenance', () => {
render(() => (
<ActionReviewDialog
@@ -8,6 +8,7 @@ import type {
ActionPolicyReasonCode,
ActionVerificationTruthStatus,
} from '@/types/actionAudit';
import { buildPatrolAttentionPath, PATROL_PATH } from '@/routing/resourceLinks';
import type { MetadataBadgeTone } from '@/components/shared/MetadataBadge';
import type { UpgradeDestination } from '@/utils/upgradeNavigation';
@@ -21,17 +22,19 @@ export const formatActionName = (value: string): string =>
.trim()
.replace(/\b\w/g, (letter) => letter.toUpperCase());
const getActionOriginSurface = (origin?: ActionAuditOrigin): string =>
origin?.surface.trim().toLowerCase() ?? '';
const isPatrolActionOrigin = (surface: string): boolean =>
surface === 'operational_trust_attention' ||
surface === 'patrol_control' ||
surface === 'pulse_patrol' ||
surface === 'patrol';
export const getActionOriginLabel = (origin?: ActionAuditOrigin): string | undefined => {
const surface = origin?.surface.trim().toLowerCase();
const surface = getActionOriginSurface(origin);
if (!surface) return undefined;
if (
surface === 'operational_trust_attention' ||
surface === 'patrol_control' ||
surface === 'pulse_patrol' ||
surface === 'patrol'
) {
return 'From Patrol';
}
if (isPatrolActionOrigin(surface)) return 'From Patrol';
if (surface === 'pulse_assistant' || surface === 'assistant' || surface === 'chat') {
return 'From Assistant';
}
@@ -39,6 +42,21 @@ export const getActionOriginLabel = (origin?: ActionAuditOrigin): string | undef
return 'From Pulse';
};
export interface ActionOriginDestination {
href: string;
exact: boolean;
}
export const getActionOriginDestination = (
origin?: ActionAuditOrigin,
): ActionOriginDestination | undefined => {
if (!isPatrolActionOrigin(getActionOriginSurface(origin))) return undefined;
const operationalRecordId = origin?.operationalRecordId?.trim();
return operationalRecordId
? { href: buildPatrolAttentionPath(operationalRecordId), exact: true }
: { href: PATROL_PATH, exact: false };
};
export interface ActionInboxStatePresentation {
accentClass: string;
label: string;
+2
View File
@@ -283,6 +283,8 @@ export interface ActionAuditOrigin {
findingId?: string;
investigationId?: string;
proposalId?: string;
operationalRecordId?: string;
evidenceIds?: string[];
}
export interface PatrolActionReference {