|
| 1 | +"""Rule groups and packs: which bundled rule/tooldef files belong to what (#1141). |
| 2 | +
|
| 3 | +Every bundled ``rules/*.yaml`` and ``tooldefs/*.yaml`` belongs to exactly one |
| 4 | +entry here. ``kind="group"`` entries are default-on ("os-focused"); a human may |
| 5 | +opt one out. ``kernel`` groups can never be opted out. ``kind="pack"`` entries |
| 6 | +are vendor/tool sets that stay off until a human enables them. |
| 7 | +""" |
| 8 | + |
| 9 | +from __future__ import annotations |
| 10 | + |
| 11 | +from dataclasses import dataclass |
| 12 | +from pathlib import Path |
| 13 | +from typing import Dict, Tuple |
| 14 | + |
| 15 | + |
| 16 | +@dataclass(frozen=True) |
| 17 | +class Entry: |
| 18 | + name: str |
| 19 | + kind: str # "group" | "pack" |
| 20 | + description: str |
| 21 | + rules: Tuple[str, ...] = () |
| 22 | + tooldefs: Tuple[str, ...] = () |
| 23 | + kernel: bool = False |
| 24 | + binaries: Tuple[str, ...] = () # vendor CLIs whose presence suggests a pack |
| 25 | + |
| 26 | + |
| 27 | +_ENTRIES = ( |
| 28 | + # --- default groups ----------------------------------------------------- |
| 29 | + Entry("control-plane", "group", "Protected damage-control/agent config paths", ("control-plane.yaml",), kernel=True), |
| 30 | + Entry("core", "group", "Filesystem, process and history destruction", ("core.yaml",), kernel=True), |
| 31 | + Entry("git", "group", "Destructive git operations + git/gh/uv tooldefs", ("git.yaml",), ("git.yaml", "gh.yaml", "uv.yaml")), |
| 32 | + Entry("agentwire", "group", "AgentWire state and session destruction", ("agentwire.yaml",)), |
| 33 | + Entry("payloads", "group", "Rules scanning payloads wrapped in other commands", ("payloads.yaml",)), |
| 34 | + Entry("remote", "group", "ssh-only remote hazards (reboot, service stop)", ("remote.yaml",)), |
| 35 | + Entry("mcp-tools", "group", "Per-tool policy for MCP tool calls", ("mcp-tools.yaml",)), |
| 36 | + Entry("outbound", "group", "Outward-facing sends (email, SMS, push)", ("outbound.yaml",)), |
| 37 | + # --- packs -------------------------------------------------------------- |
| 38 | + Entry("aws", "pack", "AWS CLI", ("aws.yaml",), ("aws.yaml",), binaries=("aws",)), |
| 39 | + Entry("gcp", "pack", "gcloud", ("gcp.yaml",), ("gcp.yaml",), binaries=("gcloud",)), |
| 40 | + Entry("firebase", "pack", "Firebase CLI", ("firebase.yaml",), binaries=("firebase",)), |
| 41 | + Entry("cloud-hosting", "pack", "Vercel, Netlify, Cloudflare, Heroku, Fly, DO, Supabase (+ gh repo delete)", ("cloud-hosting.yaml",), binaries=("vercel", "netlify", "wrangler", "heroku", "flyctl", "doctl", "supabase")), |
| 42 | + Entry("databases", "pack", "Database CLIs and SQL", ("databases.yaml",), binaries=("psql", "mysql", "mongosh", "redis-cli")), |
| 43 | + Entry("containers", "pack", "Docker and Kubernetes", ("containers.yaml",), ("docker.yaml", "kubectl.yaml"), binaries=("docker", "kubectl")), |
| 44 | + Entry("infrastructure", "pack", "Terraform, Pulumi, Serverless, SAM", ("infrastructure.yaml",), ("terraform.yaml",), binaries=("terraform", "pulumi", "serverless", "sam")), |
| 45 | + Entry("publish", "pack", "Package registry publish + npm", ("publish.yaml",), ("npm.yaml",), binaries=("npm", "twine")), |
| 46 | + Entry("gws", "pack", "Google Workspace CLI", ("gws.yaml",), ("gws.yaml",), binaries=("gws",)), |
| 47 | +) |
| 48 | + |
| 49 | +REGISTRY: Dict[str, Entry] = {e.name: e for e in _ENTRIES} |
| 50 | + |
| 51 | + |
| 52 | +def bundled_files(package_dir: Path) -> Dict[str, set]: |
| 53 | + """The ``{"rules": {...}, "tooldefs": {...}}`` file names actually bundled.""" |
| 54 | + return { |
| 55 | + "rules": {p.name for p in (package_dir / "hooks" / "damage-control" / "rules").glob("*.yaml")}, |
| 56 | + "tooldefs": {p.name for p in (package_dir / "tooldefs").glob("*.yaml")}, |
| 57 | + } |
| 58 | + |
| 59 | + |
| 60 | +def unclassified(package_dir: Path) -> Dict[str, list]: |
| 61 | + """Bundled files owned by no entry, or by more than one, per section.""" |
| 62 | + bundled = bundled_files(package_dir) |
| 63 | + out: Dict[str, list] = {} |
| 64 | + for section in ("rules", "tooldefs"): |
| 65 | + owners: Dict[str, list] = {} |
| 66 | + for e in _ENTRIES: |
| 67 | + for f in getattr(e, section): |
| 68 | + owners.setdefault(f, []).append(e.name) |
| 69 | + bad = [f"{f}: owned by {owners.get(f, [])}" for f in sorted(bundled[section] | set(owners)) |
| 70 | + if f not in bundled[section] or len(owners.get(f, [])) != 1] |
| 71 | + if bad: |
| 72 | + out[section] = bad |
| 73 | + return out |
0 commit comments