v1.7.0: bundle pi-atelier at a pinned tag; pin pi to an audited 0.84.1
Publish Docker Image / resolve-versions (push) Successful in 9s
Lint / actionlint (push) Successful in 15s
Lint / hadolint (push) Successful in 13s
Publish Docker Image / base-decide (push) Successful in 8s
Publish Docker Image / build-base (push) Successful in 41m22s
Publish Docker Image / smoke-studio (push) Successful in 5m16s
Publish Docker Image / smoke (push) Successful in 7m31s
Publish Docker Image / build-variant-studio (push) Successful in 18m31s
Publish Docker Image / build-variant (push) Successful in 27m18s
Publish Docker Image / promote-base-latest (push) Successful in 11s
Publish Docker Image / update-description (push) Successful in 12s

Two changes that belong together, because the first is what makes the second
dangerous to get wrong.

pi-atelier (TUI sidebar + status rail) is now vendored to /opt/pi-atelier at
PI_ATELIER_REF=v0.8.0 and registered by entrypoint-user.sh — the pi-fork /
pi-observational-memory / pi-studio pattern, deliberately NOT
`pi install npm:pi-atelier`, which writes into ~/.pi/npm-global on the config
volume where it shadows the image and pins nothing. Unlike its siblings it gets
no `npm install`: atelier declares zero runtime deps (peerDeps only, satisfied by
the baked pi) and has no build step, so pi loads its TypeScript straight from the
checkout via package.json `pi.extensions`.

pi is no longer resolved to npm `latest` at build time. The pin lives in
Dockerfile.variant and CI reads it from there, so a local `docker build` and a CI
release ship the same versions by construction. The pin is a CHECKPOINT, NOT A
FREEZE: bumping stays a one-line change; what stops is *unreviewed* adoption of
whatever shipped that morning, in the same build that then gets tagged and
published. CI fails when a pin is not concrete or not actually published on npm,
and warns — never adopts — when npm latest moves ahead, naming what to re-check.

Why this pairing needed care: pi-atelier 0.6.0/0.7.0 wrap pi's PRIVATE TUI
renderer, and under pi 0.84 that wrapper recurses — pi hangs at startup burning
CPU with no error. Upstream fixed the recursion in 0.7.1 and restored the
non-overlapping split in 0.7.2; 0.8.0 is additive on top. atelier's own
peerDependencies still say >=0.80.7, which does not express that floor, so
nothing in npm metadata could have warned us. The floor is therefore encoded as
an executable rule — pi >= 0.84 => pi-atelier >= 0.7.1 — asserted in both
smoke-test.sh (build time) and recreate-sanity-check.sh (after a real recreate),
verified against a 4x4 version matrix.

Existing volumes needed migration, not just vendoring: a hand-installed
`npm:pi-atelier` entry is counted as already-registered by the entrypoint guard,
so every existing volume would have kept its unpinned npm copy — and a 0.6.x copy
next to pi 0.84 is exactly the startup hang. The entrypoint now drops that one
exact string (settings.json.bak.atelier.<ts> backup, distinct prefix so it cannot
clobber the template merge's backup in the same second) and lets the pinned /opt
copy register. Tested against a real settings.json: only that entry removed,
other packages and all keys intact, idempotent, and unparseable JSON leaves the
file untouched. DEVBOX_ATELIER=0 opts out entirely — in the entrypoint rather
than via `pi uninstall`, because this component's failure mode is "pi will not
start", which cannot be repaired from inside pi.

0.84.1 was audited for this release, not merely adopted: theme/TUI changes are
additive, the session format is unchanged (CURRENT_SESSION_VERSION = 3 in both
0.83.0 and 0.84.1 with an identical migrateV1ToV2/migrateV2ToV3 ladder, so
existing transcripts are neither migrated nor at risk and pi-session-repair stays
valid), and the Node engine floor is unmoved at >=22.19.0. CI resolves the
atelier tag to its PEELED commit SHA — atelier uses annotated tags, so the
unpeeled ref is a tag object, not a commit; pi-studio's lightweight tags never
exposed that distinction.

Also: docs for overriding the read-only ~/.ssh/config from the container —
container-only keys in ~/.ssh-local, hardened authorized_keys, the fact that
`from=` must allow the HOST's addresses because container egress is NAT'd through
it, and the macOS-only-keyword trap (`UseKeychain` is fatal to Linux OpenSSH and
takes out dssh/pi --ssh while the host keeps working). Corrects two claims in
"Naming LAN peers": ssh-lan.conf is not ProxyJump-only, and first-time creation
does need one restart because the Include is emitted only when the file already
exists at start.
This commit is contained in:
2026-08-07 21:34:41 +02:00
parent 62a2a79b1c
commit 43cd6e22f2
9 changed files with 667 additions and 39 deletions
+142 -3
View File
@@ -11,13 +11,101 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
---
## Unreleased
## v1.7.0 — 2026-08-07
Staged, not yet tagged — more changes are queued for the next release before CI
is kicked off.
Minor release. Headline: **pi-atelier is now part of the image** — the TUI
sidebar/status rail every container previously had to hand-install — and **pi is
pinned to an audited version instead of tracking npm `latest`**.
*Why minor and not patch:* the policy above reserves patch for "pi version bumps,
smaller fixes" and minor for "new variants, significant base additions". Bundling
a new companion package into every image is the same shape as v1.1.0, which went
minor for bundling pi-studio; v1.4.0 likewise went minor for adding typst. This
release also adds a new build-arg pair, a new opt-out env var, and a settings
migration, so patch would understate it.
### Added
- **pi-atelier vendored at `/opt/pi-atelier`, pinned to `v0.8.0`** — the TUI
sidebar (ordered panels, split-pane, themes) is now part of the image instead
of something each user hand-installs. Vendored + registered at container start
by `entrypoint-user.sh`, the same pattern as pi-fork/pi-observational-memory/
pi-studio, and deliberately **not** `pi install npm:pi-atelier`: an npm
install writes into `~/.pi/npm-global` on the config volume, which shadows the
image and pins nothing — the footgun that once hid a missing `fork` tool for
six weeks. Unlike its siblings it gets **no `npm install`**: pi-atelier
declares zero runtime dependencies (only peerDeps, satisfied by the baked pi)
and has no build step, so pi loads its TypeScript straight from the checkout
(`pi.extensions``extensions/index.ts`).
- **A version FLOOR, encoded as an executable test.** pi-atelier 0.6.0/0.7.0
wrap pi's private TUI renderer in a way that recurses under pi 0.84: pi hangs
at startup with sustained CPU and no error message. Upstream fixed the
recursion in 0.7.1 and restored the non-overlapping split in 0.7.2
("avoiding the recursive render path that caused startup hangs and sustained
CPU usage"); 0.8.0 is additive on top of that. atelier's own
`peerDependencies` still say `>=0.80.7`, which does **not** express the floor,
so nothing in npm metadata could have warned us. `smoke-test.sh` and
`recreate-sanity-check.sh` now assert the pairing rule **pi ≥ 0.84 ⇒
pi-atelier ≥ 0.7.1** — verified against a 4×4 version matrix — so a bad
combination fails the build instead of publishing an image whose TUI never
starts. CI resolves the pinned tag to its **peeled commit SHA**; atelier uses
annotated tags, so the unpeeled ref is a tag object, not a commit (pi-studio's
lightweight tags never exposed that distinction).
- **`DEVBOX_ATELIER=0`** opts out: the entrypoint removes pi-atelier from pi's
`packages[]` instead of registering it. The switch lives in the entrypoint
rather than being "just run `pi uninstall`" because this component's failure
mode is *pi will not start*, which cannot be repaired from inside pi.
- **Migration for hand-installed copies.** A pre-existing `npm:pi-atelier` entry
is dropped from `packages[]` (with a `settings.json.bak.atelier.<ts>` backup)
so the pinned `/opt` copy takes over. This is not cosmetic: the registration
guard counts `npm:<name>` as already-registered, so without this step every
existing volume would have kept its unpinned npm copy — and a 0.6.x copy
alongside pi 0.84 is exactly the startup hang above. Only that one exact
string is removed; jq-parse failures or a missing file leave settings
untouched, and the backup prefix is distinct from the template merge's so two
rewrites in the same second cannot overwrite each other's backup.
### Changed
- **pi-toolkit's `pi-atelier.json` modernised to atelier's current schema**
(pi-toolkit `0e1369e`, cross-repo — it reaches the image through the pinned
`PI_TOOLKIT_REF` clone). The seeded config had been written against the pre-0.7
vocabulary: `segments``segmentLayout` with explicit per-segment visibility,
`ornament: "none"``{"id":"brand","visible":false}`, `showExtensionStatuses`
`{"id":"statuses","visible":true}`, plus the sidebar toggles that did not
exist when it was written (`showSidebarAgent`, `showSidebarTodos`, and
`showSidebarOnStartup`, new in atelier 0.8.0). Upstream still reads the old
keys, but only as non-authoritative legacy inputs, so the file worked while
silently missing every sidebar control added since. Verified by loading the old
and new file through pi-atelier 0.8.0's own `loadConfig()`: zero warnings from
each and an identical *effective* config, so it is a pure schema
modernisation — every deliberate choice (compact density, 60/85 context
thresholds, notifications off) is preserved. `sidebarPanelLayout` is left unset
on purpose so the panel set tracks upstream as atelier adds panels.
- **pi is now PINNED, not `latest`: `PI_VERSION=0.84.1`** (`Dockerfile.variant`).
CI's `resolve-versions` job used to resolve `@earendil-works/pi-coding-agent`
to npm `latest`, which meant every release silently adopted whatever pi had
shipped that morning — unaudited — in the same build that then got tagged and
published. A pi minor can move the private TUI/renderer internals pi-atelier
wraps (0.84 vs atelier 0.6.0: startup hang) or the session `.jsonl` format
`pi-session-repair` parses. **The pin is a checkpoint, not a freeze**
bumping stays a routine one-line change; what stops is *unreviewed* adoption.
0.84.1 was audited for this release: theme/TUI additions are additive, the
session format is unchanged (`CURRENT_SESSION_VERSION = 3` in both 0.83.0 and
0.84.1, identical `migrateV1ToV2`/`migrateV2ToV3` ladder, so existing
transcripts are neither migrated nor at risk), and the Node engine floor is
unmoved at `>=22.19.0`.
- The pins live in the **Dockerfiles** and CI reads them from there (a
`checkout` was added to `resolve-versions`), so a local `docker build` and a
CI release ship the same versions by construction instead of by convention.
- CI **fails** the build when the pin is not a concrete version, and when the
pinned version is not actually published on npm — catching a typo, an
unpublished version, or one yanked after we audited it, at resolve time with
a clear message rather than as an `npm install` error mid-build.
- CI **warns** (`::warning::`, never adopts) when npm `latest` is ahead of the
pin, naming the newer version and what to re-check. That warning is the
prompt to audit and bump — not something to silence.
- **mempalace pin `3.5.0``3.6.0`** (`Dockerfile.base` `MEMPALACE_VERSION`),
in lockstep with opencode-devbox v2.9.0 as the pin's own comment requires.
3.6.0 (2026-07-17) is PyPI latest and is additive/reliability only — secure
@@ -35,6 +123,57 @@ is kicked off.
### Documentation
- **New README section: "Using pi-atelier (TUI sidebar)"** — what the status rail
and sidebar give you, the `alt+a` / `/atelier` entry points, session-scoped
`/atelier sidebar on|off` versus persistent Save, and `DEVBOX_ATELIER=0` to opt
out. Plus the config story: why `~/.pi/agent/pi-atelier.json` is **copied, not
symlinked** (atelier saves via write-temp-then-`rename(2)`, and `rename`
replaces a symlink rather than following it, so a symlink would silently detach
on the first save), why `install.sh` therefore only seeds it when absent, which
keys are current versus legacy-compatibility, and the 92-column auto-hide /
64-column main-pane floor so a narrow terminal degrades gracefully.
- **Documents how to authenticate the container to a LAN peer with its own key**
(README: *Giving the container its own key for a peer*) — the gap the existing
*Naming LAN peers* section left open. That section explained `ProxyJump`
*routing* while asserting `HostName`/`User`/`IdentityFile` are "inherited from
the matching block in your real `~/.ssh/config`", which is precisely what fails
in a container: host keys are normally passphrase-protected and unlocked by the
macOS Keychain or an `ssh-agent`, neither of which exists here, so the key can
never be decrypted — `Permission denied (publickey)` while the identical
`ssh peer` works fine in a host terminal — and `~/.ssh` is read-only, so no
usable key can be added there either. The new walkthrough (throwaway example
keys) covers a passphraseless keypair in the `devbox-ssh-local` volume so it
survives `--force-recreate`; a hardened `authorized_keys` line (`restrict`,
`from=`, optional `permitopen`); the non-obvious detail that `from=` must allow
the **host's** addresses, plural, because container egress is NAT'd through the
host and a roaming laptop presents a different one per network (a `from=`
mismatch is indistinguishable from a wrong key in the error message); the
`IdentityFile` override in the host-owned `ssh-lan.conf`; and verification with
`-o ControlPath=none` so a warm ControlMaster cannot fake a pass. States
explicitly that no private key is in the published image — the volume is
created at runtime on the operator's own machine.
- **Corrects two claims in *Naming LAN peers***: (1) `ssh-lan.conf` is not
`ProxyJump`-only — it is `Include`d before `~/.ssh/config`, so by
first-value-wins *any* option set there wins, which is what makes the
`IdentityFile` override above possible; (2) "newly added peers work
immediately, no container or session restart needed" holds only for *edits* to
an existing file. Creating it for the first time **does** need one restart,
because `setup-lan-access.sh` emits the
`Include ~/.config/devbox-shell/ssh-lan.conf` line only
`if [ -r "$SSH_LAN_CONF" ]` at container start — until then ssh never reads it,
which presents exactly as "my override is being ignored".
- **Adds *macOS-only keywords in a shared `~/.ssh/config`***. The same file is
read by macOS ssh and by the container's Linux OpenSSH, where macOS-only
keywords are fatal rather than ignored: one `UseKeychain yes` in a `Host *`
block yields `Bad configuration option: usekeychain` /
`terminating, 1 bad configuration options` and takes down `dssh`/`dscp`,
`pi --ssh`, `scp` and every helper that shells out to ssh — while the host
keeps working, so it presents as a container regression rather than a host
config error. Fix is `IgnoreUnknown UseKeychain` *ahead of* the keyword (macOS
still honours it, Linux skips it), plus keeping such a `Host *` block below
OrbStack's `Include ~/.orbstack/ssh/config`, which documents in its own comment
that it must come first.
- Documents **per-variant image description labels** (committed and pushed after
the v1.6.4 tag without a changelog entry). Both published variants used to
inherit `Dockerfile.base`'s `description="pi-devbox — base image