fix(agents): add safe read-only commands to reviewer bash allow-list (#193)

* fix(agents): add safe read-only commands to reviewer bash allow-list

* docs(adr): add ADR-085 reviewer allow-list expansion rationale

* docs(handoff): add handoff for reviewer allow-list expansion

* docs(handoff): set PR number

---------

Co-authored-by: opencode-agent <agent@opencode.local>
This commit is contained in:
Sergey 2026-08-01 00:01:57 +03:00 committed by GitHub
parent 56bd356951
commit f0701558d3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 93 additions and 0 deletions

View file

@ -22,8 +22,14 @@ permission:
"cat *": allow
"head*": allow
"tail*": allow
"sed -n *": allow
"wc*": allow
"diff*": allow
"date": allow
"date *": allow
"sort": allow
"sort *": allow
"sort * -o *": deny
"gh pr diff*": allow
"gh pr checkout*": allow
"gh issue*": allow

View file

@ -0,0 +1,57 @@
# ADR-085: Expand reviewer bash allow-list with safe read-only commands
## Статус
Accepted (2026-07-31, PR#193)
## Контекст
Анализ логов `~/.local/share/opencode/log/opencode.log` (202 740 строк) выявил 313 срабатываний catch-all `"*": deny` в frontmatter `reviewer.md` (`permission.bash`). Среди заблокированных — безопасные read-only inspection-команды, которые не нужны для write-bypass, но мешают ревьюеру продуктивно работать:
- `date +%Y-%m-%d` — временные метки (лог: line 4877)
- `sed -n '305,320p' .opencode/opencode.json` — печать диапазона строк (лог: line 9955)
- `sort` — сортировка в stdout (лог: line 3148)
- `git ls-files .opencode/skills/` — листинг файлов в индексе (лог: line 9773) — `git ls-files*` уже в allow-list (строка 66); срабатывание catch-all вероятно артефакт устаревшей версии opencode или нюанс matching-движка. Паттерн `git ls-files*` формально верен и не требует правки.
Catch-all `"*": deny` — by design: read-only контракт ревьюера (`edit: deny` + whitelist). ADR-005 убрал `echo *` чтобы закрыть bypass `echo "x" > file.txt`. Но catch-all блокирует и команды, не создающих write-bypass, снижая продуктивность ревьюера.
## Решение
Расширить allow-list в `.opencode/agents/reviewer.md` секция `permission.bash`, добавив безопасные read-only команды (findLast — последние правила побеждают; новые allow стоят после catch-all `"*": deny`):
- `date` — allow
- `date *` — allow
- `sed -n *` — allow (print-mode: `-n` подавляет авто-вывод, `p` печатает; без `-n` sed печатает все строки + может `w`/`i`/`c` команды → write-bypass)
- `sort` — allow
- `sort *` — allow
- `sort * -o *` — deny (defense-in-depth: `sort -o out.txt` пишет файл через процесс, обход `edit: deny` — прямой аналог ADR-005 про echo). Поставлен ПОСЛЕ `sort *: allow`: findLast → deny побеждает для `-o` варианта, allow побеждает для остальных.
НЕ добавлены команды с write-bypass риском:
- `echo *` (ADR-005 — `echo "x" > file.txt`)
- `cp` (mutation)
- `awk` (может `>` писать)
- `xxd` (может `-r` писать)
- `sed` без `-n` (может `w`/`i`/`c` команды писать файлы)
Паттерн `git ls-files*` оставлен без изменений — формально корректен, срабатывание в логе не воспроизведено.
### Risk Assessment
**`sed -n *` с командой `w`:** `sed -n '1p; w out.txt'` пишет файл внутренними средствами sed. Нельзя полностью закрыть через glob (команда `w` embedded в sed-скрипт, не отдельный аргумент). Риск принят как низкий:
- Редкая конструкция (reviewer использует `sed -n` для печати диапазонов, не для записи).
- Альтернатива (`cat`/`head`/`tail` для диапазона) неудобна для больших файлов (нужен offset+limit, `head`/`tail` требуют byte-count или line-count, а не range).
- `cat *` уже в allow-list и тоже может `cat > file` через shell redirect — но shell redirect обрабатывается shell, не командой; `sed -n ... w file` — внутренняя команда sed. Аналогичный уровень риска.
- Defense-in-depth: `edit: deny` остаётся; write-bypass через `sed -n ... w` теоретически возможен, но reviewer не имеет мотивации писать файлы (read-only агент).
**`date --set`:** `date --set="2024-01-01"` меняет системное время. Риск принят:
- В контейнере (основной deployment) обычно нет root — `date --set` упадёт с permission denied.
- На хосте нужен `sudo` (не в allow-list → deny).
- `date *: allow` допускает `--set`, но альтернатива (`date --set*: deny` после `date *: allow`) создаёт избыточное правило для нереалистичного сценария. Решено задокументировать риск здесь, а не множить deny-правила.
- `date` без аргументов и `date +FORMAT` (основные use-cases ревьюера) — безопасны.
## Альтернативы
- **Не добавлять `sed -n`** — отклонено: `head`/`tail` требуют byte/line count, а не range; `sed -n '305,320p'` — стандартный способ печати диапазона строк. Без него ревьюер вынужден `cat` весь файл и искать глазами, что не работает для больших файлов.
- **Запретить `sed -n ... w` через glob** — отклонено: glob-matching opencode не поддерживает гранулярные опции внутри аргумента (команда `w` embedded в sed-скрипт). Аналогично ADR-005 про echo: "bash permission matching не поддерживает гранулярные операторы".
- **Добавить `date --set*: deny`** — отклонено: избыточное правило для нереалистичного сценария (нужен root/sudo, которых нет в allow-list). Документирование риска в ADR предпочтительнее множить deny-правила.
- **Убрать catch-all `"*": deny`** — отклонено: catch-all by design (read-only контракт, defense-in-depth). Расширение allow-list точечно, без ослабления perimeter.
- **Параллельно расширить allow-list для других агентов (general, docs-reviewer)** — вне scope (issue #191 явно ограничен reviewer.md).
## Примечание о нумерации ADR
Спека issue #191 в секции "Контракты" указывала номер ADR как `084` (со словами "номер = max(existing)+1 = 084, фикс ADR-079"). Фактически `084` уже занят (`084-pr-192-add-boosty-chat-id-env.md`, PR#192). Согласно ADR-079 (нумерация = max(existing)+1, не count+1), следующий свободный номер = `085`. Использован `085`. Опечатка в спеке зафиксирована в handoff, продолжено без отклонений.

View file

@ -0,0 +1,30 @@
---
pr: 193
title: fix(agents): add safe read-only commands to reviewer bash allow-list
---
## Что сделано
- Расширён bash allow-list в `.opencode/agents/reviewer.md` безопасными read-only командами:
- `date` и `date *` — временные метки (лог показал 313 срабатываний catch-all, среди них `date +%Y-%m-%d`)
- `sed -n *` — печать диапазона строк без авто-вывода (`-n` подавляет, `p` печатает)
- `sort` и `sort *` — сортировка в stdout
- `sort * -o *: deny` — defense-in-depth: `sort -o out.txt` пишет файл через процесс, обход `edit: deny` (аналог ADR-005 про echo). Поставлен ПОСЛЕ `sort *: allow` (findLast → deny побеждает для `-o` варианта, allow для остальных).
- Паттерн `git ls-files*` оставлен без изменений (уже присутствует в allow-list, строка 66). Проверка glob-matching в opencode: `git ls-files*` без пробела матчит `git ls-files` (без аргументов) и `git ls-files<anything>` включая `git ls-files .opencode/skills/` — паттерн корректен. Срабатывание catch-all в логе — вероятно артефакт устаревшей версии или другой причины; паттерн формально верен и не требует правки.
- Создан ADR-085 (`docs/decisions/085-pr-193-reviewer-bash-allowlist.md`) с обоснованием и risk-assessment для `sed -n ... w` и `date --set`.
- Guard-скрипт `check-permissions.py` запущен — новые правила не флагуются как опасные (exit 0).
- Global mirror `~/.config/opencode/agents/reviewer.md` синхронизирован через skill `configure-opencode` (edit in workspace → commit → push → pull на хосте; НЕ прямой правкой, согласно ADR-078).
## Почему
Анализ `opencode.log` (202 740 строк) выявил 313 срабатываний catch-all `"*": deny` в `reviewer.md`. Среди заблокированных — безопасные read-only inspection-команды, которые не создают write-bypass, но мешают ревьюеру продуктивно работать (печать диапазона строк, сортировка, временные метки). Catch-all by design (ADR-005 убрал `echo *` чтобы закрыть bypass), но он блокирует и безопасные команды. Расширение allow-list восстанавливает продуктивность ревьюера без ослабления read-only контракта.
## Pending
- После merge: `git pull` на хосте + рестарт opencode (MCP/agents грузятся при старте) — правки в `reviewer.md` не видны до рестарта.
- Для Docker-сетапа: `git pull` + `docker compose restart opencode`.
## Watch out
- `sed -n *` с командой `w` (`sed -n '1p; w out.txt'`) пишет файл внутренними средствами sed — нельзя закрыть через glob. Риск принят как низкий (редкая конструкция, альтернативы `cat`/`head`/`tail` неудобны для больших файлов). См. ADR-085 Risk Assessment.
- `date --set="..."` меняет системное время. В контейнере обычно нет root, на хосте нужен sudo (не в allow-list). Риск принят. См. ADR-085.
- `sort * -o *: deny` должен стоять ПОСЛЕ `sort *: allow` (findLast). При перестановке правил deny перестанет работать.
- НЕ добавлены write-bypass команды: `echo *` (ADR-005), `cp`, `awk` (может `>`), `xxd` (может `-r`), `sed` без `-n` (может `w`/`i`/`c`).
- `edit: deny` и catch-all `"*": deny` сохранены — read-only контракт ревьюера не нарушен.
- Спека issue #191 содержала опечатку: номер ADR указан как `084` (max+1), но `084` уже занят (`084-pr-192-add-boosty-chat-id-env.md`). Использован `085` (фактический max+1 согласно ADR-079). Зафиксировано, продолжено без отклонений.