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:
2026-10-30 01:00:02 -07:00
commit 64c222a084
29 changed files with 1945 additions and 0 deletions
+301
View File
@@ -0,0 +1,301 @@
# opencode-sandbox
[English](README.md) · **Русский**
Дайте [OpenCode 2](https://opencode.ai) работать над проектом с полными правами — без
вопросов «разрешить этот путь?», — а его ошибки пусть остаются внутри песочницы.
```bash
cd my-project
opencode-sandbox init # один раз на проект
opencode-sandbox run # фильтр → netlock → самопроверки → веб-интерфейс OpenCode
```
## Что это даёт
| | |
|---|---|
| **Изоляция файлов** | bubblewrap: агент пишет только в проект, его worktree и разрешённые вами пути. Остальная система — только чтение; SSH-ключи, облачные учётки, `~/.kube/config`, профили браузеров и конфиги других агентов скрыты |
| **Сетевой фильтр** | весь HTTP(S) идёт через squid с правилами проекта; прод и внутренние сети недоступны, пока вы их не разрешите |
| **Netlock** | nftables на уровне ядра отклоняет всё, что идёт в обход фильтра: прямой TCP, IP-адреса, ping, обычный SSH |
| **Без подтверждений** | внутри песочницы агенту разрешено всё, кроме `sudo` и `systemd-run` |
| **Worktree** | одна сессия — одно дерево; создаются и удаляются командой `opencode-sandbox wt`, с хуками проекта (порты, `.env`, запуск стека) |
| **Дополнительно** | панель «Токены» в веб-интерфейсе (состав контекста, расход, скорость модели), скилы после compaction возвращаются, [cc-safety-net](https://github.com/kenryu42/cc-safety-net) против разрушительных git/rm, уведомления на рабочий стол |
Только Linux: bubblewrap, nftables, systemd, Docker.
## Содержание
- [Требования](#требования) · [Установка](#установка) · [Быстрый старт](#быстрый-старт)
- [Файлы проекта](#файлы-проекта) · [Настройки](#настройки) · [Сетевые правила](#сетевые-правила)
- [Worktree](#worktree) · [Скилы и плагины](#скилы-и-плагины)
- [Как это устроено](#как-это-устроено) · [Модель безопасности и ограничения](#модель-безопасности-и-ограничения)
- [Решение проблем](#решение-проблем) · [Удаление](#удаление) · [Команды](#команды)
## Требования
- Linux с непривилегированными user namespaces (bubblewrap должен работать от обычного пользователя)
- `docker`, `bwrap` (bubblewrap), `sg` (shadow-utils), `curl`, `jq`, `ncat` (nmap-ncat), `git`
- `sudo` один раз — для установки netlock
- OpenCode 2: `npm i -g @opencode/cli` или локальная установка, путь — в `OCSB_OPENCODE_BIN`
- по желанию: `xdg-dbus-proxy` — уведомления из песочницы
## Установка
```bash
git clone --recursive <адрес-репозитория> ~/opencode-sandbox
cd ~/opencode-sandbox
make install # ссылка opencode-sandbox в ~/.local/bin, плагины
opencode-sandbox doctor # проверка зависимостей и настроек хоста
```
Первый `run` ставит netlock через `sudo` (или вручную: `opencode-sandbox netlock setup`).
Вас добавят в группу `opencode-sandbox`; перелогиниваться не нужно.
Обновление: `git pull && make plugins`.
## Быстрый старт
```bash
cd my-project
opencode-sandbox init # создаст .opencode-sandbox/ и .cc-safety-net/policy.json
$EDITOR .opencode-sandbox/egress.conf # разрешите хосты, которые нужны проекту
ssh-add # только если агенту нужен git по SSH
opencode-sandbox run
```
`run` поднимает фильтр, проверяет netlock, прогоняет самопроверки и запускает OpenCode.
Браузер откроется на `http://127.0.0.1:4196/`: логин `opencode`, пароль — в
`~/.local/share/opencode-sandbox/<проект>/password` (выводится при первом запуске).
Закоммитьте `.opencode-sandbox/` (кроме `local/`) и `.cc-safety-net/` — каждый, кто
склонирует проект, получит ту же песочницу.
## Файлы проекта
```
my-project/
├── .opencode-sandbox/
│ ├── sandbox.conf настройки (bash) в git
│ ├── egress.conf сетевые правила в git
│ ├── hooks/
│ │ ├── wt-post-add после создания worktree в git
│ │ └── wt-pre-rm перед удалением worktree в git
│ └── local/ личные переопределения, не в git
│ ├── sandbox.conf
│ └── egress.conf
└── .cc-safety-net/policy.json исключения cc-safety-net в git
```
Состояние проекта лежит вне репозитория, в `~/.local/share/opencode-sandbox/<имя>/`:
база сессий OpenCode, кэши, пароль веб-интерфейса, конфиг Docker-клиента, сгенерированный
squid.conf.
## Настройки
`sandbox.conf` — файл bash. Слои, побеждает последний:
1. умолчания пакета
2. `~/.config/opencode-sandbox/sandbox.conf` — ваши, для всех проектов
3. `.opencode-sandbox/sandbox.conf` — проекта
4. `.opencode-sandbox/local/sandbox.conf` — ваши, для этого проекта
5. `OCSB_<ИМЯ>=…` в окружении — на один запуск (только скаляры)
`opencode-sandbox config` показывает итоговые значения. Когда файлы читаются,
`OCSB_PROJECT_DIR` уже задан — его можно использовать в значениях. Относительные пути —
от проекта; `~/` работает.
| Настройка | По умолчанию | Что задаёт |
|---|---|---|
| `OCSB_NAME` | имя каталога проекта | имя каталога состояния и контейнера фильтра |
| `OCSB_WEB_PORT` | `4196` | порт веб-интерфейса; сам OpenCode — на следующем |
| `OCSB_EGRESS_PORT` | `3128` | порт фильтра на 127.0.0.1 и шлюзе docker0 |
| `OCSB_OPENCODE_BIN` | `opencode` из PATH | бинарник OpenCode 2 |
| `OCSB_ENV_FILE` | — | файл `KEY=VALUE` с токенами MCP-серверов и провайдера модели |
| `OCSB_KUBECONFIG` | — | kubeconfig агента (`~/.kube/config` остаётся скрытым) |
| `OCSB_ENV` | `()` | дополнительные `KEY=VALUE` для OpenCode |
| `OCSB_RW` | `()` | дополнительные пути на запись |
| `OCSB_HIDE` | `()` | дополнительные скрытые пути |
| `OCSB_RW_HOME` | `.cache .npm .asdf .bundle .gem …` | каталоги инструментов в `~` на запись (если есть) |
| `OCSB_HIDE_HOME` | `.gnupg .aws .kube/config .claude …` | скрытые пути в `~` |
| `OCSB_SKILLS` | `(opencode-sandbox)` | скилы, которые монтируются агенту |
| `OCSB_SKILL_DIRS` | `()` | дополнительные каталоги со скилами |
| `OCSB_PLUGINS` | skill-compact-resurrect | файлы плагинов из `plugins/` (или абсолютные пути) |
| `OCSB_PANEL` | `1` | панель «Токены» в веб-интерфейсе |
| `OCSB_NOTIFICATIONS` | `1` | уведомления через фильтрующий прокси D-Bus |
| `OCSB_SANDBOX` | `1` | `0` — только фильтр и netlock, без bubblewrap |
| `OCSB_WORKTREES_DIR` | `../<проект>-worktrees` | куда класть worktree |
| `OCSB_WT_PREFIX` | — | каталог дерева = `<префикс><slug>` |
| `OCSB_WT_BASE` | текущий `HEAD` | от какой ветки создаются деревья |
| `OCSB_WT_SLUG_REGEX` | — | регулярка с одной группой: slug из имени ветки |
| `OCSB_OPENCODE_CONFIG` | `~/.config/opencode-sandbox/opencode.jsonc` или пакета | глобальный конфиг OpenCode |
| `OCSB_STATE_DIR` | `~/.local/share/opencode-sandbox/<имя>` | каталог состояния |
| `OCSB_NETLOCK_GROUP` | `opencode-sandbox` | группа netlock |
| `OCSB_DOCKER_GW` | адрес docker0 | где контейнеры видят фильтр |
Пример — проект с внутренними сервисами, файлом токенов и worktree:
```bash
OCSB_NAME=shop
OCSB_WEB_PORT=4296 # разные порты — несколько проектов одновременно
OCSB_EGRESS_PORT=3138
OCSB_ENV_FILE=~/.config/shop/agent.env
OCSB_HIDE=(../shop-secrets)
OCSB_ENV=("SHOP_ENV=dev")
OCSB_WT_PREFIX=shop-wt-
OCSB_WT_BASE=main
OCSB_WT_SLUG_REGEX='SHOP-([0-9]+)'
```
## Сетевые правила
Политика — чёрный список: что не запрещено, то разрешено. Всегда запрещены, если не
разрешены явно: внутренние сети (10/8, 172.16/12, 192.168/16, 100.64/10, link-local,
loopback), имена без точки, порты кроме 80 и 443.
```
allow jira.example.com gitlab.example.com # побеждает block
allow-regex ^[a-z0-9-]+\.staging\.example\.com$
block .example.com # домен и все поддомены
block-regex ^prod-
connect git.example.com:2222 10.0.0.5:6443 # туннели: git по SSH, API Kubernetes
expect-block https://prod.example.com/ # самопроверки для `run` и `egress verify`
expect-pass https://gitlab.example.com/
expect-pass https://10.0.0.5:6443/version -k # опции curl — после URL
```
Порядок проверки: `connect` → порты кроме 80/443 запрещены → `allow` → `block` → имена без
точки и внутренние сети запрещены → остальное разрешено.
Слои (правила складываются): [egress/defaults.conf](egress/defaults.conf) →
`~/.config/opencode-sandbox/egress.conf` → `.opencode-sandbox/egress.conf` →
`.opencode-sandbox/local/egress.conf`.
**Куда класть хосты компании.** В `egress.conf` проекта: он лежит в репозитории компании,
и новый коллега получает рабочие правила вместе с `git clone`. То, что нельзя коммитить, —
в `local/egress.conf`. В самом пакете хостов компаний нет.
Изменения применяются при следующем `run` или сразу командой `opencode-sandbox egress up`
(перечитывает правила без перезапуска squid). `egress render` показывает сгенерированный
squid.conf, `egress logs` — каждый запрос и отказ в реальном времени.
## Worktree
```bash
opencode-sandbox wt add feature-SHOP-42-cart --seed # аргументы после ветки — хуку
opencode-sandbox wt rm 42 # откажет, если есть незакоммиченное
opencode-sandbox wt rm 42 --force # выбросит незакоммиченное
opencode-sandbox wt list
```
`wt add` создаёт `<OCSB_WORKTREES_DIR>/<OCSB_WT_PREFIX><slug>` в новой ветке от
`OCSB_WT_BASE` и запускает в нём `hooks/wt-post-add`. `wt rm` запускает `hooks/wt-pre-rm`,
проверяет незакоммиченные изменения и удаляет дерево (ветка остаётся). Хуки получают
`WT_DIR`, `WT_BRANCH`, `WT_SLUG`, `MAIN_ROOT` и дополнительные аргументы; ошибка хука
прерывает команду. Шаблоны — `hooks/*.example` после `init`.
Агенту сказано создавать worktree только для изменений кода в новой ветке — не для
вопросов, поиска и ревью.
## Скилы и плагины
- **Скилы.** `OCSB_SKILLS` — каталоги скилов (с `SKILL.md`), которые монтируются агенту
только на чтение как глобальные: из [skills/](skills/) или ваших `OCSB_SKILL_DIRS`. Скилл
`opencode-sandbox` объясняет агенту среду: что доступно на запись, что значит 403 от
прокси, как попросить хост, как работать с worktree.
- **Плагины** пакета — git submodules в [plugins/](plugins/):
[context-panel](plugins/context-panel) (панель «Токены») и
[skill-compact-resurrect](plugins/skill-compact-resurrect) (скилы переживают compaction).
- **Глобальный конфиг OpenCode** [opencode/opencode.jsonc](opencode/opencode.jsonc)
добавляет opencode-notifier и cc-safety-net. Замените его своим
`~/.config/opencode-sandbox/opencode.jsonc`. Собственный `opencode.jsonc` проекта (модель,
MCP-серверы, плагины проекта) работает как обычно.
## Как это устроено
```
opencode-sandbox run
├─ docker: opencode-sandbox-egress-<имя> squid, сеть хоста, 127.0.0.1 + docker0
├─ xdg-dbus-proxy сессионная шина — только уведомления
└─ sg opencode-sandbox netlock: группе доступны только loopback и DNS
└─ bwrap / только чтение; проект, worktree, состояние — запись
└─ opencode serve :4197 прокси в окружении, свои XDG, права = allow
браузер → :4196 (панель внутри OpenCode) → :4197
```
Внутри песочницы:
- **На запись:** проект, каталог worktree, каталог состояния, `OCSB_RW`, `OCSB_RW_HOME`,
`/tmp`. Всё остальное — только чтение.
- **Скрыто:** `OCSB_HIDE_HOME`, `OCSB_HIDE`, приватные ключи в `~/.ssh` (`config`,
`known_hosts` и `*.pub` видны; подписывает ssh-agent хоста).
- **Сеть:** `HTTP(S)_PROXY` указывают на фильтр. `GIT_SSH_COMMAND` пускает ssh через него
с `-F ~/.ssh/config` (внутри user namespace системные конфиги ssh, принадлежащие root,
выглядят как принадлежащие `nobody`, и ssh их отвергает). `DOCKER_CONFIG` передаёт прокси
сборкам и контейнерам.
- **OpenCode:** свои каталоги `XDG_*` в каталоге состояния (ваш `~/.config/opencode` не
используется); права — через `OPENCODE_CONFIG_CONTENT`: разрешено всё, `sudo` и
`systemd-run` запрещены.
- **Сессионная шина:** заменена на `xdg-dbus-proxy`, который пропускает только
`org.freedesktop.Notifications`; настоящая шина позволила бы агенту запускать процессы вне
песочницы через `systemd-run --user`.
- **Браузер:** `xdg-open` агенту отключён; интерфейс открывает `run` из вашей обычной сессии.
- **Без тихого отката:** при `OCSB_SANDBOX=1` и неработающем bubblewrap `run` не запустится.
**Netlock** (`netlock setup`, sudo) создаёт группу `opencode-sandbox`, добавляет в неё вас и
ставит `opencode-sandbox-netlock.service`: таблица nftables разрешает сокетам этой группы
только `127.0.0.0/8`, `::1` и DNS-серверы из `resolv.conf`, остальное отклоняет. OpenCode
запускается с этой группой — под правило попадают агент и всё, что он запускает.
## Модель безопасности и ограничения
Цель — удержать агента, который ошибается (не тот путь, `rm -rf` не там, запрос в прод), а
не остановить целенаправленного злоумышленника.
- **Docker — выход наружу.** Агенту доступен сокет Docker, а Docker может смонтировать любой
путь хоста (`docker run -v /:/host`). Если это важно — не давайте агенту Docker; закрыть
это можно фильтрующим прокси для сокета или rootless Podman.
- **Общее ядро.** bubblewrap — это namespaces, а не виртуальная машина.
- **Разрешённые хосты — канал утечки.** Данные могут уйти на любой хост, который пропускают
правила.
- **Проект доступен на запись.** Ошибки внутри него настоящие; cc-safety-net блокирует
типичные разрушительные команды, git хранит историю — коммитьте чаще.
- `/tmp` общий с хостом.
## Решение проблем
| Симптом | Причина и что делать |
|---|---|
| `Read-only file system` | путь вне разрешённых на запись. Если агенту он правда нужен — добавьте в `OCSB_RW` |
| `403` от прокси, `CONNECT tunnel failed` | хост не разрешён: добавьте `allow` (или `connect host:port` для порта не 443) в `egress.conf`, выполните `opencode-sandbox egress up` |
| git по SSH: `Permission denied (publickey)` | в ssh-agent нет ключей: `ssh-add` перед `run` |
| git по SSH: `403` | добавьте `connect <хост>:<порт>` в `egress.conf` |
| `port … is busy` | другой проект или старый экземпляр: задайте другие `OCSB_WEB_PORT` / `OCSB_EGRESS_PORT` |
| `netlock is NOT working` | `opencode-sandbox netlock setup`, затем `netlock verify` |
| `bubblewrap cannot create a sandbox` | отключены непривилегированные user namespaces (`kernel.unprivileged_userns_clone`, ограничения AppArmor) |
| нет панели «Токены» | открывайте порт 4196, а не 4197; журнал — `<состояние>/data/opencode/log/context-panel.log` |
| нет уведомлений | установите `xdg-dbus-proxy` |
| `docker build` не скачивает образы | реестры доступны через squid: смотрите `egress logs` |
## Удаление
```bash
opencode-sandbox egress down # в каждом проекте
opencode-sandbox netlock teardown # sudo; группа остаётся: sudo groupdel opencode-sandbox
cd ~/opencode-sandbox && make uninstall
rm -rf ~/.local/share/opencode-sandbox # состояние: сессии, кэши, пароли
docker rmi opencode-sandbox-egress
```
## Команды
```
opencode-sandbox init добавить .opencode-sandbox/ в текущий репозиторий
opencode-sandbox config итоговые настройки
opencode-sandbox doctor зависимости и настройки хоста
opencode-sandbox run [аргументы opencode] всё сразу, затем OpenCode
opencode-sandbox prepare фильтр + netlock + самопроверки, без OpenCode
opencode-sandbox egress up|down|status|logs|verify|render
opencode-sandbox netlock setup|teardown|status|verify
opencode-sandbox wt add <ветка> [аргументы] | rm <имя|путь> [--force] | list
```