Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 45b6239777 | |||
| d00eef2acb | |||
| fb35c549b5 | |||
| 649fc44c5b | |||
| 89a8dc7fab | |||
| 6625d66f3a | |||
| 8caafc3f49 | |||
| 71b12a9ed4 | |||
| fb49828826 | |||
| 02be95ac1f | |||
| d68674d11e | |||
| 8c27894cf2 | |||
| 291ae5345e | |||
| 38d8832d34 | |||
| 32586f19e7 | |||
| aaf1be0bcb | |||
| 92212fa447 | |||
| 3d46c6615e | |||
| fa6e9dc9d6 | |||
| bd0627a557 | |||
| 67da05b99b | |||
| 4563b4d76d | |||
| f19c35da32 | |||
| 6002c6299d | |||
| d73bf2e9d3 | |||
| 3a59e15563 | |||
| d1db595f17 | |||
| 26384fe9f1 | |||
| b33e9dc592 |
@@ -0,0 +1,35 @@
|
||||
# Keep the Docker build context minimal and prevent stray files (notably
|
||||
# `.git`) from ever being pulled in by a future broad COPY. Both Dockerfiles
|
||||
# only COPY `rootfs/` and `entrypoint*.sh`, so everything below is safe to
|
||||
# exclude from the context.
|
||||
#
|
||||
# DO NOT add `rootfs/`, `entrypoint.sh`, `entrypoint-user.sh`, or the
|
||||
# Dockerfiles here — they are required to build the image.
|
||||
|
||||
# VCS / CI metadata
|
||||
.git
|
||||
.gitea
|
||||
.gitignore
|
||||
.dockerignore
|
||||
|
||||
# Lint / editor config
|
||||
.hadolint.yaml
|
||||
.editorconfig
|
||||
|
||||
# Docs & project meta
|
||||
README.md
|
||||
DOCKER_HUB.md
|
||||
CHANGELOG.md
|
||||
AGENTS.md
|
||||
IDEAS.md
|
||||
LICENSE
|
||||
THIRD_PARTY.md
|
||||
docs
|
||||
|
||||
# Local orchestration & examples (compose runs the image; not a build input)
|
||||
docker-compose.yml
|
||||
docker-compose.mempalace.yml
|
||||
.env.example
|
||||
|
||||
# Repo tooling / tests (run from a checkout, not baked into the image)
|
||||
scripts
|
||||
@@ -9,6 +9,19 @@ WORKSPACE_PATH=~/projects
|
||||
# Path to SSH keys on host
|
||||
SSH_KEY_PATH=~/.ssh
|
||||
|
||||
# ── MemPalace memory (local by default) ───────────────────────────
|
||||
# By default the mempalace.ts extension spawns a LOCAL mempalace-mcp stdio
|
||||
# server (palace at ~/.mempalace). Uncomment the devbox-palace volume in
|
||||
# docker-compose.yml to persist it across container recreation.
|
||||
#
|
||||
# To instead share ONE MemPalace across containers/harnesses (pi + opencode
|
||||
# + native), set the URL below. When set, the extension connects over HTTP
|
||||
# and NO local mempalace-mcp is spawned; the devbox-palace volume is then
|
||||
# irrelevant. MEMPALACE_REMOTE_TOKEN, if set, is sent as a bearer token.
|
||||
# Serve it with: mempalace-mcp --transport http --host 0.0.0.0 --port 8765
|
||||
# MEMPALACE_REMOTE_URL=http://mempalace.lan:8765/mcp
|
||||
# MEMPALACE_REMOTE_TOKEN=
|
||||
|
||||
# ── LAN access from the container (host-OS-agnostic) ─────────────────
|
||||
# On VM-backed hosts (macOS OrbStack / Docker Desktop) the container can't
|
||||
# reach the host's directly-attached LAN peers by default. The entrypoint
|
||||
|
||||
@@ -38,6 +38,16 @@ concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}
|
||||
cancel-in-progress: false
|
||||
|
||||
# Gitea Actions' default step shell is `sh -e {0}` (dash), which rejects
|
||||
# bash-only syntax like `set -o pipefail`, `[[ ]]`, and arrays. Setting the
|
||||
# default to bash workflow-wide eliminates the whole class of "forgot
|
||||
# `shell: bash` on this step" bugs (hit twice: ed49b8d resolve-versions,
|
||||
# b7197e8/b33e9dc promote-base-latest). All existing dash steps use only
|
||||
# POSIX syntax, so bash (a superset) runs them unchanged.
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
env:
|
||||
BUILDKIT_PROGRESS: plain
|
||||
IMAGE: ${{ vars.DOCKERHUB_USERNAME }}/pi-devbox
|
||||
@@ -125,6 +135,7 @@ jobs:
|
||||
toolkit_ref: ${{ steps.resolve.outputs.toolkit_ref }}
|
||||
extensions_ref: ${{ steps.resolve.outputs.extensions_ref }}
|
||||
studio_ref: ${{ steps.resolve.outputs.studio_ref }}
|
||||
studio_tag: ${{ steps.resolve.outputs.studio_tag }}
|
||||
mempalace_toolkit_ref: ${{ steps.resolve.outputs.mempalace_toolkit_ref }}
|
||||
steps:
|
||||
- name: Resolve pi version + companion refs
|
||||
@@ -189,16 +200,40 @@ jobs:
|
||||
require_sha MEMPALACE_TOOLKIT_REF "$MEMPALACE_TOOLKIT_REF"
|
||||
echo "mempalace_toolkit_ref=${MEMPALACE_TOOLKIT_REF}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# pi-studio (omaclaren/pi-studio) → commit SHA for :latest-studio.
|
||||
STUDIO_REF=$(curl -sf -H "Accept: application/vnd.github.sha" \
|
||||
"https://api.github.com/repos/omaclaren/pi-studio/commits/main" || true)
|
||||
# pi-studio (omaclaren/pi-studio) → newest SEMVER TAG's commit SHA
|
||||
# for the :*-studio images. Upstream stopped publishing GitHub
|
||||
# *Releases* at v0.5.55 but keeps tagging every version (vX.Y.Z) and
|
||||
# pushing to main, so pinning main HEAD risked baking half-finished
|
||||
# commits that land after a tag. Take the newest stable tag instead.
|
||||
# List ALL tags in one `git ls-remote` call — the REST tags API
|
||||
# paginates at 100 and this repo already has >140 tags, so page 1 is
|
||||
# NOT guaranteed to hold the newest — pick the highest X.Y.Z with
|
||||
# `sort -V` (pre-releases like -rc1 excluded by the strict filter),
|
||||
# then resolve its commit SHA (a SHA, not a moving tag, preserves
|
||||
# cache-busting + reproducibility and is what require_sha demands).
|
||||
STUDIO_TAGS=$(git ls-remote --tags "https://github.com/omaclaren/pi-studio.git" || true)
|
||||
STUDIO_TAG=$(printf '%s\n' "$STUDIO_TAGS" | awk '{print $2}' \
|
||||
| sed -n 's#^refs/tags/##p' \
|
||||
| grep -E '^v?[0-9]+\.[0-9]+\.[0-9]+$' \
|
||||
| sort -V | tail -n1 || true)
|
||||
if [ -z "${STUDIO_TAG:-}" ]; then
|
||||
echo "::error::Could not resolve a pi-studio semver tag (git ls-remote empty/unreachable). Refusing to fall back to a floating ref."
|
||||
exit 1
|
||||
fi
|
||||
# Prefer the peeled ^{} line (annotated tags); fall back to the
|
||||
# direct ref (lightweight tags, which pi-studio currently uses).
|
||||
STUDIO_REF=$(printf '%s\n' "$STUDIO_TAGS" | awk -v t="refs/tags/${STUDIO_TAG}^{}" '$2==t{print $1}')
|
||||
if [ -z "$STUDIO_REF" ]; then
|
||||
STUDIO_REF=$(printf '%s\n' "$STUDIO_TAGS" | awk -v t="refs/tags/${STUDIO_TAG}" '$2==t{print $1}')
|
||||
fi
|
||||
require_sha PI_STUDIO_REF "$STUDIO_REF"
|
||||
echo "studio_ref=${STUDIO_REF}" >> "$GITHUB_OUTPUT"
|
||||
echo "studio_tag=${STUDIO_TAG}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
echo "Resolved PI_VERSION=${PI_VERSION}"
|
||||
echo "Resolved PI_FORK_REF=${FORK_REF}, PI_OBSMEM_REF=${OBSMEM_REF}"
|
||||
echo "Resolved PI_TOOLKIT_REF=${TOOLKIT_REF}, PI_EXTENSIONS_REF=${EXTENSIONS_REF}"
|
||||
echo "Resolved PI_STUDIO_REF=${STUDIO_REF}"
|
||||
echo "Resolved PI_STUDIO_REF=${STUDIO_REF} (pi-studio ${STUDIO_TAG})"
|
||||
echo "Resolved MEMPALACE_TOOLKIT_REF=${MEMPALACE_TOOLKIT_REF}"
|
||||
|
||||
# ── Phase 2: build & push base (multi-arch), only when needed ──────
|
||||
@@ -380,6 +415,7 @@ jobs:
|
||||
PI_EXTENSIONS_REF=${{ needs.resolve-versions.outputs.extensions_ref }}
|
||||
INSTALL_STUDIO=true
|
||||
PI_STUDIO_REF=${{ needs.resolve-versions.outputs.studio_ref }}
|
||||
PI_STUDIO_VERSION=${{ needs.resolve-versions.outputs.studio_tag }}
|
||||
MEMPALACE_TOOLKIT_REF=${{ needs.resolve-versions.outputs.mempalace_toolkit_ref }}
|
||||
RELEASE_TAG=smoke-studio
|
||||
SOURCE_REVISION=${{ github.sha }}
|
||||
@@ -521,6 +557,7 @@ jobs:
|
||||
TOOLKIT_REF: ${{ needs.resolve-versions.outputs.toolkit_ref }}
|
||||
EXTENSIONS_REF: ${{ needs.resolve-versions.outputs.extensions_ref }}
|
||||
STUDIO_REF: ${{ needs.resolve-versions.outputs.studio_ref }}
|
||||
STUDIO_TAG: ${{ needs.resolve-versions.outputs.studio_tag }}
|
||||
MEMPALACE_TOOLKIT_REF: ${{ needs.resolve-versions.outputs.mempalace_toolkit_ref }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
@@ -543,6 +580,7 @@ jobs:
|
||||
--build-arg "MEMPALACE_TOOLKIT_REF=${MEMPALACE_TOOLKIT_REF}" \
|
||||
--build-arg "INSTALL_STUDIO=true" \
|
||||
--build-arg "PI_STUDIO_REF=${STUDIO_REF}" \
|
||||
--build-arg "PI_STUDIO_VERSION=${STUDIO_TAG}" \
|
||||
--build-arg "RELEASE_TAG=${RELEASE_TAG}" \
|
||||
--build-arg "BUILD_DATE=${BUILD_DATE}" \
|
||||
--build-arg "SOURCE_REVISION=${GITHUB_SHA:-}" \
|
||||
@@ -600,6 +638,13 @@ jobs:
|
||||
-u ${{ vars.DOCKERHUB_USERNAME }} \
|
||||
-p "${{ secrets.DOCKERHUB_TOKEN }}"
|
||||
- name: Re-tag base-<hash> as base-latest (only if stale)
|
||||
# shell: bash is REQUIRED — Gitea Actions' default step shell is
|
||||
# `sh -e {0}` (dash), which rejects `set -o pipefail` with
|
||||
# "Illegal option -o pipefail" and aborts the step before the
|
||||
# crane digest-compare runs, leaving base-latest un-promoted.
|
||||
# Same footgun as ed49b8d (resolve-versions). Regression shipped
|
||||
# in b7197e8, caught on the v1.2.4 release (run 418).
|
||||
shell: bash
|
||||
env:
|
||||
BASE_HASH_REF: ${{ env.IMAGE }}:${{ needs.base-decide.outputs.base_tag }}
|
||||
BASE_LATEST_REF: ${{ env.IMAGE }}:base-latest
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
name: Lint
|
||||
|
||||
# Durable guard against CI-workflow bugs — most importantly the recurring
|
||||
# "bash-only syntax under the default `sh`/dash shell" footgun that broke
|
||||
# resolve-versions (ed49b8d) and promote-base-latest (b7197e8 → run 418).
|
||||
# actionlint runs shellcheck against each `run:` step using its *effective*
|
||||
# shell, so `set -o pipefail` under dash is flagged as SC3040 before any
|
||||
# expensive build runs. This is cheap (~10s) and independent of the build
|
||||
# pipeline, so it fires on every push/PR — not just on release tags, which
|
||||
# is where the build workflow (docker-publish.yml) is otherwise only
|
||||
# triggered.
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: lint-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
actionlint:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install shellcheck
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends shellcheck python3-yaml
|
||||
|
||||
- name: Gitea shell guard (catches the actionlint blind spot)
|
||||
# actionlint models GitHub Actions, where the default run shell is
|
||||
# bash, so it does NOT flag bash syntax in a step that merely OMITS
|
||||
# `shell:` — which is exactly how ed49b8d and b7197e8 manifested on
|
||||
# Gitea (default sh/dash). This guard enforces that every run: step
|
||||
# resolves to bash under Gitea's real defaults. Run it BEFORE
|
||||
# actionlint so the more precise diagnostic surfaces first.
|
||||
run: bash scripts/check-workflow-shell.sh .gitea/workflows
|
||||
|
||||
- name: Install actionlint (pinned)
|
||||
env:
|
||||
ACTIONLINT_VERSION: 1.7.7
|
||||
run: |
|
||||
curl -fsSL \
|
||||
"https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" \
|
||||
| tar -xz -C /usr/local/bin actionlint
|
||||
actionlint --version
|
||||
|
||||
- name: Run actionlint
|
||||
# SHELLCHECK_OPTS excludes pure-style codes (quoting/style opinions)
|
||||
# so the guard stays focused on correctness bugs — crucially the
|
||||
# SC3xxx "not POSIX / wrong shell" family that catches the pipefail
|
||||
# footgun. Do NOT exclude SC3040 (set -o pipefail under sh) or any
|
||||
# other SC3xxx code.
|
||||
env:
|
||||
SHELLCHECK_OPTS: "-e SC2086 -e SC2016 -e SC2129 -e SC2001 -e SC2312"
|
||||
# Pass explicit paths: actionlint's no-arg mode auto-detects a
|
||||
# project by looking for `.github/workflows`, which doesn't exist in
|
||||
# this `.gitea/workflows` repo and hard-fails with exit 3
|
||||
# ("no project was found"). Globbing the workflow files is the
|
||||
# supported way to lint a non-GitHub layout.
|
||||
run: actionlint -color .gitea/workflows/*.yml
|
||||
|
||||
hadolint:
|
||||
# Lint the two Dockerfiles that ARE the project (the shell/actions linting
|
||||
# above never looked at them). Config — ignored rules + failure threshold
|
||||
# — lives in .hadolint.yaml, which hadolint reads automatically, so a local
|
||||
# `hadolint Dockerfile.base` reproduces CI exactly.
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install hadolint (pinned)
|
||||
env:
|
||||
HADOLINT_VERSION: 2.14.0
|
||||
run: |
|
||||
curl -fsSL \
|
||||
"https://github.com/hadolint/hadolint/releases/download/v${HADOLINT_VERSION}/hadolint-Linux-x86_64" \
|
||||
-o /usr/local/bin/hadolint
|
||||
chmod +x /usr/local/bin/hadolint
|
||||
hadolint --version
|
||||
|
||||
- name: Run hadolint
|
||||
run: hadolint Dockerfile.base Dockerfile.variant
|
||||
@@ -0,0 +1,27 @@
|
||||
# hadolint configuration for pi-devbox.
|
||||
#
|
||||
# Both Dockerfiles are linted in CI (.gitea/workflows/lint.yml → `hadolint`
|
||||
# job). hadolint reads this file automatically, so a local
|
||||
# `hadolint Dockerfile.base` reproduces CI exactly.
|
||||
#
|
||||
# The ignores below are DELIBERATE project choices — they mirror the
|
||||
# philosophy of the shellcheck excludes already applied to `run:` steps
|
||||
# (SHELLCHECK_OPTS in lint.yml). Anything NOT listed here still fails the
|
||||
# build at `warning` and above, so new Dockerfile smells are caught going
|
||||
# forward.
|
||||
ignored:
|
||||
- DL3008 # "pin apt versions" — intentionally unpinned: the base tracks
|
||||
# Debian stable and runs `apt-get upgrade`, so pinning point
|
||||
# versions would rot and fight security updates.
|
||||
- DL3016 # "pin npm versions" — pi's version IS pinned, but via the
|
||||
# PI_VERSION build-arg (CI-resolved from npm), not the npm CLI.
|
||||
- DL4006 # "set -o pipefail before a pipe" — the piped RUNs are
|
||||
# download|extract steps with their own retries / `set -e`.
|
||||
# Switching the global SHELL to bash is a larger, base-affecting
|
||||
# change — tracked in IDEAS.md.
|
||||
- DL3003 # "use WORKDIR, not cd" — cosmetic in the few `cd` RUNs here.
|
||||
- SC2086 # "double-quote to prevent word-splitting" — the same code is
|
||||
# excluded for shell `run:` steps in lint.yml; splitting is
|
||||
# intentional in these contexts.
|
||||
|
||||
failure-threshold: warning
|
||||
@@ -22,13 +22,16 @@ re-brand of opencode-devbox's `pi-only` variant.
|
||||
by copying `/opt/pi-extensions/skill/` over the committed `rootfs/` snapshot
|
||||
(Option 1 over Option 2 — see `skills/VENDORED.md`).
|
||||
- `entrypoint.sh` — UID/GID alignment as root, then drops to `developer`.
|
||||
- `entrypoint-user.sh` — per-container start: SSH ControlMaster socket
|
||||
dir, LAN-access setup, MemPalace init, pi-toolkit + pi-extensions
|
||||
deploy, mempalace-bridge symlink, fork/recall + pi-studio pi-install,
|
||||
optional `studio-expose` bridge (when `STUDIO_EXPOSE=1`), image-baked
|
||||
skills symlink-in, skillset deploy.
|
||||
- `entrypoint-user.sh` — per-container start: prints the `pi-devbox-version`
|
||||
banner first (which build/commit is running, from the manifest below),
|
||||
then SSH ControlMaster socket dir, LAN-access setup, MemPalace init,
|
||||
pi-toolkit + pi-extensions deploy, mempalace-bridge symlink, fork/recall +
|
||||
pi-studio pi-install, optional `studio-expose` bridge (when
|
||||
`STUDIO_EXPOSE=1`), image-baked skills symlink-in, skillset deploy.
|
||||
- `rootfs/` — files baked into the image (bash aliases, inputrc,
|
||||
setup-lan-access.sh, `studio-expose` helper). Also
|
||||
setup-lan-access.sh, `studio-expose` helper, `pi-devbox-version` — wraps
|
||||
`/etc/pi-devbox/build-manifest.json` into a human-readable summary + live
|
||||
drift check, see README “Build provenance”). Also
|
||||
`usr/local/share/pi-devbox/skills/<name>/SKILL.md` — image-baked agent
|
||||
skills (the repo-authored `pi-devbox-environment`, plus vendored fallback
|
||||
copies of `pi-extensions` and `mempalace` — see `skills/VENDORED.md`)
|
||||
@@ -153,10 +156,14 @@ deprecated artifacts (to be removed in opencode-devbox v2.0.0).
|
||||
|
||||
## What we DON'T install (and why)
|
||||
|
||||
- **No texlive** (~600 MB–1 GB). Users who need PDF export from pandoc
|
||||
or pi-studio can install on demand: `sudo apt-get install texlive-xetex
|
||||
texlive-latex-recommended`. The planned `:latest-studio-tex` variant
|
||||
will bake this in.
|
||||
- **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`).
|
||||
- **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`).
|
||||
|
||||
+318
@@ -11,6 +11,324 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
|
||||
|
||||
---
|
||||
|
||||
## Unreleased
|
||||
|
||||
_Nothing yet — next release's changes will accrue here._
|
||||
|
||||
## v1.6.1 — 2026-07-22
|
||||
|
||||
Patch release. Headline: **pi `0.80.6` → `0.81.1`** (npm `latest`) — the first
|
||||
pi bump since v1.5.0.
|
||||
|
||||
> **Note on v1.6.0.** The `v1.6.0` git tag was cut on 2026-07-17 (agent-browser +
|
||||
> `pi-devbox-version`, see below) but never reached Docker Hub: the variant
|
||||
> publish was blocked by an intermittent SYN-drop fault on the on-prem CI
|
||||
> network (`ci-network-diagnosis.md`, since resolved). v1.6.1 lands v1.6.0's
|
||||
> content **plus** the pi bump in one release; there is no `v1.6.0` image on
|
||||
> Docker Hub. The `v1.6.0` git tag is left in place as an accurate record of
|
||||
> what was intended on that day.
|
||||
|
||||
### Changed
|
||||
|
||||
- **pi `0.80.6` → `0.81.1`.** The CI resolves `pi@latest` at build time; latest
|
||||
is now `0.81.1`. The intermediate `0.81.0` is deliberately skipped: 0.81.0
|
||||
removed the default stream fallback for extensions using the pre-0.81
|
||||
`@earendil-works/pi-agent-core` API, which `pi-observational-memory` relies
|
||||
on (`agentLoop` + `stream.result()` in the observer/reflector/dropper
|
||||
agents). 0.81.1 restored the fallback ([earendil-works/pi#6915][pi-6915]),
|
||||
making 0.81.1 — but not 0.81.0 — a safe drop-in. `pi-fork` only imports
|
||||
types from `pi-agent-core` and is unaffected. Everything since v1.5.0's
|
||||
baked `0.80.6` (i.e. `0.80.7`–`0.80.10`, `0.81.0`, `0.81.1`) was audited for
|
||||
breaking changes against the two baked extensions — none affect this image.
|
||||
The Node engine requirement rose to `>=22.19.0` in `0.81.0`; the base still
|
||||
ships `22.23.1` (nodesource 22.x), so no engine bump is needed. Highlights
|
||||
users inherit from the upstream jump: **local llama.cpp router support**
|
||||
(search + download Hugging Face models, explicit load/unload, live
|
||||
progress); **full pi-ai provider extensions** (extensions can now register
|
||||
complete providers with native auth, model refresh, filtering, and
|
||||
streaming); **Qwen Token Plan** subscription providers; **resilient
|
||||
compaction / branch-summary retries** on transient provider failures with
|
||||
lifecycle events exposed to interactive, JSON, RPC, and SDK consumers;
|
||||
expanded usage accounting for tools, compaction, and branch summaries.
|
||||
Base-affecting (npm install line rebuilds), so `base-<hash>` rebuilds. See
|
||||
the [pi changelog][pi-changelog] for the full list.
|
||||
|
||||
[pi-6915]: https://github.com/earendil-works/pi/issues/6915
|
||||
[pi-changelog]: https://github.com/earendil-works/pi/blob/main/CHANGELOG.md
|
||||
|
||||
## v1.6.0 — 2026-07-13
|
||||
|
||||
> ⚠️ **Never published to Docker Hub.** Tagged in git on 2026-07-17 but the
|
||||
> variant publish was blocked by a site-network fault before the image reached
|
||||
> the registry. Superseded by v1.6.1, which carries this release's content
|
||||
> forward alongside the `pi 0.81.1` bump.
|
||||
|
||||
### Added
|
||||
|
||||
- **`agent-browser` — headless browser automation, baked into every variant.**
|
||||
The base now ships the [`agent-browser`](https://www.npmjs.com/package/agent-browser)
|
||||
CLI plus a Playwright-fetched Chromium, so the agent can drive a real browser
|
||||
(open/click/fill/`eval`/screenshot/snapshot) and *verify* front-end work
|
||||
involving live DOM or WebGL instead of guessing. The `agent-browser` skill
|
||||
(from the skillset repo) was previously a no-op because the binary was
|
||||
absent; it now works out of the box. Two pieces: the standalone Rust CLI
|
||||
(npm, `NPM_CONFIG_PREFIX=/usr` so it survives the `~/.pi/npm-global` volume),
|
||||
and a Chromium fetched via `playwright install --with-deps chromium` into
|
||||
`PLAYWRIGHT_BROWSERS_PATH=/usr/local/share/ms-playwright` (a system path,
|
||||
never shadowed by the `/home/developer` volume — unlike agent-browser's own
|
||||
`~/.agent-browser/browsers` default). A stable `/usr/local/bin/agent-chrome`
|
||||
symlink, exported as `AGENT_BROWSER_EXECUTABLE_PATH`, insulates the config
|
||||
from Playwright's per-version `chromium-<rev>` directory name. Debian trixie
|
||||
`--with-deps` dependency resolution verified (the t64 renames are handled).
|
||||
The global AGENTS.md managed block
|
||||
(`rootfs/usr/local/share/pi-devbox/pi-global-AGENTS.append.md`) gains a short
|
||||
pointer so agents discover the capability. Adds ~625 MB (Chromium; Playwright's
|
||||
unused headless-shell build is dropped and the apt/npm caches cleaned in-layer
|
||||
to stay lean). Base-affecting, rebuilds `base-<hash>`.
|
||||
|
||||
- **`pi-devbox-version` command.** Wraps `/etc/pi-devbox/build-manifest.json`
|
||||
into a human-readable summary (release tag, build date, source revision,
|
||||
baked `pi_version`, and short SHAs for every `/opt` component) instead of
|
||||
requiring users to know the manifest path and pipe it through `jq`
|
||||
themselves. Also flags **live drift** — if `pi --version` no longer matches
|
||||
what was baked at build time, the `pi:` line calls that out rather than
|
||||
silently trusting the manifest. `--json` dumps the raw manifest for
|
||||
scripting; `--quiet` gives a one-line `release_tag (source_revision)` form.
|
||||
Printed automatically once at container start (`entrypoint-user.sh`, before
|
||||
the rest of the setup output), and stays available on demand for the rest
|
||||
of the session. Exits 1 with a short notice — rather than failing silently
|
||||
— on images built before this file existed. Base-affecting (new
|
||||
`rootfs/usr/local/bin/pi-devbox-version`), rebuilds `base-<hash>`.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Bundled `pi-toolkit` settings template: `pi-fork` balanced tier bumped to
|
||||
`eu.anthropic.claude-sonnet-5`** (was `claude-sonnet-4-6`), matching the model
|
||||
now in use. The image clones `pi-toolkit@main` into `/opt/pi-toolkit` at build
|
||||
time, so the next build bundles it automatically (pi-toolkit `0010417`); the
|
||||
same commit also refreshes the template's `enabledModels` and the README
|
||||
examples. Seed-only: existing containers keep their live `~/.pi/agent/settings.json`
|
||||
(the entrypoint merge is live-wins), so only fresh `~/.pi` volumes are affected.
|
||||
|
||||
## v1.5.0 — 2026-07-13
|
||||
|
||||
### Added
|
||||
|
||||
- **Seeded global gitignore now ignores `**/.claude/settings.local.json`.** Claude
|
||||
Code's per-machine local settings file holds machine-specific permissions and
|
||||
can carry credentials, so it should never be committed. The seed
|
||||
(`rootfs/home/developer/.gitignore_global`, baked to `/etc/skel-devbox/`) gains
|
||||
the pattern so fresh containers match a host global that already ignores it.
|
||||
Existing containers are unaffected (the seed is copied only when
|
||||
`~/.gitignore_global` is absent); their file can be updated by hand. Base-
|
||||
affecting (`Dockerfile.base` COPY of the seed), rebuilds `base-<hash>`.
|
||||
|
||||
- **Readable Neovim colours out of the box.** The base now ships a system-wide
|
||||
Neovim config (`/etc/xdg/nvim/sysinit.vim`) that enables `termguicolors`,
|
||||
plus the `kitty-terminfo` package. Vanilla Neovim otherwise fell back to a
|
||||
256-colour palette over ssh/kitty and rendered strings and comments in a
|
||||
muddy, low-contrast dark colour. `sysinit.vim` is Neovim's system vimrc: it
|
||||
loads for every user before any personal `~/.config/nvim` and can still be
|
||||
overridden per-user (`:set notermguicolors`, or your own init). Base-affecting
|
||||
(`Dockerfile.base` apt package + COPY), rebuilds `base-<hash>`.
|
||||
|
||||
- **Terminal support beyond kitty: `ncurses-term` + a compiled `xterm-ghostty`
|
||||
alias.** The base previously shipped only `ncurses-base` (xterm-256color,
|
||||
tmux), so SSHing in from a modern emulator degraded to a dumb fallback. The
|
||||
base now installs `ncurses-term` (terminfo for WezTerm, Alacritty, foot, st,
|
||||
and the base `ghostty` entry, among many others) and compiles an
|
||||
`xterm-ghostty` alias with `tic -x` (`use=ghostty`) — Ghostty connects as
|
||||
`TERM=xterm-ghostty` and no distro packages that name. Combined with
|
||||
`kitty-terminfo` (xterm-kitty) and xterm-256color (iTerm2's default, already
|
||||
in ncurses-base), the common modern terminals now resolve their TERM. The
|
||||
approach mirrors the maintainer's ansible `common` role. Base-affecting
|
||||
(`Dockerfile.base` apt + COPY + `tic` RUN, plus a new
|
||||
`rootfs/usr/local/share/terminfo-src/ghostty.terminfo`), rebuilds `base-<hash>`.
|
||||
|
||||
- **Repository hygiene: `LICENSE`, `THIRD_PARTY.md`, and `.dockerignore`.** The
|
||||
repo declared MIT only in prose; it now ships an actual `LICENSE` file (MIT,
|
||||
© Joakim Persson) plus `THIRD_PARTY.md` recording that the published images
|
||||
bundle third-party software under its own terms (pi, pi-fork,
|
||||
pi-observational-memory, pi-studio — all MIT; gosu Apache-2.0; Debian packages
|
||||
under their respective licenses). A new `.dockerignore` trims the build
|
||||
context to what the Dockerfiles actually `COPY` (`rootfs/` + `entrypoint*.sh`),
|
||||
keeping `.git`, docs, `scripts/`, and compose files out — cheaper context and
|
||||
no risk of a future broad `COPY` pulling in `.git`. Not base-affecting (the
|
||||
base hash covers only `Dockerfile.base` + `rootfs/` + `entrypoint*.sh`);
|
||||
image contents are byte-identical.
|
||||
|
||||
- **Dockerfile linting (`hadolint`) in CI, plus an `IDEAS.md` backlog.** The
|
||||
lint workflow already ran actionlint + shellcheck on `run:` steps but never
|
||||
looked at the two Dockerfiles that are the heart of the project. A new
|
||||
`hadolint` job (pinned v2.14.0, same download-pin pattern as actionlint) lints
|
||||
`Dockerfile.base` and `Dockerfile.variant`; `.hadolint.yaml` grandfathers the
|
||||
deliberate choices (unpinned apt/npm, `cd`-in-`RUN`, `SC2086` — mirroring the
|
||||
existing shellcheck excludes) and fails on anything new at `warning`+.
|
||||
`IDEAS.md` parks the vetted-but-unscheduled follow-ups (SHA-pin CI actions,
|
||||
trivy scanning, buildx SBOM/provenance attestations, a local `Makefile`,
|
||||
renovate). Repo/CI only — not baked into the image.
|
||||
|
||||
### Changed
|
||||
|
||||
- **`-studio` images now pin pi-studio to its newest *semver tag* instead of
|
||||
`main` HEAD.** Upstream `omaclaren/pi-studio` abandoned GitHub *Releases* at
|
||||
v0.5.55 but keeps tagging every version (currently `v0.9.36`) and pushing to
|
||||
`main`; tracking `main` HEAD risked baking half-finished commits that land
|
||||
after a tag. CI (`resolve-versions`) now lists every tag via a single
|
||||
`git ls-remote` (the REST tags API paginates at 100 and the repo already has
|
||||
>140 tags), selects the highest `X.Y.Z` with `sort -V` (pre-releases
|
||||
excluded by a strict filter), and pins that tag's commit SHA into
|
||||
`PI_STUDIO_REF`. Pinning the SHA (not the moving tag) preserves cache-busting
|
||||
and reproducibility, is what `require_sha` demands, and is recorded in the
|
||||
`se.jordbo.pi-devbox.pi-studio-ref` image label. The human-readable tag (e.g.
|
||||
`v0.9.36`) is now also recorded in a new `se.jordbo.pi-devbox.pi-studio-version`
|
||||
label for at-a-glance identification (`docker inspect`). Studio-variant only —
|
||||
not base-affecting; takes effect on the next `-studio` build. No change to the
|
||||
resolved commit today (`v0.9.36` == current `main` HEAD).
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`pandoc --pdf-engine=typst` now works without `-V mainfont`.** pandoc's
|
||||
bundled typst template (`/usr/share/pandoc/data/templates/template.typst`)
|
||||
defaults the document font to an empty tuple (`font: ()`), so a naked
|
||||
`pandoc --pdf-engine=typst` (and `studio_export_pdf` in some cases) failed
|
||||
with `error: font fallback list must not be empty` unless the caller passed
|
||||
`-V mainfont="..."`. The base now patches that template default to
|
||||
`Libertinus Serif` (typst's own bundled default font) at build time, so PDF
|
||||
export works out of the box. Base-affecting (`Dockerfile.base` RUN), rebuilds
|
||||
`base-<hash>`. README gains a "Generating a PDF with pandoc + typst" section
|
||||
with the working command and how to override the font via `-V mainfont`.
|
||||
|
||||
---
|
||||
|
||||
## v1.4.0 — 2026-07-11
|
||||
|
||||
Minor release. Headline: **PDF export works out of the box** — the base now
|
||||
ships **`typst`** as the pandoc PDF engine (`pandoc --pdf-engine=typst`), so
|
||||
`studio_export_pdf` / `pandoc -o out.pdf` no longer fail with "xelatex not
|
||||
found". Also adds a **host SSH reachability check at shell startup**. Both are
|
||||
base-affecting (`Dockerfile.base` apt+RUN for typst/xz-utils; `.bash_aliases`
|
||||
for the SSH check is COPYd into the base), so the base rebuilds and both land in
|
||||
`base-<hash>`. pi auto-resolves `latest` at build time (0.80.3 → 0.80.6);
|
||||
mempalace stays pinned at 3.5.0 (current PyPI latest).
|
||||
|
||||
### Added
|
||||
|
||||
- **Host SSH reachability check at shell startup.** `~/.bash_aliases` (baked
|
||||
into the image) now runs a one-time SSH probe on the first bash session of
|
||||
each container. If the Mac host is not reachable (Remote Login disabled or
|
||||
the `devbox_jump` key not yet authorized) it prints a clear warning with the
|
||||
exact two steps to fix it, including the container's public key inline.
|
||||
Subsequent shells in the same container skip the check (flag in `/tmp`,
|
||||
cleared on recreate). Silent when SSH is working. Complements the existing
|
||||
key-generation message in `setup-lan-access.sh` which only fires once at key
|
||||
creation time and can easily be missed. Commit `4563b4d`.
|
||||
|
||||
- **`typst` — lightweight PDF engine for pandoc (Markdown→PDF).** `pandoc` has
|
||||
shipped in the base since v1.0.0 but as a front-end only — with no PDF
|
||||
back-end installed, `studio_export_pdf` / `pandoc -o out.pdf` failed with
|
||||
"xelatex not found". The base now installs `typst`, a single ~30 MB static
|
||||
Rust binary (no LaTeX), used via `pandoc --pdf-engine=typst`. Chosen over a
|
||||
~600 MB TeX Live install; a fuller TeX Live remains the higher-fidelity
|
||||
fallback for anyone needing LaTeX-exact output (install on demand). Also adds
|
||||
`xz-utils` to the apt layer (typst ships a `.tar.xz` asset that `tar` needs
|
||||
`xz` to extract). Installed with the standard `latest` GitHub-release idiom;
|
||||
pin with `--build-arg TYPST_VERSION=vX.Y.Z`. This lands in `base-<hash>`
|
||||
(Dockerfile.base changed). Supersedes the previously-planned
|
||||
`:latest-studio-tex` variant — typst is small enough to ship in BASE, so no
|
||||
separate TeX variant is needed. See `pi-devbox-roadmap`.
|
||||
|
||||
---
|
||||
|
||||
## v1.3.0 — 2026-07-02
|
||||
|
||||
Minor release. Headline: **shared/external MemPalace** — the `mempalace.ts`
|
||||
bridge can now point at one MemPalace HTTP server (`MEMPALACE_REMOTE_URL`,
|
||||
optional `MEMPALACE_REMOTE_TOKEN`) shared across containers/harnesses instead of
|
||||
a per-container local palace; ships `docker-compose.mempalace.yml` for the
|
||||
server. Also ships the **`nano` + `micro`** non-modal editors and a **CI
|
||||
workflow-lint layer** (Gitea-accurate sh-vs-bash guard + actionlint/shellcheck),
|
||||
with the `docker-publish.yml` bash-defaults and `promote-base-latest` shell
|
||||
fixes. pi stays `0.80.3`; the base image rebuilds (the mempalace-toolkit ref
|
||||
advanced and `Dockerfile.base` gained nano/micro), so the new bridge and editors
|
||||
land in `base-<hash>`.
|
||||
|
||||
### Added
|
||||
|
||||
- **Share one MemPalace across containers via `MEMPALACE_REMOTE_URL`.** The
|
||||
`mempalace.ts` bridge (from `mempalace-toolkit`) can now connect to a shared
|
||||
MemPalace over HTTP instead of spawning a per-container local server: set
|
||||
`MEMPALACE_REMOTE_URL=http://<host>:8765/mcp` (optionally
|
||||
`MEMPALACE_REMOTE_TOKEN`) in `.env` and no local `mempalace-mcp` is spawned.
|
||||
A new `docker-compose.mempalace.yml` stands up such a shared server
|
||||
(`mempalace-mcp --transport http`). Leaving the URL unset keeps the default
|
||||
local-per-container palace. See `.env.example`. (The HTTP transport is
|
||||
unauthenticated — keep it on a trusted network or behind a reverse proxy.)
|
||||
|
||||
- **Two non-modal terminal editors alongside `nvim`: `nano` and `micro`.**
|
||||
The image previously shipped only `nvim` (with `EDITOR=nvim`), a modal
|
||||
vi-style editor. Not everyone is comfortable with vi keybindings, so both
|
||||
a classic and a modern non-modal option now ship:
|
||||
- **`nano`** (apt) — ~2.8 MB installed. Its dependencies (`libc6`,
|
||||
`libncursesw6`, `libtinfo6`) are already present via `nvim`/`less`/`htop`/
|
||||
`tmux`, so it pulls in **no extra packages**. On-screen shortcut hints
|
||||
(`^O` write, `^X` exit) make it the lowest-friction fallback.
|
||||
- **`micro`** — ~12 MB, a single static Go binary installed from GitHub
|
||||
releases (same pattern as `bat`/`eza`/`zoxide`). Desktop-style keybindings
|
||||
(`Ctrl+S` save, `Ctrl+Q` quit, `Ctrl+C/V/X`, `Ctrl+Z` undo), mouse
|
||||
support, and syntax highlighting out of the box. Pin with
|
||||
`--build-arg MICRO_VERSION=vX.Y.Z`; defaults to `latest`.
|
||||
|
||||
Combined footprint is ~15 MB (<0.5% of the ~3.2 GB image). **`EDITOR`
|
||||
stays `nvim`** — the new editors are opt-in via `export EDITOR=micro`
|
||||
(or `nano`) and/or `git config --global core.editor micro`.
|
||||
|
||||
Note: micro's upstream repo moved `zyedidia/micro` → `micro-editor/micro`;
|
||||
the Dockerfile uses the canonical URL because the old org's
|
||||
`/releases/latest` redirect lands on another `/latest` URL (the org
|
||||
rename), which would defeat the tag-parsing `latest`-resolution idiom.
|
||||
These are base-image additions, so they only land once the `base-<hash>`
|
||||
rebuilds (this file changed, so the next build picks them up).
|
||||
|
||||
### Added (CI)
|
||||
|
||||
- **Workflow lint (`.gitea/workflows/lint.yml`) running on every push and PR.**
|
||||
Two complementary checks, so CI-workflow bugs are caught before an expensive
|
||||
build runs:
|
||||
- **`scripts/check-workflow-shell.sh`** — a Gitea-accurate guard that fails
|
||||
if any `run:` step doesn't resolve to `bash` under Gitea's real defaults.
|
||||
This catches the exact recurrence class (omit `shell:`, use bash syntax),
|
||||
which **actionlint alone does not** — actionlint models GitHub Actions
|
||||
(default shell = bash) and so assumes a shell-less step is bash, whereas
|
||||
Gitea's default is `sh`/dash.
|
||||
- **`actionlint` + `shellcheck`** — catches explicit `shell: sh` + bash
|
||||
syntax (SC3040 etc.), expression errors, and general workflow mistakes.
|
||||
Style-only shellcheck codes are excluded; the SC3xxx "wrong shell" family
|
||||
is kept.
|
||||
|
||||
### Changed (CI)
|
||||
|
||||
- **Workflow-level `defaults: run: shell: bash` in `docker-publish.yml`.**
|
||||
Gitea Actions defaults each `run:` step to `sh` (dash), so every bash-syntax
|
||||
step had to individually remember `shell: bash` — a discipline requirement
|
||||
that failed twice (ed49b8d, b7197e8). Setting the default workflow-wide
|
||||
eliminates the whole class. All pre-existing dash steps use only POSIX
|
||||
syntax, so bash (a superset) runs them unchanged.
|
||||
|
||||
### Fixed (CI)
|
||||
|
||||
- **`promote-base-latest` now sets `shell: bash` on the base-latest re-tag
|
||||
step.** The `b7197e8` fix (v1.2.4) moved the digest-compare into that step
|
||||
with `set -euo pipefail`, but Gitea Actions' default step shell is `sh`
|
||||
(dash), which rejects `-o pipefail` (`Illegal option -o pipefail`) and aborts
|
||||
the step before the `crane copy` runs. On the v1.2.4 release (run 418) this
|
||||
left `base-latest` un-promoted, still pointing at the v1.2.3 base — the four
|
||||
consumer tags (`v1.2.4`, `latest`, `v1.2.4-studio`, `latest-studio`) were
|
||||
unaffected because they `FROM` the exact `base-<hash>`, not `base-latest`.
|
||||
Same footgun as `ed49b8d` (`resolve-versions needs shell: bash`).
|
||||
|
||||
---
|
||||
|
||||
## v1.2.4 — 2026-06-29
|
||||
|
||||
Patch release. Headline: **pi `0.80.2` → `0.80.3`** (npm `latest`). Also ships a
|
||||
|
||||
+2
-1
@@ -156,4 +156,5 @@ Optional volumes for MemPalace (commented out by default — uncomment in `docke
|
||||
|
||||
## License
|
||||
|
||||
MIT (the image; pi and the bundled tools each carry their own licenses).
|
||||
MIT (the image; pi and the bundled tools each carry their own licenses). See
|
||||
`LICENSE` and `THIRD_PARTY.md` in the [source repo](https://gitea.jordbo.se/joakimp/pi-devbox).
|
||||
|
||||
+170
-2
@@ -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-06-09 (v1.0.0 — decoupled from opencode-devbox)
|
||||
# BASE_REBUILD_DATE: 2026-07-13 (Unreleased — agent-browser CLI + Playwright Chromium for headless browser automation; prior: typst PDF engine + xz-utils + pandoc typst-template default-font patch)
|
||||
#
|
||||
# ── Lineage note ─────────────────────────────────────────────────────
|
||||
# Adapted from opencode-devbox/Dockerfile.base (commit before v1.16.2).
|
||||
@@ -46,7 +46,14 @@ ENV DEBIAN_FRONTEND=noninteractive
|
||||
# Additions vs the upstream opencode-devbox base (2026-06-09):
|
||||
# pandoc — Markdown↔HTML/PDF/etc. conversion. Required by pi-studio
|
||||
# preview/export pipelines and broadly useful for any
|
||||
# agent-driven document workflow. ~200 MB.
|
||||
# agent-driven document workflow. ~200 MB. NOTE: pandoc is
|
||||
# only the front-end — PDF output needs a back-end engine.
|
||||
# We ship `typst` (installed further down) as the
|
||||
# lightweight default engine (`pandoc --pdf-engine=typst`)
|
||||
# instead of a ~600 MB TeX Live install.
|
||||
# xz-utils — `xz` decompressor. tar shells out to it for `.tar.xz`
|
||||
# assets (typst ships .tar.xz). ~0.5 MB. Also generally
|
||||
# useful for extracting xz-compressed archives.
|
||||
# graphviz — `dot` rendering for many diagram tools. ~10 MB.
|
||||
# See the bundled `dot-watch` helper for live .dot -> PNG
|
||||
# re-render (handy with pi-studio's image preview).
|
||||
@@ -57,6 +64,25 @@ ENV DEBIAN_FRONTEND=noninteractive
|
||||
# pi-studio's container-loopback server to the container's
|
||||
# external interface so a published port can reach it.
|
||||
# ~1 MB; generally useful for any port-forwarding need.
|
||||
# nano — small, non-modal terminal editor for users who don't want
|
||||
# a vi-based editor. ~2.8 MB installed; its deps (libc6,
|
||||
# libncursesw6, libtinfo6) are already pulled in by nvim/less/
|
||||
# htop/tmux, so it adds no extra packages. Companion to nvim
|
||||
# and the `micro` binary installed further down. EDITOR stays
|
||||
# nvim; users opt in via `export EDITOR=nano`.
|
||||
# kitty-terminfo — terminfo entry for the kitty terminal (TERM=xterm-kitty).
|
||||
# ~77 KB, terminfo file only (no kitty binary). Without it,
|
||||
# ncurses apps fall back and Neovim can't reliably detect
|
||||
# true-colour from kitty over ssh; installing it makes
|
||||
# TERM=xterm-kitty understood. Pairs with the system-wide
|
||||
# Neovim termguicolors default (etc/xdg/nvim/sysinit.vim).
|
||||
# ncurses-term — broad terminfo bundle (wezterm, alacritty, foot, st, the
|
||||
# base `ghostty` entry, and many more) so SSHing in from a
|
||||
# modern emulator resolves its TERM instead of degrading to a
|
||||
# dumb fallback. xterm-kitty is NOT in it (hence kitty-terminfo
|
||||
# above); TERM=xterm-ghostty is compiled from an alias further
|
||||
# down (ncurses ships `ghostty`, not `xterm-ghostty`). iTerm2
|
||||
# defaults to xterm-256color (ncurses-base), so needs nothing.
|
||||
RUN apt-get update && \
|
||||
apt-get upgrade -y --no-install-recommends && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
@@ -89,9 +115,13 @@ RUN apt-get update && \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
pandoc \
|
||||
xz-utils \
|
||||
graphviz \
|
||||
imagemagick \
|
||||
socat \
|
||||
nano \
|
||||
kitty-terminfo \
|
||||
ncurses-term \
|
||||
&& ln -s /usr/bin/fdfind /usr/local/bin/fd \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
@@ -231,6 +261,33 @@ RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "arm64" ;
|
||||
ln -s /opt/nvim-linux-${ARCH}/bin/nvim /usr/local/bin/nvim && \
|
||||
nvim --version | head -1
|
||||
|
||||
# micro — modern, non-modal terminal editor. Ships alongside nvim so users
|
||||
# who aren't comfortable with vi-style modal editing have a friendly option:
|
||||
# desktop-style keybindings (Ctrl+S save, Ctrl+Q quit, Ctrl+C/V/X, Ctrl+Z
|
||||
# undo), mouse support, and syntax highlighting out of the box. A single
|
||||
# static Go binary (~12 MB) installed from GitHub releases, exactly like
|
||||
# bat/eza/zoxide below. EDITOR stays nvim (see below); users opt in with
|
||||
# `export EDITOR=micro` or `git config --global core.editor micro`.
|
||||
#
|
||||
# NOTE: upstream moved zyedidia/micro -> micro-editor/micro. The old org URL
|
||||
# still 302s, but its /releases/latest redirect lands on ANOTHER /latest URL
|
||||
# (the org rename), so the tag-parsing idiom below would resolve "latest"
|
||||
# instead of a version. Use the canonical micro-editor/micro URL.
|
||||
# Arch asset naming differs from the others: amd64 -> linux64, arm64 ->
|
||||
# linux-arm64. The tarball extracts to micro-<version>/micro.
|
||||
ARG MICRO_VERSION=latest
|
||||
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "linux64" ;; arm64) echo "linux-arm64" ;; *) echo "linux64" ;; esac) && \
|
||||
V="${MICRO_VERSION}" && \
|
||||
if [ "$V" = "latest" ]; then \
|
||||
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/micro-editor/micro/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
||||
fi && \
|
||||
V="${V#v}" && [ -n "$V" ] && \
|
||||
echo "Installing micro ${V}" && \
|
||||
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/micro-editor/micro/releases/download/v${V}/micro-${V}-${ARCH}.tar.gz" | tar -xz -C /tmp && \
|
||||
install /tmp/micro-${V}/micro /usr/local/bin/micro && \
|
||||
rm -rf /tmp/micro-${V} && \
|
||||
micro --version
|
||||
|
||||
# bat
|
||||
ARG BAT_VERSION=latest
|
||||
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
||||
@@ -387,6 +444,11 @@ ENV LANG=en_US.UTF-8
|
||||
ENV LANGUAGE=en_US:en
|
||||
ENV LC_ALL=en_US.UTF-8
|
||||
ENV EDITOR=nvim
|
||||
# Advertise 24-bit colour so colour-aware tools (Neovim's own auto-detect, bat,
|
||||
# delta, ...) use true colour instead of a 256-colour fallback. Safe for the
|
||||
# modern terminals this devbox targets; override by exporting `COLORTERM=`
|
||||
# (empty) from a terminal that lacks true-colour support.
|
||||
ENV COLORTERM=truecolor
|
||||
ENV PATH="/home/developer/.local/bin:/home/developer/.cargo/bin:${PATH}"
|
||||
|
||||
# ── Node.js (required for pi + MCP servers + tldr) ──
|
||||
@@ -395,6 +457,58 @@ RUN curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors https://deb.nodesour
|
||||
apt-get install -y --no-install-recommends nodejs && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# ── agent-browser — headless browser automation for the agent ────────
|
||||
# Gives the agent a real browser it can drive (open/click/fill/eval/
|
||||
# screenshot) so front-end work involving live DOM or WebGL can be VERIFIED
|
||||
# rather than guessed at. The `agent-browser` skill (shipped from the
|
||||
# skillset repo, not this image) documents the CLI; without this block that
|
||||
# skill is a no-op because the binary isn't present. Verified end-to-end
|
||||
# 2026-07-13: drives the baked Chromium headless (open + screenshot + eval
|
||||
# into a WebGL SPA) — doctor's launch test passes in ~0.5s.
|
||||
#
|
||||
# TWO pieces, because agent-browser is a standalone Rust CLI that ships NO
|
||||
# browser of its own — it only drives one you provide:
|
||||
# 1. the CLI itself (npm; ~70 MB of prebuilt native binaries), and
|
||||
# 2. a Chromium, which we fetch via Playwright.
|
||||
#
|
||||
# Why Playwright fetches the browser (and NOT `agent-browser install`):
|
||||
# agent-browser's own installer drops Chrome under ~/.agent-browser/browsers
|
||||
# — inside /home/${USER_NAME}, which is a NAMED VOLUME at runtime, so a
|
||||
# build-time download would be SHADOWED (invisible) once the volume mounts.
|
||||
# Playwright honours PLAYWRIGHT_BROWSERS_PATH, so we place the browser under
|
||||
# /usr/local/share (never shadowed) and hand agent-browser a STABLE symlink
|
||||
# via AGENT_BROWSER_EXECUTABLE_PATH — the symlink insulates the ENV from
|
||||
# Playwright's per-version, per-ARCH browser directory (`chrome-linux` on arm64,
|
||||
# `chrome-linux64` on amd64 — Chrome-for-Testing), so we `find` the `chrome`
|
||||
# binary rather than hardcode the path; the headless-shell binary is named
|
||||
# `chrome-headless-shell`, so `-name chrome` skips it.
|
||||
#
|
||||
# `playwright install --with-deps chromium` also apt-installs Chromium's
|
||||
# runtime libs; verified to resolve correctly on Debian trixie (exit 0 — the
|
||||
# t64 library renames are handled by Playwright's dep list). Build runs as
|
||||
# root, so the apt step works. NPM_CONFIG_PREFIX=/usr keeps both CLIs on /usr
|
||||
# so they survive the ~/.pi/npm-global volume mount (same trick the variant
|
||||
# uses for pi). After fetching, we DROP Playwright's `chromium_headless_shell-*`
|
||||
# build — agent-browser drives the full chrome (verified, incl. headless), so the
|
||||
# headless shell is dead weight — and clean the apt/npm caches, trimming the
|
||||
# layer to ~625 MB (Chromium) from ~960 MB. Still the bulk of the base's size,
|
||||
# and the one real tradeoff of shipping this to every variant.
|
||||
ARG AGENT_BROWSER_VERSION=latest
|
||||
ARG PLAYWRIGHT_VERSION=latest
|
||||
ENV PLAYWRIGHT_BROWSERS_PATH=/usr/local/share/ms-playwright
|
||||
RUN NPM_CONFIG_PREFIX=/usr npm install -g \
|
||||
"agent-browser@${AGENT_BROWSER_VERSION}" \
|
||||
"playwright@${PLAYWRIGHT_VERSION}" && \
|
||||
playwright install --with-deps chromium && \
|
||||
CHROME="$(find "${PLAYWRIGHT_BROWSERS_PATH}" -type f -name chrome -path '*/chromium-*/*' | head -n1)" && \
|
||||
[ -n "$CHROME" ] && ln -sf "$CHROME" /usr/local/bin/agent-chrome && \
|
||||
agent-browser --version && \
|
||||
test -x "$(readlink -f /usr/local/bin/agent-chrome)" && \
|
||||
rm -rf "${PLAYWRIGHT_BROWSERS_PATH}"/chromium_headless_shell-* && \
|
||||
npm cache clean --force && \
|
||||
rm -rf /var/lib/apt/lists/* /root/.npm /tmp/*
|
||||
ENV AGENT_BROWSER_EXECUTABLE_PATH=/usr/local/bin/agent-chrome
|
||||
|
||||
# ── tldr (tealdeer) — community-maintained command examples ──────────
|
||||
# Tealdeer is a Rust port of the tldr-pages client; ~5 MB static binary,
|
||||
# ~135 MB smaller than the Node tldr global. Same `tldr` command, same UX.
|
||||
@@ -410,6 +524,41 @@ RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64"
|
||||
chmod +x /usr/local/bin/tldr && \
|
||||
tldr --version
|
||||
|
||||
# ── 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).
|
||||
#
|
||||
# Follows the `latest` GitHub-release convention (like tealdeer/uv/bat).
|
||||
# typst ships a `.tar.xz` asset (hence xz-utils in the apt layer above)
|
||||
# that extracts to typst-<arch>-unknown-linux-musl/typst. Pin a specific
|
||||
# tag with --build-arg TYPST_VERSION=vX.Y.Z.
|
||||
#
|
||||
# We also patch pandoc's bundled typst template
|
||||
# (/usr/share/pandoc/data/templates/template.typst): its conf() defaults the
|
||||
# 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.
|
||||
ARG TYPST_VERSION=latest
|
||||
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
||||
V="${TYPST_VERSION}" && \
|
||||
if [ "$V" = "latest" ]; then \
|
||||
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/typst/typst/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
||||
fi && \
|
||||
V="${V#v}" && [ -n "$V" ] && \
|
||||
echo "Installing typst ${V}" && \
|
||||
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/typst/typst/releases/download/v${V}/typst-${ARCH}-unknown-linux-musl.tar.xz" | tar -xJ -C /tmp && \
|
||||
install /tmp/typst-${ARCH}-unknown-linux-musl/typst /usr/local/bin/typst && \
|
||||
rm -rf /tmp/typst-${ARCH}-unknown-linux-musl && \
|
||||
typst --version && \
|
||||
sed -i 's/^ font: (),$/ font: ("Libertinus Serif",),/' /usr/share/pandoc/data/templates/template.typst && \
|
||||
grep -q 'font: ("Libertinus Serif",),' /usr/share/pandoc/data/templates/template.typst
|
||||
|
||||
# ── yq (mikefarah) — YAML processor, jq's companion for YAML ─────────
|
||||
# Installed as the mikefarah Go binary — NOT Debian's `yq` apt package, which
|
||||
# is the unrelated Python kislyuk/yq (a jq wrapper with different syntax and
|
||||
@@ -481,6 +630,23 @@ COPY rootfs/home/developer/.bash_aliases /etc/skel-devbox/.bash_aliases
|
||||
COPY rootfs/home/developer/.inputrc /etc/skel-devbox/.inputrc
|
||||
COPY rootfs/home/developer/.gitignore_global /etc/skel-devbox/.gitignore_global
|
||||
|
||||
# ── Editor defaults: system-wide Neovim true-colour ──────────────────
|
||||
# /etc/xdg/nvim/sysinit.vim is Neovim's system vimrc: it loads for every user
|
||||
# (before any personal ~/.config/nvim) and can still be overridden per-user.
|
||||
# Enables termguicolors so the default theme renders in 24-bit colour instead
|
||||
# of a muddy 256-colour fallback. Pairs with kitty-terminfo (installed above).
|
||||
COPY rootfs/etc/xdg/nvim/sysinit.vim /etc/xdg/nvim/sysinit.vim
|
||||
|
||||
# ── Terminal support: xterm-ghostty terminfo alias ──────────────────
|
||||
# ncurses-term (installed above) covers wezterm/alacritty/foot/st and the base
|
||||
# `ghostty` entry, but Ghostty connects with TERM=xterm-ghostty, for which no
|
||||
# distro packages an entry. Ship a thin alias (use=ghostty) and compile it into
|
||||
# the system terminfo db with `tic -x`, so it inherits the maintained ghostty
|
||||
# capability set. The `infocmp` check fails the build if the entry didn't land.
|
||||
COPY rootfs/usr/local/share/terminfo-src/ghostty.terminfo /usr/local/share/terminfo-src/ghostty.terminfo
|
||||
RUN tic -x -o /usr/share/terminfo /usr/local/share/terminfo-src/ghostty.terminfo && \
|
||||
infocmp -x xterm-ghostty >/dev/null
|
||||
|
||||
# ── Entrypoint ────────────────────────────────────────────────────────
|
||||
COPY rootfs/usr/local/lib/pi-devbox/ /usr/local/lib/pi-devbox/
|
||||
# Image-baked skills + the global-AGENTS append snippet. Under /usr/local so a
|
||||
@@ -490,11 +656,13 @@ COPY rootfs/usr/local/lib/pi-devbox/ /usr/local/lib/pi-devbox/
|
||||
COPY rootfs/usr/local/share/pi-devbox/ /usr/local/share/pi-devbox/
|
||||
COPY rootfs/usr/local/bin/studio-expose /usr/local/bin/studio-expose
|
||||
COPY rootfs/usr/local/bin/dot-watch /usr/local/bin/dot-watch
|
||||
COPY rootfs/usr/local/bin/pi-devbox-version /usr/local/bin/pi-devbox-version
|
||||
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||
COPY entrypoint-user.sh /usr/local/bin/entrypoint-user.sh
|
||||
RUN chmod +x /usr/local/bin/entrypoint.sh /usr/local/bin/entrypoint-user.sh \
|
||||
/usr/local/bin/studio-expose \
|
||||
/usr/local/bin/dot-watch \
|
||||
/usr/local/bin/pi-devbox-version \
|
||||
/usr/local/lib/pi-devbox/*.sh 2>/dev/null || true
|
||||
|
||||
# Start as root — entrypoint adjusts UID/GID then drops to developer
|
||||
|
||||
+6
-1
@@ -158,6 +158,10 @@ RUN if [ -f /opt/pi-toolkit/pi-global-AGENTS.md ] && \
|
||||
ARG INSTALL_STUDIO=false
|
||||
ARG PI_STUDIO_REPO=https://github.com/omaclaren/pi-studio.git
|
||||
ARG PI_STUDIO_REF=main
|
||||
# PI_STUDIO_VERSION is the human-readable tag (e.g. v0.9.36) that PI_STUDIO_REF
|
||||
# was resolved from; recorded as a label below for at-a-glance identification.
|
||||
# Only meaningful for the studio variant (default `none` otherwise).
|
||||
ARG PI_STUDIO_VERSION=none
|
||||
RUN if [ "${INSTALL_STUDIO}" = "true" ]; then \
|
||||
set -e; \
|
||||
rm -rf /opt/pi-studio && mkdir -p /opt/pi-studio && \
|
||||
@@ -225,7 +229,8 @@ LABEL org.opencontainers.image.version="${RELEASE_TAG}" \
|
||||
se.jordbo.pi-devbox.pi-fork-ref="${PI_FORK_REF}" \
|
||||
se.jordbo.pi-devbox.pi-obsmem-ref="${PI_OBSMEM_REF}" \
|
||||
se.jordbo.pi-devbox.mempalace-toolkit-ref="${MEMPALACE_TOOLKIT_REF}" \
|
||||
se.jordbo.pi-devbox.pi-studio-ref="${PI_STUDIO_REF}"
|
||||
se.jordbo.pi-devbox.pi-studio-ref="${PI_STUDIO_REF}" \
|
||||
se.jordbo.pi-devbox.pi-studio-version="${PI_STUDIO_VERSION}"
|
||||
|
||||
# The manifest is written from GROUND TRUTH — the actual checked-out HEAD
|
||||
# of each /opt clone and the live `pi --version` — not merely the intended
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
# Ideas & backlog
|
||||
|
||||
A living list of potential improvements for pi-devbox that are **not yet
|
||||
scheduled**. This is intentionally lightweight — a place to park ideas so they
|
||||
aren't lost between sessions. When an item ships, describe it in
|
||||
[`CHANGELOG.md`](CHANGELOG.md) and remove it from here.
|
||||
|
||||
Rough effort tags: 🟢 small · 🟡 medium · 🔴 large. Status: `idea` (unvetted) ·
|
||||
`planned` (agreed, not started).
|
||||
|
||||
---
|
||||
|
||||
## Supply-chain hardening
|
||||
|
||||
- 🟡 `planned` — **Pin CI actions to commit SHAs.** The workflows use floating
|
||||
major tags (`actions/checkout@v4`, `docker/build-push-action@v7`,
|
||||
`docker/setup-buildx-action@v4`, `docker/login-action@v3`,
|
||||
`docker/setup-qemu-action@v3`). This is inconsistent with the project's own
|
||||
philosophy of SHA-pinning *content* refs (pi, pi-studio, pi-fork, …) to defeat
|
||||
floating refs. Pin each action to a SHA with a trailing `# vX.Y.Z` comment.
|
||||
Pairs naturally with the renovate item below to keep the pins fresh.
|
||||
|
||||
- 🟡 `planned` — **Vulnerability scanning in CI.** No CVE scan runs on the
|
||||
published images today. Add a `trivy image` (or grype) job to
|
||||
`docker-publish.yml` after `smoke`. Start non-blocking (report only), then
|
||||
tighten to fail on `HIGH`/`CRITICAL` with an available fix.
|
||||
|
||||
- 🟢🟡 `planned` — **Standardize build provenance → buildx SBOM + attestations.**
|
||||
The image already carries hand-rolled provenance (OCI labels +
|
||||
`build-manifest`). `docker/build-push-action` can emit a standard SBOM and
|
||||
SLSA provenance attestation nearly for free (`provenance: mode=max`,
|
||||
`sbom: true`). Makes provenance machine-consumable and pairs well with the
|
||||
trivy item (scan the SBOM).
|
||||
|
||||
## Dockerfile hardening
|
||||
|
||||
- 🟡 `idea` — **Address hadolint DL4006 properly.** Currently ignored in
|
||||
`.hadolint.yaml`. The clean fix is `SHELL ["/bin/bash", "-o", "pipefail",
|
||||
"-c"]` so piped `RUN`s fail on the first non-zero stage. This changes the
|
||||
default `RUN` shell from `sh` to `bash` for all subsequent layers, so it is
|
||||
base-affecting and needs a careful pass over existing `RUN`s before removing
|
||||
the ignore.
|
||||
|
||||
## Developer experience
|
||||
|
||||
- 🟢 `idea` — **`Makefile`/`justfile` for local iteration.** Reproducing a CI
|
||||
build locally means hand-assembling many `--build-arg`s. Thin targets
|
||||
(`make build-base`, `make build-variant`, `make smoke`, `make lint`) would
|
||||
make local testing painless and document the canonical invocations.
|
||||
|
||||
- 🟡 `idea` — **Dependency-update automation (renovate).** With CI actions
|
||||
SHA-pinned (above), a `renovate.json` keeps those pins — plus the pinned tool
|
||||
versions (`ACTIONLINT_VERSION`, `HADOLINT_VERSION`, gosu, etc.) — current via
|
||||
automated PRs. Requires a renovate runner against the Gitea instance.
|
||||
|
||||
## Housekeeping
|
||||
|
||||
- 🟢 `idea` — **Registry retention for `base-<hash>` tags.** The base-hash
|
||||
caching scheme accumulates `base-<hash>` tags over time. Confirm whether the
|
||||
registry prunes old ones, and add a retention/cleanup step if not.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Joakim Persson
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -29,13 +29,18 @@ on the host.
|
||||
- ChromaDB embedding model pre-warmed at build time (`all-MiniLM-L6-v2`)
|
||||
|
||||
The host-mounted palace at `~/.mempalace` is shared across the host and
|
||||
this container so all your agents share one brain.
|
||||
this container so all your agents share one brain. To instead share a palace
|
||||
across *several* containers/harnesses, set `MEMPALACE_REMOTE_URL` to a shared
|
||||
MemPalace HTTP endpoint (see `.env.example` and `docker-compose.mempalace.yml`);
|
||||
the bridge then connects over HTTP and spawns no local server.
|
||||
|
||||
### Modern CLI tooling
|
||||
|
||||
| Tool | Purpose |
|
||||
|---|---|
|
||||
| `nvim` | Neovim text editor |
|
||||
| `nvim` | Neovim text editor (modal / vi-style) |
|
||||
| `nano` | Small non-modal editor (on-screen shortcut hints) |
|
||||
| `micro` | Modern non-modal editor (Ctrl+S/Ctrl+Q keys, mouse, syntax highlighting) |
|
||||
| `tmux` | Terminal multiplexer (configured for 0-indexed sessions) |
|
||||
| `ripgrep`, `fd` | Fast file content / filename search |
|
||||
| `fzf` | Fuzzy finder |
|
||||
@@ -49,6 +54,17 @@ this container so all your agents share one brain.
|
||||
| `gosu` | Privilege de-escalation in entrypoint |
|
||||
| `htop`, `tree`, `less` | Inspection utilities |
|
||||
|
||||
The default `$EDITOR` is `nvim`. Three editors ship so you can pick your
|
||||
comfort level — if you'd rather not use a vi-style editor, `nano` and `micro`
|
||||
are both non-modal. Set your preference with `export EDITOR=micro` (or `nano`)
|
||||
in your shell profile, and/or `git config --global core.editor micro`.
|
||||
|
||||
Neovim ships with a system-wide default (`/etc/xdg/nvim/sysinit.vim`) that turns
|
||||
on `termguicolors`, so its colours render in 24-bit instead of a muddy
|
||||
256-colour fallback over ssh/kitty. The `kitty-terminfo` entry is also bundled
|
||||
so `TERM=xterm-kitty` is understood. Override either in your own
|
||||
`~/.config/nvim`.
|
||||
|
||||
### Document and image tooling
|
||||
|
||||
- `pandoc` — universal Markdown↔HTML/Org/RST/etc. converter
|
||||
@@ -139,8 +155,10 @@ Currently published:
|
||||
|
||||
Planned for an upcoming minor release:
|
||||
|
||||
- `joakimp/pi-devbox:latest-studio-tex` — `-studio` plus `texlive-xetex`
|
||||
for PDF export from Studio. Adds ~600 MB on top of `-studio`.
|
||||
- *(shipped in Unreleased/base)* **PDF export from Studio/pandoc** now works:
|
||||
the base image ships **`typst`** as the PDF engine (`pandoc --pdf-engine=typst`),
|
||||
a single ~30 MB static binary — no separate `-tex` variant needed.
|
||||
`texlive-xetex` stays the higher-fidelity fallback (install on demand).
|
||||
|
||||
## Using pi-studio (`-studio` variant)
|
||||
|
||||
@@ -275,9 +293,30 @@ Assuming the compose file publishes `127.0.0.1:8765:8765` (see method B):
|
||||
> 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`).
|
||||
|
||||
> PDF export (`/studio-pdf`, `studio_export_pdf`) needs a LaTeX engine,
|
||||
> which is **not** in `-studio` (only the planned `-studio-tex`). HTML
|
||||
> export, KaTeX, Mermaid, and all REPL features work without it.
|
||||
> 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.
|
||||
|
||||
### Generating a PDF with pandoc + typst
|
||||
|
||||
The base ships `pandoc` (front-end) and `typst` (PDF engine), so Markdown → PDF
|
||||
works out of the box:
|
||||
|
||||
```bash
|
||||
pandoc doc.md --pdf-engine=typst -o doc.pdf
|
||||
```
|
||||
|
||||
The base patches pandoc's bundled typst template so it defaults to the
|
||||
**Libertinus Serif** font. Without that patch a naked `--pdf-engine=typst`
|
||||
fails with `error: font fallback list must not be empty`, because the upstream
|
||||
template leaves the font unset. To pick a different face, pass one of the fonts
|
||||
typst can see (`typst fonts` lists them — DejaVu Serif/Sans/Mono, Libertinus
|
||||
Serif, New Computer Modern):
|
||||
|
||||
```bash
|
||||
pandoc doc.md --pdf-engine=typst -V mainfont="New Computer Modern" -o doc.pdf
|
||||
```
|
||||
|
||||
### Graphviz diagrams in Studio: `dot-watch`
|
||||
|
||||
@@ -654,6 +693,34 @@ truth** — the actual checked-out commit of each `/opt` clone and the live
|
||||
docker run --rm --entrypoint= joakimp/pi-devbox:latest cat /etc/pi-devbox/build-manifest.json
|
||||
```
|
||||
|
||||
Inside a running container, `pi-devbox-version` wraps that manifest into a
|
||||
human-readable summary — no need to remember the file path or pipe it
|
||||
through `jq` yourself:
|
||||
|
||||
```console
|
||||
$ pi-devbox-version
|
||||
pi-devbox v1.5.0
|
||||
built: 2026-07-13T17:53:16Z (source d68674d11e06)
|
||||
pi: 0.80.6
|
||||
components:
|
||||
pi-toolkit: 9a8f6faeaa08
|
||||
pi-extensions: 61c98e004e3d
|
||||
pi-fork: 4a09af4ef527
|
||||
pi-observational-memory: 27a5195eaf90
|
||||
mempalace-toolkit: 96699f2a1781
|
||||
pi-studio: 2ef38ef31cea
|
||||
```
|
||||
|
||||
It also flags **live drift** — if `pi --version` no longer matches what was
|
||||
baked at build time (e.g. something on a persisted volume shadowed the
|
||||
image's binary), the `pi:` line calls that out instead of silently trusting
|
||||
the manifest. `--json` dumps the raw manifest for scripting; `--quiet` gives
|
||||
a one-line `release_tag (source_revision)` form. It also prints once,
|
||||
automatically, at container start (from `entrypoint-user.sh`, before the
|
||||
rest of the setup output) — so you see which build you're in without
|
||||
asking. Exits 1 with a short notice on images built before this file
|
||||
existed, rather than failing silently.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Image grew unexpectedly
|
||||
@@ -744,4 +811,7 @@ The pi coding-agent itself is [@earendil-works/pi-coding-agent](https://www.npmj
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
MIT — see [`LICENSE`](LICENSE). This covers the repository's own contents
|
||||
(Dockerfiles, entrypoint scripts, `rootfs/` seeds, CI, docs). The published
|
||||
images bundle third-party software under their own licenses; see
|
||||
[`THIRD_PARTY.md`](THIRD_PARTY.md).
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
# Third-party notices
|
||||
|
||||
pi-devbox is distributed under the MIT License (see [`LICENSE`](LICENSE)), which
|
||||
covers **this repository's own contents** — the Dockerfiles, entrypoint scripts,
|
||||
`rootfs/` seeds, CI workflows, and docs.
|
||||
|
||||
The **published container images** (`joakimp/pi-devbox:*`) additionally *bundle*
|
||||
third-party software, each of which remains under its own license. This file is
|
||||
a good-faith summary; the authoritative sources are the upstream projects and,
|
||||
for OS packages, the per-package copyright files inside the image at
|
||||
`/usr/share/doc/<package>/copyright`.
|
||||
|
||||
## pi and its extensions (installed in the variant layer)
|
||||
|
||||
| Component | Upstream | License |
|
||||
| --- | --- | --- |
|
||||
| pi (`@earendil-works/pi-coding-agent`) | npm | MIT |
|
||||
| pi-fork | github.com/elpapi42/pi-fork | MIT |
|
||||
| pi-observational-memory | github.com/elpapi42/pi-observational-memory | MIT |
|
||||
| pi-studio *(`-studio` variant only)* | github.com/omaclaren/pi-studio | MIT |
|
||||
| pi-toolkit, pi-extensions, mempalace-toolkit | authored by the maintainer (Joakim Persson) | MIT |
|
||||
|
||||
## Tooling baked into the base image
|
||||
|
||||
| Component | Upstream | License (best effort) |
|
||||
| --- | --- | --- |
|
||||
| gosu | github.com/tianon/gosu | Apache-2.0 |
|
||||
| Node.js | nodejs.org | MIT (bundles components under their own licenses) |
|
||||
| uv | github.com/astral-sh/uv | Apache-2.0 OR MIT |
|
||||
| Neovim | neovim.io | Apache-2.0 + Vim license |
|
||||
| Pandoc | pandoc.org | GPL-2.0-or-later |
|
||||
| Typst | github.com/typst/typst | Apache-2.0 |
|
||||
| ripgrep / fd / micro / tealdeer / yq (mikefarah) | respective repos | MIT / Apache-2.0 / Unlicense (varies) |
|
||||
|
||||
## Base OS
|
||||
|
||||
The image is built `FROM` a Debian base and installs packages via `apt`. Debian
|
||||
and its packages are distributed under their respective licenses (GPL, LGPL,
|
||||
MIT, BSD, and others). See each package's copyright file in the image under
|
||||
`/usr/share/doc/<package>/copyright`.
|
||||
|
||||
---
|
||||
|
||||
*Licenses marked "best effort" are widely known but were not each verified at
|
||||
the exact bundled version; consult the upstream project for authoritative
|
||||
terms. Corrections welcome.*
|
||||
@@ -0,0 +1,84 @@
|
||||
# Shared MemPalace server (optional) — one palace for many clients.
|
||||
#
|
||||
# Runs `mempalace-mcp` over HTTP so several containers/harnesses (pi +
|
||||
# opencode + native) can share ONE palace instead of each keeping its own.
|
||||
# Point every client at it by setting, in that client's .env:
|
||||
#
|
||||
# MEMPALACE_REMOTE_URL=http://<reachable-host>:8765/mcp
|
||||
#
|
||||
# (see .env.example). When set, the client connects over HTTP and does NOT
|
||||
# spawn its own local mempalace-mcp.
|
||||
#
|
||||
# Start: docker compose -f docker-compose.mempalace.yml up -d
|
||||
# Stop: docker compose -f docker-compose.mempalace.yml down
|
||||
# Logs: docker compose -f docker-compose.mempalace.yml logs -f
|
||||
#
|
||||
# Why reuse the devbox image? mempalace-mcp is already installed in it, and
|
||||
# reusing it GUARANTEES the server's mempalace version matches the clients'
|
||||
# (both are pinned by the same image build). Override with a slimmer image via
|
||||
# MEMPALACE_SERVER_IMAGE if you prefer (it must provide `mempalace-mcp`).
|
||||
#
|
||||
# ⚠ SECURITY: mempalace-mcp's HTTP transport has NO authentication of its own.
|
||||
# Do NOT expose port 8765 to an untrusted network. The default below binds to
|
||||
# 127.0.0.1 (host loopback) only. To let sibling containers reach it, either
|
||||
# attach them to the shared `mempalace-net` network (container-to-container, no
|
||||
# host port needed — use http://mempalace-server:8765/mcp), or front it with a
|
||||
# reverse proxy that enforces MEMPALACE_REMOTE_TOKEN as `Authorization: Bearer`.
|
||||
|
||||
name: mempalace-server
|
||||
|
||||
services:
|
||||
mempalace:
|
||||
image: ${MEMPALACE_SERVER_IMAGE:-joakimp/pi-devbox:latest}
|
||||
container_name: mempalace-server
|
||||
# Bypass the devbox entrypoint (dev-shell/LAN/config setup) and run the
|
||||
# HTTP MCP server directly. HOME + explicit --palace pin the data path so
|
||||
# it does not depend on the image's default user/HOME. Runs as root so it
|
||||
# can initialise the fresh named volume; the volume is dedicated to this
|
||||
# server (clients reach it over HTTP, never by mounting it).
|
||||
entrypoint: []
|
||||
user: "0:0"
|
||||
environment:
|
||||
- HOME=/data
|
||||
command:
|
||||
- mempalace-mcp
|
||||
- --transport
|
||||
- http
|
||||
- --host
|
||||
- "0.0.0.0"
|
||||
- --port
|
||||
- "8765"
|
||||
- --palace
|
||||
- /data/.mempalace
|
||||
restart: unless-stopped
|
||||
# Loopback-only by default (see SECURITY note). Use "8765:8765" to expose on
|
||||
# all host interfaces, or drop `ports:` entirely and rely on mempalace-net.
|
||||
ports:
|
||||
- "127.0.0.1:8765:8765"
|
||||
volumes:
|
||||
# The shared palace data — precious; back this up.
|
||||
- mempalace-shared:/data/.mempalace
|
||||
# Embedding-model cache (~79 MB, disposable) so search does not re-download.
|
||||
- mempalace-shared-chroma:/data/.cache/chroma
|
||||
networks:
|
||||
- mempalace-net
|
||||
healthcheck:
|
||||
# A tools/list round-trip proves the server is answering MCP (python3 is
|
||||
# always present — mempalace itself is a python tool in the image).
|
||||
test:
|
||||
- CMD
|
||||
- python3
|
||||
- -c
|
||||
- "import urllib.request,json; d=json.dumps({'jsonrpc':'2.0','id':1,'method':'tools/list','params':{}}).encode(); r=urllib.request.Request('http://127.0.0.1:8765/mcp',data=d,headers={'Content-Type':'application/json','Accept':'application/json'}); urllib.request.urlopen(r,timeout=5).read()"
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 60s
|
||||
|
||||
volumes:
|
||||
mempalace-shared:
|
||||
mempalace-shared-chroma:
|
||||
|
||||
networks:
|
||||
mempalace-net:
|
||||
name: mempalace-net
|
||||
+4
-1
@@ -76,7 +76,10 @@ services:
|
||||
# Persist uv data (Python installs, tool installs)
|
||||
- devbox-uv:/home/developer/.local/share/uv
|
||||
|
||||
# Optional: persist MemPalace data (conversation memory, knowledge graph)
|
||||
# Optional: persist MemPalace data (conversation memory, knowledge graph).
|
||||
# Applies to the LOCAL palace only (the default). In EXTERNAL mode
|
||||
# (MEMPALACE_REMOTE_URL set in .env) the shared server owns the data, so
|
||||
# this volume is irrelevant.
|
||||
# - devbox-palace:/home/developer/.mempalace
|
||||
|
||||
# Optional: persist ChromaDB embedding model cache (~79 MB)
|
||||
|
||||
@@ -1,6 +1,14 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# ── Startup banner: which pi-devbox build is this? ─────────────────
|
||||
# Printed FIRST, before the setup noise below, so it's the first thing
|
||||
# visible when the container starts (CMD is `bash -l`, tty:true in compose,
|
||||
# so this reaches the same stream as the interactive shell the user lands
|
||||
# in). Reads the ground-truth manifest baked in Dockerfile.variant; a no-op
|
||||
# with a short stderr notice on images built before it existed.
|
||||
command -v pi-devbox-version >/dev/null 2>&1 && pi-devbox-version || true
|
||||
|
||||
# ── SSH ControlMaster socket dir ────────────────────────────────
|
||||
# Companion to /etc/ssh/ssh_config.d/00-devbox-controlmaster.conf in the
|
||||
# base image — that file declares ControlPath=/tmp/sshcm/%r@%h:%p; this
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
" pi-devbox — system-wide Neovim defaults.
|
||||
"
|
||||
" This is Neovim's *system vimrc*: it loads for every user before any personal
|
||||
" ~/.config/nvim, and personal configs can still override it.
|
||||
"
|
||||
" Enable 24-bit ("true") colour. Without it, Neovim's default theme is squeezed
|
||||
" into a 256-colour palette where strings/comments become a muddy, low-contrast
|
||||
" dark colour — a common complaint over ssh/kitty where COLORTERM often isn't
|
||||
" propagated into the container. Modern terminals (kitty, WezTerm, iTerm2,
|
||||
" Alacritty, ...) all support true colour; the bundled kitty-terminfo also lets
|
||||
" Neovim auto-detect it, but forcing it here guarantees readable colour
|
||||
" regardless of how the terminal type / COLORTERM reach the container.
|
||||
"
|
||||
" Opt out for a session: :set notermguicolors
|
||||
" Override permanently: set your own value in ~/.config/nvim/init.lua
|
||||
if has('termguicolors')
|
||||
set termguicolors
|
||||
endif
|
||||
@@ -54,6 +54,38 @@ alias gs='git status'
|
||||
alias gd='git diff'
|
||||
alias gl='git log --oneline --graph --decorate -20'
|
||||
|
||||
# ── Host SSH reachability check (once per container lifetime) ───────────────
|
||||
# Warns at first shell startup if the Mac host is not reachable via SSH.
|
||||
# Only runs inside a container, only if the jump key exists, and only once
|
||||
# per container lifetime (/tmp flag is cleared on recreate).
|
||||
_devbox_check_host_ssh() {
|
||||
[ -f "/.dockerenv" ] || return 0
|
||||
local ssh_cfg="$HOME/.ssh-local/config"
|
||||
[ -f "$ssh_cfg" ] || return 0
|
||||
local key_pub="$HOME/.ssh-local/devbox_jump_ed25519.pub"
|
||||
[ -f "$key_pub" ] || return 0
|
||||
local flag="/tmp/.devbox_host_ssh_ok"
|
||||
[ -f "$flag" ] && return 0
|
||||
if ssh -F "$ssh_cfg" \
|
||||
-o BatchMode=yes \
|
||||
-o ConnectTimeout=2 \
|
||||
-o StrictHostKeyChecking=accept-new \
|
||||
mac true 2>/dev/null; then
|
||||
touch "$flag"
|
||||
return 0
|
||||
fi
|
||||
local pub_key
|
||||
pub_key=$(cat "$key_pub")
|
||||
printf '\n\033[1;33m⚠ devbox: Mac host not reachable via SSH\033[0m\n'
|
||||
printf ' Some tools use SSH to run commands on the Mac host.\n'
|
||||
printf ' Fix (run both on the Mac):\n\n'
|
||||
printf ' \033[1mStep 1\033[0m System Settings → General → Sharing → Remote Login → ON\n\n'
|
||||
printf ' \033[1mStep 2\033[0m echo '"'"'%s'"'"' >> ~/.ssh/authorized_keys\n' "$pub_key"
|
||||
printf '\n Then open a new shell in the container to verify.\n\n'
|
||||
}
|
||||
_devbox_check_host_ssh
|
||||
unset -f _devbox_check_host_ssh
|
||||
|
||||
# ── LAN access via the host (dssh) ───────────────────────────────────
|
||||
# When running on a VM-backed host (macOS OrbStack / Docker Desktop), the
|
||||
# entrypoint's setup-lan-access.sh generates ~/.ssh-local/config so the host
|
||||
|
||||
@@ -9,3 +9,6 @@
|
||||
*.orig
|
||||
*.swp
|
||||
*.tmp
|
||||
|
||||
# AI/LLM tool local settings — machine-specific perms + credentials, never commit
|
||||
**/.claude/settings.local.json
|
||||
|
||||
Executable
+83
@@ -0,0 +1,83 @@
|
||||
#!/usr/bin/env bash
|
||||
# pi-devbox-version — show which pi-devbox image build is running.
|
||||
#
|
||||
# WHY THIS EXISTS
|
||||
# The image bakes ground-truth build info into /etc/pi-devbox/build-manifest.json
|
||||
# at `docker build` time (see Dockerfile.variant): the release tag, build date,
|
||||
# source commit, live `pi --version` at build time, and the actual checked-out
|
||||
# commit of every /opt component clone. That answers "what image am I running?"
|
||||
# — but only if you know to go look for the file. This wraps it into one
|
||||
# command, prints it human-first at container start (see entrypoint-user.sh),
|
||||
# and stays available on demand for the rest of the session.
|
||||
#
|
||||
# USAGE
|
||||
# pi-devbox-version human-readable summary (default)
|
||||
# pi-devbox-version --json raw manifest JSON (for scripting)
|
||||
# pi-devbox-version --quiet one-line "release_tag (source_revision)" form
|
||||
#
|
||||
# EXIT STATUS
|
||||
# 0 on success. 1 if the manifest is missing (e.g. an image built before
|
||||
# this file existed, or a non-pi-devbox base) — prints a short notice
|
||||
# to stderr rather than failing silently.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
MANIFEST=/etc/pi-devbox/build-manifest.json
|
||||
MODE="human"
|
||||
|
||||
case "${1:-}" in
|
||||
--json) MODE="json" ;;
|
||||
--quiet|-q) MODE="quiet" ;;
|
||||
--help|-h)
|
||||
sed -n '2,20p' "$0" | sed 's/^# \?//'
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ ! -f "$MANIFEST" ]; then
|
||||
echo "pi-devbox-version: no build manifest at $MANIFEST" >&2
|
||||
echo " (image predates the manifest, or this isn't a pi-devbox image)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v jq >/dev/null 2>&1; then
|
||||
echo "pi-devbox-version: jq not found; dumping raw manifest instead" >&2
|
||||
cat "$MANIFEST"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$MODE" = "json" ]; then
|
||||
cat "$MANIFEST"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
release_tag=$(jq -r '.release_tag' "$MANIFEST")
|
||||
build_date=$(jq -r '.build_date' "$MANIFEST")
|
||||
source_rev=$(jq -r '.source_revision' "$MANIFEST")
|
||||
pi_version_baked=$(jq -r '.pi_version' "$MANIFEST")
|
||||
|
||||
if [ "$MODE" = "quiet" ]; then
|
||||
printf '%s (%s)\n' "$release_tag" "${source_rev:0:7}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Live drift check: has `pi` been upgraded since this container was built?
|
||||
# (image is immutable, but a volume-persisted ~/.pi could in theory shadow
|
||||
# the baked binary — this stays honest rather than trusting the manifest
|
||||
# blindly, same "ground truth over intent" spirit as how the manifest
|
||||
# itself is generated in Dockerfile.variant.)
|
||||
pi_version_live=""
|
||||
if command -v pi >/dev/null 2>&1; then
|
||||
pi_version_live=$(pi --version 2>/dev/null | head -n1 | tr -d '\r\n')
|
||||
fi
|
||||
|
||||
printf 'pi-devbox %s\n' "$release_tag"
|
||||
printf ' built: %s (source %s)\n' "$build_date" "${source_rev:0:12}"
|
||||
if [ -n "$pi_version_live" ] && [ "$pi_version_live" != "$pi_version_baked" ]; then
|
||||
printf ' pi: %s \033[33m(baked as %s — drift detected)\033[0m\n' "$pi_version_live" "$pi_version_baked"
|
||||
else
|
||||
printf ' pi: %s\n' "${pi_version_live:-$pi_version_baked}"
|
||||
fi
|
||||
|
||||
printf ' components:\n'
|
||||
jq -r '.components | to_entries[] | select(.value != null) | " \(.key): \(.value[0:12])"' "$MANIFEST"
|
||||
@@ -19,6 +19,18 @@ be discovered at runtime, never assumed. And interactive shell aliases
|
||||
tool, so spell out the underlying command (e.g.
|
||||
`ssh -F "$HOME/.ssh-local/config" mac …`).
|
||||
|
||||
## Browser automation is available (agent-browser)
|
||||
|
||||
This image bakes the **`agent-browser`** CLI plus a headless Chromium, so you can
|
||||
drive a real browser — open pages, click/fill/`eval`, snapshot the DOM, take
|
||||
screenshots — to **verify** front-end work (live DOM, WebGL, layout, popup
|
||||
positioning) instead of guessing. Reach for it whenever a task involves a web UI
|
||||
or checking how a page actually renders. `AGENT_BROWSER_EXECUTABLE_PATH` is
|
||||
preset to the baked browser, so `agent-browser open <url>` works out of the box
|
||||
(headless). Run `agent-browser skills get core --full` for the command set and
|
||||
workflow patterns (always version-matched to the CLI); the `agent-browser` skill
|
||||
under `~/.agents/skills/` mirrors it when the skillset is mounted.
|
||||
|
||||
## Session start: load the mempalace skill
|
||||
|
||||
If MemPalace MCP tools (e.g. `mempalace_search`, `mempalace_diary_write`) are in
|
||||
|
||||
@@ -91,6 +91,22 @@ are "command not found" there — you must spell out the underlying command.
|
||||
If a command "works in my terminal but not when the agent runs it," this alias
|
||||
gap is the first thing to suspect.
|
||||
|
||||
**`dscp`/`scp` with accented filenames on a macOS host.** macOS stores filenames
|
||||
in Unicode **NFD** (decomposed — e.g. `ä` is `a` + combining U+0308), while the
|
||||
string you type or paste is usually **NFC** (precomposed `ä`, U+00E4). The bytes
|
||||
differ, so a precomposed remote path *silently* fails to match on the host —
|
||||
`scp … "mac:'~/Desktop/Skärmavbild ….png'"` returns *No such file or directory*
|
||||
even though the file plainly exists. Sidestep the encoding entirely: let the
|
||||
**remote shell expand a wildcard**, or list the directory first and copy the
|
||||
exact name it prints.
|
||||
|
||||
```sh
|
||||
# glob dodges the NFC/NFD mismatch (the remote shell matches the real bytes):
|
||||
scp -F "$HOME/.ssh-local/config" "mac:~/Desktop/Sk*rmavbild*.png" ./
|
||||
# or read the exact filename first, then copy that:
|
||||
ssh -F "$HOME/.ssh-local/config" mac 'ls -1 ~/Desktop/*.png'
|
||||
```
|
||||
|
||||
## 3. Reaching the Docker host and its LAN over SSH
|
||||
|
||||
When the host is VM-backed (e.g. OrbStack / Docker Desktop on macOS) the
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# xterm-ghostty — alias of the maintained ncurses `ghostty` terminfo entry.
|
||||
#
|
||||
# Ghostty sets TERM=xterm-ghostty by default, but the ncurses terminfo
|
||||
# database (Debian: ncurses-term) ships the entry under the name `ghostty`
|
||||
# only — there is no `xterm-ghostty` alias, and no distro packages one. This
|
||||
# thin alias makes xterm-ghostty resolve to the same upstream-maintained
|
||||
# capability set, so SSH sessions from a Ghostty terminal work without
|
||||
# vendoring Ghostty's full (Zig-generated) terminfo here.
|
||||
#
|
||||
# `use=ghostty` is resolved by `tic` at compile time against the base
|
||||
# `ghostty` entry from ncurses-term (installed in Dockerfile.base before the
|
||||
# compile step). Compiled with `tic -x`.
|
||||
xterm-ghostty|Ghostty terminal emulator (xterm-ghostty alias),
|
||||
use=ghostty,
|
||||
Executable
+65
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
# Gitea-accurate guard against the recurring "bash syntax under the default
|
||||
# sh/dash shell" footgun (ed49b8d resolve-versions; b7197e8 promote-base-latest,
|
||||
# run 418).
|
||||
#
|
||||
# WHY A CUSTOM CHECK AND NOT JUST actionlint:
|
||||
# actionlint models *GitHub* Actions, whose default `run` shell is bash. It
|
||||
# therefore assumes a step that omits `shell:` runs under bash, and does NOT
|
||||
# flag `set -o pipefail` there. Gitea Actions' default is `sh` (dash), so the
|
||||
# exact bug we hit (omit `shell:`, use bash syntax) is invisible to actionlint.
|
||||
# actionlint only fires when a step *explicitly* declares `shell: sh`.
|
||||
#
|
||||
# THE INVARIANT THIS ENFORCES:
|
||||
# Every `run:` step in every .gitea/workflows/*.yml must resolve to an
|
||||
# effective shell of `bash` — via the step's own `shell:`, a job-level
|
||||
# `defaults.run.shell`, or a workflow-level `defaults.run.shell`. Any step
|
||||
# that would fall through to Gitea's `sh` default is a FAILURE, because a
|
||||
# future author adding bash syntax to it fails silently in CI.
|
||||
#
|
||||
# Pair this with actionlint (which catches explicit `shell: sh` + bash syntax,
|
||||
# expression errors, and much else). Together they cover the class on Gitea.
|
||||
set -euo pipefail
|
||||
|
||||
WF_DIR="${1:-.gitea/workflows}"
|
||||
|
||||
python3 - "$WF_DIR" <<'PY'
|
||||
import sys, glob, os
|
||||
try:
|
||||
import yaml
|
||||
except ImportError:
|
||||
sys.stderr.write("ERROR: python3 yaml module missing (apt install python3-yaml)\n")
|
||||
sys.exit(2)
|
||||
|
||||
wf_dir = sys.argv[1]
|
||||
files = sorted(glob.glob(os.path.join(wf_dir, "*.yml")) + glob.glob(os.path.join(wf_dir, "*.yaml")))
|
||||
if not files:
|
||||
sys.stderr.write(f"ERROR: no workflow files under {wf_dir}\n")
|
||||
sys.exit(2)
|
||||
|
||||
problems = []
|
||||
for f in files:
|
||||
with open(f) as fh:
|
||||
doc = yaml.safe_load(fh) or {}
|
||||
wf_shell = (((doc.get("defaults") or {}).get("run") or {}).get("shell"))
|
||||
jobs = doc.get("jobs") or {}
|
||||
for jname, job in jobs.items():
|
||||
job = job or {}
|
||||
job_shell = (((job.get("defaults") or {}).get("run") or {}).get("shell"))
|
||||
steps = job.get("steps") or []
|
||||
for i, step in enumerate(steps):
|
||||
step = step or {}
|
||||
if "run" not in step:
|
||||
continue # `uses:` steps have no shell
|
||||
eff = step.get("shell") or job_shell or wf_shell or "sh" # Gitea default = sh
|
||||
if eff != "bash":
|
||||
name = step.get("name") or f"step[{i}]"
|
||||
problems.append(f"{f}: job '{jname}' / '{name}': effective shell = '{eff}' (Gitea default is sh; declare shell: bash or a bash default)")
|
||||
|
||||
if problems:
|
||||
sys.stderr.write("Workflow shell guard FAILED — bash default not guaranteed:\n")
|
||||
for p in problems:
|
||||
sys.stderr.write(f" - {p}\n")
|
||||
sys.exit(1)
|
||||
print(f"Workflow shell guard OK — all run: steps in {len(files)} workflow file(s) resolve to bash.")
|
||||
PY
|
||||
+26
-1
@@ -6,6 +6,10 @@
|
||||
# Verifies:
|
||||
# - pi binary present and (if EXPECTED_PI_VERSION set) matches CI's resolved version
|
||||
# - new v1.0.0 base additions (pandoc, graphviz, imagemagick, yq, tealdeer)
|
||||
# - typst PDF engine for pandoc (Unreleased) — `pandoc --pdf-engine=typst`
|
||||
# - non-modal editors nano + micro (alongside nvim)
|
||||
# - terminfo for modern emulators: xterm-kitty, xterm-ghostty, wezterm,
|
||||
# alacritty, foot (kitty-terminfo + ncurses-term + compiled ghostty alias)
|
||||
# - tmux 0-indexing baked in /etc/tmux.conf (required for pi-studio variants)
|
||||
# - pi-toolkit cloned at /opt/pi-toolkit
|
||||
# - pi-extensions cloned at /opt/pi-extensions
|
||||
@@ -15,6 +19,8 @@
|
||||
# - mempalace bridge symlink present
|
||||
# - settings.json bootstrapped
|
||||
# - pi-fork + pi-observational-memory registered via `pi install`
|
||||
# - pi-devbox-version command present + wraps the build manifest correctly
|
||||
# (human, --json, --quiet)
|
||||
# - (studio variant only, auto-detected) pi-studio cloned + prebuilt
|
||||
# client bundle present + registered via `pi install`
|
||||
# - image size within threshold
|
||||
@@ -24,7 +30,8 @@ set -euo pipefail
|
||||
IMAGE="${1:?usage: $0 <image>}"
|
||||
PASS=0; FAIL=0
|
||||
# pi-devbox v1.0.0 (decoupled from opencode-devbox) added pandoc, graphviz,
|
||||
# imagemagick, yq, tealdeer, and a baked /etc/tmux.conf. Local arm64 build
|
||||
# imagemagick, yq, tealdeer, a baked /etc/tmux.conf, and the non-modal
|
||||
# editors nano + micro (~15 MB combined). Local arm64 build
|
||||
# observed 3.20 GB. CI amd64 builds may differ slightly; threshold below
|
||||
# carries +300 MB margin to absorb arch differences without false reds.
|
||||
# Tighten in a follow-up release once amd64 actuals are observed in CI logs.
|
||||
@@ -71,9 +78,17 @@ run "git" "git --version"
|
||||
run "aws" "aws --version"
|
||||
run "uv" "uv --version"
|
||||
run "nvim" "nvim --version"
|
||||
run "nano" "nano --version"
|
||||
run "micro" "micro --version"
|
||||
run "kitty-terminfo" "infocmp -x xterm-kitty >/dev/null 2>&1"
|
||||
run "terminfo: modern emulators (ncurses-term)" 'for t in wezterm alacritty foot ghostty st-256color; do infocmp -x "$t" >/dev/null 2>&1 || exit 1; done'
|
||||
run "terminfo: xterm-ghostty alias (tic)" "infocmp -x xterm-ghostty >/dev/null 2>&1"
|
||||
run "nvim true-colour default (sysinit.vim)" "nvim --headless -c 'lua os.exit(vim.o.termguicolors and 0 or 1)'"
|
||||
run "mempalace-mcp" "mempalace-mcp --help"
|
||||
# v1.0.0 base additions — verify presence and basic functionality.
|
||||
run "pandoc" "pandoc --version"
|
||||
run "typst" "typst --version"
|
||||
run "pandoc+typst PDF engine" "printf '# hi\n' | pandoc --pdf-engine=typst -o /tmp/_smoke.pdf - && test -s /tmp/_smoke.pdf; rm -f /tmp/_smoke.pdf"
|
||||
run "graphviz (dot)" "dot -V"
|
||||
run "imagemagick" "magick --version"
|
||||
run "yq (mikefarah v4)" "yq --version | grep -qE 'mikefarah.*version v4'"
|
||||
@@ -146,6 +161,16 @@ run_expect "manifest records pi_version" \
|
||||
# non-studio variant) — 'unknown' means a clone silently failed to resolve.
|
||||
run "manifest has no unresolved ('unknown') components" \
|
||||
"! grep -q '\"unknown\"' /etc/pi-devbox/build-manifest.json"
|
||||
# pi-devbox-version wraps the manifest into a human-first command (this
|
||||
# PR); verify the binary is present, executable, and both output modes work.
|
||||
run "pi-devbox-version binary present + executable" \
|
||||
"test -x /usr/local/bin/pi-devbox-version"
|
||||
run_expect "pi-devbox-version human output shows release tag" \
|
||||
"pi-devbox-version" "pi-devbox "
|
||||
run_expect "pi-devbox-version --json round-trips the manifest" \
|
||||
"pi-devbox-version --json" '"release_tag"'
|
||||
run_expect "pi-devbox-version --quiet is a compact one-liner" \
|
||||
"pi-devbox-version --quiet | wc -l" "1"
|
||||
# OCI labels live in the image config, not the container fs — inspect them
|
||||
# from the host docker rather than via `docker run`.
|
||||
LBL=$(docker inspect --format '{{ index .Config.Labels "se.jordbo.pi-devbox.pi-extensions-ref" }}' "$IMAGE" 2>/dev/null || true)
|
||||
|
||||
Reference in New Issue
Block a user