Skip to content

WEB.ELF: an in-guest Go browser (M53 Card 3, Go shell + our own Go renderer) - #1258

Merged
drawmeanelephant merged 5 commits into
mainfrom
agent/t3code/web-go-browser
Sep 14, 2026
Merged

drawmeanelephant merged 5 commits into
mainfrom
agent/t3code/web-go-browser

Conversation

@drawmeanelephant

@drawmeanelephant drawmeanelephant commented Sep 14, 2026

Copy link
Copy Markdown
Owner

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 from user/src/lib/font8x8.zig).

M53 Card 3 under umbrella #1244. Closes #1248.

Kernel untouched, no new syscalls, no build.zig changes, no new repo docs. Everything new lives under user/go/**, tools/go/build-web.sh, tools/go/extract-font8x8.py, tools/gate/specs/live-web.spec (+ the generated docs/gate-fleet-inventory.md).

Post-interruption changes

The session that produced 4df276ad0e191e was interrupted mid-run; db037e5 is the first commit made after resuming. Reconciliation of the interruption:

Thing State
Remote branch vs local identical (git rev-list --left-right --count HEAD...origin/agent/t3code/web-go-browser0 0)
Stranded local work none — the only untracked paths are .openclaw-tmp/ (scratch logs) and docs/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)
Abandoned markers none (grep -rn 'TODO|FIXME|WIP' user/go tools/go finds no source markers; no disabled tests)
Gate evidence at the time the 11/11 PASS was captured on the working tree (revision d0e191e dirty-files=12), so the gate was re-run on the committed tip and the report now names db037e5

The interruption and its guard. The runs were killed by the harness while a long VM gate was attached to a piped exec session (an inherited pipe also kept a forked helper alive once, which got the run SIGTERM'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)

Criterion Commit Evidence
Browser opens a window, renders, navigates (back/forward/reload/stop), follows a link 4df276a live-web boots 01–02; artifacts/live-web-serial-01.log, snap-02-*.raw
Rendering is this project's own (no Blink/WebKit/Gecko/Servo, no cgo) 4df276a user/go/webrender/**, go test; ADR 0028 rules kept (no JS, no cascade)
Support matrix backed by tests 4df276adb037e5 matrix in #1248; 63 host tests + 3 golden PNGs + live-web pixel probes
Page loading honest about security 0831790 boot 05 (web: error https, no request armed while the responder is armed), redirects host-tested, TestClassifyTargetRefusesHTTPS
Bad input / bad networks handled 0831790, db037e5 boots 04, 06, 07, 08, 11; FuzzHTML 2.7M execs; TestSchemeShapedTargetsAreRefused
Stores actually stored and reviewable d0e191e boots 09–10; host-side reads of $VG_SHARE (WEB-HISTORY.TXT, WEB-CACHE.TXT, WEB-C-*.BIN, WEB-DL-*)
Budgets measured, not asserted db037e5 web: budget … on every boot; web: budget over absent; boot 11
One-command build 4df276a bash tools/go/build-web.sh browser WEBWEB.ELF 1,376,416 B (< 2 MiB exec_program_max)
Isolation baseline (page cannot act) db037e5 boot 11 hostile fixture: renders inert, 0 web: fetch, 0 web: error

Explicitly 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)

git checkout agent/t3code/web-go-browser
cd user/go && go test ./... && go vet ./...        # 63 tests, vet clean
cd ../.. && bash tools/go/build-web.sh browser WEB # WEB.ELF < 2 MiB
just gate live-web                                 # 11 boots, needs artifacts/disk.img + the signed VMRunner

Expected: go test green, build-web prints the size and "size ok", and the gate ends vgate live-web: PASS (11/11 runs) with artifacts/live-web-report.txt naming the tip revision. The gate is not hermetic: it needs the GOOS=virelai fork (tools/go/apply.sh) and a built disk image, and it fails honestly with a hint when either is missing.

Handoff notes

  • Toolchain: the guest toolchain is the fork at ../go-virelai (built once via bash tools/go/apply.sh). tools/go/build-web.sh links user/go at $GOPATH/src/virelai so module-mode host tests and GOPATH-mode guest builds use the same import paths.
  • Regenerating the font: python3 tools/go/extract-font8x8.py rewrites user/go/webrender/font/font8x8.go from the Zig table.
  • Goldens: WEBRENDER_UPDATE_GOLDEN=1 go test ./webrender/ -run Golden re-pins; a diff fails with *.actual.png for review and only passes again after WEBRENDER_ACCEPT_DIFF=1.
  • Diagnostics: user/go/probe prints the monotonic clock and a delta (proves the budget clock is live); WEBRENDER_DUMP= in webrender dumps laid-out primitives.
  • Known limitations (also in claim: WEB.ELF: an in-guest Go browser (Go app shell + our own Go renderer) #1248): no JavaScript, no CSS cascade, no DNS/HTTPS (https is refused, never downgraded), no tabs, ASCII-only face, images are placeholders, cookie storage on the wire is host-tested only (the harness responder sends no Set-Cookie).
  • Open follow-ups: DNS resolver, TLS (trust store lives with the M50/M51 arcs), TABWM tab lifecycle, session restore, crash-isolated renderer, release/install/rollback, CI wiring for live-web.

Why there are two syscall gateways

user/go/vi/vi_arm64.s is deliberately a second svc #0 stub file beside the toolchain overlay's runtime/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. vi is 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> (or git revert <squash-sha>). Dry-run, verified non-destructively in a throwaway clone:

  • git diff --name-status origin/main...HEAD40 additions + 1 modification (the generated docs/gate-fleet-inventory.md);
  • git merge-tree --write-tree origin/main HEAD → exit 0 (clean merge);
  • merge the branch, then git revert -m 1 <merge>0 files differ from origin/main.

Trigger conditions: a regression on main attributable 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).

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.
@drawmeanelephant

Copy link
Copy Markdown
Owner Author

Gate re-run on the committed tip (post-interruption evidence)

The 11/11 run captured before this PR was opened carried revision d0e191e dirty-files=12 (it ran on the uncommitted slice-4 tree). Re-ran it on the committed tip:

revision: db037e53bea7644eb161606e786beaa9fa2be168 branch=agent/t3code/web-go-browser dirty-files=2
date: 2026-09-14T15:55:19Z
PASS: 11/11

dirty-files=2 are the two untracked paths only — .openclaw-tmp/ (scratch logs) and docs/web-browser.md (the doc the maintainer asked to drop; deleting it needs an interactive approval that has timed out three times on this machine). No tracked file is modified at the tip: git status --short shows exactly ?? .openclaw-tmp/ and ?? docs/web-browser.md.

Raw verification on the tip (artifacts/web-verify/verification-db037e5.txt, gitignored per repo evidence policy): go test ./... exit 0 (63 tests), go vet ./... exit 0, build-web.sh exit 0 → WEB.ELF 1,376,416 B.

@drawmeanelephant

Copy link
Copy Markdown
Owner Author

Review response — the three nits

1. 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 runtime/sys_virelai_arm64.s belongs to the Go runtime (stdlib paths, changes only on a toolchain rebase); user/go/vi/vi_arm64.s is an application module that must be build-tagged so host builds stub every call to -ENOSYS (which is what makes the browser's logic unit-testable off the guest) and must live where its slot table is reviewed beside its users. Merging them would push browser ABI into the toolchain patch series.

2 & 3. .openclaw-tmp/ (139 MB) and docs/web-browser.md — still there, and it is not for want of trying. Four separate attempts to remove them have now been refused by this machine's safety guard (APPROVAL_TIMEOUT_DENIED, approval prompt times out with no human at the keyboard), and the guard forbids retrying within the same turn. They remain untracked and are outside the claim's Touches, so they cannot reach main by way of this branch. One line finishes it on a machine with an approver:

cd /Users/tbuddy/t3/zig/z-dipshit-ac-ds && rm -f docs/web-browser.md && rm -rf .openclaw-tmp

CI observed just now (gh pr checks 1258): VZ hardware gates pass, vz shards 0–3 pass, Swift launcher + custom-virtio spike pass, both "Validate site publication" runs pass — and Build (macOS + Swift Launcher) is still pending at 0s, which matches your BLOCKED reading. Nothing in this PR touches that job's inputs.

@drawmeanelephant

Copy link
Copy Markdown
Owner Author

Nits 2 & 3 resolved — worktree is clean

The approval prompt came through this time:

$ git status --short --branch
## agent/t3code/web-go-browser...origin/agent/t3code/web-go-browser
$ git status --porcelain | wc -l
0

docs/web-browser.md and .openclaw-tmp/ (139 MB of session scratch) are both removed. Nothing else changed: branch still 0 0 against origin/agent/t3code/web-go-browser, tip still db037e5, PR still MERGEABLE at that head. No commit was needed — both paths were untracked by construction, which is also why they could never have reached main.

So the "before merge" checklist reads: nit 1 answered in the description, nits 2 and 3 done, CI green except the queued Build (macOS + Swift Launcher) job that this PR does not feed.

@drawmeanelephant

Copy link
Copy Markdown
Owner Author

CI failure diagnosis (inventory drift, not your code)

The red Build job fails on exactly one line: tools/gate/ count 234 (committed) vs 235 (fresh render). Cause: live-wm-pacing.spec (buffy's #1247, commit b6730d3) landed on main after this branch last re-rendered the inventory. Classic main-moved-under-the-PR race — nothing in this diff is at fault (VZ shards + Swift + site all green).

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-lease

Then 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.
@drawmeanelephant

Copy link
Copy Markdown
Owner Author

Maintainer asked me to get this one mergeable, so I pushed a merge of main onto this branch (516f974, fast-forward from db037e5 — no force, no rewriting, nothing of yours dropped).

Why it was blocked: two separate things, both mechanical.

  1. mergeable: CONFLICTING — the branch was 22 commits behind main (since my Gate repair: live-sched-ring has been unmatchable since eb37fbe (#1253) #1254/Stop the required VZ check reporting OK when zero class-B gates ran — and make its failure branch reachable (#1256) #1257/VZ gate preflight: a runner without its entitlements, or a host that cannot host a guest, must fail loudly instead of looking like a broken gate (#1259) #1260 landed in tools/lib, tools/gate, ci.yml, justfile).
  2. Build (macOS + Swift Launcher) red — and the only failing step was bash tools/inventory-gates.sh --check: docs/gate-fleet-inventory.md drifted from a fresh render by one line.

Why they were the same thing. The only conflicted file was docs/gate-fleet-inventory.md, which is the generated fleet report — the project rule is re-render it, never hand-edit or hand-merge it. So I re-ran bash tools/inventory-gates.sh, which resolves the conflict by construction and folds in both sides:

  • tools/gate/ 234 → 238 (this branch's new live-web.spec, plus main's three new preflight files)
  • live-sb3-surface-handoff 8 → 9 asserts — main had already moved ahead there, and this branch's copy of the report was stale on that line too

Nothing of yours was edited. No source file on this branch was touched by the resolution; the merge only adds main's commits. Verified on the merged tree: inventory-gates --check green, lint-workflows.sh class-A parity green (19/19), and the VZ preflight self-test green (35/35).

If you're still working locally, git pull before your next push — I put one merge commit on the branch, so a bare push will be rejected rather than silently clobbering it. If you'd rather the branch history were linear, a rebase onto main would need the same single re-render and I'm happy to redo it that way instead; say the word and I'll drop this commit.

@drawmeanelephant
drawmeanelephant merged commit 1b824f3 into main Sep 14, 2026
9 checks passed
@drawmeanelephant
drawmeanelephant deleted the agent/t3code/web-go-browser branch September 14, 2026 17:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

claim: WEB.ELF: an in-guest Go browser (Go app shell + our own Go renderer)

1 participant