diff --git a/.changeset/docker-operator-tooling.md b/.changeset/docker-operator-tooling.md new file mode 100644 index 0000000000..e69db4e4e0 --- /dev/null +++ b/.changeset/docker-operator-tooling.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: The Docker image now ships gh, tailscale, and cloudflared alongside git and ripgrep. +category: feature +dev: Runner stage adds the GitHub CLI (backs `githubAuthMode: "gh-cli"`, which the auth route tells operators to set up with `gh auth login`), cloudflared (backs dashboard remote access, whose in-app installer cannot bootstrap itself reliably in a slim container), and tailscale, each from its vendor's signed apt repository rather than a curl-to-shell installer. Installing tailscale does not make `tailscaled` runnable on its own — that still needs `--cap-add NET_ADMIN --device /dev/net/tun` at `docker run`. Package names and repo URLs are asserted in scripts/__tests__/dockerfile-workspace-manifests.test.mjs. diff --git a/Dockerfile b/Dockerfile index d323302960..234ad23fa3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -78,7 +78,41 @@ ENV PORT=4040 # reach for `rg` as their primary search tool; without it they silently degrade to slower/partial # fallbacks inside the container while working fine on a developer machine that has it installed. RUN apt-get update \ - && apt-get install -y --no-install-recommends git ca-certificates ripgrep \ + && apt-get install -y --no-install-recommends git ca-certificates ripgrep curl gnupg \ + && rm -rf /var/lib/apt/lists/* + +# FNXC:DockerRun 2026-08-18-06:40: gh, tailscale, and cloudflared ship in the image. +# Rationale per tool: `gh` backs Fusion's GitHub integration (githubAuthMode "gh-cli" is a documented +# option and the auth route tells operators to run `gh auth login`, which is impossible if the binary +# is absent); `cloudflared` backs the dashboard's remote-access feature, whose installer cannot +# bootstrap itself reliably inside a slim container; `tailscale` gives the same box a private-network +# option. All three come from their vendors' own apt repositories with signed keyrings rather than +# curl-to-shell installers, so upgrades and signature checks follow the normal apt path. +# +# NOTE: installing tailscale does NOT make `tailscaled` runnable by itself — the daemon additionally +# needs `--cap-add NET_ADMIN --device /dev/net/tun` on `docker run`. Shipping the binary is the part +# the image can own; granting kernel capabilities stays an explicit operator decision. +# +# External integration evidence: +# gh — repo https://github.com/cli/cli, docs https://cli.github.com/, +# apt https://cli.github.com/packages, binary `gh`, key +# githubcli-archive-keyring.gpg (vendor-signed; upstream-pending-verification) +# tailscale — repo https://github.com/tailscale/tailscale, docs https://tailscale.com/download/linux, +# apt https://pkgs.tailscale.com/stable/debian, binaries `tailscale`/`tailscaled`, +# key bookworm.noarmor.gpg (vendor-signed; upstream-pending-verification) +# cloudflared — repo https://github.com/cloudflare/cloudflared, docs https://pkg.cloudflare.com/, +# apt https://pkg.cloudflare.com/cloudflared, binary `cloudflared`, +# key cloudflare-main.gpg (vendor-signed; upstream-pending-verification) +RUN install -m 0755 -d /etc/apt/keyrings \ + && curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg -o /etc/apt/keyrings/githubcli-archive-keyring.gpg \ + && chmod go+r /etc/apt/keyrings/githubcli-archive-keyring.gpg \ + && echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" > /etc/apt/sources.list.d/github-cli.list \ + && curl -fsSL https://pkgs.tailscale.com/stable/debian/bookworm.noarmor.gpg -o /usr/share/keyrings/tailscale-archive-keyring.gpg \ + && curl -fsSL https://pkgs.tailscale.com/stable/debian/bookworm.tailscale-keyring.list -o /etc/apt/sources.list.d/tailscale.list \ + && curl -fsSL https://pkg.cloudflare.com/cloudflare-main.gpg -o /usr/share/keyrings/cloudflare-main.gpg \ + && echo "deb [signed-by=/usr/share/keyrings/cloudflare-main.gpg] https://pkg.cloudflare.com/cloudflared bookworm main" > /etc/apt/sources.list.d/cloudflared.list \ + && apt-get update \ + && apt-get install -y --no-install-recommends gh tailscale cloudflared \ && rm -rf /var/lib/apt/lists/* RUN corepack enable && corepack prepare pnpm@10.33.0 --activate diff --git a/scripts/__tests__/dockerfile-workspace-manifests.test.mjs b/scripts/__tests__/dockerfile-workspace-manifests.test.mjs index 5c015de1de..b6c36a5269 100644 --- a/scripts/__tests__/dockerfile-workspace-manifests.test.mjs +++ b/scripts/__tests__/dockerfile-workspace-manifests.test.mjs @@ -132,3 +132,25 @@ test("runner stage installs ca-certificates alongside git", () => { "runner stage must install ripgrep — the coding agents Fusion drives use `rg` as their primary search tool", ); }); + +/* +FNXC:DockerRun 2026-08-18-06:40: +The operator tooling the image promises must actually be in it. `gh` backs the gh-cli GitHub auth +mode, `cloudflared` backs remote access, and `tailscale` is the private-network option; each is +installed from its vendor's signed apt repository. Assert the repo wiring AND the package names, so +dropping either half (a keyring without the install, or an install whose repo line was removed) fails +here instead of at first use inside a container. +*/ +test("runner stage installs gh, tailscale and cloudflared from vendor repositories", () => { + const dockerfile = readFileSync(path.join(repoRoot, "Dockerfile"), "utf8"); + const runnerStage = dockerfile.slice(dockerfile.indexOf("FROM node:22-slim AS runner")); + + for (const [tool, repo] of [ + ["gh", "https://cli.github.com/packages"], + ["tailscale", "https://pkgs.tailscale.com/stable/debian"], + ["cloudflared", "https://pkg.cloudflare.com/cloudflared"], + ]) { + assert.ok(runnerStage.includes(repo), `runner stage must configure the ${tool} apt repository (${repo})`); + assert.match(runnerStage, new RegExp(`apt-get install[^\n]*(?:\\\n[^\n]*)*\\b${tool}\\b`), `runner stage must install ${tool}`); + } +});