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.
This commit is contained in:
tiennm99 committed 2026-09-17 14:21:25 +07:00
1 parent 07991dabaf
commit f02de334e5
3 files changed
+62 -19

No files matched your search

+12 -7
View File
@@ -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"]
+38 -8
View File
@@ -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
+12 -4
View File
@@ -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