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):
- It scans
PATH+ fixed locations forgoogle-chrome,google-chrome-stable,chromium,chromium-browser, … and - on root hosts (docker default), it hands the found binary to a bundled
--no-sandboxwrapper 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-depsin the background (profile startup is never blocked for minutes), and - it then symlinks the installed binary at
/usr/local/bin/google-chrome-stableand/usr/local/bin/chromium— the exact names ego-browser's PATH scan and its bundled--no-sandboxwrapper look for. On this root container, the chain then resolves to: root detected → bundled--no-sandboxwrapper →--no-sandboxlaunch; noDISPLAY→ 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).
Install (from git — recommended)
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.0ref is what makes updates work: it resolves against the repo's version tags (each release is av<version>tag), andpnpm updatemoves the dependency within the range. This mirrors exactly howdsh-facet/dsh-recall/dsh-slopoare kept current in this profile. To publish a new version: bumpversioninpackage.json, commit, tag it, push — then rundsh 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
preparescript, so pnpm's default block on git-dependency build scripts never applies — nothing toallowBuildsinpnpm-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-remotefail with "could not read Username ... terminal prompts disabled", which is what a bare#masterinstall 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
symlinkDirto a directory on yourPATH(e.g.~/.local/bin). On root only/usr/local/binis needed — it also satisfies the bundled--no-sandboxwrapper'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
symlinkDirmissing fromPATHare all logged loudly under thedsh-browser-installlogger 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-sandboxwrapper, 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).