From 2fd184315daf503488da333ae7a05c55cb956bca Mon Sep 17 00:00:00 2001 From: rcourtman Date: Sat, 15 Aug 2026 10:25:04 +0100 Subject: [PATCH] Simplify and harden verified Patrol receipts Contract-Neutral: Patrol compact receipt payload does not change agent lifecycle or storage recovery contracts --- .husky/pre-commit | 14 ++-- .../CANONICAL_DEVELOPMENT_PROTOCOL.md | 2 + .../v6/internal/subsystems/api-contracts.md | 5 +- .../subsystems/frontend-primitives.md | 8 ++- .../subsystems/patrol-intelligence.md | 4 ++ frontend-modern/browser-verification.json | 20 +++--- frontend-modern/src/api/patrolAttention.ts | 1 - .../features/patrol/PatrolRecentWorkPanel.tsx | 72 ++++++++++++------- .../__tests__/PatrolRecentWorkPanel.test.tsx | 29 ++++++-- internal/api/attention_handlers_test.go | 11 ++- internal/api/attention_receipts.go | 9 --- .../browser_verification_guard_test.py | 9 +++ 12 files changed, 121 insertions(+), 63 deletions(-) diff --git a/.husky/pre-commit b/.husky/pre-commit index 6205332b5..d6fce3b3a 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -67,6 +67,12 @@ staged_files_match() { printf '%s\n' "$STAGED_FILES" | grep -qE "$1" } +# Normalize staged source before any guard hashes or audits it. The browser +# verification receipt binds to exact frontend bytes, so formatting after its +# guard can produce a commit that passed locally but correctly fails in CI. +python3 scripts/release_control/format_staged_go.py +python3 scripts/release_control/format_staged_frontend.py + echo "Running browser verification guard..." python3 scripts/release_control/browser_verification_guard.py @@ -157,14 +163,6 @@ else echo "Governance files not present, skipping governance checks." fi - -# Run Go formatting -python3 scripts/release_control/format_staged_go.py - -# Run frontend formatting (prettier over staged frontend-modern/src files, -# index-directly like the Go formatter; skips if prettier is not installed) -python3 scripts/release_control/format_staged_frontend.py - # Run Go linting (if golangci-lint is available) if command -v golangci-lint >/dev/null 2>&1; then if staged_files_match '(^|/).*\.go$|(^|/)go\.(mod|sum|work|work\.sum)$|^\.golangci\.ya?ml$'; then diff --git a/docs/release-control/v6/internal/CANONICAL_DEVELOPMENT_PROTOCOL.md b/docs/release-control/v6/internal/CANONICAL_DEVELOPMENT_PROTOCOL.md index d9705d9bf..f7f7542c9 100644 --- a/docs/release-control/v6/internal/CANONICAL_DEVELOPMENT_PROTOCOL.md +++ b/docs/release-control/v6/internal/CANONICAL_DEVELOPMENT_PROTOCOL.md @@ -538,6 +538,8 @@ Local formatter steps must also stay scoped to staged files so the hook does not mutate unrelated dirty worktree state. When formatting staged Go files, the formatter must operate on staged blobs in the git index rather than rewriting the whole repo or restaging whole files. +Formatters must run before any staged-content audit that hashes or otherwise +binds evidence to exact bytes, including the browser verification guard. For runtime subsystem changes, the same commit must now include: diff --git a/docs/release-control/v6/internal/subsystems/api-contracts.md b/docs/release-control/v6/internal/subsystems/api-contracts.md index 5248667a3..3c2a26622 100644 --- a/docs/release-control/v6/internal/subsystems/api-contracts.md +++ b/docs/release-control/v6/internal/subsystems/api-contracts.md @@ -7422,7 +7422,10 @@ action offers. `GET /api/ai/patrol/attention/receipts` is the canonical recent work source for both autonomous Patrol actions and Operational Trust attention actions. It admits only completed execution with a confirmed postcondition and therefore never returns pending, failed, contradicted, inconclusive, rejected, -or unrelated-origin work as a receipt. The +or unrelated-origin work as a receipt. The compact payload carries the action, +resource reference, verification time, and evidence class; raw executor and +readback summaries remain on the full action-audit contract instead of leaking +transport-shaped detail into the quiet Patrol home. The frontend may use action eligibility and approval fields to decide whether an item needs another operator decision under the effective autonomy level, but that projection grants no action authority and cannot override server planning, diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index c1b84efc2..1c028036b 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -781,9 +781,11 @@ daily-page primitive and must not be added beside the effective mode. The compact `Recently handled` list consumes server-authored Patrol work receipts rather than filtering generic history in the browser. It may format the canonical capability name and verification time with shared presentation -helpers, but it must preserve the server's verified-only membership, evidence -summary, newest-first order, empty state, and last-truthful-data behavior during -a refresh error. +helpers, and it reuses the action resource presenter for durable resources that +have left the live registry. It must preserve the server's verified-only +membership, evidence class, newest-first order, empty state, and +last-truthful-data behavior during a refresh error. Raw executor verification +summaries stay in Activity history rather than becoming quiet-home copy. For Patrol, that includes the Open work description: it may use concise row-level guidance such as review evidence, approve a change, inspect automatic actions, or review verification results, but it must remain descriptive copy diff --git a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md index 76a6a170c..9a9c4f695 100644 --- a/docs/release-control/v6/internal/subsystems/patrol-intelligence.md +++ b/docs/release-control/v6/internal/subsystems/patrol-intelligence.md @@ -103,6 +103,10 @@ inconclusive, rejected, unknown, unavailable, and unrelated-origin work must never be styled as handled work. Objective summary language similarly counts protection only from active objectives whose server-authored coverage state is `covered`. +Compact receipts present only the verified action, verification trust class, +time, and shared resource identity. Raw executor and readback summaries remain +available in Activity history; they must not be copied into the quiet Patrol +home or force an operator to interpret transport and lifecycle telemetry. The effective autonomy label and plain-language consequence stay visible, but the four-level selector is a secondary disclosure. A plan-locked daily operator diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 2783f66a4..8ce58a726 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,17 +1,15 @@ { "version": 1, - "base_sha": "7fb39f65cf25a3fc822f5795abc99dd670e93a48", - "verified_at": "2026-08-15T07:42:09Z", + "base_sha": "d3a345bd66d635b59427cdfb06bd030a053daded", + "verified_at": "2026-08-15T09:17:52Z", "result": "passed", "changed_paths": [ "frontend-modern/src/api/patrolAttention.ts", - "frontend-modern/src/features/patrol/PatrolRecentWorkPanel.tsx", - "frontend-modern/src/features/patrol/patrolHomePresentation.ts" + "frontend-modern/src/features/patrol/PatrolRecentWorkPanel.tsx" ], "content_sha256": { - "frontend-modern/src/api/patrolAttention.ts": "2038e240e586d096f289e477e05c0acdb9d33eb08fe5791dd8080666a43350df", - "frontend-modern/src/features/patrol/PatrolRecentWorkPanel.tsx": "c996100b27f7101a2a64c742ff0c1602cb2017397afad22a3da5e8e768ac8798", - "frontend-modern/src/features/patrol/patrolHomePresentation.ts": "41991e1b605df792768237e488edf0d1f8c1190896e5be865dfbd5f24017e9aa" + "frontend-modern/src/api/patrolAttention.ts": "035e201a4c9a67c230f67004113b632c8e9ee82edf36bea0acaf86d5b9725b0e", + "frontend-modern/src/features/patrol/PatrolRecentWorkPanel.tsx": "b62d2cd4ccd5c2da4442a54d04863047f667fa5f076975046d3c2a0dc15e560e" }, "routes": [ "/patrol" @@ -27,14 +25,16 @@ } ], "states": [ - "Recently handled populated with six newest server-filtered verified Patrol receipts", + "Recently handled populated with six newest server-filtered verified Patrol receipts in calm operator language", + "Raw executor lifecycle telemetry and full opaque resource ids are absent from the compact receipt list", + "Executing-agent and independent-observer trust labels render from the canonical evidence class", "Refresh failure keeps the last truthful receipts visible with an explicit unavailable message", "Refresh recovery clears the error and reloads the same six verified receipts", "Activity history handoff remains adjacent to verified receipts", - "Phone-width receipt list wraps verification evidence without horizontal overflow" + "Phone-width receipt list stays within the 390-pixel document width" ], "interactions": [ - "refreshed the populated Recently handled list", + "refreshed the populated Recently handled list and confirmed compact action, trust, and resource presentation", "stopped the disposable server and refreshed to exercise the error state", "confirmed six existing receipts remained visible during the refresh error", "restored the healthy server and refreshed to clear the error", diff --git a/frontend-modern/src/api/patrolAttention.ts b/frontend-modern/src/api/patrolAttention.ts index b7b5347aa..88b7f4799 100644 --- a/frontend-modern/src/api/patrolAttention.ts +++ b/frontend-modern/src/api/patrolAttention.ts @@ -104,7 +104,6 @@ export interface PatrolWorkReceipt { resourceType?: string; capabilityName: string; verifiedAt: string; - verificationSummary: string; evidenceClass: 'none' | 'agent_attested' | 'independent'; originSurface: 'patrol' | 'operational_trust_attention'; findingId?: string; diff --git a/frontend-modern/src/features/patrol/PatrolRecentWorkPanel.tsx b/frontend-modern/src/features/patrol/PatrolRecentWorkPanel.tsx index 69c704e01..0bbaf269e 100644 --- a/frontend-modern/src/features/patrol/PatrolRecentWorkPanel.tsx +++ b/frontend-modern/src/features/patrol/PatrolRecentWorkPanel.tsx @@ -3,7 +3,11 @@ import CheckCircleIcon from 'lucide-solid/icons/circle-check'; import RefreshIcon from 'lucide-solid/icons/refresh-cw'; import { getPatrolWorkReceipts, type PatrolWorkReceipt } from '@/api/patrolAttention'; import { Button } from '@/components/shared/Button'; -import { formatActionName } from '@/features/actions/actionPresentation'; +import { + formatActionName, + getActionResourcePresentation, + verificationTruthLabel, +} from '@/features/actions/actionPresentation'; import { formatRelativeTime } from '@/utils/format'; const RECEIPT_LIMIT = 6; @@ -102,31 +106,49 @@ export function PatrolRecentWorkPanel() { >
    - {(item) => ( -
  1. -
  2. +
  3. - )} + + ); + }}
diff --git a/frontend-modern/src/features/patrol/__tests__/PatrolRecentWorkPanel.test.tsx b/frontend-modern/src/features/patrol/__tests__/PatrolRecentWorkPanel.test.tsx index bfdece216..97bc6249c 100644 --- a/frontend-modern/src/features/patrol/__tests__/PatrolRecentWorkPanel.test.tsx +++ b/frontend-modern/src/features/patrol/__tests__/PatrolRecentWorkPanel.test.tsx @@ -20,7 +20,6 @@ const receipt = (overrides: Partial = {}): PatrolWorkReceipt resourceName: 'Jellyfin', capabilityName: 'restart', verifiedAt: '2026-08-14T07:05:00Z', - verificationSummary: 'Playback recovered.', evidenceClass: 'agent_attested', originSurface: 'patrol', findingId: 'finding-1', @@ -42,12 +41,32 @@ describe('PatrolRecentWorkPanel', () => { render(() => ); - expect(await screen.findByText('Playback recovered.')).toBeInTheDocument(); + expect(await screen.findByText('Confirmed by executing agent')).toBeInTheDocument(); expect(screen.getAllByText('Verified')).toHaveLength(1); - expect(screen.getByText('Restart verified')).toBeInTheDocument(); + expect(screen.getByText('Restart completed')).toBeInTheDocument(); + expect(screen.getByText('Jellyfin')).toBeInTheDocument(); expect(apiMocks.getReceipts).toHaveBeenCalledWith(6); }); + it('turns a retired opaque resource id into a calm resource label', async () => { + apiMocks.getReceipts.mockResolvedValue( + response([ + receipt({ + resourceId: 'app-container-4c19e723fbfae98b', + resourceName: 'app-container-4c19e723fbfae98b', + evidenceClass: 'independent', + }), + ]), + ); + + render(() => ); + + expect(await screen.findByText('App container')).toBeInTheDocument(); + expect(screen.getByText(/…fae98b/)).toBeInTheDocument(); + expect(screen.getByText('Confirmed by independent observer')).toBeInTheDocument(); + expect(screen.queryByText('app-container-4c19e723fbfae98b')).not.toBeInTheDocument(); + }); + it('does not imply successful work when there is no verified receipt', async () => { apiMocks.getReceipts.mockResolvedValue(response([])); @@ -64,12 +83,12 @@ describe('PatrolRecentWorkPanel', () => { apiMocks.getReceipts.mockRejectedValueOnce(new Error('relay unavailable')); render(() => ); - await screen.findByText('Playback recovered.'); + await screen.findByText('Confirmed by executing agent'); document.dispatchEvent(new Event('visibilitychange')); await waitFor(() => expect(screen.getByText('Verified work is unavailable')).toBeInTheDocument(), ); - expect(screen.getByText('Playback recovered.')).toBeInTheDocument(); + expect(screen.getByText('Confirmed by executing agent')).toBeInTheDocument(); }); }); diff --git a/internal/api/attention_handlers_test.go b/internal/api/attention_handlers_test.go index b62621423..cda066f1d 100644 --- a/internal/api/attention_handlers_test.go +++ b/internal/api/attention_handlers_test.go @@ -61,10 +61,19 @@ func TestAttentionHandlersReceiptsUseVerifiedPatrolActionsBeforeLimit(t *testing t.Fatalf("receipt order = %#v", payload.Data) } for _, receipt := range payload.Data { - if receipt.VerificationSummary == "" || receipt.EvidenceClass != unifiedresources.ActionEvidenceAgentAttested { + if receipt.EvidenceClass != unifiedresources.ActionEvidenceAgentAttested { t.Fatalf("receipt omitted verification proof: %#v", receipt) } } + var wirePayload struct { + Data []map[string]any `json:"data"` + } + if err := json.Unmarshal(response.Body.Bytes(), &wirePayload); err != nil { + t.Fatal(err) + } + if _, found := wirePayload.Data[0]["verificationSummary"]; found { + t.Fatalf("compact receipt leaked executor verification summary: %#v", wirePayload.Data[0]) + } limitedRequest := httptest.NewRequest( http.MethodGet, diff --git a/internal/api/attention_receipts.go b/internal/api/attention_receipts.go index 654d14971..5608a3c5f 100644 --- a/internal/api/attention_receipts.go +++ b/internal/api/attention_receipts.go @@ -28,7 +28,6 @@ type patrolWorkReceipt struct { ResourceType unifiedresources.ResourceType `json:"resourceType,omitempty"` CapabilityName string `json:"capabilityName"` VerifiedAt time.Time `json:"verifiedAt"` - VerificationSummary string `json:"verificationSummary"` EvidenceClass unifiedresources.ActionEvidenceClass `json:"evidenceClass"` OriginSurface string `json:"originSurface"` FindingID string `json:"findingId,omitempty"` @@ -129,13 +128,6 @@ func projectPatrolWorkReceipt( if resourceName == "" { resourceName = resourceID } - summary := strings.TrimSpace(truth.Verification.Summary) - if summary == "" { - summary = strings.TrimSpace(record.VerificationOutcome.EvidenceSummary) - } - if summary == "" { - summary = "Patrol confirmed the expected outcome after the change." - } return patrolWorkReceipt{ ActionID: record.ID, ResourceID: resourceID, @@ -143,7 +135,6 @@ func projectPatrolWorkReceipt( ResourceType: resourceType, CapabilityName: strings.TrimSpace(record.Request.CapabilityName), VerifiedAt: record.UpdatedAt.UTC(), - VerificationSummary: summary, EvidenceClass: truth.Verification.EvidenceClass, OriginSurface: originSurface, FindingID: strings.TrimSpace(record.Origin.FindingID), diff --git a/scripts/release_control/browser_verification_guard_test.py b/scripts/release_control/browser_verification_guard_test.py index 1833e9714..483ec1756 100644 --- a/scripts/release_control/browser_verification_guard_test.py +++ b/scripts/release_control/browser_verification_guard_test.py @@ -24,6 +24,7 @@ from repo_file_io import strip_local_git_env BASE_SHA = "a" * 40 CHANGED_PATH = "frontend-modern/src/components/Example.tsx" CONTENT_SHA = "c" * 64 +REPO_ROOT = Path(__file__).resolve().parents[2] def valid_receipt() -> dict: @@ -45,6 +46,14 @@ def valid_receipt() -> dict: class BrowserVerificationGuardTest(unittest.TestCase): + def test_pre_commit_formats_frontend_before_validating_receipt_hashes(self) -> None: + hook = (REPO_ROOT / ".husky" / "pre-commit").read_text(encoding="utf-8") + + formatter = hook.index("python3 scripts/release_control/format_staged_frontend.py") + guard = hook.index("python3 scripts/release_control/browser_verification_guard.py") + + self.assertLess(formatter, guard) + def test_blocks_frontend_change_when_receipt_is_not_in_commit(self) -> None: with ( patch("sys.stdin", StringIO(CHANGED_PATH + "\n")),