Запуск шлюза OpenClaw через launchd на Mac mini: абсолютный путь к node, ProcessType и исправление выхода 78 (2026-05-21)
После перезагрузки LaunchAgent шлюза OpenClaw на арендованной Mac mini M4 в Гонконге, Японии, Корее, Сингапуре или США может не привязать админ-порт, завершиться с кодом 78 в launchctl list или потратить примерно три минуты, прежде чем WebSocket-клиенты перестанут видеть аномальные закрытия 1006. Этот runbook от 21 мая 2026 нацелен на ошибки plist launchd — «голый» node в ProgramArguments и отсутствие ProcessType со значением Interactive — а не на сиротские деревья MCP. Он дополняет восстановление после перезапуска шлюза, чеклист headless SSH при первой загрузке и согласование runtime Node / nvm.
Выход 78, медленный listen и WebSocket 1006 при холодной загрузке
В сообществах на хостах Apple Silicon описывают две разные формы сбоя launchd. Мгновенный выход 78 означает, что launchd так и не выполнил exec бинарника шлюза — часто потому, что ProgramArguments начинается со строки node, пока окружение launchd имеет пустой или минимальный PATH. Задержанная готовность показывает задание «running» в launchctl list, но lsof не находит listener минутами; дашборды тогда логируют код закрытия WebSocket 1006, пока процесс наконец не получит планировщик. Оба сценария отличаются от циклов падений ThrottleInterval, которые бьют по CPU быстрыми перезапусками.
- Код выхода 78 сразу после
launchctl bootstrapили входа в систему — проверьте~/Library/LaunchAgents/*.plistна «голый»node. - 180+ секунд от загрузки до первой успешной health-пробы на иначе простаивающей mini.
- 1006 на управляющем WebSocket, пока SSH работает и диск здоров — listener ещё не поднят, а не ошибка TLS.
- Ручной
node gateway.jsпо SSH работает, а путь LaunchAgent падает — классическое расхождение PATH vs абсолютный бинарник.
node -v различается между login-оболочкой и plist, сначала прочитайте статью о расхождении runtime. Выход 78 — «бинарник не найден»; mismatch — «найден, но неверный ABI».
Почему launchd игнорирует PATH оболочки и понижает приоритет фоновых агентов
LaunchAgents наследуют урезанное окружение по сравнению с интерактивными сессиями Terminal.app. Документация и полевые треды подчёркивают, что EnvironmentVariables в plist не помогают разрешить имя интерпретатора внутри ProgramArguments — launchd разрешает исполняемый файл до применения этих ключей. Поэтому копирование plist с ноутбука, где argv[0] — node, падает на headless mini ProxyMac, даже если в том же XML экспортируется PATH.
Отдельно, когда ProcessType опущен, macOS может отнести шлюз к фоновой нагрузке с эвристиками питания и планирования после перезагрузки. Операторы сообщают о сокращении времени listen при холодной загрузке с порядка трёх минут до нескольких секунд после добавления <key>ProcessType</key><string>Interactive</string> рядом с Label. Считайте это гигиеной планировщика, а не лицензией на безнадзорные GUI-сессии — разовую работу с Keychain сочетайте с VNC по чеклисту первой загрузки, затем оставайтесь на SSH.
Матрица оператора (сигнал → первое исправление)
| Основной сигнал | Первый ответ (порядок важен) | Доказательства для сбора | Откат при ошибке | Ответственный |
|---|---|---|---|---|
| Последний код выхода 78 на label OpenClaw | Заменить argv[0] абсолютным путём $(command -v node); bootout → bootstrap | launchctl print gui/$UID/<label> + XML plist | Восстановить прежний plist из git | Platform SRE |
| Задание running, нет listener >60 с после reboot | Добавить ProcessType Interactive; подтвердить один label plist | С меткой времени lsof -nP -iTCP:<port> -sTCP:LISTEN | Убрать ProcessType, если политика запрещает desktop-сессии | Automation lead |
| Дублирующие listener на админ-порту | Следовать восстановлению single-listener | Два PID в выводе lsof | bootout дублирующего label | On-call |
| Цикл падений <30 с, высокий CPU | Настроить ThrottleInterval / KeepAlive — не эта статья | log show --predicate 'process == "launchd"' --last 5m | Откатить ключи throttle | SRE |
Исправление plist в 9 шагов (SSH на mini ProxyMac)
- Определите label:
launchctl list | grep -i openclawи запишите полное reverse-DNS имя. - Выведите живое состояние:
launchctl print gui/$(id -u)/<label>и зафиксируйте последний код выхода. - Разрешите Node: в том же пользовательском контексте выполните
command -v node(илиwhich node) и запишите абсолютный путь — обычно под/opt/homebrewили~/.nvm. - Отредактируйте plist: задайте argv[0] в
ProgramArgumentsэтим путём; путь к скрипту шлюза тоже сделайте абсолютным. - Добавьте ProcessType: вставьте Interactive в корневой dict, если задержка холодной загрузки совпадает с полевыми отчётами.
- Проверьте XML:
plutil -lint ~/Library/LaunchAgents/<file>.plistперед reload. - Перезагрузите:
launchctl bootout gui/$(id -u) <label>, затемbootstrapтого же пути (или vendor kickstart). - Time-to-listen: опрашивайте
lsofкаждые 5 секунд в течение 120 секунд; цель <15 с на M4. - Задокументируйте: закоммитьте plist в infra-репозиторий; добавьте ссылку на эту статью в runbook для следующего инженера.
/opt/homebrew/bin/node + абсолютный путь к entry-скрипту openclaw-gateway + --config + абсолютный JSON конфигурации — никогда не полагайтесь на cd в обёртке, если не задан WorkingDirectory.
Проверка listener, health-эндпоинта и стабильности WebSocket
После bootstrap подтвердите, что ровно один PID слушает настроенный админ-порт (в операторской документации часто фигурирует 18999 — сверьте с вашим config.json). Выполните curl HTTP health-маршрута, если он включён; затем подключите desktop-клиент и убедитесь, что нет 1006 в первые 30 секунд после reboot. Если health проходит, а инструменты MCP падают, переключитесь на гигиену сирот MCP, а не на повторное редактирование plist шлюза.
Раз в квартал делайте reboot-тест на хостах автоматизации: регрессии launchd часто проявляются только после обновлений безопасности macOS, а не при правках plist в тот же день по SSH. Логируйте uname -r рядом с time-to-listen в системе тикетов.
Профилактика: plists как код и staging-labels
- Храните plists в git с абсолютным путём Node из сборки образа (префикс Homebrew или nvm по умолчанию).
- Разделяйте labels LaunchAgent для dev/staging/prod на одной mini — см. руководство по восстановлению на коллизии портов.
- CI smoke: после деплоя утверждайте listener <20 с через SSH-скрипт, прежде чем помечать хост здоровым.
- Одноразовая lab mini в HK/JP/KR/SG/US для экспериментов с plist — дешевле, чем отладка на production-оркестраторе.
FAQ
Почему LaunchAgent OpenClaw сразу завершается с кодом 78? launchd разрешает ProgramArguments до применения EnvironmentVariables. Строка node без пути не работает, когда PATH пуст под launchd. Замените её абсолютным путём из command -v node, затем выполните bootout и bootstrap plist снова.
Почему шлюзу нужны минуты, чтобы начать слушать порт после перезагрузки? Стандартные plist LaunchAgent могут не содержать ProcessType Interactive, и macOS понижает приоритет фонового запуска на минуты. Добавление ProcessType Interactive в корневой dict часто сокращает time-to-listen с примерно трёх минут до нескольких секунд на mini с Apple Silicon.
Чем это отличается от циклов падений ThrottleInterval? Проблемы ThrottleInterval дают штормы быстрых перезапусков с высокой загрузкой CPU. Выход 78 — одноразовый сбой конфигурации до запуска шлюза. Медленный listen без циклов падений указывает на ProcessType или планировщик ресурсов — а не на KeepAlive, борющийся с битым бинарником.
Почему арендованная Mac mini — правильное место для укрепления launchd OpenClaw
Plist шлюза — это инфраструктура: он должен пережить reboot, обновления ОС и инженеров, знающих только пути Homebrew с ноутбука. Mini на Apple Silicon M4 дают предсказуемое время холодной загрузки, macOS launchd совпадает с документированным потоком LaunchAgent OpenClaw, а размещение в HK / JP / KR / SG / US держит задержку control plane рядом с API, которые вы автоматизируете. ProxyMac позволяет клонировать проверенный plist на staging mini, доказать sub-minute listen по SSH, затем промоутить тот же XML в production — см. цены для выбора региона и справку по паттернам доступа.
Проверьте plist launchd на staging-железе
Арендуйте Mac mini HK / JP / KR / SG / US для укрепления шлюза OpenClaw