Releases: yinnho/aginxbrowser
Release list
v0.2.6
v0.2.6 is a rendering + data-durability batch: CSS sizing keywords and calc() widths land in the diting engine, a durable local cache sits behind every fetch/search, and a round of CDP/stealth contract fixes ships.
Highlights
- Durable local cache — every
fetchandsearchresult lands in a local SQLite (FTS5) store automatically and stays queryable across restarts; CJK text indexes per-glyph. Newcache_*MCP tools let an agent search its own past reads. On shared/multi-tenant installs setAGINXBROWSER_STORE_SCOPE=session. - CSS sizing keywords —
width: min-content / max-content / fit-contentnow resolve in the diting layout engine with real measure passes over the subtree; fit-content applies the CSSfit-content(stretch, max-content)clamp. Verified pixel-for-pixel against the Blitz pipeline, stylesheet and inline paths. - calc() widths + clean shrink-to-fit — mixed percent+px
calc()on width/min/max resolves exactly once against the settled containing block (obscura#767 family), and shrink-to-fit now trims run-edge whitespace so indentation whitespace stops inflating fit-content widths (obscura#764 family).
Engine / JS
- XHR binary round-trips (arraybuffer/blob response bodies), CSSOM initial values in
getComputedStyle, and a binding-queue cap (obscura#754/#716/#771). min-width/max-widthsizing keywords remain unparsed for now — the width slot is the honest scope of this batch.
Fixes
- CDP:
Runtime.evaluatesync-path exception semantics pinned against Chrome;RemoteObjectprimitives carry real values (undefinedno longer collapses tonull, no spurious handles — obscura#779);setUserAgentOverridenow appliesacceptLanguageto the wire header and navigator (obscura#777 class). - Networking: known-blocked domains auto-proxy on page/session/CDP navigations, and the robots.txt gate falls back to a legacy-TLS (CBC) transport for CBC-only servers — rustls's missing TLS 1.2 CBC suites no longer turn into a 403 (obscura#769).
session_clickreturns the landed page text, matching the/clickevidence contract.
Install / upgrade: curl -fsSL https://browser.aginx.net/install.sh | bash (pin with VERSION=v0.2.6). Hosted MCP endpoint: https://browser.aginx.net/mcp.
v0.2.5
v0.2.5 adds a new platform (static musl binaries for AginxOS devices) and a rendering-accuracy batch: baseline alignment, atomic inline-blocks, and a robots.txt policy flip.
Highlights
- New platform:
aarch64-unknown-linux-musl— a fully static, feature-minimal binary (bare file +.sha256, agpkg convention) built with cargo zigbuild. AginxOS devices (Pixel 5 class, busybox, no glibc) pull it on first boot.stealth/screenshotstay opt-in and are not in this asset. - Inline baseline alignment (blitz#750 semantics) — mixed font sizes and text+inline-block runs now sit on a shared baseline instead of sharing a top edge. Inline-blocks align to the baseline of their last in-flow line box; empty or replaced boxes fall back to the bottom edge. Implemented as a post-layout per-line shift (taffy leaves can't report baselines), so uniform-font runs keep bit-identical geometry.
display: inline-blockas atomic shrink-to-fit boxes — inline lists and chip stacks wrap inside their containing block instead of rendering as full-width blocks or one ~1000px line that inflates table min-content floors (obscura#750 family).- robots.txt checking is now opt-in — flips v0.2.4's default-on policy: set
AGINXBROWSER_HONOR_ROBOTS=1to enable. The robots fetch also follows the same direct/proxy decision as the page fetch, so a proxied robots request can no longer cache a DenyAll against a directly reachable page.
Engine / JS
- deno_core 0.350 → 0.411 (v8 137 → 150.4) —
#[op2(async(deferred))], 4-argumentModuleLoader::load, manual HandleScope assembly; v8 150.2+ is also what makes the official musl prebuilt archive possible. - Blob URLs throw Chrome-shaped
TypeErrors, mint v4 UUIDs, and revoke from both stores. Runtime.callFunctionOnawaited-rejection shape pinned against Chrome (obscura#746 contrast).
Fixes
- Script bodies no longer count as visible text, and empty Tier-1 conversions are deferred — cuts challenge-page flicker on byte-WAF sites.
- CDP: mouse-click checkbox activation clears
indeterminate. - Stealth bootstrap blob layer finishes the diting rename.
Operations
- Per-domain rate limit + session page budget ship as env-tunable guardrails (
AGINXBROWSER_DOMAIN_RATE_PER_MIN,AGINXBROWSER_SESSION_PAGE_LIMIT) — a browser, not a crawler.
Install / upgrade: curl -fsSL https://browser.aginx.net/install.sh | bash (pin with VERSION=v0.2.5). Hosted MCP endpoint: https://browser.aginx.net/mcp.
v0.2.4 — localStorage persistence, robots.txt by default, 30+ engine fixes
v0.2.4 ships 30+ fixes landed since v0.2.3 — the biggest batch yet. Everything below is verified by the hosted instance and covered by tests in-repo.
Highlights
- localStorage survives restarts — per-origin storage is flushed to disk and reloaded on boot (upstream obscura#629). Sessions, consent banners, and login hints now persist across process restarts.
- robots.txt compliance on by default — RFC 9309 parser, honored on every autonomous fetch path (
/fetch,/screenshot,/download,/click,/eval, body-grab).AGINXBROWSER_RESPECT_ROBOTS=0to opt out. - Session recording & replay — interactive sessions record every action and can export a runnable curl replay script (
session_export). - MCP session semantics — new
session_listtool;Mcp-Session-Iddual semantics (join existing / spawn new) documented. - One-line installer hardened — release-asset mirror fallback, writable-bindir detection, docker hint for linux/arm64. Pin with
VERSION=v0.2.4.
Benchmark vs headless Chrome
Real-page tier reporting added (/bench):
| metric | aginxbrowser (auto) | headless Chrome |
|---|---|---|
| page load | 532 ms | 4053 ms |
| memory / page | 227 MB | 2.1 GB |
Chrome in the same region also hard-failed 12.5% of pages on CN routing; aginxbrowser 0%.
Rendering engine (diting)
getComputedStylenow reads the full stylesheet cascade, not just inline styles — and returns a whole-object snapshot (obscura#738 trap + perf).- Hit-testing follows paint order, so
z-indexstacking is respected byelementFromPoint(obscura#738). transform: translate(...)is computed into geometry (obscura#740).- Float layout: zones follow document order, same-side floats form a rail, Vector 2022 grid scaffold — float displacement no longer escapes grid containers; navboxes land at the bottom where they belong.
matchMediaevaluates against the live viewport.
CDP / automation
dispatchMouseEventemits the pointer pair before the mouse pair (obscura#739).Input.insertTextimplemented (obscura#577).- Box-model/quads emit integer coordinates (obscura#576).
objectIdlookups: JSON literals resolve honestly; number remote objects spelled like Chrome (obscura#541).setExtraHTTPHeadersfans out to the stealth transport (obscura#571).- Enter in a textarea splices the newline at the caret.
- Pending JS navigation drained on click/eval surfaces (obscura#618).
- srcset: prefetch the selection winner only, not every candidate (obscura#667/#662 class).
Network / stealth
- Implicit env-proxy matcher pinned off:
HTTP_PROXY/HTTPS_PROXY/ALL_PROXYno longer silently hijack engine traffic —AGINXBROWSER_PROXYis the only proxy knob (obscura#491). - One source of truth for the User-Agent across HTTP transports and the in-page JS persona (obscura#481).
fetch/XHRresponse bodies and robots.txt streams are size-capped (obscura#581 OOM).
Housekeeping
- Compiler-flagged dead code swept; import-map prefix-match
never_loopfixed. - README catches up with the engine registry (diting modules).
- Docker verify strips the leading
vfrom the image tag.
Full changelog: v0.2.3...v0.2.4
v0.2.3
v0.2.3 — status page, ARM64 images, and a Docker image that actually boots
A packaging release: three changes, all for self-hosters — the last one because the first multi-arch images would have shipped dead.
Docker images were dead on arrival — fixed (b642f3b)
The new verify job executes the merged multi-arch image on a real arm64 runner before a release is called done, and its very first run caught this: every image published before now exited 127 at boot — error while loading shared libraries: libfontconfig.so.1. The screenshot feature's font enumeration links fontconfig/freetype dynamically and debian:bookworm-slim ships neither. Nobody had ever run a pushed image (the hosted instance runs the bare binary), so it had never surfaced. The runtime stage now installs libfontconfig1, and ldd in CI confirms every dynamic library resolves.
Status page at / and /status (03d5602)
Opening http://<host>:8089/ in a browser now serves a real page instead of a 404: version, uptime, live session count, capability chips, and the endpoint table — server-rendered, no client JS, no external assets, works on an offline LAN box. Agents keep using /health and /doctor; /status is the canonical alias.
This is what Umbrel's app store requires of an install ("must open to a web UI / status page"), and honestly it was overdue for plain self-hosting too. Uptime and the session readout are pinned by a smoke test.
Multi-arch Docker images: amd64 + arm64 (9e0dfbf + d6fbda3)
Docker images now ship as multi-arch manifest lists for linux/amd64 and linux/arm64 — Raspberry Pi 4/5 and ARM NAS boxes can pull and run them directly. Each architecture builds natively on GitHub's free arm64 runners (cross-compiling the V8 + BoringSSL stack under QEMU costs an hour; native builds take ~12 min), a merge job stitches the per-arch tags into one manifest per release tag, and the verify job then boots the merged image on arm64 as user 1000 with the cookie store bind-mounted — the exact runtime shape Umbrel installs use — checking /health, the status page, and that the cookie store is written under the mounted data dir.
ghcr.io/yinnho/aginxbrowser and docker.io/yinnho/aginxbrowser, tags 0.2.3 / 0.2 / latest.
Smaller notes
SessionManager::session_count()— live session readout for the page (afterevict_expired)- Smoke test for the status page (
status_page_serves_html_at_root) - CI fix en route: the manifest-merge job was missing its registry login (401 from GHCR on first dry-run)
v0.2.2 — byte-WAF solving, deep V8 stacks, Performance Timeline
v0.2.2 — byte-WAF solving, deep V8 stacks, Performance Timeline
27 commits since v0.2.1. The theme: pages that used to come back empty now come back rendered — two whole classes of "site X returns a blank shell" are gone, and the JS runtime's performance surface no longer lies to analytics.
Auto-solve byte-WAF JS challenges (94c2ced + e274d9a)
juejin.cn (ByteDance properties generally) gates pages behind a <body onload="readygo()"> SHA-256 proof-of-work: no solver, no content — the page just never finishes "verifying". The engine now detects the stub (a readygo function plus a near-empty body), forwards the onload challenge, evaluates the PoW statements, and drains the challenge reload. Tier 1 defers byte-WAF stubs so the challenge script can register; Tier 2 drains the reload after the solve.
Result on juejin.cn article pages: challenge passes cleanly, ~16s cold, full rendered markdown out. Anonymous feeds still need login cookies (the recommend API returns data:null to logged-out visitors — same as raw curl), which fetch's cookies field handles.
V8 stack ceiling raised for minified bundles (e6e5d24)
A 1.09 MB Tailwind-inline docs page rendered to a RangeError: Maximum call stack size exceeded mid-bundle. Root cause is two-layered: V8 counts JS frames against an internal ~984KB limit that is independent of the hosting thread's native stack, and the default 2 MB thread dies a few thousand frames in where desktop Chrome runs the same bundle fine.
The fix sets both: every V8 runtime and session thread now spawns with an explicit deep stack (32 MB default, AGINXBROWSER_JS_STACK_MB to tune, 1–1024), and --stack-size is applied before the first isolate (with a 2 MB margin). A 20,000-frame recursion regression test pins it — past what any default-threaded embedder survives, comfortably inside Chrome's own ceiling.
Performance Timeline is real (02a779c)
mark()/measure() were no-ops and getEntriesByType() always returned [], so analytics bundles (Sentry, web-vitals wrappers) reading user-timing landed in fallback branches. Now:
mark/measurerecord real entries (500-cap buffers), Chrome-shapedTypeError/SyntaxErroron bad args,clearMarks/clearMeasureshonor namesgetEntriesByType('navigation')derives a Level-2 entry fromperformance.timing— and__diting_initnow stamps a plausible DCL/load offset chain instead of all-equal epochs (every derived duration was 0, which is its own automation tell)- paint entries (
first-paint/first-contentful-paint) derive from the DCL offset once per navigation - entries carry prototype accessors +
[native code]toString, same lie-detector posture as the FontFace shim 'resource'stays honestly empty: no per-request network timings exist here, and a fabricated waterfall would be a lying telemetry surface — worse than absent
Live on browser.aginx.net — verified on posthog.com: dcl=374ms, load=1057ms, paint ×2, page renders unchanged.
Fingerprint and locale consistency (35ec115 + 4e68950)
obscura#734 lineage, two rounds: Intl's default locale now agrees with navigator.language and the Accept-Language header (three-way consistency a detector can cross-check), and the Intl wrapper functions themselves are fingerprint-proof — property descriptors and toString output match stock V8, closing the follow-up.
Smaller fixes worth naming
margin: autoactually centers (c5799df) — in-flow blocks and abspos elements pass real auto-margins into taffy instead of computed zeros (taffy#923 lineage)- Form controls behave (767fe52 + 88b8496) — fieldset-disabled controls never activate, checkbox click clears
indeterminate, andindeterminateis a real IDL property instead of an expando (obscura#721 same-hole family) - Same-origin POSTs send
Origin(f4904fa) andfetch()/XHR carryFetch-Metadata+ client-hint headers (5a87cca) — servers that gate on header presence stop 403-ing - Idle sessions keep pumping (67bccc8) — the event loop runs 200ms slices between tool calls (timers, promise chains, fetch callbacks keep firing while a session idles); now pinned by a regression test instead of only by the WorkOS Radar incident
- Test modules split (72f7b30) — oversized test files broken into their own modules; no new abstraction
Packaging
- Docker images:
ghcr.io/yinnho/aginxbrowserbuilt by CI on every release (e0e6e63), mirrored to Docker Hubyinnho/aginxbrowserwhen secrets are present (df9bcc0 + follow-ups) - crates.io metadata (700fa53) — readme/repository/keywords wired for the future publish
llms-install.md+ logo asset for MCP directories (e731c76)
Full API reference: docs/API.md · Hosted instance: browser.aginx.net · claude mcp add aginxbrowser --transport http https://browser.aginx.net/mcp
v0.2.1
v0.2.1 — download layer, inline rects, one-line installer
Three batches since v0.2.0: a new download tool (MCP 13→14), a layout fix for flattened inline elements, and self-host onboarding (doctor + installer).
download — streaming HTTP(S) downloads (1af054a)
Agents fetch more than pages. download streams binaries/archives/datasets to disk instead of hauling them through context:
- New HTTP endpoint
POST /downloadand MCP tooldownload— tool count 13→14 - Chunked streaming to disk (no full-buffer), SHA-256 verified on completion
- Resumable via
Rangewhen the server supports it; optionaltopath,overwriteflag - SSRF-guarded by the same resolver rules as
/fetch
Fixed: flattened inline elements lost their rect (aa32edd)
Same bug family as obscura#722: plain inline wrappers (<span>, <label>, <a> with no block box) get flattened into the enclosing run during layout — correct for line-breaking, but the element lost its box entirely, so getBoundingClientRect had nothing to report (grid-fallback zeros).
The fix keeps the flatten but synthesizes each flattened element's rect as the union of its hoisted children's absolute boxes. One real bounding box per element — what coordinate-based clicking needs; nested inlines union level by level.
Before → after (16px font, Chrome ref ≈ 48×17):
| probe | before | after |
|---|---|---|
<label>toggle</label><input type=checkbox> |
grid-fallback | [8, 8, 47, 19] |
<span> mid-paragraph |
grid-fallback | [60, 16, 65, 19] |
| nested span outer / inner | grid-fallback | [21,16,44,19] / [50,16,15,19] |
<a>clickme</a> |
grid-fallback | [8, 8, 58, 19] |
Live on browser.aginx.net; locked by inline_elements_keep_real_rects_after_flatten.
aginxbrowser doctor + one-line installer (f088692)
Self-hosting used to be "clone, build, and hope". Now:
curl -fsSL https://raw.githubusercontent.com/yinnho/aginxbrowser/main/install.sh -o install.sh
less install.sh && bash install.sh
aginxbrowser doctorinstall.sh— platform detect → prebuilt tarball → SHA-256 verify →~/.local/bin, then runs doctor as a self-check (PREFIX=/VERSION=to override)aginxbrowser doctor— new subcommand, runs before the server boots: compiled features, bundled-font ink probe (rasterizes 汉字 and checks real ink, not just a cmap hit), env posture (bind/proxy/SSRF warning), one live egress probe. Exit code 1 on hard failure so containers can gate on it- Install docs fixed: prebuilt asset URLs kept the
vprefix and 404'd (aginxbrowser-v0.2.0-<target>.tar.gz, notaginxbrowser-0.2.0-…); README build commands now carry--features stealth,screenshotinstead of a featureless build that silently loses both
Checksums for this release's tarballs are the matching .sha256 assets.
Full API reference: docs/API.md · Hosted instance: browser.aginx.net · claude mcp add aginxbrowser --transport http https://browser.aginx.net/mcp
v0.2.0
fix(ci): build x86_64 macOS on macos-latest, not macos-13 macos-13 runners have no free-tier capacity (queued 40+ min); cross-compile x86_64-apple-darwin from the arm64 runner instead. Co-Authored-By: Claude <noreply@anthropic.com>