fix: post-Phase-0 corrections — data-dir bind mounts, reserved subdomains, list_active()
Unit Tests / test (push) Successful in 11m31s

Three related fixes discovered during review of Phase 0 and Phase 1 manifests:

1. validate_rendered_compose(): add allowed_data_dir param. After ${PIC_DATA_DIR}
   substitution, compose templates produce absolute paths; without this the
   validator would reject every service install.  ServiceComposer.write_compose()
   now passes its resolved data_dir so only the designated data directory is
   exempt — /etc, /proc, docker.sock etc. still blocked.

2. _RESERVED_SUBDOMAINS: remove service-level subdomains (mail, calendar, files,
   webdav, webmail). The reserved list should protect PIC infrastructure endpoints
   (api, webui, admin) — not service subdomains that official store services
   (calendar, files, webmail) must be allowed to claim.  Aligns with the
   existing _RESERVED_SUBS in service_registry.py.

3. ServiceRegistry.list_active(): new method returning only installed store
   services (no builtins). This is the forward-looking API that Phase 2 will
   make the primary read path once builtins are deleted. Adding it now unblocks
   the QA agent's test_optional_services_feature.py which was already testing
   the expected Phase 2 behaviour.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-29 07:35:43 -04:00
parent c40919d374
commit 18b50d08c1
5 changed files with 1145 additions and 9 deletions
+13 -4
View File
@@ -19,8 +19,10 @@ _CAP_DENYLIST = frozenset({
'SYS_BOOT', 'MAC_ADMIN', 'MAC_OVERRIDE', 'SYS_TIME', 'SYS_TTY_CONFIG',
})
_RESERVED_SUBDOMAINS = frozenset({
'api', 'webui', 'admin', 'www', 'mail', 'ns1', 'ns2', 'git', 'registry',
'install', 'calendar', 'files', 'webdav', 'webmail',
# Core PIC infrastructure — never allow store services to hijack these
'api', 'webui', 'admin', 'www', 'ns1', 'ns2', 'git', 'registry', 'install',
# 'mail', 'calendar', 'files', 'webdav', 'webmail' are intentionally absent:
# they belong to official PIC store services and must be claimable by them.
})
_BACKEND_DENYLIST = frozenset({
'cell-api', 'cell-caddy', 'cell-coredns', 'cell-dnsmasq',
@@ -116,12 +118,16 @@ def validate_manifest(manifest: dict) -> tuple:
return (len(errors) == 0, errors)
def validate_rendered_compose(yaml_text: str) -> tuple:
def validate_rendered_compose(yaml_text: str, allowed_data_dir: str = None) -> tuple:
"""
Parse and security-validate a rendered docker-compose YAML string.
Returns (True, []) when safe; (False, [error_strings]) otherwise.
Rejects constructs that would give a store service elevated access to the host.
allowed_data_dir: when set, absolute bind mounts under this prefix are
permitted — they come from ${PIC_DATA_DIR} substitution and land in the
designated service data directory.
"""
errors = []
@@ -176,11 +182,14 @@ def validate_rendered_compose(yaml_text: str) -> tuple:
elif cap_str not in _CAP_ALLOWLIST:
errors.append(f'{prefix}: cap_add {cap_str!r} not in allowlist')
# volumes — reject absolute host-side bind mounts
# volumes — reject absolute host-side bind mounts unless they're under
# the sanctioned data directory (injected by ServiceComposer via PIC_DATA_DIR)
for vol in svc.get('volumes') or []:
vol_str = str(vol)
src = vol_str.split(':')[0] if ':' in vol_str else vol_str
if src.startswith('/'):
if allowed_data_dir and src.startswith(allowed_data_dir):
continue
errors.append(
f'{prefix}: absolute host bind mount not allowed: {vol_str!r}'
)