Читать 7 мин

Минимизация ответов API для более безопасного контекста AI-агента

Минимизация ответов API не даёт ненужным персональным, финансовым и операционным данным попадать в контекст AI-агента благодаря узким контрактам ответов.

Минимизация ответов API для более безопасного контекста AI-агента

AI-агентам не нужна копия каждого объекта, с которым они работают. Им достаточно информации, чтобы принять следующее решение, выполнить действие и сообщить о результате. Когда API возвращает агенту полную запись клиента, счёт, тикет, настройку репозитория или объект инцидента, хотя агенту нужны только идентификатор и статус, API уже увеличивает масштаб проблемы с раскрытием данных.

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

Практическое решение называется минимизацией ответов API: для каждой задачи агента нужно определить самый маленький полезный ответ, сделать такую форму удобной для запроса, а широкую выдачу оставить исключительным сценарием под контролем человека. Речь не о том, чтобы сделать JSON красивее. Нужно уменьшить число мест, где после обычного автоматического вызова могут появиться персональные, финансовые и операционные данные.

Аутентифицированное чтение всё равно может раскрыть слишком много

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

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

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

Важно различать две вещи: авторизация отвечает на вопрос, может ли вызывающая сторона получить доступ к ресурсу; минимизация отвечает на вопрос, какую часть ресурса она получает для конкретной работы. Токен с разрешением читать invoice:123 может быть правильно авторизован, но всё равно получить небезопасное представление этого счёта.

RFC 9110 IETF описывает представления как информацию, предназначенную отражать текущее или желаемое состояние ресурса. В нём нет требования создавать одно максимально подробное представление для каждого ресурса. Это оставляет полезное пространство для проектирования. У ресурса могут быть краткое, операционное и финансовое представления, если API ясно определяет контракт каждого из них.

Не полагайтесь на инструкцию в промпте вроде «игнорируй персональные данные». Промпты влияют на поведение, а форма ответа контролирует раскрытие. Если endpoint отправил домашний адрес, агент уже получил его до того, как решил бы его проигнорировать.

Начинайте с работы, которую должен выполнить агент

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

Например, агенту, который повторяет неудачные сборки, может понадобиться такой ответ:

{
  "job_id": "job_4821",
  "state": "failed",
  "retryable": true,
  "failure_class": "transient_dependency",
  "attempts_remaining": 1
}

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

Перед изменением endpoint составьте небольшую матрицу задач. Она заставляет обсуждать вопросы, которые иначе остаются расплывчатыми:

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

Поле должно попасть в ответ только в том случае, если оно меняет ветку, которую выбирает агент, входит в запрос действия или должно появиться в пользовательском отчёте. Фраза «может пригодиться позже» не служит достаточным основанием. Именно так list-endpoint обрастают пятьюдесятью полями, и никто уже не знает, от чего зависит каждое из них.

Упражнение также показывает, какие поля лучше вычислять, а не раскрывать. Агенту не нужна запись о зарплате, чтобы понять, требуется ли для одобрения расходов руководитель. Верните approval_required: true. Ему не нужны все права доступа, чтобы понять, можно ли продолжить развёртывание. Верните deployment_permitted: false и стабильный код причины.

Это не безопасность через сокрытие. Это продуманный контракт API, который даёт вызывающей стороне нужный результат, не передавая исходную запись.

Объекты по умолчанию должны быть краткими, а не строками из базы данных

Самая надёжная схема возвращает безопасную сводку по умолчанию для обычных list- и lookup-вызовов. Подробные представления должны быть явными, отдельно авторизованными и редкими. Если каждый вызывающий должен помнить ограничительный параметр, система рано или поздно даст сбой, особенно когда библиотека добавит удобный метод и пропустит этот параметр.

Сводка клиента может выглядеть так:

{
  "id": "cus_7f31",
  "display_name": "Northwind Parts",
  "account_state": "active",
  "open_invoice_count": 2,
  "support_tier": "standard"
}

Не возвращайте email, phone, адрес, налоговый идентификатор, метаданные платёжного инструмента или произвольные заметки только потому, что строка клиента содержит эти поля. Некоторые из них могут быть нужны биллинговому приложению. В сводном контракте для операционного агента им не место.

Есть два рабочих подхода. Отдельный endpoint сводки, например GET /customers/{id}/summary, проще и легче проверять. Параметр проекции, например GET /customers/{id}?view=summary, тоже подходит, если набор представлений фиксирован и документирован. Оба варианта лучше endpoint, который возвращает всё и предлагает каждому клиенту игнорировать ненужное.

Не используйте универсальный переключатель expand=* или include=all для учётных данных, которыми пользуется агент. Во время отладки он становится самым простым путём, а затем остаётся в рабочей системе, потому что удалять его уже страшно. Если подробное представление действительно нужно, назовите его по задаче: view=collections, view=deployment_status или view=case_triage. Название задачи запускает проектное обсуждение. Слово «всё» его отменяет.

Часто возражают, что отдельные представления дублируют код. Да, часть кода преобразования придётся повторить. Но эта цена мала по сравнению с расследованием, почему в транскрипте инструмента оказались налоговый номер или внутренняя заметка об инциденте. Слой преобразования также помогает документировать владельца данных и проверять обещание, что представление для агента исключает чувствительные колонки.

Выбор полей должен использовать список разрешённых полей, а не уловку парсера

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

Такой запрос разумен:

GET /v1/invoices?state=overdue\u0026fields=id,account_id,due_date,amount,currency,collection_state\u0026limit=25

Сервер должен вернуть только поля, разрешённые для этого endpoint и учётных данных. Если вызывающая сторона просит billing_address или payment_reference, отклоните запрос с понятной ошибкой. Не добавляйте чувствительные поля молча и не принимайте произвольные вложенные пути вроде customer.*.

Контракт ответа может точно описывать это поведение:

{
  "error": {
    "code": "unsupported_field",
    "message": "Field 'payment_reference' is not available in the agent invoice view",
    "allowed_fields": [
      "id",
      "account_id",
      "due_date",
      "amount",
      "currency",
      "collection_state"
    ]
  }
}

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

К GraphQL нужно относиться так же внимательно. Клиенты действительно могут запрашивать только названные ими поля, что помогает, но схема всё равно может раскрывать чувствительные поля, вложенные связи могут многократно увеличить число записей, а алиасы усложняют анализ одного запроса. Ограничьте глубину и сложность, при необходимости отключите или ограничьте introspection для конкретного окружения и авторизуйте отдельные поля, а не только объекты верхнего уровня. Ещё важнее создать схему для агентов или сохранённые запросы для небольшого набора разрешённых задач. Широкая схема и вежливая инструкция не образуют узкий интерфейс.

OWASP API Security Top 10 выделяет проблему нарушения авторизации на уровне свойств объекта. Обычно речь идёт о том, что вызывающая сторона получает свойство, к которому ей вообще нельзя обращаться. В случае агентов появляется ещё один сценарий: вызывающая сторона технически имеет доступ, но задача не требует этого свойства, и его не следует передавать в контекст модели. Сохраняйте обе проверки. Спрашивайте: «Может ли этот ключ прочитать поле?» и затем: «Зачем оно нужно этой задаче сейчас?»

Пагинация ограничивает объём, но фильтры определяют релевантность

Сохраняйте подтверждение каждого вызова
Sallyport записывает отдельные вызовы в журнал Activity, оставляя защищённый от незаметных изменений след широких запросов.

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

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

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

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

Не путайте endpoint поиска с разрешением вернуть все подробности совпавших записей. Поиск обычно должен возвращать карточку результата: стабильный ID, название, состояние и, возможно, причину совпадения. После выбора записи вызывающая сторона может получить разрешённое подробное представление. Два вызова менее удобны, чем огромный объект результата, но зато передача чувствительных данных становится заметной и проверяемой.

Произвольный текст и вложенные записи требуют отдельной границы

Структурированные поля проще классифицировать, чем текст, написанный людьми. В произвольный текст попадают имена, номера телефонов, случайно вставленные учётные данные, обвинения, сведения о здоровье, юридические консультации и внутренние мнения. Поле description тикета кажется безобидным при ревью схемы, пока не прочитаешь реальные тикеты за неделю.

Для автономных процессов по умолчанию считайте чувствительными комментарии, заметки, описания, вложения, журналы и тела сообщений. Вместо них возвращайте категорию, короткую классификацию, созданную сервером, или количество, если этого достаточно для выбора действия. Например, агенту могут понадобиться has_customer_reply: true и latest_message_at, но не само сообщение.

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

Если для задачи действительно нужен текст, жёстко ограничьте запрос. Получайте одно сообщение по ID, а не весь тред. Запрашивайте известный лимит символов, который применяет сервер. Не включайте содержимое вложений, если пользователь отдельно не одобрил именно этот запрос. Сообщайте клиенту об усечении, например через content_truncated: true, чтобы агент не додумывал пропущенные сведения.

Вложенные данные создают более тихую версию той же проблемы. Ответ с customer, contacts, invoices, payments и events может выглядеть в коде приложения как один объект. С точки зрения раскрытия это набор независимых массивов данных. Для каждой связи требуйте отдельный endpoint или явное расширение из списка разрешённых. Затем тестируйте самый тяжёлый обычный запрос, а не только удачный сценарий с одной разреженной записью.

Обработка ошибок и наблюдаемость могут восстановить утечку

Проверяйте историю действий API
Проверяйте зашифрованный журнал аудита Sallyport с цепочкой хешей офлайн с помощью sp audit verify, без ключа хранилища.

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

Проверьте весь путь вызова. Как минимум изучите обёртку инструмента агента, режим отладки HTTP-клиента, запись запросов, настройки распределённой трассировки, сервис сообщений об ошибках, очередь задач, локальное хранилище транскриптов и процесс поддержки. Места, где заявлено «мы записываем только метаданные», нужно проверять напрямую, а не принимать на веру.

Проведите тестовую запись через непродуктивное окружение. Заполните поля, которые никогда не должны попадать в контекст агента, отличительными фиктивными значениями, например CANARY_BILLING_ADDRESS_927 и CANARY_INTERNAL_NOTE_927. Выполните настоящую задачу агента, затем найдите эти строки во всех разрешённых хранилищах журналов и трассировок. Повторите проверку для неудачного запроса, тайм-аута и некорректного ответа. Тесты только успешного пути пропускают большинство случаев случайной записи нагрузки.

Полезный журнал вызовов фиксирует действие, не дублируя содержимое:

{
  "time": "2025-03-08T14:03:12Z",
  "caller": "release-agent",
  "operation": "GET /v1/jobs/{id}/retry-status",
  "resource_id": "job_4821",
  "response_view": "retry_status",
  "field_set": ["job_id", "state", "retryable", "failure_class"],
  "result_count": 1,
  "outcome": "200"
}

Записывайте идентификаторы только там, где это допускают ваши правила хранения и доступа. В системах с повышенным риском вместо этого храните ссылку с ключом или короткоживущий идентификатор корреляции. Хеш тоже может раскрывать данные, если исходное значение выбирается из небольшого предсказуемого набора. Не называйте хеширование маскированием, не оценив, сколько вариантов может перебрать атакующий.

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

Отделяйте возможность действия от раскрытия данных в шлюзе агента

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

Sallyport хранит API- и SSH-секреты в зашифрованном хранилище и возвращает агенту результаты действий, не раскрывая сам секрет. Это защищает учётные данные, но владелец API всё равно должен решить, не содержит ли результат ненужную запись аккаунта, вывод команды или операционные подробности.

По возможности задайте для каждой задачи агента именованный шаблон запроса. Шаблон фиксирует метод, host, форму пути, разрешённые поля запроса, лимит страницы и допустимое представление ответа. Шаблон статуса релиза может принимать один ID сервиса и возвращать короткий объект состояния. Не следует разрешать ему произвольный URL и произвольное выражение fields только потому, что их легко передать дальше.

Именно здесь широкая модель прокси создаёт проблемы. Универсальный HTTP-форвардер может быть полезен при разработке, но он не различает «проверь это развёртывание» и «скачай журналы всех артефактов». Намерение нужно выразить в вызываемом действии. Когда новой задаче требуется больше данных, требуйте изменения API или нового шаблона. Это трение полезно: кто-то должен объяснить, зачем дополнительные данные должны попасть в контекст.

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

Отсутствие полей должно быть частью контракта

Не допускайте попадания SSH-ключей в транскрипты
Sallyport выполняет SSH-команды через sp-ssh, а закрытый ключ остаётся внутри зашифрованного хранилища.

Большинство API-тестов проверяет наличие ожидаемых полей. Для API, которыми пользуются агенты, нужны также проверки отсутствия запрещённых полей, в том числе когда код идёт по запасному пути.

Рядом с каждым представлением ответа держите тест списка запрещённых полей. Используйте реалистичные имена, включая вложенные связи и произвольный текст. Тест должен завершаться ошибкой, если позже сериализация добавит поле через настройку ORM по умолчанию, общий DTO или заранее загруженную связь.

forbidden = {
    "email",
    "phone",
    "billing_address",
    "tax_id",
    "payment_reference",
    "internal_note",
    "attachments",
}

body = get_invoice_agent_view("inv_1042")
assert forbidden.isdisjoint(body.keys())
assert "customer" not in body
assert "events" not in body

Такой простой тест проверяет только поля верхнего уровня. Добавьте тесты сериализации, которые обходят всё JSON-дерево, и отдельно проверяйте endpoint списков, поиска, ошибок и выгрузок. Самая серьёзная утечка часто появляется в ответе коллекции, который использует полный сериализатор подробностей, потому что так удалось сэкономить несколько строк кода.

Контрактные тесты должны также устанавливать границы размера ответа. Строгий лимит в байтах подходит не каждому объекту, но разумный потолок предупредит, если разработчик добавил неограниченное текстовое поле или связь. Используйте вместе с ним фикстуру с длинными заметками и множеством дочерних записей, иначе тест создаст ложное чувство безопасности.

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

Делайте получение подробностей заметным и временным исключением

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

Используйте отдельный endpoint или действие, которое принимает стабильный ID записи и заявленную цель. Возвращайте минимально необходимый фрагмент, например одно спорное поле платежа или выбранное сообщение клиента. Не выдавайте полный экспорт аккаунта только потому, что в том же деле есть один спорный платёж.

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

Зрелый API делает безопасный путь самым простым. У сводных представлений должны быть понятные имена, хорошая документация и стабильные поля. Широкие endpoint подробностей должны требовать осознанного действия, потому что несут больше ответственности. Если агент постоянно запрашивает чувствительное поле, не превращайте исключение в норму. Пересмотрите дизайн задачи и подумайте, не справится ли с ней серверное решение или замаскированное производное значение.

Первый полезный аудит обычно стоит начинать с endpoint списка, а не с того, которого все уже опасаются. Возьмите одну реальную задачу агента, отметьте каждое использованное поле и сравните этот список с полученным ответом. Неиспользованная часть станет следующей задачей для изменения API.

Вопросы и ответы

Как понять, какие поля API действительно нужны AI-агенту?

Агенту нужны только факты, необходимые для выбора и выполнения следующего действия. Возвращайте идентификаторы, статус и узкие бизнес-поля, которые поддерживают это действие; чувствительные сведения запрашивайте отдельно и только при наличии обоснования. Считайте окно контекста каналом распространения данных, потому что после попадания ответа туда оно им становится.

Достаточно ли пагинации для защиты чувствительных ответов API?

Постраничная выдача ограничивает объём, но не определяет, содержат ли записи неподходящие поля. Страница из десяти записей всё равно может раскрыть адреса, платёжные ссылки или внутренние заметки. Используйте и пагинацию, и выбор полей.

Стоит ли удалять чувствительные поля из существующего endpoint API?

Обычно нет. У общего endpoint для чтения слишком много клиентов и будущих сценариев использования, поэтому его сокращение может сломать легитимное программное обеспечение. Добавьте проекцию для конкретной задачи или параметр fields с явным включением, а затем постепенно переведите на него вызовы агентов.

Безопасны ли учётные данные API только для чтения у автономных агентов?

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

Как безопасно спроектировать параметр fields?

Используйте явные списки разрешённых полей, отклоняйте неизвестные имена и возвращайте документированную проекцию по умолчанию, если клиент не указал параметр. Не принимайте произвольные пути к объектам или универсальные выражения include без строгой проверки. Схема ответа должна оставаться предсказуемой, чтобы её можно было проверять и тестировать.

Можно ли показывать внутренние заметки AI-агенту для программирования?

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

Что следует записывать, когда агент вызывает чувствительный API?

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

Хранят ли провайдеры моделей данные API, попавшие в контекст агента?

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

Какие endpoint сначала проверять на слишком широкие ответы?

Начните с endpoint, которые возвращают списки, результаты поиска, объекты аккаунтов, счета, тикеты, выгрузки и тела ошибок. Сравните полный ответ с полями, которые агент использовал в итоговом действии. Самые быстрые сокращения обычно дают большие объекты со скопированными заметками или вложенными связанными записями.

Решает ли изоляция учётных данных проблему раскрытия данных в ответах API?

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

Sallyport

Sallyport выполняет API-вызовы и SSH-команды за вашего ИИ-агента. Ключи остаются в локальном хранилище на вашем Mac; вы подтверждаете каждый запуск, и каждое действие попадает в запечатанный журнал.

© 2026 Sallyport · Открытый код по лицензии Apache-2.0 · Oleg Sotnikov