diff --git a/fedac/native/ac-usb b/fedac/native/ac-usb index a375cabd16..5145561b76 100755 --- a/fedac/native/ac-usb +++ b/fedac/native/ac-usb @@ -44,13 +44,17 @@ case "${CMD}" in docker_usb " apk add --quiet dosfstools mkdir -p /mnt/usb - mount ${USB_DEV}1 /mnt/usb 2>/dev/null && { - echo '--- ${USB_DEV}1 ---' + mounted='' + for p in ${USB_DEV}1 ${USB_DEV}2; do + [ -b \"\$p\" ] && mount \"\$p\" /mnt/usb 2>/dev/null && { mounted=\"\$p\"; break; } + done + [ -n \"\$mounted\" ] && { + echo \"--- \$mounted ---\" find /mnt/usb -type f | head -50 echo '' du -sh /mnt/usb/EFI/BOOT/BOOTX64.EFI 2>/dev/null umount /mnt/usb - } || echo 'Could not mount ${USB_DEV}1' + } || echo 'Could not mount ${USB_DEV}1 or ${USB_DEV}2' " ;; sha256|hash) @@ -58,7 +62,11 @@ case "${CMD}" in docker_usb " apk add --quiet dosfstools mkdir -p /mnt/usb - mount ${USB_DEV}1 /mnt/usb + mounted='' + for p in ${USB_DEV}1 ${USB_DEV}2; do + [ -b \"\$p\" ] && mount \"\$p\" /mnt/usb 2>/dev/null && { mounted=\"\$p\"; break; } + done + [ -z \"\$mounted\" ] && { echo 'Could not mount USB'; exit 1; } sha256sum /mnt/usb/EFI/BOOT/BOOTX64.EFI 2>/dev/null || echo 'No EFI file found' umount /mnt/usb " @@ -100,7 +108,11 @@ case "${CMD}" in docker_usb " apk add --quiet dosfstools mkdir -p /mnt/usb - mount ${USB_DEV}1 /mnt/usb + mounted='' + for p in ${USB_DEV}1 ${USB_DEV}2; do + [ -b \"\$p\" ] && mount \"\$p\" /mnt/usb 2>/dev/null && { mounted=\"\$p\"; break; } + done + [ -z \"\$mounted\" ] && { echo 'Could not mount USB'; exit 1; } cat '/mnt/usb/${FILE}' 2>/dev/null || echo 'File not found: ${FILE}' umount /mnt/usb " diff --git a/fedac/native/initramfs/init b/fedac/native/initramfs/init index 58c8a9c003..f33dc2ee74 100755 --- a/fedac/native/initramfs/init +++ b/fedac/native/initramfs/init @@ -1,9 +1,5 @@ #!/bin/sh -<<<<<<< Updated upstream # AC Native OS init — DRM direct boot with crash recovery -======= -# AC Native OS init — simple DRM direct boot (proven working on all ThinkPads) ->>>>>>> Stashed changes mount -t proc proc /proc 2>/dev/null mount -t sysfs sysfs /sys 2>/dev/null diff --git a/fedac/native/src/ac-native.c b/fedac/native/src/ac-native.c index 9404208045..0f7dec4710 100644 --- a/fedac/native/src/ac-native.c +++ b/fedac/native/src/ac-native.c @@ -271,15 +271,22 @@ static void try_mount_log(void) { // Wait for USB block devices to appear (up to 2s after EFI handoff) fprintf(stderr, "[ac-native] Waiting for USB block devices...\n"); for (int w = 0; w < 100; w++) { - if (access("/dev/sda1", F_OK) == 0 || access("/dev/sdb1", F_OK) == 0) break; + if (access("/dev/sda1", F_OK) == 0 || access("/dev/sda2", F_OK) == 0 || + access("/dev/sdb1", F_OK) == 0 || access("/dev/sdb2", F_OK) == 0) break; usleep(20000); } - fprintf(stderr, "[ac-native] sda1=%s sdb1=%s\n", + fprintf(stderr, "[ac-native] sda1=%s sda2=%s sdb1=%s sdb2=%s\n", access("/dev/sda1", F_OK) == 0 ? "yes" : "no", - access("/dev/sdb1", F_OK) == 0 ? "yes" : "no"); + access("/dev/sda2", F_OK) == 0 ? "yes" : "no", + access("/dev/sdb1", F_OK) == 0 ? "yes" : "no", + access("/dev/sdb2", F_OK) == 0 ? "yes" : "no"); const char *devs[] = { - "/dev/sda1", "/dev/sdb1", "/dev/sdc1", "/dev/sdd1", - "/dev/nvme0n1p1", "/dev/nvme1n1p1", + "/dev/sda1", "/dev/sda2", + "/dev/sdb1", "/dev/sdb2", + "/dev/sdc1", "/dev/sdc2", + "/dev/sdd1", "/dev/sdd2", + "/dev/nvme0n1p1", "/dev/nvme0n1p2", + "/dev/nvme1n1p1", "/dev/nvme1n1p2", NULL }; diff --git a/oven/server.mjs b/oven/server.mjs index ebfc311a1a..b414a4fdc1 100644 --- a/oven/server.mjs +++ b/oven/server.mjs @@ -2929,6 +2929,8 @@ app.post('/os-cache-flush', (req, res) => { const RELEASES_BASE = 'https://releases-aesthetic-computer.sfo3.digitaloceanspaces.com/os'; const TEMPLATE_ISO_URL = `${RELEASES_BASE}/native-notepat-latest.iso`; const TEMPLATE_GZ_URL = `${RELEASES_BASE}/native-notepat-latest.img.gz`; // legacy fallback +const TEMPLATE_VMLINUZ_URL = `${RELEASES_BASE}/native-notepat-latest.vmlinuz`; +const TEMPLATE_CL_VMLINUZ_URL = `${RELEASES_BASE}/cl-native-notepat-latest.vmlinuz`; const CONFIG_MARKER_LEGACY = '{"handle":"","piece":"notepat","sub":"","email":""}'; const CONFIG_PAD_SIZE_LEGACY = 4096; const IDENTITY_MARKER = 'AC_IDENTITY_BLOCK_V1'; @@ -2966,6 +2968,53 @@ async function getTemplate() { return templateCache; } +function kernelUrlForVariant(variant) { + return variant === 'cl' ? TEMPLATE_CL_VMLINUZ_URL : TEMPLATE_VMLINUZ_URL; +} + +async function buildPersonalizedEfiImage({ kernelUrl, configJson }) { + const id = `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`; + const tmpBase = `/tmp/os-image-${id}`; + const kernelPath = `${tmpBase}-BOOTX64.EFI`; + const configPath = `${tmpBase}-config.json`; + const imagePath = `${tmpBase}.img`; + const efiOffsetSectors = 2048; + const efiOffsetBytes = efiOffsetSectors * 512; + let kernelData = null; + + try { + const kRes = await fetch(kernelUrl); + if (!kRes.ok) { + throw new Error(`Kernel download failed (${kRes.status})`); + } + kernelData = Buffer.from(await kRes.arrayBuffer()); + await fs.promises.writeFile(kernelPath, kernelData); + await fs.promises.writeFile(configPath, configJson); + + // Keep headroom for FAT metadata and future kernel size growth. + const minBytes = kernelData.length + Buffer.byteLength(configJson) + (32 * 1024 * 1024); + const imageSizeMiB = Math.max(384, Math.ceil(minBytes / 1048576) + 32); + + execSync(`dd if=/dev/zero of="${imagePath}" bs=1M count=${imageSizeMiB} status=none`); + execSync( + `printf 'label: gpt\nstart=${efiOffsetSectors}, type=C12A7328-F81F-11D2-BA4B-00A0C93EC93B\n' | ` + + `sfdisk --force --no-reread "${imagePath}" >/dev/null`, + ); + execSync(`mkfs.vfat -F 32 --offset=${efiOffsetSectors} "${imagePath}" >/dev/null`); + execSync(`mmd -i "${imagePath}@@${efiOffsetBytes}" ::EFI ::EFI/BOOT`); + execSync(`mcopy -o -i "${imagePath}@@${efiOffsetBytes}" "${kernelPath}" ::EFI/BOOT/BOOTX64.EFI`); + execSync(`mcopy -o -i "${imagePath}@@${efiOffsetBytes}" "${configPath}" ::config.json`); + + return await fs.promises.readFile(imagePath); + } finally { + await Promise.allSettled([ + fs.promises.unlink(kernelPath), + fs.promises.unlink(configPath), + fs.promises.unlink(imagePath), + ]); + } +} + // User config endpoint for edge worker ISO patching app.get('/api/user-config', async (req, res) => { const authHeader = req.headers.authorization || ''; @@ -3095,14 +3144,13 @@ app.get('/os-image', async (req, res) => { console.log(`[os-image] Building personalized image for @${handle} (boot: ${bootPiece}, wifi: ${wifiEnabled}, claude: ${!!claudeToken}, git: ${!!githubPat})`); - // Get template (cached in memory) - let imgData; - try { - const template = await getTemplate(); - imgData = Buffer.from(template); // copy so we don't mutate cache - } catch (err) { - return res.status(503).json({ error: `Template not available: ${err.message}` }); - } + const variant = String(req.query.variant || '').toLowerCase() === 'cl' ? 'cl' : 'c'; + const layout = String(req.query.layout || '').toLowerCase(); + const preferEfiLayout = layout === 'efi' || layout === 'img' || layout === 'raw'; + const strictEfi = + preferEfiLayout && + String(req.query.strict || '1').toLowerCase() !== '0' && + String(req.query.strict || '1').toLowerCase() !== 'false'; // Build personalized config JSON const configObj = { @@ -3117,42 +3165,94 @@ app.get('/os-image', async (req, res) => { if (!wifiEnabled) configObj.wifi = false; const configJson = JSON.stringify(configObj); - // Try new identity block format first (32KB, marker-prefixed) - const identityMarkerBuf = Buffer.from(IDENTITY_MARKER + '\n'); - let idx = imgData.indexOf(identityMarkerBuf); - let patchCount = 0; - - if (idx !== -1) { - // New format: marker + newline + JSON + zero-padding to 32KB - while (idx !== -1) { - const block = Buffer.alloc(IDENTITY_BLOCK_SIZE, 0); - const header = Buffer.from(IDENTITY_MARKER + '\n' + configJson); - header.copy(block); - block.copy(imgData, idx); - patchCount++; - idx = imgData.indexOf(identityMarkerBuf, idx + IDENTITY_BLOCK_SIZE); - } - console.log(`[os-image] Patched ${patchCount} identity block(s) for @${handle} (v1, 32KB)`); - } else { - // Legacy format: plain JSON padded to 4KB with spaces - const legacyMarkerBuf = Buffer.from(CONFIG_MARKER_LEGACY); - idx = imgData.indexOf(legacyMarkerBuf); - if (idx === -1) { - return res.status(500).json({ error: 'Template image missing config placeholder' }); + // Build a direct EFI image (single ESP partition) when requested. + // This layout matches local ac-os flash and is more firmware-compatible + // than some hybrid ISO scanners on older BIOS/UEFI implementations. + let imgData = null; + let contentType = 'application/x-iso9660-image'; + let extension = 'iso'; + let servedLayout = 'iso'; + let efiError = null; + if (preferEfiLayout) { + try { + const kernelUrl = kernelUrlForVariant(variant); + imgData = await buildPersonalizedEfiImage({ kernelUrl, configJson }); + contentType = 'application/octet-stream'; + extension = 'img'; + servedLayout = 'efi'; + console.log( + `[os-image] Built EFI image for @${handle} (${(imgData.length / 1048576).toFixed(1)}MB, variant=${variant})`, + ); + } catch (err) { + efiError = err; + console.warn(`[os-image] EFI layout build failed, falling back to ISO patch path: ${err.message}`); + if (strictEfi) { + return res.status(503).json({ + error: `EFI layout build failed: ${err.message}`, + requestedLayout: 'efi', + }); + } } - const padded = configJson + ' '.repeat(Math.max(0, CONFIG_PAD_SIZE_LEGACY - configJson.length)); - const configBytes = Buffer.from(padded); - while (idx !== -1) { - configBytes.copy(imgData, idx); - patchCount++; - idx = imgData.indexOf(legacyMarkerBuf, idx + CONFIG_PAD_SIZE_LEGACY); + } + + // Fallback: patch the template ISO in-place + if (!imgData) { + try { + const template = await getTemplate(); + imgData = Buffer.from(template); // copy so we don't mutate cache + } catch (err) { + return res.status(503).json({ error: `Template not available: ${err.message}` }); + } + + // Try new identity block format first (32KB, marker-prefixed) + const identityMarkerBuf = Buffer.from(IDENTITY_MARKER + '\n'); + let idx = imgData.indexOf(identityMarkerBuf); + let patchCount = 0; + + if (idx !== -1) { + // New format: marker + newline + JSON + zero-padding to 32KB + while (idx !== -1) { + const block = Buffer.alloc(IDENTITY_BLOCK_SIZE, 0); + const header = Buffer.from(IDENTITY_MARKER + '\n' + configJson); + header.copy(block); + block.copy(imgData, idx); + patchCount++; + idx = imgData.indexOf(identityMarkerBuf, idx + IDENTITY_BLOCK_SIZE); + } + console.log(`[os-image] Patched ${patchCount} identity block(s) for @${handle} (v1, 32KB)`); + } else { + // Legacy format: plain JSON padded to 4KB with spaces + const legacyMarkerBuf = Buffer.from(CONFIG_MARKER_LEGACY); + idx = imgData.indexOf(legacyMarkerBuf); + if (idx === -1) { + return res.status(500).json({ error: 'Template image missing config placeholder' }); + } + const padded = configJson + ' '.repeat(Math.max(0, CONFIG_PAD_SIZE_LEGACY - configJson.length)); + const configBytes = Buffer.from(padded); + while (idx !== -1) { + configBytes.copy(imgData, idx); + patchCount++; + idx = imgData.indexOf(legacyMarkerBuf, idx + CONFIG_PAD_SIZE_LEGACY); + } + console.log(`[os-image] Patched ${patchCount} config location(s) for @${handle} (legacy, 4KB)`); } - console.log(`[os-image] Patched ${patchCount} config location(s) for @${handle} (legacy, 4KB)`); } - // Stream the patched ISO (Fedora Media Writer / Balena Etcher / dd compatible) - addServerLog('success', '💿', `OS ISO for @${handle} (${(imgData.length / 1048576).toFixed(1)}MB)`); - res.setHeader('Content-Type', 'application/x-iso9660-image'); + // Stream the personalized image (ISO patch path or EFI-first image path) + addServerLog('success', '💿', `OS image for @${handle} (${(imgData.length / 1048576).toFixed(1)}MB)`); + if (preferEfiLayout && servedLayout !== 'efi') { + addServerLog('warn', '⚠️', `OS image fallback for @${handle}: requested EFI but served ISO`); + } + res.setHeader('Content-Type', contentType); + res.setHeader('Cache-Control', 'no-store, no-cache, must-revalidate'); + res.setHeader('Pragma', 'no-cache'); + res.setHeader('Expires', '0'); + res.setHeader('X-AC-OS-Requested-Layout', preferEfiLayout ? 'efi' : 'iso'); + res.setHeader('X-AC-OS-Layout', servedLayout); + if (efiError) { + res.setHeader('X-AC-OS-Fallback', '1'); + res.setHeader('X-AC-OS-Fallback-Reason', String(efiError.message || 'unknown').slice(0, 180)); + } // Get latest build name for filename let releaseName = 'native'; try { @@ -3166,7 +3266,7 @@ app.get('/os-image', async (req, res) => { const d = new Date(); const p = (n) => String(n).padStart(2, '0'); const ts = `${d.getFullYear()}.${p(d.getMonth()+1)}.${p(d.getDate())}.${p(d.getHours())}.${p(d.getMinutes())}.${p(d.getSeconds())}`; - res.setHeader('Content-Disposition', `attachment; filename="@${handle}-os-${bootPiece}-${coreName}-${ts}.iso"`); + res.setHeader('Content-Disposition', `attachment; filename="@${handle}-os-${bootPiece}-${coreName}-${ts}.${extension}"`); res.setHeader('Content-Length', imgData.length); res.end(imgData); }); diff --git a/system/deno.lock b/system/deno.lock index 89841e782d..0e52ec17ad 100644 --- a/system/deno.lock +++ b/system/deno.lock @@ -612,7 +612,7 @@ "workspace": { "packageJson": { "dependencies": [ - "npm:@atproto/api@0.18", + "npm:@atproto/api@~0.19.4", "npm:@aws-sdk/client-s3@^3.975.0", "npm:@aws-sdk/s3-request-presigner@^3.975.0", "npm:@ffmpeg/core@~0.12.10", diff --git a/system/public/aesthetic.computer/boot.mjs b/system/public/aesthetic.computer/boot.mjs index 0e5534298d..e3f9d40f6e 100644 --- a/system/public/aesthetic.computer/boot.mjs +++ b/system/public/aesthetic.computer/boot.mjs @@ -713,11 +713,24 @@ window.safeSessionStorageRemove = safeSessionStorageRemove; const IMPORT_MAX_RETRIES = 3; const IMPORT_RETRY_DELAY = 1500; -async function importWithRetry(modulePath, retries = IMPORT_MAX_RETRIES, useWsBundle = false) { - const loader = window.acModuleLoader; - const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'; - const retryDelay = isLocalhost ? 300 : IMPORT_RETRY_DELAY; - let triedWs = false; +async function importWithRetry(modulePath, retries = IMPORT_MAX_RETRIES, useWsBundle = false) { + const loader = window.acModuleLoader; + const isLocalhost = window.location.hostname === 'localhost' || window.location.hostname === '127.0.0.1'; + const retryDelay = isLocalhost ? 300 : IMPORT_RETRY_DELAY; + let triedWs = false; + + const clearLoaderCacheForPath = async (relativePath) => { + if (!loader || !relativePath) return; + try { loader.modules?.delete?.(relativePath); } catch (_) {} + try { loader.blobUrls?.delete?.(relativePath); } catch (_) {} + try { loader.bundleContents?.delete?.(relativePath); } catch (_) {} + if (loader.db) { + try { + const tx = loader.db.transaction("modules", "readwrite"); + tx.objectStore("modules").delete(relativePath); + } catch (_) {} + } + }; const waitForWsConnection = async () => { if (!loader?.connecting) return; @@ -727,16 +740,20 @@ async function importWithRetry(modulePath, retries = IMPORT_MAX_RETRIES, useWsBu ]); }; - const tryLoadViaWs = async () => { - if (!loader?.loadWithDeps) throw new Error('ws-unavailable'); - triedWs = true; - await waitForWsConnection(); - if (!loader.connected) throw new Error('ws-not-connected'); - const relativePath = modulePath.replace(/^\.\//, '').split('?')[0]; - const blobUrl = await loader.loadWithDeps(relativePath, 5000); - if (blobUrl && blobUrl.startsWith('blob:')) { - return await import(blobUrl); - } + const tryLoadViaWs = async () => { + if (!loader?.loadWithDeps) throw new Error('ws-unavailable'); + triedWs = true; + await waitForWsConnection(); + if (!loader.connected) throw new Error('ws-not-connected'); + const relativePath = modulePath.replace(/^\.\//, '').split('?')[0]; + // Respect cache-busted URLs by clearing cached module before WS load. + if (modulePath.includes('?v=')) { + await clearLoaderCacheForPath(relativePath); + } + const blobUrl = await loader.loadWithDeps(relativePath, 5000); + if (blobUrl && blobUrl.startsWith('blob:')) { + return await import(blobUrl); + } throw new Error('ws-missing-blob'); }; diff --git a/system/public/aesthetic.computer/disks/os.mjs b/system/public/aesthetic.computer/disks/os.mjs index 4378c6643b..a2767b354d 100644 --- a/system/public/aesthetic.computer/disks/os.mjs +++ b/system/public/aesthetic.computer/disks/os.mjs @@ -916,17 +916,21 @@ async function startDownload(needsPaint) { downloadMB = 0; downloadTotalMB = 0; const piece = BOOT_PIECES[bootPieceIdx]; - downloadStatus = "building @" + (handle || "user") + " os... (boot to: " + piece + ")"; - console.log("[os] Starting download:", osLabel(), "piece:", piece); - needsPaint(); - + downloadStatus = "building @" + (handle || "user") + " os... (boot to: " + piece + ")"; + console.log("[os] Starting download:", osLabel(), "piece:", piece); + needsPaint(); + try { - const query = "?piece=" + encodeURIComponent(piece) + "&wifi=" + (wifiEnabled ? "1" : "0") + (variantIdx === 1 ? "&variant=cl" : ""); + const query = + "?piece=" + encodeURIComponent(piece) + + "&wifi=" + (wifiEnabled ? "1" : "0") + + "&layout=efi&strict=1&cb=" + Date.now() + + (variantIdx === 1 ? "&variant=cl" : ""); const isoCandidates = [ OVEN_ORIGIN + "/os-image" + query, OVEN + "/os-image" + query, ]; - const MIN_EXPECTED_ISO_BYTES = 100 * 1024 * 1024; + const MIN_EXPECTED_IMAGE_BYTES = 50 * 1024 * 1024; let res = null; let usedUrl = ""; @@ -949,9 +953,17 @@ async function startDownload(needsPaint) { continue; } + const servedLayout = (attempt.headers.get("x-ac-os-layout") || "").toLowerCase(); + if (servedLayout && servedLayout !== "efi") { + console.warn("[os] Rejecting non-EFI response:", servedLayout, "from", url); + try { attempt.body?.cancel(); } catch (_) {} + lastErr = "Server returned '" + servedLayout + "' image instead of EFI"; + continue; + } + const len = parseInt(attempt.headers.get("content-length") || "0"); - if (len > 0 && len < MIN_EXPECTED_ISO_BYTES) { - console.warn("[os] Rejecting suspiciously small ISO response:", len, "bytes from", url); + if (len > 0 && len < MIN_EXPECTED_IMAGE_BYTES) { + console.warn("[os] Rejecting suspiciously small image response:", len, "bytes from", url); try { attempt.body?.cancel(); } catch (_) {} lastErr = "Received suspiciously small image (" + len + " bytes)"; continue; @@ -965,7 +977,7 @@ async function startDownload(needsPaint) { if (!res) { throw new Error(lastErr || "Download failed"); } - console.log("[os] Download source:", usedUrl); + console.log("[os] Download source:", usedUrl, "layout:", res.headers.get("x-ac-os-layout") || "?"); const contentLength = parseInt(res.headers.get("content-length") || "0"); downloadTotalMB = contentLength / 1048576; @@ -989,8 +1001,11 @@ async function startDownload(needsPaint) { downloadStatus = "preparing file..."; needsPaint(); - const total = chunks.reduce((s, c) => s + c.length, 0); - const combined = new Uint8Array(total); + const total = chunks.reduce((s, c) => s + c.length, 0); + if (total < MIN_EXPECTED_IMAGE_BYTES) { + throw new Error("Image too small (" + (total / 1048576).toFixed(1) + "MB), refusing to save"); + } + const combined = new Uint8Array(total); let offset = 0; for (const chunk of chunks) { combined.set(chunk, offset); @@ -1003,7 +1018,7 @@ async function startDownload(needsPaint) { const d = new Date(); const p = (n) => String(n).padStart(2, "0"); const ts = `${d.getFullYear()}.${p(d.getMonth()+1)}.${p(d.getDate())}.${p(d.getHours())}.${p(d.getMinutes())}.${p(d.getSeconds())}`; - const filename = `@${handle || "user"}-os-${piece}-${coreName}-${ts}.iso`; + const filename = `@${handle || "user"}-os-${piece}-${coreName}-${ts}.img`; console.log("[os] Download complete:", filename, (total / 1048576).toFixed(1) + "MB"); dlFn(filename, combined, { type: "application/octet-stream" });