// Trusted host dispatch for declared tools. No plugin code is imported until the // current organization setting and the complete installed package pass verification. import { spawn } from 'node:child_process'; import { existsSync, realpathSync } from 'node:fs'; import { join } from 'node:path'; import { pathToFileURL, fileURLToPath } from 'node:url'; import { organizationPlugins, assertPluginCapabilities, pluginName } from './organization.mjs'; import { verifiedPlugin, claimPluginImport } from './pinned.mjs'; import { bad, validArguments } from './manifest.mjs'; const SCRIPT = `let input=''; for await(const b of process.stdin)input+=b; const {guardPluginImports}=await import('file:///plugin-guard.mjs');guardPluginImports('/plugin'); const {entry,args}=JSON.parse(input); const m=await import(entry); process.stdout.write(JSON.stringify(await m.execute(args)));`; function sandboxTool(dir,entry,args) { // A fresh mount/network/user namespace with only runtime libraries and the pinned package. // No home, config, workspace, sockets or inherited environment is exposed. const binds=['/usr','/lib','/lib64','/bin'].filter(existsSync).flatMap(p=>['--ro-bind',realpathSync(p),p]); return new Promise((resolve,reject)=>{ const child=spawn('bwrap',['--die-with-parent','--unshare-all','--new-session','--proc','/proc','--dev','/dev','--tmpfs','/tmp',...binds,'--ro-bind',realpathSync(process.execPath),'/node','--ro-bind',fileURLToPath(new URL('./imports.mjs',import.meta.url)),'/plugin-guard.mjs','--ro-bind',dir,'/plugin','--chdir','/plugin','--clearenv','/node','--input-type=module','-e',SCRIPT],{env:{PATH:'/usr/bin:/bin'},stdio:['pipe','pipe','pipe']}); let out='',size=0;const timer=setTimeout(()=>child.kill('SIGKILL'),30000); child.stdout.on('data',b=>{size+=b.length;if(size>1024*1024)child.kill('SIGKILL');else out+=b;}); child.stderr.resume();child.on('error',()=>{clearTimeout(timer);reject(new Error('sandbox refused'));}); child.stdin.on('error',()=>{});child.stdin.end(JSON.stringify({entry:`file:///plugin/${entry}`,args})); child.on('close',code=>{clearTimeout(timer);try{if(code!==0 || size>1024*1024)throw Error();resolve(JSON.parse(out));}catch{reject(new Error('sandbox refused'));}}); }); } export function pluginService({dataRoot,read}) { function check(business,instance) { const b=read(business);const entry=b.roles?.[instance];let role=b.definitions?.[entry?.definition] ?? entry; if(!role)bad('plugin-disabled'); role=structuredClone(role); for(const vars of [b.vars,entry?.vars]) { if(Array.isArray(vars?.['limits.tools']))role.tools=(role.tools ?? []).filter(t=>vars['limits.tools'].includes(t)); if(Array.isArray(vars?.['limits.authority']) && role.authority)for(const key of ['withinRole','crossRole'])role.authority[key]=(role.authority[key] ?? []).filter(a=>vars['limits.authority'].includes(a)); } const view=organizationPlugins(dataRoot,b.plugins);assertPluginCapabilities(role,view);return {role,view}; } async function call(business,instance,name,args) { const prefix=pluginName(name);if(!prefix)bad('invalid-request'); const {role,view}=check(business,instance); if(!role.tools?.includes(name))bad('not-authorized'); const tool=view.manifests.flatMap(m=>m.tools).find(t=>t.name===name);if(!tool)bad('plugin-disabled',prefix); if(!validArguments(args,tool.arguments))bad('invalid-request'); const checked=verifiedPlugin(dataRoot,prefix),{dir,manifest}=checked; if(tool.runs==='sandbox') { try {return await sandboxTool(dir,tool.entry,structuredClone(args));} catch {bad('plugin-tool-failed',prefix);} } claimPluginImport(checked); let module; try {module=await import(pathToFileURL(join(dir,tool.entry)).href+`?pin=${manifest.sha256}`);} catch {bad('plugin-tool-failed',prefix);} // A concurrent disable while import was in flight must refuse before execute. check(business,instance);verifiedPlugin(dataRoot,prefix); if(typeof module.execute!=='function')bad('missing-export'); // Plugin code cannot impersonate a trusted refusal by throwing a matching error name. try {return await module.execute(structuredClone(args));} catch {bad('plugin-tool-failed',prefix);} } return {check,call}; }