Runs OpenCode 2 with full permissions while containing its mistakes: - egress filter: squid in a container per project; squid.conf is rendered from layered egress.conf files (package, user, project, local) with allow/block/connect rules and expect-pass/expect-block self-tests - netlock: nftables rejects direct egress for the opencode-sandbox group (installed once with sudo as a systemd unit) - bubblewrap: read-only host, writable project, worktrees and state; credentials hidden; ssh keys via the host's ssh-agent; session bus replaced by an xdg-dbus-proxy limited to notifications; no permission prompts (sudo and systemd-run denied); refuses to run unsandboxed - per-project state in ~/.local/share/opencode-sandbox/<name> - worktrees: `wt add|rm|list` with project hooks - skills mounted per project; the opencode-sandbox skill explains the environment to the agent - plugins as submodules: context-panel, skill-compact-resurrect; global config adds opencode-notifier and cc-safety-net - README in English and Russian
92 lines
3.5 KiB
Bash
92 lines
3.5 KiB
Bash
# shellcheck shell=bash
|
|
# Worktrees for parallel agent sessions. Generic part here; project specifics (ports, .env,
|
|
# starting a stack, seeding) go into hooks:
|
|
# .opencode-sandbox/hooks/wt-post-add after `git worktree add`, cwd = the new worktree
|
|
# .opencode-sandbox/hooks/wt-pre-rm before removal (stop the stack), cwd = the worktree
|
|
# Hooks get WT_DIR, WT_BRANCH, WT_SLUG, MAIN_ROOT in the environment and the extra
|
|
# arguments of `wt add` / `wt rm` as their arguments. A failing hook aborts the command.
|
|
|
|
ocsb_wt_main_root() {
|
|
git -C "${OCSB_PROJECT_DIR}" rev-parse --path-format=absolute --git-common-dir | sed 's|/\.git$||'
|
|
}
|
|
|
|
ocsb_wt_slug() {
|
|
local branch=$1 slug=""
|
|
if [[ -n ${OCSB_WT_SLUG_REGEX} && ${branch} =~ ${OCSB_WT_SLUG_REGEX} ]]; then
|
|
slug=${BASH_REMATCH[1]}
|
|
fi
|
|
[[ -n ${slug} ]] || slug=$(printf '%s' "${branch}" | tr '/' '-' | tr -c 'a-zA-Z0-9._-' '-')
|
|
echo "${slug}"
|
|
}
|
|
|
|
ocsb_wt_hook() {
|
|
local name=$1 dir=$2
|
|
shift 2
|
|
local hook=${OCSB_PROJECT_DIR}/.opencode-sandbox/hooks/${name}
|
|
[[ -x ${hook} ]] || return 0
|
|
ocsb_info "hook ${name}"
|
|
(cd "${dir}" && "${hook}" "$@")
|
|
}
|
|
|
|
ocsb_wt_add() {
|
|
[[ $# -ge 1 ]] || ocsb_die "usage: opencode-sandbox wt add <branch> [hook args]"
|
|
local branch=$1 main slug dir base
|
|
shift
|
|
git check-ref-format --branch "${branch}" >/dev/null 2>&1 || ocsb_die "invalid branch name: ${branch}"
|
|
main=$(ocsb_wt_main_root)
|
|
slug=$(ocsb_wt_slug "${branch}")
|
|
dir=${OCSB_WORKTREES_DIR}/${OCSB_WT_PREFIX}${slug}
|
|
[[ -e ${dir} ]] && ocsb_die "already exists: ${dir}"
|
|
base=${OCSB_WT_BASE:-HEAD}
|
|
if [[ ${base} != HEAD ]] && git -C "${main}" rev-parse -q --verify "origin/${base}" >/dev/null; then
|
|
echo " ${base} is $(git -C "${main}" rev-list --count "${base}..origin/${base}") commit(s) behind origin/${base} (git fetch if needed)"
|
|
fi
|
|
mkdir -p "${OCSB_WORKTREES_DIR}"
|
|
git -C "${main}" worktree add -b "${branch}" "${dir}" "${base}"
|
|
export WT_DIR=${dir} WT_BRANCH=${branch} WT_SLUG=${slug} MAIN_ROOT=${main}
|
|
ocsb_wt_hook wt-post-add "${dir}" "$@"
|
|
echo "✓ worktree ${dir} (branch ${branch}); remove: opencode-sandbox wt rm ${slug}"
|
|
}
|
|
|
|
ocsb_wt_rm() {
|
|
[[ $# -ge 1 ]] || ocsb_die "usage: opencode-sandbox wt rm <name|path> [--force]"
|
|
local name=$1 force=0 main dir branch status a extra=()
|
|
shift
|
|
for a in "$@"; do
|
|
if [[ ${a} == --force ]]; then force=1; else extra+=("${a}"); fi
|
|
done
|
|
main=$(ocsb_wt_main_root)
|
|
case "${name}" in
|
|
/*) dir=${name} ;;
|
|
*) dir=${OCSB_WORKTREES_DIR}/${OCSB_WT_PREFIX}${name} ;;
|
|
esac
|
|
[[ -d ${dir} ]] || ocsb_die "no such worktree: ${dir}"
|
|
[[ $(realpath "${dir}") != "$(realpath "${main}")" ]] || ocsb_die "refusing to remove the main worktree"
|
|
branch=$(git -C "${dir}" rev-parse --abbrev-ref HEAD 2>/dev/null || true)
|
|
export WT_DIR=${dir} WT_BRANCH=${branch} WT_SLUG=${name##*/} MAIN_ROOT=${main}
|
|
ocsb_wt_hook wt-pre-rm "${dir}" "${extra[@]}"
|
|
status=$(git -C "${dir}" status --porcelain)
|
|
if [[ -n ${status} && ${force} -ne 1 ]]; then
|
|
echo "${status}" >&2
|
|
ocsb_die "worktree has uncommitted changes — commit them or pass --force (they will be lost)"
|
|
fi
|
|
if [[ ${force} -eq 1 ]]; then
|
|
git -C "${main}" worktree remove --force "${dir}"
|
|
else
|
|
git -C "${main}" worktree remove "${dir}"
|
|
fi
|
|
git -C "${main}" worktree prune
|
|
echo "✓ removed ${dir} (branch ${branch:-?} kept)"
|
|
}
|
|
|
|
ocsb_wt() {
|
|
local sub=${1:-list}
|
|
[[ $# -gt 0 ]] && shift
|
|
case "${sub}" in
|
|
add) ocsb_wt_add "$@" ;;
|
|
rm) ocsb_wt_rm "$@" ;;
|
|
list) git -C "${OCSB_PROJECT_DIR}" worktree list ;;
|
|
*) ocsb_die "wt: unknown subcommand ${sub} (add|rm|list)" ;;
|
|
esac
|
|
}
|