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

+20 -5
View File
@@ -31,7 +31,7 @@ if (!profile.startsWith(path.resolve(os.tmpdir()) + path.sep)) throw Error('Unsa
app.setPath('userData', profile);
const output = path.join(root, 'artifacts');
fs.mkdirSync(output, { recursive: true });
const methods = ['createTab','activateTab','closeTab','groupTab','mutateBookmarks','updateSettings','authorizeSetting','state', 'windowControls', 'setChromeLayout', 'shareURL', 'enroll', 'sso', 'navigate', 'back', 'forward', 'reload', 'ai', 'inspectFile', 'agentConnection', 'toggleAI', 'toggleVault', 'vaultStatus', 'vaultCreate', 'vaultUnlock', 'vaultLock', 'vaultList', 'vaultSave', 'vaultDelete', 'vaultFill', 'vaultGenerate'];
const methods = ['organizeLibrary','applyLibraryOrganization','createTab','activateTab','closeTab','groupTab','mutateBookmarks','updateSettings','authorizeSetting','state', 'windowControls', 'setChromeLayout', 'shareURL', 'enroll', 'sso', 'navigate', 'back', 'forward', 'reload', 'ai', 'inspectFile', 'agentConnection', 'toggleAI', 'toggleVault', 'vaultStatus', 'vaultCreate', 'vaultUnlock', 'vaultLock', 'vaultList', 'vaultSave', 'vaultDelete', 'vaultFill', 'vaultGenerate'];
const preload = path.join(profile, 'showcase-preload.cjs');
fs.writeFileSync(preload, `const {contextBridge,ipcRenderer}=require('electron'); const methods=${JSON.stringify(methods)}; const api={};for(const method of methods)api[method]=(...args)=>ipcRenderer.invoke('showcase:call',method,args);api.onState=callback=>{const listener=(_event,state)=>callback(state);ipcRenderer.on('showcase:state',listener);return()=>ipcRenderer.removeListener('showcase:state',listener);};contextBridge.exposeInMainWorld('safetynet',api);`);
const fixtures = [
@@ -68,6 +68,8 @@ ipcMain.handle('showcase:call', (event, method, args) => {
else if(method==='createTab'){const tab={id:`showcase-native-${++nextTab}`,url:'',title:'New Tab',group:args[0]?.group||'',startPage:true,color:'#b8c7dd',canGoBack:false,canGoForward:false};state.tabs.push(tab);state.activeTabId=tab.id;data=publish(event.sender);}
else if(method==='activateTab'){assert.ok(state.tabs.some(tab=>tab.id===args[0]));state.activeTabId=args[0];data=publish(event.sender);}
else if(method==='closeTab'){const index=state.tabs.findIndex(tab=>tab.id===args[0]);assert.ok(index>=0);state.tabs.splice(index,1);if(!state.tabs.length){state.tabs.push({id:`showcase-native-${++nextTab}`,url:'',title:'New Tab',group:'',startPage:true,color:'#b8c7dd',canGoBack:false,canGoForward:false});}if(state.activeTabId===args[0])state.activeTabId=state.tabs[Math.min(index,state.tabs.length-1)].id;data=publish(event.sender);}
else if(method==='organizeLibrary'){assert.ok(state.libraryAI?.suggestionsEnabled);data={proposalId:'fixture-review-id',plan:{bookmarks:[{id:'showcase-1',folder:'Engineering'}],tabs:[]}};}
else if(method==='applyLibraryOrganization'){assert.equal(args[0].proposalId,'fixture-review-id');assert.equal(args[0].confirm,true);state.bookmarks.personal.items.find(item=>item.id==='showcase-1').folder='Engineering';state.bookmarks.personal.revision++;data={completed:true};}
else if(method==='groupTab'){assert.ok(state.settings.visible&&!state.settings.hiddenSections.includes('library')&&!state.settings.lockedSections.includes('library'));const tab=state.tabs.find(tab=>tab.id===args[0].id);assert.ok(tab);tab.group=args[0].group;data=publish(event.sender);}
else if(method==='mutateBookmarks'){const input=args[0];if(!state.settings.visible||state.settings.hiddenSections.includes('library')||state.settings.lockedSections.includes('library')||input.expectedRevision!==state.bookmarks.personal.revision)return {ok:false,error:'Managed bookmark refused'};assert.equal(input.action,'add');state.bookmarks.personal={revision:state.bookmarks.personal.revision+1,items:[...state.bookmarks.personal.items,input.bookmark]};data=state;event.sender.send('showcase:state',state);}
else if(method==='updateSettings'){if(!state.settings.available||!state.settings.visible||state.settings.hiddenSections.includes('appearance')||state.settings.lockedSections.includes('appearance'))return{ok:false,error:'Managed setting refused'};state.settings={...state.settings,appearance:{...args[0]}};data=state.settings;}
@@ -113,7 +115,7 @@ async function accent(value) {
async function shot(name) {
console.log('Showcase capture: ' + name);
await execute('document.fonts.ready'); await delay(90);
const measurements = await execute(`(()=>{const rect=id=>{const element=document.getElementById(id),r=element.getBoundingClientRect();return{hidden:element.hidden,x:r.x,y:r.y,width:r.width,height:r.height}};const brokenImages=[...document.images].filter(image=>!image.complete||!image.naturalWidth).map(image=>image.getAttribute('src'));return{viewport:{width:innerWidth,height:innerHeight,zoom:devicePixelRatio},theme:document.documentElement.dataset.theme,effectiveTheme:document.documentElement.dataset.effectiveTheme,accent:document.documentElement.dataset.accent,tabMode:document.documentElement.dataset.tabMode,chrome:rect('chrome'),toolbar:document.querySelector('.toolbar').getBoundingClientRect().height,library:rect('library'),ai:rect('ai'),vault:rect('vault'),startPage:rect('start-page'),overview:rect('overview'),tools:rect('tools-menu'),scrollWidth:document.documentElement.scrollWidth,brokenImages,error:document.getElementById('error').textContent,passwordFieldsEmpty:[...document.querySelectorAll('input[type=password]')].every(input=>!input.value)}})()`);
const measurements = await execute(`(()=>{const rect=id=>{const element=document.getElementById(id),r=element.getBoundingClientRect();return{hidden:element.hidden,x:r.x,y:r.y,width:r.width,height:r.height}};const brokenImages=[...document.images].filter(image=>image.hasAttribute('src')&&(!image.complete||!image.naturalWidth)).map(image=>image.getAttribute('src'));return{viewport:{width:innerWidth,height:innerHeight,zoom:devicePixelRatio},theme:document.documentElement.dataset.theme,effectiveTheme:document.documentElement.dataset.effectiveTheme,accent:document.documentElement.dataset.accent,tabMode:document.documentElement.dataset.tabMode,chrome:rect('chrome'),toolbar:document.querySelector('.toolbar').getBoundingClientRect().height,library:rect('library'),ai:rect('ai'),vault:rect('vault'),startPage:rect('start-page'),overview:rect('overview'),tools:rect('tools-menu'),scrollWidth:document.documentElement.scrollWidth,brokenImages,error:document.getElementById('error').textContent,passwordFieldsEmpty:[...document.querySelectorAll('input[type=password]')].every(input=>!input.value)}})()`);
assert.equal(measurements.toolbar, 64); assert.ok([64, 104].includes(measurements.chrome.height));
assert.equal(measurements.scrollWidth, measurements.viewport.width); assert.deepEqual(measurements.brokenImages, []); assert.equal(measurements.passwordFieldsEmpty, true); assert.equal(measurements.error, '');
if (!measurements.library.hidden) assert.equal(measurements.library.width, 240);
@@ -153,9 +155,22 @@ app.whenReady().then(async () => {
await waitFor(`document.getElementById('bookmark-editor').open`,false);
assert.equal(state.bookmarks.managed.length+state.bookmarks.personal.items.length,7);
assert.equal(await execute(`JSON.parse(localStorage.getItem('sbrowser.bookmarks')).length`), 6,'User bookmarks must not be mutated through renderer storage');
await click('new-tab'); await click('sidebar-toggle');
await click('new-tab'); assert.equal(await execute(`document.getElementById('library').hidden`),true,'Sidebar starts collapsed');
assert.equal(await execute(`document.querySelectorAll('#favorites-grid .favorite-card').length`), 7);
await click('sidebar-toggle');await execute(`document.getElementById('group-name').value='Work'`);await click('group-add');assert.equal(state.tabs.find(t=>t.id===state.activeTabId).group,'Work');await execute(`document.getElementById('group-name').value='Research';document.querySelector('[aria-label="Rename group Work"]').click()`);await waitFor(`document.querySelector('[aria-label="Ungroup tabs in Research"]')!==null`,true);assert.equal(state.tabs.find(t=>t.id===state.activeTabId).group,'Research');await execute(`document.querySelector('[aria-label="Ungroup tabs in Research"]').click()`);await waitFor(`document.querySelector('[aria-label="Ungroup tabs in Research"]')===null`,true);assert.equal(state.tabs.find(t=>t.id===state.activeTabId).group,'');await click('sidebar-toggle');
const aligned=await execute(`(()=>{const cards=[...document.querySelectorAll('#favorites-grid .favorite-card')];const first=cards[0].getBoundingClientRect().top;return cards.filter(c=>c.getBoundingClientRect().top===first).map(c=>c.querySelector('.favorite-icon').getBoundingClientRect().top)})()`);assert.ok(aligned.length>1);assert.ok(aligned.every(top=>Math.abs(top-aligned[0])<1),'Managed and personal favorites share icon baseline');
await click('favorites-manage');assert.equal(await execute(`document.getElementById('bookmark-manager').open`),true);assert.equal(await execute(`document.querySelectorAll('#manager-list .manager-row').length`),7);assert.equal(await execute(`document.querySelector('#manager-list [data-bookmark-id="showcase-company"] .manager-row-actions')===null`),true);assert.equal(await execute(`document.getElementById('manager-ai').disabled`),true);await execute(`document.getElementById('bookmark-search').value='github';document.getElementById('bookmark-search').dispatchEvent(new Event('input'))`);assert.equal(await execute(`document.querySelectorAll('#manager-list .manager-row').length`),1);await shot('sbrowser-bookmark-manager');state.libraryAI={suggestionsEnabled:true,autoApplyEnabled:false,busy:false};window.webContents.send('showcase:state',snapshot());await delay(100);await click('manager-ai');assert.equal(await execute(`document.getElementById('manager-proposal').hidden`),false);assert.match(await execute(`document.getElementById('manager-diff').textContent`),/GitHub.*Engineering/);assert.equal(state.bookmarks.personal.items.find(item=>item.id==='showcase-1').folder,'Work','Preview has no side effects');await click('manager-apply');assert.equal(state.bookmarks.personal.items.find(item=>item.id==='showcase-1').folder,'Engineering');await click('bookmark-manager-close');
await shot('sbrowser-light');
state.brand={name:'Northstar Company',color:'#166b68',logoDataUrl:'data:image/png;base64,'+fs.readFileSync(path.join(root,'docs/brand/safetynet-icon.png')).toString('base64')};
state.startPage={title:'Welcome to Northstar.',subtitle:'Your company workspace, news and favorite tools.',newsEnabled:true,news:[{id:'news-1',title:'Company update: our new intranet is here',url:'https://intranet.example.test/news',publishedAt:'2026-10-13T12:00:00Z'},{id:'news-2',title:'Security week: join the local AI workshop',url:'https://intranet.example.test/events'}]};
window.webContents.send('showcase:state',snapshot());await delay(150);
assert.equal(await execute(`document.getElementById('start-brand-name').textContent`),'Northstar Company');
assert.equal(await execute(`document.querySelectorAll('#company-news-items .company-news-card').length`),2);
assert.equal(await execute(`document.getElementById('start-company-logo').hidden`),false);
await shot('sbrowser-company-branding');
state.startPage={newsEnabled:false};state.brand={name:'SafetyNet'};window.webContents.send('showcase:state',snapshot());await delay(100);
assert.equal(await execute(`document.getElementById('company-news').hidden`),true);assert.equal(await execute(`document.getElementById('start-company-logo').hasAttribute('src')`),false);
await appearance('dark', 'compact'); await shot('sbrowser-dark');
await click('tab-overview'); assert.equal(await execute(`document.querySelectorAll('#overview-grid .overview-card').length`), 2); await shot('sbrowser-overview');
await click('overview-close'); await appearance('light', 'separate'); await click('tools-toggle'); await shot('sbrowser-settings');
@@ -163,7 +178,7 @@ app.whenReady().then(async () => {
await appearance('light', 'separate'); await accent('blue');
await click('passwords'); await shot('sbrowser-vault'); await click('vault-close');
await appearance('light', 'compact'); window.setContentSize(800, 650); await delay(150); await shot('sbrowser-compact');
await click('tools-toggle'); await click('ai-toggle'); await shot('sbrowser-compact-panels'); await click('ai-close');
await click('tools-toggle'); await click('ai-toggle'); await shot('sbrowser-compact-panels'); await click('ai-close');window.setContentSize(480,740);await delay(120);await shot('sbrowser-narrow');window.setContentSize(800,650);
await execute(`document.getElementById('address').value='https://example.test/slow';document.getElementById('address-form').requestSubmit();`);
await click('new-tab'); await delay(300);
assert.equal(await execute(`document.querySelector('.browser-tab.active .tab-label').textContent`), 'New Tab');
@@ -193,7 +208,7 @@ app.whenReady().then(async () => {
assert.equal(records.filter(record => record.method === 'windowControls').length, 3);
fs.writeFileSync(path.join(output, 'sbrowser-ui-layout.json'), JSON.stringify({ fixtureOnly: true, managedSettingsProof:{liveLocks:true,hiddenSections:true,globalHidden:true,localStorageCannotOverride:true,reloadPreservesPolicy:true,directUpdateRejected:true,globalHiddenPreservesApprovedFeatures:true},screenshots, records, consoleErrors }, null, 2));
assert.deepEqual(consoleErrors, []);
finished = true; clearTimeout(watchdog); console.log('UI showcase passed: eight screenshots, live system themes, persisted accents, real DOM interactions and delayed-navigation regression.');
finished = true; clearTimeout(watchdog); console.log('UI showcase passed: ten screenshots, live system themes, persisted accents, real DOM interactions and delayed-navigation regression.');
if(window.webContents.debugger.isAttached())window.webContents.debugger.detach();
window.destroy(); app.exit(0);
}).catch(error => { clearTimeout(watchdog); console.error(error.message); if (!finished) app.exit(1); });