@@ -2,7 +2,7 @@ import { execFileSync, spawnSync } from 'node:child_process';
22import { randomUUID } from 'node:crypto' ;
33import { lstatSync , realpathSync } from 'node:fs' ;
44import type { ContainerProfile , TaskFilesystems } from './profile.ts' ;
5- import { AGENT_IMAGE , BASE_IMAGE , CLAUDE_VERSION , CODEX_VERSION } from './image.ts' ;
5+ import { BASE_IMAGE , CLAUDE_VERSION , CODEX_VERSION } from './image.ts' ;
66
77const dockerEnvironment = ( secrets : Readonly < Record < string , string > > = { } ) => ( {
88 PATH : process . env . PATH , DOCKER_HOST : process . env . DOCKER_HOST , ...secrets ,
@@ -44,8 +44,9 @@ export interface TaskStorageLimits {
4444
4545/** Allocate bounded, engine-owned task filesystems and keep them mounted. */
4646export function prepareTaskFilesystems ( stagingDirectory : string , limits : TaskStorageLimits ,
47- timeoutMs = 60_000 ) : TaskFilesystems {
47+ imageId : string , timeoutMs = 60_000 ) : TaskFilesystems {
4848 for ( const [ name , value ] of Object . entries ( limits ) ) validLimit ( value , name ) ;
49+ if ( ! / ^ s h a 2 5 6 : [ 0 - 9 a - f ] { 64 } $ / . test ( imageId ) ) throw new Error ( 'Task filesystems require the immutable built image ID.' ) ;
4950 const remaining = createDeadline ( timeoutMs ) ;
5051 const staging = realpathSync ( stagingDirectory ) ;
5152 if ( / [ \n , ] / . test ( staging ) ) throw new Error ( 'Staging path cannot be represented as a Docker mount.' ) ;
@@ -62,29 +63,25 @@ export function prepareTaskFilesystems(stagingDirectory: string, limits: TaskSto
6263 }
6364 const seed = [
6465 'set -eu' ,
65- 'cp -a /run/codeboost-staging/. /work/' ,
66- 'cp -a /work/.git/. /metadata/' ,
66+ 'cp -a --no-preserve=ownership,timestamps /run/codeboost-staging/. /work/' ,
67+ 'cp -a --no-preserve=ownership,timestamps /work/.git/. /metadata/' ,
6768 'rm -rf /work/.git' ,
6869 'mkdir /work/.git' ,
69- 'touch /metadata/.codeboost-ready' ,
70- 'exec sleep infinity' ,
70+ 'chown -R 10001:10001 /work /metadata' ,
7171 ] . join ( '; ' ) ;
7272 docker ( [ 'run' , '--detach' , '--name' , keeper , '--read-only' , '--user' , '10001:10001' , '--network=none' ,
7373 '--cap-drop=ALL' , '--security-opt=no-new-privileges' , '--pids-limit=32' , '--memory=128m' , '--cpus=.25' ,
74+ '--mount' , `type=volume,source=${ workVolume } ,target=/work` ,
75+ '--mount' , `type=volume,source=${ metadataVolume } ,target=/metadata` ,
76+ '--label' , 'io.codeboost.task-storage=keeper' , '--entrypoint' , 'sleep' , imageId , 'infinity' ] ,
77+ { timeoutMs : remaining ( ) } ) ;
78+ docker ( [ 'run' , '--rm' , '--read-only' , '--user' , '0:0' , '--network=none' , '--cap-drop=ALL' ,
79+ '--cap-add=CHOWN' , '--cap-add=DAC_OVERRIDE' , '--cap-add=FOWNER' , '--security-opt=no-new-privileges' , '--pids-limit=32' ,
80+ '--memory=128m' , '--cpus=.25' ,
7481 '--mount' , `type=bind,source=${ staging } ,target=/run/codeboost-staging,readonly` ,
7582 '--mount' , `type=volume,source=${ workVolume } ,target=/work` ,
7683 '--mount' , `type=volume,source=${ metadataVolume } ,target=/metadata` ,
77- '--label' , 'io.codeboost.task-storage=keeper' , '--entrypoint' , 'sh' , AGENT_IMAGE , '-c' , seed ] , { timeoutMs : remaining ( ) } ) ;
78- while ( true ) {
79- const ready = spawnSync ( 'docker' , [ 'exec' , keeper , 'sh' , '-c' ,
80- 'test -f /metadata/.codeboost-ready && rm /metadata/.codeboost-ready' ] , {
81- timeout : remaining ( ) , env : dockerEnvironment ( ) ,
82- stdio : [ 'ignore' , 'ignore' , 'ignore' ] ,
83- } ) ;
84- if ( ready . status === 0 ) break ;
85- if ( ready . error ) throw new Error ( 'Timed out preparing bounded task filesystems.' ) ;
86- Atomics . wait ( new Int32Array ( new SharedArrayBuffer ( 4 ) ) , 0 , 0 , 25 ) ;
87- }
84+ '--entrypoint' , 'sh' , imageId , '-c' , seed ] , { timeoutMs : remaining ( ) } ) ;
8885 return Object . freeze ( { keeper, workVolume, metadataVolume, ...limits } ) ;
8986 } catch ( error ) {
9087 spawnSync ( 'docker' , [ 'rm' , '--force' , keeper ] , { env : dockerEnvironment ( ) , stdio : 'ignore' } ) ;
@@ -113,7 +110,8 @@ export function validateContainer(container: string, profile: ContainerProfile,
113110 { Id ?: string ; Config ?: { User ?: string ; Entrypoint ?: string [ ] ; Labels ?: Record < string , string > } } | undefined ;
114111 const imageId = image ?. Id , labels = image ?. Config ?. Labels ?? { } ;
115112 const host = inspect . HostConfig ;
116- if ( ! imageId || inspect . Image !== imageId || inspect . Config . Image !== profile . expectedImage
113+ if ( ! imageId || imageId !== profile . expectedImage || inspect . Image !== profile . expectedImage
114+ || inspect . Config . Image !== profile . expectedImage
117115 || image ?. Config ?. User !== '10001:10001'
118116 || JSON . stringify ( image . Config ?. Entrypoint ) !== JSON . stringify ( [ '/usr/local/bin/codeboost-container-probe' ] )
119117 || labels [ 'org.opencontainers.image.base.name' ] !== BASE_IMAGE
@@ -127,15 +125,22 @@ export function validateContainer(container: string, profile: ContainerProfile,
127125 || ! host . ReadonlyRootfs || host . Privileged
128126 || ! host . CapDrop ?. map ( value => value . toUpperCase ( ) ) . includes ( 'ALL' )
129127 || ! host . SecurityOpt ?. some ( value => value . startsWith ( 'no-new-privileges' ) )
130- || host . NetworkMode !== profile . networkMode || host . PidMode === 'host ' || host . IpcMode === 'host '
128+ || host . NetworkMode !== 'none' || host . PidMode !== ' ' || host . IpcMode !== 'private '
131129 || ( host . Devices ?. length ?? 0 ) !== 0 || ( host . DeviceRequests ?. length ?? 0 ) !== 0 || host . PidsLimit !== 128
132130 || host . Memory !== 512 * 1024 * 1024 || host . NanoCpus !== 1_000_000_000 )
133131 throw new Error ( 'Container daemon configuration is missing required lockdown.' ) ;
134132 const tmpfs = host . Tmpfs ?? { } ;
135- for ( const path of [ '/tmp' , '/home/codeboost' ] ) if ( ! tmpfs [ path ] ?. includes ( 'size=' ) )
136- throw new Error ( `Container is missing bounded tmpfs ${ path } .` ) ;
137- if ( profile . vendor === 'codex' && ! tmpfs [ '/run/codeboost-auth/codex' ] ?. includes ( 'size=' ) )
138- throw new Error ( 'Codex state directory must be bounded tmpfs.' ) ;
133+ const expectedTmpfs = new Map ( [
134+ [ '/tmp' , [ 'rw' , 'nosuid' , 'nodev' , 'size=33554432' , 'nr_inodes=4096' , 'mode=1777' ] ] ,
135+ [ '/home/codeboost' , [ 'rw' , 'nosuid' , 'nodev' , 'size=1048576' , 'nr_inodes=128' , 'uid=10001' , 'gid=10001' , 'mode=0700' ] ] ,
136+ ...( profile . vendor === 'codex' ? [ [ '/run/codeboost-auth/codex' ,
137+ [ 'rw' , 'nosuid' , 'nodev' , 'size=4194304' , 'nr_inodes=256' , 'uid=10001' , 'gid=10001' , 'mode=0700' ] ] as const ] : [ ] ) ,
138+ ] ) ;
139+ if ( Object . keys ( tmpfs ) . length !== expectedTmpfs . size ) throw new Error ( 'Container tmpfs mount set changed.' ) ;
140+ for ( const [ path , expected ] of expectedTmpfs ) {
141+ const actual = new Set ( ( tmpfs [ path ] ?? '' ) . split ( ',' ) ) ;
142+ if ( expected . some ( option => ! actual . has ( option ) ) ) throw new Error ( `Container tmpfs ${ path } is missing required options.` ) ;
143+ }
139144 const mounts = new Map ( inspect . Mounts . map ( item => [ item . Destination , item ] ) ) ;
140145 const allowedMounts = new Set ( [ '/work' , '/work/.git' , '/run/codeboost-input' ,
141146 ...( profile . vendor === 'codex' ? [ '/run/codeboost-auth/codex/auth.json' ] : [ ] ) ] ) ;
@@ -164,15 +169,17 @@ export function validateContainer(container: string, profile: ContainerProfile,
164169 || volume . Labels ?. [ 'io.codeboost.task-storage' ] !== expected [ 0 ]
165170 || ! optionString . split ( ',' ) . includes ( `size=${ expected [ 1 ] } ` )
166171 || ! optionString . split ( ',' ) . includes ( `nr_inodes=${ expected [ 2 ] } ` )
167- || ! optionString . split ( ',' ) . includes ( 'nosuid' ) || ! optionString . split ( ',' ) . includes ( 'nodev' ) )
172+ || ! optionString . split ( ',' ) . includes ( 'uid=10001' ) || ! optionString . split ( ',' ) . includes ( 'gid=10001' )
173+ || ! optionString . split ( ',' ) . includes ( 'mode=0755' ) || ! optionString . split ( ',' ) . includes ( 'nosuid' )
174+ || ! optionString . split ( ',' ) . includes ( 'nodev' ) )
168175 throw new Error ( 'Task volume does not match its bounded tmpfs allocation.' ) ;
169176 }
170177 const keeper = JSON . parse ( docker ( [ 'container' , 'inspect' , profile . filesystems . keeper ] , { timeoutMs : remaining ( ) } ) ) [ 0 ] as
171178 { State ?: { Running ?: boolean } ; Config ?: { Image ?: string ; User ?: string ; Labels ?: Record < string , string > } ;
172179 HostConfig ?: { ReadonlyRootfs ?: boolean ; Privileged ?: boolean ; NetworkMode ?: string ; CapDrop ?: string [ ] | null ;
173180 SecurityOpt ?: string [ ] | null } ; Mounts ?: Array < { Type : string ; Name ?: string ; Destination : string ; RW : boolean } > } | undefined ;
174181 const keeperVolumes = new Map ( ( keeper ?. Mounts ?? [ ] ) . filter ( item => item . Type === 'volume' ) . map ( item => [ item . Destination , item ] ) ) ;
175- if ( ! keeper ?. State ?. Running || keeper . Config ?. Image !== AGENT_IMAGE || keeper . Config ?. User !== '10001:10001'
182+ if ( ! keeper ?. State ?. Running || keeper . Config ?. Image !== profile . expectedImage || keeper . Config ?. User !== '10001:10001'
176183 || keeper . Config ?. Labels ?. [ 'io.codeboost.task-storage' ] !== 'keeper' || ! keeper . HostConfig ?. ReadonlyRootfs
177184 || keeper . HostConfig . Privileged || keeper . HostConfig . NetworkMode !== 'none'
178185 || ! keeper . HostConfig . CapDrop ?. map ( value => value . toUpperCase ( ) ) . includes ( 'ALL' )
0 commit comments