306 lines
21 KiB
Markdown
306 lines
21 KiB
Markdown
# 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 https://git.neotokai.ru/public/opencode-sandbox.git ~/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
|
||
```
|
||
|
||
## Лицензия
|
||
|
||
[MIT](LICENSE)
|