@@ -578,6 +578,138 @@ test("base.dockerfile guards root host uid with useradd -o", () => {
578578 assert . match ( content , / u s e r a d d - o - u \$ \{ H O S T _ U I D \} / ) ;
579579} ) ;
580580
581+ test ( "base.dockerfile handles the preinstalled ubuntu UID conflict" , onPlatforms ( "linux" , "darwin" ) , async ( t ) => {
582+ const content = fs . readFileSync ( filePath ( "lib/sandbox/runtimes/base.dockerfile" ) , "utf8" ) ;
583+ const runBlock = content
584+ . split ( / ^ (? = F R O M | U S E R | E N V | A R G | R U N | W O R K D I R | C M D | C O P Y | A D D ) / m)
585+ . find ( ( block ) => block . startsWith ( "RUN " ) && block . includes ( "useradd" ) ) ;
586+ assert . ok ( runBlock , "expected a RUN block creating devuser" ) ;
587+
588+ const shellBody = runBlock . replace ( / ^ R U N \s + / , "" ) . replace ( / \\ \n \s * / g, " " ) . trim ( ) ;
589+ const stubDir = fs . mkdtempSync ( path . join ( os . tmpdir ( ) , "agent-infra-sandbox-user-stubs-" ) ) ;
590+ const logFile = path . join ( stubDir , "invocations.log" ) ;
591+ const stubs = {
592+ id : [
593+ "#!/bin/sh" ,
594+ "printf 'id %s\\n' \"$*\" >> \"$INVOCATIONS_LOG\"" ,
595+ "[ \"$*\" = \"-u ubuntu\" ] && [ -n \"$STUB_UBUNTU_UID\" ] || exit 1" ,
596+ "printf '%s\\n' \"$STUB_UBUNTU_UID\""
597+ ] ,
598+ userdel : [
599+ "#!/bin/sh" ,
600+ "printf 'userdel %s\\n' \"$*\" >> \"$INVOCATIONS_LOG\"" ,
601+ 'exit "${STUB_USERDEL_STATUS:-0}"'
602+ ] ,
603+ groupadd : [
604+ "#!/bin/sh" ,
605+ "printf 'groupadd %s\\n' \"$*\" >> \"$INVOCATIONS_LOG\""
606+ ] ,
607+ useradd : [
608+ "#!/bin/sh" ,
609+ "printf 'useradd %s\\n' \"$*\" >> \"$INVOCATIONS_LOG\""
610+ ]
611+ } ;
612+
613+ try {
614+ for ( const [ name , lines ] of Object . entries ( stubs ) ) {
615+ fs . writeFileSync ( path . join ( stubDir , name ) , `${ lines . join ( "\n" ) } \n` , { mode : 0o755 } ) ;
616+ }
617+
618+ const cases = [
619+ {
620+ name : "removes a matching ubuntu account before creating devuser" ,
621+ uid : "1000" ,
622+ gid : "1000" ,
623+ ubuntuUid : "1000" ,
624+ userdelStatus : "0" ,
625+ status : 0 ,
626+ invocations : [
627+ "id -u ubuntu" ,
628+ "userdel -r ubuntu" ,
629+ "groupadd -g 1000 devuser" ,
630+ "useradd -u 1000 -g 1000 -m -s /bin/bash devuser"
631+ ]
632+ } ,
633+ {
634+ name : "preserves the macOS 501 mapping when ubuntu has UID 1000" ,
635+ uid : "501" ,
636+ gid : "20" ,
637+ ubuntuUid : "1000" ,
638+ userdelStatus : "0" ,
639+ status : 0 ,
640+ invocations : [
641+ "id -u ubuntu" ,
642+ "groupadd -g 20 devuser" ,
643+ "useradd -u 501 -g 20 -m -s /bin/bash devuser"
644+ ]
645+ } ,
646+ {
647+ name : "creates devuser when the ubuntu account is absent" ,
648+ uid : "1000" ,
649+ gid : "1000" ,
650+ ubuntuUid : "" ,
651+ userdelStatus : "0" ,
652+ status : 0 ,
653+ invocations : [
654+ "id -u ubuntu" ,
655+ "groupadd -g 1000 devuser" ,
656+ "useradd -u 1000 -g 1000 -m -s /bin/bash devuser"
657+ ]
658+ } ,
659+ {
660+ name : "keeps the root mapping branch unchanged" ,
661+ uid : "0" ,
662+ gid : "0" ,
663+ ubuntuUid : "1000" ,
664+ userdelStatus : "0" ,
665+ status : 0 ,
666+ invocations : [
667+ "groupadd -o -g 0 devuser" ,
668+ "useradd -o -u 0 -g 0 -m -s /bin/bash devuser"
669+ ]
670+ } ,
671+ {
672+ name : "stops before group creation when ubuntu cleanup fails" ,
673+ uid : "1000" ,
674+ gid : "1000" ,
675+ ubuntuUid : "1000" ,
676+ userdelStatus : "1" ,
677+ status : 1 ,
678+ invocations : [
679+ "id -u ubuntu" ,
680+ "userdel -r ubuntu"
681+ ]
682+ }
683+ ] ;
684+
685+ for ( const scenario of cases ) {
686+ await t . test ( scenario . name , ( ) => {
687+ fs . rmSync ( logFile , { force : true } ) ;
688+ const result = spawnSync ( "/bin/sh" , [ "-c" , shellBody ] , {
689+ env : {
690+ ...process . env ,
691+ PATH : `${ stubDir } :${ process . env . PATH } ` ,
692+ HOST_UID : scenario . uid ,
693+ HOST_GID : scenario . gid ,
694+ INVOCATIONS_LOG : logFile ,
695+ STUB_UBUNTU_UID : scenario . ubuntuUid ,
696+ STUB_USERDEL_STATUS : scenario . userdelStatus
697+ } ,
698+ encoding : "utf8"
699+ } ) ;
700+ const invocations = fs . existsSync ( logFile )
701+ ? fs . readFileSync ( logFile , "utf8" ) . trim ( ) . split ( "\n" ) . filter ( Boolean )
702+ : [ ] ;
703+
704+ assert . equal ( result . status , scenario . status , result . stderr ) ;
705+ assert . deepEqual ( invocations , scenario . invocations ) ;
706+ } ) ;
707+ }
708+ } finally {
709+ fs . rmSync ( stubDir , { recursive : true , force : true } ) ;
710+ }
711+ } ) ;
712+
581713test ( "composeDockerfile rejects unknown runtimes" , async ( ) => {
582714 const sandboxDockerfile = await loadFreshEsm < typeof import ( "../../../lib/sandbox/dockerfile.ts" ) > ( "lib/sandbox/dockerfile.js" ) ;
583715
0 commit comments