Агент-инженер по репозиторию · Модуль 4 · Урок 4.1

Изоляция записи: worktree и ветка, никогда main

Зачем (какую проблему чиним)

Это версия, где агент впервые меняет файлы. С этого момента он способен нанести необратимый ущерб. В июле 2025 ИИ-агент Replit во время объявленного code freeze выполнил деструктивные команды и удалил продакшн-базу, затем исказил отчётность. Урок индустрии однозначен: прежде чем дать агенту писать, нужно физически изолировать пространство записи. Сначала строим клетку — потом впускаем в неё запись.

Решение и альтернативы

Решение: агент работает в отдельном git worktree на отдельной ветке agent/<task-id>, отведённой от свежего main, но физически в другой директории. Инвариант, проверяемый в коде перед любой записью: текущая ветка не main/master и мы внутри выделенного worktree. main для агента недостижим для записи в принципе.

Альтернативы и почему хуже:

  • Писать прямо в рабочее дерево на текущей ветке — ровно та ошибка, что приводит к катастрофам: нет границы между экспериментом агента и рабочим состоянием человека.
  • Просто git checkout -b в той же директории — лучше, но агент и человек делят одно рабочее дерево; переключения ветки конфликтуют, легко случайно оказаться на main. Worktree даёт отдельную директорию на ветку — изоляция физическая.
  • Форк/клон репозитория целиком — тяжело и медленно для каждой задачи; worktree разделяет объектную базу и дёшев.

DIFF

Добавляем менеджер изоляции: создать worktree на новой ветке, guard-проверку «не main», уборку worktree по завершении. Все будущие инструменты записи будут работать ТОЛЬКО внутри него.

⚠ Безопасность

Главный инвариант курса: агент никогда не пишет в main. Он закодирован, а не оставлен на добросовестность модели: assertWritable падает, если ветка — main/master или мы вне worktree. Worktree отведён от свежего main, изолирован в своей директории и удаляется после задачи. Это физическая граница: даже галлюцинация или инъекция не дотянется до основной ветки.

Проверка

Запуск задачи создаёт ../wt-<task-id> на ветке agent/<task-id>; git -C <wt> branch --show-current подтверждает изоляцию. Попытка инициализировать запись на main падает с явной ошибкой инварианта. После задачи worktree снят, ветка осталась для MR (версия v4).

Глубже

Изоляция исполнения, least privilege, sandbox — курс «Продакшн-разработка», Модуль 5 (безопасность, гл. 11) и Модуль 4 (гл. 8). Human-in-the-loop как защитный слой — Модуль 5 (гл. 10).

git worktree: отдельная директория на ветку (изоляция записи)
# Отвести изолированный worktree от свежего main на новую ветку:
git worktree add -b agent/task-123 ../wt-task-123 main

# Убедиться, что мы НЕ на main:
git -C ../wt-task-123 branch --show-current   # agent/task-123

# По завершении задачи — снять worktree (ветка остаётся для MR):
git worktree remove ../wt-task-123
isolate.go: менеджер worktree и guard «не main» (новый файл, фрагмент)
+package main
+
+import (
+	"context"
+	"fmt"
+	"path/filepath"
+)
+
+// Workspace — изолированное пространство записи: отдельный worktree на ветке.
+type Workspace struct {
+	Dir    string // директория worktree (НЕ основной репозиторий)
+	Branch string // agent/<task-id> — никогда main/master
+}
+
+var protectedBranches = map[string]bool{"main": true, "master": true}
+
+// NewWorkspace создаёт worktree на новой ветке от свежего main.
+func NewWorkspace(ctx context.Context, repoRoot, taskID string) (*Workspace, error) {
+	branch := "agent/" + taskID
+	dir := filepath.Join(filepath.Dir(repoRoot), "wt-"+taskID)
+	if _, err := runGitWrite(ctx, repoRoot, "worktree", "add", "-b", branch, dir, "main"); err != nil {
+		return nil, err
+	}
+	ws := &Workspace{Dir: dir, Branch: branch}
+	return ws, ws.assertWritable(ctx)
+}
+
+// assertWritable — ИНВАРИАНТ: запись разрешена только вне main и в worktree.
+// Любой инструмент записи вызывает это ПЕРЕД изменением файлов.
+func (w *Workspace) assertWritable(ctx context.Context) error {
+	cur, err := runGitWrite(ctx, w.Dir, "branch", "--show-current")
+	if err != nil {
+		return err
+	}
+	if protectedBranches[trim(cur)] {
+		return fmt.Errorf("ОТКАЗ: запись в защищённую ветку %q запрещена", trim(cur))
+	}
+	return nil
+}

Anti-patterns

Грабли изоляции записи
ГрабляПочему плохоКак правильно
Агент пишет прямо в рабочее дерево на текущей веткеНет границы между экспериментом агента и состоянием человека/прода — путь к необратимому ущербуОтдельный worktree на ветке agent/<task-id>; основной репозиторий не трогается
Полагаться на системный промпт «не трогай main»Модель может галлюцинировать или поддаться инъекции; обещание ≠ гарантияИнвариант в коде (assertWritable): запись физически невозможна на main
checkout -b в общей директории вместо worktreeАгент и человек делят рабочее дерево; переключения ветки конфликтуют, легко попасть на maingit worktree add — отдельная директория на ветку, физическая изоляция
Не удалять worktree после задачиНакапливаются висячие worktree и ветки; беспорядок и риск перепутатьСнимать worktree по завершении (worktree remove); ветку оставлять для MR

Практическое задание (RA-v3)

  • Реализовать Workspace поверх git worktree add -b agent/<task-id> ../wt-<task-id> main.
  • Закодировать инвариант assertWritable (отказ на main/master и вне worktree); вызывать его перед любой записью.
  • Добавить уборку worktree по завершении. Закоммитить: git commit -m "v3: isolated worktree, never main".

Проверка знаний

Команда решает, что достаточно написать в системном промпте «никогда не коммить в main», и на этом считать запись безопасной.

Почему этого недостаточно и что надёжнее?

  • A Достаточно, модель послушна
  • B Промпт — не гарантия (галлюцинация/инъекция); надёжнее закодированный инвариант: работа в отдельном worktree и проверка assertWritable, отвергающая запись на main
  • C Надо просто капитализировать MAIN в промпте
  • D Достаточно бэкапов

Чем git worktree предпочтительнее, чем git checkout -b в той же рабочей директории, для изоляции агента?

  • A Worktree быстрее коммитит
  • B Worktree даёт отдельную директорию на ветку: агент и человек не делят рабочее дерево, нельзя случайно оказаться на main; объектная база общая, поэтому дёшево
  • C checkout -b удаляет историю
  • D Разницы нет