Session Shell 権限ポリシー
問題
POST /session/:id/shell は、LLM ツール呼び出しや通常のエージェント権限調停フローを経由せず、デーモン経由で直接シェルコマンドを実行します。この変更前、このエンドポイントは非厳密なミューテーションであり、デーモントークンとセッション ID、またはトークンなしのループバック開発者デフォルトでアクセス可能でした。
直接シェルを操作するインターフェースに対して、これは過度な権限です。デーモン運用者が明示的にこのインターフェースを有効にし、かつ呼び出し元がターゲットセッションにアタッチされていることを証明しない限り、シェルコマンドを実行できるべきではありません。
目標
- デフォルトで直接セッションシェルを無効にする。
qwen serve --enable-session-shellによる運用者の明示的なオプトインを必須とする。- オプトインが有効になる前に、Bearer トークンの設定を必須とする。
- 宛先セッションに登録されているクライアント ID を必須とする。
- REST ルート、ACP HTTP ディスパッチャー、およびブリッジ実行シンクで同じポリシーを適用する。
- 通常のエージェントシェルツール承認および権限調停は変更しない。
非目標
- 直接シェルを
PermissionMediator経由でルーティングしない。 - プロンプト送信、プロンプトキューイング、または SDK の保留中プロンプトの動作を変更しない。
- シェル固有のレートリミッターを追加しない。
- オプトインフラグの環境変数エイリアスを追加しない。
設計
runQwenServe は Bearer トークンを一度解決し、トリミングします。その後、以下の有効なブール値を 1 つ計算します。
sessionShellCommandEnabled =
opts.enableSessionShell === true && token !== undefined;その値はブリッジ、REST アプリ、および ACP ディスパッチャーに伝播されます。createServeApp を直接呼び出す組み込み呼び出し元は、空でない文字列チェックを使用してトークンの存在を計算するため、厳密なミューテーションゲーティングとシェル機能の公開の両方において、token: '' はトークンなしと同じように動作します。
REST ルートは mutate({ strict: true }) を使用します。トークンなしのループバックデーモンでは、厳密なゲートがハンドラの実行前に 401 token_required を返します。トークンが設定されている場合、ハンドラは無効化されたシェルを session_shell_disabled で拒否し、次に X-Qwen-Client-Id を要求し、コマンドボディを検証し、最後にブリッジに委任します。
ACP ディスパッチャーは、古いクライアントのために _qwen/session/shell をディスパッチ可能なまま保持しますが、有効なポリシーが有効になっていない限り、初期化時の _qwen.methods リストでそれを公開しません。無効化された ACP 呼び出しは、コマンドをログに記録したりブリッジを呼び出したりせずに、安定した session_shell_disabled JSON-RPC エラーを返します。有効化された呼び出しは、引き続き接続がセッションを所有していることを要求し、ブリッジによってスタンプされたセッションバインドクライアント ID を使用する必要があります。
ブリッジは executeShellCommand() で最終的な多層防御チェックを強制します。チェック項目は、無効化、クライアント ID の欠落、不明なセッション、そして未バインドのクライアント ID です。これらのチェックをパスした後でのみ、シェルイベントをパブリッシュし、コマンドを実行し、シェル履歴を書き込みます。
エラー契約
REST:
- トークンなし:
401,code: token_required - 無効:
403,code/errorKind: session_shell_disabled - クライアント ID なし:
403,code/errorKind: client_id_required - 不正な形式または未バインドのクライアント ID: 既存の
400 invalid_client_id - 不明なセッション: 既存の
404 SessionNotFoundErrorマッピング
ACP:
- 無効:
RPC.INVALID_REQUEST,data.errorKind: session_shell_disabled - セッションバインドクライアント ID なし:
RPC.INVALID_REQUEST,data.errorKind: client_id_required - 所有されていないセッションおよび無効なクライアント ID は既存の JSON-RPC マッピングを維持
互換性
DaemonSessionClient.shellCommand() は、セッションクライアントがセッションにバインドされたクライアント ID を保持しているため、デーモンが明示的に有効化され認証されている場合に引き続き動作します。単体の DaemonClient.shellCommand(sessionId, command) は opts.clientId を渡す必要があり、そうでない場合は client_id_required を受け取ります。
テストカバレッジ
この実装は、ブリッジ、REST、ACP トランスポート、serve ブート、およびコマンドパーサーに特化したテストでカバーされています。最も価値の高いチェックは、デフォルト無効の動作、トークンなしの厳密なゲーティング、機能の公開、ACP 初期化メソッドフィルタリング、ブリッジシンクの強制、およびセッションにバインドされたクライアント ID の伝播です。