Implement managed browser security, individual agents and observability

This commit is contained in:
Karl committed 2026-10-13 16:58:15 +02:00
1 parent ff482ebe23
commit ea448de2d4
172 files changed
+5404 -347

No files matched your search

+215 -54
View File
@@ -20,7 +20,7 @@ function diagnosticSpan(name,attributes,operation){
let diagnosticsConfiguration='';
function configureDiagnostics(config){const value=config||{enabled:false},context=diagnosticContext(),key=JSON.stringify([value,context]);if(key===diagnosticsConfiguration)return;try{diagnostics.configure(value,context);diagnosticsConfiguration=key;}catch{diagnosticsConfiguration='';try{diagnostics.configure({enabled:false});}catch{}}}
function diagnosticCorrelation(){try{const ids=diagnostics.correlation?.();return /^[a-f0-9]{32}$/.test(ids?.traceId||'')&&/[1-9a-f]/.test(ids.traceId)&&/^[a-f0-9]{16}$/.test(ids?.spanId||'')&&/[1-9a-f]/.test(ids.spanId)?{traceId:ids.traceId,spanId:ids.spanId}:{};}catch{return {};}}
let win,view,browserSession,managementSession,core,device,policy,ready=false,syncing=false,aiPanel=false,vaultPanel=false,vault,vaultTimer;
let win,view,browserSession,managementSession,core,device,policy,ready=false,syncing=false,aiPanel=false,vaultPanel=false,vault,vaultTimer,fileUploadGate,uploadLeaseFatal=false;
let chromeHeight=104,leftSidebar=false,startPage=false,overviewPanel=false,toolsOpen=false,pageTitle='',pageColor='#edf1f6';
const {createTabRegistry}=require('./tab-registry.cjs');
let tabs,tabsRevision=0;
@@ -41,7 +41,7 @@ const {createVault}=require('./vault.cjs');
const {createDeveloperToolsGuard,developerAssetRequestAllowed}=require('./developer-tools.cjs');
const {settingsState,sectionVisible,sectionAllowed,featureAllowed,requireFeature,requireSection,validateAppearance}=require('./managed-settings.cjs');
const {bookmarkState,validateMutation:validateBookmarkMutation,libraryVisible}=require('./bookmarks.cjs');
let bookmarkBusy=false,personalBookmarks={revision:0,items:[]},bookmarkSnapshotJSON='';
let bookmarkBusy=false,personalBookmarks={revision:0,items:[]},bookmarkSnapshotJSON='',libraryOrganizer;
function effectiveBookmarkPolicy(){return policy?{...policy,browser:{...policy.browser,bookmarks:{...policy.browser.bookmarks,personal:personalBookmarks}}}:null;}
let appearancePreferences={};
function managedSettings(){return settingsState({policy,ready,preferences:appearancePreferences});}
@@ -94,9 +94,34 @@ async function configureVisualPage(enabled,tab=activeTab()){
ipcMain.on('managed:visual-configured',handler);contents.send('managed:visual-config',{enabled,token});
});
}
let companyNews=[],companyNewsKey='',companyNewsFetchedAt=0;
let message='Enroll this browser to receive company policy.',syncTimer,localInspector,auditQueue=[],flushing=false,agentService,agentStarting=false,securityCore;
const recentRequests=new Map();
const agentFile=()=>path.join(app.getPath('userData'),'agent-connection.json');
const agentGrants=require('./agent-grants.cjs');
const agentActorStorage=new (require('node:async_hooks').AsyncLocalStorage)();
let agentServiceMode;
function effectiveAgentPolicy(){if(!policy)return policy;const {grants}=agentGrants.reconcileGenerationFloor(policy.agents.grants||[],device?.agentGenerationFloor||{});return {...policy,agents:{...policy.agents,grants}};}
function approvedAgentGrants(){return agentGrants.activeGrants(effectiveAgentPolicy());}
function actorGrantAllowed(actor,action,url){return agentGrants.authorizeGrant({policy:effectiveAgentPolicy(),ready:ready&&featureAllowed(managedSettings(),'agents'),principalId:actor?.principalId,generation:actor?.generation,action,url,hostMatches:core.hostMatches}).allowed;}
function agentRequestOwnerAllowed(source){return policy?.agents?.identityMode!=='individual'||!!source?.guarded;}
function actorDestinationAllowed(url,actor=agentActorStorage.getStore()){if(!actor?.action)return core.destinationAllowed(url,policy);return core.destinationAllowed(url,policy)&&actorGrantAllowed(actor,actor.action,url);}
async function reconcileAgentPolicy(){
const result=agentGrants.reconcileGenerationFloor(policy.agents.grants||[],device.agentGenerationFloor||{});
if(JSON.stringify(result.floor)!==JSON.stringify(device.agentGenerationFloor||{})){device.agentGenerationFloor=result.floor;saveDevice(device);}
const mode=policy.agents.identityMode;
if(agentService&&(agentServiceMode!==mode||!policy.agents.enabled||policy.browser.settings.hiddenSections.includes('agents')||policy.browser.settings.lockedSections.includes('agents'))){agentService.revoke();await agentService.close();agentService=null;agentServiceMode=undefined;}
if(mode==='individual'){try{fs.unlinkSync(agentFile());}catch(error){if(error.code!=='ENOENT')throw error;}agentService?.reconcilePrincipals(result.grants);}
}
async function pairIndividualAgent(principalId){
if(typeof principalId!=='string')throw Error('Select an approved agent');
const grant=approvedAgentGrants().find(g=>g.id===principalId),initial=policy,service=agentService;
if(!grant||!service||agentServiceMode!=='individual')throw Error('Agent grant unavailable');
const connection=service.issuePrincipal(grant.id,grant.generation,grant.expiresAt);
const credentialPath=await agentGrants.persistConnection({filePath:agentGrants.descriptorPath(app.getPath('userData'),grant.id),connection,protect:value=>require('./dpapi.cjs').capabilityCrypto('protect',value),isCurrent:()=>ready&&policy===initial&&agentService===service&&featureAllowed(managedSettings(),'agents')&&actorGrantAllowed({principalId:grant.id,generation:grant.generation},grant.scopes[0])});
return {address:service.address,credentialPath,principalId:grant.id,generation:grant.generation};
}
app.commandLine.appendSwitch('disable-http-cache');
const uiUrl=pathToFileURL(path.join(__dirname,'index.html')).href;
const configPath=()=>path.join(app.getPath('userData'),'enrollment.json');
@@ -123,13 +148,18 @@ function managementUrl(value){
const managementRequests=new Map();
function request(...args){return diagnosticSpan('browser.management',{operation:args[1]==='/api/device/ai'?'ai':args[1]==='/api/device/enroll'?'enroll':'sync'},()=>requestImpl(...args));}
async function requestImpl(server,route,body,credential){
if(route==='/api/device/evidence'){
if(!device||device.server!==server||device.token!==credential)throw Error('Protected evidence identity unavailable');
return require('./management-channel.cjs').protectedEvidenceRequest({device,request,save:saveDevice,payload:body});
}
if(route==='/api/device/policy'){
if(!device||device.server!==server||device.token!==credential)throw Error('Protected management identity unavailable');
return require('./management-channel.cjs').protectedPolicyRequest({device,request,save:saveDevice});
}
const headers={};if(body)headers['content-type']='application/json';if(credential)headers.authorization=`Bearer ${credential}`;
const correlation=diagnosticCorrelation();if(correlation.traceId&&correlation.spanId)headers.traceparent=`00-${correlation.traceId}-${correlation.spanId}-01`;
if(route==='/api/device/enroll'||route==='/api/device/sso/poll')headers['x-safetynet-pq-bootstrap']='1';
const controller=new AbortController(),timer=setTimeout(()=>controller.abort(),route.endsWith('/ai')?65000:10000);managementRequests.set(controller,route);
const controller=new AbortController(),timer=setTimeout(()=>controller.abort(),route.endsWith('/ai')||route==='/api/device/library/propose'?65000:10000);managementRequests.set(controller,route);
try{
const fetcher=managementSession?managementSession.fetch.bind(managementSession):fetch;
const r=await fetcher(`${server}${route}`,{method:body?'POST':'GET',headers,body:body?JSON.stringify(body):undefined,credentials:'omit',redirect:'error',signal:controller.signal});
@@ -137,7 +167,7 @@ async function requestImpl(server,route,body,credential){
const data=JSON.parse(Buffer.concat(chunks).toString('utf8'));if(!r.ok && r.status!==202){const e=Error('Management request refused');e.status=r.status;throw e;}return data;
}finally{clearTimeout(timer);managementRequests.delete(controller);}
}
function state(){return {bookmarks:bookmarkState({policy:effectiveBookmarkPolicy(),ready,settings:managedSettings()}),settings:managedSettings(),enrolled:!!device,deviceId:device?.id||'',ready,clipboardPending:clipboardGuard?.isPending()||false,...tabs?.summary(),tabsRevision:++tabsRevision,canNavigate:ready||!!(stickyBlock()&&managementHealthy),loginCheckPending:ready&&!!policy?.security.visualPhishing.enabled&&(!activeTab()?.security.visualApproval||activeTab().security.visualApproval.generation!==activeTab().navigationId),message,brand:policy?.brand||{name:'SafetyNet',color:'#137c66'},policyVersion:policy?.version||0,server:device?.server||'',url:view?.webContents.getURL()||'',aiEnabled:ready&&!!policy?.ai.enabled,documentScanEnabled:ready&&policy?.inspection?.documentAnalysisEnabled!==false,passwordManagerEnabled:ready&&!!policy?.passwordManager?.enabled,vaultLocked:vault?.status().locked??true,vaultExists:vault?.status().exists??fs.existsSync(path.join(app.getPath('userData'),'password-vault.json')),agentsEnabled:ready&&!!policy?.agents.enabled,agentConnected:!!agentService};}
function state(){return {startPage:{...(policy?.startPage||{}),news:ready?companyNews:[]},libraryAI:{suggestionsEnabled:ready&&!!policy?.ai.enabled&&!!policy?.library?.aiSuggestionsEnabled&&sectionAllowed(managedSettings(),'library')&&featureAllowed(managedSettings(),'ai'),autoApplyEnabled:ready&&!!policy?.library?.aiAutoApplyEnabled&&policy?.ai?.trustMode==='local',busy:libraryOrganizer?.isBusy()||false},bookmarks:bookmarkState({policy:effectiveBookmarkPolicy(),ready,settings:managedSettings()}),settings:managedSettings(),enrolled:!!device,deviceId:device?.id||'',ready,clipboardPending:clipboardGuard?.isPending()||false,...tabs?.summary(),tabsRevision:++tabsRevision,canNavigate:ready||!!(stickyBlock()&&managementHealthy),loginCheckPending:ready&&!!policy?.security.visualPhishing.enabled&&(!activeTab()?.security.visualApproval||activeTab().security.visualApproval.generation!==activeTab().navigationId),message,brand:policy?.brand||{name:'SafetyNet',color:'#137c66'},policyVersion:policy?.version||0,server:device?.server||'',url:view?.webContents.getURL()||'',aiEnabled:ready&&!!policy?.ai.enabled,documentScanEnabled:ready&&policy?.inspection?.documentAnalysisEnabled!==false,passwordManagerEnabled:ready&&!!policy?.passwordManager?.enabled,vaultLocked:vault?.status().locked??true,vaultExists:vault?.status().exists??fs.existsSync(path.join(app.getPath('userData'),'password-vault.json')),agentIdentityMode:policy?.agents.identityMode||'legacy',agentGrants:ready&&featureAllowed(managedSettings(),'agents')?approvedAgentGrants().map(({id,name,generation,expiresAt})=>({id,name,generation,expiresAt})):[],agentsEnabled:ready&&!!policy?.agents.enabled,agentConnected:!!agentService};}
function emit(){if(win&&!win.isDestroyed()){const settings=managedSettings();if(!settings.visible)toolsOpen=false;if(!sectionAllowed(settings,'appearance'))chromeHeight=settings.appearance.tabMode==='compact'?64:104;if(!featureAllowed(settings,'ai'))aiPanel=false;if(!featureAllowed(settings,'passwords')){vaultPanel=false;vault?.lock();}if(!libraryVisible(settings))leftSidebar=false;layout();win.webContents.send('managed:state',{...state(),pageTitle,pageColor,windowMaximized:win.isMaximized()});win.setTitle(`S Browser · ${state().brand.name}`);}}
function layout(){const tab=activeTab();if(!win||!tab)return;view=tab.view;startPage=tab.startPage;const [width,height]=win.getContentSize(),left=leftSidebar?240:0;view.setBounds({x:left,y:chromeHeight,width:Math.max(0,width-left-((aiPanel||vaultPanel)?360:0)),height:Math.max(0,height-chromeHeight)});view.setVisible(ready&&tab.guarded&&!startPage&&!overviewPanel&&!toolsOpen);}
async function pageTheme(tab=activeTab()){
@@ -147,7 +177,7 @@ async function pageTheme(tab=activeTab()){
}catch{}
}
function lockVault(){const unlocked=vault&&!vault.status().locked;vault?.lock();if(unlocked)audit('vault.locked');emit();}
function quarantine(reason){ready=false;if(policy&&Date.parse(policy.expiresAt)<=Date.now())configureDiagnostics({enabled:false});for(const [controller,route] of managementRequests)if(route==='/api/device/ai')controller.abort();message=reason;vaultPanel=false;vault?.lock();tabs?.quarantine();emit();}
function quarantine(reason){ready=false;fileUploadGate?.cancelAll();if(policy&&Date.parse(policy.expiresAt)<=Date.now()){configureDiagnostics({enabled:false});const expiredService=agentService;agentService=null;agentServiceMode=undefined;expiredService?.revoke();void expiredService?.close().catch(()=>{});}for(const [controller,route] of managementRequests)if(route==='/api/device/ai')controller.abort();enforceEvidencePolicy();message=reason;vaultPanel=false;vault?.lock();tabs?.quarantine();emit();}
function vaultSettings(){return {enabled:ready&&!!policy?.passwordManager?.enabled,allowSaving:!!policy?.passwordManager?.allowSaving,allowAutofill:!!policy?.passwordManager?.allowAutofill,idleLockSeconds:policy?.passwordManager?.idleLockSeconds||300};}
function vaultStatus(){return {...(vault?.status()||{exists:fs.existsSync(path.join(app.getPath('userData'),'password-vault.json')),locked:true}),...vaultSettings()};}
function requireVault(){
@@ -157,20 +187,33 @@ function requireVault(){
if(!vault||vault.status().idleSeconds!==settings.idleLockSeconds){vault?.lock();vault=createVault({filePath:path.join(app.getPath('userData'),'password-vault.json'),idleSeconds:settings.idleLockSeconds,protect:bytes=>safeStorage.encryptString(bytes.toString('base64')),unprotect:bytes=>Buffer.from(safeStorage.decryptString(bytes),'base64')});}
return vault;
}
function persistAudit(){requireEncryption();fs.writeFileSync(path.join(app.getPath('userData'),'audit-queue.bin'),safeStorage.encryptString(JSON.stringify(auditQueue)),{mode:0o600});}
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{}}
}
function enforceEvidencePolicy(){
if(policy&&(policy.evidence?.enabled!==true||policy.evidence.captureDownloads!==true||!(Date.parse(policy.expiresAt)>Date.now()))){for(const [controller,route]of managementRequests)if(route==='/api/device/evidence'||route.startsWith('/api/device/channel/evidence'))controller.abort();if(auditQueue.some(e=>e.protectedEvidence)){auditQueue=require('./evidence-delivery.cjs').stripPendingEvidence(auditQueue);persistAudit();}}
}
async function flushAudit(){
if(!device||flushing||!auditQueue.length)return;flushing=true;
try{for(let n=0;n<100&&auditQueue.length;n++){await request(device.server,'/api/device/events',auditQueue[0],device.token);auditQueue.shift();persistAudit();}}
try{for(let n=0;n<100&&auditQueue.length;n++){await require('./evidence-delivery.cjs').deliverAuditRecord({record:auditQueue[0],send:(route,body)=>request(device.server,route,body,device.token),getContext:()=>({ready,policy,deviceId:device.id})});auditQueue.shift();persistAudit();}}
catch{}finally{flushing=false;}
}
async function audit(kind,ruleId,host,fileFacts){
async function audit(kind,ruleId,host,fileFacts,requestFacts,evidencePayload,agentFacts){
if(!device||!policy)return;
// Operational correlation contains no rule text, hostname or forensic payload.
try{diagnostics.event('browser.security',diagnosticContext({decision:kind.endsWith('.block')?'block':kind.endsWith('.denied')?'deny':'unknown'}));}catch{}
try{diagnostics.event('browser.security',diagnosticContext({decision:kind.endsWith('.block')?'block':kind.endsWith('.denied')?'deny':kind.endsWith('.authorized')||kind.endsWith('.released')?'allow':'unknown',reason:ruleId}));}catch{}
if(auditQueue.length>=10000){quarantine('Security event queue is full. Contact your administrator.');return;}
try{const file=fileFacts?require('../../packages/core/transfer-facts.cjs').transferFacts(fileFacts):undefined;auditQueue.push({eventId:require('node:crypto').randomUUID(),kind,...(ruleId?{ruleId}:{}),...(host?{host}:{}),...(file?{file}:{}),...diagnosticCorrelation(),policyVersion:policy.version});persistAudit();flushAudit();}
try{const file=fileFacts?require('../../packages/core/transfer-facts.cjs').transferFacts(fileFacts):undefined;const request=requestFacts?require('../../packages/core/request-transfer-facts.cjs').requestTransferFacts(requestFacts):undefined;const agent=agentFacts?require('../../packages/core/agent-audit.cjs').agentAuditFacts(agentFacts):undefined;auditQueue.push({eventId:require('node:crypto').randomUUID(),kind,...(ruleId?{ruleId}:{}),...(host?{host}:{}),...(file?{file}:{}),...(request?{request}:{}),...(agent?{agent}:{}),...(evidencePayload&&require('./evidence-delivery.cjs').collectionAllowed({ready,policy})?{protectedEvidence:{deviceId:device.id,payload:require('./evidence-delivery.cjs').validatedEvidence(evidencePayload)}}:{}),...diagnosticCorrelation(),policyVersion:policy.version});persistAudit();const eventId=auditQueue.at(-1).eventId;flushAudit();return eventId;}
catch{quarantine('Security events cannot be persisted. Contact your administrator.');}
}
function auditAgent(event){
const action=event.action==='agent.read'?'read':event.action||'agent-request',outcome=event.outcome==='blocked'?'denied':event.outcome;
const helper=require('../../packages/core/agent-audit.cjs'),session=helper.AgentAuditSchema.shape.sessionId.safeParse(event.sessionId);
const agent=helper.agentAuditFacts({action,outcome,...(event.scope?{scope:event.scope}:{}),...(session.success&&session.data?{sessionId:session.data}:{}),...(event.principalId?{principalId:event.principalId,credentialGeneration:event.generation}:{})});
return audit(['denied','failed'].includes(outcome)?'agent.denied':'agent.action',`${event.action||'agent-request'}:${event.outcome}`,undefined,undefined,undefined,undefined,agent);
}
function applyPolicy(envelope){return diagnosticSpan('browser.policy',{},()=>applyPolicyImpl(envelope));}
async function applyPolicyImpl(envelope){
const next=assertPolicyAssignment(core.verifyPolicy(envelope,device.publicKey,device.minimumVersion||0),device.id);
@@ -181,7 +224,7 @@ async function applyPolicyImpl(envelope){
if(process.env.SAFETYNET_POLICY_KEY_FILE&&fs.readFileSync(process.env.SAFETYNET_POLICY_KEY_FILE,'utf8').trim()!==device.publicKey.trim())throw Error('Policy signing key does not match deployment pin');
if(policy&&next.version===policy.version){
// Same versions must describe identical policy; reject equivocation.
if(JSON.stringify(next)!==JSON.stringify(policy))throw Error('Policy changed without a new version');acceptBookmarks();if(Date.parse(policy.expiresAt)>Date.now())configureDiagnostics(policy.diagnostics);emit();return;
if(JSON.stringify(next)!==JSON.stringify(policy))throw Error('Policy changed without a new version');acceptBookmarks();await reconcileAgentPolicy();if(Date.parse(policy.expiresAt)>Date.now())configureDiagnostics(policy.diagnostics);emit();return;
}
configureDiagnostics({enabled:false});quarantine('Applying company policy…');
const proxy=next.proxy;
@@ -191,9 +234,9 @@ async function applyPolicyImpl(envelope){
app.configureHostResolver(resolverOptions(next.enterprise?.dns));
await browserSession.clearHostResolverCache();recentRequests.clear();
await managementSession.clearHostResolverCache();
policy=next;device.minimumVersion=next.version;acceptBookmarks();configureDiagnostics(next.diagnostics);
policy=next;if(!next.evidence.enabled||!next.evidence.captureDownloads)enforceEvidencePolicy();device.minimumVersion=next.version;acceptBookmarks();configureDiagnostics(next.diagnostics);
for(const tab of tabs.all()){await configureVisualPage(next.security.visualPhishing.enabled,tab);if(!next.browser.allowDevTools){tab.security.developerToolsGuard?.close();tab.view.webContents.closeDevTools();}}
if((!next.agents.enabled||next.browser.settings.hiddenSections.includes('agents')||next.browser.settings.lockedSections.includes('agents'))&&agentService){agentService.revoke();await agentService.close();agentService=null;try{fs.unlinkSync(agentFile());}catch{}}
await reconcileAgentPolicy();
await audit('policy.applied');
}
async function sync(){
@@ -210,14 +253,16 @@ async function sync(){
}
if(Date.parse(policy.expiresAt)<=Date.now())throw Error('Company policy expired');
if(clipboardFatal)throw Error('Managed renderer security failed; restart the browser');
if(uploadLeaseFatal)throw Error('Upload protection failed; restart the browser');
const posture=await diagnosticSpan('browser.network',{operation:'scan'},()=>networkPosture.check(policy.enterprise?.vpn));
if(!posture.allowed)throw Error('Required corporate network is unavailable. Connect your company VPN.');
managementHealthy=true;
const blocked=stickyBlock();
if(blocked&&blocked.policyVersion===policy.version&&(blockedTab()?.security.visualBlocked?policy.security.visualPhishing.enabled:policy.security.content.blockedPhrases.length)){message=blocked.message;ready=false;emit();layout();return;}
for(const tab of tabs.all()){tab.security.visualBlocked=null;tab.security.contentBlocked=null;}
const recovering=!ready;ready=true;tabs.resume(policy.version);message=`Managed · policy v${policy.version}`;emit();layout();
const recovering=!ready;ready=true;flushAudit();tabs.resume(policy.version);message=`Managed · policy v${policy.version}`;emit();layout();
await ensureAgentAPI();
void refreshCompanyNews();
if(recovering){for(const tab of tabs.all()){const current=tab.view.webContents.getURL();if(/^https?:\/\//.test(current)&&core.destinationAllowed(current,policy))tab.view.webContents.reload();else if(current!=='about:blank'||tabs.all().length===1)await navigate({url:policy.browser.homeUrl,tabId:tab.id});}emit();}
}catch(e){
// A deliberate page quarantine aborts in-flight management requests too.
@@ -229,6 +274,9 @@ async function sync(){
}
finally{syncing=false;}
}
async function refreshCompanyNews(){
const p=policy,key=JSON.stringify([p?.version,p?.startPage]);if(!ready||!p?.startPage?.newsEnabled){companyNews=[];companyNewsKey='';return;}if(key===companyNewsKey&&Date.now()-companyNewsFetchedAt<300000)return;companyNewsKey=key;companyNewsFetchedAt=Date.now();companyNews=[];emit();try{const result=await request(device.server,'/api/device/start-page/news',undefined,device.token);if(!ready||policy!==p||result.policyVersion!==p.version)return;companyNews=Array.isArray(result.news)?result.news.slice(0,12).filter(item=>typeof item.title==='string'&&item.title.length<=200&&typeof item.url==='string'&&/^https:\/\//.test(item.url)&&!new URL(item.url).username&&!new URL(item.url).password).map(item=>({title:item.title,url:item.url,publishedAt:item.publishedAt||null})):[];emit();}catch{if(policy===p){companyNews=[];emit();}}
}
async function completeEnrollment(server,result){
if(device)throw Error('This installation is already enrolled');
const candidate={server,id:result.id,token:result.token,publicKey:result.publicKey,minimumVersion:0};
@@ -247,13 +295,14 @@ async function navigateImpl(input,captured,options={}){
const tab=tabs.get(captured?.sessionId||input?.tabId||activeTab().id),contents=tab.view.webContents;
if(captured&&(!currentTabContext(captured.token)||captured.webContents!==contents))throw Error('Agent context changed');
const url=address.includes('://')?address:`https://${address}`;
if(!policy||!['https:','http:'].includes(new URL(url).protocol)||!core.destinationAllowed(url,policy))throw Error('Destination is blocked by company policy');
if(!policy||!['https:','http:'].includes(new URL(url).protocol)||!actorDestinationAllowed(url))throw Error('Destination is blocked by company policy');
const blocked=stickyBlock(),documentURL=value=>{const parsed=new URL(value);parsed.hash='';return parsed.href;};
if(blocked&&managementHealthy&&Date.parse(policy.expiresAt)>Date.now()&&blockedTab()===tab&&documentURL(url)!==documentURL(blocked.checkedURL)){
for(const record of tabs.all()){record.security.visualBlocked=null;record.security.contentBlocked=null;}ready=true;tabs.resume(policy.version);message='Checking destination…';
}
if(!ready)throw Error('Browser is not managed and ready');
if(options.signal?.aborted)throw Error('Agent action cancelled');
if(!agentActorStorage.getStore()?.principalId&&!captured)tab.security.agentAuthority=null;
const initial=policy;tab.startPage=false;if(tab===activeTab())startPage=false;layout();
const abort=()=>contents.stop();options.signal?.addEventListener('abort',abort,{once:true});
try{await contents.loadURL(url);}finally{options.signal?.removeEventListener('abort',abort);}
@@ -262,7 +311,15 @@ async function navigateImpl(input,captured,options={}){
if(captured?.token.foreground&&(!refreshed.foreground||captured.token.activation!==refreshed.token.activation))throw Error('Agent foreground changed during navigation');
emit();return refreshed||state();
}
function agentContext(id){const tab=id?tabs.get(id):activeTab(),foreground=tab===activeTab();return {ready:ready&&featureAllowed(managedSettings(),'agents'),interactive:foreground&&!tab.startPage&&!overviewPanel&&!toolsOpen,policy,policyVersion:policy?.version,sessionId:tab.id,foreground,token:tabContext(tab.id,{foreground}),webContents:tab.view.webContents,destinationAllowed:core.destinationAllowed};}
async function agentHistory(action,captured,{signal}={}){
if(!currentTabContext(captured.token)||captured.foreground!==true)throw Error('Agent context changed');
const tab=tabs.get(captured.sessionId),contents=tab.view.webContents,initial=policy,activation=captured.token.activation;
const current=(phase='after')=>ready&&policy===initial&&!signal?.aborted&&!contents.isDestroyed()&&tabs.fromContents(contents.id)===tab&&activeTab()===tab&&tabContext(tab.id,{foreground:true}).activation===activation&&(phase!=='before'||currentTabContext(captured.token));
const allowed=url=>actorDestinationAllowed(url)&&(!initial.agents.allowedHosts.length||initial.agents.allowedHosts.some(pattern=>core.hostMatches(new URL(url).hostname,pattern)));
const result=await diagnosticSpan('browser.navigation',{operation:action},()=>require('./history-navigation.cjs').performHistory({contents,action,allowed,current,signal}));
if(!current())throw Error('History context changed');emit();return {completed:result.completed,context:agentContext(tab.id)};
}
function agentContext(id){const tab=id?tabs.get(id):activeTab(),foreground=tab===activeTab();return {ready:ready&&featureAllowed(managedSettings(),'agents'),interactive:foreground&&!tab.startPage&&!overviewPanel&&!toolsOpen,policy,policyVersion:policy?.version,sessionId:tab.id,foreground,token:tabContext(tab.id,{foreground}),webContents:tab.view.webContents,destinationAllowed:url=>actorDestinationAllowed(url)};}
function managedAI(...args){return diagnosticSpan('browser.ai',{},()=>managedAIImpl(...args));}
async function managedAIImpl(prompt){
requireFeatureSection('ai');
@@ -277,7 +334,7 @@ async function managedAIImpl(prompt){
if(!ready||policy.version!==initial.version)throw Error('Policy changed during AI request');return result;
}
async function mutateManagedBookmarks(input){
if(bookmarkBusy||syncing)throw Error('Management update in progress. Try again.');
if(bookmarkBusy||syncing||libraryOrganizer?.isBusy())throw Object.assign(Error('Management update in progress. Try again.'),{status:409,code:'browser-management-busy'});
const initial=policy,identity=device;
const payload=validateBookmarkMutation(input,{policy:effectiveBookmarkPolicy(),ready,settings:managedSettings(),destinationAllowed:core.destinationAllowed});
bookmarkBusy=true;
@@ -290,11 +347,37 @@ async function mutateManagedBookmarks(input){
return state();
}finally{bookmarkBusy=false;}
}
function requireLibraryAI(){
requireManagedSection('library');requireFeatureSection('ai');
if(!ready||!policy?.ai.enabled||!policy.library?.aiSuggestionsEnabled||Date.parse(policy.expiresAt)<=Date.now())throw Error('AI library organization is disabled by company policy');
if(!policy.browser.bookmarks.allowUserEdit)throw Error('Bookmark organization is controlled by your administrator');
}
function nativeLibraryRows(){return tabs.all().filter(tab=>!tab.startPage&&/^https?:\/\//.test(tab.view.webContents.getURL())).map(tab=>({id:tab.id,title:String(tab.title).replace(/[\u0000-\u001f\u007f]/g,' ').slice(0,120),url:tab.view.webContents.getURL()}));}
function nativeLibraryEpoch(){return JSON.stringify(tabs.all().map(tab=>[tab.id,tab.navigationId,tab.title,tab.view.webContents.getURL(),tab.group]));}
function assertAgentLibraryDestinations(snapshot){if(agentActorStorage.getStore()?.principalId&&[...(snapshot.tabs||[]),...(snapshot.bookmarks||[])].some(row=>!actorDestinationAllowed(row.url)))throw Error('Agent destination denied');}
function getLibraryOrganizer(){
if(libraryOrganizer)return libraryOrganizer;
libraryOrganizer=require('./library-organization.cjs').createLibraryOrganization({
capture:async()=>{const library=await import('../../packages/core/library.mjs');requireLibraryAI();if(bookmarkBusy||syncing)throw Error('Management update in progress');const rows=library.LibraryTabsSchema.parse(nativeLibraryRows());if(agentActorStorage.getStore()?.principalId&&[...rows,...personalBookmarks.items].some(row=>!actorDestinationAllowed(row.url)))throw Error('Agent destination denied');return {policy,identity:device,policyVersion:policy.version,bookmarkRevision:personalBookmarks.revision,bookmarks:structuredClone(personalBookmarks.items),tabs:rows,snapshotHash:library.librarySnapshotHash(rows),epoch:nativeLibraryEpoch()};},
assertCurrent:(snapshot,{afterCommit=false,result}={})=>{requireLibraryAI();assertAgentLibraryDestinations(snapshot);if(policy!==snapshot.policy||device!==snapshot.identity||nativeLibraryEpoch()!==snapshot.epoch||personalBookmarks.revision!==snapshot.bookmarkRevision||afterCommit&&(result.version!==policy.version||result.snapshotHash!==snapshot.snapshotHash))throw Error('Library context changed. Generate a fresh proposal.');},
inspect:async snapshot=>{const library=await import('../../packages/core/library.mjs');const bytes=Buffer.from(JSON.stringify({bookmarks:library.libraryMetadata(snapshot.bookmarks),tabs:library.libraryMetadata(snapshot.tabs)}));let verdict=core.inspectForAI(bytes,snapshot.policy);if(verdict.decision!=='allow')throw Error('Company policy blocks library metadata');if(snapshot.policy.dlp.semantic.enabled||snapshot.policy.security.yara.enabled){if(!localInspector)throw Error('Local inspector unavailable');verdict=await localInspector.inspect(bytes,snapshot.policy);if(verdict.decision!=='allow'&&!core.allowsConfidentialLocal(verdict,snapshot.policy))throw Error('Local policy blocks library metadata');}verdict=await scanEnterprise(bytes,snapshot.policy,'ai');if(verdict.decision!=='allow')throw Error('Enterprise scanner blocks library metadata');},
propose:snapshot=>request(snapshot.identity.server,'/api/device/library/propose',{policyVersion:snapshot.policyVersion,bookmarkRevision:snapshot.bookmarkRevision,tabs:snapshot.tabs,snapshotHash:snapshot.snapshotHash},snapshot.identity.token),
commit:async(proposalId,snapshot,automatic)=>{const result=await request(snapshot.identity.server,'/api/device/library/apply',{proposalId,policyVersion:snapshot.policyVersion,expectedRevision:snapshot.bookmarkRevision,snapshotHash:snapshot.snapshotHash,mode:automatic?'automatic':'manual'},snapshot.identity.token);const signed=(await import('../../packages/core/bookmarks.mjs')).verifyBookmarkSnapshot(result.bookmarks,snapshot.identity.publicKey,{deviceId:snapshot.identity.id,policyVersion:snapshot.policyVersion,minimumRevision:snapshot.bookmarkRevision});if(signed.revision!==result.revision||![snapshot.bookmarkRevision,snapshot.bookmarkRevision+1].includes(signed.revision))throw Error('Invalid signed organization revision');if(signed.revision===snapshot.bookmarkRevision&&JSON.stringify(signed.items)!==JSON.stringify(snapshot.bookmarks))throw Error('Bookmarks changed without a new revision');return {...result,verifiedBookmarks:signed};},
applyTabs:(assignments,result)=>{for(const row of assignments)tabs.get(row.id);const signed=result.verifiedBookmarks;personalBookmarks={revision:signed.revision,items:signed.items};bookmarkSnapshotJSON=JSON.stringify(signed.items);device.bookmarkRevision=signed.revision;device.bookmarkDigest=require('node:crypto').createHash('sha256').update(bookmarkSnapshotJSON).digest('hex');saveDevice(device);for(const row of assignments)tabs.get(row.id).group=row.group;},
autoAllowed:()=>policy?.library?.aiAutoApplyEnabled===true&&policy?.ai?.trustMode==='local',
audit,changed:emit
});return libraryOrganizer;
}
async function organizeLibrary(options){return getLibraryOrganizer().plan(options);}
async function applyLibraryOrganization(input,options){
const result=await getLibraryOrganizer().apply(input,options);
await sync();return result;
}
async function ensureAgentAPI(){
if(!ready||!policy.agents.enabled||!featureAllowed(managedSettings(),'agents')||agentService||agentStarting)return;agentStarting=true;
try{
const getContext=agentContext;
const actions=createAgentAdapters({getContext,appControls:agentAppControls(),getSessions:()=>tabs.all().map(tab=>({id:tab.id,title:tab.title,foreground:tab===activeTab()})),contextCurrent:currentTabContext,navigate,ai:managedAI,
const actions=createAgentAdapters({getContext,appControls:agentAppControls(),getSessions:()=>tabs.all().filter(tab=>!agentActorStorage.getStore()?.principalId||actorDestinationAllowed(tab.view.webContents.getURL())).map(tab=>({id:tab.id,title:tab.title,foreground:tab===activeTab()})),contextCurrent:currentTabContext,navigate,history:agentHistory,ai:managedAI,
inspect:async(text,context)=>{
const aiContext=context.action==='ai';
let verdict=aiContext?core.inspectForAI(Buffer.from(text),context.policy):core.inspect(Buffer.from(text),context.policy);if(verdict.decision==='block')return verdict;
@@ -302,21 +385,31 @@ async function ensureAgentAPI(){
if(!localInspector)return {decision:'block'};const result=await diagnosticSpan('browser.inspect.local',{bytes:Buffer.byteLength(text)},()=>localInspector.inspect(Buffer.from(text),context.policy));
verdict=aiContext&&core.allowsConfidentialLocal(result,context.policy)?{...result,decision:'allow',classification:'local-only'}:result;
}if(verdict.decision!=='allow')return verdict;const external=await diagnosticSpan('browser.inspect.enterprise',{bytes:Buffer.byteLength(text)},()=>scanEnterprise(Buffer.from(text),context.policy,aiContext?'ai':'upload'));return external.decision==='allow'?verdict:external;
},audit:event=>audit('agent.denied',`${event.action}:${event.outcome}`)});
for(const action of ['click','type']){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)=>diagnosticSpan('browser.agent',{operation:action},()=>original(...args));}
agentService=await startAgentControl({actions,
},audit:auditAgent});
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','click','type','select','check'].includes(action)){const tab=tabs.get(args.sessionId||activeTab().id);tab.security.agentAuthority={principalId:context.principalId,generation:context.generation,action};}
return original(args,context);
}));}
agentServiceMode=policy.agents.identityMode;agentService=await startAgentControl({actions,principalMode:agentServiceMode,principals:agentServiceMode==='individual'?effectiveAgentPolicy().agents.grants:[],
traceOperation:(operation,fn)=>diagnosticSpan('browser.agent',{operation},fn),
onError:error=>{if(!app.isPackaged&&process.env.SAFETYNET_SMOKE_ARTIFACTS)fs.appendFileSync(path.join(process.env.SAFETYNET_SMOKE_ARTIFACTS,'smoke-entry.txt'),`Agent action diagnostic: ${error.reason}\n`);},
authorize:({action,args,scope})=>{
if(!ready||!policy?.agents.enabled||Date.parse(policy.expiresAt)<=Date.now()||!policy.agents.scopes.includes(action))return {allowed:false};
if(action==='sessions'||Object.hasOwn(agentAppControls(),action))return {allowed:true,role:'agent',scopes:[scope]};
authorize:({action,args,scope,principalId,generation})=>{
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==='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};
if(['tabs:activate','tabs:close','tabs:group'].includes(action)){let tab;try{tab=tabs.get(args.sessionId);}catch{return {allowed:false};}const url=tab.view.webContents.getURL();if(url!=='about:blank'&&!checkURL(url))return {allowed:false};}
if(Object.hasOwn(agentAppControls(),action))return {allowed:true,role:'agent',scopes:[scope]};
let tab;try{tab=args.sessionId?tabs.get(args.sessionId):activeTab();}catch{return {allowed:false};}
const url=action==='navigate'?args.url:tab.view.webContents.getURL();
if(!core.destinationAllowed(url,policy))return {allowed:false};
if(policy.agents.allowedHosts.length&&!policy.agents.allowedHosts.some(pattern=>core.hostMatches(new URL(url).hostname,pattern)))return {allowed:false};
if(!checkURL(url))return {allowed:false};
return {allowed:true,role:'agent',scopes:[scope]};
},audit:event=>audit(event.outcome==='denied'?'agent.denied':'agent.action',`${event.action}:${event.outcome}`),
persistCredential:async connection=>{const protectedConnection=await require('./dpapi.cjs').capabilityCrypto('protect',JSON.stringify(connection));fs.writeFileSync(agentFile(),JSON.stringify({format:'windows-dpapi-v1',protectedConnection}),{mode:0o600});}
},audit:auditAgent,
persistCredential:connection=>agentGrants.persistConnection({filePath:agentFile(),connection,protect:value=>require('./dpapi.cjs').capabilityCrypto('protect',value),isCurrent:()=>ready&&policy?.agents.identityMode==='legacy'&&policy.agents.enabled})
});emit();
}finally{agentStarting=false;}
}
@@ -333,15 +426,46 @@ async function dnsGateImpl(url,currentPolicy){
function hooks(){
browserSession.setPermissionRequestHandler((_wc,_permission,callback)=>callback(false));
browserSession.setPermissionCheckHandler(()=>false);
const leaseRoot=app.isPackaged?process.resourcesPath:path.join(__dirname,'../..');
fileUploadGate=require('./file-upload-gate.cjs').createFileUploadGate({
maxBytes:context=>context.policy.dlp.maxBytes,
isCurrent:context=>!uploadLeaseFatal&&ready&&policy===context.policy&&Date.parse(context.policy.expiresAt)>Date.now()&&context.pending()&&(!context.source||currentTabContext(context.source)),
// Phase callbacks cancel denied requests. Live handles remain held until a
// native terminal event; stop()/quarantine alone is not proof of termination.
abort:()=>{},
createLease:options=>diagnosticSpan('browser.upload.lease',{count:options.paths.length},()=>require('./upload-lease.cjs').acquireUploadLease({...options,
pythonPath:path.resolve(leaseRoot,app.isPackaged?'python/python.exe':'.runtime/windows/python/python.exe'),
helperPath:path.resolve(leaseRoot,app.isPackaged?'inspector/upload_lease.py':'apps/inspector/upload_lease.py'),
onLost:()=>{uploadLeaseFatal=true;options.onLost();quarantine('Upload protection failed. Restart the browser.');diagnosticSpan('browser.upload.fault',{decision:'block',reason:'upload-lease-lost'},()=>audit('upload.block','upload-lease-lost')).catch(()=>{});}})),
inspectPart:(bytes,{kind,context,filename})=>diagnosticSpan('browser.upload.part',{bytes:bytes.length},async()=>{
const p=context.policy;
if(kind==='file'){
const transfer=require('../../packages/core/file-transfer.cjs').inspectFileTransfer(bytes,p,'upload',{filename});
if(transfer.decision!=='allow')return transfer;
}
const verdict=core.inspect(bytes,p);
if(verdict.decision!=='allow'&&verdict.ruleId!=='uninspectable')return verdict;
const needsLocal=verdict.ruleId==='uninspectable'||p.dlp.semantic.enabled||p.security.yara.enabled;
if(needsLocal){
if(!localInspector)return {decision:'block',ruleId:'inspector-unavailable'};
const local=await diagnosticSpan('browser.inspect.local',{bytes:bytes.length},()=>localInspector.inspect(bytes,p));
if(local.decision!=='allow'||local.coverage?.complete!==true)return local;
}
return diagnosticSpan('browser.inspect.enterprise',{bytes:bytes.length},()=>scanEnterprise(bytes,p,'upload'));
})});
for(const event of ['onCompleted','onErrorOccurred'])browserSession.webRequest[event]({urls:['<all_urls>']},details=>{
if(!fileUploadGate.has(details.id))return;
diagnosticSpan('browser.upload.release',{},()=>fileUploadGate.terminal(details.id)).catch(()=>{uploadLeaseFatal=true;quarantine('Upload protection cleanup failed. Restart the browser.');diagnosticSpan('browser.upload.fault',{decision:'block',reason:'upload-lease-release-failed'},()=>audit('upload.block','upload-lease-release-failed')).catch(()=>{});});
});
const download=createDownloadGate({getContext:id=>{const tab=tabs.fromContents(id);return tab?{ready,policy,webContentsId:id,token:tabContext(tab.id,{foreground:false})}:null;},isCurrent:context=>!!context&&currentTabContext(context.token)&&policy===context.policy,
fetchResponse:(url,options)=>diagnosticSpan('browser.request',{operation:'download'},()=>browserSession.fetch(url,options)),scan:(bytes,p)=>diagnosticSpan('browser.inspect.enterprise',{bytes:bytes.length},()=>scanDownloadEnterprise(bytes,p,localInspector)),
fetchResponse:(url,options)=>diagnosticSpan('browser.request',{operation:'download'},()=>browserSession.fetch(url,options)),scan:(bytes,p)=>diagnosticSpan('browser.inspect.enterprise',{bytes:bytes.length},()=>scanDownloadEnterprise(bytes,p,localInspector?{inspect:(data,pol)=>diagnosticSpan('browser.inspect.local',{bytes:data.length},()=>localInspector.inspect(data,pol)),scanDownload:(data,pol)=>diagnosticSpan('browser.yara',{operation:'scan',bytes:data.length},()=>localInspector.scanDownload(data,pol))}:null)),
choosePath:async name=>{const chosen=await dialog.showSaveDialog(win,{title:'Save scanned download (choose a new file)',defaultPath:path.join(app.getPath('downloads'),name)});return chosen.canceled?null:chosen.filePath;},
writeFile:async(file,bytes,current)=>{
if(!current()||!path.isAbsolute(file)||file.includes('\\\\?\\')||file.slice(2).includes(':'))throw Error('Save path rejected');
const handle=await fs.promises.open(file,'wx',0o600);let release=false;
try{if(!current())throw Error('Policy changed');await handle.writeFile(bytes);await handle.sync();if(!current())throw Error('Policy changed');release=true;}
finally{await handle.close();if(!release)await fs.promises.unlink(file).catch(()=>{});}
},audit,status:text=>{message=text;emit();}});
},audit:(kind,rule,host,facts,evidence)=>audit(kind,rule,host,facts,undefined,evidence),status:text=>{message=text;emit();}});
browserSession.on('will-download',(event,item,contents)=>{
const url=item.getURL(),name=item.getFilename(),tracked=recentRequests.get(`${contents?.id}:${url}`);event.preventDefault();
if(!/^https?:\/\//.test(url)){audit('download.block','unsupported-download');return;}
@@ -353,14 +477,15 @@ function hooks(){
if(developerAssetRequestAllowed(details,{ready,policy,tabs:tabs.all()})){callback({cancel:false});return;}
let called=false;const finish=cancel=>{if(!called){called=true;callback({cancel});}};
const initialPolicy=policy,source=details.webContentsId>0?tabs.fromContents(details.webContentsId):null,sourceContext=source?tabContext(source.id,{foreground:false}):null;
const sourceCurrent=()=>!sourceContext||currentTabContext(sourceContext);const timer=setTimeout(()=>finish(true),40000);
const sourceCurrent=()=>require('./request-source.cjs').requestSourceCurrent({details,source,token:sourceContext,registry:tabs,currentDocument:currentTabContext});let timer;
// Async gates complete before Chromium may transmit the request.
diagnosticSpan('browser.request',{requestType:details.resourceType,...(source?{sessionId:source.id}:{})},async()=>{try{
if(!ready||!policy||Date.parse(policy.expiresAt)<=Date.now()||(details.webContentsId>0&&(!source||!source.guarded))){finish(true);return;}
if(!core.destinationAllowed(details.url,policy)){audit('navigation.block',null,new URL(details.url).hostname);finish(true);return;}
timer=setTimeout(()=>{if(called)return;finish(true);audit('navigation.block','request-inspection-timeout',new URL(details.url).hostname).catch(()=>{});},40000);
if(!ready||!policy||Date.parse(policy.expiresAt)<=Date.now()||!agentRequestOwnerAllowed(source)||(details.webContentsId>0&&(!source||!source.guarded))){if(ready&&policy&&(!agentRequestOwnerAllowed(source)||details.webContentsId>0&&(!source||!source.guarded)))await audit('navigation.block','invalid-source-owner',new URL(details.url).hostname);finish(true);return;}
if(!core.destinationAllowed(details.url,policy)||(source?.security.agentAuthority&&!actorDestinationAllowed(details.url,source.security.agentAuthority))){audit('navigation.block',null,new URL(details.url).hostname);finish(true);return;}
recentRequests.set(`${details.webContentsId}:${details.url}`,{method:details.method,time:Date.now()});if(recentRequests.size>2000)for(const [url,value]of recentRequests)if(value.time<Date.now()-120000||recentRequests.size>2000)recentRequests.delete(url);
try{await dnsGate(details.url,initialPolicy);}catch(error){audit('dns.block',error.message,new URL(details.url).hostname);finish(true);return;}
if(!ready||policy!==initialPolicy||!sourceCurrent()){finish(true);return;}
if(called||!ready||policy!==initialPolicy||!sourceCurrent()){if(!called&&ready&&policy===initialPolicy&&!sourceCurrent())await audit('navigation.block','request-source-context-changed',new URL(details.url).hostname);finish(true);return;}
if(details.resourceType==='webSocket'&&!policy.browser.allowWebSockets){audit('dlp.block','websocket-disabled',new URL(details.url).hostname);finish(true);return;}
const address=new URL(details.url),query=address.pathname+address.search;
const queryVerdict=core.inspect(Buffer.from(query),policy);
@@ -369,17 +494,25 @@ function hooks(){
if(policy.security.visualPhishing.enabled&&(uploads.length||(address.search&&details.resourceType==='xhr')||details.resourceType==='webSocket')){
const checked=source?await checkVisual(source):{decision:'unknown'};if(checked.decision!=='allow'||!ready||policy!==initialPolicy||!sourceCurrent()){finish(true);return;}
}
// Electron file/data-pipe interception cannot bind scanning to the bytes later sent.
// Block these paths rather than scanning a mutable file and allowing a race.
if(uploads.some(part=>part.type==='file')){
const context={policy:initialPolicy,source:sourceContext,pending:()=>!called};
const verdict=await fileUploadGate.inspect(details,context);
if(verdict.decision!=='allow'){await audit('upload.block',verdict.ruleId||'inspection-incomplete',address.hostname);finish(true);return;}
await audit('upload.authorized',null,address.hostname,undefined,verdict.request);
finish(uploadLeaseFatal||!ready||policy!==initialPolicy||!sourceCurrent());return;
}
// Data-pipe/blob bodies still lack an immutable bounded native snapshot.
if(uploads.some(part=>!part.bytes || part.file || part.blobUUID)){audit('dlp.block','uninspectable-upload',new URL(details.url).hostname);finish(true);return;}
if(!['GET','HEAD','OPTIONS'].includes(details.method)&&!uploads.length){audit('dlp.block','missing-request-body',new URL(details.url).hostname);finish(true);return;}
if(uploads.length){
if(uploads.reduce((n,p)=>n+p.bytes.length,0)>policy.dlp.maxBytes){audit('dlp.block','size-limit',new URL(details.url).hostname);finish(true);return;}
const requestBytes=Buffer.concat(uploads.map(p=>Buffer.from(p.bytes)));
const transfer=require('../../packages/core/file-transfer.cjs').inspectFileTransfer(requestBytes,policy,'upload');
const fileFacts={bytes:requestBytes.length,detectedType:transfer.detectedType,sha256:transfer.sha256};
const releaseUpload=async()=>{await audit('upload.authorized',null,new URL(details.url).hostname,fileFacts);finish(!ready||policy!==initialPolicy||!sourceCurrent());};
if(transfer.decision!=='allow'){await audit('upload.block',transfer.ruleId,new URL(details.url).hostname,fileFacts);requestBytes.fill(0);finish(true);return;}
// An arbitrary request may contain credentials even when its magic bytes look binary.
// Keep size/type for analysts; never fingerprint request bodies in ordinary audit.
const requestFacts=require('../../packages/core/request-transfer-facts.cjs').requestFactsForTransfer(transfer);
const releaseUpload=async()=>{if(called)return;await audit('upload.authorized',null,new URL(details.url).hostname,undefined,requestFacts);finish(!ready||policy!==initialPolicy||!sourceCurrent());};
if(transfer.decision!=='allow'){await audit('upload.block',transfer.ruleId,new URL(details.url).hostname,undefined,requestFacts);requestBytes.fill(0);finish(true);return;}
const verdict=core.inspect(requestBytes,policy);requestBytes.fill(0);
if(verdict.decision==='block'&&verdict.ruleId!=='uninspectable'){audit('dlp.block',verdict.ruleId,new URL(details.url).hostname);finish(true);return;}
if(verdict.ruleId==='uninspectable'||policy.dlp.semantic.enabled||policy.security.yara.enabled||icapRequired(policy,'upload')){
@@ -387,7 +520,7 @@ function hooks(){
if(needsLocal&&!localInspector){audit('dlp.block','inspector-unavailable',new URL(details.url).hostname);finish(true);return;}
const snapshot=Buffer.concat(uploads.map(p=>Buffer.from(p.bytes))),currentPolicy=policy;
try{let result=needsLocal?await diagnosticSpan('browser.inspect.local',{bytes:snapshot.length},()=>localInspector.inspect(snapshot,currentPolicy)):{decision:'allow'};if(result.decision==='allow')result=await diagnosticSpan('browser.inspect.enterprise',{bytes:snapshot.length},()=>scanEnterprise(snapshot,currentPolicy,'upload'));
const cancel=!ready||policy.version!==currentPolicy.version||!sourceCurrent()||result.decision!=='allow';
const cancel=called||!ready||policy.version!==currentPolicy.version||!sourceCurrent()||result.decision!=='allow';
if(cancel){audit('dlp.block',result.ruleId||'policy-changed',new URL(details.url).hostname);finish(true);}else await releaseUpload();
}catch{audit('dlp.block','inspection-incomplete',new URL(details.url).hostname);finish(true);}finally{snapshot.fill(0);}return;
}else{await releaseUpload();return;}
@@ -395,14 +528,38 @@ function hooks(){
finish(false);
}catch{finish(true);}finally{clearTimeout(timer);}}).catch(()=>finish(true));
});
// Session request and permission gates are installed exactly once.
browserSession.webRequest.onBeforeSendHeaders({urls:['<all_urls>']},(details,callback)=>{
if(developerAssetRequestAllowed(details,{ready,policy,tabs:tabs.all()})){callback({cancel:false});return;}
let called=false;const finish=(cancel,requestHeaders)=>{if(called)return;called=true;callback(cancel?{cancel:true}:{cancel:false,requestHeaders});};
const initialPolicy=policy,source=details.webContentsId>0?tabs.fromContents(details.webContentsId):null,sourceContext=source?tabContext(source.id,{foreground:false}):null;
const sourceCurrent=()=>require('./request-source.cjs').requestSourceCurrent({details,source,token:sourceContext,registry:tabs,currentDocument:currentTabContext});
const current=()=>!called&&ready&&policy===initialPolicy&&Date.parse(initialPolicy?.expiresAt)>Date.now()&&sourceCurrent();
let timer;
diagnosticSpan('browser.request',{requestType:details.resourceType,...(source?{sessionId:source.id}:{})},async()=>{
timer=setTimeout(()=>{if(called)return;finish(true);audit('dlp.block','headers-inspection-timeout',new URL(details.url).hostname).catch(()=>{});},40000);
try{
if(!current()||!agentRequestOwnerAllowed(source)||(details.webContentsId>0&&(!source||!source.guarded))||!core.destinationAllowed(details.url,initialPolicy)||(source?.security.agentAuthority&&!actorDestinationAllowed(details.url,source.security.agentAuthority))){if(ready&&policy===initialPolicy&&(!agentRequestOwnerAllowed(source)||details.webContentsId>0&&(!source||!source.guarded)))await audit('navigation.block','invalid-source-owner',new URL(details.url).hostname);finish(true);return;}
const {snapshotHeaders,inspectHeaders}=require('./header-inspection.cjs');
let snapshot;try{snapshot=snapshotHeaders(details.requestHeaders,initialPolicy);}catch(error){await audit('dlp.block',error.message==='headers-size-limit'?'headers-size-limit':'headers-uninspectable',new URL(details.url).hostname);finish(true);return;}
const verdict=await diagnosticSpan('browser.request.headers',{bytes:snapshot.bytes,count:snapshot.count,requestType:details.resourceType},()=>inspectHeaders(snapshot,{policy:initialPolicy,inspect:core.inspect,isCurrent:current,
inspectLocal:localInspector?(bytes,p)=>diagnosticSpan('browser.inspect.local',{bytes:bytes.length},()=>localInspector.inspect(bytes,p)):null,
inspectEnterprise:(bytes,p)=>diagnosticSpan('browser.inspect.enterprise',{bytes:bytes.length},()=>scanEnterprise(bytes,p,'upload'))}));
if(!current()){if(ready&&policy===initialPolicy&&!sourceCurrent())await audit('navigation.block','header-source-context-changed',new URL(details.url).hostname);finish(true);return;}
if(verdict.decision!=='allow'){await audit('dlp.block',verdict.ruleId||'headers-inspection-incomplete',new URL(details.url).hostname);finish(true);return;}
// Chromium receives the exact immutable snapshot that was approved.
finish(false,snapshot.headers);
}catch{if(current())await audit('dlp.block','headers-inspection-incomplete',new URL(details.url).hostname);finish(true);}
finally{clearTimeout(timer);}
}).catch(()=>finish(true));
});
// Session request, header and permission gates are installed exactly once.
}
function installSiteHooks(tab){
const contents=tab.view.webContents,security=tab.security;
const developerTools=createDeveloperToolsGuard({contents,getContext:()=>({ready,foreground:tab===activeTab()&&!tab.startPage&&!overviewPanel&&!toolsOpen,policy,url:contents.getURL(),token:tabContext(tab.id)}),isCurrent:context=>currentTabContext(context.token),audit:kind=>audit(kind),createFrontend:()=>{const frontend=new BrowserWindow({show:false,width:1000,height:750,title:'S Browser · Privileged developer tools',webPreferences:{session:browserSession,nodeIntegration:false,contextIsolation:true,sandbox:true,devTools:false}});security.developerFrontend=frontend.webContents;frontend.webContents.setWindowOpenHandler(()=>({action:'deny'}));return frontend;}});
security.developerToolsGuard=developerTools;
contents.setWindowOpenHandler(({url})=>{createNativeTab({url}).catch(e=>{message=e.message;emit();});return {action:'deny'};});
contents.on('will-navigate',(event,url)=>{if(!ready||!core.destinationAllowed(url,policy))event.preventDefault();});
contents.setWindowOpenHandler(({url})=>{createNativeTab({url,agentAuthority:tab.security.agentAuthority}).catch(e=>{message=e.message;emit();});return {action:'deny'};});
contents.on('will-navigate',(event,url)=>{if(!ready||!core.destinationAllowed(url,policy)||(tab.security.agentAuthority&&!actorDestinationAllowed(url,tab.security.agentAuthority)))event.preventDefault();});
contents.on('will-attach-webview',event=>event.preventDefault());
contents.on('before-input-event',(event,input)=>{if(developerTools.handleInput(event,input))return;const approval=security.visualApproval;if(policy?.security.visualPhishing.enabled&&(!approval||approval.generation!==tab.navigationId||approval.version!==policy.version||approval.url!==contents.getURL())){event.preventDefault();checkVisual(tab).catch(()=>{});}});
contents.on('did-start-navigation',(_event,_url,_inPlace,isMainFrame)=>{if(isMainFrame){tabs.navigation(tab.id);if(_url!=='about:blank')tab.startPage=false;}});
@@ -420,29 +577,31 @@ function createNativeTab(input={}){return diagnosticSpan('browser.tab',{operatio
async function createNativeTabImpl(input={}){
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||''});installSiteHooks(tab);
const tab=tabs.create({group:input.group||''});tab.security.agentAuthority=input.agentAuthority||agentActorStorage.getStore()?.principalId&&{...agentActorStorage.getStore()};installSiteHooks(tab);
try{
await tab.view.webContents.loadURL('about:blank');
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)await navigate({url:input.url,tabId:tab.id});emit();return state();
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();
}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(!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();}
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();}
function agentAppControls(){
const checked=(operation)=>async(args,{signal,policyVersion}={})=>{
if(signal?.aborted||!ready||policy?.version!==policyVersion||!featureAllowed(managedSettings(),'agents'))throw Error('Managed application changed');
return operation(args);
return operation(args,{signal});
};
return {
'application:state':checked(()=>({ready,policyVersion:policy.version,activeTabId:activeTab()?.id,tabCount:tabs.all().length})),
'settings:get':checked(()=>managedSettings()),
'settings:update':checked(args=>updateManagedAppearance(args.appearance)),
'bookmarks:list':checked(()=>({...bookmarkState({policy:effectiveBookmarkPolicy(),ready,settings:managedSettings()}),policyVersion:policy.version})),
'bookmarks:list':checked(()=>{const result=bookmarkState({policy:effectiveBookmarkPolicy(),ready,settings:managedSettings()});if(agentActorStorage.getStore()?.principalId){result.managed=result.managed.filter(row=>actorDestinationAllowed(row.url));result.personal.items=result.personal.items.filter(row=>actorDestinationAllowed(row.url));}return {...result,policyVersion:policy.version};}),
'bookmarks:mutate':checked(async args=>{await mutateManagedBookmarks(args);return {completed:true};}),
'library:organize-plan':checked((_args,options)=>organizeLibrary(options)),
'library:organize-apply':checked((args,options)=>applyLibraryOrganization(args,options)),
'tabs:create':checked(async args=>{const result=await createNativeTab(args);return {completed:true,sessionId:result.activeTabId};}),
'tabs:activate':checked(args=>{selectTab(args.sessionId);emit();return {completed:true,sessionId:args.sessionId};}),
'tabs:close':checked(async args=>{await closeNativeTab(args.sessionId);return {completed:true};}),
@@ -461,7 +620,7 @@ app.whenReady().then(async()=>{
ipcMain.on('managed:clipboard-sandbox-failure',event=>{if(tabs?.fromContents(event.sender.id)||event.sender===win?.webContents){clipboardFatal=true;quarantine('Managed renderer security failed. Restart this browser.');}});
powerMonitor.on('lock-screen',lockVault);powerMonitor.on('suspend',lockVault);
securityCore=await import('../../packages/core/security.mjs');
localInspector=startInspector(app.isPackaged?process.resourcesPath:path.join(__dirname,'../..'),app.isPackaged);
localInspector=startInspector(app.isPackaged?process.resourcesPath:path.join(__dirname,'../..'),app.isPackaged,{onStages:stages=>diagnostics.importStages(stages)});
browserSession=session.fromPartition('persist:safetynet-managed',{cache:false});
// Management has its own cookie jar and no website request gate. It uses the
// same company proxy and Chromium DNS configuration as managed website traffic.
@@ -496,6 +655,8 @@ app.whenReady().then(async()=>{
ipc('managed:window-control',action=>{if(action==='minimize')win.minimize();else if(action==='maximize'){win.isMaximized()?win.unmaximize():win.maximize();}else if(action==='close')win.close();else throw Error('Unknown window action');});
ipc('managed:authorize-setting',section=>{requireManagedSection(section);return {authorized:true,policyVersion:policy.version};});
ipc('managed:bookmarks-mutate',mutateManagedBookmarks);
ipc('managed:library-organize',organizeLibrary);
ipc('managed:library-apply',applyLibraryOrganization);
ipc('managed:settings-update',updateManagedAppearance);
ipc('managed:chrome-layout',async input=>{
if(!input||![64,104].includes(input.height)||['sidebarOpen','startPageOpen','overviewOpen'].some(key=>typeof input[key]!=='boolean'))throw Error('Unsupported browser layout');
@@ -573,9 +734,9 @@ app.whenReady().then(async()=>{
finally{if(credential){credential.username='';credential.password='';}}
});
ipc('managed:ai',managedAI);
ipc('managed:agent-connection',()=>{
ipc('managed:agent-connection',async principalId=>{
requireFeatureSection('agents');
if(!ready||!policy.agents.enabled||!agentService)throw Error('Agent access is disabled by company policy');return {address:agentService.address,credentialPath:agentFile()};
if(!ready||!policy.agents.enabled||!agentService)throw Error('Agent access is disabled by company policy');if(policy.agents.identityMode==='individual')return pairIndividualAgent(principalId);if(principalId!==undefined)throw Error('Individual agent selection requires individual mode');return {address:agentService.address,credentialPath:agentFile()};
});
ipcMain.on('managed:page-content',async(event,input)=>{
const tab=tabs.fromContents(event.sender.id);if(!tab||event.senderFrame!==event.sender.mainFrame||!ready||typeof input?.text!=='string'||input.text.length>250000)return;
@@ -607,7 +768,7 @@ app.whenReady().then(async()=>{
win.on('resize',layout);win.on('maximize',emit);win.on('unmaximize',emit);win.on('blur',lockVault);win.on('minimize',lockVault);win.on('closed',()=>{vault?.lock();clearInterval(vaultTimer);win=null;clearInterval(syncTimer);localInspector?.stop();agentService?.close();});
loadAppearance();
await win.loadFile(path.join(__dirname,'index.html'));layout();
try{device=loadDevice();const queuePath=path.join(app.getPath('userData'),'audit-queue.bin');if(device&&fs.existsSync(queuePath))auditQueue=JSON.parse(safeStorage.decryptString(fs.readFileSync(queuePath)));if(device){await sync();await audit('browser.started');}}catch(e){quarantine(`Enrollment could not load: ${e.message}`);}
try{device=loadDevice();const queuePath=path.join(app.getPath('userData'),'audit-queue.bin');if(device&&fs.existsSync(queuePath)){auditQueue=require('../../packages/core/request-transfer-facts.cjs').migrateQueuedRequestFacts(JSON.parse(safeStorage.decryptString(fs.readFileSync(queuePath))));persistAudit();}if(device){await sync();await audit('browser.started');}}catch(e){quarantine(`Enrollment could not load: ${e.message}`);}
const refreshMs=!app.isPackaged&&process.env.SAFETYNET_SMOKE_USERDATA===app.getPath('userData')?1000:15000;
syncTimer=setInterval(()=>{if(policy&&Date.parse(policy.expiresAt)<=Date.now())quarantine('Company policy expired');sync();},refreshMs);
networkTimer=setInterval(async()=>{
@@ -619,7 +780,7 @@ app.whenReady().then(async()=>{
let wasLocked=true;vaultTimer=setInterval(()=>{const locked=vault?.status().locked??true;if(locked!==wasLocked){if(locked)audit('vault.locked');wasLocked=locked;emit();}},1000);
}).catch(error=>{console.error(error.stack);app.quit();});
app.on('window-all-closed',()=>app.quit());
app.on('before-quit',()=>{tabs?.close();});
app.on('before-quit',()=>{fileUploadGate?.dispose();tabs?.close();});
app.on('will-quit',()=>{Promise.resolve(diagnostics.shutdown()).catch(()=>{});vault?.lock();clearInterval(vaultTimer);clearInterval(networkTimer);localInspector?.stop();agentService?.close();});
app.on('certificate-error',(event,_wc,_url,_error,_certificate,callback)=>{event.preventDefault();callback(false);});
app.on('login',(event,_wc,_details,authInfo,callback)=>{