Files
hermes-agent/docker/sandbox-desktop.Dockerfile
Teknium 188a1a5efd
Some checks failed
Deploy Site / deploy-vercel (push) Has been cancelled
Deploy Site / deploy-docs (push) Has been cancelled
auto-fix lint issues & formatting / Generate eslint --fix patch (push) Has been cancelled
auto-fix lint issues & formatting / Apply patch (push) Has been cancelled
Skills Index Freshness Check / check-freshness (push) Has been cancelled
OSV-Scanner / Scan lockfiles (push) Has been cancelled
Publish nousresearch/hermes-sandbox:desktop, the terminal-backend sandbox image with a display stack (#121110)
* feat(docker): publish nousresearch/hermes-sandbox:desktop for terminal backends

The terminal backends (docker, modal, daytona, singularity) all default to
nikolaik/python-nodejs:python3.11-nodejs20, a bare Python+Node base. For Bot
Screen, computer_use and the browser to run INSIDE that sandbox instead of on
the gateway host, the sandbox image needs the display stack.

docker/sandbox-desktop.Dockerfile is that base plus:
  - the everyday tools it lacked (jq, ripgrep, fd, tmux, less, nano, vim,
    zip, rsync, tree, procps, htop, sudo for the base's uid-1000 `pn`)
  - the exact package set the Hermes -desktop image installs (TigerVNC,
    Xfce components, dbus, xauth, fonts)
  - Playwright's headed Chromium (same build as the -desktop image)
  - cua-driver 0.28.2 from its pinned release tarball

No Hermes inside; the default user stays root like the base so nothing
changes for people who just switch docker_image. Desktop processes run as
`pn`. 4.27 GB on amd64.

docker.yml gains a `sandbox` variant with its own cache scope and repository
(nousresearch/hermes-sandbox:desktop, :main-desktop, :<release>-desktop);
the docker-integration suite is skipped for it (no Hermes to test) and
docker/sandbox-desktop-smoke.sh runs instead: as `pn`, every launcher and
cua-driver binary resolves, the real launcher.sh publishes :20, the RFB
socket completes the 3.8 handshake relayed over `docker exec -i` stdio, and
a headed Chromium maps a window on that display. hadolint lints the new
Dockerfile in docker-lint.yml.

* ci(docker): build the sandbox image on release/dispatch, not every main push

Leaves docker.yml exactly as on main. The sandbox image carries no Hermes code,
so two 4 GB multi-arch builds per merge bought nothing. sandbox-image.yml builds
and smokes on a PR that edits its own Dockerfile/smoke, and publishes only on a
release or a manual dispatch with publish=true. Stable tag stays :desktop.

* feat(docker): sandbox desktop base on python3.13-nodejs26

Matches the Hermes image (Python 3.13 / Node 26) and the top of requires-python;
the default docker_image tag it inherited was Python 3.11 / Node 20. Same pn
uid 1000, Debian 13; smoke (launcher, RFB relay, headed Chromium) passes.

* feat(docker): sandbox desktop image bakes agent-browser and Pillow

The runtime PR wraps the browser tools' agent-browser invocation in the sandbox
and grabs the Screen pane thumbnail inside it (the X socket and its cookie live
there). Both need to be in the image the default tag publishes, or the runtime
finds a tag-compatible image without them. agent-browser is pinned to the range
the gateway resolves (tools/browser_tool.py AGENT_BROWSER_NPX_SPEC), scripts off,
so the first browser_navigate in a fresh sandbox never waits on npm. The smoke
checks both as pn.

* docker(sandbox-desktop): login sessions find the browser (PLAYWRIGHT_BROWSERS_PATH via /etc/environment)

Dockerfile ENV reaches docker exec only. As the target of the ssh backend
(sshd on top of this image) a login session gets its environment from PAM,
so agent-browser reported 'Chrome not found' over ssh while working under
docker exec. Write the browser location to /etc/environment as well; the
smoke test asserts a clean login environment resolves it.
2026-09-28 01:13:26 -07:00

95 lines
5.4 KiB
Docker

# Terminal-backend sandbox image WITH a desktop: the default sandbox base every
# docker/modal/daytona/singularity user already runs (nikolaik/python-nodejs), plus
# the tools that base was missing, plus the same display stack the -desktop Hermes
# image carries (TigerVNC + Xfce components + headed Chromium) and cua-driver, so
# Bot Screen, computer_use and the browser can live INSIDE the sandbox instead of
# on the gateway host. No Hermes runtime in here; the gateway shells in.
#
# docker build -f docker/sandbox-desktop.Dockerfile -t nousresearch/hermes-sandbox:desktop .
#
# Published as nousresearch/hermes-sandbox:desktop by .github/workflows/sandbox-image.yml
# on releases and manual dispatch only: it carries no Hermes code, so it does not track main.
# The tag lives in the ARG so CI and a local build read one place; hadolint cannot
# see through the substitution, hence the inline ignore.
ARG SANDBOX_BASE=nikolaik/python-nodejs:python3.13-nodejs26
# hadolint ignore=DL3006
FROM ${SANDBOX_BASE}
SHELL ["/bin/bash", "-euo", "pipefail", "-c"]
ARG CUA_DRIVER_VERSION=0.28.2
ARG TARGETARCH
ENV DEBIAN_FRONTEND=noninteractive \
PLAYWRIGHT_BROWSERS_PATH=/opt/playwright \
LANG=C.UTF-8
# Everyday tools the nikolaik base lacks (checked 2026-09: no jq, rg, fd, tmux,
# less, vim/nano, zip, rsync, tree, procps beyond ps). Kept to what agents reach
# for from a shell; language toolchains come from the base.
RUN apt-get -o Acquire::Retries=3 update && \
apt-get -o Acquire::Retries=3 install -y --no-install-recommends \
jq ripgrep fd-find tmux less nano vim-tiny zip rsync tree procps htop \
file bsdextrautils ca-certificates locales sudo && \
ln -sf /usr/bin/fdfind /usr/local/bin/fd && \
rm -rf /var/lib/apt/lists/*
# Display stack: identical package set to the Hermes -desktop image (Dockerfile,
# HERMES_BOT_DESKTOP=1) so tools/bot_desktop/launcher.sh finds the same binaries.
# Components are launched individually by the launcher, never xfce4-session.
RUN apt-get -o Acquire::Retries=3 update && \
apt-get -o Acquire::Retries=3 install -y --no-install-recommends \
tigervnc-standalone-server xfce4-panel xfwm4 xfdesktop4 xfce4-settings xfce4-terminal \
dbus-x11 x11-xserver-utils x11-utils x11-xkb-utils xauth fonts-dejavu-core \
at-spi2-core libgtk-3-0 libnss3 libasound2 libxss1 && \
rm -rf /var/lib/apt/lists/*
# Headed Chromium through Playwright (the same build the Hermes -desktop image
# uses), so agent-browser and the dock's Browser icon share one binary and one
# --user-data-dir. --with-deps pulls the Chromium runtime libraries. agent-browser
# itself is baked (the CLI the browser tools drive), pinned to the same range the
# gateway resolves (tools/browser_tool.py AGENT_BROWSER_NPX_SPEC), scripts off:
# the first browser_navigate in a fresh sandbox must not wait on an npm fetch.
RUN for i in 1 2 3; do \
npx --yes playwright@1 install --with-deps chromium && break || \
{ [ "$i" = 3 ] && exit 1; echo "playwright chromium install failed (attempt $i); retrying in 10s"; sleep 10; }; \
done && chmod -R a+rX /opt/playwright && \
npm install -g --ignore-scripts --no-audit --fetch-retries=5 "agent-browser@^0.26.0" && \
agent-browser --version
# cua-driver: computer_use's MCP driver. Pinned release tarball from the
# cua-driver-rs-v* tags (never releases/latest: prereleases publish 0 assets).
RUN set -eu; \
case "${TARGETARCH:-amd64}" in \
amd64) arch=x86_64 ;; \
arm64) arch=arm64 ;; \
*) echo "unsupported TARGETARCH ${TARGETARCH}"; exit 1 ;; \
esac; \
mkdir -p /opt/cua-driver; \
curl -fsSL "https://github.com/trycua/cua/releases/download/cua-driver-rs-v${CUA_DRIVER_VERSION}/cua-driver-rs-${CUA_DRIVER_VERSION}-linux-${arch}-binary.tar.gz" \
| tar -xz -C /opt/cua-driver; \
ln -sf /opt/cua-driver/cua-driver /usr/local/bin/cua-driver; \
/usr/local/bin/cua-driver --version
# Pillow: the Screen pane's thumbnail is grabbed INSIDE the sandbox (the X socket and its cookie live
# here, not on the gateway host). Also used by the base for ad-hoc image work.
RUN pip install --no-cache-dir "pillow>=10" && python3 -c "from PIL import ImageGrab"
# The base's default user stays root, exactly like nikolaik today, so existing
# docker_image users see no ownership or PATH change. Desktop processes (Xvnc,
# Xfce, Chromium, cua-driver) are exec'd as the base's unprivileged `pn` (uid
# 1000): Chromium refuses to run as root without --no-sandbox and cua-driver's
# AT-SPI bus wants a real user session. `pn` may sudo for ad-hoc installs.
RUN echo "pn ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/pn && chmod 0440 /etc/sudoers.d/pn && \
mkdir -p /tmp/.X11-unix && chmod 1777 /tmp/.X11-unix && \
mkdir -p /tmp/hermes-runtime && chown pn:pn /tmp/hermes-runtime && chmod 0700 /tmp/hermes-runtime
# Runtime dir for dbus/Xvnc; containers have no logind to create it. Both /tmp
# paths above are fixed by the X11 protocol / seeded per container, never shared.
ENV XDG_RUNTIME_DIR=/tmp/hermes-runtime
# Dockerfile ENV reaches `docker exec` only. When this image is the target of the
# ssh backend (sshd added on top), a login session gets its environment from PAM,
# so the browser location must also live where pam_env reads it or agent-browser
# reports "Chrome not found" over ssh while working under docker exec.
RUN printf 'PLAYWRIGHT_BROWSERS_PATH=/opt/playwright\nXDG_RUNTIME_DIR=/tmp/hermes-runtime\n' >> /etc/environment
CMD ["sleep", "infinity"]