'use strict'; function domAction(mode, selector, text, expectedUrl) { if (location.href !== expectedUrl) throw Error('Document changed before action'); const visible = e => { if (!e || e.closest('[hidden],[aria-hidden="true"],script,style,template,noscript')) return false; for (let node=e; node; node=node.parentElement) { const style=getComputedStyle(node); if(style.display==='none'||style.visibility==='hidden'||style.opacity==='0') return false; } return e.isConnected !== false && e.getClientRects().length > 0; }; const sensitive = e => { const forbidden = /password|passwd|secret|token|credential|api.?key|auth|one.?time|otp|credit.?card|cc-number|cc-csc/i; for (let node = e; node; node = node.parentElement) { if (node.getAttribute('data-sensitive') !== null && node.getAttribute('data-sensitive') !== undefined) return true; if (['name','id','autocomplete','aria-label','data-sensitive','action'].some(a => forbidden.test(node.getAttribute(a) || ''))) return true; } return /^(password|file|hidden)$/i.test(e.type || ''); }; if (mode === 'read') { const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT); const parts = []; let length = 0, visited = 0, node; while ((node = walker.nextNode()) && visited++ < 20000 && length < 10000) { const element = node.parentElement; if (!element || element.closest('input,textarea,select,option,[contenteditable],script,style,template,noscript') || !visible(element) || sensitive(element)) continue; const value = (node.textContent || '').replace(/\s+/g, ' ').trim(); if (value) { const piece = value.slice(0, 10000 - length); parts.push(piece); length += piece.length + 1; } } return { text: parts.join('\n').slice(0,10000) }; } if (['select','check'].includes(mode) && document.querySelectorAll(selector).length !== 1) throw Error('Target is ambiguous'); const e = document.querySelector(selector); if (!visible(e) || sensitive(e) || e.disabled || e.closest('[disabled],[aria-disabled="true"],[inert]') || e.getAttribute('aria-disabled') === 'true') throw Error('Target is unavailable or sensitive'); if (mode === 'select') { if (e.tagName !== 'SELECT' || e.multiple) throw Error('Target cannot be selected'); const optionUnavailable = option => { if (option.disabled || option.parentElement?.disabled || sensitive(option)) return true; for(let node=option;node&&node!==e;node=node.parentElement) { if(node.hidden || node.getAttribute('aria-hidden')==='true' || node.getAttribute('aria-disabled')==='true') return true; const style=getComputedStyle(node);if(style.display==='none'||style.visibility==='hidden'||style.opacity==='0') return true; } return false; }; const matches = Array.from(e.options).filter(option => option.value === text); if (matches.length !== 1 || optionUnavailable(matches[0])) throw Error('Option is unavailable or ambiguous'); Object.getOwnPropertyDescriptor(HTMLSelectElement.prototype,'value').set.call(e,text); e.dispatchEvent(new Event('input',{bubbles:true})); e.dispatchEvent(new Event('change',{bubbles:true})); if (location.href !== expectedUrl || e.value !== text || !visible(e) || sensitive(e) || e.disabled || e.closest('[disabled],[aria-disabled="true"],[inert]') || optionUnavailable(matches[0]) || !Array.from(e.options).includes(matches[0]) || Array.from(e.options).filter(option=>option.value===text).length!==1) throw Error('Target changed during action'); return {completed:true}; } if (mode === 'check') { if (e.tagName !== 'INPUT' || e.type !== 'checkbox' || typeof text !== 'boolean') throw Error('Target cannot be checked'); if (e.checked !== text) e.click(); if (location.href !== expectedUrl || e.checked !== text || !visible(e) || sensitive(e) || e.disabled || e.closest('[disabled],[aria-disabled="true"],[inert]')) throw Error('Target changed during action'); return {completed:true}; } if (mode === 'click') { if (!/^(A|BUTTON|INPUT)$/.test(e.tagName) || (e.tagName === 'INPUT' && !/^(button|submit|checkbox|radio)$/i.test(e.type))) throw Error('Target cannot be clicked'); const rect=e.getBoundingClientRect(),target={tag:e.tagName.toLowerCase()},role=e.getAttribute('role'); if(['button','link','textbox','checkbox','radio','combobox','menuitem','option'].includes(role))target.role=role; const x=rect.left+rect.width/2,y=rect.top+rect.height/2;if(Number.isFinite(x)&&Number.isFinite(y)&&x>=0&&y>=0&&x<=innerWidth&&y<=innerHeight){target.x=Math.round(x);target.y=Math.round(y);} e.click(); return {completed:true,target}; } if (mode === 'type') { if (e.readOnly || !(e.tagName === 'TEXTAREA' || (e.tagName === 'INPUT' && /^(text|search|email|url|tel)$/i.test(e.type)))) throw Error('Target cannot receive text'); const prototype = e.tagName === 'TEXTAREA' ? HTMLTextAreaElement.prototype : HTMLInputElement.prototype; Object.getOwnPropertyDescriptor(prototype,'value').set.call(e,text); e.dispatchEvent(new Event('input',{bubbles:true})); e.dispatchEvent(new Event('change',{bubbles:true})); return {completed:true}; } throw Error('Unsupported DOM action'); } const secret = text => /-----BEGIN (?:[A-Z ]+ )?PRIVATE KEY-----|\b(?:AKIA|ASIA)[A-Z0-9]{16}\b|\b(?:sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,})\b|\b(?:password|passwd|api[_ -]?key|access[_ -]?token|client[_ -]?secret)\s*[:=]\s*["']?[^\s"']{4,}/i.test(text); // Fixed inspection code: no caller-supplied JavaScript, credential fields or hidden DOM. function inspectControls(expectedUrl) { if(location.href!==expectedUrl)throw Error('Document changed before action'); const controls=[]; for(const e of document.querySelectorAll('a,button,input,textarea,select')){ if(controls.length>=100)break; const forbidden=/password|passwd|secret|token|credential|api.?key|auth|one.?time|otp|credit.?card|cc-number|cc-csc/i; let sensitive=false; for(let node=e;node;node=node.parentElement)if((node.getAttribute('data-sensitive')!==null&&node.getAttribute('data-sensitive')!==undefined)||['name','id','autocomplete','aria-label','data-sensitive','action'].some(a=>forbidden.test(node.getAttribute(a)||'')))sensitive=true; const style=getComputedStyle(e); if(sensitive||/^(password|file|hidden)$/i.test(e.type||'')||e.closest('[hidden],[aria-hidden="true"]')||!e.getClientRects().length||style.display==='none'||style.visibility==='hidden'||style.opacity==='0')continue; let selector=''; if(/^[a-zA-Z_][a-zA-Z0-9_-]{0,100}$/.test(e.id||'')&&document.querySelectorAll('#'+e.id).length===1)selector='#'+e.id; else if(/^[a-zA-Z0-9_-]{1,100}$/.test(e.getAttribute('name')||'')){const candidate=e.tagName.toLowerCase()+'[name="'+e.getAttribute('name')+'"]';if(document.querySelectorAll(candidate).length===1)selector=candidate;} const row={selector,role:e.tagName.toLowerCase(),label:(e.getAttribute('aria-label')||'').slice(0,160),text:(e.innerText||'').slice(0,300),value:typeof e.value==='string'?e.value.slice(0,300):''}; if(e.tagName==='INPUT'&&e.type==='checkbox')row.checked=e.checked===true; if(e.tagName==='SELECT') { row.options=[]; for(const option of Array.from(e.options)) { if(row.options.length>=20)break; let excluded=false; for(let node=option;node;node=node.parentElement) { if(node.hidden||node.getAttribute('aria-hidden')==='true'||(node.getAttribute('data-sensitive')!==null&&node.getAttribute('data-sensitive')!==undefined)||['name','id','autocomplete','aria-label','action'].some(a=>forbidden.test(node.getAttribute(a)||'')))excluded=true; const style=getComputedStyle(node);if(style.display==='none'||style.visibility==='hidden'||style.opacity==='0')excluded=true; } if(!excluded&&String(option.value).length<=128)row.options.push({value:String(option.value),label:String(option.label||option.textContent||'').slice(0,160),disabled:option.disabled===true||option.parentElement?.disabled===true}); } } controls.push(row); } return {text:"",controls}; } function scrollPage(expectedUrl,x,y){if(location.href!==expectedUrl)throw Error('Document changed before action');window.scrollTo({left:x,top:y,behavior:'instant'});return {completed:true};} const script = (mode,args,url) => `(${domAction.toString()})(${JSON.stringify(mode)},${JSON.stringify(args.selector || '')},${JSON.stringify(mode==='check'?args.checked:mode==='select'?args.value:args.text || '')},${JSON.stringify(url)})`; const hostMatches = (hostname, pattern) => { const name=hostname.toLowerCase().replace(/\.$/,''),p=String(pattern).toLowerCase(); return p.startsWith('*.') ? name.endsWith(p.slice(1)) && name!==p.slice(2) : name===p; }; function createAgentAdapters({getContext,getSessions,contextCurrent,navigate,history,ai,inspect,audit,onInteraction,appControls={}} = {}) { if ([getContext,getSessions,contextCurrent,navigate,ai,inspect,audit].some(fn=>typeof fn!=='function')) throw TypeError('All adapter callbacks are required'); const snapshot = (args,signal,action) => { const c = getContext(args.sessionId); if (signal?.aborted || !c?.ready || !c.policy?.agents?.enabled || !c.webContents || c.webContents.isDestroyed?.() || (c.policy.expiresAt && Date.parse(c.policy.expiresAt)<=Date.now())) throw Error('Managed session unavailable'); if (!c.sessionId || !c.token || (args.sessionId !== undefined && args.sessionId !== c.sessionId)) throw Error('Unknown managed session'); if (['click','type','scroll','select','check','back','forward','reload'].includes(action) && (c.foreground !== true || c.interactive===false)) throw Error('Managed session unavailable'); const url = c.webContents.getURL(); const changesDestination=['navigate','back','forward'].includes(action); if (typeof c.destinationAllowed!=='function' || (!changesDestination && (!/^https?:\/\//.test(url) || c.destinationAllowed(url,c.policy)!==true))) throw Error('Session destination blocked'); const agents=c.policy.agents; if (!Array.isArray(agents.scopes) || !agents.scopes.includes(action)) throw Error('Agent scope denied'); if (!Array.isArray(agents.allowedHosts) || (!changesDestination && agents.allowedHosts.length && !agents.allowedHosts.some(p=>hostMatches(new URL(url).hostname,p)))) throw Error('Agent destination denied'); return {...c,url,action,version:c.policyVersion ?? c.policy.version}; }; const check = (initial,args,signal,expectedUrl=initial.url) => { const current = snapshot({...args,sessionId:initial.sessionId},signal,initial.action); if (!contextCurrent(initial.token) || current.version !== initial.version || current.webContents !== initial.webContents || current.url !== expectedUrl) throw Error('Managed session changed during action'); return current; }; const classify = async (text,initial,args,signal,allowSensitive=false) => { if (typeof text!=='string' || text.length>10000 || secret(text)) throw Error('Sensitive content cannot be returned'); const verdict = await inspect(text,initial);check(initial,args,signal); if (!verdict || !['allow','block'].includes(verdict.decision)) throw Error('Inspection unavailable'); const reserved=/^(?:private-key|aws-access-key|api-secret|uninspectable|size-limit|inspection-incomplete|yara-.*)$/; const knownCorporateRule=verdict.ruleId==='content-blocklist' || initial.policy.dlp?.rules?.some(rule=>rule.id===verdict.ruleId && ['literal','sha256','fragment-sha256'].includes(rule.kind)); const override=allowSensitive && knownCorporateRule && !reserved.test(verdict.ruleId || '') && !verdict.malware && verdict.engine!=='yara' && verdict.coverage?.complete!==false; if (verdict.decision==='block' && !override) { await audit({action:'agent.read',outcome:'blocked'});throw Error('Company policy blocks content access'); } return text; }; const dom = mode => async (args,{signal}={}) => { const initial = snapshot(args,signal,mode); if (mode!=='read' && (typeof args.selector!=='string' || args.selector.length>512 || !/^[a-zA-Z0-9_#.*\[\]="' >+~-]+$/.test(args.selector))) throw Error('Invalid selector'); if (mode==='type' && (typeof args.text!=='string' || args.text.length>16384)) throw Error('Invalid text'); if (mode==='select' && (typeof args.value!=='string' || args.value.length>16384)) throw Error('Invalid value'); if (mode==='check' && typeof args.checked!=='boolean') throw Error('Invalid checked state'); const result = await initial.webContents.executeJavaScript(script(mode,args,initial.url),false); check(initial,args,signal); if (mode==='read') return {text:await classify(result?.text,initial,args,signal,initial.policy.agents.allowSensitiveRead===true)}; if(mode==='click' && result?.completed===true && onInteraction){ const source=result.target,target=source&&['a','button','input'].includes(source.tag)?{tag:source.tag}:undefined; if(target&&['button','link','textbox','checkbox','radio','combobox','menuitem','option'].includes(source.role))target.role=source.role; if(target&&['x','y'].every(k=>Number.isSafeInteger(source[k])&&source[k]>=0&&source[k]<=100000)){target.x=source.x;target.y=source.y;} await onInteraction({action:'click',sessionId:initial.sessionId,pageUrl:initial.url,outcome:'completed',sensitiveField:false,...(target?{target}:{})});check(initial,args,signal);} return {completed:result?.completed===true}; }; const appActions=['application:state','settings:get','settings:update','bookmarks:list','bookmarks:mutate','tabs:create','tabs:activate','tabs:close','tabs:group','library:organize-plan','library:organize-apply']; // Native history owns target resolution and pre-release destination checks. A // refreshed token permits intentional navigation but never a different tab. const historyAdapters=Object.fromEntries(['back','forward','reload'].map(action=>[action,async(args={}, {signal}={})=>{ const initial=snapshot(args,signal,action); if(typeof history!=='function')throw Error('Managed session unavailable'); const result=await history(action,initial,{signal}), refreshed=result?.context; if(result?.completed!==true || !refreshed || refreshed.sessionId!==initial.sessionId || refreshed.webContents!==initial.webContents || (refreshed.policyVersion??refreshed.policy?.version)!==initial.version)throw Error('Managed session changed during action'); const url=refreshed.webContents.getURL(); let parsed;try{parsed=new URL(url);}catch{throw Error('Destination blocked');} if(!['http:','https:'].includes(parsed.protocol)||parsed.username||parsed.password||refreshed.destinationAllowed?.(url,refreshed.policy)!==true||(refreshed.policy?.agents?.allowedHosts?.length&&!refreshed.policy.agents.allowedHosts.some(p=>hostMatches(parsed.hostname,p))))throw Error('Destination blocked'); check({...refreshed,action,version:initial.version,url},args,signal,url); return {completed:true}; }])); const appSnapshot=action=>{const c=getContext();if(!c?.ready||!c.policy?.agents?.enabled||!c.policy.agents.scopes?.includes(action)||(c.policy.expiresAt&&Date.parse(c.policy.expiresAt)<=Date.now()))throw Error('Agent scope denied');return c;}; const appAdapters=Object.fromEntries(appActions.map(action=>[action,async(args={}, {signal}={})=>{ const initial=appSnapshot(action),version=initial.policyVersion??initial.policy.version; const current=()=>{const c=appSnapshot(action);if(signal?.aborted||c.policy!==initial.policy||(c.policyVersion??c.policy.version)!==version)throw Error('Managed session changed during action');}; if(signal?.aborted||typeof appControls[action]!=='function')throw Error('Managed session unavailable'); if(action==='tabs:create'&&args.url){const url=new URL(args.url);if(!['http:','https:'].includes(url.protocol)||url.username||url.password||initial.destinationAllowed(url.href,initial.policy)!==true||(initial.policy.agents.allowedHosts.length&&!initial.policy.agents.allowedHosts.some(p=>hostMatches(url.hostname,p))))throw Error('Agent destination denied');} current();const result=await appControls[action](args,{signal,policyVersion:version});current(); if(action==='library:organize-plan'){ const labels=[...(result?.plan?.bookmarks||[]).map(row=>row.folder),...(result?.plan?.tabs||[]).map(row=>row.group)].join('\n'); if(labels.length>16000||secret(labels))throw Error('Sensitive content cannot be returned');const verdict=await inspect(labels,{...initial,action});current();if(verdict?.decision!=='allow')throw Error('Company policy blocks content access'); } if(action==='bookmarks:list'){ const entries=[...(result?.managed||[]),...(result?.personal?.items||[])]; for(const item of entries){const text=[item.title,item.url,item.folder].filter(v=>typeof v==='string').join('\n');if(text.length>10000||secret(text))throw Error('Sensitive content cannot be returned');const verdict=await inspect(text,{...initial,action});current();if(verdict?.decision!=='allow')throw Error('Company policy blocks content access');} } return result; }])); return { ...appAdapters, ...historyAdapters, sessions:async(args={}, {signal}={})=>{ const c=getContext(); if(signal?.aborted || !c?.ready || !c.policy?.agents?.enabled || !c.policy.agents.scopes?.includes('sessions') || (c.policy.expiresAt && Date.parse(c.policy.expiresAt)<=Date.now())) throw Error('Managed session unavailable'); const version=c.policyVersion??c.policy.version; const inventory=()=>{const rows=getSessions();if(!Array.isArray(rows)||rows.length>1000)throw Error('Invalid managed inventory');return rows.map(item=>{if(typeof item.title!=='string'||item.title.length>200)throw Error('Invalid managed title');return {id:item.id,title:item.title||'Tab',foreground:item.foreground===true};});}; const rows=inventory(),binding=JSON.stringify(rows); const current=()=>{const latest=getContext();if(signal?.aborted||!latest?.ready||!latest.policy?.agents?.enabled||!latest.policy.agents.scopes?.includes('sessions')||latest.policy!==c.policy||(latest.policyVersion??latest.policy.version)!==version||(latest.policy.expiresAt&&Date.parse(latest.policy.expiresAt)<=Date.now())||(c.token&&!contextCurrent(c.token))||JSON.stringify(inventory())!==binding)throw Error('Managed session changed during action');}; // One bounded classification avoids reloading local OCR/semantic models // per tab. A blocked aggregate masks every title; opaque IDs remain usable. const candidates=rows.filter(row=>!secret(row.title)); let allow=false; if(candidates.length){ const verdict=await inspect(candidates.map(row=>row.title).join('\n'),{...c,action:'sessions'});current(); if(!verdict||!['allow','block'].includes(verdict.decision)||verdict.coverage?.complete===false||/uninspectable|size-limit|inspection-incomplete|unavailable|failure|failed|timeout|unsupported|encrypted|parser/i.test(verdict.ruleId||''))throw Error('Inspection unavailable'); allow=verdict.decision==='allow'; } current();return rows.map(row=>({...row,title:allow&&!secret(row.title)?row.title.slice(0,160):'Protected page'})); }, navigate:async(args,{signal}={})=>{const initial=snapshot(args,signal,'navigate');const url=new URL(args.url);if(!['http:','https:'].includes(url.protocol)||url.username||url.password||initial.destinationAllowed(url.href,initial.policy)!==true || (initial.policy.agents.allowedHosts.length && !initial.policy.agents.allowedHosts.some(p=>hostMatches(url.hostname,p))))throw Error('Destination blocked');const refreshed=await navigate(url.href,initial,{signal});if(!refreshed || refreshed.sessionId!==initial.sessionId || refreshed.webContents!==initial.webContents || (refreshed.policyVersion ?? refreshed.policy?.version)!==initial.version)throw Error('Managed session changed during action');check({...refreshed,action:initial.action,version:initial.version,url:url.href},args,signal,url.href);return {completed:true};}, read:dom('read'),click:dom('click'),type:dom('type'),select:dom('select'),check:dom('check'), inspect:async(args,{signal}={})=>{const initial=snapshot(args,signal,'inspect');const result=await initial.webContents.executeJavaScript(`(${inspectControls.toString()})(${JSON.stringify(initial.url)})`,false);check(initial,args,signal);if(!Array.isArray(result?.controls)||result.controls.length>100)throw Error('Invalid text response');const text=JSON.stringify(result);if(text.length>10000)throw Error('Sensitive content cannot be returned');await classify(text,initial,args,signal,initial.policy.agents.allowSensitiveRead===true);return result;}, scroll:async(args,{signal}={})=>{const initial=snapshot(args,signal,'scroll');if(![args.x,args.y].every(n=>Number.isSafeInteger(n)&&n>=0&&n<=10000000))throw Error('Invalid scroll position');const result=await initial.webContents.executeJavaScript(`(${scrollPage.toString()})(${JSON.stringify(initial.url)},${args.x},${args.y})`,false);check(initial,args,signal);return {completed:result?.completed===true};}, ai:async(args,{signal}={})=>{const initial=snapshot(args,signal,'ai');if(typeof args.prompt!=='string'||args.prompt.length>16384)throw Error('Invalid prompt');const result=await ai(args.prompt,{signal});check(initial,args,signal);const text=typeof result==='string'?result:result?.text ?? result?.answer;return {text:await classify(text,initial,args,signal,false)};} }; } module.exports={createAgentAdapters};