Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f91dff6090 | |||
| 9ebb0643c7 | |||
| 7d8ee4cea1 | |||
| a78e59fb5b | |||
| cf5c60a342 | |||
| edd6be1737 | |||
| efd254f4e6 | |||
| 8b69b3625b | |||
| b55b44e7b6 |
@@ -114,8 +114,11 @@ jobs:
|
||||
pi_version: ${{ steps.resolve.outputs.pi_version }}
|
||||
fork_ref: ${{ steps.resolve.outputs.fork_ref }}
|
||||
obsmem_ref: ${{ steps.resolve.outputs.obsmem_ref }}
|
||||
toolkit_ref: ${{ steps.resolve.outputs.toolkit_ref }}
|
||||
extensions_ref: ${{ steps.resolve.outputs.extensions_ref }}
|
||||
studio_ref: ${{ steps.resolve.outputs.studio_ref }}
|
||||
steps:
|
||||
- name: Resolve pi version + fork/obsmem refs
|
||||
- name: Resolve pi version + companion refs
|
||||
id: resolve
|
||||
run: |
|
||||
set -eu
|
||||
@@ -133,8 +136,31 @@ jobs:
|
||||
[ -n "$OBSMEM_REF" ] || OBSMEM_REF=master
|
||||
echo "fork_ref=${FORK_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
|
||||
# those repos haven't moved (and a clean diff in build-arg strings
|
||||
# when they have, defeating the registry buildcache footgun).
|
||||
# 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" \
|
||||
| jq -r '.[0].sha // "main"' 2>/dev/null || echo "main")
|
||||
EXTENSIONS_REF=$(curl -sf -H "Authorization: token ${GITEA_BUILD_TOKEN:-${GITHUB_TOKEN:-}}" \
|
||||
"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")
|
||||
[ -n "$TOOLKIT_REF" ] || TOOLKIT_REF=main
|
||||
[ -n "$EXTENSIONS_REF" ] || EXTENSIONS_REF=main
|
||||
echo "toolkit_ref=${TOOLKIT_REF}" >> "$GITHUB_OUTPUT"
|
||||
echo "extensions_ref=${EXTENSIONS_REF}" >> "$GITHUB_OUTPUT"
|
||||
# Resolve pi-studio (omaclaren/pi-studio) main HEAD to a SHA for
|
||||
# the :latest-studio variant — same cache-busting rationale.
|
||||
STUDIO_REF=$(curl -sf -H "Accept: application/vnd.github.sha" \
|
||||
"https://api.github.com/repos/omaclaren/pi-studio/commits/main" || echo "main")
|
||||
[ -n "$STUDIO_REF" ] || STUDIO_REF=main
|
||||
echo "studio_ref=${STUDIO_REF}" >> "$GITHUB_OUTPUT"
|
||||
echo "Resolved PI_VERSION=${PI_VERSION}"
|
||||
echo "Resolved PI_FORK_REF=${FORK_REF}, PI_OBSMEM_REF=${OBSMEM_REF}"
|
||||
echo "Resolved PI_TOOLKIT_REF=${TOOLKIT_REF}, PI_EXTENSIONS_REF=${EXTENSIONS_REF}"
|
||||
echo "Resolved PI_STUDIO_REF=${STUDIO_REF}"
|
||||
|
||||
# ── Phase 2: build & push base (multi-arch), only when needed ──────
|
||||
build-base:
|
||||
@@ -252,11 +278,69 @@ jobs:
|
||||
PI_VERSION=${{ needs.resolve-versions.outputs.pi_version }}
|
||||
PI_FORK_REF=${{ needs.resolve-versions.outputs.fork_ref }}
|
||||
PI_OBSMEM_REF=${{ needs.resolve-versions.outputs.obsmem_ref }}
|
||||
PI_TOOLKIT_REF=${{ needs.resolve-versions.outputs.toolkit_ref }}
|
||||
PI_EXTENSIONS_REF=${{ needs.resolve-versions.outputs.extensions_ref }}
|
||||
- name: Smoke test (amd64)
|
||||
env:
|
||||
EXPECTED_PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
|
||||
run: bash scripts/smoke-test.sh pi-devbox:smoke
|
||||
|
||||
# ── Phase 3b: amd64 smoke for the studio variant ────────────────────
|
||||
# Additive + independent of the core `smoke` job: gates ONLY
|
||||
# build-variant-studio, never the core build-variant. A studio build or
|
||||
# smoke failure therefore cannot block the :latest / :vX.Y.Z release.
|
||||
smoke-studio:
|
||||
needs: [base-decide, build-base, resolve-versions]
|
||||
if: |
|
||||
always() &&
|
||||
needs.base-decide.result == 'success' &&
|
||||
needs.resolve-versions.result == 'success' &&
|
||||
(needs.build-base.result == 'success' || needs.build-base.result == 'skipped')
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Force IPv4 for Docker Hub
|
||||
run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
|
||||
- name: Reclaim runner disk
|
||||
run: |
|
||||
rm -rf /opt/hostedtoolcache /opt/microsoft /opt/az /opt/ghc \
|
||||
/usr/local/.ghcup /usr/share/dotnet /usr/share/swift \
|
||||
/usr/local/lib/android /usr/local/share/powershell \
|
||||
/usr/local/share/chromium /usr/local/share/boost \
|
||||
/usr/lib/jvm 2>/dev/null || true
|
||||
docker system prune -af --volumes || true
|
||||
docker builder prune -af || true
|
||||
- uses: docker/setup-buildx-action@v4
|
||||
with: {driver-opts: network=host}
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Build amd64 studio variant for smoke
|
||||
uses: docker/build-push-action@v7
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile.variant
|
||||
platforms: linux/amd64
|
||||
push: false
|
||||
load: true
|
||||
tags: pi-devbox:smoke-studio
|
||||
build-args: |
|
||||
BASE_IMAGE=${{ env.IMAGE }}:${{ needs.base-decide.outputs.base_tag }}
|
||||
PI_VERSION=${{ needs.resolve-versions.outputs.pi_version }}
|
||||
PI_FORK_REF=${{ needs.resolve-versions.outputs.fork_ref }}
|
||||
PI_OBSMEM_REF=${{ needs.resolve-versions.outputs.obsmem_ref }}
|
||||
PI_TOOLKIT_REF=${{ needs.resolve-versions.outputs.toolkit_ref }}
|
||||
PI_EXTENSIONS_REF=${{ needs.resolve-versions.outputs.extensions_ref }}
|
||||
INSTALL_STUDIO=true
|
||||
PI_STUDIO_REF=${{ needs.resolve-versions.outputs.studio_ref }}
|
||||
- name: Smoke test studio (amd64)
|
||||
env:
|
||||
EXPECTED_PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
|
||||
run: bash scripts/smoke-test.sh pi-devbox:smoke-studio
|
||||
|
||||
# ── Phase 4: multi-arch publish ─────────────────────────────────────
|
||||
build-variant:
|
||||
needs: [base-decide, smoke, resolve-versions]
|
||||
@@ -301,6 +385,8 @@ jobs:
|
||||
PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
|
||||
FORK_REF: ${{ needs.resolve-versions.outputs.fork_ref }}
|
||||
OBSMEM_REF: ${{ needs.resolve-versions.outputs.obsmem_ref }}
|
||||
TOOLKIT_REF: ${{ needs.resolve-versions.outputs.toolkit_ref }}
|
||||
EXTENSIONS_REF: ${{ needs.resolve-versions.outputs.extensions_ref }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG_FLAGS=()
|
||||
@@ -316,6 +402,91 @@ jobs:
|
||||
--build-arg "PI_VERSION=${PI_VERSION}" \
|
||||
--build-arg "PI_FORK_REF=${FORK_REF}" \
|
||||
--build-arg "PI_OBSMEM_REF=${OBSMEM_REF}" \
|
||||
--build-arg "PI_TOOLKIT_REF=${TOOLKIT_REF}" \
|
||||
--build-arg "PI_EXTENSIONS_REF=${EXTENSIONS_REF}" \
|
||||
"${TAG_FLAGS[@]}" \
|
||||
.; then
|
||||
echo "==> Attempt ${attempt} succeeded"
|
||||
exit 0
|
||||
fi
|
||||
if [[ "${attempt}" -lt 3 ]]; then
|
||||
backoff=$(( attempt * 15 ))
|
||||
echo "==> Attempt ${attempt} failed, sleeping ${backoff}s before retry"
|
||||
sleep "${backoff}"
|
||||
fi
|
||||
done
|
||||
echo "==> All 3 build+push attempts failed"
|
||||
exit 1
|
||||
|
||||
# ── Phase 4b: multi-arch publish of the studio variant ───────────────
|
||||
# Additive: publishes :vX.Y.Z-studio (+ :latest-studio on release). Gated
|
||||
# on its own smoke-studio, NOT on the core build-variant, so it can ship
|
||||
# or fail independently of the core release.
|
||||
build-variant-studio:
|
||||
needs: [base-decide, smoke-studio, resolve-versions]
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: catthehacker/ubuntu:act-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: echo 'precedence ::ffff:0:0/96 100' >> /etc/gai.conf
|
||||
- run: |
|
||||
rm -rf /opt/hostedtoolcache /opt/microsoft /opt/az /opt/ghc \
|
||||
/usr/local/.ghcup /usr/share/dotnet /usr/share/swift \
|
||||
/usr/local/lib/android /usr/local/share/powershell \
|
||||
/usr/local/share/chromium /usr/local/share/boost \
|
||||
/usr/lib/jvm 2>/dev/null || true
|
||||
docker system prune -af --volumes || true
|
||||
docker builder prune -af || true
|
||||
- uses: docker/setup-qemu-action@v3
|
||||
with: {platforms: arm64}
|
||||
- uses: docker/setup-buildx-action@v4
|
||||
with: {driver-opts: network=host}
|
||||
- uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ vars.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Compute studio version-specific tags
|
||||
id: tags
|
||||
run: |
|
||||
VERSION="${{ env.RELEASE_TAG }}"
|
||||
{ echo "tags<<EOF"
|
||||
echo "${IMAGE}:${VERSION}-studio"
|
||||
if [ "${{ env.PROMOTE_LATEST }}" = "true" ]; then
|
||||
echo "${IMAGE}:latest-studio"
|
||||
fi
|
||||
echo "EOF"
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
- name: Build and push studio variant (with retry)
|
||||
shell: bash
|
||||
env:
|
||||
TAGS: ${{ steps.tags.outputs.tags }}
|
||||
BASE_IMAGE_FULL: ${{ env.IMAGE }}:${{ needs.base-decide.outputs.base_tag }}
|
||||
PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
|
||||
FORK_REF: ${{ needs.resolve-versions.outputs.fork_ref }}
|
||||
OBSMEM_REF: ${{ needs.resolve-versions.outputs.obsmem_ref }}
|
||||
TOOLKIT_REF: ${{ needs.resolve-versions.outputs.toolkit_ref }}
|
||||
EXTENSIONS_REF: ${{ needs.resolve-versions.outputs.extensions_ref }}
|
||||
STUDIO_REF: ${{ needs.resolve-versions.outputs.studio_ref }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
TAG_FLAGS=()
|
||||
while IFS= read -r t; do [[ -n "$t" ]] && TAG_FLAGS+=( -t "$t" ); done <<< "${TAGS}"
|
||||
# 3-attempt retry (see build-base step for rationale).
|
||||
for attempt in 1 2 3; do
|
||||
echo "==> Build+push attempt ${attempt}/3"
|
||||
if docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--file Dockerfile.variant \
|
||||
--push \
|
||||
--build-arg "BASE_IMAGE=${BASE_IMAGE_FULL}" \
|
||||
--build-arg "PI_VERSION=${PI_VERSION}" \
|
||||
--build-arg "PI_FORK_REF=${FORK_REF}" \
|
||||
--build-arg "PI_OBSMEM_REF=${OBSMEM_REF}" \
|
||||
--build-arg "PI_TOOLKIT_REF=${TOOLKIT_REF}" \
|
||||
--build-arg "PI_EXTENSIONS_REF=${EXTENSIONS_REF}" \
|
||||
--build-arg "INSTALL_STUDIO=true" \
|
||||
--build-arg "PI_STUDIO_REF=${STUDIO_REF}" \
|
||||
"${TAG_FLAGS[@]}" \
|
||||
.; then
|
||||
echo "==> Attempt ${attempt} succeeded"
|
||||
@@ -374,7 +545,7 @@ jobs:
|
||||
|
||||
# ── Phase 6: update Hub description (only on real release runs) ────
|
||||
update-description:
|
||||
needs: [build-variant]
|
||||
needs: [build-variant, resolve-versions]
|
||||
if: |
|
||||
always() &&
|
||||
needs.build-variant.result == 'success' &&
|
||||
@@ -385,7 +556,27 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Update Docker Hub description
|
||||
env:
|
||||
PI_VERSION: ${{ needs.resolve-versions.outputs.pi_version }}
|
||||
run: |
|
||||
# Substitute {{PI_VERSION}} placeholders in DOCKER_HUB.md so the
|
||||
# Hub page always shows which pi version is in :latest. The
|
||||
# placeholder lives in DOCKER_HUB.md (committed); CI fills it
|
||||
# at publish time using the same resolved version that was
|
||||
# baked into the variant image. No drift between page and image.
|
||||
if [ -z "${PI_VERSION}" ]; then
|
||||
echo "::error::PI_VERSION env var is empty. Likely cause: the"
|
||||
echo "::error::update-description job is missing 'resolve-versions'"
|
||||
echo "::error::in its needs: list, so needs.resolve-versions.outputs.pi_version"
|
||||
echo "::error::resolves to an empty string instead of the actual version."
|
||||
exit 1
|
||||
fi
|
||||
cp DOCKER_HUB.md /tmp/hub-full.md
|
||||
sed -i "s/{{PI_VERSION}}/${PI_VERSION}/g" /tmp/hub-full.md
|
||||
if grep -q '{{PI_VERSION}}' /tmp/hub-full.md; then
|
||||
echo "::error::DOCKER_HUB.md still contains unsubstituted {{PI_VERSION}} markers"
|
||||
exit 1
|
||||
fi
|
||||
TOKEN=$(curl -s -X POST https://hub.docker.com/v2/auth/token \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"identifier":"${{ vars.DOCKERHUB_USERNAME }}","secret":"${{ secrets.DOCKERHUB_TOKEN }}"}' \
|
||||
@@ -395,8 +586,8 @@ jobs:
|
||||
exit 1
|
||||
fi
|
||||
HTTP_CODE=$(jq -n \
|
||||
--rawfile full DOCKER_HUB.md \
|
||||
--arg short "Self-contained Linux container for the pi coding-agent — pi + companions + MemPalace + curated dev tooling. Decoupled from opencode-devbox at v1.0.0." \
|
||||
--rawfile full /tmp/hub-full.md \
|
||||
--arg short "Linux container with the pi coding-agent, MemPalace, and curated dev tooling." \
|
||||
'{"full_description": $full, "description": $short}' | \
|
||||
curl -s -o /tmp/hub-response.txt -w "%{http_code}" -X PATCH \
|
||||
"https://hub.docker.com/v2/repositories/${{ vars.DOCKERHUB_USERNAME }}/pi-devbox/" \
|
||||
@@ -409,4 +600,4 @@ jobs:
|
||||
echo "::error::Docker Hub description update failed with HTTP $HTTP_CODE"
|
||||
exit 1
|
||||
fi
|
||||
echo "Description updated."
|
||||
echo "Description updated (pi version: ${PI_VERSION})."
|
||||
|
||||
@@ -12,25 +12,32 @@ re-brand of opencode-devbox's `pi-only` variant.
|
||||
Node.js, Python toolchain, locales, ssh ControlMaster defaults, and
|
||||
`/etc/tmux.conf` with 0-indexed sessions.
|
||||
- `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`
|
||||
(`-studio` variant).
|
||||
- `entrypoint.sh` — UID/GID alignment as root, then drops to `developer`.
|
||||
- `entrypoint-user.sh` — per-container start: SSH ControlMaster socket
|
||||
dir, LAN-access setup, MemPalace init, pi-toolkit + pi-extensions
|
||||
deploy, mempalace-bridge symlink, fork/recall pi-install, skillset
|
||||
deploy, mempalace-bridge symlink, fork/recall + pi-studio pi-install,
|
||||
optional `studio-expose` bridge (when `STUDIO_EXPOSE=1`), skillset
|
||||
deploy.
|
||||
- `rootfs/` — files baked into the image (bash aliases, inputrc,
|
||||
setup-lan-access.sh).
|
||||
setup-lan-access.sh, `studio-expose` helper).
|
||||
- `scripts/smoke-test.sh` — sanity checks run by CI before pushing to Hub.
|
||||
- `.gitea/workflows/docker-publish.yml` — two-phase CI (base-decide →
|
||||
build-base → smoke → build-variant → promote-base-latest →
|
||||
update-description).
|
||||
update-description). The `-studio` variant adds independent
|
||||
`smoke-studio` + `build-variant-studio` jobs that gate only the
|
||||
`-studio` tags (never the core `:latest` release).
|
||||
|
||||
## Versioning scheme
|
||||
|
||||
- Tags follow semver. **v1.0.0** is the first decoupled release; future
|
||||
minor bumps add variants (`-studio`, `-studio-tex`); patch bumps follow
|
||||
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` +
|
||||
`joakimp/pi-devbox:latest-studio`.
|
||||
Internal tags: `joakimp/pi-devbox:base-<hash>` (content-addressed) +
|
||||
`joakimp/pi-devbox:base-latest` (alias of most recent base).
|
||||
|
||||
@@ -45,8 +52,8 @@ re-brand of opencode-devbox's `pi-only` variant.
|
||||
5. Watch CI: smoke job builds amd64 only and asserts size + extensions +
|
||||
pi version + new-base-tooling presence. Variant build is multi-arch
|
||||
(amd64 + arm64) only after smoke passes.
|
||||
6. Verify the Hub tags appear (latest + vX.Y.Z, plus base-latest if the
|
||||
base was rebuilt this run).
|
||||
6. Verify the Hub tags appear (latest + vX.Y.Z, the `-studio` pair, plus
|
||||
base-latest if the base was rebuilt this run).
|
||||
7. **Revoke any short-lived Gitea PAT** used during the release at
|
||||
`gitea.jordbo.se/user/settings/applications`.
|
||||
|
||||
@@ -108,12 +115,16 @@ deprecated artifacts (to be removed in opencode-devbox v2.0.0).
|
||||
## What we DON'T install (and why)
|
||||
|
||||
- **No texlive** (~600 MB–1 GB). Users who need PDF export from pandoc
|
||||
can install on demand: `sudo apt-get install texlive-xetex
|
||||
or pi-studio can install on demand: `sudo apt-get install texlive-xetex
|
||||
texlive-latex-recommended`. The planned `:latest-studio-tex` variant
|
||||
will bake this in.
|
||||
- **No pi-studio** (yet). Coming in v1.1.0 as the `:latest-studio`
|
||||
variant. v1.0.0 is intentionally scope-limited to "decouple, don't
|
||||
reshape."
|
||||
- **pi-studio** ships in the `:latest-studio` variant (since v1.1.0),
|
||||
vendored to `/opt/pi-studio` and registered at container start via
|
||||
`pi install /opt/pi-studio` (see Dockerfile.variant `INSTALL_STUDIO`).
|
||||
The default `:latest` image stays studio-free. Note: pi-studio binds
|
||||
`127.0.0.1` inside the container, so browser access needs host
|
||||
networking or the bundled `studio-expose` bridge (socat; auto-starts
|
||||
when `STUDIO_EXPOSE=1`) — see README "Using pi-studio".
|
||||
- **No Julia/R/GHCi/Clojure runtimes**. Use `uv run --with X` for
|
||||
Python REPLs; `apt install` other-language runtimes ad-hoc per
|
||||
container if needed.
|
||||
@@ -121,8 +132,9 @@ deprecated artifacts (to be removed in opencode-devbox v2.0.0).
|
||||
## Backward compatibility
|
||||
|
||||
- The host `~/.mempalace` bind-mount path is unchanged.
|
||||
- Volume names (`devbox-pi-config`, `devbox-bash-history`,
|
||||
`devbox-nvim-data`, `devbox-uv-tools`, `devbox-chroma-cache`) are
|
||||
- Volume names (`devbox-pi-config`, `devbox-ssh-local`,
|
||||
`devbox-shell-history`, `devbox-zoxide`, `devbox-nvim-data`,
|
||||
`devbox-uv`; optional `devbox-palace`, `devbox-chroma-cache`) are
|
||||
unchanged.
|
||||
- `~/.pi/agent/` layout inside the container is unchanged; existing
|
||||
named volumes work without recreation.
|
||||
|
||||
+65
-1
@@ -13,7 +13,71 @@ Pre-v1.0.0 tags followed the pi npm version (`v{pi_version}[letter]`).
|
||||
|
||||
## Unreleased
|
||||
|
||||
_(no changes since v1.0.0)_
|
||||
## v1.1.0 — 2026-06-10
|
||||
|
||||
### Added — `:latest-studio` variant
|
||||
|
||||
- **New `-studio` image variant** bundling
|
||||
[pi-studio](https://github.com/omaclaren/pi-studio) — a two-pane
|
||||
browser workspace (prompt/response editor, live KaTeX/Mermaid preview,
|
||||
tmux-backed literate REPLs for Shell/Python/IPython/Julia/R/GHCi/Clojure)
|
||||
plus the `/studio` slash command and `studio_repl_send` /
|
||||
`studio_export_*` agent tools. Published as `:latest-studio` and
|
||||
`:vX.Y.Z-studio` (multi-arch).
|
||||
- pi-studio is **vendored to `/opt/pi-studio`** at build time (gated by
|
||||
`INSTALL_STUDIO=true`, ref pinned via CI-resolved `PI_STUDIO_REF`) and
|
||||
registered on container start by `entrypoint-user.sh` via
|
||||
`pi install /opt/pi-studio` — the same pattern as pi-fork /
|
||||
pi-observational-memory. No build step: pi-studio ships its browser
|
||||
bundle prebuilt in git. The non-studio `:latest` image is unchanged.
|
||||
- CI gains independent `smoke-studio` + `build-variant-studio` jobs that
|
||||
gate **only** the studio tags, so a studio build/smoke failure can
|
||||
never block the core `:latest` / `:vX.Y.Z` release.
|
||||
- `STUDIO_PORT=8765` baked as an advisory default.
|
||||
- **`studio-expose` helper + `socat` (base).** Because pi-studio binds the
|
||||
container's loopback, a published Docker port can't reach it. The new
|
||||
`studio-expose` helper (socat, added to the base) bridges the container's
|
||||
loopback to its egress interface on the same port; set `STUDIO_EXPOSE=1`
|
||||
in compose to auto-start it on boot (default off — Studio stays
|
||||
loopback-only otherwise). `socat` is in the base for all variants.
|
||||
- **README "Using pi-studio" section.** Documents the container access
|
||||
reality: pi-studio hard-binds `127.0.0.1` inside the container
|
||||
(`.listen(port,"127.0.0.1")`, no `--host` flag), so a plain `-p`
|
||||
publish does not reach it. Documents the two working paths — host
|
||||
networking (recommended on OrbStack) and a loopback bridge for bridge
|
||||
networking — plus the remote `ssh -L` forward and the **mosh caveat**
|
||||
(mosh cannot forward ports; run a parallel `ssh -L` alongside it).
|
||||
|
||||
## v1.0.1 — 2026-06-10
|
||||
|
||||
Patch release. Works around an upstream MemPalace bug that broke pi at
|
||||
first prompt against the Anthropic Claude API.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`mempalace_diary_write` schema rejected by Anthropic API.** Mempalace
|
||||
3.3.x and 3.4.0 advertise `diary_write`'s `input_schema` with a
|
||||
top-level `anyOf: [{required:[entry]}, {required:[content]}]` to
|
||||
express "either `entry` or `content` must be supplied". Anthropic's
|
||||
tools API rejects top-level `anyOf` / `oneOf` / `allOf` outright, so
|
||||
pi failed to register tools at session start with
|
||||
`tools.<n>.custom.input_schema: input_schema does not support oneOf,
|
||||
allOf, or anyOf at the top level`. `Dockerfile.base` now patches the
|
||||
installed `mcp_server.py` after `uv tool install` to drop the `anyOf`
|
||||
block and require `["agent_name", "entry"]` instead. The mempalace
|
||||
handler still accepts `content` server-side as a kwarg alias, so
|
||||
callers using either name keep working. Tracked upstream:
|
||||
[issue #1728](https://github.com/MemPalace/mempalace/issues/1728),
|
||||
[PR #1735](https://github.com/MemPalace/mempalace/pull/1735).
|
||||
The workaround is idempotent + self-deactivating and will be removed
|
||||
once a fixed mempalace release lands on PyPI.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Mempalace pinned to 3.4.0** via `MEMPALACE_VERSION` build arg.
|
||||
Future bumps must be a reviewable diff rather than an implicit pull
|
||||
of `latest` (the broken 3.3.x/3.4.0 schema slipping in unannounced
|
||||
is what caused this release).
|
||||
|
||||
## v1.0.0 — 2026-06-09
|
||||
|
||||
|
||||
+80
-33
@@ -1,15 +1,21 @@
|
||||
# pi-devbox
|
||||
|
||||
A Docker container with [pi coding-agent](https://github.com/earendil-works/pi) pre-installed, built on top of [opencode-devbox](https://hub.docker.com/r/joakimp/opencode-devbox)'s base image. Pi gets a fully-loaded development environment in one `docker run`.
|
||||
A self-contained Docker container for the [pi coding-agent](https://github.com/earendil-works/pi) — pi + companion repos + MemPalace + a curated set of dev tooling, ready to run.
|
||||
|
||||
> **Current `:latest` ships pi `{{PI_VERSION}}`** (resolved at build time; see [Versioning](#versioning)).
|
||||
|
||||
## Image variants
|
||||
|
||||
| Tag | Size (compressed) | What you get |
|
||||
|---|---|---|
|
||||
| `joakimp/pi-devbox:latest` | ~700 MB | Pi + companion repos, on top of the opencode-devbox base |
|
||||
| `joakimp/pi-devbox:vX.Y.Z` | same | Pinned pi version (tracks the [pi npm package version](https://www.npmjs.com/package/@earendil-works/pi-coding-agent)) |
|
||||
| Tag | Architectures | Size (compressed) | What you get |
|
||||
|---|---|---|---|
|
||||
| `joakimp/pi-devbox:latest` | amd64, arm64 | ~1.1 GB | Self-contained: base + pi `{{PI_VERSION}}` + companions |
|
||||
| `joakimp/pi-devbox:vX.Y.Z` | amd64, arm64 | same | Pinned semver release |
|
||||
| `joakimp/pi-devbox:latest-studio` | amd64, arm64 | ~1.15 GB | `latest` + [pi-studio](https://github.com/omaclaren/pi-studio): browser prompt editor, KaTeX/Mermaid preview, tmux-backed literate REPLs |
|
||||
| `joakimp/pi-devbox:vX.Y.Z-studio` | amd64, arm64 | same | Pinned semver studio release |
|
||||
| `joakimp/pi-devbox:base-latest` | amd64, arm64 | ~1.0 GB | Base layer alias (internal building block; pull `:latest` instead) |
|
||||
| `joakimp/pi-devbox:base-<hash>` | amd64, arm64 | ~1.0 GB | Content-addressed base; immutable. Stable parent for variant rebuilds. |
|
||||
|
||||
Multi-arch: `linux/amd64`, `linux/arm64`.
|
||||
> **pi-studio (`-studio` tags):** launch with `/studio --no-browser --port 8765` inside a pi session. The server binds `127.0.0.1` **inside the container**, so reach it via host networking or a loopback bridge (and `ssh -L` for a remote host; mosh needs a parallel `ssh -L`). Full recipe: [README → Using pi-studio](https://gitea.jordbo.se/joakimp/pi-devbox#using-pi-studio--studio-variant).
|
||||
|
||||
## Quick start
|
||||
|
||||
@@ -38,42 +44,82 @@ Full setup guide — authentication for each provider (Anthropic, OpenAI, Gemini
|
||||
|
||||
## What's inside
|
||||
|
||||
pi-devbox is a re-brand of the **pi-only build** — it builds
|
||||
`FROM joakimp/pi-devbox:base-pi-only` and adds no layers of its own. That
|
||||
building-block tag is produced by opencode-devbox's CI (from
|
||||
`Dockerfile.variant` with `INSTALL_OPENCODE=false`) but published here, in the
|
||||
pi-devbox repo, so an opencode-devbox tag never ships without opencode.
|
||||
The pi-only build is lean
|
||||
and pi-focused (no opencode — use `opencode-devbox:latest-with-pi` if you want
|
||||
both).
|
||||
Everything below is inherited from that single source of truth.
|
||||
### pi and companions
|
||||
|
||||
Base tooling:
|
||||
|
||||
- **Debian trixie** (latest stable)
|
||||
- **Node.js** (LTS), **uv** (Python tooling), **rustup** (Rust on-demand)
|
||||
- **AWS CLI v2** + AWS Bedrock-ready config
|
||||
- **MemPalace** + MCP server — persistent agent memory across sessions, queryable via `mempalace_*` tools inside pi
|
||||
- **Gitea MCP** server
|
||||
- **Dev tools**: neovim (LazyVim defaults), tmux, bat, eza, fzf, zoxide, ripgrep, git-lfs, make
|
||||
- **Shell**: bash with history tuning, prefix-search bindings, fzf/zoxide integration
|
||||
- **Host-OS-agnostic LAN access** — on VM-backed hosts (macOS OrbStack / Docker Desktop) the host is set up as an SSH jump to reach LAN peers (`dssh` alias; `DEVBOX_LAN_ACCESS`/`HOST_SSH_USER`). No-op on native Linux.
|
||||
|
||||
pi and companions:
|
||||
|
||||
- **pi** ([`@earendil-works/pi-coding-agent`](https://www.npmjs.com/package/@earendil-works/pi-coding-agent)) — baked at `/usr/bin/pi`, version set by the pi-only base build
|
||||
- **pi `{{PI_VERSION}}`** ([`@earendil-works/pi-coding-agent`](https://www.npmjs.com/package/@earendil-works/pi-coding-agent)) — installed at `/usr/bin/pi`
|
||||
- **[pi-toolkit](https://gitea.jordbo.se/joakimp/pi-toolkit)** — keybindings (mosh/tmux-friendly Shift+Enter, Ctrl+J, Alt+J newline bindings), AWS env loader, settings template
|
||||
- **[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
|
||||
- **mempalace bridge** — MCP extension auto-symlinked so pi can read/write the same palace as opencode-devbox
|
||||
- **mempalace bridge** — MCP extension auto-symlinked so pi reads/writes the host-mounted palace
|
||||
|
||||
The entrypoint deploys/registers all of these on first container start. Re-running is idempotent and preserves user edits.
|
||||
|
||||
### MemPalace (persistent agent memory)
|
||||
|
||||
- **MemPalace** + MCP server — semantic search over conversation history, knowledge graph, diary; queryable via 29 `mempalace_*` tools inside pi
|
||||
- ChromaDB ONNX embedding model pre-warmed at build time (`all-MiniLM-L6-v2`)
|
||||
- Bind-mount your host's `~/.mempalace` and the host-pi and container-pi share one brain
|
||||
|
||||
### Document and image tooling
|
||||
|
||||
- **pandoc** — universal Markdown↔HTML/Org/RST/etc. conversion. Useful well beyond pi: agent-driven doc exports, format conversion, etc.
|
||||
- **graphviz** (`dot`) — diagram rendering pipelines
|
||||
- **imagemagick** (`magick`) — image conversion / resizing
|
||||
|
||||
### Modern CLI tooling
|
||||
|
||||
- **Editor**: neovim (LazyVim defaults), tmux (configured for 0-indexed sessions)
|
||||
- **Search/nav**: ripgrep, fd, fzf, zoxide
|
||||
- **Display**: bat, eza, htop, tree
|
||||
- **Data**: jq, yq
|
||||
- **Help**: tldr (tealdeer — Rust port; run `tldr --update` once to populate cache)
|
||||
- **Git**: git-lfs, git-crypt, gitleaks (for pre-commit secret scanning)
|
||||
- **Build**: gcc, g++, make, patch
|
||||
- **Misc**: gosu, age, rsync, less
|
||||
|
||||
### Language toolchains
|
||||
|
||||
- **Python**: system Python 3 + **uv** (preferred) for fast Python package management. Run any Python REPL/notebook stack on demand without bloating the image:
|
||||
```bash
|
||||
uv run --with ipython ipython
|
||||
uv run --with jupyterlab jupyter lab --no-browser --port 8888
|
||||
uv run --with marimo marimo edit
|
||||
```
|
||||
- **Node.js** v22 + npm (used by pi itself)
|
||||
- **Rust** — `rustup-init` is on PATH; install toolchains on demand
|
||||
- **Go** — opt-in via `--build-arg INSTALL_GO=true` if rebuilding from source
|
||||
|
||||
### Cloud + secrets
|
||||
|
||||
- **AWS CLI v2** — for SSO + Bedrock auth (pi's preferred LLM provider for the maintainer's setup)
|
||||
- **Gitea MCP** server — for Gitea API access from inside pi
|
||||
- **age**, **git-crypt** — encryption tooling
|
||||
|
||||
### SSH and networking
|
||||
|
||||
- OpenSSH client with **ControlMaster auto** preconfigured on a writable socket path (`/tmp/sshcm/`). Mitigates ssh banner-exchange failures behind CGNAT-restricted residential ISPs (~4-flow caps).
|
||||
- A **LAN-access helper** that auto-configures ssh jump-via-host on VM-backed hosts (OrbStack / Docker Desktop on macOS) so the container can reach the host's directly-attached LAN peers (`dssh <peer>` alias; `DEVBOX_LAN_ACCESS` / `HOST_SSH_USER`).
|
||||
|
||||
## Versioning
|
||||
|
||||
Tags follow the pi npm version: `v0.74.0`, `v0.75.0`, etc. `latest` always points at the most recent release. The pi binary is inherited from `joakimp/pi-devbox:base-pi-only`, so each release follows an opencode-devbox release that bakes the target pi version. (`base-pi-only` is an internal building-block tag — pull `latest` or a `vX.Y.Z` tag instead.)
|
||||
From v1.0.0 onward, pi-devbox uses **semver**:
|
||||
|
||||
For container-level rebuilds on the same pi version (security updates, base bumps, fixes) the tag gets a letter suffix: `v0.74.0b`, `v0.74.0c`, …
|
||||
- **Major** — architectural changes. v1.0.0 is the first decoupled release, where pi-devbox got its own self-contained build chain (previously it was a thin re-brand of opencode-devbox's `pi-only` variant).
|
||||
- **Minor** — new image variants, significant base additions.
|
||||
- **Patch** — pi version bumps, smaller fixes.
|
||||
|
||||
The pi binary version inside any given release is shown in this description (currently **`{{PI_VERSION}}`** for `:latest`) and asserted by smoke tests to match what's documented — version drift is caught at CI time, not on user pull.
|
||||
|
||||
> **Pre-v1.0.0 history.** Tags v0.74.0…v0.79.0 followed the pi npm version directly (`v{pi_version}[letter]`). Those images remain on Hub but are deprecated in favor of `:latest` / `:v1.X.Y`. The legacy `:base-pi-only*` tags were CI artifacts of the old opencode-devbox-based build pipeline; they will be removed in a future opencode-devbox v2.0.0.
|
||||
|
||||
### Build pipeline
|
||||
|
||||
pi-devbox is built in two phases:
|
||||
|
||||
1. **Base** (`Dockerfile.base`) → `base-<hash>` tag, content-addressed over `Dockerfile.base` + `rootfs/` + `entrypoint*.sh`. Rebuilt only when those change.
|
||||
2. **Variant** (`Dockerfile.variant`) → `:latest` and `:vX.Y.Z`. FROMs the base, adds the pi install + companions.
|
||||
|
||||
`base-latest` is an alias of the most recent base.
|
||||
|
||||
## Persistent state
|
||||
|
||||
@@ -86,6 +132,7 @@ User edits and pi-installed packages survive container recreation when you mount
|
||||
| `devbox-zoxide` | `/home/developer/.local/share/zoxide` | zoxide directory jump database |
|
||||
| `devbox-nvim-data` | `/home/developer/.local/share/nvim` | neovim plugin & Mason package state |
|
||||
| `devbox-uv` | `/home/developer/.local/share/uv` | uv Python installs and tool cache |
|
||||
| `devbox-ssh-local` | `/home/developer/.ssh-local` | LAN-jump key (one-time host authorization survives recreate) |
|
||||
|
||||
Optional volumes for MemPalace (commented out by default — uncomment in `docker-compose.yml` to persist conversation memory across restarts):
|
||||
|
||||
@@ -101,10 +148,10 @@ Optional volumes for MemPalace (commented out by default — uncomment in `docke
|
||||
## Source
|
||||
|
||||
- **This image**: https://gitea.jordbo.se/joakimp/pi-devbox
|
||||
- **Base image**: https://gitea.jordbo.se/joakimp/opencode-devbox (Hub: `joakimp/opencode-devbox`)
|
||||
- **pi**: https://github.com/earendil-works/pi
|
||||
- **pi-toolkit**: https://gitea.jordbo.se/joakimp/pi-toolkit
|
||||
- **pi-extensions**: https://gitea.jordbo.se/joakimp/pi-extensions
|
||||
- **MemPalace**: https://github.com/MemPalace/mempalace
|
||||
|
||||
## License
|
||||
|
||||
|
||||
+43
-1
@@ -50,6 +50,10 @@ ENV DEBIAN_FRONTEND=noninteractive
|
||||
# graphviz — `dot` rendering for many diagram tools. ~10 MB.
|
||||
# imagemagick — image conversion / resizing for thumbnails, etc. ~50 MB.
|
||||
# yq — YAML-aware companion to jq.
|
||||
# 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.
|
||||
RUN apt-get update && \
|
||||
apt-get upgrade -y --no-install-recommends && \
|
||||
apt-get install -y --no-install-recommends \
|
||||
@@ -85,6 +89,7 @@ RUN apt-get update && \
|
||||
pandoc \
|
||||
graphviz \
|
||||
imagemagick \
|
||||
socat \
|
||||
&& ln -s /usr/bin/fdfind /usr/local/bin/fd \
|
||||
&& apt-get clean \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
@@ -273,14 +278,49 @@ RUN ARCH=$(case "${TARGETARCH}" in amd64) echo "x86_64" ;; arm64) echo "aarch64"
|
||||
# Always installed in the base. Set INSTALL_MEMPALACE=false at base-build
|
||||
# time to shave ~300 MB.
|
||||
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 (see workaround RUN below + issue #1728). Pinning
|
||||
# makes mempalace upgrades a reviewable diff rather than a surprise.
|
||||
ARG MEMPALACE_VERSION=3.4.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 && \
|
||||
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
|
||||
|
||||
# ── workaround: strip top-level anyOf from mempalace_diary_write schema ──
|
||||
# Mempalace 3.3.x/3.4.0 advertise diary_write's input_schema with a
|
||||
# top-level `anyOf: [{required:[entry]}, {required:[content]}]` to express
|
||||
# "either entry or content must be supplied". Anthropic's tools API rejects
|
||||
# top-level anyOf/oneOf/allOf, so pi/Claude fail at session start with
|
||||
# `tools.<n>.custom.input_schema: input_schema does not support oneOf,
|
||||
# allOf, or anyOf at the top level`.
|
||||
#
|
||||
# Patch the advertised schema to require ["agent_name", "entry"] and remove
|
||||
# the anyOf block. The handler keeps accepting `content` server-side as a
|
||||
# kwarg alias so existing callers still work.
|
||||
#
|
||||
# Idempotent and self-deactivating: once upstream releases the fix the
|
||||
# regex no longer matches and this RUN is a silent no-op.
|
||||
# Upstream tracking:
|
||||
# https://github.com/MemPalace/mempalace/issues/1728
|
||||
# https://github.com/MemPalace/mempalace/pull/1735
|
||||
# TODO: remove this RUN once a mempalace release containing PR #1735 is on
|
||||
# PyPI and installed by the line above.
|
||||
RUN if [ "${INSTALL_MEMPALACE}" = "true" ]; then \
|
||||
MP_FILE="$(find /opt/uv-tools/mempalace -path '*/mempalace/mcp_server.py' | head -n1)" && \
|
||||
if [ -z "$MP_FILE" ]; then echo "mempalace mcp_server.py not found" >&2; exit 1; fi && \
|
||||
perl -0777 -i -pe 's/(?:[ \t]*\#[^\n]*\n)*[ \t]*"required":\s*\[\s*"agent_name"\s*\]\s*,\s*\n[ \t]*"anyOf":\s*\[\s*\n[ \t]*\{\s*"required":\s*\[\s*"entry"\s*\]\s*\}\s*,\s*\n[ \t]*\{\s*"required":\s*\[\s*"content"\s*\]\s*\}\s*,?\s*\n[ \t]*\]\s*,\s*\n/ "required": ["agent_name", "entry"],\n/s' "$MP_FILE" && \
|
||||
if grep -q '"required": \["agent_name", "entry"\]' "$MP_FILE"; then \
|
||||
echo "mempalace diary_write anyOf workaround: applied (or already clean)"; \
|
||||
else \
|
||||
echo "WARN: mempalace diary_write anyOf workaround did not match expected schema — upstream may have changed shape" >&2; \
|
||||
fi ; \
|
||||
fi
|
||||
|
||||
# ── mempalace-toolkit — bash wrappers for session/docs mining ────────
|
||||
ARG INSTALL_MEMPALACE_TOOLKIT=true
|
||||
ARG MEMPALACE_TOOLKIT_REF=main
|
||||
@@ -395,9 +435,11 @@ COPY rootfs/home/developer/.inputrc /etc/skel-devbox/.inputrc
|
||||
|
||||
# ── Entrypoint ────────────────────────────────────────────────────────
|
||||
COPY rootfs/usr/local/lib/pi-devbox/ /usr/local/lib/pi-devbox/
|
||||
COPY rootfs/usr/local/bin/studio-expose /usr/local/bin/studio-expose
|
||||
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/lib/pi-devbox/*.sh 2>/dev/null || true
|
||||
|
||||
# Start as root — entrypoint adjusts UID/GID then drops to developer
|
||||
|
||||
+60
-12
@@ -50,16 +50,16 @@ ARG PI_OBSMEM_REPO=https://github.com/elpapi42/pi-observational-memory.git
|
||||
ARG PI_OBSMEM_REF=master
|
||||
|
||||
RUN set -e && \
|
||||
git_clone_retry() { \
|
||||
url="$1"; ref="$2"; dest="$3"; \
|
||||
for i in 1 2 3 4 5; do \
|
||||
if git clone --depth 1 --branch "$ref" "$url" "$dest"; then return 0; fi; \
|
||||
rm -rf "$dest"; \
|
||||
echo "git clone $url failed (attempt $i/5), retrying in $((i*5))s..."; \
|
||||
sleep $((i*5)); \
|
||||
done; \
|
||||
return 1; \
|
||||
} && \
|
||||
# git_fetch_ref: clone-equivalent helper that accepts EITHER a branch name
|
||||
# OR a commit SHA as $ref. Uses `git fetch <ref> + checkout FETCH_HEAD`
|
||||
# which (a) works with both name and SHA forms uniformly, and (b) defeats
|
||||
# the registry-buildcache footgun when CI passes a resolved SHA. The
|
||||
# earlier helper `git_clone_retry` (using `git clone --branch`) only
|
||||
# worked with branch names — a SHA-resolved build-arg made `git clone
|
||||
# --branch <40-char-SHA>` fail with "Remote branch not found". Surfaced
|
||||
# in pi-devbox v1.0.0-rerun (run 374) 2026-06-10 and fixed by switching
|
||||
# all four clones to git_fetch_ref. Both Gitea and GitHub allow fetching
|
||||
# arbitrary commits by default (uploadpack.allowReachableSHA1InWant).
|
||||
git_fetch_ref() { \
|
||||
url="$1"; ref="$2"; dest="$3"; \
|
||||
rm -rf "$dest"; mkdir -p "$dest"; \
|
||||
@@ -77,8 +77,8 @@ RUN set -e && \
|
||||
NPM_CONFIG_PREFIX=/usr npm install -g @earendil-works/pi-coding-agent@${PI_VERSION} ; \
|
||||
fi && \
|
||||
pi --version && \
|
||||
git_clone_retry https://gitea.jordbo.se/joakimp/pi-toolkit.git "${PI_TOOLKIT_REF}" /opt/pi-toolkit && \
|
||||
git_clone_retry https://gitea.jordbo.se/joakimp/pi-extensions.git "${PI_EXTENSIONS_REF}" /opt/pi-extensions && \
|
||||
git_fetch_ref "https://gitea.jordbo.se/joakimp/pi-toolkit.git" "${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_FORK_REPO}" "${PI_FORK_REF}" /opt/pi-fork && \
|
||||
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) && \
|
||||
@@ -88,6 +88,54 @@ RUN set -e && \
|
||||
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)"
|
||||
|
||||
# ── Optional: pi-studio (:latest-studio variant) ─────────────────────
|
||||
# pi-studio (omaclaren/pi-studio) is a pi-package + theme providing a
|
||||
# two-pane browser workspace: prompt/response editor, KaTeX/Mermaid live
|
||||
# preview, and tmux-backed literate REPLs. Off by default; the studio
|
||||
# variant sets INSTALL_STUDIO=true.
|
||||
#
|
||||
# Vendored to /opt/pi-studio and registered at container start by
|
||||
# entrypoint-user.sh via `pi install /opt/pi-studio` — the SAME pattern
|
||||
# as pi-fork / pi-observational-memory above. We deliberately do NOT run
|
||||
# `pi install <git-url>` at build time: that writes into ~/.pi/agent,
|
||||
# which is a named volume, so a build-time install collides with / is
|
||||
# shadowed by the volume on first run. Vendoring to /opt (an image layer)
|
||||
# + a runtime local-path install keeps it on the image and idempotent.
|
||||
#
|
||||
# No build step is needed: pi-studio ships its browser bundle prebuilt in
|
||||
# git (client/studio-client.js) and pi loads index.ts directly; its
|
||||
# package.json scripts are only test/typecheck. So we just fetch + install
|
||||
# the 3 prod deps (@earendil-works/pi-ai, @sinclair/typebox, ws).
|
||||
#
|
||||
# PI_STUDIO_REF is CI-resolved to a commit SHA to defeat the registry-
|
||||
# buildcache cache-hit footgun (see the PI_VERSION note above).
|
||||
ARG INSTALL_STUDIO=false
|
||||
ARG PI_STUDIO_REPO=https://github.com/omaclaren/pi-studio.git
|
||||
ARG PI_STUDIO_REF=main
|
||||
RUN if [ "${INSTALL_STUDIO}" = "true" ]; then \
|
||||
set -e; \
|
||||
rm -rf /opt/pi-studio && mkdir -p /opt/pi-studio && \
|
||||
git -C /opt/pi-studio init -q && \
|
||||
git -C /opt/pi-studio remote add origin "${PI_STUDIO_REPO}" && \
|
||||
ok=0; for i in 1 2 3 4 5; do \
|
||||
if git -C /opt/pi-studio fetch --depth 1 origin "${PI_STUDIO_REF}" && \
|
||||
git -C /opt/pi-studio checkout -q FETCH_HEAD; then ok=1; break; fi; \
|
||||
echo "git fetch pi-studio@${PI_STUDIO_REF} failed (attempt $i/5), retrying in $((i*5))s..."; \
|
||||
sleep $((i*5)); \
|
||||
done; \
|
||||
[ "$ok" = "1" ] && \
|
||||
(cd /opt/pi-studio && npm install --omit=dev --no-audit --no-fund) && \
|
||||
echo "pi-studio at $(cd /opt/pi-studio && git rev-parse --short HEAD)"; \
|
||||
fi
|
||||
|
||||
# STUDIO_PORT: advisory default consumed by docker-compose port publishing
|
||||
# and the recommended `/studio --no-browser --port "$STUDIO_PORT"` launch.
|
||||
# Harmless in the non-studio variant. NOTE: pi-studio hard-binds the server
|
||||
# to 127.0.0.1 inside the container (index.ts: .listen(port,"127.0.0.1")),
|
||||
# so reaching it from a browser needs a loopback bridge or host networking —
|
||||
# see the "Using pi-studio" section in README.md.
|
||||
ENV STUDIO_PORT=8765
|
||||
|
||||
# ── Optional: Go toolchain ───────────────────────────────────────────
|
||||
# Off by default; opt in for users who run Go tools inside the devbox.
|
||||
ARG INSTALL_GO=false
|
||||
|
||||
@@ -98,7 +98,7 @@ git clone https://gitea.jordbo.se/joakimp/pi-devbox
|
||||
cd pi-devbox
|
||||
cp .env.example .env # edit if needed
|
||||
docker compose up -d
|
||||
docker compose exec devbox bash
|
||||
docker compose exec -u developer devbox bash
|
||||
```
|
||||
|
||||
You're now in the container as user `developer` with `pi` on PATH and
|
||||
@@ -131,16 +131,100 @@ Currently published:
|
||||
|---|---|---|
|
||||
| `joakimp/pi-devbox:latest` | base + pi + tooling | ~3.2 GB |
|
||||
| `joakimp/pi-devbox:vX.Y.Z` | pinned-version equivalent | ~3.2 GB |
|
||||
| `joakimp/pi-devbox:latest-studio` | `latest` + [pi-studio](https://github.com/omaclaren/pi-studio) (browser prompt editor, KaTeX/Mermaid preview, tmux-backed literate REPLs) | ~3.25 GB |
|
||||
| `joakimp/pi-devbox:vX.Y.Z-studio` | pinned-version studio equivalent | ~3.25 GB |
|
||||
|
||||
Planned for upcoming minor releases:
|
||||
Planned for an upcoming minor release:
|
||||
|
||||
- `joakimp/pi-devbox:latest-studio` — adds [pi-studio](https://github.com/omaclaren/pi-studio)
|
||||
for browser-based prompt editing, KaTeX/Mermaid preview, and
|
||||
literate REPLs (Shell / Python / IPython / Julia / R / GHCi /
|
||||
Clojure). Adds ~50 MB.
|
||||
- `joakimp/pi-devbox:latest-studio-tex` — also adds `texlive-xetex`
|
||||
- `joakimp/pi-devbox:latest-studio-tex` — `-studio` plus `texlive-xetex`
|
||||
for PDF export from Studio. Adds ~600 MB on top of `-studio`.
|
||||
|
||||
## Using pi-studio (`-studio` variant)
|
||||
|
||||
The `-studio` images bundle [pi-studio](https://github.com/omaclaren/pi-studio):
|
||||
a two-pane browser workspace with a prompt/response editor, live
|
||||
KaTeX/Mermaid preview, and tmux-backed literate REPLs (Shell / Python /
|
||||
IPython / Julia / R / GHCi / Clojure). It is registered automatically on
|
||||
container start (no `pi install` needed) and exposes the `/studio` slash
|
||||
command plus the `studio_repl_send` / `studio_export_*` agent tools.
|
||||
|
||||
Inside a pi session in the container:
|
||||
|
||||
```
|
||||
/studio --no-browser --port 8765 # pin a fixed port; STUDIO_PORT=8765 is the baked default
|
||||
/studio --status # reprint the tokenized URL
|
||||
```
|
||||
|
||||
### Reaching the UI from your browser (the container caveat)
|
||||
|
||||
pi-studio **hard-binds its server to `127.0.0.1` inside the container**
|
||||
(`index.ts`: `.listen(port, "127.0.0.1")`) and serves a tokenized URL.
|
||||
There is no `--host`/bind flag. This matters for a container: a plain
|
||||
`docker run -p 8765:8765` publish forwards to the container's *external*
|
||||
interface, **not** its loopback, so it will not reach Studio. Two paths
|
||||
work:
|
||||
|
||||
**A. Host networking (simplest — OrbStack / single-host, no bridge).**
|
||||
Run the container with host networking so the container's loopback is the
|
||||
host's loopback:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
devbox:
|
||||
network_mode: host # container 127.0.0.1 == host 127.0.0.1
|
||||
```
|
||||
|
||||
Then `http://127.0.0.1:8765/?token=…` works in a browser on the Docker
|
||||
host. This is the most secure option (Studio never leaves loopback). Note:
|
||||
host networking changes `host.docker.internal` semantics, so weigh it
|
||||
against the LAN-jump SSH feature if you use that.
|
||||
|
||||
**B. `studio-expose` bridge (portable — any networking mode).** Publish a
|
||||
port and run the bundled `studio-expose` helper, which uses `socat` to
|
||||
bridge the container's loopback to its external interface (binding the
|
||||
egress IP on the same port, so the token URL Studio printed works
|
||||
verbatim):
|
||||
|
||||
```yaml
|
||||
services:
|
||||
devbox:
|
||||
ports:
|
||||
- "127.0.0.1:8765:8765" # host-localhost only
|
||||
environment:
|
||||
- STUDIO_EXPOSE=1 # auto-start the bridge on container boot
|
||||
```
|
||||
|
||||
With `STUDIO_EXPOSE=1`, the entrypoint starts the bridge for you; just run
|
||||
`/studio --port 8765` in your pi session. To bridge manually instead
|
||||
(leave `STUDIO_EXPOSE` unset), run `studio-expose` in a container shell:
|
||||
|
||||
```bash
|
||||
studio-expose # bridges $STUDIO_PORT (default 8765); --help for details
|
||||
```
|
||||
|
||||
> **Security:** the bridge intentionally exposes Studio beyond loopback;
|
||||
> its tokenized URL is the only auth. Keep the host-side publish on
|
||||
> `127.0.0.1:` and use `ssh -L` for remote access. Default is **off**.
|
||||
|
||||
### Remote host (SSH / mosh)
|
||||
|
||||
When the Docker host is remote, keep Studio on localhost and forward the
|
||||
port from your laptop:
|
||||
|
||||
```bash
|
||||
ssh -L 8765:127.0.0.1:8765 user@docker-host # then open the token URL locally
|
||||
```
|
||||
|
||||
**mosh cannot forward ports** (no `-L`/`-R` equivalent). To use Studio
|
||||
over a mosh session, run a *separate* `ssh -L 8765:127.0.0.1:8765 host`
|
||||
tunnel alongside mosh (mosh for the shell, ssh for the port), or reach the
|
||||
host's published port directly over a trusted network (LAN / Tailscale /
|
||||
WireGuard).
|
||||
|
||||
> PDF export (`/studio-pdf`, `studio_export_pdf`) needs a LaTeX engine,
|
||||
> which is **not** in `-studio` (only the planned `-studio-tex`). HTML
|
||||
> export, KaTeX, Mermaid, and all REPL features work without it.
|
||||
|
||||
## docker-compose.yml — basic shape
|
||||
|
||||
```yaml
|
||||
@@ -152,37 +236,45 @@ services:
|
||||
container_name: pi-devbox
|
||||
stdin_open: true
|
||||
tty: true
|
||||
env_file: .env
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- TZ=${TZ:-Europe/Stockholm}
|
||||
- TERM=xterm-256color
|
||||
- AWS_PROFILE=${AWS_PROFILE:-}
|
||||
- AWS_REGION=${AWS_REGION:-eu-west-1}
|
||||
- GITEA_ACCESS_TOKEN=${GITEA_ACCESS_TOKEN:-}
|
||||
- GITEA_HOST=${GITEA_HOST:-}
|
||||
- GITHUB_PERSONAL_ACCESS_TOKEN=${GITHUB_PERSONAL_ACCESS_TOKEN:-}
|
||||
volumes:
|
||||
# Workspace: your host source tree, read-write
|
||||
- ${HOST_WORKSPACE:-./workspace}:/workspace:rw
|
||||
# Workspace: your host source tree
|
||||
- ${WORKSPACE_PATH:-.}:/workspace
|
||||
# SSH keys: read-only from host
|
||||
- ${HOME}/.ssh:/home/developer/.ssh:ro
|
||||
# AWS config: read-only from host
|
||||
- ${HOME}/.aws:/home/developer/.aws:ro
|
||||
# MemPalace: bind-mounted so host pi and container pi share a brain
|
||||
- ${HOME}/.mempalace:/home/developer/.mempalace:rw
|
||||
- ${SSH_KEY_PATH:-~/.ssh}:/home/developer/.ssh:ro
|
||||
# Per-container persistent state
|
||||
- devbox-pi-config:/home/developer/.pi
|
||||
- devbox-bash-history:/home/developer/.cache/bash
|
||||
- devbox-ssh-local:/home/developer/.ssh-local
|
||||
- devbox-shell-history:/home/developer/.cache/bash
|
||||
- devbox-zoxide:/home/developer/.local/share/zoxide
|
||||
- devbox-nvim-data:/home/developer/.local/share/nvim
|
||||
- devbox-uv-tools:/opt/uv-tools
|
||||
- devbox-chroma-cache:/home/developer/.cache/chroma
|
||||
- devbox-uv:/home/developer/.local/share/uv
|
||||
# Optional (uncomment to enable):
|
||||
# - ~/.aws:/home/developer/.aws # AWS creds
|
||||
# - devbox-palace:/home/developer/.mempalace # persist palace
|
||||
# - devbox-chroma-cache:/home/developer/.cache/chroma # embedding cache
|
||||
|
||||
volumes:
|
||||
devbox-pi-config:
|
||||
devbox-bash-history:
|
||||
devbox-ssh-local:
|
||||
devbox-shell-history:
|
||||
devbox-zoxide:
|
||||
devbox-nvim-data:
|
||||
devbox-uv-tools:
|
||||
devbox-chroma-cache:
|
||||
devbox-uv:
|
||||
# devbox-palace:
|
||||
# devbox-chroma-cache:
|
||||
```
|
||||
|
||||
See `.env.example` in the repo for available environment variables.
|
||||
See `docker-compose.yml` and `.env.example` in the repo for the full
|
||||
template (build-from-source args, LAN-jump and skillset mounts, MemPalace
|
||||
persistence). To share one palace between host pi and the container,
|
||||
bind-mount your host `~/.mempalace` to `/home/developer/.mempalace`.
|
||||
|
||||
## uv-driven REPL recipes
|
||||
|
||||
@@ -238,15 +330,16 @@ to refresh.
|
||||
|
||||
| Path inside container | Volume | What survives |
|
||||
|---|---|---|
|
||||
| `/workspace` | host bind-mount | host filesystem |
|
||||
| `~/.ssh` | host bind-mount (read-only) | host filesystem |
|
||||
| `~/.aws` | host bind-mount (read-only) | host filesystem |
|
||||
| `~/.mempalace` | host bind-mount | host filesystem |
|
||||
| `/workspace` | host bind-mount (`WORKSPACE_PATH`) | host filesystem |
|
||||
| `~/.ssh` | host bind-mount (read-only, `SSH_KEY_PATH`) | host filesystem |
|
||||
| `~/.pi` | named volume `devbox-pi-config` | `down -v` wipes |
|
||||
| `~/.cache/bash` | named volume | `down -v` wipes |
|
||||
| `~/.local/share/nvim` | named volume | `down -v` wipes |
|
||||
| `/opt/uv-tools` | named volume | `down -v` wipes |
|
||||
| `~/.cache/chroma` | named volume | `down -v` wipes |
|
||||
| `~/.ssh-local` | named volume `devbox-ssh-local` | `down -v` wipes |
|
||||
| `~/.cache/bash` | named volume `devbox-shell-history` | `down -v` wipes |
|
||||
| `~/.local/share/zoxide` | named volume `devbox-zoxide` | `down -v` wipes |
|
||||
| `~/.local/share/nvim` | named volume `devbox-nvim-data` | `down -v` wipes |
|
||||
| `~/.local/share/uv` | named volume `devbox-uv` | `down -v` wipes |
|
||||
| `~/.mempalace` | host bind-mount or `devbox-palace` (optional) | host / volume |
|
||||
| `~/.cache/chroma` | `devbox-chroma-cache` (optional) | `down -v` wipes |
|
||||
|
||||
Anything not on a volume is on the writable layer and is lost on
|
||||
container recreate.
|
||||
@@ -302,9 +395,9 @@ set -g pane-base-index 0
|
||||
```
|
||||
|
||||
This is the default tmux indexing. It's baked here because `pi-studio`
|
||||
(planned for `:latest-studio`) hard-codes its tmux send target to
|
||||
`<session>:0.0`. If you override `base-index` to 1 in a personal
|
||||
`~/.tmux.conf`, pi-studio will fail with "can't find window: 0".
|
||||
(shipped in the `:latest-studio` variant) hard-codes its tmux send
|
||||
target to `<session>:0.0`. If you override `base-index` to 1 in a
|
||||
personal `~/.tmux.conf`, pi-studio will fail with "can't find window: 0".
|
||||
|
||||
## AWS Bedrock auth
|
||||
|
||||
@@ -327,8 +420,11 @@ pi-devbox is built from this repo's CI in two phases:
|
||||
where `<hash>` is content-addressed over `Dockerfile.base`,
|
||||
`rootfs/`, and `entrypoint*.sh`. Rebuilt only when these change.
|
||||
2. **Variant** (`Dockerfile.variant`) — `FROM ${BASE_IMAGE}` and adds
|
||||
the pi install. The `:latest` and `vX.Y.Z` tags are produced from
|
||||
this layer; future Studio variants will extend further.
|
||||
the pi install (+ pi-studio when `INSTALL_STUDIO=true`). The `:latest`
|
||||
/ `vX.Y.Z` and `:latest-studio` / `vX.Y.Z-studio` tags are produced
|
||||
from this layer. The studio variant builds via independent
|
||||
`smoke-studio` + `build-variant-studio` CI jobs that gate only the
|
||||
`-studio` tags.
|
||||
|
||||
Tag naming:
|
||||
|
||||
@@ -337,6 +433,7 @@ Tag naming:
|
||||
| `base-<hash>` | base image — internal building block |
|
||||
| `base-latest` | promoted alias of the most recent base |
|
||||
| `latest`, `vX.Y.Z` | variant: base + pi |
|
||||
| `latest-studio`, `vX.Y.Z-studio` | variant: base + pi + pi-studio |
|
||||
|
||||
CI resolves `PI_VERSION` to a concrete version string before building
|
||||
to defeat a registry-buildcache hit on `npm install -g
|
||||
|
||||
+32
-10
@@ -99,16 +99,19 @@ if command -v pi &>/dev/null; then
|
||||
"$HOME/.pi/agent/extensions/mempalace.ts"
|
||||
fi
|
||||
|
||||
# pi-fork (fork tool) + pi-observational-memory (recall tool).
|
||||
# These are pi packages (not symlink-style extensions): they're cloned to
|
||||
# /opt with node_modules baked at BUILD time, then registered here via
|
||||
# `pi install <local-path>`. A local-path install is instant + in-place
|
||||
# (pi loads the extension directly from /opt) + idempotent (no duplicate
|
||||
# package entry on re-run), and stores a relative path that resolves into
|
||||
# the image-layer /opt so it survives volume recreate. The fork/recall
|
||||
# tools register on the NEXT pi start (extensions bind at startup). Guard
|
||||
# on settings.json so we only install once per volume.
|
||||
for _pkg in /opt/pi-fork /opt/pi-observational-memory; do
|
||||
# pi-fork (fork tool) + pi-observational-memory (recall tool) + (in the
|
||||
# :latest-studio variant only) pi-studio (/studio command + studio_*
|
||||
# tools + theme). These are pi packages (not symlink-style extensions):
|
||||
# they're cloned to /opt with node_modules baked at BUILD time, then
|
||||
# registered here via `pi install <local-path>`. A local-path install is
|
||||
# instant + in-place (pi loads the extension directly from /opt) +
|
||||
# idempotent (no duplicate package entry on re-run), and stores a relative
|
||||
# path that resolves into the image-layer /opt so it survives volume
|
||||
# recreate. The tools/command register on the NEXT pi start (extensions
|
||||
# bind at startup). Guard on settings.json so we only install once per
|
||||
# volume. /opt/pi-studio is present only in the studio variant; the
|
||||
# `[ -d ]` test makes this a no-op everywhere else.
|
||||
for _pkg in /opt/pi-fork /opt/pi-observational-memory /opt/pi-studio; do
|
||||
[ -d "$_pkg" ] || continue
|
||||
_name=$(basename "$_pkg")
|
||||
if ! grep -q "$_name" "$HOME/.pi/agent/settings.json" 2>/dev/null; then
|
||||
@@ -118,6 +121,25 @@ if command -v pi &>/dev/null; then
|
||||
done
|
||||
fi
|
||||
|
||||
# ── pi-studio: optional loopback bridge (opt-in) ──────────────────────
|
||||
# pi-studio binds its server to 127.0.0.1 inside the container, which a
|
||||
# published Docker port cannot reach. When STUDIO_EXPOSE is truthy (set in
|
||||
# compose), start the `studio-expose` socat bridge in the background so a
|
||||
# published port + `ssh -L` tunnel can reach Studio once the user runs
|
||||
# `/studio --port "$STUDIO_PORT"`. Default OFF — Studio stays loopback-only
|
||||
# (its secure default) unless explicitly opted in. Guarded on the studio
|
||||
# variant (/opt/pi-studio) so it is a no-op in the plain image.
|
||||
case "${STUDIO_EXPOSE:-}" in
|
||||
1|true|TRUE|yes|on)
|
||||
if [ -d /opt/pi-studio ] && command -v studio-expose &>/dev/null && command -v socat &>/dev/null; then
|
||||
echo "STUDIO_EXPOSE set — starting studio-expose bridge on port ${STUDIO_PORT:-8765} (background)"
|
||||
nohup studio-expose "${STUDIO_PORT:-8765}" >/tmp/studio-expose.log 2>&1 &
|
||||
else
|
||||
echo "STUDIO_EXPOSE set but studio-expose/socat/pi-studio unavailable — skipping bridge"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
|
||||
# ── Skillset: deploy skills/instructions from mounted skillset repo ──
|
||||
# When the skillset repo is mounted (at $HOME/skillset or /workspace/skillset),
|
||||
# run the deploy script to create relative symlinks for skills and instructions.
|
||||
|
||||
Executable
+75
@@ -0,0 +1,75 @@
|
||||
#!/usr/bin/env bash
|
||||
# studio-expose — make a container-loopback pi-studio server reachable
|
||||
# through a published Docker port.
|
||||
#
|
||||
# WHY THIS EXISTS
|
||||
# pi-studio hard-binds its HTTP/WebSocket server to 127.0.0.1 inside the
|
||||
# container (index.ts: `.listen(port, "127.0.0.1")`) and there is no
|
||||
# --host / bind flag. A plain `docker run -p 8765:8765` forwards to the
|
||||
# container's EXTERNAL interface (eth0), not its loopback, so it cannot
|
||||
# reach Studio. This helper runs a socat TCP relay that listens on the
|
||||
# container's egress IP and forwards to 127.0.0.1:<port>, so a published
|
||||
# port (and an `ssh -L` tunnel from your laptop) can reach Studio.
|
||||
#
|
||||
# SECURITY
|
||||
# This intentionally exposes Studio beyond loopback — anything that can
|
||||
# reach the container's network interface (and the host port you publish)
|
||||
# can connect. Studio's tokenized URL is the only auth. Mitigate by
|
||||
# publishing the host port on localhost only:
|
||||
# ports: ["127.0.0.1:${STUDIO_PORT}:${STUDIO_PORT}"]
|
||||
# and use `ssh -L` for remote access. Bridge nothing you don't intend to.
|
||||
#
|
||||
# USAGE
|
||||
# studio-expose [PORT] # bridge PORT (default: $STUDIO_PORT or 8765)
|
||||
# studio-expose --help
|
||||
#
|
||||
# Typically: inside a pi session run `/studio --no-browser --port 8765`,
|
||||
# then in a container shell run `studio-expose` (or set STUDIO_EXPOSE=1 in
|
||||
# compose to auto-start it on container boot — see entrypoint-user.sh).
|
||||
#
|
||||
# Runs in the foreground; Ctrl-C to stop. The entrypoint auto-start path
|
||||
# runs it backgrounded.
|
||||
set -euo pipefail
|
||||
|
||||
PORT="${1:-${STUDIO_PORT:-8765}}"
|
||||
|
||||
if [ "$PORT" = "--help" ] || [ "$PORT" = "-h" ]; then
|
||||
sed -n '2,31p' "$0" | sed 's/^# \{0,1\}//'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
case "$PORT" in
|
||||
''|*[!0-9]*) echo "studio-expose: invalid port '$PORT'" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
if ! command -v socat >/dev/null 2>&1; then
|
||||
echo "studio-expose: socat not found in PATH" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Container's primary egress IPv4. In Docker the container hostname resolves
|
||||
# to its eth0 address, so `hostname -I` lists it; we take the first
|
||||
# non-loopback IPv4. We must bind this specific address rather than 0.0.0.0
|
||||
# — binding 0.0.0.0 would collide with Studio's own 127.0.0.1:PORT listener
|
||||
# (0.0.0.0 includes loopback) and fail with EADDRINUSE. `ip route get` is a
|
||||
# fallback only when iproute2 happens to be present (not in the base image).
|
||||
BIND_IP="$(hostname -I 2>/dev/null | tr ' ' '\n' \
|
||||
| grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$' | grep -vE '^127\.' | head -n1)"
|
||||
if [ -z "${BIND_IP:-}" ] && command -v ip >/dev/null 2>&1; then
|
||||
BIND_IP="$(ip -4 route get 1.1.1.1 2>/dev/null | awk '{for(i=1;i<=NF;i++) if($i=="src"){print $(i+1); exit}}')"
|
||||
fi
|
||||
[ -n "${BIND_IP:-}" ] || BIND_IP="$(hostname -i 2>/dev/null | awk '{print $1}')"
|
||||
if [ -z "${BIND_IP:-}" ]; then
|
||||
echo "studio-expose: could not determine container egress IP" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "studio-expose: bridging ${BIND_IP}:${PORT} -> 127.0.0.1:${PORT}"
|
||||
echo "studio-expose: open the tokenized URL pi-studio printed; if the host"
|
||||
echo "studio-expose: publishes ${PORT}, reach it at http://127.0.0.1:${PORT}/?token=..."
|
||||
echo "studio-expose: (remote host: ssh -L ${PORT}:127.0.0.1:${PORT} user@host)"
|
||||
|
||||
# fork: one child per connection (handles concurrent + long-lived WebSocket
|
||||
# connections). reuseaddr: survive quick restarts. Studio need not be up yet
|
||||
# — connections simply fail until `/studio --port ${PORT}` is running.
|
||||
exec socat "TCP-LISTEN:${PORT},bind=${BIND_IP},fork,reuseaddr" "TCP:127.0.0.1:${PORT}"
|
||||
@@ -15,6 +15,8 @@
|
||||
# - mempalace bridge symlink present
|
||||
# - settings.json bootstrapped
|
||||
# - pi-fork + pi-observational-memory registered via `pi install`
|
||||
# - (studio variant only, auto-detected) pi-studio cloned + prebuilt
|
||||
# client bundle present + registered via `pi install`
|
||||
# - image size within threshold
|
||||
|
||||
set -euo pipefail
|
||||
@@ -76,6 +78,8 @@ run "graphviz (dot)" "dot -V"
|
||||
run "imagemagick" "magick --version"
|
||||
run "yq" "yq --version"
|
||||
run "tldr (tealdeer)" "tldr --version"
|
||||
run "socat" "socat -V"
|
||||
run "studio-expose helper" "test -x /usr/local/bin/studio-expose"
|
||||
|
||||
# ── tmux 0-indexing (required for pi-studio variants) ─────────────────
|
||||
echo ""
|
||||
@@ -95,6 +99,20 @@ run "pi-fork clone + node_modules" \
|
||||
run "pi-observational-memory clone + node_modules" \
|
||||
"test -f /opt/pi-observational-memory/package.json && test -d /opt/pi-observational-memory/node_modules"
|
||||
|
||||
# pi-studio is present only in the :latest-studio variant. Auto-detect by
|
||||
# probing /opt/pi-studio so this one script covers both variants.
|
||||
if docker run --rm --entrypoint="" "$IMAGE" sh -c 'test -d /opt/pi-studio' >/dev/null 2>&1; then
|
||||
STUDIO_VARIANT=1
|
||||
echo " ℹ️ pi-studio detected — running studio assertions"
|
||||
run "pi-studio clone + node_modules" \
|
||||
"test -f /opt/pi-studio/package.json && test -d /opt/pi-studio/node_modules"
|
||||
run "pi-studio prebuilt client bundle" \
|
||||
"test -f /opt/pi-studio/client/studio-client.js"
|
||||
else
|
||||
STUDIO_VARIANT=0
|
||||
echo " ℹ️ pi-studio not present (non-studio variant) — skipping studio clone checks"
|
||||
fi
|
||||
|
||||
# ── Runtime deployment (needs entrypoint to run) ──────────────────────
|
||||
echo ""
|
||||
echo "── Runtime deployment ──"
|
||||
@@ -150,6 +168,20 @@ done
|
||||
exec_test "pi-fork registered (fork tool)" 'grep -q pi-fork $HOME/.pi/agent/settings.json && echo ok'
|
||||
exec_test "pi-observational-memory registered (recall tool)" 'grep -q pi-observational-memory $HOME/.pi/agent/settings.json && echo ok'
|
||||
|
||||
# pi-studio registration (studio variant only) — registered by the same
|
||||
# entrypoint-user.sh local-path install loop as fork/obsmem.
|
||||
if [ "${STUDIO_VARIANT:-0}" = "1" ]; then
|
||||
for i in $(seq 1 15); do
|
||||
if docker exec "$CID" grep -q pi-studio \
|
||||
/home/developer/.pi/agent/settings.json 2>/dev/null; then
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
exec_test "pi-studio registered (/studio command + studio_* tools)" \
|
||||
'grep -q pi-studio $HOME/.pi/agent/settings.json && echo ok'
|
||||
fi
|
||||
|
||||
# ── /tmp/sshcm directory created by entrypoint ────────────────────────
|
||||
exec_test "/tmp/sshcm dir mode 700 (ssh ControlMaster)" \
|
||||
'test -d /tmp/sshcm && [ "$(stat -c %a /tmp/sshcm)" = "700" ] && echo ok'
|
||||
|
||||
Reference in New Issue
Block a user