Add policy-bound encrypted agent session restoration and audit traces
This commit is contained in:
1 parent
e8a654fb38
commit
b991ee9da6
27 files changed
+405
-24
No files matched your search
+21
-6
@@ -192,7 +192,8 @@ function requireVault(){
|
||||
}
|
||||
function persistAudit(){
|
||||
requireEncryption();const target=path.join(app.getPath('userData'),'audit-queue.bin'),temporary=target+'.'+require('node:crypto').randomUUID()+'.tmp';let fd;
|
||||
try{fd=fs.openSync(temporary,'wx',0o600);fs.writeFileSync(fd,safeStorage.encryptString(JSON.stringify(auditQueue)));fs.fsyncSync(fd);fs.closeSync(fd);fd=undefined;fs.renameSync(temporary,target);}finally{if(fd!==undefined)fs.closeSync(fd);try{fs.unlinkSync(temporary);}catch{}}
|
||||
const stage=(operation,fn)=>diagnostics.spanSync('browser.security',diagnosticContext({operation}),fn);
|
||||
try{const serialized=stage('audit.serialize',()=>JSON.stringify(auditQueue));const encrypted=stage('audit.encrypt',()=>safeStorage.encryptString(serialized));stage('audit.write',()=>{fd=fs.openSync(temporary,'wx',0o600);fs.writeFileSync(fd,encrypted);});stage('audit.fsync',()=>fs.fsyncSync(fd));stage('audit.commit',()=>{fs.closeSync(fd);fd=undefined;fs.renameSync(temporary,target);});}finally{if(fd!==undefined)fs.closeSync(fd);try{fs.unlinkSync(temporary);}catch{}}
|
||||
}
|
||||
function enforceEvidencePolicy(){
|
||||
if(!policy)return;const delivery=require('./evidence-delivery.cjs');
|
||||
@@ -415,6 +416,7 @@ async function ensureAgentAPI(){
|
||||
const payload=await diagnosticSpan('browser.evidence',{operation:'evidence.capture',sessionId:metadata.sessionId},()=>require('./evidence-capture.cjs').buildInteractionEvidence({...metadata,url:metadata.pageUrl,captureSource:'agent-adapter'},policy));if(!payload)return;
|
||||
const actor=agentActorStorage.getStore();await auditAgent({action:'click',outcome:'completed',sessionId:metadata.sessionId,...(actor?.principalId?{principalId:actor.principalId,generation:actor.generation}:{}),protectedInteraction:payload});
|
||||
}});
|
||||
Object.assign(actions,agentSessionControls());
|
||||
for(const action of ['click','type','select','check']){const original=actions[action];actions[action]=async(...args)=>{const tab=tabs.get(args[0]?.sessionId||activeTab().id),context=tabContext(tab.id);const checked=await checkVisual(tab);if(!currentTabContext(context)||checked.decision!=='allow')throw Error('Visual login protection has not approved this page');return original(...args);};}
|
||||
for(const action of Object.keys(actions)){const original=actions[action];actions[action]=(args,context={})=>agentActorStorage.run({...context,action},()=>diagnosticSpan('browser.agent',{operation:action},async()=>{
|
||||
if(context.principalId&&['navigate','back','forward','reload','history:go','vault:fill','click','type','select','check'].includes(action)){const tab=tabs.get(args.sessionId||activeTab().id);tab.security.agentAuthority={principalId:context.principalId,generation:context.generation,action};}
|
||||
@@ -427,7 +429,7 @@ async function ensureAgentAPI(){
|
||||
const actor={principalId,generation,action};
|
||||
if(!actorGrantAllowed(actor,action))return {allowed:false};
|
||||
const checkURL=url=>actorDestinationAllowed(url,actor);
|
||||
if(action==='sessions')return {allowed:true,role:'agent',scopes:[scope]};
|
||||
if(action==='sessions'||['sessions:save','sessions:list','sessions:restore'].includes(action))return {allowed:true,role:'agent',scopes:[scope]};
|
||||
if(action==='bookmarks:mutate'&&args.bookmark?.url&&!checkURL(args.bookmark.url))return {allowed:false};
|
||||
if(action==='bookmarks:mutate'&&args.action!=='add'){const existing=personalBookmarks.items.find(row=>row.id===(args.id||args.bookmark?.id));if(!existing||!checkURL(existing.url))return {allowed:false};}
|
||||
if(action==='tabs:create'&&args.url&&!checkURL(args.url))return {allowed:false};
|
||||
@@ -627,21 +629,34 @@ function installSiteHooks(tab){
|
||||
});
|
||||
}
|
||||
function createNativeTab(input={}){return diagnosticSpan('browser.tab',{operation:'create'},()=>createNativeTabImpl(input));}
|
||||
async function createNativeTabImpl(input={}){
|
||||
async function createNativeTabImpl(input={},controls={}){
|
||||
if(!input||typeof input!=='object'||Object.keys(input).some(key=>!['group','url'].includes(key)))throw Error('Invalid native tab request');
|
||||
if(input.group)requireManagedSection('library');
|
||||
const tab=tabs.create({group:input.group||''});tab.security.agentAuthority=input.agentAuthority||agentActorStorage.getStore()?.principalId&&{...agentActorStorage.getStore()};installSiteHooks(tab);
|
||||
controls.assertCurrent?.();const tab=tabs.create({group:input.group||''});
|
||||
try{
|
||||
controls.onCreated?.(tab);tab.security.agentAuthority=agentActorStorage.getStore()?.principalId&&{...agentActorStorage.getStore()};installSiteHooks(tab);
|
||||
await tab.view.webContents.loadURL('about:blank');
|
||||
controls.assertCurrent?.();
|
||||
await diagnosticSpan('browser.clipboard',{operation:'create',sessionId:tab.id},()=>attachPageClipboardGuard(tab.view.webContents,()=>{clipboardFatal=true;quarantine('Managed clipboard protection failed; restart the browser');audit('clipboard.block','clipboard-primitive-unavailable');app.quit();}));
|
||||
tab.guarded=true;selectTab(tab.id);
|
||||
if(input.url){if(tab.security.agentAuthority){await agentActorStorage.run(tab.security.agentAuthority,()=>navigate({url:input.url,tabId:tab.id}));}else await navigate({url:input.url,tabId:tab.id});}emit();return state();
|
||||
controls.assertCurrent?.();tab.guarded=true;selectTab(tab.id);controls.onActivated?.(tab);controls.assertCurrent?.();
|
||||
if(input.url){if(tab.security.agentAuthority){await agentActorStorage.run(tab.security.agentAuthority,()=>navigate({url:input.url,tabId:tab.id},undefined,{signal:controls.signal}));}else await navigate({url:input.url,tabId:tab.id},undefined,{signal:controls.signal});}controls.assertCurrent?.();emit();return state();
|
||||
}catch(error){if(tabs.all().includes(tab)){tabs.remove(tab.id);if(tabs.active()?.guarded)selectTab(tabs.active().id);}throw error;}
|
||||
}
|
||||
|
||||
function updateManagedAppearance(input){requireManagedSection('appearance');appearancePreferences={...appearancePreferences,...validateAppearance(input)};persistAppearance();emit();return managedSettings();}
|
||||
async function closeNativeTab(id){tabs.remove(id);if(!tabs.all().length)await createNativeTab();else selectTab(tabs.active().id);emit();return state();}
|
||||
function groupNativeTab(input){requireManagedSection('library');if(libraryOrganizer?.isBusy())throw Error('Library organization is running');if(!input||typeof input.group!=='string'||input.group.length>40)throw Error('Invalid native tab group');tabs.get(input.id).group=input.group;emit();return state();}
|
||||
let sessionSnapshotStore;
|
||||
function agentSessionControls(){
|
||||
requireEncryption();sessionSnapshotStore??=require('./session-snapshot.cjs').createSessionSnapshotStore({filePath:path.join(app.getPath('userData'),'managed-sessions.bin'),protect:bytes=>safeStorage.encryptString(bytes.toString('base64')),unprotect:bytes=>Buffer.from(safeStorage.decryptString(bytes),'base64')});
|
||||
const sessionContext=id=>{const context=agentContext(id),actor=agentActorStorage.getStore();return {...context,ready:context.ready&&actorGrantAllowed(actor,actor?.action),actor:context.actor||Object.freeze({agentId:'legacy-paired-agent',generation:1})};};
|
||||
return require('./agent-session-adapters.cjs').createAgentSessionAdapters({getContext:sessionContext,getSessions:()=>tabs.all().map(tab=>({id:tab.id,title:tab.title,group:tab.group})),contextCurrent:currentTabContext,
|
||||
getOwner:context=>({deviceId:device.id,ownerId:JSON.stringify([policy.agents.identityMode,context.actor.agentId,context.actor.generation])}),store:sessionSnapshotStore,inspect:inspectManagedAgentText,
|
||||
traceOperation:(operation,fn)=>diagnosticSpan('browser.session',{operation:'sessions.'+operation},fn),
|
||||
restore:(collection,options)=>require('./session-restore.cjs').restoreNativeSession(collection,options,{registry:tabs,store:sessionSnapshotStore,
|
||||
create:async(input,controls)=>{const result=await diagnosticSpan('browser.tab',{operation:'create'},()=>createNativeTabImpl(input,controls));return result.activeTabId;},activate:id=>{selectTab(id);emit();},traceOperation:(operation,fn)=>diagnosticSpan('browser.session',{operation:'sessions.'+operation},fn)})
|
||||
});
|
||||
}
|
||||
function agentAppControls(){
|
||||
const actorIdentity=()=>{const actor=agentActorStorage.getStore();return actor?.principalId?`${actor.principalId}:${actor.generation}`:'legacy';};
|
||||
const windowControls=require('./window-controls.cjs').createWindowControls({getWindow:()=>win,getContext:()=>({ready:ready&&featureAllowed(managedSettings(),'agents'),policy,policyVersion:policy?.version,deviceId:device?.id,actor:actorIdentity()}),isCurrent:context=>ready&&policy===context.policy&&device?.id===context.deviceId&&actorIdentity()===context.actor,authorize:action=>actorGrantAllowed(agentActorStorage.getStore(),action),traceOperation:(operation,fn)=>diagnosticSpan('browser.window',{operation},fn)});
|
||||
|
||||
Reference in new issue
Block a user