Files
pi-devbox/.env.example
T
joakimp 45850bc973
Lint / hadolint (push) Successful in 8s
Lint / actionlint (push) Successful in 25s
entrypoint: put back the shell state a recreate eats
cli_utils' install.sh reaches PATH by symlinking bin/ into ~/.local/bin.
That is persistent on a host and ephemeral in a container, so the same
installer produced opposite durability and every --force-recreate sent the
human back to typing /workspace/cli_utils/bin/git-status-all. Re-link at
start instead, and add a per-device boot hook so the next question of this
shape needs no image change at all.

Symlinks rather than a PATH edit in an rc file, deliberately: ~/.local/bin
is already ahead of /usr/local/bin in ENV PATH, so links resolve in
NON-interactive shells too (docker exec, agent tool shells, scripts). An
rc-file PATH edit cannot reach those because ~/.bashrc returns early when
not interactive — measured, that asymmetry is exactly why `command -v
git-status-all` failed in one shell and worked in another on the same box.
Shell FUNCTIONS remain the sourced file's job; a symlink cannot carry them.

Guards, because ~/.local/bin is shared: a real file is never clobbered, a
symlink pointing elsewhere is never stolen, ours are refreshed, and links
into a cli_utils/bin whose target vanished are pruned — a dangling link on
PATH reads as a broken container rather than a removed script.

The hook (~/.config/devbox-shell/init.sh) adds no trust boundary: that dir
is already sourced into every interactive shell by the baked bash_aliases,
so it is already arbitrary code from the same owner. Only WHEN it runs is
new. bash <file>, never sourced, exit status ignored, output to a log.

Caught before commit, and the reason the loops use `if` bodies instead of
`&&` chains: under `set -euo pipefail` a for-loop whose last command is a
false test exits non-zero, and with no match the /workspace/*/cli_utils
glob stays literal — so the first draft would have failed to START a
container on every machine that does not have this repo, rather than merely
skipping the links. Re-tested with set -e in place: no-cli_utils/empty-HOME
no-op, guards, idempotence, CLI_UTILS_LINK=0, a hook that exits 7, and a
hook that tries to mutate CLI_UTILS_BIN — all exit 0 with intact state.

Not covered by CI: docker-publish.yml runs only on tags and lint.yml lints
workflow run: steps, so neither executes this file. Validated by extracting
both sections and running them against fixtures, then for real in a live
v1.8.10 container. No smoke assertion added on purpose — the positive path
needs a /workspace mount smoke does not have, and asserting it there would
repeat the v1.8.0 mistake of a smoke check written against a stage that
does not exist at run time.

Moves the base hash (base-decide folds `cat entrypoint.sh
entrypoint-user.sh`), so this rides along with the next tag's ~40-minute
base rebuild rather than justifying a tag of its own.
2026-08-27 21:52:51 +02:00

161 lines
9.5 KiB
Bash

# pi-devbox environment configuration
# Copy this file to .env and fill in your values:
# cp .env.example .env
# ── Workspace ────────────────────────────────────────────────────────
# Path on host to mount as /workspace in the container
WORKSPACE_PATH=~/projects
# Path to SSH keys on host
SSH_KEY_PATH=~/.ssh
# ── MemPalace memory (local by default) ───────────────────────────
# By default the mempalace.ts extension spawns a LOCAL mempalace-mcp stdio
# server (palace at ~/.mempalace). Uncomment the devbox-palace volume in
# docker-compose.yml to persist it across container recreation — that one
# volume now covers the mined conversation transcripts too, since the pi and
# opencode feeders stage inside the palace root (<palace-root>/pi-stage), so
# the staged files and the palace dedup keys pointing at them cannot be
# separated.
#
# That palace root is resolved with mempalace's own precedence
# ($MEMPALACE_PALACE_PATH -> $MEMPAL_PALACE_PATH -> ~/.mempalace/config.json ->
# ~/.mempalace/palace), and the feeders derive their stage FROM it
# (<palace-root>/pi-stage). Neither the image nor the entrypoint exports it, by
# design: pinning the palace without carrying the stage along re-creates the
# very split that a shared root removed. Override it only to move the palace off
# the default -- e.g. onto a different mount -- and only to a path with the SAME
# persistence as the palace itself. A stage that outlives its palace (or dies
# first) makes a scoped `mempalace sync` prune conversation drawers, because
# their dedup key is the staged path. Setting it to the default buys nothing.
# Unlike WORKSPACE_PATH/SSH_KEY_PATH above, this is a path INSIDE the container.
# MEMPALACE_PALACE_PATH=/home/developer/.mempalace/palace
#
# To instead share ONE MemPalace across containers/harnesses (pi + opencode
# + native), set the URL below. When set, the extension connects over HTTP
# and NO local mempalace-mcp is spawned; the devbox-palace volume is then
# irrelevant. MEMPALACE_REMOTE_TOKEN, if set, is sent as a bearer token.
#
# Serve it with: mempalace serve --host 172.17.0.1 --port 8765
#
# NOT `mempalace-mcp --transport http --host 0.0.0.0`: `serve` is the turnkey
# wrapper that mints/keeps a bearer token (0600, passed via env so it stays out
# of `ps`) and can terminate TLS. Two binds to avoid:
# 0.0.0.0 - exposes the palace to the whole LAN.
# 127.0.0.1 - behind a tunnel this 403s every proxied request (the Host pin
# is only enforced on loopback binds) AND silently starts with
# no token at all, since auto-minting is gated on the bind being
# non-loopback. Bind the docker0 gateway: reachable from the host
# and its containers (so a newt/proxy container works), not from
# the LAN. Set MEMPALACE_MCP_HTTP_TOKEN explicitly server-side.
# MEMPALACE_REMOTE_URL=https://mempalace.example.com/mcp
# MEMPALACE_REMOTE_TOKEN=
# ── MemPalace: automatic capture of pi sessions ───────────────────────
# The mempalace.ts extension feeds this container's pi transcripts into the
# palace by itself: on session_shutdown, and on a debounced agent_settled so a
# crash loses at most one window rather than the whole session. The entrypoint
# also runs a catch-up at container start, which is the only thing that can
# recover transcripts after a hard kill (no handler runs on SIGKILL).
# Nothing below is required for the local-palace case; the defaults work.
#
# MEMPALACE_FEED=0 # disable automatic capture entirely
# MEMPALACE_FEED_DEBOUNCE_MS=600000 # min gap between mid-session feeds (10 min)
# MEMPALACE_FEED_WING=wing_conversations
#
# REMOTE PALACE ONLY (MEMPALACE_REMOTE_URL set above): the palace is on another
# host, and `mempalace_mine` resolves its source path in the SERVER process, so
# the server cannot see this container's transcripts. The feeder therefore
# rsyncs its staged exports into a per-device inbox on the palace host and asks
# the server to mine its own local copy. Without MEMPALACE_PI_SSH_TARGET the
# feeder is skipped (a remote palace with no inbox has nothing to mine).
# MEMPALACE_PI_SSH_TARGET where to rsync to, as user@host:path
# MEMPALACE_PI_REMOTE_PATH what that inbox is called ON THE SERVER — i.e. the
# path the SERVER PROCESS can open. If the palace
# server runs in Docker, that is the container path
# (see docker-compose.mempalace.yml). If it runs
# NATIVELY (systemd unit / uv tool / plain
# `mempalace serve`), it sees host paths, so this
# must equal the path half of
# MEMPALACE_PI_SSH_TARGET. Getting this wrong is
# quiet: rsync still succeeds and only the mine
# fails with "source directory not found", so
# transcripts ship and are filed nowhere. The feeder
# warns in preflight when the two paths disagree.
# MEMPALACE_PI_DEVICE inbox subdirectory for this machine (default: hostname)
# MEMPALACE_PI_SSH_TARGET=user@palace-host:/srv/mempalace-feed
# MEMPALACE_PI_REMOTE_PATH=/data/feed
# MEMPALACE_PI_DEVICE=
# ── LAN access from the container (host-OS-agnostic) ─────────────────
# On VM-backed hosts (macOS OrbStack / Docker Desktop) the container can't
# reach the host's directly-attached LAN peers by default. The entrypoint
# then sets up the host as an SSH jump (use the `dssh` alias). Reach the host
# with `dssh host`; for named LAN peers put `ProxyJump host` overrides in a
# host-owned ~/.config/devbox-shell/ssh-lan.conf (bind-mounted in) rather than
# editing ~/.ssh/config. On native Linux Docker the LAN is reachable directly
# and this is a no-op.
# See the opencode-devbox README for the full walkthrough.
#
# DEVBOX_LAN_ACCESS: auto (default) | jump | off
# DEVBOX_LAN_ACCESS=auto
# HOST_SSH_USER: your username on the host (required for the jump). On first
# start the entrypoint prints the public key to authorize on the host.
# HOST_SSH_USER=
# DEVBOX_HOST_ALIAS: host hostname to reach (default host.docker.internal).
# DEVBOX_HOST_ALIAS=host.docker.internal
# DEVBOX_LAN_AUTOJUMP_PRIVATE: 1 = ProxyJump any private (RFC1918) IP through
# the host, so bare `dssh user@<ip>` works on whatever LAN you're roaming on.
# DEVBOX_LAN_AUTOJUMP_PRIVATE=0
# ── pi-atelier (TUI sidebar) ─────────────────────────────────────────
# The image vendors pi-atelier at a pinned, audited tag and registers it on
# container start. Set to 0 to opt out: the entrypoint then removes it from
# pi's `packages[]` instead of registering it. This lives here rather than
# being a `pi uninstall` because a broken TUI extension's failure mode is
# "pi will not start", which you cannot fix from inside pi.
# DEVBOX_ATELIER=1
# ── Git Configuration ────────────────────────────────────────────────
# Set BOTH. If unset, every repo inside the container fails with
# "Author identity unknown" on first commit, and an agent asked to commit
# will guess an identity from git log — often the wrong one. The e-mail is
# per-machine (work machines use the corporate address, personal machines the
# private one), so it belongs in this per-machine .env, never in a skill or a
# repo-local override. Consumed by entrypoint-user.sh -> ~/.gitconfig, which is
# NOT persistent across container recreate — this file is the source of truth.
GIT_USER_NAME=
GIT_USER_EMAIL=
# ── Gitea (for gitea-mcp MCP server) ────────────────────────────────
# GITEA_ACCESS_TOKEN=
# GITEA_HOST=https://gitea.example.com
# ── GitHub (optional, for GitHub MCP / git operations) ───────────────
# GITHUB_PERSONAL_ACCESS_TOKEN=
# ── AWS (optional, for AWS CLI / Bedrock) ────────────────────────────
# AWS_REGION=eu-west-1
# AWS_PROFILE=default
# AWS_ACCESS_KEY_ID=
# AWS_SECRET_ACCESS_KEY=
# ── Skillset (agent skills and instructions) ─────────────────────────
# If you have a skillset repo, the entrypoint auto-deploys skills and
# instructions on container start using relative symlinks.
# Detection is automatic if the skillset lives at WORKSPACE_PATH/skillset.
# SKILLSET_CONTAINER_PATH=
# ── cli_utils (standalone commands from a mounted checkout) ──────────
# If a cli_utils repo is mounted, the entrypoint symlinks its bin/ commands
# into ~/.local/bin on every start, so they survive container recreate and
# resolve in non-interactive shells too (docker exec, agent tool shells).
# Detection is automatic at WORKSPACE_PATH/cli_utils (or one level below).
# CLI_UTILS_CONTAINER_PATH=
# CLI_UTILS_LINK=0 # disable the linking entirely
# ── Locale ───────────────────────────────────────────────────────────
# LANG=sv_SE.UTF-8
# LANGUAGE=sv_SE:sv
# LC_ALL=sv_SE.UTF-8