前篇文章在 QNAP NAS 的 Container Station 建立了 agent-hub 容器,讓 Herdr 伺服器與多個 CLI 代理人常駐執行。實際使用一段時間後,最常遇到的情境是人在外面,代理人卡在一個需要確認的提示上,整個流程停擺,而手機上除了 SSH 加 Termux 之外沒有足夠方便的操作方式。CyberQ 再實測補上這一塊,以 Collie 這個 Herdr 外掛提供手機專用的 Web 介面,透過 Tailscale 限定只有自己的 tailnet 裝置能連進來,並把整套東西塞進同一份 docker-compose 部署在 QNAP NAS 上。
本文假設讀者已完成前篇的 agent-hub 容器部署,我們進行後續的應用擴展。本文補上這一塊:以 Collie 這個 Herdr 外掛提供手機專用的 Web 介面,透過 Tailscale 限定只有自己的 tailnet 裝置能連進來,並把整套東西塞進同一份 docker-compose 部署在 NAS 上。
這是簡單版。如果你不想用 Tailscale 官方的雲端控制伺服器,而是自架 Headscale,那條路會遇到完全不同的問題,請看另一篇。
這篇文章的驗證範圍
本文的內容分兩類,文中會逐處標明:
實測:容器建置、Collie 安裝與行為、資安閘門、以及 QNAP 平台特有的坑,CyberQ 全部在一台 TS-855X(QuTS hero h6.0.2.3591)上實際跑過。
依上游文件:tailscale serve 在 Tailscale 官方雲端上的 HTTPS 行為與身分標頭注入。筆者的驗證環境是自架 Headscale,這一段照官方文件敘述並在文中標示出來。社群回報的經驗與文件一致,而且官方雲端這條路要踩的坑明顯比自架少,主要就是憑證與身分標頭都由 Tailscale 直接提供,不必自己準備反向代理。
為什麼不是 SSH 加 Termux
Collie 作者在文件中點出 Termux 加 SSH 進 Herdr TUI 的三個痛點:手機螢幕鍵盤打字困難,終端機無法使用語音輸入,每次都要重新 SSH 登入。Collie 的目標是一個透過 Tailscale 存取、不必反覆登入的行動版 Web 介面,讓人用手機原生鍵盤與語音輸入檢視與指揮代理人。
目前 Herdr 的行動端方案大致有四種:
| 方案 | 型態 | 連線方式 | 授權 | 適合情境 |
|---|---|---|---|---|
| Collie(AltanS/collie) | Herdr 外掛,Bun 橋接程式加 PWA | tailscale serve,或自備反向代理 | MIT,免費 | 單人單 tailnet 的自架環境 |
| Herdr Mobile | Android 原生 App | 直接 SSH 至主機 | 試用加買斷 | 不想自架任何 Web 服務 |
| herdr-mobile-relay(0cv) | 每台主機各跑一個 relay | 社群 WebRTC 閘道或臨時 Tunnel | 開源 | 要同時管理多台電腦 |
| Moshi | 第三方行動客戶端 | 依官方文件 | 商業產品 | 想在鎖定畫面核准代理人動作 |
有一點要更正常見的說法,Google Play 上的「Herdr Mobile」查得到,但其開發者欄位與官方網站指向的是第三方網域,不是 Herdr 專案本身。稱它為官方 App 缺乏依據。
選 Collie 的理由有三點,它不需要依賴任何雲端中繼,流量只在 tailnet 內走。它是 Herdr 官方外掛機制的一部分,安裝與更新都透過 herdr plugin 完成,它對資安邊界的描述極為坦白,很適合實作環境中有考慮到資安的用戶使用。
先看清楚 Collie 的資安定位
Collie 的文件開宗明義寫著,它在設計上就是對主機的遠端 shell 存取。橋接程式的一個 API 呼叫可以把任意按鍵送進活著的終端機 pane,因此任何能連到這個 URL 的人都能讀取所有 pane 的內容並以你的身分執行任何指令。沒有沙箱,沒有指令白名單。
CyberQ 實測驗證過,在剛裝好、還沒做任何設定的狀態下:
curl -X POST http://127.0.0.1:8787/api/pane/w4:p1/reply \
-H 'Content-Type: application/json' \
-d '{"text":"id -un > /tmp/proof.txt; hostname >> /tmp/proof.txt","submit":true}'
# {"ok":true}
# /tmp/proof.txt 裡面就是這台機器的使用者名與主機名但預設狀態不是最終狀態
很多介紹停在上面那段就結束,這會給讀者錯誤的印象。Collie 1.0 之後有三種互相獨立的寫入閘門,可以疊加:
| 機制 | 問的問題 | 信任的對象 | 撤銷方式 |
|---|---|---|---|
COLLIE_TRUSTED_USER | 這個請求是不是本人? | tailscale serve 注入的身分標頭 | 改 .env 後重啟 |
COLLIE_DEVICE_HEADER | 這台裝置在不在清單上? | 你的代理注入的標頭 | 改允許清單後重啟 |
| 裝置配對 | 這台裝置有沒有我發的憑證? | 不信任網路上的任何東西 | collie devices revoke <label>,即時生效 |
實測結果值得記下來。還沒有配對任何裝置時,寫入閘門是關的,上面那個 curl 會成功。配對一台裝置之後,連 loopback 直連的寫入都會被拒:
未配對的客戶端寫入 403
未配對的客戶端讀取 200
loopback 直連寫入 403 ← 配對之前這一行是 200,而且真的執行了指令所以「把 URL 視同 root 登入」應該理解成,這是預設狀態的描述,而 collie pair 就是關掉它的那個開關。三種閘門都只管寫入,讀取一律放行給任何通過同源檢查的請求。
另外要修正一個常見說法。文件說寫入動作會記進 <state-dir>/audit.log,實測確認格式是 JSONL、權限 0600。但只有成功的寫入會被記錄,被閘門拒絕的嘗試既不進 audit.log 也不進服務日誌。如果你的稽核需求包含「有沒有人試過但被擋下來」,這部分就需要留意。
最後,官方文件反覆強調絕對不可以用 tailscale funnel 發布 Collie,這點是真的,funnel 會把它暴露到公網。
部署架構
手機(Tailscale App 加 Collie PWA)
│ tailnet 內 HTTPS,MagicDNS 名稱 agent-hub.<tailnet>.ts.net
▼
ts-agent-hub 容器(tailscale/tailscale 映像)
│ tailscale serve 終止 TLS,注入 Tailscale-User-Login
│ 反代至 127.0.0.1:8787(與 agent-hub 共用網路命名空間)
▼
agent-hub 容器
├─ Collie 橋接程式(Bun,只綁 127.0.0.1:8787)
├─ Herdr 伺服器
└─ 各家 CLI 代理人,各自活在 Herdr 的 pane 內兩個容器共用網路命名空間,所以 sidecar 的 127.0.0.1 就是 agent-hub 的 127.0.0.1。這樣 NAS 本身的 Tailscale 節點與代理人環境的節點是兩個獨立身分,可以在 ACL 中分開授權。
本文實作的前置條件
一台可執行 Container Station 的 QNAP NAS
一個 Tailscale 帳號,管理主控台已啟用 MagicDNS 與 HTTPS Certificates(tailscale serve 需要 tailnet 憑證)
Herdr 0.7.0 以上(本文實測 0.8.2)
手機安裝 Tailscale App 並登入同一個 tailnet
實作步驟
步驟一:Tailscale 端的標籤、ACL 與授權金鑰
在 tailnet policy file 建立 tag:agent-hub,並限制只有指定帳號可以連到該標籤節點的 443:
{
"tagOwners": {
"tag:agent-hub": ["autogroup:admin"]
},
"grants": [
{
"src": ["[email protected]"],
"dst": ["tag:agent-hub"],
"ip": ["tcp:443"]
}
]
}若既有 policy 仍是預設的全開規則,請一併調整修改,否則上述 grant 沒有實質限制效果。
接著在管理主控台的 Keys 頁面產生授權金鑰,勾選 Pre-authorized,Tags 選 tag:agent-hub,不勾 Ephemeral。
提醒一個容易誤判的地方,金鑰如果帶了標籤,客戶端就不可以再給 --advertise-tags,否則會被回 requested tags are invalid or not permitted,而那個訊息會讓人以為是 tagOwners 寫錯,二選一即可。
步驟二:擴充 agent-hub 映像
在前篇的 Dockerfile 加入 Bun,並改用啟動腳本取代 sleep infinity。
FROM ubuntu:24.04
ENV DEBIAN_FRONTEND=noninteractive
# locales 不是裝飾。Herdr 用框線字元畫側欄,在預設的 POSIX locale 下
# ncurses 會退回 VT100 替代字元集,每一條框線都會渲染成字母 q。
RUN apt-get update && apt-get install -y --no-install-recommends \
curl ca-certificates git openssh-client unzip \
python3 python3-venv tini jq less ripgrep locales \
&& locale-gen en_US.UTF-8 \
&& rm -rf /var/lib/apt/lists/*
ENV LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8 TERM=xterm-256color
# Ubuntu 24.04 內建 Node 18,@google/gemini-cli 需要 20 以上,所以不能用發行版套件
RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& rm -rf /var/lib/apt/lists/*
# Herdr:單一 Rust 執行檔
RUN curl -fsSL https://herdr.dev/install.sh | sh
ENV PATH="/root/.local/bin:${PATH}"
# Bun:Collie 唯一的硬性相依。它的安裝腳本需要 unzip,
# 少了會停在 error: unzip is required to install bun(實測踩過)。
RUN curl -fsSL https://bun.sh/install | bash
ENV PATH="/root/.bun/bin:${PATH}"
RUN npm install -g --no-fund --no-audit \
@anthropic-ai/claude-code @openai/codex @google/gemini-cli
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
WORKDIR /workspace
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["/usr/local/bin/entrypoint.sh"]啟動腳本負責三件事,拉起 Herdr 伺服器、等 socket 就緒、首次啟動時安裝 Collie 並啟動它。
#!/bin/sh
set -e
SOCK="${HERDR_SOCKET_PATH:-/root/.config/herdr/herdr.sock}"
herdr server &
HERDR_PID=$!
i=0
while [ $i -lt 30 ]; do
[ -S "$SOCK" ] && break
i=$((i+1)); sleep 1
done
[ -S "$SOCK" ] || { echo "herdr socket not ready: $SOCK"; exit 1; }
if ! herdr plugin list --json 2>/dev/null | grep -q '"herdr.collie"'; then
herdr plugin install AltanS/collie -y
fi
herdr plugin action invoke start --plugin herdr.collie || true
wait $HERDR_PID網路上流傳的腳本片段常用 node 去解析 herdr plugin list --json,讀 p.id 與 p.path,那樣寫會直接失敗。實際輸出是 JSON-RPC 信封,欄位名稱不同:
{"id":"cli:plugin","result":{"plugins":[{"plugin_id":"herdr.collie","plugin_root":"/...","version":"1.1.0"}]}}上面腳本用的是字串 grep,剛好不受影響。
步驟三:docker-compose
name: agent-hub
services:
tailscale:
image: tailscale/tailscale:v1.102.3
container_name: ts-agent-hub
hostname: agent-hub
restart: unless-stopped
environment:
- TS_AUTHKEY=${TS_AUTHKEY:?Set TS_AUTHKEY in .env}
- TS_HOSTNAME=agent-hub
- TS_STATE_DIR=/var/lib/tailscale
- TS_SERVE_CONFIG=/config/serve.json
- TS_USERSPACE=false
# 金鑰已經帶了 tag,這裡不要再給 --advertise-tags
- TS_ACCEPT_DNS=false
volumes:
- /share/Container/agent-hub/ts-state:/var/lib/tailscale
- /share/Container/agent-hub/ts-serve.json:/config/serve.json:ro
devices:
- /dev/net/tun:/dev/net/tun
cap_add:
- NET_ADMIN
- NET_RAW
networks:
- agentnet
# Hermes 那個 stack 的網路要掛在 sidecar 上,不是掛在 agent-hub 上。
# 理由見下方「QNAP 平台的五個坑」第一點。
- hermesnet
agent-hub:
build: .
container_name: agent-hub
restart: unless-stopped
network_mode: "service:tailscale"
depends_on:
- tailscale
stdin_open: true
tty: true
environment:
- HERDR_SOCKET_PATH=/root/.config/herdr/herdr.sock
- OPENAI_BASE_URL=http://192.168.2.131:8100/v1
- OPENAI_API_KEY=${GATEWAY_KEY:?Set GATEWAY_KEY in .env}
volumes:
# 不要用 /share 底下自己建的目錄,理由見下方第三點
- /share/Container/agent-hub/workspace:/workspace
- /share/Container/agent-hub/state/local-share:/root/.local/share
- /share/Container/agent-hub/state/config:/root/.config
- /share/Container/agent-hub/logs:/var/log/agents
networks:
agentnet:
driver: bridge
hermesnet:
external: true
name: hermes-agent_defaultserve 設定檔:
{
"TCP": { "443": { "HTTPS": true } },
"Web": {
"${TS_CERT_DOMAIN}:443": {
"Handlers": { "/": { "Proxy": "http://127.0.0.1:8787" } }
}
}
}${TS_CERT_DOMAIN} 會由容器替換成該節點的 MagicDNS 完整名稱。
建置與啟動(Container Station 的 docker 不在 PATH 上):
export DOCKER_CONFIG=/share/Container/agent-hub/.docker
DK=/share/ZFS1_DATA/.qpkg/container-station/bin/docker
cd /share/Container/agent-hub && $DK compose up -d --build步驟四:讓容器裡真的有 AI 代理人
這一步絕對不能跳過,Herdr 的側欄只看得見 Herdr 自己啟動的 pane。剛部署完的容器裡沒有人開過 TUI、沒有代理人在跑,所以 Collie 的 /api/snapshot 會回 agents: 0, shellPanes: 0,手機上是一個技術完全正常、但畫面空無一物的介面。實測確認過這個狀態。
要它有內容:
$DK exec -it agent-hub herdr在 TUI 裡開 pane、把代理人跑起來。訂閱制 CLI 的首次登入是 OAuth 裝置流程,在無頭容器裡要人到瀏覽器前面點,這是這條路線最麻煩的一段,請預留一點時間處理。
步驟五:設定與配對
$DK exec -it agent-hub sh -c '
CFG="$(herdr plugin config-dir herdr.collie)"
cat >> "$CFG/.env" <<EOF
[email protected]
COLLIE_PUBLIC_HOSTS=agent-hub.tail1234.ts.net
COLLIE_PORT=8787
EOF
chmod 600 "$CFG/.env"
herdr plugin action invoke restart --plugin herdr.collie
'COLLIE_TRUSTED_USER 填你的 tailnet 登入帳號,tailscale serve 會在每個請求注入 Tailscale-User-Login,且該標頭由 serve 自己設定,客戶端無法透過代理偽造。
COLLIE_PUBLIC_HOSTS 在這種拆兩個容器的設計裡是必填(實測)。容器內沒有 tailscale CLI,它在 sidecar 裡,Collie 的自動探索拿不到 tailnet 名稱,Host 閘門會對每個請求回 403。
接著在手機開 tailnet URL,然後一定要做配對:
$DK exec agent-hub herdr plugin action invoke push-keys --plugin herdr.collie # 推播用,選用
$DK exec agent-hub sh -c 'cd "$(herdr plugin config-dir herdr.collie)/.." && collie pair'collie pair 印出 8 碼、10 分鐘有效。在手機的 Settings 找到 Paired devices 輸入它。
有個實務上很容易誤判的地方,手機能打字進 pane,不代表配對成功。沒有配對任何裝置時寫入閘門是關的,所以打字本來就會成功。要確認配對真的成立,看 collie devices list 有沒有列出裝置。
配對權杖綁瀏覽器與來源網址,所以同一支手機的不同瀏覽器算兩台裝置,而且 iOS 上主畫面的 PWA 是獨立的儲存區,裝完 PWA 之後還要再配對一次。
步驟六:PWA 與推播
在手機用 Safari(iOS)或 Chrome(Android)開啟 tailnet URL,加入主畫面。iOS 上只有 Safari 做得到,Firefox 沒有這個選項。
推播預設關閉,三步開啟:
collie push-keys mailto:[email protected] # 產生 VAPID 金鑰寫入 .env,權限 600
collie restart
# 然後在手機的 PWA 裡:Settings 允許通知
collie push-test "標題" "內容"Collie 只在代理人進入 blocked 或 done 狀態時推播,那正好是人需要介入處理 AI 做完事情、卡關,或等待下一個指令的時機。
這個手機推播的訂閱綁在推播端點上,重裝 PWA 或換網址都會產生新端點,而舊的不會自動消失。所以要處理的畫,用 collie push list 看、collie push forget <端點尾段> 清掉。
QNAP 平台的五個小地方要處理
全部是實測碰到的,以下是實測和需要處理的地方。
一、network_mode: "service:tailscale" 會切斷容器對外的自訂網路。 共用網路命名空間之後,agent-hub 自己宣告 networks 是無效的。如果它需要連到 NAS 上另一個 stack 的容器(例如 Hermes),那個網路必須掛在 sidecar 上。
二、bridge 網路的容器連不到 NAS 自己的 LAN IP。 打 http://192.168.2.2:8642 會 timeout,不是拒絕,連 docker0 的 gateway IP 也一樣。解法是加入對方 stack 的網路,用容器名稱找它。
三、注意共享資料夾是不是真的共享資料夾。 QNAP 的 /share 本身是一個 tmpfs,真正的共享資料夾是底下的符號連結(例如 Container -> ZFS18_DATA/Container)。如果你在 /share 底下直接建目錄當掛載點,那個目錄其實躺在一個 16 MB 的 tmpfs 上,內容重開機就消失。Collie 的 checkout 加 bun install 會直接把它塞爆,症狀是 git 回一整頁 unable to write file,看起來像權限問題,其實是空間問題。
四、重啟 sidecar 會把依附容器的網路命名空間換掉。 compose restart tailscale 之後,agent-hub 裡的橋接程式就沒有在監聽了,serve 回 502。兩個容器要一起重啟。
五、Container Station 的 docker 是會覆寫 HOME 的 wrapper。 它強制 export HOME=$QPKG_DIR/homes/$(id -un),而那個目錄非 admin 帳號建不進去,於是 build 直接死在 mkdir permission denied,錯誤訊息完全沒提到跟 HOME 有關。解法是設定 DOCKER_CONFIG。
Collie 日常維運指令
| 動作 | Herdr action |
|---|---|
| 啟動 / 停止 / 重啟 | invoke start / stop / restart |
| 狀態 / 版本 | invoke status / version |
| 更新 | invoke update |
| 跨大版更新 | invoke update-major |
| 移除服務 | invoke uninstall |
| 推播金鑰 / 測試 | invoke push-keys / push-test |
實測 invoke update 從 Collie 1.0.1 升到 1.0.2 花 20 秒,再升到 1.1.0 花 5 秒,配對紀錄、推播訂閱與 .env 都存活。人類可讀的輸出要用 herdr plugin log list --plugin herdr.collie 讀,直接 invoke 只會回一個 JSON 信封。
容器內沒有 systemd,Collie 會退回 nohup 執行,狀態顯示 pid <n> (unsupervised),崩潰後不會自動重啟,要依賴容器層的 restart 策略。
優缺點與適用性
優點
這個方案最棒的是標榜零雲端中繼,所有的流量從手機到 NAS 全程在 tailnet 私有網路內。存取模型可稽核,包括 ACL 管誰能連、配對權杖管誰能寫、audit.log 記錄成功的寫入。回覆框是普通文字欄位,手機語音輸入也可以直接搭配使用喔。
NAS 全年開機 7×24 服務,AI 代理人不會因為工作站關機而中斷。
缺點與風險
它本質上是遠端 shell,配對之後仍然是遠端 shell,只是多了一道憑證。單人設計,沒有多租戶隔離,客戶資料所在的環境不建議納入。輪詢每 1500 毫秒一次,狀態變化到手機顯示有一到兩秒延遲。
而最大的實務障礙是把代理人真的跑在容器裡,訂閱制 CLI 的無頭登入不好處理,如果你的代理人其實跑在工作站上,這條路線鏡射到的會是空的,所以這適合把 AI 代理人跑在 NAS 上的用戶。
如果你的 AI 代理人跑在別台機器上,或你不想用 Tailscale 官方的控制伺服器,請看 CyberQ 的下一篇:以自架 Headscale 加上獨立的算力節點,把 Collie 放在代理人真正所在的機器上。
參考來源
- Collie GitHub repo(README 與 ARCHITECTURE.md)https://github.com/AltanS/collie
- Herdr 官方文件 https://herdr.dev/docs/
- Herdr GitHub repo https://github.com/herdrdev/herdr
- Herdr 支援的代理人清單 https://herdr.dev/docs/agents/
- Herdr 評測(架構與授權條款)https://www.fossengineer.com/herdr-terminal-agent-multiplexer/
- Herdr Mobile 官方 App https://play.google.com/store/apps/details?id=dev.herdr.mobile
- herdr-mobile-relay https://github.com/0cv/herdr-mobile-relay
- Moshi 的 Herdr 指南 https://getmoshi.app/guides/herdr
- Tailscale 的 QNAP 整合文件 https://tailscale.com/docs/integrations/qnap
- Tailscale 進駐 QNAP App Center 公告 https://tailscale.com/blog/qnap-app-center
- Tailscale QPKG 專案 https://github.com/tailscale/tailscale-qpkg












