572430237f
3.6.0 (2026-07-17, PyPI latest) is additive/reliability only: secure
`mempalace serve` remote mode, optional Milvus backend, atomic KG
supersede(), conversation chronology, mining exclusions, plus recovery and
locking fixes.
Reviewed for MCP tool-schema changes before bumping — that being the exact
regression class this pin exists to catch, after an unpinned install once
swept in the broken 3.3.x/3.4.0 diary_write schema. There are none, and
nothing touches diary_write, so the perl workaround removed in v1.2.2 stays
removed.
Two fixes matter for how this image uses mempalace: read-only mode now covers
checkpoint + delete_by_source in _MUTATING_TOOLS (#1930), and agent
attribution is preserved in mempalace_checkpoint (#2023/#2034) — the latter
because the diary protocol relies on per-agent attribution.
Also adds a CHANGELOG Unreleased block that backfills the per-variant image
description labels (1fd524e), pushed after the v1.6.4 tag without an entry.
Not tagged: more changes are queued for the next release. Pushing to main
triggers only lint.yml (actionlint + hadolint) — the image build/publish
workflow is tag-only. Verified clean against the CI-pinned hadolint 2.14.0.
684 lines
38 KiB
Docker
684 lines
38 KiB
Docker
# pi-devbox — base image (variant-independent layers)
|
|
#
|
|
# This Dockerfile produces an image tagged base-<hash>, used as the parent
|
|
# for all published variants of pi-devbox. It contains everything that does
|
|
# not depend on variant-specific build-args (the pi install moves to
|
|
# Dockerfile.variant).
|
|
#
|
|
# The base is rebuilt only when this file or anything it COPYs in changes
|
|
# (rootfs/, entrypoint*.sh). Version bumps to PI_VERSION etc. do NOT
|
|
# trigger a base rebuild.
|
|
#
|
|
# To force a base rebuild for fresh apt packages without other code
|
|
# changes, bump the BASE_REBUILD_DATE comment below. The hash is
|
|
# content-addressed over this file, so any byte change invalidates the
|
|
# cache. Recommended cadence: once per release for security updates.
|
|
#
|
|
# 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).
|
|
# pi-devbox was previously a thin re-brand of opencode-devbox's pi-only
|
|
# variant; this file is the start of an independent build chain. The
|
|
# opencode-devbox install logic (INSTALL_OPENCODE, INSTALL_OMOS) does
|
|
# not appear here. The base is otherwise broadly equivalent so generic
|
|
# upstream improvements (CVE updates, new dev tooling) can be cherry-
|
|
# picked between repos.
|
|
# ─────────────────────────────────────────────────────────────────────
|
|
|
|
ARG DEBIAN_VERSION=trixie-slim
|
|
FROM debian:${DEBIAN_VERSION} AS base
|
|
|
|
ARG TARGETARCH
|
|
|
|
LABEL maintainer="joakimp"
|
|
LABEL description="pi-devbox — base image (variant-independent)"
|
|
LABEL org.opencontainers.image.source="https://gitea.jordbo.se/joakimp/pi-devbox"
|
|
|
|
# Avoid interactive prompts during build
|
|
ENV DEBIAN_FRONTEND=noninteractive
|
|
|
|
# ── Core system packages ─────────────────────────────────────────────
|
|
# apt-get upgrade picks up any security/CVE fixes published between
|
|
# debian:trixie-slim base-image rebuilds. Paired with the index update
|
|
# and the install in the same layer so we don't bloat image history.
|
|
#
|
|
# 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. 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).
|
|
# imagemagick — image conversion / resizing for thumbnails, etc. ~50 MB.
|
|
# (yq is NOT apt-installed: Debian's `yq` is the unrelated Python tool;
|
|
# mikefarah's Go yq is installed as a pinned binary further down.)
|
|
# socat — TCP relay. Powers `studio-expose`, which bridges
|
|
# 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 \
|
|
ca-certificates \
|
|
curl \
|
|
wget \
|
|
git \
|
|
openssh-client \
|
|
gnupg \
|
|
jq \
|
|
ripgrep \
|
|
fd-find \
|
|
tree \
|
|
less \
|
|
htop \
|
|
tmux \
|
|
make \
|
|
patch \
|
|
diffutils \
|
|
git-crypt \
|
|
age \
|
|
file \
|
|
sudo \
|
|
locales \
|
|
procps \
|
|
unzip \
|
|
gcc \
|
|
g++ \
|
|
rsync \
|
|
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/*
|
|
|
|
# ── tmux defaults: 0-indexed windows and panes ───────────────────────
|
|
# pi-studio (omaclaren/pi-studio) hard-codes its tmux send target to
|
|
# `<session>:0.0`. Containers that ship tmux with default options are
|
|
# already 0-indexed; this file makes the assumption explicit so future
|
|
# /etc/tmux.conf consumers can read it. Users can override per-user
|
|
# in ~/.tmux.conf if they want 1-indexing — pi-studio will then fail
|
|
# to find its REPL session.
|
|
RUN printf '%s\n' \
|
|
'# pi-devbox baked default — see Dockerfile.base.' \
|
|
'# pi-studio targets tmux session :0.0; do not change these here.' \
|
|
'set -g base-index 0' \
|
|
'set -g pane-base-index 0' \
|
|
> /etc/tmux.conf
|
|
|
|
# ── SSH client defaults: ControlMaster on a writable socket path ──────
|
|
# Why this exists: the devbox typically mounts ~/.ssh from the host as
|
|
# read-only (security: keys are readable, but agents can't tamper with
|
|
# config / known_hosts / authorized_keys / plant a malicious ProxyCommand).
|
|
# OpenSSH's default ControlPath is ~/.ssh/cm/... which is unwritable on
|
|
# such mounts, so any attempt to use ControlMaster fails. Symptoms:
|
|
# unix_listener: cannot bind to path /home/.../.ssh/cm/...: Read-only file system
|
|
# kex_exchange_identification: Connection closed by remote host
|
|
# The latter manifests downstream of CGNAT per-destination flow caps
|
|
# (~4 concurrent flows on most European residential ISPs) which silently
|
|
# drop further SYNs once exceeded — making fresh ssh attempts fail with
|
|
# banner-exchange timeouts that look like a remote problem.
|
|
#
|
|
# Fix: set a system-wide default ControlPath in /tmp (per-container,
|
|
# tmpfs-friendly, always writable) so multiplexing Just Works without
|
|
# touching the read-only ~/.ssh mount. Per-host overrides in user's
|
|
# ~/.ssh/config still win — Debian's default /etc/ssh/ssh_config has
|
|
# `Include /etc/ssh/ssh_config.d/*.conf` *before* the `Host *` block,
|
|
# so user config can override these defaults if desired.
|
|
#
|
|
# CAVEAT (and why it is handled elsewhere): a user per-host override that
|
|
# points ControlPath BACK under the read-only ~/.ssh (e.g. the common CGNAT
|
|
# idiom `ControlPath ~/.ssh/cm/%r@%h:%p`) re-introduces the unwritable-socket
|
|
# failure — a system drop-in here can never override a user's per-host value.
|
|
# For `pi --ssh`, the ssh-controlmaster extension handles this by detecting an
|
|
# unwritable system ControlPath and falling back to its own /tmp master; for
|
|
# `ssh -F ~/.ssh-local/config` (dssh/dscp), setup-lan-access.sh redirects
|
|
# ControlPath into the writable ~/.ssh-local. See CHANGELOG "Unreleased".
|
|
#
|
|
# ControlPersist=10m means the master socket sticks around 10 min after
|
|
# the last session closes, so consecutive ssh calls in a workflow reuse
|
|
# the same TCP flow. Companion entrypoint-user.sh creates /tmp/sshcm
|
|
# (mode 700) on each container start.
|
|
RUN mkdir -p /etc/ssh/ssh_config.d && \
|
|
printf '%s\n' \
|
|
'# Devbox-baked default. See Dockerfile.base "SSH client defaults".' \
|
|
'# Override per-host in ~/.ssh/config if the master socket location' \
|
|
'# needs to differ.' \
|
|
'Host *' \
|
|
' ControlMaster auto' \
|
|
' ControlPath /tmp/sshcm/%r@%h:%p' \
|
|
' ControlPersist 10m' \
|
|
' ServerAliveInterval 30' \
|
|
' ServerAliveCountMax 6' \
|
|
> /etc/ssh/ssh_config.d/00-devbox-controlmaster.conf && \
|
|
chmod 644 /etc/ssh/ssh_config.d/00-devbox-controlmaster.conf
|
|
|
|
# ── Go-compiled tools (install from GitHub to avoid CVEs in Debian's old Go builds)
|
|
#
|
|
# Version policy: default is `latest` — resolved at build time by
|
|
# following the /releases/latest redirect and reading the tag from the
|
|
# Location header. Every base rebuild picks up the newest upstream
|
|
# release. Explicit pins still work via build-args (e.g.
|
|
# --build-arg GOSU_VERSION=1.19).
|
|
|
|
# gosu — privilege de-escalation
|
|
ARG GOSU_VERSION=latest
|
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \
|
|
V="${GOSU_VERSION}" && \
|
|
if [ "$V" = "latest" ]; then \
|
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/tianon/gosu/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
fi && \
|
|
V="${V#v}" && [ -n "$V" ] && \
|
|
echo "Installing gosu ${V}" && \
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/tianon/gosu/releases/download/${V}/gosu-${ARCH}" -o /usr/local/bin/gosu && \
|
|
chmod +x /usr/local/bin/gosu && \
|
|
gosu --version
|
|
|
|
# fzf — fuzzy finder
|
|
ARG FZF_VERSION=latest
|
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \
|
|
V="${FZF_VERSION}" && \
|
|
if [ "$V" = "latest" ]; then \
|
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/junegunn/fzf/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
fi && \
|
|
V="${V#v}" && [ -n "$V" ] && \
|
|
echo "Installing fzf ${V}" && \
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/junegunn/fzf/releases/download/v${V}/fzf-${V}-linux_${ARCH}.tar.gz" | tar -xz -C /usr/local/bin fzf && \
|
|
fzf --version
|
|
|
|
# git-lfs
|
|
ARG GIT_LFS_VERSION=latest
|
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \
|
|
V="${GIT_LFS_VERSION}" && \
|
|
if [ "$V" = "latest" ]; then \
|
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/git-lfs/git-lfs/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
fi && \
|
|
V="${V#v}" && [ -n "$V" ] && \
|
|
echo "Installing git-lfs ${V}" && \
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/git-lfs/git-lfs/releases/download/v${V}/git-lfs-linux-${ARCH}-v${V}.tar.gz" | tar -xz -C /tmp && \
|
|
install /tmp/git-lfs-${V}/git-lfs /usr/local/bin/git-lfs && \
|
|
rm -rf /tmp/git-lfs-${V} && \
|
|
git lfs install --system && \
|
|
git-lfs --version
|
|
|
|
# gitleaks
|
|
ARG GITLEAKS_VERSION=latest
|
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x64" ;; arm64) echo "arm64" ;; *) echo "x64" ;; esac) && \
|
|
V="${GITLEAKS_VERSION}" && \
|
|
if [ "$V" = "latest" ]; then \
|
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/gitleaks/gitleaks/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
fi && \
|
|
V="${V#v}" && [ -n "$V" ] && \
|
|
echo "Installing gitleaks ${V}" && \
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/gitleaks/gitleaks/releases/download/v${V}/gitleaks_${V}_linux_${ARCH}.tar.gz" | tar -xz -C /usr/local/bin gitleaks && \
|
|
chmod +x /usr/local/bin/gitleaks && \
|
|
gitleaks version
|
|
|
|
# neovim
|
|
ARG NVIM_VERSION=latest
|
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "arm64" ;; *) echo "x86_64" ;; esac) && \
|
|
V="${NVIM_VERSION}" && \
|
|
if [ "$V" = "latest" ]; then \
|
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/neovim/neovim/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
fi && \
|
|
V="${V#v}" && [ -n "$V" ] && \
|
|
echo "Installing neovim ${V}" && \
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/neovim/neovim/releases/download/v${V}/nvim-linux-${ARCH}.tar.gz" | tar -xz -C /opt && \
|
|
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) && \
|
|
V="${BAT_VERSION}" && \
|
|
if [ "$V" = "latest" ]; then \
|
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/sharkdp/bat/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
fi && \
|
|
V="${V#v}" && [ -n "$V" ] && \
|
|
echo "Installing bat ${V}" && \
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/sharkdp/bat/releases/download/v${V}/bat-v${V}-${ARCH}-unknown-linux-musl.tar.gz" | tar -xz -C /tmp && \
|
|
install /tmp/bat-v${V}-${ARCH}-unknown-linux-musl/bat /usr/local/bin/bat && \
|
|
rm -rf /tmp/bat-v${V}-* && \
|
|
bat --version
|
|
|
|
# eza
|
|
ARG EZA_VERSION=latest
|
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
|
V="${EZA_VERSION}" && \
|
|
if [ "$V" = "latest" ]; then \
|
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/eza-community/eza/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
fi && \
|
|
V="${V#v}" && [ -n "$V" ] && \
|
|
echo "Installing eza ${V}" && \
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/eza-community/eza/releases/download/v${V}/eza_${ARCH}-unknown-linux-gnu.tar.gz" | tar -xz -C /usr/local/bin && \
|
|
eza --version | head -1
|
|
|
|
# zoxide
|
|
ARG ZOXIDE_VERSION=latest
|
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
|
V="${ZOXIDE_VERSION}" && \
|
|
if [ "$V" = "latest" ]; then \
|
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/ajeetdsouza/zoxide/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
fi && \
|
|
V="${V#v}" && [ -n "$V" ] && \
|
|
echo "Installing zoxide ${V}" && \
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/ajeetdsouza/zoxide/releases/download/v${V}/zoxide-${V}-${ARCH}-unknown-linux-musl.tar.gz" | tar -xz -C /usr/local/bin zoxide && \
|
|
zoxide --version
|
|
|
|
# uv — fast Python package manager. Note: uv tags don't prefix with "v".
|
|
ARG UV_VERSION=latest
|
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
|
V="${UV_VERSION}" && \
|
|
if [ "$V" = "latest" ]; then \
|
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/astral-sh/uv/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
fi && \
|
|
V="${V#v}" && [ -n "$V" ] && \
|
|
echo "Installing uv ${V}" && \
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/astral-sh/uv/releases/download/${V}/uv-${ARCH}-unknown-linux-musl.tar.gz" | tar -xz -C /tmp && \
|
|
install /tmp/uv-${ARCH}-unknown-linux-musl/uv /usr/local/bin/uv && \
|
|
install /tmp/uv-${ARCH}-unknown-linux-musl/uvx /usr/local/bin/uvx && \
|
|
rm -rf /tmp/uv-* && \
|
|
uv --version
|
|
|
|
# ── MemPalace — local-first AI memory system ─────────────────────────
|
|
# Provides semantic search over conversation history via 29 MCP tools.
|
|
# Always installed in the base. Set INSTALL_MEMPALACE=false at base-build
|
|
# time to shave ~300 MB.
|
|
#
|
|
# Stall protection (fixed 2026-06-13; self-heal added 2026-06-25):
|
|
# mempalace-mcp is launched by the `mempalace.ts` pi extension from
|
|
# mempalace-toolkit (cloned below). That extension applies a per-REQUEST
|
|
# timeout in its JSON-RPC client and kills the child on stall, so a virtiofs
|
|
# cold-open of chroma.sqlite3 / HNSW load can no longer hang the pi TUI
|
|
# uninterruptibly. A stall-kill is no longer a permanent latch either: the
|
|
# next tool call respawns the server with capped exponential backoff (the
|
|
# budget resets on any successful response). Tunables:
|
|
# MEMPALACE_MCP_TIMEOUT_MS (default 60000), MEMPALACE_MCP_INIT_TIMEOUT_MS
|
|
# (default 300000 — generous so a genuine first cold-open isn't killed),
|
|
# MEMPALACE_MCP_MAX_RESPAWNS (default 2; 0 disables self-heal),
|
|
# MEMPALACE_MCP_RESPAWN_BACKOFF_MS (default 1000); timeouts of 0 disable.
|
|
# Defaults live in the extension, so no ENV is needed here. A standalone
|
|
# stdio-watchdog shim is NOT needed — the extension already owns
|
|
# request/response correlation. See CHANGELOG.md "Unreleased > Fixed".
|
|
ARG INSTALL_MEMPALACE=true
|
|
# Pin to a known-good version. Bump deliberately, not implicitly: an
|
|
# unpinned install silently swept in mempalace 3.3.x/3.4.0 with a broken
|
|
# diary_write schema. Pinning makes mempalace upgrades a reviewable diff
|
|
# rather than a surprise.
|
|
#
|
|
# 3.5.0 (2026-06) shipped the upstream fix for the top-level-anyOf diary_write
|
|
# schema (issue #1728 / PR #1717, merged 2026-06-14): the advertised schema
|
|
# is now `"required": ["agent_name"]` with entry/content enforced at dispatch,
|
|
# which Anthropic's tools API accepts — so the old mcp_server.py perl
|
|
# workaround that used to live below is gone.
|
|
#
|
|
# 3.6.0 (2026-07-17, PyPI latest) is additive/reliability only — secure
|
|
# `mempalace serve` remote mode, optional Milvus backend, atomic KG
|
|
# supersede(), conversation chronology, mining exclusions, plus recovery and
|
|
# locking fixes. Reviewed for MCP tool-schema changes before bumping (that
|
|
# being the exact regression class this pin exists to catch): there are NONE,
|
|
# and nothing touches diary_write. Two fixes matter for how this image uses
|
|
# mempalace: read-only mode now covers checkpoint + delete_by_source in
|
|
# _MUTATING_TOOLS (#1930), and agent attribution is preserved in
|
|
# mempalace_checkpoint (#2023/#2034).
|
|
#
|
|
# Keep in lockstep with opencode-devbox when bumping.
|
|
ARG MEMPALACE_VERSION=3.6.0
|
|
ENV UV_TOOL_DIR=/opt/uv-tools
|
|
ENV UV_TOOL_BIN_DIR=/usr/local/bin
|
|
RUN if [ "${INSTALL_MEMPALACE}" = "true" ]; then \
|
|
mkdir -p /opt/uv-tools && \
|
|
uv tool install --no-cache "mempalace==${MEMPALACE_VERSION}" && \
|
|
/opt/uv-tools/mempalace/bin/python -c "import mempalace; print('mempalace', mempalace.__version__ if hasattr(mempalace, '__version__') else 'installed')" ; \
|
|
fi
|
|
|
|
# (The mempalace diary_write top-level-anyOf workaround that patched
|
|
# mcp_server.py here was removed in v1.2.2 — fixed upstream in mempalace
|
|
# 3.5.0 via issue #1728 / PR #1717 (merged 2026-06-14). See CHANGELOG.md.)
|
|
|
|
# ── mempalace-toolkit — bash wrappers for session/docs mining ────────
|
|
ARG INSTALL_MEMPALACE_TOOLKIT=true
|
|
ARG MEMPALACE_TOOLKIT_REF=main
|
|
# MEMPALACE_TOOLKIT_REPO defaults to the canonical gitea origin but is
|
|
# overridable so a relocated/forked build can clone from a mirror or a
|
|
# different host without editing this Dockerfile (mirrors the
|
|
# PI_FORK_REPO / PI_OBSMEM_REPO / PI_STUDIO_REPO pattern in the variant).
|
|
ARG MEMPALACE_TOOLKIT_REPO=https://gitea.jordbo.se/joakimp/mempalace-toolkit.git
|
|
# MEMPALACE_TOOLKIT_REF accepts EITHER a branch name OR a commit SHA. CI
|
|
# resolves it to a SHA (resolve-versions job) and folds that SHA into the
|
|
# base-decide hash so the base rebuilds when the toolkit moves. `git clone
|
|
# --branch <40-char-SHA>` fails ("Remote branch not found") — the same
|
|
# footgun fixed in Dockerfile.variant (v1.0.0-rerun, run 374) — so use
|
|
# `git fetch <ref> + checkout FETCH_HEAD`, which works for name and SHA.
|
|
RUN if [ "${INSTALL_MEMPALACE}" = "true" ] && [ "${INSTALL_MEMPALACE_TOOLKIT}" = "true" ]; then \
|
|
rm -rf /opt/mempalace-toolkit && mkdir -p /opt/mempalace-toolkit && \
|
|
git -C /opt/mempalace-toolkit init -q && \
|
|
git -C /opt/mempalace-toolkit remote add origin "${MEMPALACE_TOOLKIT_REPO}" && \
|
|
ok=0; for i in 1 2 3 4 5; do \
|
|
if git -C /opt/mempalace-toolkit fetch --depth 1 origin "${MEMPALACE_TOOLKIT_REF}" && \
|
|
git -C /opt/mempalace-toolkit checkout -q FETCH_HEAD; then ok=1; break; fi; \
|
|
echo "git fetch mempalace-toolkit@${MEMPALACE_TOOLKIT_REF} failed (attempt $i/5), retrying in $((i*5))s..."; \
|
|
sleep $((i*5)); \
|
|
done; \
|
|
[ "$ok" = "1" ] && \
|
|
ln -sf /opt/mempalace-toolkit/bin/mempalace-session /usr/local/bin/mempalace-session && \
|
|
ln -sf /opt/mempalace-toolkit/bin/mempalace-docs /usr/local/bin/mempalace-docs && \
|
|
chmod +x /opt/mempalace-toolkit/bin/mempalace-session /opt/mempalace-toolkit/bin/mempalace-docs && \
|
|
mempalace-session --help >/dev/null && \
|
|
mempalace-docs --help >/dev/null && \
|
|
echo "mempalace-toolkit installed at $(cd /opt/mempalace-toolkit && git rev-parse --short HEAD)" ; \
|
|
fi
|
|
|
|
# rustup — Rust toolchain manager (init binary only; toolchains installed at runtime)
|
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://static.rust-lang.org/rustup/dist/${ARCH}-unknown-linux-gnu/rustup-init" -o /usr/local/bin/rustup-init && \
|
|
chmod +x /usr/local/bin/rustup-init
|
|
|
|
# gitea-mcp — MCP server for Gitea API
|
|
ARG GITEA_MCP_VERSION=latest
|
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "arm64" ;; *) echo "x86_64" ;; esac) && \
|
|
V="${GITEA_MCP_VERSION}" && \
|
|
if [ "$V" = "latest" ]; then \
|
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://gitea.com/gitea/gitea-mcp/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
fi && \
|
|
V="${V#v}" && [ -n "$V" ] && \
|
|
echo "Installing gitea-mcp ${V}" && \
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://gitea.com/gitea/gitea-mcp/releases/download/v${V}/gitea-mcp_Linux_${ARCH}.tar.gz" \
|
|
| tar -xz -C /usr/local/bin/ gitea-mcp && \
|
|
chmod +x /usr/local/bin/gitea-mcp && \
|
|
gitea-mcp --version
|
|
|
|
# Locales
|
|
RUN sed -i -E '/(en_US|en_GB|sv_SE|da_DK|nb_NO|fi_FI|de_DE|fr_FR|es_ES|it_IT|pt_BR|nl_NL|pl_PL|ja_JP|ko_KR|zh_CN)\.UTF-8/s/^# //g' /etc/locale.gen && locale-gen
|
|
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) ──
|
|
ARG NODE_VERSION=22
|
|
RUN curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors https://deb.nodesource.com/setup_${NODE_VERSION}.x | bash - && \
|
|
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.
|
|
ARG TEALDEER_VERSION=latest
|
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64" ;; *) echo "x86_64" ;; esac) && \
|
|
V="${TEALDEER_VERSION}" && \
|
|
if [ "$V" = "latest" ]; then \
|
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/tealdeer-rs/tealdeer/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
fi && \
|
|
V="${V#v}" && [ -n "$V" ] && \
|
|
echo "Installing tealdeer ${V}" && \
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/tealdeer-rs/tealdeer/releases/download/v${V}/tealdeer-linux-${ARCH}-musl" -o /usr/local/bin/tldr && \
|
|
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
|
|
# version line, e.g. 3.x). The cloud-init repo's deploy.sh/provision.sh
|
|
# require mikefarah yq v4 (the unrelated Debian python yq is v3.x). Follows
|
|
# the repo's `latest` convention (like tealdeer/uv/etc.); the smoke test pins
|
|
# the contract to major v4, so a future yq v5 fails CI instead of silently
|
|
# breaking provision.sh. Pin a specific tag with --build-arg YQ_VERSION=vX.Y.Z.
|
|
ARG YQ_VERSION=latest
|
|
RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "amd64" ;; arm64) echo "arm64" ;; *) echo "amd64" ;; esac) && \
|
|
V="${YQ_VERSION}" && \
|
|
if [ "$V" = "latest" ]; then \
|
|
V=$(curl -sI --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/mikefarah/yq/releases/latest" | awk 'tolower($1)=="location:" { sub(/\r$/,"",$2); n=split($2,a,"/"); print a[n] }'); \
|
|
fi && \
|
|
[ -n "$V" ] && \
|
|
echo "Installing mikefarah yq ${V}" && \
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://github.com/mikefarah/yq/releases/download/${V}/yq_linux_${ARCH}" -o /usr/local/bin/yq && \
|
|
chmod +x /usr/local/bin/yq && \
|
|
yq --version
|
|
|
|
# ── AWS CLI v2 (for SSO/Bedrock authentication) ─────────────────────
|
|
RUN ARCH=$(case "${TARGETARCH}" in \
|
|
amd64) echo "x86_64" ;; \
|
|
arm64) echo "aarch64" ;; \
|
|
*) echo "x86_64" ;; \
|
|
esac) && \
|
|
curl -fsSL --retry 5 --retry-delay 5 --retry-all-errors "https://awscli.amazonaws.com/awscli-exe-linux-${ARCH}.zip" -o /tmp/awscli.zip && \
|
|
unzip -q /tmp/awscli.zip -d /tmp && \
|
|
/tmp/aws/install && \
|
|
rm -rf /tmp/aws /tmp/awscli.zip && \
|
|
aws --version
|
|
|
|
# ── Non-root user ────────────────────────────────────────────────────
|
|
ARG USER_NAME=developer
|
|
ARG USER_UID=1000
|
|
ARG USER_GID=1000
|
|
|
|
RUN groupadd --gid ${USER_GID} ${USER_NAME} && \
|
|
useradd --uid ${USER_UID} --gid ${USER_GID} -m -s /bin/bash ${USER_NAME} && \
|
|
echo "${USER_NAME} ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers.d/${USER_NAME}
|
|
|
|
# Standard directories
|
|
RUN mkdir -p /workspace \
|
|
/home/${USER_NAME}/.pi/agent/extensions \
|
|
/home/${USER_NAME}/.agents/skills \
|
|
/home/${USER_NAME}/.cache/bash \
|
|
/home/${USER_NAME}/.ssh && \
|
|
chown -R ${USER_NAME}:${USER_NAME} /workspace /home/${USER_NAME}
|
|
|
|
# ── Pre-warm chromadb embedding model ──────────────────────────────
|
|
RUN if [ "${INSTALL_MEMPALACE}" = "true" ]; then \
|
|
gosu ${USER_NAME} /opt/uv-tools/mempalace/bin/python -c "\
|
|
from chromadb.utils.embedding_functions import ONNXMiniLM_L6_V2; \
|
|
ef = ONNXMiniLM_L6_V2(); \
|
|
_ = ef(['warmup']); \
|
|
print('chromadb embedding model warmed: all-MiniLM-L6-v2')" && \
|
|
ls -lh /home/${USER_NAME}/.cache/chroma/onnx_models/all-MiniLM-L6-v2/ ; \
|
|
fi
|
|
|
|
# ── User-writable npm global prefix on the devbox-pi-config volume ──
|
|
# Build-time installs use NPM_CONFIG_PREFIX=/usr (see Dockerfile.variant).
|
|
# Runtime npm/pi installs use this prefix → land on the named volume.
|
|
ENV NPM_CONFIG_PREFIX=/home/${USER_NAME}/.pi/npm-global
|
|
ENV PATH="/home/${USER_NAME}/.pi/npm-global/bin:${PATH}"
|
|
|
|
# ── Shell defaults (bash history, aliases, readline) ─────────────────
|
|
RUN mkdir -p /etc/skel-devbox
|
|
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
|
|
# named volume over a home dir can't shadow them; linked into ~/.agents/skills
|
|
# by entrypoint-user.sh, and the snippet is concatenated onto the global
|
|
# AGENTS.md in Dockerfile.variant (after pi-toolkit, which owns that file).
|
|
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
|
|
WORKDIR /workspace
|
|
|
|
ENTRYPOINT ["entrypoint.sh"]
|
|
CMD ["bash", "-l"]
|