-
Notifications
You must be signed in to change notification settings - Fork 3
feat(sandbox): replace chromium with obscura in the sandbox image #99
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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" \ | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win 🔎 Supported by static analysis🤖 get_repo_knowledge executed:
Length of output: 4553 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- Dockerfile relevant lines ---'
cat -n container/Dockerfile | sed -n '1,150p'
printf '%s\n' '--- changed-file diff summary ---'
git diff --stat -- container/Dockerfile
printf '%s\n' '--- changed hunk ---'
git diff -- container/Dockerfile | sed -n '1,180p'
printf '%s\n' '--- Obscura references ---'
rg -n -C 3 'obscura|0\.2\.2|latest/download' . --glob '!container/Dockerfile'Repository: chriswritescode-dev/opencode-forge Length of output: 16202 🏁 Script executed: #!/bin/bash
set -eu
api='https://api.github.com/repos/h4ckf0r0day/obscura'
printf '%s\n' '--- v0.2.2 release metadata ---'
curl -fsSL "$api/releases/tags/v0.2.2" |
jq '{tag_name, draft, prerelease, assets: [.assets[] | {name, browser_download_url, digest}]}'
printf '%s\n' '--- latest release metadata ---'
curl -fsSL "$api/releases/latest" |
jq '{tag_name, published_at, assets: [.assets[] | {name, browser_download_url, digest}]}'Repository: chriswritescode-dev/opencode-forge Length of output: 12065 Pin the Obscura artifact to v0.2.2. The 🧰 Tools🪛 Checkov (3.3.11)[low] 1-196: Ensure that HEALTHCHECK instructions have been added to container images (CKV_DOCKER_2) 🤖 Prompt for AI Agents |
||
| | tar xz -C /usr/local/bin \ | ||
|
Comment on lines
+109
to
+110
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win 🔎 Supported by static analysis🤖 get_repo_knowledge executed:
Length of output: 27692 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- Dockerfile lines 1-130 ---'
sed -n '1,130p' container/Dockerfile
printf '%s\n' '--- Dockerfile shell directives and Obscura references ---'
rg -n '^(FROM|SHELL|USER)|obscura|pipefail|set -' container/Dockerfile
printf '%s\n' '--- relevant tracked files ---'
git ls-files 'container/*' | sortRepository: chriswritescode-dev/opencode-forge Length of output: 8149 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- default Docker shell pipeline status ---'
/bin/sh -c '{ tar -czf - --files-from=/dev/null; exit 42; } | tar -xzf - -C /tmp'
printf 'default_pipeline_status=%s\n' "$?"
printf '%s\n' '--- pipefail behavior when supported ---'
if /bin/sh -c 'set -o pipefail' 2>/dev/null; then
/bin/sh -c 'set -o pipefail; { tar -czf - --files-from=/dev/null; exit 42; } | tar -xzf - -C /tmp'
printf 'pipefail_pipeline_status=%s\n' "$?"
else
printf '%s\n' 'pipefail=unsupported'
fiRepository: chriswritescode-dev/opencode-forge Length of output: 286 Make archive download failures fail the image build. Docker runs this 🧰 Tools🪛 Checkov (3.3.11)[low] 1-196: Ensure that HEALTHCHECK instructions have been added to container images (CKV_DOCKER_2) 🤖 Prompt for AI AgentsSource: Linters/SAST tools |
||
| && 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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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)). | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🤖 get_repo_knowledge executed:
Length of output: 47763 🏁 Script executed: #!/bin/bash
set -eu
printf '%s\n' '--- relevant documentation ---'
cat -n docs/api/_media/sandbox.md | sed -n '20,35p'
cat -n docs/sandbox.md | sed -n '20,35p'
printf '%s\n' '--- sandbox Dockerfile candidates ---'
git ls-files | grep -Ei '(^|/)(dockerfile|.*sandbox.*)$' || true
printf '%s\n' '--- Node.js installation references ---'
rg -n -i --glob '!node_modules' --glob '!dist' 'nodesource|setup_(current|[0-9]+)\.x|node\.js|nodejs|node-version|FROM .*node' . | head -200Repository: chriswritescode-dev/opencode-forge Length of output: 8825 Align the sandbox documentation with
📍 Affects 2 files
🤖 Prompt for AI Agents |
||
|
|
||
| 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 | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -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 <url> 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.', | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Use copy-safe syntax for the fetch example. The text uses Proposed fix- 'Obscura is available inside the sandbox as a headless browser: obscura fetch --dump text|markdown|html|links <url> 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.',
+ 'Obscura is available inside the sandbox as a headless browser: use obscura fetch --dump FORMAT URL (FORMAT is one of text, markdown, html, or links) to return 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.',📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| ].join('\n') | ||||||
|
|
||||||
| export const SANDBOX_OFF_NOTE = [ | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -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) | ||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+42
to
+49
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win Assert the endpoint and fetch contract. The test checks only the command names and the generic CDP limitation. It still passes if the note removes Proposed assertions expect(SANDBOX_CONTEXT_NOTE).toContain('obscura fetch')
+ expect(SANDBOX_CONTEXT_NOTE).toContain('obscura fetch --dump')
expect(SANDBOX_CONTEXT_NOTE).toContain('obscura serve')
+ expect(SANDBOX_CONTEXT_NOTE).toContain('127.0.0.1:9222')
+ expect(SANDBOX_CONTEXT_NOTE).toContain('Puppeteer and Playwright')📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| describe('SANDBOX_OFF_NOTE', () => { | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Remeasure the image before publishing its size.
README.md#L567-L567: replace or qualify theroughly 1.65 GBmeasurement.docs/api/README.md#L569-L569: apply the same updated measurement.📍 Affects 2 files
README.md#L567-L567(this comment)docs/api/README.md#L569-L569🤖 Prompt for AI Agents