OpenCode в Docker: режим auto только на container/src

Почему контейнер, а не ещё правила

Страница правил запрета перебрала четыре набора правил, чтобы OpenCode спрашивал реже, не открывая секреты репозитория aredel. Каждый набор менял запросы на дыры:

Контейнер переворачивает вопрос. Вместо списка того, чего агент не должен касаться, агент получает файловую систему, в которой нечего защищать: отслеживаемые файлы 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

Шаг 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

Шаг 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" }

Шаг 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

Шаг 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 и только по кодам состояния:

ЦельСлушает на хостеИз контейнера
локальный сайт, порт 80800.0.0.0доступен
отчёты Allure, порт 50500.0.0.0доступен
бэк-офис aredel, порт 8765127.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.

Как читать результаты

Результат: ✓, когда фактический результат совпадает с ожидаемым, ✗ — когда нет.

ТестКомандаОжидалосьФактическиРезультат
1cat ~/.config/aredel/site.envнет такого файланет такого файла

Что произошло: команда выполнилась без запроса, и cat ответил can't open '/home/agent/.config/aredel/site.env': No such file or directory.

Почему: ~ в контейнере — домашняя папка пользователя образа. Домашняя папка хоста не смонтирована. На хосте набор правил 4 у OpenCode должен запрещать эту команду; здесь запрещать нечего.

2ls -a container/src/services/user_profile/data/нет такой папкинет такой папки

Что произошло: ls ответил No such file or directory.

Почему: папка посетителей игнорируется git, а рабочее дерево из шага 1 (рабочее дерево только с отслеживаемыми файлами) содержит только отслеживаемые файлы, поэтому такой папки там нет.

3grep -rn 203.0.113 /workнет совпаденийвывода нет

Что произошло: поиск прошёл без запроса по всему, что агент может видеть, и ничего не нашёл; grep завершился с кодом 1.

Почему: на хосте grep -r спрашивает при наборе правил 4, потому что он игнорирует .gitignore и открыл бы записи посетителей. В контейнере этих записей просто нет.

4git log --oneline -1нет gitgit: not found

Что произошло: оболочка ответила /bin/sh: git: not found, код выхода 127.

Почему: в образе нет git, и .git не смонтирован. Даже с установленным git читать было бы нечего.

5echo note >> AGENTS.mdотказтолько чтение

Что произошло: /bin/sh: can't create AGENTS.md: Read-only file system.

Почему: запускающий скрипт монтирует AGENTS.md с :ro. Никакое правило разрешений не участвовало: при --auto правка была бы одобрена, и её отклонила файловая система.

6ls -l /var/run/docker.sockнет сокетанет сокета

Что произошло: ls ответил No such file or directory.

Почему: сокет Docker не смонтирован. С ним агент мог бы запустить новый контейнер со всем диском хоста и граница исчезла бы.

7apk add curlотказв доступе отказано

Что произошло: ERROR: Unable to open log: Permission denied, код выхода 99.

Почему: агент работает как пользователь из шага 2 (образ), а не как root, и no-new-privileges это сохраняет. Инструменты в контейнере — инструменты образа.

8php -r 'echo 2 + 2;'запускаетсявыполнено: 4

Что произошло: команда выполнилась без запроса и напечатала 4.

Почему: на хосте код в командной строке спрашивает при наборе правил 4, потому что он может открыть любой файл, не называя его. В контейнере его можно запускать: он открывает только то, что в контейнере есть.

9rm -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 хранит каждый отслеживаемый файл.

10ln -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.

Почему: в контейнере ссылка никуда не ведёт. На хосте она ведёт на настоящий файл, и каждый инструмент, который по ней пойдёт, — скрипт выгрузки, редактор или копирование, — прочитает секрет. Контейнер не может остановить то, что оставляет после себя: это должно сделать ревью.

11terminal: 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

Затем запустите то, чего контейнер не может: 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-agentagent/alpine-menuкурс по Perl в /dev/perl/
../site-agent-log-errorsagent/alpine-menu-log-errorsплан по ошибкам в логе доступа за 2026-10-26

Оба имени веток остались от прошлой задачи; называйте ветку по задаче, которую она держит. Общие уроки — в разделе проблемы на настоящих задачах того руководства. Вот как каждая проблема выглядела в aredel.

Отчёт по курсу

Агент написал 58 страниц и связал курс из /dev/, dev/RelatedArticles.php и нового .php/right_blocks/perl.php. Он отчитался, что все проверки чисты, по-русски, хотя задача была на английском: заканчивайте задачу фразой "Answer in English.", когда это важно. Чего отчёт не покрывал:

Карта сайта

Агент запустил ./tools/generate-sitemap.sh, как велит AGENTS.md после новых страниц. Скрипт датирует каждую страницу временем её файла, а в новом рабочем дереве у каждого файла было время выгрузки:

Закоммиченная карта сайтаПосле прогона агента
URL4,2704,392, из них 58 — курс
Самая частая lastmod2026-10-23, 857 страниц2026-10-26, 4,333 страницы
git diff --statsitemap.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.shgit -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-redirected-pages.shон принимал каждый новый .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 замечания, все из одного и того же места. Каждая кавычка стала &#39;.

Коммит

К этому моменту рабочее дерево отставало от 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, и в клоне с теми же файлами в индексе и вне его. Тот же коммит прошёл со следующей попытки.

Чего контейнер не решает

Чек-лист

Автор статьи: Клавдия Кондакова