Агент-инженер по репозиторию · Модуль 2 · Урок 2.3
Read-only git: инструмент git diff
Зачем (какую проблему чиним)
Агент должен понимать состояние изменений: что уже поменяно в рабочем дереве, что в истории. Это нужно и для ревью (версия, где агент комментирует чужой diff), и как фундамент для версии v3, где агент сам будет писать код и проверять минимальность своего диффа. Добавим read-only доступ к git.
Решение и альтернативы
Решение: инструмент git_diff запускает git как внешний процесс через os/exec с явным списком аргументов (никогда через строку shell), только для безопасных read-команд (diff, status, log, show). Это первый случай запуска внешней программы — и мы сразу вводим правило, которое в уроке 2.4 станет полноценным allowlist.
Альтернативы: go-git (чистый Go) — мощно, но тяжеловесно для наших нужд и расходится в поведении с системным git, к которому агент будет коммитить позже. sh -c "git diff " + args — категорически нет: конкатенация в shell-строку = инъекция команд. Запускаем бинарь напрямую с массивом аргументов.
DIFF
Добавляем безопасный запуск git: фиксированный бинарь, аргументы массивом, рабочая директория — корень репозитория, таймаут по контексту.
⚠ Безопасность
exec.CommandContext(ctx, "git", args...) — да; exec.Command("sh", "-c", str) — никогда. Передача аргументов массивом исключает инъекцию команд (нет шелл-интерпретации). Подкоманда git проверяется по списку разрешённых (diff/status/log/show) — write-команды (commit/push/checkout) на версии v1 запрещены. Таймаут через контекст не даёт зависшему git заблокировать агента.
Проверка
Внесите правку в файл, спросите «покажи текущий diff» — агент вызывает git_diff и пересказывает изменения. Попытка передать подкоманду commit отклоняется на версии v1.
Глубже
Песочницы и изолированные среды исполнения инструментов — курс «Продакшн-разработка», Модуль 4 (гл. 8) и Модуль 5 (безопасность, гл. 11).
+package main
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "os/exec"
+)
+
+// readonlyGit — подкоманды git, разрешённые на версии v1 (только чтение).
+var readonlyGit = map[string]bool{"diff": true, "status": true, "log": true, "show": true}
+
+// runGit запускает git как процесс с аргументами-МАССИВОМ (без shell!).
+func runGit(ctx context.Context, root string, args ...string) (string, error) {
+ if len(args) == 0 || !readonlyGit[args[0]] {
+ return "", fmt.Errorf("подкоманда git запрещена на этой версии: %v", args)
+ }
+ cmd := exec.CommandContext(ctx, "git", args...) // НЕ "sh -c": нет инъекции
+ cmd.Dir = root
+ var out, errb bytes.Buffer
+ cmd.Stdout, cmd.Stderr = &out, &errb
+ if err := cmd.Run(); err != nil {
+ return "", fmt.Errorf("git %v: %s", args, errb.String())
+ }
+ return out.String(), nil
+}Anti-patterns
| Грабля | Почему плохо | Как правильно |
|---|---|---|
exec.Command("sh", "-c", "git "+userArg) | Конкатенация в shell-строку = инъекция команд (; rm -rf) | exec.CommandContext(ctx, "git", args...) с аргументами массивом |
| Разрешить любые подкоманды git сразу | commit/push/checkout меняют состояние — на версии read-only это нарушение инварианта | Allowlist read-only подкоманд (diff/status/log/show); запись — позже и в изоляции (версия v3) |
| Запуск без таймаута/контекста | Зависший git блокирует воркер агента бесконечно | CommandContext + дедлайн: отмена убивает процесс |
Практическое задание (RA-v1)
- Добавить
git_diffповерхrunGitс allowlist read-only подкоманд и таймаутом по контексту. - Передавать аргументы git только массивом; запретить любую форму shell-конкатенации.
- Закоммитить:
git commit -m "v1: read-only git_diff tool".
Проверка знаний
Инструмент собирает команду как exec.Command("sh", "-c", "git diff "+arg), где arg приходит из аргументов модели.
Какая проблема и как исправить?
Верный ответ: B
B. Конкатенация недоверенного аргумента в sh -c позволяет вставить ; rm -rf ~ и т.п. Запуск бинаря напрямую с массивом аргументов исключает shell-интерпретацию. Логирование (D) полезно, но проблему инъекции не решает.