跳到主要內容

Hermes S6 容器監管

修改、調試或擴展 Hermes Agent Docker 鏡像中的 s6-overlay 監管樹——添加新服務、調試配置文件網關、理解架構 B 主程序模式。

技能元數據

來源可選 — 使用 hermes skills install official/devops/hermes-s6-container-supervision 安裝
路徑optional-skills/devops/hermes-s6-container-supervision
版本1.0.0
作者Hermes Agent
許可證MIT
平臺linux
標籤docker, s6, supervision, gateway, profiles
相關技能hermes-agent, hermes-agent-dev

參考:完整 SKILL.md

信息

以下是 Hermes 在觸發此技能時加載的完整技能定義。這是技能激活時代理看到的指令。

Hermes s6-overlay 容器監管

何時使用此技能

在處理以下情況時加載此技能:

  • 在 Hermes Docker 鏡像中添加或刪除靜態服務(應在每次容器啟動時受監管的內容,如儀表板)
  • 診斷為何按配置文件的網關未啟動、重啟或在 docker restart 後無法存活
  • 理解為何容器的 CMD 是 /opt/hermes/docker/main-wrapper.sh 以及帶前導破折號的參數如何傳遞給用戶程序
  • 修改 cont-init.d 啟動腳本(UID 重映射、卷種子填充、配置文件協調)
  • 更改按配置文件網關的渲染運行腳本(第 4 階段)

如果你只是運行 Hermes Agent 並希望使用 Docker,請參閱 website/docs/user-guide/docker.md

架構概覽

/init                                  ← PID 1 (s6-overlay v3.2.3.0)
├── cont-init.d ← oneshot setup, runs as root
│ ├── 01-hermes-setup ← docker/stage2-hook.sh
│ │ ├── UID/GID remap
│ │ ├── chown /opt/data
│ │ ├── chown /opt/data/profiles (every boot)
│ │ ├── seed .env / config.yaml / SOUL.md
│ │ └── skills_sync.py
│ └── 02-reconcile-profiles ← hermes_cli.container_boot
│ ├── chown /run/service (hermes-writable for runtime register)
│ └── walk $HERMES_HOME/profiles/<name>/gateway_state.json
│ → recreate /run/service/gateway-<name>/
│ → auto-start only those with prior_state == "running"

├── s6-rc.d (static services, in /etc/s6-overlay/s6-rc.d/)
│ ├── main-hermes/run ← exec sleep infinity (no-op slot)
│ └── dashboard/run ← if HERMES_DASHBOARD=1, runs `hermes dashboard`

├── /run/service (s6-svscan watches; tmpfs)
│ ├── gateway-coder/ ← runtime-registered per-profile
│ │ ├── type ("longrun")
│ │ ├── run ("#!/command/with-contenv sh ... exec s6-setuidgid hermes hermes -p coder gateway run")
│ │ ├── down (marker — present means "registered but don't auto-start")
│ │ └── log/run (s6-log → $HERMES_HOME/logs/gateways/coder/current)
│ └── ...

└── CMD ("main program") ← /opt/hermes/docker/main-wrapper.sh
└── routes user args: bare exec | hermes subcommand | hermes (no args)
— exec'd by /init with stdin/stdout/stderr inherited (TTY for --tui)

關鍵文件

路徑角色
Dockerfiles6-overlay 安裝 + cont-init.d 接線 + ENTRYPOINT ["/init", "/opt/hermes/docker/main-wrapper.sh"]
docker/stage2-hook.sh“舊入口點邏輯”——UID 重映射、chown、種子填充、技能同步。作為 cont-init.d/01-hermes-setup 運行。
docker/cont-init.d/02-reconcile-profiles在每次啟動時調用 hermes_cli.container_boot,以從持久卷恢復配置文件網關槽位。
docker/main-wrapper.sh容器的 CMD。路由用戶參數,通過 s6-setuidgid 降級到 hermes,exec 執行所選程序。
docker/s6-rc.d/main-hermes/run無操作 sleep infinity——存在該槽位是為了使 s6-rc 用戶 bundle 有效;主 hermes 作為 CMD 運行,而非作為受監管的服務。
docker/s6-rc.d/dashboard/run條件服務——除非 HERMES_DASHBOARD 為真值,否則執行 exec sleep infinity
docker/entrypoint.sh向後兼容墊片,exec 執行 stage2 hook。硬編碼舊入口點路徑的外部腳本仍然有效。
hermes_cli/service_manager.pyS6ServiceManagerregister_profile_gateway, unregister_profile_gateway, start/stop/restart/is_running, list_profile_gateways
hermes_cli/container_boot.pyreconcile_profile_gateways()——遍歷持久配置文件,重新生成 s6 槽位,輸出 container-boot.log
hermes_cli/gateway.py::_dispatch_via_service_manager_if_s6攔截 hermes gateway start/stop/restart 並在容器中運行時路由到 s6。

為什麼採用架構 B(CMD 作為主程序,而非 s6 監管)

原始計劃(v1–v3)要求主 hermes 作為受監管的 s6-rc 服務運行。兩個真實的 s6-overlay v3 機制阻礙了這一點:

  1. cont-init.d 腳本不接收 CMD 參數——因此 stage2 hook 無法解析 docker run <image> chat -q "hi" 來設置供服務 run 腳本使用的 HERMES_ARGS
  2. /run/s6/basedir/bin/halt 不會傳播寫入 /run/s6-linux-init-container-results/exitcode 的退出碼。無論何種情況,容器始終以 143 (SIGTERM) 退出。s6 作者 skarnet 在 issue #477 中確認:“如果你想要容器關閉,你需要要麼讓你的 CMD 退出,或者,如果你沒有 CMD,則寫入你想要的容器退出碼然後調用 halt”

因此,我們使用 s6-overlay 原生的 CMD 模式:ENTRYPOINT ["/init", "/opt/hermes/docker/main-wrapper.sh"]。/init 自動將 wrapper 前置到用戶參數之前——因此 docker run <image> --version 變為 /init main-wrapper.sh --version,且 --version 不會被 /init 的 POSIX shell 攔截。wrapper 通過 s6-setuidgid 降級到 hermes,然後 exec 執行所選程序。程序的退出碼成為容器退出碼,完全符合 pre-s6 tini 契約。

權衡:主 hermes 在 s6 下不受監管。這與其在 tini(pre-s6 鏡像)下的行為完全一致。儀表板監管是唯一的保證——而 /run/service/ 下的按配置文件網關獲得完全監管。

快速食譜

驗證運行中的容器中 s6 是否為 PID 1

docker exec <c> sh -c 'cat /proc/1/comm; readlink /proc/1/exe'
# Expect: s6-svscan or init / /package/admin/s6/.../s6-svscan

檢查配置文件網關服務

# /command/ isn't on docker-exec PATH — use absolute path
docker exec <c> /command/s6-svstat /run/service/gateway-<name>
# "up (pid …) … seconds" → running
# "down (exitcode N) … seconds, normally up, want up, …" → s6 wants it up but the process keeps exiting (crash loop)
# "down … normally up, ready …" → user stopped it

手動啟動/停止服務

docker exec <c> /command/s6-svc -u /run/service/gateway-<name>   # up
docker exec <c> /command/s6-svc -d /run/service/gateway-<name> # down
docker exec <c> /command/s6-svc -t /run/service/gateway-<name> # SIGTERM (restart)

查看 cont-init 協調器日誌

docker exec <c> tail -n 50 /opt/data/logs/container-boot.log
# 2026-05-21T06:18:05+0000 profile=coder prior_state=running action=started
# 2026-05-21T06:18:05+0000 profile=writer prior_state=stopped action=registered

添加新的靜態服務

  1. 創建 docker/s6-rc.d/<name>/type,內容為 longrun\n,並創建 docker/s6-rc.d/<name>/run(使用 #!/command/with-contenv sh + # shellcheck shell=sh)。
  2. 在 run 腳本頂部通過 s6-setuidgid hermes 切換到 hermes 用戶(除非你特別需要 root 權限)。
  3. 創建空的 docker/s6-rc.d/<name>/dependencies.d/base,使其等待 base bundle。
  4. 創建空的 docker/s6-rc.d/user/contents.d/<name>,使其加入 user bundle。
  5. Dockerfile 中的 COPY docker/s6-rc.d/ 會自動拾取它——無需其他更改。

更改每個 profile 的 gateway 運行命令

編輯 hermes_cli/service_manager.py 中的 S6ServiceManager._render_run_script。該函數在啟動協調期間也由 hermes_cli/container_boot.py::_register_service 調用,因此它是單一事實來源。更新 tests/hermes_cli/test_service_manager.py::test_s6_register_creates_service_dir_and_triggers_scan 中相應的斷言。

運行 Docker 測試 harness

docker build -t hermes-agent-harness:latest .
HERMES_TEST_IMAGE=hermes-agent-harness:latest scripts/run_tests.sh tests/docker/ -v
# Expect 19 passed, 0 xfailed against the s6 image

該 harness 位於 tests/docker/ 中,當 Docker 不可用時會被跳過。每個測試的超時時間已增加到 180 秒(參見 tests/docker/conftest.py)。

常見陷阱

通過 docker exec 出現 "command not found"

/command/(s6-overlay 放置其二進制文件的位置)僅對由監督樹生成的進程(服務、cont-init.d、main-wrapper.sh)在 PATH 中可用。docker exec <c> s6-svstat … 會因為 "command not found" 而失敗;請始終使用絕對路徑 /command/s6-svstathermes 二進制文件之所以有效,是因為 Dockerfile 將 /opt/hermes/.venv/bin 添加到了運行時 ENV PATH 中。

Profile 目錄所有權

cont-init 協調器以 hermes 身份運行(02-reconcile-profiles 中的 s6-setuidgid hermes)。如果 profile 目錄最終歸 root 所有(例如,因為默認情況下以 root 身份運行了 docker exec <c> hermes profile create …),協調器將無法讀取 SOUL.md 並因 PermissionError 而失敗。緩解措施:stage2-hook.sh每次啟動時都將 $HERMES_HOME/profiles 的所有權更改為 hermes,且具有冪等性。不要刪除該代碼塊。

docker exec 寫入的文件歸 root 所有

docker exec 默認為 root。要麼傳遞 --user hermes,要麼依賴下次重啟時的 stage2 chown 掃描。不要手動以 root 身份在 $HERMES_HOME/profiles/<name>/ 下寫入文件——下一次協調過程會清理它們,但進行中的操作可能會遇到權限錯誤。

服務槽存在但 s6-svstat 顯示 "s6-supervise not running"

服務目錄位於 tmpfs 上,並在容器重啟時被清除。要麼 cont-init 協調器尚未運行(在 docker restart 後稍等片刻),要麼它失敗了。檢查 docker logs <c> | grep '02-reconcile'

Gateway 啟動後立即退出(svstat 中顯示 down (exitcode 1)

最可能的原因是 profile 沒有配置模型或認證。服務槽是正確的——gateway 本身未配置。首先運行 hermes -p <profile> setup。s6 監督器將持續重啟它;這是期望的行為(當你修復配置後,下一次嘗試將成功並保持運行)。

協調器跳過了某個 profile

協調器以 SOUL.md 的存在 作為“真實 profile”標記的關鍵依據。hermes profile create 總是會生成它。如果 profile 目錄缺少 SOUL.md(孤立目錄、部分恢復、備份進行中),協調器會故意跳過它。添加一個 SOUL.md(即使為空)以重新啟用。

“救命,容器以 143 退出!”

檢查是否有東西調用了 s6-svscanctl -t/run/s6/basedir/bin/halt——兩者都會導致 /init 開始階段 3 關閉,但返回 143 (SIGTERM) 而不是期望的退出代碼。這是從 A 到 B 的第二階段架構轉變。對於具有真實退出代碼的容器關閉,你必須讓 CMD (main-wrapper.sh) 正常退出;不要試圖從 finish 腳本控制退出。

  • hermes-agent-dev:通用 hermes-agent 代碼庫導航
  • hermes-tool-quirks:特定的 Hermes-tool 變通方法(sed/grep 等)——在調試 s6 棧與 hermes 內置工具的交互時加載。