---
read_when:
    - exec または Plugin の承認ライフサイクル、ストレージ、プロトコル、認可の変更
    - チャンネルへの承認リンクまたはネイティブ承認コントロールの追加
    - 子セッションの承認を親ビューまたはオーケストレータービューに反映する
summary: Control UI、ネイティブアプリ、チャネル、親セッションにわたる、永続的でディープリンク可能な承認の設計
title: 複数サーフェスでのオペレーター承認
x-i18n:
    generated_at: "2026-07-26T09:43:32Z"
    model: gpt-5.6
    postprocess_version: locale-links-v1
    prompt_version: 32
    provider: openai
    source_hash: 9defdaada1911df1184f64429e1787c4881e735c433d6dbc30a5946e11cc7cce
    source_path: refactor/operator-approvals.md
    workflow: 16
---

# マルチサーフェスのオペレーター承認

この設計は [#103505](https://github.com/openclaw/openclaw/issues/103505) を追跡します。プロセスローカルな承認権限を、Gateway が所有し SQLite を基盤とする単一のライフサイクルに置き換えます。Gateway が所有するすべての exec または plugin/tool 承認には、1 つの安定した ID、1 つの認証済み Control UI ルート、アトミックな先着回答優先の解決、およびその発生元と祖先セッションストリームへのオペレーター専用プロジェクションが与えられます。

インラインアクションとディープリンクは共存します。承認モードの切り替えはありません。

## 目標

- exec および plugin/tool ゲート用の、永続性を持つ単一の承認オブジェクト。
- 安定した `${controlUiBasePath}/approve/{approvalId}` ルート。
- 認可された任意の Control UI、ネイティブアプリ、またはチャンネルサーフェスからの解決。
- 並行するサーフェス間でのアトミックな先着回答優先の動作。
- 同一内容の再試行に対する冪等性。競合する遅延回答は勝者を上書きできない。
- タイムアウト、不正な信頼済み判定、ルートの欠落、キャンセル、および再起動はフェイルクローズする。
- 要求イベントと終端イベントは、発生元セッションおよび関連するすべての親／オーケストレーター所有者に到達する。
- チャンネルは型付きの承認アクションとナビゲーションアクションを受信し、トランスポートのコールバックデータはチャンネル内に限定される。
- 既存の exec/plugin Gateway メソッドは互換性を維持しつつ、その実装を単一のサービスに集約する。

## 対象外

- Gateway の再起動をまたいで、ブロックされたツール実行自体を永続化または再開すること。
- 承認 ID または URL をベアラー認証情報にすること。
- 承認プロンプトをモデルから見えるトランスクリプトへ追加すること、または親エージェントを起動すること。
- 承認ポリシー、プロダクトコマンド、またはレビュー担当者の認可をチャンネル Plugin に移すこと。
- チャンネル、デバイス、または祖先ごとに承認状態を複製すること。
- 終端結果を明確にするために必要な場合を除き、exec の許可リスト、Plugin ポリシーの合成、または `allow-always` の永続化を再設計すること。
- 最初の段階で、Gateway を持たない組み込み TUI にリモートアクセスできるようにすること。これはローカル専用のままであり、レビュー担当者が存在しない場合はフェイルクローズしなければならない。

## ロールアウト前のベースラインとエビデンスマップ

この表は、#103505 が作成された時点の実装状態を記録します。以下のロールアウトセクションでは、そのベースライン上に構築される永続レジストリ、型付きアクション、ディープリンクページ、およびネイティブクライアントの各段階を追跡します。

| サーフェス           | ベースラインのエントリポイントと所有者                                                                                                                                  | ベースラインの動作と不足点                                                                                                                                                                    |
| ----------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| エージェント exec        | `src/agents/bash-tools.exec-approval-request.ts`、`src/agents/bash-tools.exec-host-shared.ts`                                                                   | 2 フェーズの `exec.approval.*` 登録により、早期の `/approve` 競合を防止しますが、タイムアウトは依然として `askFallback` を通じて許可に変わる可能性があります。                                                        |
| Plugin ツールゲート  | `src/agents/agent-tools.before-tool-call.ts`                                                                                                                    | `plugin.approval.*` を要求します。`timeoutBehavior: "allow"` はタイムアウトしたゲートを承認できます。組み込みモードでは、`src/infra/embedded-plugin-approval-broker.ts` に別のプロセスローカル権限があります。 |
| Plugin Node ゲート  | `src/gateway/node-invoke-plugin-policy.ts`                                                                                                                      | Plugin マネージャーを介して直接作成およびブロードキャストし、サーバーメソッドのライフサイクルの一部を重複させています。                                                                                 |
| Gateway 権限 | `src/gateway/server-aux-handlers.ts`、`src/gateway/exec-approval-manager.ts`、`src/gateway/server-methods/approval-shared.ts`                                   | exec と Plugin の別々のマネージャーがプロセスローカルなマップを使用します。終端エントリは 15 秒間存続します。先着回答優先は単一プロセス内でのみ成立します。                                          |
| Gateway プロトコル  | `packages/gateway-protocol/src/schema/exec-approvals.ts`、`packages/gateway-protocol/src/schema/plugin-approvals.ts`、`src/gateway/methods/core-descriptors.ts` | exec には保留中のみの `get` があります。Plugin には `get` がありません。ディープリンク用の種類に依存しない終端検索は存在しません。                                                                                   |
| 配信          | `src/infra/exec-approval-channel-runtime.ts`、`src/infra/approval-native-runtime.ts`、`src/infra/approval-handler-runtime.ts`                                   | 発生元ルーティング、承認者への DM、保留中項目の再生、ネイティブハンドラー、およびプロセス内の終端クリーンアップをサポートします。別のフォローアップで、永続的な終端状態の整合処理を追加します。                          |
| ポータブルアクション  | `src/interactive/payload.ts`、`src/plugin-sdk/interactive-runtime.ts`、`src/plugin-sdk/approval-reply-runtime.ts`                                               | 承認ボタンは `/approve ...` を含むコマンドアクションです。URL と Web App のターゲットは型なしのボタンフィールドです。                                                                           |
| Telegram          | `extensions/telegram/src/approval-handler.runtime.ts`、`extensions/telegram/src/button-types.ts`                                                                | レンダラーは、非公開のコールバックデータを生成する前にコマンドテキストを解析して承認セマンティクスを認識します。                                                                                     |
| Control UI        | `ui/src/app/exec-approval.ts`、`ui/src/app/overlays.ts`、`ui/src/components/exec-approval.ts`                                                                   | 承認 UI はグローバルモーダルです。`ui/src/app-route-paths.ts` と `ui/src/app-routes.ts` は完全一致ルートを使用し、不明なパスをチャットに書き換えます。                                                    |
| セッション所有権 | `src/agents/subagent-registry.types.ts`、`src/agents/subagent-registry-read.ts`、`src/config/sessions/types.ts`                                                 | コントローラー、要求元、明示的な親、および従来のスポーン所有権は存在しますが、承認イベントはそれらのセッションストリームへプロジェクションされません。                                                    |
| 共有状態      | `src/state/openclaw-state-schema.sql`、`src/state/openclaw-state-db.ts`                                                                                         | 既存の即時トランザクションと Kysely の条件付き更新により、`state/openclaw.sqlite` で永続的な比較交換が可能です。                                                                   |

現在の代表的なテストには、`src/gateway/exec-approval-manager.test.ts`、`src/gateway/server-methods/approval-shared.test.ts`、`src/agents/bash-tools.exec-gateway-approval.e2e.test.ts`、`extensions/telegram/src/approval-handler.runtime.test.ts`、および `ui/src/e2e/approval-flow.e2e.test.ts` が含まれます。

Plugin SDK は、引き続き唯一のチャンネル／Plugin 境界です。承認ランタイムとプレゼンテーションの変更は、既存の `src/plugin-sdk/approval-*.ts` および `src/plugin-sdk/interactive-runtime.ts` サブパスを通じてエクスポートしなければなりません。Plugin の本番コードは Gateway の内部実装をインポートしてはなりません。

## 先行事例

Omnigent は、有用な UX と失敗時のセマンティクスを提供しています。

- [`approval.py`](https://github.com/omnigent-ai/omnigent/blob/46e3cd9754c3b8567f7b09f4d19b6249dabe0e80/omnigent/runtime/policies/approval.py) は ASK を待機させ、ポリシーごとのタイムアウトを適用し、完全に一致する受諾のみを承認として扱います。
- [`sessions.py`](https://github.com/omnigent-ai/omnigent/blob/46e3cd9754c3b8567f7b09f4d19b6249dabe0e80/omnigent/server/routes/sessions.py) には、サーバー側のネイティブハーネスゲートと、祖先への要求／解決プロジェクションが含まれています。
- [`ApprovePage.tsx`](https://github.com/omnigent-ai/omnigent/blob/46e3cd9754c3b8567f7b09f4d19b6249dabe0e80/web/src/pages/ApprovePage.tsx) は、スタンドアロンのモバイル承認ページを提供します。

そのストレージに関する主張を無批判に模倣してはなりません。現在アクティブな保留状態は [`_elicitation_registry.py`](https://github.com/omnigent-ai/omnigent/blob/46e3cd9754c3b8567f7b09f4d19b6249dabe0e80/omnigent/server/_elicitation_registry.py) でプロセスローカルに保持されており、未使用の保留テーブルは [`e3b1f2a4c9d7_drop_pending_tool_calls_table.py`](https://github.com/omnigent-ai/omnigent/blob/46e3cd9754c3b8567f7b09f4d19b6249dabe0e80/omnigent/db/migrations/versions/e3b1f2a4c9d7_drop_pending_tool_calls_table.py) によって削除されています。OpenClaw は意図的にさらに先へ進みます。SQLite を信頼できる情報源とし、すべての終端遷移をデータベースの比較交換として実行します。

## アーキテクチャと所有権

Gateway がライフサイクルを所有します。

1. エージェント、Plugin フック、または Node ポリシーが、種類固有の要求とプロセスローカルな実行バインディングを提供します。
2. Gateway がそれを検証し、サニタイズ済みのレビュー担当者向けプロジェクションを構築します。
3. 承認サービスが発生元／所有者の対象者を計算して正規行を挿入し、その後プロセス内の待機処理を登録します。
4. 永続的な挿入後、Gateway は既存の承認イベント、セッションプロジェクション、チャンネル通知、およびネイティブプッシュを発行します。
5. すべてのサーフェスが同じサービスを通じて解決します。
6. サービスが 1 つの終端遷移をコミットし、ランタイムの待機処理を起動して、終端プロジェクションを発行します。
7. イベント配信の失敗によって、コミット済みの決定がロールバックされることはありません。クライアントは `approval.get` またはリストの再生を通じて復旧します。

所有権の境界：

- `src/gateway/`：承認サービス、認可、RPC アダプター、URL 構築、待機処理のライフサイクル、およびイベント発行。
- `src/state/`：共有スキーマと生成された Kysely 型。
- `src/infra/`：サニタイズ済みの承認ビューモデルとポータブルプレゼンテーションの構築。
- `src/agents/`：返された判定の要求、待機、および適用。永続化は行いません。
- `src/channels/` および `extensions/*`：型付きアクションのレンダリング、チャンネルユーザーの認可、非公開コールバックのエンコード、および配信済みコントロールの更新。
- `src/plugin-sdk/`：公開承認およびプレゼンテーション契約のみ。
- `ui/`：スタンドアロンページと既存のキュー／モーダルクライアント。

プロセス内の待機処理は通知メカニズムであり、権限ではありません。登録処理では要求を公開する前に行を挿入し、待機処理を同期的に設置するため、これらの手順の間に解決処理が割り込むことはできません。それ以降のすべての解決処理は、その待機処理を完了させる前に SQLite を通じてコミットします。

## 永続レコード

共有状態データベースに 1 つの `operator_approvals` テーブルを追加します。

| 列                                             | 目的                                                                                                                                       |
| -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `approval_id`                                      | グローバルに一意な正規 ID。プロトコル互換性のため既存の exec ID と `plugin:` ID は維持しますが、プレフィックスから種別を推測してはなりません。      |
| `resolution_ref`                                   | 正規 ID を格納できないトランスポートコールバック用の、一意な完全 SHA-256 base64url ロケーター。これは認可情報でも公開 URL ID でもありません。 |
| `kind`                                             | 閉じた `exec \| plugin` 判別子。                                                                                                        |
| `status`                                           | 閉じた `pending \| allowed \| denied \| expired \| cancelled` 状態。                                                                          |
| `presentation_json`                                | 検証済みで種別タグ付きのレビュー担当者向けプロジェクション。生のランタイムリクエスト、コマンドバインディング、コールバックペイロードはプロセスローカルのままです。               |
| `source_agent_id`, `source_session_key`            | ソース ID とセッションプロジェクションのアンカー。セッションキーは永続的ですが、ローテーションされるセッション UUID は永続的ではありません。                                          |
| `audience_session_keys_json`                       | 制限付き幅優先の所有権探索によって生成される、順序付きで重複排除された JSON 配列。リクエストイベントと終端イベントは同じスナップショットを使用します。 |
| `requested_by_device_id`, `requested_by_client_id` | 永続的なリクエスト元／監査メタデータ。接続 ID はメモリ内に保持され、複数サーフェスをまたぐプリンシパルではありません。                                         |
| `reviewer_device_ids_json`                         | 信頼された承認ランタイムからのみ提供される、明示的に対象指定された任意のレビュー担当者デバイス。                                                  |
| `runtime_epoch`                                    | 保留された実行を所有するプロセスエポック。再起動後に孤立した行をキャンセルするために使用します。                                                     |
| `created_at_ms`, `expires_at_ms`, `updated_at_ms`  | 権威あるタイミング情報。                                                                                                                         |
| `decision`                                         | ユーザーによる明示的な判断が存在する場合の、その判断。                                                                                                       |
| `terminal_reason`                                  | `user`、`timeout`、`malformed-verdict`、`no-route`、`run-aborted`、`gateway-restart` などの閉じた理由。                                |
| `resolved_at_ms`, `resolver_kind`, `resolver_id`   | 勝者と監査用 ID はサーバー側に保持されます。レビュー担当者向けプロジェクションでは、生の解決者 ID を省略します。                                           |
| `consumed_at_ms`, `consumed_by`                    | `allow-once` 用の独立したリプレイ防止機構。消費しても記録済みの判断を消去してはなりません。                                                       |

必須インデックス：

| インデックス                                      | 目的                                                                     |
| ------------------------------------------ | --------------------------------------------------------------------------- |
| unique `(resolution_ref)`                  | 挿入時に列をまたぐ `approval_id`/`resolution_ref` の曖昧性を拒否します。 |
| `(status, expires_at_ms)`                  | 保留中の承認を検索し、権威ある期限と整合させます。               |
| `(source_session_key, created_at_ms DESC)` | 1 つのソースセッションについて最近の承認をリプレイします。                             |
| `(resolved_at_ms)`                         | 固定の保持ポリシーに従って、保持されている終端承認を削除します。  |

オーディエンス配列は小さく、上限が設定されています。セッションで絞り込んだリプレイでは、まず Kysely を通じて可視な保留中の行を選択し、次にアプリケーションコードで上限付きのオーディエンス配列をデコードしてフィルタリングします。文字列照合や生の SQL JSON クエリは使用しません。

終端行は 30 日間保持し、`src/audit/audit-event-store.ts` のメタデータ監査保持期間と一致させます。削除は固定のメンテナンスポリシーであり、新しい設定サーフェスではありません。データベースは非公開のローカルコントロールプレーン状態ですが、レビュー担当者向け API は保存された完全なリクエストやランタイムバインディングを決して公開してはなりません。

## 状態機械と比較交換

有効な遷移は次のものだけです：

- `pending -> allowed`：明示的な `allow-once` または `allow-always`。
- `pending -> denied`：明示的な拒否、信頼された不正形式の終端判定、または配信経路なし。
- `pending -> expired`：権威ある期限への到達。
- `pending -> cancelled`：実行の中断、正常なシャットダウン、または再起動時の孤立状態復旧。

許可以外のすべての終端状態では、実効判定は拒否です。

解決処理では、単一の即時 SQLite トランザクションと、次と同等の Kysely 条件付き更新を使用します：

```sql
UPDATE operator_approvals
SET status = ?, decision = ?, terminal_reason = ?, resolved_at_ms = ?
WHERE approval_id = ?
  AND status = 'pending'
  AND expires_at_ms > ?;
```

更新対象の行がない場合、同じトランザクションでレコードを読み取ります：

- 存在しない、または未認可：見つからないことを返し、存在を明かしません。
- まだ保留中だが期限に到達：比較交換によって `expired` に変更し、その終端行を返します。
- 記録済みの判断と同一：記録済みの勝者とともに冪等な成功を返します。
- 異なる判断：統合 API は記録済みの勝者とともに `applied: false` を返します。レガシーアダプターは、出荷済み契約で必要な場合に `APPROVAL_ALREADY_RESOLVED` を維持します。
- 任意の終端状態：決して変更しません。

`now == expires_at_ms` は期限切れです。Gateway の時刻が権威を持ちます。

`allow-once` の実行では、既存の正確なコマンド／システム実行コンテキストに紐付けられた、`consumed_at_ms IS NULL` に対する 2 回目の CAS を使用します。承認行は消費後も監査記録として残ります。

認証できない、または承認を特定できない不正形式の HTTP/RPC 入力は、変更を加えずに拒否され、承認を成立させることは決してありません。既知の承認について、信頼されたハーネス／待機処理から不正形式の終端判定を受信した場合は、`denied` に遷移します。

## Gateway API

種別に依存しないレビュー担当者向けメソッドを追加します：

| メソッド                                    | 契約                                                                                                                                                                                                            |
| ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `approval.get { id }`                     | 可視な保留中プロジェクション、または保持されている終端プロジェクションを返します。                                                                                                                                                          |
| `approval.resolve { id, kind, decision }` | 正規 ID または固定サイズのトランスポート参照を受け入れた後、認可、種別と許可された判断の検証、期限の整合、終端 CAS を実行します。レスポンスには常に正規 ID が含まれます。 |

CAS が成功したら、コミット済みのプロジェクションを直ちに返します。レガシーイベント、チャネルフォワーダー、プッシュ終端処理はベストエフォートの後続処理です。遅いサーフェスや失敗したサーフェスによって、勝者となったレスポンスが遅延したりロールバックされたりしてはなりません。

種別固有のリクエスト検証は `exec.approval.request` と `plugin.approval.request` に残します。既存の `exec.approval.get/list/waitDecision/resolve` と `plugin.approval.list/waitDecision/resolve` は出荷済みの Gateway API であるため、正規サービスへのプロトコル境界アダプターになります。内部呼び出し元は同じ変更内でサービスへ移行します。

レビュー担当者向けプロジェクションはタグ付きユニオンです：

```ts
type OperatorApproval = {
  id: string;
  status: OperatorApprovalStatus;
  presentation:
    | { kind: "exec"; commandText: string /* 安全な exec プレビュー */ }
    | { kind: "plugin"; title: string; description: string /* 安全な Plugin プレビュー */ };
  // 共通のライフサイクルフィールド
};
```

安定したパスは導出されるものであり、永続化されません。`approval.get` は `urlPath` を返します。承認済みの公開オリジンを把握しているサーフェスは、絶対 `url` も受け取ることができます。レビュー担当者向けスナップショットでは、ソースとオーディエンスのセッションキーを省略します。Gateway は、独立した `session.approval` プロジェクション用に、それらのルーティングキーをサーバー側で保持します。

## イベントとポータブルアクション

PR 1 では、出荷済みのイベント名、ペイロード、既存のレコード単位の受信者フィルターを維持します：

- `exec.approval.requested`
- `exec.approval.resolved`
- `plugin.approval.requested`
- `plugin.approval.resolved`

これらのレガシーイベントには完全なランタイムリクエストが含まれる可能性があるため、承認スコープ内のすべてのクライアントへファンアウトしてはなりません。PR 5 では、レガシーイベントの配信範囲を拡大する代わりに、サニタイズ済みのライフサイクルプロジェクションを通じて、タグ付きライフサイクルフィールド（`status`、`sourceSessionKey`、`urlPath`、終端メタデータ、プレゼンテーションレベルの `kind`）を追加します。

承認スコープの `session.approval` プロジェクションイベントを追加します。永続化されたオーディエンスキーとともに正規イベントを一度公開し、完全一致するセッションのサブスクライバーは、一致するキーごとに同じイベントを受信します：

- `sessionKey`：プロジェクションを受信するストリーム。
- `sourceSessionKey`：ゲートを発生させた子／ソース。
- `phase`：承認状態によって判別される `pending \| terminal`。
- 安全な `OperatorApproval` プロジェクション 1 つ。

クライアントは `sessions.messages.subscribe { key, agentId?, includeApprovals: true }` でオプトインします。成功レスポンスには `approvalReplay` が追加され、その完全一致するストリームキーについて、購読クライアントにもレコード単位のレビュー権限がある現在の保留中承認が最大 1,000 件含まれます。`truncated: false` により、フィルタリング済みのリプレイが権威を持ち、再接続するクライアントはローカルの保留中セットをそれで置き換えます。`truncated: true` は過負荷シグナルであり、クライアントは正規検索または後続のライフサイクルイベントによって確定するまで、未確認のローカルエントリを保持しなければなりません。リプレイ中に後から検出された永続的なタイムアウトは、新しいスナップショットが返される前に、購読済みかつレコード単位で認可されたオーディエンスにのみ終端トゥームストーンを送出します。`operator.admin` は直接オプトインできます。より限定されたクライアントには、ペアリング済みデバイス ID と `operator.approvals` の両方が必要です。セッションの購読だけでは、承認の可視性は決して付与されません。

`src/gateway/server-broadcast.ts` で、イベントを `operator.approvals` の下に登録します。このプロジェクションは観測専用です。トランスクリプト行の追加、`sessions.changed` の送出、エージェントの起動は決して行いません。

`src/interactive/payload.ts` の `MessagePresentationAction` を拡張します：

```ts
type MessagePresentationAction =
  | { type: "command"; command: string }
  | { type: "callback"; value: string }
  | {
      type: "approval";
      approvalId: string;
      approvalKind: "exec" | "plugin";
      decision: ExecApprovalDecision;
    }
  | { type: "url"; url: string }
  | { type: "web-app"; url: string };
```

コアは型付きの決定アクションを構築し、承認済みの絶対 Control UI オリジンが利用可能な場合は、別途レビューリンクを構築します。チャネルは承認アクションを独自のコールバック形式にエンコードし、解決結果を正規サービスに送信します。コールバックでは、収まる場合は正確な正規 ID を使用し、そうでない場合は行に固有の完全ダイジェスト `resolution_ref` を使用します。この参照はコンパクトな検索キーにすぎません。通常の Gateway 認証、レコードの認可、明示的な種別、許可された決定の検証、期限の調整、最初の回答に対する CAS は引き続き適用されます。チャネルは、ID を切り詰めたり、ハッシュプレフィックスを解決したり、`/approve` テキストを解析したり、ID プレフィックスから種別を推測したりしてはなりません。

`button.url`、`button.webApp`、およびコマンドを基盤とする承認コントロールは、非推奨の Plugin SDK 互換入力として維持します。SDK 境界で正規化し、同じ PR ですべてのバンドル済み内部呼び出し元を移行します。`/approve {id} {decision}` は、ボタンのセマンティック契約ではなく、テキストのフォールバックおよび CLI／チャットコマンドとして残ります。

## Control UI

ルートは `${basePath}/approve/{approvalId}` です。ID が唯一のパスパラメーターであり、ソースセッションの識別情報はレコードから取得します。

現在のルーターには完全一致の静的ルートがあり、不明なパスを Chat に書き換えるため、通常のルート正規化より前に `ui/src/app/bootstrap.ts` でこのディープリンクを検出します。通常の Gateway／認証設定を再利用しますが、サイドバーシェルとグローバルモーダルの外側にスタンドアロンの承認ページをレンダリングします。

ドキュメントは、その URL を提供した Gateway が所有します。初回接続では、フルアプリに永続化されたリモート Gateway の選択を無視しますが、その選択の設定は変更もコピーもしません。認証のみ、提供元 Gateway を対象とするセッションスコープに維持されます。信頼されたネイティブ認証、または別途確認された `gatewayUrl` オーバーライドによって、接続先を変更できます。コアは、Plugin HTTP ルートおよび静的拡張機能の検出より先に、1 セグメントの `/approve` 名前空間を予約します。これには、`.json` または `.js` で終わる ID も含まれます。Control UI の提供が無効な場合、予約済みルートは `404` でフェイルクローズします。遅延チャンクの読み込み失敗によってセキュリティ上の決定がスピナーのまま停止しないように、このページはメインの Control UI バンドルに含めます。

ページの状態：

- 読み込み中
- 認証が必要
- 保留中
- 解決中
- ここで承認または拒否済み
- 別の場所で解決済み
- 期限切れ
- キャンセル済み
- 禁止／見つかりません
- 再試行可能な接続エラー

このページは、2 つ目の未認証 REST API ではなく、Gateway RPC を呼び出します。ブラウザーを更新すると、永続状態を再読み込みします。Gateway の認証情報を URL、クエリ、フラグメントに含めることはありません。

## 認可とプライバシー

URL は位置指定子であり、権限ではありません。解決には以下が必要です。

1. 認証済みの Gateway 接続。
2. `operator.approvals` または `operator.admin`。
3. レコードレベルのレビュー担当者認可。

レコードレベルのルール：

- `operator.admin` はレビューできます。
- `reviewer_device_ids` が存在する場合、それが権威ある情報です。リストに記載されたペアリング済みの
  `operator.approvals` デバイスのみがレビューできます。要求元デバイスもリストに含まれていない限り、
  暗黙的なアクセス権はありません。
- 明示的なレビュー担当者リストがない場合、要求元のペアリング済み
  `operator.approvals` デバイスは自身のレコードをレビューできます。
- 要求元またはレビュー担当者のバインディングがない真のレガシーレコードでは、アップグレードによって
  すでに保留中の作業が取り残されないよう、ペアリング済みデバイスの広範な可視性を維持します。
- デバイスを持たない内部ランタイムは、スコープ付きの
  承認ランタイム接続を介して読み取りはできませんが、解決はできます。この権限は、
  サーバー認証済みのランタイムトークンのみから付与されます。公開 `approval.resolve` フィールドで
  この権限を生成することはできません。
- 稼働中の要求元接続の所有権は、レガシーアダプターに対して引き続き有効です。
  一致するクライアント名から推測されることはありません。
- オーディエンスのメンバーシップが変更するのは表示のみです。認可を拡大することはありません。

`approval.get` は、サニタイズ済みのレビュー担当者向けプロジェクションのみを公開し、内部のソース／オーディエンスルーティングキーは省略します。PR 5 の `session.approval` イベントは、Gateway が永続化されたオーディエンススナップショットをサーバー側で適用した後、その単一の宛先 `sessionKey` と `sourceSessionKey` を伝送します。既存の exec／Plugin イベントは、利用側が移行するまで、従来のペイロードと制限された受信者を維持します。実行可能な要求、コマンドバインディング、継続処理は、プロセスローカルの待機処理にのみ残ります。永続行には、安全な表示情報と、ライフサイクル、ルーティング、監査のメタデータが含まれます。生の環境値、認証情報、認証ヘッダー、チャネルコールバックデータは保存しません。

## オーディエンスのプロジェクション

挿入前にオーディエンスを一度だけ計算し、順序付きスナップショットを永続化します。所有権はグラフであり、常に単一の親チェーンとは限りません。子には現在のコントローラーと元の要求元の両方が存在する場合があり、それらの所有者が異なるルートにつながる可能性があります。

決定的な幅優先探索を使用します。

1. ソースセッションキーをキューの初期要素にします。
2. デキューした各キーについて、最新のサブエージェントレジストリ行を読み取り、異なる 2 つの所有権エッジを固定順序でエンキューします。`controllerSessionKey`、次に `requesterSessionKey` の順です。
3. 使用可能なレジストリ行が存在する場合、ステアリング後に古くなっている可能性のあるセッションエントリの系統も追跡してはなりません。それ以外の場合は、現在の単一のフォールバックエッジ `parentSessionKey ?? spawnedBy` をエンキューします。
4. エンキュー時に正規化と重複排除を行い、最初に見つかった最短経路を採用します。
5. 一意のキーが 64 個に達したら停止します。このオーディエンスサイズ上限によって探索の深さも制限されます。

レジストリのソースは `src/agents/subagent-registry-read.ts` です。所有権フィールドは `src/agents/subagent-registry.types.ts` で定義されています。セッションのフォールバックフィールドは `src/config/sessions/types.ts` で定義されています。

承認の保留中にフォーカス／コントローラーの所有権が変更された場合でも、要求時と終了時のプロジェクションでは、永続化された同じオーディエンスを使用します。これにより、要求プロジェクションを受信したすべてのオーディエンスセッションストリームに対して終了時のクリーンアップが保証されます。解決は常にソースの承認 ID を対象とし、オーディエンスセッションが複製された承認状態を受信することはありません。転送されたチャネルメッセージのクリーンアップについては、後述する別個の配信ロケーターのフォローアップで対応します。

承認のためだけに、トランスクリプトメッセージを書き込んだり、システムプロンプトを注入したり、所有者のターンを開始したり、`sessions.changed` を発行したりしてはなりません。

## 配信済みサーフェスの収束

ネイティブ承認ハンドラーはすでに、アクティブなコントロールを置換または廃止できる十分な期間、配信済みメッセージのエントリを保持しています。汎用の転送済み承認メッセージは現在 `MessageReceipt` を破棄するため、別のサーフェスで決定すると、古いコントロールが保留中に見える状態で残る可能性があります。別個のフォローアップでは、共有状態データベース内の `operator_approval_deliveries` 子テーブルによってこの差を解消します。

各行には、承認 ID、一意の配信 ID、チャネル／アカウント／正確なルート、サイズ制限があり JSON 検証済みのチャネル固有メッセージロケーター、配信タイムスタンプ、終了処理状態を保存します。コールバックデータ、決定トークン、生の承認要求は保存しません。チャネルはロケーターのエンコードとメッセージの変更を所有し、コアは正規ステータス、対象の選択、再試行ポリシー、フォールバック終了テキストを所有します。

配信登録と終了時の解決は、競合しても安全に処理されます。

1. 保留中の送信から受領情報が返された後、1 つのトランザクション内で配信ロケーターを挿入し、親承認のステータスを読み取ります。
2. 親がすでに終了状態の場合、遅れて配信されたものを保留中のまま残さず、即時終了処理をスケジュールします。
3. コミットされた終了遷移ごとに、未終了のすべての配信行を個別にスケジュールします。破棄可能なブロードキャストをトリガーにはしません。
4. チャネルの終了処理機構は、`replaced`、`retired`、または `unsupported` を報告します。置換済みの場合は重複する終了メッセージを抑制し、廃止済みの場合は既存の終了フォローアップを送信します。未対応または失敗の場合は、承認 CAS をロールバックせずにフォールバックします。
5. 起動時に、未完了の配信がある終了済み承認を再試行し、Gateway の再起動に対するクリーンアップの耐障害性を確保します。

このトランスポートライフサイクルは、任意の配信アダプターフックであり、レンダラーでもモデル向けメッセージアクションでもありません。QQ の C2C／グループメッセージには現在、編集、削除、キーボードクリアの API がありません。このアダプターは引き続き未対応であり、トランスポートに変更 API が追加されるまでは、後からクリックされた際に正規の事実を表示することしかできません。

## 再起動、タイムアウト、ルートのセマンティクス

SQLite の永続化は、実行の再開を意味しません。コマンド／ツールのバインディングにはセキュリティ上重要なランタイム情報が含まれる可能性があり、再開可能なジョブ契約でもないため、メモリ内に残します。

Gateway の起動時：

- 新しいランタイムエポックを生成します。
- 古いエポックの保留中行を、理由 `gateway-restart` とともに `cancelled` へアトミックに遷移させます。
- 何が起きたかを URL で説明できるように行を保持します。
- ランタイムバインディングが存在しない状態で、後から承認された処理を実行することはありません。

タイマーはウェイクアップの最適化です。期限の権威ある情報は `expires_at_ms` に保存され、読み取り、待機、解決のすべてで期限切れの調整を実行します。

最終的な厳格動作：

- タイムアウト -> `expired`、拒否。
- ルートなし -> `denied`、拒否。
- 実行中止 -> `cancelled`、拒否。
- 不正な信頼済み判定 -> `denied`、拒否。
- 許可された明示的な許可決定のみ -> `allowed`。

現在出荷されている exec の動作は、依然としてこの契約と競合しています。

- `src/agents/bash-tools.exec-host-shared.ts` は `askFallback` を適用する場合があります。
- `docs/tools/exec-approvals.md` と `docs/cli/approvals.md` にそのサーフェスが記載されています。

Plugin の承認は現在、タイムアウトおよび不正な判定時にフェイルクローズします。レガシーの
`timeoutBehavior` フィールドは引き続き受け付けられますが、無視されます。exec の厳格セマンティクスに関する
フォローアップでは、コード、型、ドキュメント、テスト、変更履歴をまとめて更新し、
所有者／セキュリティ担当者による明示的なレビューを受ける必要があります。`askFallback` は移行中も、
ゲート前のポリシー選択を説明するために使用できますが、作成済みの
保留中レコードのタイムアウトを承認に変えてはなりません。

## 互換性計画

- Gateway プロトコルへの追加。プロトコルのバージョンは上げません。
- 外部境界では、既存の exec／Plugin のメソッドとイベントを維持します。
- `plugin:` プレフィックスを含む既存の ID を維持しますが、プレフィックスを型情報として使用することは停止します。
- `/approve` テキストコマンドの動作を維持します。
- レガシーのボタン URL／Web App フィールドとコマンドアクションを、Plugin SDK の互換入力として維持します。新しいコア出力は型付きです。
- 同じ型付きアクションの変更ですべてのバンドル済みチャネルと内部呼び出し元を移行します。
- 新しい URL／ページと、後続のタイムアウト動作変更について、変更履歴のエントリを追加します。
- 誘導モード設定は追加しません。

## ロールアウト

### PR 1：永続的なライフサイクル

- この設計メモ。
- 共有 SQLite スキーマ、Kysely 生成、ストア、30 日間のプルーニング。
- Gateway 承認サービス、ランタイム待機処理ブリッジ、再起動時の孤立処理。
- 統合された `approval.get/resolve`。
- exec／Plugin メソッドアダプター。
- 最初の回答を優先する処理、冪等性、期限切れ、認可、消費のテスト。
- この段階では UI またはチャネルの動作変更はありません。

### PR 2：型付きアクションとチャネルコールバック

- 型付きの承認、URL、Web App アクション。
- コアのプレゼンテーションビルダーと Plugin SDK のエクスポート。
- 明示的な所有者種別を持つ、トランスポート非公開のコールバックエンコーディング。
- トランスポート制限を超える正規 ID のための、永続的な固定サイズのコールバック参照。
- コマンドテキストと承認 ID の推論を廃止する、バンドル済みチャネルの移行。
- クリックされたサーフェスにおける正規の初回回答結果と、ベストエフォートによるアクティブなネイティブ終端更新。永続的なチャネルメッセージの終端化は引き続きフォローアップとする。
- SDK とバンドル済みチャネルのテスト。

### PR 3: Control UI のディープリンク

- 独立した認証済み承認ページと、ベースパスを考慮した起動時ルーティング。
- オペレーターが保存したリモート選択を変更しない、提供元 Gateway へのバインド。
- アセット形式の ID を含む、コア所有の承認 HTTP 名前空間。
- Gateway が生成する URL ペイロードと、ライフサイクルイベントが提供されるまでの保留状態ポーリング。
- モバイル幅、再接続、競合する回答、再読み込み、マウント済みパスの検証。

### PR 4: ネイティブクライアント

- iOS と Android のレビューサーフェスは、種別を認識する `approval.get/resolve` を使用する。watchOS は、レビュー担当者向けに安全なプロンプトと決定を、ペアリングされた iPhone を介して中継する。
- Watch は、コンパクトな中継契約でサポートされる実行決定、つまり一度だけ許可と拒否を提供する。
- 正規の初回回答による終端結果が、ローカルの試行済み決定状態に置き換わる。
- 解決確認応答が失われた場合や曖昧な場合、正規の再読み取りが完了するまで操作を無効化する。
- 以前にリリースされた Gateway v4 インスタンスでは、限定的なレガシーメソッドへのフォールバックにより実行レビューを維持する。保持されるサーフェス間の終端状態には統一メソッドが必要となる。
- レビュー担当者向けの警告と所有者コンテキストは、iPhone、Watch、Android のすべてで表示されたままとなる。
- ネイティブの単体テスト、ビルド、プラットフォーム検証。

### PR 5: 祖先へのライフサイクル伝播

- PR 1 で永続化されたオーディエンススナップショットからの `session.approval` 保留／終端配信。
- トランスクリプトの変更やエージェントの起動を伴わない、完全一致セッション購読、再接続時の再生、終端トゥームストーン。
- ライフサイクルコールバックは永続的な挿入／CAS の後に実行され、承認権限には決してならない。
- ネストされたサブエージェントと再接続の検証。

### PR 6: フェイルクローズ動作

- `node-invoke-plugin-policy.ts` と組み込み Plugin ブローカーを、重複する権限から移行する。
- タイムアウト、不正形式、ルートなし、バインド、一度だけ許可の消費に関する厳格なセマンティクス。
- 要求が保留になった後は適用せず、リリース済みの許容的なタイムアウト設定を非推奨にする。
- 複数サーフェスでの競合と障害注入の検証。

### フォローアップ: 永続的なリモートメッセージのクリーンアップ

- 転送配信ロケーターを永続化し、再起動後に配信済みのすべてのチャネルメッセージを終端化する。
- このトランスポートライフサイクルを、正規の承認権限および型付きプレゼンテーションアクションから分離したままにする。

## テスト

必要な重点的カバレッジ:

- SQLite を再オープンしても、保留および終端のプロジェクションが保持される。
- 2 つの同時リゾルバーから、CAS の勝者が正確に 1 つだけ生じる。
- 同一決定の再試行は冪等に成功し、競合する再試行は記録済みの勝者を返す。
- 期限以降の解決では承認できない。
- `allow-once` は終端監査状態を消去せず、正確に一度だけ消費できる。
- 起動時に古いランタイムエポックをキャンセルする。
- 未認可の検索および解決では、レコードの存在を明らかにしない。
- 明示的なレビュー担当者許可リストと、一般的なペアリング済み `operator.approvals` の動作。
- 実行用と Plugin 用のレガシーメソッドが同じストアを共有する。
- Gateway の request/list/get/resolve スキーマと、追加的なイベントペイロード。
- 型付きアクションの正規化、フォールバックレンダリング、SDK エクスポート、バンドル済みチャネルの切り替え。
- Telegram のコールバックエンコーディングにはトランスポート非公開データが含まれ、コマンド文字列の推論は含まれない。
- 直接の子、分岐したコントローラー／要求元所有者、ネストされた所有者、再割り当て、セッションフィールドのフォールバック、循環、オーディエンスサイズ上限。
- 要求時と終端時のオーディエンス配列が同一である。
- 所有者プロジェクションによってトランスクリプトの変更やエージェントの起動が発生しない。
- Control UI ルートが `/` と設定済みのベースパスで動作し、更新後に保留または終端の結果が表示される。
- Control UI と Telegram から同時に回答した場合、一方の勝者が表示され、敗者側には「別の場所で解決済み」と表示される。
- ネイティブ承認識別子と Gateway 所有者識別子が、ルーティングおよび照合を通じて正確な UTF-8 バイト列を保持する。
- ネイティブ RPC ファミリーのネゴシエーションは、受け入れられた Gateway ルートごとに正規またはレガシーの一方のファミリーを固定し、使用後に暗黙的なダウングレードを決して行わない。
- ネイティブの解決確認応答が失われた場合、正規の再読み取りまでアクションを無効化する。再読み取りに失敗しても、勝者を捏造したり Watch の更新を確認済みとしたりできない。
- Watch のスナップショット要求相関は、完全に一致するペアリング済み Gateway 所有者と、完了した正規の iPhone 再読み取りに対してのみ受け入れられる。
- モバイル幅の承認ページ、Telegram アクションのクリーンアップ、Android、iPhone、Watch をまたぐ保留／解決／遅延敗者の 1 往復を含む、Testbox／Crabbox によるユーザーパス検証。

## 可観測性

承認 ID、種別、送信元セッションキー、ステータス、理由、レイテンシーを含む、構造化されたコンテンツ非依存の遷移ログを出力する。プレビューや生のバインドは決してログに記録しない。

追跡項目:

- 種別ごとの要求数。
- 種別／ステータス／理由ごとの終端数。
- 保留ゲージ。
- 要求から終端までのレイテンシー。
- 解決競合の結果: 勝者、冪等な再試行、競合、期限切れ。
- 配信ルート数とルートなしによる拒否。
- 起動時の孤立キャンセル。
- オーディエンスサイズ。

後続のイベント配信に失敗しても、コミット済みの遷移は成功とする。ライフサイクル購読者は、PR 5 の再生と正規検索によって復旧する。永続的なチャネルメッセージの終端化は、上記の独立したフォローアップのままとする。

## 未決定事項

1. **外部から到達可能な Control UI のオリジン。** すべてのスナップショットは、安定した相対 `urlPath` を保持する。絶対 URL を通知できるのは、Gateway の公開に成功した後、キャッシュ済みの Tailscale Serve/Funnel ロケーションからのみとする。`allowedOrigins`、要求の Host ヘッダー、`gateway.remote.url`、表示専用のループバック／LAN 候補は正規オリジンではない。Telegram は、認証済みの Mini App ラッパーを使用して、ブートストラップを通じて承認パスを保持できる。任意のリバースプロキシは、別途レビューされた明示的な公開 URL 契約が存在するまで相対指定のみに制限する。チャネルにオリジンを推測させてはならない。
2. **実行の厳格なタイムアウト互換性切り替え。** Plugin の承認タイムアウトはフェイルクローズとなり、`timeoutBehavior` は非推奨となる。残るリリース済みの `askFallback` 契約について、保留中の要求がタイムアウトした後に実行の認可を停止するには、明示的な所有者／セキュリティレビュー、変更履歴、ドキュメント、移行／非推奨化の決定が必要となる。
3. **Gateway なしの組み込みモード。** 推奨事項: 当初はローカル専用とし、Gateway が存在する場合は正規サービスのクライアントにする。サーバーが解決できないディープリンクを通知してはならない。
