From f02de334e5cedf5b71066c50a556836c0d82bda0 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Thu, 17 Sep 2026 14:21:25 +0700 Subject: [PATCH] feat(paseo): install SDKMAN on start, and rename the wrapper entrypoint SDKMAN goes into ~/.sdkman whenever that directory is missing, the same way the agent CLIs arrive: as paseo, through gosu, onto the volume, where `sdk install` can write and the candidates persist. No variable gates it. The chown of /home/paseo moves out of the AGENT_CLIS guard, since SDKMAN now needs it even when no agent is named, and the agent loop reads AGENT_CLIS into a local first so `set -u` does not trip on it being unset. SDKMAN_DIR is set in the image, and the current/bin of java, scala, gradle, maven and sbt joins PATH: `sdk` itself is a shell function from the rc hook and so exists in terminals only, while the daemon and an agent's non-interactive commands read no rc file and need the binaries on PATH. paseo-sudo-entrypoint was named for the one thing it used to do. It is /usr/local/bin/entrypoint now, matching the source file. --- paseo/Dockerfile | 19 ++++++++++++------- paseo/README.md | 46 +++++++++++++++++++++++++++++++++++++-------- paseo/entrypoint.sh | 16 ++++++++++++---- 3 files changed, 62 insertions(+), 19 deletions(-) diff --git a/paseo/Dockerfile b/paseo/Dockerfile index 9e4a696..4ce1f6e 100644 --- a/paseo/Dockerfile +++ b/paseo/Dockerfile @@ -44,12 +44,17 @@ RUN install -d -m 0755 /etc/apt/keyrings \ && rm -f /tmp/glab.deb \ && rm -rf /var/lib/apt/lists/* -# --- agent cli path -------------------------------------------------------- -# Puts the $HOME directories the agent installers write to on PATH. -ENV PATH=/home/paseo/.local/bin:/home/paseo/.opencode/bin:$PATH +# --- agent cli and sdkman path --------------------------------------------- +# Puts the $HOME directories the agent installers and SDKMAN write to on PATH. +ENV SDKMAN_DIR=/home/paseo/.sdkman +ENV PATH=/home/paseo/.local/bin:/home/paseo/.opencode/bin:\ +$SDKMAN_DIR/candidates/java/current/bin:\ +$SDKMAN_DIR/candidates/scala/current/bin:\ +$SDKMAN_DIR/candidates/gradle/current/bin:\ +$SDKMAN_DIR/candidates/maven/current/bin:\ +$SDKMAN_DIR/candidates/sbt/current/bin:$PATH # --- entrypoint ------------------------------------------------------------ -# Wraps the image's entrypoint to set the paseo user's password while still -# 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"] +# Wraps the image's entrypoint with the root-stage setup -- see entrypoint.sh. +COPY --chmod=0755 entrypoint.sh /usr/local/bin/entrypoint +ENTRYPOINT ["/usr/bin/tini", "--", "/usr/local/bin/entrypoint"] diff --git a/paseo/README.md b/paseo/README.md index 91cbc8e..e74444f 100644 --- a/paseo/README.md +++ b/paseo/README.md @@ -4,8 +4,8 @@ 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). +agent CLIs and [SDKMAN](#sdkman) are not baked in; `entrypoint.sh` installs +them into `$HOME` on start, see [Agents](#agents). ## Setup @@ -122,11 +122,37 @@ themselves in place afterwards. Pi and Oh My Pi are separate projects sharing an ancestor; their commands do not collide. +## SDKMAN + +`entrypoint.sh` installs [SDKMAN](https://sdkman.io) on start too, into +`~/.sdkman`, whenever that directory is missing. No variable gates it — the +JVM toolchain is small next to an agent CLI and the image ships no Java at all. + +Install what you need from a terminal: + +``` +sdk install java +sdk install gradle +``` + +`sdk` is a shell function, defined by the hook the installer appends to +`.bashrc` and `.zshrc`, so it exists in terminals only. The `current/bin` +directory of five candidates — `java`, `scala`, `gradle`, `maven`, `sbt` — is +on the image's `PATH` regardless, so the binaries themselves resolve for the +daemon and for commands an agent runs non-interactively, where no rc file is +read. Install a candidate outside that five and you get the `sdk` function in a +terminal but not the binary elsewhere; add its `current/bin` to the `PATH` line +in the `Dockerfile` if you want it there. + +`SDKMAN_DIR` is set in the image, to the same `~/.sdkman` the installer would +have picked on its own. It is what puts the candidate paths above and the +install location in one place. + ## Storage | Volume | Mount | Holds | | --- | --- | --- | -| `paseo-home` | `/home/paseo` | Daemon state, agent configs, credentials (`.claude`, `.codex`, `.config/*`) | +| `paseo-home` | `/home/paseo` | Daemon state, agent CLIs and their configs and credentials (`.claude`, `.codex`, `.config/*`), SDKMAN and its candidates | | `paseo-workspace` | `/workspace` | Code the agents work on | The agent CLIs, `gh` and `glab` all keep their config under `/home/paseo`, so @@ -170,18 +196,22 @@ all here: 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` is installed as `/usr/local/bin/entrypoint`, next to the + base image's `paseo-docker-entrypoint`, which it wraps. It was + `paseo-sudo-entrypoint` when setting the `sudo` password was all it did. - `entrypoint.sh` runs before the base entrypoint, not after: that one ends in - `exec gosu paseo` and never returns, and by then is no longer root. Both of - its jobs need root — `chpasswd`, and `gosu paseo` for the agent installs. + `exec gosu paseo` and never returns, and by then is no longer root. Every + job it has needs root — `chpasswd`, the `chown`, and `gosu paseo` for the + SDKMAN and agent installs. - It sets the `paseo` password on every start rather than at build, so the password never lands in an image layer, and because `/etc/shadow` is in the image rather than on a volume and reverts on each recreate. The password is piped, not passed as an argument, since arguments are visible in `ps`; `chpasswd` splits on the first colon, so a colon in the password is fine. An empty `PASEO_PASSWORD` leaves the account locked and `sudo` unusable. -- It also `chown`s `/home/paseo` before installing anything. A freshly created - volume can arrive owned by root, and the base entrypoint's own `chown` has - not run yet at that point. +- It also `chown`s `/home/paseo` before installing anything, agent CLI or + SDKMAN. A freshly created volume can arrive owned by root, and the base + entrypoint's own `chown` has not run yet at that point. - `sudo` resets `PATH` to its `secure_path`, which excludes `/usr/local/go/bin`. Use `sudo env PATH="$PATH" go ...` or the full path. - The agent `PATH` entries belong in the image, not in a shell rc: the daemon diff --git a/paseo/entrypoint.sh b/paseo/entrypoint.sh index a6dc8a4..6d0e0af 100755 --- a/paseo/entrypoint.sh +++ b/paseo/entrypoint.sh @@ -1,7 +1,7 @@ #!/usr/bin/env bash # Runs as root ahead of the image's own entrypoint: sets the paseo user's -# login password, then installs any agent CLI named in AGENT_CLIS that is not -# already on PATH. See README.md. +# login password, installs SDKMAN, then installs any agent CLI named in +# AGENT_CLIS that is not already on PATH. See README.md. set -euo pipefail if [[ "$(id -u)" == "0" && -n "${PASEO_PASSWORD:-}" ]]; then @@ -19,10 +19,18 @@ agent_installer() { esac } -if [[ "$(id -u)" == "0" && -n "${AGENT_CLIS:-}" ]]; then +if [[ "$(id -u)" == "0" ]]; then chown paseo:paseo /home/paseo - for agent in ${AGENT_CLIS//,/ }; do + if [[ ! -d "${SDKMAN_DIR:-/home/paseo/.sdkman}" ]]; then + echo "entrypoint: installing sdkman" + gosu paseo bash -c 'curl -fsSL https://get.sdkman.io | bash' \ + || echo "entrypoint: sdkman failed to install, continuing" >&2 + fi + + agents="${AGENT_CLIS:-}" + + for agent in ${agents//,/ }; do installer="$(agent_installer "$agent")" if [[ -z "$installer" ]]; then