Skip to content

Releases: yinnho/aginxbrowser

v0.2.6

Choose a tag to compare

@yinnho yinnho released this 31 Aug 15:26

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 fetch and search result lands in a local SQLite (FTS5) store automatically and stays queryable across restarts; CJK text indexes per-glyph. New cache_* MCP tools let an agent search its own past reads. On shared/multi-tenant installs set AGINXBROWSER_STORE_SCOPE=session.
  • CSS sizing keywordswidth: min-content / max-content / fit-content now resolve in the diting layout engine with real measure passes over the subtree; fit-content applies the CSS fit-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-width sizing keywords remain unparsed for now — the width slot is the honest scope of this batch.

Fixes

  • CDP: Runtime.evaluate sync-path exception semantics pinned against Chrome; RemoteObject primitives carry real values (undefined no longer collapses to null, no spurious handles — obscura#779); setUserAgentOverride now applies acceptLanguage to 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_click returns the landed page text, matching the /click evidence 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

Choose a tag to compare

@github-actions github-actions released this 29 Aug 23:01

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/screenshot stay 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-block as 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=1 to 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-argument ModuleLoader::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.callFunctionOn awaited-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

Choose a tag to compare

@github-actions github-actions released this 29 Aug 03:01

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=0 to 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_list tool; Mcp-Session-Id dual 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)

  • getComputedStyle now 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-index stacking is respected by elementFromPoint (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.
  • matchMedia evaluates against the live viewport.

CDP / automation

  • dispatchMouseEvent emits the pointer pair before the mouse pair (obscura#739).
  • Input.insertText implemented (obscura#577).
  • Box-model/quads emit integer coordinates (obscura#576).
  • objectId lookups: JSON literals resolve honestly; number remote objects spelled like Chrome (obscura#541).
  • setExtraHTTPHeaders fans 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_PROXY no longer silently hijack engine traffic — AGINXBROWSER_PROXY is 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/XHR response bodies and robots.txt streams are size-capped (obscura#581 OOM).

Housekeeping

  • Compiler-flagged dead code swept; import-map prefix-match never_loop fixed.
  • README catches up with the engine registry (diting modules).
  • Docker verify strips the leading v from the image tag.

Full changelog: v0.2.3...v0.2.4

v0.2.3

Choose a tag to compare

@github-actions github-actions released this 27 Aug 18:55

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 (after evict_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

Choose a tag to compare

@yinnho yinnho released this 27 Aug 17:01

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/measure record real entries (500-cap buffers), Chrome-shaped TypeError/SyntaxError on bad args, clearMarks/clearMeasures honor names
  • getEntriesByType('navigation') derives a Level-2 entry from performance.timing — and __diting_init now 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: auto actually 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, and indeterminate is a real IDL property instead of an expando (obscura#721 same-hole family)
  • Same-origin POSTs send Origin (f4904fa) and fetch()/XHR carry Fetch-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/aginxbrowser built by CI on every release (e0e6e63), mirrored to Docker Hub yinnho/aginxbrowser when 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

Choose a tag to compare

@github-actions github-actions released this 26 Aug 07:17

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 /download and MCP tool download — tool count 13→14
  • Chunked streaming to disk (no full-buffer), SHA-256 verified on completion
  • Resumable via Range when the server supports it; optional to path, overwrite flag
  • 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 doctor
  • install.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 v prefix and 404'd (aginxbrowser-v0.2.0-<target>.tar.gz, not aginxbrowser-0.2.0-…); README build commands now carry --features stealth,screenshot instead 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

Choose a tag to compare

@github-actions github-actions released this 26 Aug 00:29
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>