#!/usr/bin/env bash set -euo pipefail # ── SSH ControlMaster socket dir ──────────────────────────────── # Companion to /etc/ssh/ssh_config.d/00-devbox-controlmaster.conf in the # base image — that file declares ControlPath=/tmp/sshcm/%r@%h:%p; this # creates the directory with the right permissions on every container # start. /tmp is per-container so the dir doesn't survive recreation; # baking it into a Dockerfile layer would be wrong. # Mode 700 is required — OpenSSH refuses to use a ControlPath dir that # others can write to. mkdir -p /tmp/sshcm chmod 700 /tmp/sshcm # ── LAN access: generic host-OS-agnostic reachability helper ──────── # On VM-backed hosts (macOS OrbStack / Docker Desktop) the container can't # reach the host's directly-attached LAN peers by default; this generates a # writable ~/.ssh-local/config that uses the host as an SSH jump. On native # Linux (LAN reachable directly) it is a no-op. Controlled by DEVBOX_LAN_ACCESS # (auto|jump|off) + HOST_SSH_USER. Always non-fatal. See the script header. if [ -r /usr/local/lib/pi-devbox/setup-lan-access.sh ]; then bash /usr/local/lib/pi-devbox/setup-lan-access.sh || true fi # ── Shell defaults: copy baked files from /etc/skel-devbox/ if absent # Respects host bind-mounts and user customizations — existing files # are never overwritten. To restore defaults: rm ~/.bash_aliases (or # .inputrc) and recreate the container, or cp from /etc/skel-devbox/ # directly. SKEL_DIR="/etc/skel-devbox" if [ -d "$SKEL_DIR" ]; then for f in .bash_aliases .inputrc; do if [ -f "$SKEL_DIR/$f" ] && [ ! -e "$HOME/$f" ]; then cp "$SKEL_DIR/$f" "$HOME/$f" fi done fi # ── MemPalace: initialize palace for the workspace if mempalace is installed # Creates the palace directory structure on first run. Idempotent — skips # if palace already exists, so upgrades from older versions preserve # existing data. `--yes` auto-accepts detected entities so the init is # non-interactive. if command -v mempalace &>/dev/null && [ -d /workspace ]; then PALACE_DIR="${HOME}/.mempalace" if [ ! -d "$PALACE_DIR/palace" ]; then echo "Initializing MemPalace for workspace (non-interactive)..." # /dev/null 2>&1 || true fi fi # ── Git config defaults ────────────────────────────────────────────── if [ -n "${GIT_USER_NAME:-}" ] && ! git config --global user.name &>/dev/null; then git config --global user.name "$GIT_USER_NAME" fi if [ -n "${GIT_USER_EMAIL:-}" ] && ! git config --global user.email &>/dev/null; then git config --global user.email "$GIT_USER_EMAIL" fi # ── pi: deploy toolkit + extensions + mempalace bridge ───────────── # pi is always installed in pi-devbox; no INSTALL_PI guard needed. # Each install.sh is idempotent and backs up real files before linking, # so re-running across container restarts is safe. # # Order: pi-toolkit first (creates ~/.pi/agent/keybindings.json symlink # and writes the AWS env loader), then pi-extensions (symlinks our # extensions), then settings.json bootstrap from the toolkit template, # then the mempalace bridge symlink (one-liner; mempalace-toolkit's # install_skill is intentionally skipped to avoid racing with skillset # auto-deploy below). if command -v pi &>/dev/null; then if [ -d /opt/pi-toolkit ]; then (cd /opt/pi-toolkit && ./install.sh --yes) || \ echo "WARN: pi-toolkit install.sh failed (continuing)" fi if [ -d /opt/pi-extensions ]; then (cd /opt/pi-extensions && ./install.sh --yes) || \ echo "WARN: pi-extensions install.sh failed (continuing)" fi # Bootstrap settings.json from template if absent (pi rewrites this # file at runtime — lastChangelogVersion, etc — so we can't symlink it). if [ ! -f "$HOME/.pi/agent/settings.json" ] && \ [ -f /opt/pi-toolkit/settings.example.json ]; then cp /opt/pi-toolkit/settings.example.json "$HOME/.pi/agent/settings.json" fi # pi↔mempalace MCP bridge — single extension symlink. if [ -f /opt/mempalace-toolkit/extensions/pi/mempalace.ts ] && \ command -v mempalace &>/dev/null && \ [ ! -L "$HOME/.pi/agent/extensions/mempalace.ts" ]; then ln -sf /opt/mempalace-toolkit/extensions/pi/mempalace.ts \ "$HOME/.pi/agent/extensions/mempalace.ts" fi # 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 `. 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 pi install "$_pkg" >/dev/null 2>&1 || \ echo "WARN: pi install $_name failed (continuing)" fi 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. # This ensures skills resolve correctly inside the container regardless of # where the repo lives on the host. Idempotent — second run is a no-op. # # Detection order: # 1. SKILLSET_CONTAINER_PATH env var (explicit, for non-standard layouts) # 2. $HOME/skillset (dedicated volume mount via SKILLSET_PATH in compose) # 3. /workspace/skillset (skillset is directly inside workspace root) SKILLSET_DEPLOY="" if [ -n "${SKILLSET_CONTAINER_PATH:-}" ] && [ -x "${SKILLSET_CONTAINER_PATH}/deploy-skills.sh" ]; then SKILLSET_DEPLOY="${SKILLSET_CONTAINER_PATH}/deploy-skills.sh" elif [ -x "$HOME/skillset/deploy-skills.sh" ]; then SKILLSET_DEPLOY="$HOME/skillset/deploy-skills.sh" elif [ -x /workspace/skillset/deploy-skills.sh ]; then SKILLSET_DEPLOY="/workspace/skillset/deploy-skills.sh" fi if [ -n "$SKILLSET_DEPLOY" ]; then "$SKILLSET_DEPLOY" --bootstrap --prune-stale >/dev/null 2>&1 || true fi # ── Execute command ────────────────────────────────────────────────── exec "$@"