mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
fix(actions): link reviews back to Patrol records
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -283,6 +283,8 @@ export interface ActionAuditOrigin {
|
||||
findingId?: string;
|
||||
investigationId?: string;
|
||||
proposalId?: string;
|
||||
operationalRecordId?: string;
|
||||
evidenceIds?: string[];
|
||||
}
|
||||
|
||||
export interface PatrolActionReference {
|
||||
|
||||
Reference in New Issue
Block a user