From 15aa2027c49742625ddaac67ed4aee992a337326 Mon Sep 17 00:00:00 2001 From: leetcrypt Date: Mon, 8 Jun 2026 11:38:43 -0700 Subject: [PATCH] refactor(sandbox): remove container browser-GUI (noVNC), keep vbox VM GUI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The docker/podman headless-container desktop (XFCE + TigerVNC + websockify + noVNC published on host 127.0.0.1:6080) is removed. It was unreliable: the desktop stack installed asynchronously during provisioning with every step wrapped in `|| true` (silent failures), while the host port mapping existed the moment the container ran — so opening the browser before websockify bound 6080 inside the container reset the connection (ERR_CONNECTION_RESET), with no readiness signal and no loud failure. Containers also can't be rendered as a real desktop by VirtualBox (no framebuffer/display), so the only desktop path that stays is the native VirtualBox VM GUI. Removed: - sbx.rs: GUI_PORT const; PortHolder/port_holder()/kill_port_holder() (the port-consent gate existed only for the noVNC publish); the `gui` param + `-p 127.0.0.1:6080:6080` block in prepare(); HH_SBX_GUI env in dk_bootstrap(); the `gui` param on provision(). - app.rs: PendingGuiLaunch + PortPrompt structs; the port_prompt App field; the `gui` field on PendingSudoLaunch; the port-consent modal + the sudo-submit port check; want_gui/gui parsing + the container-GUI launch message; `gui` threaded through spawn_launch and all call sites. - sandbox-bootstrap.sh: the entire HH_SBX_GUI=1 desktop block. - ui.rs / usage strings: dropped "[gui]"/"noVNC desktop" from the docker/podman help and the /sbx usage line. Kept (unchanged): the VirtualBox VM GUI — gui_launch() (VBoxManage startvm --type gui), launch_vbox_gui, `/sbx vbox gui ` and the `/sbx gui` alias. The `gui` keyword is still stripped from positionals so `/sbx vbox gui ` parses. cargo check passes clean (no warnings). Co-Authored-By: Claude Opus 4.6 --- hh/scripts/sandbox-bootstrap.sh | 57 ----------- hh/src/app.rs | 171 ++------------------------------ hh/src/sbx.rs | 116 +--------------------- hh/src/ui.rs | 8 +- 4 files changed, 19 insertions(+), 333 deletions(-) diff --git a/hh/scripts/sandbox-bootstrap.sh b/hh/scripts/sandbox-bootstrap.sh index ee654aa..9527e09 100644 --- a/hh/scripts/sandbox-bootstrap.sh +++ b/hh/scripts/sandbox-bootstrap.sh @@ -50,63 +50,6 @@ if ! apt-get install -y --no-install-recommends $PKGS; then done fi -# ---- Optional GUI desktop over noVNC ----------------------------------------- -# Opt-in via HH_SBX_GUI=1 (set by the launcher for `/sbx gui`). -# Containers are headless, so a GUI means: install a lightweight XFCE desktop + -# a VNC server, and bridge it to a browser-reachable noVNC websocket on port -# $HH_SBX_GUI_PORT (default 6080). The launcher publishes that port on the host -# loopback (127.0.0.1) only, so the desktop is reachable from this machine's -# browser but never the LAN. Heavy (pulls a desktop), hence strictly opt-in. -if [[ "${HH_SBX_GUI:-0}" == "1" ]]; then - GUI_PORT="${HH_SBX_GUI_PORT:-6080}" - GUI_PASS="${HH_SBX_GUI_PASS:-hackhouse}" - # XFCE (minimal) + a terminal, the X session glue, a VNC server, and the - # noVNC web client + websockify bridge. All apt — works on Ubuntu/Debian/Kali. - # tigervnc-tools carries `vncpasswd`, which --no-install-recommends would - # otherwise drop (it ships only as a recommend of the standalone server) — - # without it the passwd write below is a silent no-op and VNC never auths. - apt-get install -y --no-install-recommends \ - xfce4 xfce4-terminal dbus-x11 xfonts-base x11-xserver-utils \ - tigervnc-standalone-server tigervnc-tools novnc websockify || true - - export USER=root HOME=/root - # TigerVNC 1.15 (Kali/Debian trixie) reads its config from ~/.config/tigervnc - # and aborts with a fatal "Could not migrate /root/.vnc" if the legacy ~/.vnc - # exists, so write straight to the new path. /tmp/.X11-unix must exist + be - # sticky for the X socket; a fresh container may not have it. - VNCDIR=/root/.config/tigervnc - mkdir -p "$VNCDIR" /tmp/.X11-unix - chmod 1777 /tmp/.X11-unix 2>/dev/null || true - # Non-interactive VNC password (obfuscated, not strong auth — the real gate is - # the loopback-only publish + the room password). vncpasswd -f reads stdin. - printf '%s\n' "$GUI_PASS" | vncpasswd -f > "$VNCDIR/passwd" 2>/dev/null || true - chmod 600 "$VNCDIR/passwd" 2>/dev/null || true - cat > "$VNCDIR/xstartup" <<'XS' -#!/bin/sh -unset SESSION_MANAGER DBUS_SESSION_BUS_ADDRESS -exec dbus-launch startxfce4 -XS - chmod +x "$VNCDIR/xstartup" - - # Start the VNC server on display :1 (TCP 5901). -localhost no lets websockify - # (running in the same netns) reach it; nothing outside the container can — - # only $GUI_PORT is published, and only to host loopback. - if command -v vncserver >/dev/null 2>&1; then - vncserver -kill :1 >/dev/null 2>&1 || true - rm -f /tmp/.X1-lock /tmp/.X11-unix/X1 2>/dev/null || true - vncserver :1 -geometry 1280x800 -depth 24 -localhost no >/dev/null 2>&1 || true - fi - # noVNC web client → websocket → local VNC. Daemonize (nohup, detached stdio) - # so it outlives this `exec bash -s` provision session and keeps serving for - # the life of the container (PID 1 is `sleep infinity`). - if command -v websockify >/dev/null 2>&1; then - NOVNC_WEB=/usr/share/novnc - [[ -d "$NOVNC_WEB" ]] || NOVNC_WEB=/usr/share/webapps/novnc - nohup websockify --web="$NOVNC_WEB" "$GUI_PORT" localhost:5901 \ - >/var/log/hh-novnc.log 2>&1 & - fi -fi - # ---- Goose harness (the /ai agent's sandbox `!task` path) -------------------- # Goose ships as a release binary (not apt), so install it via its official # installer into a system path so every container user can run it. Best-effort: diff --git a/hh/src/app.rs b/hh/src/app.rs index 0a3742b..dbb0930 100644 --- a/hh/src/app.rs +++ b/hh/src/app.rs @@ -185,7 +185,6 @@ pub struct PendingSudoLaunch { pub cols: u16, pub start_daemon: bool, pub install_first: bool, - pub gui: bool, } /// Launch parameters captured when installing VirtualBox needs root but sudo @@ -218,31 +217,6 @@ pub struct SudoPrompt { pub pending: PendingPrivileged, } -/// Launch parameters captured when a GUI sandbox's noVNC port is already bound -/// by another process. Held aside while the port-consent modal asks whether to -/// kill the holder; the launch fires once the owner confirms. Carries the -/// (already-collected) sudo password, if any, so the gate works on both the -/// no-sudo and sudo-submit launch paths. -pub struct PendingGuiLaunch { - pub backend: sbx::Backend, - pub image: String, - pub members: Vec, - pub rows: u16, - pub cols: u16, - pub start_daemon: bool, - pub install_first: bool, - pub gui: bool, - pub password: Option, -} - -/// Active "port in use — kill it?" consent prompt for a GUI sandbox launch. -/// While `Some`, y/n keystrokes resolve this instead of feeding chat. -pub struct PortPrompt { - pub pid: i32, - pub holder: String, - pub pending: PendingGuiLaunch, -} - pub struct App { pub me: String, pub lines: Vec, @@ -306,10 +280,6 @@ pub struct App { /// and creds aren't cached). While `Some`, keystrokes feed this masked /// buffer instead of chat — the secret never leaves the client. pub sudo_prompt: Option, - /// Active "noVNC port already in use — kill the holder?" consent prompt for a - /// GUI sandbox launch (None unless the port is busy). While `Some`, y/n keys - /// resolve it instead of feeding chat. - pub port_prompt: Option, } impl App { @@ -346,7 +316,6 @@ impl App { layout: Layout::default(), focused_pane: None, sudo_prompt: None, - port_prompt: None, } } @@ -462,7 +431,7 @@ impl App { self.connected = true; self.chat_scroll = 0; self.sys(format!("joined as {} ⛧", self.me)); - self.sys("/sbx [gui] · /drive (F2 releases) · /ai start · /ai · /send · /sendroom · /pw show password · /help full command list · PgUp/PgDn scroll chat · ctrl-q quit"); + self.sys("/sbx · /drive (F2 releases) · /ai start · /ai · /send · /sendroom · /pw show password · /help full command list · PgUp/PgDn scroll chat · ctrl-q quit"); } Net::Message(l) => self.push_line(l), Net::Roster { users, capacity } => { @@ -1079,33 +1048,6 @@ pub async fn run(params: net::ConnParams, mut session: Session, mut theme: Theme } else { match pending { PendingPrivileged::Launch(pending) => { - if let Some(h) = - if pending.gui { sbx::port_holder(sbx::GUI_PORT) } else { None } - { - // Root's now authenticated, but the noVNC port - // is taken — carry the password into the - // port-consent gate so the launch can still fire - // (with sudo) once the holder is cleared. - app.sys(format!( - "⚠ port {} (noVNC) in use by pid {} ({}) — kill it and proceed? [y/N] · Esc cancels", - sbx::GUI_PORT, h.pid, h.name - )); - app.port_prompt = Some(PortPrompt { - pid: h.pid, - holder: h.name, - pending: PendingGuiLaunch { - backend: pending.backend, - image: pending.image, - members: pending.members, - rows: pending.rows, - cols: pending.cols, - start_daemon: pending.start_daemon, - install_first: pending.install_first, - gui: pending.gui, - password: Some(password), - }, - }); - } else { launching = true; app.sys("🔒 authenticating + launching…"); spawn_launch( @@ -1117,14 +1059,12 @@ pub async fn run(params: net::ConnParams, mut session: Session, mut theme: Theme pending.cols, pending.start_daemon, pending.install_first, - pending.gui, Some(password), pty_tx.clone(), broker_tx.clone(), app_tx.clone(), ); } - } PendingPrivileged::VboxInstall(p) => { // Root authenticated → install VirtualBox // (sudo -S reads this password) then boot @@ -1163,50 +1103,6 @@ pub async fn run(params: net::ConnParams, mut session: Session, mut theme: Theme } _ => {} } - } else if app.port_prompt.is_some() { - // GUI-launch port-consent modal: the noVNC port is held - // by another process. y/Y kills the named pid then fires - // the held launch; n/N/Esc aborts. Swallows keys so the - // y/n never lands in chat. - match k.code { - KeyCode::Char('y') | KeyCode::Char('Y') => { - let PortPrompt { pid, holder, pending } = - app.port_prompt.take().unwrap(); - app.sys(format!( - "⛧ killing {holder} (pid {pid}) to free port {}…", - sbx::GUI_PORT - )); - if sbx::kill_port_holder(sbx::GUI_PORT, pid) { - launching = true; - app.sys("🖥 port freed — launching…"); - spawn_launch( - pending.backend, - pending.image, - app.me.clone(), - pending.members, - pending.rows, - pending.cols, - pending.start_daemon, - pending.install_first, - pending.gui, - pending.password, - pty_tx.clone(), - broker_tx.clone(), - app_tx.clone(), - ); - } else { - app.sys(format!( - "⚠ couldn't free port {} — launch aborted", - sbx::GUI_PORT - )); - } - } - KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => { - app.port_prompt = None; - app.sys("⛧ launch aborted — port left untouched"); - } - _ => {} - } } else if k.modifiers.contains(KeyModifiers::CONTROL) && matches!(k.code, KeyCode::Char('x')) && !app.driving @@ -2060,7 +1956,7 @@ fn handle_command( let mut p = rest.split_whitespace(); match p.next() { // Grammar: `/sbx