The site's own logic runs unchanged on Linux and on zig-metal-unikernel, a machine with no operating system. The only thing that differs between the two is what sits under a small, named seam. This file says where that seam is, and what still reaches around it.
The test that the seam holds is already a lint: tools/lint_portable.shale
(run by ops/check_zig) walks router.zig's imports — everything this site
serves — and fails if any of it reaches for the host. So "the application
reaches for nothing host-specific" is not an aspiration here; it compiles, or
it does not. This note is about shrinking the surface the host must implement
on the far side of that line.
| what the application reaches for | the seam | provided by |
|---|---|---|
| the clock | Io.Clock.now(.real, io) |
the io value |
| randomness | io.random(buf) — OS-seeded CSPRNG |
the io value |
| every data file it keeps | zig-webbox's store.zig (read, write, stat, readAt, …), taking io |
the io value's disk |
| a live stream (SSE) | bus.zig — the app describes a Kept; the host serves it |
Hub/Bus |
| where its data lives | roots.zig — six roots from two directories |
the host's main |
| a request's inputs | zig-webbox's http.zig — target, header, cookie, queryValue (OWNED copies) |
std.http.Server.Request |
| a request's outputs | req.respond(...) directly, plus http.notFound/redirect |
std.http.Server.Request |
| a log line | — (none; the host logs) | the host |
Three things carry the whole seam:
-
The
iovalue (std.Io). One handle, threaded through nearly every call, carries the clock, the CSPRNG, and the disk the Store reads. A host builds it: on Linux it isstd.Io.Threaded, backed by the OS; on zig-metal-unikernel it is the kernel's own, backed by the volume and a metal entropy source. The application never asks which — it asksio. This is why clock and random, which a note might expect to find reached around the seam, are already inside it:Io.Clock.nowandio.randomare the seam. -
The Store (zig-webbox's
store.zig). Every file the application keeps goes through it, and it enforces FAT's rules on every host (names, case-folding, the folder-entry ceiling) so Linux cannot quietly accept what the volume will refuse. -
The Bus (
bus.zig). The application describes a stream and records aKept; the host keeps it — blocking on a per-connection task on Linux, rendering the next arrived event on the single loop on metal. One source, two hosts.
Config is a near-miss worth naming: the source differs (Linux reads
environment variables and a config file in webbox's linux_config.zig; metal
reads zig-metal-unikernel.conf), but
both end at roots.point, so the application proper never sees the
difference — only the two mains do. That is the seam working as intended:
the host-specific part is confined to the host's entry point.
-
The response half of HTTP. The input half is already behind the seam: zig-webbox's
http.zigholds the only code allowed to touchreq.head.*, enforced bytools/lint_head_access.shale, so a head value can never dangle past the body read. The output half is not: many of the site's files callreq.respond(...)onstd.http.Server.Requestdirectly, with their ownextra_headers(grep -l 'respond(' zig-server/src/*.zig | wc -lcounts them). So the application still depends on the wholestd.http.Serverresponse API, and the host must provide exactly that — a large surface, and the one place the "reaches for nothing host-specific" property is carried bystd.httpbeing present on both hosts rather than by a small interface the application owns. -
Nothing else of substance. These go through the seam, not around it:
- uploads — served by the
stat/readAtof zig-webbox'sstore.zig(the Range path reads positionally through the Store), not a direct host read; - sessions — the secret is read through the Store; signing and verifying
are pure (
users.zig), so no host call; - the site's own files —
pages/,gallery/,downloads/are read throughstore.read, notstd.fs; onlybrand.zig's images are@embedFiled into the binary, which is no host call at all.
- uploads — served by the
-
The log. The application has no log seam, because it does not log — the host logs (the serial console on metal,
std.debug.printon Linux), and/admin/hostreports the host's own facts. That is no problem; it would only become one if the application wanted to emit a line both hosts render the same way, and then it would want a seam like the others rather than a reach for the host.
Pull req.respond behind zig-webbox's http.zig, the way req.head is: give http.zig
success responses to match its error ones (ok, json, html, … beside
notFound and redirect), move every handler's req.respond(...) to them,
and add a lint beside lint_head_access.shale that lets only zig-webbox's http.zig call
req.respond. The application's dependency on std.http.Server would then be
http.zig's small surface. It moves calls, not logic, so zig-metal-unikernel's
chat judge (probe/judge_gopher.py), which compares every page from metal and
from Linux byte for byte, would show it changed nothing. It is not scheduled.