"""Architecture-conformance checks for the control plane and for skill-driven projects (software-engineering skill, G2 frozen artefacts). Zero third-party dependencies; standard library only. This module imports no ``cloud.*`` module, so it can never violate the layer bans it enforces, and it can be reused verbatim against a skill project's own directory tree (see ``check_skill_project``). The four checks, each with a stated rule and a stated reason: 1. Contract version (``check_contract_version``). Rule mirrors the skill's contract-freeze change protocol: after freeze, a version change is allowed only with a coordinator-approved change record that names backward compatibility, the affected clients, and (for a breaking change) a regression task per affected client; the version must advance by exactly one. Reason: a silent contract drift is how parallel client threads diverge; the protocol is the only sanctioned path, so the check enforces the protocol instead of merely documenting it. 2. Forbidden directories (``is_within`` / ``check_task_writes``). Rule reuses the two directory definitions that already exist and invents no third one: on the control plane a task's own directory is its tenant work directory (``worker._tenant_dirs`` ``work``); in a skill project a client thread's own directory is its ``apps//`` tree. Any write outside the own directory -- including ``..`` escapes and symlink escapes, both resolved before comparison -- is a violation. Reason: cross-directory writes are how one tenant/client corrupts another's worktree. 3. Forbidden imports (``check_layer_imports``). Bans were chosen by auditing the current tree, not by wishful layering; every ban below passes on the baseline, and the repo-level test pins that: - Foundation modules (``db``, ``models``, ``queue``, ``logging_config``) must not import orchestration/service modules (``server``, ``worker``, ``pr_exec``, ``webhooks``, ``autoscaler``, ``cost``). Reason: the foundation is imported by everything; an upward import is a cycle waiting to happen and drags service state into pure helpers. - The execution module (``sandbox``) must not import persistence or orchestration (``db``, ``server``, ``worker``, ``cost``, ``auth``, ``pr``, ``pr_exec``, ``webhooks``). Reason: executors must stay swappable and testable with fake runners; decisions about mounts and networks arrive as arguments, not by reading the database. - No cloud module except the driver-adjacent ones (``db``, ``backup``, ``migrations``) may open a database connection directly (``sqlite3.connect`` or any ``psycopg*`` import). Service modules must obtain connections through ``cloud.db.connect`` or a Store. Reason: the backend switch (sqlite/postgres) lives in exactly one place; a direct driver call bypasses it. Deliberately NOT banned: ``server.py``'s use of ``sqlite3.Row`` / ``sqlite3.IntegrityError`` as type adapters on connections obtained from ``cloud.db`` -- that is adaptation, not a bypass, and banning it would fail the baseline for no safety gain. The test asserts the narrow form (no ``connect`` call, no driver import) rather than the broad form. Only static ``ast`` imports are seen; dynamic imports (``importlib``) are out of scope and are noted, not claimed. 4. Frozen stack (``check_frozen_stack``). Rule mirrors the skill's G2 rule "frozen stack must not be swapped mid-flight": a client whose stack entry changes, or disappears, after freeze fails unless an approved change record covers it; adding a brand-new client (no frozen entry) is an addition, not a swap, and passes. Reason: the shared core is built against the frozen table; a mid-flight swap silently invalidates it. All checkers return plain data (dicts/lists), never raise for a violation, so CI can report every violation in one run instead of stopping at the first. """ import ast import os from pathlib import Path FOUNDATION_MODULES = frozenset({"db", "models", "queue", "logging_config"}) ORCHESTRATION_MODULES = frozenset( {"server", "worker", "pr_exec", "webhooks", "autoscaler", "cost"}) EXECUTION_MODULES = frozenset({"sandbox"}) EXECUTION_BANNED = frozenset( {"db", "server", "worker", "cost", "auth", "pr", "pr_exec", "webhooks"}) DRIVER_ADJACENT_MODULES = frozenset({"db", "backup", "migrations"}) # ---------------------------------------------------------------- contract def parse_version(text): """Parse ``v1`` / ``1`` to int; anything else is None (unparseable is a failure, never silently treated as the frozen version).""" if text is None: return None text = str(text).strip() if text.startswith("v"): text = text[1:] if text.isdigit(): return int(text) return None def _change_record_problems(change, breaking): """Missing pieces of a contract-change record, per the protocol.""" problems = [] if not isinstance(change, dict): return ["change record missing (frozen contract changed without " "a coordinator-approved change record)"] if not change.get("approved"): problems.append("change not coordinator-approved") if change.get("backward_compatible") is None: problems.append("backward compatibility not declared") affected = change.get("affected_clients") if not affected or not isinstance(affected, list): problems.append("affected clients not listed") if breaking and not change.get("regression_task"): problems.append("breaking change without a regression task") return problems def check_contract_version(frozen, current, change=None): """Return ``{"ok": bool, "reasons": [...]}``. Equal versions pass regardless of ``change``. A different version passes only when it is exactly frozen + 1 and ``change`` satisfies the protocol; a breaking change is one where ``backward_compatible`` is False. """ frozen_n = parse_version(frozen) current_n = parse_version(current) if frozen_n is None or current_n is None: return {"ok": False, "reasons": ["unparseable contract version: frozen=%r " "current=%r" % (frozen, current)]} if current_n == frozen_n: return {"ok": True, "reasons": []} reasons = [] if current_n != frozen_n + 1: reasons.append("contract version must advance by exactly one " "(frozen v%d -> v%d, not v%d)" % (frozen_n, frozen_n + 1, current_n)) breaking = isinstance(change, dict) and \ change.get("backward_compatible") is False reasons.extend(_change_record_problems(change, breaking)) return {"ok": not reasons, "reasons": reasons} # ------------------------------------------------------------- frozen stack def check_frozen_stack(frozen, current, change=None): """``frozen``/``current`` map client -> stack string. Changed or removed entries fail without an approved change record (same shape as the contract-change record, minus the version rules); added clients pass as additions. """ frozen = frozen or {} current = current or {} reasons = [] swapped = sorted(c for c in frozen if c not in current or current[c] != frozen[c]) if not swapped: return {"ok": True, "reasons": [], "swapped": []} problems = _change_record_problems(change, breaking=False) if problems: reasons.append("frozen stack changed for %s without an approved " "change record: %s" % (swapped, "; ".join(problems))) return {"ok": not reasons, "reasons": reasons, "swapped": swapped} # ------------------------------------------------------------ directories def is_within(root, candidate): """True iff ``candidate`` resolves inside ``root`` (symlinks and ``..`` resolved first, so both escape routes are caught).""" try: root_p = Path(root).resolve() cand_p = Path(candidate).resolve() except (OSError, RuntimeError): return False return cand_p == root_p or root_p in cand_p.parents def check_task_writes(own_dir, paths): """Return the subset of ``paths`` written outside ``own_dir``.""" return [p for p in paths if not is_within(own_dir, p)] # ----------------------------------------------------------------- imports def scan_imports(source): """Static import scan of one module's source. Returns ``{"cloud": set, "driver_connect": bool, "driver_import": bool}``: ``cloud`` holds the ``cloud.`` top-level names imported; ``driver_connect`` is True on any ``sqlite3.connect`` call; ``driver_import`` is True on any ``psycopg*`` import. A bare ``import sqlite3`` (Row/exception adapters) sets neither flag. """ cloud_imports = set() driver_connect = False driver_import = False try: tree = ast.parse(source) except SyntaxError: return {"cloud": cloud_imports, "driver_connect": driver_connect, "driver_import": driver_import, "parse_error": True} for node in ast.walk(tree): if isinstance(node, ast.Import): for alias in node.names: if alias.name == "sqlite3": continue if alias.name.startswith("psycopg"): driver_import = True if alias.name == "cloud" or alias.name.startswith("cloud."): parts = alias.name.split(".") if len(parts) > 1: cloud_imports.add(parts[1]) elif isinstance(node, ast.ImportFrom): if node.module and node.module.startswith("psycopg"): driver_import = True if node.module == "cloud" or (node.module or "").startswith("cloud."): if node.module == "cloud": for alias in node.names: cloud_imports.add(alias.name) else: cloud_imports.add(node.module.split(".")[1]) elif isinstance(node, ast.Call): func = node.func if isinstance(func, ast.Attribute) and func.attr == "connect": value = func.value if isinstance(value, ast.Name) and value.id == "sqlite3": driver_connect = True return {"cloud": cloud_imports, "driver_connect": driver_connect, "driver_import": driver_import} def check_layer_imports(sources): """``sources`` maps module name -> source text. Returns a list of violation dicts; empty means the layer rules hold.""" violations = [] for module, source in sorted(sources.items()): scanned = scan_imports(source) if scanned.get("parse_error"): violations.append({"module": module, "rule": "parse", "detail": "source does not parse; " "imports cannot be verified"}) continue if module in FOUNDATION_MODULES: bad = scanned["cloud"] & ORCHESTRATION_MODULES if bad: violations.append({"module": module, "rule": "foundation_must_not_import_orchestration", "detail": sorted(bad)}) if module in EXECUTION_MODULES: bad = scanned["cloud"] & EXECUTION_BANNED if bad: violations.append({"module": module, "rule": "execution_must_not_import_persistence_or_orchestration", "detail": sorted(bad)}) if module not in DRIVER_ADJACENT_MODULES: if scanned["driver_connect"] or scanned["driver_import"]: violations.append({"module": module, "rule": "service_must_not_open_driver_connection_directly", "detail": "use cloud.db.connect or a Store"}) return violations def scan_cloud_package(repo_root): """Run ``check_layer_imports`` over every ``cloud/*.py`` in a repo checkout (tests and caches excluded). This is the repo-level test that makes a layer violation fail CI.""" cloud_dir = Path(repo_root) / "cloud" sources = {} if cloud_dir.is_dir(): for path in sorted(cloud_dir.glob("*.py")): sources[path.stem] = path.read_text(encoding="utf-8") return check_layer_imports(sources) def check_skill_project(project_root, frozen_version, current_version, frozen_stack, current_stack, own_dir, writes, change=None): """One-call conformance for a skill project: contract version, frozen stack, and directory writes together. Returns a dict with one entry per check plus an overall ``ok``.""" contract = check_contract_version(frozen_version, current_version, change=change) stack = check_frozen_stack(frozen_stack, current_stack, change=change) outside = check_task_writes(own_dir, writes) directory = {"ok": not outside, "reasons": (["writes outside own directory: %s" % outside] if outside else [])} return {"ok": contract["ok"] and stack["ok"] and directory["ok"], "contract": contract, "stack": stack, "directory": directory}