Fix: developer-writable npm prefix for pi install
Validate / docs-check (push) Successful in 23s
Validate / validate-base (push) Has started running
Validate / validate-omos (push) Has started running
Validate / validate-with-pi (push) Has been cancelled
Validate / validate-omos-with-pi (push) Has been cancelled
Validate / docs-check (push) Successful in 23s
Validate / validate-base (push) Has started running
Validate / validate-omos (push) Has started running
Validate / validate-with-pi (push) Has been cancelled
Validate / validate-omos-with-pi (push) Has been cancelled
NPM_CONFIG_PREFIX is now /home/developer/.pi/npm-global, with that
prefix's bin/ prepended to PATH. Without this, 'pi install npm:<pkg>'
(and any 'npm install -g') by the developer user would EACCES against
the system prefix (/usr).
The new prefix lives on the devbox-pi-config named volume, so:
- User-installed pi packages (themes, skills, extensions) survive
container recreate AND image rebuild, complementing pi's auto-
restore from settings.json with one less cold-start step.
- A user-driven 'npm install -g @mariozechner/pi-coding-agent' lands
on the volume and wins over the baked pi via PATH order.
Build-time 'npm install -g' calls (opencode, pi, oh-my-opencode-slim)
are unaffected: the new ENVs are declared after those steps in the
Dockerfile, so the baked binaries still install to /usr at build time
and are not shadowed by the volume mount at runtime.
Verified end-to-end with a Bun-driven smoke test: as developer,
'npm install -g cowsay' inside the container succeeds, the binary
lands on PATH, and survives a fresh container against the same volume.
DOCKER_HUB.md regenerated (24997/25000 bytes, 3-byte headroom — was
138 before; future README additions to the persistence section need
to trim something else first).
Docs updated: Dockerfile inline comments, README persistence section,
AGENTS install contract, DOCKER_HUB persistence table, .env.example
notes, CHANGELOG Unreleased entry.
This commit is contained in:
+8
-4
@@ -75,11 +75,15 @@ SSH_KEY_PATH=~/.ssh
|
|||||||
# palace path — wing data is mutually visible to either harness.
|
# palace path — wing data is mutually visible to either harness.
|
||||||
#
|
#
|
||||||
# Pi version is baked at build time via PI_VERSION (default: latest at
|
# Pi version is baked at build time via PI_VERSION (default: latest at
|
||||||
# build). `pi update` inside the container would write to the npm global
|
# build). The baked `pi` binary is at /usr/bin/pi (system npm prefix);
|
||||||
# prefix, which is not on a named volume — updates do not persist across
|
# rebuild the image to upgrade it. NPM_CONFIG_PREFIX is set to
|
||||||
# `--rm` containers. Rebuild the image to upgrade pi.
|
# /home/developer/.pi/npm-global, so anything installed via
|
||||||
|
# `pi install npm:...` or `npm install -g` as the developer user
|
||||||
|
# (themes, skills, extensions, including a user-installed pi itself)
|
||||||
|
# lands on the named volume and survives container recreate AND image
|
||||||
|
# rebuilds. A user-installed pi wins via PATH order over the baked one.
|
||||||
#
|
#
|
||||||
# Pi config (settings.json, extensions toggle state) persists in the
|
# Pi config (settings.json, extensions toggle state, sessions, auth) persists in the
|
||||||
# devbox-pi-config named volume mounted at ~/.pi/.
|
# devbox-pi-config named volume mounted at ~/.pi/.
|
||||||
#
|
#
|
||||||
# To launch pi from a `compose run` invocation:
|
# To launch pi from a `compose run` invocation:
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ When bumping the opencode version, also bump `OPENCODE_VERSION` in `Dockerfile`
|
|||||||
- **MemPalace install path** — installed via `uv tool install` into `/opt/uv-tools/mempalace/`. Both the `mempalace` CLI and the `mempalace-mcp` MCP server binary are shipped as entry points by the mempalace package itself and placed on PATH by uv as shims whose shebangs point at the venv's Python. No hand-rolled wrapper is needed. Do not use `pip install --break-system-packages` — that was the previous approach and has been removed. Do not use `["python3", "-m", "mempalace.mcp_server"]` in `opencode.jsonc` — system Python can't import from the uv venv.
|
- **MemPalace install path** — installed via `uv tool install` into `/opt/uv-tools/mempalace/`. Both the `mempalace` CLI and the `mempalace-mcp` MCP server binary are shipped as entry points by the mempalace package itself and placed on PATH by uv as shims whose shebangs point at the venv's Python. No hand-rolled wrapper is needed. Do not use `pip install --break-system-packages` — that was the previous approach and has been removed. Do not use `["python3", "-m", "mempalace.mcp_server"]` in `opencode.jsonc` — system Python can't import from the uv venv.
|
||||||
- **generate-config.py idempotency** — the script MUST never overwrite an existing `opencode.jsonc` or legacy `opencode.json`. Config persists in the `devbox-opencode-config` named volume; accidentally clobbering that file would destroy hand-edits. The smoke test asserts this.
|
- **generate-config.py idempotency** — the script MUST never overwrite an existing `opencode.jsonc` or legacy `opencode.json`. Config persists in the `devbox-opencode-config` named volume; accidentally clobbering that file would destroy hand-edits. The smoke test asserts this.
|
||||||
- **Skillset auto-deploy** — on every container start, `entrypoint-user.sh` looks for a skillset repo (detection order: `$SKILLSET_CONTAINER_PATH` → `$HOME/skillset` → `/workspace/skillset`) and runs `deploy-skills.sh --bootstrap --prune-stale`. This creates relative symlinks in `~/.agents/skills/` and `~/.config/opencode/instructions/`. Do NOT bind-mount `~/.agents/skills/` from the host — the container manages its own skills with relative symlinks that differ from the host's. The named volume `devbox-opencode-config` persists the deployed config across restarts.
|
- **Skillset auto-deploy** — on every container start, `entrypoint-user.sh` looks for a skillset repo (detection order: `$SKILLSET_CONTAINER_PATH` → `$HOME/skillset` → `/workspace/skillset`) and runs `deploy-skills.sh --bootstrap --prune-stale`. This creates relative symlinks in `~/.agents/skills/` and `~/.config/opencode/instructions/`. Do NOT bind-mount `~/.agents/skills/` from the host — the container manages its own skills with relative symlinks that differ from the host's. The named volume `devbox-opencode-config` persists the deployed config across restarts.
|
||||||
- **Config persistence via named volume** — `devbox-opencode-config` is a Docker named volume mounted at `~/.config/opencode/`. It is NOT a host bind mount by default. This separation allows both native and containerized opencode to coexist on the same machine without symlink conflicts. Users who need to override can replace the named volume with a host bind mount in their compose file. **Same pattern for pi:** `devbox-pi-config` is mounted at `~/.pi/` and persists user toggles (`/ext`-disabled extensions) and `~/.pi/agent/settings.json` edits across container recreate.
|
- **Config persistence via named volume** — `devbox-opencode-config` is a Docker named volume mounted at `~/.config/opencode/`. It is NOT a host bind mount by default. This separation allows both native and containerized opencode to coexist on the same machine without symlink conflicts. Users who need to override can replace the named volume with a host bind mount in their compose file. **Same pattern for pi:** `devbox-pi-config` is mounted at `~/.pi/` and persists user toggles (`/ext`-disabled extensions), `~/.pi/agent/settings.json` edits, and — because `NPM_CONFIG_PREFIX` is set to `~/.pi/npm-global` — anything installed via `pi install npm:...` or `npm install -g` as the developer user, across container recreate AND image rebuild.
|
||||||
- **pi install contract** — `INSTALL_PI=true` (default false) opt-in build arg. `pi` is npm-installed globally at build time; the npm prefix is NOT on a named volume, so `pi update` inside the container does not persist across `--rm` containers. Image rebuild is the upgrade path — same contract as `OPENCODE_VERSION`. The pi-toolkit and pi-extensions repos are git-cloned into `/opt/` at build time, then their `install.sh` runs from `entrypoint-user.sh` on each container start to symlink into `~/.pi/agent/` (which lives on the named volume). The mempalace pi-bridge is symlinked manually from `/opt/mempalace-toolkit/extensions/pi/mempalace.ts` — we do NOT call mempalace-toolkit's full `install.sh` because its `install_skill` step would race with skillset auto-deploy `--prune-stale`.
|
- **pi install contract** — `INSTALL_PI=true` (default false) opt-in build arg. The baked `pi` binary is npm-installed globally to `/usr` at build time (system prefix). At runtime, `NPM_CONFIG_PREFIX=/home/developer/.pi/npm-global` is set in the image ENV with that prefix's `bin/` prepended to `PATH` — so any `pi install npm:...` or `npm install -g` invoked by the developer user lands on the named volume and survives everything except `docker compose down -v`. The new ENVs are declared *after* all build-time `npm install -g` calls in the Dockerfile so they don't redirect the baked installs into a path that the volume mount would later shadow. If the user runs `npm install -g @mariozechner/pi-coding-agent` themselves, the user-installed copy on the volume wins via `PATH` order; otherwise image rebuild is the upgrade path for the baked pi (same contract as `OPENCODE_VERSION`). The pi-toolkit and pi-extensions repos are git-cloned into `/opt/` at build time, then their `install.sh` runs from `entrypoint-user.sh` on each container start to symlink into `~/.pi/agent/` (which lives on the named volume). The mempalace pi-bridge is symlinked manually from `/opt/mempalace-toolkit/extensions/pi/mempalace.ts` — we do NOT call mempalace-toolkit's full `install.sh` because its `install_skill` step would race with skillset auto-deploy `--prune-stale`.
|
||||||
- **Pi deploy ordering matters in entrypoint-user.sh** — `pi-toolkit` runs first (creates `keybindings.json` symlink and writes pi-env.zsh), then `pi-extensions`, then `settings.json` template bootstrap, then mempalace bridge symlink. mempalace-toolkit's `check_pi_toolkit` probe (when called from the host install path) expects keybindings to already be present — not currently called from container, but ordering matches host convention.
|
- **Pi deploy ordering matters in entrypoint-user.sh** — `pi-toolkit` runs first (creates `keybindings.json` symlink and writes pi-env.zsh), then `pi-extensions`, then `settings.json` template bootstrap, then mempalace bridge symlink. mempalace-toolkit's `check_pi_toolkit` probe (when called from the host install path) expects keybindings to already be present — not currently called from container, but ordering matches host convention.
|
||||||
- **Default CMD is `bash -l`** — not a harness. `docker compose run --rm devbox` drops the user into a login shell to choose: `aws sso login`, then `opencode` or `pi` (or any tool). Pass the harness explicitly to launch directly: `docker compose run --rm devbox opencode` / `docker compose run --rm devbox pi`. `docker compose exec` bypasses entrypoint+CMD entirely (existing user workflow unchanged).
|
- **Default CMD is `bash -l`** — not a harness. `docker compose run --rm devbox` drops the user into a login shell to choose: `aws sso login`, then `opencode` or `pi` (or any tool). Pass the harness explicitly to launch directly: `docker compose run --rm devbox opencode` / `docker compose run --rm devbox pi`. `docker compose exec` bypasses entrypoint+CMD entirely (existing user workflow unchanged).
|
||||||
- **Docker Hub description update** — uses `/v2/auth/token` endpoint (not the deprecated `/v2/users/login`). Auth uses `identifier`/`secret` fields, returns `access_token`, sent as `Bearer`. Short description must be ≤100 bytes.
|
- **Docker Hub description update** — uses `/v2/auth/token` endpoint (not the deprecated `/v2/users/login`). Auth uses `identifier`/`secret` fields, returns `access_token`, sent as `Bearer`. Short description must be ≤100 bytes.
|
||||||
|
|||||||
@@ -15,6 +15,10 @@ Docs-only updates that don't trigger a new image build (Docker Hub description w
|
|||||||
- **Docs:** README pi section gains a `### Setup` paragraph mentioning the prebuilt `latest-with-pi` and `latest-omos-with-pi` Docker Hub tags, mirroring the OMOS section's `latest-omos` mention.
|
- **Docs:** README pi section gains a `### Setup` paragraph mentioning the prebuilt `latest-with-pi` and `latest-omos-with-pi` Docker Hub tags, mirroring the OMOS section's `latest-omos` mention.
|
||||||
- **Docs:** AGENTS.md tag-scheme paragraph corrected from "four Docker Hub tags per release" to eight (the v1.14.41b CI matrix expansion). Reclaim-disk job list updated from the four pre-pi jobs to all eight current `load: true` jobs.
|
- **Docs:** AGENTS.md tag-scheme paragraph corrected from "four Docker Hub tags per release" to eight (the v1.14.41b CI matrix expansion). Reclaim-disk job list updated from the four pre-pi jobs to all eight current `load: true` jobs.
|
||||||
|
|
||||||
|
Image changes (will ship in the next tagged release):
|
||||||
|
|
||||||
|
- **Fix:** `pi install npm:<pkg>` (and any `npm install -g`) by the `developer` user no longer EACCES against the system npm prefix. `NPM_CONFIG_PREFIX` is now `/home/developer/.pi/npm-global` and the prefix's `bin/` is prepended to `PATH`. The directory lives on the `devbox-pi-config` named volume, so user-installed pi packages (themes, skills, extensions) survive container recreation and image rebuilds. Build-time `npm install -g` calls (opencode, pi, oh-my-opencode-slim) are unaffected because the new ENVs are declared after those steps in the Dockerfile, so the baked binaries still install to `/usr` and are not shadowed by the volume mount.
|
||||||
|
|
||||||
## v1.14.41b — 2026-05-08
|
## v1.14.41b — 2026-05-08
|
||||||
|
|
||||||
**Optional pi as second harness.**
|
**Optional pi as second harness.**
|
||||||
|
|||||||
+3
-3
@@ -375,10 +375,10 @@ pi ships pre-wired with the mempalace bridge — the `mempalace.ts` extension is
|
|||||||
|
|
||||||
| Path in container | Volume | Contains |
|
| Path in container | Volume | Contains |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `/home/developer/.pi` | `devbox-pi-config` (default) | `settings.json`, `/ext`-disabled extensions, pi user state |
|
| `/home/developer/.pi` | `devbox-pi-config` (default) | `settings.json`, `agent/extensions/`, `auth.json`, `sessions/`, plus `git/` and `npm-global/` (`NPM_CONFIG_PREFIX` points here) so `pi install npm:` / `git:` and `npm i -g` survive container recreate and image rebuild. |
|
||||||
| `/home/developer/.mempalace` | `devbox-palace` (uncomment to enable) | Shared palace — visible to both pi and opencode |
|
| `/home/developer/.mempalace` | `devbox-palace` (uncomment) | Shared palace — visible to pi and opencode |
|
||||||
|
|
||||||
The `pi` binary, pi-toolkit, and pi-extensions are baked into the image; `pi update` does **not** persist across `--rm` containers — image rebuild is the upgrade path.
|
Baked pi (`/usr/bin/pi`, `/opt/pi-*`) ships in the image; rebuild to upgrade. `npm i -g @mariozechner/pi-coding-agent` lands on the volume and wins via `PATH`.
|
||||||
|
|
||||||
Full build args, extension list, and toolkit detail: <https://gitea.jordbo.se/joakimp/opencode-devbox#pi-alternativecomplementary-harness>
|
Full build args, extension list, and toolkit detail: <https://gitea.jordbo.se/joakimp/opencode-devbox#pi-alternativecomplementary-harness>
|
||||||
|
|
||||||
|
|||||||
+27
-4
@@ -293,10 +293,14 @@ RUN if [ "${INSTALL_OPENCODE}" = "true" ]; then \
|
|||||||
# start so symlinks land under ~/.pi/agent/ on the named volume.
|
# start so symlinks land under ~/.pi/agent/ on the named volume.
|
||||||
#
|
#
|
||||||
# Pi version is pinned by PI_VERSION (default: latest at build time).
|
# Pi version is pinned by PI_VERSION (default: latest at build time).
|
||||||
# `pi update` inside the container would write to the npm global
|
# The baked pi binary lives at /usr/bin/pi (system npm prefix); the
|
||||||
# prefix, which is not on a volume — so updates do NOT persist across
|
# user-writable NPM_CONFIG_PREFIX (~/.pi/npm-global, set further down)
|
||||||
# `--rm` containers. Same contract as OPENCODE_VERSION: rebuild the
|
# is only consulted by `pi install npm:<pkg>` and `npm install -g` at
|
||||||
# image to upgrade pi.
|
# runtime — it does NOT shadow the baked pi unless the user does
|
||||||
|
# `npm install -g @mariozechner/pi-coding-agent` themselves, in which
|
||||||
|
# case the user-installed copy on the volume wins via PATH order. Same
|
||||||
|
# contract as OPENCODE_VERSION otherwise: rebuild the image to upgrade
|
||||||
|
# the baked pi.
|
||||||
ARG INSTALL_PI=false
|
ARG INSTALL_PI=false
|
||||||
ARG PI_VERSION=latest
|
ARG PI_VERSION=latest
|
||||||
ARG PI_TOOLKIT_REF=main
|
ARG PI_TOOLKIT_REF=main
|
||||||
@@ -418,6 +422,25 @@ print('chromadb embedding model warmed: all-MiniLM-L6-v2')" && \
|
|||||||
ls -lh /home/${USER_NAME}/.cache/chroma/onnx_models/all-MiniLM-L6-v2/ ; \
|
ls -lh /home/${USER_NAME}/.cache/chroma/onnx_models/all-MiniLM-L6-v2/ ; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── User-writable npm global prefix on the devbox-pi-config volume ──
|
||||||
|
# By default npm's global prefix is /usr (writable only by root) so any
|
||||||
|
# `pi install npm:<pkg>` or `npm install -g <pkg>` invoked by the
|
||||||
|
# developer user would EACCES. Pointing the prefix into ~/.pi places
|
||||||
|
# user-installed packages on the named volume, which means they survive
|
||||||
|
# container recreation AND image rebuilds (complementing pi's auto-
|
||||||
|
# restore from settings.json with one less cold-start step).
|
||||||
|
#
|
||||||
|
# These ENVs land AFTER all build-time `npm install -g` calls
|
||||||
|
# (opencode, pi, oh-my-opencode-slim) so those still install to /usr at
|
||||||
|
# build time. They take effect for every runtime invocation regardless
|
||||||
|
# of shell init: docker compose run/exec, login shells, non-interactive
|
||||||
|
# commands. npm auto-creates the prefix directory on first install.
|
||||||
|
#
|
||||||
|
# Harmless when INSTALL_PI=false (and no named volume mounted at ~/.pi):
|
||||||
|
# the dir just lives on the container's writable layer.
|
||||||
|
ENV NPM_CONFIG_PREFIX=/home/${USER_NAME}/.pi/npm-global
|
||||||
|
ENV PATH="/home/${USER_NAME}/.pi/npm-global/bin:${PATH}"
|
||||||
|
|
||||||
# ── Shell defaults (bash history, aliases, readline) ─────────────────
|
# ── Shell defaults (bash history, aliases, readline) ─────────────────
|
||||||
# Shipped under /etc/skel-devbox/ rather than copied directly to the
|
# Shipped under /etc/skel-devbox/ rather than copied directly to the
|
||||||
# user's home. The entrypoint copies them to /home/developer/ only if
|
# user's home. The entrypoint copies them to /home/developer/ only if
|
||||||
|
|||||||
@@ -451,7 +451,15 @@ docker compose exec -u developer devbox bash
|
|||||||
|
|
||||||
### Persistence
|
### Persistence
|
||||||
|
|
||||||
`~/.pi/` is mounted on the `devbox-pi-config` named volume. User toggles via `/ext`, edits to `~/.pi/agent/settings.json`, and any pi state survive container recreate. `pi update` writes to the npm global prefix which is *not* on a volume — image rebuild is the upgrade path.
|
`~/.pi/` is mounted on the `devbox-pi-config` named volume. Everything below survives container recreate **and** image rebuilds:
|
||||||
|
|
||||||
|
- `~/.pi/agent/settings.json` (provider/model, theme selection, the `mcp` block, and the `packages` array tracking installed pi packages).
|
||||||
|
- `~/.pi/agent/extensions/` (hand-placed extensions and the symlinks deployed by `pi-extensions/install.sh`).
|
||||||
|
- `~/.pi/agent/sessions/`, `~/.pi/agent/auth.json`.
|
||||||
|
- `~/.pi/agent/git/<host>/<path>/` (pi packages installed via `pi install git:...`).
|
||||||
|
- `~/.pi/npm-global/` (pi packages installed via `pi install npm:...`, plus any `npm install -g` invoked as the `developer` user). `NPM_CONFIG_PREFIX` is pre-set in the image, the prefix's `bin/` is on `PATH`, and the directory itself lives on the volume — so user-installed themes, skills, and extensions survive everything short of `docker compose down -v`.
|
||||||
|
|
||||||
|
The **baked** pi binary (and pi-toolkit / pi-extensions repos under `/opt/`) live on the image filesystem, not the volume. Image rebuild is the upgrade path for those — same contract as `OPENCODE_VERSION`. If you `npm install -g @mariozechner/pi-coding-agent` yourself, the user-installed copy on the volume wins via `PATH` order and survives image rebuilds.
|
||||||
|
|
||||||
### Configuration
|
### Configuration
|
||||||
|
|
||||||
|
|||||||
@@ -145,10 +145,10 @@ pi ships pre-wired with the mempalace bridge — the `mempalace.ts` extension is
|
|||||||
|
|
||||||
| Path in container | Volume | Contains |
|
| Path in container | Volume | Contains |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `/home/developer/.pi` | `devbox-pi-config` (default) | `settings.json`, `/ext`-disabled extensions, pi user state |
|
| `/home/developer/.pi` | `devbox-pi-config` (default) | `settings.json`, `agent/extensions/`, `auth.json`, `sessions/`, plus `git/` and `npm-global/` (`NPM_CONFIG_PREFIX` points here) so `pi install npm:` / `git:` and `npm i -g` survive container recreate and image rebuild. |
|
||||||
| `/home/developer/.mempalace` | `devbox-palace` (uncomment to enable) | Shared palace — visible to both pi and opencode |
|
| `/home/developer/.mempalace` | `devbox-palace` (uncomment) | Shared palace — visible to pi and opencode |
|
||||||
|
|
||||||
The `pi` binary, pi-toolkit, and pi-extensions are baked into the image; `pi update` does **not** persist across `--rm` containers — image rebuild is the upgrade path.
|
Baked pi (`/usr/bin/pi`, `/opt/pi-*`) ships in the image; rebuild to upgrade. `npm i -g @mariozechner/pi-coding-agent` lands on the volume and wins via `PATH`.
|
||||||
|
|
||||||
Full build args, extension list, and toolkit detail: <https://gitea.jordbo.se/joakimp/opencode-devbox#pi-alternativecomplementary-harness>
|
Full build args, extension list, and toolkit detail: <https://gitea.jordbo.se/joakimp/opencode-devbox#pi-alternativecomplementary-harness>
|
||||||
""",
|
""",
|
||||||
|
|||||||
Reference in New Issue
Block a user