自律エージェントを curl | bash でホストに直接入れるのは、正直こわい。 Canonical の新しい開発環境ツール Workshop(LXD ベース)でサンドボックス化し、 ホストとは必要なディレクトリだけ共有しながら HERMES Agent を動かしてみた記録です。

検証環境: Ubuntu 24.04.4 LTS / Workshop 0.9.1 / LXD 6.8


TL;DR

  • Workshop は LXD 上に再現可能な隔離開発環境を作る CLI。workshop init/launch/exec で完結する。
  • HERMES Agent(Nous Research, MIT)は永続記憶・自動スキル生成・ブラウザ制御を持つ自律 AI エージェント。インストールは curl | bash。まさに「ホストで直に動かしたくない」タイプのソフト。
  • この2つを組み合わせ、ホストのファイルシステムからは隔離しつつ、明示した1ディレクトリだけ双方向共有する構成を作った。
  • ハマりどころは4つ:①LXD 未導入 ②LXD のバージョン要件(6.8+、LTS の 5.21 では不可) ③mount インターフェースの作法 ④Docker 併用ホストでのブリッジ接続ハング。本記事はこの4つの解決を主軸に進む。

なぜサンドボックス化するのか

HERMES Agent は次のような性質を持つ。

  • インストールが curl -fsSL .../install.sh | bash(中身を全部読んでから入れる人は少ない)
  • LLM の指示で自律的にコマンドを実行する
  • ブラウザ制御でウェブにアクセスする
  • 73 個の組み込みスキルを持ち、ファイル操作・ネットワークアクセスを行う

便利だが、ホストの $HOME をそのまま触れる場所で動かすのは攻撃面が大きい。Workshop は公式に「実験的な AI ライブラリや自律エージェントを、攻撃面を絞って動かす」ユースケースを掲げている。今回はその通りの使い方をする。

ゴールはシンプルに3つ。

  1. Workshop の隔離環境(LXD コンテナ)の中で HERMES を動かす
  2. ホストの $HOME など共有していない場所にはアクセスさせない
  3. ただし、成果物のやり取り用に1ディレクトリだけ双方向共有する

登場ツール

ツール役割
WorkshopCanonical 製。YAML で環境を定義し、LXD 上に隔離開発環境を構築する CLI。workshopd デーモン + workshop CLI。SDK をプラグインのように足せる。
LXDWorkshop のバックエンド。システムコンテナ/VM を管理する。6.8 以上が必須(後述)。
HERMES AgentNous Research 製のオープンソース自律 AI エージェント(MIT)。CLI は hermes、メッセージング連携は hermes gateway

ステップ0: Workshop と LXD の準備

sudo snap install workshop --classic

ここまでは済ませていた。しかし最初の workshop コマンドがいきなり失敗する。

error: cannot communicate with workshopd: ... connection reset by peer

ハマり① LXD が無いと workshopd が起動しない

デーモンのログを見ると原因は一目瞭然だった。

journalctl -u snap.workshop.workshopd.service --no-pager -n 20
cannot run workshop: cannot connect to LXD:
  dial unix /var/lib/lxd/unix.socket: no such file or directory
Maybe LXD isn't installed?
To install LXD: 'sudo snap install lxd'

Workshop はバックエンドに LXD を必須とする。素直に入れる。

sudo snap install lxd
sudo lxd init --minimal      # デフォルトのストレージプール + ブリッジを非対話で作成
sudo snap restart workshop   # LXD ができたのでデーモンを再起動

ハマり② LXD のバージョン要件が厳しい(これが本記事最大の罠)

再起動して workshop list を叩くと、今度は別のエラー。

error: system is not healthy: incompatible backend:
LXD server version "5.21.4" is not supported; required >= 6.8.*

sudo snap install lxd のデフォルトチャンネルは LTS の 5.21/stable。安定志向で良さそうに見えるが、Workshop は LXD 6.8 以上を要求するため、これでは動かない。LXD 6 系は 6/stable チャンネルにある。

sudo snap refresh lxd --channel=6/stable
sudo snap restart workshop
lxd --version    # 6.8

教訓: Workshop を入れるなら、最初から sudo snap install lxd --channel=6/stable でよい。 公式ページの「LXD 6.8+」という要件は飾りではなく厳格にチェックされる。

これで workshop list のエラーが not a project(プロジェクト未作成)に変わった。バックエンド要件はクリアした合図だ。

補足: systemctl is-active snap.workshop.workshopd.serviceinactive でも問題ない。 workshopd はソケット起動型で、CLI を叩いた瞬間に自動起動する。


ステップ1: プロジェクトと Workshop の作成

Workshop は「プロジェクトディレクトリ + .workshop/ 配下の定義 YAML」で動く。

mkdir -p ~/hermes-sandbox/shared   # shared は後でホスト共有に使う
cd ~/hermes-sandbox

workshop init hermes --sdks uv
# → .workshop/hermes.yaml が生成される

生成された定義はシンプルだ。

# .workshop/hermes.yaml
name: hermes
base: [email protected]
sdks:
  - name: uv

uv SDK を入れたのは、HERMES が内部で Python(uv)を使う想定だったため。実際にはインストーラが必要なものを自前で揃えるので必須ではないが、付けておいて損はない。

そして起動。

workshop launch hermes   # ベースイメージを取得 → "hermes" launched
workshop list            # STATUS: Ready

中に入れることを確認する。

workshop exec hermes -- bash -c 'whoami; echo HOME=$HOME; grep PRETTY /etc/os-release'
# workshop
# HOME=/home/workshop
# PRETTY_NAME="Ubuntu 24.04.4 LTS"

コンテナ内ではユーザー workshop、ホームは /home/workshop。これがサンドボックスの世界だ。


ステップ2: ホスト ⇔ サンドボックスのファイル共有(mount インターフェース)

ここが Workshop ならではの作法。Workshop の共有モデルは snapd 風のプラグ/スロットになっている。

  • 各 SDK が mount プラグを宣言する。プラグは host-source(ホスト側のパス)と workshop-target(コンテナ内のパス)の対応を持つ。
  • プラグは mount インターフェースを介して system:mount スロットに接続される。

実際、最初から入れた uv SDK もキャッシュ用に mount を1本持っていた。

workshop info hermes
sdks:
  uv:
    mounts:
      cache:
        host-source:      ~/.local/share/workshop/id/.../hermes/mount/uv/cache
        workshop-target:  /home/workshop/.cache/uv
workshop connections
# INTERFACE  PLUG             SLOT                 NOTES
# mount      hermes/uv:cache  hermes/system:mount  -

つまり自分用の共有ディレクトリも、mount プラグを1本生やせば実現できる

カスタム SDK を sketch-sdk で定義する

任意のホストディレクトリを共有するには、mount プラグを宣言する独自 SDK を作る。Workshop には試作用の sketch SDK(sketch-sdk)という仕組みがあり、エディタでテンプレートを編集して適用できる。

テンプレートの記法はこうなっている(抜粋)。

name: sketch
plugs:
  # ホストディレクトリをマウントする
  models:
    interface: mount
    workshop-target: /home/workshop/models

これに倣って、共有用プラグ shared を定義する。

name: sketch
plugs:
  shared:
    interface: mount
    workshop-target: /home/workshop/shared
workshop sketch-sdk hermes
# → エディタが開くので上記を書いて保存

記事の再現性のため、$EDITOR を「目的の YAML を流し込んで終了する小さなスクリプト」にしておくと、 対話なしで適用できる。CI やドキュメント化に向く。

適用後、workshop infosketch SDK と mount shared(target=/home/workshop/shared)が現れる。

永続化(eject)してプロジェクトに固定する

sketch のままだと試作扱いなので、--eject でプロジェクト SDK に昇格させる。

workshop sketch-sdk hermes --eject --name shared
# → .workshop/shared/sdk.yaml として保存される
# .workshop/shared/sdk.yaml
name: shared
plugs:
  shared:
    interface: mount
    workshop-target: /home/workshop/shared

eject した SDK は、定義 YAML の sdks:参照名 project-shared として追記して refresh する。

# .workshop/hermes.yaml
sdks:
  - name: uv
  - name: project-shared    # ← 追記
workshop refresh hermes

小さな罠: 参照名は project-shared だが、インストール後の SDK 名・プラグ名は shared。 以降のコマンドではプラグを hermes/shared:shared と指定する。

ホスト側ソースを実ディレクトリに向ける(remount)

refresh 直後の host-source は Workshop 管理下のデフォルトディレクトリを指している。これを自分の ~/hermes-sandbox/shared に差し替える。

workshop remount hermes/shared:shared ~/hermes-sandbox/shared

ここでもう一つの注意点。差し替え先が空でないディレクトリの場合、データ破損を防ぐためにワークショップを停止してから remount する必要がある。

workshop stop hermes
workshop remount hermes/shared:shared ~/hermes-sandbox/shared
workshop start hermes

remount で設定したソースは永続で、refresh 後も「最後に remount したソース」が使われる。最終的にこうなる。

mounts:
  shared:
    host-source:      ~/hermes-sandbox/shared
    workshop-target:  /home/workshop/shared

双方向共有を検証する

# ホストで作成
echo "hello-from-host" > ~/hermes-sandbox/shared/from_host.txt

# サンドボックス内から読める
workshop exec hermes -- cat /home/workshop/shared/from_host.txt
# hello-from-host

# サンドボックス内で作成
workshop exec hermes -- bash -c 'echo hello-from-workshop > /home/workshop/shared/from_workshop.txt'

# ホストから読める
cat ~/hermes-sandbox/shared/from_workshop.txt
# hello-from-workshop

UID の対応も LXD の idmap が面倒を見てくれる。コンテナ内では workshop:workshop、ホストでは toming:toming の所有として、それぞれ自然に読み書きできる。


ステップ3: HERMES Agent をサンドボックスに入れる

ここでようやく、本来こわい curl | bash安心して実行できる。隔離されているからだ。

workshop exec hermes -- bash -lc \
  'curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash'

ハマり③ 特定ホストへの接続だけがハングする(Docker × LXD ブリッジ)

ところが、最初の実行で git clone132 秒でタイムアウトして失敗した。

fatal: unable to access 'https://github.com/NousResearch/hermes-agent.git/':
  Failed to connect to github.com port 443 ... Couldn't connect to server
✗ Failed to clone repository

奇妙なのは「全滅ではない」こと。コンテナ内から各サイトへ curl して切り分けると、ホスト選択的な失敗だった。

接続先結果
raw.githubusercontent.com / pypi.org / nodejs.org✅ 通る
github.com / duckduckgo.com❌ タイムアウト(rc=28)

DNS は正常(github.com は解決できている)。そしてホストでは Docker が複数コンテナを稼働中だった。これは Workshop が workshop warnings で最初から警告していた内容そのものだ。

firewall rules may be blocking network traffic on the workshopbr0 bridge:
the FORWARD chain policy is set to DROP ... likely caused by Docker.

何が起きていたか: Docker は iptables/nftablesFORWARD チェーンを DROP ポリシーにし、DOCKER-USER チェーンで自分のトラフィックだけを許可する。Workshop のブリッジ workshopbr0 経由の転送はこの網に引っかかる。さらに PMTUD(経路 MTU 探索)用の ICMP も落とされるため、大きな TLS 証明書チェーンを返すホスト(github.com=Azure 系など)への接続だけがハングし、CDN(Fastly)系の小さい応答は通る、という選択的な症状になっていた。

解決は、Workshop の警告が提示する nft ルールをそのまま入れるだけ。

sudo nft insert rule ip filter DOCKER-USER iifname workshopbr0 accept
sudo nft insert rule ip filter DOCKER-USER oifname workshopbr0 ct state related,established accept
ルール意味
iifname workshopbr0 acceptWorkshop ブリッジ(コンテナ→外部)の転送を許可
oifname workshopbr0 ct state related,established acceptその戻りトラフィックを許可

適用後、コンテナ内から github.com / duckduckgo.com とも HTTP 200。原因が確定した。

注意: この nft ルールは再起動や Docker のネットワーク再構築で消える。 恒久化するなら、起動時にこのルールを再投入する仕組み(systemd unit など)を別途用意する。

インストール完了

ファイアウォール修正後に再実行すると、今度は素直に通った。

Done: 73 new, 0 updated, 0 unchanged. 73 total bundled.
✓ Skills synced to ~/.hermes/skills/
┌─────────────────────────────────────────────────────────┐
│              ✓ Installation Complete!                    │
└─────────────────────────────────────────────────────────┘

インストーラの挙動:

  • Node.js 22 LTS を ~/.hermes/node/ に自動導入(ブラウザツール用)
  • リポジトリを clone し、Python の仮想環境を構築
  • 73 個のスキルを同梱、~/.hermes/(約 1.1G)に config / .env / sessions / skills を配置
  • 端末が無いため setup ウィザードはスキップ → LLM プロバイダ設定は後から hermes setup

余談: base イメージで universe リポジトリが無効なため、aptripgrep/ffmpeg が入らない。 どちらも任意機能(ファイル検索は grep にフォールバック)なので、今回はそのままにした。


ステップ4: 動作確認と隔離境界の検証

まずは HERMES 自体が動くこと。

workshop exec hermes -- bash -lc 'hermes --version'
# Hermes Agent v0.16.0 (2026.6.5) · Python 3.11.15

workshop exec hermes -- bash -lc 'hermes doctor'
# ✓ Python 3.11.15 / ✓ Virtual environment active
# ✓ OpenAI SDK / Rich / python-dotenv / PyYAML / HTTPX
# ✓ ~/.hermes/.env file exists

隔離境界の検証(本企画の核心)

サンドボックスの意味があるかを、実際に確かめる。共有ディレクトリのにダミーの機密ファイルを置き、中から見えないことを確認する。

# 共有ディレクトリの外(ホストの $HOME 直下)に置く
echo "TOP-SECRET" > ~/host_only_secret.txt

# サンドボックス内からホストの $HOME を覗こうとする
workshop exec hermes -- bash -lc 'ls /home/toming'
# ls: cannot access '/home/toming': No such file or directory   ← 不可視

# 共有ディレクトリだけは見える
workshop exec hermes -- bash -lc 'ls /home/workshop/shared/'
# from_host.txt  from_workshop.txt

期待通り、HERMES からホストのファイルシステムは見えず、明示的に共有した ~/hermes-sandbox/shared だけにアクセスできる。自律エージェント + ブラウザ制御 + curl-bash インストールという、本来なら身構える組み合わせを、ホストへの攻撃面を最小限に絞って動かせている。


最終構成

項目
ホスト OSUbuntu 24.04.4 LTS
バックエンドLXD 6.8(6/stable。5.21 LTS では不可)
Workshop0.9.1
プロジェクト~/hermes-sandbox(定義は .workshop/)
サンドボックス内Ubuntu 24.04.4 / user=workshop / HOME=/home/workshop
共有マウントhost ~/hermes-sandbox/shared ↔ workshop /home/workshop/shared(双方向・永続)
HERMESv0.16.0 / Python 3.11.15 / 73 skills / ~/.hermes/
残作業hermes setup で LLM プロバイダ(API キー)を設定

再現用クイックスタート

ここまでの試行錯誤を踏まえ、最短手順を一枚にまとめる。

# 1. 前提(最初から LXD 6 系で入れるのが正解)
sudo snap install workshop --classic
sudo snap install lxd --channel=6/stable     # 6.8+ 必須。デフォルトの 5.21 LTS では動かない
sudo lxd init --minimal
sudo snap restart workshop

# 2. Docker 併用ホストなら、ブリッジ転送を許可(github 等への接続ハング対策)
sudo nft insert rule ip filter DOCKER-USER iifname workshopbr0 accept
sudo nft insert rule ip filter DOCKER-USER oifname workshopbr0 ct state related,established accept

# 3. プロジェクト作成・起動
mkdir -p ~/hermes-sandbox/shared && cd ~/hermes-sandbox
workshop init hermes --sdks uv
workshop launch hermes

# 4. 共有マウント用 SDK を定義(.workshop/shared/sdk.yaml)
#    name: shared / plugs.shared.interface: mount / workshop-target: /home/workshop/shared
#    hermes.yaml の sdks に - name: project-shared を追記
workshop refresh hermes
workshop stop hermes
workshop remount hermes/shared:shared ~/hermes-sandbox/shared
workshop start hermes

# 5. HERMES をサンドボックス内にインストール
workshop exec hermes -- bash -lc \
  'curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash'
workshop exec hermes -- bash -lc 'hermes doctor'

# 6. LLM 設定(対話)。サンドボックスに入って setup を実行
workshop shell hermes
#   (中で) hermes setup

おわりに

Workshop は「devcontainer の LXD ネイティブ版」と言える位置づけで、特に自律 AI エージェントのサンドボックスとして相性が良い。今回得られた実践的な勘所は次の通り。

  • LXD は最初から 6/stable。LTS(5.21)では Workshop が動かない。
  • ファイル共有は mount プラグsketch-sdkejectremount の流れを押さえれば、任意ディレクトリを双方向共有できる。非空ディレクトリへの remount は停止が必要。
  • Docker 併用ホストではブリッジ転送に注意workshop warnings の指示通り DOCKER-USER に nft ルールを足す。症状が「特定ホストだけハング」なので原因に気づきにくい。
  • 結果として、ホストの $HOME を一切見せずに、自律エージェントを安全に走らせ、必要な成果物だけを1ディレクトリで受け渡せる構成が完成した。

次の一歩は hermes setup で LLM プロバイダ(Nous Portal / OpenRouter など)を設定すること。隔離は済んでいるので、ここから先は安心してエージェントに遊んでもらえる。


検証日: 2026-06-11 / Ubuntu 24.04.4 LTS