feat(bridge): deep-inspect HTTP endpoint + recursive DOM inspector #task-264

This commit is contained in:
2026-03-09 18:24:41 +09:00
parent dddbd2b96f
commit a07d9d3803
5 changed files with 756 additions and 89 deletions

View File

@@ -5,3 +5,4 @@
| 001 | 08:00~09:17 | 승인 실행 메커니즘 연구 + step-type별 VS Code 명령 분기 구현 | included in 002 | 🔧 | | 001 | 08:00~09:17 | 승인 실행 메커니즘 연구 + step-type별 VS Code 명령 분기 구현 | included in 002 | 🔧 |
| 002 | 09:21~15:07 | SDK 승인 명령 미등록 확정 + Renderer DOM Click 구현 | `4497e96` | 🔧 | | 002 | 09:21~15:07 | SDK 승인 명령 미등록 확정 + Renderer DOM Click 구현 | `4497e96` | 🔧 |
| 003 | 15:32~17:59 | Renderer v3 deep DOM traversal (iframe/webview/shadow 관통) | `32bf5ae` | 🔧 | | 003 | 15:32~17:59 | Renderer v3 deep DOM traversal (iframe/webview/shadow 관통) | `32bf5ae` | 🔧 |
| 004 | 18:08~18:23 | Deep inspect HTTP endpoint (/deep-inspect) + 렌더러 재귀 인스펙터 | | 🔧 |

View File

@@ -411,6 +411,10 @@ let observerHttpServer = null;
const pendingResponses = new Map(); const pendingResponses = new Map();
// Click trigger: extension sets this, renderer polls and clicks button // Click trigger: extension sets this, renderer polls and clicks button
let clickTrigger = null; let clickTrigger = null;
// Deep inspect trigger: curl sets this, renderer picks it up and POSTs results back
let deepInspectRequested = false;
let deepInspectResult = null;
let deepInspectWaiters = [];
/** Derive a deterministic port from project name (range 10000-60000) */ /** Derive a deterministic port from project name (range 10000-60000) */
function getDeterministicPort(name) { function getDeterministicPort(name) {
let hash = 0; let hash = 0;
@@ -507,6 +511,65 @@ function startObserverHttpBridge() {
} }
return; return;
} }
// GET /deep-inspect — trigger deep DOM inspection from renderer
if (req.method === 'GET' && url.pathname === '/deep-inspect') {
deepInspectRequested = true;
logToFile('[HTTP] deep-inspect triggered — waiting for renderer result...');
// Wait up to 10s for renderer to POST result
const timeout = setTimeout(() => {
deepInspectWaiters = deepInspectWaiters.filter(w => w !== waiter);
if (deepInspectResult) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(deepInspectResult));
}
else {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'timeout', message: 'Renderer did not respond in 10s. Is the v3 script loaded?' }));
}
}, 10000);
const waiter = (data) => {
clearTimeout(timeout);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(data));
};
deepInspectWaiters.push(waiter);
return;
}
// GET /deep-inspect-trigger — renderer polls this
if (req.method === 'GET' && url.pathname === '/deep-inspect-trigger') {
const requested = deepInspectRequested;
deepInspectRequested = false;
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ inspect: requested }));
return;
}
// POST /deep-inspect-result — renderer posts inspection results here
if (req.method === 'POST' && url.pathname === '/deep-inspect-result') {
let body = '';
req.on('data', (c) => body += c);
req.on('end', () => {
try {
const data = JSON.parse(body);
deepInspectResult = data;
logToFile(`[HTTP] deep-inspect result received (${body.length} bytes)`);
// Write to file for reference
const inspectFile = path.join(bridgePath, 'deep-inspect-result.json');
fs.writeFileSync(inspectFile, JSON.stringify(data, null, 2));
// Notify waiters
const waiters = [...deepInspectWaiters];
deepInspectWaiters = [];
waiters.forEach(w => w(data));
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true }));
}
catch (e) {
logToFile(`[HTTP] deep-inspect-result parse error: ${e.message}`);
res.writeHead(400);
res.end(JSON.stringify({ error: e.message }));
}
});
return;
}
// GET /ping — health check // GET /ping — health check
if (url.pathname === '/ping') { if (url.pathname === '/ping') {
res.writeHead(200); res.writeHead(200);
@@ -642,55 +705,133 @@ function generateApprovalObserverScript(_port) {
}catch(e){} }catch(e){}
} }
// ── DOM Structure Dump (one-time on startup) ── // ── Deep DOM Inspector (recursive, POSTs results to bridge) ──
function dumpDOMStructure(){ function runDeepInspect(){
if(_domDumped)return; var result={timestamp:new Date().toISOString(),windowURL:window.location.href,windowOrigin:window.location.origin,windowProtocol:window.location.protocol,framesCount:window.frames.length,nodes:[]};
_domDumped=true; log('DEEP-INSPECT: starting recursive DOM analysis...');
try{
// Count iframes function inspectDoc(doc,depth,label){
var iframes=document.querySelectorAll('iframe'); var node={label:label,depth:depth,accessible:true,url:'',buttons:[],roleBtns:[],iframes:[],webviews:[],shadowDOMs:0,totalElements:0};
log('DOM-DUMP: '+iframes.length+' iframes found'); if(!doc){node.accessible=false;node.error='null document';result.nodes.push(node);return;}
for(var i=0;i<iframes.length;i++){ try{node.url=(doc.URL||doc.documentURI||'unknown').substring(0,200);}catch(e){node.url='blocked';}
var f=iframes[i]; try{node.title=(doc.title||'').substring(0,100);}catch(e){}
var fInfo=' iframe#'+i+' class="'+f.className.substring(0,50)+'" src="'+(f.src||'').substring(0,80)+'"'; try{node.readyState=doc.readyState;}catch(e){}
try{
var idoc=f.contentDocument; // CSP
if(idoc){ try{
var btns=idoc.querySelectorAll('button'); var csp=doc.querySelectorAll('meta[http-equiv="Content-Security-Policy"]');
fInfo+=' → ACCESSIBLE ('+btns.length+' buttons)'; if(csp.length>0){node.csp=[];for(var c=0;c<csp.length;c++){node.csp.push((csp[c].content||'').substring(0,200));}}
// Dump first 5 button texts }catch(e){}
for(var b=0;b<Math.min(5,btns.length);b++){
log(' btn['+b+']: "'+((btns[b].textContent||'').trim()).substring(0,40)+'"'); try{
} var allEls=doc.querySelectorAll('*');
} node.totalElements=allEls.length;
}catch(e){ // Buttons
fInfo+=' → BLOCKED ('+e.message.substring(0,40)+')'; var btns=doc.querySelectorAll('button');
for(var i=0;i<btns.length;i++){
var b=btns[i];
var txt=(b.textContent||'').trim().substring(0,80);
if(!txt)continue;
var cls=(b.className||'').substring(0,60);
var disabled=b.disabled;
var hidden=b.hidden||false;
try{if(!b.offsetParent&&b.style.display!=='fixed')hidden=true;}catch(e){}
var aria=b.getAttribute('aria-label')||'';
var ttl=b.getAttribute('title')||'';
node.buttons.push({text:txt,class:cls,disabled:disabled,hidden:hidden,aria:aria,title:ttl});
} }
log(fInfo); // role=button
} var rbs=doc.querySelectorAll('[role="button"]');
// Count webview elements for(var r=0;r<rbs.length;r++){
var webviews=document.querySelectorAll('webview'); if(rbs[r].tagName==='BUTTON')continue;
log('DOM-DUMP: '+webviews.length+' <webview> elements'); var rtxt=(rbs[r].textContent||'').trim().substring(0,60);
for(var w=0;w<webviews.length;w++){ node.roleBtns.push({tag:rbs[r].tagName.toLowerCase(),text:rtxt});
var wInfo=' webview#'+w+' src="'+(webviews[w].src||'').substring(0,80)+'"'; }
// Shadow DOMs
for(var s=0;s<allEls.length;s++){
var sr=allEls[s].shadowRoot;
if(sr){node.shadowDOMs++;inspectDoc(sr,depth+1,'shadow(<'+allEls[s].tagName.toLowerCase()+' class="'+(allEls[s].className||'').substring(0,30)+'">)');}
}
// Iframes
var ifs=doc.querySelectorAll('iframe');
for(var fi=0;fi<ifs.length;fi++){
var f=ifs[fi];
var finfo={index:fi,class:(f.className||'').substring(0,60),src:(f.src||'').substring(0,150),id:f.id||'',sandbox:f.getAttribute('sandbox')||'',allow:f.getAttribute('allow')||'',accessible:false,cwExists:false,cwFrames:0};
try{
var idoc=f.contentDocument||(f.contentWindow&&f.contentWindow.document);
if(idoc){finfo.accessible=true;inspectDoc(idoc,depth+1,'iframe#'+fi+'('+finfo.class.substring(0,30)+')');
}else{finfo.error='contentDocument=null';}
}catch(e){finfo.error=e.message.substring(0,80);}
try{var cw=f.contentWindow;if(cw){finfo.cwExists=true;finfo.cwFrames=cw.length;try{finfo.cwLocation=cw.location.href;}catch(e2){finfo.cwLocation='blocked: '+e2.message.substring(0,40);}}}
catch(e){}
node.iframes.push(finfo);
}
// Webviews
var wvs=doc.querySelectorAll('webview');
for(var wi=0;wi<wvs.length;wi++){
var wv=wvs[wi];
var winfo={index:wi,src:(wv.src||'').substring(0,150),class:(wv.className||'').substring(0,60),partition:wv.getAttribute('partition')||'',preload:wv.getAttribute('preload')||'',nodeintegration:wv.getAttribute('nodeintegration')||'',webpreferences:wv.getAttribute('webpreferences')||'',hasExecJS:typeof wv.executeJavaScript==='function',contentDocAccessible:false};
try{var wdoc=wv.contentDocument;if(wdoc){winfo.contentDocAccessible=true;inspectDoc(wdoc,depth+1,'webview#'+wi+'.contentDocument');}}catch(e){winfo.contentDocError=e.message.substring(0,60);}
node.webviews.push(winfo);
}
}catch(e){node.error=e.message;}
result.nodes.push(node);
return node;
}
inspectDoc(document,0,'MainDocument');
// Webview executeJavaScript probe (async)
var webviews=document.querySelectorAll('webview');
var probesPending=webviews.length;
result.webviewProbes=[];
if(probesPending===0)postResults();
for(var pw=0;pw<webviews.length;pw++){
(function(wv,idx){
if(typeof wv.executeJavaScript!=='function'){result.webviewProbes.push({index:idx,error:'executeJavaScript not available'});probesPending--;if(probesPending<=0)postResults();return;}
try{ try{
var wdoc=webviews[w].contentDocument; wv.executeJavaScript('(function(){var btns=document.querySelectorAll("button");var allEls=document.querySelectorAll("*");var ifs=document.querySelectorAll("iframe");var wvs=document.querySelectorAll("webview");var btnArr=[];for(var i=0;i<btns.length;i++){var b=btns[i];var txt=(b.textContent||"").trim();var cls=(b.className||"").substring(0,50);var dis=b.disabled;var hid=b.hidden||!b.offsetParent;btnArr.push({text:txt.substring(0,60),class:cls,disabled:dis,hidden:hid,aria:b.getAttribute("aria-label")||"",title:b.getAttribute("title")||""});}var rbs=document.querySelectorAll("[role=button]");var rbArr=[];for(var j=0;j<rbs.length;j++){if(rbs[j].tagName!=="BUTTON")rbArr.push({tag:rbs[j].tagName.toLowerCase(),text:(rbs[j].textContent||"").trim().substring(0,40)});}var sc=0;for(var k=0;k<allEls.length;k++){if(allEls[k].shadowRoot)sc++;}return JSON.stringify({url:document.URL,title:document.title,totalElements:allEls.length,buttons:btnArr,roleBtns:rbArr,iframes:ifs.length,webviews:wvs.length,shadowDOMs:sc});})()')
if(wdoc)wInfo+=' → ACCESSIBLE'; .then(function(r){
else wInfo+=' → null contentDocument'; try{var d=JSON.parse(r);result.webviewProbes.push({index:idx,success:true,data:d});log('DEEP-INSPECT: webview#'+idx+' probe OK: '+d.buttons.length+' buttons, '+d.totalElements+' elements');}catch(e){result.webviewProbes.push({index:idx,parseError:e.message,raw:r});}
}catch(e){wInfo+=' → BLOCKED ('+e.message.substring(0,40)+')';} probesPending--;if(probesPending<=0)postResults();
log(wInfo); })
.catch(function(e){
result.webviewProbes.push({index:idx,execError:e.message});
log('DEEP-INSPECT: webview#'+idx+' execJS error: '+e.message);
probesPending--;if(probesPending<=0)postResults();
});
}catch(e){
result.webviewProbes.push({index:idx,callError:e.message});
probesPending--;if(probesPending<=0)postResults();
}
})(webviews[pw],pw);
}
function postResults(){
var summary='nodes='+result.nodes.length;
var totalBtns=0;for(var n=0;n<result.nodes.length;n++)totalBtns+=result.nodes[n].buttons.length;
summary+=' totalButtons='+totalBtns+' webviewProbes='+result.webviewProbes.length;
log('DEEP-INSPECT complete: '+summary);
// Also log buttons from each node
for(var n2=0;n2<result.nodes.length;n2++){
var nd=result.nodes[n2];
if(nd.buttons.length>0){
log(' '+nd.label+': '+nd.buttons.length+' buttons');
for(var bi=0;bi<Math.min(15,nd.buttons.length);bi++){
log(' ['+bi+'] "'+nd.buttons[bi].text+'"'+(nd.buttons[bi].disabled?' DISABLED':'')+(nd.buttons[bi].hidden?' HIDDEN':''));
}
}
} }
// Main doc buttons // POST to bridge
var mainBtns=document.querySelectorAll('button'); fetch(BASE+'/deep-inspect-result',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(result)})
log('DOM-DUMP: '+mainBtns.length+' buttons in main document'); .then(function(){log('DEEP-INSPECT results posted to bridge');})
for(var m=0;m<Math.min(10,mainBtns.length);m++){ .catch(function(e){log('DEEP-INSPECT post error: '+e.message);});
log(' main-btn['+m+']: "'+((mainBtns[m].textContent||'').trim()).substring(0,50)+'"'); }
}
// Report to bridge
fetch(BASE+'/ping?dom_dump='+iframes.length+'f_'+webviews.length+'wv_'+mainBtns.length+'btn').catch(function(){});
}catch(e){log('DOM-DUMP error: '+e.message);}
} }
// Auto-dump on startup (3s delay)
function dumpDOMStructure(){runDeepInspect();}
// ── Port Discovery: async fetch()-based (sync XHR blocked in Electron renderer) ── // ── Port Discovery: async fetch()-based (sync XHR blocked in Electron renderer) ──
var HARDCODED_PORT=${_port}; var HARDCODED_PORT=${_port};
@@ -975,6 +1116,14 @@ function generateApprovalObserverScript(_port) {
// FALLBACK: periodic scan every 3s for any missed mutations // FALLBACK: periodic scan every 3s for any missed mutations
setInterval(scheduleScan,3000); setInterval(scheduleScan,3000);
// ── DEEP-INSPECT POLLING: curl→Bridge→Renderer→Results ──
setInterval(function(){
if(!_ready||!BASE)return;
fetch(BASE+'/deep-inspect-trigger?t='+Date.now()).then(function(r){return r.json();}).then(function(d){
if(d.inspect){log('🔍 Deep inspect triggered via HTTP');runDeepInspect();}
}).catch(function(){});
},2000);
// ── TRIGGER-CLICK: Extension→Renderer bridge for programmatic button clicks ── // ── TRIGGER-CLICK: Extension→Renderer bridge for programmatic button clicks ──
// Extension sets clickTrigger via tryApprovalStrategies → renderer polls and clicks // Extension sets clickTrigger via tryApprovalStrategies → renderer polls and clicks
// v3: uses deepFindButtons() to traverse iframes, webviews, shadow DOMs // v3: uses deepFindButtons() to traverse iframes, webviews, shadow DOMs

File diff suppressed because one or more lines are too long

View File

@@ -385,6 +385,11 @@ const pendingResponses = new Map<string, { approved: boolean } | null>();
// Click trigger: extension sets this, renderer polls and clicks button // Click trigger: extension sets this, renderer polls and clicks button
let clickTrigger: { action: 'approve' | 'reject'; timestamp: number } | null = null; let clickTrigger: { action: 'approve' | 'reject'; timestamp: number } | null = null;
// Deep inspect trigger: curl sets this, renderer picks it up and POSTs results back
let deepInspectRequested = false;
let deepInspectResult: any = null;
let deepInspectWaiters: Array<(data: any) => void> = [];
/** Derive a deterministic port from project name (range 10000-60000) */ /** Derive a deterministic port from project name (range 10000-60000) */
function getDeterministicPort(name: string): number { function getDeterministicPort(name: string): number {
let hash = 0; let hash = 0;
@@ -475,6 +480,65 @@ function startObserverHttpBridge(): Promise<number> {
return; return;
} }
// GET /deep-inspect — trigger deep DOM inspection from renderer
if (req.method === 'GET' && url.pathname === '/deep-inspect') {
deepInspectRequested = true;
logToFile('[HTTP] deep-inspect triggered — waiting for renderer result...');
// Wait up to 10s for renderer to POST result
const timeout = setTimeout(() => {
deepInspectWaiters = deepInspectWaiters.filter(w => w !== waiter);
if (deepInspectResult) {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(deepInspectResult));
} else {
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ status: 'timeout', message: 'Renderer did not respond in 10s. Is the v3 script loaded?' }));
}
}, 10000);
const waiter = (data: any) => {
clearTimeout(timeout);
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(data));
};
deepInspectWaiters.push(waiter);
return;
}
// GET /deep-inspect-trigger — renderer polls this
if (req.method === 'GET' && url.pathname === '/deep-inspect-trigger') {
const requested = deepInspectRequested;
deepInspectRequested = false;
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ inspect: requested }));
return;
}
// POST /deep-inspect-result — renderer posts inspection results here
if (req.method === 'POST' && url.pathname === '/deep-inspect-result') {
let body = '';
req.on('data', (c: string) => body += c);
req.on('end', () => {
try {
const data = JSON.parse(body);
deepInspectResult = data;
logToFile(`[HTTP] deep-inspect result received (${body.length} bytes)`);
// Write to file for reference
const inspectFile = path.join(bridgePath, 'deep-inspect-result.json');
fs.writeFileSync(inspectFile, JSON.stringify(data, null, 2));
// Notify waiters
const waiters = [...deepInspectWaiters];
deepInspectWaiters = [];
waiters.forEach(w => w(data));
res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ ok: true }));
} catch (e: any) {
logToFile(`[HTTP] deep-inspect-result parse error: ${e.message}`);
res.writeHead(400); res.end(JSON.stringify({ error: e.message }));
}
});
return;
}
// GET /ping — health check // GET /ping — health check
if (url.pathname === '/ping') { if (url.pathname === '/ping') {
res.writeHead(200); res.end('pong'); res.writeHead(200); res.end('pong');
@@ -614,55 +678,133 @@ function generateApprovalObserverScript(_port: number): string {
}catch(e){} }catch(e){}
} }
// ── DOM Structure Dump (one-time on startup) ── // ── Deep DOM Inspector (recursive, POSTs results to bridge) ──
function dumpDOMStructure(){ function runDeepInspect(){
if(_domDumped)return; var result={timestamp:new Date().toISOString(),windowURL:window.location.href,windowOrigin:window.location.origin,windowProtocol:window.location.protocol,framesCount:window.frames.length,nodes:[]};
_domDumped=true; log('DEEP-INSPECT: starting recursive DOM analysis...');
try{
// Count iframes function inspectDoc(doc,depth,label){
var iframes=document.querySelectorAll('iframe'); var node={label:label,depth:depth,accessible:true,url:'',buttons:[],roleBtns:[],iframes:[],webviews:[],shadowDOMs:0,totalElements:0};
log('DOM-DUMP: '+iframes.length+' iframes found'); if(!doc){node.accessible=false;node.error='null document';result.nodes.push(node);return;}
for(var i=0;i<iframes.length;i++){ try{node.url=(doc.URL||doc.documentURI||'unknown').substring(0,200);}catch(e){node.url='blocked';}
var f=iframes[i]; try{node.title=(doc.title||'').substring(0,100);}catch(e){}
var fInfo=' iframe#'+i+' class="'+f.className.substring(0,50)+'" src="'+(f.src||'').substring(0,80)+'"'; try{node.readyState=doc.readyState;}catch(e){}
try{
var idoc=f.contentDocument; // CSP
if(idoc){ try{
var btns=idoc.querySelectorAll('button'); var csp=doc.querySelectorAll('meta[http-equiv="Content-Security-Policy"]');
fInfo+=' → ACCESSIBLE ('+btns.length+' buttons)'; if(csp.length>0){node.csp=[];for(var c=0;c<csp.length;c++){node.csp.push((csp[c].content||'').substring(0,200));}}
// Dump first 5 button texts }catch(e){}
for(var b=0;b<Math.min(5,btns.length);b++){
log(' btn['+b+']: "'+((btns[b].textContent||'').trim()).substring(0,40)+'"'); try{
} var allEls=doc.querySelectorAll('*');
} node.totalElements=allEls.length;
}catch(e){ // Buttons
fInfo+=' → BLOCKED ('+e.message.substring(0,40)+')'; var btns=doc.querySelectorAll('button');
for(var i=0;i<btns.length;i++){
var b=btns[i];
var txt=(b.textContent||'').trim().substring(0,80);
if(!txt)continue;
var cls=(b.className||'').substring(0,60);
var disabled=b.disabled;
var hidden=b.hidden||false;
try{if(!b.offsetParent&&b.style.display!=='fixed')hidden=true;}catch(e){}
var aria=b.getAttribute('aria-label')||'';
var ttl=b.getAttribute('title')||'';
node.buttons.push({text:txt,class:cls,disabled:disabled,hidden:hidden,aria:aria,title:ttl});
} }
log(fInfo); // role=button
} var rbs=doc.querySelectorAll('[role="button"]');
// Count webview elements for(var r=0;r<rbs.length;r++){
var webviews=document.querySelectorAll('webview'); if(rbs[r].tagName==='BUTTON')continue;
log('DOM-DUMP: '+webviews.length+' <webview> elements'); var rtxt=(rbs[r].textContent||'').trim().substring(0,60);
for(var w=0;w<webviews.length;w++){ node.roleBtns.push({tag:rbs[r].tagName.toLowerCase(),text:rtxt});
var wInfo=' webview#'+w+' src="'+(webviews[w].src||'').substring(0,80)+'"'; }
// Shadow DOMs
for(var s=0;s<allEls.length;s++){
var sr=allEls[s].shadowRoot;
if(sr){node.shadowDOMs++;inspectDoc(sr,depth+1,'shadow(<'+allEls[s].tagName.toLowerCase()+' class="'+(allEls[s].className||'').substring(0,30)+'">)');}
}
// Iframes
var ifs=doc.querySelectorAll('iframe');
for(var fi=0;fi<ifs.length;fi++){
var f=ifs[fi];
var finfo={index:fi,class:(f.className||'').substring(0,60),src:(f.src||'').substring(0,150),id:f.id||'',sandbox:f.getAttribute('sandbox')||'',allow:f.getAttribute('allow')||'',accessible:false,cwExists:false,cwFrames:0};
try{
var idoc=f.contentDocument||(f.contentWindow&&f.contentWindow.document);
if(idoc){finfo.accessible=true;inspectDoc(idoc,depth+1,'iframe#'+fi+'('+finfo.class.substring(0,30)+')');
}else{finfo.error='contentDocument=null';}
}catch(e){finfo.error=e.message.substring(0,80);}
try{var cw=f.contentWindow;if(cw){finfo.cwExists=true;finfo.cwFrames=cw.length;try{finfo.cwLocation=cw.location.href;}catch(e2){finfo.cwLocation='blocked: '+e2.message.substring(0,40);}}}
catch(e){}
node.iframes.push(finfo);
}
// Webviews
var wvs=doc.querySelectorAll('webview');
for(var wi=0;wi<wvs.length;wi++){
var wv=wvs[wi];
var winfo={index:wi,src:(wv.src||'').substring(0,150),class:(wv.className||'').substring(0,60),partition:wv.getAttribute('partition')||'',preload:wv.getAttribute('preload')||'',nodeintegration:wv.getAttribute('nodeintegration')||'',webpreferences:wv.getAttribute('webpreferences')||'',hasExecJS:typeof wv.executeJavaScript==='function',contentDocAccessible:false};
try{var wdoc=wv.contentDocument;if(wdoc){winfo.contentDocAccessible=true;inspectDoc(wdoc,depth+1,'webview#'+wi+'.contentDocument');}}catch(e){winfo.contentDocError=e.message.substring(0,60);}
node.webviews.push(winfo);
}
}catch(e){node.error=e.message;}
result.nodes.push(node);
return node;
}
inspectDoc(document,0,'MainDocument');
// Webview executeJavaScript probe (async)
var webviews=document.querySelectorAll('webview');
var probesPending=webviews.length;
result.webviewProbes=[];
if(probesPending===0)postResults();
for(var pw=0;pw<webviews.length;pw++){
(function(wv,idx){
if(typeof wv.executeJavaScript!=='function'){result.webviewProbes.push({index:idx,error:'executeJavaScript not available'});probesPending--;if(probesPending<=0)postResults();return;}
try{ try{
var wdoc=webviews[w].contentDocument; wv.executeJavaScript('(function(){var btns=document.querySelectorAll("button");var allEls=document.querySelectorAll("*");var ifs=document.querySelectorAll("iframe");var wvs=document.querySelectorAll("webview");var btnArr=[];for(var i=0;i<btns.length;i++){var b=btns[i];var txt=(b.textContent||"").trim();var cls=(b.className||"").substring(0,50);var dis=b.disabled;var hid=b.hidden||!b.offsetParent;btnArr.push({text:txt.substring(0,60),class:cls,disabled:dis,hidden:hid,aria:b.getAttribute("aria-label")||"",title:b.getAttribute("title")||""});}var rbs=document.querySelectorAll("[role=button]");var rbArr=[];for(var j=0;j<rbs.length;j++){if(rbs[j].tagName!=="BUTTON")rbArr.push({tag:rbs[j].tagName.toLowerCase(),text:(rbs[j].textContent||"").trim().substring(0,40)});}var sc=0;for(var k=0;k<allEls.length;k++){if(allEls[k].shadowRoot)sc++;}return JSON.stringify({url:document.URL,title:document.title,totalElements:allEls.length,buttons:btnArr,roleBtns:rbArr,iframes:ifs.length,webviews:wvs.length,shadowDOMs:sc});})()')
if(wdoc)wInfo+=' → ACCESSIBLE'; .then(function(r){
else wInfo+=' → null contentDocument'; try{var d=JSON.parse(r);result.webviewProbes.push({index:idx,success:true,data:d});log('DEEP-INSPECT: webview#'+idx+' probe OK: '+d.buttons.length+' buttons, '+d.totalElements+' elements');}catch(e){result.webviewProbes.push({index:idx,parseError:e.message,raw:r});}
}catch(e){wInfo+=' → BLOCKED ('+e.message.substring(0,40)+')';} probesPending--;if(probesPending<=0)postResults();
log(wInfo); })
.catch(function(e){
result.webviewProbes.push({index:idx,execError:e.message});
log('DEEP-INSPECT: webview#'+idx+' execJS error: '+e.message);
probesPending--;if(probesPending<=0)postResults();
});
}catch(e){
result.webviewProbes.push({index:idx,callError:e.message});
probesPending--;if(probesPending<=0)postResults();
}
})(webviews[pw],pw);
}
function postResults(){
var summary='nodes='+result.nodes.length;
var totalBtns=0;for(var n=0;n<result.nodes.length;n++)totalBtns+=result.nodes[n].buttons.length;
summary+=' totalButtons='+totalBtns+' webviewProbes='+result.webviewProbes.length;
log('DEEP-INSPECT complete: '+summary);
// Also log buttons from each node
for(var n2=0;n2<result.nodes.length;n2++){
var nd=result.nodes[n2];
if(nd.buttons.length>0){
log(' '+nd.label+': '+nd.buttons.length+' buttons');
for(var bi=0;bi<Math.min(15,nd.buttons.length);bi++){
log(' ['+bi+'] "'+nd.buttons[bi].text+'"'+(nd.buttons[bi].disabled?' DISABLED':'')+(nd.buttons[bi].hidden?' HIDDEN':''));
}
}
} }
// Main doc buttons // POST to bridge
var mainBtns=document.querySelectorAll('button'); fetch(BASE+'/deep-inspect-result',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(result)})
log('DOM-DUMP: '+mainBtns.length+' buttons in main document'); .then(function(){log('DEEP-INSPECT results posted to bridge');})
for(var m=0;m<Math.min(10,mainBtns.length);m++){ .catch(function(e){log('DEEP-INSPECT post error: '+e.message);});
log(' main-btn['+m+']: "'+((mainBtns[m].textContent||'').trim()).substring(0,50)+'"'); }
}
// Report to bridge
fetch(BASE+'/ping?dom_dump='+iframes.length+'f_'+webviews.length+'wv_'+mainBtns.length+'btn').catch(function(){});
}catch(e){log('DOM-DUMP error: '+e.message);}
} }
// Auto-dump on startup (3s delay)
function dumpDOMStructure(){runDeepInspect();}
// ── Port Discovery: async fetch()-based (sync XHR blocked in Electron renderer) ── // ── Port Discovery: async fetch()-based (sync XHR blocked in Electron renderer) ──
var HARDCODED_PORT=${_port}; var HARDCODED_PORT=${_port};
@@ -947,6 +1089,14 @@ function generateApprovalObserverScript(_port: number): string {
// FALLBACK: periodic scan every 3s for any missed mutations // FALLBACK: periodic scan every 3s for any missed mutations
setInterval(scheduleScan,3000); setInterval(scheduleScan,3000);
// ── DEEP-INSPECT POLLING: curl→Bridge→Renderer→Results ──
setInterval(function(){
if(!_ready||!BASE)return;
fetch(BASE+'/deep-inspect-trigger?t='+Date.now()).then(function(r){return r.json();}).then(function(d){
if(d.inspect){log('🔍 Deep inspect triggered via HTTP');runDeepInspect();}
}).catch(function(){});
},2000);
// ── TRIGGER-CLICK: Extension→Renderer bridge for programmatic button clicks ── // ── TRIGGER-CLICK: Extension→Renderer bridge for programmatic button clicks ──
// Extension sets clickTrigger via tryApprovalStrategies → renderer polls and clicks // Extension sets clickTrigger via tryApprovalStrategies → renderer polls and clicks
// v3: uses deepFindButtons() to traverse iframes, webviews, shadow DOMs // v3: uses deepFindButtons() to traverse iframes, webviews, shadow DOMs

View File

@@ -0,0 +1,367 @@
// ═══════════════════════════════════════════════════════════════════
// Deep Iframe Inspector for Antigravity
// ═══════════════════════════════════════════════════════════════════
// Usage: Paste this entire script into the AG Renderer DevTools console
// (Ctrl+Shift+I → Console tab in the main workbench window)
//
// This script recursively inspects ALL iframes, webview elements,
// and shadow DOMs to map the complete DOM tree including:
// - iframe nesting depth and origin info
// - webview elements and their executeJavaScript availability
// - shadow DOM roots
// - button inventory at each level
// - Content Security Policy headers
// - Cross-origin accessibility status
// ═══════════════════════════════════════════════════════════════════
(function deepIframeInspector() {
'use strict';
const SEP = '─'.repeat(70);
const results = [];
let nodeCount = 0;
function log(msg) {
console.log('[DEEP-INSPECT] ' + msg);
results.push(msg);
}
function indent(depth) {
return ' '.repeat(depth);
}
// ── Inspect a single document context ──
function inspectDocument(doc, depth, label) {
nodeCount++;
const id = `node_${nodeCount}`;
const pfx = indent(depth);
log(`${pfx}┌─ ${label} (depth=${depth})`);
if (!doc) {
log(`${pfx}│ ⛔ document is null/undefined`);
log(`${pfx}└─ END ${label}`);
return;
}
// Basic doc info
try {
log(`${pfx}│ URL: ${(doc.URL || doc.documentURI || 'unknown').substring(0, 120)}`);
log(`${pfx}│ title: "${(doc.title || '').substring(0, 80)}"`);
log(`${pfx}│ readyState: ${doc.readyState}`);
log(`${pfx}│ domain: ${doc.domain || 'N/A'}`);
} catch (e) {
log(`${pfx}│ ⛔ Cannot read doc properties: ${e.message}`);
}
// CSP meta tags
try {
const cspMetas = doc.querySelectorAll('meta[http-equiv="Content-Security-Policy"]');
if (cspMetas.length > 0) {
for (let i = 0; i < cspMetas.length; i++) {
log(`${pfx}│ CSP[${i}]: ${(cspMetas[i].content || '').substring(0, 150)}`);
}
}
} catch (e) {}
// Count key elements
try {
const allEls = doc.querySelectorAll('*');
const buttons = doc.querySelectorAll('button');
const inputs = doc.querySelectorAll('input[type="button"],input[type="submit"]');
const anchors = doc.querySelectorAll('a[role="button"]');
const iframes = doc.querySelectorAll('iframe');
const webviews = doc.querySelectorAll('webview');
const divWithRole = doc.querySelectorAll('[role="button"]');
log(`${pfx}│ Elements: total=${allEls.length} buttons=${buttons.length} roleBtn=${divWithRole.length} iframes=${iframes.length} webviews=${webviews.length}`);
// Dump ALL buttons (not just first 10)
if (buttons.length > 0) {
log(`${pfx}│ ── Buttons ──`);
const maxShow = Math.min(30, buttons.length);
for (let i = 0; i < maxShow; i++) {
const b = buttons[i];
const txt = (b.textContent || '').trim().substring(0, 60);
const cls = (b.className || '').substring(0, 50);
const disabled = b.disabled ? ' [DISABLED]' : '';
const hidden = (b.hidden || !b.offsetParent) ? ' [HIDDEN]' : '';
const ariaLabel = b.getAttribute('aria-label') || '';
const title = b.getAttribute('title') || '';
log(`${pfx}│ btn[${i}]: "${txt}"${disabled}${hidden} class="${cls}" aria="${ariaLabel}" title="${title}"`);
}
if (buttons.length > maxShow) {
log(`${pfx}│ ... +${buttons.length - maxShow} more buttons`);
}
}
// Dump role="button" elements that aren't actual buttons
if (divWithRole.length > 0) {
log(`${pfx}│ ── role="button" elements ──`);
const maxShow = Math.min(15, divWithRole.length);
for (let i = 0; i < maxShow; i++) {
const el = divWithRole[i];
if (el.tagName === 'BUTTON') continue; // skip actual buttons
const txt = (el.textContent || '').trim().substring(0, 60);
const tag = el.tagName.toLowerCase();
log(`${pfx}│ roleBtn[${i}]: <${tag}> "${txt}"`);
}
}
// Recurse into shadow DOMs
let shadowCount = 0;
for (let i = 0; i < allEls.length; i++) {
const sr = allEls[i].shadowRoot;
if (sr) {
shadowCount++;
const tag = allEls[i].tagName.toLowerCase();
const cls = (allEls[i].className || '').substring(0, 40);
inspectDocument(sr, depth + 1, `ShadowRoot of <${tag} class="${cls}">`);
}
}
if (shadowCount === 0) {
log(`${pfx}│ (no shadow DOMs found)`);
}
// Recurse into iframes
for (let i = 0; i < iframes.length; i++) {
const f = iframes[i];
const src = (f.src || '').substring(0, 120);
const cls = (f.className || '').substring(0, 50);
const id = f.id || '';
const sandbox = f.getAttribute('sandbox') || 'none';
const allow = f.getAttribute('allow') || '';
let fLabel = `<iframe#${i}> id="${id}" class="${cls}" src="${src}" sandbox="${sandbox}"`;
try {
const idoc = f.contentDocument || (f.contentWindow && f.contentWindow.document);
if (idoc) {
inspectDocument(idoc, depth + 1, fLabel + ' [ACCESSIBLE]');
} else {
log(`${pfx}${fLabel} → contentDocument=null`);
// Try contentWindow info
try {
const cw = f.contentWindow;
log(`${pfx}│ contentWindow exists: ${!!cw}`);
if (cw) {
log(`${pfx}│ contentWindow.length (sub-frames): ${cw.length}`);
try { log(`${pfx}│ contentWindow.location: ${cw.location.href}`); } catch (e2) {
log(`${pfx}│ contentWindow.location: ⛔ ${e2.message.substring(0, 60)}`);
}
}
} catch (e2) {}
}
} catch (e) {
log(`${pfx}${fLabel} → ⛔ BLOCKED: ${e.message.substring(0, 80)}`);
// Still try to get some info about the blocked iframe
try {
const cw = f.contentWindow;
log(`${pfx}│ contentWindow exists: ${!!cw}`);
if (cw) {
log(`${pfx}│ contentWindow.length (sub-frames): ${cw.length}`);
}
} catch (e2) {
log(`${pfx}│ contentWindow also blocked: ${e2.message.substring(0, 60)}`);
}
}
}
// Recurse into webview elements
for (let i = 0; i < webviews.length; i++) {
const wv = webviews[i];
const src = (wv.src || '').substring(0, 120);
const cls = (wv.className || '').substring(0, 50);
const partition = wv.getAttribute('partition') || '';
const preload = wv.getAttribute('preload') || '';
const nodeInteg = wv.getAttribute('nodeintegration') || '';
const nodeIntegSub = wv.getAttribute('nodeintegrationinsubframes') || '';
const webpref = wv.getAttribute('webpreferences') || '';
log(`${pfx}│ ── <webview#${i}> ──`);
log(`${pfx}│ src: ${src}`);
log(`${pfx}│ class: ${cls}`);
log(`${pfx}│ partition: ${partition}`);
log(`${pfx}│ preload: ${preload}`);
log(`${pfx}│ nodeintegration: ${nodeInteg}`);
log(`${pfx}│ webpreferences: ${webpref}`);
// Try contentDocument
try {
const wdoc = wv.contentDocument;
if (wdoc) {
inspectDocument(wdoc, depth + 1, `<webview#${i}> contentDocument [ACCESSIBLE]`);
} else {
log(`${pfx}│ contentDocument: null`);
}
} catch (e) {
log(`${pfx}│ contentDocument: ⛔ ${e.message.substring(0, 60)}`);
}
// Check executeJavaScript availability
log(`${pfx}│ executeJavaScript: ${typeof wv.executeJavaScript}`);
if (typeof wv.executeJavaScript === 'function') {
log(`${pfx}│ 🔑 executeJavaScript IS AVAILABLE — attempting probe...`);
try {
wv.executeJavaScript(`
(function() {
var btns = document.querySelectorAll('button');
var allEls = document.querySelectorAll('*');
var iframes = document.querySelectorAll('iframe');
var webviews = document.querySelectorAll('webview');
var btnTexts = [];
for (var i = 0; i < btns.length; i++) {
var txt = (btns[i].textContent || '').trim();
var disabled = btns[i].disabled ? ' [DISABLED]' : '';
var hidden = (btns[i].hidden || !btns[i].offsetParent) ? ' [HIDDEN]' : '';
var cls = (btns[i].className || '').substring(0, 40);
btnTexts.push('"' + txt.substring(0, 50) + '"' + disabled + hidden + ' cls=' + cls);
}
// Also check for role=button
var roleBtns = document.querySelectorAll('[role="button"]');
var roleBtnTexts = [];
for (var j = 0; j < roleBtns.length; j++) {
if (roleBtns[j].tagName !== 'BUTTON') {
roleBtnTexts.push('<' + roleBtns[j].tagName.toLowerCase() + '> "' + (roleBtns[j].textContent || '').trim().substring(0, 40) + '"');
}
}
// Check for shadow DOMs
var shadowCount = 0;
for (var k = 0; k < allEls.length; k++) {
if (allEls[k].shadowRoot) shadowCount++;
}
return JSON.stringify({
url: document.URL,
title: document.title,
totalElements: allEls.length,
buttons: btns.length,
buttonTexts: btnTexts.slice(0, 30),
roleBtns: roleBtnTexts.slice(0, 15),
iframes: iframes.length,
webviews: webviews.length,
shadowDOMs: shadowCount
});
})()
`).then(function(result) {
try {
const data = JSON.parse(result);
console.log('[DEEP-INSPECT] 📦 webview#' + i + ' INTERNAL PROBE:');
console.log('[DEEP-INSPECT] URL: ' + data.url);
console.log('[DEEP-INSPECT] title: ' + data.title);
console.log('[DEEP-INSPECT] Elements: total=' + data.totalElements + ' buttons=' + data.buttons + ' iframes=' + data.iframes + ' webviews=' + data.webviews + ' shadowDOMs=' + data.shadowDOMs);
if (data.buttonTexts.length > 0) {
console.log('[DEEP-INSPECT] ── Buttons inside webview ──');
data.buttonTexts.forEach(function(t, idx) {
console.log('[DEEP-INSPECT] btn[' + idx + ']: ' + t);
});
}
if (data.roleBtns.length > 0) {
console.log('[DEEP-INSPECT] ── role="button" inside webview ──');
data.roleBtns.forEach(function(t, idx) {
console.log('[DEEP-INSPECT] roleBtn[' + idx + ']: ' + t);
});
}
} catch (e) {
console.log('[DEEP-INSPECT] raw result: ' + result);
}
}).catch(function(e) {
console.log('[DEEP-INSPECT] ⛔ executeJavaScript FAILED: ' + e.message);
});
} catch (e) {
log(`${pfx}│ ⛔ executeJavaScript call threw: ${e.message}`);
}
}
// Try getWebContentsId (Electron-specific)
try {
if (typeof wv.getWebContentsId === 'function') {
log(`${pfx}│ webContentsId: ${wv.getWebContentsId()}`);
}
} catch (e) {}
}
} catch (e) {
log(`${pfx}│ ⛔ Error during inspection: ${e.message}`);
}
log(`${pfx}└─ END ${label}`);
}
// ── Window hierarchy ──
function inspectWindowHierarchy() {
log(SEP);
log('📐 Window Hierarchy');
log(SEP);
log(`window.location.href: ${window.location.href}`);
log(`window.location.origin: ${window.location.origin}`);
log(`window.location.protocol: ${window.location.protocol}`);
log(`window.frames.length: ${window.frames.length}`);
log(`window === window.top: ${window === window.top}`);
log(`window === window.parent: ${window === window.parent}`);
log(`navigator.userAgent: ${navigator.userAgent.substring(0, 120)}`);
// Check if we're in Electron
try {
log(`process.type: ${typeof process !== 'undefined' ? process.type : 'N/A'}`);
log(`process.versions.electron: ${typeof process !== 'undefined' && process.versions ? process.versions.electron : 'N/A'}`);
} catch (e) {
log(`process info: N/A (${e.message.substring(0, 40)})`);
}
// Check webFrame (Electron renderer API)
try {
if (typeof require === 'function') {
const { webFrame } = require('electron');
log(`webFrame available: ${!!webFrame}`);
if (webFrame) {
log(`webFrame.routingId: ${webFrame.routingId}`);
}
}
} catch (e) {
log(`electron.webFrame: N/A (${e.message.substring(0, 40)})`);
}
// Check webContents access via remote/electron
try {
if (typeof require === 'function') {
const electron = require('electron');
const ipcRenderer = electron.ipcRenderer;
log(`ipcRenderer available: ${!!ipcRenderer}`);
}
} catch (e) {
log(`electron.ipcRenderer: N/A (${e.message.substring(0, 40)})`);
}
}
// ── Main ──
log('');
log('═'.repeat(70));
log(' DEEP IFRAME INSPECTOR — Antigravity DOM Analysis');
log(' Timestamp: ' + new Date().toISOString());
log('═'.repeat(70));
log('');
inspectWindowHierarchy();
log('');
log(SEP);
log('📄 Document Tree (recursive)');
log(SEP);
inspectDocument(document, 0, 'Main Document');
log('');
log(SEP);
log(`✅ Inspection complete. ${nodeCount} document contexts inspected.`);
log(SEP);
// Summary
log('');
log('📋 SUMMARY:');
log(' Copy all [DEEP-INSPECT] lines from this console');
log(' Webview executeJavaScript probe results will appear AFTER this summary (async)');
return results;
})();