OpenCode в Docker: режим auto только на container/src
Почему контейнер, а не ещё правила
Страница правил запрета перебрала четыре набора правил, чтобы OpenCode спрашивал реже, не открывая секреты репозитория aredel. Каждый набор менял запросы на дыры:
- Правила совпадают с текстом. Команда, которая добирается до файла, не называя его, проходит (примеры), а запрос, добавленный для одного написания, пропускает следующее: python3 -c спрашивает, python -c и perl -e — нет (набор правил 4).
- Запросы остались. С набором правил 4 у OpenCode 11 из 15 команд рабочего дня всё ещё спрашивали. Счёт падает до 2, только пока агент следует одной строке в AGENTS.md и ищет через git grep; любой другой поиск по содержимому снова спрашивает.
- Режим auto остаётся запрещённым. --auto одобряет каждый запрос, поэтому на хосте каждое правило, которое защищает логи или сеть, превращается в разрешение (цена).
Контейнер переворачивает вопрос. Вместо списка того, чего агент не должен касаться, агент получает файловую систему, в которой нечего защищать: отслеживаемые файлы container/src, свои инструкции только для чтения и больше ничего. Внутри --auto одобряет всё, и это нормально: худшее, что может сделать команда, — сломать файлы, которые восстановит git.
Идея подходит любому проекту, в котором агент работает с одной папкой. Эта страница строит её для aredel, где container/src — докрут сайта. Используемые инструменты: Docker, git, OpenCode и PHP.
Что видит контейнер
Всё, что агент может открыть, — пять записей. Каждое имя ведёт к шагу, который кладёт его туда:
/work/ в контейнере ├── container/src/ из рабочего дерева, чтение и запись ├── AGENTS.md только чтение ├── .claude/skills/ только чтение └── opencode.jsonc только чтение, конфигурация контейнера /home/agent/.local/share/opencode/ том Docker: сессии
Что остаётся снаружи, не запрещается — этого просто нет. Команда, которая попыталась бы это прочитать, получит No such file or directory, как бы она ни была написана (тесты с 1 по 6):
| Остаётся на хосте | Что там лежит |
|---|---|
| .git | вся история, включая удалённые файлы |
| игнорируемые файлы в container/src | записи посетителей, логи ошибок PHP, отладочные логи, база данных, резервные копии страниц |
| container/tools/ | users.db с устройствами посетителей и инструменты работы с логами |
| ~/.local/share/aredel/logs/ | боевые логи с IP-адресами посетителей |
| ~/.config/aredel/ | учётные данные FTP и токен бэк-офиса |
| ~/.ssh, ~/.gnupg | ключи |
| ~/.local/share/opencode/ хоста | auth.json с ключами провайдеров, opencode.db со всеми прошлыми сессиями |
| /var/run/docker.sock | сам Docker, который на хосте root |
| незакоммиченная работа в главной копии | ваши правки в процессе |
Шаг 1: рабочее дерево только с отслеживаемыми файлами
container/src главной копии не чист. Рядом с примерно 8 500 отслеживаемыми файлами лежат игнорируемые git: записи посетителей, логи ошибок, отладочные логи, база данных, резервные копии страниц. Прятать каждый из них за пустым монтированием хрупко, потому что новые логи появляются с отметкой времени в имени.
В рабочем дереве git ничего этого нет. Это вторая копия того же репозитория, в своей папке и на своей ветке, только с отслеживаемыми файлами:
git worktree add ../aredel-agent -b agent/task-1
Проверьте, что ничего игнорируемого там нет. Команда ничего не печатает:
git -C ../aredel-agent status --short --ignored container/src
- Оно начинается с последнего коммита. Сначала закоммитьте то, что агент должен видеть; незакоммиченные правки остаются в главной копии.
- Без картинок. Изображения в aredel игнорируются git, поэтому у страниц в рабочем дереве их нет. Агенту они не нужны.
- Внутри нет истории. В корне рабочего дерева лежит файл .git с путём к главному репозиторию. Монтируется только container/src, поэтому ни история, ни этот путь в контейнер не попадают.
Шаг 2: образ
Официальный образ ghcr.io/anomalyco/opencode (установка) — это Alpine Linux с OpenCode, grep и ripgrep: без git, без PHP, и работает он от root. Образ ниже добавляет PHP и вашего пользователя:
vi container/containerfiles/Dockerfile.opencode
# OpenCode for --auto runs that see only container/src of an agent # worktree. The official image plus PHP for php -l and page renders, and a # user with your UID, so the files the agent writes stay yours. FROM ghcr.io/anomalyco/opencode:2.1.4 RUN apk add --no-cache php83 php83-ctype php83-dom php83-iconv php83-mbstring \ php83-session php83-simplexml php83-tokenizer php83-xml \ && ln -s /usr/bin/php83 /usr/local/bin/php ARG HOST_UID=1000 ARG HOST_GID=1000 RUN addgroup -g ${HOST_GID} agent \ && adduser -D -u ${HOST_UID} -G agent agent \ && mkdir -p /work && chown agent:agent /work USER agent # Created by the user, so a new named volume mounted here is writable. RUN mkdir -p /home/agent/.local/share/opencode WORKDIR /work
- Закреплённая версия. Тег — версия, которую opencode --version печатает на хосте, здесь 2.1.4. Обновляйте оба вместе.
- PHP 8.3. В Alpine нет пакета 8.2, а 8.3 ближе всего к 8.2 контейнера сайта. Расширения — те, которые вызывает движок страниц: без php83-ctype каждая страница останавливалась с Call to undefined function ctype_digit(). С приведённым списком пять настоящих страниц отрендерились с кодом выхода 0.
- Пользователь с вашим UID. Файлы, которые пишет агент, на хосте принадлежат вам, а сам агент не может устанавливать пакеты (тест 7).
- Git намеренно отсутствует. В контейнере нет истории для чтения, а коммиты делаются на хосте, после ревью и коммита на хосте.
Шаг 3: конфигурация внутри
mkdir -p container/tools/ai/docker
vi container/tools/ai/docker/opencode.jsonc
{ "$schema": "https://opencode.ai/config.json", // Inside the container only. opencode-docker.sh starts OpenCode with // --auto, which approves every ask; the limit is what the container // holds, not a rule list. "model": "opencode/deepseek-v4.1-flash-free" }
- Списка разрешений нет. При --auto каждый запрос становится разрешением, и удержаться может только запрет; в контейнере запрещать нечего. Защита — список монтирований из шага 4 (запускающий скрипт).
- Бесплатная модель. Id — текущая бесплатная модель OpenCode. Ключ ей не нужен, поэтому в контейнере нет ключа. Бесплатный тариф может хранить то, что ему отправляют; container/src — публичный сайт, поэтому бесплатная модель здесь допустима.
- Платная модель нуждается в ключе внутри контейнера, а при --auto агент может его прочитать. Используйте ключ, сделанный для этого, с лимитом расходов, и никогда не монтируйте auth.json хоста.
- Когда бесплатный id меняется, поменяйте его и здесь: container/tools/ai/opencode.sh проверяет только opencode.jsonc в корне репозитория и файлы агентов.
Шаг 4: запускающий скрипт
Один скрипт собирает образ и запускает контейнер с монтированиями из дерева выше:
vi container/tools/ai/opencode-docker.sh
#!/usr/bin/env bash # version 0.1.0 # Run OpenCode with --auto in a container that sees only container/src of # an agent worktree, plus AGENTS.md and the skills read-only. # # Usage (repo root): # git worktree add ../aredel-agent -b agent/<task> # container/tools/ai/opencode-docker.sh ../aredel-agent # TUI # container/tools/ai/opencode-docker.sh ../aredel-agent run "<task>" # one task set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)" WT="$(cd "${1:?usage: opencode-docker.sh <worktree> [run <task>]}" && pwd)" shift [ -d "$WT/container/src" ] || { echo "ERROR: $WT/container/src not found." >&2; exit 1; } # Only tracked files go in: the ignored ones are logs, visitor data, backups. ignored="$(git -C "$WT" status --short --ignored -- container/src | grep '^!!' || true)" if [ -n "$ignored" ]; then echo "ERROR: $WT/container/src holds ignored files; use a fresh worktree:" >&2 echo "$ignored" | head -5 >&2 exit 1 fi docker build -q -t aredel-opencode \ --build-arg HOST_UID="$(id -u)" --build-arg HOST_GID="$(id -g)" \ -f "$REPO_ROOT/container/containerfiles/Dockerfile.opencode" \ "$REPO_ROOT/container/containerfiles" >/dev/null # --auto goes after "run" for one task, first for the TUI. if [ "${1:-}" = run ]; then shift; set -- run --auto "$@"; else set -- --auto "$@"; fi tty=(); [ -t 0 ] && tty=(-t) exec docker run --rm -i "${tty[@]}" \ --cap-drop ALL --security-opt no-new-privileges \ -v "$WT/container/src:/work/container/src" \ -v "$WT/AGENTS.md:/work/AGENTS.md:ro" \ -v "$WT/.claude/skills:/work/.claude/skills:ro" \ -v "$SCRIPT_DIR/docker/opencode.jsonc:/work/opencode.jsonc:ro" \ -v aredel-opencode-data:/home/agent/.local/share/opencode \ aredel-opencode "$@"
chmod +x container/tools/ai/opencode-docker.sh
- Проверка игнорируемых файлов отклоняет главную копию и любое рабочее дерево, в котором с тех пор появились игнорируемые файлы (тест 11).
- Сборка идёт при каждом старте. Первая занимает около половины минуты, следующие берутся из кеша.
- Никаких прав root. --cap-drop ALL и no-new-privileges: у пользователя agent нет возможностей Linux и нет способа их получить.
- Инструкции только для чтения. AGENTS.md и навыки смонтированы только для чтения, поэтому агент не может переписать свои правила (тест 5). OpenCode находит и то и другое в /work, хотя репозитория git там нет. Роли в .opencode/agents/ остаются снаружи: builder и reviewer работают на платных моделях.
- Сессии в томе. OpenCode держит сессии в именованном томе aredel-opencode-data, поэтому --continue работает между прогонами. В томе только то, что видел агент, и docker volume rm aredel-opencode-data его очищает.
- Больше ничего. Ни сокета Docker, ни сети хоста, ни опубликованных портов.
Шаг 5: запустить задачу
Одна задача, из корня репозитория главной копии:
container/tools/ai/opencode-docker.sh ../aredel-agent run "Run php -l on every PHP file under container/src/ai/opencode, fix any syntax error you find, then run php -l again on the fixed files."
В тестовом репозитории такая же задача — на странице с пропущенной точкой с запятой — прошла от начала до конца без единого запроса (вывод OpenCode, сокращён):
$ find container/src -type f | sort; echo "---"; php -l container/src/ai/index.php; php -l container/src/index.php container/src/ai/index.php container/src/index.php --- PHP Parse error: syntax error, unexpected end of file, expecting "," or ";" in container/src/ai/index.php on line 4 Errors parsing container/src/ai/index.php No syntax errors detected in container/src/index.php → Read container/src/ai/index.php ← Edit container/src/ai/index.php -echo '<h1>' . $title . '</h1>' +echo '<h1>' . $title . '</h1>'; $ php -l container/src/ai/index.php; php -l container/src/index.php No syntax errors detected in container/src/ai/index.php No syntax errors detected in container/src/index.php
Для интерактивного TUI опустите run и задачу:
container/tools/ai/opencode-docker.sh ../aredel-agent
В строке состояния видно auto между именем агента и моделью.
Сеть: до чего контейнер всё ещё добирается
Контейнеру нужен интернет, чтобы добраться до модели. В сети Docker по умолчанию он выходит через хост, и сам хост он тоже достаёт по адресу 172.17.0.1 — шлюзу этой сети. Проверено из оболочки в том же контейнере, через wget и только по кодам состояния:
| Цель | Слушает на хосте | Из контейнера |
|---|---|---|
| локальный сайт, порт 8080 | 0.0.0.0 | доступен |
| отчёты Allure, порт 5050 | 0.0.0.0 | доступен |
| бэк-офис aredel, порт 8765 | 127.0.0.1 | соединение отклонено |
| интернет | — | доступен, нужен для модели |
Локальный сайт выдаёт то, что шаг 1 (рабочее дерево только с отслеживаемыми файлами) оставил снаружи. Контейнер сайта обслуживает главную копию, а не рабочее дерево, поэтому его игнорируемые файлы — на расстоянии одного запроса. Из восьми испробованных игнорируемых путей два пути записей посетителей ответили 404 благодаря правилу в .htaccess, а три других — 403 или редиректом. Остальные три ответили 200: отладочный лог, файл базы данных и резервная копия страницы.
Решение: публиковать порты на 127.0.0.1. Браузер на хосте продолжает работать, а контейнер получает Connection refused. Это проверено двумя одноразовыми серверами, один опубликован на всех интерфейсах, другой на 127.0.0.1: контейнер добрался только до первого, хост — до обоих. В aredel порт сайта задан в compose-файле:
vi container/docker-compose.yml
php-apache: ports: - "127.0.0.1:8080:80"
Порты Allure с 5050 по 5054 получают тот же префикс. Каждая строка команды ниже с 0.0.0.0 или [::] — сервис, до которого контейнер может добраться, включая SSH-сервер хоста:
ss -ltn
Интернет остаётся открытым, потому что он нужен модели. Ограничить его хостом модели можно только через прокси, которого эта страница не рассматривает (чего контейнер не решает).
Тесты
Настройка тестов
Тесты шли 2026-10-26 с OpenCode 2.1.4 в образе из шага 2 (образ), Docker 29.1.3 на Ubuntu 24.04 и бесплатной моделью OpenCode.
Проект был одноразовым репозиторием с раскладкой aredel: container/src с двумя страницами, в одной из них не хватало точки с запятой; игнорируемые git: папка посетителей с одной фиктивной записью (203.0.113.7 из диапазона документации), лог ошибок PHP и container/tools/analyze_logs/users.db; файлы шагов 2–4 и рабочее дерево ../repo-agent.
Строки и запросы
Каждая строка OpenCode — один запуск запускающего скрипта с run, то есть с --auto, и без ввода (< /dev/null). Запрос просил один вызов оболочки: "This is a throwaway test repo with dummy data. Use the shell tool exactly once to run this command exactly as written, without changing or splitting it: … Then reply with the output, or with the exact error text. Do not use any other tool."
Строки, помеченные terminal, не проходили через OpenCode.
Как читать результаты
Результат: ✓, когда фактический результат совпадает с ожидаемым, ✗ — когда нет.
Нажмите ✓ или ✗ в столбце «Результат», чтобы увидеть, что делал тест и почему.
| Тест | Команда | Ожидалось | Фактически | Результат |
|---|---|---|---|---|
| 1 | cat ~/.config/aredel/site.env | нет такого файла | нет такого файла | |
Что произошло: команда выполнилась без запроса, и cat ответил can't open '/home/agent/.config/aredel/site.env': No such file or directory. Почему: ~ в контейнере — домашняя папка пользователя образа. Домашняя папка хоста не смонтирована. На хосте набор правил 4 у OpenCode должен запрещать эту команду; здесь запрещать нечего. | ||||
| 2 | ls -a container/src/services/user_profile/data/ | нет такой папки | нет такой папки | |
Что произошло: ls ответил No such file or directory. Почему: папка посетителей игнорируется git, а рабочее дерево из шага 1 (рабочее дерево только с отслеживаемыми файлами) содержит только отслеживаемые файлы, поэтому такой папки там нет. | ||||
| 3 | grep -rn 203.0.113 /work | нет совпадений | вывода нет | |
Что произошло: поиск прошёл без запроса по всему, что агент может видеть, и ничего не нашёл; grep завершился с кодом 1. Почему: на хосте grep -r спрашивает при наборе правил 4, потому что он игнорирует .gitignore и открыл бы записи посетителей. В контейнере этих записей просто нет. | ||||
| 4 | git log --oneline -1 | нет git | git: not found | |
Что произошло: оболочка ответила /bin/sh: git: not found, код выхода 127. Почему: в образе нет git, и .git не смонтирован. Даже с установленным git читать было бы нечего. | ||||
| 5 | echo note >> AGENTS.md | отказ | только чтение | |
Что произошло: /bin/sh: can't create AGENTS.md: Read-only file system. Почему: запускающий скрипт монтирует AGENTS.md с :ro. Никакое правило разрешений не участвовало: при --auto правка была бы одобрена, и её отклонила файловая система. | ||||
| 6 | ls -l /var/run/docker.sock | нет сокета | нет сокета | |
Что произошло: ls ответил No such file or directory. Почему: сокет Docker не смонтирован. С ним агент мог бы запустить новый контейнер со всем диском хоста и граница исчезла бы. | ||||
| 7 | apk add curl | отказ | в доступе отказано | |
Что произошло: ERROR: Unable to open log: Permission denied, код выхода 99. Почему: агент работает как пользователь из шага 2 (образ), а не как root, и no-new-privileges это сохраняет. Инструменты в контейнере — инструменты образа. | ||||
| 8 | php -r 'echo 2 + 2;' | запускается | выполнено: 4 | |
Что произошло: команда выполнилась без запроса и напечатала 4. Почему: на хосте код в командной строке спрашивает при наборе правил 4, потому что он может открыть любой файл, не называя его. В контейнере его можно запускать: он открывает только то, что в контейнере есть. | ||||
| 9 | rm -rf container/src/ai && ls container/src | запускается, git восстановит | выполнено, восстановлено | |
Что произошло: папка была удалена без запроса. На хосте git -C ../repo-agent status --short показал D container/src/ai/index.php, а git -C ../repo-agent restore container/src/ai вернул её. Почему: при --auto удаление не требует одобрения. Ущерб остаётся в рабочем дереве, а git хранит каждый отслеживаемый файл. | ||||
| 10 | ln -s /home/me/.config/aredel/site.env container/src/notes.txt | запускается, ревью это покажет | выполнено, ревью это показало | |
Команда (имя пользователя заменено), затем && ls -l container/src. Что произошло: ссылка была создана без запроса. На хосте она появилась в двух командах ревью и коммита на хосте: find container/src -type l напечатал container/src/notes.txt -> /home/me/.config/aredel/site.env, а git diff --summary напечатал create mode 120000 container/src/notes.txt. Почему: в контейнере ссылка никуда не ведёт. На хосте она ведёт на настоящий файл, и каждый инструмент, который по ней пойдёт, — скрипт выгрузки, редактор или копирование, — прочитает секрет. Контейнер не может остановить то, что оставляет после себя: это должно сделать ревью. | ||||
| 11 | terminal: container/tools/ai/opencode-docker.sh . run hi | отказ | отказ | |
Что произошло: запуск из главной копии тестового репозитория, где рабочее дерево — сама главная копия, остановился до Docker с ERROR: …/container/src holds ignored files; use a fresh worktree: и перечислил container/src/PHP_errors.log и container/src/services/. Код выхода 1. Почему: проверка из шага 4 (запускающий скрипт) спрашивает у git игнорируемые файлы в container/src. В новом рабочем дереве их нет; в главной копии они есть. | ||||
| 12 | задача из шага 5 (запустить задачу) | исправлено, ваше | исправлено, ваше | |
Запрос: "Run php -l on every PHP file under container/src, fix any syntax error you find, then run php -l again on the fixed file and reply with its output." Что произошло: агент запустил php -l, прочитал сломанную страницу, добавил точку с запятой и проверил снова — без запроса. На хосте git diff в рабочем дереве показал одну изменённую строку, и файл принадлежал UID 1000 — пользователю, который запустил скрипт. Почему: --auto одобрил каждый шаг. У пользователя образа UID пользователя хоста, поэтому правка не требует chown. | ||||
Обратно на хосте: ревью и коммит
Контейнер — это граница, пока агент работает. То, что он написал, выполняется позже: PHP-страницы в контейнере локального сайта, который обслуживает главную копию со всеми её игнорируемыми данными, и на хостинге после выкладки. Поэтому ревью — вторая граница. Из корня репозитория главной копии:
git -C ../aredel-agent status --short
git -C ../aredel-agent diff
find ../aredel-agent/container/src -type l
- Символические ссылки. aredel не отслеживает символические ссылки в container/src, поэтому последняя команда должна ничего не напечатать (тест 10). git diff не показывает новые файлы, которых ещё нет в индексе; status перечисляет их с ??.
- .htaccess. Читайте каждое изменение строка за строкой: эти правила решают, что отдаёт сайт, включая 404 для записей посетителей.
- PHP, который читает файлы или открывает соединения, заслуживает такой же осторожности: он работает с правами сайта.
Затем запустите то, чего контейнер не может: php -l и проверку рендера инструментами из container/tools, тесты и карту сайта после новых страниц. Сделайте коммит в рабочем дереве с явными путями, затем слейте ветку из главной копии:
git -C ../aredel-agent add container/src/ai/opencode/index.php
git -C ../aredel-agent commit -m "Fixes a syntax error on the OpenCode page"
git merge agent/task-1
Удалите рабочее дерево и его ветку. Следующая задача получит новое рабочее дерево из нового состояния main:
git worktree remove ../aredel-agent
git branch -d agent/task-1
Проблемы на первых настоящих задачах
2026-10-27 запускающим скриптом общего руководства OpenCode в Docker на Ubuntu 24.04 (страница Запускающий скрипт), версия 0.2.0, прошли две задачи. Кроме container/src он монтирует container/tools, container/docs и container/tests рабочего дерева только для чтения, а его заметки в CONTAINER.md заменяют правила AGENTS.md, которые в контейнере работать не могут. Моделью была бесплатная модель OpenCode:
| Рабочее дерево | Ветка | Задача |
|---|---|---|
| ../site-agent | agent/alpine-menu | курс по Perl в /dev/perl/ |
| ../site-agent-log-errors | agent/alpine-menu-log-errors | план по ошибкам в логе доступа за 2026-10-26 |
Оба имени веток остались от прошлой задачи; называйте ветку по задаче, которую она держит. Общие уроки — в разделе проблемы на настоящих задачах того руководства. Вот как каждая проблема выглядела в aredel.
Отчёт по курсу
Агент написал 58 страниц и связал курс из /dev/, dev/RelatedArticles.php и нового .php/right_blocks/perl.php. Он отчитался, что все проверки чисты, по-русски, хотя задача была на английском: заканчивайте задачу фразой "Answer in English.", когда это важно. Чего отчёт не покрывал:
- Урок 13 говорил, что my $last = (1..5); даёт 5. Настоящий Perl печатает пустое значение ("Все проверки проходят" покрывает то, что проверил агент).
- В 34 из 60 файлов не было блока вывода: результаты были комментариями, которые никто не запускал.
- Второй прогон исправил и то и другое, а потом сломал 47_capstone_logs.php (хуки).
Карта сайта
Агент запустил ./tools/generate-sitemap.sh, как велит AGENTS.md после новых страниц. Скрипт датирует каждую страницу временем её файла, а в новом рабочем дереве у каждого файла было время выгрузки:
| Закоммиченная карта сайта | После прогона агента | |
|---|---|---|
| URL | 4,270 | 4,392, из них 58 — курс |
| Самая частая lastmod | 2026-10-23, 857 страниц | 2026-10-26, 4,333 страницы |
| git diff --stat | sitemap.xml | 9302 |
Он также оставил sitemap.xml.bak-20261027084101. Теперь CONTAINER.md говорит агенту не запускать скрипт. Ревью восстанавливает файл и удаляет резервную копию:
git -C ../site-agent restore container/src/sitemap.xml
После слияния карта сайта пересобирается в главной копии, где у файлов их настоящие времена. Тот коммит добавил 58 URL курса.
Сессия
run "can you repeat your report but in english" запустил новую сессию, в которой не было отчёта для повтора. Следующий прогон в ../site-agent-log-errors продолжил курс из сессии общего тома opencode-agent-data. Он искал в рабочем дереве задачи по логам dev/perl и /tmp/agentwork и ждал с sleep 20. Сам курс был в безопасности в ../site-agent, где прогон с исправлениями изменил 47 файлов к 12:08.
Лог
Задача называла backup/aredel.com.access.log.20261026. backup/ игнорируется git, поэтому файла нет в рабочем дереве, а в нём IP-адреса посетителей, которые не должны попасть в бесплатную модель. list_404.py из container/tools/analyze_logs оставляет время и путь каждого обращения, больше ничего:
cd ~/github/aredel/container/tools/analyze_logs
python3 list_404.py --log-file ~/github/aredel/backup/aredel.com.access.log.20261026 --status 404 --skip-probes --output ~/github/site-agent-log-errors/container/docs/log-20261026-404.txt > /dev/null
python3 list_404.py --log-file ~/github/aredel/backup/aredel.com.access.log.20261026 --status 500 --output ~/github/site-agent-log-errors/container/docs/log-20261026-500.txt > /dev/null
Каждая строка — YYYY-MM-DD HH:MM:SS /url/path. Списки — неотслеживаемые файлы в доступном только для чтения container/docs рабочего дерева, и после задачи их удаляют.
Хуки в рабочем дереве
Первый коммит курса в ../site-agent остановился на трёх хуках:
php -l (container/src, staged)...........................................Failed - hook id: php-lint-src - exit code: 1 ERROR: src is outside the git repo. phpcs (container/src, staged)............................................Failed - hook id: phpcs-src - exit code: 1 ERROR: src is outside the git repo. check-no-redirected-pages (staged).......................................Failed - hook id: check-no-redirected-pages - exit code: 1 ERROR: new page /.php/right_blocks/perl.php is a redirect source (.htaccess line 25, 301 ^(.+\.php)/.+$). Remove the redirect or pick a different URL.
| Скрипт | Почему он упал | Исправление |
|---|---|---|
| lint-php.sh, lint-static.sh, lint-sh.sh | git -C "$SCRIPT_DIR" rev-parse --show-toplevel вернул container/tools/linters, потому что git передаёт хуку в рабочем дереве GIT_DIR без GIT_WORK_TREE. lint-sh.sh здесь пропустили: .sh в индексе не было | unset GIT_DIR GIT_WORK_TREE в начале; версии 0.0.2, 0.0.4 и 0.0.2 |
| lint-php.sh | он запускает php -l в php-apache, который монтирует src главной копии. docker compose в папке container/ рабочего дерева попадает в тот же контейнер: то же имя папки, тот же container_name | в связанном рабочем дереве он проверяет через php:8.2-cli на src рабочего дерева |
| check-no- | он принимал каждый новый .php за страницу. /.php/… совпадает с правилом PATH_INFO строки 25 и отправляется в /.php, как каждый include там | пропускает папки с точкой, как это делает generate-sitemap.sh |
С исправлением хуки нашли то, что сломал второй прогон:
Parse error: syntax error, unexpected floating-point number "127.0", expecting "," or ";" in /code/dev/perl/course/47_capstone_logs.php on line 67 Errors parsing /code/dev/perl/course/47_capstone_logs.php linted: 63, failures: 1
Две команды printf урока содержали необработанные одинарные кавычки внутри строки echo '…' страницы. На сайте страница ответила бы 500. phpcs сообщил 152 замечания, все из одного и того же места. Каждая кавычка стала '.
Коммит
К этому моменту рабочее дерево отставало от main на десять коммитов, среди них обновления ленты. Хук post-commit post-to-feed.sh ставит в индекс feed.json и arch.json после коммита с новыми страницами. Команды, которые прошли, из главной копии:
git -C ../site-agent merge --ff-only main
git -C ../site-agent add container/src/dev/perl/course/47_capstone_logs.php
git -C ../site-agent commit -m "Adds a Perl course under /dev/perl/: 50 lessons, install guides and a hub"
git -C ../site-agent commit -m "Updates feed"
git merge --ff-only agent/alpine-menu
container/tools/generate-sitemap.sh
git clean -f -- 'container/src/sitemap.xml.bak-*'
git add container/src/sitemap.xml
git commit -m "Adds the Perl course to the sitemap"
git worktree remove ../site-agent
git branch -d agent/alpine-menu
git clean удаляет только неотслеживаемые резервные копии; sitemap.xml.bak-20261015080545 отслеживается и остаётся.
Другой агент в главной копии
Коммит исправления хуков в главной копии сначала упал:
lint-php.sh self test....................................................Failed - hook id: lint-php-sh-selftest - files were modified by this hook
pre-commit отложил 7 изменённых файлов в 14:32. В 14:32:26 изменился container/src/qa/testing_top_path.php — файл, которого коммит не касался: другая сессия Claude Code и OpenCode, запущенный на хосте, работали в главной копии. Самотест прошёл отдельно, через pre-commit run, и в клоне с теми же файлами в индексе и вне его. Тот же коммит прошёл со следующей попытки.
Чего контейнер не решает
- Интернет открыт. Страница, которую читает агент, может нести инструкции отправить что-то наружу. В контейнере это код публичного сайта и, с платной моделью, ключ из шага 3 (конфигурация внутри). Ограничить трафик хостом модели можно только прокси перед контейнером.
- Порты на 0.0.0.0 остаются доступными, пока их не опубликуют на 127.0.0.1 (сеть: до чего контейнер всё ещё добирается).
- То, что пишет агент, выполняется позже. Контейнер защищает хост, пока агент работает; ревью (ревью и коммит на хосте) защищает его потом.
- Инструментов хоста нет. Карта сайта, тесты, сборка CSS и find_article.py живут в container/tools. Шаг навыка, который их вызывает, в контейнере падает, и вы запускаете его после ревью. Монтирование container/tools, даже только для чтения, вернуло бы users.db и инструменты работы с логами.
- Два места нужно держать в актуальном состоянии. Версию OpenCode в образе и id бесплатной модели в конфигурации контейнера поддерживают вручную, в шаге 2 (образ) и шаге 3 (конфигурация внутри).
- Docker — это root на хосте. Кто может запустить docker, может смонтировать любую папку. Никогда не монтируйте сокет Docker в контейнер (тест 6) и держите запрос *docker * из варианта B в opencode.jsonc хоста, чтобы OpenCode на хосте не мог запустить контейнер с другими монтированиями.
Чек-лист
- Сначала коммит, затем git worktree add; проверка с --ignored ничего не печатает.
- Тег образа совпадает с opencode --version на хосте.
- Конфигурация контейнера называет текущую бесплатную модель OpenCode или платную модель с ключом, сделанным для неё.
- Запускающий скрипт монтирует только container/src рабочего дерева, инструкции только для чтения и том сессий. Без сокета Docker.
- Порты локального сайта и каждого другого локального сервиса опубликованы на 127.0.0.1.
- После прогона: status, diff, ни одной символической ссылки, .htaccess прочитан строка за строкой, затем проверки на хосте.
- Коммит и слияние на хосте; рабочее дерево удалено.
Автор статьи: Клавдия Кондакова