opencode-config/docs/decisions/016-pr-40-permissions-lock.md
Sergey f9a9e0f854
feat(permissions): deny direct git/gh calls + role-based tool access (#40)
* feat(permissions): deny direct git/gh calls and add role-based tool access

* feat(permissions): update docs-reviewer to use commit tool

* test(permissions): add tests for deny rules and tool access

* docs(handoff): add handoff and ADR for permissions lock

* docs: update project map + handoff + ADR

---------

Co-authored-by: opencode-agent <agent@slaid098.dev>
2026-07-24 04:34:20 +03:00

47 lines
No EOL
5.2 KiB
Markdown
Raw Permalink 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.

# ADR-016: Deny direct git/gh bash calls + role-based tool access
## Статус
Accepted (2026-07-24)
## Контекст
PR #38 добавил 3 детерминированных TS tool'а (`commit`, `create_pr`, `create_issue`) с встроенной валидацией форматов. Но агенты могли всё ещё вызывать `git commit`/`gh pr create`/`gh pr merge`/`gh issue create` через raw bash (разрешено в `permission.bash`), обходя валидацию tools. Это нарушало pure-orchestrator model (ADR-010/PR#30) и контракты форматов (AGENTS.md rules не enforced).
opencode имеет два независимых уровня permissions:
1. `permission.bash` — glob-паттерны на bash-команды (findLast — последние побеждают). Custom TS tools bypass через `spawnSync` (trusted code, не проходит через `permission.bash`).
2. `agent.<name>.tools` — boolean map per-agent (включает/отключает конкретные tools для конкретного агента).
Нужно: (1) заблокировать прямые bash-вызовы мутаций, не блокируя tools, (2) настроить role-based access — кто из subagent'ов какие tools может вызывать.
## Решение
### 1. Global deny rules в `permission.bash` (`.opencode/opencode.json`)
4 deny-правила в КОНЕЦ секции `bash` (findLast — last wins, перекрывают более ранние allow):
- `"git commit *": "deny"` — блокирует прямой `git commit`, `commit` tool bypass'ит
- `"gh pr create *": "deny"` — блокирует прямой `gh pr create`, `create_pr` tool bypass'ит
- `"gh pr merge *": "deny"` — блокирует прямой `gh pr merge`, `merge_pr` tool bypass'ит
- `"gh issue create *": "deny"` — блокирует прямой `gh issue create`, `create_issue` tool bypass'ит
`git push *` остаётся `allow` (НЕ добавлен deny — push нужен для пуша веток).
### 2. Role-based tool access в `agent.<name>.tools`
| Agent | commit | create_pr | create_issue | merge_pr |
|---|---|---|---|---|
| general | ✅ | ✅ | ✅ | ❌ |
| reviewer | ❌ | ❌ | ❌ | ❌ |
| docs-reviewer | ✅ | ❌ | ❌ | ❌ |
| memory-syncer | ❌ | ❌ | ❌ | ❌ |
| main (orchestrator) | ✅ | ✅ | ✅ | ✅ (наследует global all-true) |
Main agent НЕ указан в `agent` секции → наследует global tools (all true by default). Явные restrictions только для subagents.
### 3. docs-reviewer frontmatter + prompt
`"git commit*": allow` убран из frontmatter (global deny покрывает). Prompt body: `git commit -m ...``commit({ message: ... })` в 3 местах. docs-reviewer теперь использует `commit` tool для коммита project map/handoff.
### 4. `DANGEROUS_PATTERNS` — БЕЗ ИЗМЕНЕНИЙ
Новые deny rules не требуют deterministic guards: (1) deny-действия не триггерят violations (скрипт флагует только `allow`), (2) существующие `allow` rules оставлены в opencode.json (перекрыты deny через findLast), добавление guard флагнуло бы легитимные allow. `gh pr merge*` уже имеет guard (scope=agent).
## Альтернативы
- **Удалить существующие allow rules вместо добавления deny** — отклонено: спека issue #39 явно говорит "добавить deny rules в конец" (findLast), не "удалить allow". Удаление allow нарушило бы другие use cases (e.g. `gh issue*` allow покрывает `gh issue view`, `gh issue list`). Deny в конце точечно перекрывает только create-команды.
- **Добавить DANGEROUS_PATTERNS для `git commit *: allow`/`gh pr create *: allow`/`gh issue create *: allow`** — отклонено: флагнуло бы существующие легитимные allow rules выше по файлу (которые перекрыты deny через findLast, но всё ещё присутствуют в JSON). CI упал бы на валидных конфигах. Глобальный `gh pr merge*: allow` намеренно оставлен для main agent (guard scope=agent пропускает global).
- **Per-agent `git commit*: deny` в frontmatter вместо global** — отклонено: global deny покрывает всех агентов одной строкой. Per-agent deny в 4 frontmatter файлах = дублирование. Global + defense-in-depth per-agent (только для `gh pr merge*`, уже есть) — достаточно.
- **Запретить `git push *` тоже** — отклонено: push нужен для пуша feature-веток в remote (PR workflow). Push НЕ мутация в том же смысле, что commit/create/merge — push синхронизирует локальные коммиты с remote. Issue явно запрещает deny на `git push *`.