Label Fleet alert provenance and distrust cached identity claims

This commit is contained in:
archipelago
2026-10-07 20:59:18 -04:00
parent ed94a46b07
commit ff5220869c
4 changed files with 89 additions and 9 deletions
@@ -165,3 +165,25 @@ The exact app-project typecheck also passed again after the history follow-up,
with the same 2 GB bound; log `/tmp/archy-fleet-history-typecheck.log`.
Completed logs and browser fixture sources are archived at
`~/.local/state/archipelago/release-qualification/fleet-acceptance-20261007/`.
## Alert provenance and cache migration follow-up
Alert rows now reuse the same explicit provenance label as node cards, with a
wrapping header for narrow displays. Collector alerts do not inherit trust from
the claimed/truncated node ID. Existing cached node and alert provenance is
stripped on read: historical unsigned collector responses could supply those
fields. Content, metrics and staleness remain available; only a fresh corrected
backend response may restore authenticated labels. Invalid cached alert rows
are discarded. Regression checks cover cached claimed trust and a later fresh
response, as well as actual alert-panel labels. This does not authenticate a
collector identity or replace the required backend gate.
The unchanged combined baseline passed **1,459 tests in 177 files** from a frozen
771-input copy, all hashes unchanged, at
`~/.local/state/archipelago/release-qualification/fleet-combined-ui-20261007/`.
The subsequent three-file alert/cache delta passed **31 focused tests in five
files** and the exact app-project typecheck (2 GB cap, exit 0), logs
`/tmp/archy-fleet-alert-cache-corrected.log` and
`/tmp/archy-fleet-alert-cache-typecheck.log`. This is full baseline plus focused
delta evidence, not a full-suite rerun after the delta. No production deployment
or additional artwork investigation occurred.
+3 -2
View File
@@ -20,8 +20,9 @@
:class="alertSeverityDot(alert.rule)"
></span>
<div class="flex-1 min-w-0">
<div class="flex items-center gap-2 mb-0.5">
<div class="flex flex-wrap items-center gap-x-2 gap-y-1 mb-0.5">
<span class="fleet-node-badge">{{ alert.node_id.slice(0, 8) }}</span>
<span class="text-xs text-white/50">{{ fleetSourceLabel(alert) }}</span>
<span class="text-xs text-white/40">{{ alertTypeLabel(alert.rule) }}</span>
</div>
<p class="text-sm text-white/80">{{ alert.message }}</p>
@@ -33,7 +34,7 @@
</template>
<script setup lang="ts">
import { type FleetAlert, alertSeverityDot, alertTypeLabel, formatTimestamp } from './useFleetData'
import { type FleetAlert, alertSeverityDot, alertTypeLabel, formatTimestamp, fleetSourceLabel } from './useFleetData'
defineProps<{
alerts: FleetAlert[]
@@ -41,7 +41,7 @@ it('retains previous alerts on malformed refresh without calling them current',
const wrapper = mount(defineComponent({ setup() { fleet = useFleetData(); return () => null } }))
try {
await flushPromises()
expect(fleet.fleetAlerts.value).toEqual([alert])
expect(fleet.fleetAlerts.value).toMatchObject([alert])
expect(fleet.alertsErrorMessage.value).toContain('last received alerts')
const panel = mount(FleetAlerts, { props: { alerts: fleet.fleetAlerts.value, alertsLoading: false, errorMessage: fleet.alertsErrorMessage.value } })
expect(panel.text()).toContain('Earlier alert')
@@ -111,3 +111,46 @@ it('reports explicitly unavailable history and clears the message when changing
expect(fleet.nodeHistory.value[0]?.timestamp).toBe('legacy-available')
} finally { wrapper.unmount() }
})
it('labels alert provenance without treating an unsigned collector as a trusted node', () => {
const common = { rule: 'cpu_high', message: 'Fixture alert', timestamp: '2026-10-07T12:00:00Z' }
const panel = mount(FleetAlerts, { props: { alertsLoading: false, alerts: [
{ ...common, node_id: 'collector', source: 'collector', trust_level: 'unverified', identity_authenticated: false },
{ ...common, node_id: 'trusted', source: 'federation', trust_level: 'trusted', identity_authenticated: true },
{ ...common, node_id: 'legacy', source: 'federation', trust_level: 'trusted' },
] } })
try {
const rows = panel.findAll('.fleet-node-badge').map(badge => badge.element.parentElement!.textContent)
expect(rows[0]).toContain('Collector · identity unverified')
expect(rows[1]).toContain('Trusted federation')
expect(rows[2]).toContain('Source unverified')
expect(rows[2]).not.toContain('Trusted federation')
} finally { panel.unmount() }
})
it('does not authenticate cached report claims before a fresh server response', async () => {
const provenance = { source: 'federation', trust_level: 'trusted', identity_authenticated: true }
sessionStorage.setItem('archipelago.fleet.cache.v2', JSON.stringify({
nodes: [{ node_id: 'cached-node', cpu_pct: 0, ...provenance }],
fleetAlerts: [null, { node_id: 'claimed-id', rule: 'cpu_high', message: 'Saved alert', timestamp: '', ...provenance }],
}))
vi.mocked(rpcClient.call).mockRejectedValue(new Error('Unavailable'))
let fleet!: ReturnType<typeof useFleetData>
const wrapper = mount(defineComponent({ setup() { fleet = useFleetData(); return () => null } }))
try {
await flushPromises()
expect(fleet.nodes.value[0]).toMatchObject({ cpu_pct: 0, source: 'unknown', trust_level: 'unverified', identity_authenticated: false })
expect(fleet.fleetAlerts.value).toHaveLength(1)
expect(fleet.fleetAlerts.value[0]).toMatchObject({ message: 'Saved alert', source: 'unknown', trust_level: 'unverified', identity_authenticated: false })
const panel = mount(FleetAlerts, { props: { alerts: fleet.fleetAlerts.value, alertsLoading: false } })
expect(panel.text()).toContain('Source unverified')
expect(panel.text()).not.toContain('Trusted federation')
panel.unmount()
vi.mocked(rpcClient.call).mockResolvedValue({ nodes: [{ node_id: 'cached-node', ...provenance }], alerts: [{ node_id: 'claimed-id', rule: 'cpu_high', message: 'Fresh alert', timestamp: '', ...provenance }] })
await fleet.refreshAll()
expect(fleet.nodes.value[0]?.identity_authenticated).toBe(true)
expect(fleet.fleetAlerts.value[0]?.identity_authenticated).toBe(true)
} finally { wrapper.unmount() }
})
+20 -6
View File
@@ -29,6 +29,9 @@ export interface FleetNode {
}
export interface FleetAlert {
source?: string
trust_level?: string
identity_authenticated?: boolean
node_id: string
rule: string
message: string
@@ -140,7 +143,7 @@ export function fleetNodeDisplayName(node: FleetNode): string {
return name || node.node_id.slice(0, 8)
}
export function fleetSourceLabel(node: FleetNode): string {
export function fleetSourceLabel(node: Pick<FleetNode, 'source' | 'trust_level' | 'identity_authenticated'>): string {
if (node.source === 'collector') return 'Collector · identity unverified'
if (node.source === 'federation' && node.trust_level === 'trusted' && node.identity_authenticated === true) return 'Trusted federation'
return 'Source unverified'
@@ -237,6 +240,19 @@ type FleetCache = {
const FLEET_CACHE_KEY = 'archipelago.fleet.cache.v2'
function isFleetAlert(alert: unknown): alert is FleetAlert {
if (!alert || typeof alert !== 'object') return false
const value = alert as Partial<FleetAlert>
return typeof value.node_id === 'string' && typeof value.rule === 'string' &&
typeof value.message === 'string' && typeof value.timestamp === 'string'
}
function withoutCachedProvenance<T extends object>(report: T) {
// Historical collector payloads could claim provenance. Only a fresh
// corrected server response may assign authenticated identity labels.
return { ...report, source: 'unknown', trust_level: 'unverified', identity_authenticated: false }
}
function readFleetCache(): Partial<FleetCache> {
if (typeof window === 'undefined') return {}
try {
@@ -244,8 +260,8 @@ function readFleetCache(): Partial<FleetCache> {
if (!raw) return {}
const parsed = JSON.parse(raw) as Partial<FleetCache>
return {
nodes: Array.isArray(parsed.nodes) ? parsed.nodes.map(normalizeFleetNode) : [],
fleetAlerts: Array.isArray(parsed.fleetAlerts) ? parsed.fleetAlerts : [],
nodes: Array.isArray(parsed.nodes) ? parsed.nodes.map(normalizeFleetNode).map(withoutCachedProvenance) : [],
fleetAlerts: Array.isArray(parsed.fleetAlerts) ? parsed.fleetAlerts.filter(isFleetAlert).map(withoutCachedProvenance) : [],
lastRefreshed: typeof parsed.lastRefreshed === 'string' ? parsed.lastRefreshed : '',
selectedNodeId: typeof parsed.selectedNodeId === 'string' ? parsed.selectedNodeId : null,
sortBy: parsed.sortBy === 'last-seen' || parsed.sortBy === 'name' ? parsed.sortBy : 'status',
@@ -393,9 +409,7 @@ export function useFleetData() {
method: 'telemetry.fleet-alerts',
})
if (disposed || request !== alertsRequest) return
if (!Array.isArray(data?.alerts) || data.alerts.some(alert => !alert ||
typeof alert.node_id !== 'string' || typeof alert.rule !== 'string' ||
typeof alert.message !== 'string' || typeof alert.timestamp !== 'string')) {
if (!Array.isArray(data?.alerts) || !data.alerts.every(isFleetAlert)) {
throw new Error('Invalid fleet alerts response')
}
{