diff --git a/index.html b/index.html index b0bf914..a8353b9 100644 --- a/index.html +++ b/index.html @@ -222,6 +222,8 @@
  • v0.8 重要变化
  • Docker 运行时
  • 设计洞察
  • +
  • 没有 Web UI 的真相
  • +
  • 部署实战 · LXC + Docker 双层隔离
  • @@ -943,6 +945,338 @@ metadata:

    + +
    +

    20没有 Web UI 的真相 · v0.7 → v0.8 拆分

    +

    + 读到这里你可能发现一件事 —— 本文讲了很多"platform adapter", + 但始终没讲一个网页聊天界面。这不是遗漏,是 v0.8 的事实: + Hermes 上游 v0.8 已经把 Web UI 整个删掉了。 +

    +

    证据对比

    + + + + +
    版本web/ 子项目docker-compose.deploy.yml 有 hermes-web 服务?Docker 镜像
    v0.7.x(2026-04-06 本地快照)✅ 有✅ 有(ports 4410:80)hermes-agent-hermes-agent + hermes-agent-hermes-web(两个容器)
    v0.8.0(本文分析版本)整个被删❌ 连文件都没了只剩 hermes-agent(单容器)
    +

    + v0.8.0 发布日:2026-04-08(见 RELEASE_v0.8.0.md:3)。 +

    + +

    那浏览器里能访问什么?

    +

    + gateway/platforms/api_server.py:1736-1754 列出 v0.8 API Server 的**全部** 17 个路由 + (实际 grep router\.add_ 得到): +

    +
    GET    /health
    +GET    /v1/health
    +GET    /v1/models               ← OpenAI 兼容 models 列表
    +POST   /v1/chat/completions     ← OpenAI 兼容聊天(支持 stream)
    +POST   /v1/responses            ← OpenAI Responses API 兼容
    +GET    /v1/responses/{id}
    +DEL    /v1/responses/{id}
    +
    +GET    /api/jobs                ← cron 任务管理
    +POST   /api/jobs
    +GET    /api/jobs/{id}
    +PATCH  /api/jobs/{id}
    +DEL    /api/jobs/{id}
    +POST   /api/jobs/{id}/pause
    +POST   /api/jobs/{id}/resume
    +POST   /api/jobs/{id}/run
    +
    +POST   /v1/runs
    +GET    /v1/runs/{run_id}/events ← 结构化事件 SSE 流
    +

    + 0 个 HTML 路由,0 个 static 路由,0 个 template 渲染。 + 你在浏览器里访问根路径 / 会直接得到 404,因为 API Server 没有配 root handler。 +

    + +

    仓库里还有静态网页,但 Hermes 不 serve 它们

    +

    + v0.8 仓库里确实还有两个 HTML 相关目录,但都不会被 Hermes 运行时加载: +

    + + + + +
    目录内容用途
    landingpage/665 行 HTML + 521 JS + 1178 CSS + banner/iconsNous 官网营销页(hermes-agent.nousresearch.com)— 拉人下载用,不是聊天 UI
    website/Docusaurus 项目(docusaurus.config.ts + docs/)官方文档站(hermes-agent.nousresearch.com/docs)— 静态生成,独立部署
    +

    + 也就是说:这两个目录是上游自己官网用的源码,不会被 Hermes 自己 serve,更不会跟着 docker compose 起来。 +

    + +

    那人们怎么跟 Hermes 聊?

    +

    + v0.8 把"对话界面"完全交给三类外部入口: +

    +
      +
    1. CLI 终端(hermes 命令 + prompt_toolkit TUI)— 本地最直接
    2. +
    3. 22 个消息平台(Telegram / Discord / Slack / WhatsApp / Signal / Email / Matrix / DingTalk / Feishu / WeCom / WeChat / ...)— Gateway 网关转发
    4. +
    5. REST API(/v1/* OpenAI 兼容)— 配合任意第三方前端 + (Open WebUI / LobeChat / 自建前端)
    6. +
    + +

    为什么 v0.8 删了 Web UI(推测)

    + + +
    + 所以:你直接浏览器打开 hermes.milejoy.com(我们部署的实例)默认是看不到聊天界面的 — + 看到的是我们自己写的介绍页 + 隔离层 Basic Auth。如果要聊天 UI,方案是自建一层聊天前端调用 /v1/* + 或者挂一个 Open WebUI 容器指向 Hermes API。 +
    +
    + + +
    +

    21部署实战 · 公司 VPS LXC + Docker 双层隔离

    +

    + 本文不光是源码解析,也是一次真部署的实录。目标是把 Hermes v0.8 跑到一台已经运行其他服务 + (LobeChat + PostgreSQL + RustFS 等)的公司 VPS 上,且不允许 Hermes 意外碰到邻居。 + 以下是实际方案和踩过的坑,**每一条都来自真实 debug 现场**。 +

    + +

    最终架构

    +
    Internet + │ + Nginx + Let's Encrypt (443) + │ Basic Auth (boss/mile) + │ proxy_set_header Authorization "Bearer …" + ▼ +┌──────────────────────────────────────────────────────┐ + Incus LXC 容器 hermes-box (Debian 13) + security.privileged = true + security.nesting = true + cpu=4 · mem=6GB · 快照 fresh + IP: 10.146.223.10 (incusbr0, NAT) + + ┌───────────────────────────────────────┐ + │ Docker 容器 hermes-agent │ + │ image: hermes-agent:latest (2.5GB) │ + │ privileged + seccomp:unconfined │ + │ apparmor:unconfined │ + │ 0.0.0.0:8642 (API Server) │ + │ Hermes v0.8 + Poe (custom provider) │ + │ 78 bundled skills │ + └───────────────────────────────────────┘ +└──────────────────────────────────────────────────────┘ + + LobeChat(同一宿主,不同 bridge 网络,完全隔离)
    + +

    为什么要 LXC(不只是 Docker)

    +

    + 直接 docker run 的问题是 — Docker 默认 bridge 和宿主网络之间有一定可见性, + 错误命令可以访问到邻居容器(LobeChat 的 PostgreSQL 端口)。我们想要的是: +

    +
      +
    1. 独立 namespace:Hermes 看不到宿主进程、宿主文件系统、邻居容器
    2. +
    3. 独立网络:Hermes 的 docker0 和宿主的 docker0 是两条不同的 bridge
    4. +
    5. 一键回滚:出事 incus restore hermes-box fresh,整个环境秒回基线
    6. +
    7. 一键销毁:incus delete hermes-box --force 不留任何污染
    8. +
    9. 运维栈一致:跟同一人之前的 HiClaw LXC 部署同栈,少一套技术要学
    10. +
    +

    + Incus 选 Debian 13 作为 rootfs,security.nesting=true 允许容器内部再跑 Docker, + 形成LXC → Docker → Hermes runtime 三层。 +

    + +

    9 条踩坑速查

    +

    + 这不是抱怨清单,是给下一个想做同样事情的人节省 4 小时。 +

    + +

    坑 1 · 上游 Dockerfile 漏装 git

    +

    + Dockerfile:14-15 的 apt 包列表: +

    +
    build-essential nodejs npm python3 ripgrep ffmpeg gcc python3-dev libffi-dev procps
    +

    + 没有 git。但 npm install 阶段需要 git 拉 git 类型依赖, + build 在第 17 步失败:npm ERR! syscall spawn git / errno ENOENT。 +

    +

    + 修法:sed 补一行 +

    +
    sed -i 's/procps/procps git/' Dockerfile
    + +

    坑 2 · docker-compose env_file 不支持行内注释

    +

    + .env 里写: +

    +
    TELEGRAM_BOT_TOKEN=          # 等你从 @BotFather 拿到后填
    +

    + 结果 docker compose 把整个 # 等你从 @BotFather 拿到后填 当作 TOKEN 的值, + Hermes 拿去尝试连 Telegram 时报 telegram.error.InvalidToken,容器 crash loop。 +

    +

    + 修法:所有行内注释独立成一行。env_file 解析只认行首 #。 +

    + +

    坑 3 · API_SERVER 绑 0.0.0.0 必须 API_SERVER_KEY

    +

    + 日志:Refusing to start: binding to 0.0.0.0 requires API_SERVER_KEY。 + 这是 Hermes 的硬约束(gateway/platforms/api_server.py), + 防止无 key 的 API server 对外暴露。 +

    +
    API_SERVER_KEY=$(openssl rand -hex 32)
    + +

    坑 4 · docker compose restart 不重读 env_file

    +

    + 改了 .env 之后 docker compose restart 依然用老环境变量。必须 down && up -d + 才会重读。这是 Docker Compose 的设计,不是 bug,但容易让人白花 10 分钟 debug。 +

    + +

    坑 5 · Incus 容器 DHCP 拿不到 IPv4

    +

    + incus list IPv4 列永远是空的,容器 eth0 只有 IPv6。 + 理论上 incusbr0 的 dnsmasq 应该分配 IPv4,实测 DHCPDISCOVER 发出但无 OFFER 回来。 + 可能跟宿主已有的 Docker FORWARD DROP + UFW 规则有关,iptables -I FORWARD 没修好。 +

    +

    + 修法:跳过 DHCP,直接写静态 IP 到容器内的 /etc/systemd/network/eth0.network: +

    +
    [Match]
    +Name=eth0
    +
    +[Network]
    +Address=10.146.223.10/24
    +Gateway=10.146.223.1
    +DNS=8.8.8.8
    + +

    坑 6 · Docker-in-LXC BuildKit 的 spawn sh EACCES

    +

    + 在 LXC 容器里跑 docker compose build,npm 某些原生包(better-sqlite3)postinstall 报: +

    +
    npm ERR! code EACCES
    +npm ERR! syscall spawn sh
    +npm ERR! path /opt/hermes/node_modules/better-sqlite3
    +

    + 尝试无效的修法:security.privileged=truesecurity.nesting=true、 + security.syscalls.intercept.*raw.lxc: lxc.apparmor.profile=unconfined、 + DOCKER_BUILDKIT=0(legacy builder 又被 Dockerfile 的 --chmod 卡住)。 +

    +

    + 最终有效的修法:build 不在 LXC 里做。改为: +

    +
      +
    1. 宿主机直接 docker build -t hermes-agent:latest ./source(已知能成功)
    2. +
    3. docker save hermes-agent:latest | gzip -1 > /tmp/hermes.tar.gz(~2.5GB)
    4. +
    5. incus file push /tmp/hermes.tar.gz hermes-box/tmp/
    6. +
    7. incus exec hermes-box -- docker load -i /tmp/hermes.tar.gz
    8. +
    9. LXC 内的 compose 从 build: 改为 image: hermes-agent:latest
    10. +
    11. build 完成后清宿主镜像 docker rmi hermes-agent:latest(保持宿主干净,只让 LXC 里有)
    12. +
    +

    + Build 在宿主跑,运行时在 LXC,隔离边界一点没破。宿主只多了短暂的 tar 文件。 +

    + +

    坑 7 · Python socket.socketpair() 在 Hermes 启动时 EACCES

    +

    + 容器启动后立刻 crash,Python traceback: +

    +
    File "/usr/lib/python3.13/asyncio/selector_events.py", line 120, in _make_self_pipe
    +  self._ssock, self._csock = socket.socketpair()
    +PermissionError: [Errno 13] Permission denied
    +

    + 原因:Docker default seccomp 在 LXC 嵌套里拦了 socketpair()。 +

    +

    + 修法:docker-compose.yml 给 Hermes 容器加: +

    +
        privileged: true
    +    security_opt:
    +      - seccomp:unconfined
    +      - apparmor:unconfined
    + +

    坑 8 · v0.8 官方 landing/website 路径变化

    +

    + v0.7 的 docker-compose.deploy.yml 引用 ./web 作为单独服务, + v0.8 这个目录不存在了(见第 20 章)。如果你还用老 compose 文件 build,第一步就会: +

    +
    unable to prepare context: path "./source/web" not found
    +

    + 修法:compose 文件只留 hermes-agent 服务,暴露 8642 端口即可。 +

    + +

    坑 9 · Hermes v0.8 默认拒绝浏览器 CORS

    +

    + 关键 debug 时刻。写完聊天 UI 部署上线后,用 curl 测 /v1/chat/completions 返回 200, + 但浏览器打开 UI 点发送按钮返回 HTTP 403。 +

    +

    + 根因:gateway/platforms/api_server.py:183-201 的 CORS middleware 对带 + Origin header 的请求默认拒绝(curl 不带 Origin 能过,浏览器必带 Origin 被拦)。 + _origin_allowed()api_server.py:393-401,检查 self._cors_origins: +

    +
    def _origin_allowed(self, origin: str) -> bool:
    +    if not origin:
    +        return True                       # ← curl 这类非浏览器,放行
    +    if not self._cors_origins:
    +        return False                      # ← 浏览器且未配允许列表,拦
    +    return "*" in self._cors_origins or origin in self._cors_origins
    +

    + 修法:.env 加一行 +

    +
    API_SERVER_CORS_ORIGINS=https://hermes.milejoy.com
    +

    + 多个 origin 用逗号分隔。代码读 env 在 api_server.py:322-323。改完 + docker compose down && docker compose up -d 让 env 重读。 +

    + +

    安全影响分析

    + + + + + + + + +
    维度有效的隔离失去的保护
    文件系统✅ Hermes 看不到宿主和邻居容器的 /opt容器内 root 能 mount / chroot
    进程✅ PID namespace 完全独立
    网络✅ 独立 bridge 10.146.223.0/24,跟 LobeChat docker0 不互通;宿主 Nginx 反代是唯一入口
    seccomp / AppArmor❌ 都 unconfined(为了让 socket.socketpair 能用)
    kernel 漏洞❌ 共享宿主 kernel,kernel exploit 可穿透到宿主
    销毁incus delete --force 秒清整个容器 + docker 层 + volume
    +

    + 对单用户内部场景够用。如果你要做 Manus 那种"每个用户独立 sandbox 防恶意 prompt"的场景, + 这套方案的 kernel 共享就是痛点 — 那时应该上 Firecracker / Kata Containers / gVisor, + 不是 LXC。 +

    + +

    跟 Manus(Firecracker)对比

    + + + + + + + + +
    维度Manus 公共服务Hermes 本次部署
    隔离技术每 session 一个 Firecracker microVM共用 LXC 容器(长命)
    启动时间~1-2s(microVM 冷启)~0.5s(LXC 容器本就启着)
    kernel 隔离✅(独立 kernel)❌(共享宿主 kernel)
    用户模型多租户陌生人单用户 / 内部团队
    成本模型按 session 付费固定资源预留
    运维复杂度需要 orchestrator + session 生命周期管理incus 命令 + 快照
    +

    + 结论:"威胁模型决定隔离技术"。不要看到 Manus 用 Firecracker 就觉得自己也得用 — + 先问自己这三个问题: +

    +
      +
    1. 会不会有陌生人用你的 agent?(不会 → LXC 就够)
    2. +
    3. agent 会跑用户提供的代码吗?(不会 → LXC 就够)
    4. +
    5. kernel exploit 是不是你的现实威胁?(不是 → LXC 就够)
    6. +
    + +

    最终交付

    +

    + 部署好之后的公司实例: +

    + +
    +