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:
- Fetches the piece bundle from oven
/pack-html?piece=notepat - Extracts Fedora Workstation Live ISO (~2.4 GB) into a rootfs (~9 GB uncompressed)
- Injects kiosk config: cage compositor, Firefox autoconfig, volume keys, Plymouth, systemd units
- Compresses rootfs to EROFS with LZMA (~2.7 GB)
- Partitions the USB, writes EFI boot + EROFS
- 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.shmounts 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.htmlpadded to exactly 8 MB (zeros) - Record the byte offset of
piece.htmlwithin 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
Recommended: Option A (Hybrid ISO)#
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-imageflag 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
ddthe 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