Files
safetynet/apps/browser/agent-adapters.cjs
T

266 lines
27 KiB
JavaScript

'use strict';
const {randomUUID}=require('node:crypto');
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,getHistory,goHistory,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','history:go'].includes(action) && (c.foreground !== true || c.interactive===false)) throw Error('Managed session unavailable');
const url = c.webContents.getURL();
const changesDestination=['navigate','back','forward','history:go'].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=['vault:prepare-submit','vault:status','vault:list','vault:fill','window:state','window:control','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 historySnapshots=new Map();
const historyInventory=initial=>{
if(typeof getHistory!=='function')throw Error('Managed history unavailable');
const data=getHistory(initial);
if(!data||!Array.isArray(data.entries)||data.entries.length>100||!Number.isInteger(data.activeIndex)||data.activeIndex<0||data.activeIndex>=data.entries.length)throw Error('Invalid managed history');
return {activeIndex:data.activeIndex,entries:data.entries.map(row=>{
if(typeof row.url!=='string'||row.url.length>4096||typeof row.title!=='string'||row.title.length>200)throw Error('Invalid managed history');
return {url:row.url,title:row.title};
}),...(data.nativeBinding===undefined?{}:/^[a-f0-9]{64}$/.test(data.nativeBinding)&&typeof data.nativeBinding==='string'?{nativeBinding:data.nativeBinding}:(()=>{throw Error('Invalid managed history');})())};
};
const historyCredential=text=>{let value=text;for(let i=0;i<3;i++){if(secret(value))return true;try{const next=decodeURIComponent(value);if(next===value)break;value=next;}catch{break;}}return secret(value);};
const historyAdaptersExtra={
'history:list':async(args={}, {signal}={})=>{
const initial=snapshot(args,signal,'history:list'),data=historyInventory(initial),binding=JSON.stringify(data),actorBinding=JSON.stringify(initial.actor??null);
const current=()=>{check(initial,args,signal);if(getContext(initial.sessionId)?.policy!==initial.policy||JSON.stringify(getContext(initial.sessionId)?.actor??null)!==actorBinding||JSON.stringify(historyInventory(initial))!==binding)throw Error('Managed history changed during action');};
const entries=[];
for(let index=0;index<data.entries.length;index++){
const row=data.entries[index];let url;try{url=new URL(row.url);}catch{continue;}
if(!['http:','https:'].includes(url.protocol)||initial.destinationAllowed(url.href,initial.policy)!==true||(initial.policy.agents.allowedHosts.length&&!initial.policy.agents.allowedHosts.some(p=>hostMatches(url.hostname,p))))continue;
let decodedUrl=row.url;for(let pass=0;pass<3;pass++){try{const next=decodeURIComponent(decodedUrl);if(next===decodedUrl)break;decodedUrl=next;}catch{break;}}
const credential=!!(url.username||url.password)||historyCredential(row.title+'\n'+row.url)||/[?&#](?:password|passwd|token|access[_-]?token|refresh[_-]?token|api[_-]?key|secret|client[_-]?secret|code|authorization|session|sid)=/i.test(decodedUrl);
let allowed=false;
if(!credential){
const verdict=await inspect(row.title+'\n'+row.url,{...initial,action:'history:list'});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');
allowed=verdict.decision==='allow';
}
entries.push({navigable:!credential,entryId:randomUUID(),title:allowed?row.title.slice(0,160):'Protected page',...(allowed?{url:row.url}:{}),active:index===data.activeIndex,index});
}
current();
for(const [id,item] of historySnapshots)if(item.expires<=Date.now())historySnapshots.delete(id);
while(historySnapshots.size>=32)historySnapshots.delete(historySnapshots.keys().next().value);
const snapshotId=randomUUID();historySnapshots.set(snapshotId,{initial,binding,actorBinding,entries,expires:Date.now()+60000});
return {snapshotId,entries:entries.map(({index,navigable,...row})=>row)};
},
'history:go':async(args={}, {signal}={})=>{
const initial=snapshot(args,signal,'history:go'),saved=historySnapshots.get(args.snapshotId);
if(!saved||saved.expires<=Date.now()||saved.initial.sessionId!==initial.sessionId||saved.actorBinding!==JSON.stringify(initial.actor??null)||saved.initial.webContents!==initial.webContents||saved.initial.policy!==initial.policy||saved.initial.version!==initial.version||!contextCurrent(saved.initial.token)||saved.initial.url!==initial.url||JSON.stringify(historyInventory(initial))!==saved.binding)throw Error('Managed history changed during action');
const entry=saved.entries.find(row=>row.entryId===args.entryId);
if(!entry||entry.navigable!==true||typeof goHistory!=='function')throw Error('Unknown managed history entry');
const target=JSON.parse(saved.binding).entries[entry.index].url,url=new URL(target);
if(!['http:','https:'].includes(url.protocol)||url.username||url.password||initial.destinationAllowed(target,initial.policy)!==true||(initial.policy.agents.allowedHosts.length&&!initial.policy.agents.allowedHosts.some(p=>hostMatches(url.hostname,p))))throw Error('Destination blocked');
check(initial,args,signal);historySnapshots.delete(args.snapshotId);
const result=await goHistory(entry.index,initial,{signal,historyBinding:saved.binding}),refreshed=result?.context;
if(result?.completed!==true||!refreshed||refreshed.sessionId!==initial.sessionId||refreshed.webContents!==initial.webContents||refreshed.policy!==initial.policy||(refreshed.policyVersion??refreshed.policy?.version)!==initial.version)throw Error('Managed session changed during action');
check({...refreshed,action:'history:go',version:initial.version,url:target},args,signal,target);
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,
...historyAdaptersExtra,
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};