Tier-2 Headless Ingestion (#68)
Decision (2026-06-07): JS-rendered + hostile boards are scraped by a self-hosted fleet of Playwright pods on k3s, called by a lean Rust device (thin client). No managed/external headless API — Browserbase is explicitly OUT (risk too high, 2026-06-07). The fleet is fully self-hosted. Builds + deploys ride the build-brain → boot.corbet.ch → k3s pipeline (see ~/.agent/topics/shared/fleet-build.md).
Capability ladder
| Tier | Renderer | For | Cost | Where |
|---|---|---|---|---|
| 1 | static-musl Rust device, curl/HTTP |
static HTML + JSON-LD boards | free | k3s jobcache-device; also a ≤512 MB lean Nix image for free micro-servers (see below) |
| 2 | self-hosted Playwright pods (Chromium + stealth) | JS-rendered + hostile boards | free (own compute) | k3s: Playwright-pod fleet + tier-2 device Deployment |
A board flagged render goes to Tier-2. If a board defeats Tier-2's self-hosted stealth, it is flagged for manual review — we do NOT fall back to a paid external API. The lever is improving Tier-2 stealth, not per-page spend.
Why this shape
- Playwright, not raw Rust CDP. Hostile boards need best-in-class auto-waiting + the stealth-plugin ecosystem (Node/Playwright).
chromiumoxide(Rust CDP) handles plain JS rendering but has a weak stealth story — wrong tool for the actual target. - Browser as a service, device stays Rust. The device is static-musl Rust (broker lease-cycle, uniform fleet). Don't fold Node into it. The device sends a URL, gets rendered HTML/JSON-LD back — preserves the Tier-1 device shape and the build pattern. The browser scales independently.
- Fully self-hosted. Tier-2 carries all JS/hostile volume at zero per-page cost on the k3s Playwright fleet. No external headless API (Browserbase ruled out — risk too high). Boards that defeat self-hosted stealth are flagged for review; the lever is improving Tier-2 stealth, not paying per page.
Architecture
- Browser service (new k3s Deployment): a Playwright server exposing
render(url, opts) → { html, jsonld, status }over the cluster network. Stealth config; concurrency-capped; bigger resources (Chromium ≈ 0.5–1.5 GB/instance). Image built via the build-brain — NixdockerTools(nodejs + playwright-driver + chromium) preferred for fleet-consistency; pin upstreambrowserless/chromiumas the fallback if Nix-packaging Playwright's browser paths proves too fiddly. - Tier-2 device (new k3s Deployment): the same
jobcache-devicebinary with a newrendertask class +JOBCACHE_DEVICE_TIER=headless,JOBCACHE_TASK_CLASSES=render. On arenderlease it calls the browser service (cluster-internal URL), then runs the existing extraction + broker submit. No DB/credentials (same as Tier-1 — API-only). - Capability routing (sporewright orchestrator): the device advertises
DEVICE_TIER+TASK_CLASSES; the planner tags boards needing JS asrenderwork and routes it only to tier-2 devices. Tier-3 is a per-board escalation flag when tier-2's success rate on that board drops.
Build / deploy (via build-brain — SAVED until keys / build-brain access)
- Browser-service image → add a
dockerTools.buildLayeredImage(nodejs+playwright+chromium) package tocareervector/flake.nix; add abuild_and_push … + ctr importline toinfra/infra/scripts/fleet-build.sh. - Tier-2 device → reuses
jobcache-device-image(therendertask class lives in the same binary); a new k8s Deployment with the headless tier env + largerresources. - Deploy → k3s Deployments (browser service + tier-2 device) under
jobcache/ingest/deploy/kubernetes/; capability routing shipped with the orchestrator (sporewright-server). - Both image builds and the k3s rollout are build-server work (fleet-build CronJob +
ctr import+kubectl rollout) — deferred until keys/access per the standing "save build-server work" instruction.
Implementation order (code now; build/deploy when keys ready)
- Device-side
rendertask class (Rust,crates/jobcache-device) — thin client to the browser service. [code now] - Browser-service render endpoint + its
flake.niximage derivation. [code now] - Orchestrator capability routing for
rendertasks (board JS-flagging + tier matching) in sporewright. [code now] - k3s manifests (browser-service Deployment + tier-2 device Deployment). [code now]
- Build on the build-brain + deploy on k3s (Playwright-pod fleet + tier-2 device + the ≤512 MB lean device image for free micro-servers). [SAVED — keys/access]