Агент-инженер по репозиторию · Модуль 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).

git.go: безопасный запуск git (read-only подкоманды), новый файл
+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

Грабли запуска git
ГрабляПочему плохоКак правильно
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 приходит из аргументов модели.

Какая проблема и как исправить?

  • A Проблем нет, sh удобнее
  • B Инъекция команд через shell-строку; запускать бинарь напрямую с аргументами-массивом (exec.CommandContext(ctx, "git", args...))
  • C Надо добавить sudo
  • D Надо логировать команду