Add inspected agent viewport export with policy-bound final release
This commit is contained in:
1 parent
c8b6ff9cdd
commit
e7d778aeac
24 files changed
+483
-32
No files matched your search
@@ -24,3 +24,5 @@ test('controlled credential submission and agent preparation start off and round
|
||||
const fields=new Map(),get=id=>{if(!fields.has(id))fields.set(id,{value:'',checked:false});return fields.get(id);};const p={passwordManager:{enabled:true,allowSaving:true,allowAutofill:true,idleLockSeconds:300,controlledSubmission:{enabled:true,forms:[{id:'login',actionUrl:'https://login.example/signin',usernameField:'username',passwordField:'password'}]}}},context=vm.createContext({$:get,p});
|
||||
const fillLine=source.split('\n').find(line=>line.includes("$('vault-enabled').checked=p.passwordManager.enabled"));vm.runInContext(fillLine,context);assert.equal(get('vault-controlled-submit').checked,true);const draftLine=source.split('\n').find(line=>line.includes("const forms=JSON.parse($('vault-submit-forms')"));vm.runInContext(draftLine,context);assert.deepEqual(JSON.parse(JSON.stringify(p.passwordManager.controlledSubmission)),{enabled:true,forms:[{id:'login',actionUrl:'https://login.example/signin',usernameField:'username',passwordField:'password'}]});assert.equal(p.passwordManager.allowAutofill,true);get('vault-submit-forms').value='{}';assert.throws(()=>vm.runInContext(draftLine,vm.createContext({$:get,p})));assert.ok(source.includes("'password-vault'"));
|
||||
});
|
||||
|
||||
test('visual export remains separate unchecked page-only opt-in in central and individual grants',()=>{const source=readFileSync(new URL('../apps/server/public/admin.js',import.meta.url),'utf8'),scopes=vm.runInNewContext(source.match(/const agentScopes=(\[[^;]+\]);/)[1]);assert.ok(scopes.includes('screenshot'));const tag=html.match(/<input[^>]*id="agent-scope-screenshot"[^>]*>/)[0];assert.equal(/\bchecked\b/.test(tag),false);for(const text of ['Export inspected foreground page viewport','does not authorize desktop, other windows or browser-interface capture','Sensitive-read permission does not bypass visual inspection'])assert.ok(html.includes(text));const editor=readFileSync(new URL('../apps/server/public/agent-grants-admin.js',import.meta.url),'utf8');assert.ok(editor.includes('for(const scope of agentScopes)'));assert.ok(editor.includes('check.checked=grant.scopes.includes(scope)'));assert.ok(editor.includes('check.disabled=!agentGrantsEditable'));assert.ok(editor.includes('never the desktop or browser interface'));});
|
||||
@@ -0,0 +1,6 @@
|
||||
import test from 'node:test';import assert from 'node:assert/strict';import {createRequire} from 'node:module';import {createServer} from 'node:http';import {deflateSync} from 'node:zlib';import {DiagnosticsSchema} from '../packages/core/diagnostics.mjs';const require=createRequire(import.meta.url),{createDiagnostics}=require('../apps/browser/diagnostics.cjs'),{createAgentVisualExport}=require('../apps/browser/agent-visual-export.cjs');
|
||||
function png(){const chunk=(type,data)=>{const name=Buffer.from(type),body=Buffer.concat([name,data]);let c=0xffffffff;for(const b of body){c^=b;for(let i=0;i<8;i++)c=(c&1)?0xedb88320^(c>>>1):c>>>1;}const length=Buffer.alloc(4),sum=Buffer.alloc(4);length.writeUInt32BE(data.length);sum.writeUInt32BE((c^0xffffffff)>>>0);return Buffer.concat([length,body,sum]);},header=Buffer.alloc(13);header.writeUInt32BE(1);header.writeUInt32BE(1,4);header[8]=8;header[9]=6;return Buffer.concat([Buffer.from([137,80,78,71,13,10,26,10]),chunk('IHDR',header),chunk('IDAT',deflateSync(Buffer.alloc(5))),chunk('IEND',Buffer.alloc(0))]);}
|
||||
async function fixture(t){const payloads=[],server=createServer((req,res)=>{const chunks=[];req.on('data',c=>chunks.push(c));req.on('end',()=>{payloads.push(JSON.parse(Buffer.concat(chunks).toString()));res.setHeader('content-type','application/json');res.end('{}');});}).listen(0,'127.0.0.1');await new Promise(r=>server.once('listening',r));const diagnostics=createDiagnostics();diagnostics.configure(DiagnosticsSchema.parse({enabled:true,mode:'debug',endpoint:`http://127.0.0.1:${server.address().port}`,sampleRate:1,debugSampleRate:1,flushIntervalMs:10000}));t.after(async()=>{await diagnostics.shutdown();server.closeAllConnections();await new Promise(r=>server.close(r));});const bytes=png(),context={ready:true,foreground:true,interactive:true,policy:{expiresAt:new Date(Date.now()+60000).toISOString(),agents:{enabled:true,scopes:['screenshot']}},sessionId:'00000000-0000-4000-8000-000000000001',token:{},actor:{agentId:'PRIVATE_ACTOR',generation:1},webContents:{getURL:()=> 'https://private.example/page',isDestroyed:()=>false},destinationAllowed:()=>true};let local={decision:'allow',contentType:'image',coverage:{complete:true}},safe=true;const exporter=createAgentVisualExport({getContext:()=>context,contextCurrent:()=>true,probeSensitive:async()=>({safe,revision:'PRIVATE_REVISION',width:1,height:1}),capture:async()=>bytes,inspectLocal:async()=>local,inspectEnterprise:async()=>({decision:'allow',coverage:{complete:true}}),audit:async()=>{},traceOperation:(operation,fn)=>diagnostics.span('browser.agent',{operation,url:'https://private.example/page',image:bytes.toString('base64'),actor:'PRIVATE_ACTOR'},fn)});return {bytes,diagnostics,payloads,run:()=>diagnostics.span('browser.agent',{operation:'screenshot'},()=>exporter()),block:()=>local={decision:'block',ruleId:'PRIVATE_RULE',contentType:'image',coverage:{complete:true}},unsafe:()=>safe=false,spans:()=>payloads.flatMap(p=>p.resourceSpans||[]).flatMap(p=>p.scopeSpans||[]).flatMap(p=>p.spans||[])};}
|
||||
test('visual export produces real OTLP correlated named timings without image/page/actor data',async t=>{const f=await fixture(t),image=f.bytes.toString('base64');await f.run();await f.diagnostics.flush();const spans=f.spans(),op=s=>s.attributes.find(a=>a.key==='safetynet.operation')?.value.stringValue,parent=spans.find(s=>op(s)==='screenshot');assert.ok(parent);for(const operation of ['visual.probe','visual.capture','visual.validate','visual.inspect.local','visual.inspect.enterprise','visual.audit','visual.project']){const child=spans.find(s=>op(s)===operation);assert.ok(child,operation);assert.equal(child.parentSpanId,parent.spanId);assert.equal(child.traceId,parent.traceId);assert.ok(BigInt(child.endTimeUnixNano)>=BigInt(child.startTimeUnixNano));}for(const value of ['PRIVATE_ACTOR','PRIVATE_REVISION','private.example',image])assert.equal(JSON.stringify(f.payloads).includes(value),false);assert.equal(f.diagnostics.status().lifetimeFailedExports,0);});
|
||||
test('blocked local OCR and unsafe probe export ERROR on the responsible named stage',async t=>{for(const mode of ['local','probe']){const f=await fixture(t);if(mode==='local')f.block();else f.unsafe();await assert.rejects(f.run(),e=>e.status===403&&e.message==='Managed visual export refused');await f.diagnostics.flush();const operation=mode==='local'?'visual.inspect.local':'visual.probe',stage=f.spans().find(s=>s.attributes.some(a=>a.key==='safetynet.operation'&&a.value.stringValue===operation));assert.ok(stage);assert.equal(stage.status.code,2);assert.equal(JSON.stringify(f.payloads).includes('PRIVATE_RULE'),false);if(mode==='local')assert.ok(f.bytes.every(b=>b===0));}});
|
||||
test('admin diagnostics disable preserves screenshot inspection without any exported spans',async t=>{const f=await fixture(t);f.diagnostics.configure({enabled:false});assert.equal((await f.run()).mime,'image/png');await f.diagnostics.flush();assert.equal(f.spans().length,0);assert.ok(f.bytes.every(b=>b===0));});
|
||||
@@ -0,0 +1,23 @@
|
||||
import {AsyncLocalStorage} from 'node:async_hooks';
|
||||
import test from 'node:test';import assert from 'node:assert/strict';import {createRequire} from 'node:module';import {deflateSync} from 'node:zlib';const require=createRequire(import.meta.url),{createAgentVisualExport,getVisualReleaseGuard,validateVisualResult,pngSize}=require('../apps/browser/agent-visual-export.cjs');
|
||||
function png(width=2,height=2,metadata){const crc=bytes=>{let c=0xffffffff;for(const b of bytes){c^=b;for(let i=0;i<8;i++)c=(c&1)?0xedb88320^(c>>>1):c>>>1;}return (c^0xffffffff)>>>0;},chunk=(type,data)=>{const size=Buffer.alloc(4),name=Buffer.from(type),sum=Buffer.alloc(4);size.writeUInt32BE(data.length);sum.writeUInt32BE(crc(Buffer.concat([name,data])));return Buffer.concat([size,name,data,sum]);};const ihdr=Buffer.alloc(13);ihdr.writeUInt32BE(width);ihdr.writeUInt32BE(height,4);ihdr[8]=8;ihdr[9]=6;return Buffer.concat([Buffer.from([137,80,78,71,13,10,26,10]),chunk('IHDR',ihdr),chunk('IDAT',deflateSync(Buffer.alloc(18))),...(metadata?[chunk('tEXt',Buffer.from(metadata))]:[]),chunk('IEND',Buffer.alloc(0))]);}
|
||||
function fixture(){const bytes=png(),events=[],stages=[],policy={expiresAt:new Date(Date.now()+60000).toISOString(),agents:{enabled:true,scopes:['screenshot']}},c={ready:true,foreground:true,interactive:true,policy,sessionId:'tab1',token:{},actor:{agentId:'actor',generation:1},webContents:{getURL:()=> 'https://example.com/docs',isDestroyed:()=>false},destinationAllowed:()=>true};let revision='rev1',safe=true,valid=true;const deps={getContext:()=>c,contextCurrent:()=>valid,probeSensitive:async()=>({safe,revision,width:2,height:2}),capture:async()=>bytes,inspectLocal:async()=>({decision:'allow',contentType:'image',coverage:{complete:true}}),inspectEnterprise:async()=>({decision:'allow',coverage:{complete:true}}),audit:async e=>events.push(e),traceOperation:async(op,fn)=>{stages.push(op);return fn();}};return {c,bytes,deps,events,stages,changeRevision:()=>revision='rev2',unsafe:()=>safe=false,invalidate:()=>valid=false,make:()=>createAgentVisualExport(deps)};}
|
||||
test('native PNG export requires OCR and enterprise allow, exports only canonical bounded payload',async()=>{const f=fixture(),expected=f.bytes.toString('base64'),result=await f.make()();assert.deepEqual(result,{mime:'image/png',width:2,height:2,data:expected});assert.ok(f.bytes.every(b=>b===0));assert.deepEqual(f.events,[{action:'screenshot',outcome:'authorized',sessionId:'tab1'}]);assert.deepEqual(f.stages.filter(s=>s!=='visual.probe'),['visual.capture','visual.validate','visual.inspect.local','visual.inspect.enterprise','visual.audit','visual.project']);});
|
||||
test('scope defaults off and unsafe visible fields/frames deny before capture',async()=>{for(const mode of ['scope','unsafe','foreground','interactive']){const f=fixture();let captured=0;f.deps.capture=async()=>{captured++;return f.bytes;};if(mode==='scope')f.c.policy.agents.scopes=[];if(mode==='unsafe')f.unsafe();if(mode==='foreground')f.c.foreground=false;if(mode==='interactive')f.c.interactive=false;await assert.rejects(f.make()());assert.equal(captured,0);}});
|
||||
test('local image OCR is mandatory and incomplete/blocked/nonimage verdicts fail closed',async()=>{for(const verdict of [{decision:'block',coverage:{complete:true}},{decision:'allow',contentType:'image',coverage:{complete:false}},{decision:'allow',contentType:'text',coverage:{complete:true}}]){const f=fixture();f.deps.inspectLocal=async()=>verdict;await assert.rejects(f.make()());assert.ok(f.bytes.every(b=>b===0));assert.equal(f.events.length,0);}});
|
||||
test('capture/local/enterprise/audit waits reject changed native authority and content revision',async()=>{for(const stage of ['capture','inspectLocal','inspectEnterprise','audit'])for(const change of ['policy','actor','revision']){const f=fixture(),fn=f.deps[stage];f.deps[stage]=async(...args)=>{const result=await fn(...args);if(change==='policy')f.c.policy={...f.c.policy};if(change==='actor')f.c.actor={agentId:'other',generation:1};if(change==='revision')f.changeRevision();return result;};await assert.rejects(f.make()());assert.ok(f.bytes.every(b=>b===0));}});
|
||||
test('PNG source mutation, malformed CRC, dimensions and arbitrary arguments are rejected',async()=>{const f=fixture();f.deps.inspectEnterprise=async()=>{f.bytes[40]^=1;return {decision:'allow',coverage:{complete:true}};};await assert.rejects(f.make()());for(const data of [png(4097,2),png(4096,4096),Buffer.from('not-png')])assert.throws(()=>pngSize(data));const malformed=png();malformed[32]^=1;assert.throws(()=>pngSize(malformed));const g=fixture();await assert.rejects(g.make()({rect:{x:0,y:0,width:10,height:10}}));});
|
||||
test('all failures sanitize replies and zero acquired buffers',async()=>{const f=fixture();f.deps.inspectLocal=async()=>{throw Error('PRIVATE IMAGE DATA');};await assert.rejects(f.make()(),{message:'Managed visual export refused'});assert.ok(f.bytes.every(b=>b===0));});
|
||||
|
||||
test('diagnostic failures and duplicate invocations never replay capture or alter output',async()=>{for(const mode of ['before','after','twice']){const f=fixture();let captures=0;f.deps.capture=async()=>{captures++;return f.bytes;};f.deps.traceOperation=async(_op,fn)=>{if(mode==='before')throw Error('Diagnostic failure');const result=await fn();if(mode==='after')throw Error('Diagnostic failure');await fn();return result;};assert.equal((await f.make()()).mime,'image/png');assert.equal(captures,1);assert.ok(f.bytes.every(b=>b===0));}});
|
||||
test('aborted local inspection prevents enterprise inspection and wipes captured PNG',async()=>{const f=fixture(),controller=new AbortController();let enterprise=0;f.deps.inspectLocal=async()=>{controller.abort();return {decision:'allow',contentType:'image',coverage:{complete:true}};};f.deps.inspectEnterprise=async()=>{enterprise++;return {decision:'allow',coverage:{complete:true}};};await assert.rejects(f.make()({}, {signal:controller.signal}));assert.equal(enterprise,0);assert.ok(f.bytes.every(b=>b===0));const g=fixture();await assert.rejects(g.make()({}, {signal:AbortSignal.abort()}));});
|
||||
test('pure visual projection requires canonical PNG and rejects mismatched or extra metadata',()=>{const bytes=png(),result={mime:'image/png',width:2,height:2,data:bytes.toString('base64')};assert.deepEqual(validateVisualResult(result),result);for(const value of [{...result,width:3},{...result,data:result.data+'\n'},{...result,url:'https://secret.example'},{...result,mime:'image/jpeg'}])assert.throws(()=>validateVisualResult(value));let reads=0;const hostile={...result};Object.defineProperty(hostile,'data',{get(){reads++;return result.data;}});assert.throws(()=>validateVisualResult(hostile));assert.equal(reads,0);});
|
||||
test('PNG ancillary content-bearing chunks are denied before export',()=>{const validPngWithText=png(2,2,'comment\\0PRIVATE_METADATA');assert.throws(()=>pngSize(validPngWithText));});
|
||||
|
||||
|
||||
|
||||
test('private release guard invalidates policy, actor, token, navigation and cancellation after helper return',async()=>{for(const mode of ['policy','actor','token','navigation','abort']){const f=fixture(),controller=new AbortController();const result=await f.make()({}, {signal:controller.signal}),guard=getVisualReleaseGuard(result);assert.equal(typeof guard,'function');assert.equal(Object.isFrozen(result),true);assert.deepEqual(Object.keys(result).sort(),['data','height','mime','width']);guard();if(mode==='policy')f.c.policy={...f.c.policy};if(mode==='actor')f.c.actor={agentId:'other',generation:1};if(mode==='token')f.invalidate();if(mode==='navigation')f.c.webContents.getURL=()=> 'https://example.com/changed';if(mode==='abort')controller.abort();assert.throws(guard,e=>e.status===403);assert.equal(getVisualReleaseGuard({...result}),undefined);assert.equal(getVisualReleaseGuard(JSON.parse(JSON.stringify(result))),undefined);}});
|
||||
test('private release guard restores original actor async context when transport calls outside it',async()=>{const f=fixture(),actors=new AsyncLocalStorage();let currentActor={agentId:'original-agent',generation:1};f.deps.getContext=()=>({...f.c,actor:actors.getStore()});const result=await actors.run(currentActor,()=>f.make()()),guard=getVisualReleaseGuard(result);assert.equal(actors.getStore(),undefined);assert.doesNotThrow(guard);f.c.policy={...f.c.policy};assert.throws(guard,e=>e.status===403);});
|
||||
|
||||
test('native frame identity, process/routing and loading state invalidate private release guard',async()=>{for(const mode of ['frame','process','routing','loading']){const f=fixture();f.c.webContents.mainFrame={processId:101,routingId:202,isDestroyed:()=>false};let loading=false;f.c.webContents.isLoading=()=>loading;const result=await f.make()(),guard=getVisualReleaseGuard(result);guard();if(mode==='frame')f.c.webContents.mainFrame={...f.c.webContents.mainFrame};if(mode==='process')f.c.webContents.mainFrame.processId++;if(mode==='routing')f.c.webContents.mainFrame.routingId++;if(mode==='loading')loading=true;assert.throws(guard,e=>e.status===403);}});
|
||||
test('same URL native navigation during inspection cannot export a stale screenshot',async()=>{const f=fixture();let loading=false;f.c.webContents.isLoading=()=>loading;f.c.webContents.mainFrame={processId:1,routingId:2,isDestroyed:()=>false};f.deps.inspectEnterprise=async()=>{loading=true;return {decision:'allow',coverage:{complete:true}};};await assert.rejects(f.make()(),e=>e.status===403);assert.ok(f.bytes.every(b=>b===0));});
|
||||
@@ -0,0 +1,32 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {createRequire} from 'node:module';
|
||||
import {EventEmitter} from 'node:events';
|
||||
import vm from 'node:vm';
|
||||
const require=createRequire(import.meta.url);
|
||||
const {visualDocumentProbe,probeVisualContext}=require('../apps/browser/agent-visual-probe.cjs');
|
||||
const {attachPageClipboardGuard,guardedVisualStructure,disposePageClipboardGuard}=require('../apps/browser/page-clipboard-guard.cjs');
|
||||
|
||||
function node(tag='DIV',attributes={},visible=true){return {tagName:tag,localName:tag.toLowerCase(),type:attributes.type||'',hidden:!visible,parentElement:null,getAttribute:key=>attributes[key]??null,hasAttribute:key=>Object.hasOwn(attributes,key),getBoundingClientRect:()=>({left:1,top:1,right:10,bottom:10,width:9,height:9})};}
|
||||
function probe(nodes){let change;const watched=new Map();const realm={location:{href:'https://example.com'},document:{readyState:'complete',querySelectorAll:selector=>selector==='video'?[]:nodes,getAnimations:()=>[]},crypto:{randomUUID:()=> 'private-document'},MutationObserver:class{constructor(fn){change=fn;this.records=[];}observe(root){watched.set(root,this);}takeRecords(){return this.records.splice(0);}disconnect(){}},addEventListener(){},innerWidth:1280,innerHeight:720,scrollX:0,scrollY:0,devicePixelRatio:1,visualViewport:{scale:1},getComputedStyle:()=>({display:'block',visibility:'visible',opacity:'1'})};vm.createContext(realm);return {run:()=>vm.runInContext(`(${visualDocumentProbe.toString()})('https://example.com')`,realm),change:()=>change(),mutate:root=>watched.get(root).records.push({})};}
|
||||
test('isolated probe rejects credential controls even offscreen and probes open shadow content',()=>{
|
||||
assert.equal(probe([node()]).run().safe,true);
|
||||
for(const attributes of [{type:'password'},{autocomplete:'section-login current-password'},{name:'otp'},{id:'api_key'}])assert.equal(probe([node('INPUT',attributes,false)]).run().safe,false);
|
||||
const host=node();host.shadowRoot={querySelectorAll:()=>[node('INPUT',{type:'password'})]};assert.equal(probe([host]).run().safe,false);
|
||||
assert.equal(probe([node('IFRAME')]).run().safe,false);
|
||||
const p=probe([node()]),first=p.run();p.change();assert.notEqual(p.run().revision,first.revision);
|
||||
});
|
||||
|
||||
async function guard(t,root){const api=new EventEmitter();let attached=false;api.isAttached=()=>attached;api.attach=()=>{attached=true;};api.detach=()=>{attached=false;api.emit('detach');};api.sendCommand=async method=>method==='Page.addScriptToEvaluateOnNewDocument'?{identifier:'private'}:method==='DOM.getDocument'?{root}:{};const contents={debugger:api,isDestroyed:()=>false};await attachPageClipboardGuard(contents,()=>{});t.after(()=>disposePageClipboardGuard(contents));return contents;}
|
||||
const dom=(extra={})=>({nodeName:'DIV',nodeValue:'',attributes:[],...extra});
|
||||
test('private native structure catches closed/declarative shadow roots and refuses budgets/frames',async t=>{
|
||||
for(const root of [dom({shadowRoots:[dom({shadowRootType:'closed'})]}),dom({contentDocument:dom()}),dom({nodeValue:'x'.repeat(1024*1024+1)})])await assert.rejects(guardedVisualStructure(await guard(t,root)));
|
||||
const contents=await guard(t,dom({shadowRoots:[dom({shadowRootType:'open'})]}));assert.equal((await guardedVisualStructure(contents)).safe,true);
|
||||
await assert.rejects(guardedVisualStructure({isDestroyed:()=>false}));
|
||||
});
|
||||
test('native subframes cannot fall back to untrusted page-world probes',async()=>{
|
||||
let executed=false;const frame={isDestroyed:()=>false,framesInSubtree:[{},{}]},contents={mainFrame:frame,isLoading:()=>false,executeJavaScriptInIsolatedWorld:()=>{executed=true;}};
|
||||
await assert.rejects(probeVisualContext({webContents:contents,url:'https://example.com'}));assert.equal(executed,false);
|
||||
});
|
||||
|
||||
test('shadow-root mutation and reversal cannot preserve screenshot revision',()=>{const host=node(),root={host:{isConnected:true},querySelectorAll:()=>[]};host.shadowRoot=root;const p=probe([host]),baseline=p.run();p.mutate(root);p.mutate(root);assert.equal(p.run().safe,true);assert.notEqual(p.run().revision,baseline.revision);const existing=node(),q=probe([existing]),before=q.run();existing.shadowRoot={host:{isConnected:true},querySelectorAll:()=>[]};assert.notEqual(q.run().revision,before.revision);});
|
||||
@@ -0,0 +1,16 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import {createRequire} from 'node:module';
|
||||
const require=createRequire(import.meta.url),{PNG}=require('pngjs');
|
||||
const {startAgentControl}=require('../apps/browser/agent-control.cjs');
|
||||
const {requestAgent}=require('../apps/browser/agent-transport-client.cjs');
|
||||
const {createAgentVisualExport}=require('../apps/browser/agent-visual-export.cjs');
|
||||
const image=()=>{const p=new PNG({width:16,height:16});p.data.fill(255);const bytes=PNG.sync.write(p);return {mime:'image/png',width:16,height:16,data:bytes.toString('base64')};};
|
||||
async function api(t,options={}){const events=[],result=image(),context=options.context||{ready:true,foreground:true,interactive:true,policy:{expiresAt:new Date(Date.now()+60000).toISOString(),agents:{enabled:true,scopes:['screenshot']}},token:{},sessionId:'fixture',actor:{agentId:'fixture',generation:1},webContents:{getURL:()=> 'https://example.com',isDestroyed:()=>false},destinationAllowed:()=>true};const screenshot=createAgentVisualExport({getContext:()=>context,contextCurrent:()=>true,probeSensitive:async()=>({safe:true,revision:'fixed',width:16,height:16}),capture:async()=>Buffer.from(result.data,'base64'),inspectLocal:async()=>{if(options.delayInspection)await new Promise(r=>setTimeout(r,options.delayInspection));return {decision:'allow',contentType:'image',coverage:{complete:true}};},inspectEnterprise:async()=>({decision:'allow',coverage:{complete:true}}),audit:async()=>{}});const service=await startAgentControl({authorize:async({scope})=>({allowed:true,role:'agent',scopes:[scope]}),actions:{screenshot},audit:async event=>events.push(event),...options});t.after(()=>service.close());const request=async(action='screenshot',args={})=>{const response=await requestAgent(service,{action,args});return {status:response.status,...await response.json()};};return {events,result,request,service,screenshot,context};}
|
||||
test('inspected PNG projection traverses authenticated PQ response and excludes extra arguments',async t=>{const f=await api(t),response=await f.request();assert.equal(response.status,200);assert.deepEqual(response.result,f.result);assert.ok(f.events.every(event=>event.scope==='browser:screenshot'));assert.equal(JSON.stringify(f.events).includes(f.result.data),false);for(const args of [{rect:{}},{desktop:true},{format:'jpeg'}])assert.equal((await f.request('screenshot',args)).status,400);});
|
||||
test('forged PNG metadata and revocation before encryption return no image',async t=>{const bad=await api(t,{actions:{screenshot:async()=>({...image(),width:17})}});const result=await bad.request();assert.notEqual(result.status,200);assert.equal(result.result,undefined);let allowed=true;const revoked=await api(t,{authorize:async({scope})=>({allowed,role:'agent',scopes:[scope]}),audit:async event=>{if(event.outcome==='completed')allowed=false;}});const denied=await revoked.request();assert.equal(denied.status,403);assert.equal(denied.result,undefined);});
|
||||
test('long inspection deadline is action-specific and ordinary operations retain their deadline',async t=>{const f=await api(t,{timeoutMs:20,actionTimeouts:{screenshot:1000},delayInspection:60});const read=await api(t,{timeoutMs:20,actions:{read:async()=>{await new Promise(r=>setTimeout(r,60));return {text:'public'};}}});assert.equal((await f.request()).status,200);assert.equal((await read.request('read')).status,504);await assert.rejects(startAgentControl({authorize:()=>{},actions:{},audit:()=>{},actionTimeouts:{unknown:100}}));});
|
||||
|
||||
test('original inspected context is required after completed audit even if grants and host still allow',async t=>{for(const change of ['policy','foreground','actor']){let f;f=await api(t,{audit:async event=>{if(event.outcome==='completed'){if(change==='policy')f.context.policy={...f.context.policy,dlp:{rules:[{id:'new-block'}]}};if(change==='foreground')f.context.foreground=false;if(change==='actor')f.context.actor={agentId:'other',generation:1};}}});const response=await f.request();assert.equal(response.status,403,change);assert.equal(response.result,undefined);}});
|
||||
|
||||
test('synchronous release diagnostics cannot skip or repeat the final native authority check',async t=>{for(const mode of ['before','after','twice','missing']){let f;const traceRelease=(_name,fn)=>{if(mode==='before')throw Error('Diagnostic unavailable');if(mode==='missing')return;fn();if(mode==='after')throw Error('Diagnostic unavailable');if(mode==='twice')fn();};f=await api(t,{traceRelease,audit:async event=>{if(event.outcome==='completed')f.context.foreground=false;}});const denied=await f.request();assert.equal(denied.status,403,mode);assert.equal(denied.result,undefined);const allowed=await api(t,{traceRelease});assert.equal((await allowed.request()).status,200,mode);}});
|
||||
Reference in new issue
Block a user