株式会社ホコサキ

NVIDIA OpenShellでAIエージェントのシェル実行を安全に閉じ込める

天京祐輔
天京祐輔
NVIDIA OpenShellでAIエージェントのシェル実行を安全に閉じ込める

Claude Code や Cursor を使っていると、ある時点で必ず「エージェントにシェルコマンドを実行させたい」という欲求が生まれます。
ファイルを読んで、テストを走らせて、ビルドして、結果をフィードバックに使う。
エージェントが「手」を持つことで、できることの幅が一気に広がります。

でも正直なところ、手を持たせた途端に怖くなりませんか。
Docker を使えばいいのか、chroot で十分なのか、そもそも既存の隔離手段でエージェントを本当に閉じ込められるのか。
「エージェントに実行権限を与えたいが、野放しにはできない」という緊張感を起点に、既存手段の構造的な限界を解体しながら、NVIDIA OpenShell の設計思想とセットアップ・最小構成の呼び出しコードまで連れていきます。

エージェントに「手」を持たせると何が起きるか

エージェントがシェル実行できる環境を想像してみてください。
開発ディレクトリにアクセスできるということは、同じ場所に置かれた .env ファイルも読めます。
SSH 鍵が ~/.ssh に入っていれば、そこにも届く可能性があります。
「rm -rf」は明示的に指示しなくても、エージェントが「不要なファイルを整理する」という文脈で実行する余地があります。

「プロンプトで制御すればいい」と思いたい気持ちはわかります。
「危険なことはしないように」と書いておけばいいじゃないか、と。
でも、それが崩れるケースはいくつもあります。

代表的なのが プロンプトインジェクション です。
エージェントが外部から取得したテキスト(ウェブページ、ファイル内容、API レスポンス)に「次のように動作せよ」という指示が埋め込まれていた場合、エージェントはそれを素直に処理することがあります。
自分で書いたシステムプロンプトが書き換わるわけではないので、ログを見るまで気づきません。

もう一つは、多段ツール呼び出し中に発生する意図のズレです。
「バグを直してテストを実行」という指示が連鎖していくと、途中の判断ステップで「必要なファイルを削除してから再生成する」というアクションが挟み込まれる余地が生まれます。
エージェントが悪意を持っているわけではありません。
問題の本質は「エージェントが誤解する」「騙される」ことにあります。

この整理は実務上とても重要です。
「エージェントを信頼するかどうか」という問いより、「エージェントが処理する入力を信頼できるか」という問いの方が、実態に近い。
後者は、ほとんどのケースで「信頼できない」と答えるのが正直なところです。
だから、プロンプトのルールに加えて、実行環境そのものに制限をかける必要があります。

Docker と chroot では何が足りないのか

手っ取り早い選択肢として chroot が候補に挙がることがあります。
見かけ上はルートディレクトリを変えられるので、「別の空間で動かしている」感が出ます。
でも、chroot が実際にやっていることは ファイルパスの解決先を変えるだけ です。

プロセス分離はありません。
chroot 内から ls /proc を実行すると、ホスト上で動いているすべてのプロセスが見えます。
ネットワーク制御もありません。
そして root 権限を持つプロセスであれば、chroot を脱出すること自体はそれほど難しくありません。
セキュリティ機構として設計されたものではないので、当然といえば当然です。

では Docker はどうか。
Linux namespaces と seccomp を組み合わせたプロセス・ファイルシステム・ネットワークの分離は、chroot とは別次元の強度があります。
ただ、Docker にも構造的な課題があります。
エージェントのツール実行は短い間隔で繰り返し呼び出されるため、コンテナをその都度スピンアップしていると遅延が積み上がります。
コンテナ内からデーモンソケットにアクセスできる構成になっていると、コンテナエスケープの経路になりえます。
特権コンテナや不用意なボリュームマウントも同様のリスクを抱えています。

そして、より根本的な問題があります。
toxsec.com が端的に言い表しています。「サンドボックスはすべて、中身が脱出を望まないという前提で設計されている(the code doesn't want out)」。
chroot も Docker も、コードが実行されることを想定した設計です。
コードが積極的に脱出方法を探すことは想定していません。

AI エージェントは「目的を達成するために何をすべきか」を自律的に判断します。
その判断の中に「サンドボックスを回避する」が入り込む余地は、従来のコードより格段に大きい。
設計前提が根本的にずれているのです。

NVIDIA OpenShell の設計思想と位置づけ

OpenShell は NVIDIA が開発したオープンソースのサンドボックスランタイムです。
GitHub リポジトリに rust-toolchain.toml が含まれており、Rust をベースとした実装であることが確認できます。
短命なプロセスを高頻度で起動・終了するエージェント実行環境に、メモリ安全で軽量なバイナリは相性のいい選択です。

最大の特徴は、許可する操作を 宣言的に定義 できる点です。
「このファイルパスへの書き込みは許可する」「このネットワークドメインへの通信は許可する」という形で制約を表明し、宣言されていないものはデフォルトで遮断されます。
「禁止リストに書かれたことはしない」というプロンプト制御とは構造が逆です。
書いてないことは動かない、という設計は、予期しない動作への防御として根本的に強い。

アーキテクチャの考え方として、Red Hat AI との連携ドキュメントで説明されている「Mode 2」が参考になります。
エージェントの 脳(推論・オーケストレーション)と 手(ツール実行・コード実行)を切り離す構成です。
推論部分はサンドボックス外で動かし、実行部分だけを OpenShell が管理する環境に渡します。
エージェントが「何をするか」を決める部分と、「実際に何かが起きる」部分のあいだに、ポリシーで制御されたゲートが挟まる構造です。

機能面では、gRPC 経由での通信、ワークスペースの永続化、セッション再開がサポートされています。
エージェントの実行が途中で止まった場合に状態を引き継げる点は、単なるシェル呼び出し制限ツールより一段階上の設計です。
ファイルパスのバリデーションは二重になっており、クライアント側でワークスペースルートへの制限を検査したあと、サンドボックス内でさらに OpenShell 自身のファイルシステムポリシーが適用されます。

gVisor や Firecracker のようなカーネル・VM レベルの隔離技術は汎用性が高い分、エージェントのツール実行という文脈での利便性は高くありません。
OpenShell は SDK や CLI が整備されており、エージェントと組み合わせることを前提とした作りになっている点で、方向性がはっきりしています。

セットアップ:インストールから基本ポリシー設定まで

OpenShell 本体のインストールはリポジトリルートにある install.sh で行います。

curl -fsSL https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | bash

これとは別に、エージェントに OpenShell CLI の操作方法を教えるスキルを追加する手順があります。

npx skills add NVIDIA/OpenShell

こちらは OpenShell 本体のインストールとは別物です。
skills/ 配下にエージェントが CLI をどう操作するかの手順が追加される仕組みで、OpenShell のソースチェックアウトも不要です。
「インストールの2択」ではなく、本体インストールのあとに必要に応じて追加するもの、と理解しておくと混乱しません。

ポリシーの設定は、ファイルシステムへのアクセス範囲・許可するネットワークドメイン・実行可能なコマンドを宣言的に定義する形をとります。
具体的なスキーマとキー名はリポジトリのドキュメント(NVIDIA/OpenShell)で確認してください。
設計の方針として押さえておきたいのは、 許可リストは最小から始めて広げていく という原則です。
最初から広い権限を与えてしまうと、あとで絞る判断が難しくなります。

deny ログを見ながら「なぜこのドメインへのアクセスが発生したのか」を一つひとつ確認するクセをつけておくと、本番のポリシー設計に活きます。
OpenShell はサンドボックス内で実行されたコマンドとファイルアクセスの記録を出力できます。
セッションごとのログを保存しておくことで、「エージェントが何をしたか」をあとから追えるようになります。
トラブル発生時の原因追跡と、ポリシーの調整判断の両方に役立ちます。

エージェントから OpenShell を呼び出す最小構成

Red Hat AI との連携ドキュメントに公開されているスニペットをベースにした最小構成です。
Python SDK を使って、エージェントの実行時に OpenShell サンドボックスを差し込む形になります。

from agents.extensions.sandbox.openshell import (
    OpenShellSandboxClient,
    OpenShellSandboxClientOptions,
)
from agents import Runner, RunConfig
from agents.sandbox import SandboxRunConfig

result = await Runner.run(
    agent,
    "Fix the bug and run the tests.",
    run_config=RunConfig(
        sandbox=SandboxRunConfig(
            # OpenShell ゲートウェイへの接続・実行・終了処理を担う
            client=OpenShellSandboxClient(),
            options=OpenShellSandboxClientOptions(
                cluster="my-openshell-cluster",
            ),
        ),
    ),
)

これだけで、エージェントのツール実行が gRPC 経由で OpenShell に渡されます。
OpenShellSandboxClient がサンドボックスの生成・コマンド実行・ファイル I/O・セッションの終了処理を担当します。
エージェントのコードそのものはサンドボックスを意識せず書けるのが、SDK 設計の利点です。

ファイルパスのバリデーションはクライアント側でも走ります。
ワークスペースルート(デフォルトでは /workspace)の外へのアクセスは実行前に弾かれます。
サンドボックス内の OpenShell ポリシーとは独立した二重のガードになっており、クライアントの検査をすり抜けてもサンドボックス側でもう一度止まる構造です。

本番運用で最初につまずきやすいのは、ポリシーとバージョン管理の二点です。
ポリシーは deny ログを起点に必要なドメインとコマンドを少しずつ追加していく運用を守ることで、「気づいたら何でも通る設定になっていた」状態を防げます。
バージョン管理については、SDK とゲートウェイは同じリリースで揃えておくのが推奨です。
片方だけ更新すると gRPC のインターフェース不整合が静かに起きることがあり、エラーの原因が見えにくくなります。
セッション再開を使う場合は、タイムアウト設定と終了処理のハンドリングを明示的に書いておきましょう。

OpenShell を組み込むかどうかの判断軸として、「エージェントが処理する入力を自分でコントロールできるか」という問いが使えます。
閉じた社内データだけを扱うエージェントであれば、プロンプトインジェクションのリスクは低めです。
外部からのテキスト・ファイル・API レスポンスをエージェントが処理するなら、そのリスクは無視できません。
実行権限を持たせる前に、まずサンドボックスを先に作る。
この順番を守るだけでも、後悔する確率はかなり下がります。


株式会社ホコサキは、山口県宇部を拠点に Web 制作・業務システム開発・AI 活用支援を手がけています。
エージェント構成の設計やサンドボックス環境の構築でお困りの場合は、お気軽にご相談ください。
詳しくは サービス紹介ページ をご覧ください。

    NVIDIA OpenShellでAIエージェントのシェル実行を安全に閉じ込める | 株式会社ホコサキ