# Mantia

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

## Прежде чем писать код

Прочитай [docs/handover.md](docs/handover.md), раздел **«Состояние на 15
августа 2026 — читать первым»**: там в одном месте сказано, что работает, что
проверено сквозным прогоном и что говорить альфа-тестерам. Код отвечает на «что сделано»,
этот документ — на «почему именно так», и там же список уже совершённых ошибок.
Половина из них не выводится из чтения кода и стоила времени дважды.

Нормативное ТЗ — [docs/PLAN-antidetect-browser.md](docs/PLAN-antidetect-browser.md).
При расхождении право за ним, а не за кодом.

Куда продукт идёт дальше — [docs/PRODUCT.md](docs/PRODUCT.md). Там про два
уровня («свой отпечаток — максимум доверия» и «масштабирование»), про то, что
физически нужно для десятков профилей на одной машине, и про порядок работ.
Этот документ сознательно выходит за рамки ТЗ и говорит, где именно.

Ещё несколько документов отвечают на вопросы, которые встают рано:

* [docs/fingerprint-diversity.md](docs/fingerprint-diversity.md) — сколько
  разных отпечатков даёт одна машина. Ответ измерен, а не выведен: **два**.
  Там же цена форка Chromium и почему JS-инъекция не работает;
* [docs/fingerprint-corpus.md](docs/fingerprint-corpus.md) — корпус отпечатков:
  зачем, как устроен и где стоит;
* [docs/competitors.md](docs/competitors.md) — разбор семи чужих
  антидетект-браузеров: чем они оказались внутри, что из этого берём и что
  не берём. Там же доказательство, что все всерьёз работающие — это форк
  Chromium с патчем C++, а не инъекция JS;
* [docs/mcp.md](docs/mcp.md) — как отдать открытый профиль чужой нейронке
  (Claude Code, Grok, любой API) и что она при этом видит.

**Модель может работать по подписке владельца, а не по ключу API.** Поставщик
выбирается в окне: `claude-cli` (подписка Claude, ключ не нужен — ходит через
установленную программу `claude`), `anthropic`, `groq`, `openai` (любой
OpenAI-совместимый адрес). Единственная точка вызова — `agent/llm.py`.

## Проверять как пользователь, а не как разработчик

Вынесено наверх, потому что это самая дорогая привычка проекта. Зелёный прогон
здесь **не доказывает**, что работает. Проверено на своей шкуре: пять тестов
приёмки годами «проходили» пропуском из-за прибитого пути; в двух других
единственное утверждение стояло под `if sys.platform == "win32"`; защита от
запуска чужого профиля держалась на том, что Windows запрещает двоеточие в
имени каталога — на macOS её просто не стало, и ни один тест этого не показал.

Классический случай, с которого урок начался: иконка на рабочем столе не делала
ничего — ни окна, ни ошибки, ни строки в журнале. У `pythonw.exe` нет консоли, и
`sys.stdout` там `None`. Из терминала всё работало всегда; найти это можно было
только кликнув мышью по ярлыку.

Поэтому: изменил окно — открой окно. Изменил установку — поставь из архива.
Изменил цвет — замерь контраст на композите. Утверждения о поведении системы
(всплывёт ли модалка, найдёт ли `lsof` слушателя) — **замеряй**, не выводи из
документации: дважды за сессию замер опроверг правдоподобное рассуждение.

## Что нельзя нарушать никогда

Каждый пункт — не стиль, а условие существования продукта.

**`Runtime.enable` не отправляется.** Эта команда включает домен, который видит
страница. Один вызов — и всё остальное бессмысленно. JavaScript исполняется
только через `Page.createIsolatedWorld` + `Runtime.evaluate` с явным `contextId`.

**Никакого Playwright и Selenium.** Оба детектируются, это проверено спайком.
Только сырой CDP по websocket.

**Ввод только через `Input.dispatch*`** пачками с растущим `timestamp`. Это
единственный способ получить `isTrusted: true` — свойство, которое из
JavaScript не подделать.

**Восприятие через AX-дерево, не через CSS-селекторы.** Классы вида
`UserItem_link__kgEWg` — хэш CSS-модуля, он меняется при каждой пересборке
фронтенда площадки. Сценарий на селекторах умирает молча.

**Пароли не попадают в контекст модели** (§3 п.9 ТЗ). Секреты живут в `.env`
вне репозитория, в журнал не выводятся ни в каком виде.

**Прокси fail-closed.** Умер апстрим — трафик не идёт никуда. «Тихо
продолжить» это утечка настоящего IP, то есть потерянный аккаунт.

**Любой SELECT по `profiles`/`proxies`/`tasks`/`scenarios` несёт
`deleted_at IS NULL`.** Удаление мягкое: строка остаётся ради обмена между
машинами. Забытый фильтр — удалённое видно как живое.

**Жёсткого `DELETE` профилей и прокси нет нигде.** Только
`provisioner.delete` / `proxies.delete`, иначе строка вернётся первым же
обменом с другой машиной.

## Границы задачи

Строим универсальную автоматизацию браузера и управление профилями. **Не**
строим: обход CAPTCHA, обход проверки личности и живости, подмену видеопотока
для верификации, массовую автоматическую регистрацию. Агент, упёршийся в
проверку, останавливается и зовёт человека.

## Как здесь пишут

Комментарии, докстринги и сообщения об ошибках — **по-русски**. Комментарий
объясняет ПОЧЕМУ, а не пересказывает код. Сообщение об ошибке адресовано
человеку: служебный контекст в него не дописывается.

Не изображать работающим то, что не работает. Продукт обязан честно говорить,
чего он не умеет.

## Команды

```bash
pytest tests/unit -q          # быстрые, без браузера (1361 прошло, 3 пропущено)
pytest tests/live -m live -q  # приёмка, нужен Chrome и прокси
ruff check src tests

mantia doctor          # профиль хоста и честные ограничения
mantia check           # готовность к запуску, чего не хватает
mantia fetch-chromium  # скачать сборку в пул
mantia capture-ua      # снять UA-CH эталон с установленного Chrome
mantia install-app     # ярлык в Launchpad (macOS)
mantia corpus-ingest <лог>   # разобрать лог зонда с сервера
mantia corpus-summary        # сколько разных устройств собрано
mantia serve --port 8756     # только сервер, без окна — так удобно проверять
                             # интерфейс браузером и curl'ом
mantia mcp                   # отдать открытые профили внешнему ИИ (docs/mcp.md);
                             # запускает не человек, а клиент — Claude Code
```

Запуск окна: `Mantia.bat` (Windows) → `mantia app-window`.
На macOS ярлык делается один раз командой `mantia install-app` — она кладёт
`~/Applications/Mantia.app`, и дальше программа запускается из Launchpad или
щелчком, а не из терминала. Ярлык зовёт тот интерпретатор, которым продукт
установлен: путь записывается в момент установки, поэтому после переезда venv
команду надо повторить.

Данные, база, логи и `.env` — в `MANTIA_HOME` (по умолчанию `C:\mantia`,
на macOS `~/.mantia`), **не** в репозитории.

## Платформа

Ядро платформонезависимо. Привязанное к Windows живёт в `platform/` и трёх
модулях. **Профили запускаются и на Windows, и на macOS**; вся приёмка
`tests/live` зелёная на обеих. Не сделано на macOS только украшение: заголовки
и иконки окон профилей, перетаскивание безрамочного окна за края. Подробности —
раздел 6 в `docs/handover.md`.

`tests/unit/test_platform_portability.py` следит, чтобы win32 не приезжал на
верхнем уровне модуля.

**Windows — боевая платформа, macOS — платформа разработки.** Правка, ломающая
Windows, дороже любой недоделки на macOS. Ловушки, на которых это уже
происходило:

* `os.killpg`, `os.getpgid`, `SIGTERM`, `lsof`, `ps` на Windows отсутствуют или
  значат другое;
* `SO_REUSEADDR` на Windows разрешает **двум** сокетам сесть на один порт; на
  BSD — не разрешает, там он лечит только TIME_WAIT;
* `os.access(X_OK)` на Windows отвечает про существование файла, а не про право
  запуска.

**Молчание — не universal ответ.** Функции, которой на этой ОС нет, разрешено
вернуть «ничего не сделано» — но только если она косметическая (иконка окна).
Там, где речь о прокси, о владельце порта или о привязке процесса, «не удалось
спросить» и «никого нет» обязаны быть **разными** ответами: трактовать первое
как второе значит выключить защиту, не сказав об этом. Отсюда трёхзначные
`listening_pids` и `pids_holding_user_data_dir` в `platform/posix.py`.

**Защита не должна опираться на поведение файловой системы.** Заглушка
`mantia-remote://` работала как защита только на Windows, потому что там
двоеточие в имени каталога запрещено; на macOS оно легально, и защита исчезала
молча. Проверяйте признак явно.
