From 0011ad86ad4a9973afca8928c41245608eecd1e3 Mon Sep 17 00:00:00 2001 From: Chris Scott <99081550+chriswritescode-dev@users.noreply.github.com> Date: Mon, 7 Sep 2026 03:23:42 +0000 Subject: [PATCH 1/2] feat(sandbox): replace chromium with obscura in the sandbox image --- README.md | 2 +- container/Dockerfile | 33 ++++++++++++++++++++------------- docs/api/README.md | 2 +- docs/api/_media/sandbox.md | 8 ++++---- docs/sandbox.md | 8 ++++---- 5 files changed, 30 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 5fcc411e0..efa028d64 100644 --- a/README.md +++ b/README.md @@ -564,7 +564,7 @@ docker save oc-forge-sandbox:latest -o forge-sandbox.tar msb load --input forge-sandbox.tar --tag oc-forge-sandbox:latest ``` -The default image includes Node.js (NodeSource current channel), pnpm, Bun, Python 3 + uv, ripgrep, git, jq, and a full Docker Engine (docker-ce, CLI, containerd, Buildx, and Compose from Docker's official apt repo) that runs natively inside the microVM — `docker run`, `docker build`, and `docker compose` all work in-sandbox. The daemon is started on demand by `forge-dockerd-start` (msb boots its own `agentd` as PID 1 and ignores the image's entrypoint, so nothing runs dockerd at boot); `/var/lib/docker` is backed by a dedicated block device because overlayfs cannot run on a virtiofs mount. The built image is roughly 1.65 GB. The image also ships the current Playwright Chromium build as `chromium` (Google publishes no linux/arm64 Chrome build, so Chromium is the arm64 equivalent). +The default image includes Node.js (NodeSource current channel), pnpm, Bun, Python 3 + uv, ripgrep, git, jq, and a full Docker Engine (docker-ce, CLI, containerd, Buildx, and Compose from Docker's official apt repo) that runs natively inside the microVM — `docker run`, `docker build`, and `docker compose` all work in-sandbox. The daemon is started on demand by `forge-dockerd-start` (msb boots its own `agentd` as PID 1 and ignores the image's entrypoint, so nothing runs dockerd at boot); `/var/lib/docker` is backed by a dedicated block device because overlayfs cannot run on a virtiofs mount. The built image is roughly 1.65 GB. The image also ships the Obscura headless browser engine as `obscura` (Rust with embedded V8; `obscura serve` speaks the Chrome DevTools Protocol, so Puppeteer and Playwright connect to it like headless Chrome). The `container/Dockerfile` ships with the plugin package. If the image is missing when OpenCode starts, Forge shows a warning toast with a "Build sandbox template" command in the palette. You can also trigger the build from the command palette at any time by searching for `Build sandbox template`, which opens a confirmation dialog and runs the build/save/load sequence automatically. The dialog stays open for the duration and shows a live progress bar, the current Docker step, elapsed time, and streamed build output; on failure it keeps the last lines of Docker output so the cause is visible. A first build takes several minutes. Closing the dialog does not cancel the build — it finishes in the background and reports with a toast. diff --git a/container/Dockerfile b/container/Dockerfile index e467e1e2c..803480d17 100644 --- a/container/Dockerfile +++ b/container/Dockerfile @@ -78,15 +78,9 @@ ENV HOME=/opt/forge \ RUN mkdir -p /opt/forge/.cache /opt/forge/.local/share/pnpm/store /opt/forge/.npm \ && chmod -R 0777 /opt/forge -# fallow CLI — dead-code analysis tool — and the current Playwright Chromium -# browser, installed globally with the same pnpm setup used by the rest of the -# sandbox image. Binaries are linked into /usr/local/bin so they are on PATH for -# arbitrary container UIDs. -# -# Chromium comes from playwright-core because Google publishes no linux/arm64 -# Chrome build, so on arm64 hosts it is the closest current Chrome build -# available; `install --with-deps` pulls the system libraries Chromium needs at -# runtime. +# fallow CLI — dead-code analysis tool — installed globally with the same pnpm +# setup used by the rest of the sandbox image. Binaries are linked into +# /usr/local/bin so they are on PATH for arbitrary container UIDs. # # The trailing chmod is load-bearing: these global installs run as root and populate the # pnpm store (store/v10/{files,index,projects}) with root-owned 0755 dirs, AFTER the earlier @@ -97,12 +91,25 @@ RUN mkdir -p /opt/forge/.cache /opt/forge/.local/share/pnpm/store /opt/forge/.np # Re-asserting 0777 here, as the last build step that touches /opt/forge, keeps the store # writable by any exec UID. Any future build step that runs pnpm as root must do the same. RUN pnpm add -g fallow@latest --global-bin-dir /usr/local/bin \ - && pnpm add -g playwright-core@latest --global-bin-dir /usr/local/bin \ - && package_root="$(readlink -f "$(find "$(pnpm root -g)" -maxdepth 4 -path '*/node_modules/playwright-core' -print -quit)")" \ - && node "$package_root/cli.js" install --with-deps chromium \ - && ln -s "$(node --input-type=module -e "import { chromium } from 'file://$package_root/index.mjs'; console.log(chromium.executablePath())")" /usr/local/bin/chromium \ && chmod -R 0777 /opt/forge +# Obscura (https://github.com/h4ckf0r0day/obscura) — Rust headless browser +# engine with embedded V8 — replaces the former Playwright Chromium install. +# The release tarball ships static `obscura` and `obscura-worker` binaries that +# need no system libraries beyond the base image's glibc, so nothing is apt- +# installed here. The archive layout is flat, so `tar xz` must extract straight +# into /usr/local/bin; `obscura-worker` has to sit next to `obscura` for +# parallel scraping. The architecture mapping matches dpkg naming to the +# upstream release assets (amd64 -> x86_64, arm64 -> aarch64). +RUN obscura_arch="$(case "$(dpkg --print-architecture)" in \ + amd64) echo x86_64 ;; \ + arm64) echo aarch64 ;; \ + *) echo "unsupported architecture: $(dpkg --print-architecture)" >&2; exit 1 ;; \ + esac)" \ + && curl -fsSL "https://github.com/h4ckf0r0day/obscura/releases/latest/download/obscura-${obscura_arch}-linux.tar.gz" \ + | tar xz -C /usr/local/bin \ + && chmod 0755 /usr/local/bin/obscura /usr/local/bin/obscura-worker + # Docker Engine from Docker's official apt repository. The microVM boots a real # kernel, so the daemon runs natively — no privileged-container or nested- # virtualization tricks. `/var/lib/docker` is mounted as a dedicated msb block diff --git a/docs/api/README.md b/docs/api/README.md index 913b613bf..5a3c01ac8 100644 --- a/docs/api/README.md +++ b/docs/api/README.md @@ -566,7 +566,7 @@ docker save oc-forge-sandbox:latest -o forge-sandbox.tar msb load --input forge-sandbox.tar --tag oc-forge-sandbox:latest ``` -The default image includes Node.js (NodeSource current channel), pnpm, Bun, Python 3 + uv, ripgrep, git, jq, and a full Docker Engine (docker-ce, CLI, containerd, Buildx, and Compose from Docker's official apt repo) that runs natively inside the microVM — `docker run`, `docker build`, and `docker compose` all work in-sandbox. The daemon is started on demand by `forge-dockerd-start` (msb boots its own `agentd` as PID 1 and ignores the image's entrypoint, so nothing runs dockerd at boot); `/var/lib/docker` is backed by a dedicated block device because overlayfs cannot run on a virtiofs mount. The built image is roughly 1.65 GB. The image also ships the current Playwright Chromium build as `chromium` (Google publishes no linux/arm64 Chrome build, so Chromium is the arm64 equivalent). +The default image includes Node.js (NodeSource current channel), pnpm, Bun, Python 3 + uv, ripgrep, git, jq, and a full Docker Engine (docker-ce, CLI, containerd, Buildx, and Compose from Docker's official apt repo) that runs natively inside the microVM — `docker run`, `docker build`, and `docker compose` all work in-sandbox. The daemon is started on demand by `forge-dockerd-start` (msb boots its own `agentd` as PID 1 and ignores the image's entrypoint, so nothing runs dockerd at boot); `/var/lib/docker` is backed by a dedicated block device because overlayfs cannot run on a virtiofs mount. The built image is roughly 1.65 GB. The image also ships the Obscura headless browser engine as `obscura` (Rust with embedded V8; `obscura serve` speaks the Chrome DevTools Protocol, so Puppeteer and Playwright connect to it like headless Chrome). The `container/Dockerfile` ships with the plugin package. If the image is missing when OpenCode starts, Forge shows a warning toast with a "Build sandbox template" command in the palette. You can also trigger the build from the command palette at any time by searching for `Build sandbox template`, which opens a confirmation dialog and runs the build/save/load sequence automatically. The dialog stays open for the duration and shows a live progress bar, the current Docker step, elapsed time, and streamed build output; on failure it keeps the last lines of Docker output so the cause is visible. A first build takes several minutes. Closing the dialog does not cancel the build — it finishes in the background and reports with a toast. diff --git a/docs/api/_media/sandbox.md b/docs/api/_media/sandbox.md index 53b404cc4..7fcc98e2d 100644 --- a/docs/api/_media/sandbox.md +++ b/docs/api/_media/sandbox.md @@ -26,16 +26,16 @@ msb load --input forge-sandbox.tar --tag oc-forge-sandbox:latest `msb load` registers the archive under the tag Forge looks up (`sandbox.image`, default `oc-forge-sandbox:latest`); list loaded images with `msb images --format json`. -The default image includes Node.js 24, pnpm, Bun, Python 3 + uv, ripgrep, git, jq, Chromium, and Docker Engine (see [Nested Docker](#nested-docker)). +The default image includes Node.js 24, pnpm, Bun, Python 3 + uv, ripgrep, git, jq, Obscura, and Docker Engine (see [Nested Docker](#nested-docker)). The sandbox image grants the `agent` user passwordless sudo, so loops can install whatever software they need at runtime. Commands arrive via `msb exec` without `-u`, so they run as the image's `USER agent` (keeping host-mapped worktree files owned by the host user); system-wide installs use an explicit `sudo` prefix, for example `sudo apt-get install ruby`. -### Chromium +### Obscura -The image ships the current Playwright Chromium build as `chromium`. Google publishes no linux/arm64 Chrome build, so Chromium is the arm64 equivalent of a current Chrome. Launch it headless with the usual sandbox flags: +The image ships the Obscura headless browser engine as `obscura` — a Rust-based headless browser with embedded V8, built for web scraping and agent automation. `obscura serve` speaks the Chrome DevTools Protocol, so Puppeteer and Playwright connect to it like headless Chrome: ```bash -chromium --no-sandbox --disable-dev-shm-usage --headless +obscura serve --port 9222 ``` ## How It Works diff --git a/docs/sandbox.md b/docs/sandbox.md index 53b404cc4..7fcc98e2d 100644 --- a/docs/sandbox.md +++ b/docs/sandbox.md @@ -26,16 +26,16 @@ msb load --input forge-sandbox.tar --tag oc-forge-sandbox:latest `msb load` registers the archive under the tag Forge looks up (`sandbox.image`, default `oc-forge-sandbox:latest`); list loaded images with `msb images --format json`. -The default image includes Node.js 24, pnpm, Bun, Python 3 + uv, ripgrep, git, jq, Chromium, and Docker Engine (see [Nested Docker](#nested-docker)). +The default image includes Node.js 24, pnpm, Bun, Python 3 + uv, ripgrep, git, jq, Obscura, and Docker Engine (see [Nested Docker](#nested-docker)). The sandbox image grants the `agent` user passwordless sudo, so loops can install whatever software they need at runtime. Commands arrive via `msb exec` without `-u`, so they run as the image's `USER agent` (keeping host-mapped worktree files owned by the host user); system-wide installs use an explicit `sudo` prefix, for example `sudo apt-get install ruby`. -### Chromium +### Obscura -The image ships the current Playwright Chromium build as `chromium`. Google publishes no linux/arm64 Chrome build, so Chromium is the arm64 equivalent of a current Chrome. Launch it headless with the usual sandbox flags: +The image ships the Obscura headless browser engine as `obscura` — a Rust-based headless browser with embedded V8, built for web scraping and agent automation. `obscura serve` speaks the Chrome DevTools Protocol, so Puppeteer and Playwright connect to it like headless Chrome: ```bash -chromium --no-sandbox --disable-dev-shm-usage --headless +obscura serve --port 9222 ``` ## How It Works From a258c31196635b5a387bbf3ca822c569cb2c77a9 Mon Sep 17 00:00:00 2001 From: Chris Scott <99081550+chriswritescode-dev@users.noreply.github.com> Date: Tue, 8 Sep 2026 09:26:49 -0400 Subject: [PATCH 2/2] feat(sandbox): advertise obscura headless browser in the sandbox context note --- src/sandbox/context.ts | 1 + test/sandbox/context.test.ts | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/src/sandbox/context.ts b/src/sandbox/context.ts index fb112fdae..c9f77cfe2 100644 --- a/src/sandbox/context.ts +++ b/src/sandbox/context.ts @@ -22,6 +22,7 @@ export const SANDBOX_CONTEXT_NOTE = [ 'Environment-specific tooling that is missing or incompatible is not acceptable: install or reinstall the required tooling and dependencies in the container, rerun the intended checks, and do not misreport environment-induced failures as code defects.', 'Passwordless sudo is available for installing missing tools system-wide.', 'Docker is available inside the sandbox: run forge-dockerd-start to ensure the daemon is running (idempotent, safe to run any time).', + 'Obscura is available inside the sandbox as a headless browser: obscura fetch --dump text|markdown|html|links returns a fully rendered page, and obscura serve exposes a Chrome DevTools Protocol endpoint on 127.0.0.1:9222 that Puppeteer and Playwright can attach to, implementing a subset of CDP rather than all of it. Loopback and private addresses are refused unless --allow-private-network is passed, so reaching a local dev server requires that flag.', ].join('\n') export const SANDBOX_OFF_NOTE = [ diff --git a/test/sandbox/context.test.ts b/test/sandbox/context.test.ts index 903e529d9..ab682ff4a 100644 --- a/test/sandbox/context.test.ts +++ b/test/sandbox/context.test.ts @@ -36,6 +36,18 @@ describe('SANDBOX_CONTEXT_NOTE', () => { expect(SANDBOX_CONTEXT_NOTE).toMatch(/rerun the intended checks/i) expect(SANDBOX_CONTEXT_NOTE).toMatch(/not misreport/i) }) + + it('advertises the in-image tooling an agent cannot discover on its own', () => { + expect(SANDBOX_CONTEXT_NOTE).toContain('forge-dockerd-start') + expect(SANDBOX_CONTEXT_NOTE).toContain('obscura fetch') + expect(SANDBOX_CONTEXT_NOTE).toContain('obscura serve') + // Private-address blocking is on by default, so an agent scraping its own dev + // server silently fails without this flag. + expect(SANDBOX_CONTEXT_NOTE).toContain('--allow-private-network') + // `obscura serve` implements only part of CDP, so the note must not promise + // drop-in headless Chrome (page.setContent, for one, is unimplemented). + expect(SANDBOX_CONTEXT_NOTE).toMatch(/subset of CDP/i) + }) }) describe('SANDBOX_OFF_NOTE', () => {