From 55e8f7321d912582705f0c273d67ace4feb0b448 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Thu, 17 Sep 2026 09:44:12 +0700 Subject: [PATCH] feat(paseo): install glab, and stop preinstalling the agent CLIs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The paseo user cannot write /usr/local, so a baked-in agent CLI can never apply its own update — every one of them ships an updater that expects to rewrite its own binary, and Claude Code nags about the failure at startup. Installed under $HOME they update themselves and still persist, since that is the paseo-home volume. The README now lists each vendor's installer. Bun goes with them: it was only there because omp is compiled against it. glab arrives as the .deb from GitLab's releases page, pinned by GLAB_VERSION because the URL carries the version. GitLab runs no apt repository and calls Homebrew its only officially supported Linux package manager. The Dockerfile drops its explanations along the way; they are in the README. --- paseo/Dockerfile | 56 ++++++---------------------- paseo/README.md | 97 ++++++++++++++++++++++++++++++++---------------- 2 files changed, 76 insertions(+), 77 deletions(-) diff --git a/paseo/Dockerfile b/paseo/Dockerfile index 3e8640e..f72289f 100644 --- a/paseo/Dockerfile +++ b/paseo/Dockerfile @@ -1,18 +1,9 @@ -# The official image ships no agent CLIs or language toolchains. Add them here, -# one concern per layer, cheapest and least-changing first. -# -# Stays root: the entrypoint needs root to chown the mounted volumes, then -# drops to paseo with gosu itself. Nothing installs into $HOME -- that is -# /home/paseo, a volume mount that masks anything baked in at build time. +# Adds language toolchains and CLI tooling to the official image, which ships +# none of it. Agent CLIs are not among them -- see README.md, which carries the +# reasoning for everything here. FROM ghcr.io/getpaseo/paseo:latest # --- system packages ------------------------------------------------------- -# Everyday shell tooling. git and curl are already in the base image; sudo is -# not, despite Debian's base-passwd shipping an (empty) sudo group. -# -# build-essential is the C toolchain the language layers below assume but do -# not ship: cgo, npm's node-gyp addons and Python C extensions all shell out to -# gcc and make. Kept in this layer so there is only one apt-get update. RUN apt-get update \ && apt-get install -y --no-install-recommends \ less nano jq unzip zip lsof psmisc ugrep bfs zsh sudo \ @@ -21,8 +12,6 @@ RUN apt-get update \ && usermod -aG sudo paseo # --- python ---------------------------------------------------------------- -# Debian 12 only carries 3.11, so fetch a standalone CPython build with uv -# (astral.sh/uv). Both directories sit outside $HOME. ARG PYTHON_VERSION=3.12 ENV UV_INSTALL_DIR=/usr/local/bin \ UV_PYTHON_INSTALL_DIR=/opt/python \ @@ -31,7 +20,6 @@ RUN curl -fsSL https://astral.sh/uv/install.sh | sh \ && uv python install "${PYTHON_VERSION}" --default # --- go -------------------------------------------------------------------- -# Debian 12 carries 1.19, far too old, so use the official tarball. ARG GO_VERSION=1.26.8 ENV PATH=/usr/local/go/bin:$PATH RUN curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-$(dpkg --print-architecture).tar.gz" \ @@ -39,45 +27,25 @@ RUN curl -fsSL "https://go.dev/dl/go${GO_VERSION}.linux-$(dpkg --print-architect && tar -C /usr/local -xzf /tmp/go.tar.gz \ && rm /tmp/go.tar.gz -# --- gh -------------------------------------------------------------------- -# Debian does not package gh, so use GitHub's own signed repository. +# --- gh and glab ----------------------------------------------------------- +# gh from GitHub's signed apt repository, glab from the .deb on its releases +# page. +ARG GLAB_VERSION=1.118.0 RUN install -d -m 0755 /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://gitlab.com/gitlab-org/cli/-/releases/v${GLAB_VERSION}/downloads/glab_${GLAB_VERSION}_linux_$(dpkg --print-architecture).deb" \ + -o /tmp/glab.deb \ && apt-get update \ - && apt-get install -y --no-install-recommends gh \ + && apt-get install -y --no-install-recommends gh /tmp/glab.deb \ + && rm -f /tmp/glab.deb \ && rm -rf /var/lib/apt/lists/* -# --- bun ------------------------------------------------------------------- -# Only omp needs it: @oh-my-pi/pi-coding-agent is Bun-compiled and its bin -# opens with `#!/usr/bin/env bun`, so without Bun on PATH npm installs an omp -# that cannot start. BUN_INSTALL lands it in /usr/local, not the $HOME/.bun -# default that the paseo-home volume would mask. -ENV BUN_INSTALL=/usr/local -RUN curl -fsSL https://bun.sh/install | bash - -# --- agent CLIs ------------------------------------------------------------ -# These all publish the same way: a thin launcher plus the real binary as a -# per-platform optional dependency. npm therefore installs exactly what each -# vendor's own installer would, and is not a Node wrapper. -# -# Do not swap any of them for their `curl | bash` installer -- those write to -# $HOME/.local, and $HOME is /home/paseo, a volume that masks whatever the -# build put there. Last because these change most often. -RUN npm install -g \ - @anthropic-ai/claude-code \ - @openai/codex \ - opencode-ai \ - @github/copilot \ - @earendil-works/pi-coding-agent \ - @oh-my-pi/pi-coding-agent - # --- entrypoint ------------------------------------------------------------ # Wraps the image's entrypoint to set the paseo user's password while still -# root -- see entrypoint.sh. tini stays in front of it, as in the base image. -# Last so that editing the script rebuilds only this layer. +# root -- see entrypoint.sh. COPY --chmod=0755 entrypoint.sh /usr/local/bin/paseo-sudo-entrypoint ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/paseo-sudo-entrypoint"] diff --git a/paseo/README.md b/paseo/README.md index 89c6322..4614b0d 100644 --- a/paseo/README.md +++ b/paseo/README.md @@ -1,9 +1,11 @@ # paseo [Paseo](https://paseo.sh) — self-hosted daemon and web UI for running coding -agents. Built from a local `Dockerfile` that adds six agent CLIs, `gh`, Go, -Python, a C toolchain, and shell tooling to the -[official image](https://paseo.sh/docs/docker), which ships none of it. +agents. Built from a local `Dockerfile` that adds `gh`, `glab`, Go, Python, +a C toolchain, and shell tooling to the +[official image](https://paseo.sh/docs/docker), which ships none of it. The +agent CLIs are not baked in — install them into `$HOME` yourself, see +[Agents](#agents). ## Setup @@ -16,8 +18,8 @@ Python, a C toolchain, and shell tooling to the paseo.example.com:443 ``` -4. In a terminal inside Paseo, log in once per tool you use — see - [Agents](#agents) — plus `gh auth login`. +4. In a terminal inside Paseo, install and log in to the agents you use — see + [Agents](#agents) — plus `gh auth login` and `glab auth login`. The port must be typed by hand. The UI rejects a bare hostname, and the auto-connect hint does not help: the daemon builds it from the `Host` header, @@ -56,18 +58,41 @@ Listens on `6767`, published nowhere — the platform maps the domain to it, so ## Agents -| Agent | Command | Package | Log in with | -| --- | --- | --- | --- | -| Claude Code | `claude` | `@anthropic-ai/claude-code` | `claude` | -| Codex | `codex` | `@openai/codex` | `codex login` | -| opencode | `opencode` | `opencode-ai` | `opencode auth login` | -| Copilot CLI | `copilot` | `@github/copilot` | `/login` inside `copilot` | -| Pi | `pi` | `@earendil-works/pi-coding-agent` | `pi` | -| Oh My Pi | `omp` | `@oh-my-pi/pi-coding-agent` | `omp` | +The image installs none of them. Run each vendor's own installer once, in a +terminal inside Paseo, as the `paseo` user — never with `sudo`: -All six come from npm, so another provider is one more entry on the `npm -install` line. Pi and Oh My Pi are separate projects sharing an ancestor; their -commands do not collide. +| Agent | Command | Install with | Log in with | +| --- | --- | --- | --- | +| Claude Code | `claude` | `curl -fsSL https://claude.ai/install.sh \| bash` | `claude` | +| Codex | `codex` | `curl -fsSL https://chatgpt.com/codex/install.sh \| sh` | `codex login` | +| opencode | `opencode` | `curl -fsSL https://opencode.ai/install \| bash` | `opencode auth login` | +| Copilot CLI | `copilot` | `curl -fsSL https://gh.io/copilot-install \| bash` | `/login` inside `copilot` | +| Oh My Pi | `omp` | `curl -fsSL https://omp.sh/install \| sh` | `omp` | +| Pi | `pi` | `curl -fsSL https://pi.dev/install.sh \| sh` | `pi` | + +Each lands in `$HOME` — `~/.local/bin` for most, `~/.opencode/bin` for +opencode — and appends that directory to your shell rc, so open a new terminal +afterwards. `$HOME` is the `paseo-home` volume, so both the binaries and the +logins survive a redeploy. + +The installers pull in any runtime they need, into `$HOME` as well. `omp` is +the one to know about: it is Bun-compiled, and with no Bun on `PATH` its +installer takes the prebuilt binary. Ask for the source build (`--source`) and +it installs Bun to `~/.bun` first. + +Why not bake them into the image: `paseo` cannot write `/usr/local`, so a +preinstalled CLI can never apply its own update. Every one of these ships an +updater (`claude update`, `codex update`, `omp update`, …) that expects to +rewrite its own binary, and under `/usr/local` it fails on permissions — Claude +Code nags about it at startup. Installed in `$HOME` they update themselves, and +you are never waiting on an image rebuild for a release that shipped that +morning. + +Run the installers unprivileged. Under `sudo` they target root's home and land +outside the volume; Claude Code's refuses outright. + +Pi and Oh My Pi are separate projects sharing an ancestor; their commands do +not collide. ## Storage @@ -76,9 +101,10 @@ commands do not collide. | `paseo-home` | `/home/paseo` | Daemon state, agent configs, credentials (`.claude`, `.codex`, `.config/*`) | | `paseo-workspace` | `/workspace` | Code the agents work on | -Every agent CLI and `gh` keep their config under `/home/paseo`, so all logins -survive a redeploy — the base image points `CLAUDE_CONFIG_DIR`, `CODEX_HOME` -and the `XDG_*` variables into it. Dotfiles live there too, so a `.zshrc` or +The agent CLIs, `gh` and `glab` all keep their config under `/home/paseo`, so +every login survives a redeploy — the base image points `CLAUDE_CONFIG_DIR`, +`CODEX_HOME` and the `XDG_*` variables into it. The agent binaries live there +too, since you install them yourself. Dotfiles as well, so a `.zshrc` or oh-my-zsh install persists. Anything written outside `$HOME` (`chsh`, `apt install`) is lost on rebuild. @@ -86,29 +112,34 @@ oh-my-zsh install persists. Anything written outside `$HOME` (`chsh`, | Tool | Source | Why not apt | | --- | --- | --- | -| Agent CLIs (see [above](#agents)) | npm | — | -| Bun | Official `bun.sh` install script | Debian does not package it | | `gh` | GitHub's signed apt repo | Debian does not package it | +| `glab` (`GLAB_VERSION`) | The `.deb` on GitLab's releases page | Debian does not package it, and GitLab runs no apt repo | | Go (`GO_VERSION`) | Official go.dev tarball | Debian 12 ships 1.19 | | Python (`PYTHON_VERSION`) | `uv python install` | Debian 12 ships 3.11 | | `less nano jq unzip zip lsof psmisc ugrep bfs zsh sudo` | apt | — | | `build-essential` | apt | — | -Bump a language with a build arg, e.g. `--build-arg GO_VERSION=1.27.1`. `uv` -itself is installed too. +Bump a pinned version with a build arg, e.g. `--build-arg GO_VERSION=1.27.1`. +`uv` itself is installed too. `GLAB_VERSION` is pinned rather than tracking the +latest because GitLab's download URL carries the version in the path. -Constraints worth knowing before changing the `Dockerfile`: +The `Dockerfile` itself only says what each layer installs. The reasoning is +all here: - `$HOME` is `/home/paseo`, a volume that masks anything the build writes - there. Hence `/opt/python` and `/usr/local/go` rather than the defaults, and - hence npm rather than each vendor's `curl | bash` installer, which writes to - `$HOME/.local`. npm delivers the same native binaries regardless — every one - of these packages is a thin launcher with the real binary as a per-platform - optional dependency. -- Bun exists for `omp` alone, whose bin opens with `#!/usr/bin/env bun`. - Node runs the other five. -- The agent CLIs cannot auto-update, since `paseo` cannot write `/usr/local`. - Claude Code shows a notice at startup. Rebuild to update. + there. Hence `/opt/python` and `/usr/local/go` rather than the defaults. +- Do not add agent CLIs here, or a runtime only they need. Under `/usr/local` + they cannot self-update; in `$HOME` they can, and they persist anyway. Bun + used to be here for `omp` and went the same way. See [Agents](#agents). +- One concern per layer, cheapest and least-changing first, so bumping a + version rebuilds as little as possible. +- `build-essential` is the C toolchain the language layers assume but do not + ship: cgo, npm's node-gyp addons and Python C extensions all shell out to + `gcc` and `make`. It rides along in the apt layer so there is one + `apt-get update`. +- `git` and `curl` are already in the base image. `sudo` is not, despite + Debian's `base-passwd` shipping an empty `sudo` group, so the apt layer adds + it and puts `paseo` in the group. - The image stays root: the entrypoint chowns the volumes, then drops to the `paseo` user (uid 1000) with `gosu`. - `entrypoint.sh` sets the `paseo` password from `PASEO_PASSWORD` on every