/** * Hook-jail regression — a host that used to jail its hooks and has stopped. * * Design D8's three states want three different things (see * openspec/changes/hook-process-boundary/design.md): jailed is normal and * raises nothing; a host that never jailed (a Mac) is a configuration fact * that `celilo system doctor` reports and is NOT an event; a host that ran * hooks jailed yesterday and runs them unjailed today is a kernel upgrade, an * AppArmor policy change, or a changed container profile, and is exactly the * failure that looks like nothing. Only the third state raises here. * * The state this reads is `hook-jail-mode.json`, written by `recordJailMode` * on every real module-hook run. The transition survives the record being * overwritten because an unjailed record carries `lastJailed` — the jailed * record it replaced on the same host. * * The monitor scheduling this check is a SELF-monitor: `celilo monitor add * hook_jail` creates it `suppressible: false`, so per * openspec/specs/alerting/spec.md no ancestor alert and no deploy window can * silence it. Losing confinement during a broad outage is precisely when it * must still page. * * Deliberately cheap: one local file read, no remote contact, safe on every * sweep. */ import type { AlertSeverity } from '../../db/schema'; import type { JailModeRecord } from '../../hooks/jail'; import { type FailingKey, builtinAlertKey } from './keys'; export const HOOK_JAIL_CHECK = 'hook_jail'; export interface HookJailState { /** The recorded mode, or undefined when no hook has recorded one yet. */ record: JailModeRecord | undefined; /** The host this check runs on, compared against the record's own. */ host: string; } /** * One failing key when this host used to jail and has stopped; empty * otherwise. The message names the host and the reason, which is what the * security-model spec requires the alert to carry. * * A record written by another host is ignored rather than compared: a database * or data directory restored onto a new box is a move, not a regression, and * the new box has its own history to make. */ export function hookJailFailingKeys(state: HookJailState, severity: AlertSeverity): FailingKey[] { const { record, host } = state; if (!record || record.host !== host) return []; if (record.mode !== 'unjailed' || !record.lastJailed) return []; return [ { key: builtinAlertKey(HOOK_JAIL_CHECK, 'host', host), severity, message: `Hooks on ${host} ran jailed (${record.lastJailed.backend}) until ` + `${record.lastJailed.recordedAt} and now run unjailed: ` + `${record.reason ?? 'no reason was recorded'}`, details: 'A host that quietly stops confining hooks is a failure that otherwise\n' + 'looks like nothing — the usual causes are a kernel upgrade, an AppArmor\n' + 'policy change, or a changed container profile. `celilo system doctor`\n' + 'reports what the host can do right now. The alert resolves when a hook\n' + 'runs jailed on this host again.', }, ]; }