diff --git a/src/review/codex-reviewer.ts b/src/review/codex-reviewer.ts index 2e1d9600a..66c5196b3 100644 --- a/src/review/codex-reviewer.ts +++ b/src/review/codex-reviewer.ts @@ -29,6 +29,24 @@ const DEFAULT_TIMEOUT = 5 * 60 * 1000; /** デフォルトtmuxターゲット (MacBook用) */ const DEFAULT_TMUX_TARGET = "%2"; // カエデ (CodeGen) のペイン +/** + * シェルコマンド用に文字列をエスケープ + * tmux send-keys に安全に渡すためのエスケープ処理 + * + * @param str - エスケープする文字列 + * @returns エスケープされた文字列 + */ +function escapeShellString(str: string): string { + // シェル特殊文字をエスケープ + return str + .replace(/\\/g, "\\\\") // バックスラッシュ + .replace(/"/g, '\\"') // ダブルクォート + .replace(/\$/g, "\\$") // ドル記号 + .replace(/`/g, "\\`") // バッククォート + .replace(/\n/g, "\\n") // 改行 + .replace(/\r/g, "\\r"); // キャリッジリターン +} + /** * tmuxコマンドを実行 * @@ -44,8 +62,9 @@ async function execTmux( ): Promise { const { timeout = DEFAULT_TIMEOUT, env = {} } = options; - // tmux send-keys でコマンドを送信 - const sendCommand = `tmux send-keys -t ${target} "${command}" Enter`; + // tmux send-keys でコマンドを送信 (コマンドインジェクション対策) + const escapedCommand = escapeShellString(command); + const sendCommand = `tmux send-keys -t ${target} "${escapedCommand}" Enter`; try { // タイムアウト付きで実行 @@ -138,8 +157,9 @@ function buildCodexCommand(content: string, options: ReviewOptions): string { if (useFile) { // TODO: 一時ファイルを使用する場合 - // 今回は簡易的に引数渡し - return `codex review "${content.replace(/"/g, '\\"')}"`; + // 今回は簡易的に引数渡し (コマンドインジェクション対策) + const escapedContent = escapeShellString(content); + return `codex review "${escapedContent}"`; } // オプションを付与 @@ -157,7 +177,9 @@ function buildCodexCommand(content: string, options: ReviewOptions): string { opts.push("--verbose"); } - const cmd = `codex review ${opts.join(" ")} ${content}`; + // コマンドインジェクション対策: contentをエスケープしてクォート + const escapedContent = escapeShellString(content); + const cmd = `codex review ${opts.join(" ")} "${escapedContent}"`; return cmd; } diff --git a/src/theta/decide.ts b/src/theta/decide.ts index 544e12272..16763dbd5 100644 --- a/src/theta/decide.ts +++ b/src/theta/decide.ts @@ -31,10 +31,12 @@ interface AgentDefinition { /** 利用可能なエージェント一覧 */ const AVAILABLE_AGENTS: AgentDefinition[] = [ - { id: "conductor", intents: ["*"], capacity: 10 }, + // 具体的な意図を持つエージェントを先に配置 { id: "codegen", intents: ["create", "update"], capacity: 5 }, { id: "review", intents: ["verify", "check"], capacity: 3 }, { id: "deploy", intents: ["deploy", "release"], capacity: 2 }, + // ワイルドカードエージェントは最後に配置 (fallbackとして機能) + { id: "conductor", intents: ["*"], capacity: 10 }, ]; /** @@ -179,10 +181,21 @@ function selectStrategy(intent: string): string { /** * 意図に基づいてエージェントを選択する + * + * 具体的な意図へのマッチを優先し、ワイルドカードはfallbackとして使用 */ function selectAgent(intent: string): string | undefined { - const agent = AVAILABLE_AGENTS.find((a) => a.intents.includes(intent) || a.intents.includes("*")); - return agent?.id; + // まず具体的な意図を探す + const specificAgent = AVAILABLE_AGENTS.find( + (a) => a.intents.includes(intent) && !a.intents.includes("*"), + ); + if (specificAgent) { + return specificAgent.id; + } + + // 具体的なマッチがなければワイルドカードを探す + const wildcardAgent = AVAILABLE_AGENTS.find((a) => a.intents.includes("*")); + return wildcardAgent?.id; } /**