0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

エージェントに「お願い」ではなく「強制」する ― Omnigentのガードレール入門

0
Posted at

ここまでの4本で、Omnigentの構成を理解し、Pollyでコードの並列実装とクロスレビューを、Debbyで多モデルの議論と統合を動かしてきました。合成、つまり異種のエージェントを混ぜて相互作用させることの価値は、だいぶ腹落ちしてきたところです。

ただ、複数のエージェントを並列で走らせ、しかもクラウドや無人で動かすようになると、次の心配が出てきます。勝手に危ないコマンドを打たないか、想定外にコストを使わないか、触ってほしくないファイルを触らないか。今回はDatabricksが掲げる3つの価値のうち「制御 (Control)」を扱います。概念の整理から始めて、実際にガードレールを設定し、エージェントの手が止まる瞬間まで見届けます。

なぜプロンプトでのお願いでは足りないのか

制御の話は、ここから始めるのが分かりやすいです。エージェントに「危ないコマンドを実行する前に確認してね」と指示すること自体は、誰でもやっています。ではなぜ、わざわざ仕組みとしての制御が必要なのでしょうか。

プロンプトでの指示は、あくまでモデルに渡すテキストの一部です。守るかどうかはモデルの判断に委ねられます。会話が長くなれば指示は薄れますし、解釈も揺れます。そして何より、実行そのものは止まりません。モデルが「これは確認不要だ」と判断すれば、そのままツールが動きます。

ガードレールは、この判断をモデルの外側に出します。設定として宣言され、メタハーネス層 (前回のアーキテクチャ回で見たサーバーやランナー) がツール呼び出しの直前に評価します。モデルが何を考えていようと、許可されなければツールは動きません。

omnigent-p-fig1-prompt-vs-guardrail.png

この違いが、Omnigentが「プロンプトではなくメカニズム層で強制する」と主張している部分です。

ガードレールとポリシーの関係

用語を整理しておきます。ここが分かると、あとの設定作業が迷いなく進みます。

ガードレールは、エージェント設定 (config.yaml) のトップレベルにある guardrails: というブロックです。個々のルールを束ねる入れ物だと思ってください。

ポリシーは、そのガードレールの中に並ぶ個々のルールです。Pollyの設定を開くと、破壊的な操作を止める blast_radius、1ターンあたりの並列起動数を制限する spawn_bounds などが並んでいます。

そしてポリシーエンジンが、エージェントがツールを呼ぶ直前にこれらを評価し、3つのうちどれかを返します。ALLOW (そのまま実行)、ASK (人間の承認待ちで停止)、DENY (実行させない) です。

omnigent-p-fig2-guardrail-policy.png

つまり今回やろうとしているのは、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;"

結果は空。ソースの記述と実測の両方で、この構成ではコスト系ポリシーを試せないことが確定しました。

omnigent-p-fig3-cost-to-approval.png

そこで、コスト計上に依存しないポリシーに切り替えます。カタログを見直すと、safety.ask_on_os_tools が目に留まりました。ファイルやシェルのツール呼び出しの前に、必ずユーザー承認を求めるポリシーです。Omnigentの sys_os_* ツールに加え、Claude Codeのネイティブツール (Bash, Read, Write, Edit, Glob, Grep)、Codexのネイティブツールまで広くカバーします。

しかも kindcallableparams_schemaNone、つまり引数を取りません。パスを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つ付けてください。

送信すると、すぐに止まりました。

Screenshot 2026-08-03 at 15.38.13.png

「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」と履歴が残り、エージェントは次に進みます。そして次のシェル呼び出しで、また止まります。

Screenshot 2026-08-03 at 15.39.03.png

ask_on_os_tools は引数も状態も持たないシンプルなポリシーなので、承認を記憶しません。同じ種類の操作でも、呼び出しのたびにASKを出します。先ほどのコスト上限が session_state に承認を記録して「同じチェックポイントは一度しか聞かない」のとは対照的です。ポリシーによって、ステートフルなものとそうでないものがある、ということです。

承認を重ねていくと、履歴が壮観なことになります。

Screenshot 2026-08-03 at 15.45.09.png

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 の読み込みを拒否してみます。

Screenshot 2026-08-03 at 15.45.58.png

結果は、セッションは止まりませんでした。エージェントは registry の読み込みを諦め、次の作業 (ブランチを作ってファイルをステージし、diffを確認する) に進みました。

つまりRejectは「そのツール呼び出しだけを拒否する」ものです。エージェントは拒否を受け取り、別の手段を探すか、それ無しで進もうとします。セッション全体を殺す非常停止ボタンではありません。

このときの承認プロンプトには、description として「Create branch and stage only the two intended files」と、エージェント自身の意図が添えられていました。Claude CodeのネイティブBashツールは説明付きで呼ばれるので、承認する人間が判断しやすくなっています。

omnigent-p-fig4-approval-flow.png

全部に承認を求めるのは現実的か

ここまでで、制御が確かに効くことは体感できました。同時に、実運用上の重要なことも見えてきます。

関数を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はそのツール呼び出しだけを止め、エージェントは別の手段で進むこと。そして、全ツールに承認を求める設定は無人運用と両立しないこと。

そして今回一番の収穫は、コスト上限を狙って空振りしたことかもしれません。サブスクリプション経由の実行ではコストが計上されず、コスト系ポリシーが発火しない。ドキュメントを読むだけでは気づけず、ソースと実測の両方で確かめて初めて分かりました。制御を導入するときは、そのポリシーが依存している値が自分の構成で本当に取れているか、先に確かめるのが確実です。

参考リンク

はじめてのDatabricks

はじめてのDatabricks

Databricks無料トライアル

Databricks無料トライアル

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?