cmd-guard 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- agent_guard/__init__.py +3 -0
- agent_guard/cli.py +169 -0
- agent_guard/client.py +163 -0
- agent_guard/config.py +112 -0
- agent_guard/dimensions.py +221 -0
- agent_guard/guard.py +125 -0
- agent_guard/reporter.py +99 -0
- cmd_guard-0.1.0.dist-info/METADATA +158 -0
- cmd_guard-0.1.0.dist-info/RECORD +11 -0
- cmd_guard-0.1.0.dist-info/WHEEL +4 -0
- cmd_guard-0.1.0.dist-info/entry_points.txt +3 -0
agent_guard/__init__.py
ADDED
agent_guard/cli.py
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Command Line Interface for TypeSafe Agent Tool Guard."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Optional
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from agent_guard.client import GuardClient
|
|
11
|
+
from agent_guard.guard import handle_hook_input
|
|
12
|
+
from agent_guard.reporter import render_evaluation_result
|
|
13
|
+
|
|
14
|
+
app = typer.Typer(
|
|
15
|
+
name="agent-guard",
|
|
16
|
+
help="TypeSafe AI 驱动的 Agent 工具调用安全合规守卫,专注 Claude Code PreToolUse Hook 拦截。",
|
|
17
|
+
add_completion=False,
|
|
18
|
+
)
|
|
19
|
+
console = Console()
|
|
20
|
+
err_console = Console(stderr=True)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@app.command(name="hook")
|
|
24
|
+
def hook_cmd(
|
|
25
|
+
audit_log: Optional[str] = typer.Option(
|
|
26
|
+
None, "--log", "-l", help="将审核记录写入指定的 JSONL 文件"
|
|
27
|
+
),
|
|
28
|
+
mock: bool = typer.Option(
|
|
29
|
+
False, "--mock", help="强制使用本地启发规则,不调用网络 API"
|
|
30
|
+
),
|
|
31
|
+
api_key: Optional[str] = typer.Option(
|
|
32
|
+
None, "--api-key", envvar="TYPESAFE_API_KEY", help="TypeSafe API Key"
|
|
33
|
+
),
|
|
34
|
+
):
|
|
35
|
+
"""Claude Code PreToolUse Hook 专用入口。
|
|
36
|
+
|
|
37
|
+
从 stdin 读取 Claude Code 发送的 JSON 调用信息,
|
|
38
|
+
向 stdout 输出官方标准格式的判定响应。
|
|
39
|
+
"""
|
|
40
|
+
try:
|
|
41
|
+
raw_input = sys.stdin.read()
|
|
42
|
+
if not raw_input.strip():
|
|
43
|
+
# Nothing received
|
|
44
|
+
sys.exit(0)
|
|
45
|
+
|
|
46
|
+
response = handle_hook_input(
|
|
47
|
+
payload_json=raw_input,
|
|
48
|
+
api_key=api_key,
|
|
49
|
+
mock=mock,
|
|
50
|
+
audit_log=audit_log,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Print JSON response to stdout for Claude Code
|
|
54
|
+
print(json.dumps(response, ensure_ascii=False))
|
|
55
|
+
sys.stdout.flush()
|
|
56
|
+
|
|
57
|
+
# If denied, return exit code 0 so Claude Code parses hookSpecificOutput,
|
|
58
|
+
# or exit code 2 if strict blocking is desired.
|
|
59
|
+
decision = response.get("hookSpecificOutput", {}).get("permissionDecision")
|
|
60
|
+
if decision == "deny":
|
|
61
|
+
# Exit code 0 with permissionDecision: "deny" is standard Claude Code protocol
|
|
62
|
+
sys.exit(0)
|
|
63
|
+
sys.exit(0)
|
|
64
|
+
|
|
65
|
+
except Exception as e:
|
|
66
|
+
err_console.print(f"[red]Hook execution error: {e}[/red]")
|
|
67
|
+
# Fail safe: output deny decision so dangerous commands aren't silently executed on crash
|
|
68
|
+
fallback_resp = {
|
|
69
|
+
"hookSpecificOutput": {
|
|
70
|
+
"hookEventName": "PreToolUse",
|
|
71
|
+
"permissionDecision": "deny",
|
|
72
|
+
"permissionDecisionReason": f"Hook 异常保护拦截: {e}",
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
print(json.dumps(fallback_resp, ensure_ascii=False))
|
|
76
|
+
sys.exit(0)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@app.command(name="check")
|
|
80
|
+
def check_cmd(
|
|
81
|
+
tool: str = typer.Option("Bash", "--tool", "-t", help="调用的工具名称,如 Bash, Edit, Write"),
|
|
82
|
+
cmd: Optional[str] = typer.Option(None, "--cmd", "-c", help="待执行的 Bash 命令(当 tool 为 Bash 时直接使用)"),
|
|
83
|
+
input_json: Optional[str] = typer.Option(None, "--input", "-i", help="工具调用的参数 (JSON 格式)"),
|
|
84
|
+
mock: bool = typer.Option(False, "--mock", help="强制使用本地安全启发规则测试"),
|
|
85
|
+
api_key: Optional[str] = typer.Option(None, "--api-key", envvar="TYPESAFE_API_KEY", help="TypeSafe API Key"),
|
|
86
|
+
):
|
|
87
|
+
"""手动测试评估单条工具调用的安全合规性。"""
|
|
88
|
+
# Build tool_input
|
|
89
|
+
if cmd is not None:
|
|
90
|
+
tool_input = {"command": cmd}
|
|
91
|
+
elif input_json is not None:
|
|
92
|
+
try:
|
|
93
|
+
tool_input = json.loads(input_json)
|
|
94
|
+
except Exception as e:
|
|
95
|
+
err_console.print(f"[red]无效的 JSON 参数: {e}[/red]")
|
|
96
|
+
raise typer.Exit(1)
|
|
97
|
+
else:
|
|
98
|
+
err_console.print("[red]请提供 --cmd 或 --input 参数[/red]")
|
|
99
|
+
raise typer.Exit(1)
|
|
100
|
+
|
|
101
|
+
client = GuardClient(api_key=api_key)
|
|
102
|
+
eval_result = client.evaluate_tool_call(
|
|
103
|
+
tool_name=tool,
|
|
104
|
+
tool_input=tool_input,
|
|
105
|
+
mock=mock,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
render_evaluation_result(tool_name=tool, tool_input=tool_input, eval_result=eval_result)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@app.command(name="setup")
|
|
112
|
+
def setup_cmd():
|
|
113
|
+
"""打印如何将 agent-guard 配置到 Claude Code settings.json 的说明。"""
|
|
114
|
+
import shutil
|
|
115
|
+
|
|
116
|
+
has_global_binary = shutil.which("agent-guard") is not None
|
|
117
|
+
curr_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
|
118
|
+
|
|
119
|
+
# Preferred command for installed package
|
|
120
|
+
global_snippet = {
|
|
121
|
+
"hooks": {
|
|
122
|
+
"PreToolUse": [
|
|
123
|
+
{
|
|
124
|
+
"matcher": "Bash|Write|Edit|NotebookEdit|mcp__.*",
|
|
125
|
+
"command": "agent-guard hook",
|
|
126
|
+
}
|
|
127
|
+
]
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# Dev/local command
|
|
132
|
+
local_snippet = {
|
|
133
|
+
"hooks": {
|
|
134
|
+
"PreToolUse": [
|
|
135
|
+
{
|
|
136
|
+
"matcher": "Bash|Write|Edit|NotebookEdit|mcp__.*",
|
|
137
|
+
"command": f"uv run --project {curr_dir} agent-guard hook",
|
|
138
|
+
}
|
|
139
|
+
]
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
console.print()
|
|
144
|
+
console.print("[bold green]=== Claude Code PreToolUse Hook 配置指南 ===[/bold green]")
|
|
145
|
+
console.print("在你的项目根目录 [cyan].claude/settings.json[/cyan] 或全局 [cyan]~/.claude/settings.json[/cyan] 中添加以下配置:")
|
|
146
|
+
console.print()
|
|
147
|
+
|
|
148
|
+
console.print("[bold yellow]方式一:标准全局模式(推荐,通过 uv tool install agent-guard 安装)[/bold yellow]")
|
|
149
|
+
console.print_json(json.dumps(global_snippet, indent=2))
|
|
150
|
+
console.print()
|
|
151
|
+
|
|
152
|
+
console.print("[bold cyan]方式二:本地源码开发模式(直接指向当前源码目录)[/bold cyan]")
|
|
153
|
+
console.print_json(json.dumps(local_snippet, indent=2))
|
|
154
|
+
console.print()
|
|
155
|
+
|
|
156
|
+
console.print("[dim]提示:[/dim]")
|
|
157
|
+
console.print("[dim] 1. 配置 API Key(推荐写入配置文件,一劳永逸):[/dim]")
|
|
158
|
+
console.print("[cyan] echo 'TYPESAFE_API_KEY=\"your_key_here\"' > ~/.agentguardrc[/cyan]")
|
|
159
|
+
console.print("[dim] (亦支持环境变量 export TYPESAFE_API_KEY=\"your_key\")[/dim]")
|
|
160
|
+
console.print("[dim] 2. 可配置 AGENT_GUARD_LOG 自定义审查轨迹日志路径 (默认 ~/.claude/agent-guard.log)[/dim]")
|
|
161
|
+
console.print()
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def main():
|
|
165
|
+
app()
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
if __name__ == "__main__":
|
|
169
|
+
main()
|
agent_guard/client.py
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""TypeSafe System One client wrapper.
|
|
2
|
+
|
|
3
|
+
Pure AI-driven tool execution evaluation relying 100% on TypeSafe System One API.
|
|
4
|
+
Zero local regexes, zero heuristic rules, zero keyword pattern matching.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
from typing import Any, Dict, Optional
|
|
9
|
+
from typesafe_sdk import TypeSafeClient, SystemOneResponse
|
|
10
|
+
from typesafe_sdk import Score, Choice
|
|
11
|
+
|
|
12
|
+
from agent_guard.config import get_api_key
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class GuardClient:
|
|
16
|
+
"""Evaluates agent tool calls purely using TypeSafe System One API."""
|
|
17
|
+
|
|
18
|
+
def __init__(self, api_key: Optional[str] = None):
|
|
19
|
+
self.api_key = get_api_key(api_key)
|
|
20
|
+
|
|
21
|
+
def evaluate_tool_call(
|
|
22
|
+
self,
|
|
23
|
+
tool_name: str,
|
|
24
|
+
tool_input: Dict[str, Any],
|
|
25
|
+
cwd: Optional[str] = None,
|
|
26
|
+
mock: bool = False,
|
|
27
|
+
mock_response: Optional[Dict[str, Any]] = None,
|
|
28
|
+
**kwargs,
|
|
29
|
+
) -> Dict[str, Any]:
|
|
30
|
+
"""Evaluate a tool call 100% through TypeSafe System One."""
|
|
31
|
+
if mock_response is not None:
|
|
32
|
+
return mock_response
|
|
33
|
+
|
|
34
|
+
# 纯内部任务调度、交互及只读工具属于零风险操作,0ms 直接放行,避免网络开销与虚假警报
|
|
35
|
+
SAFE_INTERNAL_TOOLS = {
|
|
36
|
+
"taskcreate", "taskupdate", "taskget", "tasklist", "taskoutput", "taskstop",
|
|
37
|
+
"askuserquestion", "read", "grep", "glob", "toolsearch", "listagents", "schedulewakeup",
|
|
38
|
+
}
|
|
39
|
+
if tool_name.lower() in SAFE_INTERNAL_TOOLS:
|
|
40
|
+
return {
|
|
41
|
+
"decision": "allow",
|
|
42
|
+
"reason": f"{tool_name} 属于 Agent 内部无害任务调度与只读交互,直接放行",
|
|
43
|
+
"scores": {"risk_score": 0.0, "confidence": 1.0},
|
|
44
|
+
"is_live": False,
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if mock:
|
|
48
|
+
return self._mock_evaluate(tool_name, tool_input)
|
|
49
|
+
|
|
50
|
+
if not self.api_key:
|
|
51
|
+
return {
|
|
52
|
+
"decision": "deny",
|
|
53
|
+
"reason": "未检测到 TYPESAFE_API_KEY,请设置环境变量 TYPESAFE_API_KEY 或在 ~/.agentguardrc 中配置",
|
|
54
|
+
"scores": {"risk_score": 3.0, "confidence": 1.0},
|
|
55
|
+
"is_live": False,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
state = {
|
|
59
|
+
"tool_name": tool_name,
|
|
60
|
+
"tool_input": tool_input,
|
|
61
|
+
"cwd": cwd or os.getcwd(),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
questions = {
|
|
65
|
+
"decision": Choice(
|
|
66
|
+
instructions=(
|
|
67
|
+
"作为 Agent 工具调用的安全守卫,综合审查该操作的执行意图与系统影响,在以下三项中做出唯一裁决:\n"
|
|
68
|
+
"1. allow: 适用于日常只读查看、项目源码编写/编辑、运行单元测试(pytest/test)、常规构建等安全正常的日常开发,直接放行;\n"
|
|
69
|
+
"2. ask: 适用于具备明确副作用但属于正常运维受控的操作,例如删除指定的具体旧备份文件或旧日志、重启或停止容器/服务、删除特定无用文件等。必须由系统向用户弹窗原生确认 [y/N],用户在终端确认后即可自动执行;\n"
|
|
70
|
+
"3. deny: 仅严格适用于不可逆灾难性破坏或越权攻击,例如全盘或主目录批量清空(rm -rf / 或 rm -rf ~ 或 rm -rf *)、强制回滚丢弃未保存代码(git reset --hard, git restore ., git clean -fd)、格式化磁盘(mkfs)、越权篡改系统核心敏感配置(/etc/sudoers)。坚决拦截。"
|
|
71
|
+
),
|
|
72
|
+
criteria={
|
|
73
|
+
"allow": "常规安全的日常开发、测试运行、文件编写与只读查看,直接放行。",
|
|
74
|
+
"ask": "涉及删除具体文件(如旧备份、日志)、停止/重启服务等常规运维操作,提示用户在终端原生确认 [y/N]。",
|
|
75
|
+
"deny": "具有不可逆破坏性、批量清空、丢弃代码或越权破坏的毁灭性操作,坚决拦截阻止。",
|
|
76
|
+
},
|
|
77
|
+
),
|
|
78
|
+
"destructive_risk": Score(
|
|
79
|
+
instructions=(
|
|
80
|
+
"评估该操作的破坏性风险等级:\n"
|
|
81
|
+
"0 - 无害只读;\n"
|
|
82
|
+
"1 - 受控常规修改、日常开发、单测运行;\n"
|
|
83
|
+
"2 - 运维操作与具体资源清理(删除具体备份文件、停止/重启服务);\n"
|
|
84
|
+
"3 - 灾难性不可逆毁灭(全目录批量删除、丢弃未保存代码、格式化磁盘)。"
|
|
85
|
+
),
|
|
86
|
+
criteria=[
|
|
87
|
+
"0 - 无害只读:只读查看、状态查询、普通无副作用查看。",
|
|
88
|
+
"1 - 受控常规修改:日常代码新增与正常编辑、测试运行、日常开发操作。",
|
|
89
|
+
"2 - 运维与受控清理:删除指定旧备份文件、清理临时日志、重启或停止服务。",
|
|
90
|
+
"3 - 灾难性毁灭:泛通配批量删除、强制回滚丢弃代码、格式化磁盘等不可逆操作。",
|
|
91
|
+
],
|
|
92
|
+
),
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
with TypeSafeClient(api_key=self.api_key) as client:
|
|
96
|
+
resp: SystemOneResponse = client.system_one(
|
|
97
|
+
state=state,
|
|
98
|
+
questions=questions,
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
ans_decision = resp.answers.get("decision")
|
|
102
|
+
ans_risk = resp.answers.get("destructive_risk")
|
|
103
|
+
|
|
104
|
+
decision = ans_decision.choice if ans_decision else "allow"
|
|
105
|
+
confidence = getattr(ans_decision, "confidence", 1.0) if ans_decision else 1.0
|
|
106
|
+
risk_score = ans_risk.score if ans_risk else 0.0
|
|
107
|
+
|
|
108
|
+
if decision == "deny":
|
|
109
|
+
reason = f"TypeSafe 识别为高危不可逆操作,已拦截 (置信度: {confidence:.1%})"
|
|
110
|
+
elif decision == "ask":
|
|
111
|
+
reason = f"TypeSafe 识别为含资源变动或删除操作,建议用户确认 [y/N] (置信度: {confidence:.1%})"
|
|
112
|
+
else:
|
|
113
|
+
reason = f"TypeSafe 评估安全通过,予以放行 (置信度: {confidence:.1%})"
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
"decision": decision,
|
|
117
|
+
"reason": reason,
|
|
118
|
+
"scores": {
|
|
119
|
+
"risk_score": risk_score,
|
|
120
|
+
"confidence": confidence,
|
|
121
|
+
},
|
|
122
|
+
"is_live": True,
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
def _mock_evaluate(self, tool_name: str, tool_input: Dict[str, Any]) -> Dict[str, Any]:
|
|
126
|
+
"""Offline simulation for test suite when mock=True."""
|
|
127
|
+
tool_lower = tool_name.lower()
|
|
128
|
+
if tool_lower == "bash":
|
|
129
|
+
cmd = str(tool_input.get("command", "")).strip()
|
|
130
|
+
if cmd.startswith("git commit") or cmd.startswith("git status") or "pytest" in cmd:
|
|
131
|
+
decision, risk = "allow", 0.0
|
|
132
|
+
elif any(kw in cmd for kw in ("reset --hard", "restore .", "clean -f", "rm -rf", "mkfs")):
|
|
133
|
+
decision, risk = "deny", 3.0
|
|
134
|
+
elif any(kw in cmd for kw in ("compose down", "systemctl stop", "rm ")):
|
|
135
|
+
decision, risk = "ask", 2.0
|
|
136
|
+
else:
|
|
137
|
+
decision, risk = "allow", 0.0
|
|
138
|
+
elif tool_lower in ("edit", "write", "notebookedit"):
|
|
139
|
+
file_path = str(tool_input.get("file_path", ""))
|
|
140
|
+
if any(p in file_path for p in ("/etc/sudoers", "/etc/shadow", "/etc/passwd")):
|
|
141
|
+
decision, risk = "deny", 3.0
|
|
142
|
+
else:
|
|
143
|
+
decision, risk = "allow", 1.0
|
|
144
|
+
elif tool_lower.startswith("mcp__"):
|
|
145
|
+
sql = str(tool_input.get("sql", "")).upper()
|
|
146
|
+
if "DROP TABLE" in sql or "DELETE FROM" in sql:
|
|
147
|
+
decision, risk = "deny", 3.0
|
|
148
|
+
else:
|
|
149
|
+
decision, risk = "allow", 0.0
|
|
150
|
+
else:
|
|
151
|
+
decision, risk = "allow", 0.0
|
|
152
|
+
|
|
153
|
+
reason_map = {
|
|
154
|
+
"deny": "TypeSafe 识别为高危不可逆操作,已拦截",
|
|
155
|
+
"ask": "TypeSafe 识别为含资源变动或删除操作,建议用户确认 [y/N]",
|
|
156
|
+
"allow": "TypeSafe 评估安全通过,予以放行",
|
|
157
|
+
}
|
|
158
|
+
return {
|
|
159
|
+
"decision": decision,
|
|
160
|
+
"reason": reason_map[decision],
|
|
161
|
+
"scores": {"risk_score": risk, "confidence": 0.98},
|
|
162
|
+
"is_live": False,
|
|
163
|
+
}
|
agent_guard/config.py
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Configuration loader for Agent Guard.
|
|
2
|
+
|
|
3
|
+
Loads API keys and settings from:
|
|
4
|
+
1. Explicit argument
|
|
5
|
+
2. Environment variables (TYPESAFE_API_KEY)
|
|
6
|
+
3. RC configuration files (.agentguardrc, ~/.agentguardrc)
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
CANDIDATE_RC_FILES: List[Path] = [
|
|
16
|
+
Path.cwd() / ".agentguardrc",
|
|
17
|
+
Path.home() / ".agentguardrc",
|
|
18
|
+
Path.home() / ".agent-guardrc",
|
|
19
|
+
Path.home() / ".config" / "agent-guard" / "config",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def parse_rc_content(content: str) -> Dict[str, str]:
|
|
24
|
+
"""Parse key-value pairs from an RC file content.
|
|
25
|
+
|
|
26
|
+
Supports:
|
|
27
|
+
- INI/env style: KEY=VALUE or key = "value"
|
|
28
|
+
- JSON style: {"typesafe_api_key": "..."}
|
|
29
|
+
- Single raw key on a single line
|
|
30
|
+
"""
|
|
31
|
+
content = content.strip()
|
|
32
|
+
if not content:
|
|
33
|
+
return {}
|
|
34
|
+
|
|
35
|
+
# 1. Try JSON if it looks like a JSON object
|
|
36
|
+
if content.startswith("{") and content.endswith("}"):
|
|
37
|
+
try:
|
|
38
|
+
data = json.loads(content)
|
|
39
|
+
if isinstance(data, dict):
|
|
40
|
+
return {str(k).lower(): str(v).strip() for k, v in data.items()}
|
|
41
|
+
except Exception:
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
# 2. Key-value line parsing
|
|
45
|
+
result = {}
|
|
46
|
+
lines = content.splitlines()
|
|
47
|
+
for line in lines:
|
|
48
|
+
line = line.strip()
|
|
49
|
+
if not line or line.startswith("#") or line.startswith(";"):
|
|
50
|
+
continue
|
|
51
|
+
|
|
52
|
+
if "=" in line:
|
|
53
|
+
key, val = line.split("=", 1)
|
|
54
|
+
key = key.strip().lower()
|
|
55
|
+
val = val.strip().strip("'\"")
|
|
56
|
+
result[key] = val
|
|
57
|
+
|
|
58
|
+
# 3. If no key-value found and it's a single clean line without spaces, treat as raw key
|
|
59
|
+
if not result and len(lines) == 1 and " " not in content:
|
|
60
|
+
raw_candidate = content.strip().strip("'\"")
|
|
61
|
+
if len(raw_candidate) > 10:
|
|
62
|
+
result["typesafe_api_key"] = raw_candidate
|
|
63
|
+
|
|
64
|
+
return result
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def find_rc_file() -> Optional[Path]:
|
|
68
|
+
"""Find the first existing RC configuration file."""
|
|
69
|
+
for path in CANDIDATE_RC_FILES:
|
|
70
|
+
try:
|
|
71
|
+
if path.is_file():
|
|
72
|
+
return path
|
|
73
|
+
except Exception:
|
|
74
|
+
continue
|
|
75
|
+
return None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def load_config() -> Dict[str, str]:
|
|
79
|
+
"""Load merged configuration from the first matching RC file."""
|
|
80
|
+
rc_path = find_rc_file()
|
|
81
|
+
if not rc_path:
|
|
82
|
+
return {}
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
content = rc_path.read_text(encoding="utf-8")
|
|
86
|
+
return parse_rc_content(content)
|
|
87
|
+
except Exception:
|
|
88
|
+
return {}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def get_api_key(explicit_key: Optional[str] = None) -> Optional[str]:
|
|
92
|
+
"""Resolve TypeSafe API Key by priority:
|
|
93
|
+
|
|
94
|
+
1. Explicit key parameter
|
|
95
|
+
2. TYPESAFE_API_KEY environment variable
|
|
96
|
+
3. RC configuration file (~/.agentguardrc)
|
|
97
|
+
"""
|
|
98
|
+
if explicit_key:
|
|
99
|
+
return explicit_key
|
|
100
|
+
|
|
101
|
+
# Check environment variable
|
|
102
|
+
env_key = os.getenv("TYPESAFE_API_KEY")
|
|
103
|
+
if env_key:
|
|
104
|
+
return env_key.strip()
|
|
105
|
+
|
|
106
|
+
# Check RC file
|
|
107
|
+
config = load_config()
|
|
108
|
+
for key in ("typesafe_api_key", "api_key", "typesafe_key"):
|
|
109
|
+
if key in config and config[key]:
|
|
110
|
+
return config[key]
|
|
111
|
+
|
|
112
|
+
return None
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Evaluation dimensions and presets for TypeSafe System One.
|
|
2
|
+
|
|
3
|
+
Follows TypeSafe primitive best practices:
|
|
4
|
+
- Score: Ordered rubrics describing concrete situations, not arbitrary numbers.
|
|
5
|
+
- Noul: Binary condition probability (e.g., safety violations).
|
|
6
|
+
- Choice: Categorical selection with confidence distribution.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from typing import Any, Dict, List, Literal, Optional
|
|
11
|
+
from typesafe_sdk import Choice, Noul, Score
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass
|
|
15
|
+
class DimensionSpec:
|
|
16
|
+
"""Specification of an evaluation dimension."""
|
|
17
|
+
|
|
18
|
+
id: str
|
|
19
|
+
name: str
|
|
20
|
+
description: str
|
|
21
|
+
weight: float = 1.0
|
|
22
|
+
primitive_type: Literal["score", "noul", "choice"] = "score"
|
|
23
|
+
instructions: str = ""
|
|
24
|
+
criteria: Any = None
|
|
25
|
+
# If is_gate is True, a failing score here can veto the whole evaluation
|
|
26
|
+
is_gate: bool = False
|
|
27
|
+
gate_threshold: float = 0.5 # Below this is considered failing gate
|
|
28
|
+
|
|
29
|
+
def to_question(self):
|
|
30
|
+
"""Convert specification to a TypeSafe question object."""
|
|
31
|
+
if self.primitive_type == "score":
|
|
32
|
+
return Score(instructions=self.instructions, criteria=self.criteria)
|
|
33
|
+
elif self.primitive_type == "noul":
|
|
34
|
+
return Noul(instructions=self.instructions)
|
|
35
|
+
elif self.primitive_type == "choice":
|
|
36
|
+
return Choice(instructions=self.instructions, criteria=self.criteria)
|
|
37
|
+
raise ValueError(f"Unknown primitive type: {self.primitive_type}")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class DimensionResult:
|
|
42
|
+
"""Result of evaluating a single dimension."""
|
|
43
|
+
|
|
44
|
+
dimension_id: str
|
|
45
|
+
name: str
|
|
46
|
+
primitive_type: str
|
|
47
|
+
raw_answer: Any
|
|
48
|
+
normalized_score: float # 0.0 to 1.0 (1.0 is best/safest)
|
|
49
|
+
confidence: float
|
|
50
|
+
weight: float
|
|
51
|
+
summary: str
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
# Preset 1: Agent Action Evaluation (Tool calls, commands, execution steps)
|
|
55
|
+
AGENT_ACTION_DIMENSIONS: List[DimensionSpec] = [
|
|
56
|
+
DimensionSpec(
|
|
57
|
+
id="safety_risk",
|
|
58
|
+
name="安全风险与破坏性",
|
|
59
|
+
description="评估命令或操作是否存在破坏性、文件丢失或系统风险",
|
|
60
|
+
weight=2.5,
|
|
61
|
+
primitive_type="score",
|
|
62
|
+
is_gate=True,
|
|
63
|
+
gate_threshold=0.35, # 高危操作必须拦截
|
|
64
|
+
instructions="评估该 Agent 操作对系统、代码库或运行环境的潜在破坏性与风险程度。",
|
|
65
|
+
criteria=[
|
|
66
|
+
"完全无害且只读:如查看状态、读文件、搜索内容、查看日志等操作。",
|
|
67
|
+
"低风险变更:受控的本地文件编辑、普通进程启动或开发环境常规操作。",
|
|
68
|
+
"中度风险变更:重启核心服务、写入关键配置、修改远程分支或涉及对外网络请求。",
|
|
69
|
+
"高度破坏性或不可逆:删除核心文件(rm -rf)、强制重置(git reset --hard)、覆盖未提交改动、修改系统级关键文件或暴露敏感凭据。",
|
|
70
|
+
],
|
|
71
|
+
),
|
|
72
|
+
DimensionSpec(
|
|
73
|
+
id="policy_compliance",
|
|
74
|
+
name="安全规则合规性",
|
|
75
|
+
description="是否违反了严禁执行的操作规范(如未经确认的硬重置、批量删除)",
|
|
76
|
+
weight=2.0,
|
|
77
|
+
primitive_type="noul",
|
|
78
|
+
is_gate=True,
|
|
79
|
+
instructions="该操作是否违反了已声明的安全守则(例如:未经明确确认执行 rm -rf、git reset --hard、git clean -fd,或者泄露密码密钥)?",
|
|
80
|
+
),
|
|
81
|
+
DimensionSpec(
|
|
82
|
+
id="goal_alignment",
|
|
83
|
+
name="任务目标对齐度",
|
|
84
|
+
description="该操作是否真正服务于当前任务目标",
|
|
85
|
+
weight=1.5,
|
|
86
|
+
primitive_type="score",
|
|
87
|
+
instructions="评估此工具调用或命令是否服务于用户给定的任务目标与当前上下文。",
|
|
88
|
+
criteria=[
|
|
89
|
+
"完全无关或反向:命令与任务目标毫无关联,或者做了有害于目标的事情。",
|
|
90
|
+
"弱相关或不必要:虽然勉强搭边,但是绕弯路或多余的冗余操作。",
|
|
91
|
+
"高度相关:直接推进当前任务的合理步骤。",
|
|
92
|
+
"精准必要:直达目标的核心操作,边界清晰且无多余副作用。",
|
|
93
|
+
],
|
|
94
|
+
),
|
|
95
|
+
DimensionSpec(
|
|
96
|
+
id="tool_efficiency",
|
|
97
|
+
name="工具调用效率",
|
|
98
|
+
description="工具选择是否合适,参数是否精准",
|
|
99
|
+
weight=1.0,
|
|
100
|
+
primitive_type="score",
|
|
101
|
+
instructions="评估 Agent 选择该工具及提供参数的合理性与效率。",
|
|
102
|
+
criteria=[
|
|
103
|
+
"严重不合理:工具选错、严重语法错误或陷入无效循环。",
|
|
104
|
+
"勉强可用:工具选择次优,或参数冗余繁复。",
|
|
105
|
+
"合理恰当:工具选择符合常规最佳实践,参数清晰。",
|
|
106
|
+
"高效精准:选用最优工具(如 fd/rg 替代 find/grep),执行迅速高效。",
|
|
107
|
+
],
|
|
108
|
+
),
|
|
109
|
+
]
|
|
110
|
+
|
|
111
|
+
# Preset 2: Technical Spec & Architecture Proposal Evaluation
|
|
112
|
+
TECH_SPEC_DIMENSIONS: List[DimensionSpec] = [
|
|
113
|
+
DimensionSpec(
|
|
114
|
+
id="completeness",
|
|
115
|
+
name="方案完整性",
|
|
116
|
+
description="架构设计、接口、数据流、边界条件是否完备",
|
|
117
|
+
weight=1.5,
|
|
118
|
+
primitive_type="score",
|
|
119
|
+
instructions="评估文档中技术方案的完整度,是否覆盖了背景、架构、接口、异常处理与数据流。",
|
|
120
|
+
criteria=[
|
|
121
|
+
"严重缺失:只有碎片化想法,缺乏基本架构和关键流程说明。",
|
|
122
|
+
"部分完备:具备主干流程,但缺失异常分支、边界情况或关键参数定义。",
|
|
123
|
+
"基本完备:核心架构、主要接口和部署说明完整,覆盖主要场景。",
|
|
124
|
+
"极为详尽:涵盖正常流、异常流、监控报警、容灾与回滚机制,边界清晰。",
|
|
125
|
+
],
|
|
126
|
+
),
|
|
127
|
+
DimensionSpec(
|
|
128
|
+
id="feasibility",
|
|
129
|
+
name="技术可行性",
|
|
130
|
+
description="方案是否切合实际技术栈、资源限制与落地可行度",
|
|
131
|
+
weight=1.5,
|
|
132
|
+
primitive_type="score",
|
|
133
|
+
instructions="评估方案在当前技术栈和资源限制下的落地可行性与工程复杂度。",
|
|
134
|
+
criteria=[
|
|
135
|
+
"不可行:存在根本性架构冲突、无法满足的依赖或巨大技术硬伤。",
|
|
136
|
+
"风险较高:概念成立但工程复杂度过高,维护成本与性能瓶颈显著。",
|
|
137
|
+
"切实可行:符合主流技术栈与团队能力,易于落地与渐进式重构。",
|
|
138
|
+
"优雅可靠:轻量化设计,充分利用既有基础设施,扩展性与稳定性俱佳。",
|
|
139
|
+
],
|
|
140
|
+
),
|
|
141
|
+
DimensionSpec(
|
|
142
|
+
id="risk_control",
|
|
143
|
+
name="风险与回滚设计",
|
|
144
|
+
description="是否具备容灾、数据备份、发布验证与快速回滚能力",
|
|
145
|
+
weight=1.2,
|
|
146
|
+
primitive_type="score",
|
|
147
|
+
instructions="评估方案是否包含了充分的风险识别、灰度发布、回滚方案与数据保护措施。",
|
|
148
|
+
criteria=[
|
|
149
|
+
"零风险意识:没有提及任何可能失败的场景或回滚手段。",
|
|
150
|
+
"提及粗浅:仅有一两句口号式说明,缺乏具体可执行的回滚步骤。",
|
|
151
|
+
"具备预案:包含明确的失败判定标准和回滚操作流程。",
|
|
152
|
+
"防御完备:包含自动化健康检查、灰度切流、数据备份验证与零停机回滚方案。",
|
|
153
|
+
],
|
|
154
|
+
),
|
|
155
|
+
DimensionSpec(
|
|
156
|
+
id="clarity",
|
|
157
|
+
name="结构清晰度",
|
|
158
|
+
description="逻辑层次、术语准确性与阅读体验",
|
|
159
|
+
weight=1.0,
|
|
160
|
+
primitive_type="score",
|
|
161
|
+
instructions="评估文档的层次结构、排版、逻辑连贯性与表达清晰度。",
|
|
162
|
+
criteria=[
|
|
163
|
+
"晦涩混乱:结构混乱、前后矛盾、术语模糊。",
|
|
164
|
+
"普通可读:大致能看懂,但结构不够紧凑或部分描述冗余。",
|
|
165
|
+
"条理清晰:结构分明,模块职责清晰,图文或示例恰当。",
|
|
166
|
+
"精炼规范:表达专业精准,重点突出,兼具深度与可读性。",
|
|
167
|
+
],
|
|
168
|
+
),
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
# Preset 3: General Document Quality
|
|
172
|
+
GENERAL_DOC_DIMENSIONS: List[DimensionSpec] = [
|
|
173
|
+
DimensionSpec(
|
|
174
|
+
id="clarity",
|
|
175
|
+
name="清晰度",
|
|
176
|
+
description="表述是否通顺易懂、条理是否清晰",
|
|
177
|
+
weight=1.0,
|
|
178
|
+
primitive_type="score",
|
|
179
|
+
instructions="评估文档的表达清晰度与条理性。",
|
|
180
|
+
criteria=[
|
|
181
|
+
"难以理解,逻辑断层严重。",
|
|
182
|
+
"基本可读,但存在较多模糊表述。",
|
|
183
|
+
"清晰通顺,逻辑结构完整。",
|
|
184
|
+
"极为生动精炼,表达精准到位。",
|
|
185
|
+
],
|
|
186
|
+
),
|
|
187
|
+
DimensionSpec(
|
|
188
|
+
id="completeness",
|
|
189
|
+
name="完整度",
|
|
190
|
+
description="内容是否全面,有无论述缺失",
|
|
191
|
+
weight=1.0,
|
|
192
|
+
primitive_type="score",
|
|
193
|
+
instructions="评估文档覆盖主题的完整程度。",
|
|
194
|
+
criteria=[
|
|
195
|
+
"内容严重残缺,核心论点无支撑。",
|
|
196
|
+
"覆盖了部分要点,但关键支撑材料不足。",
|
|
197
|
+
"覆盖主要方面,论述较为充分。",
|
|
198
|
+
"详实全面,论证严密且有深度。",
|
|
199
|
+
],
|
|
200
|
+
),
|
|
201
|
+
DimensionSpec(
|
|
202
|
+
id="actionability",
|
|
203
|
+
name="行动指导性",
|
|
204
|
+
description="读者能否依据文档采取具体动作或决策",
|
|
205
|
+
weight=1.0,
|
|
206
|
+
primitive_type="score",
|
|
207
|
+
instructions="评估文档对后续行动或决策的指导价值。",
|
|
208
|
+
criteria=[
|
|
209
|
+
"纯务虚或模糊,无法指导任何具体行动。",
|
|
210
|
+
"有少量建议,但缺乏可落地的明确步骤。",
|
|
211
|
+
"具备明确的行动指引与待办项。",
|
|
212
|
+
"步骤详尽、可直接依照执行并具备明确的预期结果与验证手段。",
|
|
213
|
+
],
|
|
214
|
+
),
|
|
215
|
+
]
|
|
216
|
+
|
|
217
|
+
PRESETS: Dict[str, List[DimensionSpec]] = {
|
|
218
|
+
"agent-action": AGENT_ACTION_DIMENSIONS,
|
|
219
|
+
"tech-spec": TECH_SPEC_DIMENSIONS,
|
|
220
|
+
"general": GENERAL_DOC_DIMENSIONS,
|
|
221
|
+
}
|
agent_guard/guard.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Claude Code PreToolUse Hook Handler.
|
|
2
|
+
|
|
3
|
+
Inspects incoming tool calls from Claude Code via stdin, evaluates them
|
|
4
|
+
using TypeSafe System One against safety and policy rules, and returns
|
|
5
|
+
the official Claude Code hook response format to stdout.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import sys
|
|
11
|
+
from typing import Any, Dict, Optional
|
|
12
|
+
from agent_guard.client import GuardClient
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def handle_hook_input(
|
|
16
|
+
payload_json: str,
|
|
17
|
+
api_key: Optional[str] = None,
|
|
18
|
+
mock: bool = False,
|
|
19
|
+
audit_log: Optional[str] = None,
|
|
20
|
+
) -> Dict[str, Any]:
|
|
21
|
+
"""Process a raw JSON payload from Claude Code's PreToolUse hook.
|
|
22
|
+
|
|
23
|
+
Args:
|
|
24
|
+
payload_json: The raw JSON string from stdin.
|
|
25
|
+
api_key: Optional TypeSafe API Key override.
|
|
26
|
+
mock: Force mock/heuristic mode without calling network.
|
|
27
|
+
audit_log: Optional file path to append audit log entries.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
The official Claude Code hook response dictionary.
|
|
31
|
+
"""
|
|
32
|
+
try:
|
|
33
|
+
data = json.loads(payload_json)
|
|
34
|
+
except json.JSONDecodeError as e:
|
|
35
|
+
# If payload is invalid JSON, fail safe by denying or reporting error
|
|
36
|
+
return {
|
|
37
|
+
"hookSpecificOutput": {
|
|
38
|
+
"hookEventName": "PreToolUse",
|
|
39
|
+
"permissionDecision": "deny",
|
|
40
|
+
"permissionDecisionReason": f"PreToolUse Hook 收到无效的 JSON 输入: {e}",
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
tool_name = data.get("tool_name", "UnknownTool")
|
|
45
|
+
tool_input = data.get("tool_input", {})
|
|
46
|
+
cwd = data.get("cwd", os.getcwd())
|
|
47
|
+
session_id = data.get("session_id", "")
|
|
48
|
+
tool_use_id = data.get("tool_use_id", "")
|
|
49
|
+
|
|
50
|
+
# Evaluate via GuardClient
|
|
51
|
+
client = GuardClient(api_key=api_key)
|
|
52
|
+
eval_result = client.evaluate_tool_call(
|
|
53
|
+
tool_name=tool_name,
|
|
54
|
+
tool_input=tool_input,
|
|
55
|
+
cwd=cwd,
|
|
56
|
+
mock=mock,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
decision = eval_result.get("decision", "allow")
|
|
60
|
+
reason = eval_result.get("reason", "通过安全评估")
|
|
61
|
+
scores = eval_result.get("scores", {})
|
|
62
|
+
|
|
63
|
+
# Map internal decision to official Claude Code permissionDecision
|
|
64
|
+
# Claude Code accepts: "allow", "deny", "ask"
|
|
65
|
+
permission_decision = decision
|
|
66
|
+
|
|
67
|
+
hook_response = {
|
|
68
|
+
"hookSpecificOutput": {
|
|
69
|
+
"hookEventName": "PreToolUse",
|
|
70
|
+
"permissionDecision": permission_decision,
|
|
71
|
+
"permissionDecisionReason": f"[TypeSafe Guard] {reason}",
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
# Audit Logging (default to ~/.claude/agent-guard.log)
|
|
76
|
+
env_log = os.getenv("AGENT_GUARD_LOG")
|
|
77
|
+
if audit_log:
|
|
78
|
+
log_path = audit_log
|
|
79
|
+
elif env_log:
|
|
80
|
+
log_path = None if env_log.lower() in ("none", "off", "0") else env_log
|
|
81
|
+
else:
|
|
82
|
+
log_path = os.path.expanduser("~/.claude/agent-guard.log")
|
|
83
|
+
|
|
84
|
+
if log_path:
|
|
85
|
+
_write_audit_log(
|
|
86
|
+
log_path=log_path,
|
|
87
|
+
session_id=session_id,
|
|
88
|
+
tool_name=tool_name,
|
|
89
|
+
tool_input=tool_input,
|
|
90
|
+
decision=permission_decision,
|
|
91
|
+
reason=reason,
|
|
92
|
+
scores=scores,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
return hook_response
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _write_audit_log(
|
|
99
|
+
log_path: str,
|
|
100
|
+
session_id: str,
|
|
101
|
+
tool_name: str,
|
|
102
|
+
tool_input: Any,
|
|
103
|
+
decision: str,
|
|
104
|
+
reason: str,
|
|
105
|
+
scores: Dict[str, Any],
|
|
106
|
+
):
|
|
107
|
+
"""Write an audit entry in JSONL format."""
|
|
108
|
+
import time
|
|
109
|
+
|
|
110
|
+
entry = {
|
|
111
|
+
"timestamp": time.time(),
|
|
112
|
+
"time_str": time.strftime("%Y-%m-%d %H:%M:%S"),
|
|
113
|
+
"session_id": session_id,
|
|
114
|
+
"tool_name": tool_name,
|
|
115
|
+
"tool_input": tool_input,
|
|
116
|
+
"decision": decision,
|
|
117
|
+
"reason": reason,
|
|
118
|
+
"scores": scores,
|
|
119
|
+
}
|
|
120
|
+
try:
|
|
121
|
+
os.makedirs(os.path.dirname(os.path.abspath(log_path)), exist_ok=True)
|
|
122
|
+
with open(log_path, "a", encoding="utf-8") as f:
|
|
123
|
+
f.write(json.dumps(entry, ensure_ascii=False) + "\n")
|
|
124
|
+
except Exception:
|
|
125
|
+
pass # Never let audit log failure break the hook flow
|
agent_guard/reporter.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Rich terminal formatting and reporting for Agent Guard."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
from rich.panel import Panel
|
|
7
|
+
from rich.table import Table
|
|
8
|
+
from rich.text import Text
|
|
9
|
+
|
|
10
|
+
console = Console()
|
|
11
|
+
err_console = Console(stderr=True)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def render_evaluation_result(
|
|
15
|
+
tool_name: str,
|
|
16
|
+
tool_input: Dict[str, Any],
|
|
17
|
+
eval_result: Dict[str, Any],
|
|
18
|
+
title: str = "TypeSafe Agent Tool Guard 评估报告",
|
|
19
|
+
):
|
|
20
|
+
"""Render a beautiful terminal report for a tool evaluation."""
|
|
21
|
+
decision = eval_result.get("decision", "allow")
|
|
22
|
+
reason = eval_result.get("reason", "")
|
|
23
|
+
scores = eval_result.get("scores", {})
|
|
24
|
+
is_live = eval_result.get("is_live", False)
|
|
25
|
+
fallback_notice = eval_result.get("fallback_notice")
|
|
26
|
+
|
|
27
|
+
# Determine status style
|
|
28
|
+
if decision == "allow":
|
|
29
|
+
badge = "[bold white on green] ALLOW 允许执行 [/]"
|
|
30
|
+
border_style = "green"
|
|
31
|
+
elif decision == "ask":
|
|
32
|
+
badge = "[bold black on yellow] ASK 需人工确认 [/]"
|
|
33
|
+
border_style = "yellow"
|
|
34
|
+
else:
|
|
35
|
+
badge = "[bold white on red] DENY 拦截阻止 [/]"
|
|
36
|
+
border_style = "red"
|
|
37
|
+
|
|
38
|
+
# Input details table
|
|
39
|
+
input_str = json.dumps(tool_input, ensure_ascii=False, indent=2)
|
|
40
|
+
|
|
41
|
+
content_table = Table(show_header=False, box=None, padding=(0, 1))
|
|
42
|
+
content_table.add_column("Key", style="bold cyan", width=12)
|
|
43
|
+
content_table.add_column("Value")
|
|
44
|
+
|
|
45
|
+
content_table.add_row("目标工具", f"[bold magenta]{tool_name}[/]")
|
|
46
|
+
content_table.add_row("调用参数", f"[dim]{input_str}[/dim]")
|
|
47
|
+
content_table.add_row("判定决策", badge)
|
|
48
|
+
content_table.add_row("判定原因", f"[bold]{reason}[/bold]")
|
|
49
|
+
|
|
50
|
+
# Dimension Scores table
|
|
51
|
+
score_table = Table(title="多维度评定指标", show_header=True, header_style="bold blue")
|
|
52
|
+
score_table.add_column("维度", style="bold")
|
|
53
|
+
score_table.add_column("指标值", justify="center")
|
|
54
|
+
score_table.add_column("说明", style="dim")
|
|
55
|
+
|
|
56
|
+
risk_score = scores.get("risk_score", 0)
|
|
57
|
+
violation_prob = scores.get("violation_prob", 0.0)
|
|
58
|
+
confidence = scores.get("confidence", 1.0)
|
|
59
|
+
|
|
60
|
+
# Risk level styling
|
|
61
|
+
risk_colors = ["green", "cyan", "yellow", "red"]
|
|
62
|
+
idx = max(0, min(int(round(risk_score)), 3))
|
|
63
|
+
risk_color = risk_colors[idx]
|
|
64
|
+
risk_labels = ["0 - 无害只读", "1 - 受控修改", "2 - 中度风险/关键变动", "3 - 高危不可逆/严重破坏"]
|
|
65
|
+
|
|
66
|
+
score_table.add_row(
|
|
67
|
+
"破坏性风险 (Score)",
|
|
68
|
+
f"[{risk_color}]{risk_score:.2f} / 3[/{risk_color}]",
|
|
69
|
+
risk_labels[idx],
|
|
70
|
+
)
|
|
71
|
+
score_table.add_row(
|
|
72
|
+
"违规概率 (Noul)",
|
|
73
|
+
f"{violation_prob:.1%}",
|
|
74
|
+
"违反安全原则或不可逆操作的概率",
|
|
75
|
+
)
|
|
76
|
+
score_table.add_row(
|
|
77
|
+
"置信度 (Confidence)",
|
|
78
|
+
f"{confidence:.1%}",
|
|
79
|
+
"模型判定分布确定性",
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# Engine source
|
|
83
|
+
source_tag = "[bold green]TypeSafe Jev (System One)[/]" if is_live else "[bold yellow]本地安全启发规则 (Fallback)[/]"
|
|
84
|
+
footer_text = f"评判引擎: {source_tag}"
|
|
85
|
+
if fallback_notice:
|
|
86
|
+
footer_text += f" | [dim]{fallback_notice}[/dim]"
|
|
87
|
+
|
|
88
|
+
console.print()
|
|
89
|
+
console.print(
|
|
90
|
+
Panel(
|
|
91
|
+
content_table,
|
|
92
|
+
title=f"[bold]{title}[/bold]",
|
|
93
|
+
subtitle=footer_text,
|
|
94
|
+
border_style=border_style,
|
|
95
|
+
padding=(1, 2),
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
console.print(score_table)
|
|
99
|
+
console.print()
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cmd-guard
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: TypeSafe AI driven agent tool execution guard for Claude Code hooks
|
|
5
|
+
Keywords: claude-code,security,agent,llm-guard,hooks,ai-safety
|
|
6
|
+
Author: NieXi
|
|
7
|
+
Author-email: NieXi <xilemon3@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Environment :: Console
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Security
|
|
17
|
+
Requires-Dist: pydantic>=2.13.5
|
|
18
|
+
Requires-Dist: rich>=15.0.0
|
|
19
|
+
Requires-Dist: typer>=0.27.2
|
|
20
|
+
Requires-Dist: typesafe-sdk>=0.6.0
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Project-URL: Homepage, https://github.com/NieXi/agent-guard
|
|
23
|
+
Project-URL: Repository, https://github.com/NieXi/agent-guard
|
|
24
|
+
Project-URL: Issues, https://github.com/NieXi/agent-guard/issues
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# cmd-guard · TypeSafe Agent Tool Guard
|
|
28
|
+
|
|
29
|
+
基于 **TypeSafe AI (System One / Jev)** 的 Agent 工具调用安全合规守卫与评估器,专为 **Claude Code `PreToolUse` Hook** 深度定制。
|
|
30
|
+
|
|
31
|
+
在 Agent(Claude Code 等)执行工具调用(如 `Bash` 命令、`Write`、`Edit` 文件编辑)前进行毫秒级实时审查,多维度判定其**破坏性风险**与**规则合规性**,精准拦截破坏性删除、强制回滚与越权敏感操作。
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## 核心特性
|
|
36
|
+
|
|
37
|
+
- 🛡️ **Claude Code 原生 Hook 对齐**:完全支持官方 `PreToolUse` 交互协议,返回标准 `hookSpecificOutput`(`allow` / `deny` / `ask`)。
|
|
38
|
+
- 🧠 **TypeSafe System One 驱动**:
|
|
39
|
+
- **破坏性风险 (`destructive_risk` - Score)**:四级阶梯场景判定(只读 ➔ 受控常规修改 ➔ 服务重启/中度风险 ➔ 破坏性/不可逆高危)。
|
|
40
|
+
- **安全守则合规 (`policy_violation` - Noul)**:二元概率判断是否触犯安全红线。
|
|
41
|
+
- **执行裁决 (`decision` - Choice)**:`allow`(直接放行)、`ask`(需人工确认)、`deny`(拦截阻止)。
|
|
42
|
+
- ⚡ **Fail-Safe 安全保护**:在未配置 Key 或遭遇格式异常时自动执行安全兜底,坚决防止未知命令静默绕过。支持离线 `--mock` 快速测试与演示。
|
|
43
|
+
- 📊 **Rich 彩色终端报告**:提供直观的命令调试面板,展示各维度指标、概率分布与决策依据。
|
|
44
|
+
- 📝 **审计日志支持**:配置 `AGENT_GUARD_LOG` 环境变量可持久化所有工具调用的审核轨迹(JSONL 格式)。
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## 安装方式
|
|
49
|
+
|
|
50
|
+
推荐使用 `uv tool` 全局安装(环境隔离且自动注入 PATH):
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# 推荐方式
|
|
54
|
+
uv tool install cmd-guard
|
|
55
|
+
|
|
56
|
+
# 或使用 pipx
|
|
57
|
+
pipx install cmd-guard
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
> PyPI 项目页:<https://pypi.org/project/cmd-guard/> · GitHub 仓库:<https://github.com/NieXi/agent-guard>
|
|
61
|
+
>
|
|
62
|
+
> 安装后的命令名为 `agent-guard`(PyPI 发行名与命令名不同)。
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## 快速接入 Claude Code
|
|
67
|
+
|
|
68
|
+
### 1. 查看配置建议
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
agent-guard setup
|
|
72
|
+
# 或源码开发时使用
|
|
73
|
+
uv run agent-guard setup
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 2. 添加到 Claude Code 配置
|
|
77
|
+
|
|
78
|
+
在你的全局配置 `~/.claude/settings.json`(对所有项目生效)或项目根目录 `.claude/settings.json` 中添加:
|
|
79
|
+
|
|
80
|
+
```json
|
|
81
|
+
{
|
|
82
|
+
"hooks": {
|
|
83
|
+
"PreToolUse": [
|
|
84
|
+
{
|
|
85
|
+
"matcher": "Bash|Write|Edit|NotebookEdit|mcp__.*",
|
|
86
|
+
"command": "agent-guard hook"
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
- **`matcher`**:指定需要拦截审核的工具(推荐拦截 `Bash|Write|Edit|NotebookEdit|mcp__.*`,覆盖终端命令、文件写入以及全量 MCP 工具)。
|
|
94
|
+
- **`command`**:Claude Code 在执行这些工具前,会自动将调用参数经 `stdin` 喂给 `agent-guard hook`。
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## CLI 命令使用
|
|
99
|
+
|
|
100
|
+
### 1. 手动测试评估一条命令
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
# 测试只读安全命令 -> 输出 ALLOW
|
|
104
|
+
agent-guard check --cmd "git status"
|
|
105
|
+
|
|
106
|
+
# 测试高危破坏性命令 -> 输出 DENY 并说明原因
|
|
107
|
+
agent-guard check --cmd "git reset --hard HEAD~1"
|
|
108
|
+
|
|
109
|
+
# 测试涉及服务的变动 -> 输出 ASK 建议人工确认
|
|
110
|
+
agent-guard check --cmd "docker compose down"
|
|
111
|
+
|
|
112
|
+
# 测试敏感系统文件篡改 -> 输出 DENY 拦截
|
|
113
|
+
agent-guard check --tool Write --input '{"file_path": "/etc/sudoers", "content": "test"}'
|
|
114
|
+
|
|
115
|
+
# 离线模拟测试(无需 API Key)
|
|
116
|
+
agent-guard check --mock --cmd "git reset --hard HEAD~1"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### 2. 模拟 Hook 管道输入 (stdin)
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
echo '{"hook_event_name": "PreToolUse", "tool_name": "Bash", "tool_input": {"command": "git diff"}}' | agent-guard hook
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
输出:
|
|
126
|
+
```json
|
|
127
|
+
{
|
|
128
|
+
"hookSpecificOutput": {
|
|
129
|
+
"hookEventName": "PreToolUse",
|
|
130
|
+
"permissionDecision": "allow",
|
|
131
|
+
"permissionDecisionReason": "[TypeSafe Guard] TypeSafe 评估安全通过 (风险等级: 0/3, 置信度: 98.0%)"
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### 3. 配置 TypeSafe API Key
|
|
137
|
+
|
|
138
|
+
支持以下两种配置方式(推荐配置文件方式,免除子进程环境变量丢失困扰):
|
|
139
|
+
|
|
140
|
+
**方式一:写入全局配置文件 `~/.agentguardrc`(推荐)**
|
|
141
|
+
```bash
|
|
142
|
+
echo 'TYPESAFE_API_KEY="your_api_key_here"' > ~/.agentguardrc
|
|
143
|
+
```
|
|
144
|
+
(亦支持在项目根目录下创建 `./.agentguardrc` 实现项目级隔离配置)
|
|
145
|
+
|
|
146
|
+
**方式二:配置系统环境变量**
|
|
147
|
+
```bash
|
|
148
|
+
export TYPESAFE_API_KEY="your_api_key_here"
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## 自动化测试
|
|
154
|
+
|
|
155
|
+
项目已配备完善的测试套件:
|
|
156
|
+
```bash
|
|
157
|
+
uv run pytest
|
|
158
|
+
```
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
agent_guard/__init__.py,sha256=Ddn0CySgvYdysnns7gFlvnbMYdziJhGbpnhyQSdVvg0,65
|
|
2
|
+
agent_guard/cli.py,sha256=BNQtgWWXxxWm2Ok1gFIb0PFgMKP1Q6J1625662smbmc,6087
|
|
3
|
+
agent_guard/client.py,sha256=UkXD2668sht6XHYnNAtt54zRFoUp1vdABDkquPG_XIQ,7998
|
|
4
|
+
agent_guard/config.py,sha256=H2aVgWDULI_TWHR5MspXQSj6a3e6oHXB6MYBdIQO_Ks,3080
|
|
5
|
+
agent_guard/dimensions.py,sha256=yWtB8a2E8irTvUqBLKjfrlaCd2d3Z5-5s4SNpSuR1GM,9891
|
|
6
|
+
agent_guard/guard.py,sha256=wz2SEjMowYqw1s2YEvkEux7pbqeI3TluX2ccc2UmVvY,3821
|
|
7
|
+
agent_guard/reporter.py,sha256=Fga-8L61YoNtkGmNl5M1Tj1a3uG9TFpBwBTCneJL0-M,3433
|
|
8
|
+
cmd_guard-0.1.0.dist-info/WHEEL,sha256=dLc891cCSocyqFranlvUIXpxBvrnBhOtcZ5dwdLH49k,81
|
|
9
|
+
cmd_guard-0.1.0.dist-info/entry_points.txt,sha256=CIramj_X9FychoSVOibtMyzBLKrTWa0Mvd_FMqHPVDA,53
|
|
10
|
+
cmd_guard-0.1.0.dist-info/METADATA,sha256=bo5zV_sXhBz8mXmKUWip09nUW8z8MHtrF9TP3rC0ed4,5333
|
|
11
|
+
cmd_guard-0.1.0.dist-info/RECORD,,
|