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.
@@ -0,0 +1,3 @@
1
+ from agent_guard.cli import app, main
2
+
3
+ __all__ = ["app", "main"]
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
@@ -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,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.12.16
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ agent-guard = agent_guard.cli:app
3
+