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';
@@ -213,8 +213,8 @@ export function ConfigurationStatus({ onOpenSection }: ConfigurationStatusProps
<SectionHeader icon={HardDrive} label="Backups & Thresholds" />
{!backup.locked && (
<Row
label="Cloud Backup"
value={backup.provider === 'disabled' ? 'Disabled' : backup.provider === 'sencho' ? 'Sencho Cloud' : `Custom S3${backup.autoUpload ? ' (auto)' : ''}`}
label="Recovery Vault"
value={backup.provider === 'disabled' ? 'Disabled' : backup.provider === 'sencho' ? 'Recovery Vault' : `Custom S3${backup.autoUpload ? ' (auto)' : ''}`}
onClick={open('cloud-backup')}
/>
)}
@@ -75,8 +75,8 @@ describe('ConfigurationStatus row visibility', () => {
expect(screen.queryByText('Scheduled tasks')).toBeNull();
// Scan policies are free, so the row renders.
expect(screen.getByText('Scan policies')).toBeDefined();
// Cloud Backup row is universal (Custom S3 is open to every tier).
expect(screen.getByText('Cloud Backup')).toBeDefined();
// Recovery Vault row is universal because Custom S3 is open to every tier.
expect(screen.getByText('Recovery Vault')).toBeDefined();
});
it('shows every row when the payload reports nothing locked', () => {
@@ -118,7 +118,7 @@ describe('ConfigurationStatus row visibility', () => {
expect(screen.getByText('Webhooks')).toBeDefined();
expect(screen.getByText('Scheduled tasks')).toBeDefined();
expect(screen.getByText('Scan policies')).toBeDefined();
expect(screen.getByText('Cloud Backup')).toBeDefined();
expect(screen.getByText('Recovery Vault')).toBeDefined();
// SSO label maps the provider to a friendly name.
expect(screen.getByText('Google')).toBeDefined();
});
@@ -62,7 +62,7 @@ const BASE_PROVIDER_OPTIONS = [
{ value: 'custom', label: 'Custom S3 (BYOB)' },
];
const SENCHO_PROVIDER_OPTION = { value: 'sencho', label: 'Sencho Cloud Backup (included)' };
const SENCHO_PROVIDER_OPTION = { value: 'sencho', label: 'Recovery Vault (included)' };
const PANEL_CLASS = 'rounded-lg border border-card-border border-t-card-border-top bg-card shadow-card-bevel p-4 space-y-3';
@@ -215,7 +215,7 @@ export function CloudBackupSection() {
const res = await apiFetch('/cloud-backup/provision', { method: 'POST' });
const data = await res.json().catch(() => ({}));
if (!res.ok || data?.error) throw new Error(data?.error || 'Provisioning failed.');
toast.success('Sencho Cloud Backup activated.');
toast.success('Recovery Vault activated.');
setSenchoProvisioned(true);
await Promise.all([loadConfig(), loadUsage(), loadSnapshots()]);
} catch (err) {
@@ -316,7 +316,7 @@ export function CloudBackupSection() {
<div className={PANEL_CLASS}>
<div className="flex items-center gap-2">
<Cloud className="w-4 h-4 text-muted-foreground" strokeWidth={1.5} />
<span className="font-medium text-sm">Activate Sencho Cloud Backup</span>
<span className="font-medium text-sm">Activate Recovery Vault</span>
</div>
<p className="text-xs text-muted-foreground">
Activates a 500 MB allowance backed by Cloudflare R2, scoped to this Admiral license.
@@ -333,7 +333,7 @@ export function CloudBackupSection() {
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
<CheckCircle2 className="w-4 h-4 text-success" strokeWidth={1.5} />
<span className="font-medium text-sm">Sencho Cloud Backup</span>
<span className="font-medium text-sm">Recovery Vault</span>
</div>
<div className="flex items-center gap-2">
<Button size="sm" variant="outline" onClick={handleTest} disabled={testing}>
@@ -371,7 +371,7 @@ export function CloudBackupSection() {
<div className="flex items-start gap-2 rounded-lg border border-glass-border bg-muted/30 px-3 py-2.5">
<Cloud className="w-4 h-4 text-muted-foreground shrink-0 mt-0.5" strokeWidth={1.5} />
<p className="text-xs text-muted-foreground">
Auto-upload is on for Sencho Cloud Backup. Every fleet snapshot is replicated within seconds.
Auto-upload is on for Recovery Vault. Every fleet snapshot is replicated within seconds.
</p>
</div>
</div>
@@ -1,4 +1,4 @@
import { useMemo, useState } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { toast } from '@/components/ui/toast-store';
@@ -13,8 +13,44 @@ import { SettingsSection } from './SettingsSection';
import { SettingsField } from './SettingsField';
import { SettingsActions, SettingsPrimaryButton } from './SettingsActions';
import { useMastheadStats } from './MastheadStatsContext';
import { ConfirmModal } from '@/components/ui/modal';
const PRICING_URL = 'https://sencho.io/pricing';
const SOURCE_URL = 'https://github.com/Studio-Saelix/sencho';
type ImageChannel = 'community' | 'hardened' | 'unknown';
interface ImageOperation {
operationId: string;
state: 'pending_pull' | 'pulling' | 'patching' | 'recreating' | 'succeeded' | 'failed';
failureCode?: string;
}
interface ImageChannelStatus {
channel: ImageChannel;
composeImageRef?: string;
operation?: ImageOperation | null;
}
interface HardenedPreflight {
preflightFingerprint: string;
currentImageRef: string;
allowedImageRef: string;
composeFilePath: string;
pinKind: string;
localRegistryAccess: string;
}
function formatChannel(channel: ImageChannel): string {
switch (channel) {
case 'community':
return 'Community';
case 'hardened':
return 'Hardened';
default:
return 'Custom';
}
}
function getTierDisplayName(tier?: string, status?: string): string {
if (tier === 'paid' && status === 'trial') return 'Sencho Admiral (Trial)';
@@ -32,6 +68,83 @@ export function LicenseSection() {
const [isActivating, setIsActivating] = useState(false);
const [isDeactivating, setIsDeactivating] = useState(false);
const [billingLoading, setBillingLoading] = useState(false);
const [channelStatus, setChannelStatus] = useState<ImageChannelStatus | null>(null);
const [acknowledging, setAcknowledging] = useState(false);
const [preflight, setPreflight] = useState<HardenedPreflight | null>(null);
const [preflightLoading, setPreflightLoading] = useState(false);
const [switching, setSwitching] = useState(false);
const loadImageChannel = useCallback(async () => {
try {
const res = await apiFetch('/license/image-channel/status', { localOnly: true });
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data?.error || 'Unable to load image channel status.');
setChannelStatus(data as ImageChannelStatus);
} catch (error) {
toast.error((error as Error)?.message || 'Unable to load image channel status.');
}
}, []);
useEffect(() => {
void loadImageChannel();
}, [loadImageChannel]);
const acknowledgeOperation = async () => {
const operation = channelStatus?.operation;
if (!operation || operation.state !== 'failed') return;
setAcknowledging(true);
try {
const res = await apiFetch(`/license/image-channel/operations/${operation.operationId}/acknowledge`, {
method: 'POST',
localOnly: true,
});
if (!res.ok) {
const data = await res.json().catch(() => ({}));
throw new Error(data?.error || 'Unable to acknowledge the failed operation.');
}
toast.success('Failed image operation acknowledged.');
await loadImageChannel();
} catch (error) {
toast.error((error as Error)?.message || 'Unable to acknowledge the failed operation.');
} finally {
setAcknowledging(false);
}
};
const openPreflight = async () => {
setPreflightLoading(true);
try {
const res = await apiFetch('/license/image-channel/preflight', { method: 'POST', localOnly: true });
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data?.error || 'Hardened Build access is unavailable.');
setPreflight(data as HardenedPreflight);
} catch (error) {
toast.error((error as Error)?.message || 'Hardened Build access is unavailable.');
} finally {
setPreflightLoading(false);
}
};
const switchToHardened = async () => {
if (!preflight) return;
setSwitching(true);
try {
const res = await apiFetch('/license/image-channel/switch', {
method: 'POST',
localOnly: true,
body: JSON.stringify({ preflightFingerprint: preflight.preflightFingerprint }),
});
const data = await res.json().catch(() => ({}));
if (!res.ok) throw new Error(data?.error || 'Image channel switch could not start.');
toast.success('Hardened Build switch initiated.');
setPreflight(null);
await loadImageChannel();
} catch (error) {
toast.error((error as Error)?.message || 'Image channel switch could not start.');
} finally {
setSwitching(false);
}
};
const openBillingPortal = async () => {
setBillingLoading(true);
@@ -116,6 +229,55 @@ export function LicenseSection() {
</div>
</SettingsField>
{!isPaid ? (
<SettingsField label="License" helper="Sencho Community is released under AGPLv3.">
<a href={SOURCE_URL} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1.5 text-sm text-brand hover:underline">
View source
<ExternalLink className="h-3.5 w-3.5" />
</a>
</SettingsField>
) : (
<SettingsField label="Recovery Vault" helper="Your Admiral subscription includes Recovery Vault entitlement.">
<span className="inline-flex items-center gap-2 text-sm text-success">
<CheckCircle className="h-4 w-4" />
Included
</span>
</SettingsField>
)}
{isPaid && channelStatus?.channel !== 'hardened' ? (
<SettingsField label="Hardened Build" helper="Review entitlement and registry access before changing image channels.">
<SettingsPrimaryButton size="sm" onClick={openPreflight} disabled={preflightLoading}>
{preflightLoading ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : null}
Switch to Hardened
</SettingsPrimaryButton>
</SettingsField>
) : null}
<SettingsField
label="Current image"
helper={channelStatus?.channel === 'hardened' && !channelStatus.composeImageRef
? 'Hardened image details are available to administrators only.'
: 'Current image channel for this control plane.'}
>
<span className="font-mono text-xs text-stat-value break-all">
{channelStatus?.composeImageRef ?? formatChannel(channelStatus?.channel ?? 'unknown')}
</span>
</SettingsField>
<SettingsField label="Channel">
<span className="text-sm text-stat-value">{formatChannel(channelStatus?.channel ?? 'unknown')}</span>
</SettingsField>
{channelStatus?.operation?.state === 'failed' ? (
<SettingsField
label="Image operation"
helper={channelStatus.operation.failureCode || 'The image operation failed before completion.'}
tone="error"
>
<Button variant="outline" size="sm" onClick={acknowledgeOperation} disabled={acknowledging}>
{acknowledging ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : null}
Acknowledge failure
</Button>
</SettingsField>
) : null}
{license?.status === 'active' && license.customerName ? (
<SettingsField label="Customer">
<span className="text-sm text-stat-value">{license.customerName}</span>
@@ -268,6 +430,27 @@ export function LicenseSection() {
</div>
</SettingsSection>
) : null}
<ConfirmModal
open={preflight !== null}
onOpenChange={(open) => !open && setPreflight(null)}
kicker="ADMIRAL ACCOUNT · HARDENED BUILD"
title="Review image switch"
confirmLabel={switching ? 'Switching' : 'Confirm switch'}
confirming={switching}
onConfirm={switchToHardened}
hint="BACK UP FIRST"
>
<div className="space-y-3 text-sm">
<dl className="grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 font-mono text-xs">
<dt className="text-stat-subtitle">CURRENT</dt><dd className="break-all text-stat-value">{preflight?.currentImageRef}</dd>
<dt className="text-stat-subtitle">TARGET</dt><dd className="break-all text-stat-value">{preflight?.allowedImageRef}</dd>
<dt className="text-stat-subtitle">REGISTRY</dt><dd className="text-stat-value">{preflight?.localRegistryAccess}</dd>
<dt className="text-stat-subtitle">COMPOSE PATH</dt><dd className="break-all text-stat-value">{preflight?.composeFilePath}</dd>
<dt className="text-stat-subtitle">PIN KIND</dt><dd className="text-stat-value">{preflight?.pinKind}</dd>
</dl>
<p className="text-stat-subtitle">Create a backup first. To roll back, restore the prior image reference in the compose file and recreate the service.</p>
</div>
</ConfirmModal>
</div>
);
}
@@ -64,7 +64,7 @@ export function SupportSection() {
<ResourceLink
icon={<Mail className="w-4 h-4" />}
title="Priority email support"
blurb="Direct support with responses within 24 hours"
blurb="Monday to Friday, 09:00 to 17:00 America/New_York. We aim to first-respond within one business day. This is not a contractual SLA or 24/7 service."
href="mailto:support@sencho.io"
external={false}
/>
@@ -75,8 +75,8 @@ export function SupportSection() {
{!isPaid && (
<SettingsCallout
icon={<Crown className="h-4 w-4" />}
title="Need faster support?"
subtitle="Admiral includes direct email support and priority issue handling."
title="Need direct support?"
subtitle="Admiral includes priority email support during published support hours."
action={
<SettingsPrimaryButton
size="sm"
+6 -6
View File
@@ -70,9 +70,9 @@ export const SETTINGS_ITEMS: readonly SettingsItemMeta[] = [
{
id: 'license',
group: 'access',
label: 'License',
description: 'Activation key, plan tier, and seat allocation.',
keywords: ['key', 'activation', 'tier', 'plan', 'seats', 'billing'],
label: 'Admiral Account',
description: 'Edition, Admiral subscription, assurance entitlements, and image channel.',
keywords: ['admiral', 'assurance', 'hardened', 'agpl', 'license', 'activation', 'subscription', 'billing'],
tier: null,
scope: 'global',
hiddenOnRemote: true,
@@ -154,9 +154,9 @@ export const SETTINGS_ITEMS: readonly SettingsItemMeta[] = [
{
id: 'cloud-backup',
group: 'infrastructure',
label: 'Cloud Backup',
description: 'Mirror fleet snapshots to Sencho Cloud Backup or any S3-compatible storage.',
keywords: ['cloud', 'backup', 'snapshot', 's3', 'r2', 'minio', 'storage', 'offsite'],
label: 'Recovery Vault',
description: 'Mirror fleet snapshots to Recovery Vault or any S3-compatible storage.',
keywords: ['recovery', 'vault', 'cloud', 'backup', 'snapshot', 's3', 'r2', 'minio', 'storage', 'offsite'],
tier: null,
scope: 'global',
adminOnly: true,