Simplify and harden verified Patrol receipts

Contract-Neutral: Patrol compact receipt payload does not change agent lifecycle or storage recovery contracts
This commit is contained in:
rcourtman
2026-08-15 10:25:04 +01:00
parent d3a345bd66
commit 2fd184315d
12 changed files with 121 additions and 63 deletions
+6 -8
View File
@@ -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
@@ -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:
@@ -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,
@@ -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
@@ -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
+10 -10
View File
@@ -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",
@@ -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;
@@ -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() {
>
<ol class="divide-y divide-border" aria-label="Verified Patrol receipts">
<For each={receipts()}>
{(item) => (
<li class="flex items-start gap-3 px-4 py-3 sm:px-5">
<CheckCircleIcon
class="mt-0.5 h-5 w-5 shrink-0 text-emerald-500"
aria-hidden="true"
/>
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-x-2 gap-y-1">
<span class="text-xs font-semibold uppercase tracking-wide text-emerald-700 dark:text-emerald-300">
Verified
</span>
<span class="text-xs text-muted">
{formatRelativeTime(item.verifiedAt, { compact: true })}
</span>
{(item) => {
const resolvedName = item.resourceName.trim();
const resource = getActionResourcePresentation(
item.resourceId,
resolvedName && resolvedName !== item.resourceId.trim()
? {
id: item.resourceId,
name: resolvedName,
type: item.resourceType ?? '',
}
: undefined,
);
return (
<li class="flex items-start gap-3 px-4 py-3 sm:px-5">
<CheckCircleIcon
class="mt-0.5 h-5 w-5 shrink-0 text-emerald-500"
aria-hidden="true"
/>
<div class="min-w-0 flex-1">
<div class="flex flex-wrap items-center gap-x-2 gap-y-1">
<span class="text-xs font-semibold uppercase tracking-wide text-emerald-700 dark:text-emerald-300">
Verified
</span>
<span class="text-xs text-muted">
{formatRelativeTime(item.verifiedAt, { compact: true })}
</span>
</div>
<p class="mt-1 text-sm font-semibold text-base-content">
{formatActionName(item.capabilityName)} completed
</p>
<p class="mt-1 text-xs leading-5 text-muted">
{verificationTruthLabel('confirmed', item.evidenceClass)}
</p>
<p class="mt-1 truncate text-xs font-medium text-base-content">
{resource.label}
<Show when={resource.detail}>
{(detail) => <span class="text-muted"> · {detail()}</span>}
</Show>
</p>
</div>
<p class="mt-1 text-sm font-semibold text-base-content">
{formatActionName(item.capabilityName)} verified
</p>
<p class="mt-1 text-xs leading-5 text-muted">{item.verificationSummary}</p>
<p class="mt-1 truncate text-xs font-medium text-base-content">
{item.resourceName}
</p>
</div>
</li>
)}
</li>
);
}}
</For>
</ol>
</Show>
@@ -20,7 +20,6 @@ const receipt = (overrides: Partial<PatrolWorkReceipt> = {}): 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(() => <PatrolRecentWorkPanel />);
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(() => <PatrolRecentWorkPanel />);
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(() => <PatrolRecentWorkPanel />);
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();
});
});
+10 -1
View File
@@ -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,
-9
View File
@@ -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),
@@ -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")),