Ctrl K
ComunidadGuías…

scripts/apply-butler-env-profile.py

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

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env python3
"""Apply Butler terminal / sandbox env profile to .env (upsert known keys only)."""

from __future__ import annotations

import argparse
import re
from pathlib import Path

PROFILES: dict[str, dict[str, str]] = {
    "lead": {
        "BUTLER_ENV_PROFILE": "lead",
        "BUTLER_ENABLE_TERMINAL": "0",
        "BUTLER_TERMINAL_SANDBOX": "0",
        "BUTLER_TERMINAL_SANDBOX_FAIL_UNAVAILABLE": "0",
        "BUTLER_MEMORY_WRITE_APPROVAL": "owner_scopes",
        "BUTLER_SKILL_WRITE_APPROVAL": "1",
        "BUTLER_TOOLSET": "wechat_minimal",
        "BUTLER_TRANSCRIPT_FTS": "1",
        "BUTLER_MEMORY_OBSERVER_QUEUE": "1",
        "BUTLER_MEMORY_OBSERVATION_RECALL": "1",
        "BUTLER_MEMORY_UNIFIED_RECALL": "1",
    },
    "dev-gateway": {
        "BUTLER_ENV_PROFILE": "dev-gateway",
        "BUTLER_ENABLE_TERMINAL": "1",
        "BUTLER_TERMINAL_PROFILE": "dev",
        "BUTLER_TERMINAL_SANDBOX": "1",
        "BUTLER_TERMINAL_SANDBOX_FAIL_UNAVAILABLE": "1",
    },
    "dev-local": {
        "BUTLER_ENV_PROFILE": "dev-local",
        "BUTLER_ENABLE_TERMINAL": "1",
        "BUTLER_TERMINAL_PROFILE": "dev",
        "BUTLER_TERMINAL_SANDBOX": "0",
        "BUTLER_TERMINAL_SANDBOX_FAIL_UNAVAILABLE": "0",
        "BUTLER_MEMORY_WRITE_APPROVAL": "0",
        "BUTLER_SKILL_WRITE_APPROVAL": "0",
        "BUTLER_TOOLSET": "full",
        "BUTLER_TRANSCRIPT_FTS": "1",
    },
    "dev-remote": {
        "BUTLER_ENV_PROFILE": "dev-remote",
        "BUTLER_ENABLE_TERMINAL": "1",
        "BUTLER_TERMINAL_PROFILE": "dev",
        "BUTLER_TERMINAL_SANDBOX": "1",
        "BUTLER_TERMINAL_SANDBOX_FAIL_UNAVAILABLE": "1",
        "BUTLER_TERMINAL_SANDBOX_NETWORK_ALLOWLIST": "1",
        "BUTLER_CC_BRIDGE": "1",
        "BUTLER_TERMINAL_ALLOWLIST_EXTRA": "claude",
        "BUTLER_MEMORY_WRITE_APPROVAL": "owner_scopes",
        "BUTLER_SKILL_WRITE_APPROVAL": "1",
        "BUTLER_TOOLSET": "full",
        "BUTLER_TRANSCRIPT_FTS": "1",
    },
}

PROFILE_COMMENTS: dict[str, list[str]] = {
    "lead": [
        "# === Terminal Profile: lead(微信生产 Lead,推荐)===",
        "# terminal 关;测试走 delegate dev / runtime readonly job;无需 bubblewrap",
        "# P3-H:observation + hybrid 统一召回(需 BUTLER_SEMANTIC_MEMORY=1 效果更佳)",
    ],
    "dev-gateway": [
        "# === Terminal Profile: dev-gateway(Linux dev 网关)===",
        "# 需: sudo apt install bubblewrap · 无 bwrap 时 terminal 硬失败",
    ],
    "dev-local": [
        "# === Terminal Profile: dev-local(本机开发)===",
        "# 无 bwrap 时 honest 关 OS 沙箱;装 bwrap 后改 dev-gateway 或 dev-remote",
    ],
    "dev-remote": [
        "# === Terminal Profile: dev-remote(微信远程开发 / CC 互补)===",
        "# 需 bubblewrap;CC 桥接 BUTLER_CC_BRIDGE=1;networkPolicy.allow + NETWORK_ALLOWLIST",
    ],
}

_MANAGED_KEYS = frozenset(
    key for profile in PROFILES.values() for key in profile
)
_PROFILE_BLOCK_START = re.compile(r"^#\s*===\s*Terminal Profile:")


def _strip_profile_block(lines: list[str]) -> list[str]:
    out: list[str] = []
    skipping = False
    for line in lines:
        if _PROFILE_BLOCK_START.match(line.strip()):
            skipping = True
            continue
        if skipping:
            stripped = line.strip()
            if not stripped:
                skipping = False
                continue
            if stripped.startswith("#"):
                continue
            key = stripped.split("=", 1)[0].strip()
            if key in _MANAGED_KEYS:
                continue
            skipping = False
        if "=" in line and not line.lstrip().startswith("#"):
            key = line.split("=", 1)[0].strip()
            if key in _MANAGED_KEYS:
                continue
        out.append(line)
    while out and not out[-1].strip():
        out.pop()
    return out


def apply_profile(env_path: Path, profile: str) -> str:
    if profile not in PROFILES:
        raise SystemExit(f"Unknown profile: {profile}. Choose: {', '.join(PROFILES)}")
    if not env_path.is_file():
        raise SystemExit(f"Missing {env_path}")

    values = PROFILES[profile]
    lines = _strip_profile_block(env_path.read_text(encoding="utf-8").splitlines())
    block: list[str] = [""] + PROFILE_COMMENTS[profile]
    for key, value in values.items():
        block.append(f"{key}={value}")
    updated = "\n".join(lines + block) + "\n"
    env_path.write_text(updated, encoding="utf-8")
    return updated


def main(argv: list[str] | None = None) -> int:
    parser = argparse.ArgumentParser(description="Apply Butler terminal/sandbox env profile")
    parser.add_argument(
        "profile",
        choices=sorted(PROFILES),
        help="lead | dev-gateway | dev-local | dev-remote",
    )
    parser.add_argument("--env", type=Path, default=Path(".env"))
    parser.add_argument("--dry-run", action="store_true")
    args = parser.parse_args(argv)

    if args.dry_run:
        for key, value in PROFILES[args.profile].items():
            print(f"{key}={value}")
        return 0

    apply_profile(args.env, args.profile)
    print(f"Applied profile '{args.profile}' to {args.env.resolve()}")
    if args.profile == "dev-gateway":
        print("Next: sudo apt-get install -y bubblewrap && restart gateway")
    elif args.profile == "dev-remote":
        print("Next: apt install bubblewrap; ensure `claude` on PATH; restart gateway")
    elif args.profile == "dev-local":
        print("Honest mode: OS sandbox off until bubblewrap is installed")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())