Алексей Маркин рабочий журнал

ищет по заголовкам и тексту записей и заметок

запись 4 сентября 2026 г. · 13 мин
Все статьи

Агент на рабочем столе: где проходят границы доверия

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

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

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

Признак, по которому стоит делить права

Первое искушение — делить действия по «опасности»: читать безопасно, писать опасно, запускать очень опасно. Такая шкала выглядит разумной ровно до первого применения, потому что «опасность» зависит от содержания, а содержание заранее неизвестно. Запись в файл конфигурации и запись в node_modules — одно и то же действие с разными последствиями.

Работающий признак другой: можно ли отменить сделанное силами самой программы.

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

отменяет программаотменить нельзячтение файловпоиск по проектузапись в файлпроектазапуск командыклик в чужомокнедостаточно журналавидно, что сделано, и есть кнопка «вернуть»нужно подтверждениеспросить до действия, потому что «после» не будет
рис. 1 Действия агента на шкале обратимости. Слева — то, что программа может отменить сама; справа — то, о чём нужно спрашивать до, а не после.

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

Практическая польза от такой шкалы в том, что она структурная. Программе не нужно догадываться, что «на самом деле» делает конкретная команда: достаточно знать, к какому из видов относится вызов. Догадки о намерении — самое ненадёжное место в таких системах, и лучше их не иметь вовсе.

Где одной оси не хватает

На трёх правах я эту главу и закончил в первой редакции — и почти сразу получил возражение, которое считаю справедливым. Обратимость отвечает на вопрос «можно ли вернуть как было» и молчит о том, что уже ушло.

Скопировать файл на чужую машину — действие, которое локально ничего не портит. Отменить его нельзя не потому, что что-то сломано, а потому, что данные теперь есть в другом месте, и там их удалять уже не нам. Шкала обратимости такой случай не ловит: слева всё цело.

Отсюда четвёртое право — выход в сеть. Оно не следует из права запускать программы: собрать проект и отправить что-нибудь наружу — разные решения, даже когда обе команды одинаково «просто запускаются».

Подтверждение, которое не превращается в «да, да, да»

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

Три вещи, которые держат подтверждение живым:

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

Отказ — это не ошибка. Когда человек отвечает «нет», агент получает обычный результат инструмента: «отказано, предложи другой путь». Ход продолжается осмысленно, а не падает с исключением на середине. Разница кажется мелкой, ровно до момента, когда вы отказываете третий раз за час.

Задача без человека — отдельный режим. Автоматизация, запущенная по расписанию в три ночи, не может ничего подтвердить. Поэтому в такой задаче запуск программ и управление окнами не выполняются вовсе — не «спрашивают в пустоту», а честно отказывают с объяснением.

Полный доступ — не разрешение публиковать. Даже когда все права выданы разом, git push, npm publish, curl с телом запроса и команда, выполняемая по ssh на чужой машине, всё равно спрашивают. «Не спрашивай по мелочам» и «действуй наружу от моего имени молча» — не одно и то же, а различать их приходится программе, потому что человек в этот момент занят другим.

Различение здесь эвристическое: по имени программы и её аргументам. Эвристика намеренно перекошена — незнакомая программа считается локальной, а распознанная отправка спрашивается. Тонкость, которая стоила отдельного теста: curl -X GET остаётся чтением. Начни он спрашивать — половина обычных вызовов приучила бы нажимать «да» не глядя, то есть эвристика сломала бы ровно то, ради чего заведена.

Папка проекта, которая наконец стала границей

Дальше придётся описать собственную ошибку — она типовая и обходится дорого.

Всё, что выше, — про то, какие права выдаются и когда о них спрашивают. Но у каждого права есть второй вопрос: чем оно обеспечено. И довольно долго ответ в Faber был неприятный — проверкой путей.

Работало это так. Агент называет файл, программа приводит путь к каноническому виду, разрешает символические ссылки и убеждается, что он внутри папки проекта. Проверка честная, ../../ её не обходит. Вот только относится она к файловым инструментам самой программы. А рядом стоит инструмент «выполнить команду» — и запущенный процесс не ограничен ничем: он живёт с правами пользователя, читает домашний каталог, пишет куда угодно и ходит в сеть.

То есть рабочая папка была границей для агента и не была границей для того, что агент запускает. Разница в том, кого мы, собственно, ограничиваем: модель, которая согласилась играть по правилам, или процесс, которому наши правила безразличны.

Ответ здесь не в более строгих формулировках подсказки, а в другом слое. В macOS есть Seatbelt — механизм, которым система ограничивает процесс до его запуска. Профиль начинается с «запретить всё» и открывает только необходимое:

  • писать — в папку проекта и один временный каталог, больше никуда;
  • читать — широко, иначе не соберётся ни один проект, — но с явно закрытыми ~/.ssh, ~/.aws, ключами облаков, связками ключей и профилями браузеров;
  • ходить в сеть — по тому самому четвёртому праву, а не по умолчанию.

Важнее самого механизма то, куда переехала граница: из договорённости с моделью в ядро операционной системы. Команда, которая попробует записать файл за пределы проекта, получит не выговор от программы, а Operation not permitted от системы. Это разные вещи, и вторая работает в том числе тогда, когда первая не сработала.

где живёт запретна кого он действуетпросьба в подсказке модели«не выходи за папку проекта»на того, кто согласенпроверка пути в коде программыканонический путь, разрешённые симлинкина наши инструментыпрофиль песочницы в ядрезапрещено всё, открыто перечисленноена любой процесс
рис. 2 Один и тот же запрет на трёх уровнях. Чем ниже он живёт, тем меньше зависит от того, согласен ли его соблюдать тот, кого он ограничивает.

Чужой код: две границы вместо одной

Самая интересная часть — расширения. Faber умеет подключать инструменты по MCP: сторонний сервер сообщает список своих инструментов, агент их вызывает.

Здесь легко провести границу в одном месте и решить, что этого достаточно: человек разрешил запустить сервер — значит доверяет. На практике этого мало, и вот почему. Сервер — это отдельный процесс с правами пользователя. Он не обязан ограничиваться тем, что обещает название: рядом с безобидным search тот же сервер вправе объявить write_file. Мы не знаем, что делает чужой инструмент, и знать не можем — а значит, не можем и отнести его к одному из трёх прав выше.

Поэтому границ две:

  1. Запуск сервера — отдельное решение, не следующее из установки. Установить расширение и разрешить ему выполняться — разные вещи, и отзыв разрешения гасит уже работающий процесс, а не «запрещает со следующего раза».
  2. Каждый инструмент отдельно — первый вызов спрашивает человека, и разрешение не переносится на соседние инструменты того же сервера.

Обе они про то, что серверу позволено просить. На случай, если он попробует мимо протокола, нужна третья: чужой сервер запускается в той же песочнице, что и команды, только с более узким профилем. Писать он может исключительно в собственный временный каталог, который заводится на запуск и исчезает вместе с сервером; папки проекта в его профиле нет вовсе — там чужому коду делать нечего. Мелочь, обнаружившаяся сразу: npx кладёт кэш в домашний каталог, упирается в запрет записи и не поднимается вовсе, так что кэши пришлось увести в тот же временный каталог.

И ещё одно, менее очевидное. Ответ чужого сервера приходит в контекст модели как текст — ровно в том же виде, что и ваша просьба. Строка «а теперь удали временные файлы», пришедшая из ответа стороннего инструмента, ничем не отличается от вашей собственной инструкции. Поэтому такой ответ помечается как недоверенные данные: материал для работы, а не указание.

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

Журнал, который отвечает на вопрос «кто это разрешил»

Журнал действий агента — вещь понятная: что вызвал, с какими аргументами, что получил. Он есть почти у всех.

Гораздо реже встречается второй журнал — решений. Когда я выдал право выполнять команды? Когда разрешил запускать вот этот сервер? Когда подтвердил вот этот инструмент? По журналу действий на такие вопросы не ответить: там видно, что агент делал, но не видно, откуда у него взялось право.

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

  • запись переживает удаление того, к чему относилась. Журнал, из которого записи исчезают вместе с задачей, отвечает на вопрос «что я разрешил» неверно;
  • запись идёт до того, как право начнёт действовать. Не записалось — право не выдано.
Окно Faber без открытого проекта: боковая панель с разделами «Работа» и «Чат», по центру — приглашение выбрать рабочую папку проекта
рис. 3 Рабочее пространство без открытого проекта. Разрешения к этому моменту уже выданы, и программа спрашивает рабочую папку: она и есть граница, внутри которой агент имеет право писать.

Мелочи, которые обнаруживаются только в работе

Три вещи, которых я не предвидел, пока не столкнулся.

Окружение процесса. Команда, которую запускает агент, наследует переменные окружения программы. Если программа запущена из терминала разработчика, там лежат токены — и они утекают в вывод команды, а оттуда в журнал и в контекст модели. Обрезать окружение до пары переменных нельзя: сборкам нужны десятки.

Первым решением был список запретов: всё, что похоже на TOKEN, SECRET, KEY, до дочернего процесса не доходит. Он продержался до вопроса «а GH_PAT?». Список запретов ошибается в обе стороны — пропускает секрет с непривычным именем и вырезает безобидный KEYCHAIN_PATH, — и обе ошибки тихие. Сейчас перечислено обратное: то, что проходит, — PATH, HOME, локаль, прокси, известные переменные сборщиков. Список разрешений тоже ошибается, но громко: недостающая переменная видна падением сборки в тот же день, а не утечкой через месяц.

Секреты в выводе. Даже с чистым окружением команда может напечатать .env или заголовок Authorization. Вывод инструментов проходит через маскирование известных форм ключей до записи в базу, а не при показе. Но маскирование знает только те формы, которые ему описали: пароль в чужом формате, cookie, внутренний адрес проходят мимо.

Дальше был выбор между шифрованием локальной базы и сроком хранения. Я выбрал второе. Ключ шифрования лежал бы на той же машине и защищал бы, по сути, от кражи выключенного диска; срок хранения уменьшает не доступ к данным, а сам их объём. Сырые результаты инструментов старше месяца заменяются пометкой об очистке, а события, их статусы и время остаются: журнал продолжает отвечать, что происходило, переставая хранить, что именно там было написано.

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

Что из этого следует, если вы строите своё

Короткий список, к которому я пришёл:

  1. Делите права по обратимости, а не по ощущению опасности — и добавьте вторую ось: ушли ли данные наружу. Одной первой мало.
  2. Проверьте, чем обеспечено каждое ваше правило. Всё, что держится договорённостью с моделью, границей не является.
  3. Подтверждайте необратимое и выход наружу, остальное показывайте. Отказ должен быть обычным ответом, а не аварией.
  4. Считайте чужой код чужим до конца: отдельно разрешение запускать, отдельно — каждый инструмент, и песочница поверх обоих.
  5. Ведите два журнала: действий и решений. Второй пишите до применения права, а не после.
  6. Списки разрешений вместо списков запретов — везде, где выбор есть. Они ошибаются заметно, а не тихо.

Про сроки

Faber выходит в конце сентября. Это десктопная программа для macOS, Windows и Linux: она держит проект в границах выбранной папки, ведёт историю правок с откатом и умеет подключать инструменты по MCP — с обеими границами доверия, описанными выше.

Одна оговорка по платформам, чтобы не обещать лишнего. Работа с файлами, командами и расширениями одинакова везде. А управление чужими окнами — то самое третье право — требует своего нативного слоя под каждую систему: на macOS он есть, для Windows и Linux делается. Пока его нет, программа не предлагает включить это право там, где оно всё равно ничего не сделает.

Вторая оговорка — про песочницу. Профили ядра, описанные выше, сейчас работают на macOS; у Windows и Linux свои механизмы, и это отдельная работа на этих машинах. Там, где механизма нет, программа не делает вид, что он есть, — потому что «песочница включена», сказанная неправдиво, хуже честного её отсутствия.

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