const {spawn}=require('node:child_process'); const fs=require('node:fs'),path=require('node:path'),crypto=require('node:crypto'); function privateInspectorPaths(root,packaged=false,environment=process.env){ const python=path.resolve(root,packaged?'python/python.exe':'.runtime/windows/python/python.exe'); const script=path.resolve(root,packaged?'inspector/server.py':'apps/inspector/server.py'); try{ for(const [file,override]of [[python,environment.SAFETYNET_PYTHON],[script,environment.SAFETYNET_INSPECTOR_SCRIPT]]){ if(override&&path.resolve(override).toLowerCase()!==file.toLowerCase())return null; if(!fs.statSync(file).isFile())return null; // Do not resolve away a junction before checking ownership boundaries. for(let item=file;;item=path.dirname(item)){ if(fs.lstatSync(item).isSymbolicLink())return null; if(path.dirname(item)===item)break; } } return {python,script}; }catch{return null;} } function startInspector(root,packaged=false,options={}){ const files=privateInspectorPaths(root,packaged);if(!files)return null; const {python,script}=files; const credential=crypto.randomBytes(32).toString('base64url'); const semanticRuntime=path.join(root,packaged?'semantic':'.runtime/windows/semantic'); // Native model loading remains wholly local; the Python adapter verifies pinned artifacts. // Explicit deployment paths take precedence, and missing artifacts fail semantic inspection closed. const environment={...process.env,INSPECTOR_TOKEN:credential,INSPECTOR_PORT:'0',INSPECTOR_PARENT_PID:String(process.pid)}; if(!environment.EMBEDDING_MODEL_DIR)environment.EMBEDDING_MODEL_DIR=path.join(semanticRuntime,'models'); if(!environment.EMBEDDING_WORKER_PATH)environment.EMBEDDING_WORKER_PATH=path.join(semanticRuntime,'searchi-gemma-worker.exe'); for(const key of ['ADMIN_TOKEN','OIDC_CLIENT_SECRET','AI_API_KEY','SIEM_HTTP_TOKEN','SIEM_KEY_FILE','SAFETYNET_PROXY_PASSWORD'])delete environment[key]; const child=spawn(python,[script],{windowsHide:true,stdio:['ignore','pipe','pipe'],env:environment}); let port,output='';const startup=new Promise((resolve,reject)=>{ const timer=setTimeout(()=>{child.kill();reject(Error('Local inspector startup timed out'));},30000); child.on('error',()=>{clearTimeout(timer);reject(Error('Local inspector failed to start'));}); child.once('exit',()=>{clearTimeout(timer);reject(Error('Local inspector stopped'));}); child.stdout.on('data',chunk=>{output+=chunk.toString();if(output.length>4096){child.kill();return;} if(output.includes('\n')){try{const value=JSON.parse(output.split('\n')[0]);if(!Number.isInteger(value.port)||value.port<1||value.port>65535)throw Error();if(process.platform==='win32'&&(value.containment?.active!==true||value.containment?.kind!=='windows-job-v1'||value.containment?.killsDescendants!==true||value.containment?.parserIsolation?.active!==true||value.containment?.parserIsolation?.kind!=='windows-appcontainer-v1'||value.containment?.parserIsolation?.networkCapabilities!==0||value.containment?.parserIsolation?.privatePipes!==true||value.containment?.parserIsolation?.readOnlyRuntime!==true))throw Error();port=value.port;clearTimeout(timer);resolve();}catch{child.kill();reject(Error('Local inspector startup rejected'));}}}); child.stderr.on('data',()=>{}); });startup.catch(()=>{}); const waitReady=async()=>{await startup;if(child.exitCode!==null||child.signalCode!==null)throw Error('Local content inspector stopped; restart the browser');}; const invoke=async(route,bytes,policy)=>{ await waitReady(); const controller=new AbortController(),timer=setTimeout(()=>controller.abort(),30000); try{ const response=await require('./loopback-http.cjs').postLoopback({port,route,token:credential,body:{data:Buffer.from(bytes).toString('base64'),policy},signal:controller.signal}); if(response.status!==200)throw Error('Local content inspection failed');const result=response.body; if(!['allow','block'].includes(result.decision)||typeof result.coverage?.complete!=='boolean')throw Error('Inspector response rejected'); if(result.decision==='allow'&&!result.coverage.complete)throw Error('Incomplete content cannot be released'); const stages=result._diagnosticStages;delete result._diagnosticStages;if(policy?.diagnostics?.enabled===true&&policy.diagnostics.mode==='debug'&&typeof options.onStages==='function'){try{options.onStages(stages);}catch{}}return result; // eslint-disable-next-line preserve-caught-error -- Expose only a fixed sanitized error; the original cause may contain sensitive paths or data. }catch(error){if(error.name==='AbortError'){child.kill();throw Error('Content inspection exceeded its time budget');}throw error;} finally{clearTimeout(timer);} }; return {stop:()=>child.kill(),waitReady,inspect:(bytes,policy)=>invoke('/inspect',bytes,policy),scanDownload:(bytes,policy)=>invoke('/scan-download',bytes,policy)}; } module.exports={startInspector,privateInspectorPaths};