|
| 1 | +"""Validated process-boundary propagation for runtime identity. |
| 2 | +
|
| 3 | +The environment is a transport, not an authority. Values received from it are |
| 4 | +validated before they influence runtime state or filesystem paths. |
| 5 | +""" |
| 6 | + |
| 7 | +from __future__ import annotations |
| 8 | + |
| 9 | +import json |
| 10 | +import os |
| 11 | +from collections.abc import Mapping |
| 12 | +from dataclasses import dataclass |
| 13 | +from pathlib import Path |
| 14 | + |
| 15 | +from .errors import ConfigurationError |
| 16 | + |
| 17 | +RUN_ID_ENV = "BASE_CLI_RUN_ID" |
| 18 | +PARENT_RUN_ID_ENV = "BASE_CLI_PARENT_RUN_ID" |
| 19 | +RUN_ROOT_ENV = "BASE_CLI_RUN_ROOT" |
| 20 | +RUN_SCOPE_ENV = "BASE_CLI_RUN_SCOPE" |
| 21 | +PROPAGATE_ENV = "BASE_CLI_PROPAGATE" |
| 22 | +TRACEPARENT_ENV = "TRACEPARENT" |
| 23 | +TRACESTATE_ENV = "TRACESTATE" |
| 24 | + |
| 25 | +_SAFE_RUN_ID_CHARS = frozenset("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789._-") |
| 26 | +_VALID_SCOPES = frozenset({"primary", "internal"}) |
| 27 | + |
| 28 | + |
| 29 | +@dataclass(frozen=True) |
| 30 | +class RuntimePropagation: |
| 31 | + """Validated values received across a process boundary.""" |
| 32 | + |
| 33 | + run_id: str | None = None |
| 34 | + parent_run_id: str | None = None |
| 35 | + run_root: Path | None = None |
| 36 | + scope: str | None = None |
| 37 | + |
| 38 | + |
| 39 | +def read_runtime_propagation(environ: Mapping[str, str] | None = None) -> RuntimePropagation: |
| 40 | + """Read and validate the base-cli runtime environment contract.""" |
| 41 | + |
| 42 | + values = os.environ if environ is None else environ |
| 43 | + if not _is_enabled(values.get(PROPAGATE_ENV)): |
| 44 | + return RuntimePropagation() |
| 45 | + |
| 46 | + run_id = _optional_run_id(values.get(RUN_ID_ENV), RUN_ID_ENV) |
| 47 | + parent_run_id = _optional_run_id(values.get(PARENT_RUN_ID_ENV), PARENT_RUN_ID_ENV) |
| 48 | + raw_root = _optional_text(values.get(RUN_ROOT_ENV)) |
| 49 | + scope = _optional_text(values.get(RUN_SCOPE_ENV)) |
| 50 | + if scope is not None: |
| 51 | + scope = scope.lower() |
| 52 | + if scope not in _VALID_SCOPES: |
| 53 | + raise ConfigurationError(f"{RUN_SCOPE_ENV} must be 'primary' or 'internal', not {scope!r}.") |
| 54 | + if raw_root is not None and run_id is None and parent_run_id is None: |
| 55 | + raise ConfigurationError( |
| 56 | + f"{RUN_ROOT_ENV} requires {RUN_ID_ENV} or {PARENT_RUN_ID_ENV}; " |
| 57 | + "a runtime path cannot be trusted without an identity." |
| 58 | + ) |
| 59 | + run_root = _validate_run_root(raw_root) if raw_root is not None else None |
| 60 | + return RuntimePropagation( |
| 61 | + run_id=run_id, |
| 62 | + parent_run_id=parent_run_id, |
| 63 | + run_root=run_root, |
| 64 | + scope=scope, |
| 65 | + ) |
| 66 | + |
| 67 | + |
| 68 | +def validate_run_root(run_root: Path, expected_run_id: str | None = None) -> Path: |
| 69 | + """Validate a parent run bundle supplied by a process boundary. |
| 70 | +
|
| 71 | + The path must be absolute, an actual directory (not a symlink), and carry |
| 72 | + a private ``run.json`` whose identity matches the supplied run ID when one |
| 73 | + is available. The returned path is resolved and safe to use as an |
| 74 | + inherited runtime root. |
| 75 | + """ |
| 76 | + |
| 77 | + candidate = Path(run_root).expanduser() |
| 78 | + if not candidate.is_absolute(): |
| 79 | + raise ConfigurationError(f"{RUN_ROOT_ENV} must be an absolute path.") |
| 80 | + if candidate != candidate.resolve(strict=False): |
| 81 | + raise ConfigurationError(f"{RUN_ROOT_ENV} must not contain symlinked path components.") |
| 82 | + try: |
| 83 | + resolved = candidate.resolve(strict=True) |
| 84 | + except OSError as exc: |
| 85 | + raise ConfigurationError(f"{RUN_ROOT_ENV} does not point to a readable directory: {exc}") from exc |
| 86 | + if not resolved.is_dir() or resolved.is_symlink(): |
| 87 | + raise ConfigurationError(f"{RUN_ROOT_ENV} must point to a real directory.") |
| 88 | + |
| 89 | + metadata_path = resolved / "run.json" |
| 90 | + if metadata_path.is_symlink() or not metadata_path.is_file(): |
| 91 | + raise ConfigurationError(f"{RUN_ROOT_ENV} must point to an active base-cli run bundle.") |
| 92 | + try: |
| 93 | + payload = json.loads(metadata_path.read_text(encoding="utf-8")) |
| 94 | + except (OSError, UnicodeDecodeError, json.JSONDecodeError) as exc: |
| 95 | + raise ConfigurationError(f"{RUN_ROOT_ENV} has no valid run metadata.") from exc |
| 96 | + metadata_run_id = payload.get("run_id") if isinstance(payload, dict) else None |
| 97 | + if not isinstance(metadata_run_id, str) or not _is_safe_run_id(metadata_run_id): |
| 98 | + raise ConfigurationError(f"{RUN_ROOT_ENV} has invalid run metadata.") |
| 99 | + if expected_run_id is not None and metadata_run_id != expected_run_id: |
| 100 | + raise ConfigurationError(f"{RUN_ROOT_ENV} belongs to run {metadata_run_id!r}, not {expected_run_id!r}.") |
| 101 | + return resolved |
| 102 | + |
| 103 | + |
| 104 | +def _validate_run_root(raw_root: str) -> Path: |
| 105 | + return validate_run_root(Path(raw_root)) |
| 106 | + |
| 107 | + |
| 108 | +def _optional_run_id(value: str | None, variable: str) -> str | None: |
| 109 | + text = _optional_text(value) |
| 110 | + if text is None: |
| 111 | + return None |
| 112 | + if not _is_safe_run_id(text): |
| 113 | + raise ConfigurationError( |
| 114 | + f"{variable} must be a single filesystem-safe run ID containing only letters, digits, '.', '_' or '-'." |
| 115 | + ) |
| 116 | + return text |
| 117 | + |
| 118 | + |
| 119 | +def _optional_text(value: str | None) -> str | None: |
| 120 | + if value is None: |
| 121 | + return None |
| 122 | + text = value.strip() |
| 123 | + return text or None |
| 124 | + |
| 125 | + |
| 126 | +def _is_safe_run_id(value: str) -> bool: |
| 127 | + return bool(value) and ".." not in value and all(character in _SAFE_RUN_ID_CHARS for character in value) |
| 128 | + |
| 129 | + |
| 130 | +def _is_enabled(value: str | None) -> bool: |
| 131 | + return value is None or value.strip().lower() not in {"0", "false", "no", "off"} |
| 132 | + |
| 133 | + |
| 134 | +__all__ = [ |
| 135 | + "PARENT_RUN_ID_ENV", |
| 136 | + "PROPAGATE_ENV", |
| 137 | + "RUN_ID_ENV", |
| 138 | + "RUN_ROOT_ENV", |
| 139 | + "RUN_SCOPE_ENV", |
| 140 | + "TRACEPARENT_ENV", |
| 141 | + "TRACESTATE_ENV", |
| 142 | + "RuntimePropagation", |
| 143 | + "read_runtime_propagation", |
| 144 | + "validate_run_root", |
| 145 | +] |
0 commit comments