Monorepo for Aesthetic.Computer aesthetic.computer

os Command — Bootable Piece OS Builder#

Status: Proposal / Roadmap Date: 2025-02-24 Author: @jeffrey + Claude


Summary#

Build an os command into the AC prompt that takes any piece name, injects it into a pre-baked Fedora base image, and produces a downloadable .iso file anyone can flash to USB with Balena Etcher.

aesthetic.computer/os:notepat

Downloads: notepat-os.iso (~2.8 GB) Flash with Etcher → boot → PALS splash → notepat fullscreen.


Current State#

Today the kiosk USB build is a manual process:

sudo bash fedac/scripts/make-kiosk-piece-usb.sh notepat /dev/sda

This script:

  1. Fetches the piece bundle from oven /pack-html?piece=notepat
  2. Extracts Fedora Workstation Live ISO (~2.4 GB) into a rootfs (~9 GB uncompressed)
  3. Injects kiosk config: cage compositor, Firefox autoconfig, volume keys, Plymouth, systemd units
  4. Compresses rootfs to EROFS with LZMA (~2.7 GB)
  5. Partitions the USB, writes EFI boot + EROFS
  6. Total time: ~15-20 minutes on a fast machine

Problems with current approach:

  • Requires Linux, root access, and a physical USB drive
  • Full rootfs extraction + EROFS compression is slow and disk-hungry (~12 GB temp)
  • Not accessible to anyone without CLI skills

Architecture: Pre-Baked Base Image + Piece Injection#

Key Insight#

The rootfs extraction, kiosk config injection, and EROFS compression are identical for every piece. The only variable is the 2-5 MB piece bundle (piece.html). We can pre-bake everything once and store a "hollowed" base image on CDN, then inject the piece at download time.

Two-Layer Image Format#

┌──────────────────────────────────┐
│  Layer 1: Base Image (pre-baked) │  ~2.7 GB EROFS, stored on CDN
│  ┌────────────────────────────┐  │
│  │ Fedora 43 rootfs           │  │
│  │ cage, Firefox, PipeWire    │  │
│  │ kiosk-session.sh           │  │
│  │ volume key daemon          │  │
│  │ PALS Plymouth + fb splash  │  │
│  │ /usr/local/share/kiosk/    │  │
│  │   └── piece.html ← PLACEHOLDER │
│  └────────────────────────────┘  │
├──────────────────────────────────┤
│  Layer 2: Piece Bundle           │  ~2-5 MB, generated per request
│  └── piece.html (from oven)     │
└──────────────────────────────────┘

Injection Strategy: Preallocated Slot in EROFS#

EROFS is read-only, so we can't modify it in-place. Two approaches:

Option A — Hybrid ISO with overlay partition

  • Base EROFS has a placeholder piece.html (1 byte)
  • A small second partition (ext4, 20 MB) holds the real piece.html
  • kiosk-session.sh mounts the overlay partition and symlinks/bind-mounts the piece
  • At download time: concatenate base ISO + piece partition
  • Pros: Fast (~seconds), base image never rebuilt
  • Cons: Slightly more complex boot chain

Option B — EROFS with preallocated padding (recommended)

  • Build base EROFS with a dummy piece.html padded to exactly 8 MB (zeros)
  • Record the byte offset of piece.html within the EROFS image
  • At download time: seek to that offset, overwrite with real piece bundle
  • Pros: Single file, simple, no boot chain changes
  • Cons: Requires EROFS internals knowledge, fragile if file layout changes

Option C — Full ISO assembly on server

  • Store pre-built components: EFI partition, kernel, initrd, base EROFS
  • At download time: build EROFS from a cached rootfs snapshot + the new piece
  • Pros: Clean, no hacks
  • Cons: Requires rootfs snapshot on server (~9 GB), EROFS build takes ~5 min

Most practical balance of simplicity and speed:

fedac-base.iso (pre-baked, ~2.8 GB, stored on DO Spaces CDN):
  Partition 1: EFI (400 MB FAT32) — GRUB + kernel + initrd
  Partition 2: FEDAC-LIVE (ext4) — squashfs.img (EROFS)
  Partition 3: FEDAC-PIECE (ext4, 20 MB) — empty, for piece injection

At download time (oven server):
  1. Stream fedac-base.iso from CDN
  2. Fetch piece bundle from /pack-html
  3. Write piece.html into partition 3
  4. Stream modified ISO to user

Implementation Plan#

Phase 1: Pre-Baked Base Image#

Build script changes (make-kiosk-piece-usb.sh):

  • Add --base-image flag that builds the base without a specific piece
  • Uses a minimal placeholder piece.html (<html><body>loading...</body></html>)
  • Adds a third 20 MB ext4 partition (FEDAC-PIECE)
  • Outputs fedac-base-{date}.iso (~2.8 GB)
  • Upload to Digital Ocean Spaces CDN

Boot chain change (kiosk-session.sh):

# Mount piece overlay if available
PIECE_PART=$(blkid -L FEDAC-PIECE 2>/dev/null || true)
if [ -n "$PIECE_PART" ] && [ -f "$PIECE_PART_MOUNT/piece.html" ]; then
  mount -o ro "$PIECE_PART" /mnt/piece
  ln -sf /mnt/piece/piece.html /usr/local/share/kiosk/piece.html
fi

Automated rebuilds:

  • GitHub Action: rebuild base image weekly or on fedac/ changes
  • Store on DO Spaces: https://assets.aesthetic.computer/os/fedac-base-latest.iso
  • Keep last 3 versions for rollback

Phase 2: Oven /os Endpoint#

New route in oven/server.mjs:

app.get('/os', async (req, res) => {
  const piece = req.query.piece || req.query.code;
  if (!piece) return res.status(400).send('Missing piece parameter');

  // 1. Fetch piece bundle
  const bundle = await createJSPieceBundle(piece, ...);
  // or createBundle() for KidLisp

  // 2. Stream base ISO from CDN
  const baseUrl = 'https://assets.aesthetic.computer/os/fedac-base-latest.iso';
  const baseStream = await fetch(baseUrl);

  // 3. Inject piece into partition 3
  // The base ISO has a known layout:
  //   - Partition 3 starts at a fixed offset (recorded at build time)
  //   - Write piece.html to the ext4 filesystem in partition 3

  // 4. Stream to user
  res.setHeader('Content-Type', 'application/octet-stream');
  res.setHeader('Content-Disposition', `attachment; filename="${piece}-os.iso"`);
  // ... stream modified ISO
});

Technical detail — partition injection: The oven server needs to write a file into an ext4 partition within the ISO. Options:

  • Use e2fsprogs (debugfs) to inject a file into ext4 without mounting
  • Use a pre-formatted ext4 image and just dd the piece into a known offset
  • Simplest: pre-format a 20 MB ext4 image at build time with a single file slot

Phase 3: AC Prompt Piece (disks/os.mjs)#

// system/public/aesthetic.computer/disks/os.mjs
function boot({ params, net, jump }) {
  const piece = params[0] || "prompt";
  const url = `${net.apiUrl}/api/os?piece=${encodeURIComponent(piece)}`;
  jump("out:" + url); // triggers download
}

export const desc = "Download a bootable OS image for any piece.";
export { boot };

Netlify redirect (netlify.toml):

[[redirects]]
  from = "/api/os"
  to = "https://oven.aesthetic.computer/os"
  status = 200

Phase 4: Progress UI#

Since ISO assembly takes 10-30 seconds, add a progress page:

aesthetic.computer/os:notepat
  ┌──────────────────────────────┐
  │                              │
  │      🎵 notepat OS           │
  │                              │
  │   Building your image...     │
  │   ████████████░░░░░  67%     │
  │                              │
  │   Fetching piece bundle...   │
  │   Assembling ISO...          │
  │                              │
  └──────────────────────────────┘

Use SSE (Server-Sent Events) for progress, similar to the existing track-media-stream endpoint.


Infrastructure Requirements#

Component Current Needed
Oven server (Fly.io) 1 GB RAM, shared CPU 2 GB RAM, 10 GB ephemeral disk
DO Spaces CDN Assets storage + base ISO storage (~3 GB)
Build runner Manual (ThinkPad) GitHub Action or dedicated VM
e2fsprogs on oven Not installed debugfs for ext4 injection

Cost estimate:

  • DO Spaces: ~$0.02/GB/month storage + $0.01/GB transfer = ~$0.10/month base + $0.03/download
  • Fly.io upgrade: ~$5/month for larger VM
  • Total: ~$5-10/month for moderate usage

File Sizes#

Component Size
Fedora base rootfs (uncompressed) ~9 GB
EROFS image (LZMA compressed) ~2.7 GB
EFI partition (kernel + initrd + GRUB) ~400 MB
Piece partition (ext4) 20 MB
Total ISO ~3.1 GB
Piece bundle (typical) 2-5 MB

User Experience#

# From any browser:
aesthetic.computer/os:notepat

# From AC prompt:
> os notepat

# What happens:
1. Page shows "Building notepat OS..." with progress
2. Oven fetches piece bundle (~2s)
3. Oven streams base ISO + injects piece (~10-30s)
4. Browser downloads notepat-os.iso (~3.1 GB)
5. User opens Balena Etcher, selects ISO, flashes to USB
6. Boot from USB → PALS splash → notepat fullscreen

Security Considerations#

  • ISO downloads should be rate-limited (expensive bandwidth)
  • Piece code runs in Firefox sandbox (same as browser)
  • No user data on the USB (live system, RAM-only)
  • Consider signing ISOs with GPG for verification

Timeline#

Phase Effort Dependencies
1. Pre-baked base image 2-3 days Current build script works
2. Oven /os endpoint 3-5 days Base image on CDN
3. AC prompt piece 1 day Oven endpoint working
4. Progress UI 1-2 days SSE infrastructure exists
Total ~1-2 weeks

Future Extensions#

  • os:@handle/piece — build OS from published user pieces
  • Custom branding — per-piece boot splash (piece's preview as Plymouth theme)
  • ARM support — Raspberry Pi images (aarch64 Fedora base)
  • Minimal base — strip Fedora to ~1 GB (remove LibreOffice, GNOME apps, etc.)
  • Delta updates — only download the piece partition for repeat builds
  • Network boot (PXE) — boot from LAN without USB