feat: add Admiral Hardened Build channel and business assurance surfaces (#1629)

* feat: add Admiral Hardened Build channel and business assurance surfaces

Introduce Studio Saelix entitlement-backed Hardened Build switching, a
single-flight image operation coordinator, Recovery Vault naming, Admiral
Account settings, and typed Fleet update failures while preserving Community
custom-repo and targetless pull-current updates.

* fix: harden image-op paths and clear CI CodeQL/pilot flake

Validate operation IDs before filesystem use, use hostname checks in Fleet
fetch mocks, sanitize registry probe logs, and swallow expected TCP teardown
errors in the pilot reverse-route post-handshake test.

* fix: sanitize image-op docker config write and probe logs

Allowlist-copy registry host keys and base64 auth before writing the
temp DOCKER_CONFIG, and log registry probe failures with a fixed message
so CodeQL no longer flags network-to-file and log-injection mediums.

* fix: address Admiral Hardened Build audit blockers

Expose imageChannel so hardened Fleet peers still POST for typed rejection, claim community updates before 202, terminalize helper failures, gate Hardened on paid, and align support/docs/e2e wording.

* fix: terminalize image ops on helper survival and aborted claims

* fix: prevent recreating persist from overwriting helper-exit failure

* test: assert helper-exit failure lands before recreating persist

* fix: keep current pointer when acknowledging a stale image operation
This commit is contained in:
Anso
2026-07-14 10:47:54 -04:00
committed by GitHub
parent 8ca8ebaa24
commit 381ed2a91f
54 changed files with 2302 additions and 125 deletions
@@ -228,12 +228,12 @@ export function NodeCard({ node, onNavigate, labelMap, updateStatus, onUpdate, u
onDismiss={isAdmin && onDismissUpdate ? () => onDismissUpdate(node.id) : undefined}
/>
)}
{updateStatus?.updateAvailable && !updateStatus.updateStatus && !updateStatus?.skipActive && !updateStatus?.updateBlocked && (
{updateStatus?.updateAvailable && !updateStatus.updateStatus && !updateStatus?.skipActive && !(updateStatus?.updateBlocked && updateStatus?.imageChannel !== 'hardened') && (
<Badge className="text-[10px] px-1.5 py-0 h-4 bg-warning/15 text-warning border-warning/30 shrink-0">
Update available
</Badge>
)}
{updateStatus?.updateBlocked && updateStatus?.updateAvailable && !updateStatus.updateStatus && !updateStatus?.skipActive && (
{(updateStatus?.updateBlocked && updateStatus?.imageChannel !== 'hardened') && updateStatus?.updateAvailable && !updateStatus.updateStatus && !updateStatus?.skipActive && (
<PinnedUpdateBadge reason={updateStatus.updateBlockedReason} />
)}
{updateStatus?.skipActive && (
@@ -314,7 +314,7 @@ export function NodeCard({ node, onNavigate, labelMap, updateStatus, onUpdate, u
)}
{/* Update button (mutating action: admin only, matches the requireAdmin route guard) */}
{isOnline && updateStatus?.updateAvailable && !updateStatus.updateStatus && !updateStatus?.skipActive && !updateStatus?.updateBlocked && onUpdate && isAdmin && (
{isOnline && updateStatus?.updateAvailable && !updateStatus.updateStatus && !updateStatus?.skipActive && !(updateStatus?.updateBlocked && updateStatus?.imageChannel !== 'hardened') && onUpdate && isAdmin && (
<div className="mt-3 pt-3 border-t border-border/50">
<Button
variant="outline"
@@ -435,13 +435,13 @@ export function NodeUpdatesSheet({
Unskip
</Button>
)}
{s.updateBlocked && s.updateAvailable && !s.updateStatus && !s.skipActive && (
{(s.updateBlocked && s.imageChannel !== 'hardened') && s.updateAvailable && !s.updateStatus && !s.skipActive && (
<PinnedUpdateBadge
reason={s.updateBlockedReason}
className="text-[10px] px-1.5 py-0 h-5 bg-muted text-muted-foreground border-card-border/40"
/>
)}
{s.updateAvailable && !s.updateStatus && !s.skipActive && !s.updateBlocked && isAdmin && (
{s.updateAvailable && !s.updateStatus && !s.skipActive && !(s.updateBlocked && s.imageChannel !== 'hardened') && isAdmin && (
<Button
variant="outline"
size="sm"
@@ -467,7 +467,7 @@ export function NodeUpdatesSheet({
Skip
</Button>
)}
{s.updateAvailable && !s.updateStatus && !s.skipActive && !s.updateBlocked && !isAdmin && (
{s.updateAvailable && !s.updateStatus && !s.skipActive && !(s.updateBlocked && s.imageChannel !== 'hardened') && !isAdmin && (
<Badge className="text-[10px] px-1.5 py-0 h-5 bg-warning/15 text-warning border-warning/30">
<CircleAlert className="w-2.5 h-2.5 mr-0.5" /> Available
</Badge>
@@ -147,6 +147,20 @@ describe('useFleetUpdateStatus', () => {
expect(toastSuccess).toHaveBeenCalledWith(expect.stringContaining('2 nodes'));
});
it('reports failed remote nodes separately after an update-all request', async () => {
apiFetchMock.mockResolvedValue(okJson({
updating: [],
skipped: [],
failed: [{ name: 'Edge', error: 'Hardened Build updates require a signed-in admin on that node.' }],
}));
const { result } = renderHook(() => useFleetUpdateStatus());
await act(async () => { await result.current.triggerUpdateAll(); });
expect(toastError).toHaveBeenCalledWith(expect.stringContaining('Edge'));
expect(toastError).toHaveBeenCalledWith(expect.stringContaining('Hardened Build'));
});
it('triggerNodeUpdate on a blocked node toasts and does not POST', async () => {
apiFetchMock.mockResolvedValue(okJson({ nodes: [...STATUSES, BLOCKED_STATUS] }));
const { result } = renderHook(() => useFleetUpdateStatus());
@@ -21,7 +21,9 @@ function parseUpdateError(err: Record<string, unknown>, fallback: string): strin
}
function toastIfUpdateBlocked(status: NodeUpdateStatus | undefined): boolean {
if (!status?.updateBlocked) return false;
// Hardened digests report updateBlocked but still accept a POST so the typed
// HARDENED_REMOTE_UPDATE_UNSUPPORTED path can surface.
if (!status?.updateBlocked || status.imageChannel === 'hardened') return false;
toast.error(status.updateBlockedReason ?? PINNED_UPDATE_BLOCKED_FALLBACK);
return true;
}
@@ -131,12 +133,19 @@ export function useFleetUpdateStatus() {
try {
const res = await apiFetch('/fleet/update-all', { method: 'POST', localOnly: true });
if (res.ok) {
const data = await res.json();
if (data.updating?.length > 0) {
toast.success(`Update initiated on ${data.updating.length} node${data.updating.length > 1 ? 's' : ''}.`);
const data = await res.json() as {
updating?: string[];
failed?: Array<{ name: string; error: string }>;
};
const updating = data.updating ?? [];
if (updating.length > 0) {
toast.success(`Update initiated on ${updating.length} node${updating.length > 1 ? 's' : ''}.`);
} else {
toast.success('All nodes are up to date.');
}
if (data.failed?.length) {
toast.error(`Update could not start on ${data.failed.map(node => node.name).join(', ')}: ${data.failed[0].error}`);
}
fetchUpdateStatus();
} else {
const err = await res.json().catch(() => ({}));
@@ -59,6 +59,8 @@ export interface NodeUpdateStatus {
updateBlocked?: boolean;
/** Human-readable block reason. Local node only. */
updateBlockedReason?: string | null;
/** Coarse image channel from meta/update-status. Hardened digests still POST. */
imageChannel?: 'community' | 'hardened' | 'unknown' | null;
}
export type ViewMode = 'grid' | 'topology';