ここまでの4本で、Omnigentの構成を理解し、Pollyでコードの並列実装とクロスレビューを、Debbyで多モデルの議論と統合を動かしてきました。合成、つまり異種のエージェントを混ぜて相互作用させることの価値は、だいぶ腹落ちしてきたところです。
ただ、複数のエージェントを並列で走らせ、しかもクラウドや無人で動かすようになると、次の心配が出てきます。勝手に危ないコマンドを打たないか、想定外にコストを使わないか、触ってほしくないファイルを触らないか。今回はDatabricksが掲げる3つの価値のうち「制御 (Control)」を扱います。概念の整理から始めて、実際にガードレールを設定し、エージェントの手が止まる瞬間まで見届けます。
なぜプロンプトでのお願いでは足りないのか
制御の話は、ここから始めるのが分かりやすいです。エージェントに「危ないコマンドを実行する前に確認してね」と指示すること自体は、誰でもやっています。ではなぜ、わざわざ仕組みとしての制御が必要なのでしょうか。
プロンプトでの指示は、あくまでモデルに渡すテキストの一部です。守るかどうかはモデルの判断に委ねられます。会話が長くなれば指示は薄れますし、解釈も揺れます。そして何より、実行そのものは止まりません。モデルが「これは確認不要だ」と判断すれば、そのままツールが動きます。
ガードレールは、この判断をモデルの外側に出します。設定として宣言され、メタハーネス層 (前回のアーキテクチャ回で見たサーバーやランナー) がツール呼び出しの直前に評価します。モデルが何を考えていようと、許可されなければツールは動きません。
この違いが、Omnigentが「プロンプトではなくメカニズム層で強制する」と主張している部分です。
ガードレールとポリシーの関係
用語を整理しておきます。ここが分かると、あとの設定作業が迷いなく進みます。
ガードレールは、エージェント設定 (config.yaml) のトップレベルにある guardrails: というブロックです。個々のルールを束ねる入れ物だと思ってください。
ポリシーは、そのガードレールの中に並ぶ個々のルールです。Pollyの設定を開くと、破壊的な操作を止める blast_radius、1ターンあたりの並列起動数を制限する spawn_bounds などが並んでいます。
そしてポリシーエンジンが、エージェントがツールを呼ぶ直前にこれらを評価し、3つのうちどれかを返します。ALLOW (そのまま実行)、ASK (人間の承認待ちで停止)、DENY (実行させない) です。
つまり今回やろうとしているのは、guardrails: にポリシーを1つ足して、ASKを実際に発火させることです。
使えるポリシーを調べる
どんなポリシーが用意されているかは、手元のソースを見るのが確実です。組み込みポリシーは omnigent/policies/builtins/ にモジュールとして並んでいます。
cd ~/src/omnigent
sed -n '1,50p' omnigent/policies/builtins/__init__.py
ここを読むと、設計が分かります。各モジュールは POLICY_REGISTRY というリストを公開していて、そこにハンドラのパス、説明、パラメータのスキーマが書かれています。サーバーは起動時にこれを discover して、APIで一覧を提供します。つまりこれが「使えるポリシーのカタログ」です。
スキャン対象のモジュールは、safety、cost、google、github、working_dir、risk_score、routing、cel、prompt、そして nessie の10個でした。
コスト上限を狙って空振りした話
最初に試したかったのは、コスト上限のポリシー (cost.cost_budget) でした。「これ以上使ったら止める」は制御として一番わかりやすく、実務でも需要があります。
ソースを読むと、仕様は明快でした。ハード上限 (max_cost_usd) に達したら高コストモデルでのツール呼び出しをDENYし、ソフトなチェックポイント (ask_thresholds_usd) を超えるたびにASKを出す。承認は session_state に記録されるので、同じチェックポイントで二度は聞かれない。ステートフルなポリシーの好例です。
ところが、ここに落とし穴がありました。このポリシーは累積コスト (total_cost_usd) を見て判定するのですが、この値はモデルのトークン単価から計算されます。そして今回の構成、つまりClaudeとCodexのサブスクリプション経由での実行では、単価ベースの課金情報が来ません。値は0.0のままです。ポリシー側は「コストが未計上のときは常にALLOW」と実装されているので、いくら回しても発火しません。
念のため実測でも確認しました。前回のスモークで作られた mlflow.db を調べたところ、コストやトークンを含むレコードは1件もありませんでした。
cd ~/work/polly-smoke
sqlite3 mlflow.db "select name, substr(content,1,300) from spans where content like '%cost%' or content like '%usd%' or content like '%token%' limit 15;"
結果は空。ソースの記述と実測の両方で、この構成ではコスト系ポリシーを試せないことが確定しました。
そこで、コスト計上に依存しないポリシーに切り替えます。カタログを見直すと、safety.ask_on_os_tools が目に留まりました。ファイルやシェルのツール呼び出しの前に、必ずユーザー承認を求めるポリシーです。Omnigentの sys_os_* ツールに加え、Claude Codeのネイティブツール (Bash, Read, Write, Edit, Glob, Grep)、Codexのネイティブツールまで広くカバーします。
しかも kind が callable で params_schema が None、つまり引数を取りません。パスを1行書くだけで有効になります。Pollyは必ずシェルとファイルを触るので、確実に発火します。
ガードレールに1行足す
Pollyの設定 (~/agents/polly-dbx/config.yaml) の guardrails.policies に、ポリシーを1つ追加します。既存のものはそのまま残します。
guardrails:
ask_timeout: 86400
policies:
blast_radius:
# (既存のまま)
spawn_bounds:
# (既存のまま)
require_approval:
type: function
function: omnigent.policies.builtins.safety.ask_on_os_tools
ここで書き方に注意点があります。既存の blast_radius などは引数が必要なので、function: の下に path: と arguments: を持つ形式です。一方 ask_on_os_tools は引数を取らないので、function: に直接ドット付きのパスを書きます。ポリシーの実装形式 (factory か callable か) によって、YAMLの書き方が変わります。
ask_timeout: 86400 はPollyの設定に元からあるもので、承認待ちが1日もつことを意味します。席を外しても大丈夫、ということです。
実際に止まる
設定したら、スモーク用リポジトリでPollyを起動します。
cd ~/work/polly-smoke
omni run ~/agents/polly-dbx/
承認プロンプトの数を抑えるため、タスクは極小にします。
calc.py に square 関数(a * a)を1つだけ追加してください。
テストも1つ付けてください。
送信すると、すぐに止まりました。
「Approval required · require_approval (tool_call)」と表示されています。require_approval は、設定で付けたポリシー名そのものです。止まったのは、Pollyが最初にワーカーの有無を確認しようとした sys_os_shell('command -v claude codex pi || true') でした。
実行しようとしたコマンドがJSONで丸ごと提示され、Approve と Reject のボタンが出ています。入力欄は「Respond to the pending request above to continue」に変わり、承認するまで先へ進めません。セッション一覧にも「Needs response」のバッジが付きます。
プロンプトでお願いしたのではなく、ツール呼び出しそのものが物理的に停止している。これが制御の実像です。
毎回聞かれる
承認すると「Approved · require_approval」と履歴が残り、エージェントは次に進みます。そして次のシェル呼び出しで、また止まります。
ask_on_os_tools は引数も状態も持たないシンプルなポリシーなので、承認を記憶しません。同じ種類の操作でも、呼び出しのたびにASKを出します。先ほどのコスト上限が session_state に承認を記録して「同じチェックポイントは一度しか聞かない」のとは対照的です。ポリシーによって、ステートフルなものとそうでないものがある、ということです。
承認を重ねていくと、履歴が壮観なことになります。
square関数を1つ足すだけのタスクなのに、承認は10回を超えました。中身も多彩です。git status、python3 のバージョン確認、pytest の確認、gh auth status、calc.py の Edit、test_calc.py の Write、pip install、venv の作成、pytest の実行、.polly の探索。
ここで副次的な発見が2つあります。
1つは、エージェントが裏で何をしているかが全部見えることです。普段は結果だけ見ているので意識しませんが、「関数を1つ足す」の裏でこれだけの操作が走っている。制御をかけると、その全量が可視化されます。
もう1つが重要です。承認リストに Bash(...)、Edit(...)、Write(...) が並んでいますが、これらはClaude Codeのネイティブツール名です。つまり親であるPollyの sys_os_shell だけでなく、サブエージェントである claude_code のツール呼び出しにも、同じポリシーが効いています。並列で走る複数のエージェント全部に、共通の制御を横断でかけられる、ということです。ここが合成と制御のつながる点です。
拒否するとどうなるか
ASKには承認と拒否の両方があります。片方だけ見て終わるのはもったいないので、Rejectも試しました。.polly/registry.json の読み込みを拒否してみます。
結果は、セッションは止まりませんでした。エージェントは registry の読み込みを諦め、次の作業 (ブランチを作ってファイルをステージし、diffを確認する) に進みました。
つまりRejectは「そのツール呼び出しだけを拒否する」ものです。エージェントは拒否を受け取り、別の手段を探すか、それ無しで進もうとします。セッション全体を殺す非常停止ボタンではありません。
このときの承認プロンプトには、description として「Create branch and stage only the two intended files」と、エージェント自身の意図が添えられていました。Claude CodeのネイティブBashツールは説明付きで呼ばれるので、承認する人間が判断しやすくなっています。
全部に承認を求めるのは現実的か
ここまでで、制御が確かに効くことは体感できました。同時に、実運用上の重要なことも見えてきます。
関数を1つ足すだけのタスクで承認が10回以上。Pollyのようなオーケストレーターは、ワーカー確認、ファイル探索、worktree作成、テスト実行と、シェルを何十回も叩きます。全部に承認を求めると、人間がボタンを押し続けることになり、「エージェントに任せて放置する」という利点が消えます。制御の強さと自動化の利便性は、まっすぐトレードオフの関係にあります。
この視点でPollyの既定設定を読み返すと、設計思想が腹落ちします。blast_radius には gate_pushes: false というオプションが指定されていて、コメントにはこう書かれていました。オーケストレーターは無人で動くので、push や merge や deploy ではASKしない。ただし壊滅的なDENYセット (force-push、rm -rf /、リモート参照へのhard-reset) は依然として適用する。
つまり実務での現実解は「全部に承認を求める」ではなく、「本当に壊滅的なものだけ問答無用でDENY、それ以外は通す」なのです。今回試した ask_on_os_tools は、制御の仕組みを理解するには最適ですが、常用するものではありません。むしろ、慣れないエージェントを初めて動かすときや、重要なリポジトリで一時的に様子を見たいときの道具、という位置づけが合っています。
なお、試したあとは設定からポリシーを外しておくのを忘れずに。付けたままだと、次のセッションでも毎回承認を求められます。
まとめ
制御を概念から実物まで追いかけました。ガードレールはポリシーを束ねる設定ブロックで、個々のポリシーがツール呼び出しの直前に評価され、ALLOW / ASK / DENY を返します。プロンプトでのお願いと違い、モデルの外側で強制されるので、会話が長くなっても薄れず、モデルの判断で無視されることもありません。
実際に動かして分かったことも多くありました。ポリシーは設定した名前で発火し、実行内容がJSONで提示されること。状態を持つポリシー (コスト上限) と持たないポリシー (今回の承認) があること。親だけでなくサブエージェントのツール呼び出しにも横断で効くこと。Rejectはそのツール呼び出しだけを止め、エージェントは別の手段で進むこと。そして、全ツールに承認を求める設定は無人運用と両立しないこと。
そして今回一番の収穫は、コスト上限を狙って空振りしたことかもしれません。サブスクリプション経由の実行ではコストが計上されず、コスト系ポリシーが発火しない。ドキュメントを読むだけでは気づけず、ソースと実測の両方で確かめて初めて分かりました。制御を導入するときは、そのポリシーが依存している値が自分の構成で本当に取れているか、先に確かめるのが確実です。
参考リンク
- omnigent-ai/omnigent (GitHub)
- Omnigent公式ドキュメント
- 第1弾: Omnigent概要
- 第2弾: Pollyスモーク
- 第3弾: アーキテクチャ
- 第4弾: Debby / 合成







