feat: add manifest_validator.py — security chokepoint for compose and manifest validation
Unit Tests / test (push) Successful in 11m18s

Rejects privileged compose configs (network_mode:host, pid:host, ipc:host,
userns_mode:host, cap_add:ALL, string commands, missing cell-network,
reserved container names). Validates manifest schema_version=3, image
digest pinning (sha256 required, :tag-only rejected), and provision hook
format. Wired into ServiceComposer.write_compose() and
ServiceStoreManager.install() as a single enforcement point.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-29 18:45:45 -04:00
parent 62b31b072b
commit 1f2f9d9f6e
4 changed files with 495 additions and 7 deletions
+31
View File
@@ -37,6 +37,10 @@ _CONTAINER_NAME_RE = re.compile(r'^cell-[a-z0-9][a-z0-9-]{0,30}$')
_ENV_VALUE_RE = re.compile(r'^[A-Za-z0-9._@:/+\-]{0,256}$')
_HOOK_BINARY_RE = re.compile(r'^[a-z][a-z0-9_-]{0,31}$')
_CAP_NAME_RE = re.compile(r'^[A-Z_]+$')
_ID_RE = re.compile(r'^[a-z][a-z0-9_-]{0,30}$')
_IMAGE_DIGEST_RE = re.compile(
r'^git\.pic\.ngo/roof/[a-zA-Z0-9._/-]+@sha256:[0-9a-f]{64}$'
)
def validate_manifest(manifest: dict) -> tuple:
@@ -49,11 +53,34 @@ def validate_manifest(manifest: dict) -> tuple:
"""
errors = []
# schema_version must be 3
schema_version = manifest.get('schema_version')
if schema_version is not None and schema_version != 3:
errors.append(
f'schema_version must be 3, got: {schema_version!r}'
)
# kind must be "store" if present — reject builtins coming in over the wire
kind = manifest.get('kind')
if kind is not None and kind != 'store':
errors.append(f'manifest kind must be "store", got: {kind!r}')
# id format check
manifest_id = manifest.get('id')
if manifest_id is not None:
if not isinstance(manifest_id, str) or not _ID_RE.match(manifest_id):
errors.append(
f'id must match ^[a-z][a-z0-9_-]{{0,30}}$, got: {manifest_id!r}'
)
# image must be digest-pinned from git.pic.ngo/roof/*
image = manifest.get('image')
if image is not None:
if not isinstance(image, str) or not _IMAGE_DIGEST_RE.match(image):
errors.append(
f'image must match git.pic.ngo/roof/*@sha256:<64-hex>, got: {image!r}'
)
# container_name structural check
cname = manifest.get('container_name')
if cname is not None:
@@ -156,6 +183,10 @@ def validate_rendered_compose(yaml_text: str, allowed_data_dir: str = None) -> t
continue
prefix = f'service {svc_name!r}'
cname = svc.get('container_name')
if cname is not None and cname in _RESERVED_CONTAINER_NAMES:
errors.append(f'{prefix}: container_name {cname!r} is reserved')
if svc.get('privileged') is True:
errors.append(f'{prefix}: privileged: true is not allowed')