Читать 7 мин

Как одобрять неподписанные локальные сборки?

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

Как одобрять неподписанные локальные сборки?

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

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

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

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

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

Разделяйте эти понятия:

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

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

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

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

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

До одобрения сессии разработчик должен увидеть четыре свидетельства:

  1. Исполняемый файл находится внутри рабочей копии или каталога сборки, которыми управляет разработчик.
  2. Родительский процесс является ожидаемым средством запуска, обычно терминалом, задачей IDE или скриптом-оберткой, которым владеет команда.
  3. Состояние репозитория и команда сборки доступны для быстрой проверки, без попыток восстановить события дня по памяти.
  4. Запрошенное назначение и учетные данные соответствуют текущей задаче.

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

Именно здесь команды часто обращаются к политическому движку: разрешить пути внутри каталога, пропускать команды, начинающиеся с agent, освободить от проверки все, что запустила определенная IDE. Так делать не стоит. Статическое правило привлекательно тем, что убирает прерывания, но оно превращает простое проверяемое решение в долговременное исключение. Любой процесс, способный создать нужный путь или цепочку родителей, унаследует это исключение.

Вместо этого используйте одобрение человека на границе сессии. В карточке запроса стоит показывать полномочия подписи процесса, если они есть. Для неподписанной сборки рядом с этим полем нужен другой контекст: путь к исполняемому файлу, родительская команда, рабочий каталог, если он доступен, и адресат, к которому процесс собирается обратиться. Тогда разработчик сможет ответить на конкретный вопрос: «Это тот клиент, который я только что собрал для этой задачи?»

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

Знакомый путь к файлу является свидетельством, но не идентичностью

Путь вроде ~/src/agent-client/dist/agent успокаивает, потому что рассказывает правдоподобную историю. Но это не граница идентичности. Вредоносный процесс может работать из этого пути, если способен записывать туда файлы, заменить результат сборки, изменить символическую ссылку или заставить средство запуска выбрать другой исполняемый файл.

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

Практический договор запуска может выглядеть так:

#!/bin/zsh
set -eu

repo="$HOME/src/agent-client"
cd "$repo"

git status --short
git rev-parse --short HEAD
exec ./build/agent-client --mcp

Полезен здесь не синтаксис shell. Важны оставляемые им свидетельства. Родительская оболочка запускает известный скрипт, рабочий каталог указывает на рабочую копию, перед стартом клиента выводится ревизия Git, а exec заменяет оболочку целевой программой, не оставляя запутанной цепочки процессов.

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

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

Проверьте процесс до одобрения сессии

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

В macOS для первой проверки подойдут такие команды. Замените пример PID на PID, показанный вашим просмотрщиком процессов или терминалом:

pid=48271

ps -o pid=,ppid=,user=,lstart=,command= -p "$pid"
ps -o pid=,ppid=,command= -p "$(ps -o ppid= -p "$pid" | tr -d ' ')"
lsof -a -p "$pid" -d cwd -Fn

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

48271 47990 alex Tue Jul 22 10:14:03 2026 /Users/alex/src/agent-client/build/agent-client --mcp
47990 23112 /bin/zsh /Users/alex/src/agent-client/scripts/run-local-agent
n/Users/alex/src/agent-client

Вы проверяете цепочку, а не собираете случайные сведения. Бинарный файл должен находиться в ожидаемом каталоге сборки. Родителем должен быть известный вам скрипт запуска. Текущий каталог должен соответствовать рабочей копии. Если процесс пришел из /private/var/folders, ~/Downloads, незнакомого кэша пакетов или неожиданной обертки, проверка не пройдена, даже если имя команды выглядит правильно.

Затем проверьте сам исполняемый файл:

client="$HOME/src/agent-client/build/agent-client"
file "$client"
codesign --display --verbose=4 "$client" 2>&1 | sed -n '1,18p'
shasum -a 256 "$client"

Для неподписанного бинарного файла codesign может сообщить, что подписи нет вовсе. Бинарный файл с ad hoc-подписью сообщает о наличии подписи, но не о публичном центре подписи. Это все равно полезная информация, но не принимайте ее за идентичность команды. Apple называет ad hoc-подпись «Sign to Run Locally» и объясняет, что ее designated requirement связан с конкретной версией кода. После пересборки свидетельства меняются, поэтому одобрение сессии не должно переживать замену процесса.

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

Разработчик должен отклонить запрос, если хоть один элемент цепочки вызывает удивление. Не одобряйте сначала, чтобы разобраться после вызова API. Именно в момент одобрения неопределенность должна стоить нескольких минут, а не расследования инцидента.

Используйте сессию как границу между одной сборкой и следующей

Связывайте вызовы с сессиями
В Activity записываются отдельные HTTP- и SSH-вызовы, связанные с журналом сессий в том же зашифрованном журнале аудита.

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

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

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

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

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

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

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

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

Одобрение сессии подходит по умолчанию для повторяемых действий разработки: чтения метаданных задач, запросов к sandbox API, получения репозитория по SSH или обновления одноразовой тестовой записи. Если требовать от человека отдельного подтверждения для каждого такого вызова, он быстро привыкнет одобрять, не читая запрос.

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

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

  • записывать данные в production-среду;
  • публиковать пакет, релиз или артефакт развертывания;
  • получать экспорт клиентских данных или другой крупный набор конфиденциальной информации;
  • изменять членство в организации, параметры аутентификации или средства восстановления доступа;
  • подключаться к production-хосту по SSH.

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

Детали запроса важны не меньше второго запроса подтверждения. Для HTTP показывайте метод и адресат, а затем достаточно пути, чтобы понять действие, не выводя конфиденциальное содержимое запроса на экран одобрения. GET /v1/test-runs/123 и DELETE /v1/projects/123 не должны выглядеть одинаково. Для SSH показывайте хост и учетную запись и просите разработчика подтвердить, почему этот хост относится к текущей задаче.

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

Считайте пересборки, смену ветки и обертки новыми свидетельствами

Дайте агентам шлюз действий
Встроенный адаптер sp mcp позволяет агентам с поддержкой MCP запрашивать действия, не получая прямого доступа к учетным данным.

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

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

В собственном процессе разработки автоматически приостанавливайте работу в трех случаях:

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

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

К переменным окружения относитесь так же внимательно, как к аргументам исполняемого файла. Клиент, запущенный с API_BASE_URL, SSH_AUTH_SOCK, PATH, DYLD_* или нестандартным путем к конфигурации, может вести себя иначе, чем просмотренный вами исходный код. Обертка должна задавать только нужные клиенту переменные, выводить несекретные значения, влияющие на маршрутизацию, и завершаться с ошибкой при отсутствии обязательных значений, а не подключать огромный профиль shell.

Например, это легко проверить:

export AGENT_ENVIRONMENT=staging
export AGENT_API_ORIGIN=https://staging.example.internal
exec ./build/agent-client --mcp

А это нет:

source "$HOME/.agent-env"
eval "$(tooling configure-agent)"
exec "$AGENT_BIN" "$@"

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

Храните учетные данные отдельно от клиента, даже если клиент собрали вы

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

Поместите секрет в шлюз действий и пусть клиент запрашивает действие по ссылке на него. Клиент должен передать контекст HTTP-запроса или SSH-команды, необходимый для работы. Шлюз добавляет учетные данные, выполняет вызов и возвращает результат. Так решение об одобрении сосредоточено на действии, а не на том, может ли клиент бессрочно владеть токеном предъявителя.

Для HTTP создайте отдельные учетные данные для разных сред и целей. Токен staging не должен попадать в production только потому, что клиент передал другой хост. Для SSH используйте отдельные записи хостов или учетные данные для разных ролей. Не полагайтесь на один универсальный ключ и обещание человека выбрать правильный адресат.

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

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

Журнал аудита должен разрешать разногласия, а не просто собирать события

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

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

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

Здесь особенно полезен журнал с защитой от незаметного изменения, поскольку локальные сборки могут изменяться теми же людьми, которые их проверяют. Sallyport строит журналы Sessions и Activity из одного зашифрованного журнала аудита, связанного хешами. Команда sp audit verify проверяет эту цепочку офлайн по шифротексту, не требуя ключа хранилища. Так команда получает проверку целостности, не открывая проверяющему доступ к секретам, связанным с действиями.

Запускайте проверку во время ревью или тренировки по реагированию на инциденты:

sp audit verify

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

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

Командные правила не дают запросам на одобрение превратиться в фоновый шум

Самая сложная часть процесса одобрения локальных сборок связана не с командной строкой. Важно сохранить человеческий смысл одобрения после месяцев обычной работы.

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

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

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

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

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

Безопасно ли одобрять неподписанные локальные сборки?

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

Одобрять локально собранного агента один раз или при каждом запуске?

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

Как понять, что локальный агент запущен из моей рабочей копии?

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

Делает ли ad hoc-подпись локально собранного агента надежным?

Ad hoc-подпись сообщает macOS об идентичности, связанной с конкретной сборкой, но не подтверждает личность издателя. Она помогает заметить случайную подмену, однако не заменяет одобрение сессии, ограниченные учетные данные и проверку активности. Apple отмечает, что designated requirement ad hoc-подписи связан с конкретной версией кода. (developer.apple.com)

Можно ли доверять любому процессу с тем же именем локальной сборки?

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

Когда неподписанной сборке нужно одобрение каждого вызова?

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

Что делать, если я одобрил не тот локальный процесс агента?

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

Можно ли команде использовать одну общую папку для неподписанных сборок агента?

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

Безопасно ли запускать локального агента через shell-скрипт?

Обычный shell-скрипт подходит, если он задает известные пути и запускает ожидаемый исполняемый файл. Проблемы начинаются, когда скрипт скачивает код, выбирает ветку, раскрывает непроверенную переменную окружения или незаметно запускает другую программу до старта агента.

Сбрасывает ли блокировка Mac доверие к локальному агенту?

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

Sallyport

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

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