Skip to content

Commit 733be1d

Browse files
committed
fix(shell): install Rust browser module with stable cargo
Split Dockerfile fragments so vibecode-linter accepts the generated-template sources and install rust-browser-connection through rustup stable instead of the base image's Cargo 1.75. Invariant: generated project images either contain /usr/local/bin/docker-git-browser-connection built by current stable cargo or fail the Docker build before MCP Playwright can start without a browser.
1 parent 3dbf864 commit 733be1d

8 files changed

Lines changed: 462 additions & 440 deletions

File tree

packages/app/src/lib/core/templates-entrypoint/tasks.ts

Lines changed: 39 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -278,41 +278,42 @@ ${renderAgentLaunch(config)}
278278
// EFFECT: generated bash calls docker-git-browser-connection, which calls Docker.
279279
// INVARIANT: browser shares the project container network namespace, so CDP is http://127.0.0.1:9223 from agents.
280280
// COMPLEXITY: O(1) entrypoint orchestration; Docker build/run is delegated to Rust.
281-
export const renderEntrypointRustBrowserConnection = (): string => [
282-
'# Unified Rust browser connection (noVNC + CDP) for MCP Playwright + Hermes — per #347.',
283-
'# Defaults are safe no-ops unless MCP Playwright is enabled.',
284-
'docker_git_start_rust_browser_connection() {',
285-
' if [[ "${MCP_PLAYWRIGHT_ENABLE:-0}" != "1" ]]; then',
286-
' return 0',
287-
' fi',
288-
'',
289-
' local browser_bin=""',
290-
' local candidate',
291-
' for candidate in /root/.cargo/bin/docker-git-browser-connection /usr/local/cargo/bin/docker-git-browser-connection $(command -v docker-git-browser-connection 2>/dev/null || true); do',
292-
' if [[ -x "$candidate" ]]; then',
293-
' browser_bin="$candidate"',
294-
' break',
295-
' fi',
296-
' done',
297-
'',
298-
' if [[ -z "$browser_bin" ]]; then',
299-
' echo "[browser] WARNING: docker-git-browser-connection not found; Playwright MCP browser is unavailable" >&2',
300-
' MCP_PLAYWRIGHT_ENABLE=0',
301-
' export MCP_PLAYWRIGHT_ENABLE',
302-
' return 0',
303-
' fi',
304-
'',
305-
' local project_container="${DOCKER_GIT_PROJECT_CONTAINER_NAME:-$(hostname)}"',
306-
' local network_mode="container:${project_container}"',
307-
' mkdir -p /var/log',
308-
' "$browser_bin" start --project "$project_container" --network "$network_mode" >> /var/log/docker-git-browser.log 2>&1 || {',
309-
' echo "[browser] WARNING: Rust browser connection failed; see /var/log/docker-git-browser.log" >&2',
310-
' MCP_PLAYWRIGHT_ENABLE=0',
311-
' export MCP_PLAYWRIGHT_ENABLE',
312-
' return 0',
313-
' }',
314-
' echo "[browser] Rust browser connection is ready via $browser_bin on $network_mode"',
315-
'}',
316-
'',
317-
'docker_git_start_rust_browser_connection',
318-
].join("\n")
281+
export const renderEntrypointRustBrowserConnection = (): string =>
282+
[
283+
"# Unified Rust browser connection (noVNC + CDP) for MCP Playwright + Hermes — per #347.",
284+
"# Defaults are safe no-ops unless MCP Playwright is enabled.",
285+
"docker_git_start_rust_browser_connection() {",
286+
" if [[ \"${MCP_PLAYWRIGHT_ENABLE:-0}\" != \"1\" ]]; then",
287+
" return 0",
288+
" fi",
289+
"",
290+
" local browser_bin=\"\"",
291+
" local candidate",
292+
" for candidate in /root/.cargo/bin/docker-git-browser-connection /usr/local/cargo/bin/docker-git-browser-connection $(command -v docker-git-browser-connection 2>/dev/null || true); do",
293+
" if [[ -x \"$candidate\" ]]; then",
294+
" browser_bin=\"$candidate\"",
295+
" break",
296+
" fi",
297+
" done",
298+
"",
299+
" if [[ -z \"$browser_bin\" ]]; then",
300+
" echo \"[browser] WARNING: docker-git-browser-connection not found; Playwright MCP browser is unavailable\" >&2",
301+
" MCP_PLAYWRIGHT_ENABLE=0",
302+
" export MCP_PLAYWRIGHT_ENABLE",
303+
" return 0",
304+
" fi",
305+
"",
306+
" local project_container=\"${DOCKER_GIT_PROJECT_CONTAINER_NAME:-$(hostname)}\"",
307+
" local network_mode=\"container:${project_container}\"",
308+
" mkdir -p /var/log",
309+
" \"$browser_bin\" start --project \"$project_container\" --network \"$network_mode\" >> /var/log/docker-git-browser.log 2>&1 || {",
310+
" echo \"[browser] WARNING: Rust browser connection failed; see /var/log/docker-git-browser.log\" >&2",
311+
" MCP_PLAYWRIGHT_ENABLE=0",
312+
" export MCP_PLAYWRIGHT_ENABLE",
313+
" return 0",
314+
" }",
315+
" echo \"[browser] Rust browser connection is ready via $browser_bin on $network_mode\"",
316+
"}",
317+
"",
318+
"docker_git_start_rust_browser_connection"
319+
].join("\n")
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
// CHANGE: isolate the long Playwright MCP wrapper from the primary Dockerfile renderer.
2+
// WHY: generated shell must stay readable while TypeScript lint keeps functions and files small enough for review.
3+
// QUOTE(ТЗ): "без TS-дублирования"
4+
// REF: issue-347
5+
// SOURCE: n/a
6+
// FORMAT THEOREM: enableMcpPlaywright -> dockerfile_contains(docker-git-playwright-mcp)
7+
// PURITY: CORE
8+
// INVARIANT: wrapper content is rendered exactly once and still delegates CDP to the Rust-created browser endpoint.
9+
// COMPLEXITY: O(n) where n is wrapper length
10+
const dockerfilePlaywrightMcpBlock = String.raw`ARG PLAYWRIGHT_MCP_VERSION=0.0.75
11+
RUN npm install -g "@playwright/mcp@${"$"}{PLAYWRIGHT_MCP_VERSION}"
12+
13+
# docker-git: wrapper that launches the MCP stdio server without blocking initialize on CDP readiness.
14+
RUN cat <<'EOF' > /usr/local/bin/docker-git-playwright-mcp
15+
#!/usr/bin/env bash
16+
set -euo pipefail
17+
18+
# Fast-path for help/version (avoid waiting for the nested browser runtime).
19+
for arg in "$@"; do
20+
case "$arg" in
21+
-h|--help|-V|--version)
22+
exec playwright-mcp "$@"
23+
;;
24+
esac
25+
done
26+
27+
CDP_ENDPOINT="http://127.0.0.1:9223"
28+
29+
# CHANGE: keep MCP initialize independent from nested browser readiness
30+
# WHY: Codex starts MCP servers during boot; blocking here closes stdio before initialize when CDP is slow.
31+
# QUOTE(issue-319): "handshaking with MCP server failed: connection closed: initialize response"
32+
# REF: issue-319
33+
# SOURCE: https://playwright.dev/mcp/configuration/options
34+
# FORMAT THEOREM: guarded_cdp(fixed_nested_browser_endpoint) -> mcp_stdio_ready_before_browser_connection
35+
# PURITY: SHELL
36+
# INVARIANT: guarded mode never exits before handing stdio to playwright-mcp
37+
# COMPLEXITY: O(1)
38+
MCP_PLAYWRIGHT_RETRY_ATTEMPTS="\${MCP_PLAYWRIGHT_RETRY_ATTEMPTS:-10}"
39+
MCP_PLAYWRIGHT_RETRY_DELAY="\${MCP_PLAYWRIGHT_RETRY_DELAY:-2}"
40+
MCP_PLAYWRIGHT_CDP_GUARD="\${MCP_PLAYWRIGHT_CDP_GUARD:-1}"
41+
MCP_PLAYWRIGHT_CDP_TIMEOUT="\${MCP_PLAYWRIGHT_CDP_TIMEOUT:-60000}"
42+
43+
EXTRA_ARGS=()
44+
if [[ "\${MCP_PLAYWRIGHT_ISOLATED:-0}" == "1" ]]; then
45+
EXTRA_ARGS+=(--isolated)
46+
fi
47+
48+
# The guarded endpoint is the nested browser opened by docker-git Open browser.
49+
# Passing the fixed HTTP URL lets Playwright MCP
50+
# re-resolve /json/version instead of pinning itself to one stale /devtools/browser/<id>.
51+
if [[ "$MCP_PLAYWRIGHT_CDP_GUARD" == "1" ]]; then
52+
exec playwright-mcp --cdp-endpoint "$CDP_ENDPOINT" --cdp-timeout "$MCP_PLAYWRIGHT_CDP_TIMEOUT" "\${EXTRA_ARGS[@]}" "$@"
53+
fi
54+
55+
# Unified Rust browser (docker-git-browser-connection) now provides the single
56+
# dg-$PROJECT-browser container with CDP on :9223 (reachable by name when --network is passed).
57+
# MCP Playwright connects directly to ws://dg-...-browser:9223 — no more separate browser-vnc or cdp-guard duplication (per #347).
58+
fetch_cdp_version() {
59+
curl -sSf --connect-timeout 3 --max-time 10 -H 'Host: 127.0.0.1:9222' "\${CDP_ENDPOINT%/}/json/version" 2>/dev/null
60+
}
61+
62+
JSON=""
63+
for attempt in $(seq 1 "$MCP_PLAYWRIGHT_RETRY_ATTEMPTS"); do
64+
if JSON="$(fetch_cdp_version)"; then
65+
break
66+
fi
67+
if [[ "$attempt" -lt "$MCP_PLAYWRIGHT_RETRY_ATTEMPTS" ]]; then
68+
echo "docker-git-playwright-mcp: waiting for nested browser runtime (attempt $attempt/$MCP_PLAYWRIGHT_RETRY_ATTEMPTS)..." >&2
69+
sleep "$MCP_PLAYWRIGHT_RETRY_DELAY"
70+
fi
71+
done
72+
73+
if [[ -z "$JSON" ]]; then
74+
echo "docker-git-playwright-mcp: failed to connect to CDP endpoint $CDP_ENDPOINT after $MCP_PLAYWRIGHT_RETRY_ATTEMPTS attempts" >&2
75+
exit 1
76+
fi
77+
78+
WS_URL="$(printf "%s" "$JSON" | node -e 'const fs=require("fs"); const j=JSON.parse(fs.readFileSync(0,"utf8")); process.stdout.write(j.webSocketDebuggerUrl || "")')"
79+
if [[ -z "$WS_URL" ]]; then
80+
echo "docker-git-playwright-mcp: webSocketDebuggerUrl missing" >&2
81+
exit 1
82+
fi
83+
84+
# Rewrite ws origin to match the CDP endpoint origin (docker DNS).
85+
BASE_WS="$(CDP_ENDPOINT="$CDP_ENDPOINT" node -e 'const { URL } = require("url"); const u=new URL(process.env.CDP_ENDPOINT); const proto=u.protocol==="https:"?"wss:":"ws:"; process.stdout.write(proto + "//" + u.host)')"
86+
WS_REWRITTEN="$(BASE_WS="$BASE_WS" WS_URL="$WS_URL" node -e 'const { URL } = require("url"); const base=new URL(process.env.BASE_WS); const ws=new URL(process.env.WS_URL); ws.protocol=base.protocol; ws.host=base.host; process.stdout.write(ws.toString())')"
87+
88+
exec playwright-mcp --cdp-endpoint "$WS_REWRITTEN" --cdp-timeout "$MCP_PLAYWRIGHT_CDP_TIMEOUT" "\${EXTRA_ARGS[@]}" "$@"
89+
EOF
90+
RUN chmod +x /usr/local/bin/docker-git-playwright-mcp`
91+
92+
export const renderDockerfilePlaywrightMcp = (): string => dockerfilePlaywrightMcpBlock.replaceAll("\\${", "${")
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// CHANGE: use the shared link-foundation JS box as the generated project base image
2+
// WHY: issue #267 asks docker-git to reuse unified box containers instead of maintaining a raw Ubuntu workspace base; the Docker Hub JS image is public and version-pinned to avoid latest drift
3+
// QUOTE(ТЗ): "Что бы не зависить только от своих обновлений, а иметь единую инфраструктру есть смысл юзать готовый репозиторий"
4+
// REF: issue-267
5+
// SOURCE: https://github.com/link-foundation/box#docker-hub---combo-boxes
6+
// FORMAT THEOREM: renderDockerfile(config) -> base_image_default(rendered) = konard/box-js:2.1.1
7+
// PURITY: CORE
8+
// INVARIANT: the rendered Dockerfile inherits JS/runtime tooling from link-foundation/box while preserving docker-git bootstrap layers
9+
// COMPLEXITY: O(1)/O(1)
10+
const dockerGitBaseImage = "konard/box-js:2.1.1"
11+
12+
// CHANGE: include tmux in generated project images for durable terminal multiplexing.
13+
// WHY: stable project SSH links attach to persisted tmux sessions instead of one-off shell processes.
14+
// QUOTE(ТЗ): n/a
15+
// REF: PR-309
16+
// SOURCE: n/a
17+
// PURITY: CORE
18+
// INVARIANT: generated base image contains the terminal multiplexer required by project SSH sessions.
19+
// COMPLEXITY: O(1)/O(1)
20+
const renderDockerfileBase = (): string =>
21+
`ARG DOCKER_GIT_BASE_IMAGE=${dockerGitBaseImage}
22+
FROM \${DOCKER_GIT_BASE_IMAGE}
23+
24+
#checkov:skip=CKV_DOCKER_8: docker-git entrypoint must start as root to prepare SSH/auth/bootstrap and run sshd
25+
USER root
26+
ARG UBUNTU_APT_MIRROR=
27+
ENV DEBIAN_FRONTEND=noninteractive
28+
ENV NVM_DIR=/usr/local/nvm
29+
30+
RUN set -eu; \
31+
if [ -n "\${UBUNTU_APT_MIRROR:-}" ]; then \
32+
sed -i \
33+
-e "s|http://archive.ubuntu.com/ubuntu|\${UBUNTU_APT_MIRROR}|g" \
34+
-e "s|http://security.ubuntu.com/ubuntu|\${UBUNTU_APT_MIRROR}|g" \
35+
/etc/apt/sources.list /etc/apt/sources.list.d/ubuntu.sources 2>/dev/null || true; \
36+
fi; \
37+
for attempt in 1 2 3 4 5; do \
38+
rm -rf /var/lib/apt/lists/*; \
39+
if apt-get -o Acquire::Retries=3 -o Acquire::By-Hash=force update; then \
40+
break; \
41+
fi; \
42+
if [ "$attempt" = "5" ]; then \
43+
echo "apt-get update failed after retries" >&2; \
44+
exit 1; \
45+
fi; \
46+
echo "apt-get update attempt \${attempt} failed; retrying..." >&2; \
47+
sleep $((attempt * 2)); \
48+
done; \
49+
apt-get -o Acquire::Retries=3 install -y --no-install-recommends \
50+
openssh-server git gh ca-certificates curl unzip bsdutils sudo tmux \
51+
make docker.io docker-compose-v2 bash-completion zsh zsh-autosuggestions xauth \
52+
ncurses-term jq \
53+
&& rm -rf /var/lib/apt/lists/*`
54+
55+
// CHANGE: install the unified Rust browser connection with a current Rust toolchain.
56+
// WHY: rust-browser-connection uses modern Cargo metadata; Ubuntu apt cargo 1.75 cannot resolve edition-2024 dependencies pulled by current crates.
57+
// QUOTE(ТЗ): "Rust-only отдельный модуль для noVNC/browser, без TS-дублирования"
58+
// REF: issue-347
59+
// SOURCE: n/a
60+
// FORMAT THEOREM: image_build_success -> executable(/usr/local/bin/docker-git-browser-connection)
61+
// PURITY: SHELL
62+
// EFFECT: Docker build downloads rustup and installs the GitHub Rust crate.
63+
// INVARIANT: generated images use rustup stable for cargo install and expose the browser binary on runtime PATH.
64+
// COMPLEXITY: O(network + cargo_build)
65+
const renderDockerfileRustBrowserConnection = (): string =>
66+
`ENV CARGO_HOME=/usr/local/cargo
67+
ENV RUSTUP_HOME=/usr/local/rustup
68+
ENV PATH="/usr/local/cargo/bin:/root/.cargo/bin:/home/box/.cargo/bin:\${PATH}"
69+
RUN set -eu; \
70+
curl --proto '=https' --tlsv1.2 -fsSL https://sh.rustup.rs -o /tmp/rustup-init.sh; \
71+
HOME=/root sh /tmp/rustup-init.sh -y --profile minimal --default-toolchain stable --no-modify-path; \
72+
rm -f /tmp/rustup-init.sh; \
73+
rustc --version; \
74+
cargo --version
75+
76+
# Install unified Rust browser connection (noVNC + CDP + single dg-*-browser guarantee)
77+
# Replaces all previous TS/MCP browser-connection duplication (per issue #347)
78+
RUN cargo install --git https://github.com/ProverCoderAI/rust-browser-connection --branch main --locked --root /usr/local docker-git-browser-connection \
79+
&& /usr/local/bin/docker-git-browser-connection --version
80+
81+
# Passwordless sudo for all users (container is disposable)
82+
RUN printf "%s\\n" "ALL ALL=(ALL) NOPASSWD:ALL" > /etc/sudoers.d/zz-all \
83+
&& chmod 0440 /etc/sudoers.d/zz-all`
84+
85+
/**
86+
* Renders the base image, package prelude, Rust toolchain, and browser module install.
87+
*
88+
* @returns Dockerfile fragment that establishes the shared project container base.
89+
* @pure true
90+
* @effect none; CORE template renderer only constructs a string.
91+
* @invariant the returned fragment starts from the configured shared JS box image and installs the Rust browser CLI.
92+
* @precondition docker-git generated entrypoint remains the container entrypoint.
93+
* @postcondition the fragment keeps root available for setup and publishes docker-git-browser-connection on PATH.
94+
* @complexity O(1) time / O(1) space.
95+
*/
96+
export const renderDockerfilePrelude = (): string =>
97+
[renderDockerfileBase(), renderDockerfileRustBrowserConnection()].join("\n\n")

0 commit comments

Comments
 (0)