Skip to content

Latest commit

 

History

History
276 lines (224 loc) · 20.5 KB

File metadata and controls

276 lines (224 loc) · 20.5 KB

Server mode — дизайн (Runner-слой, API, sandbox)

Дизайн-документ будущего серверного режима (issue #140, #156, #157). Не реализация: описывает целевые границы, контракты и требования безопасности, не меняя текущий Python-код. Решение «двигаться к server mode» и его альтернативы зафиксированы отдельно как ADR-0001; этот документ — техническая спецификация, на которую ADR ссылается.

Контекст на сегодня: единственный сетевой слой — локальный --serve (127.0.0.1, без OS-sandbox), см. web-current.md § Безопасность и SECURITY.md § Server / IDE-режим. Форма результата, которую сервер обязан сохранить, — в result-contract.md.

Оглавление


Зачем и границы

Локальный грейдер запускает доверенный код (свой или скачанный из Stepik as-is) без изоляции ФС/сети — это by design (configuration.md § Ограничения и безопасность). Любой переход к исполнению чужого кода (общий сервер, удалённый прогон, онлайн-проверка) меняет threat model и требует настоящего sandbox. Server mode — это именно такой переход, поэтому он проектируется отдельно и не включается по умолчанию.

Ключевой инвариант, переносимый из локального дизайна: ядро остаётся библиотекой. Server mode — ещё один адаптер над core/* (как CLI и Web), а не переписывание грейдинга. DAG остаётся ацикличным (architecture.md).


Runner-слой (issue #140, реализация — #136/#137/#138)

Статус: Runner/LocalRunner реализованыsrc/stepik_grader/core/runner.py. grader_core.run_single_test() делегирует subprocess-запуск LocalRunner без изменения поведения. Ниже — контракт как есть в коде (не только дизайн).

Раньше исполнение было размазано между core/grader_core.py (subprocess одного кейса) и core/microbench_runner.py (timeit). Runner — явная абстракция запуска, чтобы сменить механизм изоляции (будущий SandboxRunner), не трогая грейдинг.

Слой (реализовано в core/runner.py):

grader_core.run_single_test(...)  →  Runner.run(spec) -> RunOutcome
  • RunSpec — что запустить: путь решения (или сгенерированного wrapper-скрипта — режим stdin/function определяется до Runner, в grader_core.py), stdin, timeout, measure_memory, max_memory_mb.
  • RunOutcome — сырой итог запуска: stdout/stderr (bytes), returncode, elapsed, peak_memory_mb, timed_out, launch_error (заполнен при OSError на spawn). Маппится в case result (result-contract.md) выше по стеку (grader_core.py) — сам Runner вердиктов не выносит.

Иерархия реализаций:

Runner Изоляция Статус Где уместен
LocalRunner subprocess + таймаут + best-effort лимит памяти (POSIX) реализован (issue #138) локальный CLI/Web (доверенный код)
SandboxRunner ОС-уровень: неймспейсы/seccomp/квоты, сеть выключена, tmp-каталог дизайн, issue #157; локальный MVP реализован, issue #266 (--sandbox, core/sandbox/) server mode (недоверенный код) / локальный opt-in CLI

Локальный MVP уже есть (issue #266)core/sandbox/ реализует bubblewrap (Linux) / sandbox-exec (macOS) / Job Objects (Windows) за флагом --sandbox, тем же паттерном, что async job model (issue #262) выше: покрывает часть требований этого раздела, но не заменяет будущий сетевой server mode — работает только локально, без аутентификации/multi-tenancy/очереди. Полная таблица гарантий по ОС (асимметрия — не баг) и явные пробелы (нет сетевой изоляции на Windows, нет строгой ФС-изоляции на Windows, nsjail-fallback на Linux не реализован) — SECURITY.md § --sandbox.

Границы SandboxRunner (что он гарантирует, а что нет):

  • Гарантирует: сеть недоступна из исполняемого кода на Linux/macOS (на Windows — нет, см. ниже); запись только во временный каталог задачи; жёсткие лимиты CPU-времени, wall-времени, памяти, размера вывода и числа процессов; уборка временного каталога после прогона.
  • Не гарантирует и явно вне слоя: защита от side-channel/timing-атак, полная защита от эскалации ядра (это ответственность выбранного механизма изоляции — контейнер/VM), корректность самих тест-кейсов.

Ограничения на Windows (обновлено issue #266). Изначально этот раздел предполагал, что POSIX-специфичные механизмы (неймспейсы, seccomp, resource-квоты, SIGALRM) не имеют Windows-аналога и потребуют внешнего backend'а (контейнер/микро-VM/WSL). На практике нашёлся нативный Windows-примитив — Job Objects (CreateJobObjectW/ SetInformationJobObject/AssignProcessToJobObject) — даёт реальный kernel-enforced лимит памяти/CPU-времени/числа процессов без внешнего backend'а (core/sandbox/_windows.py, issue #266 --sandbox). Не закрыто: сетевой изоляции на Windows в этом MVP нет (AppContainer непропорционально сложен для per-run профиля — см. SECURITY.md), поэтому server mode с недоверенным кодом на Windows по-прежнему не поддерживается — локальный --sandbox не эквивалентен требованиям server mode этого документа (пункт 6 «Изоляция по клиентам» выше требует и сетевую изоляцию тоже). LocalRunner-путь (без --sandbox) на Windows остаётся как был: точного внутрипроцессного таймаута через SIGALRM нет (защита — только внешний subprocess.run(timeout=...), см. core/runner.py), лимит памяти — best-effort лишь на POSIX.

Реализация не привязана к Docker. SandboxRunner — интерфейс; конкретный backend (контейнер, nsjail/bubblewrap, микро-VM, внешний сервис) — решение этапа реализации. Docker в этом документе не выбирается и не требуется; цель — чтобы грейдинг зависел от абстракции Runner, а не от конкретного sandbox-механизма. Любая тяжёлая зависимость/демон — только по явному решению (см. запрет в ../CLAUDE.md на новые зависимости).

Инварианты слоя:

  1. Runner живёт в core/ как протокол; LocalRunner — рефактор текущего subprocess-пути без изменения поведения (регрессий в вердиктах быть не должно).
  2. Ни core/grader_core.py, ни адаптеры (CLI/Web/API) не знают, какой Runner активен — выбор инжектируется (конфиг/DI), поведение результата одинаково.
  3. SandboxRunner не ослабляет контракт результата: тот же case result, плюс аддитивный вердикт SANDBOX_VIOLATION для sandbox-нарушения — уже реализован (core/result.py, локальный --sandbox #266; правило 3 контракта).

Контракт API удалённого исполнения (issue #156)

Дизайн/контракт, не сервер. Ниже — форма запроса/ответа, жизненный цикл и классы ошибок для будущего HTTP API. Реализация API-сервера не входит в эту работу.

Локальный MVP уже есть (issue #262)POST /api/v1/runs + GET /api/v1/runs/{id} реализованы в --serve (web/runs.py/web/server.py) для bench/microbench, но это не сетевой сервер из этого раздела: in-memory реестр job'ов одного процесса, без sandbox/квот/аутентификации. Два сознательных отклонения от спекулятивного контракта ниже: (а) результат приходит ИНЛАЙН в GET /api/v1/runs/{id} (поле result), а не через отдельный GET /api/v1/runs/{id}/result; (б) словарь статусов — queued|running|done|error|cancelled: отмена — отдельный терминальный статус cancelled (issue #296, дополнительно message_id="run_cancelled"); единственное сознательное отклонение от спекулятивного контракта ниже — error вместо failed для инфраструктурного сбоя. Полная документация /api/v1/runsdocs/api.md § POST /api/v1/runs.

Запрос

// POST /api/v1/runs
{
  "solution": "исходный код или ссылка на загруженный артефакт",
  "language": "python",              // сейчас единственное значение
  "test_set_id": "...",              // ссылка на набор тест-кейсов
  "mode": "tests" | "bench" | "microbench",
  "limits": {                         // опц., перекрывает дефолты сервера в пределах максимумов
    "timeout_s": 10,
    "memory_mb": 256
  }
}

Жизненный цикл (асинхронный)

Исполнение недоверенного кода может быть медленным и требует очереди — контракт асинхронный, а не «запрос-ответ на лету»:

POST /api/v1/runs        → 202 Accepted, { "run_id", "status": "queued" }
GET  /api/v1/runs/{id}    → { "status": "queued|running|done|failed", ... }
GET  /api/v1/runs/{id}/result → RunResult (когда status=done)
  • status: queued → running → done (успешное завершение прогона, даже если вердикты WA/RE) либо failed (инфраструктурный сбой, не провал решения).
  • Результат в done — тот же Run/Solution/Case result, сериализованный в JSON. Вердикты AC/WA/TLE/RE решения не являются failed: провал решения — успешный прогон.

Классы ошибок

Транспортные ошибки отделены от вердиктов решения:

Класс HTTP Когда
validation_error 400 Битый запрос: неизвестный mode/language, лимиты вне диапазона
not_found 404 Неизвестный run_id/test_set_id
quota_exceeded 429 Превышены квоты клиента (частота/параллелизм)
sandbox_violation 200 (в результате) Код нарушил sandbox (сеть/запись/форк-бомба) — это вердикт, не HTTP-ошибка
internal_error 500 Сбой инфраструктуры (не вина решения) → status=failed

sandbox_violation сознательно попадает в тело результата как вердикт (аддитивно к AC/WA/TLE/RE), а не в HTTP-ошибку: с точки зрения прогона это исход исполнения, который надо показать пользователю рядом с diff'ом.

Версионирование

  • API версионируется в пути (/api/v1/). v1 фиксирует форму result-contract.md; ломающие изменения полей → /api/v2.
  • Локальный --serve (/api/grade) не становится этим API автоматически: он остаётся неверсионированным локальным адаптером; сетевой API — отдельная поверхность (см. фазы ниже).

Sandbox и сетевая изоляция (issue #157)

Требования безопасности к SandboxRunner в server mode. Это спецификация обязательных свойств. Класс backend, закрывающего их, выбран отдельно — OS-контейнер (namespaces + cgroups v2 + seccomp), ADR-0008; детальное отображение каждого требования ниже на конкретные Linux-примитивы (cgroups v2 / netns / mount ns / seccomp) — server-sandbox-design.md (issue #153). Конкретный OCI-рантайм и оркестрация остаются этапом реализации.

  1. Сеть выключена. У исполняемого кода нет сетевого доступа (ни исходящего, ни слушающих сокетов). Скачивание задач/тестов делает сервер до запуска решения, не само решение.
  2. Только временные каталоги. Запись разрешена лишь в приватный tmp-каталог прогона (per-run), который удаляется после завершения. Нет доступа к ФС сервера, другим прогонам, секретам, secrets.json.
  3. Квоты (жёсткие лимиты). CPU-время, wall-время, память (реальный лимит, а не best-effort как на POSIX сейчас), размер stdout/stderr, число процессов/потоков (анти-форк-бомба), размер артефактов. Превышение → вердикт (TLE для времени; sandbox_violation/специализированный для памяти/форк-бомбы).
  4. Никаких секретов в среде sandbox. OAuth-токены, secrets.json, переменные окружения сервера не пробрасываются в исполнение. Редакция в логах — logging.md.
  5. Эфемерность. Каждый прогон — чистое окружение; состояние между прогонами не сохраняется.
  6. Изоляция по клиентам. Прогоны разных клиентов не видят друг друга (ни ФС, ни процессы, ни сеть).

Пока эти свойства не обеспечены реальным механизмом изоляции, server mode с недоверенным кодом не включается — ровно как зафиксировано в SECURITY.md.


Фазовая миграция

Переход строится инкрементально, каждый шаг обратно совместим:

Фаза Что Изоляция Кому доступно
0 — сейчас Локальный CLI + --serve (127.0.0.1) нет (доверенный код) локальный пользователь
1 — Runner-абстракция Runner/LocalRunner выделены из grader_core (issue #136/#137/#138) — готово, без смены поведения как в фазе 0 локальный пользователь
2 — SandboxRunner Реализовать sandbox-backend по требованиям #157; включаем локально «на себе» ОС-уровень готово как локальный MVP (issue #266, --sandbox) — асимметрия гарантий по ОС, см. SECURITY.md
3 — API HTTP API /api/v1/runs (issue #156) поверх SandboxRunner, очередь, квоты ОС-уровень доверенные клиенты
4 — Server mode Публичный/командный сервер онлайн-проверки ОС-уровень + сетевые квоты много клиентов

Фазы 1–2 — рефактор/инфраструктура без нового продукта; продуктовый сдвиг начинается с фазы 3. Ни одна фаза не ломает локальный --serve//api/grade.


Non-goals

  • Не выбирать конкретный sandbox-рантайм (runc/crun/gVisor/Firecracker) — это решение деплоя. Класс backend (OS-контейнер + cgroups v2 + netns + seccomp) сужен ADR-0008, но конкретный рантайм и его установка — этап реализации.
  • Не реализовывать API-сервер — только контракт.
  • Не вводить зависимости/демоны в текущий код.
  • Не менять локальную threat model — фаза 0 остаётся «доверенный код без sandbox», как сейчас.
  • Мульти-язычность, авторизация клиентов, биллинг — вне рамок этого дизайна (относится к фазе 4+, отдельные issue).