mirror of
https://github.com/hanzoai/bot.git
synced 2026-10-30 17:24:26 +00:00
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.
159 lines
5.6 KiB
Bash
Executable File
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
|