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

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

Эту разницу легко не заметить, когда в карточке одобрения написано «Python» или «Node» и показан внушающий доверие значок подписи. Я видел, как проверяющие одобряли такой запрос, потому что название бинарного файла было знакомым, а потом до конца дня выясняли, какой checkout репозитория, символическая ссылка, настройка окружения или предварительно загруженный пакет на самом деле предоставили код. Знакомые исполняемые файлы заслуживают доверия, соответствующего исполняемым файлам. Они не делают произвольный исходный код заслуживающим доверия.

## Подписанный интерпретатор определяет только сам интерпретатор

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

Сравним два вызова:

```text
/usr/bin/python3 /Users/dev/work/release/publish.py
/usr/bin/python3 -c "import os; os.system('curl ...')"
```

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

Документация Python описывает разные варианты командной строки для запуска файла, выполнения команды с `-c`, запуска модуля с `-m` и чтения исходного кода из стандартного ввода. Это обычное поведение интерпретатора, а не дефект. Ошибка возникает, когда эти варианты считают программами с одной стабильной подписанной основой.

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

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

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

## Объект проверки, это кортеж выполнения

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

Для вызова Python на основе файла минимально полезный кортеж выглядит так:

```json
{
  "caller": {
    "pid": 48172,
    "signing_authority": "Example Development Team"
  },
  "interpreter": {
    "resolved_path": "/usr/local/bin/python3.12",
    "signing_identity": "Python Software Foundation",
    "sha256": "c44e...9a10"
  },
  "argv": ["/usr/local/bin/python3.12", "/private/var/run/gateway-src/publish.py"],
  "source": {
    "display_path": "/Users/dev/work/release/publish.py",
    "sha256": "6ab1...ee42"
  },
  "context": {
    "working_directory": "/Users/dev/work/release",
    "environment": {"DEPLOY_ENV": "staging"}
  },
  "requested_action": "POST https://api.example.invalid/releases"
}
```

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

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

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

## Python может скрывать код за обычными средствами запуска

Python создает впечатление, что простой вызов файла устроен легче, чем есть на самом деле. `python deploy.py` показывает, где начинается выполнение, но интерпретатор может импортировать модули из каталога скрипта, установленных пакетов, настроенных путей поиска и кода, выбранного логикой приложения. Виртуальная среда также может изменить интерпретатор, который разрешается по неквалифицированной команде `python`.

Сначала разрешите исполняемый файл, а уже потом проверяйте его подпись. Метка `python`, `python3` или `venv/bin/python` не является идентификатором. Запускающий файл может быть символической ссылкой, прослойкой или другим бинарным файлом после обновления цепочки инструментов. Шлюз должен разрешить объект, который запустит ядро, проверить именно его и записать его путь и дайджест.

Затем рассматривайте путь к исходному коду как средство отображения, а не как границу безопасности. Разрешите символические ссылки до канонического расположения для записи проверки, но не запускайте после проверки изменяемый исходный файл. Checkout репозитория может заменить `deploy.py`, не меняя путь. Символическая ссылка может указывать на другую цель. Одна лишь проверка пути не обнаруживает ни одно из этих событий.

Практическая последовательность выглядит так:

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

Эта копия нужна не для галочки. Она закрывает разрыв между проверкой и использованием. Если проверяющий одобрил дайджест `6ab1...ee42`, интерпретатор должен прочитать байты с дайджестом `6ab1...ee42`. Хеширование файла в репозитории с последующим повторным чтением этого файла Python оставляет небольшое, но реальное окно для подмены.

Импорты также требуют решения. Если `publish.py` импортирует локальный `release_helpers.py`, измененный помощник может изменить поведение, даже если входной файл остался прежним. Строгий вариант, манифест исходного кода, включающий каждый локальный модуль, разрешенный для этого выполнения. Более практичный вариант для обычной работы, вместе разместить входной скрипт и объявленное дерево локального пакета, запретить импорты за его пределами и требовать нового одобрения при изменении дайджеста манифеста.

Не делайте вид, что это обнаруживает динамические импорты, нативные расширения, `sitecustomize` или произвольный код, загруженный во время выполнения. Не обнаруживает. Если такие возможности разрешены, карточка одобрения должна назвать их. Скрипт с `importlib.import_module(os.environ["PLUGIN"])` не заслуживает такого же широкого одобрения, как автономный скрипт, лишь потому, что оба начинаются с одного подписанного интерпретатора.

## Входной файл Node, лишь часть программы

Node добавляет другой уровень неоднозначности. У `node task.js` есть входной файл, но разрешение модулей может выбирать код через `package.json`, экспорты пакета, lock-файлы, символические ссылки и текущий каталог. Документация командной строки Node также описывает предварительную загрузку, например через `--require` и `--import`; такой код может выполняться до начала входного файла.

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

```text
node tools/publish.mjs
node --import ./tools/setup.mjs tools/publish.mjs
node --require ./tools/patch.cjs tools/publish.mjs
node -e "require('child_process').execSync(process.argv[1])" "git push --force"
```

Проверяющий, который видит только `tools/publish.mjs`, не заметит код, выполняющийся раньше во втором и третьем вызовах. В последнем вызове проверенного входного файла нет. Строка команды, это артефакт исходного кода, ее нужно показать, сохранить и захешировать именно так.

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

Менеджеры пакетов создают еще одну ловушку. `npm run publish` часто воспринимается как именованная задача, но поведение определяется изменяемым `package.json`, его скриптами, lock-файлом, хуками менеджера пакетов и бинарными файлами в дереве зависимостей проекта. Перед одобрением имя задачи нужно развернуть. Покажите разрешенную команду, ревизию проекта или подготовленный манифест и каждый жизненный цикл, который будет выполнен. Если развернуть задачу нельзя, запросите более узкое одобрение или отклоните запрос. «Запустить скрипт пакета» не описывает действие, если файл пакета может измениться во время выполнения.

Для повторяемой автоматизации Node подготовьте снимок проверенного рабочего пространства или используйте неизменяемый артефакт сборки. Хеша одного `publish.mjs` достаточно только тогда, когда у скрипта нет локальных зависимостей и пути предварительной загрузки. Большинство нетривиальных проектов этому условию не соответствуют.

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

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

Сравним эти запросы:

```text
/bin/sh -c 'curl -fsS "$RELEASE_URL" | sh'
/bin/sh /private/var/run/gateway-src/release.sh
```

Для первого запроса нужны точная строка команды, все важные значения окружения и объяснение того, что получит последующая программа. Для второго нужны те же сведения о пути и содержимом скрипта, что и для Python или Node. Подпись `/bin/sh` говорит, кто предоставил анализатор. Она не делает безопасным ни один из входных источников.

Не одобряйте команду оболочки по ее первому глаголу. `git status` может быть безопасным при одном точном векторе аргументов, а `git -c credential.helper=...` меняет входные данные, которые загрузит Git. `curl` может получить данные, записать файл или передать байты другому интерпретатору. Проверяющему нужно видеть достаточно синтаксиса, чтобы заметить перенаправление и подстановку, а также достаточно контекста выполнения, чтобы понять, где разрешаются программы.

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

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

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

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

Небезопасный вариант легко узнать:

```text
1. Прочитать /workspace/scripts/publish.py
2. Вычислить и показать SHA-256
3. Дождаться одобрения
4. Запустить python /workspace/scripts/publish.py
```

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

Используйте одну из следующих моделей:

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

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

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

Используйте SHA-256 или другой современный криптографический дайджест с фиксированной кодировкой и всегда указывайте название алгоритма в записях. Отдельная шестнадцатеричная строка в будущем вызовет вопросы. Запись должна содержать `sha256:6ab1...ee42`, а не только `6ab1...ee42`.

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

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

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

```text
Caller: signed process from Example Development Team, PID 48172
Action: POST release data to api.example.invalid
Interpreter: /usr/local/bin/python3.12, signed by Python Software Foundation
Source: /Users/dev/work/release/publish.py
Reviewed bytes: sha256:6ab1...ee42
Execution copy: /private/var/run/gateway-src/6ab1...ee42/publish.py
Context: DEPLOY_ENV=staging, working directory /Users/dev/work/release
```

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

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

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

## Границы зависимостей должны быть явными

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

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

Простой манифест может содержать относительные пути и дайджесты:

```text
sha256  publish.py  6ab1...ee42
sha256  release_helpers.py  9d07...1a3c
sha256  config/targets.json  743e...64b1
```

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

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

## Журналы должны отвечать, что выполнялось, а не как это назвала система

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

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

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

Разделенные журналы Sallyport здесь полезны: они отделяют запуск агента от отдельных HTTP- или SSH-вызовов, сохраняя их общую связь через один зашифрованный журнал аудита с цепочкой хешей. Команда `sp audit verify` может проверять цепочку офлайн поверх шифротекста. Это свойство важно для проверки того, не изменились ли сохраненные свидетельства задним числом.

## Считайте изменение исходного кода новыми полномочиями

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

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

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

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