mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
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:
+6
-8
@@ -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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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")),
|
||||
|
||||
Reference in New Issue
Block a user