ループは変わらない、ツールが増えるだけ
「ループは一行も変えていない。TOOLS 配列に追加しただけだ。」——s02_tool_use.py より。
ツールを追加するとき何を変えるのか
S01 の agent は bash しか使えない。read_file / write_file / edit_file も使えるようにするには、どう変えればいいか?
多くの人の最初の反応は「ループを変える」だ——違う。ループは一行も変えなくていい。必要なのは3つだけ:
- Python のハンドラ関数を書く(
run_read(path, limit))。 - それを
TOOL_HANDLERSマッピングテーブルに登録する("read_file": lambda **kw: run_read(...))。 TOOLS配列に JSON schema の宣言を追加する(このツールの名前と受け付けるパラメータをモデルに伝える)。
ループが tool_use ブロックを受け取ると、block.name を使って dispatch map から関数を引き、実行し、出力を tool_result に詰めて返す。bash と全く同じ経路だ。
# Dispatch map: name → handler lambda TOOL_HANDLERS = { "bash": lambda **kw: run_bash(kw["command"]), "read_file": lambda **kw: run_read(kw["path"], kw.get("limit")), "write_file": lambda **kw: run_write(kw["path"], kw["content"]), "edit_file": lambda **kw: run_edit(kw["path"], kw["old_text"], kw["new_text"]), }
dispatch がどうルーティングするか
以下の widget でモデルが送ってくる可能性のある tool_use リクエストをクリックし、それが具体的な Python 関数へどうルーティングされるかを確認しよう。注目点:block.name がどの経路を通るかを決める。
safe_path:省けない防衛ライン
agent にファイルアクセス権限を与えるとき、セキュリティ上の最大リスクはパス逃逸だ:モデルは /home/user/project/ 内で作業すべきなのに、read_file("../../etc/passwd") を送ってくるケースだ。
s02 にはこれを防ぐ小さな関数がある:
def safe_path(p: str) -> Path: path = (WORKDIR / p).resolve() # 正規化、.. とシンボリックリンクを解決 if not path.is_relative_to(WORKDIR): raise ValueError(f"Path escapes workspace: {p}") return path
肝は .resolve() と .is_relative_to() の2ステップだ——絶対パスに変換してからサンドボックス内にあるかチェックする。前者を省くと foo/../../etc のようなパスが通り抜ける。後者を省くとチェック自体が行われない。
パスの安全性を判定する
以下の5つのパスはすべてモデルが送ってくる可能性のある read_file の引数だ。safe_path が通過を許可するもの・拒否するものはどれか。WORKDIR = /home/user/project を前提とする。
危険なツールを agent に追加しない
ツールを追加するのは簡単だが忘れてはならない:追加するツールはすべてモデルの能力境界を広げる。s02 では bash にブラックリスト(rm -rf /、sudo、shutdown)があり、write_file には safe_path 制約がある。本番にツールを追加する前に3つ問いかけよう:
- このツールでモデルが取り消せない操作をできるか?(rm、メール送信、git push)
- 何を漏洩させうるか?(環境変数、.ssh、Cookie)
- エラー出力が命令として回り込む可能性はあるか?(ツール出力経由の prompt injection)
s07 のレッスンでこれらの判断をコードから外に出して宣言的に管理する「権限レイヤー」を追加する。