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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

```sh
#!/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, показанный вашим просмотрщиком процессов или терминалом:

```sh
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
```

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

```text
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`, незнакомого кэша пакетов или неожиданной обертки, проверка не пройдена, даже если имя команды выглядит правильно.

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

```sh
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. Именно в момент одобрения неопределенность должна стоить нескольких минут, а не расследования инцидента.

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

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

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

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

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

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-релиза с помощью той же сборки стоит остановиться. В этом и состоит смысл такой проверки.

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

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

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

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

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

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

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

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

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

А это нет:

```sh
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` проверяет эту цепочку офлайн по шифротексту, не требуя ключа хранилища. Так команда получает проверку целостности, не открывая проверяющему доступ к секретам, связанным с действиями.

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

```sh
sp audit verify
```

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

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

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

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

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

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

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

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