# 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. /.opencode-sandbox/sandbox.conf (committed) # 4. /.opencode-sandbox/local/sandbox.conf (git-ignored, personal) # 5. OCSB_ 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_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_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: /-worktrees OCSB_WT_PREFIX="" # worktree directory = 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/ 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 .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) 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 } ocsb_find_project() { local dir=${1:-$PWD} while [[ ${dir} != / ]]; do [[ -f ${dir}/.opencode-sandbox/sandbox.conf ]] && { echo "${dir}"; return 0; } dir=$(dirname "${dir}") done 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) 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 .opencode-sandbox/sandbox.conf here or above (run: 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 }