Skip to main content

devcontainer-dev

Spin up and interact with ToolHive Studio's containerized dev environment (Xvfb + noVNC + DinD). Use when running, testing, or debugging the app in isolation — locally, in a git worktree, or in GitHub Codespaces; when touching `.devcontainer/*`, `scripts/devcontainer-*.sh`, or the `devContainer:dev` npm script; or when debugging "blank white window", "Docker daemon failed to start", or "Missing X server" errors in the devcontainer. The container is fully isolated: no host pnpm install, no host Docker socket, no host X11/GPU — experiment freely without contaminating the host.

설치로 이동

소스 정보

저장소
stacklok/toolhive-studio
최근 소스 활동
2026년 6월 18일 19:41
감지된 SKILL.md 언어
영어
스타
167
포크
24

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
devcontainer-dev
description
Spin up and interact with ToolHive Studio's containerized dev environment (Xvfb + noVNC + DinD). Use when running, testing, or debugging the app in isolation — locally, in a git worktree, or in GitHub Codespaces; when touching `.devcontainer/*`, `scripts/devcontainer-*.sh`, or the `devContainer:dev` npm script; or when debugging "blank white window", "Docker daemon failed to start", or "Missing X server" errors in the devcontainer. The container is fully isolated: no host pnpm install, no host Docker socket, no host X11/GPU — experiment freely without contaminating the host.
# Containerized Dev Environment An isolated, cross-platform test environment for ToolHive Studio. The whole Electron app — including its backend `thv` binary and the MCP-server containers it spawns — runs inside a single devcontainer. You interact with the UI via a noVNC browser tab. The entire stack (Node, Electron, display server, window manager, VNC server, Docker-in-Docker, DBus, keyring) lives in the container. **Nothing is installed on the host.** That's the whole point: every worktree can have its own container and its own experiments, with zero risk of contaminating the user's global installs. --- ## Entry point ``` pnpm devContainer:dev ``` runs `scripts/devcontainer-dev.sh` on the host. The script is "smart": - If executed on the host: runs `devcontainer up` to build/start the container, then `devcontainer exec` to run the entrypoint inside it. - If executed inside a container (detected via `/.dockerenv`): skips the build step and just runs the entrypoint directly. This path is used by GitHub Codespaces. --- ## The three scripts | Script | Runs on | Purpose | | ------------------------------------ | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `scripts/devcontainer-dev.sh` | host | Picks a host port, kills stale processes, starts readiness poller, opens browser when ready, `devcontainer exec`s the entrypoint. | | `scripts/devcontainer-entrypoint.sh` | in container | Cleans stale X/VNC state, starts Xvfb, fluxbox, x11vnc, websockify/noVNC, dbus, gnome-keyring, then runs `pnpm start`. | | `scripts/devcontainer-post-start.sh` | in container | `postStartCommand` in `devcontainer.json`. In Codespaces (detected via `$CODESPACES`) it `nohup`-launches the entrypoint in the background so the noVNC preview pane opens without user action. | --- ## What the container runs - **Node 24** (matches `.nvmrc`) - **Electron + runtime deps** — GTK, NSS, X11, etc. (see `.devcontainer/Dockerfile`) - **Display stack** — Xvfb (virtual framebuffer at 1920×1200), fluxbox (window manager, auto-fullscreens windows with `WM_CLASS=ToolHive`), x11vnc (VNC server), noVNC + websockify (browser client on port 6080) - **Secret provider stack** — dbus + gnome-keyring. Required by ToolHive's secret API; without them the backend returns 500 on secrets endpoints. Mirrors the setup in `.github/workflows/_e2e.yml`. - **Docker-in-Docker** via the `docker-in-docker:2` devcontainer feature. Supplies `/var/run/docker.sock` inside the container so the bundled `thv` CLI can spawn MCP-server containers. --- ## Finding the URL (the logs are very long) `pnpm start` + electron-forge + Vite + Electron + HMR produce a **lot** of output. The terminal scrollback often exhausts. Three recovery mechanisms are built in: 1. **Written to a file** by the launcher: `~/.cache/toolhive-studio-url`. Survives any amount of output. ```bash cat ~/.cache/toolhive-studio-url ``` 2. **Set as the terminal tab title** via OSC escape. Visible in the tab bar of most terminals regardless of scrollback state. 3. **Prominent banners** in the output — a green initial block right after `devcontainer up`, plus an inverse-video `✓ ToolHive ready — <URL>` banner that fires only once the app is actually usable. If you're piping the output: ```bash pnpm devContainer:dev 2>&1 | tee /tmp/dev.log # later: grep -E 'ToolHive ready|vnc\.html' /tmp/dev.log ``` The readiness banner is what you care about. It gates on **three** signals simultaneously: - noVNC's HTTP endpoint answers (the browser tab will actually load) - The Electron binary is running (matched via `pgrep -f 'electron/dist/electron'`) - `thv serve` is running (matched via `pgrep -f 'thv serve'` — _not_ `pgrep -x thv`, because the short-lived version-check invocation also matches on bare name) Only once all three are true does the banner fire and the host's browser auto-open. --- ## Per-worktree isolation Each git worktree gets its own independent devcontainer: - **Container identity** — labelled with `devcontainer.local_folder=<absolute-worktree-path>`. The devcontainer CLI uses this to decide which container to reuse vs create fresh. - **Node modules** — volume named `toolhive-node-modules-<basename>`, scoped to the worktree's basename. No cross-worktree install pollution. - **Host port** — the primary clone uses `:6080`; additional worktrees try `:6080` first and fall back to a Docker-assigned random port if it's taken. So multiple worktrees can run simultaneously. The actual bound port is queried with `docker port "$CONTAINER_ID" 6080/tcp` and the URL is generated from that. - **DinD, display state, keyring, etc.** — all container-local. Tearing down a worktree's container removes all of it. **The host is never touched** — no host-side `pnpm install`, no host-side `/tmp/.X11-unix` mount, no host-side Docker socket passthrough, no host GPU. Everything the app needs is inside the container. Even the NVIDIA driver on the host is unreachable from inside — the container uses CPU software rendering. --- ## Interacting with a running container Find the container ID for a given worktree: ```bash WORKDIR="$(pwd)" # or a specific worktree path CONTAINER=$(docker ps --filter "label=devcontainer.local_folder=$WORKDIR" --format '{{.ID}}' | head -1) ``` Common operations: ```bash # Shell in docker exec -it "$CONTAINER" bash # Run as the devcontainer user (usually correct for pnpm / thv commands) docker exec -u node "$CONTAINER" pnpm test # One-off inspection docker exec "$CONTAINER" ps auxf # DinD health docker exec "$CONTAINER" docker info docker exec "$CONTAINER" docker ps ``` ### Log files (written by the entrypoint) | Path | Contents | | --------------------- | --------------------------------------------------------------------------- | | `/tmp/xvfb.log` | Xvfb startup and runtime errors | | `/tmp/fluxbox.log` | Window manager | | `/tmp/x11vnc.log` | VNC server — includes client connection events | | `/tmp/websockify.log` | noVNC WebSocket proxy | | `/tmp/keyring.log` | gnome-keyring-daemon unlock output | | `/tmp/entrypoint.log` | Output of the Codespaces auto-launch entrypoint (only exists in Codespaces) | ### Killing stale state If the app gets wedged, the entrypoint's first action on every run is to pkill all the usual suspects. To do it manually: ```bash docker exec "$CONTAINER" bash -c ' pkill -f "electron/dist/electron"; pkill -f "thv serve" pkill -f Xvfb; pkill -f fluxbox; pkill -f x11vnc; pkill -f websockify rm -f /tmp/.X99-lock /tmp/.X11-unix/X99 ' ``` --- ## Driving the app as an agent The container has enough tools for an AI agent to both **see** and **interact with** the running Electron window without going through the browser / noVNC. Useful for headless testing, reproducing user-reported UI bugs, or validating a feature end-to-end. All commands run via `docker exec` against the container with `DISPLAY=:99` set (matches Xvfb's display). ### See the screen (screenshots) ```bash # Take a PNG of the whole virtual framebuffer docker exec "$CONTAINER" bash -c 'DISPLAY=:99 import -window root /tmp/shot.png' # Copy it to the host for viewing / feeding to a vision model docker cp "$CONTAINER:/tmp/shot.png" /tmp/shot.png ``` `import` is from ImageMagick. For a specific window only, use `xwininfo` to get the WID then `import -window <WID>`. ### See the window tree (what's there, where, which is focused) ```bash docker exec "$CONTAINER" bash -c 'DISPLAY=:99 xwininfo -root -tree' docker exec "$CONTAINER" bash -c 'DISPLAY=:99 xdotool getactivewindow getwindowname' docker exec "$CONTAINER" bash -c 'DISPLAY=:99 xdotool search --name ToolHive' ``` The main app window has `WM_CLASS=ToolHive` (see gotcha below). Its geometry is usually `1920x1200+0+0` once fluxbox has auto-fullscreened it. ### Interact with the UI (mouse, keyboard) ```bash # Move the mouse and click docker exec "$CONTAINER" bash -c 'DISPLAY=:99 xdotool mousemove 400 300 click 1' # Type into whatever has focus docker exec "$CONTAINER" bash -c "DISPLAY=:99 xdotool type --delay 20 'hello world'" # Send a keystroke docker exec "$CONTAINER" bash -c 'DISPLAY=:99 xdotool key ctrl+shift+i' # DevTools # Focus the main window by name docker exec "$CONTAINER" bash -c 'DISPLAY=:99 xdotool search --name ToolHive windowactivate' ``` ### Typical agent loop Screenshot → feed to vision model → decide next action → `xdotool` → screenshot again. The usual caveats apply: pixel-coordinate automation is fragile, and CSS changes or modal dialogs can throw it off. For robust long-term automation, prefer a DOM-level approach (e.g. Chrome DevTools Protocol against Electron's remote debugging port — not currently wired in, see [Future: CDP access](#future-cdp-access)). ### Future: CDP access For DOM-level agent automation (querying, clicking specific elements by selector, scraping state), the cleanest path is Chromium's DevTools Protocol. Opt-in: pass `--remote-debugging-port=9223` to Electron in the entrypoint, add `-p 9223:9223` to the container's runArgs, and the agent connects via `ws://localhost:9223/devtools/page/<id>`. Not currently enabled by default; add it when you need it. --- ## Gotchas ### Docker daemon won't start (Linux, non-stock kernels) Symptom: `(*) Failed to start docker, retrying...` in a loop; `thv` has no runtime; `dockerd.log` contains: ``` failed to create NAT chain DOCKER: iptables failed: ... can't initialize iptables table `nat': Table does not exist (do you need to insmod?) ``` Seen on **zen, hardened, XanMod, and other non-stock Linux kernels** that don't autoload iptables modules. Fix (on the host): ```bash sudo modprobe iptable_nat iptable_filter ip_tables ``` Persist across reboots: ```bash echo -e "iptable_nat\niptable_filter\nip_tables" | sudo tee /etc/modules-load.d/docker.conf ``` A complete explanation is in a comment block in `.devcontainer/devcontainer.json` directly above the `docker-in-docker` feature declaration. Mac, Windows/WSL2, Codespaces, and stock Ubuntu/Debian/Fedora are unaffected. ### Electron renders a blank white page Root cause: Docker's default `/dev/shm` is 64 MB, too small for Chromium's compositor buffers; it fails silently and paints nothing. This is already fixed in the setup: - `runArgs` has `--shm-size=2g` to grow the shared memory - The entrypoint launches Electron with `--disable-dev-shm-usage` so Chromium falls back to `/tmp` anyway If you're modifying the startup flags, keep these. ### Fluxbox fullscreen rule doesn't match The Electron main window's `WM_CLASS` is `ToolHive`, **not** `Electron`. The `~/.fluxbox/apps` file the entrypoint generates targets `(class=ToolHive)` for that reason. The `Electron`-classed windows that show up in `xwininfo` are tiny 16×16 internal helper windows — ignore them. ### Readiness false-positive (transient processes) `pgrep -x thv` would match the short-lived `thv version` / `thv --version` invocation that Electron runs during startup to verify the binary is present. The long-running backend is always `thv serve --openapi ... --port=N`, so use `pgrep -f "thv serve"` to gate on that specifically. ### Readiness false-positive (pgrep self-match) When using `pgrep -f PATTERN` from a shell command (e.g. `bash -c 'pgrep -f "thv serve"'`), the pattern string appears verbatim in the **invoking shell's argv** — and `pgrep -f` matches against the full argv of every process. So pgrep matches its own parent shell and always returns true, _even when nothing is actually running the target process_. Use the standard bracket-class trick: `pgrep -f '[t]hv serve'`. The regex character class `[t]` matches `t` literally, so it still matches `thv serve`, but the literal string `[t]hv serve` in the invoking shell's argv doesn't match the regex. See `scripts/devcontainer-dev.sh` and `.devcontainer/greeting.sh` for the pattern in use. ### Secret provider returns 500 ToolHive's secret API needs DBus + gnome-keyring. The Dockerfile installs `dbus`, `dbus-x11`, `gnome-keyring`, `libsecret-1-0`, `libsecret-1-dev`, and the entrypoint starts `dbus-launch` and unlocks the keyring with `gnome-keyring-daemon --unlock --components=secrets,ssh,pkcs11`. The exact setup mirrors `.github/workflows/_e2e.yml` — if it breaks, cross-reference there. --- ## Rebuilding the container Most script changes don't require a rebuild (scripts are bind-mounted). A rebuild is only needed when you change: - `.devcontainer/Dockerfile` (apt packages) - `.devcontainer/devcontainer.json` → `runArgs`, `features`, `mounts` To force a rebuild: ```bash npx --yes @devcontainers/cli up --workspace-folder . --remove-existing-container ``` --- ## Codespaces specifics - `forwardPorts: [6080]` exposes noVNC via the Codespaces tunnel (HTTPS). - `portsAttributes."6080".onAutoForward: "openPreview"` makes the Simple Browser preview pane open automatically. - `postStartCommand` runs `scripts/devcontainer-post-start.sh`, which (when `$CODESPACES` is set) nohup-launches the entrypoint in the background. So opening a Codespace gets you a running app with no terminal commands. - The on-host port-picking logic (`NOVNC_HOST_PORT` env var, etc.) is a no-op here — `forwardPorts` is the only mechanism that matters for the user-facing URL. --- ## What this skill is NOT
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기