2323
2424
2525POLICY_VERSION = "counterlab-runner-nonroot-v3"
26+ ADAPTER_POLICY_VERSION = "counterlab-adapter-nonroot-v1"
2627RUNNER_UID = 10001
2728RUNNER_GID = 10001
2829
@@ -96,10 +97,25 @@ def _mode_for(member: tarfile.TarInfo, *, directory: int, regular: int) -> int:
9697
9798
9899def _normalized_identity (
99- member : tarfile .TarInfo ,
100+ member : tarfile .TarInfo , * , profile : str = "runner"
100101) -> tuple [int , int , int , str , str ] | None :
101102 path = _clean_member_path (member .name )
102103
104+ if profile == "adapter" :
105+ if path in {"opt" , "opt/counterlab" , "workspace" , "fixtures" }:
106+ return (0 , 0 , 0o555 , "root" , "root" )
107+ if path == "opt/counterlab/harness.py" :
108+ return (0 , 0 , 0o555 , "root" , "root" )
109+ if path == "opt/counterlab/counterlab_sdk.py" :
110+ return (0 , 0 , 0o444 , "root" , "root" )
111+ if path == "output" :
112+ return (0 , 0 , 0o755 , "root" , "root" )
113+ if path == "tmp" :
114+ return (0 , 0 , 0o1777 , "root" , "root" )
115+ return None
116+ if profile != "runner" :
117+ raise ValueError (f"Unknown OCI normalization profile: { profile } " )
118+
103119 if _under (path , "app" ):
104120 return (
105121 0 ,
@@ -221,8 +237,8 @@ def _normalized_identity(
221237 return None
222238
223239
224- def _apply_policy (member : tarfile .TarInfo ) -> bool :
225- normalized = _normalized_identity (member )
240+ def _apply_policy (member : tarfile .TarInfo , * , profile : str = "runner" ) -> bool :
241+ normalized = _normalized_identity (member , profile = profile )
226242 if normalized is None :
227243 return False
228244 uid , gid , mode , uname , gname = normalized
@@ -252,14 +268,14 @@ def _apply_policy(member: tarfile.TarInfo) -> bool:
252268
253269
254270def _copy_normalized_tar (
255- source : tarfile .TarFile , target : tarfile .TarFile
271+ source : tarfile .TarFile , target : tarfile .TarFile , * , profile : str = "runner"
256272) -> tuple [int , tuple [str , ...]]:
257273 changed = 0
258274 samples : list [str ] = []
259275 for original in source :
260276 member = copy .copy (original )
261277 member .pax_headers = dict (original .pax_headers )
262- if _apply_policy (member ):
278+ if _apply_policy (member , profile = profile ):
263279 changed += 1
264280 if len (samples ) < 24 :
265281 samples .append (_clean_member_path (member .name ))
@@ -268,7 +284,9 @@ def _copy_normalized_tar(
268284 return changed , tuple (samples )
269285
270286
271- def rewrite_layer_bytes (value : bytes ) -> tuple [bytes , LayerRewrite ]:
287+ def rewrite_layer_bytes (
288+ value : bytes , * , profile : str = "runner"
289+ ) -> tuple [bytes , LayerRewrite ]:
272290 source_buffer = io .BytesIO (value )
273291 compressed_output = io .BytesIO ()
274292 with tarfile .open (fileobj = source_buffer , mode = "r:gz" ) as source :
@@ -283,7 +301,9 @@ def rewrite_layer_bytes(value: bytes) -> tuple[bytes, LayerRewrite]:
283301 with tarfile .open (
284302 fileobj = digesting , mode = "w|" , format = tarfile .PAX_FORMAT
285303 ) as target :
286- changed , samples = _copy_normalized_tar (source , target )
304+ changed , samples = _copy_normalized_tar (
305+ source , target , profile = profile
306+ )
287307 diff_id = f"sha256:{ digesting .hexdigest } "
288308 output = compressed_output .getvalue ()
289309 return output , LayerRewrite (
@@ -295,17 +315,19 @@ def rewrite_layer_bytes(value: bytes) -> tuple[bytes, LayerRewrite]:
295315 )
296316
297317
298- def _layer_needs_rewrite (path : Path ) -> bool :
318+ def _layer_needs_rewrite (path : Path , * , profile : str ) -> bool :
299319 with tarfile .open (path , mode = "r:gz" ) as source :
300320 for original in source :
301321 member = copy .copy (original )
302322 member .pax_headers = dict (original .pax_headers )
303- if _apply_policy (member ):
323+ if _apply_policy (member , profile = profile ):
304324 return True
305325 return False
306326
307327
308- def _rewrite_layer_file (source_path : Path , temporary_path : Path ) -> LayerRewrite :
328+ def _rewrite_layer_file (
329+ source_path : Path , temporary_path : Path , * , profile : str
330+ ) -> LayerRewrite :
309331 with source_path .open ("rb" ) as raw_source , tarfile .open (
310332 fileobj = raw_source , mode = "r:gz"
311333 ) as source , temporary_path .open ("xb" ) as raw_target :
@@ -320,7 +342,9 @@ def _rewrite_layer_file(source_path: Path, temporary_path: Path) -> LayerRewrite
320342 with tarfile .open (
321343 fileobj = digesting , mode = "w|" , format = tarfile .PAX_FORMAT
322344 ) as target :
323- changed , samples = _copy_normalized_tar (source , target )
345+ changed , samples = _copy_normalized_tar (
346+ source , target , profile = profile
347+ )
324348 diff_id = f"sha256:{ digesting .hexdigest } "
325349 return LayerRewrite (
326350 compressed_sha256 = _sha256_file (temporary_path ),
@@ -363,7 +387,12 @@ def _write_blob(directory: Path, value: bytes) -> tuple[str, int]:
363387
364388
365389def normalize_layout (
366- * , repo_root : Path , source_layout : Path , output_layout : Path , report_path : Path
390+ * ,
391+ repo_root : Path ,
392+ source_layout : Path ,
393+ output_layout : Path ,
394+ report_path : Path ,
395+ profile : str = "runner" ,
367396) -> dict [str , Any ]:
368397 root = repo_root .resolve (strict = True )
369398 source = source_layout .resolve (strict = True )
@@ -397,8 +426,13 @@ def normalize_layout(
397426 diff_ids = config .get ("rootfs" , {}).get ("diff_ids" )
398427 if not isinstance (diff_ids , list ) or len (diff_ids ) != len (layers ):
399428 raise ValueError ("Runner OCI rootfs diff_ids do not match its layers" )
400- if config .get ("config" , {}).get ("User" ) != "10001:10001" :
401- raise ValueError ("Runner OCI config must declare user 10001:10001" )
429+ expected_user = "10001:10001" if profile == "runner" else "65532:65532"
430+ if profile not in {"runner" , "adapter" }:
431+ raise ValueError (f"Unknown OCI normalization profile: { profile } " )
432+ if config .get ("config" , {}).get ("User" ) != expected_user :
433+ raise ValueError (
434+ f"{ profile .title ()} OCI config must declare user { expected_user } "
435+ )
402436
403437 output_layout .mkdir ()
404438 blob_directory = output_layout / "blobs" / "sha256"
@@ -414,7 +448,7 @@ def normalize_layout(
414448 if layer .get ("mediaType" ) != "application/vnd.oci.image.layer.v1.tar+gzip" :
415449 raise ValueError ("Runner OCI normalization requires gzip OCI layers" )
416450 layer_path = _source_blob (source , layer )
417- if not _layer_needs_rewrite (layer_path ):
451+ if not _layer_needs_rewrite (layer_path , profile = profile ):
418452 digest = _digest_value (layer )
419453 shutil .copyfile (layer_path , blob_directory / digest )
420454 updated_layers .append (dict (layer ))
@@ -425,7 +459,7 @@ def normalize_layout(
425459 continue
426460
427461 temporary = blob_directory / f"normalized-layer-{ index_value } .tar.gz"
428- rewrite = _rewrite_layer_file (layer_path , temporary )
462+ rewrite = _rewrite_layer_file (layer_path , temporary , profile = profile )
429463 final_path = blob_directory / rewrite .compressed_sha256
430464 if final_path .exists ():
431465 raise ValueError ("Normalized layer digest unexpectedly already exists" )
@@ -469,7 +503,9 @@ def normalize_layout(
469503
470504 report = {
471505 "schemaVersion" : "1" ,
472- "policyVersion" : POLICY_VERSION ,
506+ "policyVersion" : (
507+ POLICY_VERSION if profile == "runner" else ADAPTER_POLICY_VERSION
508+ ),
473509 "status" : "NORMALIZED" ,
474510 "sourceManifestDigest" : manifest_descriptor ["digest" ],
475511 "normalizedManifestDigest" : f"sha256:{ manifest_digest } " ,
@@ -489,12 +525,16 @@ def main(argv: Sequence[str] | None = None) -> int:
489525 parser .add_argument ("--source-layout" , type = Path , required = True )
490526 parser .add_argument ("--output-layout" , type = Path , required = True )
491527 parser .add_argument ("--report" , type = Path , required = True )
528+ parser .add_argument (
529+ "--profile" , choices = ("runner" , "adapter" ), default = "runner"
530+ )
492531 args = parser .parse_args (argv )
493532 report = normalize_layout (
494533 repo_root = args .repo_root ,
495534 source_layout = args .source_layout ,
496535 output_layout = args .output_layout ,
497536 report_path = args .report ,
537+ profile = args .profile ,
498538 )
499539 print (json .dumps (report , indent = 2 , sort_keys = True ))
500540 return 0
0 commit comments