fix(docker): make the dsh profile install survive pnpm's build-script gate

Follow-up to #350, which fixed the actual blocker (issue #352): `dsh plugin` is
a thin forwarder that `spawnSync`s a literal `pnpm` with no npm fallback, so an
image without pnpm dies at exit 127 and takes the whole build with it.

That PR also pinned an allowlist of the two packages whose lifecycle scripts
pnpm blocked at the time. Replace it with a policy that cannot go stale: pnpm,
unlike npm, refuses dependency build scripts by default and FAILS the install
over it (`ERR_PNPM_IGNORED_BUILDS`, exit 1, measured on pnpm 11.24), and the
names to allow move between rebuilds because `@deepseek-harness-tui/dsh-tui` is
resolved by dist-tag, not pinned: 0.9.3 pulled `@google/genai` (whose script is
a literal `preinstall: no-op`), 0.10.0-beta.x does not. An allowlist of two
names would have let the next tree break the build the same way. Allowing them
wholesale is also the exposure this image already accepts three layers up,
where `npm install -g` runs the install scripts of every transitive dep of the
five CLIs above with no gate at all.

Also correct a comment in the `/api/deepseek/install-profile` route that
asserted the opposite of what #352 proved ("dsh bundles its own package
manager, so no system pnpm is required"). The route's behavior is already
right: dsh's own "pnpm not found on PATH" stderr reaches the caller as the
OPERATION_FAILED detail, so the UI's "add a terminal profile" button names the
fix. Documented the prerequisite in docs/deepseek-integration.md, and taught
the docker-cases image smoke test about `dsh`/`omp` plus the profile check that
`dsh --version` does NOT cover.
This commit is contained in:
Codeman maintainer
2026-08-31 22:26:09 +02:00
parent 7762809202
commit d5b5f8f618
4 changed files with 50 additions and 12 deletions
+21 -8
View File
@@ -75,11 +75,17 @@ RUN curl -fsSL https://x.ai/cli/install.sh | bash \
# profile itself is installed further down, into the `agent` HOME, because # profile itself is installed further down, into the `agent` HOME, because
# Codeman deliberately does NOT seed `profiles/` from the host: it is a # Codeman deliberately does NOT seed `profiles/` from the host: it is a
# per-profile node_modules tree, host-arch-specific and far too large to copy on # per-profile node_modules tree, host-arch-specific and far too large to copy on
# every container start. The harness delegates profile dependency management to # every container start.
# pnpm, so pnpm is a build dependency rather than optional runtime tooling. # ⚠️ `pnpm` is a HARD dependency of `dsh plugin`, not optional tooling: the
# subcommand is a thin forwarder that `spawnSync`s a literal `pnpm` with no
# fallback to npm, so on an image without it the profile install below dies
# with `dsh: pnpm not found on PATH` / exit 127 and takes the whole build with
# it (issue #352). It stays on PATH at runtime too, so a container user can run
# `dsh plugin add` themselves.
RUN npm install -g @deepseek-ai/dsh pnpm \ RUN npm install -g @deepseek-ai/dsh pnpm \
&& npm cache clean --force \ && npm cache clean --force \
&& dsh --version && dsh --version \
&& pnpm --version
# OMP (Oh My Pi) is NOT on npm: a standalone binary via omp.sh's installer, which # OMP (Oh My Pi) is NOT on npm: a standalone binary via omp.sh's installer, which
# targets $HOME/.local/bin with no --dir override (verified 2026-08-27 — the # targets $HOME/.local/bin with no --dir override (verified 2026-08-27 — the
@@ -123,6 +129,16 @@ ENV HOME=/home/agent
# writable by the arbitrary uid the container actually runs as, and a profile # writable by the arbitrary uid the container actually runs as, and a profile
# installed after it would miss that fixup. DSH_HOME points the launcher at the # installed after it would miss that fixup. DSH_HOME points the launcher at the
# agent's dir while this still runs as root. # agent's dir while this still runs as root.
# ⚠️ `dangerouslyAllowAllBuilds` is what keeps that profile install from becoming
# the next #352. pnpm (unlike npm) blocks dependency lifecycle scripts by default
# and FAILS the install over it — `ERR_PNPM_IGNORED_BUILDS`, exit 1, measured on
# pnpm 11.24 — so any package in the tui's tree that ships one stops the build
# dead. An allowlist of the offenders rots: `@deepseek-harness-tui/dsh-tui` is
# resolved by dist-tag, not pinned, and 0.9.3 pulled `@google/genai` (a
# `preinstall: no-op`) where 0.10.0-beta.x does not, so the names to allow move
# under us between rebuilds. Allowing them wholesale is also the SAME exposure
# this image already accepts three layers up: `npm install -g` runs the install
# scripts of every transitive dep of the five CLIs above it, with no gate at all.
# `.omp/agent` is pre-created for the same reason `.codex` is: it is a MIXED # `.omp/agent` is pre-created for the same reason `.codex` is: it is a MIXED
# store (per-file config seeds PLUS a shared `sessions/` RW bind mount for # store (per-file config seeds PLUS a shared `sessions/` RW bind mount for
# Codeman's own host-side history/resume reads), and neither kind of artifact # Codeman's own host-side history/resume reads), and neither kind of artifact
@@ -132,11 +148,8 @@ RUN useradd -g 0 -m -d /home/agent -s /bin/bash agent \
/home/agent/.claude/projects /home/agent/.codex/sessions /home/agent/.pi/agent /home/agent/.grok \ /home/agent/.claude/projects /home/agent/.codex/sessions /home/agent/.pi/agent /home/agent/.grok \
/home/agent/.dsh /home/agent/.omp/agent \ /home/agent/.dsh /home/agent/.omp/agent \
&& DSH_HOME=/home/agent/.dsh HOME=/home/agent \ && DSH_HOME=/home/agent/.dsh HOME=/home/agent \
dsh plugin --profile dsh-tui install --ignore-scripts \ dsh plugin --profile dsh-tui add --config.dangerouslyAllowAllBuilds=true \
&& printf '%s\n' '' 'allowBuilds:' ' "@google/genai": true' ' protobufjs: true' \ @deepseek-harness-tui/dsh-tui \
>> /home/agent/.dsh/profiles/dsh-tui/pnpm-workspace.yaml \
&& DSH_HOME=/home/agent/.dsh HOME=/home/agent \
dsh plugin --profile dsh-tui add @deepseek-harness-tui/dsh-tui \
&& test -f /home/agent/.dsh/profiles/dsh-tui/package.json \ && test -f /home/agent/.dsh/profiles/dsh-tui/package.json \
&& chgrp -R 0 /home/agent \ && chgrp -R 0 /home/agent \
&& chmod -R g=u /home/agent && chmod -R g=u /home/agent
+8
View File
@@ -47,6 +47,14 @@ By hand, or to pick a different front door:
dsh plugin --profile dsh-tui add @deepseek-harness-tui/dsh-tui dsh plugin --profile dsh-tui add @deepseek-harness-tui/dsh-tui
``` ```
⚠️ **`pnpm` has to be on PATH for either route.** `dsh plugin` is a thin forwarder
that spawns a literal `pnpm` with no npm fallback, so without one it exits 127 with
`dsh: pnpm not found on PATH` — both by hand and behind the UI button, which
surfaces that same line as the install error. `npm install -g pnpm` (or
`corepack enable pnpm`) is the fix. This is what broke the Docker agent image in
[#352](https://github.com/Ark0N/Codeman/issues/352); the image now installs pnpm
alongside `dsh`.
Codeman's default is `@deepseek-harness-tui/dsh-tui` because it is by a wide Codeman's default is `@deepseek-harness-tui/dsh-tui` because it is by a wide
margin the most used community TUI, it is MIT, and it implements the status margin the most used community TUI, it is MIT, and it implements the status
contract described in §3. It is a **default, not a requirement**: any profile contract described in §3. It is a **default, not a requirement**: any profile
+14 -2
View File
@@ -2,7 +2,7 @@
Run a case inside an **isolated Docker container** instead of directly on the host. Any number of Codeman sessions can share one container (it is scoped to the case, not the session), so a whole project lives in a sandbox with its own network, resource caps, and filesystem, and you can **export the container to move it to another machine**. Run a case inside an **isolated Docker container** instead of directly on the host. Any number of Codeman sessions can share one container (it is scoped to the case, not the session), so a whole project lives in a sandbox with its own network, resource caps, and filesystem, and you can **export the container to move it to another machine**.
Docker mode is a **location overlay on cases**, the direct analog of [remote SSH cases](./remote-hosts.md): where a remote case runs a local tmux pane doing `ssh host` into a durable remote tmux server, a docker case runs a local tmux pane doing `docker exec -it` into a durable **in-container** tmux server. It is not a separate `SessionMode`, so `claude` / `shell` / `opencode` / `codex` / `gemini` / `antigravity` / `pi` / `grok` all work inside the container. Docker mode is a **location overlay on cases**, the direct analog of [remote SSH cases](./remote-hosts.md): where a remote case runs a local tmux pane doing `ssh host` into a durable remote tmux server, a docker case runs a local tmux pane doing `docker exec -it` into a durable **in-container** tmux server. It is not a separate `SessionMode`, so `claude` / `shell` / `opencode` / `codex` / `gemini` / `antigravity` / `pi` / `grok` / `deepseek` / `omp` all work inside the container.
## One-time setup: build the base image ## One-time setup: build the base image
@@ -25,9 +25,21 @@ A zero exit code only proves the layers ran, not that the toolchain works. Verif
```bash ```bash
docker run --rm codeman/agent:base bash -lc \ docker run --rm codeman/agent:base bash -lc \
'for c in claude codex gemini opencode agy pi grok; do printf "%-9s " $c; $c --version 2>&1 | head -1; done' 'for c in claude codex gemini opencode agy pi grok dsh omp; do printf "%-9s " $c; $c --version 2>&1 | head -1; done'
``` ```
⚠️ `dsh --version` is the one line above that answers a different question than the
others: `dsh` is a profile launcher, so a working binary says nothing about whether
the image can actually run a DeepSeek session. Check the profile the Dockerfile
installs into the agent's HOME as well, or a `mode: 'deepseek'` case starts a pane
that dies on arrival:
```bash
docker run --rm codeman/agent:base ls ~/.dsh/profiles/dsh-tui/package.json
```
Building that profile is also why `pnpm` is in the image: `dsh plugin` forwards straight to a literal `pnpm` and exits 127 without it (issue #352), and pnpm — unlike npm — blocks dependency lifecycle scripts by default and fails the install over it, so the profile step passes `--config.dangerouslyAllowAllBuilds=true`.
Antigravity (`agy`) and Grok (`grok`) are the two CLIs not installed from npm (Google and xAI ship standalone binaries), so each has its own Dockerfile step, adding roughly 190MB and 160MB respectively. Pi also gets its own step, because upstream documents installing it with `--ignore-scripts` and that flag must not silently change how the other npm CLIs install. Antigravity (`agy`) and Grok (`grok`) are the two CLIs not installed from npm (Google and xAI ship standalone binaries), so each has its own Dockerfile step, adding roughly 190MB and 160MB respectively. Pi also gets its own step, because upstream documents installing it with `--ignore-scripts` and that flag must not silently change how the other npm CLIs install.
Pi's credentials are seeded per-FILE rather than as a whole directory (`auth.json`, `settings.json`, `trust.json`, `models.json`, `models-store.json` out of `~/.pi/agent`), because that directory also holds `sessions/`, `extensions/`, `skills/` and the installed package trees — gigabytes on an active host. Consequence: in-container pi sessions are invisible host-side, so `pi -c` inside a Docker case only sees that container's own history. See [`pi-integration.md`](./pi-integration.md). Grok is seeded per-file for the same reason (`auth.json`, `config.toml`, `pager.toml` out of `~/.grok`, which also holds `sessions/`, `memory/` and the ~160MB binary under `downloads/`), with the same consequence for `grok -c`. See [`grok-integration.md`](./grok-integration.md). OMP is the one CLI in this family where `sessions/` is the EXCEPTION rather than the rule: `~/.omp/agent/{config.yml,mcp.json,models.yml,settings.yml}` are seeded per-file (the dir also holds SQLite caches and `terminal-sessions/`), but `~/.omp/agent/sessions/` is shared RW like codex's, not seeded, because Codeman reads it host-side for history recovery and `--resume` pinning. See [`omp-integration.md`](./omp-integration.md). Pi's credentials are seeded per-FILE rather than as a whole directory (`auth.json`, `settings.json`, `trust.json`, `models.json`, `models-store.json` out of `~/.pi/agent`), because that directory also holds `sessions/`, `extensions/`, `skills/` and the installed package trees — gigabytes on an active host. Consequence: in-container pi sessions are invisible host-side, so `pi -c` inside a Docker case only sees that container's own history. See [`pi-integration.md`](./pi-integration.md). Grok is seeded per-file for the same reason (`auth.json`, `config.toml`, `pager.toml` out of `~/.grok`, which also holds `sessions/`, `memory/` and the ~160MB binary under `downloads/`), with the same consequence for `grok -c`. See [`grok-integration.md`](./grok-integration.md). OMP is the one CLI in this family where `sessions/` is the EXCEPTION rather than the rule: `~/.omp/agent/{config.yml,mcp.json,models.yml,settings.yml}` are seeded per-file (the dir also holds SQLite caches and `terminal-sessions/`), but `~/.omp/agent/sessions/` is shared RW like codex's, not seeded, because Codeman reads it host-side for history recovery and `--resume` pinning. See [`omp-integration.md`](./omp-integration.md).
+7 -2
View File
@@ -614,8 +614,13 @@ export function registerSystemRoutes(
// same negative-pid signal as runGit() in git-clone.ts, which is the // same negative-pid signal as runGit() in git-clone.ts, which is the
// synchronous-spawn precedent this endpoint is modelled on. // synchronous-spawn precedent this endpoint is modelled on.
detached: true, detached: true,
// dsh bundles its own package manager, so no system pnpm is required — // Inherit the environment: this needs a HOME to resolve $DSH_HOME
// but it still needs a HOME to resolve $DSH_HOME against. // against, and a PATH carrying `pnpm`. ⚠️ `dsh plugin` does NOT bundle a
// package manager — it `spawnSync`s a literal `pnpm` with no npm
// fallback, so on a host without one this exits 127 and dsh's own
// stderr ("pnpm not found on PATH") is what reaches the caller through
// the OPERATION_FAILED detail below. That is the same missing
// dependency that broke the docker agent image in issue #352.
env: process.env, env: process.env,
}); });
} catch (err) { } catch (err) {