MCP servers started with uvx failed with 'Read-only file system' as soon as uv needed to write under its data directory.
187 lines
8.3 KiB
Bash
187 lines
8.3 KiB
Bash
# shellcheck shell=bash
|
|
# Shared helpers: messages, project discovery, configuration, state paths.
|
|
|
|
ocsb_die() {
|
|
echo "opencode-sandbox: $*" >&2
|
|
exit 1
|
|
}
|
|
ocsb_warn() { echo "⚠ $*" >&2; }
|
|
ocsb_info() { echo "→ $*"; }
|
|
|
|
# --- configuration ------------------------------------------------------------------------
|
|
# Layers, later wins:
|
|
# 1. defaults below
|
|
# 2. ~/.config/opencode-sandbox/sandbox.conf (user, all projects)
|
|
# 3. <project>/.opencode-sandbox/sandbox.conf (committed)
|
|
# 4. <project>/.opencode-sandbox/local/sandbox.conf (git-ignored, personal)
|
|
# 5. OCSB_<SETTING> from the environment (scalars only, one run)
|
|
# Config files are bash: scalars and arrays. OCSB_PROJECT_DIR is already set when they load.
|
|
|
|
OCSB_SCALARS=(
|
|
OCSB_NAME OCSB_WEB_PORT OCSB_EGRESS_PORT OCSB_NETLOCK_GROUP OCSB_SANDBOX OCSB_PANEL
|
|
OCSB_NOTIFICATIONS OCSB_TMP OCSB_DISPLAY OCSB_OPENCODE_BIN OCSB_ENV_FILE OCSB_KUBECONFIG OCSB_PASSWORD_FILE
|
|
OCSB_WORKTREES_DIR OCSB_WT_PREFIX OCSB_WT_BASE OCSB_WT_SLUG_REGEX OCSB_DOCKER_GW
|
|
OCSB_STATE_DIR OCSB_OPENCODE_CONFIG OCSB_EGRESS_BASE
|
|
)
|
|
OCSB_ARRAYS=(OCSB_RW OCSB_RW_HOME OCSB_HIDE OCSB_HIDE_HOME OCSB_SKILLS OCSB_SKILL_DIRS OCSB_PLUGINS OCSB_ENV)
|
|
|
|
ocsb_defaults() {
|
|
OCSB_NAME="" # default: basename of the project directory
|
|
OCSB_WEB_PORT=4196 # browser port (context panel); OpenCode itself listens on +1
|
|
OCSB_EGRESS_PORT=3128 # squid port on 127.0.0.1 and the docker0 gateway
|
|
OCSB_NETLOCK_GROUP=opencode-sandbox
|
|
OCSB_SANDBOX=1 # bubblewrap; 0 = egress filter and netlock only
|
|
OCSB_PANEL=1 # "Tokens" panel in the web UI (plugins/context-panel)
|
|
OCSB_NOTIFICATIONS=1 # desktop notifications through a filtered D-Bus proxy
|
|
OCSB_TMP=private # private: own /tmp in the state dir; shared: the host's /tmp
|
|
OCSB_DISPLAY=0 # 1: windows on your desktop through the Wayland socket (no X11)
|
|
OCSB_OPENCODE_BIN="" # default: `opencode` from PATH
|
|
OCSB_ENV_FILE="" # KEY=VALUE file with tokens, exported to OpenCode
|
|
OCSB_KUBECONFIG="" # kubeconfig for the agent (~/.kube/config stays hidden)
|
|
OCSB_PASSWORD_FILE="" # web UI password; default: generated in the state dir
|
|
OCSB_WORKTREES_DIR="" # default: <parent of project>/<project>-worktrees
|
|
OCSB_WT_PREFIX="" # worktree directory = <prefix><slug>
|
|
OCSB_WT_BASE="" # branch new worktrees start from; default: current HEAD
|
|
OCSB_WT_SLUG_REGEX="" # ERE with one group: slug from the branch name
|
|
OCSB_DOCKER_GW="" # default: address of docker0
|
|
OCSB_STATE_DIR="" # default: ~/.local/share/opencode-sandbox/<name>
|
|
OCSB_OPENCODE_CONFIG="" # global OpenCode config; default: user's or the package's
|
|
OCSB_RW=() # extra paths the agent may write (relative to the project)
|
|
# Home directories that stay writable for toolchains and caches (only if they exist)
|
|
OCSB_RW_HOME=(.cache .npm .bun .bundle .gem .asdf .local/share/mise .local/share/uv .cargo .rustup go
|
|
.m2 .gradle .docker/buildx .kube/cache)
|
|
OCSB_HIDE=() # extra paths hidden from the agent (relative to the project)
|
|
# Hidden home paths: credentials of other tools, browsers, other agents
|
|
OCSB_HIDE_HOME=(.gnupg .aws .azure .config/gcloud .kube/config .docker/config.json .netrc
|
|
.git-credentials .config/gh/hosts.yml .password-store .local/share/keyrings .mozilla
|
|
.config/google-chrome .config/chromium .config/BraveSoftware .claude .codex
|
|
.config/opencode .local/share/opencode .Xauthority .config/mozilla .config/Claude .config/Code
|
|
.config/Cursor .config/Codex)
|
|
OCSB_SKILLS=(opencode-sandbox) # skills mounted for the agent (names of skill directories)
|
|
OCSB_SKILL_DIRS=() # extra directories to look for skills in
|
|
OCSB_PLUGINS=(skill-compact-resurrect/skill-compact-resurrect.v2.ts) # files in plugins/
|
|
OCSB_ENV=() # extra KEY=VALUE for OpenCode
|
|
}
|
|
|
|
# Trusted projects (init / trust), one physical path per line. Only these are loaded:
|
|
# sandbox.conf is bash run on the host, and the agent can write anywhere in the project
|
|
# tree, so a .opencode-sandbox/ found in a worktree or a subdirectory proves nothing.
|
|
ocsb_trust_file() { echo "${XDG_CONFIG_HOME:-${HOME}/.config}/opencode-sandbox/projects"; }
|
|
ocsb_trusted() { [[ -f $(ocsb_trust_file) ]] && grep -Fxq -- "$1" "$(ocsb_trust_file)"; }
|
|
ocsb_trust() {
|
|
local dir f
|
|
dir=$(cd "${1:-.}" && pwd -P) || ocsb_die "no such directory: ${1}"
|
|
[[ -f ${dir}/.opencode-sandbox/sandbox.conf ]] || ocsb_die "no ${dir}/.opencode-sandbox/sandbox.conf"
|
|
[[ -z ${OCSB_IN_SANDBOX:-} ]] || ocsb_die "trust is a host command"
|
|
ocsb_trusted "${dir}" && { echo "already trusted: ${dir}"; return 0; }
|
|
f=$(ocsb_trust_file)
|
|
mkdir -p "$(dirname "${f}")"
|
|
echo "${dir}" >>"${f}"
|
|
echo "✓ trusted: ${dir}"
|
|
}
|
|
|
|
# Nearest trusted project at or above the directory
|
|
ocsb_find_project() {
|
|
local dir untrusted=""
|
|
dir=$(cd "${1:-.}" && pwd -P)
|
|
while [[ ${dir} != / ]]; do
|
|
if [[ -f ${dir}/.opencode-sandbox/sandbox.conf ]]; then
|
|
ocsb_trusted "${dir}" && { echo "${dir}"; return 0; }
|
|
untrusted=${untrusted:-${dir}}
|
|
fi
|
|
dir=$(dirname "${dir}")
|
|
done
|
|
[[ -z ${untrusted} ]] ||
|
|
ocsb_warn "${untrusted} is not a trusted project: read its .opencode-sandbox/ (sandbox.conf and hooks run on the host), then: opencode-sandbox trust ${untrusted}"
|
|
return 1
|
|
}
|
|
|
|
# ocsb_load_project [--optional]: find the project, load config layers, derive paths
|
|
ocsb_load_project() {
|
|
local optional=0 saved=() v f
|
|
[[ ${1:-} == --optional ]] && optional=1
|
|
for v in "${OCSB_SCALARS[@]}"; do
|
|
[[ -n ${!v+x} ]] && saved+=("${v}=${!v}")
|
|
done
|
|
ocsb_defaults
|
|
|
|
OCSB_PROJECT_DIR=""
|
|
if [[ -n ${OCSB_PROJECT:-} ]]; then
|
|
OCSB_PROJECT_DIR=$(cd "${OCSB_PROJECT}" && pwd -P)
|
|
ocsb_trusted "${OCSB_PROJECT_DIR}" || ocsb_die "OCSB_PROJECT=${OCSB_PROJECT} is not a trusted project (opencode-sandbox trust)"
|
|
else
|
|
OCSB_PROJECT_DIR=$(ocsb_find_project) || true
|
|
fi
|
|
if [[ -z ${OCSB_PROJECT_DIR} || ! -f ${OCSB_PROJECT_DIR}/.opencode-sandbox/sandbox.conf ]]; then
|
|
[[ ${optional} == 1 ]] || ocsb_die "no trusted .opencode-sandbox/ here or above (new project: opencode-sandbox init)"
|
|
fi
|
|
export OCSB_PROJECT_DIR
|
|
|
|
for f in "${XDG_CONFIG_HOME:-${HOME}/.config}/opencode-sandbox/sandbox.conf" \
|
|
${OCSB_PROJECT_DIR:+"${OCSB_PROJECT_DIR}/.opencode-sandbox/sandbox.conf"} \
|
|
${OCSB_PROJECT_DIR:+"${OCSB_PROJECT_DIR}/.opencode-sandbox/local/sandbox.conf"}; do
|
|
# shellcheck disable=SC1090
|
|
[[ -f ${f} ]] && source "${f}"
|
|
done
|
|
for v in "${saved[@]}"; do
|
|
printf -v "${v%%=*}" '%s' "${v#*=}"
|
|
done
|
|
ocsb_derive
|
|
}
|
|
|
|
ocsb_derive() {
|
|
local base
|
|
base=$(basename "${OCSB_PROJECT_DIR:-default}")
|
|
OCSB_NAME=${OCSB_NAME:-${base}}
|
|
OCSB_NAME=$(printf '%s' "${OCSB_NAME}" | tr -c 'a-zA-Z0-9_.-' '-' | tr '[:upper:]' '[:lower:]')
|
|
OCSB_STATE_DIR=${OCSB_STATE_DIR:-${XDG_DATA_HOME:-${HOME}/.local/share}/opencode-sandbox/${OCSB_NAME}}
|
|
if [[ -n ${OCSB_PROJECT_DIR} ]]; then
|
|
OCSB_WORKTREES_DIR=${OCSB_WORKTREES_DIR:-$(dirname "${OCSB_PROJECT_DIR}")/${base}-worktrees}
|
|
OCSB_WORKTREES_DIR=$(ocsb_abs "${OCSB_WORKTREES_DIR}")
|
|
fi
|
|
if [[ -z ${OCSB_DOCKER_GW} ]]; then
|
|
OCSB_DOCKER_GW=$(ip -4 -o addr show docker0 2>/dev/null | awk '{print $4}' | cut -d/ -f1 | head -1)
|
|
OCSB_DOCKER_GW=${OCSB_DOCKER_GW:-172.17.0.1}
|
|
fi
|
|
OCSB_CONTAINER="opencode-sandbox-egress-${OCSB_NAME}"
|
|
OCSB_IMAGE="opencode-sandbox-egress"
|
|
}
|
|
|
|
# Path relative to the project (or ~/..., or absolute) → absolute path
|
|
ocsb_abs() {
|
|
local p=$1
|
|
# shellcheck disable=SC2088 # a literal "~" comes from config files
|
|
case "${p}" in
|
|
/*) ;;
|
|
"~") p=${HOME} ;;
|
|
"~/"*) p=${HOME}/${p#"~/"} ;;
|
|
*) p=${OCSB_PROJECT_DIR}/${p} ;;
|
|
esac
|
|
realpath -m "${p}"
|
|
}
|
|
|
|
ocsb_print_config() {
|
|
local v
|
|
echo "# project: ${OCSB_PROJECT_DIR:-none}"
|
|
for v in "${OCSB_SCALARS[@]}"; do
|
|
printf '%s=%q\n' "${v}" "${!v}"
|
|
done
|
|
for v in "${OCSB_ARRAYS[@]}"; do
|
|
local -n arr=${v}
|
|
printf '%s=(' "${v}"
|
|
[[ ${#arr[@]} -gt 0 ]] && printf ' %q' "${arr[@]}"
|
|
printf ' )\n'
|
|
unset -n arr
|
|
done
|
|
}
|
|
|
|
# OpenCode binary: OCSB_OPENCODE_BIN (a path, relative to the project, or a command) or PATH
|
|
ocsb_opencode_bin() {
|
|
local b=${OCSB_OPENCODE_BIN:-opencode}
|
|
if [[ ${b} == */* || ${b} == "~"* ]]; then
|
|
ocsb_abs "${b}"
|
|
else
|
|
command -v "${b}" || true
|
|
fi
|
|
}
|