Files
bot/scripts/sandbox-browser-entrypoint.sh
zeekay 77e782e9cd box: a desktop with no boxd is not a box, so refuse to start
The desktop class launched the agent as `command -v boxd && boxd &` — fail-open
in both directions. An image built without boxd started anyway, and a boxd that
crashed left the desktop's other children holding `wait -n` open. Either way the
pod stays Ready and 502s every fs/proc/git call, which is the exact failure this
image's own comment claims it fails closed against.

Exec and dev get that property for free: `CMD ["boxd"]` under tini means a dead
boxd is a dead container and the scheduler replaces it. The desktop class
replaces that CMD to bring up the display, so it has to state the invariant
itself rather than inherit it.

The guard existed because this script also runs from bot's local docker path,
where an older image might predate boxd. That is the case worth failing on, not
the case worth tolerating: an image with no boxd is not a box, and refusing at
startup surfaces it at build time instead of on a user's first tool call.
2026-08-06 12:10:48 -07:00

159 lines
5.6 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
dedupe_chrome_args() {
local -A seen_args=()
local -a unique_args=()
for arg in "${CHROME_ARGS[@]}"; do
if [[ -n "${seen_args["$arg"]:+x}" ]]; then
continue
fi
seen_args["$arg"]=1
unique_args+=("$arg")
done
CHROME_ARGS=("${unique_args[@]}")
}
export DISPLAY=:1
export HOME=/tmp/hanzo-bot-home
export XDG_CONFIG_HOME="${HOME}/.config"
export XDG_CACHE_HOME="${HOME}/.cache"
CDP_PORT="${BOT_BROWSER_CDP_PORT:-${CLAWDBOT_BROWSER_CDP_PORT:-9222}}"
CDP_SOURCE_RANGE="${BOT_BROWSER_CDP_SOURCE_RANGE:-${CLAWDBOT_BROWSER_CDP_SOURCE_RANGE:-}}"
VNC_PORT="${BOT_BROWSER_VNC_PORT:-${CLAWDBOT_BROWSER_VNC_PORT:-5900}}"
NOVNC_PORT="${BOT_BROWSER_NOVNC_PORT:-${CLAWDBOT_BROWSER_NOVNC_PORT:-6080}}"
ENABLE_NOVNC="${BOT_BROWSER_ENABLE_NOVNC:-${CLAWDBOT_BROWSER_ENABLE_NOVNC:-1}}"
HEADLESS="${BOT_BROWSER_HEADLESS:-${CLAWDBOT_BROWSER_HEADLESS:-0}}"
ALLOW_NO_SANDBOX="${BOT_BROWSER_NO_SANDBOX:-${CLAWDBOT_BROWSER_NO_SANDBOX:-0}}"
NOVNC_PASSWORD="${BOT_BROWSER_NOVNC_PASSWORD:-${CLAWDBOT_BROWSER_NOVNC_PASSWORD:-}}"
DISABLE_GRAPHICS_FLAGS="${BOT_BROWSER_DISABLE_GRAPHICS_FLAGS:-1}"
DISABLE_EXTENSIONS="${BOT_BROWSER_DISABLE_EXTENSIONS:-1}"
RENDERER_PROCESS_LIMIT="${BOT_BROWSER_RENDERER_PROCESS_LIMIT:-2}"
mkdir -p "${HOME}" "${HOME}/.chrome" "${XDG_CONFIG_HOME}" "${XDG_CACHE_HOME}"
Xvfb :1 -screen 0 1280x800x24 -ac -nolisten tcp &
if [[ "${HEADLESS}" == "1" ]]; then
CHROME_ARGS=(
"--headless=new"
)
else
CHROME_ARGS=()
fi
if [[ "${CDP_PORT}" -ge 65535 ]]; then
CHROME_CDP_PORT="$((CDP_PORT - 1))"
else
CHROME_CDP_PORT="$((CDP_PORT + 1))"
fi
CHROME_ARGS+=(
"--remote-debugging-address=127.0.0.1"
"--remote-debugging-port=${CHROME_CDP_PORT}"
"--user-data-dir=${HOME}/.chrome"
"--no-first-run"
"--no-default-browser-check"
"--disable-dev-shm-usage"
"--disable-background-networking"
"--disable-features=TranslateUI"
"--disable-breakpad"
"--disable-crash-reporter"
"--no-zygote"
"--metrics-recording-only"
)
DISABLE_GRAPHICS_FLAGS_LOWER="${DISABLE_GRAPHICS_FLAGS,,}"
if [[ "${DISABLE_GRAPHICS_FLAGS_LOWER}" == "1" || "${DISABLE_GRAPHICS_FLAGS_LOWER}" == "true" || "${DISABLE_GRAPHICS_FLAGS_LOWER}" == "yes" || "${DISABLE_GRAPHICS_FLAGS_LOWER}" == "on" ]]; then
CHROME_ARGS+=(
"--disable-3d-apis"
"--disable-gpu"
"--disable-software-rasterizer"
)
fi
DISABLE_EXTENSIONS_LOWER="${DISABLE_EXTENSIONS,,}"
if [[ "${DISABLE_EXTENSIONS_LOWER}" == "1" || "${DISABLE_EXTENSIONS_LOWER}" == "true" || "${DISABLE_EXTENSIONS_LOWER}" == "yes" || "${DISABLE_EXTENSIONS_LOWER}" == "on" ]]; then
CHROME_ARGS+=(
"--disable-extensions"
)
fi
if [[ "${RENDERER_PROCESS_LIMIT}" =~ ^[0-9]+$ && "${RENDERER_PROCESS_LIMIT}" -gt 0 ]]; then
CHROME_ARGS+=("--renderer-process-limit=${RENDERER_PROCESS_LIMIT}")
fi
if [[ "${ALLOW_NO_SANDBOX}" == "1" ]]; then
CHROME_ARGS+=(
"--no-sandbox"
"--disable-setuid-sandbox"
)
fi
dedupe_chrome_args
chromium "${CHROME_ARGS[@]}" about:blank &
for _ in $(seq 1 50); do
if curl -sS --max-time 1 "http://127.0.0.1:${CHROME_CDP_PORT}/json/version" >/dev/null; then
break
fi
sleep 0.1
done
SOCAT_LISTEN_ADDR="TCP-LISTEN:${CDP_PORT},fork,reuseaddr,bind=0.0.0.0"
if [[ -n "${CDP_SOURCE_RANGE}" ]]; then
SOCAT_LISTEN_ADDR="${SOCAT_LISTEN_ADDR},range=${CDP_SOURCE_RANGE}"
fi
socat "${SOCAT_LISTEN_ADDR}" "TCP:127.0.0.1:${CHROME_CDP_PORT}" &
if [[ "${ENABLE_NOVNC}" == "1" && "${HEADLESS}" != "1" ]]; then
# VNC auth passwords are max 8 chars; use a random default when not provided.
if [[ -z "${NOVNC_PASSWORD}" ]]; then
NOVNC_PASSWORD="$(< /proc/sys/kernel/random/uuid)"
NOVNC_PASSWORD="${NOVNC_PASSWORD//-/}"
NOVNC_PASSWORD="${NOVNC_PASSWORD:0:8}"
fi
NOVNC_PASSWD_FILE="${HOME}/.vnc/passwd"
mkdir -p "${HOME}/.vnc"
x11vnc -storepasswd "${NOVNC_PASSWORD}" "${NOVNC_PASSWD_FILE}" >/dev/null
chmod 600 "${NOVNC_PASSWD_FILE}"
x11vnc -display :1 -rfbport "${VNC_PORT}" -shared -forever -rfbauth "${NOVNC_PASSWD_FILE}" -localhost &
websockify --web /usr/share/novnc/ "${NOVNC_PORT}" "localhost:${VNC_PORT}" &
fi
# A desktop box is still a box: the display is what makes it computer-use, but
# the agent that serves /v1/box (files, exec, git) is the same boxd every other
# class runs. The exec stage sets `CMD ["boxd"]` and this entrypoint replaces
# that CMD, so without this line the desktop class would be the one image in the
# chain nothing can schedule.
#
# It is REQUIRED, not best-effort. The old line was
#
# command -v boxd >/dev/null 2>&1 && boxd &
#
# which is fail-open twice over: an image built without boxd starts anyway, and
# a boxd that dies leaves the desktop's other children holding `wait -n` open.
# Either way the pod stays Ready and 502s every fs/proc/git call, which is the
# failure the rest of this image is built to avoid — the exec stage's `CMD
# ["boxd"]` under tini means a dead boxd is a dead container and the scheduler
# replaces it. The desktop class replaces that CMD, so it has to say the same
# thing itself instead of inheriting it.
#
# "But it also runs from bot's local docker path" was the reason for the guard.
# An image with no boxd is not a box, locally or otherwise; refusing at startup
# is how that gets noticed at build time rather than on the first tool call.
if ! command -v boxd >/dev/null 2>&1; then
echo "boxd not found in PATH — this image is not a box; every /v1/box call would 502" >&2
exit 1
fi
boxd &
# `wait -n` returns when the FIRST child exits, and every child here is
# load-bearing: Xvfb, the VNC pair, boxd. Whichever dies takes the container
# with it, so the pod goes NotReady and gets replaced rather than lingering as a
# Ready pod that cannot answer.
wait -n