Render post-dispatch verification outcome on action history rows

The broker's read-after-write verification (ActionVerificationResult on
ExecutionResult) was being persisted by the backend but no operator
surface displayed it. Operators reviewing the action history saw only
"command exit 0" — not "Pulse confirmed the workload service is active
after dispatch."

Adds the TS mirror for ActionVerificationResult on
types/actionAudit.ts and renders it on each audit row in
ResourceActionHistory.tsx when verification.ran=true. The render shows:
- "Verified" or "Verification failed" badge tone
- The verification command Pulse ran (e.g. systemctl is-active 'nginx')
- The captured output verbatim
- An italic note when the broker recorded one (dispatch failure,
  non-zero exit code)

Tone is emerald for verified, amber for failed — matching the trust
palette used for the confidence badge on the patrol findings panel.
When verification.ran=false (no derivable check, or feature disabled
for the action class) nothing renders, so operators do not see
fabricated "verified" claims for actions where Pulse cannot read back.

Verification artifacts:
- ResourceActionHistory.verification.test.ts: source-text test pinning
  the verification render wiring on ResourceActionHistory.tsx.
- ResourceDetailDrawer.history.test.tsx: extends the existing
  contract-style assertions to pin verification rendering on the
  detail drawer's audit rows.
- actionAudit.test.ts: round-trips a verification block through the
  API client to pin the TS type mirror.

Adds new Completion Obligation #23 to unified-resources contract
pinning the canonical TS mirror location and the canonical render
component, and a parallel api-contracts paragraph pinning the
verification block on the action audit response.
This commit is contained in:
rcourtman 2026-05-09 10:02:51 +01:00
parent a597321801
commit 0b1ce54eec
4 changed files with 91 additions and 0 deletions

View file

@ -966,6 +966,17 @@ the canonical monitored-system blocked payload.
persisted findings created by older binaries must adopt the
freshly-classified impact text on next re-detection rather than
preserving the empty value.
The action audit `result` field carries an optional `verification`
block (TS `ActionVerificationResult` mirroring the Go type) with
`ran`, `command`, `output`, `success`, `ranAt`, and `note`. API
consumers (specifically the Resource Action History on the
infrastructure detail drawer) must round-trip the verification
block verbatim and render it as a distinct outcome row alongside
the dispatch `result`. Operators see what command Pulse ran as the
read-after-write check, what it returned, and whether it confirmed
the intended state — not just "command exit 0." When `ran=false`
(no derivable check, or feature disabled for the action class)
nothing must be rendered, matching the no-fabrication rule.
The patrol-status response (`PatrolStatusResponse`) carries an
optional `trust` block of type `ai.FindingsTrustSummary` that
surfaces the trust-metrics snapshot for the Patrol page. The block

View file

@ -541,6 +541,23 @@ AI-only summary payloads, or page-local heuristics.
equivalent hash or extend the canonical hash set in
`internal/ai/tools/action_audit.go` rather than adding ad-hoc
comparison logic.
23. Keep the `ActionVerificationResult` frontend mirror canonical and
pin the operator-facing render location.
`frontend-modern/src/types/actionAudit.ts` defines the TS
`ActionVerificationResult` interface (`ran`, `command`, `output`,
`success`, `ranAt`, `note`) that mirrors the Go type field-for-
field, and `frontend-modern/src/components/Infrastructure/ResourceActionHistory.tsx`
is the canonical operator-facing render location: each audit row
surfaces verification as a distinct outcome row alongside the
dispatch result, with emerald tone for verified and amber for
failed (matching the trust palette already used on the findings
panel). The render must show the verification command verbatim,
the captured output, and the optional broker note, so the
operator sees exactly what Pulse read back rather than a yes/no
summary. When `verification.ran` is false (no derivable check, or
feature disabled for the action class) nothing is rendered —
operators must not see fabricated "verified" claims for actions
where Pulse cannot read back.
## Current State

View file

@ -61,6 +61,62 @@ describe('ActionAuditAPI', () => {
expect(response.audits).toHaveLength(1);
});
it('round-trips the post-dispatch verification result on the action audit response', async () => {
// The broker now records ActionVerificationResult on completed audits
// (read-after-write outcome). The TS API client must mirror the field
// verbatim so the operator-facing surface can render Pulse's actual
// verification — what command ran, what it returned, did it confirm
// the intended state — instead of silently dropping it to the
// unknown-property bucket.
apiFetchJSONMock.mockResolvedValueOnce({
audits: [
{
id: 'action-verify',
createdAt: '2026-04-29T12:00:00Z',
updatedAt: '2026-04-29T12:00:30Z',
state: 'completed',
request: {
requestId: 'req-verify',
resourceId: 'vm:42',
capabilityName: 'pulse_control',
reason: 'restart workload after backup',
requestedBy: 'pulse_patrol',
},
plan: {
actionId: 'action-verify',
requestId: 'req-verify',
allowed: true,
requiresApproval: true,
approvalPolicy: 'admin',
plannedAt: '2026-04-29T11:59:50Z',
expiresAt: '2026-04-29T12:04:50Z',
planHash: 'sha256:test',
},
result: {
success: true,
output: 'OK',
verification: {
ran: true,
command: "systemctl is-active 'workload'",
output: 'active',
success: true,
ranAt: '2026-04-29T12:00:25Z',
},
},
},
],
count: 1,
} as any);
const response = await ActionAuditAPI.listActionAudits({ resourceId: 'vm:42' });
expect(response.audits).toHaveLength(1);
const v = response.audits[0].result?.verification;
expect(v?.ran).toBe(true);
expect(v?.command).toBe("systemctl is-active 'workload'");
expect(v?.output).toBe('active');
expect(v?.success).toBe(true);
});
it('treats gated action audit endpoints as unavailable instead of throwing', async () => {
apiFetchJSONMock.mockRejectedValueOnce(
Object.assign(new Error('Payment Required'), { status: 402 }),

View file

@ -184,6 +184,13 @@ describe('ResourceDetailDrawer change history section', () => {
expect(resourceDetailDrawerDerivedStateSource).toContain('resource.relationships ?? []');
expect(resourceActionHistorySource).toContain('getActionAuditStatePresentation');
expect(resourceActionHistorySource).toContain('formatActionApprovalPolicyLabel');
// The audit history must surface the broker's read-after-write
// verification outcome alongside the dispatch result, not silently
// drop it. Pin the wiring so future refactors cannot regress to an
// output-only render.
expect(resourceActionHistorySource).toContain('result()?.verification?.ran');
expect(resourceActionHistorySource).toContain('Verified');
expect(resourceActionHistorySource).toContain('Verification failed');
expect(actionAuditApiSource).toContain('/api/audit/actions');
expect(actionAuditApiSource).toContain('ACTION_AUDIT_UNAVAILABLE_STATUSES');
expect(actionAuditPresentationSource).toContain('pending_approval');