From 89c95de18cca89f43ab1103033b8763b633c7ba0 Mon Sep 17 00:00:00 2001 From: Variet Worker Date: Fri, 10 Apr 2026 08:02:41 +0900 Subject: [PATCH] fix(bridge): resolve missing Discord embed bodies by extracting detailed RPC payload from step_probe --- .agents/references/known-issues.md | 6 +++ docs/devlog/2026-04-09.md | 1 + extension/src/http-bridge.ts | 2 +- extension/src/step-probe.ts | 84 +++++++++++++----------------- test.js | 47 +++++++++++++++++ 5 files changed, 91 insertions(+), 49 deletions(-) create mode 100644 test.js diff --git a/.agents/references/known-issues.md b/.agents/references/known-issues.md index 32019cd..b7736e2 100644 --- a/.agents/references/known-issues.md +++ b/.agents/references/known-issues.md @@ -10,6 +10,12 @@ --- +### [2026-04-09] [Bridge] Discord Body Content Missing Due to Step Probe Dummy Payload +- **증상**: 대규모 UI 마이그레이션 후, 디스코드 승인 메시지 본문에 실행할 코드/명령어가 완전히 누락되고 "Step #15"와 같은 디폴트 텍스트만 전송됨. +- **원인**: Native UI 변경으로 인해 DOM observer가 추출한 버튼 텍스트("Always run")가 `http-bridge.ts` 필터 우회 및 bot.py에서 지연(defer) 처리됨. 반면 `step-probe.ts`가 `GetAllCascadeTrajectories` 폴링을 통해 동시에 발생시킨 dummy pending payload (명령어 상세 내용이 없이 `Step #XX` 라는 텍스트만 포함)가 봇에 의해 먼저 자동 승인되면서 정작 실제 코드 영역 정보가 증발함. +- **해결**: `step-probe.ts` 내에 `formatStepProbeCommand` 헬퍼 함수를 추가하여, WAITING 상태 스텝의 `argumentsJson` 데이터를 직접 파싱하고 `CommandLine`, `TargetFile` 등 실제 명령어와 상세 인자/코드를 `command`와 `description`으로 할당하여 브릿지로 넘기도록 패치함. DOM 옵저버의 불안정성과 관계없이 일관된 본문 전달 보장. +- **주의**: UI 스크래핑에 의존하는 DOM Observer 방식은 UI 레이아웃, 아이콘 삽입 등에 취약하므로, 상세 페이로드 추출은 항상 100% 신뢰 가능한 SDK RPC(`step-probe.ts`) 데이터를 우선 사용하도록 구성해야 함. + ## 포맷 ### [2026-03-23] [Extension] Cross-Project DOM Observer Leakage diff --git a/docs/devlog/2026-04-09.md b/docs/devlog/2026-04-09.md index 8924cb8..1f1ef5b 100644 --- a/docs/devlog/2026-04-09.md +++ b/docs/devlog/2026-04-09.md @@ -6,3 +6,4 @@ | 002 | 22:30 | Agent UI 버튼 무시 버그 긴급수정 (CodeLens 필터교정) | `HEAD` | ✅ | | 003 | 23:15 | Native UI 아이콘 글루잉 대응 스캐너 픽스 (DOM Regex 매칭 강화) | `HEAD` | ✅ | | 004 | 00:10 | Discord Signal Relay & Auto-Approve Body Null 버그 수정 (False Positive 차단) | "HEAD" | ✅ | +| 005 | 23:00 | fix: Resolve empty Discord embed body by populating detailed step-probe payload | \\ | ? | diff --git a/extension/src/http-bridge.ts b/extension/src/http-bridge.ts index 233de9a..ee5e4f4 100644 --- a/extension/src/http-bridge.ts +++ b/extension/src/http-bridge.ts @@ -213,7 +213,7 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) { } // "Run" button → step_probe handles these with full command detail // Only let through if session is stalled AND step_probe hasn't created a pending yet - if (/^Run\b/i.test(cmd)) { + if (/^(?:Always\s*)?Run\b/i.test(cmd)) { if (!ctx.sessionStalled || ctx.lastPendingStepIndex >= 0) { ctx.logToFile(`[HTTP] filtered "Run" — ${!ctx.sessionStalled ? 'not stalled' : 'step_probe pending exists'}`); res.writeHead(200, { 'Content-Type': 'application/json' }); diff --git a/extension/src/step-probe.ts b/extension/src/step-probe.ts index 94db34c..6bd7634 100644 --- a/extension/src/step-probe.ts +++ b/extension/src/step-probe.ts @@ -102,6 +102,36 @@ export function resetPendingStateForReconnect(): void { // handleDiffReviewResponse → moved to ./approval-handler.ts +export function formatStepProbeCommand(toolName: string, actualIndex: number, stepType: string, toolCall: any): { cmd: string, desc: string, isSafe: boolean } { + let cmd = toolName; + let desc = `Step #${actualIndex} (${stepType.replace('CORTEX_STEP_TYPE_', '')})`; + let isSafe = false; + if (toolCall?.argumentsJson) { + try { + const args = JSON.parse(toolCall.argumentsJson); + isSafe = args.SafeToAutoRun === true; + if (args.CommandLine) { + cmd = toolName; + desc = args.CommandLine; + } else if (args.TargetFile) { + cmd = `${toolName}: ${args.TargetFile.split(/[\\/]/).pop()}`; + if (args.CodeContent) desc = args.CodeContent; + else if (args.ReplacementChunks) desc = JSON.stringify(args.ReplacementChunks, null, 2); + else desc = toolCall.argumentsJson; + } else { + const val = args.DirectoryPath || args.SearchPath || args.AbsolutePath || args.Url || args.Query || args.Prompt || Object.values(args).find((v: any) => typeof v === 'string' && v.length > 2); + if (val) { + cmd = toolName; + desc = String(val); + } else { + cmd = `${toolName}: ${Object.keys(args).join(', ')}`; + } + } + } catch { } + } + return { cmd, desc, isSafe }; +} + /** * Write a registration file for the Bot to discover session → project mapping. * Called automatically on first step event per session. @@ -481,20 +511,8 @@ function setupMonitor() { if (oStep?.status === 'CORTEX_STEP_STATUS_WAITING') { const toolCall = oStep?.metadata?.toolCall; const toolName = toolCall?.name || (oStep.type || '').replace('CORTEX_STEP_TYPE_', '').toLowerCase(); - let command = toolName; - if (toolCall?.argumentsJson) { - try { - const args = JSON.parse(toolCall.argumentsJson); - if (args.CommandLine) command = `${toolName}: ${args.CommandLine.substring(0, 1500)}`; - else if (args.TargetFile) command = `${toolName}: ${args.TargetFile}`; - else { - // Show first meaningful value (path, query, etc.) - const val = args.DirectoryPath || args.SearchPath || args.AbsolutePath || args.Url || args.Query || args.Prompt || Object.values(args).find((v: any) => typeof v === 'string' && v.length > 2); - command = val ? `${toolName}: ${String(val).substring(0, 500)}` : `${toolName}: ${Object.keys(args).join(', ')}`; - } - } catch { command = toolName; } - } const actualIndex = offset + osi; + const { cmd: command, desc: detailedDescription, isSafe: isSafeToAutoRun } = formatStepProbeCommand(toolName, actualIndex, oStep.type || '', toolCall); ctx.logToFile(`[STEP-PROBE] ★ WAITING (via offset)! step=${actualIndex} type=${oStep.type} cmd='${command}'`); if (actualIndex !== ctx.lastPendingStepIndex) { ctx.stallProbed = true; @@ -512,13 +530,14 @@ function setupMonitor() { writePendingApproval({ conversation_id: ctx.activeSessionId, command, - description: `Step #${actualIndex} (${(oStep.type || '').replace('CORTEX_STEP_TYPE_', '')})`, + description: detailedDescription, step_type: ['view_file', 'list_dir', 'find_by_name', 'read_file', 'grep_search'].includes(toolName) ? 'file_permission' : ['write_to_file', 'replace_file_content', 'multi_replace_file_content'].includes(toolName) ? 'code_edit' : ['browser_subagent', 'open_browser_url'].includes(toolName) ? 'browser_subagent' : toolName, step_index: actualIndex, source: 'step_probe_offset', + safe_to_auto_run: isSafeToAutoRun, }); } } @@ -543,27 +562,9 @@ function setupMonitor() { // Extract command from metadata.toolCall or direct fields const toolCall = step?.metadata?.toolCall; const toolName = toolCall?.name || stepType.replace('CORTEX_STEP_TYPE_', '').toLowerCase(); - let command = toolName; - let isSafeToAutoRun = false; + const { cmd: command, desc: description, isSafe: isSafeToAutoRun } = formatStepProbeCommand(toolName, si, stepType, toolCall); - // Parse argumentsJson for command details - if (toolCall?.argumentsJson) { - try { - const args = JSON.parse(toolCall.argumentsJson); - isSafeToAutoRun = args.SafeToAutoRun === true; - if (args.CommandLine) { - command = `${toolName}: ${args.CommandLine.substring(0, 1500)}`; - } else if (args.TargetFile) { - command = `${toolName}: ${args.TargetFile}`; - } else { - const val = args.DirectoryPath || args.SearchPath || args.AbsolutePath || args.Url || args.Query || args.Prompt || Object.values(args).find((v: any) => typeof v === 'string' && v.length > 2); - command = val ? `${toolName}: ${String(val).substring(0, 500)}` : `${toolName}: ${Object.keys(args).join(', ')}`; - } - } catch { command = toolName; } - } - - const description = `Step #${si} (${stepType.replace('CORTEX_STEP_TYPE_', '')})`; ctx.logToFile(`[STEP-PROBE] ★ WAITING! step=${si} type=${stepType} cmd='${command}'`); if (si !== ctx.lastPendingStepIndex) { @@ -629,21 +630,8 @@ function setupMonitor() { foundWaitingInOffset = true; const toolCall = oStep?.metadata?.toolCall; const toolName = toolCall?.name || (oStep.type || '').replace('CORTEX_STEP_TYPE_', '').toLowerCase(); - let command = toolName; - let isSafeToAutoRun = false; - if (toolCall?.argumentsJson) { - try { - const args = JSON.parse(toolCall.argumentsJson); - isSafeToAutoRun = args.SafeToAutoRun === true; - if (args.CommandLine) command = `${toolName}: ${args.CommandLine.substring(0, 1500)}`; - else if (args.TargetFile) command = `${toolName}: ${args.TargetFile}`; - else { - const val = args.DirectoryPath || args.SearchPath || args.AbsolutePath || args.Url || args.Query || args.Prompt || Object.values(args).find((v: any) => typeof v === 'string' && v.length > 2); - command = val ? `${toolName}: ${String(val).substring(0, 500)}` : `${toolName}: ${Object.keys(args).join(', ')}`; - } - } catch { command = toolName; } - } const actualIndex = utf8Offset + osi; + const { cmd: command, desc: detailedDescription, isSafe: isSafeToAutoRun } = formatStepProbeCommand(toolName, actualIndex, oStep.type || '', toolCall); ctx.logToFile(`[STEP-PROBE] ★ WAITING (via UTF-8 offset)! step=${actualIndex} type=${oStep.type} cmd='${command}'`); if (actualIndex !== ctx.lastPendingStepIndex) { ctx.stallProbed = true; @@ -654,7 +642,7 @@ function setupMonitor() { writePendingApproval({ conversation_id: ctx.activeSessionId, command, - description: `Step #${actualIndex} (${(oStep.type || '').replace('CORTEX_STEP_TYPE_', '')})`, + description: detailedDescription, step_type: ['view_file', 'list_dir', 'find_by_name', 'read_file', 'grep_search'].includes(toolName) ? 'file_permission' : ['write_to_file', 'replace_file_content', 'multi_replace_file_content'].includes(toolName) ? 'code_edit' : ['browser_subagent', 'open_browser_url'].includes(toolName) ? 'browser_subagent' diff --git a/test.js b/test.js new file mode 100644 index 0000000..ebcfae3 --- /dev/null +++ b/test.js @@ -0,0 +1,47 @@ +const jsdom = require('jsdom'); +const { JSDOM } = jsdom; +const dom = new JSDOM($html); +const document = dom.window.document; + +function extractContext(b){ + var curr = b.parentElement; + var bestDesc = ''; + var btnText = (b.innerText || b.textContent || '').trim(); + + for (var i = 0; i < 15 && curr; i++) { + var codeEl = curr.querySelector('pre, code, [class*="command"], [class*="terminal"], [class*="code"]'); + if (codeEl && codeEl !== b && !b.contains(codeEl)) { + var codeText = (codeEl.innerText || codeEl.textContent || '').trim(); + if (codeText.length > 0 && codeText !== btnText) { + return codeText.substring(0, 500); + } + } + + var full = (curr.innerText || curr.textContent || ''); + var btnRawText = (b.textContent || ''); + var desc = full.replace(btnRawText, '').trim(); + if (desc.length > 5 && desc !== btnText && bestDesc.length < desc.length) { + bestDesc = desc; + } + + var cname = curr.className; + if (typeof cname === 'string' && (cname.includes('message') || cname.includes('step') || cname.includes('markdown'))) { + break; + } + curr = curr.parentElement; + } + return bestDesc.substring(0, 500); +} + +const btns = document.querySelectorAll('button'); +let ran = false; +for(let b of btns) { + let t = (b.textContent||'').trim(); + if(t === 'Always run' || t === 'Run') { + const desc = extractContext(b); + console.log("Found button: " + t); + console.log("Extracted Description: " + desc); + ran = true; + } +} +if(!ran) console.log("No matching button found");