Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d619a6e2ec | |||
| 2abfee141b | |||
| c346a106a3 | |||
| 8de0fad776 | |||
| ed49b8d97a | |||
| 9eff3f3c48 |
@@ -58,6 +58,9 @@ jobs:
|
|||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Guard — base *_REF args must be folded into the base hash
|
||||||
|
run: bash scripts/check-base-hash.sh
|
||||||
|
|
||||||
- name: Compute base tag from Dockerfile.base + dependencies
|
- name: Compute base tag from Dockerfile.base + dependencies
|
||||||
id: compute
|
id: compute
|
||||||
run: |
|
run: |
|
||||||
@@ -126,53 +129,72 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Resolve pi version + companion refs
|
- name: Resolve pi version + companion refs
|
||||||
id: resolve
|
id: resolve
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -euo pipefail
|
||||||
# Query npm registry directly; catthehacker/ubuntu:act-latest's npm
|
AUTH_HEADER="Authorization: token ${GITEA_BUILD_TOKEN:-${GITHUB_TOKEN:-}}"
|
||||||
# is not reliably on PATH in act_runner job containers.
|
|
||||||
PI_VERSION=$(curl -sf "https://registry.npmjs.org/@earendil-works%2Fpi-coding-agent/latest" | jq -r '.version')
|
# Fail loud rather than silently shipping a floating branch. A
|
||||||
|
# transient network/API failure must ABORT the release, not bake
|
||||||
|
# an unpinned ref that defeats both cache-busting AND after-the-
|
||||||
|
# fact reproducibility. (Previously each lookup fell back to
|
||||||
|
# `main`/`master` via `|| echo`.)
|
||||||
|
require_sha() { # $1=label $2=value
|
||||||
|
if ! printf '%s' "${2:-}" | grep -qiE '^[0-9a-f]{40}$'; then
|
||||||
|
echo "::error::Could not resolve $1 to a commit SHA (got '${2:-<empty>}'). Refusing to fall back to a floating ref — published images must stay reproducible. Check connectivity and GITEA_BUILD_TOKEN/GITHUB_TOKEN."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# pi version from npm (catthehacker/ubuntu:act-latest's npm is not
|
||||||
|
# reliably on PATH in act_runner job containers, so query directly).
|
||||||
|
PI_VERSION=$(curl -sf "https://registry.npmjs.org/@earendil-works%2Fpi-coding-agent/latest" | jq -r '.version' 2>/dev/null || true)
|
||||||
|
if ! printf '%s' "${PI_VERSION:-}" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+'; then
|
||||||
|
echo "::error::Could not resolve pi version from npm (got '${PI_VERSION:-<empty>}')."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
echo "pi_version=${PI_VERSION}" >> "$GITHUB_OUTPUT"
|
echo "pi_version=${PI_VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
# Resolve pi-fork / pi-observational-memory git refs to commit
|
|
||||||
# SHAs so the build-arg string changes whenever upstream moves.
|
# pi-fork / pi-observational-memory (GitHub) → commit SHAs.
|
||||||
FORK_REF=$(curl -sf -H "Accept: application/vnd.github.sha" \
|
FORK_REF=$(curl -sf -H "Accept: application/vnd.github.sha" \
|
||||||
"https://api.github.com/repos/elpapi42/pi-fork/commits/master" || echo "master")
|
"https://api.github.com/repos/elpapi42/pi-fork/commits/master" || true)
|
||||||
|
require_sha PI_FORK_REF "$FORK_REF"
|
||||||
OBSMEM_REF=$(curl -sf -H "Accept: application/vnd.github.sha" \
|
OBSMEM_REF=$(curl -sf -H "Accept: application/vnd.github.sha" \
|
||||||
"https://api.github.com/repos/elpapi42/pi-observational-memory/commits/master" || echo "master")
|
"https://api.github.com/repos/elpapi42/pi-observational-memory/commits/master" || true)
|
||||||
[ -n "$FORK_REF" ] || FORK_REF=master
|
require_sha PI_OBSMEM_REF "$OBSMEM_REF"
|
||||||
[ -n "$OBSMEM_REF" ] || OBSMEM_REF=master
|
|
||||||
echo "fork_ref=${FORK_REF}" >> "$GITHUB_OUTPUT"
|
echo "fork_ref=${FORK_REF}" >> "$GITHUB_OUTPUT"
|
||||||
echo "obsmem_ref=${OBSMEM_REF}" >> "$GITHUB_OUTPUT"
|
echo "obsmem_ref=${OBSMEM_REF}" >> "$GITHUB_OUTPUT"
|
||||||
# Also resolve pi-toolkit / pi-extensions main HEADs to SHAs so a
|
|
||||||
# workflow_dispatch re-run produces byte-identical images when
|
# pi-toolkit / pi-extensions (Gitea) → commit SHAs. Gitea API
|
||||||
# those repos haven't moved (and a clean diff in build-arg strings
|
# requires auth even for public-repo commit listing.
|
||||||
# when they have, defeating the registry buildcache footgun).
|
TOOLKIT_REF=$(curl -sf -H "$AUTH_HEADER" \
|
||||||
# Gitea API requires auth even for public-repo commit listing.
|
|
||||||
TOOLKIT_REF=$(curl -sf -H "Authorization: token ${GITEA_BUILD_TOKEN:-${GITHUB_TOKEN:-}}" \
|
|
||||||
"https://gitea.jordbo.se/api/v1/repos/joakimp/pi-toolkit/commits?limit=1&sha=main" \
|
"https://gitea.jordbo.se/api/v1/repos/joakimp/pi-toolkit/commits?limit=1&sha=main" \
|
||||||
| jq -r '.[0].sha // "main"' 2>/dev/null || echo "main")
|
| jq -r '.[0].sha // empty' 2>/dev/null || true)
|
||||||
EXTENSIONS_REF=$(curl -sf -H "Authorization: token ${GITEA_BUILD_TOKEN:-${GITHUB_TOKEN:-}}" \
|
require_sha PI_TOOLKIT_REF "$TOOLKIT_REF"
|
||||||
|
EXTENSIONS_REF=$(curl -sf -H "$AUTH_HEADER" \
|
||||||
"https://gitea.jordbo.se/api/v1/repos/joakimp/pi-extensions/commits?limit=1&sha=main" \
|
"https://gitea.jordbo.se/api/v1/repos/joakimp/pi-extensions/commits?limit=1&sha=main" \
|
||||||
| jq -r '.[0].sha // "main"' 2>/dev/null || echo "main")
|
| jq -r '.[0].sha // empty' 2>/dev/null || true)
|
||||||
[ -n "$TOOLKIT_REF" ] || TOOLKIT_REF=main
|
require_sha PI_EXTENSIONS_REF "$EXTENSIONS_REF"
|
||||||
[ -n "$EXTENSIONS_REF" ] || EXTENSIONS_REF=main
|
|
||||||
echo "toolkit_ref=${TOOLKIT_REF}" >> "$GITHUB_OUTPUT"
|
echo "toolkit_ref=${TOOLKIT_REF}" >> "$GITHUB_OUTPUT"
|
||||||
echo "extensions_ref=${EXTENSIONS_REF}" >> "$GITHUB_OUTPUT"
|
echo "extensions_ref=${EXTENSIONS_REF}" >> "$GITHUB_OUTPUT"
|
||||||
# Resolve mempalace-toolkit main HEAD to a SHA. UNLIKE the others,
|
|
||||||
# mempalace-toolkit is cloned in Dockerfile.base, so this SHA is
|
# mempalace-toolkit (Gitea) → commit SHA. UNLIKE the others this
|
||||||
# ALSO folded into the base-decide hash to force a base rebuild
|
# is cloned in Dockerfile.base, so the SAME SHA is ALSO folded
|
||||||
# when the toolkit moves (without it, a toolkit-only fix silently
|
# into the base-decide hash (see that job) to force a base rebuild
|
||||||
# fails to land unless Dockerfile.base itself changes).
|
# when the toolkit moves — otherwise a toolkit-only fix silently
|
||||||
MEMPALACE_TOOLKIT_REF=$(curl -sf -H "Authorization: token ${GITEA_BUILD_TOKEN:-${GITHUB_TOKEN:-}}" \
|
# fails to land unless Dockerfile.base itself changes.
|
||||||
|
MEMPALACE_TOOLKIT_REF=$(curl -sf -H "$AUTH_HEADER" \
|
||||||
"https://gitea.jordbo.se/api/v1/repos/joakimp/mempalace-toolkit/commits?limit=1&sha=main" \
|
"https://gitea.jordbo.se/api/v1/repos/joakimp/mempalace-toolkit/commits?limit=1&sha=main" \
|
||||||
| jq -r '.[0].sha // "main"' 2>/dev/null || echo "main")
|
| jq -r '.[0].sha // empty' 2>/dev/null || true)
|
||||||
[ -n "$MEMPALACE_TOOLKIT_REF" ] || MEMPALACE_TOOLKIT_REF=main
|
require_sha MEMPALACE_TOOLKIT_REF "$MEMPALACE_TOOLKIT_REF"
|
||||||
echo "mempalace_toolkit_ref=${MEMPALACE_TOOLKIT_REF}" >> "$GITHUB_OUTPUT"
|
echo "mempalace_toolkit_ref=${MEMPALACE_TOOLKIT_REF}" >> "$GITHUB_OUTPUT"
|
||||||
# Resolve pi-studio (omaclaren/pi-studio) main HEAD to a SHA for
|
|
||||||
# the :latest-studio variant — same cache-busting rationale.
|
# pi-studio (omaclaren/pi-studio) → commit SHA for :latest-studio.
|
||||||
STUDIO_REF=$(curl -sf -H "Accept: application/vnd.github.sha" \
|
STUDIO_REF=$(curl -sf -H "Accept: application/vnd.github.sha" \
|
||||||
"https://api.github.com/repos/omaclaren/pi-studio/commits/main" || echo "main")
|
"https://api.github.com/repos/omaclaren/pi-studio/commits/main" || true)
|
||||||
[ -n "$STUDIO_REF" ] || STUDIO_REF=main
|
require_sha PI_STUDIO_REF "$STUDIO_REF"
|
||||||
echo "studio_ref=${STUDIO_REF}" >> "$GITHUB_OUTPUT"
|
echo "studio_ref=${STUDIO_REF}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
echo "Resolved PI_VERSION=${PI_VERSION}"
|
echo "Resolved PI_VERSION=${PI_VERSION}"
|
||||||
echo "Resolved PI_FORK_REF=${FORK_REF}, PI_OBSMEM_REF=${OBSMEM_REF}"
|
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_TOOLKIT_REF=${TOOLKIT_REF}, PI_EXTENSIONS_REF=${EXTENSIONS_REF}"
|
||||||
@@ -299,6 +321,9 @@ jobs:
|
|||||||
PI_OBSMEM_REF=${{ needs.resolve-versions.outputs.obsmem_ref }}
|
PI_OBSMEM_REF=${{ needs.resolve-versions.outputs.obsmem_ref }}
|
||||||
PI_TOOLKIT_REF=${{ needs.resolve-versions.outputs.toolkit_ref }}
|
PI_TOOLKIT_REF=${{ needs.resolve-versions.outputs.toolkit_ref }}
|
||||||
PI_EXTENSIONS_REF=${{ needs.resolve-versions.outputs.extensions_ref }}
|
PI_EXTENSIONS_REF=${{ needs.resolve-versions.outputs.extensions_ref }}
|
||||||
|
MEMPALACE_TOOLKIT_REF=${{ needs.resolve-versions.outputs.mempalace_toolkit_ref }}
|
||||||
|
RELEASE_TAG=smoke
|
||||||
|
SOURCE_REVISION=${{ github.sha }}
|
||||||
- name: Smoke test (amd64)
|
- name: Smoke test (amd64)
|
||||||
env:
|
env:
|
||||||
EXPECTED_PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
|
EXPECTED_PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
|
||||||
@@ -355,6 +380,9 @@ jobs:
|
|||||||
PI_EXTENSIONS_REF=${{ needs.resolve-versions.outputs.extensions_ref }}
|
PI_EXTENSIONS_REF=${{ needs.resolve-versions.outputs.extensions_ref }}
|
||||||
INSTALL_STUDIO=true
|
INSTALL_STUDIO=true
|
||||||
PI_STUDIO_REF=${{ needs.resolve-versions.outputs.studio_ref }}
|
PI_STUDIO_REF=${{ needs.resolve-versions.outputs.studio_ref }}
|
||||||
|
MEMPALACE_TOOLKIT_REF=${{ needs.resolve-versions.outputs.mempalace_toolkit_ref }}
|
||||||
|
RELEASE_TAG=smoke-studio
|
||||||
|
SOURCE_REVISION=${{ github.sha }}
|
||||||
- name: Smoke test studio (amd64)
|
- name: Smoke test studio (amd64)
|
||||||
env:
|
env:
|
||||||
EXPECTED_PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
|
EXPECTED_PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
|
||||||
@@ -406,10 +434,12 @@ jobs:
|
|||||||
OBSMEM_REF: ${{ needs.resolve-versions.outputs.obsmem_ref }}
|
OBSMEM_REF: ${{ needs.resolve-versions.outputs.obsmem_ref }}
|
||||||
TOOLKIT_REF: ${{ needs.resolve-versions.outputs.toolkit_ref }}
|
TOOLKIT_REF: ${{ needs.resolve-versions.outputs.toolkit_ref }}
|
||||||
EXTENSIONS_REF: ${{ needs.resolve-versions.outputs.extensions_ref }}
|
EXTENSIONS_REF: ${{ needs.resolve-versions.outputs.extensions_ref }}
|
||||||
|
MEMPALACE_TOOLKIT_REF: ${{ needs.resolve-versions.outputs.mempalace_toolkit_ref }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
TAG_FLAGS=()
|
TAG_FLAGS=()
|
||||||
while IFS= read -r t; do [[ -n "$t" ]] && TAG_FLAGS+=( -t "$t" ); done <<< "${TAGS}"
|
while IFS= read -r t; do [[ -n "$t" ]] && TAG_FLAGS+=( -t "$t" ); done <<< "${TAGS}"
|
||||||
|
BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||||
# 3-attempt retry (see build-base step for rationale).
|
# 3-attempt retry (see build-base step for rationale).
|
||||||
for attempt in 1 2 3; do
|
for attempt in 1 2 3; do
|
||||||
echo "==> Build+push attempt ${attempt}/3"
|
echo "==> Build+push attempt ${attempt}/3"
|
||||||
@@ -423,6 +453,10 @@ jobs:
|
|||||||
--build-arg "PI_OBSMEM_REF=${OBSMEM_REF}" \
|
--build-arg "PI_OBSMEM_REF=${OBSMEM_REF}" \
|
||||||
--build-arg "PI_TOOLKIT_REF=${TOOLKIT_REF}" \
|
--build-arg "PI_TOOLKIT_REF=${TOOLKIT_REF}" \
|
||||||
--build-arg "PI_EXTENSIONS_REF=${EXTENSIONS_REF}" \
|
--build-arg "PI_EXTENSIONS_REF=${EXTENSIONS_REF}" \
|
||||||
|
--build-arg "MEMPALACE_TOOLKIT_REF=${MEMPALACE_TOOLKIT_REF}" \
|
||||||
|
--build-arg "RELEASE_TAG=${RELEASE_TAG}" \
|
||||||
|
--build-arg "BUILD_DATE=${BUILD_DATE}" \
|
||||||
|
--build-arg "SOURCE_REVISION=${GITHUB_SHA:-}" \
|
||||||
"${TAG_FLAGS[@]}" \
|
"${TAG_FLAGS[@]}" \
|
||||||
.; then
|
.; then
|
||||||
echo "==> Attempt ${attempt} succeeded"
|
echo "==> Attempt ${attempt} succeeded"
|
||||||
@@ -487,10 +521,12 @@ jobs:
|
|||||||
TOOLKIT_REF: ${{ needs.resolve-versions.outputs.toolkit_ref }}
|
TOOLKIT_REF: ${{ needs.resolve-versions.outputs.toolkit_ref }}
|
||||||
EXTENSIONS_REF: ${{ needs.resolve-versions.outputs.extensions_ref }}
|
EXTENSIONS_REF: ${{ needs.resolve-versions.outputs.extensions_ref }}
|
||||||
STUDIO_REF: ${{ needs.resolve-versions.outputs.studio_ref }}
|
STUDIO_REF: ${{ needs.resolve-versions.outputs.studio_ref }}
|
||||||
|
MEMPALACE_TOOLKIT_REF: ${{ needs.resolve-versions.outputs.mempalace_toolkit_ref }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
TAG_FLAGS=()
|
TAG_FLAGS=()
|
||||||
while IFS= read -r t; do [[ -n "$t" ]] && TAG_FLAGS+=( -t "$t" ); done <<< "${TAGS}"
|
while IFS= read -r t; do [[ -n "$t" ]] && TAG_FLAGS+=( -t "$t" ); done <<< "${TAGS}"
|
||||||
|
BUILD_DATE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||||
# 3-attempt retry (see build-base step for rationale).
|
# 3-attempt retry (see build-base step for rationale).
|
||||||
for attempt in 1 2 3; do
|
for attempt in 1 2 3; do
|
||||||
echo "==> Build+push attempt ${attempt}/3"
|
echo "==> Build+push attempt ${attempt}/3"
|
||||||
@@ -504,8 +540,12 @@ jobs:
|
|||||||
--build-arg "PI_OBSMEM_REF=${OBSMEM_REF}" \
|
--build-arg "PI_OBSMEM_REF=${OBSMEM_REF}" \
|
||||||
--build-arg "PI_TOOLKIT_REF=${TOOLKIT_REF}" \
|
--build-arg "PI_TOOLKIT_REF=${TOOLKIT_REF}" \
|
||||||
--build-arg "PI_EXTENSIONS_REF=${EXTENSIONS_REF}" \
|
--build-arg "PI_EXTENSIONS_REF=${EXTENSIONS_REF}" \
|
||||||
|
--build-arg "MEMPALACE_TOOLKIT_REF=${MEMPALACE_TOOLKIT_REF}" \
|
||||||
--build-arg "INSTALL_STUDIO=true" \
|
--build-arg "INSTALL_STUDIO=true" \
|
||||||
--build-arg "PI_STUDIO_REF=${STUDIO_REF}" \
|
--build-arg "PI_STUDIO_REF=${STUDIO_REF}" \
|
||||||
|
--build-arg "RELEASE_TAG=${RELEASE_TAG}" \
|
||||||
|
--build-arg "BUILD_DATE=${BUILD_DATE}" \
|
||||||
|
--build-arg "SOURCE_REVISION=${GITHUB_SHA:-}" \
|
||||||
"${TAG_FLAGS[@]}" \
|
"${TAG_FLAGS[@]}" \
|
||||||
.; then
|
.; then
|
||||||
echo "==> Attempt ${attempt} succeeded"
|
echo "==> Attempt ${attempt} succeeded"
|
||||||
|
|||||||
@@ -14,15 +14,23 @@ re-brand of opencode-devbox's `pi-only` variant.
|
|||||||
- `Dockerfile.variant` — `FROM base-<hash>`, adds pi + companions
|
- `Dockerfile.variant` — `FROM base-<hash>`, adds pi + companions
|
||||||
(`pi-toolkit`, `pi-extensions`, `pi-fork`, `pi-observational-memory`)
|
(`pi-toolkit`, `pi-extensions`, `pi-fork`, `pi-observational-memory`)
|
||||||
and, when `INSTALL_STUDIO=true`, vendors `pi-studio` to `/opt/pi-studio`
|
and, when `INSTALL_STUDIO=true`, vendors `pi-studio` to `/opt/pi-studio`
|
||||||
(`-studio` variant).
|
(`-studio` variant). Also appends the pi-devbox managed block from
|
||||||
|
`pi-global-AGENTS.append.md` onto pi-toolkit's `pi-global-AGENTS.md` (the
|
||||||
|
single global instruction slot pi loads) so containers proactively load the
|
||||||
|
baked `pi-devbox-environment` skill. Idempotent via a marker grep.
|
||||||
- `entrypoint.sh` — UID/GID alignment as root, then drops to `developer`.
|
- `entrypoint.sh` — UID/GID alignment as root, then drops to `developer`.
|
||||||
- `entrypoint-user.sh` — per-container start: SSH ControlMaster socket
|
- `entrypoint-user.sh` — per-container start: SSH ControlMaster socket
|
||||||
dir, LAN-access setup, MemPalace init, pi-toolkit + pi-extensions
|
dir, LAN-access setup, MemPalace init, pi-toolkit + pi-extensions
|
||||||
deploy, mempalace-bridge symlink, fork/recall + pi-studio pi-install,
|
deploy, mempalace-bridge symlink, fork/recall + pi-studio pi-install,
|
||||||
optional `studio-expose` bridge (when `STUDIO_EXPOSE=1`), skillset
|
optional `studio-expose` bridge (when `STUDIO_EXPOSE=1`), image-baked
|
||||||
deploy.
|
skills symlink-in, skillset deploy.
|
||||||
- `rootfs/` — files baked into the image (bash aliases, inputrc,
|
- `rootfs/` — files baked into the image (bash aliases, inputrc,
|
||||||
setup-lan-access.sh, `studio-expose` helper).
|
setup-lan-access.sh, `studio-expose` helper). Also
|
||||||
|
`usr/local/share/pi-devbox/skills/<name>/SKILL.md` — image-baked agent
|
||||||
|
skills (e.g. `pi-devbox-environment`) symlinked into `~/.agents/skills/` by
|
||||||
|
the entrypoint, available with or without a mounted skillset — plus
|
||||||
|
`usr/local/share/pi-devbox/pi-global-AGENTS.append.md` (the global-AGENTS
|
||||||
|
pointer concatenated in `Dockerfile.variant`).
|
||||||
- `scripts/smoke-test.sh` — sanity checks run by CI before pushing to Hub.
|
- `scripts/smoke-test.sh` — sanity checks run by CI before pushing to Hub.
|
||||||
- `.gitea/workflows/docker-publish.yml` — two-phase CI (base-decide →
|
- `.gitea/workflows/docker-publish.yml` — two-phase CI (base-decide →
|
||||||
build-base → smoke → build-variant → promote-base-latest →
|
build-base → smoke → build-variant → promote-base-latest →
|
||||||
@@ -33,7 +41,8 @@ re-brand of opencode-devbox's `pi-only` variant.
|
|||||||
## Versioning scheme
|
## Versioning scheme
|
||||||
|
|
||||||
- Tags follow semver. **v1.0.0** is the first decoupled release; future
|
- Tags follow semver. **v1.0.0** is the first decoupled release; future
|
||||||
minor bumps add variants (`-studio`, `-studio-tex`); patch bumps follow
|
minor bumps add variants (`-studio`, `-studio-tex`) or significant base
|
||||||
|
additions (e.g. v1.2.0 image-baked agent skills); patch bumps follow
|
||||||
pi npm version updates and small fixes.
|
pi npm version updates and small fixes.
|
||||||
- Docker Hub tags: `joakimp/pi-devbox:vX.Y.Z` + `joakimp/pi-devbox:latest`
|
- Docker Hub tags: `joakimp/pi-devbox:vX.Y.Z` + `joakimp/pi-devbox:latest`
|
||||||
+ (since v1.1.0) `joakimp/pi-devbox:vX.Y.Z-studio` +
|
+ (since v1.1.0) `joakimp/pi-devbox:vX.Y.Z-studio` +
|
||||||
|
|||||||
+146
-2
@@ -11,7 +11,151 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Unreleased
|
## v1.2.0 — 2026-06-22
|
||||||
|
|
||||||
|
Minor release: **image-baked agent skills** — a new base mechanism that ships
|
||||||
|
skills inside the image (independent of any mounted skillset repo) — plus the
|
||||||
|
first such skill, `pi-devbox-environment`, and pi `0.79.9` → `0.79.10`
|
||||||
|
(auto-resolved from npm `latest` at build).
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Image-baked agent skills.** Skills under
|
||||||
|
`/usr/local/share/pi-devbox/skills/<name>/` are now symlinked into
|
||||||
|
`~/.agents/skills/` by `entrypoint-user.sh` on every start, making them
|
||||||
|
available **with or without** a mounted `skillset` repo. The symlink points
|
||||||
|
at the image path (so it survives volume recreate, unlike anything baked
|
||||||
|
under a home dir a named volume would shadow) and is created only when
|
||||||
|
absent, so a same-named skillset skill or user override is never clobbered.
|
||||||
|
The skillset deploy classifies these as foreign-links and its `--prune-stale`
|
||||||
|
pass leaves them untouched.
|
||||||
|
- **`pi-devbox-environment` skill** (the first image-baked skill). Teaches
|
||||||
|
agents the container-shaped facts that are easy to get wrong: the
|
||||||
|
persistence/ephemerality tier model (what survives `down -v` / image
|
||||||
|
update), host + LAN SSH reachability and ControlMaster, split-horizon DNS
|
||||||
|
*mechanisms*, the interactive-vs-tool-shell alias gotcha (`dssh`/`dscp`/
|
||||||
|
`cat`→`bat` don't exist in the non-interactive bash tool), the tmux 0-index
|
||||||
|
constraint, uv-first Python, and pi-studio reachability. Deliberately
|
||||||
|
environment-agnostic — host OS, hostnames, internal domains, and nameservers
|
||||||
|
are discovered at runtime, never hardcoded.
|
||||||
|
- **Proactive skill awareness via the global `AGENTS.md`.** `Dockerfile.variant`
|
||||||
|
appends a short, gated pointer (`pi-global-AGENTS.append.md`) onto
|
||||||
|
pi-toolkit's `pi-global-AGENTS.md` — the single global instruction slot pi
|
||||||
|
loads at startup — so containers load the `pi-devbox-environment` skill
|
||||||
|
proactively rather than only on description match. The pointer fires only
|
||||||
|
inside a pi-devbox container (checks for `/usr/local/lib/pi-devbox/`).
|
||||||
|
Build-time append is idempotent via a marker grep; runtime is unaffected
|
||||||
|
(the file is root-owned and re-symlinked by pi-toolkit each boot).
|
||||||
|
- **Smoke-test coverage** for the new mechanism: build-time presence of the
|
||||||
|
baked skill + append snippet + the merged marker in `pi-global-AGENTS.md`,
|
||||||
|
and a runtime assertion that `~/.agents/skills/pi-devbox-environment` is
|
||||||
|
linked after the entrypoint runs.
|
||||||
|
|
||||||
|
### Bumped: pi 0.79.9 → 0.79.10
|
||||||
|
|
||||||
|
Resolved from npm `latest` at build (v1.1.7 shipped `0.79.9`). See the
|
||||||
|
[pi changelog](https://github.com/earendil-works/pi/blob/main/CHANGELOG.md)
|
||||||
|
for the upstream `0.79.10` notes.
|
||||||
|
|
||||||
|
## v1.1.7 — 2026-06-21
|
||||||
|
|
||||||
|
Patch release: pi `0.79.8` → `0.79.9` (auto-resolved at build), plus the
|
||||||
|
`ssh-lan.conf` LAN-peer documentation that landed on `main` after v1.1.6.
|
||||||
|
Companion refs are auto-resolved to SHAs at build as before.
|
||||||
|
|
||||||
|
### Bumped: pi 0.79.8 → 0.79.9
|
||||||
|
|
||||||
|
Notable upstream changes (from [pi releases](https://github.com/earendil-works/pi/releases/tag/v0.79.9)):
|
||||||
|
|
||||||
|
- **Chat-template thinking compatibility** — OpenAI-compatible custom
|
||||||
|
providers can map pi thinking levels into `chat_template_kwargs`, enabling
|
||||||
|
vLLM/Hugging Face chat-template models (e.g. DeepSeek) to use
|
||||||
|
provider-native thinking controls.
|
||||||
|
- **GLM-5.2 provider improvements** — corrected Fireworks OpenAI-compatible
|
||||||
|
routing and OpenRouter `xhigh` thinking support, improving `/model`
|
||||||
|
behaviour and high-effort reasoning for GLM-5.2.
|
||||||
|
- **Fixes** — same-directory session switches now reuse imported extension
|
||||||
|
modules (fresh instances + lifecycle events preserved); deep session
|
||||||
|
branches no longer take quadratic time to build context; Markdown
|
||||||
|
streaming code-fence rendering no longer flickers on partial closing
|
||||||
|
fences; fuzzy `edit` matches preserve untouched line blocks instead of
|
||||||
|
rewriting the whole file; `/model` hides Copilot models unavailable to the
|
||||||
|
account and ranks exact provider-prefixed matches first.
|
||||||
|
|
||||||
|
### Docs: document `~/.config/devbox-shell/ssh-lan.conf` for naming LAN peers
|
||||||
|
|
||||||
|
The host-owned, bind-mounted `~/.config/devbox-shell/ssh-lan.conf` is the
|
||||||
|
intended place to add `ProxyJump host` overrides for **named** LAN peers (so
|
||||||
|
`pi --ssh <peer>` / `dssh <peer>` route through the host), but it was only
|
||||||
|
mentioned in `.env.example` and the `setup-lan-access.sh` header — never in the
|
||||||
|
README. Added a "Naming LAN peers" subsection to the README troubleshooting
|
||||||
|
block (plus a pointer from the SSH/ControlMaster section), and corrected the
|
||||||
|
stale `setup-lan-access.sh` comment that suggested editing the read-only
|
||||||
|
`~/.ssh/config` instead of `ssh-lan.conf`.
|
||||||
|
|
||||||
|
## v1.1.6 — 2026-06-19
|
||||||
|
|
||||||
|
Build provenance + reproducibility hardening, plus pi `0.79.7` → `0.79.8`
|
||||||
|
(auto-resolved at build). Companion refs are auto-resolved to SHAs at build
|
||||||
|
as before.
|
||||||
|
|
||||||
|
### Bumped: pi 0.79.7 → 0.79.8
|
||||||
|
|
||||||
|
Notable upstream changes (from [pi releases](https://github.com/earendil-works/pi/releases/tag/v0.79.8)):
|
||||||
|
|
||||||
|
- **Selective provider base entry points** — SDK users can pair
|
||||||
|
`@earendil-works/pi-ai/base` and `@earendil-works/pi-agent-core/base` with
|
||||||
|
explicit provider registration to keep bundled apps from including unused
|
||||||
|
provider transports.
|
||||||
|
- **Mistral prompt caching** — Mistral sessions use provider-side prompt
|
||||||
|
caching keyed on the pi session ID, with cached-token usage/cost
|
||||||
|
accounting.
|
||||||
|
- **Post-compaction token estimates** — compact results and compaction
|
||||||
|
events now include estimated post-compaction token counts.
|
||||||
|
- **OpenRouter Fusion alias** — `openrouter/fusion` available as a built-in
|
||||||
|
OpenRouter model alias.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- **Self-describing images: OCI labels + on-disk build manifest.** The
|
||||||
|
variant build now records exactly which pi version and companion-repo
|
||||||
|
commits were baked into each image. Previously the SHAs resolved by CI
|
||||||
|
only ever reached the build log (which rotates), so a published tag was
|
||||||
|
not reconstructable after the fact — confirming what shipped meant
|
||||||
|
triangulating from `git`, `pi --version`, and extension source.
|
||||||
|
- OCI labels: `org.opencontainers.image.{version,revision,created}` plus
|
||||||
|
`se.jordbo.pi-devbox.{pi,pi-toolkit,pi-extensions,pi-fork,pi-obsmem,mempalace-toolkit,pi-studio}-*ref` —
|
||||||
|
inspect with `docker inspect`.
|
||||||
|
- `/etc/pi-devbox/build-manifest.json` written from **ground truth** (the
|
||||||
|
actual checked-out `HEAD` of each `/opt` clone + live `pi --version`),
|
||||||
|
not just the intended build-args, so it also exposes a clone that
|
||||||
|
silently resolved to the wrong ref. The provenance ARGs are declared
|
||||||
|
last so a changing `BUILD_DATE` never invalidates the expensive
|
||||||
|
install/clone layers.
|
||||||
|
- **`scripts/check-base-hash.sh` — base-rebuild invariant guard.** Every
|
||||||
|
floating `ARG *_REF` consumed by `Dockerfile.base` must be folded into the
|
||||||
|
`base_tag` hash, or a ref-only change won't trigger a base rebuild (the
|
||||||
|
v1.1.2 mempalace-toolkit staleness footgun). The guard fails CI the moment
|
||||||
|
someone adds an `ARG *_REF` to `Dockerfile.base` without folding it in; it
|
||||||
|
runs in the `base-decide` job and locally. Smoke-test gained assertions for
|
||||||
|
the manifest (present, no `"unknown"` components) and the OCI labels.
|
||||||
|
- **Overridable companion repo URLs.** The three gitea-hosted companions
|
||||||
|
(`pi-toolkit`, `pi-extensions`, `mempalace-toolkit`) gained `*_REPO`
|
||||||
|
build-args defaulting to their canonical `gitea.jordbo.se` origin —
|
||||||
|
matching the existing `PI_FORK_REPO` / `PI_OBSMEM_REPO` / `PI_STUDIO_REPO`
|
||||||
|
pattern. A relocated or forked build can now repoint a companion at a
|
||||||
|
mirror, another host, or a local path (`--build-arg PI_EXTENSIONS_REPO=...`)
|
||||||
|
without editing the Dockerfiles. Defaults are unchanged, so the canonical
|
||||||
|
CI build is byte-identical.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- **`resolve-versions` now fails loud instead of falling back to a floating
|
||||||
|
branch.** Each pi-version / companion-ref lookup previously degraded to
|
||||||
|
`main`/`master` on a transient API/network failure (`|| echo "main"`),
|
||||||
|
silently shipping an unpinned ref that defeats both cache-busting and
|
||||||
|
reproducibility. Resolution now validates each result is a 40-hex commit
|
||||||
|
SHA (and pi a real semver) and aborts the release otherwise.
|
||||||
|
|
||||||
## v1.1.5 — 2026-06-18
|
## v1.1.5 — 2026-06-18
|
||||||
|
|
||||||
@@ -407,7 +551,7 @@ dependencies.
|
|||||||
### Future work
|
### Future work
|
||||||
|
|
||||||
- v1.1.0: `:latest-studio` variant (adds [pi-studio](https://github.com/omaclaren/pi-studio)).
|
- v1.1.0: `:latest-studio` variant (adds [pi-studio](https://github.com/omaclaren/pi-studio)).
|
||||||
- v1.2.0: `:latest-studio-tex` variant (adds texlive-xetex for PDF export).
|
- v1.3.0: `:latest-studio-tex` variant (adds texlive-xetex for PDF export).
|
||||||
|
|
||||||
## v0.79.0 — 2026-06-08
|
## v0.79.0 — 2026-06-08
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ Full setup guide — authentication for each provider (Anthropic, OpenAI, Gemini
|
|||||||
- **[pi-extensions](https://gitea.jordbo.se/joakimp/pi-extensions)** — 7 user-facing extensions: `ext-toggle`, `mcp-loader`, `todo`, `ssh-controlmaster`, `notify`, `git-checkpoint`, `confirm-destructive`
|
- **[pi-extensions](https://gitea.jordbo.se/joakimp/pi-extensions)** — 7 user-facing extensions: `ext-toggle`, `mcp-loader`, `todo`, `ssh-controlmaster`, `notify`, `git-checkpoint`, `confirm-destructive`
|
||||||
- **`fork`** ([pi-fork](https://github.com/elpapi42/pi-fork)) and **`recall`** ([pi-observational-memory](https://github.com/elpapi42/pi-observational-memory)) tools
|
- **`fork`** ([pi-fork](https://github.com/elpapi42/pi-fork)) and **`recall`** ([pi-observational-memory](https://github.com/elpapi42/pi-observational-memory)) tools
|
||||||
- **mempalace bridge** — MCP extension auto-symlinked so pi reads/writes the host-mounted palace
|
- **mempalace bridge** — MCP extension auto-symlinked so pi reads/writes the host-mounted palace
|
||||||
|
- **image-baked agent skills** — skills under `/usr/local/share/pi-devbox/skills/` (e.g. `pi-devbox-environment`, which teaches agents the container's persistence/networking/DNS/tmux/REPL specifics) are symlinked into `~/.agents/skills/` on start, available with or without a mounted skillset repo
|
||||||
|
|
||||||
The entrypoint deploys/registers all of these on first container start. Re-running is idempotent and preserves user edits.
|
The entrypoint deploys/registers all of these on first container start. Re-running is idempotent and preserves user edits.
|
||||||
|
|
||||||
|
|||||||
+11
-1
@@ -351,6 +351,11 @@ RUN if [ "${INSTALL_MEMPALACE}" = "true" ]; then \
|
|||||||
# ── mempalace-toolkit — bash wrappers for session/docs mining ────────
|
# ── mempalace-toolkit — bash wrappers for session/docs mining ────────
|
||||||
ARG INSTALL_MEMPALACE_TOOLKIT=true
|
ARG INSTALL_MEMPALACE_TOOLKIT=true
|
||||||
ARG MEMPALACE_TOOLKIT_REF=main
|
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
|
# 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
|
# 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
|
# base-decide hash so the base rebuilds when the toolkit moves. `git clone
|
||||||
@@ -360,7 +365,7 @@ ARG MEMPALACE_TOOLKIT_REF=main
|
|||||||
RUN if [ "${INSTALL_MEMPALACE}" = "true" ] && [ "${INSTALL_MEMPALACE_TOOLKIT}" = "true" ]; then \
|
RUN if [ "${INSTALL_MEMPALACE}" = "true" ] && [ "${INSTALL_MEMPALACE_TOOLKIT}" = "true" ]; then \
|
||||||
rm -rf /opt/mempalace-toolkit && mkdir -p /opt/mempalace-toolkit && \
|
rm -rf /opt/mempalace-toolkit && mkdir -p /opt/mempalace-toolkit && \
|
||||||
git -C /opt/mempalace-toolkit init -q && \
|
git -C /opt/mempalace-toolkit init -q && \
|
||||||
git -C /opt/mempalace-toolkit remote add origin https://gitea.jordbo.se/joakimp/mempalace-toolkit.git && \
|
git -C /opt/mempalace-toolkit remote add origin "${MEMPALACE_TOOLKIT_REPO}" && \
|
||||||
ok=0; for i in 1 2 3 4 5; do \
|
ok=0; for i in 1 2 3 4 5; do \
|
||||||
if git -C /opt/mempalace-toolkit fetch --depth 1 origin "${MEMPALACE_TOOLKIT_REF}" && \
|
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; \
|
git -C /opt/mempalace-toolkit checkout -q FETCH_HEAD; then ok=1; break; fi; \
|
||||||
@@ -476,6 +481,11 @@ COPY rootfs/home/developer/.inputrc /etc/skel-devbox/.inputrc
|
|||||||
|
|
||||||
# ── Entrypoint ────────────────────────────────────────────────────────
|
# ── Entrypoint ────────────────────────────────────────────────────────
|
||||||
COPY rootfs/usr/local/lib/pi-devbox/ /usr/local/lib/pi-devbox/
|
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/studio-expose /usr/local/bin/studio-expose
|
||||||
COPY rootfs/usr/local/bin/dot-watch /usr/local/bin/dot-watch
|
COPY rootfs/usr/local/bin/dot-watch /usr/local/bin/dot-watch
|
||||||
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
|
||||||
|
|||||||
+82
-2
@@ -41,6 +41,12 @@ ARG USER_NAME=developer
|
|||||||
ARG PI_VERSION=latest
|
ARG PI_VERSION=latest
|
||||||
ARG PI_TOOLKIT_REF=main
|
ARG PI_TOOLKIT_REF=main
|
||||||
ARG PI_EXTENSIONS_REF=main
|
ARG PI_EXTENSIONS_REF=main
|
||||||
|
# Repo URLs default to the canonical gitea origin but are overridable so a
|
||||||
|
# relocated/forked build can clone from a mirror or a different host
|
||||||
|
# without editing this Dockerfile — same pattern as PI_FORK_REPO /
|
||||||
|
# PI_OBSMEM_REPO / PI_STUDIO_REPO below.
|
||||||
|
ARG PI_TOOLKIT_REPO=https://gitea.jordbo.se/joakimp/pi-toolkit.git
|
||||||
|
ARG PI_EXTENSIONS_REPO=https://gitea.jordbo.se/joakimp/pi-extensions.git
|
||||||
# pi-fork (fork tool) + pi-observational-memory (recall tool) live on GitHub
|
# pi-fork (fork tool) + pi-observational-memory (recall tool) live on GitHub
|
||||||
# under elpapi42. CI resolves these to commit SHAs to defeat the same
|
# under elpapi42. CI resolves these to commit SHAs to defeat the same
|
||||||
# cache-hit footgun that affects PI_VERSION.
|
# cache-hit footgun that affects PI_VERSION.
|
||||||
@@ -77,8 +83,8 @@ RUN set -e && \
|
|||||||
NPM_CONFIG_PREFIX=/usr npm install -g @earendil-works/pi-coding-agent@${PI_VERSION} ; \
|
NPM_CONFIG_PREFIX=/usr npm install -g @earendil-works/pi-coding-agent@${PI_VERSION} ; \
|
||||||
fi && \
|
fi && \
|
||||||
pi --version && \
|
pi --version && \
|
||||||
git_fetch_ref "https://gitea.jordbo.se/joakimp/pi-toolkit.git" "${PI_TOOLKIT_REF}" /opt/pi-toolkit && \
|
git_fetch_ref "${PI_TOOLKIT_REPO}" "${PI_TOOLKIT_REF}" /opt/pi-toolkit && \
|
||||||
git_fetch_ref "https://gitea.jordbo.se/joakimp/pi-extensions.git" "${PI_EXTENSIONS_REF}" /opt/pi-extensions && \
|
git_fetch_ref "${PI_EXTENSIONS_REPO}" "${PI_EXTENSIONS_REF}" /opt/pi-extensions && \
|
||||||
git_fetch_ref "${PI_FORK_REPO}" "${PI_FORK_REF}" /opt/pi-fork && \
|
git_fetch_ref "${PI_FORK_REPO}" "${PI_FORK_REF}" /opt/pi-fork && \
|
||||||
git_fetch_ref "${PI_OBSMEM_REPO}" "${PI_OBSMEM_REF}" /opt/pi-observational-memory && \
|
git_fetch_ref "${PI_OBSMEM_REPO}" "${PI_OBSMEM_REF}" /opt/pi-observational-memory && \
|
||||||
(cd /opt/pi-fork && npm install --omit=dev --no-audit --no-fund) && \
|
(cd /opt/pi-fork && npm install --omit=dev --no-audit --no-fund) && \
|
||||||
@@ -88,6 +94,24 @@ RUN set -e && \
|
|||||||
echo "pi-fork at $(cd /opt/pi-fork && git rev-parse --short HEAD)" && \
|
echo "pi-fork at $(cd /opt/pi-fork && git rev-parse --short HEAD)" && \
|
||||||
echo "pi-observational-memory at $(cd /opt/pi-observational-memory && git rev-parse --short HEAD)"
|
echo "pi-observational-memory at $(cd /opt/pi-observational-memory && git rev-parse --short HEAD)"
|
||||||
|
|
||||||
|
# ── pi-devbox awareness: append our pointer to the global AGENTS.md ──
|
||||||
|
# pi loads a SINGLE global instruction file (~/.pi/agent/AGENTS.md), which
|
||||||
|
# pi-toolkit's install.sh re-symlinks to /opt/pi-toolkit/pi-global-AGENTS.md on
|
||||||
|
# every container start. There is no second global slot, and that file is
|
||||||
|
# root-owned (not writable by the runtime user), so we compose at BUILD time:
|
||||||
|
# append the pi-devbox managed block to pi-toolkit's file here, after the clone.
|
||||||
|
# Idempotent via a marker grep so a rebuilt layer never double-appends. This
|
||||||
|
# makes every container proactively aware of the pi-devbox-environment skill;
|
||||||
|
# the snippet itself is gated (only fires when /usr/local/lib/pi-devbox exists).
|
||||||
|
RUN if [ -f /opt/pi-toolkit/pi-global-AGENTS.md ] && \
|
||||||
|
! grep -q 'pi-devbox:managed-block' /opt/pi-toolkit/pi-global-AGENTS.md; then \
|
||||||
|
printf '\n' >> /opt/pi-toolkit/pi-global-AGENTS.md && \
|
||||||
|
cat /usr/local/share/pi-devbox/pi-global-AGENTS.append.md >> /opt/pi-toolkit/pi-global-AGENTS.md && \
|
||||||
|
echo "appended pi-devbox block to pi-global-AGENTS.md" ; \
|
||||||
|
else \
|
||||||
|
echo "pi-devbox block already present or pi-global-AGENTS.md missing (skipped)" ; \
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Optional: pi-studio (:latest-studio variant) ─────────────────────
|
# ── Optional: pi-studio (:latest-studio variant) ─────────────────────
|
||||||
# pi-studio (omaclaren/pi-studio) is a pi-package + theme providing a
|
# pi-studio (omaclaren/pi-studio) is a pi-package + theme providing a
|
||||||
# two-pane browser workspace: prompt/response editor, KaTeX/Mermaid live
|
# two-pane browser workspace: prompt/response editor, KaTeX/Mermaid live
|
||||||
@@ -154,4 +178,60 @@ RUN if [ "${INSTALL_GO}" = "true" ]; then \
|
|||||||
ln -s /usr/local/go/bin/gofmt /usr/local/bin/gofmt; \
|
ln -s /usr/local/go/bin/gofmt /usr/local/bin/gofmt; \
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── Build provenance: OCI labels + on-disk build manifest ────────────
|
||||||
|
# Records exactly which pi version and companion-repo commits were baked
|
||||||
|
# into THIS image, so a published tag is self-describing and reproducible
|
||||||
|
# after the fact (CI logs rotate; a released image must not depend on
|
||||||
|
# them). Previously the resolved SHAs only ever reached the CI build log.
|
||||||
|
#
|
||||||
|
# These ARGs are declared LAST, immediately before the layer that uses
|
||||||
|
# them, so a changing BUILD_DATE / RELEASE_TAG / SOURCE_REVISION never
|
||||||
|
# invalidates the expensive pi-install / clone layers above.
|
||||||
|
ARG RELEASE_TAG=dev
|
||||||
|
ARG BUILD_DATE=
|
||||||
|
ARG SOURCE_REVISION=
|
||||||
|
# MEMPALACE_TOOLKIT_REF is consumed in Dockerfile.base; re-declared here
|
||||||
|
# only so its intended ref lands in the label set alongside the others.
|
||||||
|
ARG MEMPALACE_TOOLKIT_REF=main
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.version="${RELEASE_TAG}" \
|
||||||
|
org.opencontainers.image.revision="${SOURCE_REVISION}" \
|
||||||
|
org.opencontainers.image.created="${BUILD_DATE}" \
|
||||||
|
se.jordbo.pi-devbox.pi-version="${PI_VERSION}" \
|
||||||
|
se.jordbo.pi-devbox.pi-toolkit-ref="${PI_TOOLKIT_REF}" \
|
||||||
|
se.jordbo.pi-devbox.pi-extensions-ref="${PI_EXTENSIONS_REF}" \
|
||||||
|
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}"
|
||||||
|
|
||||||
|
# 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
|
||||||
|
# build-args. That way it also exposes a clone that silently resolved to
|
||||||
|
# something other than the requested ref. pi-studio is present only in the
|
||||||
|
# studio variant (JSON null otherwise).
|
||||||
|
RUN set -e; \
|
||||||
|
mkdir -p /etc/pi-devbox; \
|
||||||
|
rev() { git -C "$1" rev-parse HEAD 2>/dev/null || echo "unknown"; }; \
|
||||||
|
PI_V="$(pi --version 2>/dev/null | head -n1 | tr -d '\r\n')"; \
|
||||||
|
STUDIO_REV='null'; \
|
||||||
|
if [ -d /opt/pi-studio/.git ]; then STUDIO_REV="\"$(rev /opt/pi-studio)\""; fi; \
|
||||||
|
{ \
|
||||||
|
echo '{'; \
|
||||||
|
echo " \"release_tag\": \"${RELEASE_TAG}\","; \
|
||||||
|
echo " \"build_date\": \"${BUILD_DATE}\","; \
|
||||||
|
echo " \"source_revision\": \"${SOURCE_REVISION}\","; \
|
||||||
|
echo " \"pi_version\": \"${PI_V}\","; \
|
||||||
|
echo " \"components\": {"; \
|
||||||
|
echo " \"pi-toolkit\": \"$(rev /opt/pi-toolkit)\","; \
|
||||||
|
echo " \"pi-extensions\": \"$(rev /opt/pi-extensions)\","; \
|
||||||
|
echo " \"pi-fork\": \"$(rev /opt/pi-fork)\","; \
|
||||||
|
echo " \"pi-observational-memory\": \"$(rev /opt/pi-observational-memory)\","; \
|
||||||
|
echo " \"mempalace-toolkit\": \"$(rev /opt/mempalace-toolkit)\","; \
|
||||||
|
echo " \"pi-studio\": ${STUDIO_REV}"; \
|
||||||
|
echo " }"; \
|
||||||
|
echo '}'; \
|
||||||
|
} > /etc/pi-devbox/build-manifest.json; \
|
||||||
|
echo "── build manifest ──"; cat /etc/pi-devbox/build-manifest.json
|
||||||
|
|
||||||
# WORKDIR / ENTRYPOINT / CMD inherited from base.
|
# WORKDIR / ENTRYPOINT / CMD inherited from base.
|
||||||
|
|||||||
@@ -442,6 +442,38 @@ session/docs mining; the 29 MCP tools (search, kg-query, drawer-add,
|
|||||||
diary-write, etc.) are wired into pi automatically by the pi-extensions
|
diary-write, etc.) are wired into pi automatically by the pi-extensions
|
||||||
mempalace bridge.
|
mempalace bridge.
|
||||||
|
|
||||||
|
## Agent skills
|
||||||
|
|
||||||
|
pi discovers skills under `~/.agents/skills/`. Two delivery paths feed that
|
||||||
|
directory, and they compose:
|
||||||
|
|
||||||
|
- **Image-baked skills (always present).** Skills shipped *inside* the image
|
||||||
|
live under `/usr/local/share/pi-devbox/skills/` and are symlinked into
|
||||||
|
`~/.agents/skills/` by `entrypoint-user.sh` on every start. They need no
|
||||||
|
external mount, survive volume recreate (the source is an image path, not a
|
||||||
|
home dir a named volume would shadow), and are created only when absent so a
|
||||||
|
same-named skillset skill or user override is never clobbered. The bundled
|
||||||
|
**`pi-devbox-environment`** skill is delivered this way — it teaches agents
|
||||||
|
the container's persistence model, host/LAN SSH reachability, split-DNS
|
||||||
|
mechanisms, the interactive-vs-tool-shell alias gotcha (`dssh`/`dscp`),
|
||||||
|
tmux 0-indexing, uv-first Python, and pi-studio reachability, all as
|
||||||
|
*mechanisms* (deployment-specific hostnames/domains/nameservers are
|
||||||
|
discovered at runtime, never hardcoded).
|
||||||
|
- **Skillset repo (optional).** If a `skillset` repo is mounted (at
|
||||||
|
`$HOME/skillset` or `/workspace/skillset`, or via `SKILLSET_CONTAINER_PATH`),
|
||||||
|
`deploy-skills.sh` symlinks its skills in too. Image-baked skills are
|
||||||
|
classified as foreign-links by its `--prune-stale` pass and left untouched.
|
||||||
|
|
||||||
|
To make agents *proactively* load a baked skill at session start (rather than
|
||||||
|
only on description match), the image appends a short, gated pointer to the
|
||||||
|
global `AGENTS.md` at build time (see `pi-global-AGENTS.append.md`). The
|
||||||
|
pointer fires only inside a pi-devbox container (it checks for
|
||||||
|
`/usr/local/lib/pi-devbox/`).
|
||||||
|
|
||||||
|
To add another image-baked skill: drop a `SKILL.md` under
|
||||||
|
`rootfs/usr/local/share/pi-devbox/skills/<name>/`; the `COPY` in
|
||||||
|
`Dockerfile.base` and the entrypoint symlink loop pick it up automatically.
|
||||||
|
|
||||||
## SSH and ControlMaster
|
## SSH and ControlMaster
|
||||||
|
|
||||||
The base image preconfigures `Host *` ssh defaults:
|
The base image preconfigures `Host *` ssh defaults:
|
||||||
@@ -479,7 +511,11 @@ this without editing the read-only config:
|
|||||||
cannot fail on the read-only socket dir.
|
cannot fail on the read-only socket dir.
|
||||||
- **`ssh -F ~/.ssh-local/config` / `dssh` / `dscp`** — `setup-lan-access.sh`
|
- **`ssh -F ~/.ssh-local/config` / `dssh` / `dscp`** — `setup-lan-access.sh`
|
||||||
redirects `ControlPath` into the writable `~/.ssh-local/cm` for every host
|
redirects `ControlPath` into the writable `~/.ssh-local/cm` for every host
|
||||||
(the sidecar is rendered on all host OSes).
|
(the sidecar is rendered on all host OSes). To name LAN peers that should
|
||||||
|
jump via the host, add `ProxyJump host` overrides in the host-owned
|
||||||
|
`~/.config/devbox-shell/ssh-lan.conf` (see
|
||||||
|
[Naming LAN peers](#naming-lan-peers)) rather than the read-only
|
||||||
|
`~/.ssh/config`.
|
||||||
|
|
||||||
## tmux and 0-indexed sessions
|
## tmux and 0-indexed sessions
|
||||||
|
|
||||||
@@ -537,6 +573,68 @@ pi-coding-agent@latest` (the build-arg string would otherwise be
|
|||||||
byte-identical across releases and the layer would silently reuse the
|
byte-identical across releases and the layer would silently reuse the
|
||||||
previous version's bytes).
|
previous version's bytes).
|
||||||
|
|
||||||
|
### Building a fork / relocated build
|
||||||
|
|
||||||
|
The canonical build clones its companions from `gitea.jordbo.se`. Every
|
||||||
|
companion repo URL is an overridable build-arg (defaulting to the canonical
|
||||||
|
origin), so a fork or a build on a host that can't reach that gitea can
|
||||||
|
repoint each one at a mirror, another host, or a local `file://` path
|
||||||
|
**without editing the Dockerfiles**:
|
||||||
|
|
||||||
|
| Build-arg | Default | Dockerfile |
|
||||||
|
|---|---|---|
|
||||||
|
| `PI_TOOLKIT_REPO` | `https://gitea.jordbo.se/joakimp/pi-toolkit.git` | variant |
|
||||||
|
| `PI_EXTENSIONS_REPO` | `https://gitea.jordbo.se/joakimp/pi-extensions.git` | variant |
|
||||||
|
| `MEMPALACE_TOOLKIT_REPO` | `https://gitea.jordbo.se/joakimp/mempalace-toolkit.git` | base |
|
||||||
|
| `PI_FORK_REPO` | `https://github.com/elpapi42/pi-fork.git` | variant |
|
||||||
|
| `PI_OBSMEM_REPO` | `https://github.com/elpapi42/pi-observational-memory.git` | variant |
|
||||||
|
| `PI_STUDIO_REPO` | `https://github.com/omaclaren/pi-studio.git` | variant |
|
||||||
|
|
||||||
|
Each has a matching `*_REF` arg (branch name or commit SHA). Example — build
|
||||||
|
the variant against forked toolkit/extensions and a pinned pi:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# base first (mempalace-toolkit lives here)
|
||||||
|
docker build -f Dockerfile.base -t myorg/pi-devbox:base-dev \
|
||||||
|
--build-arg MEMPALACE_TOOLKIT_REPO=https://github.com/myorg/mempalace-toolkit.git .
|
||||||
|
|
||||||
|
# then the variant FROM that base
|
||||||
|
docker build -f Dockerfile.variant -t myorg/pi-devbox:dev \
|
||||||
|
--build-arg BASE_IMAGE=myorg/pi-devbox:base-dev \
|
||||||
|
--build-arg PI_VERSION=0.79.7 \
|
||||||
|
--build-arg PI_TOOLKIT_REPO=https://github.com/myorg/pi-toolkit.git \
|
||||||
|
--build-arg PI_EXTENSIONS_REPO=https://github.com/myorg/pi-extensions.git .
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: the gitea companions clone anonymously (no token needed); only the
|
||||||
|
`resolve-versions` CI job calls the gitea *API* (which needs a token even
|
||||||
|
for public repos). A plain `docker build` like the above skips that job
|
||||||
|
entirely, so no credentials are required for a local/forked build.
|
||||||
|
|
||||||
|
Provenance build-args (all optional; populate the OCI labels and
|
||||||
|
`/etc/pi-devbox/build-manifest.json` — see below): `RELEASE_TAG`,
|
||||||
|
`BUILD_DATE`, `SOURCE_REVISION`. CI sets these automatically; a manual build
|
||||||
|
leaves them at harmless defaults.
|
||||||
|
|
||||||
|
### Build provenance (labels + manifest)
|
||||||
|
|
||||||
|
Every published image is self-describing. Inspect the OCI labels without
|
||||||
|
pulling the filesystem:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker inspect --format '{{json .Config.Labels}}' joakimp/pi-devbox:latest | jq .
|
||||||
|
```
|
||||||
|
|
||||||
|
`org.opencontainers.image.{version,revision,created}` plus
|
||||||
|
`se.jordbo.pi-devbox.*-ref` record the intended pi version and companion
|
||||||
|
refs. The on-disk `/etc/pi-devbox/build-manifest.json` records **ground
|
||||||
|
truth** — the actual checked-out commit of each `/opt` clone and the live
|
||||||
|
`pi --version` — so a tag is reconstructable after CI logs rotate:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
docker run --rm --entrypoint= joakimp/pi-devbox:latest cat /etc/pi-devbox/build-manifest.json
|
||||||
|
```
|
||||||
|
|
||||||
## Troubleshooting
|
## Troubleshooting
|
||||||
|
|
||||||
### Image grew unexpectedly
|
### Image grew unexpectedly
|
||||||
@@ -553,6 +651,28 @@ auto-runs on container start and writes `~/.ssh-local/config` with a
|
|||||||
ssh-jump-via-host configuration. Set `DEVBOX_LAN_ACCESS=jump` and
|
ssh-jump-via-host configuration. Set `DEVBOX_LAN_ACCESS=jump` and
|
||||||
`HOST_SSH_USER=<your-mac-user>` in `.env` if auto-detection fails.
|
`HOST_SSH_USER=<your-mac-user>` in `.env` if auto-detection fails.
|
||||||
|
|
||||||
|
#### Naming LAN peers
|
||||||
|
|
||||||
|
`DEVBOX_LAN_ACCESS` / `HOST_SSH_USER` only set up the *jump* to the host. To
|
||||||
|
make a **named** peer route through it — so `pi --ssh alpserv-2`,
|
||||||
|
`dssh alpserv-2`, etc. resolve the ProxyJump — add a `ProxyJump host` override
|
||||||
|
for it in the host-owned, bind-mounted `~/.config/devbox-shell/ssh-lan.conf`
|
||||||
|
(**not** `~/.ssh/config`, which is mounted read-only):
|
||||||
|
|
||||||
|
```
|
||||||
|
Host pve pve-2 alpserv-2 lagret
|
||||||
|
ProxyJump host
|
||||||
|
```
|
||||||
|
|
||||||
|
`HostName` / `User` / `IdentityFile` are inherited from the matching block in
|
||||||
|
your real `~/.ssh/config` (first-value-wins, so only `ProxyJump` is taken from
|
||||||
|
here). This file is `Include`d *before* `~/.ssh/config` and read fresh on every
|
||||||
|
connection — newly added peers work immediately, no container or session
|
||||||
|
restart needed — and the peer names stay out of the published image (they're a
|
||||||
|
fact about your specific LAN, not the image). Alternatively, set
|
||||||
|
`DEVBOX_LAN_AUTOJUMP_PRIVATE=1` to ProxyJump *any* RFC1918 address through the
|
||||||
|
host without naming peers (see `.env.example`).
|
||||||
|
|
||||||
### Smoke-testing a local build
|
### Smoke-testing a local build
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -40,6 +40,32 @@ if [ -d "$SKEL_DIR" ]; then
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── Image-baked skills: link into ~/.agents/skills ───────────────────
|
||||||
|
# Skills shipped IN the image (under /usr/local/share/pi-devbox/skills/) are
|
||||||
|
# made available regardless of whether a skillset repo is mounted. Done EARLY
|
||||||
|
# — before the pi-toolkit/extensions deploy below — so the symlinks exist by
|
||||||
|
# the time anything gates on "container ready": the smoke-test readiness probe
|
||||||
|
# waits on pi-deploy markers (keybindings.json, mempalace.ts) that only land
|
||||||
|
# AFTER this point, so linking here closes a sample-too-early race that failed
|
||||||
|
# the runtime skill-link assertion. Pointing at the image path (/usr/local/...)
|
||||||
|
# keeps the skill fresh from the image and surviving volume recreate (unlike
|
||||||
|
# anything baked under a home dir, which a named volume would shadow). Created
|
||||||
|
# only when absent, so a same-named skillset skill (deployed later, at the end
|
||||||
|
# of this script) or a user override is never clobbered; the skillset deploy
|
||||||
|
# classifies these as foreign-links and its --prune-stale pass leaves them
|
||||||
|
# alone (only dangling symlinks are pruned).
|
||||||
|
DEVBOX_SKILLS_SRC=/usr/local/share/pi-devbox/skills
|
||||||
|
if [ -d "$DEVBOX_SKILLS_SRC" ]; then
|
||||||
|
mkdir -p "$HOME/.agents/skills"
|
||||||
|
for _sk in "$DEVBOX_SKILLS_SRC"/*/; do
|
||||||
|
[ -d "$_sk" ] || continue
|
||||||
|
_skname=$(basename "$_sk")
|
||||||
|
if [ ! -e "$HOME/.agents/skills/$_skname" ]; then
|
||||||
|
ln -s "${_sk%/}" "$HOME/.agents/skills/$_skname"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
# ── MemPalace: initialize palace for the workspace if mempalace is installed
|
# ── MemPalace: initialize palace for the workspace if mempalace is installed
|
||||||
# Creates the palace directory structure on first run. Idempotent — skips
|
# Creates the palace directory structure on first run. Idempotent — skips
|
||||||
# if palace already exists, so upgrades from older versions preserve
|
# if palace already exists, so upgrades from older versions preserve
|
||||||
|
|||||||
@@ -188,7 +188,9 @@ if [ -r "${HOME}/.ssh/config" ]; then
|
|||||||
|
|
||||||
# Your own target hosts. Scope reset to match-all so this Include applies to
|
# Your own target hosts. Scope reset to match-all so this Include applies to
|
||||||
# every target (an Include is otherwise scoped to the enclosing Host block).
|
# every target (an Include is otherwise scoped to the enclosing Host block).
|
||||||
# Add 'ProxyJump host' to LAN entries here (or in ssh-lan.conf above).
|
# To make a LAN peer jump via the host, add 'ProxyJump host' to its entry in
|
||||||
|
# the host-owned ~/.config/devbox-shell/ssh-lan.conf (Included above) — NOT
|
||||||
|
# here in ~/.ssh/config, which is typically bind-mounted read-only.
|
||||||
Host *
|
Host *
|
||||||
Include ~/.ssh/config
|
Include ~/.ssh/config
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
<!-- pi-devbox:managed-block — appended to the global AGENTS.md at image build
|
||||||
|
time (Dockerfile.variant), after pi-toolkit is cloned. Keep this short:
|
||||||
|
it is a pointer, the depth lives in the skill. -->
|
||||||
|
|
||||||
|
## Running inside pi-devbox
|
||||||
|
|
||||||
|
If the directory `/usr/local/lib/pi-devbox/` exists (or your shell prompt is
|
||||||
|
prefixed `[devbox]`, or `~/.ssh-local/config` is present), you are in a
|
||||||
|
**pi-devbox container** — a Docker environment whose persistence, networking,
|
||||||
|
DNS, host/LAN reachability, tmux, and Python/REPL behaviour differ from a normal
|
||||||
|
workstation. Before any task that touches **reaching the host or its LAN, SSH,
|
||||||
|
DNS/name resolution, what survives container recreate, running Python/REPLs,
|
||||||
|
tmux, or pi-studio**, read `~/.agents/skills/pi-devbox-environment/SKILL.md`.
|
||||||
|
|
||||||
|
Key reflex from that skill: **the deployment specifics are not universal** — the
|
||||||
|
host OS, hostnames, internal domains, and nameservers vary per instance and must
|
||||||
|
be discovered at runtime, never assumed. And interactive shell aliases
|
||||||
|
(`dssh`, `dscp`, `cat`→`bat`) do **not** exist in your non-interactive bash
|
||||||
|
tool, so spell out the underlying command (e.g.
|
||||||
|
`ssh -F "$HOME/.ssh-local/config" mac …`).
|
||||||
@@ -0,0 +1,207 @@
|
|||||||
|
---
|
||||||
|
name: pi-devbox-environment
|
||||||
|
description: >-
|
||||||
|
Operate correctly inside a pi-devbox container. Load when running inside
|
||||||
|
pi-devbox (detection: the directory `/usr/local/lib/pi-devbox/` exists, the
|
||||||
|
shell prompt is prefixed `[devbox]`, or `~/.ssh-local/config` is present) and
|
||||||
|
the task touches any of: reaching the Docker host or its LAN, SSH, DNS name
|
||||||
|
resolution, what survives container recreate (persistence vs ephemerality),
|
||||||
|
running Python or other REPLs, tmux, or the pi-studio browser UI. Covers the
|
||||||
|
persistence model, the interactive-vs-tool-shell alias gotcha
|
||||||
|
(dssh/dscp/cat=bat exist only in interactive bash), host + LAN SSH
|
||||||
|
reachability and ControlMaster, split-horizon DNS mechanisms, the tmux
|
||||||
|
0-index constraint, uv-first Python, and pi-studio reachability. This skill
|
||||||
|
teaches MECHANISMS only — concrete hostnames, usernames, internal domains,
|
||||||
|
nameservers, and even the host OS vary per deployment and MUST be discovered
|
||||||
|
at runtime, never assumed or hardcoded.
|
||||||
|
---
|
||||||
|
|
||||||
|
# pi-devbox environment
|
||||||
|
|
||||||
|
You are (or may be) running inside **pi-devbox**: a Docker container that ships
|
||||||
|
pi, MemPalace, and a curated tool stack, with the host source tree mounted at
|
||||||
|
`/workspace`. This skill is about the *container-shaped* facts that change how
|
||||||
|
you should act — things that are easy to get wrong because they differ from a
|
||||||
|
normal workstation shell.
|
||||||
|
|
||||||
|
> **Golden rule: this environment is a template, not a fixed deployment.**
|
||||||
|
> The host could be macOS, Windows, or Linux. There may or may not be LAN
|
||||||
|
> peers, a VPN, split-DNS, a skillset mount, or the `-studio` variant. Detect
|
||||||
|
> and verify the specifics live (commands below) — do **not** assume any
|
||||||
|
> particular hostname, domain, nameserver, or OS. Where this skill shows
|
||||||
|
> example values they are illustrative placeholders.
|
||||||
|
|
||||||
|
## 0. Am I in pi-devbox, and what's true *here*?
|
||||||
|
|
||||||
|
Cheap detection signals (any one is sufficient):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
[ -d /usr/local/lib/pi-devbox ] && echo "pi-devbox image"
|
||||||
|
[ -r "$HOME/.ssh-local/config" ] && echo "LAN/host SSH sidecar present"
|
||||||
|
case "$PS1" in *'[devbox]'*) echo "interactive devbox shell";; esac
|
||||||
|
```
|
||||||
|
|
||||||
|
Then orient before acting:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cat /etc/os-release | head -2 # container distro (usually Debian)
|
||||||
|
ls -la /usr/local/lib/pi-devbox/ # which devbox helpers exist
|
||||||
|
sed -n '/^Host /,$p' ~/.ssh-local/config 2>/dev/null # host/LAN reachability, if any
|
||||||
|
mount | grep -E ' /workspace | /home/\S+/\.ssh ' # what's bind-mounted
|
||||||
|
```
|
||||||
|
|
||||||
|
## 1. Persistence vs ephemerality — know before you write
|
||||||
|
|
||||||
|
The container has **three storage tiers with very different lifetimes**. Pick
|
||||||
|
the right one or work is silently lost on the next recreate/update.
|
||||||
|
|
||||||
|
| Tier | Examples | Survives `down`? | Survives `down -v`? | Survives image update / `--force-recreate`? |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| **Host bind-mount** | `/workspace`, usually `~/.ssh` (ro), often `~/.mempalace` | yes | yes (lives on host) | yes |
|
||||||
|
| **Named volume** | `~/.pi`, `~/.ssh-local`, `~/.cache/bash`, `~/.local/share/{uv,nvim,zoxide}` | yes | **no** | yes |
|
||||||
|
| **Writable container layer** | anything else: `sudo apt install …`, `rustup`/`ghc`/`R` toolchains, files in `/tmp`, `/opt` edits | yes | **no** | **no** |
|
||||||
|
|
||||||
|
Practical consequences:
|
||||||
|
|
||||||
|
- **Durable work goes in `/workspace`** (it's the host filesystem, UID-aligned —
|
||||||
|
what you write appears with the user's normal ownership on the host).
|
||||||
|
- **Runtime-installed system packages and language toolchains are ephemeral.**
|
||||||
|
If a task needs them reproducibly, it belongs in the image (Dockerfile) or a
|
||||||
|
project manifest, not an ad-hoc `apt install`. Tell the user when you install
|
||||||
|
something that won't survive.
|
||||||
|
- **`~/.pi` is a named volume**, so things baked into the *image* under
|
||||||
|
`/home/<user>/...` are **shadowed** by the volume on existing containers and
|
||||||
|
only seen on a fresh volume. Image-owned content that must always be live
|
||||||
|
belongs under an image path like `/usr/local/...` or `/opt/...` and is linked
|
||||||
|
in by the entrypoint — not dropped into a home directory that a volume covers.
|
||||||
|
|
||||||
|
## 2. Interactive shell vs. your tool shell (a real footgun)
|
||||||
|
|
||||||
|
The conveniences below are defined in `~/.bash_aliases` and **only exist in an
|
||||||
|
interactive login shell.** Your `bash` *tool* runs non-interactively, so these
|
||||||
|
are "command not found" there — you must spell out the underlying command.
|
||||||
|
|
||||||
|
| Interactive alias | Non-interactive equivalent to actually run |
|
||||||
|
|---|---|
|
||||||
|
| `dssh <host>` | `ssh -F "$HOME/.ssh-local/config" <host>` |
|
||||||
|
| `dscp …` | `scp -F "$HOME/.ssh-local/config" …` |
|
||||||
|
| `cat file` (→ `bat`) | `cat file` works, but output differs; use `command cat` for raw |
|
||||||
|
| `ll`, `la` (→ `eza`/`ls`) | `ls -lh`, `ls -lha` |
|
||||||
|
|
||||||
|
If a command "works in my terminal but not when the agent runs it," this alias
|
||||||
|
gap is the first thing to suspect.
|
||||||
|
|
||||||
|
## 3. Reaching the Docker host and its LAN over SSH
|
||||||
|
|
||||||
|
When the host is VM-backed (e.g. OrbStack / Docker Desktop on macOS) the
|
||||||
|
entrypoint's `setup-lan-access.sh` writes a **writable SSH sidecar** at
|
||||||
|
`~/.ssh-local/config`. It always provides:
|
||||||
|
|
||||||
|
- A `Host *` block redirecting `ControlPath` into the writable `~/.ssh-local/cm`
|
||||||
|
(because `~/.ssh` is typically bind-mounted **read-only**, so a master socket
|
||||||
|
can't be created under it), plus `Include ~/.ssh/config`.
|
||||||
|
- Aliases **`host` / `mac`** → `host.docker.internal` (user comes from
|
||||||
|
`HOST_SSH_USER`) — i.e. SSH back into the Docker host.
|
||||||
|
- On VM-backed hosts only: an **SSH-jump-via-host** block so the container can
|
||||||
|
reach the host's directly-attached LAN peers (`ProxyJump host`). On a native
|
||||||
|
Linux host the LAN is usually reachable directly and this jump block is
|
||||||
|
omitted — **so don't assume a jump path exists; read the sidecar.**
|
||||||
|
|
||||||
|
Use it (remember §2 — spell it out in tool bash):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ssh -F "$HOME/.ssh-local/config" mac 'hostname; whoami' # reach the host
|
||||||
|
ssh -F "$HOME/.ssh-local/config" <lan-peer> '…' # reach a LAN peer (if configured)
|
||||||
|
```
|
||||||
|
|
||||||
|
Two related mechanisms (don't reinvent them):
|
||||||
|
|
||||||
|
- **ControlMaster multiplexing** is preconfigured (`/tmp/sshcm/`) to survive
|
||||||
|
CGNAT per-destination flow caps on residential ISPs. If `~/.ssh/config` pins
|
||||||
|
a `ControlPath` under the read-only `~/.ssh`, override with
|
||||||
|
`-o ControlPath=none` (or use the sidecar, which already redirects it).
|
||||||
|
- **`pi --ssh <host>`** rewires pi's own read/write/edit/bash tools to run on a
|
||||||
|
remote host; it has its own writable-socket fallback. See the `pi-extensions`
|
||||||
|
skill for that path.
|
||||||
|
|
||||||
|
## 4. DNS / name resolution — environment-specific, verify live
|
||||||
|
|
||||||
|
How a name resolves here is **not universal** and depends on the host's
|
||||||
|
networking. The container's own resolver is just `/etc/resolv.conf`, but the
|
||||||
|
*host* (which you reach via §3, and whose DNS the container may inherit) can use
|
||||||
|
**split-horizon DNS** to send certain internal domains to specific nameservers
|
||||||
|
while everything else goes to a default resolver/VPN gateway. The mechanism is
|
||||||
|
OS-specific and **may not be present at all**:
|
||||||
|
|
||||||
|
- **macOS host:** per-domain files in `/etc/resolver/<domain>`, each listing
|
||||||
|
`nameserver` lines. Reading them (over `ssh … mac`) is a fine way to learn the
|
||||||
|
real split-DNS map — *for that one machine.*
|
||||||
|
- **Linux host:** typically `systemd-resolved` split DNS (per-link `Domains=`
|
||||||
|
routing) or `/etc/resolv.conf` `search`/`nameserver`.
|
||||||
|
- **Windows host:** the NRPT (Name Resolution Policy Table) plays the per-suffix
|
||||||
|
role; WSL2 inherits host resolution via mirrored networking + DNS tunneling.
|
||||||
|
|
||||||
|
Operating rules:
|
||||||
|
|
||||||
|
1. **Never hardcode a domain→nameserver mapping or a specific nameserver IP** —
|
||||||
|
it is per-deployment and changes between users and even VPN states.
|
||||||
|
2. **Verify by reading the live config**, e.g. `cat /etc/resolv.conf` in the
|
||||||
|
container, or `ssh … mac 'cat /etc/resolver/* 2>/dev/null'` on a macOS host.
|
||||||
|
3. **Reachability needs both DNS *and* a route.** A name resolving to an
|
||||||
|
internal address is useless if packets to that subnet don't have a path
|
||||||
|
(e.g. via the VPN or the §3 jump). Check both when something "resolves but
|
||||||
|
won't connect."
|
||||||
|
4. If you discover deployment-specific facts (a domain, a nameserver, a
|
||||||
|
reachable peer), prefer recording them in MemPalace over baking them into
|
||||||
|
code or this skill.
|
||||||
|
|
||||||
|
## 5. tmux is 0-indexed — don't change it
|
||||||
|
|
||||||
|
The image ships `/etc/tmux.conf` with `base-index 0` / `pane-base-index 0`
|
||||||
|
because **pi-studio hard-codes its tmux send target to `<session>:0.0`.** If you
|
||||||
|
(or a user `~/.tmux.conf`) set `base-index 1`, pi-studio fails with "can't find
|
||||||
|
window: 0". Leave the indexing alone in this environment.
|
||||||
|
|
||||||
|
## 6. Python and other languages: uv-first, toolchains are ephemeral
|
||||||
|
|
||||||
|
- A system `python3` exists, but **prefer `uv`** for REPLs and project envs —
|
||||||
|
it's installed and its store (`~/.local/share/uv`) is a persisted volume.
|
||||||
|
- Throwaway REPL: `uv run --with ipython ipython`
|
||||||
|
- Project env: `cd /workspace/proj && uv init && uv add <pkgs> && uv run …`
|
||||||
|
(the `pyproject.toml` + `uv.lock` travel with the repo — the durable choice).
|
||||||
|
- Other language toolchains (Rust via rustup, R, GHC, Clojure, Go) are
|
||||||
|
**runtime opt-ins on the ephemeral layer** unless baked into the image — they
|
||||||
|
do not survive `down -v` or an image update. Flag this when installing.
|
||||||
|
|
||||||
|
## 7. pi-studio reachability (only in the `-studio` variant)
|
||||||
|
|
||||||
|
Present only if `/opt/pi-studio` exists / the `studio_*` tools are in your tool
|
||||||
|
list. pi-studio **binds to `127.0.0.1` inside the container** with no host-bind
|
||||||
|
flag, so a plain `docker -p` publish can't reach it. Two supported paths:
|
||||||
|
|
||||||
|
- **Host networking** (`network_mode: host`): container loopback == host
|
||||||
|
loopback; open the tokenized URL on the host. (Changes
|
||||||
|
`host.docker.internal` semantics — weigh against §3 LAN jump.)
|
||||||
|
- **`studio-expose` bridge** (`STUDIO_EXPOSE=1` or run `studio-expose &`): a
|
||||||
|
`socat` relay from the container's external interface to its loopback, so a
|
||||||
|
published `127.0.0.1:PORT` + `ssh -L PORT:127.0.0.1:PORT host` reaches it.
|
||||||
|
|
||||||
|
The real auth token comes from the `/studio` slash command (`/studio --status`
|
||||||
|
to reprint), **not** from `studio-expose`. For Graphviz, use `dot-watch` →
|
||||||
|
PNG (Studio renders Mermaid natively and previews PNG, but not SVG/DOT).
|
||||||
|
|
||||||
|
## 8. MemPalace is the shared brain
|
||||||
|
|
||||||
|
MemPalace data is usually a **host bind-mount**, so a pi on the host and a pi in
|
||||||
|
this container share one palace (SQLite WAL: many readers, one writer). Use it
|
||||||
|
to persist the deployment-specific facts this skill deliberately refuses to
|
||||||
|
hardcode. Details are in the `mempalace` skill.
|
||||||
|
|
||||||
|
## Checklist before acting in this environment
|
||||||
|
|
||||||
|
- [ ] Writing durable output? → `/workspace`, not the ephemeral layer.
|
||||||
|
- [ ] Using `dssh`/`dscp`/`ll` in the bash tool? → spell out the real command.
|
||||||
|
- [ ] Assuming a hostname / domain / nameserver / host OS? → stop, detect it.
|
||||||
|
- [ ] "Resolves but won't connect"? → check route *and* DNS (§3 + §4).
|
||||||
|
- [ ] `apt`/toolchain install? → tell the user it's ephemeral unless imaged.
|
||||||
|
- [ ] Touching tmux indexing? → don't (§5).
|
||||||
Executable
+43
@@ -0,0 +1,43 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# check-base-hash.sh — guard the base-rebuild invariant.
|
||||||
|
#
|
||||||
|
# Every floating `ARG *_REF` consumed by Dockerfile.base MUST be folded
|
||||||
|
# into the base_tag hash in the docker-publish workflow. Otherwise a
|
||||||
|
# ref-only change to that dependency does not change the base hash, the
|
||||||
|
# Docker Hub probe finds the old base tag, and the base is NOT rebuilt —
|
||||||
|
# the dependency fix silently fails to land. This is the v1.1.2-class
|
||||||
|
# staleness footgun (then it was mempalace-toolkit; this guard stops the
|
||||||
|
# next one before it ships).
|
||||||
|
#
|
||||||
|
# Runs in CI (base-decide job) and locally: bash scripts/check-base-hash.sh
|
||||||
|
set -euo pipefail
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
WF=".gitea/workflows/docker-publish.yml"
|
||||||
|
DF="Dockerfile.base"
|
||||||
|
|
||||||
|
# Extract the hash-compute block: the `HASH=$( … ) | sha256sum | cut`
|
||||||
|
# brace-group in the "Compute base tag" step. This lives in a separate
|
||||||
|
# file from the workflow, so scanning $WF here is free of the self-match
|
||||||
|
# hazard an inline workflow step would have.
|
||||||
|
block=$(awk '/HASH=\$\(/{f=1} f{print} f && /cut -c1-12/{exit}' "$WF")
|
||||||
|
if [ -z "$block" ]; then
|
||||||
|
echo "::error::could not locate the HASH=\$( … ) | sha256sum block in $WF"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
refs=$(grep -oE '^ARG [A-Z0-9_]+_REF' "$DF" | awk '{print $2}' | sort -u)
|
||||||
|
fail=0
|
||||||
|
for r in $refs; do
|
||||||
|
lc=$(printf '%s' "$r" | tr '[:upper:]' '[:lower:]')
|
||||||
|
if ! printf '%s' "$block" | grep -q "outputs.$lc"; then
|
||||||
|
echo "::error::Dockerfile.base declares '$r' but it is NOT folded into the base_tag hash in $WF."
|
||||||
|
echo "::error::Add echo \"\${{ needs.resolve-versions.outputs.$lc }}\" inside the HASH=\$( … ) | sha256sum block, or a $r-only change will silently fail to rebuild the base."
|
||||||
|
fail=1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ "$fail" = 0 ]; then
|
||||||
|
echo "OK: all Dockerfile.base *_REF args are folded into base_tag (${refs:-none})."
|
||||||
|
fi
|
||||||
|
exit $fail
|
||||||
@@ -80,6 +80,12 @@ run "yq" "yq --version"
|
|||||||
run "tldr (tealdeer)" "tldr --version"
|
run "tldr (tealdeer)" "tldr --version"
|
||||||
run "socat" "socat -V"
|
run "socat" "socat -V"
|
||||||
run "studio-expose helper" "test -x /usr/local/bin/studio-expose"
|
run "studio-expose helper" "test -x /usr/local/bin/studio-expose"
|
||||||
|
run "image-baked pi-devbox-environment skill" \
|
||||||
|
"test -f /usr/local/share/pi-devbox/skills/pi-devbox-environment/SKILL.md"
|
||||||
|
run "global-AGENTS append snippet present" \
|
||||||
|
"test -f /usr/local/share/pi-devbox/pi-global-AGENTS.append.md"
|
||||||
|
run "pi-devbox block merged into pi-global-AGENTS.md" \
|
||||||
|
"grep -q 'pi-devbox:managed-block' /opt/pi-toolkit/pi-global-AGENTS.md"
|
||||||
|
|
||||||
# ── tmux 0-indexing (required for pi-studio variants) ─────────────────
|
# ── tmux 0-indexing (required for pi-studio variants) ─────────────────
|
||||||
echo ""
|
echo ""
|
||||||
@@ -113,6 +119,28 @@ else
|
|||||||
echo " ℹ️ pi-studio not present (non-studio variant) — skipping studio clone checks"
|
echo " ℹ️ pi-studio not present (non-studio variant) — skipping studio clone checks"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# ── Build provenance (manifest + OCI labels) ─────────────────────────
|
||||||
|
echo ""
|
||||||
|
echo "── Build provenance ──"
|
||||||
|
run "/etc/pi-devbox/build-manifest.json present" \
|
||||||
|
"test -f /etc/pi-devbox/build-manifest.json"
|
||||||
|
run_expect "manifest records pi-extensions component" \
|
||||||
|
"cat /etc/pi-devbox/build-manifest.json" '"pi-extensions"'
|
||||||
|
run_expect "manifest records pi_version" \
|
||||||
|
"cat /etc/pi-devbox/build-manifest.json" '"pi_version"'
|
||||||
|
# Every component must be a resolved commit (or null for pi-studio in the
|
||||||
|
# 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"
|
||||||
|
# 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)
|
||||||
|
if [ -n "$LBL" ] && [ "$LBL" != "<no value>" ]; then
|
||||||
|
printf " ✅ OCI label se.jordbo.pi-devbox.pi-extensions-ref=%s\n" "$LBL"; PASS=$((PASS+1))
|
||||||
|
else
|
||||||
|
printf " ❌ OCI label se.jordbo.pi-devbox.pi-extensions-ref missing or empty\n"; FAIL=$((FAIL+1))
|
||||||
|
fi
|
||||||
|
|
||||||
# ── Runtime deployment (needs entrypoint to run) ──────────────────────
|
# ── Runtime deployment (needs entrypoint to run) ──────────────────────
|
||||||
echo ""
|
echo ""
|
||||||
echo "── Runtime deployment ──"
|
echo "── Runtime deployment ──"
|
||||||
@@ -134,6 +162,7 @@ for i in $(seq 1 45); do
|
|||||||
if docker exec "$CID" sh -c '
|
if docker exec "$CID" sh -c '
|
||||||
test -L /home/developer/.pi/agent/keybindings.json && \
|
test -L /home/developer/.pi/agent/keybindings.json && \
|
||||||
test -L /home/developer/.pi/agent/extensions/mempalace.ts && \
|
test -L /home/developer/.pi/agent/extensions/mempalace.ts && \
|
||||||
|
test -L /home/developer/.agents/skills/pi-devbox-environment && \
|
||||||
count=$(ls -1 /home/developer/.pi/agent/extensions/*.ts 2>/dev/null | wc -l) && \
|
count=$(ls -1 /home/developer/.pi/agent/extensions/*.ts 2>/dev/null | wc -l) && \
|
||||||
[ "$count" -ge 4 ]
|
[ "$count" -ge 4 ]
|
||||||
' >/dev/null 2>&1; then
|
' >/dev/null 2>&1; then
|
||||||
@@ -155,6 +184,7 @@ exec_test "keybindings.json (pi-toolkit)" 'test -L $HOME/.pi/agent/keybi
|
|||||||
exec_test "extensions ≥ 4 (pi-extensions)" 'count=$(ls -1 $HOME/.pi/agent/extensions/*.ts 2>/dev/null | wc -l); [ $count -ge 4 ] && echo "$count extensions"'
|
exec_test "extensions ≥ 4 (pi-extensions)" 'count=$(ls -1 $HOME/.pi/agent/extensions/*.ts 2>/dev/null | wc -l); [ $count -ge 4 ] && echo "$count extensions"'
|
||||||
exec_test "mempalace.ts bridge" 'test -L $HOME/.pi/agent/extensions/mempalace.ts && echo ok'
|
exec_test "mempalace.ts bridge" 'test -L $HOME/.pi/agent/extensions/mempalace.ts && echo ok'
|
||||||
exec_test "settings.json bootstrapped" 'test -f $HOME/.pi/agent/settings.json && echo ok'
|
exec_test "settings.json bootstrapped" 'test -f $HOME/.pi/agent/settings.json && echo ok'
|
||||||
|
exec_test "pi-devbox-environment skill linked" 'test -L $HOME/.agents/skills/pi-devbox-environment && test -f $HOME/.agents/skills/pi-devbox-environment/SKILL.md && echo ok'
|
||||||
|
|
||||||
# pi-fork + pi-observational-memory are registered by entrypoint-user.sh via
|
# pi-fork + pi-observational-memory are registered by entrypoint-user.sh via
|
||||||
# `pi install /opt/<pkg>`, which runs slightly after the keybindings marker.
|
# `pi install /opt/<pkg>`, which runs slightly after the keybindings marker.
|
||||||
|
|||||||
Reference in New Issue
Block a user