Самое важное из мира AI — в канале MAX AgentLabОтдельные разборы, инструменты и практические схемы Читайте Agent Lab в Telegram Разборы, кейсы и новости о практических AI-агентах без лишней воды.

Pi в Docker: безопасный стенд для сравнения локальных и облачных агентов

Pi в Docker: безопасный стенд для сравнения локальных и облачных агентов
Temporary fallback cover; replace in editorial pass.

Уровень: средний · Время чтения: ~12 минут · Обновлено: 8 октября 2026

Pi — минималистичный coding-агент для терминала: у него небольшой системный промпт, короткий набор инструментов (чтение и запись файлов, правка, shell) и поддержка разных провайдеров моделей. Поэтому его удобно использовать как стенд для исследований: обвязки мало, и разница в поведении сильнее зависит от модели.

Но у такого агента есть shell. Если запустить его в рабочем каталоге хоста, он сможет прочитать ~/.ssh, .env соседних проектов, токены в ~/.config и всё, что доступно вашему пользователю. При сравнении нескольких моделей, особенно слабых локальных, вероятность случайной разрушительной команды только растёт.

Ниже — стенд, который решает обе задачи сразу: Pi работает в контейнере без root, видит только одноразовую копию тестового репозитория и получает ровно один ключ API за запуск. Одна и та же задача прогоняется на нескольких моделях, результаты собираются в отдельный каталог.

Что получится в итоге

Модель угроз: от чего защищаемся

Чтобы не переоценить защиту, сразу зафиксируем рамки:

Шаг 1. Структура каталога

pi-lab/
├── Dockerfile
├── fixture/            # эталонная задача, только для чтения
│   ├── package.json
│   ├── src/slugify.js
│   └── test/slugify.test.js
├── prompts/task.txt
├── config/models.json  # описание локального провайдера
├── secrets/            # .env-файлы с ключами, в .gitignore
├── runs/               # результаты прогонов
└── run-one.sh
mkdir -p pi-lab/{fixture/src,fixture/test,prompts,config,secrets,runs}
cd pi-lab
printf 'secrets/\nruns/\n' > .gitignore
chmod 700 secrets

Шаг 2. Образ с закреплённой версией

Закрепляйте и базовый образ, и версию Pi. Иначе через неделю вы сравните уже другой агент.

# Dockerfile
FROM node:22-bookworm-slim

# Минимум инструментов, которые агенту обычно нужны для работы с кодом
RUN apt-get update \
 && apt-get install -y --no-install-recommends git ca-certificates ripgrep \
 && rm -rf /var/lib/apt/lists/*

# Подставьте конкретную версию вместо <VERSION>; проверьте: npm view @mariozechner/pi-coding-agent versions
ARG PI_VERSION=<VERSION>
RUN npm install -g @mariozechner/pi-coding-agent@${PI_VERSION}

# Непривилегированный пользователь
RUN useradd --create-home --uid 10001 agent
USER agent
WORKDIR /work

ENTRYPOINT ["pi"]
docker build --build-arg PI_VERSION=<VERSION> -t pi-lab:pinned .
docker run --rm pi-lab:pinned --help

Последняя команда сразу показывает, какие флаги поддерживает ваша версия: неинтерактивный режим, выбор провайдера и модели. Дальше в статье используются -p, --provider и --model. Если в вашей версии они называются иначе, поправьте скрипт.

Шаг 3. Фикстура: одна проверяемая задача

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

// fixture/src/slugify.js — намеренно неполная реализация
function slugify(input) {
  return input.toLowerCase().replace(/ /g, '-');
}
module.exports = { slugify };
// fixture/test/slugify.test.js
const test = require('node:test');
const assert = require('node:assert');
const { slugify } = require('../src/slugify');

test('схлопывает пробелы и обрезает края', () => {
  assert.strictEqual(slugify('  Hello   World  '), 'hello-world');
});
test('удаляет пунктуацию', () => {
  assert.strictEqual(slugify('Agents, Docker & Pi!'), 'agents-docker-pi');
});
// fixture/package.json
{ "name": "fixture", "private": true, "scripts": { "test": "node --test" } }
# prompts/task.txt
В репозитории падают тесты в test/slugify.test.js.
Исправь src/slugify.js так, чтобы все тесты проходили.
Не меняй файлы тестов. Не устанавливай зависимости.
Когда закончишь, запусти npm test и кратко опиши изменения.

Тестам не нужны npm-зависимости: агенту незачем ходить в реестр пакетов, а результат не зависит от сети.

Шаг 4. Подключение локальной модели

Для локальных моделей удобно использовать Ollama или другой сервер с OpenAI-совместимым API на хосте. Pi описывает собственные провайдеры в файле models.json в каталоге конфигурации агента (обычно ~/.pi/agent/). Примерная структура — точные поля сверьте с README своей версии:

{
  "providers": {
    "ollama": {
      "baseUrl": "http://host.docker.internal:11434/v1",
      "api": "openai-completions",
      "apiKey": "ollama",
      "models": [
        { "id": "qwen2.5-coder:14b" }
      ]
    }
  }
}

Имя модели здесь условное. Укажите ту, что действительно скачана: ollama list.

Шаг 5. Секреты: один ключ на запуск

Для каждого облачного провайдера заведите отдельный файл с одной переменной. Никаких общих .env с десятком ключей.

# secrets/anthropic.env
ANTHROPIC_API_KEY=...

# secrets/openai.env
OPENAI_API_KEY=...
chmod 600 secrets/*.env

Используйте отдельные ключи с лимитом бюджета в консоли провайдера. Если агент выведет ключ в лог или передаст его куда-нибудь, ущерб будет ограничен, а ключ легко отозвать.

Шаг 6. Скрипт одного прогона

Главное правило воспроизводимости: каждый запуск начинается с чистой копии фикстуры. Агент никогда не работает в оригинале.

#!/usr/bin/env bash
# run-one.sh <label> <provider> <model> [env-file]
set -euo pipefail

LABEL="$1"; PROVIDER="$2"; MODEL="$3"; ENV_FILE="${4:-}"
STAMP="$(date +%Y%m%d-%H%M%S)"
OUT="runs/${STAMP}-${LABEL}"
mkdir -p "$OUT/work" "$OUT/home"

# Чистая копия задачи + git для точного diff
cp -a fixture/. "$OUT/work/"
git -C "$OUT/work" init -q
git -C "$OUT/work" add -A
git -C "$OUT/work" -c user.email=lab@local -c user.name=lab commit -qm baseline

# Конфигурация агента: только models.json, без ваших реальных настроек
mkdir -p "$OUT/home/.pi/agent"
cp config/models.json "$OUT/home/.pi/agent/models.json"

ENV_ARGS=()
[ -n "$ENV_FILE" ] && ENV_ARGS=(--env-file "$ENV_FILE")

set +e
timeout 15m docker run --rm \
  --user 10001:10001 \
  --cap-drop=ALL \
  --security-opt no-new-privileges \
  --read-only \
  --tmpfs /tmp:rw,size=256m \
  --memory 2g --cpus 2 --pids-limit 256 \
  --add-host=host.docker.internal:host-gateway \
  -e HOME=/home/agent \
  -v "$PWD/$OUT/home:/home/agent" \
  -v "$PWD/$OUT/work:/work" \
  "${ENV_ARGS[@]}" \
  pi-lab:pinned \
  -p "$(cat prompts/task.txt)" --provider "$PROVIDER" --model "$MODEL" \
  > "$OUT/agent.log" 2>&1
echo "exit=$?" > "$OUT/agent.exit"
set -e

# Проверка результата — отдельным контейнером, без агента и без ключей
git -C "$OUT/work" diff > "$OUT/changes.diff"
git -C "$OUT/work" diff --stat -- test/ > "$OUT/tests-touched.txt"
docker run --rm --network none --read-only --user 10001:10001 \
  -v "$PWD/$OUT/work:/work:ro" -w /work --entrypoint node \
  pi-lab:pinned --test > "$OUT/verify.log" 2>&1 \
  && echo PASS > "$OUT/verdict" || echo FAIL > "$OUT/verdict"

echo "$OUT: $(cat "$OUT/verdict")"
chmod +x run-one.sh

Что делают ключевые флаги:

Шаг 7. Прогон на нескольких моделях

# Локальная модель: ключ не нужен
./run-one.sh local-qwen ollama qwen2.5-coder:14b

# Облачные модели: подставьте идентификаторы, которые поддерживает ваша версия Pi
./run-one.sh cloud-a anthropic <model-id> secrets/anthropic.env
./run-one.sh cloud-b openai    <model-id> secrets/openai.env

Ответ модели недетерминирован, поэтому один прогон ничего не доказывает. Запустите каждую конфигурацию несколько раз (например, 5) и смотрите на долю успешных:

for i in 1 2 3 4 5; do ./run-one.sh local-qwen ollama qwen2.5-coder:14b; done

# Сводка по меткам
for d in runs/*/; do
  label="$(basename "$d" | cut -d- -f3-)"
  echo "$label $(cat "$d/verdict")"
done | sort | uniq -c

Проверка результата

По каждому прогону у вас есть пять артефактов. Смотрите на все, а не только на PASS/FAIL:

Простой санитарный тест изоляции перед серьёзными экспериментами: попросите агента в отдельном прогоне вывести содержимое /home/agent, /root и список смонтированных файловых систем. Убедитесь, что ничего лишнего он не видит.

Типовые ошибки

Ограничения стенда

Что дальше

Когда базовый стенд заработает, добавьте вторую и третью задачи (рефакторинг, работа с несколькими файлами, задача без тестов с ревью diff) и egress-прокси. Другие практические разборы собраны в разделе гайдов, а термины из статьи — в глоссарии.