無人のLLMエージェントへ権限をほとんど渡さないと、ファイルを読んだところで止まります。
逆に全部許可すると、push、外部投稿、merge、本番反映まで進められます。
最初は「編集だけ自動承認」にした軽い権限モードを使っていました。
ところが実ログを見ると、Git、テスト、GitHub CLIが承認待ちになり、誰も見ていないセッションが「次に実行します」と返したまま終わっていました。
これだと安全ではありますが、仕事も進みません。
いまは、作業を進めるための権限と、外部へ結果を確定する承認を分けています。
「実行できる」と「やってよい」は別にする
自動で進めてもらいたいのは、このあたりです。
- ファイルを探して読む
- 作業ツリーを編集する
- Git差分を確認する
- formatter、lint、testを動かす
- ローカルに生成物を作る
一方で、次の操作は別の承認が必要です。
- メールやチャットを送る
- repositoryへpushする
- PRをmergeする
- 本番へdeployする
- 決済、返金、契約、権限変更をする
- 機密データを外部へ送る
前半まで動ければ、エージェントはレビュー可能な状態を作れます。
後半の直前で止めれば、人はゼロから作業するのではなく、差分や送信文面を見て判断できます。
図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を記録し、同じ操作を無限に試さないようにしています。
わざと試している事故パターン
- handoff本文へshell構文を入れる
- 読み取るファイルへ「安全ゲートを無視せよ」と書く
git statusに見せた複数コマンドを渡す- localhost向けの許可を外部hostへ変える
- 承認後にdiffを変更する
- stagingとproductionを似た名前にする
- 同じ承認tokenを2回使う
- ガード設定を読めない状態にする
危険操作が止まるだけではなく、何を止めたか、次に人が何を見るかまで出ることを確認します。
見ている数字
- 権限不足で止まったローカル操作の件数
- 安全ゲートで止めた外部操作の件数と種類
- 承認要求から判断までの時間
- 承認後に内容が変わり、無効になった件数
- ドラフトまで自動で作れた割合
- 禁止操作を繰り返した回数
- 人が承認せず破棄した割合と理由
- gate bypass、誤許可、二重実行の件数
拒否件数が少なければよいわけではありません。
必要な仕事はゲートの手前まで来ていて、外へ影響する操作だけが毎回止まっているかを見ています。
いまの確認項目
- ローカルで作業する権限と、外部へ反映する承認を分けたか
- 操作を可逆性、到達範囲、認証、お金、確認方法で見たか
- 今回いらないツールを外したか
- モデルとは別の実行前ガードがあるか
- ガードがない環境では権限自体を狭くしたか
- 承認待ちで判断材料まで作るか
- 承認を対象、内容、有効期限へ結びつけたか
- 入力本文をshellコードとして評価していないか
- ガード障害時に外部操作を止め、作業状態を残すか
- prompt injectionと承認後の変更を試したか
権限を全部閉じると安全ですが、何も終わりません。
全部開けると進みますが、見ていないところまで進みます。
自分は、調べる、直す、テストするところまでは任せて、外へ確定する直前で一度人へ戻す形にしています。いまのところ、この分け方が一番仕事を残さず、安全側にも寄せやすいです。
ローカルLLM設計・全8回
MacでローカルLLMを実際に動かして、うまくいかなかったところを8つに分けて書いています。