fix(observer/bridge): v14 — strict 5-level DOM scope, CSS/code/icon junk filter, auto-version sync (v0.5.47) #task-619

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
This commit is contained in:
Variet Worker
2026-04-15 14:55:58 +09:00
parent 2e32be96fe
commit ed90cbf874
4 changed files with 51 additions and 59 deletions

View File

@@ -2,7 +2,7 @@
"name": "gravity-bridge", "name": "gravity-bridge",
"displayName": "Gravity Bridge", "displayName": "Gravity Bridge",
"description": "Discord-based unified approval system for Antigravity AI interactions.", "description": "Discord-based unified approval system for Antigravity AI interactions.",
"version": "0.5.46", "version": "0.5.47",
"publisher": "variet", "publisher": "variet",
"engines": { "engines": {
"vscode": "^1.100.0" "vscode": "^1.100.0"

View File

@@ -55,6 +55,17 @@ export async function setupApprovalObserver(
if (patcher && typeof patcher.getScriptPath === 'function') { if (patcher && typeof patcher.getScriptPath === 'function') {
let baseScript = ''; let baseScript = '';
try { baseScript = integration.build(); } catch { 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 combinedScript = baseScript + '\n' + observerJS;
const scriptPath = patcher.getScriptPath(); const scriptPath = patcher.getScriptPath();
fs.writeFileSync(scriptPath, combinedScript, 'utf8'); fs.writeFileSync(scriptPath, combinedScript, 'utf8');

View File

@@ -304,6 +304,16 @@ function _handlePending(req: any, res: any, ctx: HttpBridgeContext) {
res.end(JSON.stringify({ ok: false, filtered: true })); res.end(JSON.stringify({ ok: false, filtered: true }));
return; 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 // "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 // Only filter when step_probe IS actively tracking AND cmd is still generic button text
if (/^(?:Always\s*)?Run\b/i.test(cmd)) { if (/^(?:Always\s*)?Run\b/i.test(cmd)) {

View File

@@ -1,7 +1,7 @@
export function generateApprovalObserverScript(_port: number): string { export function generateApprovalObserverScript(_port: number): string {
return ` return `
// ── Gravity Bridge v13: Prompt-Skip + Fallback Guard ── // ── Gravity Bridge v14: Strict Scope + Junk Filter ──
// v13: When all code els are prompt-only, disable fallback text collection entirely // v14: Strict 5-level DOM scope, CSS/source code/icon-glue filters, no fallback
(function(){ (function(){
'use strict'; 'use strict';
var BASE='',_obs=false,_sent={},_ready=false; var BASE='',_obs=false,_sent={},_ready=false;
@@ -10,7 +10,7 @@ export function generateApprovalObserverScript(_port: number): string {
var CLEANUP_MS=300000; var CLEANUP_MS=300000;
function log(m){console.log('[GB Observer] '+m);} 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 // DIAGNOSTIC BEACON: immediate POST to confirm script execution in renderer
try { try {
@@ -109,40 +109,46 @@ export function generateApprovalObserverScript(_port: number): string {
return null; return null;
} }
// v13: Climb DOM tree to find context near the button // v14: Climb DOM tree to find context near the button
// Priority: pre/code (skip prompt-only) > substantial span/div/p text > aria-label > button text // STRICT SCOPE: Only 5 levels up — beyond that we're in unrelated UI territory.
// PROMPT_ONLY: matches terminal prompts like "...\project >" or "PS C:\...">" with no actual command // JUNK FILTERS: CSS rules, source code, Material icon gluing are all rejected.
var PROMPT_ONLY_RE = /^[^\\n]*[\\/\>\xbb$#]\\s*$/; // 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) { function extractContextFromNearby(btn) {
var node = btn; var node = btn;
var _debugTrail = []; var _debugTrail = [];
var _fallbackText = ''; // Best span/div/p text found (used if no pre/code) var _bestCodeText = '';
var _bestCodeText = ''; // Best code text found across all depths
var _bestCodeHeader = ''; var _bestCodeHeader = '';
var _sawCodeEls = false; // v13: did we encounter any code/pre elements? var _sawCodeEls = false;
var _promptOnlySkipped = false; // v13: were ALL code elements prompt-only? var _allSkipped = true;
for (var depth = 0; depth < 20 && node; depth++) { for (var depth = 0; depth < 5 && node; depth++) {
if (!node.querySelector) { _debugTrail.push('d'+depth+':noQS'); node = node.parentElement; continue; } 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"]'); 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); _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++) { for (var ci = 0; ci < codeEls.length; ci++) {
var codeText = cleanLines((codeEls[ci].textContent || '').trim().substring(0, 500)); var codeText = cleanLines((codeEls[ci].textContent || '').trim().substring(0, 500));
if (!codeText || codeText.length <= 5) continue; if (!codeText || codeText.length <= 5) continue;
if (/^Running\\s*\\d/i.test(codeText)) continue; if (/^Running\\s*\\d/i.test(codeText)) continue;
_depthHadCode = true; _sawCodeEls = true;
// v12: Skip prompt-only text (e.g. "...\gravity_control >") - no actual command
if (PROMPT_ONLY_RE.test(codeText.trim())) { if (PROMPT_ONLY_RE.test(codeText.trim())) {
_debugTrail.push('skip_prompt_ci='+ci+':'+codeText.substring(0,30)); _debugTrail.push('skip_prompt_ci='+ci+':'+codeText.substring(0,30));
continue; continue;
} }
_depthAllPrompt = false; if (JUNK_CODE_RE.test(codeText)) {
// This code element has actual content - capture it _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) { if (!_bestCodeText || codeText.length > _bestCodeText.length) {
_bestCodeText = codeText; _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"]'); var headerEl = node.querySelector('h1, h2, h3, [class*="header"], [class*="title"], [class*="cursor-pointer"]');
if (headerEl) { if (headerEl) {
var hClone = headerEl.cloneNode(true); 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) { if (_bestCodeText) {
var parts = []; var parts = [];
if (_bestCodeHeader) parts.push(_bestCodeHeader); if (_bestCodeHeader) parts.push(_bestCodeHeader);
parts.push(_bestCodeText); parts.push(_bestCodeText);
log('CONTEXT-OK d='+depth+' src=code trail='+_debugTrail.join(' > ')); log('CONTEXT-OK d='+depth+' src=code trail='+_debugTrail.join(' > '));
_lastContextDebug = _debugTrail.join(' > '); _lastContextDebug = _debugTrail.join(' > ');
return parts.join(' '); return parts.join(' \u2014 ');
}
// 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;
}
}
}
} }
node = node.parentElement; node = node.parentElement;
} }
// v13: If code elements existed but were all prompt-only, return just button text if (_sawCodeEls && _allSkipped) {
// This ensures http-bridge's Run/Always run filter can handle it properly log('CONTEXT-SKIP-ALL trail='+_debugTrail.join(' > '));
if (_promptOnlySkipped) {
log('CONTEXT-PROMPT-ONLY trail='+_debugTrail.join(' > '));
_lastContextDebug = _debugTrail.join(' > '); _lastContextDebug = _debugTrail.join(' > ');
return cleanButtonText(btn); 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') || ''; var ariaLabel = btn.getAttribute('aria-label') || btn.getAttribute('title') || '';
log('CONTEXT-FAIL trail='+_debugTrail.join(' > ')); log('CONTEXT-FAIL trail='+_debugTrail.join(' > '));
_lastContextDebug = _debugTrail.join(' > '); _lastContextDebug = _debugTrail.join(' > ');