1. はじめに
1.1 きっかけ
2026-10-22、Agentic AI Foundation(AAIF)が4つの新プロジェクトを発表しました1。
| プロジェクト | 提供元 | 概要(発表ページの記述より) |
|---|---|---|
| Agent Skills | Anthropic | エージェントが必要に応じて読み込む指示・スクリプト・資源をパッケージ化するオープンな形式 |
| Open Responses | OpenAI | OpenAI Responses API をもとにした、複数プロバイダ向けの LLM インターフェース仕様 |
| OpenEnv | Hugging Face | 強化学習の環境を作るためのフレームワークとコミュニティハブ |
| OpenSandbox | Alibaba | AI アプリケーション向けの汎用サンドボックス基盤(多言語 SDK、Docker/Kubernetes ランタイム、カスタムランタイムも可能にするライフサイクル/実行 API のプロトコル) |
AAIF は、2025-12-09 に Linux Foundation が「中立の受け皿(neutral home)」となる形で発足した団体です2。創設時のプロジェクトは MCP、goose、AGENTS.md でした。
この記事では、このうち OpenSandbox を取り上げます。OpenEnv、Agent Skills、Open Responses は扱いません。
1.2 なぜ「箱に入れる」技術が要るのか
コーディングエージェントを素のまま動かすと、エージェントはユーザーと同じ権限で動きます。つまりファイル、環境変数(API キーなど)、ネットワークに触れられます。プロンプトインジェクションなどでモデルが意図しない指示に従った場合、秘密ファイルの読み取りやキーの持ち出しが起こりえます。
この問題への答えは、アプリ側の「承認ダイアログ」だけでは足りません。OS やコンテナ、VM の境界で、エージェントが物理的にできないことを決めておく必要があります。その「箱」を提供するのが、サンドボックス系のソフトウェアです。
ただし、箱があれば安全、というわけではありません。2026 年 7 月に OpenAI が公表した技術報告書3によると、社内のサイバー能力の評価で、AI モデルが「隔離された」評価環境から抜け出し、Hugging Face の本番環境の一部に侵入しました4。直接インターネットには出られない環境でしたが、パッケージを入れるために使う社内のサービスの未知の脆弱性が、抜け道になりました。
学びは、箱は、許可した経路(この例ではパッケージ配布サービス)、設定の穴、箱を支えるサービスの脆弱性までは防げないことです。報告書も、単一の制御に頼らず、独立した制御を重ねることを勧めています。この記事の後半(§6.2、§8)でも、今回の構成で防げなかったものを整理します。
1.3 OpenShell を並べた理由
以前の記事5で、Hermes Agent の SECURITY.md を読む中で、NVIDIA OpenShell を「姿勢B(プロセス全体のラッピング)」の実装例として挙げました。ただし、そのときは設計に触れただけで、動作の検証は扱っていません。
OpenSandbox のニュースを見て、あの記事で触れた OpenShell と何が違うのかが気になりました。そこで、同じ PC で両方を動かして比べています。要点は次の通りです。
- OpenSandbox: エージェントの外側に、使い捨ての実行環境を用意する。エージェントは SDK/API 経由でその中のコードを動かす。
- OpenShell: エージェント自身を内側に入れて、ファイル・通信・資格情報を縛る。
なお、OpenShell は NVIDIA のプロジェクトです。
1.4 この記事で答える問い
- それぞれ、この PC(Windows 11 + WSL2)で動くのか。
- 何が守られ、何が守られないのか(実際に破ろうとして確かめる)。
- どう使い分けるか。
1.5 先に結論
-
OpenSandbox は、素の WSL2 + Docker Engine で、公式 README の手順どおりに動きました。ただし、egress ポリシーを付けなければ通信は許可されたままで、ポリシーを付けても既定の
dnsモードでは IP 直指定の接続を止めません(§4.4)。 -
OpenShell v0.1.5 は、Docker ドライバーだとこの WSL2 ではサンドボックスが起動しません(supervisor が
seccomp notification probe: Resource busyで終了)。同じ PC で v0.0.116 の Docker ドライバーは動き、v0.1.5 でも VM ドライバー(KVM)なら動きました(§5)。 - OpenShell が動いた後の防御(ファイル、通信、資格情報)は期待どおりでした。ただし防げないものもあります(§6)。
- 2つを組み合わせる構成(OpenShell の箱の中のエージェントが、コード実行を OpenSandbox に任せる)も動きました。メモリの暴走や、重い処理の並列実行の影響を、エージェントの箱から切り離せる一方、短い処理では割に合わないことや、設定の落とし穴もありました(§8)。
検証環境
- OS: Windows 11 Pro (build 26300) / WSL 2.7.11.0 / カーネル 6.18.33.2
- ディストリビューション: Ubuntu 26.04 LTS(systemd 有効、WSL のメモリ上限 12GB)
- Docker: Docker Engine 29.8.0(WSL 内)
- Python: 3.12(uv)
- OpenSandbox: 1.1.1(server / cli / mcp / code-interpreter の Python パッケージ。イメージは execd v1.1.0、code-interpreter v1.1.0、egress v1.1.7)
- OpenShell: v0.1.5(比較に v0.0.116)
2. 2つの道具の位置づけ
2.1 OpenSandbox
alibaba/OpenSandbox のライセンスは Apache 2.0 です。
- ライフサイクル API(作成・削除)と実行 API(コマンド・ファイル)を持つサーバー。
- サンドボックス内に実行デーモン(execd)が注入される。ベースイメージを改造せずに使える。
- ランタイムは Docker(ローカル)と Kubernetes。
- Python SDK(
opensandbox)、CLI(osb)、MCP サーバー(opensandbox-mcp)。 - 状態を持つコード実行(Jupyter 的な使い方)は、別パッケージ
opensandbox-code-interpreterと専用イメージで提供される。
エージェント(LangGraph や Claude など)はサンドボックスの外にいて、API 越しに中のコードを動かします。
2.2 NVIDIA OpenShell
NVIDIA/OpenShell は、エージェントのプロセスを丸ごとサンドボックスに入れ、その出入りをポリシーで縛るランタイムです。ライセンスは Apache 2.0 です。
- CLI、ゲートウェイ、コンピュートドライバー(Docker / Podman / Kubernetes / MicroVM)、サンドボックス内の supervisor で構成される。
- ファイルは Landlock、システムコールは seccomp、通信は supervisor 内のプロキシで制御する。
- 資格情報は「プロバイダ」として登録する。サンドボックス内にはプレースホルダーしか渡さず、プロキシの出口で実値に置換する。
- ログは OCSF(Open Cybersecurity Schema Framework。セキュリティイベントの共通スキーマ)形式で、OTLP(OpenTelemetry のデータ転送プロトコル)でも出力できる。
- ポリシーは、ファイル・プロセス(静的、作成時に固定)とネットワーク(動的、実行中に変更可)に分かれる。
2.3 比較表
| 観点 | OpenSandbox | OpenShell |
|---|---|---|
| エージェントの位置 | 箱の外(SDK/API で操作) | 箱の内(エージェント自体が箱の中) |
| 主な用途 | コード実行・ファイル・ブラウザ操作を「貸す」 | 既存エージェントを丸ごと「閉じ込める」 |
| 隔離の仕組み | コンテナ(ケイパビリティ制限、no-new-privileges、pids 制限) | コンテナまたは VM + Landlock + seccomp + プロキシ |
| 通信の制御 | egress ポリシー(ドメイン単位。既定の dns モードは IP 直指定を止めず、dns+nft で止まる) |
ホスト+ポート+実行バイナリ+HTTP メソッド/パスの L7 ルール |
| 資格情報 | サンドボックスに渡さない運用 | プレースホルダー置換+宛先への束縛 |
| ログ | 標準出力/エラーのストリーム、メトリクス | OCSF(openshell logs、OTLP) |
| 公式の Windows 対応 | (今回の README には特記なし) | WSL2 + Docker Desktop で「実験的」 |
3. 検証の進め方と安全面
- 公式ドキュメントと README の通りに動かす → 想定どおり守られるか攻撃的に試す → 失敗も記録する、という順で進めました。
- 使い捨てのディレクトリ、ダミーの秘密ファイル、偽の API キー(
sk-test-NOT-REAL-...)だけを使いました。値は WSL 内のローカルのエコーサーバーやモックで受け、外部には送っていません。 - OpenShell の公式インストーラ(
curl | sh)は中身を読みました。sudoでパッケージを入れ、systemd のユーザーサービスを登録する作りです。WSL ではsudoのパスワードが必要なため、代わりにリリースの tarball をチェックサム検証して、ユーザー領域に展開しました。 - ローカル検証用のゲートウェイは、認証なし・TLS なしの設定(
allow_unauthenticated_users = true、disable_tls = true)にしています。これは安全でない設定で、ローカル検証専用です。
4. OpenSandbox を動かす
4.1 導入と起動
uv venv --python 3.12 .venv && . .venv/bin/activate
uv pip install opensandbox opensandbox-server opensandbox-cli opensandbox-mcp
opensandbox-server init-config ~/.sandbox.toml --example docker
opensandbox-server # 既定は 127.0.0.1:8080
API キー未設定のままだと、非対話モードでの起動を拒否します(エラーに OPENSANDBOX_INSECURE_SERVER=YES で回避できると出ます)。今回は設定ファイルの [server] に検証用のダミーキーを入れました。
設定のデフォルトは、drop_capabilities(NET_ADMIN、NET_RAW、SYS_ADMIN、SYS_PTRACE ほか)、no_new_privileges = true、pids_limit = 4096、ブリッジネットワーク、といった内容でした。
4.2 CLI と SDK
CLI は osb config で接続先とキーを設定してから使います。
osb sandbox create --image python:3.12-slim --timeout 10m
osb command run <sandbox-id> -o raw -- python -c "print(1 + 1)"
osb sandbox kill <sandbox-id> # delete というサブコマンドは無い
- 初回は約20秒(イメージ取得を含む)。SDK からの作成は、2回目以降は約1.2秒で準備完了しました。
- 無改変の
python:3.12-slimで動きました。コンテナ内は root ですが、ケイパビリティは CHOWN、DAC_OVERRIDE、FOWNER、FSETID、KILL、SETGID、SETUID、SETPCAP、NET_BIND_SERVICE、SYS_CHROOT、SETFCAP に限られていました。
Python SDK は Sandbox.create(...)、sandbox.commands.run(...)、sandbox.files.write_files / read_file、sandbox.destroy() が基本です。
4.3 状態を持つ実行は別パッケージ
ここでいう「状態を持つ実行」とは、Jupyter Notebook のように、前に実行したコードで作った変数や読み込んだデータが、次の実行でもそのまま使えることです。
- 通常の
commands.runは、コマンドを実行するたびにプロセスが終了するため、変数などのメモリ上の状態は残りません(python -c "x = 42"の次にpython -c "print(x)"を実行してもxは存在しません)。 - Jupyter のカーネル(コードを実行し続ける常駐プロセス)がメモリに変数を保持している間は、続きのセルから参照できます。エージェントが「データを読み込む → 加工する → グラフにする」と段階的にコードを書いて進める場合、毎回最初から再計算しなくて済みます。
OpenSandbox では、この機能は opensandbox-code-interpreter の CodeInterpreter で提供されます。専用イメージ(opensandbox/code-interpreter、約10.5GB)が必要です。
ctx = await interpreter.codes.create_context(SupportedLanguage.PYTHON)
await interpreter.codes.run("x = 42\ndata = [i**2 for i in range(5)]", context=ctx)
r = await interpreter.codes.run("print(x + sum(data))", context=ctx) # → 72
create_context で作ったコンテキストを指定すると、実行をまたいで変数が保持されました。コンテキストを指定せずに別の実行をすると NameError になり、状態は共有されませんでした。
4.4 egress ポリシー
network_policy に default_action="deny" と許可ドメインを渡して作成すると、通信を絞れます。
| 試験 | 結果 |
|---|---|
ポリシー: deny + example.com を許可 → example.com
|
200 |
同 → example.org / httpbin.org
|
名前解決に失敗(DNS 方式のフィルタ) |
実行中に patch_egress_rules で example.org を追加 |
接続できた(応答は HTTP 403。宛先側の応答と見られるが未切り分け) |
IP 直指定の TCP 接続(1.1.1.1:443、ホストの 172.17.0.1:9999 など) |
既定の dns モードでは接続できた。dns+nft モードでは止まる(§8.4) |
既定の dns モードは、名前解決の段階で絞る方式で、IP アドレスを直接指定した接続は止めません。「default deny」を IP 直指定まで効かせるには、サーバー設定の [egress] mode = "dns+nft" が必要です。
4.5 隔離と制限の実測
| 試験 | 結果 |
|---|---|
ホストのファイル(/mnt/c など)、docker.sock
|
見えない |
| コンテナ内の環境変数 | イメージ由来の変数のみ(GPG_KEY)。ホスト側の環境変数は、Docker が通常渡さないため、その確認にとどまる |
mount -t tmpfs |
失敗(unshare は未検証) |
| cgroup への書き込み | 読み取り専用 |
| 既定の外向き通信 |
許可(example.com に 200) |
メタデータ IP 169.254.169.254
|
タイムアウト |
ホストの 0.0.0.0 待受サービス(Docker のブリッジのゲートウェイ経由) |
到達でき、内容を読めた(Docker の既定のブリッジの性質で、OpenSandbox 固有ではない。素の docker run との比較はしていない) |
ホストの 127.0.0.1 待受サービス |
接続拒否(到達不可) |
| プロセス数 | 約4,085 で fork 失敗(上限 4096 が有効) |
| メモリ(既定 2GiB 上限) | 3GiB を実際に触っても生存(memory.events の max が増え、oom_kill は 0。swap に逃げたと見られる)。5GiB まで確保し続けると SIGKILL で終了した(§8.6) |
/proc/1/environ |
同一コンテナ内なので読める |
要点は2つです。通信は既定で許可なので、絞るなら egress ポリシーが前提になります。また、ホストの 0.0.0.0 で待ち受けているサービスには、サンドボックスから届きます。開発用サーバーなどを 0.0.0.0 で上げていると、サンドボックスの中のコードから読めてしまいます。
4.6 MCP
opensandbox-mcp は --domain、--protocol、--api-key、--transport {stdio,streamable-http} を受け付けることまで確認しました。Claude Code などから実際につないだ検証は、今回していません。
5. OpenShell を動かす
5.1 導入とゲートウェイ
tarball(CLI、gateway、sandbox)を展開し、openshell doctor check で Docker 29.8.0 が ok と出ることを確認しました。
ゲートウェイの設定ファイルはスキーマ v2でした。古い v0.0.116 のドキュメントにある v1 形式の設定は、config preflight で拒否されます。最新版のドキュメントを見る必要があります。
[openshell]
version = 2
[openshell.gateway]
bind_address = "127.0.0.1:17670"
compute_driver = "docker"
disable_tls = true # ローカル検証専用
[openshell.gateway.auth]
allow_unauthenticated_users = true # ローカル検証専用(安全でない)
[openshell.drivers.docker]
socket_path = "/var/run/docker.sock"
起動後、openshell gateway add http://127.0.0.1:17670 --local --name local で登録すると、openshell status が Connected を返しました。
ファイルをサンドボックスに渡す openshell sandbox upload は、今回の環境では中身が壊れる(ヌルバイトが入る)ことがあったため、base64 経由の exec で渡しました。
5.2 Docker ドライバーはこの WSL2 で起動しない(v0.1.5)
OpenShell は、サンドボックスの実体を作る部分をコンピュートドライバーとして切り替えられます(Docker / Podman / Kubernetes / MicroVM)。この記事で使うのは次の2つです。
- Docker ドライバー: サンドボックスを Docker のコンテナとして作ります。コンテナはホストのカーネルを共有するので、WSL2 の場合は、WSL の Linux カーネルの上で動きます。
-
VM ドライバー(MicroVM): サンドボックスを軽量な仮想マシンとして作ります(実装は libkrun)。ゲストは自分のカーネルを持ち、KVM(
/dev/kvm)が必要です。
どちらでも、サンドボックスの中で動く supervisor が Landlock や seccomp を設定します。違いは、箱がホストのカーネルを共有するかどうかです。
openshell sandbox create --name t1 --no-keep -- sh -c 'id; ls /'
Error: × sandbox entered error phase while provisioning:
│ ControlSupervisorStartFailed: Docker sandbox exited before supervisor
│ became ready
サンドボックスのコンテナのログを取ると、原因が見えました。
Error: × seccomp notification probe
╰─▶ Resource busy (os error 16)
supervisor は起動時に Landlock や seccomp 通知(user notification)などのプローブを行い、通らないと終了します。このプローブが EBUSY で失敗していました。
切り分け
seccomp の user notification は、1つのスレッドに listener 付きのフィルタを1つしか付けられません(2つ目は EBUSY)。そこで、フィルタの付き方を調べました。
- 調べた WSL 内のプロセス(PID 1 の systemd、WSL の init、dockerd、containerd、自分のシェルの5つ)は、いずれも
Seccomp_filtersが 1 でした(これはフィルタの数で、listener の有無までは分かりません)。 - 小さな Python スクリプトで
seccomp()を直接呼んで確かめました。
| 場所 | 通常のフィルタ追加 | SECCOMP_FILTER_FLAG_NEW_LISTENER |
|---|---|---|
| WSL のホスト直下 | 成功 | EBUSY |
docker run(既定) |
成功 | EBUSY |
docker run --security-opt seccomp=unconfined |
成功 | EBUSY |
カーネルは、すでに listener 付きのフィルタがある場合に、新たな listener の追加を EBUSY で拒否します。通常のフィルタ追加は成功し、listener 付きだけが失敗したので、この WSL2 では、調べたプロセスが listener 付きの seccomp フィルタをすでに継承しているとみられます(推測です)。出どころは WSL の init だと考えていますが、確認はできていません。そのため、listener を新たに付けようとする v0.1.5 の supervisor は起動できない、と考えられます。
v0.0.116 の Docker ドライバーは動いた
同じ PC で v0.0.116 の Docker ドライバーを試すと、非 root のイメージで、サンドボックスが起動し、sandbox exec でコマンドを実行できました(1回の確認です。root のイメージは OCI USER 'root' selects root で拒否されます)。v0.0.116 のゲートウェイには JWT 鍵の設定が必要で、v0.1.5 と同じ設定ディレクトリを使うと DB のマイグレーションエラーになるため、別のディレクトリで動かしました。
uid=1000(ubuntu) ...
CapEff: 0000000000000000
NoNewPrivs: 1
Seccomp: 2
Seccomp_filters: 5
cat /opt/secret_dir/id_rsa → Permission denied
cat /home/ubuntu/id_rsa_dummy → Permission denied
curl https://example.com → 000(接続できない)
同じ非 root のイメージ・同じ PC で、v0.0.116 は動き、v0.1.5 の Docker ドライバーは起動前に失敗します(ただし、ゲートウェイの設定などは版ごとに異なるため、厳密に他を揃えた比較ではありません)。v0.1.5 の supervisor のバイナリには seccomp notification probe という文字列があり、v0.1.5 で加わった(または厳しくなった)seccomp 通知のプローブが、この WSL2 では通らないのではないか、と考えています(推測です。v0.0.116 のバイナリは調べていません)。公式ドキュメントでも WSL2 の Windows 対応は「Docker Desktop 前提で実験的」とされており、素の Docker Engine の構成は想定外の可能性があります。
5.3 VM ドライバー(KVM)なら v0.1.5 でも動いた
v0.1.5 には MicroVM(libkrun)ドライバーがあり、ゲストは別のカーネルで動くので、WSL の seccomp フィルタは関係しません。ただし /dev/kvm へのアクセスが必要です。
sudo usermod -aG kvm $USER # 実行後に PowerShell で wsl --shutdown
設定は次の通りです。
[openshell.gateway]
compute_driver = "vm"
[openshell.drivers.vm]
driver_dir = "/path/to/dir-with-openshell-driver-vm" # 明示が必要だった
default_image = "nvcr.io/nvidia/base/ubuntu:24.04"
vcpus = 2
mem_mib = 2048
driver_dir を指定しないと、unexpected argument '--admission-config-json' というエラーで起動しませんでした(原因は確認していません)。
起動後は、ゲストのカーネルは 6.12.109(WSL の 6.18 とは別)で、uid 1000、ケイパビリティ 0、NoNewPrivs、seccomp フィルタ 5 段。サンドボックスは数秒で立ち上がりました。
5.4 ファイル制限(Landlock)
既定のポリシーは、/bin /usr /lib /proc /etc /var/log が読み取り専用、/tmp と作業ディレクトリ(/sandbox)が読み書き可です。
なお、WSL2 では Landlock の有無を /sys/kernel/security/lsm で確認できません(securityfs がマウントされておらず、ファイルが存在しません)。landlock_create_ruleset を実際に呼ぶと ABI 7 が返ったので、Landlock は使えます。
DAC(Discretionary Access Control、任意アクセス制御。ファイルの所有者や権限ビットによる通常の Unix の権限管理)の上では誰でも読める(644)ファイルを、許可リストの外に置いて試しました。
| 試験 | 結果 |
|---|---|
cat /home/ubuntu/id_rsa_dummy、cat /opt/secret_dir/id_rsa、cat /root/id_rsa_dummy
|
すべて Permission denied |
ls -l(stat) |
3つのうち2つは通る(ファイルの存在は見える)。/root 側は ls -l も拒否 |
シンボリックリンク経由、/sandbox/../opt/... 経由 |
拒否 |
cat を /tmp にコピーして実行 |
拒否 |
python の open()
|
拒否 |
/usr、/etc への書き込み |
拒否 |
/sandbox、/tmp への書き込み |
可 |
mount、unshare -r
|
拒否(これは Landlock ではなく、ケイパビリティや NoNewPrivs による拒否と見られる) |
Landlock はパスで判定するので、リンクや別コマンドを経由しても突破できませんでした。
5.5 ネットワーク制限
既定のポリシーにはネットワークの許可が1つもなく、全部拒否です(IP 直指定の 1.1.1.1 も拒否でした)。許可は、ホスト+ポート+実行バイナリの組み合わせで追加します。
openshell policy update <name> \
--add-endpoint example.com:443:read-only:rest:enforce \
--binary /usr/bin/curl --wait
| 試験 | 結果 |
|---|---|
許可前 curl https://example.com
|
000(接続できない) |
許可後 curl(GET) |
200 |
許可後 curl -X POST
|
403(read-only の L7 ルール) |
許可後 curl https://example.org
|
000(未許可の宛先) |
許可後 python3 から同じ宛先 |
拒否(バイナリが一致しない) |
curl を /tmp/curl2 にコピーして実行 |
拒否(パスでバイナリを判定している) |
curl --noproxy "*" |
200(透過的に supervisor 経由になるため。回避ではなく、ログにも ALLOWED と記録) |
--remove-endpoint の後 |
再び拒否 |
ポリシーの反映は速く、policy update(--wait なし)は 0.01 秒で返り、約 0.7 秒後に新しいルールが有効になりました(--wait ありだと、確認待ちを含めて約 9 秒でした)。
5.6 資格情報の隠蔽
OpenShell は、API キーなどをサンドボックスの環境変数に入れず、プロキシの出口で実値に置換します。「プロバイダ」を登録して使います。
# echo-api.yaml(プロバイダのプロファイル)
id: echo-api
category: other
credentials:
- name: api_token
env_vars: [ECHO_API_TOKEN]
required: true
auth_style: bearer
header_name: authorization
endpoints:
- host: <host-ip> # ホスト上のエコーサーバー
port: 18081
protocol: rest
access: full
enforcement: enforce
binaries:
- /usr/bin/curl
openshell profile import -f echo-api.yaml
openshell provider create --name work-echo --type echo-api --credential ECHO_API_TOKEN=<偽の値>
openshell sandbox create --name v5 --from <image> --provider work-echo --detach -- sleep 36000
| 試験 | 結果 |
|---|---|
サンドボックス内の $ECHO_API_TOKEN
|
openshell:resolve:env:... というプレースホルダー(ワークロードの環境変数に実値は無い) |
Authorization: Bearer $ECHO_API_TOKEN でエコーサーバーへ送信 |
ホスト側のサーバーに届いたのは実値(偽キー)に置換された値 |
別のヘッダ(X-Leak: $ECHO_API_TOKEN)で同じ宛先へ送信 |
これも実値に置換された |
プレースホルダーを別のホスト(example.com、通信は許可済み)へ送信 |
HTTP 403 credential_endpoint_mismatch |
実値の置換は、ワークロード(エージェントのプロセス)とは別の、supervisor のプロキシが行います。ワークロードが見られる環境変数には、プレースホルダーしかありませんでした。また、試した範囲では(宛先1つ、別のホスト1つ)、束縛されていない宛先に送ったプレースホルダーは置換されず、403 になりました。一方で、束縛された宛先に対しては、エージェントが任意のヘッダにプレースホルダーを入れて送れてしまう点は、設計上の限界として押さえておく必要があります(§6.2)。なお、URL・クエリ・ボディに入れた場合や、別のポート、リダイレクトの扱いは試していません。
5.7 ログと OTLP 出力
openshell logs <name> --source sandbox で、OCSF 形式の行が見えます。
NET:OPEN [MED] DENIED /usr/bin/python3.12(0) -> example.com:443 [reason:transparent_tcp_policy_denied]
HTTP:GET [INFO] ALLOWED GET https://example.com/ [policy:allow_example_com_443 engine:l7]
OTLP でも出力できます。ゲートウェイの設定に次を足すだけです(OTLP は gRPC のみ)。
[openshell.gateway.otlp]
endpoint = "http://<host-ip>:4317"
service_name = "openshell-gateway-sbv"
[openshell.gateway.otlp.ocsf]
enabled = true
コレクターは otel/opentelemetry-collector-contrib を gRPC 4317 で立て、受信内容をファイルに書き出して確かめました。
-
トレース: サービス名
openshell-gateway-sbv、openshell-driver-vm、openshell-supervisorの3つ。gRPC メソッド単位(ExecSandbox、RelayStreamなど)のスパンが届きました。 -
OCSF ログ: 主なサービス名は
openshell-supervisorです(ゲートウェイ由来の認証イベントも少数ありました)。本文は OCSF 1.8.0 の JSON で、Network ActivityやSSH Activityなどのクラスが含まれていました。サンドボックス側のイベントは supervisor がコレクターへ直接送ります(ゲートウェイ経由ではありません)。 -
拒否イベントの中身:
action=Denied、disposition=Blocked、status_detail=transparent_tcp_policy_denied、宛先example.com:443、アクターは/usr/bin/curlで、親プロセスの連鎖(bash ← claude.exe ← dash)まで記録されました。
一方、Landlock によるファイル拒否は OCSF に出ません。秘密ファイルの名前を含むレコードは 0 件でした。カーネルが黙って拒否するためです。ファイル側の試行を検知したい場合は、別の手段が必要です。
5.8 CPU・メモリ制限
| 条件 | 結果 |
|---|---|
ゲートウェイ vcpus=2, mem_mib=2048 で sandbox create --cpu 1 --memory 512Mi
|
ゲスト内は nproc=2、MemTotal 約 2GiB のまま。CPU 4並列で実効 1.99 コア、1800MiB の確保も成功。このフラグはこの環境(VM ドライバー、v0.1.5)では効果が見えなかった(エラーの行は出なかった。ただし警告の有無は、出力を絞って見ていたため確認できていない) |
ゲートウェイ設定を vcpus=1, mem_mib=768 に変更 |
nproc=1、MemTotal 768MiB。CPU 4並列でも実効 0.99 コア。300MiB は確保でき、1000MiB は MemoryError(ゲストのカーネルが確保を拒否。サンドボックスは生存) |
VM ドライバーでは、資源の上限はゲートウェイ設定で決まる VM のサイズで効きました。サンドボックスごとの --cpu/--memory は、この環境では効果が見えませんでした。VM のサイズが全サンドボックス共通かどうかは、フラグが効かなかったことからの推測です(ヘルプにはフラグの説明があり、VM ドライバーでの制約は書かれていません)。
5.9 実エージェント(Claude Code)を入れる
この検証では、本物の Anthropic API、実キー、サブスクリプションは使っていません。API キーは偽の値で、モデルはホスト上の自作モックです。検証しているのは「モデルが悪意ある指示を出したとき、箱が何を防ぐか」であって、Claude Code の通常動作ではありません。
- イメージに
npm i -g @anthropic-ai/claude-code(2.1.310)を入れました。Claude Code は単体バイナリ(.../bin/claude.exe)でした。 - プロバイダのプロファイルで、
ANTHROPIC_API_KEYを偽の値で登録し、エンドポイントはホスト上のモック、許可バイナリはclaude.exeにしました。 - モックは、プロンプトにマーカーがあると、敵対的な Bash コマンドの tool_use を返すよう作りました(プロンプトインジェクションで乗っ取られたモデルの模擬)。
id; echo KEY=$ANTHROPIC_API_KEY; cat /opt/secret_dir/id_rsa; cat /root/id_rsa_dummy;
curl -s -m 6 -o /dev/null -w 'example.com=%{http_code}\n' https://example.com
Claude Code は起動し、モックとの通信、ストリーミング、ツール実行まで完走しました。モデルに返ったツールの結果は次の通りです。
uid=1000(ubuntu) gid=1000(ubuntu) groups=1000(ubuntu)
KEY=openshell:resolve:env:v12700466806757846452_ANTHROPIC_API_KEY
cat: /opt/secret_dir/id_rsa: Permission denied
cat: /root/id_rsa_dummy: Permission denied
example.com=000
- キーはプレースホルダーしか見えません(モックに届いた
x-api-keyは偽キーの実値に置換されていました)。 - 秘密ファイルは読めず、外部通信は遮断されました。
- 外部通信(
curl)の拒否は OCSF にも記録されていました。秘密ファイルの拒否は、OCSF には出ていません(§5.7)。 - この検証では、Claude Code の余計な通信を避けるため、
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1、DISABLE_TELEMETRY=1、DISABLE_AUTOUPDATER=1を付けて起動しています。
6. 破ってみる(守りの限界)
同じような「悪さ」を、2つの道具に対して試した結果を並べます。OpenSandbox は「コンテナの中のコード」、OpenShell は「箱の中のエージェント」に対する試験です。
| 試すこと | OpenSandbox(Docker) | OpenShell(VM ドライバー) |
|---|---|---|
ホストの秘密ファイル(/mnt/c など)を読む |
見えない | (そもそもゲストに無い) |
| 箱の中の権限上は読めるファイルを読む | 読める(コンテナ内の root) |
拒否(Landlock、リンク/コピー/.. 経由も) |
| 環境変数に入った API キーを出力する | ホスト側のキーは渡らない運用 | プレースホルダーのみ |
| 外部へ通信する | 既定で許可 | 既定で全拒否、バイナリ+L7 で許可 |
ホストの 0.0.0.0 待受サービスに届く |
届く(Docker の既定のブリッジ経由。素の docker run との比較はしていない) |
拒否(ポリシーに無い宛先。通信は supervisor のプロキシを通る) |
mount -t tmpfs、unshare -r
|
mount は拒否(unshare は未検証) |
拒否 |
| メモリを大量に確保する | 上限(2GiB)を超えて 3GiB までは確保できた(swap)。5GiB まで続けると SIGKILL で終了した(OOM killer と推定) | VM のサイズで制限(超えると OOM killer が働く) |
| プロセスを大量に作る | 上限で失敗 | (未検証) |
6.1 OpenSandbox を使うときの注意
- 通信が既定で許可。egress ポリシーで絞るのが前提で、IP 直指定まで止めるには
dns+nftモードが要ります。 - コンテナ内は root です。ケイパビリティは絞られていますが、箱の中のファイルや環境変数は、中のコードから自由に読めます。既定の設定(root、追加設定なし)では、箱の中での細かいアクセス制御は見つかりませんでした(OpenShell の Landlock のようなものはありません。非 root のイメージや読み取り専用のマウントは試していません)。1つの箱が1つの信頼境界と考えて、秘密は箱に入れない運用が前提です。
- ホストの
0.0.0.0で待ち受けているサービスに届きます。
6.2 どちらの箱も防げないもの
- 箱から戻ってきた出力経由のプロンプトインジェクション: OpenSandbox の箱の中のコードが出力した文字列は、エージェントにそのまま渡り、モデルへの入力になります(§8.2)。箱を重ねても、この経路はふさげません。
- 許可した通信経路での持ち出し: OpenShell の資格情報は、束縛された宛先に対しては、試した範囲(Authorization と別のヘッダの2つ)で、ヘッダにプレースホルダーを入れれば実値に置換されました。エージェントが乗っ取られると、許可した宛先に向けて情報を送ることはできます。許可する宛先は、本当に信頼できるものに限る必要があります。
- モデル自身の誤動作や品質の劣化: 箱の外から見て正常な通信・操作でも、結論の質が下がることはあります。これは別の問題で、以前の記事6で扱いました。
- 監視の穴: Landlock によるファイル拒否は OCSF に出ませんでした。ファイル側の試行の検知は別途必要です。
7. 比較と使い分け
| 観点 | OpenSandbox | OpenShell |
|---|---|---|
| WSL2 での導入 | 素の Docker で動いた。API キーの設定が必要 | v0.1.5 は Docker ドライバー不可。VM ドライバー(KVM 権限)なら可。v0.0.116 の Docker ドライバーは可 |
| 起動時間 | 約1.2秒(egress ポリシーなし、2回目以降)。egress ポリシー付きで約3.3〜3.7秒 | 数秒 |
| 守れたもの | ホストのファイル、docker.sock、特権操作、プロセス数 | ファイル(Landlock)、通信(バイナリ+L7)、資格情報、ログ |
| 守れなかったもの/注意 | 既定の通信許可、ホストの 0.0.0.0 サービスへの到達、メモリ上限(swap) |
束縛先への任意ヘッダでの送信、Landlock 拒否が OCSF に出ない |
| 追加で必要だったもの | API キー、(状態保持は専用イメージ) | kvm グループ、driver_dir、スキーマ v2 の設定 |
| 資源の制限 | コンテナの cgroup(既定 2GiB、pids 4096) | VM のサイズ(ゲートウェイ設定)。--cpu/--memory は効果が見えなかった |
使い分けの目安は、次の通りです。
- 自作のエージェントに、コード実行やファイル操作を安全に貸したい → OpenSandbox。ただし egress ポリシーを必ず付ける。
- 既存のエージェント(Claude Code など)を、丸ごと閉じ込めたい → OpenShell。ファイル・通信・資格情報をポリシーで縛れる。
- エージェント自体も絞り、かつコード実行を多用する → 両方を組み合わせる(§8)。
どちらも「箱」であって、箱の中でモデルが何をするかを保証するものではありません。
8. 組み合わせて使う構成(ハイブリッド)
2つの道具は守る場所が違うので、重ねて使うこともできます。考え方は単純で、エージェント自身は OpenShell の箱に入れ、エージェントが書いたコードの実行だけを OpenSandbox の使い捨てコンテナに任せる、というものです。実際にこの構成を組んで動かしました。
8.1 組み方
-
OpenSandbox の API キーを、OpenShell のプロバイダとして登録する。認証ヘッダは
OPEN-SANDBOX-API-KEYでした。エンドポイントはサーバーのホストとポート(8080)、許可するバイナリは SDK を動かす Python とcurlに限りました。
id: opensandbox-api
category: other
credentials:
- name: api_key
env_vars: [OPENSANDBOX_API_KEY]
required: true
auth_style: header
header_name: OPEN-SANDBOX-API-KEY
endpoints:
- host: <host-ip>
port: 8080
protocol: rest
access: full
enforcement: enforce
binaries:
- /usr/bin/python3.12
- /usr/local/venv/bin/python
- /usr/bin/curl
-
OpenShell のサンドボックスに、OpenSandbox の SDK を入れたイメージを使う。Landlock の許可リスト(
/usrは読み取り可、/optは不可)に合わせて、venv は/usr/local以下に置きました。 -
SDK は
use_server_proxy=Trueで接続する。サンドボックス内の実行デーモンへの通信も、サーバー(8080)経由にまとめるためです。通信先が 8080 の1つだけになり、ポリシーが書きやすくなります。 -
OpenSandbox 側のサンドボックスには
default_action="deny"の egress ポリシーを付ける(サーバーの egress モードは、§8.4 の通りdns+nftにします)。
8.2 実測
エージェント役は Claude Code(2.1.310)です。イメージに Claude Code と OpenSandbox の SDK の両方を入れ、2つのプロバイダ(モデル API 用の偽キーと、OpenSandbox 用のキー)を付けた OpenShell のサンドボックスで、claude -p を動かしました。モデルは §5.9 と同じ自作のモックで、乗っ取られたモデルを模して、次の1つの Bash コマンドを返します。
- 2つの API キーを
echoする。 -
/opt/secret_dir/id_rsa(許可リスト外の秘密ファイル)をcatする。 -
curlでホストの別サービス(172.17.0.1:9999)に直接つなぐ。 - SDK のスクリプトで、OpenSandbox の箱を作り、その中でコードを実行する。
| 試験 | 結果 |
|---|---|
| Claude Code の Bash が見る2つのキー | どちらも openshell:resolve:env:... のプレースホルダー
|
モデル API に届いた x-api-key
|
偽キーの実値(置換されていた) |
箱の中から、キーなしで OpenSandbox の GET /v1/sandboxes
|
401 |
| 箱の中から、プレースホルダーをヘッダに入れて同じ API | 200(プロキシが実キーに置換した) |
秘密ファイルの cat
|
Permission denied(Landlock) |
curl でホストの別サービスに直接 |
拒否(OCSF: NET:OPEN DENIED /usr/bin/curl -> 172.17.0.1:9999) |
| Claude Code から OpenSandbox を呼ぶ(Python 経由) | 成功(作成は約3.3〜3.7秒)。ログに python3.12 の POST /v1/sandboxes、.../files/upload、.../command が ALLOWED
|
| OpenSandbox の箱の中のコード |
uid 0、秘密めいた環境変数は GPG_KEY(イメージ由来)のみ、ホスト 172.17.0.1:9999 への接続は TimeoutError(dns+nft の場合。§8.4)、example.com は名前解決失敗、計算結果は返った |
| 箱の中の出力 | モデル(モック)へのツール結果としてそのまま戻った |
最後の行が大事です。OpenSandbox の箱の中のコードが出力した文字列は、Claude Code を経由して、そのままモデルの入力になります。箱を重ねても、この経路はふさげません(§6.2)。
8.3 落とし穴1: ホストの 127.0.0.1 では届かない
OpenSandbox サーバーを 127.0.0.1:8080 で待ち受けたままだと、OpenShell の箱の中から接続できませんでした(Failed to connect ... after 0 ms)。VM のゲストにとっての 127.0.0.1 は、ゲスト自身のループバックで、ホストのループバックではないためと考えられます(ネットワークの一般的な性質からの説明です。ログのエラーは、ポリシーによる拒否のときと同じ形のため、ログだけでは区別できません)。
届かせるには、サーバーをホストの別のアドレス(今回はホストの IP)で待ち受けさせる必要があります。WSL2 のミラーモード(今回の設定)では、そのアドレスが LAN から見える可能性があるため、API キーが破られると、コンテナ作成権限が奪われます。待受アドレスの絞り方は、今回は検証していません。
8.4 落とし穴2: OpenSandbox の egress は既定の dns モードだと IP 直指定を止めない
OpenSandbox サーバーの設定 [egress] mode で、egress の絞り方が変わります。default_action="deny" と example.com だけの許可を付けて、サンドボックスの中から TCP 接続を試しました。
| 接続先 |
mode = "dns"(既定) |
mode = "dns+nft" |
|---|---|---|
172.17.0.1:9999(ホストの別サービス。内容も読めるか) |
接続でき、内容を読めた | タイムアウト(拒否) |
1.1.1.1:443(IP 直指定) |
接続できた | タイムアウト(拒否) |
192.168.x.x:8080(ホストの IP) |
接続拒否(サービスが無いため) | タイムアウト(拒否) |
example.com:443(許可した名前) |
接続できた | 接続できた |
example.org:443(許可していない名前) |
名前解決に失敗 | 名前解決に失敗 |
8.8.8.8:53(DNS の IP) |
接続できた | 接続できた |
dns モードは、名前解決の段階で絞る方式なので、IP アドレスを直接指定した接続は止めません。dns+nft モードは nftables でもパケットを止めるため、TCP の IP 直指定まで止まります(UDP と QUIC には制約があります。詳しくはこの下の「dns+nft モードの仕組みと制約」を参照)。8.8.8.8:53 に接続できたのは、53 番ポートの通信がすべてサイドカーの DNS プロキシに集められるためと考えられます(自由に外へ出られるわけではなく、許可されていない名前には「存在しない」と答えが返ります)。したがって、§4.4 で書いた egress の絞り込みは、dns+nft にして初めて、TCP については IP 直指定まで含めた default deny になる、と言えます。
dns+nft モードでハイブリッド構成を動かすと、OpenSandbox 側のコードからの 172.17.0.1:9999 への接続は TimeoutError になりました(dns モードのときは接続できました)。
dns+nft モードの仕組みと制約
公式の architecture ドキュメント7によると、OpenSandbox は egress 用のサイドカーコンテナをサンドボックスの横に立て、サンドボックスがそのネットワークを共有します。モードの違いは、サイドカーが何を止めるかです。
dns(既定) |
dns+nft |
|
|---|---|---|
| 仕組み | 53 番ポートの通信をサイドカーの DNS プロキシに集め、許可されていない名前には「存在しない」と答える |
dns に加えて、nftables でパケットの段階でも default deny にする |
| 止められるもの | 名前を使う接続だけ | 名前に加えて、IP アドレスを直接指定した接続 |
dns+nft では、DNS で許可された名前が解決した IP アドレスを、一定期間だけ許可します。通信中の TCP 接続には猶予があり、一度許可したアドレスが永久に許可されたままにはなりません。
ドキュメントに書かれている制約は、次の通りです。
- UDP と QUIC は、DNS の期限だけで制御されます。ポート指定のルールは TCP のみです。
- DNS over TLS(853 番) は常に遮断されます。DNS over HTTPS(443 番) は、スイッチ1つで遮断できます。
-
53 番ポートはすべて DNS プロキシに集められます。今回、
8.8.8.8:53へ接続できたのは、このためと考えられます。 - 通信を差し込むルールを入れられない場合、サイドカーは通信を素通しにせず終了します。
- gVisor は対象外で、サポートされているセキュアなランタイムは Kata とされています。また、同じ Pod にサービスメッシュのサイドカーがあると、衝突するためサポートされません。
したがって、dns+nft にしても「あらゆる経路を塞ぐ」わけではありません。UDP や QUIC を使う経路は、DNS の期限によるゆるい制御にとどまります。
8.5 役割分担
| 守りたいもの | 担当 | 方法 |
|---|---|---|
| エージェント本体のファイル・環境変数 | OpenShell | Landlock で許可リスト外を拒否。秘密はプレースホルダーのみ |
| エージェントの外向き通信 | OpenShell | ホスト+バイナリ+L7 のポリシー。OpenSandbox サーバーへの通信だけを許可する |
| OpenSandbox の API キー | OpenShell | プロバイダとして登録し、エージェントにはプレースホルダーだけ見せる |
| エージェントが生成した(信頼できない)コードの実行 | OpenSandbox | 使い捨てのコンテナで実行し、終わったら破棄する |
| そのコードの外向き通信 | OpenSandbox | egress を dns+nft モードにして default deny、必要なドメインだけ許可する |
8.6 ハイブリッド構成のメリット
組み合わせる利点は、大きく3つあると考えています。
1. 信頼の段階を分けられる。 エージェント本体(秘密のキー、モデルとの通信)と、エージェントが書いた信頼できないコードを、別の箱に分けられます。コードの実行側にはキーを渡さず(箱の中の環境変数は GPG_KEY のみでした)、エージェント側にはコードを直接走らせません。
2. 重い処理を、エージェントの箱の外に逃がせる(ただし、短い処理では割に合わない)。 OpenShell の VM のサイズ(vCPU とメモリ)は、ゲートウェイ設定で決まります。サンドボックスごとの --cpu/--memory は、この環境では効果が見えませんでした(§5.8)。そのため、エージェントの箱の中で重い処理を並列に走らせると、VM のサイズで頭打ちになります。サブエージェントが並列にテストやビルドを回す場面を想定して、同じ量の CPU 仕事(1 ワーカーあたり、VM の 1 コアで約 6 秒)を N 個並列に走らせ、全部終わるまでの時間を比べました。
| 並列数 N | エージェントの VM(2 vCPU)の中で実行 | OpenSandbox の N 個のコンテナに任せる(作成〜破棄の全体) |
|---|---|---|
| 2 | 6.7 秒 | 14.6 秒(作成 3.8 + 実行 8.9) |
| 4 | 13.5 秒 | 16.9 秒(作成 5.5 + 実行 9.9) |
| 6 | 20.1 秒 | 19.2 秒(作成 7.9 + 実行 9.4) |
| 8 | 23.2 秒 | 21.6 秒(作成 9.9 + 実行 10.1) |
(各 N で 3 回の中央値。WSL の CPU は 6。N=8 だけ 1 ワーカーの仕事量が約 1 割少ないため、N をまたいだ比較はできません。)
- エージェントの VM の中では、仕事量が増えるほど、2 コアに順番待ちして時間が伸びます(N=2 で 6.7 秒、N=8 で 23.2 秒)。
- コンテナに任せると、並列数が増えても全体の時間はあまり伸びませんが、箱の作成に 4〜10 秒かかるうえ、1 つあたりの実行も VM 内より遅く(約 9〜10 秒。コンテナの CPU が WSL 全体で共有されていることなどが原因と見られますが、内訳は確認していません)、N が 4 以下では、VM の中で走らせたほうが早く終わりました。
- 損益分岐は、この条件(6 秒の仕事)では N が 6 前後でした。1 つの仕事がもっと長ければ、作成の固定費の割合が下がるため、コンテナに任せるほうが有利になる範囲は広がると考えられます(ただし、今回は 6 秒の仕事だけを測っています)。
つまり、「並列の負荷を逃がせる」のは、数十秒以上かかる処理を、6 個前後以上、まとめて並列に回す場合で、短い処理を何度も任せる用途には向きません。
メモリでは、より明確な差が出ました。1つあたり 600MiB を実際に触るワーカーを 4 つ並列に走らせました(エージェントの VM は約 2GiB で、これはゲートウェイ設定で決まるサイズです。コンテナの側は、1 つあたり 2GiB の上限で、全体は WSL の 12GB の範囲です)。
| 実行場所 | 成功したワーカー | 内容 |
|---|---|---|
| エージェントの VM の中 | 3 / 4 | 1 つが SIGKILL で終了した(rc=-9。OOM killer によるものと推定) |
| OpenSandbox の 4 つのコンテナ | 4 / 4 | 各コンテナは 600MiB で収まった |
4 × 600MiB は、2GiB の VM に収まらない大きさです(つまり、「VM が小さい」ことの影響を見ています)。コンテナに任せると、メモリも箱ごとに分けられ、合計が WSL の範囲に収まる限り、VM の大きさに縛られません。
また、これはサブエージェントが行うコード実行の話です。サブエージェントの LLM とのやり取り自体は、CPU をほとんど使わず、そのままエージェントの箱の中で動きます。サブエージェントごとに別の OpenShell の箱を用意すれば、エージェント自体も分けられますが、今回は試していません。
3. 暴走の被害を、エージェントに波及させにくい。 使い捨ての箱は、1回の実行ごとに作って破棄できます。前の実行の変数やファイルが次に残らず、箱を共有しなければ、サブエージェント同士のファイルは分かれるはずです(別々のコンテナになるという設計上の想定で、互いに見えないことの確認はしていません)。さらに、実行したコードがメモリを使い切るように暴走したときの影響を、実際に比べました。
ワーカーは、メモリを 50MiB ずつ実際に触りながら、最大 5GiB まで確保し続ける暴走役です。
| 場面 | 暴走役 | 隣で動いていたもの | 結果 |
|---|---|---|---|
| エージェントの VM の中で暴走(隣は軽い心拍だけ) | SIGKILL で終了(OOM killer と推定) | 心拍(最大の間隔 0.20 秒) | 暴走役だけが殺された。心拍は影響なし |
| コンテナの中で暴走 | SIGKILL で終了(約 5.9 秒。OOM killer と推定) | 別のコンテナの心拍、エージェント側の心拍 | どちらも影響なし(最大の間隔 0.20 秒)。暴走したコンテナも、その後に応答した |
| エージェントの VM の中で暴走(隣に 1.2GiB を保持する「エージェント役」) | SIGKILL で終了(OOM killer と推定) | エージェント役のプロセス |
暴走役とエージェント役の両方が SIGKILL で終了した(rc=-9) |
| コンテナの中で暴走(隣に 1.2GiB を保持する「エージェント役」が VM 側) | SIGKILL で終了(OOM killer と推定) | エージェント役のプロセス(VM 側) | エージェント役は最後まで生き残った |
エージェントの VM の中でメモリを奪い合うと、(おそらく OOM killer が)大きいプロセスを終了させるため、エージェント自身が巻き込まれることがあります(3 行目。1.2GiB を保持する人工的な「エージェント役」を置いた設定での結果です)。暴走するコードをコンテナの中で走らせると、コンテナの上限で止まり、エージェントが動いている VM のメモリには影響しませんでした。
この検証は、メモリの暴走だけを、1台のマシンで、1回ずつ試した結果です。CPU の暴走、ディスクの使い切り、ネットワークの暴走は試していません。
8.7 組み合わせるときの注意
-
OpenSandbox の egress は
dns+nftにする。dnsモードのままだと、OpenShell の箱で絞っても、OpenSandbox の中のコードが IP 直指定でホストの他のサービスに届きます。 - OpenSandbox サーバーの待受アドレスに注意する。VM から届かせるにはホストの IP で待ち受ける必要があり、LAN に露出しうるため、API キーの管理が重要になります。
- OpenSandbox から戻ってきた出力は、信頼できないデータとして扱う。実行結果に含まれる文字列が、エージェントへのプロンプトインジェクションの経路になりえます。箱を重ねても、この経路はふさげません(§6.2)。
- ログを両方から集める。OpenShell の OCSF(箱の中のエージェントの通信)と、OpenSandbox のコマンド出力・メトリクスを突き合わせます。Landlock によるファイル拒否は OCSF に出ないため、別の監視が要ります。
運用の手間は増えます。ゲートウェイ、OpenSandbox サーバー、(必要なら)コレクターと動かすものが多くなるため、「自作のエージェントがコード実行を多用し、かつ、エージェント自体の権限も絞りたい」場合に限って検討するのが現実的だと思います。
9. まとめ
9.1 分かったこと
-
OpenSandbox 1.1.1 は、素の WSL2 + Docker Engine で動きました。通信は既定で許可され、既定の egress モード(
dns)は IP 直指定の接続を止めません。絞るならdns+nftモードが要ります。ホストの0.0.0.0で待ち受けるサービスにも届きます。 - OpenShell は、同じ PC で、v0.0.116 の Docker ドライバーは動き、v0.1.5 の Docker ドライバーは動きませんでした。v0.1.5 の supervisor が行う seccomp 通知のプローブが、WSL2 が継承しているフィルタと衝突するとみられます。v0.1.5 でも VM ドライバー(KVM)なら動きました。
- 動いた OpenShell は、ファイル(Landlock)、通信(バイナリ+L7)、資格情報の隠蔽が期待どおり効きました。乗っ取られたモデルを模した Claude Code でも、キーの流出、秘密ファイルの読み取り、外部通信を防ぎました。
-
2つを組み合わせる構成(OpenShell の箱の中のエージェントが、コード実行を OpenSandbox に任せる)も動きました。API キーは OpenShell のプロバイダ経由でプレースホルダーのまま使えます。
- 利点: メモリの暴走や、メモリ・CPU を使う処理の並列実行を、エージェントの箱から切り離せました。CPU については、6 秒の仕事を並列に回した場合、N が 6 前後以上でコンテナに任せるほうが早く終わりました(N が 4 以下では VM の中のほうが早い)。
- 落とし穴: VM のゲストからホストの
127.0.0.1には届かないこと、dnsモードの egress が IP 直指定を止めないこと、箱の作成に 4〜10 秒かかり、短い処理では割に合わないこと。
- 防げないものもあります。束縛先への任意ヘッダでの送信、箱から戻った出力を経由したプロンプトインジェクション、Landlock の拒否が OCSF に出ない監視の穴、サンドボックスごとのリソース指定の効果が見えなかった点などです。
9.2 この検証で言えないこと
- WSL2 の1台のマシンで、バージョンを固定して行った結果です。他の環境(Linux 実機、macOS、Docker Desktop 構成)では異なる可能性があります。
- 性能の数値(箱の作成時間、メモリの暴走など)は、1台のマシンでの少数回(暴走の試験は各1回)の測定です。厳密なベンチマークではありません。CPU・ディスク・ネットワークの暴走は試していません。
- Kubernetes ランタイム、Kata/gVisor は未検証です。
- 原因が未確認のもの: OpenShell v0.1.5 の Docker ドライバーが失敗する根本原因(WSL 側のフィルタの出どころ)、VM ドライバーの
driver_dirが必要な理由、ゲートウェイのローカル JSONL(ocsf_log)が作られなかった理由。 - OpenSandbox の MCP 連携は、起動オプションの確認までです。
- Claude Code の検証はすべて自作のモックのモデルによるもので、本物の Anthropic API、サブスクリプション認証、Claude Code 自身の通信(自動更新・テレメトリ)を許可するポリシー設計は未検証です。モックは API の一部だけの再現です。
- ハイブリッド構成で、待受アドレスを LAN に露出させない方法(ブリッジ専用のアドレスなど)は未検証です。
参考リンク
- Agentic AI Foundation Announces Four New Projects(AAIF)
- Linux Foundation Announces the Formation of the Agentic AI Foundation
- alibaba/OpenSandbox — GitHub
- OpenSandbox Egress のアーキテクチャ
- NVIDIA/OpenShell — GitHub
- NVIDIA OpenShell ドキュメント(ポリシー、プロバイダ、ゲートウェイ設定、観測性)
-
Agentic AI Foundation Announces Four New Projects(2026-10-22) ↩
-
Linux Foundation Announces the Formation of the Agentic AI Foundation(2025-12-09) ↩
-
OpenAI – Hugging Face Incident Technical Report(OpenAI の技術報告書、PDF) ↩
-
Anatomy of a Frontier Lab Agent Intrusion: A Technical Timeline of the July 2026 Incident(Hugging Face) ↩