1c4239e9b0
Functional (not verbatim) port of the build-provenance, CI-hardening, SSH and shell fixes from the sibling pi-devbox repo, adapted to opencode-devbox's companions and two-variant (base/omos) shape. Defaults unchanged → canonical CI build stays byte-identical apart from the opencode bump and the (cache-free) provenance layer. Fixed: - SSH read-only ~/.ssh ControlPath: setup-lan-access.sh now renders the writable ~/.ssh-local/config sidecar (ControlPath redirect + Include) on EVERY host OS instead of exit 0-ing on native Linux; jump-specific blocks gated behind new NEED_JUMP flag. dssh/dscp + ControlMaster now survive a read-only ~/.ssh on native-Linux hosts. (pi-devbox v1.1.5) - bash history loss in nested/tmux shells: DEVBOX_HIST_SET no longer exported so each shell re-installs its own history -a flush. (pi-devbox v1.1.4) Added: - build provenance: OCI labels + /etc/opencode-devbox/build-manifest.json written from ground truth (opencode --version, installed omos version, /opt/mempalace-toolkit HEAD); wired into build-variant-* and smoke-* jobs; smoke-test.sh asserts manifest + label. (pi-devbox v1.1.6) - scripts/check-base-hash.sh CI guard: fails if a Dockerfile.base ARG *_REF is not folded into the base_tag hash. (pi-devbox v1.1.6) - overridable MEMPALACE_TOOLKIT_REPO build-arg in Dockerfile.base. (v1.1.6) Changed: - resolve-versions: fail-loud validation (SHA / semver) that aborts the release instead of silently falling back to floating main; adds shell: bash (set -o pipefail is illegal under the runner default dash). (pi-devbox v1.1.6) Bumped: - opencode-ai 1.17.7 → 1.17.8 (current npm latest stable). Deferred (needs a decision): opencode.json merge-on-recreate — see CHANGELOG.
236 lines
11 KiB
Bash
236 lines
11 KiB
Bash
#!/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 + writable SSH sidecar: host-OS-agnostic helper ──────
|
|
# Generates the writable ~/.ssh-local/config on EVERY host OS: a `Host *`
|
|
# ControlPath redirect into ~/.ssh-local/cm (so `ssh -F` / dssh / dscp work
|
|
# even when ~/.ssh is bind-mounted read-only) plus `Include ~/.ssh/config`. On
|
|
# VM-backed hosts (macOS OrbStack / Docker Desktop) it ALSO adds an
|
|
# SSH-jump-via-host block so the container can reach the host's
|
|
# directly-attached LAN peers; on native Linux (LAN reachable directly) the
|
|
# jump block is omitted but the sidecar is still rendered. Controlled by
|
|
# DEVBOX_LAN_ACCESS (auto|jump|off) + HOST_SSH_USER. Always non-fatal. See the
|
|
# script header.
|
|
if [ -r /usr/local/lib/opencode-devbox/setup-lan-access.sh ]; then
|
|
bash /usr/local/lib/opencode-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
|
|
|
|
# ── v2.0.0 migration: relocate npm global prefix off the legacy ~/.pi path ──
|
|
# Pre-v2.0.0 images set NPM_CONFIG_PREFIX=~/.pi/npm-global (a pi-specific
|
|
# path). v2.0.0 removed pi and moved the prefix to
|
|
# ~/.config/opencode/npm-global, which is a persistent named volume in every
|
|
# compose layout (the old ~/.pi volume was only in docker-compose.yml). If a
|
|
# user upgraded with the old ~/.pi volume still mounted, copy their
|
|
# previously globally-installed npm packages to the new prefix once so they
|
|
# remain on PATH. The marker keeps this idempotent and a no-op for fresh
|
|
# installs; the whole block is harmless when the old path is absent.
|
|
NEW_NPM_PREFIX="$HOME/.config/opencode/npm-global"
|
|
OLD_NPM_PREFIX="$HOME/.pi/npm-global"
|
|
MIGRATION_MARKER="$NEW_NPM_PREFIX/.migrated-from-dot-pi"
|
|
if [ -d "$OLD_NPM_PREFIX" ] && [ ! -f "$MIGRATION_MARKER" ]; then
|
|
echo "Migrating npm global prefix: ~/.pi/npm-global -> ~/.config/opencode/npm-global"
|
|
mkdir -p "$NEW_NPM_PREFIX"
|
|
# cp -n: never overwrite a file already in the new prefix (a freshly
|
|
# installed package wins over the legacy copy).
|
|
for sub in lib bin share; do
|
|
if [ -d "$OLD_NPM_PREFIX/$sub" ]; then
|
|
mkdir -p "$NEW_NPM_PREFIX/$sub"
|
|
cp -an "$OLD_NPM_PREFIX/$sub/." "$NEW_NPM_PREFIX/$sub/" 2>/dev/null || true
|
|
fi
|
|
done
|
|
touch "$MIGRATION_MARKER" 2>/dev/null || true
|
|
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 — the container entrypoint has no usable stdin for
|
|
# prompts anyway.
|
|
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
|
|
|
|
# ── Generate opencode config from env vars if no config mounted ──────
|
|
# Delegated to a standalone Python script for clarity and testability.
|
|
# The script is idempotent: it never overwrites an existing opencode.json
|
|
# (bind-mounted from host, persisted in named volume, or previously
|
|
# generated) and no-ops if OPENCODE_PROVIDER is unset.
|
|
python3 /usr/local/lib/opencode-devbox/generate-config.py
|
|
|
|
# ── 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
|
|
|
|
# ── OMOS bundled skills: symlink from the image into the flat skills dir ──
|
|
# The oh-my-opencode-slim package bundles its skills at a fixed, image-internal
|
|
# path (npm global prefix /usr — see Dockerfile.variant). Historically the omos
|
|
# *installer* COPIED them into ~/.config/opencode/skills/ on first run only,
|
|
# freezing them in the persistent `devbox-opencode-config` named volume: pulling
|
|
# a newer image never refreshed them, and the only update path was
|
|
# `OMOS_RESET=true` (which also clobbers the user's hand-tuned opencode config).
|
|
#
|
|
# Instead we symlink them from the IMAGE into ~/.agents/skills/ — the same flat
|
|
# dir skillset uses, which opencode scans (directly and via the ~/.claude/skills
|
|
# pointer). Because the link targets live in the image, `docker compose pull` +
|
|
# recreate updates the skills for free: no installer run, no config reset.
|
|
#
|
|
# ~/.agents/skills/ is authoritative. The legacy ~/.config/opencode/skills/ real
|
|
# dir is intentionally bypassed; a one-time migration backs up (never destroys)
|
|
# the frozen copies it holds so they stop shadowing the fresh image-sourced
|
|
# skills. The migration marker lives in the parent config dir, not inside
|
|
# skills/, so it never interferes with that directory's contents.
|
|
#
|
|
# Absolute symlink (not relative like skillset): the target is always inside the
|
|
# container at a fixed /usr path, and ~/.agents/skills/ is an ephemeral
|
|
# container-layer dir rebuilt each start — there is no host/container path
|
|
# divergence to guard against. The whole block is non-fatal (`{ … } || true`):
|
|
# a transient ln/mv failure must never brick container startup, mirroring the
|
|
# skillset deploy above. Runs AFTER skillset deploy so OMOS wins any name
|
|
# collision (e.g. `simplify`) via `ln -sfn`. Gated by OMOS_SKILLS (default true)
|
|
# and the presence of the bundled skills (omos-variant images only).
|
|
if [ "${OMOS_SKILLS:-true}" = "true" ]; then
|
|
OMOS_SKILLS_SRC=""
|
|
for cand in \
|
|
/usr/lib/node_modules/oh-my-opencode-slim/src/skills \
|
|
/usr/local/lib/node_modules/oh-my-opencode-slim/src/skills; do
|
|
if [ -d "$cand" ]; then OMOS_SKILLS_SRC="$cand"; break; fi
|
|
done
|
|
if [ -n "$OMOS_SKILLS_SRC" ]; then
|
|
{
|
|
AGENTS_SKILLS_DIR="$HOME/.agents/skills"
|
|
OPENCODE_SKILLS_DIR="$HOME/.config/opencode/skills"
|
|
OMOS_SKILLS_MARKER="$HOME/.config/opencode/.omos-skills-migrated"
|
|
mkdir -p "$AGENTS_SKILLS_DIR"
|
|
for skill_path in "$OMOS_SKILLS_SRC"/*/; do
|
|
[ -d "$skill_path" ] || continue
|
|
name="$(basename "$skill_path")"
|
|
# OMOS wins collisions: -f replaces an existing symlink (e.g. skillset's).
|
|
ln -sfn "${skill_path%/}" "$AGENTS_SKILLS_DIR/$name"
|
|
# One-time unshadow: back up — never destroy — the frozen real copy the
|
|
# old installer left in the persistent config volume. `! -L` ensures we
|
|
# only ever touch a real dir, never a symlink a user/skillset created.
|
|
if [ ! -f "$OMOS_SKILLS_MARKER" ] \
|
|
&& [ -d "$OPENCODE_SKILLS_DIR/$name" ] \
|
|
&& [ ! -L "$OPENCODE_SKILLS_DIR/$name" ]; then
|
|
mv "${OPENCODE_SKILLS_DIR:?}/$name" \
|
|
"${OPENCODE_SKILLS_DIR}/${name}.bak.$(date +%s)"
|
|
fi
|
|
done
|
|
touch "$OMOS_SKILLS_MARKER" 2>/dev/null || true
|
|
} || true
|
|
fi
|
|
fi
|
|
|
|
CONFIG_DIR="$HOME/.config/opencode"
|
|
OMOS_CONFIG="$CONFIG_DIR/oh-my-opencode-slim.json"
|
|
|
|
# ── oh-my-opencode-slim setup (multi-agent orchestration) ────────────
|
|
# Activated by ENABLE_OMOS=true. Requires the image to be built with
|
|
# INSTALL_OMOS=true (which installs bun + the oh-my-opencode-slim package).
|
|
OMOS_CONFIG="$CONFIG_DIR/oh-my-opencode-slim.json"
|
|
|
|
if [ "${ENABLE_OMOS:-false}" = "true" ]; then
|
|
if ! command -v bun &>/dev/null; then
|
|
echo "WARNING: ENABLE_OMOS=true but bun is not installed."
|
|
echo "Rebuild with: docker compose build --build-arg INSTALL_OMOS=true"
|
|
elif [ ! -f "$OMOS_CONFIG" ]; then
|
|
echo "Setting up oh-my-opencode-slim agents..."
|
|
|
|
# Determine installer flags
|
|
OMOS_TMUX_FLAG="no"
|
|
if [ "${OMOS_TMUX:-false}" = "true" ]; then
|
|
OMOS_TMUX_FLAG="yes"
|
|
fi
|
|
|
|
# Skills are NOT installer-managed any more — they are symlinked from the
|
|
# image into ~/.agents/skills/ by the OMOS bundled-skills block above
|
|
# (gated by OMOS_SKILLS). Always pass --skills=no so the installer never
|
|
# writes frozen copies into the persistent config volume.
|
|
bun x oh-my-opencode-slim@latest install \
|
|
--no-tui \
|
|
--tmux="${OMOS_TMUX_FLAG}" \
|
|
--skills=no
|
|
|
|
echo "oh-my-opencode-slim configured successfully."
|
|
else
|
|
echo "oh-my-opencode-slim config found at $OMOS_CONFIG (use OMOS_RESET=true to overwrite)."
|
|
|
|
# Allow reset via env var (creates backup automatically)
|
|
if [ "${OMOS_RESET:-false}" = "true" ]; then
|
|
echo "OMOS_RESET=true — regenerating oh-my-opencode-slim config..."
|
|
OMOS_TMUX_FLAG="no"
|
|
[ "${OMOS_TMUX:-false}" = "true" ] && OMOS_TMUX_FLAG="yes"
|
|
|
|
bun x oh-my-opencode-slim@latest install \
|
|
--no-tui \
|
|
--tmux="${OMOS_TMUX_FLAG}" \
|
|
--skills=no \
|
|
--reset
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# ── Execute command ──────────────────────────────────────────────────
|
|
exec "$@"
|