import { execFileSync, spawnSync } from 'node:child_process'; import { readFileSync, writeFileSync, linkSync, unlinkSync, openSync, closeSync, fstatSync, realpathSync, statSync, constants } from 'node:fs'; import { join, isAbsolute } from 'node:path'; import { pathToFileURL } from 'node:url'; import { refuse, shortRoot, nonce, within } from './lane-state.mjs'; const HOST_PI_AUTH='/home/jwoltje/.pi/agent/auth.json'; function composeFiles(run){ if(!/^sha256:[a-f0-9]{64}$/.test(run.imageId??''))refuse('invalid pinned image ID'); if(typeof run.checkout!=='string'||!isAbsolute(run.checkout))refuse('invalid candidate checkout'); return [join(run.checkout,'compose.yaml'),join(shortRoot(run.tempRoot),'compose-no-build.yaml')]; } export function writeComposeOverride(run){ const [,file]=composeFiles(run); const text=`services:\n mosaic-agent:\n build: !reset null\n image: ${JSON.stringify(run.imageId)}\n`; // Publish a complete file without replacing another guard's override. const pending=file+'.'+nonce(); try{ writeFileSync(pending,text,{mode:0o600,flag:'wx'}); try{linkSync(pending,file);}catch(e){if(e.code!=='EEXIST')throw e;} }finally{try{unlinkSync(pending);}catch(e){if(e.code!=='ENOENT')throw e;}} let fd; try{ fd=openSync(file,constants.O_RDONLY|constants.O_NOFOLLOW|constants.O_NONBLOCK); const st=fstatSync(fd); if(!st.isFile()||st.uid!==process.getuid()||(st.mode&0o777)!==0o600||st.size!==Buffer.byteLength(text)||readFileSync(fd,'utf8')!==text) refuse('unsafe or changed Compose override'); }catch{refuse('unsafe or changed Compose override');} finally{if(fd!==undefined)closeSync(fd);} return file; } // compose.yaml mounts PI_AUTH_FILE, defaulting to the host's real pi auth file (row 244). function laneAuthFile(env,run){ const file=env.PI_AUTH_FILE;let real,root; try{real=realpathSync(file);root=realpathSync(run.tempRoot);}catch{refuse('Compose auth mount not a lane fixture');} if(!isAbsolute(file)||file===HOST_PI_AUTH||real===HOST_PI_AUTH||!within(root,real)||!statSync(real).isFile())refuse('Compose auth mount not a lane fixture'); } export function guardDocker(argv,run,env=process.env){ if(argv[0]==='info'&&argv.length===1)return argv; if(argv[0]==='image'&&argv[1]==='inspect'&&argv[2]===run.tag&&argv.length===3)return argv; if(argv[0]==='compose'&&argv[1]==='run'){ const service=argv.indexOf('mosaic-agent',2); if(service<0||argv.slice(2,service).some(a=>!['--rm','-T','--no-deps','--interactive','-i'].includes(a)))refuse('Docker Compose run options not allowed'); laneAuthFile(env,run); const [base,override]=composeFiles(run); return ['compose','-f',base,'-f',override,'run','--pull','never','--no-deps',...argv.slice(2)]; } refuse('Docker mutation or unowned command'); } export function cleanupProject(run,docker='docker'){ if(!/^mg197-[ab]-[a-f0-9]{12}$/.test(run.project))refuse('invalid Docker ownership project'); const invoke=typeof docker==='function'?docker:(args)=>execFileSync(docker,args,{encoding:'utf8',stdio:['ignore','pipe','pipe']}); const removed=[]; for(const [kind,list,filter] of [['container',['ps','-aq'],'com.docker.compose.project'],['network',['network','ls','-q'],'com.docker.compose.project']]){ const ids=invoke([...list,'--filter',`label=${filter}=${run.project}`]).trim().split(/\s+/).filter(Boolean); for(const id of ids){if(!/^[a-f0-9]{12,64}$/.test(id))refuse('invalid Docker resource ID'); const label=invoke([kind,'inspect',id,'--format',`{{index .${kind==='container'?'Config.':''}Labels "${filter}"}}`]).trim(); if(label!==run.project)refuse('foreign Docker resource'); invoke(kind==='container'?['container','rm','-f',id]:['network','rm',id]);removed.push({kind,id}); } const left=invoke([...list,'--filter',`label=${filter}=${run.project}`]).trim();if(left)refuse('Docker cleanup incomplete'); } return {clean:true,removed}; } if(process.argv[1]&&import.meta.url===pathToFileURL(process.argv[1]).href){ try{const run=JSON.parse(readFileSync(process.env.MOSAIC_GATE_DOCKER_RECORD,'utf8'));if(process.env.COMPOSE_PROJECT_NAME!==run.project)refuse('Compose project mismatch'); const argv=guardDocker(process.argv.slice(2),run,process.env);if(argv[0]==='compose')writeComposeOverride(run);const result=spawnSync(run.docker,argv,{stdio:'inherit'});process.exit(result.status??1); }catch(e){process.stderr.write(`${e.message}\n`);process.exit(2);} }