Files
opencode-sandbox/README.ru.md
T
k.denisov 55d217e3cf Report read-only paths after a session; known issues document
- run (on exit) and doctor scan the OpenCode log for 'Read-only file system' / EROFS, map
  the agent's XDG mirror back to the home directory and print a ready OCSB_RW_HOME line;
  writable, hidden and protected project paths are skipped
- docs/known-issues.md: open gaps and planned work (Docker, blocklist egress, DNS, shared
  network namespace, resource limits, undo, planted repos, credentials, agents working
  around guards); linked from both READMEs
2026-10-30 05:52:37 -07:00

341 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/` — каждый, кто
склонирует проект, получит ту же песочницу. `sandbox.conf` и хуки — это bash, который
выполняется на вашем хосте, поэтому загружаются только доверенные проекты: `init` доверяет
созданному проекту, а в клоне прочитайте `.opencode-sandbox/` и выполните
`opencode-sandbox trust` (список — `~/.config/opencode-sandbox/projects`).
`.opencode-sandbox/` в worktree или подкаталоге игнорируется.
## Файлы проекта
```
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_DISPLAY` | `0` | `1` — окна агента (например, браузер Playwright) на вашем экране через сокет Wayland |
| `OCSB_TMP` | `private` | `private` — свой `/tmp` агента в каталоге состояния; `shared` — `/tmp` хоста |
| `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`. Всё остальное — только чтение.
- **Свой `/tmp`:** в `/tmp` хоста лежат сокеты, через которые можно выйти из песочницы
(tmux, X11, сокеты браузеров, других программ), поэтому у агента свой `/tmp` — в
`<состояние>/tmp` (файлы старше 7 дней удаляются при запуске). Скриншоты и логи агента ищите
там. Docker смонтирует путь из `/tmp` из `/tmp` хоста — монтируйте пути проекта.
- **Только чтение внутри проекта:** то, что хост выполняет вне песочницы, — `.opencode-sandbox/`,
`.cc-safety-net/`, `.git/hooks`, `.git/config` (`core.fsmonitor` оттуда срабатывает при любом
`git status`, хватит prompt'а оболочки), `core.hooksPath`, если он внутри проекта, — а также
правила egress и домашний каталог safety-net в каталоге состояния. Сам `.git` — точка
монтирования, его нельзя переименовать и подменить. git работает; не проходит только запись
`git config` (например, `push -u` не запишет upstream). После каждой сессии `run` сообщает
о других `.git` в проекте и worktree, о worktree, чей `.git` указывает в другое место, и о
переопределениях для отдельных worktree; `doctor` проверяет то же.
- **Скрыто:** `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`.
- **Экран:** по умолчанию нет — `DISPLAY`, `WAYLAND_DISPLAY` и `XAUTHORITY` пустые, cookie X
скрыт; браузеры — headless. С `OCSB_DISPLAY=1` агент получает только сокет Wayland (и
`/dev/dri` для видеокарты): его окна появляются на вашем экране, но, в отличие от X11, клиент
Wayland не может читать чужие окна и подделывать в них ввод. X11 не пробрасывается никогда. `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
запускается с этой группой — под правило попадают агент и всё, что он запускает.
## Модель безопасности и ограничения
Открытые проблемы и планы (на английском): [docs/known-issues.md](docs/known-issues.md).
Цель — удержать агента, который ошибается (не тот путь, `rm -rf` не там, запрос в прод), а
не остановить целенаправленного злоумышленника.
- **Docker — выход наружу.** Агенту доступен сокет Docker, а Docker может смонтировать любой
путь хоста (`docker run -v /:/host`). Если это важно — не давайте агенту Docker; закрыть
это можно фильтрующим прокси для сокета или rootless Podman.
- **Общее ядро.** bubblewrap — это namespaces, а не виртуальная машина.
- **Разрешённые хосты — канал утечки.** Данные могут уйти на любой хост, который пропускают
правила.
- **Проект доступен на запись.** Ошибки внутри него настоящие; cc-safety-net блокирует
типичные разрушительные команды, git хранит историю — коммитьте чаще.
- **Файлы, которые вы запускаете сами.** Хуки, конфиги и метаданные git защищены (см. выше),
но код агент правит: изменённый `Makefile`, скрипт в `package.json` или файл в `bin/`
выполнится с вашими правами, когда вы его запустите. Смотрите `git diff`, прежде чем
запускать команды проекта на хосте.
- **Подложенные репозитории обнаруживаются, а не предотвращаются.** `.git`, созданный агентом
в новом каталоге или новом worktree, найдёт проверка после сессии; не запускайте там git,
пока не посмотрели.
- С `OCSB_TMP=shared` доступны `/tmp` хоста и его сокеты.
- С `OCSB_DISPLAY=1` агент может открывать окна на вашем экране и пользоваться буфером обмена,
пока его окно в фокусе.
## Решение проблем
| Симптом | Причина и что делать |
|---|---|
| `Read-only file system`, не стартует MCP-сервер или инструмент | путь вне разрешённых на запись. После каждой сессии (и в `doctor`) `run` перечисляет пути, упёршиеся в «только чтение», из лога OpenCode с готовой строкой, например `OCSB_RW_HOME+=(.local/share/pnpm)`; добавьте её в `sandbox.conf` (или `local/sandbox.conf`) и перезапустите. Остальное — `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` |
| браузерные инструменты падают (`Missing X server`, `Failed to connect to Wayland display`) | в песочнице нет экрана: включите `OCSB_DISPLAY=1` (сессия Wayland) или запускайте браузер headless (Playwright MCP: `--headless`) |
| не найти файл, который агент положил в `/tmp` | он в `~/.local/share/opencode-sandbox/<имя>/tmp` |
| `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 trust [каталог] доверять существующему .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)