Files
stack/packages/mosaic/framework/tools/lease-broker/check-runtime-launches.py
jason.woltje abd2791f59
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/push/publish Pipeline was successful
feat(mosaic): WI-2 mutator-class guard for directive-freshness (#837)
WI-2: mutator-class guard for the compaction directive-freshness mechanism — command-position parser (prefix x var-indirection unified) + primitive-anchored invariant backstop + all-tools-hook fail-close. Parser-complete on principle: realistic evasion matrix RED-regression-covered, residual exotic evasions proven backstop-caught (B-tests), lens-convergence reached.

closes #829
2026-07-18 05:51:58 +00:00

437 lines
15 KiB
Python

#!/usr/bin/env python3
"""Fail CI when production code launches Claude/Pi outside the lease gate."""
from __future__ import annotations
import argparse
import json
import re
import shlex
import sys
from pathlib import Path
from typing import Final, NamedTuple, Sequence
SCANNED_ROOTS: Final = ("packages", "apps", "plugins", "tools")
SCANNED_SUFFIXES: Final = {
".bash",
".cjs",
".js",
".json",
".mjs",
".py",
".sh",
".ts",
".tsx",
".yaml",
".yml",
".zsh",
}
SKIPPED_DIRECTORIES: Final = {
".git",
".next",
".turbo",
"coverage",
"dist",
"node_modules",
}
DANGEROUS_PRIMITIVE: Final = "--dangerously-" + "skip-permissions"
CHOKE_POINT_SUFFIX: Final = "framework/tools/lease-broker/launch-runtime.py"
SHELL_SUFFIXES: Final = {".bash", ".sh", ".zsh"}
# A launch line is gated only when it CALLS the common wrapper in command
# position, invokes the TypeScript adapter, or constructs/runs a `mosaic`
# runtime command. A marker in a comment, string argument, echo, or unrelated
# variable can never satisfy these invocation-shaped patterns.
GATED_PATTERNS: Final = (
re.compile(r"(?:^\s*|=>\s*)execLeaseGatedRuntime\s*\("),
re.compile(
r"\bexecRuntime\s*\(\s*[\"']python3[\"']\s*,\s*"
r"\[\s*launcher\s*,.*[\"']--runtime[\"']\s*,\s*runtime\s*,\s*[\"']--[\"']"
),
re.compile(
r"(?:^|[;&|]\s*|\bexec\s+|\b(?:LAUNCH_COMMAND|launch_cmd)\s*=\s*\(?|\becho\s+[\"'])"
r"mosaic\s+(?:yolo\s+)?(?:claude|pi|claudex|[\"']?\$\{?runtime\}?[\"']?|[\"']?\$MOSAIC_AGENT_RUNTIME[\"']?)\b"
),
re.compile(r"\[\s*[\"']mosaic[\"']\s*,\s*(?:[\"']yolo[\"']\s*,\s*)?(?:runtime|[\"'](?:claude|pi|claudex)[\"'])"),
)
DIRECT_PATTERNS: Final = (
# Shell/process command forms, including here-doc command examples that an
# operator could execute verbatim.
re.compile(r"^\s*(?:claude|pi)(?:\s|$)"),
re.compile(r"(?:^|[;&|]\s*|\bexec\s+|\bcommand\s+)(?:claude|pi)\s+(?:-p\b|--dangerously\b|--print\b)"),
re.compile(r"\bexec\s+(?:claude|pi)(?:\s|$)"),
re.compile(r"\bexec\s+(?:/[^\s/]+)+/(?:claude|pi)(?:\s|$)"),
re.compile(r"\bexec\s+[\"']?\$(?:\{?runtime\}?|MOSAIC_AGENT_RUNTIME)\b"),
# JS/TS and Python process APIs with a literal runtime binary.
re.compile(
r"\b(?:spawn|spawnSync|exec|execSync|execFile|execFileSync|execv|execvp|execvpe|Popen|run|call|system|check_call|check_output)\s*\(\s*(?:\[\s*)?[\"'](?:claude|pi)(?:[\"']|\s)"
),
re.compile(
r"\b(?:spawn|spawnSync|exec|execSync|execFile|execFileSync|execv|execvp|execvpe|Popen|run|call|system|check_call|check_output)\s*\(\s*(?:\[\s*)?[\"'](?:/[^\"'/]+)+/(?:claude|pi)[\"']"
),
# Launch-command arrays and the prior @mosaicstack/coord dynamic default.
re.compile(r"\b(?:spawn|spawnSync|exec|execSync|execFile|execFileSync)\s*\(\s*runtime\b"),
re.compile(r"\b(?:command|launchCommand|LAUNCH_COMMAND)\s*=\s*(?:\(|\[)\s*[\"']?(?:claude|pi)\b"),
re.compile(r"\b(?:command|launchCommand|LAUNCH_COMMAND)\s*=\s*(?:\(|\[)\s*[\"']?\$(?:\{?runtime\}?|MOSAIC_AGENT_RUNTIME)\b"),
re.compile(r"\breturn\s*\[\s*runtime\s*,\s*[\"'](?:-p|--dangerously)"),
re.compile(r"\$\(\s*(?:claude|pi)(?:\s|$)"),
re.compile(r"\beval\s+[\"'](?:claude|pi)(?:\s|[\"'])"),
)
RUNTIME_ASSIGNMENT: Final = re.compile(
r"^\s*([A-Za-z_][A-Za-z0-9_]*)\s*=\s*[\"']?(?:claude|pi)(?:\s|[\"']|[;&|]|$)"
)
TYPESCRIPT_WRAPPER_INVOCATION: Final = re.compile(
r"(?m)^\s*execRuntime\s*\(\s*[\"']python3[\"']\s*,\s*"
r"\[\s*launcher\s*,[^\]]*[\"']--runtime[\"']\s*,\s*runtime\s*,\s*"
r"[\"']--[\"']\s*,\s*runtime(?:\s*,|\s*\])",
re.DOTALL,
)
class LaunchSite(NamedTuple):
path: Path
line_number: int
line: str
classification: str
def is_test_path(path: Path) -> bool:
name = path.name.lower()
return (
"__tests__" in path.parts
or ".spec." in name
or ".test." in name
or name.endswith("_unittest.py")
or name.startswith("test-")
or name.startswith("test_")
)
def strip_comments(path: Path, line: str, in_block_comment: bool = False) -> tuple[str, bool]:
suffix = path.suffix.lower()
hash_comments = suffix in {".bash", ".py", ".sh", ".yaml", ".yml", ".zsh"}
slash_comments = suffix in {".cjs", ".js", ".mjs", ".ts", ".tsx"}
output: list[str] = []
quote: str | None = None
escaped = False
index = 0
while index < len(line):
if in_block_comment:
end = line.find("*/", index)
if end < 0:
return "".join(output), True
in_block_comment = False
index = end + 2
continue
character = line[index]
following = line[index + 1] if index + 1 < len(line) else ""
if quote is not None:
output.append(character)
if escaped:
escaped = False
elif character == "\\":
escaped = True
elif character == quote:
quote = None
index += 1
continue
if character in {"'", '"', "`"}:
quote = character
output.append(character)
index += 1
continue
if slash_comments and character == "/" and following == "/":
break
if slash_comments and character == "/" and following == "*":
in_block_comment = True
index += 2
continue
if hash_comments and character == "#":
if suffix == ".py" or index == 0 or line[index - 1].isspace():
break
output.append(character)
index += 1
return "".join(output), in_block_comment
def is_choke_point(path: Path) -> bool:
return path.as_posix().endswith(CHOKE_POINT_SUFFIX)
def shell_commands(line: str) -> list[list[str]]:
candidate = line.rstrip().removesuffix("\\").rstrip()
try:
lexer = shlex.shlex(candidate, posix=True, punctuation_chars=";&|")
lexer.whitespace_split = True
lexer.commenters = ""
tokens = list(lexer)
except ValueError:
return []
commands: list[list[str]] = []
current: list[str] = []
for token in tokens:
if token and all(character in ";&|" for character in token):
if current:
commands.append(current)
current = []
else:
current.append(token)
if current:
commands.append(current)
return commands
def is_wrapper_invocation(path: Path, line: str) -> bool:
if path.suffix.lower() not in SHELL_SUFFIXES:
return False
separator = re.search(r"\s--(?:\s|$)", line)
if separator is None:
return False
# Everything after the wrapper separator is opaque runtime argv and may
# contain an open quote continued on later physical lines. Parse only the
# complete command-position prefix through the separator.
wrapper_prefix = line[: separator.end()]
for command in shell_commands(wrapper_prefix):
if command and command[0] == "exec":
command = command[1:]
if not command:
continue
if command[0] == "python3":
if len(command) < 2 or not command[1].endswith("launch-runtime.py"):
continue
arguments = command[2:]
elif command[0].endswith(("launch-runtime.py", "launch-runtime.sh")):
arguments = command[1:]
else:
continue
try:
runtime_index = arguments.index("--runtime")
separator_index = arguments.index("--")
except ValueError:
continue
if runtime_index + 1 < len(arguments) and runtime_index < separator_index:
return True
return False
def is_gated_line(path: Path, line: str) -> bool:
return is_wrapper_invocation(path, line) or any(
pattern.search(line) for pattern in GATED_PATTERNS
)
def shell_command_tokens(path: Path, line: str) -> list[str]:
if path.suffix.lower() not in SHELL_SUFFIXES:
return []
assignment = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=")
case_arm = re.match(r"^\s*[^;&()]+\)\s*", line)
command_source = line[case_arm.end() :] if case_arm is not None else line
resolved: list[str] = []
for command in shell_commands(command_source):
index = 0
while index < len(command) and assignment.match(command[index]):
index += 1
# Resolve command position once for every literal and variable caller.
# Prefixes may be nested in either order (for example `exec env A=1`).
while index < len(command):
if command[index] in {"command", "exec", "nohup"}:
index += 1
continue
if command[index] == "env":
index += 1
while index < len(command) and (
command[index].startswith("-") or assignment.match(command[index])
):
index += 1
continue
break
if index < len(command):
resolved.append(command[index])
return resolved
def runtime_variable_reference(token: str, runtime_variables: set[str]) -> bool:
match = re.fullmatch(r"\$(?:([A-Za-z_][A-Za-z0-9_]*)|\{([A-Za-z_][A-Za-z0-9_]*)\})", token)
if match is None:
return False
return (match.group(1) or match.group(2)) in runtime_variables
def is_shell_direct_invocation(
path: Path, line: str, runtime_variables: set[str]
) -> bool:
return any(
Path(token).name in {"claude", "pi"}
or runtime_variable_reference(token, runtime_variables)
for token in shell_command_tokens(path, line)
)
def is_direct_line(path: Path, line: str, runtime_variables: set[str]) -> bool:
return is_shell_direct_invocation(path, line, runtime_variables) or any(
pattern.search(line) for pattern in DIRECT_PATTERNS
)
def executes_runtime_variable(
path: Path, line: str, runtime_variables: set[str]
) -> bool:
if any(
runtime_variable_reference(token, runtime_variables)
for token in shell_command_tokens(path, line)
):
return True
for variable in runtime_variables:
reference = rf"\$(?:{re.escape(variable)}|\{{{re.escape(variable)}\}})"
if re.search(rf"\beval\s+[\"']?{reference}", line):
return True
return False
def typescript_wrapper_lines(path: Path, source: str) -> set[int]:
if path.suffix.lower() not in {".js", ".mjs", ".ts", ".tsx"}:
return set()
return {
source.count("\n", 0, match.start()) + 1
for match in TYPESCRIPT_WRAPPER_INVOCATION.finditer(source)
}
def classify_text(path: Path, source: str) -> list[LaunchSite]:
sites: list[LaunchSite] = []
validated_typescript_wrappers = typescript_wrapper_lines(path, source)
runtime_variables: set[str] = set()
gated_continuation = False
in_block_comment = False
for line_number, physical_line in enumerate(source.splitlines(), start=1):
line, in_block_comment = strip_comments(path, physical_line, in_block_comment)
stripped = line.strip()
if not stripped:
gated_continuation = False
continue
assignment = RUNTIME_ASSIGNMENT.match(line)
if assignment is not None:
runtime_variables.add(assignment.group(1))
primitive_violation = DANGEROUS_PRIMITIVE in line and not is_choke_point(path)
line_is_direct = is_direct_line(path, line, runtime_variables) or executes_runtime_variable(
path, line, runtime_variables
)
line_is_gated = line_number in validated_typescript_wrappers or is_gated_line(path, line)
if primitive_violation:
sites.append(
LaunchSite(path, line_number, physical_line.rstrip(), "dangerous-primitive")
)
elif line_is_direct and not gated_continuation:
# Direct syntax always wins over a same-line marker. Only the command
# continuation of a previously validated wrapper may contain the raw
# runtime binary itself.
sites.append(LaunchSite(path, line_number, physical_line.rstrip(), "direct"))
elif line_is_gated:
sites.append(LaunchSite(path, line_number, physical_line.rstrip(), "gated"))
gated = line_is_gated or gated_continuation
gated_continuation = gated and line.rstrip().endswith("\\")
return sites
def scan_text(path: Path, source: str) -> list[LaunchSite]:
return [site for site in classify_text(path, source) if site.classification != "gated"]
def source_files(root: Path):
for relative_root in SCANNED_ROOTS:
search_root = root / relative_root
if not search_root.is_dir():
continue
for path in search_root.rglob("*"):
if not path.is_file() or path.suffix.lower() not in SCANNED_SUFFIXES:
continue
if any(part in SKIPPED_DIRECTORIES for part in path.parts):
continue
if is_test_path(path):
continue
yield path
def scan_repository(root: Path) -> list[LaunchSite]:
violations: list[LaunchSite] = []
for path in source_files(root):
try:
source = path.read_text(encoding="utf-8")
except UnicodeDecodeError:
violations.append(LaunchSite(path, 0, "non-UTF-8 source", "unscannable"))
continue
violations.extend(scan_text(path.relative_to(root), source))
return violations
def inventory_repository(root: Path) -> list[LaunchSite]:
inventory: list[LaunchSite] = []
for path in source_files(root):
try:
source = path.read_text(encoding="utf-8")
except UnicodeDecodeError:
inventory.append(LaunchSite(path.relative_to(root), 0, "non-UTF-8 source", "unscannable"))
continue
relative_path = path.relative_to(root)
inventory.extend(classify_text(relative_path, source))
return inventory
def format_violation(violation: LaunchSite) -> str:
return f"{violation.path}:{violation.line_number}: {violation.classification}: {violation.line.strip()}"
def main(argv: Sequence[str] | None = None) -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--root", type=Path, default=Path.cwd())
parser.add_argument("--json", action="store_true")
arguments = parser.parse_args(argv)
root = arguments.root.resolve()
inventory = inventory_repository(root)
violations = [site for site in inventory if site.classification != "gated"]
if arguments.json:
print(
json.dumps(
{
"gated": sum(site.classification == "gated" for site in inventory),
"total": len(inventory),
"sites": [
{
"path": str(site.path),
"line": site.line_number,
"classification": site.classification,
"source": site.line.strip(),
}
for site in inventory
],
},
sort_keys=True,
)
)
else:
for site in inventory:
print(format_violation(site))
print(
f"runtime launch inventory: "
f"{len(inventory) - len(violations)} gated/{len(inventory)} total"
)
if violations:
print("ungated consequential runtime launch detected", file=sys.stderr)
return 1
return 0
if __name__ == "__main__":
raise SystemExit(main())