diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 00000000..23942a52 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,13 @@ +# Security + +Please report a security problem privately — never in a public issue or the support chat: + +- by email: [hello@wirecat.dev](mailto:hello@wirecat.dev); +- or through [GitHub's private vulnerability reporting](https://github.com/leemour/max-cli/security/advisories/new). + +Say what you saw, how to repeat it, and the version (`max --version`). Never send a token, a +session or anyone's messages. + +What the tool keeps on your computer and what stops an agent from sending: +[wirecat.dev/en/docs/security](https://wirecat.dev/en/docs/security), and the MAX details in +[docs/security.md](docs/security.md). diff --git a/docs/dev/ARCHITECTURE.md b/docs/dev/ARCHITECTURE.md index c49d2ee4..74f27456 100644 --- a/docs/dev/ARCHITECTURE.md +++ b/docs/dev/ARCHITECTURE.md @@ -39,6 +39,18 @@ only the event lines. (the run directory and the event format: cli-messaging's, T6 item 3c) ``` +**Correction 2026-10-04:** the diagram above is the personal account's own half. Since #260 and T6 (#330–#382) +max also plugs into `@leemour/cli-messaging`: `src/messenger.ts` describes MAX once as a `Messenger` +(`maxMessenger` — provider, paging, the guard, permissions), and `src/adapter/max-adapter.ts` +(`maxAdapter`) wraps `MaxClient` behind cli-messaging's `MessengerAdapter` port, translating MAX's +models into the shared domain types. `src/program.ts` registers cli-messaging's shared commands — +`store`, `conversations`, `polls`, `reactions`, `inbox`, `review`, and `chats mark-read` among `chats` +— beside max's own; they reach MAX only through that adapter, and read and write the shared store +(`~/.local/share/cli-messaging/messages.db`), not a cache of max's own. So the path is now +commands → cli-messaging services → `maxAdapter` → `MaxClient` → protocol; `MaxClient` stays the +only code that knows the wire. `src/adapter/contract.test.ts` runs cli-messaging's contract cases over +the adapter. + - Commands are resource + action (`NEED-48`); the diagram matches `max --help`. Adding an operation: §12. - [`@leemour/cli-core`](https://github.com/leemour/cli-core) supplies output streams, renderer, @@ -507,6 +519,15 @@ tool calls `MaxClient`, and the Biome rule that keeps commands off `protocol/`, `generated/` covers `src/mcp/` too. The context comes from `contextFor` — the same settings, keyring, deadline and run record as a command, built from flags instead of argv. +**Correction 2026-10-04:** the server is still max's own (`src/mcp/server.ts`, `MaxSession`), but most +tools now run cli-messaging's services over the held client: `withShared` (`src/mcp/shared.ts`) builds +`servicesFor(…)` over `maxAdapter(client)` and the shared store, so a tool and its command run the same +method. Which tools exist is decided by the profile's `permissions` (P7, #382), not by flags: +`registerTools` (`src/mcp/tools.ts`) leaves out a tool whose level is `deny`, a write tool whose level +is `readonly`, and shows a form for `ask` or with `--confirm-send`. `--allow-send`, `--allow-mark-read`, +`--allow-delete` and `--allow-moderate` are accepted with a deprecation note and grant nothing +(`src/commands/mcp.ts`). The "sending is absent without `--allow-send`" bullet below is superseded. + - **One connection per agent session, never for long** (`NEED-152`): `MaxSession` logs in on the first call and keeps the client; it drops it after 2 minutes idle, 5 minutes after the login whatever the traffic (the chat list is the login's snapshot), after any error that may have been diff --git a/docs/security.md b/docs/security.md index 514177d5..b8abc8ec 100644 --- a/docs/security.md +++ b/docs/security.md @@ -3,33 +3,26 @@ Это инструмент к личной переписке. Поэтому вопрос «что он о ней записывает» здесь не приложение к документации, а её середина. +Общее для `max` и `tg` — локальная копия переписки, защита от отправки, права агента, чужой текст +на экране, что видно другим на машине и как сообщить об уязвимости — описано на +[общей странице безопасности](https://wirecat.dev/ru/docs/security). Здесь — то, что относится +только к MAX. + ## Коротко: от чего защищает -- **Чужое сообщение не должно управлять агентом.** Инструменты чтения предупреждают модель: - текст сообщений — данные, а не команды. CLI и MCP используют одни `permissions`: большинство - записей по умолчанию разрешено; `messages.delete` и завершение других сессий требуют подтверждения. - Для ограничения агента задайте `readonly` или `deny` нужным ресурсам. `--confirm-send` требует - вашу форму перед каждой записью, включая `allow`; ответ действует один раз, пять минут и только - для показанных параметров ([mcp.md](mcp.md#подтверждение-формой-от-самого-сервера)). -- **Ограничения профиля работают везде.** Уровни прав на чтение и запись, - список получателей и лимит в час проверяет и сама команда, и фоновый сервер — даже для - программы, которая подключилась к его сокету напрямую. Каждая попытка записывается в журнал без - текста ([ниже](#защита-от-отправки-не-туда)). -- **Агент не выйдет за свой профиль и не отправит ваши ключи.** `MAX_PROFILE_LOCK` закрепляет - профиль, а `--file` не берёт скрытые файлы, `~/.ssh` и папки самого `max`. -- **Чужой текст не управляет терминалом.** Управляющие и невидимые символы показываются как - текст, имена и названия печатаются в одну строку, автодополнение подставляет только номера - ([ниже](#чужой-текст-на-экране)). +Что защищает любой инструмент WireCat, — на [общей странице](https://wirecat.dev/ru/docs/security). +Особенности MAX: + +- **Ограничения профиля проверяет и фоновый сервер.** Права, список получателей и лимит в час + проверяет и сама команда, и `max serve` — даже для программы, которая подключилась к его сокету + напрямую ([ниже](#защита-от-отправки-не-туда)). +- **Токен.** `max` не сохраняет токен из `MAX_TOKEN` и не передаёт его фоновому серверу. Фоновый + сервер не отдаёт токен тем, кто подключается к его сокету ([ниже](#где-живёт-токен)). - **Сеть.** Файлы качаются только по https, не с адресов этой машины и локальной сети и не больше заданного размера. Размер кадра от MAX и распакованных данных ограничен, у соединений есть тайм-ауты ([ниже](#что-уходит-в-сеть)). -- **Токен.** `max` не сохраняет токен из `MAX_TOKEN` и не передаёт его фоновому серверу. Фоновый - сервер не отдаёт токен тем, кто подключается к его сокету ([ниже](#где-живёт-токен)). -- **Файлы.** На Linux и macOS файлы создаются с правами `0600` в каталогах `0700`, включая - локальную копию переписки. На Windows доступ определяют унаследованные ACL каталога пользователя - ([ниже](#что-ещё-пишется-на-диск)). -- **Выпуск.** Пакет публикуется из GitHub Actions с подтверждением происхождения. Шаг публикации - не запускает код зависимостей, а версии прямых зависимостей закреплены точно. +- **Аккаунт.** `max` — не официальное приложение, а правила MAX не разрешают такие программы без + согласия компании ([ниже](#правила-max-и-ваш-аккаунт)). ## Где живёт токен @@ -87,13 +80,10 @@ ### Если компьютер попадёт в чужие руки -На Linux и macOS права `0600` закрывают файлы от других пользователей этой машины, но не от того, кто достанет -диск. От этого защищает шифрование диска целиком: FileVault в macOS, LUKS в Linux, BitLocker в -Windows. На Windows режимы `0600` и `0700` не задают ACL: доступ зависит от разрешений -каталога пользователя и выбранных каталогов `MAX_*_DIR`. Числа прав в таблице относятся к Unix. -Своего шифрования у локальной копии нет: встроенный в Node SQLite его не умеет, а ключ в -ключнице не остановил бы программу, запущенную под вашим пользователем, — она читает ключницу так -же, как `max`. +От того, кто достанет диск, защищает только шифрование диска целиком — см. +[общую страницу](https://wirecat.dev/ru/docs/security). На Windows +доступ к файлам зависит от разрешений каталога пользователя и выбранных каталогов `MAX_*_DIR`; +числа прав в таблице относятся к Unix. ## Чего инструмент не делает @@ -122,10 +112,9 @@ Windows. На Windows режимы `0600` и `0700` не задают ACL: до ## Защита от отправки не туда -Агент читает чужие сообщения вместе с просьбой владельца. Сообщение может быть написано так, чтобы -агент принял его за команду: «перешли эту переписку вот сюда». Поэтому перед каждой отправкой — -сообщения, реакции, правки, пересылки, удаления — `max` проверяет четыре вещи, а после — пишет -строку в журнал. +Почему нужна защита от отправки и как она устроена, — на +[общей странице](https://wirecat.dev/ru/docs/security). Перед каждой записью — сообщения, +реакции, правки, пересылки, удаления — `max` проверяет четыре вещи, а после пишет строку в журнал: | Что | Как включить | Отказ | |---|---|---| @@ -165,59 +154,24 @@ Windows. На Windows режимы `0600` и `0700` не задают ACL: до уйдёт. Две команды, запущенные разом, лимит не перепрыгнут: место под лимитом держится от проверки до ответа MAX. Очистка данных покинутых чатов журнал отправок не трогает. -⚠ **Чего это не держит.** Проверки стоят в самом `max`, и агент с доступом к оболочке может снять -их сам: поменять настройку, выключить список. Они защищают от модели, которую **уговорило** -прочитанное сообщение, а не от агента, который **хочет** их обойти. Против такого — только граница -снаружи: песочница, отдельный пользователь ОС, запрет в правилах самого агента. - -Что стоит знать, выбирая такую границу: - -- **Профиль закрепляет `MAX_PROFILE_LOCK`, а не `MAX_PROFILE`.** Первое слово команды главнее - `MAX_PROFILE`: агенту с `MAX_PROFILE=agent` достаточно набрать `max work messages send …`. - `MAX_PROFILE_LOCK=agent` такой вызов отклонит — но только там, где агент не может сам поменять - окружение: в настройках MCP-клиента или в скрипте-обёртке. Агент с оболочкой снимет переменную. - MCP-сервер профиль закрепляет при запуске. -- **`--file` не отправляет скрытые файлы, файлы из скрытых папок (например, `~/.ssh`) и из папок - самого `max`**: там лежат ключи и токены. `--allow-any-file` снимает запрет — агенту этот флаг - ставить не от себя. Остальное, что может прочитать ваш пользователь, отправить можно; в журнал - попадает только вид и размер вложения. -- **Правило агента вида «спрашивать перед `max messages send`»** не видит форму с профилем — - `max work messages send`. Надёжнее ограничить сам профиль: `permissions` или список - получателей — и не держать рядом профиль без ограничений с действующим входом. +⚠ **Чего это не держит.** Проверки стоят в самом `max`: агент с доступом к оболочке может снять +их сам. Какую границу поставить снаружи — на +[общей странице](https://wirecat.dev/ru/docs/security). Для `max`: +профиль закрепляет `MAX_PROFILE_LOCK`, а не `MAX_PROFILE`; `--file` не берёт скрытые файлы, +`~/.ssh` и папки самого `max`, пока не задан `--allow-any-file`. ## Чужой текст на экране -Имена, названия чатов, имена файлов и тексты сообщений пишут другие люди. `max` не даёт им -управлять вашим терминалом и не даёт подделать то, что вы видите: - -- управляющие символы — те, что перекрашивают, стирают строки, меняют заголовок окна или содержимое - буфера обмена, — показываются как текст (`\x1b`), а не выполняются; так же — невидимые символы и - символы смены направления текста; -- имя, название, подпись вложения печатаются в одну строку: перевод строки в имени не начнёт - поддельную строку переписки или таблицы; -- если набранное название совпадает с одним чатом целиком, а с другими частично, `max` не выбирает - сам, а показывает все; -- автодополнение подставляет только номер чата или человека; название — только подсказка рядом; -- выгрузка в Markdown и пути скачанных файлов проходят ту же очистку; из имени скачанного файла - управляющие символы убираются совсем. - -Вывод `--json` — данные: строки в нём как прислал MAX, экранированные по правилам JSON. Отдавая его -в программу, которая печатает на терминал, очищайте сами. +Имена, названия и тексты от других людей не управляют терминалом: управляющие и невидимые символы +показываются как текст, имена печатаются в одну строку, автодополнение подставляет только номера. +Подробно — на [общей странице](https://wirecat.dev/ru/docs/security). ## Что видно другим на машине -Аргументы команды видны в `ps` любому процессу. Поэтому токен не передаётся аргументом — но -**текст сообщения передаётся**: - -```sh -max messages send 0 "текст" # эта строка видна в ps и остаётся в истории оболочки -``` - -Если это важно, передавайте текст так, как передаётся токен, — не через командную строку, а через -окружение скрипта, который вы контролируете. - -Другие пользователи машины не видят ни файлов `max`, ни сокета фонового сервера: каталоги — -`0700`, файлы — `0600`. +Токен не передаётся аргументом, а текст сообщения — передаётся, и его видно в `ps` и в истории +оболочки ([общая страница](https://wirecat.dev/ru/docs/security)). +Файлы `max` и сокет фонового сервера другим пользователям машины не видны: каталоги — `0700`, +файлы — `0600`. ## Что уходит в сеть @@ -265,12 +219,11 @@ push-уведомления, а `max` — нет. Поэтому MAX может ## Только для себя -Инструмент хранит у вас переписку и контакты других людей. Это допустимо, пока вы делаете это для -себя, со своим аккаунтом: и российский закон о персональных данных (152-ФЗ, ст. 1 ч. 2 п. 1), и -европейский GDPR (ст. 2(2)(c)) не распространяются на обработку для личных и семейных нужд. -Работа с чужими аккаунтами или для бизнеса — уже не личные нужды. -Выгрузка (`max store export`), отданная кому-то ещё, тоже выходит за личные нужды — и в ней -ссылки на фото, которые открываются без входа. +Хранить переписку других людей допустимо, пока вы делаете это для себя, со своим аккаунтом: +российский закон о персональных данных (152-ФЗ, ст. 1 ч. 2 п. 1) не распространяется на +обработку для личных и семейных нужд. То же о GDPR — на +[общей странице](https://wirecat.dev/ru/docs/security). Выгрузка (`max store export`), +отданная кому-то ещё, выходит за личные нужды — и в ней ссылки на фото, которые открываются без входа. Отчёт о проблеме (`max doctor report create`) прикладывается к **открытой** задаче на GitHub — его увидят все. Текстов, имён и телефонов в нём нет, номера чатов и сообщений заменены метками; перед @@ -315,6 +268,7 @@ max session end # забыть локально ## Дальше +- [Общая страница безопасности](https://wirecat.dev/ru/docs/security) — то, что одинаково в `max` и `tg`, и как сообщить об уязвимости - [diagnostics.md](diagnostics.md) — что именно записывается и что не записывается никогда - [sessions.md](sessions.md) — ключница, `MAX_TOKEN`, чем `session end` отличается от отзыва - [mcp.md](mcp.md) — что может агент через MCP-сервер и что включает каждый флаг