Ctrl K
ComunidadGuías…

lockdown.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""
Company-wide lockdown: the policy files IT pushes to every developer machine (MDM, Intune, Jamf, Ansible...)
so each coding agent runs Squidbrake's hook and a developer can't switch it off.

  squidbrake lockdown --url https://gateway.yourcompany.com --out squidbrake-lockdown

writes one folder per agent, each in that vendor's own managed-settings format, plus a README saying where each
file goes on macOS, Linux and Windows. One policy (rules.yaml on the gateway) then decides for every agent:
the hooks only ask the gateway. What each vendor file adds on top:

  claude-code   managed-settings.json: Squidbrake's hooks, only managed hooks run, no --dangerously-skip-permissions,
                and a short native deny list as a backstop
  codex         requirements.toml: Squidbrake's managed hook, only managed hooks run, hooks forced on, no
                danger-full-access sandbox (no --yolo)
  gemini-cli    settings.json (system): Squidbrake's BeforeTool hook, YOLO mode off; policies/squidbrake.toml: the
                same backstop as an admin-tier policy
  cursor        hooks.json (enterprise): Squidbrake's hooks with failClosed

Every machine also needs Squidbrake installed (`pipx install squidbrake`) and its own agent key in the
GATEWAY_API_KEY environment variable (made in the dashboard's Team tab with "Works for" set to that person).
Keys are never written into these files: one file goes to every machine, and a key in it would be everyone's.

Formats follow each vendor's managed-settings documentation as of October 2026; check `squidbrake lockdown`'s README
against your agent versions before a fleet-wide rollout.
"""
from __future__ import annotations

import json
from pathlib import Path

AGENTS = ("claude-code", "codex", "gemini-cli", "cursor")

# Catastrophic commands blocked by the agent itself as well, so they stay blocked even if the hook process
# can't start (Claude Code and Codex let a call through when a command hook crashes or times out).
# Kept short and exact on purpose: in Claude Code's rules `*` is a wildcard and Gemini's commandPrefix matches
# prefixes, so `rm -rf /*` or a prefix `rm -rf /` there would also block `rm -rf /tmp/build`.
# Everything else is judged by the gateway, with your rules and history.
CLAUDE_DENY = ["rm -rf /", "rm -rf ~", "rm -rf ~/", "rm -rf $HOME", "sudo rm -rf /"]
GEMINI_DENY_REGEX = [r"^\s*(sudo\s+)?rm\s+-(rf|fr)\s+(/|/\*|~|~/|~/\*|\$HOME)\s*$",
                     r"^\s*(sudo\s+)?mkfs(\.\w+)?\s",
                     r"^\s*(sudo\s+)?dd\s.*\bof=/dev/(sd|nvme|disk|hd)"]

WHERE = {
    "claude-code": {"macOS": "/Library/Application Support/ClaudeCode/managed-settings.json",
                    "Linux / WSL": "/etc/claude-code/managed-settings.json",
                    "Windows": r"C:\Program Files\ClaudeCode\managed-settings.json",
                    "MDM instead": "macOS: plist domain com.anthropic.claudecode; Windows: REG_SZ value 'Settings' under "
                                   r"HKLM\SOFTWARE\Policies\ClaudeCode (the JSON as its value)"},
    "codex": {"macOS / Linux": "/etc/codex/requirements.toml",
              "Windows": r"%ProgramData%\OpenAI\Codex\requirements.toml",
              "also": "create the hooks folder it names (/etc/squidbrake/codex-hooks, or "
                      r"C:\ProgramData\squidbrake\codex-hooks); Codex checks that it exists"},
    "gemini-cli": {"macOS": "/Library/Application Support/GeminiCli/settings.json and .../GeminiCli/policies/squidbrake.toml",
                   "Linux": "/etc/gemini-cli/settings.json and /etc/gemini-cli/policies/squidbrake.toml",
                   "Windows": r"C:\ProgramData\gemini-cli\settings.json and C:\ProgramData\gemini-cli\policies\squidbrake.toml",
                   "also": "the policies folder must be owned by root (Windows: not writable by standard users)"},
    "cursor": {"macOS": "/Library/Application Support/Cursor/hooks.json", "Linux / WSL": "/etc/cursor/hooks.json",
               "Windows": r"C:\ProgramData\Cursor\hooks.json"},
}


def _hook_args(url: str, *extra: str) -> list[str]:
    return [*extra, "--url", url]


def _cmd(url: str, *extra: str) -> str:
    return " ".join(["squidbrake", *_hook_args(url, *extra)])


def claude_code(url: str) -> dict:
    hook = {"type": "command", "command": "squidbrake", "args": _hook_args(url, "hook", "--source", "claude-code")}
    return {
        "hooks": {
            "PreToolUse": [{"matcher": "*", "hooks": [{**hook, "timeout": 600}]}],
            "PostToolUse": [{"matcher": "*", "hooks": [{**hook, "timeout": 30}]}],
            "PostToolUseFailure": [{"matcher": "*", "hooks": [{**hook, "timeout": 30}]}],
            "UserPromptSubmit": [{"hooks": [{**hook, "timeout": 15}]}],
        },
        "allowManagedHooksOnly": True,
        "permissions": {
            "disableBypassPermissionsMode": "disable",
            "deny": [f"Bash({c})" for c in CLAUDE_DENY],
        },
    }


def codex(url: str) -> str:
    cmd = _cmd(url, "agent-hook", "codex")
    return f'''# Squidbrake lockdown for Codex: install as requirements.toml (see README.md)
allowed_sandbox_modes = ["read-only", "workspace-write"]   # no danger-full-access, so no --yolo
allow_managed_hooks_only = true

[features]
hooks = true   # managed hooks run even if a user turns hooks off

[hooks]
managed_dir = "/etc/squidbrake/codex-hooks"
windows_managed_dir = 'C:\\ProgramData\\squidbrake\\codex-hooks'

[[hooks.PreToolUse]]
matcher = "Bash|shell|apply_patch|Edit|Write"

[[hooks.PreToolUse.hooks]]
type = "command"
command = "{cmd}"
command_windows = "{cmd}"
timeout = 600
statusMessage = "Squidbrake: checking"
'''


def gemini_settings(url: str) -> dict:
    return {
        "hooks": {"BeforeTool": [{"matcher": "run_shell_command|write_file|replace|read_file|read_many_files",
                                  "hooks": [{"name": "squidbrake", "type": "command",
                                             "command": _cmd(url, "agent-hook", "gemini-cli"), "timeout": 600000}]}]},
        "security": {"disableYoloMode": True},
    }


def gemini_policy() -> str:
    rules = "\n".join(f'''[[rule]]
toolName = "run_shell_command"
commandRegex = {json.dumps(c)}
decision = "deny"
priority = 900
denyMessage = "Blocked by company policy (Squidbrake backstop)"
''' for c in GEMINI_DENY_REGEX)
    return "# Squidbrake backstop, admin tier: these stay blocked even if the hook can't run\n\n" + rules


def cursor(url: str) -> dict:
    cmd = _cmd(url, "agent-hook", "cursor")
    entry = {"command": cmd, "type": "command", "timeout": 600, "matcher": "", "failClosed": True}
    return {"version": 1, "hooks": {"beforeShellExecution": [entry], "beforeReadFile": [entry]}}


def readme(url: str, agents: list[str]) -> str:
    lines = [
        "# Squidbrake lockdown", "",
        f"These files make every developer machine send its coding agents' actions to {url} and stop a",
        "developer from switching that off. Push them with your device management (Jamf, Intune, Kandji, Ansible...).", "",
        "On every machine:", "",
        "1. Install Squidbrake: `pipx install squidbrake` (the `squidbrake` command must be on PATH for the agent).",
        "2. Set `GATEWAY_API_KEY` to that person's own agent key (dashboard -> Team -> Add -> AI agent, \"Works for\" = them).",
        "   One key per person means the dashboard shows whose agent did what, and second-person approval works.",
        "3. Copy each file below to its place. Restart the agents.", "",
        "Check a machine with `squidbrake connect status`.", "",
    ]
    for a in agents:
        lines += [f"## {a}", ""] + [f"- **{k}:** `{v}`" if k not in ("also", "MDM instead") else f"- {k}: {v}"
                                    for k, v in WHERE[a].items()] + [""]
    lines += [
        "## What to know", "",
        "- The hook asks the gateway; if the gateway can't be reached, the hook blocks the action (fail closed).",
        "- If the hook itself can't start (Squidbrake not installed, PATH wrong), Claude Code and Codex let the call",
        "  through. The short native deny list in these files still blocks the catastrophic commands in that case.",
        "  Cursor's hooks use failClosed, so they block.",
        "- Codex runs a new hook only after it's trusted; managed hooks from requirements.toml are trusted by policy.",
        "- Vendors change these formats. Test on one machine per agent before a fleet-wide rollout.", "",
    ]
    return "\n".join(lines)


def write(url: str, out: Path, agents: list[str] | None = None) -> list[Path]:
    """Write the lockdown files under out/. Returns the files written."""
    url = url.rstrip("/")
    if not url.startswith(("http://", "https://")):
        raise ValueError("the gateway URL must start with http:// or https://")
    agents = [a for a in (agents or AGENTS)]
    unknown = [a for a in agents if a not in AGENTS]
    if unknown:
        raise ValueError(f"no lockdown for {', '.join(unknown)}; supported: {', '.join(AGENTS)}")
    files: dict[str, str] = {}
    if "claude-code" in agents:
        files["claude-code/managed-settings.json"] = json.dumps(claude_code(url), indent=2) + "\n"
    if "codex" in agents:
        files["codex/requirements.toml"] = codex(url)
    if "gemini-cli" in agents:
        files["gemini-cli/settings.json"] = json.dumps(gemini_settings(url), indent=2) + "\n"
        files["gemini-cli/policies/squidbrake.toml"] = gemini_policy()
    if "cursor" in agents:
        files["cursor/hooks.json"] = json.dumps(cursor(url), indent=2) + "\n"
    files["README.md"] = readme(url, agents)
    written = []
    for rel, body in files.items():
        p = out / rel
        p.parent.mkdir(parents=True, exist_ok=True)
        p.write_text(body, encoding="utf-8")
        written.append(p)
    return written