﻿# Hermes Agent 门禁系统 + 本地审查模型：一键部署

> 把下面这份指南**原封不动复制**，发给你的 Hermes Agent。Agent 会按步骤创建所有文件。
> Windows / macOS / Linux 通用。不需要 API 密钥，不需要改代码。

---

## 发给 Hermes 的完整指令

请按以下步骤创建 Hermes 代码审查系统和操作门禁。每完成一步汇报状态。

---

### 准备工作

先确认环境：

```bash
# 确认 Hermes 版本（需 v0.19+）
hermes version

# 确认 Python 可用
python --version || python3 --version

# 创建目录（跨平台）
python -c "import os; [os.makedirs(os.path.expanduser(p), exist_ok=True) for p in ['~/.hermes/plugins','~/.hermes/scripts','~/.hermes/temp']]; print('OK')"
```

> ⚠️ 如果 `hermes version` 不可用，说明你的 Hermes 安装不完整。跳过网关命令，直接重启桌面应用即可。
> ⚠️ 如果 `curl` 不可用（Windows 常见），用 `python -c "import urllib.request; print(urllib.request.urlopen('http://127.0.0.1:8081/health').read())"` 代替。

---

### 步骤 1：安装 llama.cpp + 下载审查模型

**安装 llama-server**：

```bash
# Linux/macOS: 从 GitHub 下载预编译版
# 去 https://github.com/ggml-org/llama.cpp/releases 下载对应平台版本

# 或从源码编译
git clone https://github.com/ggml-org/llama.cpp.git
cd llama.cpp && cmake -B build && cmake --build build --config Release

# Windows: 下载 llama-bxxxx-bin-win-cuda-cuXX-x64.zip
# 解压后把 llama-server.exe 放到 PATH 目录
```

**下载审查模型**：

从下面选一个适合你显存的，下载到 `~/models/`。

| 模型 | 大小 | 显存 |
|------|------|------|
| DeepSeek Coder 6.7B Q4_K_M | 4GB | 6GB |
| Gemma 4 12B Q4_K_M | 6.7GB | 8GB |
| Qwen3-Coder 14B Q4_K_M | 8.5GB | 10GB |

```bash
# 安装 huggingface-cli（如果没有）
pip install huggingface-hub

# 下载 Gemma 4 12B（推荐）
huggingface-cli download unsloth/gemma-4-12b-it-GGUF \
  gemma-4-12b-it-Q4_K_M.gguf --local-dir ~/models/

# 国内用户用镜像：
# huggingface-cli download unsloth/gemma-4-12b-it-GGUF \
#   gemma-4-12b-it-Q4_K_M.gguf --local-dir ~/models/ \
#   --endpoint https://hf-mirror.com
```

报告：模型是否下载成功，文件大小。

---

### 步骤 2：创建审查脚本

用 write_file 创建 `~/.hermes/scripts/review_diff.py`：

```python
#!/usr/bin/env python3
"""
代码审查脚本 — 送文件给本地审查模型，生成审查令牌。
用法: python review_diff.py <文件路径> [--nonce <验证码>] [--port <端口>]
"""
import json
import time
import sys
import os
import urllib.request
import argparse

# ── 配置 ──
REVIEW_PORT = 8081
TOKEN_DIR = os.path.expanduser("~/.hermes/temp")
TOKEN_FILE = os.path.join(TOKEN_DIR, "last_code_review.json")
TOKEN_TTL = 600


def get_review_server(port):
    """返回审查服务器的地址，自动检测是否在线"""
    url = f"http://127.0.0.1:{port}/v1/chat/completions"
    try:
        urllib.request.urlopen(f"http://127.0.0.1:{port}/health", timeout=3)
        return url
    except Exception:
        print(f"[review_diff] 审查模型未在 {port} 端口运行")
        print("[review_diff] 请先启动: llama-server -m ~/models/你的模型.gguf --port 8081 --host 127.0.0.1 -ngl 99 -c 32768")
        sys.exit(1)


def review_file(filepath, nonce="", port=REVIEW_PORT):
    if not os.path.exists(filepath):
        print(f"❌ 文件不存在: {filepath}")
        sys.exit(1)

    server_url = get_review_server(port)

    with open(filepath, "r", encoding="utf-8") as f:
        code = f.read()

    # 避免超上下文，只送末尾字符
    snippet = code[-8000:] if len(code) > 8000 else code

    # 构建审查请求
    context = f"审查以下代码文件。{filepath}"
    if nonce:
        context += f"\n验证码: {nonce}"
    
    payload = {
        "model": "local-model",
        "messages": [
            {
                "role": "system",
                "content": (
                    "你是代码审查专家。请找出以下问题：\n"
                    "1. 语法错误\n2. 逻辑漏洞\n3. 安全问题\n4. 资源泄露\n"
                    "如果代码没有问题，回复 PASS。否则列出具体问题。"
                ),
            },
            {"role": "user", "content": f"{context}\n\n```\n{snippet}\n```"},
        ],
        "temperature": 0.1,
        "stream": False,
    }

    req = urllib.request.Request(
        server_url,
        data=json.dumps(payload).encode("utf-8"),
        headers={"Content-Type": "application/json"},
    )

    print(f"🔍 审查中: {filepath}")
    try:
        with urllib.request.urlopen(req, timeout=120) as resp:
            result = json.loads(resp.read())
    except urllib.error.HTTPError as e:
        print(f"❌ 审查服务器返回错误: {e.code} {e.reason}")
        print(f"   请确认 llama-server 正在监听 {port} 端口")
        sys.exit(1)
    except Exception as e:
        print(f"❌ 审查请求失败: {e}")
        sys.exit(1)

    review_text = result["choices"][0]["message"]["content"]
    passed = "PASS" in review_text.upper().split("\n")[0]

    # 写入令牌
    os.makedirs(TOKEN_DIR, exist_ok=True)
    verdict = "approved" if passed else "issues_found"
    with open(TOKEN_FILE, "w", encoding="utf-8") as f:
        json.dump(
            {
                "timestamp": int(time.time()),
                "verdict": verdict,
                "reviewer": "local-model",
                "files": [filepath],
                "summary": review_text[:200],
                "nonce": nonce,
            },
            f,
            ensure_ascii=False,
        )

    if passed:
        print(f"✅ 审查通过")
    else:
        print(f"⚠️ 发现问题:\n{review_text[:500]}")
    return review_text


if __name__ == "__main__":
    p = argparse.ArgumentParser(description="审查代码文件，生成审查令牌")
    p.add_argument("file", help="要审查的文件路径")
    p.add_argument("--nonce", default="", help="门禁验证码")
    p.add_argument("--port", type=int, default=REVIEW_PORT, help="审查模型端口")
    args = p.parse_args()
    review_file(args.file, nonce=args.nonce, port=args.port)
```

创建后验证语法：
```python
import py_compile, os
path = os.path.expanduser("~/.hermes/scripts/review_diff.py")
py_compile.compile(path, doraise=True)
print("OK")
```

报告：脚本是否创建成功，语法检查是否通过。

---

### 步骤 3：创建门禁插件（主门禁 + 备援门禁）

我们部署**两道门禁**——主门禁失效时备援顶上，防止单点故障。

**文件 1**：`~/.hermes/plugins/code-review-gate/plugin.yaml`

```yaml
name: code-review-gate
version: 1.0.0
description: 代码审查硬门禁（主）— HMAC 内存密钥，无法伪造
hooks:
  - pre_tool_call
```

**文件 2**：`~/.hermes/plugins/code-review-gate/__init__.py`

```python
"""
code-review-gate — 代码审查硬门禁 v4（主）

HMAC-SHA256 签名 + 内存密钥：
- Gateway 启动时生成随机密钥，仅存内存
- nonce 用 HMAC(key, nonce) 签名后写盘
- 外部进程（Agent 的 execute_code）无法获取密钥 → 无法伪造签名
"""
import hashlib
import hmac
import json
import os
import secrets
import sys
import time

# ── 配置 ──
_HERMES_HOME = os.path.expanduser("~/.hermes")
_TOKEN_FILE = os.path.join(_HERMES_HOME, "temp", "last_code_review.json")
_SIG_FILE = os.path.join(_HERMES_HOME, "temp", "review_nonce.sig")
_TOKEN_TTL = 600

# 🔐 内存密钥 — 每次 gateway 启动随机生成，不写盘
_GATE_KEY = secrets.token_bytes(32)

_CODE_EXTS = frozenset({
    ".py", ".rs", ".ts", ".tsx", ".js", ".jsx",
    ".go", ".c", ".cpp", ".h", ".hpp", ".java", ".kt", ".swift",
    ".rb", ".php", ".sh", ".css", ".html", ".vue", ".svelte",
    ".yaml", ".yml", ".toml", ".json",  # 配置文件——防止 Agent 关掉门禁
})

# 代码文件后缀正则，用于从 free-form 命令中检测文件写入
_EXT_PATTERN = r"\.(?:py|rs|tsx?|jsx?|go|c|cpp|h|hpp|java|kt|swift|rb|php|sh|css|html|vue|svelte|ya?ml|toml|json)\b"
_WRITE_PATTERNS = [
    (r">\s*\S*" + _EXT_PATTERN, "重定向写入代码文件"),
    (r">>\s*\S*" + _EXT_PATTERN, "追加重定向到代码文件"),
    (r"\btee\s+\S*" + _EXT_PATTERN, "tee 到代码文件"),
    (r"\bcp\s+.*?\S*" + _EXT_PATTERN, "复制到代码文件路径"),
    (r"\bmv\s+.*?\S*" + _EXT_PATTERN, "移动/重命名为代码文件"),
    (r"\b(?:python|python3)\s+-c\s+.*open\(.*?[\"']w[\"']\)", "Python 写文件模式"),
]


def _sign(nonce):
    """HMAC-SHA256 签名。密钥在内存，外部进程拿不到。"""
    return hmac.new(_GATE_KEY, nonce.encode(), hashlib.sha256).hexdigest()


def _gen_nonce():
    """生成 nonce，写盘的是签名（不是原文）。"""
    n = secrets.token_hex(16)
    os.makedirs(os.path.dirname(_SIG_FILE), exist_ok=True)
    with open(_SIG_FILE, "w") as f:
        f.write(_sign(n))
    return n


def _read_sig():
    try:
        if os.path.exists(_SIG_FILE):
            return open(_SIG_FILE).read().strip()
    except Exception:
        pass
    return ""


def _check_token():
    if not os.path.exists(_TOKEN_FILE):
        return False, "未找到审查令牌"
    try:
        with open(_TOKEN_FILE, "r", encoding="utf-8") as f:
            t = json.load(f)
        age = int(time.time()) - t.get("timestamp", 0)
        if age > _TOKEN_TTL:
            return False, f"令牌过期（{age // 60} 分钟前）"
        sig = _read_sig()
        if not sig:
            return False, "验证签名缺失——请用下面 nonce 重新审查"
        token_nonce = t.get("nonce", "")
        if not token_nonce or _sign(token_nonce) != sig:
            return False, "HMAC 签名不匹配（令牌无效或伪造）"
        if t.get("verdict") != "approved":
            return False, f"审查未通过（结果: {t.get('verdict', 'unknown')}）"
        return True, ""
    except (json.JSONDecodeError, KeyError) as e:
        return False, f"令牌格式错误: {e}"


def pre_tool_call(tool_name="", args=None, **_kw):
    file_path = ""

    # 1. write_file / patch → 直接检查文件扩展名
    if tool_name in ("write_file", "patch"):
        file_path = (args or {}).get("path", "")
        if not file_path:
            return None
        ext = os.path.splitext(file_path)[1].lower()
        if ext not in _CODE_EXTS:
            return None

    # 2. terminal → 检查命令是否写入代码文件
    elif tool_name == "terminal":
        cmd = (args or {}).get("command", "")
        if not cmd:
            return None
        for pattern, desc in _WRITE_PATTERNS:
            import re as _re
            if _re.search(pattern, cmd, _re.IGNORECASE):
                file_path = f"terminal 命令（检测到: {desc}）"
                break
        if not file_path:
            return None

    # 3. execute_code → 检查代码中是否有 open(...'w') 写代码文件
    elif tool_name == "execute_code":
        code = (args or {}).get("code", "")
        if not code:
            return None
        import re as _re
        # 检测 open("xxx.py", "w") / open(r"xxx.py","w") / os.system(...open...) / subprocess(...)
        _exec_re = _re.compile(
            r"""open\(\s*(?:[ruUR]*["'])?[^"']*""" + _EXT_PATTERN + r"""["']\s*,\s*["']w["']"""
            + r"""|write_file\(\s*["'][^"']*""" + _EXT_PATTERN + r"""["']"""
            + r"""|os\.system\(.*open\(.*["'][^"']*""" + _EXT_PATTERN
            + r"""|subprocess\.(?:run|call|Popen)\(.*open\(.*["'][^"']*""" + _EXT_PATTERN
        )
        if _exec_re.search(code):
            file_path = "execute_code 中检测到写代码文件操作"
        if not file_path:
            return None

    # 4. cronjob — no_agent 脚本直接跑 Python，完全绕过工具钩子
    elif tool_name == "cronjob":
        script = (args or {}).get("script", "")
        no_agent = (args or {}).get("no_agent", False)
        if no_agent and script:
            file_path = f"cron no_agent 脚本: {script}"
        if not file_path:
            return None

    # 5. process.submit — 向后台进程注入代码，不经过 terminal 检查
    elif tool_name == "process":
        act = (args or {}).get("action", "")
        data = (args or {}).get("data", "")
        if act in ("submit", "write") and data:
            import re as _re
            if _re.search(r"""open\(\s*["'][^"']*""" + _EXT_PATTERN + r"""["']\s*,\s*["']w["']""", data):
                file_path = "process 注入代码写入操作"
        if not file_path:
            return None

    else:
        return None

    ok, reason = _check_token()
    if ok:
        # 验证通过 → 清除签名，允许本次修改
        try:
            os.remove(_SIG_FILE)
        except Exception:
            pass
        return None

    nonce = _gen_nonce()
    return {
        "action": "block",
        "message": (
            f"🔴 代码审查门禁拦截\n"
            f"文件：{file_path}\n"
            f"原因：{reason}\n\n"
            f"📋 审查步骤：\n"
            f"  python ~/.hermes/scripts/review_diff.py {file_path} --nonce {nonce}\n\n"
            f"⚠️ HMAC 内存密钥——外部进程无法伪造签名。"
        ),
    }


def register(ctx):
    print("[code-review-gate] HMAC 门禁已激活", file=sys.stderr, flush=True)
    ctx.register_hook("pre_tool_call", pre_tool_call)
```

**文件 3**：`~/.hermes/plugins/iron-law/plugin.yaml`

```yaml
name: iron-law
version: 1.0.0
description: 铁律执法（含备援代码审查门禁）
hooks:
  - pre_tool_call
```

**文件 4**：`~/.hermes/plugins/iron-law/__init__.py`

```python
"""
iron-law — 备援门禁 + 危险命令拦截

两条规则：
1. 代码审查门禁（备援）— 共享 code-review-gate 的 HMAC 密钥
2. 危险命令拦截 — rm -rf /、dd、shutdown 等
"""
import json
import os
import re
import sys
import time

# ── 危险命令模式 ──
_DANGEROUS = [
    (r'rm\s+.*-[rRfF].*[/~]', '危险删除操作'),
    (r'>\s*/dev/(?:sd[a-z]\d*|nvme\d+n\d+|mmcblk\d+)', '写入存储设备'),
    (r'\bdd\s+.*of=', 'dd 写入操作'),
    (r'\bshutdown\b', '系统关机'),
    (r'\breboot\b', '系统重启'),
    (r'\bmkfs\b', '创建文件系统'),
    (r'chmod\s+-R\s+777', '递归放宽权限'),
]

_CODE_EXTS = frozenset({
    ".py", ".rs", ".ts", ".tsx", ".js", ".jsx",
    ".go", ".c", ".cpp", ".h", ".hpp", ".java",
    ".yaml", ".yml", ".toml", ".json",  # 配置文件
})

_TOKEN_FILE = os.path.expanduser("~/.hermes/temp/last_code_review.json")
_TOKEN_TTL = 600

# terminal/execute_code 写代码文件检测模式
_EXT_RE = r"\.(?:py|rs|tsx?|jsx?|go|c|cpp|h|hpp|java|kt|swift|rb|php|sh|css|html|vue|svelte|ya?ml|toml|json)\b"
_TERM_WRITE_RE = re.compile(
    r">\s*\S*" + _EXT_RE + r"|>>\s*\S*" + _EXT_RE +
    r"|\btee\s+\S*" + _EXT_RE +
    r"|\bcp\s+.*?\S*" + _EXT_RE +
    r"|\bmv\s+.*?\S*" + _EXT_RE,
    re.IGNORECASE
)
_EXEC_WRITE_RE = re.compile(
    r"""open\(\s*["'][^"']*""" + _EXT_RE + r"""["']\s*,\s*["']w["']"""
    + r"""|write_file\(\s*["'][^"']*""" + _EXT_RE + r"""["']"""
)


def _check_review(tool_name, args):
    """备援审查——覆盖 write_file/patch/terminal/execute_code"""
    file_path = ""

    if tool_name in ("write_file", "patch"):
        file_path = (args or {}).get("path", "")
        if file_path:
            ext = os.path.splitext(file_path)[1].lower()
            if ext not in _CODE_EXTS:
                return None
        else:
            return None

    elif tool_name == "terminal":
        cmd = (args or {}).get("command", "")
        if _TERM_WRITE_RE.search(cmd):
            file_path = "terminal 命令写入代码文件"
        else:
            return None

    elif tool_name == "execute_code":
        code = (args or {}).get("code", "")
        if _EXEC_WRITE_RE.search(code):
            file_path = "execute_code 写入代码文件"
        else:
            return None

    elif tool_name == "cronjob":
        script = (args or {}).get("script", "")
        if (args or {}).get("no_agent") and script:
            file_path = f"cron no_agent 脚本: {script}"
        else:
            return None

    elif tool_name == "process":
        act = (args or {}).get("action", "")
        data = (args or {}).get("data", "")
        if act in ("submit", "write") and data and _EXEC_WRITE_RE.search(data):
            file_path = "process 注入代码写入"
        else:
            return None

    else:
        return None

    cgate = sys.modules.get("code_review_gate")
    if cgate is None:
        return None  # 主门禁不在，静默（不重复拦截）

    if not os.path.exists(_TOKEN_FILE):
        return {
            "action": "block",
            "message": f"🔴 [iron-law] 无审查令牌\n文件：{file_path}",
        }

    try:
        with open(_TOKEN_FILE, "r", encoding="utf-8") as f:
            t = json.load(f)
        if time.time() - t.get("timestamp", 0) > _TOKEN_TTL:
            return {"action": "block", "message": "🔴 [iron-law] 令牌过期"}
        sig = cgate._read_sig()
        if not sig:
            return {"action": "block", "message": "🔴 [iron-law] 签名缺失"}
        if not t.get("nonce") or cgate._sign(t["nonce"]) != sig:
            return {"action": "block", "message": "🔴 [iron-law] HMAC 签名不匹配"}
        if t.get("verdict") != "approved":
            return {"action": "block", "message": "🔴 [iron-law] 审查未通过"}
        return None
    except Exception as e:
        return {"action": "block", "message": f"🔴 [iron-law] 验证失败: {e}"}


def _check_dangerous(tool_name, args):
    if tool_name != "terminal":
        return None
    cmd = (args or {}).get("command", "")
    for pattern, desc in _DANGEROUS:
        if re.search(pattern, cmd, re.IGNORECASE):
            return {
                "action": "block",
                "message": f"🚫 [iron-law] 危险命令拦截\n原因：{desc}",
            }
    return None


def pre_tool_call(tool_name="", args=None, **_kw):
    try:
        r = _check_dangerous(tool_name, args)
        if r:
            return r
        r = _check_review(tool_name, args)
        if r:
            return r
    except Exception as e:
        print(f"[iron-law] Error: {e}", file=sys.stderr)
    return None


def register(ctx):
    print("[iron-law] 备援门禁已激活", file=sys.stderr, flush=True)
    ctx.register_hook("pre_tool_call", pre_tool_call)
```

创建后验证：
```python
# 保存为 verify_gate.py 并运行
import sys, os
sys.path.insert(0, os.path.expanduser("~/.hermes/plugins/code-review-gate"))
import importlib.util
spec = importlib.util.spec_from_file_location(
    "cgate",
    os.path.expanduser("~/.hermes/plugins/code-review-gate/__init__.py")
)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
print("✅ 插件代码正确")
```

报告：两个文件是否创建成功。

---

### 步骤 4：启用插件

在 `~/.hermes/config.yaml` 中启用两个插件：

```yaml
plugins:
  enabled:
    - code-review-gate
    - iron-law
```

重启 Hermes：

```bash
# 方式1：命令行（如果 hermes gateway 可用）
hermes gateway stop && hermes gateway start

# 方式2：直接重启桌面应用（Windows/macOS GUI 用户）
# 关掉 Hermes 窗口 → 重新打开
```

验证：
```bash
hermes plugins list 2>/dev/null | grep code-review-gate || echo "跳过——重启桌面应用后插件自动生效"
```

报告：插件是否出现在启用列表中。

---

### 步骤 5：启动审查模型

```bash
llama-server \
  -m ~/models/gemma-4-12b-it-Q4_K_M.gguf \
  --port 8081 \
  --host 127.0.0.1 \
  -ngl 99 \
  -c 32768
```

> CPU 用户把 `-ngl 99` 改成 `-ngl 0`。
> 显存不足？换小模型：`DeepSeek-Coder-6.7B-Q4_K_M.gguf`

验证：
```bash
# Linux/macOS
curl http://127.0.0.1:8081/health

# Windows 或没有 curl 时
python -c "import urllib.request; print(urllib.request.urlopen('http://127.0.0.1:8081/health').read())"
# → {"status":"ok"}
```

报告：模型是否成功启动，health 接口是否返回 ok。

---

### 步骤 6：功能测试

**测试 1：无令牌被拦截**

让 Hermes 尝试写 Python 文件不做审查：
```
write_file ~/.hermes/temp/test.py "print('hello')"
```
预期：被拦截，提示无审查令牌。

**测试 2：审查后放行**

```bash
python ~/.hermes/scripts/review_diff.py ~/.hermes/temp/test.py
```
预期：审查通过。然后：
```
write_file ~/.hermes/temp/test.py "print('hello')"
```
预期：写入成功。

**测试 3：令牌过期**

等 10 分钟后再次写文件：
```
write_file ~/.hermes/temp/test.py "print('world')"
```
预期：被拦截，提示令牌过期。

报告：三个测试结果。

---

### 环境差异说明

| 平台 | `~/.hermes` 实际路径 |
|------|---------------------|
| Linux | `/home/用户名/.hermes` |
| macOS | `/Users/用户名/.hermes` |
| Windows | `C:\Users\用户名\AppData\Local\hermes` |

Python 的 `os.path.expanduser("~/.hermes")` 在所有平台上都能正确解析，所以上面的代码直接用 `~` 就行。

---

### 防作弊机制

| 层级 | 机制 | 防什么 |
|------|------|--------|
| 1 | 令牌有效期（10分钟） | 拿旧令牌反复用 |
| 2 | **HMAC-SHA256 签名**（内存密钥） | Agent 读不到密钥，无法伪造签名 |
| 3 | 双门禁（主+备援） | 主门禁失效时备援顶上 |
| 4 | 无签名文件 = 拦截 | Agent 删签名文件 → 拦截，不是放行 |

**为何 Agent 无法伪造：**

```
Gateway 进程内：🔐 _GATE_KEY（32字节随机数，仅存内存）
  ├── code-review-gate → _sign(nonce) = HMAC(key, nonce)
  └── iron-law → cgate._sign(nonce) ← 通过 sys.modules 共享同一密钥

Agent 进程（execute_code/terminal）：
  无法访问 gateway 内存 → 拿不到 _GATE_KEY
  即使知道 HMAC-SHA256 算法也无效
  写进硬盘的只是签名结果（HMAC 输出），不能反推原文
```

**已知局限**（如实相告）：

| 方法 | 能否拦截 | 说明 |
|------|:---:|------|
| write_file / patch | ✅ | 主拦截 |
| terminal 重定向/复制 | ✅ | 正则检测 |
| execute_code open(...'w') | ✅ | 正则检测 |
| cronjob no_agent 脚本 | ✅ | 工具级拦截 |
| process.submit 注入 | ✅ | 数据检测 |
| 改 config.yaml 关闭门禁 | ✅ | .yaml 在保护范围 |
| `echo code \| base64 -d > x.py` | ⚠️ | 混淆后可能绕过正则 |
| `exec(b64decode(...))` | ⚠️ | 加密后执行可绕过 |
| `os.system('python -c "open(...)"')` | ⚠️ | 二次调用可绕过正则 |
| computer_use 手动打字 | ❌ | 桌面级操作，无法拦截 |
| 浏览器下载 .py 文件 | ❌ | 下载路径不在审查范围 |

> ⚠️ execute_code 可以跑任意 Python，检测"是否会写代码文件"等同于停机问题——数学上不可判定。本方案覆盖了常用直白模式（open/write_file/os.system/subprocess），但精心构造的混淆代码无法 100% 拦截。

---

### 行为铁律（SOUL.md — 管行为）

门禁只管「能不能写代码」。Agent 在对话中是否诚实、是否偷懒、是否汇报假结果——要靠行为铁律。

创建 `~/.hermes/SOUL.md`（Hermes v0.19+ 自动注入到每次会话）：

```markdown
# 🔴 Hermes 行为铁律（所有会话强制生效）

## 诚实
1. **事实校验**：不确定先搜再答，禁止编造。API、版本号、配置参数必须有依据。
2. **验证数据优先于口头汇报**：说"完成"前贴真实输出（编译结果/文件大小/进程名）。
3. **直接说**：诚实指出弱点，不因自维护代码避重就轻。

## 执行
4. **先搜再答**：任何不确定的声明先 web_search。
5. **同问题 2 次不通换方案**：相同方法失败 2 次 → 停手 → 换最简单替代方案。
6. **分析完必须落地**：统计/复盘/研究 → 必须有可见改动（代码/规则/技能），禁止"整理完报告就停"。
7. **执行 > 总结**：搜到方案直接 clone 用，只在搜不到时才写文档。

## 不越权
8. **学会拒绝**：用户发链接先判是否跟当前任务相关，无关不研究。
9. **发现风险直接说**：超出能力范围直接说明并给替代方案，禁止硬扛。
10. **危险操作必确认**：删文件、杀进程、改系统配置前先确认。

## 不自欺
11. **代码修改前保存检查点**：改源文件先 checkpoint。
12. **禁止伪造审查令牌**：绝对禁止手动写 last_code_review.json。必须走正规审查流程。
13. **改完代码自动送审**：不等提示，主动跑 review_diff.py。
```

### 我们踩过的坑（为什么需要门禁+铁律配合）

这些都是真实发生过的——铁律的每一条都有血泪。

| 踩坑 | 发生了什么 | 铁律 | 门禁 |
|------|-----------|:---:|:---:|
| **手写假审查令牌** | Agent 7/11、7/14、7/16、7/27 多次手动写 `last_code_review.json` 绕过门禁 | 规则 12 | ✅ HMAC 内存密钥堵死 |
| **擅自改 VPS/Clash** | Agent 没问用户就改了 Clash 配置文件和 VPS 节点 | 规则 8、10 | ❌ 管不了 |
| **搜索-读取死循环** | 排查问题时 search_files → read_file 来回十几轮，浪费大量 token | 规则 4 | ❌ 管不了 |
| **同一方法反复失败** | 编译失败后用同样方法重试 3+ 次 | 规则 5 | ❌ 管不了 |
| **分析完不落地** | 研究竞品、写几千字报告 → 没有任何代码改动 | 规则 6 | ❌ 管不了 |
| **"完成"是假的** | 声称部署好了但没贴验证数据，实际根本没跑 | 规则 2 | ❌ 管不了 |
| **改 code-review-gate 不送审** | Agent 改门禁插件本身的代码但跳过了审查 | 规则 13 | ✅ 门禁也拦自己（.py 文件） |

> 规律很明显：**门禁管得住的 = 代码修改类操作；铁律要管的 = 决策类行为。** 两者缺一不可。

| 症状 | 检查 |
|------|------|
| 插件没拦截 | `hermes plugins list` 看插件状态 |
| 审查脚本连接失败 | `curl :8081/health`；确认 `llama-server` 在跑 |
| 写文件一直报 nonce 不匹配 | 用门禁提示里的 nonce，不要自己编 |
| Hermes 启动时报插件错误 | 先临时注释掉 config.yaml 里的 `code-review-gate`，修好插件再恢复 |
| Windows 上 Python 找不到 | 确保 Python 在 PATH 中，或用绝对路径 |

> 如果需要紧急关闭门禁：在 config.yaml 中删除 `code-review-gate`，重启 Hermes。

---

以上全部完成后，报告：「门禁系统部署完成 ✅」
