WEB.ELF: an in-guest Go browser (M53 Card 3, Go shell + our own Go renderer) - #1258
Conversation
M53 Card 3 (umbrella #1244, claim #1248). A browser at EL0: Go app shell (window chrome, navigation + history, HTTP fetch over the existing TCP slots, error pages) over the project's own Go rendering library. - user/go/vi: the guest OS layer in Go — arm64 svc #0 stubs plus typed wrappers over the existing ADR 0007 slots (no new syscall, no kernel or build.zig change). Host builds stub every call to -ENOSYS so app logic stays unit-testable off the guest. - user/go/webrender: HTML tokenizer + tree builder, one compiled-in UA style table (no CSS cascade, per ADR 0028 D2), block + inline layout with greedy word wrap, span rasterizer over the kernel's rect-only fill primitive, and the repo's own 8x8 face (extracted from user/src/lib/font8x8.zig by tools/go/extract-font8x8.py). - user/go/browser: WEB.ELF — chrome below the kernel's title band (the kernel paints over the top 16 rows), back/forward/reload/stop, link click navigation, distinct error pages, and an inspectable history ledger on the share. - tools/go/build-web.sh: one command, GOOS=virelai GOARCH=arm64, GOPATH mode with user/go linked at $GOPATH/src/virelai, fails if the ELF exceeds exec_program_max. WEB.ELF is 1,310,880 B. - tools/gate/specs/live-web.spec: four boots (local page, link-click navigation, HTTP fetch over the host responder, missing-target error page) with serial asserts and scanout pixel probes; PASS 4/4. Two live findings the spec now pins: the kernel's own title band covers the app's top 16 rows, and a single present can be composited after the marker that describes it (the app re-presents after a short sleep, and prints its navigation markers only after the new frame is up).
The browser now states what it will and will not do on the wire, and stays
responsive while it does it.
- https is refused explicitly ("no TLS trust store yet; nothing was sent in
the clear") and can never fall back to plain TCP. classifyTarget() is a
pure function so that decision is host-tested, and the live gate asserts
the request was never armed while a responder for the same address was
armed and idle (boot 05). This is the honest state of TLS on an OS with no
trust store; the trust work belongs to the M50/M51 arcs.
- hostnames are refused (no resolver) instead of hanging (boot 06), and a
malformed URL is a defined error (boot 07).
- redirects are followed with Location resolution (absolute, root-relative,
relative, protocol-relative), a 5-hop cap, loop detection, and a distinct
error for each failure mode (host-tested).
- the load path is now a state machine stepped from the event loop instead
of a blocking fetch, so Stop/cancel and window-close work mid-load. The
live gate injects the cancel key into a request relayed to a silent host
peer and asserts `web: error cancelled` (boot 08).
- `web: settled` keeps its meaning: the first frame is published only once
the page (or its error page) is on screen, even when the socket is still
in flight when the window opens.
Host suite: 49 tests (vi 7, webrender 26, browser 16), go vet clean.
Live gate: PASS 8/8.
…ice 3) The browser now keeps its data on the host share, in plain text a person can read, and says when it is showing something it stored rather than fetched. - history, bookmarks, cookies, cache and downloads are all files on the share: one schema line plus tab-separated rows. `web: stores history=… bookmarks=… cookies=… cache=… downloads=…` is printed at boot, read from disk, which is how a gate can tell persistence from memory. - the cache key is FNV-1a over the URL (its offset basis is pinned by a host test), bodies are written through writeFileAll (open/truncate/write), and the index carries the key, URL, size, time and body path. - when the network cannot deliver a cached page, the browser renders the stored copy and labels it: `web: offline <url>` plus an on-screen "offline copy (network unavailable)". An offline copy is never passed off as fresh. - Set-Cookie rows are parsed and stored, and matching rows are sent back as a Cookie header (host-suffix and path-prefix matching, both host-tested). - controls: b bookmark toggle, d delete the newest history row, c clear cookies, k clear cache, s save the page to the share. Deletes compact the ledger through a truncate+rewrite, so the schema line survives. - new slots used, none added: 34 (file delete) and 36 (file truncate). Host suite: 59 tests (vi 7, webrender 29, browser 23), go vet clean. Live gate: PASS 10/10 — boot 09 writes the stores (the host reads the share back), boot 10 is a fresh boot with no responder that reads them back (history=10, cache=2, downloads=1) and serves the cached page as an offline copy.
…ice 4) Numbers, and the security claim made testable. - Budgets are measured in-guest with the runtime's monotonic clock (CNTPCT_EL0 via runtime.nanotime, no syscall) and printed with the first frame: `web: budget startup-ms=… wait-ms=… render-ms=… settle-ms=… parse-ms=… layout-ms=… paint-ms=…`. Cold start to a published frame for a page already on the share is 2-3 ms; a fetched page adds ~1 ms of peer wait; parse/layout/paint are each under a millisecond on the fixtures. `startup` deliberately excludes the network wait, which is reported separately (`wait-ms`) and only budgeted when nothing had to be fetched. - A stage over budget prints `web: budget over <stage> <ms>` and the gate asserts that line is absent. A broken measurement (missing or non-monotonic timestamp) prints `web: budget over invalid …` rather than clamping to a flattering zero — that check caught two real instrumentation bugs during this slice. - `user/go/browser/testdata/hostile.html` plus `live-web` boot 11: a page whose script body would fetch a URL and read a file, with iframe/object/img pointing at the browser's own store files and javascript:/file: links. It must render as inert text with no request armed and no error; the live gate asserts exactly that (0 occurrences of `web: fetch`, `web: error`, `web: budget over`) with 3572 ink pixels on screen, and a host test asserts the script body never reaches the rendered text. - scheme-shaped targets are refused as schemes (`javascript:`, `file:`, `mailto:`, `data:`) instead of being rewritten into a file-channel path. - `<img>` is a block element now: image placeholders actually lay out (they previously produced nothing at all). - `user/go/probe` is a diagnostic fixture that prints the monotonic clock and its delta, so the budget instrumentation can be re-checked on this GOOS. Host suite: 63 tests (vi 7, webrender 29, browser 27), go vet clean. Live gate: PASS 11/11, WEB.ELF 1,376,416 B.
Gate re-run on the committed tip (post-interruption evidence)The 11/11 run captured before this PR was opened carried
Raw verification on the tip ( |
Review response — the three nits1. Two syscall gateways — justified, and now written down. Added a "Why there are two syscall gateways" section to the PR description. Short form: the overlay 2 & 3. cd /Users/tbuddy/t3/zig/z-dipshit-ac-ds && rm -f docs/web-browser.md && rm -rf .openclaw-tmpCI observed just now ( |
Nits 2 & 3 resolved — worktree is cleanThe approval prompt came through this time:
So the "before merge" checklist reads: nit 1 answered in the description, nits 2 and 3 done, CI green except the queued |
CI failure diagnosis (inventory drift, not your code)The red Fix from this branch's worktree (re-render, never hand-edit): git fetch origin && git rebase origin/main
bash tools/inventory-gates.sh
git add docs/gate-fleet-inventory.md
git commit -m "gate-fleet-inventory: re-render after live-wm-pacing landed"
git push --force-with-leaseThen CI should go fully green. (Side note: commit messages here say 'M53 card 1' for pacing while #1244 is the M53 Go-first-app umbrella — worth a rename to keep the status rows clean.) |
The branch was 22 commits behind main and the only conflict was docs/gate-fleet-inventory.md, the GENERATED fleet report -- which the project rules say is re-rendered, never hand-merged. Re-ran `bash tools/inventory-gates.sh`, which resolves it and folds in both sides: this branch's new live-web spec and main's own additions (tools/gate/ 234 -> 238, live-sb3-surface-handoff 8 -> 9 asserts, which main had already moved ahead on). No source file belonging to this branch was modified by the resolution. `bash tools/inventory-gates.sh --check` is green on the merged tree, as is the class-A parity lint and the VZ preflight self-test.
|
Maintainer asked me to get this one mergeable, so I pushed a merge of Why it was blocked: two separate things, both mechanical.
Why they were the same thing. The only conflicted file was
Nothing of yours was edited. No source file on this branch was touched by the resolution; the merge only adds If you're still working locally, |
What this is
WEB.ELF— a web browser that runs at EL0 on VirelaiOS: a Go application shell (window chrome, navigation + history, HTTP fetch over the existing TCP slots, error pages, persistent stores) over the project's own Go rendering library (user/go/webrender: HTML tokenizer/tree builder, one compiled-in UA style table, block+inline layout, span rasterizer, 8x8 face extracted fromuser/src/lib/font8x8.zig).M53 Card 3 under umbrella #1244. Closes #1248.
Kernel untouched, no new syscalls, no
build.zigchanges, no new repo docs. Everything new lives underuser/go/**,tools/go/build-web.sh,tools/go/extract-font8x8.py,tools/gate/specs/live-web.spec(+ the generateddocs/gate-fleet-inventory.md).Post-interruption changes
The session that produced
4df276a…d0e191ewas interrupted mid-run;db037e5is the first commit made after resuming. Reconciliation of the interruption:git rev-list --left-right --count HEAD...origin/agent/t3code/web-go-browser→0 0).openclaw-tmp/(scratch logs) anddocs/web-browser.md(a doc the maintainer asked to drop; deleting it needs an interactive approval that times out on this machine, so it is left untracked and uncommitted)grep -rn 'TODO|FIXME|WIP' user/go tools/gofinds no source markers; no disabled tests)revision d0e191e dirty-files=12), so the gate was re-run on the committed tip and the report now namesdb037e5The interruption and its guard. The runs were killed by the harness while a long VM gate was attached to a piped
execsession (an inherited pipe also kept a forked helper alive once, which got the runSIGTERM'd). The guard now used for every long gate: launch detached with output to a file, poll the file, and never hold the gate on a pipe; helpers redirect their stdio to/dev/null. Work is pushed after each slice, so an interruption cannot strand it.Change ledger (criterion → commit → artifact)
4df276alive-webboots 01–02;artifacts/live-web-serial-01.log,snap-02-*.raw4df276auser/go/webrender/**,go test; ADR 0028 rules kept (no JS, no cascade)4df276a–db037e5live-webpixel probes0831790web: error https, no request armed while the responder is armed), redirects host-tested,TestClassifyTargetRefusesHTTPS0831790,db037e5FuzzHTML2.7M execs;TestSchemeShapedTargetsAreRefusedd0e191e$VG_SHARE(WEB-HISTORY.TXT,WEB-CACHE.TXT,WEB-C-*.BIN,WEB-DL-*)db037e5web: budget …on every boot;web: budget overabsent; boot 114df276abash tools/go/build-web.sh browser WEB→WEB.ELF1,376,416 B (< 2 MiBexec_program_max)db037e5web: fetch, 0web: errorExplicitly re-scoped (not missing-by-accident). DNS resolver, TLS, TABWM-backed tabs, session restore, crash-isolated renderer, a versioned release artifact with install/rollback, and CI wiring for this spec are out of this card's scope by the maintainer's direction on #1248 ("keep it thin", per-card detail on the issue; kernel untouched; no
docs/status.md). They are listed as follow-ups below, not silently dropped.How to verify (from a clean checkout)
Expected:
go testgreen,build-webprints the size and "size ok", and the gate endsvgate live-web: PASS (11/11 runs)withartifacts/live-web-report.txtnaming the tip revision. The gate is not hermetic: it needs theGOOS=virelaifork (tools/go/apply.sh) and a built disk image, and it fails honestly with a hint when either is missing.Handoff notes
../go-virelai(built once viabash tools/go/apply.sh).tools/go/build-web.shlinksuser/goat$GOPATH/src/virelaiso module-mode host tests and GOPATH-mode guest builds use the same import paths.python3 tools/go/extract-font8x8.pyrewritesuser/go/webrender/font/font8x8.gofrom the Zig table.WEBRENDER_UPDATE_GOLDEN=1 go test ./webrender/ -run Goldenre-pins; a diff fails with*.actual.pngfor review and only passes again afterWEBRENDER_ACCEPT_DIFF=1.user/go/probeprints the monotonic clock and a delta (proves the budget clock is live);WEBRENDER_DUMP=inwebrenderdumps laid-out primitives.Set-Cookie).live-web.Why there are two syscall gateways
user/go/vi/vi_arm64.sis deliberately a secondsvc #0stub file beside the toolchain overlay'sruntime/sys_virelai_arm64.s. The overlay is part of the Go runtime — it lives in the fork outside this repo, serves the stdlib (write1,nanotime, sbrk) and changes only when the toolchain is rebased.viis an application-level module: it must be build-tagged so a host build stubs every call to-ENOSYS(that is what keeps the browser's logic unit-testable off the guest), and it must live in this repo so its slot table is reviewed next to the code that uses it. Folding app syscalls into the runtime overlay would put browser ABI into the toolchain patch series and make the host-testable split impossible.Rollback plan
Pre-merge (this PR open): nothing to revert — the branch is additive and unmerged; closing the PR and deleting the branch removes it with no effect on
main.Post-merge:
git revert -m 1 <merge-sha>(orgit revert <squash-sha>). Dry-run, verified non-destructively in a throwaway clone:git diff --name-status origin/main...HEAD→ 40 additions + 1 modification (the generateddocs/gate-fleet-inventory.md);git merge-tree --write-tree origin/main HEAD→ exit 0 (clean merge);git revert -m 1 <merge>→ 0 files differ fromorigin/main.Trigger conditions: a regression on
mainattributable to these paths, or the gate failing on a clean checkout. No data migration and no persisted state are involved — the runtime stores live in the VM share, per-run, and nothing outside the repo is written.Side-effect statement
No force-push, no history rewrite (both amends happened before the branch was first pushed), no remote branch deleted, no existing file modified except the generated gate inventory, no configuration or data changes. Coordination actions: filed claim #1248, commented on #1244 (card home) and #1248 (slice evidence).