Files
opencode-devbox/entrypoint.sh
joakimp f51e9f52a1 Add INSTALL_PI build arg for pi as second harness
Optional integration of pi-coding-agent alongside opencode in the same
container. Both harnesses share the mempalace install and palace path —
wing/diary entries are mutually visible.

Build:
  --build-arg INSTALL_PI=true              # opt-in
  --build-arg PI_VERSION=0.73.1            # pin a version (default: latest)
  --build-arg INSTALL_OPENCODE=false       # build pi-only image

Dockerfile:
  • New INSTALL_PI block: npm install -g @mariozechner/pi-coding-agent
    + git-clones pi-toolkit and pi-extensions to /opt/.
  • Existing opencode install gated behind new INSTALL_OPENCODE arg
    (default true; existing builds unaffected).
  • mkdir adds ~/.pi/agent/extensions for the named volume mount root.
  • CMD changed from ['opencode'] to ['bash', '-l']. compose run --rm
    devbox now drops to a login shell so users pick the harness; pass
    'opencode' or 'pi' explicitly to launch directly. compose exec
    workflows are unaffected (bypass entrypoint+CMD).

entrypoint.sh:
  • Adds ~/.pi to volume ownership loop.

entrypoint-user.sh:
  • New 'pi: deploy toolkit + extensions + mempalace bridge' block runs
    pi-toolkit/install.sh, pi-extensions/install.sh, settings.json
    template bootstrap, then symlinks the mempalace.ts bridge directly.
    Order: toolkit before extensions before bridge. mempalace-toolkit's
    full install.sh is intentionally NOT called (its install_skill
    would race with skillset auto-deploy --prune-stale).

docker-compose.yml:
  • New devbox-pi-config named volume mounted at /home/developer/.pi.
    Persists user toggles (/ext-disabled extensions) and settings.json
    edits across container recreate. Mirrors devbox-opencode-config
    pattern from v1.14.33.

scripts/smoke-test.sh:
  • New --variant with-pi (threshold 2700 MB) and --variant omos-with-pi
    (3400 MB).
  • Pi assertions gated on `command -v pi`: version, /opt/pi-toolkit
    clone HEAD, /opt/pi-extensions clone HEAD, deployed keybindings
    symlink, ≥4 extension symlinks, mempalace.ts bridge symlink,
    settings.json bootstrap.
  • Pi state assertions use docker exec from the host (not 'run'),
    since the container has no docker CLI.
  • opencode core test now gated on INSTALL_OPENCODE presence.

scripts/generate-dockerhub-md.py:
  • SECTION_RULES adds 'pi (alternative/complementary harness)': drop.
    Section stays in README; dropped from DOCKER_HUB.md to keep under
    the 25 kB Docker Hub limit.

Docs:
  • README adds full 'pi (alternative/complementary harness)' section.
  • AGENTS.md codifies pi install contract, deploy ordering, named
    volume rationale, and CMD change.
  • CHANGELOG.md gets an Unreleased entry.
  • .env.example documents new build args.
  • docker-compose.yml example args block updated.

Verification (local builds on arm64):
  • Default (INSTALL_PI=false): 1871 MB, all assertions pass — no
    regression.
  • INSTALL_PI=true: 2110 MB (within 2700 threshold), 37 assertions
    pass including pi version, all 7 extensions deployed (6 from
    pi-extensions + mempalace.ts bridge), settings.json bootstrap.

Not yet:
  • CI workflow updates to add -with-pi tag variants. Deferred until
    local path stabilizes through user testing.
  • pi-devbox separate repo for fully stripped pi-only image. Phase 2.
2026-05-07 23:58:37 +02:00

122 lines
5.6 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
USER_NAME="developer"
CURRENT_UID=$(id -u "$USER_NAME")
CURRENT_GID=$(id -g "$USER_NAME")
# ── UID/GID adjustment ───────────────────────────────────────────────
# Priority per dimension: env var > auto-detect from /workspace > no-op
# UID and GID are detected independently so a GID-only mismatch (e.g. host
# user has UID 1000 but primary group at GID 1001) is still corrected.
TARGET_UID="${USER_UID:-}"
TARGET_GID="${USER_GID:-}"
if [ -d /workspace ]; then
WORKSPACE_UID=$(stat -c '%u' /workspace 2>/dev/null || stat -f '%u' /workspace 2>/dev/null || echo "")
WORKSPACE_GID=$(stat -c '%g' /workspace 2>/dev/null || stat -f '%g' /workspace 2>/dev/null || echo "")
# Adopt workspace UID if env var not set and workspace is non-root-owned
if [ -z "$TARGET_UID" ] && [ -n "$WORKSPACE_UID" ] && [ "$WORKSPACE_UID" != "0" ] && [ "$WORKSPACE_UID" != "$CURRENT_UID" ]; then
TARGET_UID="$WORKSPACE_UID"
fi
# Adopt workspace GID if env var not set and workspace group differs
if [ -z "$TARGET_GID" ] && [ -n "$WORKSPACE_GID" ] && [ "$WORKSPACE_GID" != "0" ] && [ "$WORKSPACE_GID" != "$CURRENT_GID" ]; then
TARGET_GID="$WORKSPACE_GID"
fi
fi
# Apply UID/GID changes if needed
if [ -n "$TARGET_GID" ] && [ "$TARGET_GID" != "$CURRENT_GID" ]; then
groupmod -g "$TARGET_GID" "$USER_NAME" 2>/dev/null || true
find /home/"$USER_NAME" -not -path "/home/$USER_NAME/.ssh/*" -group "$CURRENT_GID" -exec chgrp "$TARGET_GID" {} + 2>/dev/null || true
echo "Adjusted developer GID to $TARGET_GID"
fi
if [ -n "$TARGET_UID" ] && [ "$TARGET_UID" != "$CURRENT_UID" ]; then
usermod -u "$TARGET_UID" "$USER_NAME" 2>/dev/null || true
find /home/"$USER_NAME" -not -path "/home/$USER_NAME/.ssh/*" -user "$CURRENT_UID" -exec chown "$TARGET_UID" {} + 2>/dev/null || true
echo "Adjusted developer UID to $TARGET_UID"
fi
# ── SSH key permissions ──────────────────────────────────────────────
# If SSH keys are mounted, fix permissions (skip if read-only mount)
if [ -d "/home/$USER_NAME/.ssh" ] && [ "$(ls -A "/home/$USER_NAME/.ssh" 2>/dev/null)" ]; then
if touch "/home/$USER_NAME/.ssh/.perm_test" 2>/dev/null; then
rm -f "/home/$USER_NAME/.ssh/.perm_test"
chmod 700 "/home/$USER_NAME/.ssh"
find "/home/$USER_NAME/.ssh" -type f -name "id_*" ! -name "*.pub" -exec chmod 600 {} \; 2>/dev/null || true
find "/home/$USER_NAME/.ssh" -type f -name "*.pub" -exec chmod 644 {} \; 2>/dev/null || true
[ -f "/home/$USER_NAME/.ssh/known_hosts" ] && chmod 644 "/home/$USER_NAME/.ssh/known_hosts"
[ -f "/home/$USER_NAME/.ssh/config" ] && chmod 600 "/home/$USER_NAME/.ssh/config"
fi
fi
# ── Fix ownership of named volume mount points ──────────────────────
# Named volumes are created as root on first use. Fix ownership so the
# developer user can write to them.
FINAL_UID="${TARGET_UID:-$CURRENT_UID}"
FINAL_GID="${TARGET_GID:-$CURRENT_GID}"
# First, fix parent dirs that Docker auto-creates as root:root when it
# materializes nested mount points (e.g. mounting a volume at
# .local/state/opencode creates .local/state as root). Non-recursive —
# we only need the dir node itself; children are handled below or were
# created by the user.
for parent in \
/home/"$USER_NAME"/.local \
/home/"$USER_NAME"/.local/share \
/home/"$USER_NAME"/.local/state \
/home/"$USER_NAME"/.cache \
/home/"$USER_NAME"/.config; do
if [ -d "$parent" ] && [ "$(stat -c '%u' "$parent" 2>/dev/null)" != "$FINAL_UID" ]; then
chown "$FINAL_UID":"$FINAL_GID" "$parent" 2>/dev/null || true
fi
done
for dir in \
/home/"$USER_NAME"/.local/share/opencode \
/home/"$USER_NAME"/.local/state/opencode \
/home/"$USER_NAME"/.local/share/uv \
/home/"$USER_NAME"/.local/share/zoxide \
/home/"$USER_NAME"/.local/share/nvim \
/home/"$USER_NAME"/.mempalace \
/home/"$USER_NAME"/.cache/bash \
/home/"$USER_NAME"/.cache/chroma \
/home/"$USER_NAME"/.rustup \
/home/"$USER_NAME"/.cargo \
/home/"$USER_NAME"/.vscode-server \
/home/"$USER_NAME"/.config/opencode \
/home/"$USER_NAME"/.config/nvim \
/home/"$USER_NAME"/.pi \
/home/"$USER_NAME"/.agents/skills; do
[ -d "$dir" ] || continue
# Sentinel-file fast path: on volumes with thousands of files (nvim
# plugins, palace data) the recursive chown used to cost multiple
# seconds on every container start even when ownership was already
# correct. Now we write a sentinel after a successful chown and skip
# the walk when the sentinel matches the target UID:GID.
#
# If USER_UID changes between runs (user switches hosts, different
# workspace owner), the sentinel won't match and the full chown runs.
sentinel="$dir/.devbox-owner"
expected="$FINAL_UID:$FINAL_GID"
if [ -f "$sentinel" ] && [ "$(cat "$sentinel" 2>/dev/null)" = "$expected" ]; then
continue
fi
# Recursive chown needed. Only do it when the top-level differs too
# (covers the common case of fresh root-owned named volumes).
if [ "$(stat -c '%u' "$dir" 2>/dev/null)" != "$FINAL_UID" ]; then
chown -R "$FINAL_UID":"$FINAL_GID" "$dir" 2>/dev/null || true
fi
# Write sentinel so subsequent starts skip the recursive walk.
# Suppress errors — a read-only mount would fail here, but that would
# already have failed above on the chown itself.
echo "$expected" > "$sentinel" 2>/dev/null || true
done
# ── Drop to developer user for remaining setup ──────────────────────
exec gosu "$USER_NAME" /usr/local/bin/entrypoint-user.sh "$@"