Add protected interaction evidence and measured debugging phases

This commit is contained in:
Karl committed 2026-10-13 18:04:02 +02:00
1 parent ea448de2d4
commit 244f01630b
42 files changed
+640 -87

No files matched your search

+24 -3
View File
@@ -53,7 +53,10 @@ function domAction(mode, selector, text, expectedUrl) {
}
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');
e.click(); return {completed:true};
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');
@@ -103,7 +106,7 @@ 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,appControls={}} = {}) {
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);
@@ -142,6 +145,11 @@ function createAgentAdapters({getContext,getSessions,contextCurrent,navigate,his
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'];
@@ -181,7 +189,20 @@ function createAgentAdapters({getContext,getSessions,contextCurrent,navigate,his
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');
return getSessions().map(item=>({id:item.id,title:String(item.title || 'Tab').slice(0,160),foreground:item.foreground===true}));
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'),