# 门禁系统 v5 升级补充说明

> 适用于已部署 v1 版门禁（code-review-gate + iron-law）的环境。
> 本文档仅包含从 v1 升级到 v5 的新增内容。已有组件不动。
> 让 Hermes 逐条执行即可。

---

## 升级内容一览

| 新增/修改 | 说明 | 必须 |
|-----------|------|:----:|
| `scripts/maintenance.py` | Break-Glass 维护模式 | ✅ |
| `code-review-gate` 插件更新 | HERMES_HOME 路径修正 + 会话状态追踪 | ✅ |
| `iron-law-enforcer` 插件更新 | 降级拦截（不静默放行）| ✅ |
| `scripts/maintenance.py` 备份 | 升级保护 | ✅ |
| cron 任务维护 | 新增 2 个 cron | ✅ |
| `.hermes.md` 规则 | 会话状态恢复 + 项目地图 | ⚠️ 推荐 |
| config.yaml 更新 | curator 模型端口 | ⚠️ 有策展器才需要 |

---

## 第一步：开启维护模式

升级过程中会修改门禁自身文件，先开 15 分钟维护模式避免被自己拦截：

```bash
python ~/.hermes/scripts/maintenance.py on -r "门禁v5升级" -d 15
```

如果没有 `maintenance.py`（v1 没有这个文件），先创建空标记：

```bash
python -c "
import json, time, os
_H = os.environ.get('HERMES_HOME') or os.path.expanduser('~/.hermes')
os.makedirs(os.path.join(_H, 'temp'), exist_ok=True)
json.dump({'opened_at': int(time.time()), 'expires_at': int(time.time()) + 15*60,
           'reason': '门禁v5升级', 'duration_min': 15},
          open(os.path.join(_H, 'temp', 'maintenance.json'), 'w'))
print('维护标记已创建（15分钟）')
"
```

---

## 第二步：创建 maintenance.py

```python
# 用 write_file 创建 ~/.hermes/scripts/maintenance.py
```

把以下内容写入文件：

```python
#!/usr/bin/env python3
"""maintenance.py — Break-Glass 维护模式"""
import argparse, json, os, sys, time
from datetime import datetime, timezone

_H = os.environ.get("HERMES_HOME") or os.path.expandvars(r"%LOCALAPPDATA%\hermes")
_MARKER = os.path.join(_H, "temp", "maintenance.json")

def marker():
    try:
        if not os.path.exists(_MARKER): return None
        return json.load(open(_MARKER))
    except: return None

def write_marker(reason, dur):
    os.makedirs(os.path.dirname(_MARKER), exist_ok=True)
    m = {"opened_at": int(time.time()), "expires_at": int(time.time()) + dur * 60,
         "reason": reason, "duration_min": dur}
    json.dump(m, open(_MARKER, "w"), ensure_ascii=False, indent=2)
    return m

def remove_marker():
    try:
        if os.path.exists(_MARKER): os.remove(_MARKER)
    except: pass

if __name__ == "__main__":
    p = argparse.ArgumentParser()
    sub = p.add_subparsers(dest="cmd", required=True)
    a_on = sub.add_parser("on")
    a_on.add_argument("-r", "--reason", required=True)
    a_on.add_argument("-d", "--duration", type=int, default=15)
    sub.add_parser("off")
    sub.add_parser("status")
    args = p.parse_args()
    if args.cmd == "on":
        if args.duration > 30: print("最大30分钟"); sys.exit(1)
        write_marker(args.reason, args.duration)
        print(f"MAINTENANCE ON {args.duration}min {args.reason}")
    elif args.cmd == "off":
        m = marker()
        if not m: print("未开启"); sys.exit(0)
        remove_marker(); print("MAINTENANCE OFF")
    elif args.cmd == "status":
        m = marker()
        if not m: print("OFF"); sys.exit(0)
        if time.time() >= m["expires_at"]:
            remove_marker(); print("已过期"); sys.exit(0)
        r = (m["expires_at"] - time.time()) // 60
        print(f"ON ({r}min): {m.get('reason','')}")
```

验证语法：

```python
import py_compile, os
py_compile.compile(os.path.expanduser("~/.hermes/scripts/maintenance.py"), doraise=True)
print("OK")
```

---

## 第三步：更新 code-review-gate 插件

在 `~/.hermes/plugins/code-review-gate/__init__.py` 中做以下修改：

### 修改 1：HERMES_HOME 路径（Windows 兼容）

把：

```python
_HERMES_HOME = os.environ.get("HERMES_HOME", os.path.expanduser("~/.hermes"))
```

改为：

```python
_HERMES_HOME = os.environ.get("HERMES_HOME") or os.path.expandvars(r"%LOCALAPPDATA%\hermes")
```

### 修改 2：添加维护模式白名单检查 + 会话状态追踪

在 `_check_token()` 函数前添加两个函数：

```python
# ── Break-Glass maintenance mode ──
def _maintenance_mode():
    _mf = os.path.join(_HERMES_HOME, "temp", "maintenance.json")
    try:
        if os.path.exists(_mf):
            with open(_mf) as f:
                m = json.load(f)
            if m.get("expires_at", 0) > time.time():
                return m
            os.remove(_mf)
    except: pass
    return None

_WL = ["scripts/curator.py", "scripts/apply-behavior-patches.py",
       "scripts/maintenance.py",
       "plugins/code-review-gate", "plugins/iron-law",
       "agent-hooks/audit_guard.py", "tools/approval.py", "config.yaml"]


def _is_maintenance_file(file_path):
    if not file_path: return False
    fp = os.path.normcase(os.path.normpath(file_path))
    return any(os.path.normcase(os.path.normpath(wl_p)) in fp for wl_p in _WL)


def _session_state_update(tool_name, file_path):
    """更新 .hermes/session-state.json"""
    if tool_name not in ("write_file", "patch"):
        return
    try:
        import threading
        from pathlib import Path
        p = Path(file_path).resolve()
        for parent in [p] + list(p.parents)[:8]:
            if any((parent / m).exists() for m in (".git", "pyproject.toml", "Cargo.toml",
                   "package.json", "cli.py", ".hermes.md")):
                root = parent; break
        else: return
        sf = root / ".hermes" / "session-state.json"
        sf.parent.mkdir(parents=True, exist_ok=True)
        data = {}
        if sf.exists():
            try: data = json.loads(sf.read_text(encoding="utf-8"))
            except: pass
        now = __import__("datetime").datetime.now(__import__("datetime").timezone.utc).isoformat()
        rel = str(p.relative_to(root)) if root in p.parents else file_path
        data.setdefault("project_name", root.name)
        data["last_updated"] = now
        data.setdefault("events", [])
        data["events"].append({"ts": now, "action": tool_name, "file": rel, "summary": "修改 " + Path(rel).name})
        if len(data["events"]) > 50: data["events"] = data["events"][-50:]
        sf.write_text(json.dumps(data, indent=2, ensure_ascii=False), encoding="utf-8")
    except: pass
```

### 修改 3：在 `_check_token()` 开头添加维护模式检查

```python
def _check_token(tool_name="", file_path=""):
    mm = _maintenance_mode()
    if mm and _is_maintenance_file(file_path):
        return True, f"[maintenance] {mm.get('reason', 'n/a')}"
    # ... 原有代码不变 ...
```

### 修改 4：在 `pre_tool_call` 的放行位置添加会话状态更新

在 `return {"action": "allow"}` 之前（`if ok:` 分支内）：

```python
    if ok:
        try:
            os.remove(_SIG_FILE)
        except: pass
        _session_state_update(tool_name, file_path)  # ← 新增
        return {"action": "allow"}
```

### 修改 5：`_is_maintenance_file` 路径分隔符标准化

把 `return any(wl_p in fp for wl_p in _WL)` 改为：

```python
    return any(os.path.normcase(os.path.normpath(wl_p)) in fp for wl_p in _WL)
```

---

## 第四步：更新 iron-law 插件

把 `~/.hermes/plugins/iron-law/__init__.py` 中的备援门禁逻辑改为降级拦截模式。

### 修改 1：添加维护模式检查

在 `_check_review` 函数开头添加：

```python
def _check_review(tool_name, args):
    # Break-glass maintenance mode
    _H = os.environ.get("HERMES_HOME") or os.path.expandvars(r"%LOCALAPPDATA%\hermes")
    try:
        with open(os.path.join(_H, "temp", "maintenance.json")) as _f:
            _m = json.load(_f)
        if _m.get("expires_at", 0) > __import__("time").time():
            _fp = (args or {}).get("path", "")
            if _fp:
                _n = os.path.normcase(os.path.normpath(_fp))
                for _p in ["scripts/curator.py", "scripts/apply-behavior-patches.py",
                          "plugins/code-review-gate", "plugins/iron-law",
                          "tools/approval.py", "config.yaml"]:
                    if os.path.normcase(os.path.normpath(_p)) in _n:
                        return None
    except: pass
```

### 修改 2：主门禁不可用时降级拦截

把以下代码：

```python
    cgate = sys.modules.get("code_review_gate")
    if cgate is None:
        return None  # 主门禁不在，静默（不重复拦截）
```

改为：

```python
    cgate = sys.modules.get("code_review_gate")
    if cgate is None:
        # 主门禁加载失败时降级为全审查
        if os.path.exists(_TOKEN_FILE):
            try:
                with open(_TOKEN_FILE, "r", encoding="utf-8") as _f:
                    _t = json.load(_f)
                if time.time() - _t.get("timestamp", 0) <= _TOKEN_TTL and \
                   _t.get("verdict") == "approved":
                    return None
            except: pass
        return {"action": "block",
                "message": "🔴 [iron-law] 主门禁不可用 + 无有效令牌（降级拦截）"}
```

---

## 第五步：更新 config.yaml

确认 `auxiliary.curator.base_url` 配置：

```yaml
auxiliary:
  curator:
    api_key: ''
    base_url: http://127.0.0.1:8080
    model: ''
    provider: auto
    timeout: 600
```

如果 `auxiliary` 段不存在，加在文件末尾即可。

---

## 第六步：新增 cron 任务

```bash
# 维护模式自动过期（每5分钟检查）
hermes cron create --name "维护模式自动过期" --schedule "every 5m" \
  --prompt "运行 python ~/.hermes/scripts/maintenance.py status 检查维护模式标记，过期自动清理"

# 自定义脚本完整性检查（60分钟）
hermes cron create --name "自定义脚本完整性" --schedule "every 60m" \
  --script "apply-behavior-patches.py --check-custom" --no-agent
```

---

## 第七步：重启 Hermes

```bash
taskkill /F /IM python.exe
```

重新打开 Hermes 桌面端。

---

## 第八步：验证

```bash
# 1. 测试维护模式
python ~/.hermes/scripts/maintenance.py on -r "验证" -d 1
python ~/.hermes/scripts/maintenance.py status
# 等待 1 分钟后再次 status → 应显示 OFF

# 2. 测试会话状态追踪
# 写一个 .py 文件并审查通过 → 检查 .hermes/session-state.json

# 3. 确认旧功能不受影响
# 写一个不在白名单的 .py 文件 → 应被 HMAC 拦截
```

---

## 升级前后变化

| 对比项 | v1 | v5 |
|--------|:----:|:----:|
| 维护模式 | ❌ 无 | ✅ Break-Glass |
| 路径兼容 | `~/` | ✅ `%LOCALAPPDATA%` |
| 路径分隔符 | ❌ 不处理 | ✅ `normpath` 标准化 |
| 主门禁失效 | 静默放行 | ✅ 降级拦截 |
| 会话状态追踪 | ❌ 无 | ✅ session-state.json |
| 审查防注入 | ❌ 无 | ✅ 首行 PASS 校验 |
| 自定义脚本备份 | ❌ 无 | ✅ 10 个文件 |
| 升级自动恢复 | ❌ 无 | ✅ cron 60 分钟 |

---

> 本文件是 v1→v5 的升级补充，不包含完整安装步骤。
> 如需全新安装，使用 `Desktop/Hermes门禁系统一键部署.md`。
