Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions Caddyfile
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
@api path /api/* /api
reverse_proxy @api localhost:8090

# OpenMCP relay (tronbrowser.dev/mcp/tron): same API process. Its descriptor
# is the static /.well-known/openmcp.json below, which is what makes the
# listing "verified" on a catalog (served from this origin, not registered).
@mcp path /mcp/* /mcp
reverse_proxy @mcp localhost:8090

# Security headers on every response.
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
Expand All @@ -35,6 +41,8 @@
header /install.sh Content-Type "text/x-shellscript; charset=utf-8"
header /sitemap.xml Content-Type "application/xml; charset=utf-8"
header /manifest.json Content-Type "application/manifest+json; charset=utf-8"
header /.well-known/openmcp.json Content-Type "application/json; charset=utf-8"
header /.well-known/openmcp.json Access-Control-Allow-Origin "*"

# Caching: long for images, SHORT for code/HTML so updates propagate fast
# (no content hashing yet — don't let stale JS strand logged-in users).
Expand Down
76 changes: 59 additions & 17 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,25 +1,67 @@
# Single container for tronbrowser.dev: Caddy serves the static site and
# reverse-proxies /api to the bundled Hono API (one service, one domain).
# Built from the monorepo root context.
# reverse-proxies /api and /mcp to the bundled Hono API (one service, one
# domain). Built from the monorepo root context.
#
# The API now carries the OpenMCP relay at /mcp/tron, so the image also holds
# its two engines: ungoogled-chromium (the portable Linux build, headless) and
# Obscura. Both are pinned below; bump the ARGs to move them.

# --- build the API ---
FROM node:24-slim AS api
WORKDIR /api
COPY services/api/package.json ./
RUN npm install --no-audit --no-fund
COPY services/api/src ./src
RUN printf '%s' '{"compilerOptions":{"target":"ES2023","module":"NodeNext","moduleResolution":"NodeNext","outDir":"dist","rootDir":"src","strict":true,"skipLibCheck":true,"esModuleInterop":true},"include":["src"]}' > tsconfig.build.json \
&& npx tsc -p tsconfig.build.json \
&& npm prune --omit=dev
ARG UNGOOGLED_CHROMIUM_VERSION=152.0.7977.82-1
ARG OBSCURA_VERSION=0.2.2

# --- final: caddy + node ---
FROM caddy:2-alpine
# --- build the API (a pnpm workspace member: it imports @tronbrowser/sdk) ---
FROM node:24-bookworm-slim AS api
RUN corepack enable && corepack prepare pnpm@9.12.0 --activate
WORKDIR /repo
COPY package.json pnpm-workspace.yaml pnpm-lock.yaml tsconfig.base.json ./
COPY packages/browser-core/package.json packages/browser-core/
COPY packages/agent-runtime/package.json packages/agent-runtime/
COPY packages/sdk/package.json packages/sdk/
COPY services/api/package.json services/api/
RUN pnpm install --frozen-lockfile --filter @tronbrowser/api...
COPY packages/browser-core packages/browser-core
COPY packages/agent-runtime packages/agent-runtime
COPY packages/sdk packages/sdk
COPY services/api services/api
RUN pnpm --filter @tronbrowser/browser-core --filter @tronbrowser/agent-runtime --filter @tronbrowser/sdk --filter @tronbrowser/api build \
&& pnpm --filter @tronbrowser/api deploy --prod /out

# --- engines: fetched once at build time, not at boot ---
FROM debian:bookworm-slim AS engines
ARG UNGOOGLED_CHROMIUM_VERSION
ARG OBSCURA_VERSION
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl xz-utils && rm -rf /var/lib/apt/lists/*
RUN set -eu; arch="$(uname -m)"; case "$arch" in x86_64) uc=x86_64; ob=x86_64 ;; aarch64) uc=arm64; ob=aarch64 ;; *) echo "unsupported arch $arch" >&2; exit 1 ;; esac; \
mkdir -p /opt/ungoogled-chromium /opt/obscura; \
curl -fsSL "https://github.com/ungoogled-software/ungoogled-chromium-portablelinux/releases/download/${UNGOOGLED_CHROMIUM_VERSION}/ungoogled-chromium-${UNGOOGLED_CHROMIUM_VERSION}-${uc}_linux.tar.xz" \
| tar -xJ --strip-components=1 -C /opt/ungoogled-chromium; \
test -x /opt/ungoogled-chromium/chrome; \
curl -fsSL "https://github.com/h4ckf0r0day/obscura/releases/download/v${OBSCURA_VERSION}/obscura-${ob}-linux-stealth.tar.gz" \
| tar -xz -C /opt/obscura; \
test -x /opt/obscura/obscura

# --- final: caddy + node + tor + the engines ---
# Debian rather than Alpine: the portable ungoogled-chromium and Obscura are
# glibc binaries. Caddy is a static binary, copied from its own image.
FROM node:24-bookworm-slim
COPY --from=caddy:2 /usr/bin/caddy /usr/bin/caddy
# openssh-client: the store provisions BBS publisher accounts and generates
# ed25519 keypairs via `ssh`/`ssh-keygen` (services/api/src/store/fileshost.ts).
# tor: runs a Tor v3 hidden service in this same container so tronbrowser.dev is
# reachable over a stable .onion (start.sh writes torrc and boots it). The onion
# key persists on a Railway volume mounted at /var/lib/tor/hidden_service.
RUN apk add --no-cache nodejs openssh-client tor
# The lib* rows are what headless Chromium links against; fonts so text renders.
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates openssh-client tor \
libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 \
libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libasound2 libpango-1.0-0 \
libcairo2 libatspi2.0-0 libxshmfence1 libx11-6 libx11-xcb1 libxcb1 libxext6 libglib2.0-0 \
libdbus-1-3 libexpat1 fonts-liberation fonts-noto-color-emoji \
&& rm -rf /var/lib/apt/lists/*
COPY --from=engines /opt/ungoogled-chromium /opt/ungoogled-chromium
COPY --from=engines /opt/obscura /opt/obscura
ENV TRON_MCP_CHROMIUM_BIN=/opt/ungoogled-chromium/chrome \
OBSCURA_BIN=/opt/obscura/obscura
COPY Caddyfile /etc/caddy/Caddyfile
COPY apps/web/public/ /srv/
# Extension store (tronbrowser.dev/store) — static frontend; dynamic bits hit
Expand All @@ -29,9 +71,9 @@ COPY apps/extensions/public/ /srv/store/
# symlinks to them for local dev, but Docker COPY won't follow symlinks pointing
# outside the copied dir — so copy the real files in (these override the links).
COPY logo.svg favicon.svg hero.svg banner.png /srv/
COPY --from=api /api/dist /api/dist
COPY --from=api /api/node_modules /api/node_modules
COPY --from=api /api/package.json /api/package.json
COPY --from=api /out/dist /api/dist
COPY --from=api /out/node_modules /api/node_modules
COPY --from=api /out/package.json /api/package.json
# DB migrations run on boot (start.sh) so schema never drifts from the deploy.
COPY scripts/db-migrate.mjs /api/db-migrate.mjs
COPY packages/storage/migrations /api/migrations
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,12 @@ Linux phones (Librem 5 / PinePhone / Ubuntu Touch). See
to the cloud SQLite (Turso) or your own self-hosted backend.
- Keeps all Chromium features: extensions, profiles, bookmarks, history, PWAs,
DevTools.
- **`tron automate`** — an MCP server for agents: Obscura renders pages first
(fast, light), the Chromium session takes over for full-JS pages and bot
walls. Stdio for a local host, HTTP with an OpenMCP descriptor for a catalog.
The same relay runs hosted at **tronbrowser.dev/mcp/tron** (keyless
`fetch_page` + `screenshot_page`, listed on openmcp.logicsrc.com). See
[docs/mcp.md](docs/mcp.md).

## Monorepo

Expand Down
12 changes: 12 additions & 0 deletions apps/web/public/.well-known/openmcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"openmcp": "0.1",
"mcp": "https://tronbrowser.dev/mcp/tron",
"name": "TronBrowser",
"description": "Fetch any public web page as markdown, text, links or html, or take a screenshot, through TronBrowser: Obscura renders first (fast, light) and the ungoogled-chromium engine takes over for JS-heavy pages and bot walls. Keyless; a few fetches at a time.",
"url": "https://tronbrowser.dev",
"auth": { "kind": "none", "open": ["fetch_page", "screenshot_page"] },
"tags": ["browser", "fetch", "scrape", "screenshot", "chromium", "ungoogled-chromium", "obscura", "web", "hosted"],
"operator": "https://logicsrc.com/.well-known/openprofile.md",
"tools": ["fetch_page", "screenshot_page"],
"catalogs": ["https://openmcp.logicsrc.com"]
}
84 changes: 84 additions & 0 deletions apps/web/public/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@
tron run <script> Run a JS/TS script using @tronbrowser/sdk (--headless/--trace)
tron analyze [goal] Analyze/fill a form or page (--data, --execute, --json)
tron mcp Run a local MCP server over stdio (--headless)
tron automate MCP server: Obscura for scraping, Chromium for full-JS
pages (fetch_page picks; --engine pins one)
tron automate serve Same server over HTTP with an OpenMCP descriptor
tron automate fetch <url>
Print a page as markdown (--format text|links|html)
tron automate status Which engines are usable (--json)
tron trace start|stop Record commands into a .trontrace bundle
tron replay <bundle> Replay a recorded trace against the session
tron upgrade Update to the latest release ('tron update' works too)
Expand Down Expand Up @@ -225,6 +231,20 @@
command -v node >/dev/null 2>&1 || { echo "tron mcp needs Node.js (>=22) on PATH." >&2; exit 1; }
[ -f "$ENTRY" ] || { echo "This TronBrowser build lacks the MCP server. Run: tron upgrade" >&2; exit 1; }
exec env TRON_SESSION_BIN="$(session_bin)" node "$_ld/tron-node.mjs" "$ENTRY" "$@" ;;
automate)
# Obscura for scraping, the managed Chromium session for full-JS pages, one
# MCP server (PRD M3.8). Obscura lives next to the launcher (obscura-bin/),
# put there by the installer; TRON_OBSCURA_BIN tells the runtime where.
shift
_ld="$(dirname "$(readlink -f "$CURRENT" 2>/dev/null || echo "$CURRENT")")"
ENTRY="$_ld/sdk/automate-bin.js"
command -v node >/dev/null 2>&1 || { echo "tron automate needs Node.js (>=22) on PATH." >&2; exit 1; }
[ -f "$ENTRY" ] || { echo "This TronBrowser build lacks the automate runtime. Run: tron upgrade" >&2; exit 1; }
_ob="${TRON_OBSCURA_BIN:-$_ld/obscura-bin/obscura}"
if [ ! -x "$_ob" ] && [ "${1:-}" != "status" ] && [ "${TRON_OBSCURA_QUIET:-0}" != "1" ]; then
echo "tron automate: Obscura is not installed; every fetch will use Chromium. Install it with: curl -fsSL $INSTALL_URL | sh -s -- ensure-obscura" >&2
fi
exec env TRON_SESSION_BIN="$(session_bin)" TRON_OBSCURA_BIN="$_ob" node "$_ld/tron-node.mjs" "$ENTRY" "$@" ;;
run)
# Execute a JS/TS automation script that imports @tronbrowser/sdk (PRD M3.4).
shift
Expand Down Expand Up @@ -626,6 +646,65 @@
rm -rf "$tmp"; return 1
}

# Obscura (github.com/h4ckf0r0day/obscura, Apache-2.0) is the scraping engine
# behind `tron automate`: its own renderer plus V8, ~30 MB a page, tens of
# milliseconds on a light page. It is two ~100 MB binaries, so it is not in the
# release tarball; the installer fetches the pinned release next to the
# launcher (obscura-bin/) instead. Best-effort: without it `tron automate` runs
# everything through Chromium. Skip with TB_NO_OBSCURA_INSTALL=1, pin another
# release with TRONBROWSER_OBSCURA_VERSION.
OBSCURA_VERSION="${TRONBROWSER_OBSCURA_VERSION:-0.2.2}"

obscura_asset() { # -> asset name for this OS/arch, or nothing
os="$(uname -s)"; arch="$(uname -m)"
case "$os" in
Linux) ob_os=linux ;;
Darwin) ob_os=macos ;;
*) return 1 ;;
esac
case "$arch" in
x86_64|amd64) ob_arch=x86_64 ;;
aarch64|arm64) ob_arch=aarch64 ;;
*) return 1 ;;
esac
# The stealth build is a superset: --stealth turns the TLS fingerprint and
# tracker blocklist on, and without the flag it behaves like the plain build.
echo "obscura-${ob_arch}-${ob_os}-stealth.tar.gz"
}

download_obscura() { # dest_dir
dst="$1"
asset="$(obscura_asset)" || return 1
url="https://github.com/h4ckf0r0day/obscura/releases/download/v${OBSCURA_VERSION}/${asset}"
tmp="$(mktemp -d)"
info "Downloading Obscura ${OBSCURA_VERSION} ($asset)…"
if fetch "$url" "$tmp/obscura.tgz" 2>/dev/null && tar -xzf "$tmp/obscura.tgz" -C "$tmp" 2>/dev/null && [ -f "$tmp/obscura" ]; then
mkdir -p "$dst"
cp "$tmp/obscura" "$dst/obscura"
[ -f "$tmp/obscura-worker" ] && cp "$tmp/obscura-worker" "$dst/obscura-worker"
chmod +x "$dst/obscura" "$dst/obscura-worker" 2>/dev/null || true
echo "$OBSCURA_VERSION" > "$dst/VERSION"
rm -rf "$tmp"
[ -x "$dst/obscura" ] && return 0
fi
rm -rf "$tmp"; return 1
}

ensure_obscura() {
[ "${TB_NO_OBSCURA_INSTALL:-0}" = "1" ] && return 0
obdest="$APP_DIR/obscura-bin"
_ldir="$(find "$APP_DIR" -maxdepth 3 -type f -name tronbrowser 2>/dev/null | head -n1)"
[ -n "$_ldir" ] && obdest="$(dirname "$_ldir")/obscura-bin"
if [ -x "$obdest/obscura" ] && [ "$(cat "$obdest/VERSION" 2>/dev/null)" = "$OBSCURA_VERSION" ]; then return 0; fi
info "Setting up Obscura (the scraping engine for 'tron automate')…"
if download_obscura "$obdest"; then
info "Installed Obscura $OBSCURA_VERSION to $obdest"
return 0
fi
warn "Couldn't install Obscura; 'tron automate' will use Chromium for every page. Retry with: curl -fsSL $INSTALL_URL | sh -s -- ensure-obscura"
return 1
}

# Make a `tor` daemon available for the in-browser 🧅 Tor toggle. We install OUR
# OWN standalone tor (the Tor Expert Bundle) and run it on our OWN port (9071),
# never touching any system tor. We prefer the bundle because the system `tor`
Expand Down Expand Up @@ -770,6 +849,7 @@
# abort an install that has already put the browser on disk.
ensure_tor || true # so the in-browser 🧅 Tor toggle works out of the box
ensure_certutil || true # so Moshpit names load over HTTPS on first launch
ensure_obscura || true # so 'tron automate' has its scraping engine
brand_macos_icon "$(dirname "$bin")/tronbrowser.png"

info "Installed TronBrowser $tag to $APP_DIR"
Expand Down Expand Up @@ -877,6 +957,7 @@
ensure_browser # still make sure Ungoogled Chromium is installed
ensure_tor || true # and that Tor is available for the toggle
ensure_certutil || true # and that Moshpit trust can be written
ensure_obscura || true # and that the scraping engine is current
brand_macos_icon "$(find "$APP_DIR" -maxdepth 3 -name tronbrowser.png 2>/dev/null | head -n1)" # re-apply icon (Chromium updates reset it)
info "Re-install anyway with: TB_FORCE=1 tron upgrade"
return
Expand Down Expand Up @@ -920,6 +1001,8 @@
TRONBROWSER_REPO GitHub repo (default: $REPO)
TRONBROWSER_CACHE_LIMIT_MB clear profile caches on upgrade once they exceed
this (default: 1024; 0 disables)
TRONBROWSER_OBSCURA_VERSION Obscura release to install for 'tron automate'
(default: 0.2.2; TB_NO_OBSCURA_INSTALL=1 skips it)
EOF
}

Expand All @@ -936,6 +1019,7 @@
esac ;;
remove|uninstall) do_remove ;;
ensure-tor) ensure_tor ;;
ensure-obscura) ensure_obscura ;;
ensure-certutil) ensure_certutil ;;
version|--version|-v) do_version ;;
help|--help|-h) usage ;;
Expand Down
Loading
Loading