From ed90cbf8748798d541ebad301e5da2ec64f09c3e Mon Sep 17 00:00:00 2001 From: Variet Worker Date: Wed, 15 Apr 2026 14:55:58 +0900 Subject: [PATCH] =?UTF-8?q?fix(observer/bridge):=20v14=20=E2=80=94=20stric?= =?UTF-8?q?t=205-level=20DOM=20scope,=20CSS/code/icon=20junk=20filter,=20a?= =?UTF-8?q?uto-version=20sync=20(v0.5.47)=20#task-619?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root causes fixed: 1. extractContextFromNearby depth 20→5 — stops grabbing unrelated UI/editor code 2. JUNK_CODE_RE — rejects CSS rules, JS source code, extension internals 3. ICON_GLUE_RE — rejects Material icon text glued with content 4. Fallback span/div/p collection REMOVED entirely (always grabbed chat text) 5. html-patcher strips old observer from integration.build() cache 6. http-bridge server-side JUNK_CONTENT_RE as last line of defense --- extension/package.json | 2 +- extension/src/html-patcher.ts | 11 ++++ extension/src/http-bridge.ts | 10 ++++ extension/src/observer-script.ts | 87 +++++++++++--------------------- 4 files changed, 51 insertions(+), 59 deletions(-) diff --git a/extension/package.json b/extension/package.json index 7ed88b4..05685cc 100644 --- a/extension/package.json +++ b/extension/package.json @@ -2,7 +2,7 @@ "name": "gravity-bridge", "displayName": "Gravity Bridge", "description": "Discord-based unified approval system for Antigravity AI interactions.", - "version": "0.5.46", + "version": "0.5.47", "publisher": "variet", "engines": { "vscode": "^1.100.0" diff --git a/extension/src/html-patcher.ts b/extension/src/html-patcher.ts index a32413f..9e3f5d8 100644 --- a/extension/src/html-patcher.ts +++ b/extension/src/html-patcher.ts @@ -55,6 +55,17 @@ export async function setupApprovalObserver( if (patcher && typeof patcher.getScriptPath === 'function') { let baseScript = ''; try { baseScript = integration.build(); } catch { baseScript = ''; } + // Strip old Gravity Bridge observer IIFE from baseScript. + // integration.build() caches the previous session's script, so without + // stripping, the old observer (e.g. v12) runs alongside the new one (v13), + // and the old one wins because it executes first. + if (baseScript.includes('Gravity Bridge v')) { + const oldVer = baseScript.match(/Gravity Bridge v(\d+)/); + const newVer = observerJS.match(/Gravity Bridge v(\d+)/); + logToFile(`[OBSERVER] baseScript contains old observer ${oldVer?.[0] || '?'}, new is ${newVer?.[0] || '?'} — stripping old`); + // Remove the old observer IIFE: starts with "// ── Gravity Bridge" comment, ends at the last "})();" before EOF or next section + baseScript = baseScript.replace(/\n?\/\/\s*[─═].*Gravity Bridge[\s\S]*$/, ''); + } const combinedScript = baseScript + '\n' + observerJS; const scriptPath = patcher.getScriptPath(); fs.writeFileSync(scriptPath, combinedScript, 'utf8'); diff --git a/extension/src/http-bridge.ts b/extension/src/http-bridge.ts index b80eb30..066925e 100644 --- a/extension/src/http-bridge.ts +++ b/extension/src/http-bridge.ts @@ -304,6 +304,16 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) { res.end(JSON.stringify({ ok: false, filtered: true })); return; } + // v14: Server-side junk content filter — CSS, source code, icon glue + // This is the last line of defense regardless of observer version + const JUNK_CONTENT_RE = /(!important|::selection|background-color:|var\(--|font-size:|border-[a-z]+:|padding:|margin:|display:\s|===|!==|\|\||\.test\(|\.match\(|\.replace\(|_RE[.\s]|\brawDesc\b|\brawCmd\b|\benrichedCmd\b|\bquerySelector\b)/; + const ICON_GLUE_RE = /(alternate_email|content_copy|content_paste|check_circle|chevron_right|chevron_left|keyboard_arrow|arrow_drop_down|arrow_drop_up|more_horiz|more_vert|expand_more|expand_less)[a-zA-Z]/; + if (JUNK_CONTENT_RE.test(cmd) || ICON_GLUE_RE.test(cmd)) { + ctx.logToFile(`[HTTP] filtered junk content: "${cmd.substring(0, 80)}"`); + res.writeHead(200, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ ok: false, filtered: true, reason: 'junk_content' })); + return; + } // "Run" button → step_probe handles these with full command detail // Only filter when step_probe IS actively tracking AND cmd is still generic button text if (/^(?:Always\s*)?Run\b/i.test(cmd)) { diff --git a/extension/src/observer-script.ts b/extension/src/observer-script.ts index 2011084..463595d 100644 --- a/extension/src/observer-script.ts +++ b/extension/src/observer-script.ts @@ -1,7 +1,7 @@ export function generateApprovalObserverScript(_port: number): string { return ` -// ── Gravity Bridge v13: Prompt-Skip + Fallback Guard ── -// v13: When all code els are prompt-only, disable fallback text collection entirely +// ── Gravity Bridge v14: Strict Scope + Junk Filter ── +// v14: Strict 5-level DOM scope, CSS/source code/icon-glue filters, no fallback (function(){ 'use strict'; var BASE='',_obs=false,_sent={},_ready=false; @@ -10,7 +10,7 @@ export function generateApprovalObserverScript(_port: number): string { var CLEANUP_MS=300000; function log(m){console.log('[GB Observer] '+m);} - log('v13 Script loaded — Prompt-Skip + Fallback Guard'); + log('v14 Script loaded — Strict Scope + Junk Filter'); // DIAGNOSTIC BEACON: immediate POST to confirm script execution in renderer try { @@ -109,40 +109,46 @@ export function generateApprovalObserverScript(_port: number): string { return null; } - // v13: Climb DOM tree to find context near the button - // Priority: pre/code (skip prompt-only) > substantial span/div/p text > aria-label > button text - // PROMPT_ONLY: matches terminal prompts like "...\project >" or "PS C:\...">" with no actual command - var PROMPT_ONLY_RE = /^[^\\n]*[\\/\>\xbb$#]\\s*$/; + // v14: Climb DOM tree to find context near the button + // STRICT SCOPE: Only 5 levels up — beyond that we're in unrelated UI territory. + // JUNK FILTERS: CSS rules, source code, Material icon gluing are all rejected. + // NO FALLBACK: span/div/p text collection is removed entirely — it always grabs chat/UI text. + var PROMPT_ONLY_RE = /^[^\\n]*[\\/\>\\xbb$#]\\s*$/; + // v14: Detect CSS rules, JS source code, or extension internals in code text + var JUNK_CODE_RE = /(!important|::selection|background-color:|var\\(--|font-size:|border-[a-z]|padding:|margin:|display:\\s|\\{[^}]*:[^}]*\\}|===|!==|\\|\\||\\bfunction\\s*\\(|\\bconst\\s+\\w+\\s*=|\\bvar\\s+\\w+\\s*=|\\bif\\s*\\(|\\breturn\\b|\\bimport\\s|\\bexport\\s|\\bclass\\s+\\w|\\bnew\\s+\\w|\\.test\\(|\\.match\\(|\\.replace\\(|_RE[.\\s]|\\brawDesc\\b|\\brawCmd\\b|\\benrichedCmd\\b|\\bquerySelector)/; + // v14: Detect Material icon text glued with content + var ICON_GLUE_RE = /(alternate_email|content_copy|content_paste|check_circle|chevron_right|chevron_left|keyboard_arrow|arrow_drop_down|arrow_drop_up|more_horiz|more_vert|expand_more|expand_less)[a-zA-Z]/; function extractContextFromNearby(btn) { var node = btn; var _debugTrail = []; - var _fallbackText = ''; // Best span/div/p text found (used if no pre/code) - var _bestCodeText = ''; // Best code text found across all depths + var _bestCodeText = ''; var _bestCodeHeader = ''; - var _sawCodeEls = false; // v13: did we encounter any code/pre elements? - var _promptOnlySkipped = false; // v13: were ALL code elements prompt-only? - for (var depth = 0; depth < 20 && node; depth++) { + var _sawCodeEls = false; + var _allSkipped = true; + for (var depth = 0; depth < 5 && node; depth++) { if (!node.querySelector) { _debugTrail.push('d'+depth+':noQS'); node = node.parentElement; continue; } - // Look for code/pre blocks (actual command text) var codeEls = node.querySelectorAll('pre, code, [class*="terminal"]'); _debugTrail.push('d'+depth+':tag='+((node.tagName||'?').toLowerCase())+',cls='+(((typeof node.className==='string')?node.className:'').substring(0,60))+',codeEls='+codeEls.length); - var _depthHadCode = false; - var _depthAllPrompt = true; for (var ci = 0; ci < codeEls.length; ci++) { var codeText = cleanLines((codeEls[ci].textContent || '').trim().substring(0, 500)); if (!codeText || codeText.length <= 5) continue; if (/^Running\\s*\\d/i.test(codeText)) continue; - _depthHadCode = true; - // v12: Skip prompt-only text (e.g. "...\gravity_control >") - no actual command + _sawCodeEls = true; if (PROMPT_ONLY_RE.test(codeText.trim())) { _debugTrail.push('skip_prompt_ci='+ci+':'+codeText.substring(0,30)); continue; } - _depthAllPrompt = false; - // This code element has actual content - capture it + if (JUNK_CODE_RE.test(codeText)) { + _debugTrail.push('skip_junk_ci='+ci+':'+codeText.substring(0,30)); + continue; + } + if (ICON_GLUE_RE.test(codeText)) { + _debugTrail.push('skip_iconglue_ci='+ci+':'+codeText.substring(0,30)); + continue; + } + _allSkipped = false; if (!_bestCodeText || codeText.length > _bestCodeText.length) { _bestCodeText = codeText; - // Also try to get a header/title near this container var headerEl = node.querySelector('h1, h2, h3, [class*="header"], [class*="title"], [class*="cursor-pointer"]'); if (headerEl) { var hClone = headerEl.cloneNode(true); @@ -154,56 +160,21 @@ export function generateApprovalObserverScript(_port: number): string { } } } - // v13: Track whether code elements were seen and all were prompt-only - if (_depthHadCode) { - _sawCodeEls = true; - if (_depthAllPrompt) _promptOnlySkipped = true; - } - // v12: If we found a good code text, return it immediately at this depth if (_bestCodeText) { var parts = []; if (_bestCodeHeader) parts.push(_bestCodeHeader); parts.push(_bestCodeText); log('CONTEXT-OK d='+depth+' src=code trail='+_debugTrail.join(' > ')); _lastContextDebug = _debugTrail.join(' > '); - return parts.join(' — '); - } - // v13: If we saw code elements but all were prompt-only, do NOT collect fallback text - // This prevents capturing chat messages / UI text from nearby DOM elements - if (_promptOnlySkipped) { - _debugTrail.push('fallback_blocked_prompt_only'); - } else if (depth <= 8 && !_fallbackText) { - // v11: Look for substantial text in span/div/p as fallback context - var textEls = node.querySelectorAll('span, div, p'); - for (var ti = 0; ti < Math.min(textEls.length, 20); ti++) { - var tText = (textEls[ti].textContent || '').trim(); - if (tText.length > 10 && tText.length < 500 && !isNoiseLine(tText)) { - // Skip if it's just the button text itself - var btnTxt = cleanButtonText(btn); - if (tText !== btnTxt && tText.indexOf(btnTxt) !== 0) { - _fallbackText = tText.substring(0, 300); - _debugTrail.push('fallback_d='+depth+':'+_fallbackText.substring(0,40)); - break; - } - } - } + return parts.join(' \u2014 '); } node = node.parentElement; } - // v13: If code elements existed but were all prompt-only, return just button text - // This ensures http-bridge's Run/Always run filter can handle it properly - if (_promptOnlySkipped) { - log('CONTEXT-PROMPT-ONLY trail='+_debugTrail.join(' > ')); + if (_sawCodeEls && _allSkipped) { + log('CONTEXT-SKIP-ALL trail='+_debugTrail.join(' > ')); _lastContextDebug = _debugTrail.join(' > '); return cleanButtonText(btn); } - // v11: Use fallback text from span/div/p if available - if (_fallbackText && _fallbackText.length > 5) { - log('CONTEXT-OK src=fallback trail='+_debugTrail.join(' > ')); - _lastContextDebug = _debugTrail.join(' > '); - return cleanLines(_fallbackText); - } - // Last resort: try aria-label or title on the button var ariaLabel = btn.getAttribute('aria-label') || btn.getAttribute('title') || ''; log('CONTEXT-FAIL trail='+_debugTrail.join(' > ')); _lastContextDebug = _debugTrail.join(' > ');