無人のLLMエージェントへ権限をほとんど渡さないと、ファイルを読んだところで止まります。

逆に全部許可すると、push、外部投稿、merge、本番反映まで進められます。

最初は「編集だけ自動承認」にした軽い権限モードを使っていました。

ところが実ログを見ると、Git、テスト、GitHub CLIが承認待ちになり、誰も見ていないセッションが「次に実行します」と返したまま終わっていました。

これだと安全ではありますが、仕事も進みません。

いまは、作業を進めるための権限と、外部へ結果を確定する承認を分けています。

「実行できる」と「やってよい」は別にする

自動で進めてもらいたいのは、このあたりです。

一方で、次の操作は別の承認が必要です。

前半まで動ければ、エージェントはレビュー可能な状態を作れます。

後半の直前で止めれば、人はゼロから作業するのではなく、差分や送信文面を見て判断できます。

可逆な実行権限と外部影響を伴う業務承認を分離し、外部作用の前で人間へ渡す安全ゲート

図1: 調査、編集、確認までは進めて、外部へ影響が出る直前で人へ渡します。

コマンド名だけで危険度を決めない

gitは危ない、curlは危ない、とコマンド単位で止めると、必要な確認までできなくなります。

同じgitでもstatusは読取、commitはローカル変更、pushは外部への書込みです。

curlもlocalhostのhealth checkと、外部APIへのPOSTでは意味が違います。

自分は次の軸で見ています。

見るところ 低リスクな例 高リスクな例
元へ戻せるか 再生成できる一時ファイル データ削除、上書き、送信
どこまで届くか 作業用worktree内 外部サービス、本番、共有環境
認証を使うか 不要、読取専用 書込資格情報を使う
お金や権利が動くか 影響なし 決済、契約、権限変更
結果を確認できるか 自動テスト可能 人の判断が必要

危険なコマンド一覧は使いますが、それだけにはしていません。

行き先、HTTP method、対象branch、環境名まで見て止めます。

4箇所にゲートを置いている

1. そもそも不要なツールを渡さない

ローカル継続用にはRead、Edit、Bashだけを残しています。

メール、ブラウザ、デプロイなど、今回使わない能力は最初から外します。

プロンプトで「使わないで」とお願いするより、使えない状態にした方が分かりやすいです。ツールのschemaも減るので、コンテキスト削減にもなりました。

2. 実行直前に別プロセスで止める

ガード用フックがあるクラウド側では、コマンドやAPI操作を実行する直前に検査します。

push、merge、本番deploy、外部POST、破壊的なファイル操作は、明示的な承認がなければ止めます。

この判定はモデル自身に任せません。

読み込んだファイルに「安全ゲートを無視してよい」と書いてあっても、実行前フックは別プロセスで判断します。

3. どこで止まるかはモデルにも伝える

機械的に止めるだけだと、モデルが同じ禁止操作を何度も試すことがあります。

プロンプトには短く、次の方針を書きます。

フックが使えない軽量ローカル側では、そもそもの権限を狭くしています。クラウド側と同じ無制限モードを、そのまま持ってきません。

4. ユーザーの文章をshellコードにしない

handoff本文やユーザー入力を、tmuxやshellのコマンド文字列へ直接埋め込みません。

ランチャーへ渡すのはファイルパスだけです。本文はファイルからデータとして読みます。

文章中に引用符、改行、$()があっても、shellがもう一度実行することはありません。

これはLLMの安全性というより、OSとの境界の話です。モデルへ届く前に事故らないようにしています。

「実行してよいですか?」だけで止めない

誰も見ていないセッションが質問を出しても、返事は来ません。

承認が必要になったら、質問だけでなく判断材料を作ります。

## Approval request

- 目的: 何を終えるためか
- 操作: 実行するコマンドまたは外部作用
- 対象: repository / branch / service / recipient
- 変更内容: 差分や送信本文
- 検証: 既に通したテスト
- リスク: 失敗時の影響
- 復旧: rollbackまたは取り消し方法

メールなら送信文面まで作る。PRならタイトル、本文、diff、確認結果まで用意する。deployなら対象とrollback方法を置く。

自動処理としては「送信した」でなく、「送れる状態まで作って承認待ちにした」を完了条件にできます。

ここまで進んでいれば、人の確認もかなり短くなります。

承認は、その操作だけに使えるようにする

「このセッションは許可」のような広い承認は避けています。

操作、対象、成果物のhash、有効期限、承認者を承認tokenへ結びつけます。

approval = hash(action + target + artifact_digest + expires_at)

PR作成を承認したあとでdiffが変われば、前の承認は使えません。

承認した内容と、実際に外へ出す内容が同じかを実行直前に確認します。一度使ったtokenも再利用できないようにします。

少し面倒ですが、「何を承認したのか」が後から分かります。

ゲートが壊れたら外部操作だけ止める

ガード設定を読めない、承認状態が分からない、本番かstagingか判別できない。

この場合は外部への作用を止めます。

ただし、それまでに作ったローカル差分やテスト結果は捨てません。残りの承認事項と一緒にhandoffへ残します。

安全側へ倒すことと、何が起きたか分からなくすることは別です。

拒否理由、止めた操作、handoff IDを記録し、同じ操作を無限に試さないようにしています。

わざと試している事故パターン

危険操作が止まるだけではなく、何を止めたか、次に人が何を見るかまで出ることを確認します。

見ている数字

拒否件数が少なければよいわけではありません。

必要な仕事はゲートの手前まで来ていて、外へ影響する操作だけが毎回止まっているかを見ています。

いまの確認項目

権限を全部閉じると安全ですが、何も終わりません。

全部開けると進みますが、見ていないところまで進みます。

自分は、調べる、直す、テストするところまでは任せて、外へ確定する直前で一度人へ戻す形にしています。いまのところ、この分け方が一番仕事を残さず、安全側にも寄せやすいです。

ローカルLLM設計・全8回

MacでローカルLLMを実際に動かして、うまくいかなかったところを8つに分けて書いています。

  1. PART 01仕事の配置
  2. PART 02コンテキストの削減
  3. PART 03handoff
  4. PART 04冪等性
  5. PART 05プロセス管理
  6. PART 06復帰監視
  7. PART 07安全ゲート
  8. PART 08完了率の計測