7d8ee4cea1
pi-studio binds the container's 127.0.0.1, which a published Docker port can't reach. Add a robust, portable bridge rather than a doc-only one-liner: - Dockerfile.base: add socat (~1 MB, generally useful TCP relay). - rootfs/usr/local/bin/studio-expose: socat TCP relay listening on the container's egress IPv4 (not 0.0.0.0 — that would EADDRINUSE against Studio's loopback listener) forwarding to 127.0.0.1:PORT on the SAME port, so Studio's printed token URL works verbatim. Robust egress-IP detection (hostname -I, loopback-filtered; ip route get fallback), --help, port validation, foreground. - entrypoint-user.sh: opt-in STUDIO_EXPOSE=1 auto-starts the bridge in the background (studio variant only). Default OFF — Studio stays loopback-only (its secure default) unless explicitly opted in. - README: 'Using pi-studio' now documents host-networking (A) and the studio-expose/STUDIO_EXPOSE bridge (B) with a security note; ssh -L for remote, mosh caveat retained. - smoke-test: assert socat + studio-expose present (base-level). - CHANGELOG/AGENTS updated. No tag — stopping for review.
167 lines
8.3 KiB
Bash
Executable File
167 lines
8.3 KiB
Bash
Executable File
#!/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: mempalace init has an interactive "Mine this directory
|
|
# now? [Y/n]" prompt that --yes does not auto-answer in all paths.
|
|
# Without redirected stdin, the process blocks here forever when run
|
|
# from `docker run -it` (the TTY keeps stdin open). EOF on stdin
|
|
# makes the prompt fall through to its default (skip).
|
|
mempalace init --yes /workspace </dev/null >/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 <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
|
|
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 "$@"
|