Legionのサンドボックス
Legionの「専門家エージェント」はOpenCodeで実現されていて、そのOpenCodeはサンドボックスの中で動いています。
「Claw系(なんとかClawの類)」は色々な危険を孕んでいると言われたため、フォロワー達はだいたいサンドボックスの中で動くようになっています。 これはLegionも同じです。 今回はこれの実装についてメモしておきます。
概要
簡単に言ってしまえば、
を使っています。
隔離環境と言うとVM(Dockerとか)を使うという話になりがちですが、今回のようなお手軽に隔離環境が作れればいいという話であれば、Bubblewrapで十分だろうと思います。
Bubblewrapとは
詳しい説明は公式サイトとか見てもらうといいので、詳しいことはそっちに譲るとして、ごく簡単なまとめをします。
- ユーザー権限で動く隔離環境
- 各種リソースが自分に許されたものしか見えない
- 敢えて副作用を保存しない限り、終了後は全てが消えてなくなる
という隔離環境を作るためのコマンドです。
Dockerとかとの大きな違いは、「コンテナイメージ」を持たないということです。 バイナリとか環境とかは、「ホスト環境のうち許されたものが見える」という設定をします。 今回のように「隔離できれば十分」な用途であれば、DockerやPodmanを使うよりも簡単にできます。
コード
今はこんな感じで使っています。
import { spawn } from 'node:child_process';
import { readFileSync } from 'node:fs';
import { parse } from 'dotenv';
const persona = process.argv[2];
if (!persona) {
console.error('persona name required');
process.exit(1);
}
const dotEnv = parse(readFileSync('.env'));
const allowedPrefixes = [
'OPENAI_',
'GOOGLE_',
'GEMINI_',
'ANTHROPIC_',
'LEGION_',
'OPENCODE_',
'NODE_',
'WORKSPACE',
'CONFIG_DIR'
];
const pickEnv = (source) =>
Object.fromEntries(
Object.entries(source).filter(([key]) =>
allowedPrefixes.some((prefix) => key.startsWith(prefix))
)
);
const daemonEnv = {
PATH: '/usr/bin:/bin',
HOME: '/',
NODE_ENV: dotEnv.NODE_ENV ?? process.env.NODE_ENV ?? 'production',
LEGION_CONFIG_DIR: '/config',
LEGION_ROLES_DIR: '/roles',
LEGION_WORKERS_DIR: '/workers',
LEGION_WORKSPACE_DIR: '/workspace',
LEGION_LOG_DIR: '/logs',
...pickEnv(dotEnv),
};
const args = [
'--die-with-parent',
'--share-net',
'--unshare-pid',
'--unshare-ipc',
'--ro-bind', '/usr', '/usr',
'--ro-bind', '/bin', '/bin',
'--ro-bind', '/lib', '/lib',
'--dir', '/etc',
'--ro-bind', '/etc/resolv.conf', '/etc/resolv.conf',
'--ro-bind-try', '/etc/hosts', '/etc/hosts',
'--ro-bind-try', '/etc/nsswitch.conf', '/etc/nsswitch.conf',
'--ro-bind-try', '/etc/gai.conf', '/etc/gai.conf',
'--ro-bind-try', '/etc/services', '/etc/services',
'--ro-bind', '/etc/ssl', '/etc/ssl',
'--ro-bind', './src', '/src',
'--ro-bind', './node_modules', '/node_modules',
'--ro-bind', './config', '/config',
'--ro-bind', './roles', '/roles',
'--bind', './workers', '/workers',
'--bind', './workspace', '/workspace',
'--bind', './logs', '/logs',
'--proc', '/proc',
'--dev', '/dev',
'--tmpfs', '/tmp',
'--chdir', '/',
'/usr/bin/node',
'/src/cli/index.js',
'--debug',
'llm',
'--watch',
'--persona',
persona,
];
const child = spawn('bwrap', args, {
stdio: 'inherit',
env: daemonEnv,
});
child.on('exit', (code, signal) => {
if (signal) {
process.kill(process.pid, signal);
return;
}
process.exit(code ?? 1);
});
この中の
'/usr/bin/node',
'/src/cli/index.js',
'--debug',
'llm',
'--watch',
'--persona',
persona,
が実際のコマンドの起動のためのもので、その前あたりに書かれているのがBubblewrapの設定です。
OpenCodeを使った「専門家エージェント」は、/src/cli/index.jsから起動しています。
この例では、「起動させたOpenCodeに.envを見せない」ために、起動コマンドで.envを読んで環境変数として渡すようにしています。
もちろんこの隔離環境からは.envは見えません。
これは.envの中に書かれがちのクレデンシャルの類をハーネスに見せないようにする工夫です。
--bindは更新可能な状態でファイルやディレクトリを見せるために、--ro-bindは読取専用で見せるためです。
ここを上手く運用することで、「汚して欲しくないもの」と「結果を書いて欲しいもの」を区別することができるわけです。
結果
たったこれだけのことですが、ハーネスという「じゃじゃ馬」が隔離環境の中に押し込めるというのは、セキュリティ的には安心です。 もちろん動作環境は見せなければなりませんが、OpenCodeにありがちの「あるものは見れるしいじれる」みたいなお行儀の悪い子には、最低でもこれくらいはしておく必要があります。
Bubblewrapはいわゆるコンテナとは違いますから、起動/停止のオーバーヘッドはほぼありません。 また、終了時にはプロセスの後片付けまでしてくれるので、親(この場合はdaemon)が死んだ時に「起動したOpenCodeがポートを握ったままゾンビ化する」みたいなことがなくなります。
PS.
本当に蛇足でしかないんですが、shell版も置いておきます。
ただし、これは環境変数や.envの問題への対応はしていません。
最後の2行は「本来のコマンド」と「shell」を切り替えて試すためです。 何が嬉しいかと言えば、shellを起動するとそこでshellになりますから、色々コマンドを入れて試してみることができるわけです。
#!/usr/bin/env bash
set -euo pipefail
PERSONA="${1:?persona name required}"
BASE="./workers/opencode"
WORKSPACE="${BASE}/workspace"
CONFIG="${BASE}/config"
# mkdir -p "${WORKSPACE}"
exec bwrap \
--die-with-parent \
--share-net \
--unshare-pid \
--unshare-ipc \
--ro-bind /usr /usr \
--ro-bind /bin /bin \
--ro-bind /lib /lib \
--dir /etc \
--ro-bind /etc/resolv.conf /etc/resolv.conf \
--ro-bind-try /etc/hosts /etc/hosts \
--ro-bind-try /etc/nsswitch.conf /etc/nsswitch.conf \
--ro-bind-try /etc/gai.conf /etc/gai.conf \
--ro-bind-try /etc/services /etc/services \
--ro-bind /etc/ssl /etc/ssl \
--ro-bind ./src /src \
--ro-bind ./node_modules /node_modules \
--ro-bind ./.env /.env \
--ro-bind ./config /config \
--ro-bind ./roles /roles \
--bind ./workers /workers \
--bind ./workspace /workspace \
--bind ./logs /logs \
--proc /proc \
--dev /dev \
--tmpfs /tmp \
--chdir / \
/usr/bin/node src/cli/index.js --debug llm --watch --persona ${PERSONA}
#/bin/bash
PS2.
ChatGPTに書かせたドキュメントを置いておきます。