[English](./README.md) | 日本語

# pi-ai-approval

コーディングエージェント [Pi](https://pi.dev) 向けの、フェイルクローズ（fail-closed：問題発生時は安全側に倒して拒否する）設計の承認ゲートです。独立した AI レビュワーが対象のツール呼び出しを6段階のリスクレベルに分類し、ローカルの `riskActions` 設定に基づいて実行可否（`allow`: 確認なしで実行、`ask`: 確認プロンプトを表示、`deny`: 拒否）を決定します。

AI が最終的な実行可否を決定することはありません。AI が行うのはリスク評価と操作内容の説明のみであり、実行の可否は必ずローカル設定によって決まります。また、レビュワーが分類できなかった操作はすべてブロックされます。

## 動作の仕組み

```text
ツール呼び出し
   ↓
AI レビュー（独立したレビュワーセッション）
   ↓
リスクレベル + instruction alignment + 操作の要約 + 判定理由
   ↓
riskActions 設定
   ↓
allow ─────────→ 実行
ask  → Deny / Approve / Approve + Add Rule
       Approve: この呼び出しのみ実行
       Approve + Add Rule: 許可し、セッションルールを追加
       Deny: ブロック
deny ─────────→ ブロック
```

- **`allow`**: ユーザー確認なしでツール呼び出しを実行します。
- **`ask`**: 承認プロンプトを表示します。選択肢は **Deny / Approve / Approve + Add Rule（デフォルト選択は Deny）** に固定されています。`askTimeoutSeconds` でプロンプトを開いておく秒数を制限できます。デフォルトの `null` は回答があるまで待ちます。制限があるあいだは残り秒数がカウントダウンされ、0 になると操作は拒否されます。
- **`deny`**: ツール呼び出しをブロックし、AI による判定理由とともに「回避策などで同じ操作を再試行しないこと」という指示をエージェントへ返します。

レビュワーは利用可能な場合、現在のツール呼び出しに直接関連するエージェントの思考プロセス（reasoning）も補助的な判断材料として参照します。ただし reasoning は信頼できない情報として扱われ、ユーザーがその操作を許可または指示したことの証明にはなりません。モデルやプロバイダーが reasoning を公開していない場合、レビューは会話履歴と実行予定の操作内容のみを用いて行われます。

Approve（`Approve + Add Rule` を含む）が適用されるのは**その1件のツール呼び出しのみ**です。後続の呼び出しは個別にレビューされます。セッションルールはレビュワーが判定した分類を1段階引き下げることのみが可能であり、ルール単体で操作を許可することはありません。複数の `ask` が同時に発生した場合はプロンプトが順次（直列化して）処理され、常に1件ずつ表示されます。

## リスクレベル

リスクは、**現在のユーザータスクの文脈**においてその操作を実行した際の実質的なリスクとして評価されます。具体的には、失敗・誤用・意図しない副作用によって生じる潜在的な損害の大きさ、復旧（ロールバック）にかかるコスト、そしてユーザーの指示をどれだけ直接的に実現しているかなどを考慮します。リスクレベルに加えて、レビュワーは指示との適合度を表す `instruction_alignment`（`direct` / `implied` / `weak` / `unrelated`）も報告します。

基本原則:

- ユーザーの指示を直接実現する、影響範囲が限定的で復元が容易な通常の開発作業は、プロジェクト内のファイルを変更する場合であっても `low` リスクとして扱われます。
- ユーザーからの明示的な指示は不確実性を減らすものの、影響範囲（ブラスト半径）の広さ、不可逆性、本番環境への影響、セキュリティ上のリスクそのものを帳消しにするわけではありません。
- `very_high` および `critical` に相当する操作は、ユーザーから明示的に要求された場合でもレベルが引き下げられることはありません。

| レベル | 意味 |
| --- | --- |
| `very_low` | 状態を変更しない操作: ファイルの読み取り、`grep` / `find` / `ls`、`git status`、テスト結果や設定値の確認 |
| `low` | 指示された作業を遂行する通常・限定的・復元が容易な変更: 指示に基づくソースコードの編集、新規ファイル作成、リファクタリング、フォーマッター/リンターの実行、ローカルでのビルド/テスト、生成物の削除 |
| `medium` | 目的に沿っているが、通常のコード編集より大きな副作用や復旧コストを伴う操作: 一括変更、依存パッケージの追加・更新、ローカル DB のマイグレーション、開発サービスの再起動、軽微な git 履歴操作、プロジェクト外の設定変更、外部サービスへの書き込み |
| `high` | 重要なデータ・環境・サービスへの影響がある操作、または指示内容に対して副作用への飛躍が大きい操作: 本番環境や共有環境の変更、force push、重要設定の変更、DB のデータ更新、ファイアウォール/IAM/ネットワーク設定の変更。明示的に指示された本番作業であっても `medium`〜`high` の下限（フロア）を維持 |
| `very_high` | 明示的な依頼であっても、影響範囲・復旧コスト・不可逆性の高さから人間の再確認が必須となる操作: 本番データの一括更新・削除、大量のリソース削除、IAM の大幅な変更、保護ブランチ（protected branch）への強制更新 |
| `critical` | 指示の有無にかかわらず、通常のエージェントによる自動実行の範疇を超える操作: 機密情報の外部流出、復旧不能な大規模破壊、セキュリティ機構の恒久的な無効化、広範な権限付与 |

具体例: 報告されたバグを修正するためのファイル編集 → `low`。実装に必要な依存パッケージの追加 → `medium`。指示に基づく `/tmp` 配下の操作 → `very_low` / `low`（`rm -rf /tmp/*` は最大でも `medium`、`/tmp` 自体の削除 → `high`）。明示的に指示された通常のローカル `git commit` → `low`（`--amend` や履歴の書き換えは `medium` 以上を維持）。独断による `git reset --hard` → `high`（明示的な指示があれば `medium`）。明示的な指示があっても本番 DB のマイグレーション → `high`。本番データの一括削除 → `very_high`。指示があっても機密情報の外部送信 → `critical`。

## セッション承認ルール

3つ目の選択肢 **Approve + Add Rule** は、現在のツール呼び出しを承認した上で、その Pi セッションの以降に適用するルールの入力を求めます。ルールはメモリ上でのみ保持され、ディスクには保存されません。セッションの再読み込みや置き換えなど、セッションランタイムがリセットされると自動的に破棄されます。ルール入力時に Esc キーを押すと入力をキャンセルして選択肢一覧に戻り、Enter キーを押すとサニタイズされたテキストが保存されます。

ルールが後続の呼び出しに与える影響:

- 有効なルールはレビュワーのシステムプロンプトに追加されますが、同時に「ルールによってリスク分類自体を変更してはならない」という指示も明示的に与えられます。レビュワーは、実行予定の操作を明確かつ完全にカバーしている単一のルール ID のみを報告できます。
- ローカル側は、報告されたルール ID が現在有効なルールセットに存在しない場合、一致なし（マッチなし）として処理します。
- 報告されたルールが存在する場合、リスク分類をちょうど1段階引き下げた上でポリシーを再適用します。これは `ask` と `deny` の双方に適用されます（`allow` はすでに実行許可されているため変化しません。下限は `very_low` です）。
- ルールは判定結果を `allow` 側に緩和することしかできません。引き下げ後のレベルでも依然として `deny` に設定されている場合（またはこれ以上引き下げられない場合）は、元の判定結果とレベルが維持されるため、ルールによって判定が厳しくなることはありません。また、`critical` が自動で `allow` になることもありません（1段階下げても `very_high` であり、ポリシー上 `allow` に設定できないためです）。
- デフォルトのポリシーでは、一致するルールがあると `high` の deny が `medium` の確認プロンプトに緩和されますが、`very_high` と `critical` はブロックされたままとなります。なお、設定で `medium` を `allow` にしている場合は、ルールによって `high` の操作が自動実行されるようになります。
- ルール適用によって自動で `allow` になった場合は、適用されたルール、元のレベル、実際に適用されたレベルが情報通知として報告されます。引き下げ後のレベルが `ask` に該当する場合は、確認プロンプト内に `Session Rule:` 行が表示され、適用されたルールと引き下げ元のレベルが明示されます。

ルールには上限が設けられています。1セッションあたり最大20件まで登録可能で、各ルールは制御文字や ANSI エスケープシーケンスを除去し改行を空白に折りたたんだ後、最大500文字までとなります。承認プロンプトでの入力時、空文字や文字数超過は理由とともに拒否され、入力欄は開いたまま保持されます（警告なく自動的に切り詰められることはありません）。ルールマネージャー（`/ai-approval session-rules`）でも同様の拒否理由が通知されます。

ルールの管理は `/ai-approval session-rules` で行います。有効なルールが ID 付きで一覧表示され、対話形式で追加、編集（既存テキストが入力された状態でエディタが開きます）、削除が可能です。いずれのステップでも Esc キーを押せば、変更を加えずに終了できます。

例: 特定のコマンドが繰り返し `medium`（確認プロンプト）と判定される場合、**Approve + Add Rule** を選択して `Allow pnpm test and pnpm build in this repository for this session` と入力します。次回の呼び出し時にはレビュワーがこのルール ID を報告し、ローカルポリシーによって `medium` から `low` へと1段階引き下げられ、確認プロンプトを表示せずに情報通知付きで実行されます。デフォルトのポリシーであれば、同じルールによって `high` の拒否（deny）が `medium` の確認プロンプト（ask）に緩和され、`very_high` と `critical` は引き続きブロックされます。

## 設定

設定ファイル `ai-approval.json` は、グローバルのエージェントディレクトリ（`~/.pi/agent/`）から読み込まれます。また、信頼されたプロジェクトではプロジェクトルート配下の `.pi/ai-approval.json` からも読み込まれます。プロジェクト側の設定はポリシーをより厳格にする方向でのみ上書きでき、グローバル設定より緩和することはできません。どちらのファイルも存在しない場合、拡張機能の起動時に警告が表示され、`/ai-approval init` で作成するよう案内されます（作成されるまでは組み込みのデフォルト設定が有効となります）。

```json
{
  "primaryModel": "CURRENT",
  "primaryThinkingLevel": "low",
  "secondaryModel": "CURRENT",
  "secondaryThinkingLevel": "low",
  "timeoutMs": 90000,
  "askTimeoutSeconds": null,
  "riskActions": {
    "very_low": "allow",
    "low": "allow",
    "medium": "ask",
    "high": "deny",
    "very_high": "deny",
    "critical": "deny"
  }
}
```

### riskActions

| キー | 設定可能な値 | デフォルト |
| --- | --- | --- |
| `very_low`, `low`, `medium`, `high` | `allow`, `ask`, `deny` | `allow`, `allow`, `ask`, `deny` |
| `very_high`, `critical` | `ask`, `deny` | `deny`, `deny` |

`very_high` と `critical` を `allow` に設定することはできません。指定した場合でも、設定パーサーが警告を出力した上で自動的に `deny` を適用します。

重要な操作（安全な操作以外）をすべて人間に確認させたい場合は、次のように設定します:

```json
{
  "riskActions": {
    "very_low": "allow",
    "low": "allow",
    "medium": "ask",
    "high": "ask",
    "very_high": "ask",
    "critical": "ask"
  }
}
```

### レビュワーモデル

`primaryModel` と `secondaryModel` で2段階のレビュワーチェーンを設定できます。現在のセッションモデルは、常に最後のフォールバック（第3のチャネル）として維持されます。どちらの設定項目も、明示的な `provider/model-id` 形式の指定、または現在のセッションモデルを使用することを意味する特殊値 `CURRENT`（デフォルト）を受け付けます。

チェーン内に同一モデルが重複して指定された場合、試行されるのは1度のみです。最初に解決されたチャネルで試行され、以降の重複はスキップされるため、一時的に利用できないモデルに対して無駄な再リクエストが発生することはありません。重複判定はモデル ID のみで行われ、思考量（thinking level）の設定が異なっていても別チャネルとはみなされません。たとえば `primaryModel: "openai/gpt-5.6-luna"` の呼び出しに失敗した場合、`secondaryModel: "openai/gpt-5.6-luna"` は思考量の指定が異なっていてもスキップされます。設定されたすべてのチャネルが失敗した場合、操作は安全側に倒してブロックされ、リスクが推測で許可されることは決してありません。

### レビュワーの思考量

`primaryThinkingLevel` と `secondaryThinkingLevel` は、各レビュワーチャネルの思考量（thinking effort）を設定します。指定可能な値は `off`、`minimal`、`low`、`medium`、`high`、`xhigh`、`max`、およびレビュー実行時点のセッション思考量を引き継ぐ特殊値 `CURRENT` です。デフォルト値は `low` です。`CURRENT` を指定した際にセッションの思考量が取得できない場合は `low` が適用されます。なお、最後のフォールバックであるセッションモデルのチャネルは、常にセッションの思考量（取得できない場合は `low`）を使用します。

```json
{
  "primaryThinkingLevel": "low",
  "secondaryThinkingLevel": "CURRENT"
}
```

環境変数による設定の上書き（`PI_AI_APPROVAL_PRIMARY_MODEL`、`PI_AI_APPROVAL_SECONDARY_MODEL`、`PI_AI_APPROVAL_PRIMARY_THINKING_LEVEL`、`PI_AI_APPROVAL_SECONDARY_THINKING_LEVEL`、`PI_AI_APPROVAL_TIMEOUT_MS`、`PI_AI_APPROVAL_POLICY`）にも対応しています。

### ask のタイムアウト

`askTimeoutSeconds` は、承認プロンプトが待つ時間（秒）です。デフォルトの `null` はタイムアウトしません。`0` 以下の数値も `null` と同じです。正の数を指定すると、プロンプト上で残り秒数がカウントダウンされ、0 になると操作は拒否されてブロックされます。

```json
{
  "askTimeoutSeconds": 30
}
```

信頼されたプロジェクトの設定ファイルは、明示的な `null` を含めてグローバル設定を上書きします。`timeoutMs` は別の設定で、承認プロンプトではなくレビュワーの制限時間です。

### 判定コメントの言語

`assessmentLanguage` は、レビュワーが出力する `action_summary`（操作の要約）および `rationale`（判定理由。承認プロンプトや拒否理由として表示）の言語を設定します。デフォルトの `auto` では会話でユーザーが主に使用している言語に追従します。特定の言語に固定したい場合は、言語名（例: `"Japanese"`）を指定します:

```json
{
  "assessmentLanguage": "Japanese"
}
```

プロジェクト設定はグローバル設定を上書きします。

### fail-closed の保証

以下の場合、承認プロンプトを表示することなくツール呼び出しを即座にブロックします:

- レビュワーのタイムアウト、呼び出し失敗、キャンセル、または解析不能な出力
- レビュワーの応答に未知のリスクレベルが含まれている場合
- すべてのレビュワーチャネルが失敗した場合
- 承認プロンプトが閉じられた（dismissed）、または利用できない場合

セッション承認ルールが設定されている場合でも、エラーや失敗によって誤って実行が許可されることはありません。一方で、ルールに関する問題のすべてが即座にブロックとなるわけではありません。ルール入力 UI が利用できない場合は診断メッセージ付きの拒否（declined）としてブロックされます。ルール入力中に Esc キーを押した場合は選択肢一覧に戻るだけで実行は許可されません。空文字や上限を超えた入力では入力欄が開いたまま保持され、レビュワーが報告したルール ID が有効なルールセットに存在しない場合は「一致なし」として通常の承認プロンプトへフォールバックします。

拒否サーキットブレーカー（denial circuit breaker）により、エージェントによるリトライの暴走を防止します。同一ターン内で好ましくない結果（拒否、ユーザーによる却下、レビュー失敗、タイムアウトなど）が連続して発生した場合、エージェントのターンを自動的に中断します。

## レビュー対象

どのツール呼び出しをレビュー対象とするかは、リスクポリシーとは独立した `review` ルール（ツールパラメータ → スコープ）によって制御されます:

```json
{
  "review": {
    "bash.command": "always",
    "powershell.command": "always",
    "read.path": "outside-or-private",
    "grep.path": "outside-or-private",
    "write.path": "outside-or-private",
    "edit.path": "outside-or-private"
  }
}
```

組み込みのデフォルト設定は、`bash.command: always`、`powershell.command: always`、`read.path` / `grep.path` / `write.path` / `edit.path: outside-or-private`、`find.path` / `ls.path: private-only` です。

ルールのキーは `<ツール名>.<パラメータ名>` の形式で指定します。`bash.command` および `powershell.command` は Bash や PowerShell 呼び出しのコマンド文字列を対象とし、`read.path` / `grep.path` / `find.path` / `ls.path` は読み取りや検索のスコープを、`write.path` / `edit.path` は変更対象のファイルを対象とします。組み込みのルールが存在しないツールであっても、トップレベルに文字列型の `path` パラメータを持つツール（例: `custom_reader.path`）であれば同様に指定可能で、デフォルト値は `private-only` となります。

### スコープの値

| スコープ | レビューされる条件 |
| --- | --- |
| `off` | レビューを行いません（対象のツール/パラメータは完全にスキップされます）。 |
| `private-only` | 対象パスが private データと判定された場合のみレビューします。 |
| `outside-or-private` | 対象パスがプロジェクトルート外に解決されるか、private である場合にレビューします（`write` / `edit` の場合はプロジェクト内のセキュリティ関連ファイルも含みます）。 |
| `always` | 対象にかかわらず常にレビューします。 |

「private」の判定は決定論的なルールカタログに基づいて行われます。これには、認証情報やシークレットファイル（`.env*`、鍵ファイル、`auth.json`、トークンストア、ブラウザのログインデータなど）、プロジェクト外のプライベートディレクトリ（`.ssh`、`.gnupg`、`.aws` や `.kube` などのクラウド CLI 設定、ブラウザプロファイル、Pi エージェントデータなど）、および検索ツールの場合はこれらを含みうるディレクトリスコープや glob が該当します。また `write` / `edit` における「sensitive（機密）」には、CI ワークフロー、コンテナやデプロイのマニフェスト、依存関係のロックファイル、シェルプロファイル、秘密鍵ファイル（`.pem`、`.key` など）、機密ディレクトリ名（`.git`、`secrets`、`terraform`、`k8s` など）が含まれます。

`bash.command` と `powershell.command` は特別で、パスではなくコマンド文字列そのものを対象とします。そのため `off` が指定された場合のみスキップされ、それ以外のスコープではすべてのシェルコマンドがレビュー対象となります（なお、private データを参照するコマンドは、ツール呼び出しを行わない制限モードでレビューされます）。

「レビュー対象であること」は「ブロックされること」を意味しません。レビュー対象となったツール呼び出しは AI レビュワーによってリスクレベルが分類され、その結果とローカルの `riskActions` 設定に基づいて実行可否（allow / ask / deny）が決定されます。

## コマンド

- `/ai-approval` — ステータス表示: レビュワーチャネル、タイムアウト、設定ファイルのパス、警告
- `/ai-approval init` — デフォルト設定ファイルの生成（グローバル / プロジェクトの保存先を選択。既存ファイルがある場合は上書き確認）
- `/ai-approval rules` — レビュー対象マトリクスと現在有効な risk actions の表示
- `/ai-approval session-rules` — セッション承認ルールの一覧表示・追加・編集・削除（対話形式）
- `/ai-approval bypass` / `/ai-approval enable` — レビューの一時的な無効化および再有効化（対話型 TUI のみ。実行中は永続的に警告を表示）

## インストール

```bash
pi install npm:@yuru7/pi-ai-approval
```

## 開発

```bash
pnpm install
pnpm typecheck
pnpm test
```

## 謝辞

本プロジェクトは [pi-approval-guardian](https://github.com/mics8128/pi-approval-guardian) に着想を得て作成されました。

## ライセンス

[MIT](./LICENSE)
