dsh-browser-install

Automatically installs Playwright Chromium (npx playwright install chromium --with-deps) into the Docker container running DeepSeek Harness, and bridges the installed binary onto PATH so the dsh-ego-browser side-bar plugin finds it — no manual "find the binary / symlink it / set EGO_LINUX_* env vars / wrap it with --no-sandbox" steps for agents ever again.

Why this plugin exists

dsh-ego-browser discovers its browser the following way (see lib/index.js → resolveEgoEnv and runtime/ego-linux/src/chrome.mjs):

  1. It scans PATH + fixed locations for google-chrome, google-chrome-stable, chromium, chromium-browser, … and
  2. on root hosts (docker default), it hands the found binary to a bundled --no-sandbox wrapper that only looks at fixed paths (/usr/bin/*, /opt/google/..., /usr/local/bin/google-chrome-stable, /snap/bin/chromium).

Playwright installs Chromium into ~/.cache/ms-playwright/chromium-<rev>/chrome-linux{,64}/chrome — a location invisible to both scans. That is why agents kept having to hunt the binary down, symlink/wrap it, and set EGO_LINUX_CHROME / EGO_LINUX_EXTRA_ARGS by hand.

This plugin does exactly that bridging once, automatically:

  • on load it checks the Playwright cache (idempotent, no-op when the browser is already there),
  • if the browser is missing it runs npx --yes playwright install chromium --with-deps in the background (profile startup is never blocked for minutes), and
  • it then symlinks the installed binary at /usr/local/bin/google-chrome-stable and /usr/local/bin/chromium — the exact names ego-browser's PATH scan and its bundled --no-sandbox wrapper look for. On this root container, the chain then resolves to: root detected → bundled --no-sandbox wrapper → --no-sandbox launch; no DISPLAY → automatic headless. No env vars, no manual wrapper, no agent steps.

Nothing is ever clobbered: a pre-existing system Chrome or a user wrapper keeps winning (the plugin only creates links where nothing of its own/an external binary exists).

This repo lives on git.notexpectedyet.com, so the install uses the same git+https://…#semver: convention as the other plugins in the profile (dsh-facet, dsh-recall, dsh-slopo). On GitHub the equivalent shorthand would be github:<you>/dsh-browser-install#semver:^0.1.0:

dsh plugin --profile web add "git+https://git.notexpectedyet.com/Not-Expected-Yet/dsh-browser-install.git#semver:^0.1.0"
  • The #semver:^0.1.0 ref is what makes updates work: it resolves against the repo's version tags (each release is a v<version> tag), and pnpm update moves the dependency within the range. This mirrors exactly how dsh-facet/dsh-recall/dsh-slopo are kept current in this profile. To publish a new version: bump version in package.json, commit, tag it, push — then run

    dsh plugin --profile web update                        # all deps
    dsh plugin --profile web update dsh-browser-install    # just this one
    
  • Bundle changes only become active after a profile restart: dsh --profile web.

  • No build step: the bundle ships plain ESM and declares no prepare script, so pnpm's default block on git-dependency build scripts never applies — nothing to allowBuilds in pnpm-workspace.yaml.

  • pnpm resolves git deps over HTTPS, so the repo must be public on the forge (or the DSH host needs git credentials). A private/new repo makes git ls-remote fail with "could not read Username ... terminal prompts disabled", which is what a bare #master install hit here.

Alternative: install from a local checkout

dsh plugin --profile web add /root/workspaces/Plugin\ Development/dsh-browser-install

A directory install is a symlinked file: dependency — handy while iterating. For persistent installs and updates, prefer the git flow above.

Restart and verify

Restart the profile, then check the plugin loaded and did its job:

dsh --profile web
dsh --profile web --dump-config | grep -A3 browser-install
ls -la /usr/local/bin/google-chrome-stable        # -> .../ms-playwright/chromium-*/chrome-linux64/chrome

Then from any agent session, ego_doctor should report a browser binary, and ego_space_open/ego_navigate work with no environment prep.

Non-root hosts: set symlinkDir to a directory on your PATH (e.g. ~/.local/bin). On root only /usr/local/bin is needed — it also satisfies the bundled --no-sandbox wrapper's absolute-path list.

Config

All fields are optional. Override them in your profile's cordis.patch.yml (or in the bundle row's config):

- id: browser-install
  name: "dsh-browser-install"
  config:
    withDeps: true          # run `--with-deps` (apt system deps); needs root/apt
    target: chromium        # chromium | chromium-headless-shell | chrome
    package: playwright     # npx package spec, e.g. "playwright@1.54.1"
    forceInstall: false     # always re-run the install on load
    installOnStartup: true  # install in background when the browser is missing
    cacheDir: ""            # empty = $PLAYWRIGHT_BROWSERS_PATH | ~/.cache/ms-playwright
    symlinkDir: /usr/local/bin
    symlinkNames: [google-chrome-stable, chromium]
    installTimeoutMs: 600000
Field Default Meaning
installOnStartup true Run the Playwright install when the plugin loads and no matching browser is found. The install runs in the background so profile startup is not blocked.
forceInstall false Always run the install on load, even when a browser is already present (e.g. after a Playwright upgrade).
package playwright Package spec passed to npx --yes <package> install .... Pin with e.g. playwright@1.54.1.
target chromium Which Playwright browser to ensure: chromium (full build), chromium-headless-shell, or chrome (Google Chrome). Default matches what ego-browser expects.
withDeps true Pass --with-deps so Playwright also installs the browser's system dependencies via apt (requires root; the DSH image runs as root).
cacheDir auto Playwright browsers cache root. Empty = $PLAYWRIGHT_BROWSERS_PATH, else ~/.cache/ms-playwright. When set here, the installer child also gets PLAYWRIGHT_BROWSERS_PATH.
symlinkDir /usr/local/bin Directory for the PATH links; must be on the DSH process PATH.
symlinkNames [google-chrome-stable, chromium] Link names to create. google-chrome-stable is the one ego-browser's PATH scan and its --no-sandbox wrapper both check.
installTimeoutMs 600000 Max time for the background install before it is killed.

Releases

master releases itself. Every push runs .gitea/workflows/release.yml (Gitea Actions), which reads the commit subjects since the last tag, bumps package.json, prepends CHANGELOG.md, tags vX.Y.Z, and publishes the Gitea release with those subjects as the body. The tag is what dsh plugin --profile web update moves to via the #semver:^0.1.0 install ref — no tags, no update, which is why this workflow exists.

Commit subjects are release input

Subject Bump Notes section
feat: … minor Added
feat!: …, fix(x)!: …, BREAKING, or [major] major Breaking
fix: …, perf: …, revert: … patch Fixed
docs: … patch Documented
plain sentence / anything else patch Other (or flat list)
[skip release] — nothing published

feat counts only as its own word, so Fix the feature flag leak is a patch. [minor] raises a patch subject to a minor without a feat. Only the subject line is read.

First release

With no tags yet, the released version is the one declared in package.json (currently v0.1.0) — the first tag matches what the code says, and every later release moves forward from it.

Manual run

workflow_dispatch is enabled for the first release (no tags yet) and for a release whose push failed after the tag was created; trigger it from the Actions tab.

Checking before pushing

npm run release:plan     # would this HEAD release? (yes/no)
npm run release:notes    # the exact notes it would publish
npm run test:release     # the checks behind the bump and the notes

Behaviour notes

  • No-clobber links: a link whose target lives inside the Playwright cache is treated as ours and refreshed to the newest installed revision; any other existing file/symlink is left untouched (and wins).
  • Lifecycle: the background installer child is killed if the plugin is unloaded mid-install (ctx.effect). The PATH links are intentionally not removed on unload — a running browser may be using them and they are harmless, idempotent shared state.
  • Error visibility: install failures, symlink errors, and a symlinkDir missing from PATH are all logged loudly under the dsh-browser-install logger scope, so an agent can read the reason instead of guessing.
  • ego-browser settings untouched: the plugin does not modify dsh-ego-browser's config or any EGO_LINUX_* env vars — it only fills the gap (install + visible symlink) that makes ego-browser's existing auto-adaptation work (root → --no-sandbox wrapper, no DISPLAY → headless).

Uninstall

dsh plugin --profile web remove dsh-browser-install

The symlinks are left in place (harmless; the browser keeps working for ego-browser). Remove them manually if you truly want them gone:

rm -f /usr/local/bin/google-chrome-stable /usr/local/bin/chromium

Development

Pure helpers (resolveCacheDir, resolveBrowserBinary, planSymlinks, applySymlinkPlan, buildInstallCmd) are exported for testing; the plugin itself needs no build step (plain ESM, no TypeScript compile).

  • Node ≥ 22 (the harness image ships v24).
  • Requires DSH ≥ 0.1.2-rc.1 (same baseline as dsh-ego-browser).
  • Runtime dependency: @deepseek-ai/schemastery (already present in the profile).
S
Description
No description provided
Readme
65 KiB
0 Stars 2 Watchers 0 Forks
v0.1.0
Latest
2026-10-01 09:00:58 +01:00
Languages
Shell 57.8%
JavaScript 42.2%