Skill: pengpengliu1212-art/risk-guard (version 1.0.1)

--- name: risk-guard description: "JVS Claw 风险守卫技能。诊断常见假性故障，对高风险操作强制执行前确认，确保任何不可逆操作都由先生掌控。触发词：自检、诊断、风险确认。" --- > **依赖说明**：本技能使用 Python 标准库（urllib、subprocess、json、pathlib），无需额外安装。需 Python 3.6+，仅支持 Windows。 # 🛡️ Risk Guard — 风险守卫技能 > 本技能**只诊断，不自动修复**。任何重启、删除、覆盖等不可逆操作都必须先生确认后才能执行。 > > **例外**：自动清理工作区中超过10分钟的 `.lock` 和 `.tmp` 文件（安全可逆）。 ## 核心原则 1. **先生始终掌控不可逆操作** — 删除、格式化、覆盖等高风险操作必须先生确认 2. **假性故障优先软修** — 路径错误、输出量大、超时等问题不轻易重启 3. **执行前必判断风险等级** — 查 Skill → 查工具 → 查先生是否在场 --- ## 一、风险分级（L0-L3） | 等级 | 操作类型 | 示例 | 确认要求 | |------|---------|------|---------| | **L0** | 只读、安全可逆 | 搜索、读文件、查询状态 | 无需确认 | | **L1** | 修改数据但不删除 | 写文件、改配置、重命名 | 执行后告知先生 | | **L2** | 删除/覆盖/不可逆 | 删除文件、格式化、覆盖配置 | **执行前必须先生确认** | | **L3** | 高危不可逆+系统级 | 删除系统文件、泄露密钥、修改注册表 | **禁止或先生明确授权** | --- ## 二、假性故障自检清单 > 以下命令需在 **PowerShell** 中执行（skill自动处理，先生无需手动运行） 先生工具卡住时，按以下顺序快速判断： ### 检查1：输出量过大（最常见） 先生看到"已完成"但没有输出？可能是递归遍历大量文件导致前端卡死。 ```powershell Get-ChildItem $env:TEMP -File | Measure-Object | Select-Object -ExpandProperty Count ``` 如果返回数字（小于1万）→ exec 正常，是假故障，先生继续正常使用。 ### 检查2：路径不存在 ```powershell Test-Path "目标路径" ``` 返回 False → 路径不存在，不是工具坏了。 ### 检查3：端口占用 ```powershell netstat -ano | findstr "18789" ``` 如果有其他进程占用 → 报告先生，询问是否结束。 > 注：部分端口检查可能需要管理员权限才能获取准确结果。 ### 检查4：subagent 残留 ```powershell tasklist | findstr "node.exe" ``` 如果超过20个 node.exe → subagent 泄漏，先生手动结束。 --- ## 三、高风险操作先生确认模板 当 Agent 判断操作为 L2 时，必须先生确认后才能执行： ``` ⚠️ 检测到高风险操作 操作：[描述具体操作] 风险等级：L2（不可逆） 影响范围：[说明可能的后果] 先生，请确认是否执行？ 请回复：确认 / 取消 ``` --- ## 四、不可逆操作禁止清单（L3） 以下操作**绝对禁止**执行，除非先生明确说"临时授权"： - 删除工作区外的任何文件 - 修改系统注册表 - 格式化磁盘 - 泄露 API 密钥 - 修改 gateway.bind、gateway.auth 等安全配置 - 执行 `rm -rf` 或等效的危险命令 --- ## 五、诊断执行 先生触发词：`自检` / `诊断` / `风险确认` 诊断内容： 1. exec 基础功能 2. 输出量/假故障 3. 网关进程状态 4. 网关端口监听 5. 网关健康端点 6. 残留锁文件 7. node 进程泄漏 8. 工作区路径 --- ## 六、文件结构 ``` risk-guard/ ├── SKILL.md ├── skill.json └── scripts/ └── diagnose.py # 诊断脚本（只诊断，不自动修复） ```

path: scripts/diagnose.py
content: """
risk-guard 诊断脚本 v1.4
修复：#1-fallback去掉 #2-node进程fallback误判 #3-锁文件删除需确认 #4-阈值30 #5-base64编码 #6-日志轮转 #7-锁文件验证 #8-dry-run模式
"""

import subprocess
import os
import sys
import json
import base64
import shutil
import argparse
from pathlib import Path
from datetime import datetime

# UTF-8 输出
if sys.stdout.encoding != 'utf-8':
    sys.stdout.reconfigure(encoding='utf-8', errors='replace')

# ---------- 配置 ----------
def get_workspace():
    home = os.environ.get("USERPROFILE", os.path.expanduser("~"))
    candidates = [
        os.path.join(home, ".jvs", ".openclaw", "workspace"),
        os.path.join(home, ".openclaw", "workspace"),
        os.path.join(home, "jvs_claw_workspace"),
    ]
    for c in candidates:
        if os.path.exists(c):
            return c
    return ""

WORKSPACE = get_workspace()
LOG_DIR = os.path.join(WORKSPACE, "memory") if WORKSPACE else ""
HEALTH_URL = "http://127.0.0.1:18789/health"
GATEWAY_PORT = 18789
NODE_THRESHOLD = int(os.environ.get("RISK_GUARD_NODE_THRESHOLD", "30"))
DRY_RUN = False

# ---------- 参数解析 ----------
def parse_args():
    global DRY_RUN
    parser = argparse.ArgumentParser(description="Risk Guard 自检脚本")
    parser.add_argument("--dry-run", action="store_true", help="仅输出检查结果，不执行任何清理操作")
    args = parser.parse_args()
    DRY_RUN = args.dry_run

# ---------- 日志（带轮转） ----------
LOG_FILE = os.path.join(LOG_DIR, "risk_guard.log") if LOG_DIR else ""

def log(msg):
    ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    line = f"[{ts}] {msg}"
    print(line)
    if LOG_FILE:
        try:
            if LOG_DIR:
                os.makedirs(LOG_DIR, exist_ok=True)
            # 日志轮转 > 10MB
            if os.path.exists(LOG_FILE) and os.path.getsize(LOG_FILE) > 10 * 1024 * 1024:
                old = LOG_FILE + ".old"
                if os.path.exists(old):
                    os.remove(old)
                shutil.move(LOG_FILE, old)
            with open(LOG_FILE, "a", encoding="utf-8") as f:
                f.write(line + "\n")
        except Exception:
            pass

# ---------- PowerShell 执行（base64编码避免转义问题） ----------
def ps_cmd(cmd, timeout=15):
    """通过base64编码执行PowerShell命令，彻底避免转义问题"""
    try:
        encoded = base64.b64encode(cmd.encode("utf-16-le")).decode()
        full_cmd = f'powershell.exe -NoProfile -NonInteractive -EncodedCommand {encoded}'
        result = subprocess.run(
            full_cmd, shell=True, capture_output=True,
            text=True, timeout=timeout,
            encoding="utf-8", errors="replace"
        )
        return result.returncode, result.stdout.strip(), result.stderr.strip()
    except subprocess.TimeoutExpired:
        return -1, "", "TIMEOUT"
    except Exception as e:
        return -2, "", str(e)

def run_cmd(cmd, timeout=10):
    """执行普通命令"""
    try:
        result = subprocess.run(
            cmd, shell=True, capture_output=True,
            text=True, timeout=timeout,
            encoding="utf-8", errors="replace"
        )
        return result.returncode, result.stdout.strip(), result.stderr.strip()
    except subprocess.TimeoutExpired:
        return -1, "", "TIMEOUT"
    except Exception as e:
        return -2, "", str(e)

# ---------- 检查函数 ----------
def check_exec():
    code, out, _ = run_cmd("echo test")
    return ("ok" if code == 0 and "test" in out else "fail"), "exec正常" if code == 0 else f"exec异常(code={code})"

def check_temp_files():
    code, out, _ = ps_cmd("Get-ChildItem $env:TEMP -File | Measure-Object | Select-Object -ExpandProperty Count")
    if code == 0:
        try:
            count = int(out.strip())
            status = "warning" if count > 50000 else "ok"
            msg = f"临时文件{count}个({'过多' if count > 50000 else '正常'})"
            return status, msg
        except:
            return "unknown", f"无法解析: {out[:30]}"
    return "unknown", "命令失败"

def check_gateway_process():
    """网关进程（只查openclaw，不在node上fallback）"""
    code, out, _ = run_cmd("tasklist | findstr openclaw")
    if code == 0 and out:
        return "ok", "网关进程正常"
    return "critical", "网关进程未运行！需要先生确认是否启动"

def check_gateway_port():
    """网关端口（Get-NetTCPConnection主逻辑，无fallback避免非英文系统误判）"""
    code, out, _ = ps_cmd(
        f"Get-NetTCPConnection -LocalPort {GATEWAY_PORT} -ErrorAction SilentlyContinue | "
        f"Where-Object {{$_.State -eq 'Listen'}} | Measure-Object | Select-Object -ExpandProperty Count"
    )
    if code == 0:
        try:
            count = int(out.strip())
            if count > 0:
                return "ok", f"端口{GATEWAY_PORT}监听正常"
        except:
            pass
    return "warn", f"端口{GATEWAY_PORT}未监听，可能被占用"

def check_gateway_health():
    """网关健康端点（urllib标准库）"""
    try:
        import urllib.request
        req = urllib.request.Request(HEALTH_URL, headers={"User-Agent": "RiskGuard/1.0"})
        with urllib.request.urlopen(req, timeout=5) as r:
            return ("ok" if r.status == 200 else "warn"), f"健康端点(HTTP {r.status})"
    except Exception as e:
        return "warn", f"健康端点连接失败"

def check_lock_files():
    """锁文件清理（dry-run模式不删除；直接尝试删除，PermissionError表示被占用）"""
    if not WORKSPACE:
        return "skip", "无法确定工作区，跳过"
    removed = 0
    skipped = 0
    for f in os.listdir(WORKSPACE):
        if not (f.endswith(".lock") or f.endswith(".tmp")):
            continue
        path = os.path.join(WORKSPACE, f)
        try:
            age = datetime.now().timestamp() - os.path.getmtime(path)
            if age <= 600:
                continue
            if not DRY_RUN:
                os.remove(path)
                removed += 1
            else:
                removed += 1  # dry-run 计数但不实际删除
        except PermissionError:
            skipped += 1
        except Exception:
            pass
    if skipped > 0:
        msg = f"清理{removed}个，跳过{skipped}个被占用文件"
    elif removed > 0:
        msg = f"已清理{removed}个过期锁文件" if not DRY_RUN else f"[dry-run] 将清理{removed}个过期文件"
    else:
        msg = "[dry-run] 无需清理" if DRY_RUN else "无残留锁文件"
    status = "info" if (removed > 0 or skipped > 0) else "ok"
    return status, msg

def check_node_leak():
    """node进程泄漏（精确匹配node.exe，阈值可配置）"""
    code, out, _ = run_cmd("tasklist | findstr node.exe")
    if code == 0 and out:
        lines = [l for l in out.split("\n") if l.strip()]
        count = len(lines)
        if count > NODE_THRESHOLD:
            return "warning", f"发现{count}个node.exe进程，超过阈值{NODE_THRESHOLD}"
        return "ok", f"node进程正常({count}个)"
    return "ok", "无node进程"

def check_workspace():
    if not WORKSPACE:
        return "warn", "无法确定工作区路径"
    if os.path.exists(WORKSPACE):
        return "ok", f"工作区正常: {WORKSPACE}"
    return "critical", f"工作区不存在: {WORKSPACE}"

# ---------- 主流程 ----------
STATUS_MAP = {
    "ok": "[OK]", "info": "[INFO]", "skip": "[SKIP]",
    "warn": "[WARN]", "warning": "[WARN]", "critical": "[CRITICAL]",
    "fail": "[FAIL]", "error": "[ERROR]", "unknown": "[UNK]"
}

def main():
    parse_args()
    mode_note = " [DRY-RUN 仅诊断]" if DRY_RUN else ""
    print(f"=== Risk Guard 自检开始{mode_note} ===")
    log(f"=== Risk Guard 自检开始{mode_note} ===")

    checks = [
        ("exec基础功能",   check_exec),
        ("临时文件",       check_temp_files),
        ("网关进程",       check_gateway_process),
        ("网关端口",       check_gateway_port),
        ("网关健康端点",   check_gateway_health),
        ("锁文件清理",     check_lock_files),
        ("node进程泄漏",   check_node_leak),
        ("工作区路径",     check_workspace),
    ]

    issues = []
    for name, fn in checks:
        try:
            status, msg = fn()
            prefix = STATUS_MAP.get(status, "[???]")
            print(f"{prefix} [{name}] {msg}")
            if status in ("critical", "fail"):
                issues.append(f"[{name}] {msg}")
        except Exception as e:
            print(f"[ERROR] [{name}] {e}")
            issues.append(f"[{name}] 异常: {e}")

    print()
    if issues:
        print("=== 发现问题 ===")
        for i in issues:
            print(f"  - {i}")
        print()
        print("=== 建议先生操作 ===")
        print("1. 【假故障】工具已完成但无输出 → 继续正常使用")
        print("2. 【网关进程缺失】→ 先生确认是否运行 'openclaw gateway start'")
        print("3. 【node泄漏】→ 先生手动结束可疑进程")
    else:
        print("=== 所有检查通过，未发现明显问题 ===")

    log(f"自检完成，发现{len(issues)}个问题")

    # 保存JSON报告
    if LOG_DIR:
        try:
            os.makedirs(LOG_DIR, exist_ok=True)
            report_path = os.path.join(LOG_DIR, "risk_guard_last.json")
            with open(report_path, "w", encoding="utf-8") as f:
                json.dump({
                    "time": datetime.now().isoformat(),
                    "workspace": WORKSPACE,
                    "dry_run": DRY_RUN,
                    "issues": issues
                }, f, ensure_ascii=False, indent=2)
        except:
            pass

    return issues

if __name__ == "__main__":
    main()


path: skill.json
content: {
  "name": "risk-guard",
  "description": "JVS Claw 风险守卫技能。诊断常见假性故障，对高风险操作强制执行前确认，确保任何不可逆操作都由先生掌控。触发词：自检、诊断、风险确认。",
  "version": "1.0.1",
  "author": "大鱼 Cyrus & 双鱼座005",
  "publish": "https://clawhub.ai/skills/risk-guard",
  "requirements": []
}


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal:  | SkillSpector: suspicious (CRITICAL)