Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ea6605bfae | |||
| a6abe17edc | |||
| 5becbba6c8 | |||
| 77929b73df | |||
| 10a8030620 | |||
| a8f5b35d2a | |||
| 1e4c238a83 | |||
| bf265f6d54 |
@@ -397,7 +397,7 @@ jobs:
|
||||
echo "fork_ref=${FORK_REF}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# pi-observational-memory → the PINNED SHA in Dockerfile.variant.
|
||||
# Until v1.11.1 this resolved `master` and passed it as --build-arg,
|
||||
# Until v1.11.2 this resolved `master` and passed it as --build-arg,
|
||||
# which OVERRIDES the ARG default: the pin bound only local builds,
|
||||
# and CI baked it only while master == pin (v1.9.5, v1.10.2, v1.11.0).
|
||||
# The pin is a master SHA, not a tag, on purpose: obsmem tags rarely
|
||||
|
||||
@@ -96,6 +96,12 @@ jobs:
|
||||
# on every push, because the smoke test only runs at release time.
|
||||
run: bash scripts/test-link-pi-models.sh
|
||||
|
||||
- name: link-pi-extensions.sh contract (two-sided, no docker needed)
|
||||
# Same reasoning as the models step above: the entrypoint links top-level
|
||||
# *.ts from the mounted fleet directory into ~/.pi/agent/extensions, and
|
||||
# must never replace a user's own extension of the same name.
|
||||
run: bash scripts/test-link-pi-extensions.sh
|
||||
|
||||
- name: Dockerfile.variant still opens with its BuildKit check directive
|
||||
# See scripts/check-dockerfile-directives.sh: without that first line the
|
||||
# smoke jobs fail with `invalid format delimiter 'ghadelimiter_...'` and
|
||||
|
||||
@@ -354,14 +354,24 @@ deprecated artifacts (to be removed in opencode-devbox v2.0.0).
|
||||
|
||||
## What we DON'T install (and why)
|
||||
|
||||
- **No texlive** (~600 MB–1 GB). PDF export from pandoc / pi-studio works
|
||||
out of the box via **`typst`** (~30 MB static binary), which the base ships
|
||||
as the pandoc PDF engine (`pandoc --pdf-engine=typst`) — small enough to live
|
||||
in base rather than a dedicated `:latest-studio-tex` variant. We don't bake in
|
||||
a full TeX Live: it's heavy and typst covers the common Markdown→PDF case.
|
||||
Users needing LaTeX-exact output can install the higher-fidelity fallback on
|
||||
demand: `sudo apt-get install texlive-xetex texlive-latex-recommended` (then
|
||||
`pandoc --pdf-engine=xelatex`).
|
||||
- **No texlive** (~0.5–1 GB). The base ships **`typst`** (~30 MB static
|
||||
binary) as a PDF engine for pandoc, so
|
||||
`pandoc doc.md --pdf-engine=typst -o doc.pdf` works out of the box. That
|
||||
covers the common Markdown→PDF case, small enough to live in base rather
|
||||
than in a dedicated `:latest-studio-tex` variant. Callers must ask for
|
||||
typst: pandoc's own default engine is `pdflatex`, which is not installed.
|
||||
**pi-studio's PDF export (`/studio-pdf`, `studio_export_pdf`) does not use
|
||||
typst.** It runs pandoc with `$PANDOC_PDF_ENGINE`, defaulting to `xelatex`,
|
||||
so in this image it fails with `xelatex not found`. Do not "fix" that with
|
||||
`ENV PANDOC_PDF_ENGINE=typst`. pi-studio adds its own LaTeX header to the
|
||||
document, and typst prints that header as page text while pandoc exits 0.
|
||||
That ENV only becomes a fix once pi-studio stops sending its LaTeX header
|
||||
to non-LaTeX engines. Until then, users who need Studio's PDF export, or
|
||||
LaTeX output, install TeX Live on demand with the recipe in README
|
||||
"PDF export from Studio needs LaTeX" (37 packages, ~490 MB with
|
||||
`--no-install-recommends`). A plain `sudo apt-get install texlive-xetex`
|
||||
also works, but its recommends make it 202 packages and ~1 GB, including
|
||||
systemd and a Java runtime.
|
||||
- **pi-studio** ships in the `:latest-studio` variant (since v1.1.0),
|
||||
vendored to `/opt/pi-studio` and registered at container start via
|
||||
`pi install /opt/pi-studio` (see Dockerfile.variant `INSTALL_STUDIO`).
|
||||
|
||||
+292
-11
@@ -11,15 +11,246 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
|
||||
|
||||
---
|
||||
|
||||
## v1.11.1 — 2026-10-14
|
||||
## v1.12.0 — 2026-10-18
|
||||
|
||||
Patch release. Headlines: **pi 1.2.0**, **`pi -p` returns in seconds** on a
|
||||
Minor release. Headlines: **pi 1.3.0**, **mempalace 3.11.0**, **pi-atelier
|
||||
v0.16.0**, and **fleet pi extensions linked in at every container start**
|
||||
(*Added*, below). Every component is at its newest upstream release; each
|
||||
pinned one was audited for breaking changes against this image before tagging
|
||||
(*Changed*, below), and none was found. Unchanged upstream, re-checked
|
||||
2026-10-18: pi-observational-memory `022da5e` (= `master`), pi-studio `v0.10.1`
|
||||
(`76db01f`, newest tag), pi-fork `e69725c`, pi-toolkit `3760d90`,
|
||||
mempalace-toolkit `d5a03f4`. The skillset is still at `1e02b44` and its
|
||||
mempalace `SKILL.md` is byte-identical to the recorded `96b840e`, so the
|
||||
snapshot is not refreshed (`vendor-mempalace-skill.sh --check`: OK). The base
|
||||
image is rebuilt (rootfs, `Dockerfile.base` and the mempalace pin all changed),
|
||||
so the base tools pinned to `latest` re-resolve too: uv `0.13.0` → `0.13.1`,
|
||||
gitea-mcp `v1.8.1` → `v1.9.0` (additive: mTLS, extra headers,
|
||||
`create_organization`), Node `v24.21.0` → `v24.22.0`; nvim, fzf, gitleaks,
|
||||
bat, eza, zoxide, git-lfs, gosu, tealdeer, typst, yq, agent-browser and
|
||||
playwright unchanged.
|
||||
|
||||
### Components that move with this release
|
||||
|
||||
| Component | Baked in v1.11.2 | v1.12.0 | Why |
|
||||
|---|---|---|---|
|
||||
| **pi** | `1.2.0` | **`1.3.0`** | audited, see *Changed* below |
|
||||
| **mempalace** | `3.10.0` | **`3.11.0`** | audited, see *Changed* below |
|
||||
| **pi-atelier** | `v0.15.0` (`3befe02`) | **`v0.16.0`** (`3b42825`) | audited, see *Changed* below |
|
||||
| pi-extensions | `750b2a8` | **`4c1d28d`** (or later: CI resolves `main` at build time) | the `confirm-destructive` gate stops prompting on harmless redirects and catches `tee`/`dd of=` (below); picks up the AWS SSO keeper's 24-hour times too (below) |
|
||||
|
||||
### Upgrade notes
|
||||
|
||||
- **Local palaces re-mine their conversations once.** mempalace 3.11.0's
|
||||
exchange chunker no longer discards text, so the first conversation mine
|
||||
after upgrading re-chunks and re-embeds each conversation file it covers
|
||||
(drawers of transcripts no longer on disk are kept). This only applies to a
|
||||
devbox with its own palace: with `MEMPALACE_REMOTE_URL` set, the hub does the
|
||||
mining, and nothing changes until the hub itself is upgraded.
|
||||
- **The central palace (synlig) is still on 3.10.0.** That is the safe
|
||||
direction (newer client), and the bridge's tools come from the hub, so
|
||||
shared-palace devices see no change. When synlig is upgraded, its first
|
||||
conversation mine re-mines every inbox transcript still on disk (upstream
|
||||
estimates 40 minutes to 3 hours per million drawers): back it up first and
|
||||
run it at a quiet time.
|
||||
|
||||
### Added
|
||||
|
||||
- **Fleet pi extensions are linked in at every container start**
|
||||
(`rootfs/usr/local/lib/pi-devbox/link-pi-extensions.sh`, called from
|
||||
`entrypoint-user.sh` next to `link-pi-models.sh`). pi auto-loads every `.ts` in
|
||||
`~/.pi/agent/extensions/`, which lives in the `devbox-pi-config` volume: a baked
|
||||
extension would only seed a fresh volume once, and a hand-copied one is a
|
||||
per-device step that drifts — the two problems `link-pi-models.sh` was added
|
||||
for on 2026-10-12, one level down. The script links each **top-level** `*.ts`
|
||||
from the mounted fleet directory (default `/opt/fleet-pi`, the same directory
|
||||
that already carries `models.json`; `PI_EXTENSIONS_SRC` overrides, `=off`
|
||||
disables) and is never destructive: a real file of that name in
|
||||
`~/.pi/agent/extensions/` is left alone, so a user's own copy of an extension
|
||||
always wins over the fleet one. A symlink rather than a single-file bind mount
|
||||
because `git pull` replaces a file by rename (measured on Docker Desktop,
|
||||
2026-10-12), which is the documented reason `models.json` is shared this way.
|
||||
Nested `*.ts` are deliberately NOT linked — the directory is fleet pi config,
|
||||
not an extension tree. `scripts/test-link-pi-extensions.sh` covers the contract
|
||||
with 16 two-sided cases and runs in `lint.yml`; `smoke-test.sh` and
|
||||
`recreate-sanity-check.sh` gained the matching in-image and on-device checks.
|
||||
Motivated by the ERDC System One classifier extension: four hosts mount
|
||||
`common/pi` from `docker-compose-repo`, and three of them are not reachable
|
||||
from a devbox to create a symlink by hand.
|
||||
|
||||
### Changed
|
||||
|
||||
- **pi `1.2.0` → `1.3.0`.** Audited 2026-10-18 on tor-ms22 from an isolated npm
|
||||
prefix, against the 1.2.0 tarballs unpacked side by side:
|
||||
- 1.3.0 has **no** breaking-changes section. Its behaviour changes don't
|
||||
reach this image: `--model` and `enabledModels` now resolve a bare model
|
||||
id across providers by which one has credentials (every model here is
|
||||
written `amazon-bedrock/…`, and pi-fork's profiles name a provider);
|
||||
resumed sessions keep their active tools; a model switch drops the old
|
||||
model's thinking; `kimi-coding` base-URL overrides need `/v1` (not used).
|
||||
- pi-agent-core and pi-tui are byte-identical between 1.2.0 and 1.3.0
|
||||
(24 and 184 files, same tree hashes), and so is pi's
|
||||
`dist/core/extensions`. The obsmem turn cap lives in the first and
|
||||
atelier's painting in the second.
|
||||
- The only export removed from any type declaration is `parseModelPattern`
|
||||
(never exported from the package index); pi-ai dropped its
|
||||
`assistant-message-frame` re-export. Neither has a consumer. All 54 names
|
||||
that pi-toolkit, pi-extensions, pi-fork, obsmem, pi-studio, pi-atelier,
|
||||
mempalace-toolkit and the fleet extensions in `/opt/fleet-pi` import at
|
||||
runtime resolve in 1.3.0; an injected fake name is reported missing.
|
||||
- With a copy of tor-ms22's real `~/.pi/agent`: `pi -p` exits 0 in 10 s
|
||||
(1.2.0: 9 s) with nothing on stderr. The TUI loads every extension with no
|
||||
extension issues; the mempalace bridge registers its 45 tools;
|
||||
`/om:status`, `/om:view` and the `/studio` commands are offered; and the
|
||||
atelier sidebar paints ACTIVITY/READY/TOOLS in fullscreen and regular mode
|
||||
(with `-ne`: no sidebar). pi shows its 1.3.0 "What's new" once and records
|
||||
`lastChangelogVersion`; nothing else in `settings.json` changes.
|
||||
- **mempalace `3.10.0` → `3.11.0`.** Core requirements unchanged. Measured with
|
||||
both wheels in throwaway venvs and a throwaway palace:
|
||||
- `mempalace init --yes` (the entrypoint's first-run call) still writes
|
||||
`config.json`, rc 0.
|
||||
- A palace built by 3.10.0 opens under 3.11.0, searches, and serves MCP with
|
||||
no embedder warning. Its one conversation file was re-mined once (see
|
||||
*Upgrade notes*) and skipped on the next mine.
|
||||
- MCP over stdio lists 47 tools instead of 45. None is removed or renamed;
|
||||
`mempalace_get_drawers` and `mempalace_delete_drawers` are new, and the 14
|
||||
read-only tools now carry `readOnlyHint`.
|
||||
- The removed `EntityRegistry.research()` / `confirm_research()` have no
|
||||
caller in pi-devbox, mempalace-toolkit, skillset or myconfigs.
|
||||
- **pi-atelier `v0.15.0` → `v0.16.0`.** Additive: a contributed sidebar panel
|
||||
can send label/value rows with an optional progress bar (`keyValues`; plain
|
||||
`rows` remain the fallback), two new roles (`success`, `caution`), and the
|
||||
context meter no longer draws a hard-coded dark background, so it works on
|
||||
light terminals. The panel protocol is unchanged (channel
|
||||
`pi-atelier:sidebar-panels`, version 1, 24 rows of 160 chars), as is the pi
|
||||
floor (`>=0.84.0`). On pi 1.3.0 it paints in both renderers, and the fleet's
|
||||
`fleet:todos` panel (`fleet-todos-panel.ts`, plain rows) showed a todo
|
||||
written in a real turn, as on v0.15.0. The v0.15.0 run still drew the old
|
||||
meter background and the v0.16.0 run did not, so each ran the version it
|
||||
claims.
|
||||
- **DOCKER_HUB.md no longer says "29 `mempalace_*` tools".** The palace server
|
||||
decides the set; it now says so, with the measured counts.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`confirm-destructive` asked for approval on nearly every `2>/dev/null`**
|
||||
(pi-extensions `4c1d28d`, two commits on top of the `0d8e417` named below).
|
||||
Its "write to device" pattern was a bare `>\s*/dev/`, so redirecting a
|
||||
standard stream into the null device looked like a write to a block device —
|
||||
the extension's own comment cites 20 of 70 bash calls in one 2026-10-16
|
||||
session, and a gate that prompts that often trains the user to approve without
|
||||
reading. `/dev/null`, `stdout`, `stderr`, `tty`, `/dev/fd/*` and `/dev/shm` are
|
||||
now exempt, matched as whole names so `/dev/nullfoo` still prompts and a
|
||||
`/dev/shm` path containing `..` is not exempt. The same gate got harder to
|
||||
slip past: `tee` and `dd of=` (which writes from stdin and has no `if=`) are
|
||||
now write forms, and quoted device paths are recognised. Its suite covers the
|
||||
new cases — `node --test test/confirm-destructive.test.mjs` in
|
||||
pi-extensions `4c1d28d`: 2 tests, 2 pass, 0 fail (measured 2026-10-16).
|
||||
|
||||
- **The AWS SSO keeper showed 12-hour times under the image's default
|
||||
locale** (pi-extensions `0d8e417`). The status line (`AWS ✓ since …` /
|
||||
`until …`), `/aws-sso status`, the "session ended at" login notice and the
|
||||
"last login" line used `toLocaleTimeString()` / `toLocaleString()`. Those
|
||||
follow the locale, and `Dockerfile.base` sets `LC_ALL=en_US.UTF-8`, so the
|
||||
TUI read `AWS ✓ since 01:25 PM`. Times are now always `HH:MM`, and "last
|
||||
login" is `YYYY-MM-DD HH:MM:SS`, the forms `docs/aws-sso.md` already showed.
|
||||
A device that overrides the locale in `.env` (the `sv_SE.UTF-8` example in
|
||||
`.env.example`) already saw 24-hour times and sees no change. Measured in a
|
||||
v1.11.2 container: Node formats 13:25 as `"01:25 PM"` under `en_US.UTF-8`
|
||||
and `C.UTF-8` and as `"13:25"` under `sv_SE.UTF-8`. The upstream test suite
|
||||
at `0d8e417` passes 18/18 under `en_US.UTF-8`, including the new
|
||||
"hhmm/fmtStamp are 24-hour, locale-independent" case.
|
||||
|
||||
- **README "Using pi-studio" said pi-studio has no bind flag.** It has had
|
||||
`/studio --listen-all` (binds `0.0.0.0`) since pi-studio 0.9.60, so the
|
||||
section now lists it as path **C**, next to host networking (A) and the
|
||||
`studio-expose` bridge (B). It also warns not to combine B and C on one
|
||||
port, because whichever starts second fails. With `STUDIO_EXPOSE=1`,
|
||||
`/studio --listen-all --port 8765` fails with
|
||||
`listen EADDRINUSE: address already in use 0.0.0.0:8765`. In the other
|
||||
order, `studio-expose` exits 1 on socat's `bind(…): Address already in
|
||||
use` (both measured on tor-ms22, v1.11.2). The same stale "no bind
|
||||
flag" claim is corrected in the three other places that carried it:
|
||||
the `studio-expose` header (and so its `--help`, whose `sed` range
|
||||
grows from `2,31p` to `2,35p`), §7 of the baked `pi-devbox-environment`
|
||||
skill (which also gains the whole-URL, published-port and `curl` notes
|
||||
below), and the `STUDIO_PORT` comment in `Dockerfile.variant`.
|
||||
- **The remote recipe described a pi-studio 0.9 URL.** Since 0.10 the printed
|
||||
URL carries `studioTabState` and `hostedWorkspace` as well as the token, and
|
||||
a token-only URL is answered `409`. The recipe now says to open the whole
|
||||
URL, and names the copy hazard: the URL wraps, a side panel's text can land
|
||||
inside it, and Studio answers *"This link looks damaged…"*. It also says
|
||||
Studio must run on the **published** port (only the laptop's side of
|
||||
`ssh -L` is free to choose) and that `--port` does not move a running server
|
||||
(`/studio --stop` first). Two more additions: `--watch <file>` for a rendered,
|
||||
read-only preview, and an on-host `curl` that tells "chain up" (`403`) from
|
||||
"Studio down or on another port" (empty reply). A remote session hit exactly
|
||||
that: Studio was started on `18765` while only `8765` is published.
|
||||
- **README said PDF export from Studio works out of the box. It does not.**
|
||||
Since v1.4.0 (`67da05b`) the README's Studio section has said that
|
||||
`/studio-pdf` and `studio_export_pdf` use `typst`. Since v1.9.1
|
||||
(`6353d59`) the image-tags section has also said that "PDF export from
|
||||
Studio/pandoc works out of the box". That holds for pandoc run directly
|
||||
(`pandoc doc.md --pdf-engine=typst -o doc.pdf`), but not for Studio.
|
||||
pi-studio runs pandoc with `$PANDOC_PDF_ENGINE`, falling back to `xelatex`,
|
||||
and the image has never set that variable (`git log -S PANDOC_PDF_ENGINE`
|
||||
finds no commit). So `/studio-pdf` and `studio_export_pdf` fail with
|
||||
`xelatex not found`. That was seen at v1.6.3 and again at v1.11.2. The
|
||||
README now says so in both places that made the claim, and adds a
|
||||
"PDF export from Studio needs LaTeX" section with two warnings and a
|
||||
working recipe:
|
||||
- **Setting `PANDOC_PDF_ENGINE=typst` makes it worse.** The error message
|
||||
suggests it, but pi-studio adds its own LaTeX header to the document
|
||||
(unless the document sets `header-includes`). Under typst, pandoc exits
|
||||
0 and prints that header as text above the first heading. Seen in the same PDF through two renderers, MuPDF and
|
||||
Chromium.
|
||||
- **`texlive-xetex`, the README's old fallback, works but is twice the
|
||||
size it needs to be.** Installed with apt's defaults it pulls in its
|
||||
recommends: 202 packages and 1053 MB, including systemd, D-Bus and a
|
||||
Java runtime (`openjdk-21-jre-headless`, 194 MB). The same 202 come from
|
||||
the old `AGENTS.md` recipe, `texlive-xetex texlive-latex-recommended`.
|
||||
With `--no-install-recommends`, `texlive-xetex` alone is not enough:
|
||||
the export then lacks `lmodern.sty`, `ulem.sty` and the `pzdr` font.
|
||||
The README's recipe adds the three packages that provide them,
|
||||
`lmodern texlive-plain-generic texlive-fonts-recommended`, and comes to
|
||||
37 packages and 491 MB, after `apt-get update`. Each recipe was
|
||||
measured from the image's own 401 packages on v1.11.2 with pi-studio
|
||||
0.10.1, and the exported page was looked at, not only its exit code.
|
||||
|
||||
The same claim is corrected in `AGENTS.md` ("What we DON'T install"),
|
||||
which now also warns against `ENV PANDOC_PDF_ENGINE=typst`, and in two
|
||||
comments in `Dockerfile.base`'s typst block. `base_tag` hashes all of
|
||||
`Dockerfile.base`, comments included, so this changes the base hash too.
|
||||
The base is rebuilt in the next release anyway (above).
|
||||
|
||||
## v1.11.2 — 2026-10-14
|
||||
|
||||
**v1.11.1 was tagged and never published.** Run 724 failed one smoke assertion
|
||||
in each variant (`smoke` 102 passed / **1 failed**, `smoke-studio` 105 / **1**):
|
||||
*esbuild works at every install site* reported "no esbuild install found at
|
||||
all". The image was right and the assertion was wrong. esbuild was never a
|
||||
direct pi dependency. It came through `@earendil-works/chord` (pi 1.1.0 → chord
|
||||
→ esbuild 0.28.2, at the pi and pi-fork sites), and pi 1.2.0 dropped chord, so
|
||||
a correct 1.2.0 image has no esbuild. pi 1.2.0's tree has no esbuild import;
|
||||
1.1.0's chord bundle had one. This is the mistake the clipboard assertion made
|
||||
at v1.10.0. Every publish job skipped, so no `v1.11.1`, `latest` or Hub
|
||||
description was written. The one artefact pushed is the base `base-6f3b20266de1`.
|
||||
It is content-addressed and this release reuses it, which is why
|
||||
`Dockerfile.base`'s `BASE_REBUILD_DATE` comment still names v1.11.1: editing it
|
||||
would change the hash and rebuild an identical base. **Everything below ships
|
||||
here, with the assertion fixed** (*Fixed*, first entry). The tag moves rather
|
||||
than being re-pointed, as at v1.10.2. One component moved between the two tags:
|
||||
pi-studio published **v0.10.0 and v0.10.1** the same morning, CI builds the
|
||||
newest pi-studio tag, and this release adopts 0.10.1 after testing it (*Changed*
|
||||
below) rather than pinning 0.9.63.
|
||||
|
||||
Patch release. Headlines: **pi 1.2.0**, **pi-studio 0.10.1** (two editors, a
|
||||
Prompt and a Document, by default), **`pi -p` returns in seconds** on a
|
||||
devbox instead of idling for five minutes (mempalace-toolkit), **MCP servers use
|
||||
pi's built-in support** (pi-extensions' `mcp-loader.ts` is retired), and **CI
|
||||
now builds the pinned pi-observational-memory commit** instead of whatever its
|
||||
`master` is that day. Unchanged upstream, re-checked 2026-10-14:
|
||||
pi-observational-memory `022da5e`, pi-atelier `v0.15.0`, pi-studio `v0.9.63`,
|
||||
pi-fork `e69725c`, pi-toolkit `3760d90`, mempalace `3.10.0`, Node 24 (v24.21.0
|
||||
pi-observational-memory `022da5e`, pi-atelier `v0.15.0`, pi-fork `e69725c`, pi-toolkit `3760d90`, mempalace `3.10.0`, Node 24 (v24.21.0
|
||||
LTS). The skillset moved (`96b840e` → `1e02b44`), but the vendored mempalace
|
||||
`SKILL.md` is byte-identical, so the snapshot is not refreshed. The base image is
|
||||
rebuilt (the rootfs skill floor and the mempalace-toolkit ref both changed), so
|
||||
@@ -27,14 +258,33 @@ the base tools pinned to `latest` re-resolve too.
|
||||
|
||||
### Components that move with this release
|
||||
|
||||
| Component | Baked in v1.11.0 | v1.11.1 | Why |
|
||||
| Component | Baked in v1.11.0 | v1.11.2 | Why |
|
||||
|---|---|---|---|
|
||||
| **pi** | `1.1.0` | **`1.2.0`** | audited, see *Changed* below |
|
||||
| **pi-studio** (studio variant) | `v0.9.63` (`fae3ad3`) | **`v0.10.1`** (`76db01f`) | tagged 2026-10-14, tested under pi 1.2.0, see *Changed* below |
|
||||
| pi-extensions | `a88fa99` | **`750b2a8`** (or later: CI resolves `main` at build time) | `mcp-loader.ts` retired; `install.sh` prunes stale links (below) |
|
||||
| mempalace-toolkit | `975ab92` | **`d5a03f4`** (or later) | `pi -p` returns in seconds: `2f0edf5` clears the feed's leftover 5-min timer, `f590a3a` skips the feed in print/JSON mode, and `d5a03f4` skips the wake-up and mailbox poll there too (below). Also `a30af61`: the bridge's `RemoteMcpClient` is now the only copy, and `scripts/check-mcp-client-sync.sh` is removed (comment/README only) |
|
||||
|
||||
### Fixed
|
||||
|
||||
- **The smoke test's esbuild assertion is now "present iff something declares
|
||||
it"**, like clipboard's. Because esbuild arrives transitively, "declared" is
|
||||
not read from pi's `package.json`. It is derived from every installed package
|
||||
root and every `/opt` repo root, counting dependencies, optional dependencies
|
||||
and peer dependencies. Anchoring on package roots means a test fixture's
|
||||
`package.json` cannot count. Both directions fail: declared but missing (the
|
||||
prune removed function), and installed with nothing declaring it (residue).
|
||||
The new body was run before tagging. On published v1.11.0-studio it reports
|
||||
"declared (2 package(s)) and compiles at 2 site(s)". With every esbuild dir
|
||||
removed it fails, naming both chord `package.json` files. With
|
||||
`@esbuild/<platform>` pruned it fails with "esbuild broken at …". On the new
|
||||
images it reports that nothing declares esbuild. It also fails a fake esbuild
|
||||
dir that nothing declares, and a package root that declares esbuild with no
|
||||
install, but ignores a test fixture's `package.json`. The whole
|
||||
`smoke-test.sh` then ran against arm64 candidates, built locally with run
|
||||
724's resolved build args on the same base. The old script reproduced CI's
|
||||
102/1 on core. The new one passed core 103/0 and studio 106/0, with pi-studio
|
||||
0.10.1 and with 0.9.63.
|
||||
- **The pi-observational-memory pin now binds CI builds** (v1.11.0's known
|
||||
issue). `resolve-versions` resolved the repo's `master` and passed it as
|
||||
`--build-arg PI_OBSMEM_REF`, which overrides the Dockerfile's pinned default,
|
||||
@@ -71,7 +321,7 @@ the base tools pinned to `latest` re-resolve too.
|
||||
predates v1.11.0. `pi-task` and `fork` children run `--no-extensions` and
|
||||
were never affected. `scripts/test-feed-exit.sh` in mempalace-toolkit pins
|
||||
both changes, and each change's pre-fix source fails it. On images before
|
||||
v1.11.1, scripts that call `pi -p` should keep their `timeout` or set
|
||||
v1.11.2, scripts that call `pi -p` should keep their `timeout` or set
|
||||
`MEMPALACE_FEED=0`.
|
||||
- **`pi -p` no longer gets the palace wake-up.** In print and JSON mode the
|
||||
mempalace bridge now skips the wake-up injection and the mid-session mailbox
|
||||
@@ -114,11 +364,42 @@ the base tools pinned to `latest` re-resolve too.
|
||||
reply was cut off at the model's output limit. That is text mode only;
|
||||
`pi-task` and `fork` run `--mode json`. The first start after the upgrade
|
||||
shows 1.2.0's What's New once. Read from the code, not reproduced: pi-studio
|
||||
marks pi busy on `agent_start`, which 1.2.0 fires after the session already
|
||||
counts as busy. A Studio request sent in that gap (the `before_agent_start`
|
||||
handlers) gets pi's "Agent is already processing" error instead of Studio's
|
||||
own "pi is busy". Before 1.2.0 the same request started a second run that
|
||||
failed, so retrying is the answer either way.
|
||||
(0.9.63 and 0.10.1 alike) marks pi busy on `agent_start`, which 1.2.0 fires
|
||||
after the session already counts as busy. A request that reaches pi in that
|
||||
gap (the `before_agent_start` handlers) gets pi's "Agent is already
|
||||
processing" error instead of Studio's own "pi is busy". Before 1.2.0 the same
|
||||
request started a second run that failed, so retrying is the answer either
|
||||
way. From the browser it is hard to reach: three **Run Prompt** clicks 30 ms
|
||||
and 150 ms apart produced exactly one run in 0.10.1.
|
||||
- **pi-studio `v0.9.63` → `v0.10.1`** (studio variant). 0.10.0 is the largest
|
||||
Studio change in a while. By default there are two editors: a **Prompt** for
|
||||
what you send to pi and a **Document** for the file you are working on. You
|
||||
can switch with `/studio --single` / `--dual` or from the footer. Unsaved text
|
||||
survives reloads, and `/studio` reopens your last workspace (`--blank`,
|
||||
`--last`); its state lives under `~/.pi/agent/pi-studio`, on the preserved
|
||||
volume. There is a new **Files** pane, a two-row toolbar, and **Add to
|
||||
Prompt**. 0.10.1 fixes the Run button flickering while a run starts. Upstream
|
||||
typechecks against pi 1.2.0; its peers stay `*`.
|
||||
- **What pi-devbox relies on is unchanged:** the package manifest
|
||||
(`./index.ts`, `./themes`), the committed `client/studio-client.js`, the 11
|
||||
`studio_*` tools, the 6 commands, and the `--no-browser`, `--port` and
|
||||
`--listen-all` flags that `studio-expose` and the README use. The set of
|
||||
environment variables read is identical.
|
||||
- **Tested under pi 1.2.0 before tagging**, in a copy of tor-ms22's real
|
||||
`~/.pi/agent` with 0.10.1 swapped in:
|
||||
- pi starts with no extension issues, and `/studio --no-browser --port
|
||||
18765` prints its token URL.
|
||||
- In headless Chromium the new UI renders with no console errors.
|
||||
- A prompt typed into the Prompt and sent with **Run Prompt** came back
|
||||
through ERDC in both Studio and the terminal.
|
||||
- The Files pane lists the workspace, and **Open in Document** loads a file.
|
||||
- Quitting pi frees the port.
|
||||
- **In the image:** the 0.10.1 studio candidate passes the full smoke test,
|
||||
106/0. It is 11 MB smaller than with 0.9.63, because the npm package shrank.
|
||||
Inside it, pi's TUI starts with pi-studio among the extensions and no
|
||||
extension issues.
|
||||
- **To go back**, if 0.10 does not suit: v1.11.0-studio still has 0.9.63.
|
||||
Inside a container, `/studio --single` restores the one-editor layout.
|
||||
- **MCP servers now use pi's built-in MCP support; pi-extensions'
|
||||
`mcp-loader.ts` is retired.** pi has shipped MCP as a built-in extension
|
||||
(`builtin:mcp`) since 0.99.0: servers go in `~/.pi/agent/mcp.json` or are
|
||||
|
||||
+1
-1
@@ -61,7 +61,7 @@ The entrypoint deploys/registers all of these on first container start. Re-runni
|
||||
|
||||
### MemPalace (persistent agent memory)
|
||||
|
||||
- **MemPalace** + MCP server — semantic search over conversation history, knowledge graph, diary; queryable via 29 `mempalace_*` tools inside pi
|
||||
- **MemPalace** + MCP server — semantic search over conversation history, knowledge graph, diary; queryable via `mempalace_*` tools inside pi (the palace server decides the set: the baked 3.11.0 lists 47, a 3.10.0 central server 45)
|
||||
- ChromaDB ONNX embedding model pre-warmed at build time (`all-MiniLM-L6-v2`)
|
||||
- Bind-mount your host's `~/.mempalace` and the host-pi and container-pi share one brain
|
||||
|
||||
|
||||
+37
-9
@@ -14,7 +14,7 @@
|
||||
# content-addressed over this file, so any byte change invalidates the
|
||||
# cache. Recommended cadence: once per release for security updates.
|
||||
#
|
||||
# BASE_REBUILD_DATE: 2026-10-14 (v1.11.1 — rootfs skill floor to pi-extensions 750b2a8, mempalace-toolkit d5a03f4; apt and base tools re-resolve (upstream unchanged since 2026-10-12: uv 0.13.0, nvim 0.12.6, fzf 0.74.5); previous marker 2026-10-12 / v1.11.0)
|
||||
# BASE_REBUILD_DATE: 2026-10-18 (v1.12.0 — mempalace 3.11.0, link-pi-extensions.sh, studio-expose and skill doc fixes; apt and base tools re-resolve (upstream on 2026-10-18: uv 0.13.1, gitea-mcp 1.9.0, Node v24.22.0; nvim 0.12.6, fzf 0.74.5 unchanged); previous marker 2026-10-14 / v1.11.1)
|
||||
#
|
||||
# ── Lineage note ─────────────────────────────────────────────────────
|
||||
# Adapted from opencode-devbox/Dockerfile.base (commit before v1.16.2).
|
||||
@@ -639,7 +639,30 @@ ARG INSTALL_MEMPALACE=true
|
||||
# positive control logged one). So 3.10.0's new CLI write-routing policy never
|
||||
# runs against the hub from this image; the client pin touches first-run
|
||||
# `mempalace init` and the on-disk layout, nothing else in remote mode.
|
||||
ARG MEMPALACE_VERSION=3.10.0
|
||||
#
|
||||
# v1.12.0: 3.10.0 -> 3.11.0 (PyPI 2026-10-14). Core requirements unchanged.
|
||||
# Three "Upgrade notes", checked against our callers and measured with both
|
||||
# wheels in throwaway uv venvs and a throwaway palace:
|
||||
# - `EntityRegistry.research()` / `confirm_research()` removed: 0 callers in
|
||||
# pi-devbox, mempalace-toolkit, skillset or myconfigs.
|
||||
# - The first conversation mine re-mines each covered conversation file once
|
||||
# (the exchange chunker stopped discarding text). LOCAL palaces only from
|
||||
# this image: in remote mode the hub does the mining. Measured: a palace
|
||||
# built by 3.10.0 (84 drawers) re-mined its one convo file under 3.11.0
|
||||
# (83 -> 84 drawers, old ones replaced, 2 s); the next mine skipped it.
|
||||
# - A palace refuses to open with a different embedder than the one that
|
||||
# built it. The default (MiniLM) is unchanged and nothing here sets
|
||||
# `embedding_model`; the 3.10.0-built palace opened, searched and served
|
||||
# MCP under 3.11.0 with no warning.
|
||||
# `mempalace init --yes` (entrypoint) still writes config.json, rc 0. MCP over
|
||||
# stdio: 45 -> 47 tools, none removed or renamed (+delete_drawers,
|
||||
# +get_drawers); 14 read-only tools now carry `readOnlyHint`. Skew: synlig's
|
||||
# hub is still 3.10.0 (measured 2026-10-18), the newer-client direction, and
|
||||
# the bridge's tool list comes from the hub, so nothing changes for shared-
|
||||
# palace devices until synlig upgrades. When it does, ITS first convo mine
|
||||
# re-mines every inbox transcript still on disk (upstream: 40 min to 3 h per
|
||||
# million drawers); back it up and run it at a quiet time.
|
||||
ARG MEMPALACE_VERSION=3.11.0
|
||||
# Recorded as a label HERE, not in Dockerfile.variant, for three reasons: the
|
||||
# value lives next to the ARG that defines it (a second copy in the variant
|
||||
# would be one more pin able to drift, which is the class check-doc-drift.sh
|
||||
@@ -822,12 +845,16 @@ RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64"
|
||||
|
||||
# ── typst — lightweight PDF engine for pandoc (Markdown→PDF) ─────────
|
||||
# pandoc (apt-installed above) is only a front-end; rendering PDF needs a
|
||||
# back-end engine. Rather than a ~600 MB TeX Live install, we ship typst:
|
||||
# a single ~30 MB static Rust binary with no LaTeX dependency. pi-studio's
|
||||
# PDF export (studio_export_pdf) and pandoc invocations use it via
|
||||
# `pandoc --pdf-engine=typst`. A fuller TeX Live remains the higher-
|
||||
# fidelity fallback for anyone who needs LaTeX-exact output (not shipped
|
||||
# here — install on demand or in a future variant).
|
||||
# back-end engine. Rather than a ~0.5–1 GB TeX Live install, we ship typst:
|
||||
# a single ~30 MB static Rust binary with no LaTeX dependency. Callers must
|
||||
# ask for it with `pandoc --pdf-engine=typst`, because pandoc's own default
|
||||
# engine is pdflatex, which is not installed. pi-studio's PDF export
|
||||
# (studio_export_pdf, /studio-pdf) does NOT use typst. It runs pandoc with
|
||||
# $PANDOC_PDF_ENGINE, defaulting to xelatex, so it fails with "xelatex not
|
||||
# found". Setting PANDOC_PDF_ENGINE=typst is not a fix: pi-studio adds its
|
||||
# own LaTeX header, and typst prints that header as page text while pandoc
|
||||
# exits 0. TeX Live is not shipped here; README "PDF export from Studio needs
|
||||
# LaTeX" has the on-demand recipe (~490 MB).
|
||||
#
|
||||
# Follows the `latest` GitHub-release convention (like tealdeer/uv/bat).
|
||||
# typst ships a `.tar.xz` asset (hence xz-utils in the apt layer above)
|
||||
@@ -839,7 +866,8 @@ RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64"
|
||||
# document font to an empty tuple (`font: ()`), so a naked
|
||||
# `pandoc --pdf-engine=typst` fails with "font fallback list must not be empty"
|
||||
# unless the caller passes `-V mainfont=...`. We default it to Libertinus Serif
|
||||
# (typst's own bundled default font) so PDF export works out-of-the-box.
|
||||
# (typst's own bundled default font) so a naked `pandoc --pdf-engine=typst`
|
||||
# works out of the box.
|
||||
ARG TYPST_VERSION=latest
|
||||
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
||||
V="${TYPST_VERSION}" && \
|
||||
|
||||
+51
-11
@@ -261,7 +261,7 @@ ARG USER_NAME=developer
|
||||
# obsmem 022da5e registers /om:status and /om:view (control: none); and all 11
|
||||
# pi-extensions a88fa99 extensions load with no error (control: no /aws-sso).
|
||||
#
|
||||
# v1.11.1: 1.1.0 -> 1.2.0 (npm latest, 2026-10-12). MEASURED 2026-10-14 on
|
||||
# v1.11.2: 1.1.0 -> 1.2.0 (npm latest, 2026-10-12). MEASURED 2026-10-14 on
|
||||
# tor-ms22 from an isolated npm prefix (nothing in /usr or ~/.pi touched):
|
||||
# 1. FOUR `### Breaking Changes` in 1.2.0, none of which reaches us:
|
||||
# - before_agent_start also fires for sendMessage(triggerTurn) runs and is
|
||||
@@ -289,7 +289,35 @@ ARG USER_NAME=developer
|
||||
# none); a copy of tor-ms22's real ~/.pi/agent (all packages, pi-extensions
|
||||
# 750b2a8) starts with the same [Extensions] list as on 1.1.0 and no
|
||||
# [Extension issues]; `pi -p` answers PONG via ERDC in 1.0 s, Bedrock in 5.2 s.
|
||||
ARG PI_VERSION=1.2.0
|
||||
#
|
||||
# v1.12.0: 1.2.0 -> 1.3.0 (npm latest, 2026-10-15). MEASURED 2026-10-18 on
|
||||
# tor-ms22 from an isolated npm prefix, against the 1.2.0 tarballs unpacked
|
||||
# side by side:
|
||||
# 1. NO `### Breaking Changes` section in 1.3.0. Behaviour changes, none of
|
||||
# which reaches us: `--model`/`enabledModels` resolve a bare id across
|
||||
# providers by credentials (ours are all `amazon-bedrock/...`, and
|
||||
# pi-fork's effortProfiles name a provider); resumed sessions restore
|
||||
# their active tools; thinking from another model is dropped on a model
|
||||
# switch; `kimi-coding` base-URL overrides need `/v1` (not used).
|
||||
# 2. pi-agent-core and pi-tui 1.2.0 -> 1.3.0: dist BYTE-IDENTICAL (tree
|
||||
# hashes 6a2819f6 / 92eecf92, 24 / 184 files), and so is pi-coding-agent's
|
||||
# dist/core/extensions. So obsmem's turn cap (agent-core) and atelier's
|
||||
# painting (pi-tui) cannot have moved there; atelier's internals
|
||||
# TuiMainScreen / renderLayoutFrame are in the same number of dist files.
|
||||
# 3. Public API: the only removed export in any .d.ts is
|
||||
# `parseModelPattern` (model-resolver, never re-exported from the package
|
||||
# index), and pi-ai dropped its `assistant-message-frame` re-export. 0
|
||||
# consumers of either. All 54 runtime-imported names across pi-toolkit,
|
||||
# pi-extensions, pi-fork, obsmem, pi-studio, pi-atelier, mempalace-toolkit
|
||||
# and /opt/fleet-pi resolve in 1.3.0 (control: an injected fake name is
|
||||
# reported missing).
|
||||
# 4. engines.node is still `>=22.19.0`; internal deps are now exact pins.
|
||||
# Exercised before tagging, with a copy of tor-ms22's real ~/.pi/agent:
|
||||
# `pi -p` exits 0 in 10 s (1.2.0: 9 s), nothing on stderr; the TUI loads every
|
||||
# extension with no [Extension issues], the bridge registers its 45 tools,
|
||||
# /om:status, /om:view and the /studio commands are offered, and atelier
|
||||
# paints ACTIVITY/READY/TOOLS in both renderers (controls with -ne: none).
|
||||
ARG PI_VERSION=1.3.0
|
||||
ARG PI_TOOLKIT_REF=main
|
||||
ARG PI_EXTENSIONS_REF=main
|
||||
# Repo URLs default to the canonical gitea origin but are overridable so a
|
||||
@@ -318,8 +346,8 @@ ARG PI_OBSMEM_REPO=https://github.com/elpapi42/pi-observational-memory.git
|
||||
# dist/index.ts instead of 32 TS sources. The bundle is committed, so
|
||||
# `npm install --omit=dev` below needs no build step. src/agents/ is
|
||||
# UNCHANGED between the two SHAs, so the finishTurn migration is intact.
|
||||
# v1.11.1: unchanged (master is still 022da5e, 6 commits past the 3.1.4 tag).
|
||||
# CI NOW BUILDS THIS PIN. Until v1.11.1 docker-publish.yml's resolve-versions
|
||||
# v1.11.2: unchanged (master is still 022da5e, 6 commits past the 3.1.4 tag).
|
||||
# CI NOW BUILDS THIS PIN. Until v1.11.2 docker-publish.yml's resolve-versions
|
||||
# resolved obsmem from `master` and passed it as --build-arg, which overrides
|
||||
# this default (verified two-sided on buildx 2026-10-12), so CI got the pin only
|
||||
# while master == pin. resolve-versions now reads this line, refuses anything
|
||||
@@ -414,11 +442,22 @@ ARG PI_ATELIER_REPO=https://github.com/michaelmjhhhh/pi-atelier.git
|
||||
# panels and simplifies the Activity rows. The startup preference is still
|
||||
# `showSidebarOnStartup` (default true, src/config.ts:38). Painted in both
|
||||
# renderers on pi 1.1.0 before tagging (see the PI_VERSION note).
|
||||
# v1.11.1: unchanged (still the newest tag); painted in both renderers on
|
||||
# v1.11.2: unchanged (still the newest tag); painted in both renderers on
|
||||
# pi 1.2.0 before tagging.
|
||||
ARG PI_ATELIER_REF=v0.15.0
|
||||
# v1.12.0: v0.15.0 -> v0.16.0 (3b42825, 2026-10-15). Additive: contributed
|
||||
# panels may send label/value rows with an optional progress bar (`keyValues`;
|
||||
# plain `rows` stay the fallback) and two new roles, and the context meter
|
||||
# loses its hard-coded dark background. Protocol unchanged (channel
|
||||
# `pi-atelier:sidebar-panels`, version 1, 24 rows, 160 chars), peer floor
|
||||
# unchanged (pi >=0.84.0), still zero runtime dependencies. On pi 1.3.0 it
|
||||
# paints in both renderers, and the fleet's contributed `fleet:todos` panel
|
||||
# (/opt/fleet-pi/fleet-todos-panel.ts, plain rows) shows a todo written in a
|
||||
# real turn, as on v0.15.0. The v0.15.0 arm still draws the old meter
|
||||
# background (48;2;48;53;56) and the v0.16.0 arm does not, so the arms ran the
|
||||
# code they claim.
|
||||
ARG PI_ATELIER_REF=v0.16.0
|
||||
# Human-readable tag PI_ATELIER_REF was resolved from; recorded as a label.
|
||||
ARG PI_ATELIER_VERSION=v0.15.0
|
||||
ARG PI_ATELIER_VERSION=v0.16.0
|
||||
|
||||
RUN set -e && \
|
||||
# git_fetch_ref: clone-equivalent helper that accepts EITHER a branch name
|
||||
@@ -676,10 +715,11 @@ RUN if [ "${INSTALL_STUDIO}" = "true" ]; then \
|
||||
|
||||
# STUDIO_PORT: advisory default consumed by docker-compose port publishing
|
||||
# and the recommended `/studio --no-browser --port "$STUDIO_PORT"` launch.
|
||||
# Harmless in the non-studio variant. NOTE: pi-studio hard-binds the server
|
||||
# to 127.0.0.1 inside the container (index.ts: .listen(port,"127.0.0.1")),
|
||||
# so reaching it from a browser needs a loopback bridge or host networking —
|
||||
# see the "Using pi-studio" section in README.md.
|
||||
# Harmless in the non-studio variant. NOTE: by default pi-studio binds the
|
||||
# server to 127.0.0.1 inside the container, so reaching it from a browser
|
||||
# needs host networking, the studio-expose loopback bridge, or the opt-in
|
||||
# `/studio --listen-all` (pi-studio >= 0.9.60; never together with the
|
||||
# bridge on one port) — see the "Using pi-studio" section in README.md.
|
||||
ENV STUDIO_PORT=8765
|
||||
|
||||
# ── Optional: Go toolchain ───────────────────────────────────────────
|
||||
|
||||
@@ -175,10 +175,12 @@ Currently published:
|
||||
| `joakimp/pi-devbox:latest-studio` | `latest` + [pi-studio](https://github.com/omaclaren/pi-studio) (browser prompt editor, KaTeX/Mermaid preview, tmux-backed literate REPLs) | ~3.25 GB |
|
||||
| `joakimp/pi-devbox:vX.Y.Z-studio` | pinned-version studio equivalent | ~3.25 GB |
|
||||
|
||||
Both variants ship **`typst`** as the pandoc PDF engine
|
||||
(`pandoc --pdf-engine=typst`), a single ~30 MB static binary, so PDF export from
|
||||
Studio/pandoc works out of the box — no separate `-tex` variant needed.
|
||||
`texlive-xetex` stays the higher-fidelity fallback (install on demand).
|
||||
Both variants ship **`typst`**, a single ~30 MB static binary, as a PDF engine
|
||||
for pandoc, so `pandoc doc.md --pdf-engine=typst -o doc.pdf` works out of the
|
||||
box (see [Generating a PDF with pandoc + typst](#generating-a-pdf-with-pandoc--typst)).
|
||||
**pi-studio's PDF export does not use typst.** `/studio-pdf` and
|
||||
`studio_export_pdf` need a LaTeX engine, which neither variant ships (see
|
||||
[PDF export from Studio needs LaTeX](#pdf-export-from-studio-needs-latex)).
|
||||
|
||||
## Using pi-studio (`-studio` variant)
|
||||
|
||||
@@ -198,12 +200,11 @@ Inside a pi session in the container:
|
||||
|
||||
### Reaching the UI from your browser (the container caveat)
|
||||
|
||||
pi-studio **hard-binds its server to `127.0.0.1` inside the container**
|
||||
(`index.ts`: `.listen(port, "127.0.0.1")`) and serves a tokenized URL.
|
||||
There is no `--host`/bind flag. This matters for a container: a plain
|
||||
By default pi-studio **binds its server to `127.0.0.1` inside the
|
||||
container** and serves a tokenized URL. This matters for a container: a plain
|
||||
`docker run -p 8765:8765` publish forwards to the container's *external*
|
||||
interface, **not** its loopback, so it will not reach Studio. Two paths
|
||||
work:
|
||||
interface, **not** its loopback, so it will not reach a default Studio.
|
||||
Three paths work (A and B keep Studio itself on loopback; C does not):
|
||||
|
||||
**A. Host networking (simplest — OrbStack / single-host, no bridge).**
|
||||
Run the container with host networking so the container's loopback is the
|
||||
@@ -215,8 +216,9 @@ services:
|
||||
network_mode: host # container 127.0.0.1 == host 127.0.0.1
|
||||
```
|
||||
|
||||
Then `http://127.0.0.1:8765/?token=…` works in a browser on the Docker
|
||||
host. This is the most secure option (Studio never leaves loopback). Note:
|
||||
Then the URL Studio prints (`http://127.0.0.1:8765/?token=…`) works in a
|
||||
browser on the Docker host. This is the most secure option (Studio never
|
||||
leaves loopback). Note:
|
||||
host networking changes `host.docker.internal` semantics, so weigh it
|
||||
against the LAN-jump SSH feature if you use that.
|
||||
|
||||
@@ -253,6 +255,20 @@ studio-expose & # bridges $STUDIO_PORT (default 8765); --help for detai
|
||||
> its tokenized URL is the only auth. Keep the host-side publish on
|
||||
> `127.0.0.1:` and use `ssh -L` for remote access. Default is **off**.
|
||||
|
||||
**C. `/studio --listen-all` (pi-studio ≥ 0.9.60, no bridge).** pi-studio has
|
||||
its own opt-in flag: `/studio --no-browser --listen-all --port 8765` binds
|
||||
`0.0.0.0`, so the published port reaches it without `socat`. The URL it
|
||||
prints still says `127.0.0.1`, so it works verbatim through a same-port
|
||||
publish. Keep the publish on `127.0.0.1:` as in B: anyone who can reach the
|
||||
port and has the full URL controls Studio, including running prompts and
|
||||
reading or writing files, and there is no TLS.
|
||||
|
||||
> **Use B or C on a port, not both.** Whichever starts second fails with
|
||||
> `EADDRINUSE`: `0.0.0.0` includes the egress IP the bridge binds. With
|
||||
> `STUDIO_EXPOSE=1` the bridge holds `$STUDIO_PORT` from boot, so
|
||||
> `/studio --listen-all --port 8765` fails. If the bridge is running, start
|
||||
> Studio *without* `--listen-all`.
|
||||
|
||||
### Remote host (SSH / mosh)
|
||||
|
||||
When the Docker host is remote, keep Studio on localhost and forward the
|
||||
@@ -298,25 +314,79 @@ Assuming the compose file publishes `127.0.0.1:8765:8765` (see method B):
|
||||
TUI, not a shell command:
|
||||
```
|
||||
/studio --no-browser --port 8765
|
||||
/studio --no-browser --port 8765 --watch notes.md # or: a rendered, read-only preview of a file
|
||||
/studio --status # reprint the URL anytime
|
||||
```
|
||||
Copy the `http://…:8765/?token=<token>` it prints. **This** is where
|
||||
the real token comes from — not `studio-expose`.
|
||||
**This** is where the real token comes from — not `studio-expose`.
|
||||
The port must be the **published** one (`8765` here): the tunnel, the
|
||||
publish and the bridge all lead to it, and Studio on any other port is
|
||||
unreachable from outside. `--port` does not move a running server (Studio
|
||||
only warns), so run `/studio --stop` first to change it.
|
||||
3. **On your laptop** — open the ssh port-forward alongside mosh:
|
||||
```bash
|
||||
ssh -L 8765:127.0.0.1:8765 user@docker-host
|
||||
ssh -N -o ExitOnForwardFailure=yes -L 8765:127.0.0.1:8765 user@docker-host
|
||||
```
|
||||
4. **In your laptop browser** — open `http://127.0.0.1:8765/?token=<token>`
|
||||
(keep the port and token verbatim; only the host part is `127.0.0.1`).
|
||||
Only the **left** port is yours to choose. If `8765` is already taken on
|
||||
the laptop (for example by the laptop's own devbox publish), use
|
||||
`-L 18765:127.0.0.1:8765` and change only the port in the URL.
|
||||
4. **In your laptop browser** — open the **whole** URL Studio printed. It
|
||||
already says `127.0.0.1`, and it carries more than the token
|
||||
(`token`, `studioTabState`, `hostedWorkspace` since pi-studio 0.10, or
|
||||
`docPath`/`watchFile`/… for `--watch`). A URL cut down to just the token
|
||||
is answered `409`.
|
||||
The URL is long and wraps across TUI lines; a side panel next to it (such
|
||||
as the pi-atelier sidebar) can end up inside the copied text, and Studio
|
||||
then answers *"This link looks damaged…"*. Rejoin the lines without the
|
||||
panel text, or widen the terminal and copy it again.
|
||||
|
||||
> **Order check:** nothing listens on the container's `127.0.0.1:8765`
|
||||
> until step 2 runs. If the browser can't connect, verify Studio is up
|
||||
> (`/studio --status`) and the bridge is running (`ps aux | grep socat`).
|
||||
> **on the published port** (`/studio --status`) and the bridge is running
|
||||
> (`ps aux | grep socat`). On the Docker host,
|
||||
> `curl -s http://127.0.0.1:8765/` answers `403` when the whole chain is up;
|
||||
> an empty reply means the bridge has nothing behind it, i.e. Studio is down
|
||||
> or on another port.
|
||||
|
||||
> PDF export (`/studio-pdf`, `studio_export_pdf`) uses **`typst`**, shipped in
|
||||
> the base image as the pandoc PDF engine (`pandoc --pdf-engine=typst`). For
|
||||
> LaTeX-exact output you can install `texlive-xetex` on demand as a heavier
|
||||
> fallback. HTML export, KaTeX, Mermaid, and all REPL features work regardless.
|
||||
### PDF export from Studio needs LaTeX
|
||||
|
||||
Studio's PDF export (`/studio-pdf` and the `studio_export_pdf` tool) fails out
|
||||
of the box:
|
||||
|
||||
```text
|
||||
pandoc PDF export failed with exit code 47: xelatex not found
|
||||
```
|
||||
|
||||
pi-studio runs pandoc with the engine named in `PANDOC_PDF_ENGINE`, and uses
|
||||
`xelatex` when that is unset. The image ships no LaTeX engine and does not set
|
||||
`PANDOC_PDF_ENGINE`. HTML export, KaTeX, Mermaid and the REPLs are not
|
||||
affected.
|
||||
|
||||
**Do not set `PANDOC_PDF_ENGINE=typst`**, even though the error message
|
||||
suggests setting it. pi-studio adds its own LaTeX header
|
||||
(`\usepackage{titlesec}`, `\titleformat…`) to the document, and typst does
|
||||
not understand LaTeX. pandoc exits 0, and the header is printed as text above
|
||||
your first heading. There is no error, only a wrong PDF.
|
||||
|
||||
There are two ways out:
|
||||
|
||||
- **Run pandoc yourself** (nothing to install):
|
||||
`pandoc doc.md --pdf-engine=typst -o doc.pdf`. See the next section.
|
||||
- **Install the LaTeX packages Studio needs.** This adds 37 packages and
|
||||
about 490 MB, and they are gone when the container is recreated:
|
||||
|
||||
```bash
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
texlive-xetex lmodern texlive-plain-generic texlive-fonts-recommended
|
||||
```
|
||||
|
||||
The image starts with an empty package index, so run `apt-get update`
|
||||
first. With `--no-install-recommends`, `texlive-xetex` on its own is not
|
||||
enough: Studio's export then lacks `lmodern.sty`, `ulem.sty` and the `pzdr`
|
||||
font, which the other three packages provide. A plain
|
||||
`sudo apt-get install texlive-xetex` also works, but its recommended
|
||||
packages make it 202 packages and about 1 GB, including systemd, D-Bus and
|
||||
a Java runtime. (Measured on v1.11.2 with pi-studio 0.10.1.)
|
||||
|
||||
### Generating a PDF with pandoc + typst
|
||||
|
||||
@@ -916,6 +986,27 @@ To share one file across devices, keep it in a host directory and mount that
|
||||
link. Check with `pi --list-models <provider>`. `recreate-sanity-check.sh`
|
||||
verifies the link whenever a shared file is mounted.
|
||||
|
||||
### The same directory can carry pi extensions
|
||||
|
||||
pi auto-loads every `.ts` in `~/.pi/agent/extensions/`, which lives in the same
|
||||
volume and so has the same problem. The entrypoint therefore links top-level
|
||||
`*.ts` files from the mounted directory in as well, one line each:
|
||||
|
||||
pi extension: linked erdc-classifiers.ts -> /opt/fleet-pi/erdc-classifiers.ts
|
||||
|
||||
- Only **top-level** `*.ts` files are linked: `models.json`, a README or a
|
||||
nested checkout in that directory is not an extension, and one is never loaded
|
||||
by accident.
|
||||
- Same never-destructive rule, with the same `NOTE` lines, and the same
|
||||
reasoning that makes a link right where a single-file mount is not (a pulled
|
||||
change is picked up at once).
|
||||
- **Your own copy wins.** A real file of the same name in
|
||||
`~/.pi/agent/extensions/` is left alone, so a local edit of a shared extension
|
||||
is never overwritten.
|
||||
- `PI_EXTENSIONS_SRC` points at another directory, and `PI_EXTENSIONS_SRC=off`
|
||||
disables it. `recreate-sanity-check.sh` verifies each link whenever the shared
|
||||
directory holds `*.ts`.
|
||||
|
||||
## Build pipeline
|
||||
|
||||
pi-devbox is built from this repo's CI in two phases:
|
||||
@@ -1198,7 +1289,7 @@ persisted volumes survived, and pi runtime wiring is intact:
|
||||
```bash
|
||||
./scripts/recreate-sanity-check.sh # auto-detects variant
|
||||
./scripts/recreate-sanity-check.sh --expected-image-version 1.8.9 # assert the pi-devbox release tag
|
||||
./scripts/recreate-sanity-check.sh --expected-version 1.2.0 # assert the pi coding agent version
|
||||
./scripts/recreate-sanity-check.sh --expected-version 1.3.0 # assert the pi coding agent version
|
||||
```
|
||||
|
||||
Those are **two different versions**, and the flags are not interchangeable:
|
||||
@@ -1232,15 +1323,15 @@ user's pull.
|
||||
|
||||
### Version pins: pi, pi-atelier, mempalace
|
||||
|
||||
Three components are pinned to an exact version **in the repo** instead of being
|
||||
Four components are pinned to an exact version **in the repo** instead of being
|
||||
resolved to `latest` at build time:
|
||||
|
||||
| Component | Pin | Where |
|
||||
|---|---|---|
|
||||
| pi | `1.2.0` | `ARG PI_VERSION` — `Dockerfile.variant` |
|
||||
| pi | `1.3.0` | `ARG PI_VERSION` — `Dockerfile.variant` |
|
||||
| pi-obsmem | `022da5e5554d04dfdc876ed7dac4a42c6b83dfd4` | `ARG PI_OBSMEM_REF` — `Dockerfile.variant` |
|
||||
| pi-atelier | `v0.15.0` | `ARG PI_ATELIER_REF` — `Dockerfile.variant` |
|
||||
| mempalace | `3.10.0` | `ARG MEMPALACE_VERSION` — `Dockerfile.base` |
|
||||
| pi-atelier | `v0.16.0` | `ARG PI_ATELIER_REF` — `Dockerfile.variant` |
|
||||
| mempalace | `3.11.0` | `ARG MEMPALACE_VERSION` — `Dockerfile.base` |
|
||||
|
||||
The objective is **not** to freeze versions. Bumping is routine — usually one
|
||||
line plus a changelog note. The objective is that adopting a new upstream
|
||||
|
||||
@@ -381,6 +381,16 @@ if command -v pi &>/dev/null; then
|
||||
bash /usr/local/lib/pi-devbox/link-pi-models.sh || true
|
||||
fi
|
||||
|
||||
# Shared pi extensions: link each TOP-LEVEL *.ts in that same mounted directory
|
||||
# into ~/.pi/agent/extensions (default /opt/fleet-pi; PI_EXTENSIONS_SRC overrides,
|
||||
# =off disables). Same reasoning and same never-destructive rule as models.json
|
||||
# above: without it every fresh devbox-pi-config volume needed a manual `ln -s`
|
||||
# per extension per device, and the copies drift. A user's own file of the same
|
||||
# name wins over the fleet copy. Rationale and cases: the script's header.
|
||||
if [ -r /usr/local/lib/pi-devbox/link-pi-extensions.sh ]; then
|
||||
bash /usr/local/lib/pi-devbox/link-pi-extensions.sh || true
|
||||
fi
|
||||
|
||||
# Bootstrap settings.json from template if absent (pi rewrites this
|
||||
# file at runtime — lastChangelogVersion, etc — so we can't symlink it).
|
||||
_pi_settings="$HOME/.pi/agent/settings.json"
|
||||
|
||||
@@ -3,13 +3,17 @@
|
||||
# through a published Docker port.
|
||||
#
|
||||
# WHY THIS EXISTS
|
||||
# pi-studio hard-binds its HTTP/WebSocket server to 127.0.0.1 inside the
|
||||
# container (index.ts: `.listen(port, "127.0.0.1")`) and there is no
|
||||
# --host / bind flag. A plain `docker run -p 8765:8765` forwards to the
|
||||
# By default pi-studio binds its HTTP/WebSocket server to 127.0.0.1 inside
|
||||
# the container. A plain `docker run -p 8765:8765` forwards to the
|
||||
# container's EXTERNAL interface (eth0), not its loopback, so it cannot
|
||||
# reach Studio. This helper runs a socat TCP relay that listens on the
|
||||
# container's egress IP and forwards to 127.0.0.1:<port>, so a published
|
||||
# port (and an `ssh -L` tunnel from your laptop) can reach Studio.
|
||||
# reach a default Studio. This helper runs a socat TCP relay that listens
|
||||
# on the container's egress IP and forwards to 127.0.0.1:<port>, so a
|
||||
# published port (and an `ssh -L` tunnel from your laptop) can reach Studio.
|
||||
#
|
||||
# The alternative (pi-studio >= 0.9.60) is `/studio --listen-all`, which
|
||||
# binds 0.0.0.0 and needs no bridge. Use one or the other on a port, never
|
||||
# both: whichever starts second fails with EADDRINUSE (0.0.0.0 includes
|
||||
# the egress IP this relay binds).
|
||||
#
|
||||
# SECURITY
|
||||
# This intentionally exposes Studio beyond loopback — anything that can
|
||||
@@ -34,7 +38,7 @@ set -euo pipefail
|
||||
PORT="${1:-${STUDIO_PORT:-8765}}"
|
||||
|
||||
if [ "$PORT" = "--help" ] || [ "$PORT" = "-h" ]; then
|
||||
sed -n '2,31p' "$0" | sed 's/^# \{0,1\}//'
|
||||
sed -n '2,35p' "$0" | sed 's/^# \{0,1\}//'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env bash
|
||||
# link-pi-extensions.sh — point ~/.pi/agent/extensions/*.ts at shared, mounted files.
|
||||
#
|
||||
# pi auto-loads every .ts file in ~/.pi/agent/extensions/. In pi-devbox that
|
||||
# directory lives in the devbox-pi-config named volume, which makes both obvious
|
||||
# ways of providing a fleet extension wrong — the same two ways as models.json
|
||||
# (see link-pi-models.sh, whose header this mirrors):
|
||||
# - baked into the image, it would only seed a FRESH volume, once;
|
||||
# - copied by hand into each volume, it is one more per-device step, and the
|
||||
# copies drift the first time the extension changes.
|
||||
# Instead, keep the .ts files in the same host directory that already holds
|
||||
# models.json, mount that directory read-only, and let this script link them in
|
||||
# at every container start:
|
||||
#
|
||||
# volumes:
|
||||
# - ./pi-shared:/opt/fleet-pi:ro # a directory whose top-level *.ts are extensions
|
||||
#
|
||||
# A SYMLINK, not a single-file bind mount, and for the measured reason given in
|
||||
# link-pi-models.sh: `git pull` replaces a file by rename, so a single-file mount
|
||||
# keeps the old inode and serves stale content until the next recreate. A link
|
||||
# follows the path, so a pulled change reaches the next pi session.
|
||||
#
|
||||
# Only TOP-LEVEL *.ts files are linked — no recursive search. The directory is
|
||||
# fleet-shared pi config: models.json, its own docs and any sub-checkout are not
|
||||
# extensions, and silently loading them would be worse than missing one.
|
||||
#
|
||||
# PI_EXTENSIONS_SRC overrides the directory (default /opt/fleet-pi).
|
||||
# PI_EXTENSIONS_SRC=off (or empty) disables this script.
|
||||
#
|
||||
# Never destructive, per file. It only ever creates a link where nothing exists:
|
||||
# - no file at the destination → link created
|
||||
# - the destination is already ours → nothing
|
||||
# - a real file with that name exists → left alone; one NOTE line says why
|
||||
# (the user's own extension of that name deliberately wins over the fleet copy)
|
||||
# - the destination links elsewhere → left alone; a deliberate choice
|
||||
# A link this script made whose target later disappears (mount removed) is
|
||||
# harmless and needs no cleanup: pi reports the unreadable extension and the rest
|
||||
# of the session is unaffected.
|
||||
#
|
||||
# Called by entrypoint-user.sh. Safe to run by hand at any time.
|
||||
set -u
|
||||
|
||||
src="${PI_EXTENSIONS_SRC-/opt/fleet-pi}"
|
||||
case "$src" in
|
||||
"" | off | OFF | Off) exit 0 ;;
|
||||
esac
|
||||
[ -d "$src" ] || exit 0
|
||||
|
||||
agent_dir="${PI_CODING_AGENT_DIR:-$HOME/.pi/agent}"
|
||||
dest_dir="$agent_dir/extensions"
|
||||
|
||||
for f in "$src"/*.ts; do
|
||||
[ -f "$f" ] || continue
|
||||
name="$(basename "$f")"
|
||||
dest="$dest_dir/$name"
|
||||
|
||||
if [ -L "$dest" ]; then
|
||||
# Ours already, or someone else's deliberate link: either way, nothing to do.
|
||||
[ "$(readlink "$dest")" = "$f" ] || \
|
||||
echo "NOTE: $dest links to $(readlink "$dest"), not $f; left alone"
|
||||
continue
|
||||
fi
|
||||
if [ -e "$dest" ]; then
|
||||
echo "NOTE: $dest is a real file; not linking $f (move it away to use the fleet copy)"
|
||||
continue
|
||||
fi
|
||||
|
||||
mkdir -p "$dest_dir" 2>/dev/null || true
|
||||
if ln -s "$f" "$dest" 2>/dev/null; then
|
||||
echo "pi extension: linked $name -> $f"
|
||||
else
|
||||
echo "WARN: could not link $dest -> $f (continuing)"
|
||||
fi
|
||||
done
|
||||
exit 0
|
||||
@@ -356,8 +356,8 @@ window: 0". Leave the indexing alone in this environment.
|
||||
## 7. pi-studio reachability (only in the `-studio` variant)
|
||||
|
||||
Present only if `/opt/pi-studio` exists / the `studio_*` tools are in your tool
|
||||
list. pi-studio **binds to `127.0.0.1` inside the container** with no host-bind
|
||||
flag, so a plain `docker -p` publish can't reach it. Two supported paths:
|
||||
list. By default pi-studio **binds to `127.0.0.1` inside the container**, so a
|
||||
plain `docker -p` publish can't reach it. Three supported paths:
|
||||
|
||||
- **Host networking** (`network_mode: host`): container loopback == host
|
||||
loopback; open the tokenized URL on the host. (Changes
|
||||
@@ -365,10 +365,25 @@ flag, so a plain `docker -p` publish can't reach it. Two supported paths:
|
||||
- **`studio-expose` bridge** (`STUDIO_EXPOSE=1` or run `studio-expose &`): a
|
||||
`socat` relay from the container's external interface to its loopback, so a
|
||||
published `127.0.0.1:PORT` + `ssh -L PORT:127.0.0.1:PORT host` reaches it.
|
||||
- **`/studio --listen-all`** (pi-studio ≥ 0.9.60): binds `0.0.0.0`, so the
|
||||
published port reaches it with no bridge; the printed URL still says
|
||||
`127.0.0.1`. **Never on the same port as the bridge**: whichever starts
|
||||
second fails with `EADDRINUSE`. `ss -ltnp | grep ":$STUDIO_PORT"` showing a
|
||||
`socat` line means the bridge is up, so start Studio without `--listen-all`.
|
||||
|
||||
The real auth token comes from the `/studio` slash command (`/studio --status`
|
||||
to reprint), **not** from `studio-expose`. For Graphviz, use `dot-watch` →
|
||||
PNG (Studio renders Mermaid natively and previews PNG, but not SVG/DOT).
|
||||
to reprint), **not** from `studio-expose`. Hand over the **whole** printed
|
||||
URL: since pi-studio 0.10 it also carries `studioTabState` + `hostedWorkspace`
|
||||
(or `docPath`/`watchFile`/… for `--watch`), and a token-only URL is answered
|
||||
`409`. The URL wraps across TUI lines; if a side panel's text lands inside the
|
||||
copy, Studio answers *"This link looks damaged…"*. Studio must run on the
|
||||
**published** port (only the laptop side of `ssh -L` is free to choose), and
|
||||
`--port` on a running server only warns, so `/studio --stop` first. On the
|
||||
Docker host, `curl -s http://127.0.0.1:$STUDIO_PORT/` answering `403` means
|
||||
the chain is up; an empty reply means Studio is down or on another port.
|
||||
|
||||
For Graphviz, use `dot-watch` → PNG (Studio renders Mermaid natively and
|
||||
previews PNG, but not SVG/DOT).
|
||||
|
||||
## 8. MemPalace is the shared brain
|
||||
|
||||
|
||||
@@ -295,6 +295,28 @@ if [ -n "$_mj_src" ] && [ -f "$_mj_src" ]; then
|
||||
fi
|
||||
unset _mj _mj_src
|
||||
|
||||
# shared fleet extensions (link-pi-extensions.sh) — same shape as models.json
|
||||
# above: checked only when the fleet directory actually holds *.ts, so a device
|
||||
# without a shared directory is not judged by it.
|
||||
_le_src="${PI_EXTENSIONS_SRC-/opt/fleet-pi}"
|
||||
case "$_le_src" in "" | off | OFF | Off) _le_src="" ;; esac
|
||||
if [ -n "$_le_src" ] && ls -1 "$_le_src"/*.ts >/dev/null 2>&1; then
|
||||
for _le_f in "$_le_src"/*.ts; do
|
||||
_le_name="$(basename "$_le_f")"; _le_dest="$HOME/.pi/agent/extensions/$_le_name"
|
||||
if [ -L "$_le_dest" ] && [ "$(readlink "$_le_dest")" = "$_le_f" ]; then
|
||||
pass "~/.pi/agent/extensions/$_le_name -> $_le_f (shared fleet extension)"
|
||||
elif [ -L "$_le_dest" ]; then
|
||||
warn "~/.pi/agent/extensions/$_le_name links to $(readlink "$_le_dest"), not the mounted $_le_f"
|
||||
elif [ -e "$_le_dest" ]; then
|
||||
warn "~/.pi/agent/extensions/$_le_name is a real file, so the mounted $_le_f is NOT in use"
|
||||
else
|
||||
fail "$_le_src holds $_le_name but ~/.pi/agent/extensions/$_le_name is missing (image without link-pi-extensions.sh? run: ln -s $_le_f ~/.pi/agent/extensions/)"
|
||||
fi
|
||||
done
|
||||
unset _le_f _le_name _le_dest
|
||||
fi
|
||||
unset _le_src
|
||||
|
||||
# extensions deployed (pi-extensions) — expect ≥4 *.ts
|
||||
EXT_COUNT=$(ls -1 "$HOME"/.pi/agent/extensions/*.ts 2>/dev/null | wc -l | tr -d ' ')
|
||||
if [ "$EXT_COUNT" -ge 4 ]; then
|
||||
|
||||
+37
-3
@@ -30,7 +30,8 @@
|
||||
# client bundle present + registered via `pi install`
|
||||
# - no foreign npm-11 platform packages (@esbuild, clipboard) beyond the host
|
||||
# - no build-time npm cache (/root/.npm) shipped in the image
|
||||
# - esbuild compiles everywhere; clipboard present iff pi declares it
|
||||
# - esbuild present iff something declares it, and compiles everywhere;
|
||||
# clipboard present iff pi declares it
|
||||
# - image size within threshold
|
||||
|
||||
set -euo pipefail
|
||||
@@ -674,6 +675,7 @@ exec_test "mempalace.ts bridge" 'test -L $HOME/.pi/agent/exte
|
||||
exec_test "settings.json bootstrapped" 'test -f $HOME/.pi/agent/settings.json && echo ok'
|
||||
exec_test "no shared models.json mounted → none linked" 'test ! -e $HOME/.pi/agent/models.json && test ! -L $HOME/.pi/agent/models.json'
|
||||
exec_test "link-pi-models.sh links a mounted models.json" 'mkdir -p /tmp/lpm && echo "{}" > /tmp/lpm/models.json && PI_MODELS_JSON=/tmp/lpm/models.json PI_CODING_AGENT_DIR=/tmp/lpm/agent bash /usr/local/lib/pi-devbox/link-pi-models.sh >/dev/null && test "$(readlink /tmp/lpm/agent/models.json)" = /tmp/lpm/models.json && rm -rf /tmp/lpm'
|
||||
exec_test "link-pi-extensions.sh links a mounted *.ts" 'mkdir -p /tmp/lpe/src && echo "export default {}" > /tmp/lpe/src/x.ts && PI_EXTENSIONS_SRC=/tmp/lpe/src PI_CODING_AGENT_DIR=/tmp/lpe/agent bash /usr/local/lib/pi-devbox/link-pi-extensions.sh >/dev/null && test "$(readlink /tmp/lpe/agent/extensions/x.ts)" = /tmp/lpe/src/x.ts && rm -rf /tmp/lpe'
|
||||
exec_test "pi-devbox-environment skill linked" 'test -L $HOME/.agents/skills/pi-devbox-environment && test -f $HOME/.agents/skills/pi-devbox-environment/SKILL.md && echo ok'
|
||||
exec_test "pi-extensions skill linked (fallback)" 'test -L $HOME/.agents/skills/pi-extensions && test -f $HOME/.agents/skills/pi-extensions/SKILL.md && echo ok'
|
||||
exec_test "mempalace skill linked (fallback)" 'test -L $HOME/.agents/skills/mempalace && test -f $HOME/.agents/skills/mempalace/SKILL.md && echo ok'
|
||||
@@ -985,8 +987,40 @@ run "no build-time npm cache shipped (/root/.npm)" \
|
||||
# runbook as "if this fails, revert the release", and it duly failed for the
|
||||
# wrong reason on the first machine that ran it. A check must fail only for the
|
||||
# thing it is checking.
|
||||
run "esbuild works at every install site (prune removed weight, not function)" \
|
||||
'sites=$(find /usr/lib/node_modules /opt -type d -path "*/node_modules/esbuild" -prune 2>/dev/null); if [ -z "$sites" ]; then echo "no esbuild install found at all" >&2; exit 1; fi; for d in $sites; do node -e "require(\"$d\").transformSync(\"const x:number=1\",{loader:\"ts\"})" || { echo "esbuild broken at $d" >&2; exit 1; }; done; echo ok'
|
||||
#
|
||||
# PRESENT IFF DECLARED, like clipboard below. esbuild was never a direct pi
|
||||
# dependency: it arrived through @earendil-works/chord (pi 1.1.0 > chord >
|
||||
# esbuild 0.28.2, at both the pi and pi-fork sites). pi 1.2.0 dropped chord, so
|
||||
# a correct 1.2.0 image has NO esbuild, and the old "must exist" form failed the
|
||||
# v1.11.1 tag build (102/103 core, 105/106 studio, nothing published) against a
|
||||
# good image — the same mistake the clipboard check made at v1.10.0. Because it
|
||||
# is transitive, "declared" cannot be read from pi's package.json: it is
|
||||
# derived from every INSTALLED package root (node_modules/<pkg>/package.json,
|
||||
# node_modules/@scope/<pkg>/package.json) and every /opt repo root, counting
|
||||
# dependencies, optionalDependencies and peerDependencies (devDependencies are
|
||||
# not installed: --omit=dev). Anchoring on package roots keeps a test fixture's
|
||||
# package.json from counting. Both directions fail: declared but missing (the
|
||||
# prune removed function), and installed with nothing declaring it (residue).
|
||||
run "esbuild: present iff something declares it, and works at every install site (prune removed weight, not function)" \
|
||||
'declarers=$(find /usr/lib/node_modules /opt -regextype posix-extended -type f \( -regex ".*/node_modules/(@[^/]+/)?[^/]+/package\.json" -o -regex "/opt/[^/]+/package\.json" \) 2>/dev/null | node -e "
|
||||
const fs = require(\"fs\"); const hit = [];
|
||||
for (const f of fs.readFileSync(0, \"utf8\").split(String.fromCharCode(10)).filter(Boolean)) {
|
||||
let j; try { j = JSON.parse(fs.readFileSync(f, \"utf8\")); } catch (e) { continue; }
|
||||
if (!j || j.name === \"esbuild\") continue;
|
||||
for (const k of [\"dependencies\", \"optionalDependencies\", \"peerDependencies\"]) {
|
||||
if (j[k] && Object.prototype.hasOwnProperty.call(j[k], \"esbuild\")) { hit.push(f); break; }
|
||||
}
|
||||
}
|
||||
console.log(hit.join(\" \"));") || { echo "declarer scan itself failed" >&2; exit 1; };
|
||||
sites=$(find /usr/lib/node_modules /opt -type d -path "*/node_modules/esbuild" -prune 2>/dev/null);
|
||||
if [ -n "$declarers" ]; then
|
||||
[ -n "$sites" ] || { echo "declared by $declarers but NO install found - prune removed function" >&2; exit 1; };
|
||||
for d in $sites; do node -e "require(\"$d\").transformSync(\"const x:number=1\",{loader:\"ts\"})" || { echo "esbuild broken at $d" >&2; exit 1; }; done;
|
||||
echo "ok: declared ($(echo $declarers | wc -w) package(s)) and compiles at $(echo $sites | wc -w) site(s)";
|
||||
else
|
||||
[ -z "$sites" ] || { echo "nothing declares esbuild yet installs remain: $sites" >&2; exit 1; };
|
||||
echo "ok: nothing declares esbuild (pi >= 1.2.0 dropped chord); no install expected";
|
||||
fi'
|
||||
|
||||
# Clipboard is the family pruned second. Upstream pi 0.86.0 (#9163) REPLACED the
|
||||
# external `@mariozechner/clipboard` dependency with bundled macOS/Windows/X11
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
#!/usr/bin/env bash
|
||||
# test-link-pi-extensions.sh — two-sided tests for
|
||||
# rootfs/usr/local/lib/pi-devbox/link-pi-extensions.sh.
|
||||
#
|
||||
# Runs the real script against a throwaway HOME / PI_CODING_AGENT_DIR, so it never
|
||||
# touches a live ~/.pi/agent. Every "must link" case has a "must NOT touch" twin:
|
||||
# the script's whole contract is that it only ever creates a link where nothing
|
||||
# exists, so the negative cases are the important half — and one of them (a real
|
||||
# extension of the same name wins) is what keeps a user's own file safe.
|
||||
#
|
||||
# bash scripts/test-link-pi-extensions.sh # CI (lint.yml) and locally
|
||||
# LINK_PI_EXTENSIONS=/path/to/variant.sh bash scripts/test-link-pi-extensions.sh
|
||||
set -u
|
||||
here="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
SCRIPT="${LINK_PI_EXTENSIONS:-$here/rootfs/usr/local/lib/pi-devbox/link-pi-extensions.sh}"
|
||||
T="$(mktemp -d)"; trap 'rm -rf "$T"' EXIT
|
||||
PASS=0; FAIL=0
|
||||
ok() { printf ' ✅ %s\n' "$1"; PASS=$((PASS+1)); }
|
||||
bad() { printf ' ❌ %s — %s\n' "$1" "$2"; FAIL=$((FAIL+1)); }
|
||||
|
||||
# One fresh world per case: a shared dir holding two extensions (plus files that
|
||||
# are NOT extensions), an empty HOME, agent dir unset.
|
||||
world() {
|
||||
rm -rf "$T/w"; mkdir -p "$T/w/shared" "$T/w/home"
|
||||
printf 'export default {}\n' > "$T/w/shared/alpha.ts"
|
||||
printf 'export default {}\n' > "$T/w/shared/beta.ts"
|
||||
printf '{"providers":{}}\n' > "$T/w/shared/models.json"
|
||||
printf 'not an extension\n' > "$T/w/shared/README.md"
|
||||
SRC="$T/w/shared"; AG="$T/w/home/.pi/agent"; DEST="$AG/extensions"
|
||||
}
|
||||
# run [env assignments...] — run the script with HOME and PI_CODING_AGENT_DIR pointed into the world.
|
||||
# "$@" goes FIRST so an option like `-u PI_EXTENSIONS_SRC` is still parsed as one by env.
|
||||
run() { OUT="$(env "$@" HOME="$T/w/home" PI_CODING_AGENT_DIR="$AG" bash "$SCRIPT" 2>&1)"; RC=$?; }
|
||||
|
||||
echo "── link-pi-extensions.sh ($SCRIPT)"
|
||||
|
||||
world; run PI_EXTENSIONS_SRC="$SRC"
|
||||
if [ "$RC" = 0 ] && [ "$(readlink "$DEST/alpha.ts")" = "$SRC/alpha.ts" ] \
|
||||
&& [ "$(readlink "$DEST/beta.ts")" = "$SRC/beta.ts" ] && [[ "$OUT" == *"linked alpha.ts"* ]]; then
|
||||
ok "links every top-level *.ts when the shared dir exists and nothing is there (creates the extensions dir)"
|
||||
else bad "link when absent" "rc=$RC link=$(readlink "$DEST/alpha.ts" 2>&1) out=[$OUT]"; fi
|
||||
|
||||
run PI_EXTENSIONS_SRC="$SRC"
|
||||
if [ "$RC" = 0 ] && [ -z "$OUT" ] && [ "$(readlink "$DEST/alpha.ts")" = "$SRC/alpha.ts" ]; then
|
||||
ok "second run is a silent no-op (every container start runs it)"
|
||||
else bad "idempotent" "rc=$RC out=[$OUT]"; fi
|
||||
|
||||
world; run PI_EXTENSIONS_SRC="$SRC"
|
||||
if [ ! -e "$DEST/models.json" ] && [ ! -L "$DEST/models.json" ] \
|
||||
&& [ ! -e "$DEST/README.md" ] && [ ! -L "$DEST/README.md" ]; then
|
||||
ok "non-.ts files in the shared dir are ignored (models.json, README.md)"
|
||||
else bad "non-.ts ignored" "dest holds: $(ls -A "$DEST" 2>&1 | tr '\n' ' ')"; fi
|
||||
|
||||
# A nested .ts must NOT be linked: the dir is fleet pi config, not an extension
|
||||
# tree, and loading something nobody intended to publish as an extension is worse
|
||||
# than missing one.
|
||||
world; mkdir -p "$SRC/nested"; printf 'export default {}\n' > "$SRC/nested/gamma.ts"; run PI_EXTENSIONS_SRC="$SRC"
|
||||
if [ "$RC" = 0 ] && [ ! -e "$DEST/gamma.ts" ] && [ ! -L "$DEST/gamma.ts" ]; then
|
||||
ok "does not recurse — a nested *.ts is left alone (only top level is the contract)"
|
||||
else bad "no recursion" "rc=$RC found=$(readlink "$DEST/gamma.ts" 2>&1)"; fi
|
||||
|
||||
world; mkdir -p "$SRC/dir.ts"; run PI_EXTENSIONS_SRC="$SRC"
|
||||
if [ "$RC" = 0 ] && [ ! -e "$DEST/dir.ts" ] && [ ! -L "$DEST/dir.ts" ]; then
|
||||
ok "a directory whose name ends in .ts is not linked"
|
||||
else bad "dir named *.ts" "rc=$RC out=[$OUT]"; fi
|
||||
|
||||
world; mkdir -p "$DEST"; printf 'my own\n' > "$DEST/alpha.ts"; before="$(sha256sum "$DEST/alpha.ts")"; run PI_EXTENSIONS_SRC="$SRC"
|
||||
if [ "$RC" = 0 ] && [ ! -L "$DEST/alpha.ts" ] && [ "$(sha256sum "$DEST/alpha.ts")" = "$before" ] \
|
||||
&& [[ "$OUT" == NOTE*"real file"* ]] && [ "$(readlink "$DEST/beta.ts")" = "$SRC/beta.ts" ]; then
|
||||
ok "never replaces the user's own extension of that name (content unchanged, one NOTE line) — and still links the others"
|
||||
else bad "real file kept" "rc=$RC islink=$([ -L "$DEST/alpha.ts" ] && echo y) out=[$OUT]"; fi
|
||||
|
||||
world; mkdir -p "$DEST"; ln -s /somewhere/else.ts "$DEST/alpha.ts"; run PI_EXTENSIONS_SRC="$SRC"
|
||||
if [ "$RC" = 0 ] && [ "$(readlink "$DEST/alpha.ts")" = /somewhere/else.ts ] && [[ "$OUT" == NOTE*"left alone"* ]]; then
|
||||
ok "never re-points a link the user made to another file"
|
||||
else bad "foreign link kept" "rc=$RC link=$(readlink "$DEST/alpha.ts") out=[$OUT]"; fi
|
||||
|
||||
world; rm -rf "$SRC"; run PI_EXTENSIONS_SRC="$SRC"
|
||||
if [ "$RC" = 0 ] && [ ! -e "$DEST" ] && [ ! -L "$DEST" ] && [ -z "$OUT" ]; then
|
||||
ok "no shared dir → creates nothing and says nothing (the default for most users)"
|
||||
else bad "absent source" "rc=$RC out=[$OUT]"; fi
|
||||
|
||||
world; rm -f "$SRC"/*.ts; run PI_EXTENSIONS_SRC="$SRC"
|
||||
if [ "$RC" = 0 ] && [ ! -e "$DEST" ] && [ ! -L "$DEST" ] && [ -z "$OUT" ]; then
|
||||
ok "a shared dir with no *.ts → creates nothing (the dir is not treated as an extension itself)"
|
||||
else bad "empty source dir" "rc=$RC out=[$OUT]"; fi
|
||||
|
||||
for v in off OFF ""; do
|
||||
world; run PI_EXTENSIONS_SRC="$v"
|
||||
if [ "$RC" = 0 ] && [ ! -e "$DEST" ] && [ ! -L "$DEST" ] && [ -z "$OUT" ]; then
|
||||
ok "PI_EXTENSIONS_SRC='$v' disables it"
|
||||
else bad "disable '$v'" "rc=$RC out=[$OUT]"; fi
|
||||
done
|
||||
|
||||
world; run PI_EXTENSIONS_SRC="$SRC"; rm -f "$SRC"/alpha.ts; run PI_EXTENSIONS_SRC="$SRC"
|
||||
if [ "$RC" = 0 ] && [ -L "$DEST/alpha.ts" ] && [ "$(readlink "$DEST/alpha.ts")" = "$SRC/alpha.ts" ] && [ -z "$OUT" ]; then
|
||||
ok "our link whose target vanished (mount removed) is left as is, silently"
|
||||
else bad "dangling own link" "rc=$RC out=[$OUT]"; fi
|
||||
|
||||
# pi honours PI_CODING_AGENT_DIR, so the links must land THERE, not in $HOME/.pi/agent.
|
||||
world; AG="$T/w/elsewhere/agent"; DEST="$AG/extensions"; run PI_EXTENSIONS_SRC="$SRC"
|
||||
if [ "$RC" = 0 ] && [ "$(readlink "$DEST/alpha.ts" 2>/dev/null)" = "$SRC/alpha.ts" ] \
|
||||
&& [ ! -e "$T/w/home/.pi/agent/extensions" ]; then
|
||||
ok "links into PI_CODING_AGENT_DIR when set (and leaves \$HOME/.pi/agent alone)"
|
||||
else bad "PI_CODING_AGENT_DIR" "rc=$RC link=$(readlink "$DEST/alpha.ts" 2>&1) out=[$OUT]"; fi
|
||||
|
||||
# 'off' must be a switch, not merely a path that happens not to exist: with a
|
||||
# directory literally named "off" in the working directory it must still disable.
|
||||
world; mkdir -p "$T/w/off"; printf 'export default {}\n' > "$T/w/off/alpha.ts"
|
||||
OUT="$(cd "$T/w" && env PI_EXTENSIONS_SRC=off HOME="$T/w/home" PI_CODING_AGENT_DIR="$AG" bash "$SCRIPT" 2>&1)"; RC=$?
|
||||
if [ "$RC" = 0 ] && [ ! -e "$DEST" ] && [ ! -L "$DEST" ]; then
|
||||
ok "PI_EXTENSIONS_SRC=off disables it even when a directory named 'off' exists in the cwd"
|
||||
else bad "off is a switch" "rc=$RC link=$(readlink "$DEST/alpha.ts" 2>&1) out=[$OUT]"; fi
|
||||
|
||||
# The default is /opt/fleet-pi — the same dir that holds models.json. Whether it
|
||||
# exists, and whether it holds any *.ts, depends on where this runs (a fleet devbox:
|
||||
# maybe both; CI: neither), so derive the expectation instead of assuming it.
|
||||
world; run -u PI_EXTENSIONS_SRC
|
||||
default_ts="$(ls -1 /opt/fleet-pi/*.ts 2>/dev/null | head -1)"
|
||||
if [ -n "$default_ts" ]; then
|
||||
_dn="$(basename "$default_ts")"
|
||||
if [ "$RC" = 0 ] && [ "$(readlink "$DEST/$_dn" 2>/dev/null)" = "$default_ts" ]; then
|
||||
ok "PI_EXTENSIONS_SRC unset → default /opt/fleet-pi (here: holds $_dn) is linked"
|
||||
else bad "default path (with .ts)" "rc=$RC link=$(readlink "$DEST/$_dn" 2>&1) out=[$OUT]"; fi
|
||||
unset _dn
|
||||
else
|
||||
if [ "$RC" = 0 ] && [ ! -e "$DEST" ] && [ ! -L "$DEST" ] && [ -z "$OUT" ]; then
|
||||
ok "PI_EXTENSIONS_SRC unset → default /opt/fleet-pi (absent or has no *.ts here) → nothing"
|
||||
else bad "default path (no .ts)" "rc=$RC out=[$OUT]"; fi
|
||||
fi
|
||||
|
||||
echo "── $PASS passed, $FAIL failed"
|
||||
[ "$FAIL" = 0 ]
|
||||
Reference in New Issue
Block a user