opencode-sandbox: OpenCode 2 in a sandbox for any project
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
This commit is contained in:
Executable
+89
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env bash
|
||||
# Netlock, run as root (opencode-sandbox netlock setup|teardown calls it through sudo).
|
||||
# Processes whose primary group is GROUP may only talk to loopback (where the egress filter
|
||||
# listens) and to DNS servers from resolv.conf; everything else is rejected. The rules are
|
||||
# loaded by a systemd unit and survive reboots.
|
||||
# netlock-root.sh setup <group> <user>
|
||||
# netlock-root.sh teardown
|
||||
set -euo pipefail
|
||||
|
||||
TABLE=opencode_sandbox
|
||||
CONF_DIR=/etc/opencode-sandbox
|
||||
RULES="${CONF_DIR}/netlock.nft"
|
||||
UNIT=opencode-sandbox-netlock.service
|
||||
UNIT_PATH="/etc/systemd/system/${UNIT}"
|
||||
|
||||
[[ ${EUID} -eq 0 ]] || { echo "must run as root" >&2; exit 1; }
|
||||
|
||||
setup() {
|
||||
local group=$1 user=${2:-} gid ns dns_rules=""
|
||||
getent group "${group}" >/dev/null || groupadd "${group}"
|
||||
gid=$(getent group "${group}" | cut -d: -f3)
|
||||
if [[ -n ${user} ]] && ! id -nG "${user}" | tr ' ' '\n' | grep -qx "${group}"; then
|
||||
usermod -aG "${group}" "${user}"
|
||||
echo " ${user} added to group ${group} (log out and in, or use the new group via sg)"
|
||||
fi
|
||||
|
||||
# The loopback resolver (systemd-resolved) is covered by the loopback rule; non-local
|
||||
# nameservers are allowed one by one, not port 53 anywhere
|
||||
while read -r ns; do
|
||||
case "${ns}" in
|
||||
127.* | ::1) ;;
|
||||
*:*) dns_rules+=" meta skgid ${gid} ip6 daddr ${ns} meta l4proto { tcp, udp } th dport 53 accept"$'\n' ;;
|
||||
*) dns_rules+=" meta skgid ${gid} ip daddr ${ns} meta l4proto { tcp, udp } th dport 53 accept"$'\n' ;;
|
||||
esac
|
||||
done < <(awk '$1 == "nameserver" { print $2 }' /etc/resolv.conf)
|
||||
|
||||
mkdir -p "${CONF_DIR}"
|
||||
cat >"${RULES}" <<EOF
|
||||
# Generated by opencode-sandbox (lib/netlock-root.sh) — edit the script, not this file.
|
||||
table inet ${TABLE} {
|
||||
chain output {
|
||||
type filter hook output priority 0; policy accept;
|
||||
meta skgid ${gid} ip daddr 127.0.0.0/8 accept
|
||||
meta skgid ${gid} ip6 daddr ::1 accept
|
||||
${dns_rules} meta skgid ${gid} reject with icmpx admin-prohibited
|
||||
}
|
||||
}
|
||||
EOF
|
||||
chmod 644 "${RULES}"
|
||||
|
||||
cat >"${UNIT_PATH}" <<EOF
|
||||
[Unit]
|
||||
Description=opencode-sandbox netlock (nftables, group ${group})
|
||||
Wants=network-pre.target
|
||||
Before=network-pre.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
RemainAfterExit=yes
|
||||
ExecStartPre=-/usr/sbin/nft delete table inet ${TABLE}
|
||||
ExecStart=/usr/sbin/nft -f ${RULES}
|
||||
ExecStop=/usr/sbin/nft delete table inet ${TABLE}
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl daemon-reload
|
||||
systemctl enable "${UNIT}" >/dev/null 2>&1
|
||||
systemctl restart "${UNIT}"
|
||||
echo "✓ netlock: group ${group} (gid ${gid}), unit ${UNIT} enabled"
|
||||
echo " allowed: 127.0.0.0/8, ::1${dns_rules:+, DNS servers from resolv.conf}"
|
||||
}
|
||||
|
||||
teardown() {
|
||||
if systemctl list-unit-files "${UNIT}" >/dev/null 2>&1; then
|
||||
systemctl disable --now "${UNIT}" >/dev/null 2>&1 || true
|
||||
rm -f "${UNIT_PATH}"
|
||||
systemctl daemon-reload
|
||||
fi
|
||||
nft list table inet "${TABLE}" >/dev/null 2>&1 && nft delete table inet "${TABLE}"
|
||||
rm -rf "${CONF_DIR}"
|
||||
echo "✓ netlock removed (unit ${UNIT}, table ${TABLE}); the group is kept"
|
||||
}
|
||||
|
||||
case "${1:-}" in
|
||||
setup) shift; setup "$@" ;;
|
||||
teardown) teardown ;;
|
||||
*) echo "usage: $0 setup <group> [user] | teardown" >&2; exit 1 ;;
|
||||
esac
|
||||
Reference in New Issue
Block a user