agent-moss 0.2.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.
Files changed (55) hide show
  1. agent_moss/__init__.py +3 -0
  2. agent_moss/__version__.py +1 -0
  3. agent_moss/adapters/__init__.py +7 -0
  4. agent_moss/adapters/observable.py +104 -0
  5. agent_moss/cli.py +130 -0
  6. agent_moss/engine/__init__.py +18 -0
  7. agent_moss/engine/analyzer.py +288 -0
  8. agent_moss/engine/coordinator.py +261 -0
  9. agent_moss/engine/heuristic.py +213 -0
  10. agent_moss/engine/llm_analyzer.py +250 -0
  11. agent_moss/engine/logic_rules.py +219 -0
  12. agent_moss/engine/script_analyzer.py +347 -0
  13. agent_moss/engine/skill_engine.py +195 -0
  14. agent_moss/engine/types.py +48 -0
  15. agent_moss/infra/__init__.py +0 -0
  16. agent_moss/infra/config.json +31 -0
  17. agent_moss/infra/config.py +372 -0
  18. agent_moss/infra/llm_client.py +215 -0
  19. agent_moss/infra/logging.py +95 -0
  20. agent_moss/infra/parsers.py +249 -0
  21. agent_moss/infra/policy_cache.py +175 -0
  22. agent_moss/infra/prompt_templates.py +130 -0
  23. agent_moss/profiles/__init__.py +52 -0
  24. agent_moss/profiles/base.py +106 -0
  25. agent_moss/profiles/linux.py +339 -0
  26. agent_moss/profiles/windows.py +258 -0
  27. agent_moss/rules/user_rules.json +77 -0
  28. agent_moss/server/__init__.py +1 -0
  29. agent_moss/server/app.py +55 -0
  30. agent_moss/server/middleware.py +32 -0
  31. agent_moss/server/models.py +50 -0
  32. agent_moss/server/routes.py +83 -0
  33. agent_moss/server/socket_server.py +40 -0
  34. agent_moss/skills/browser_web_access_guard.md +31 -0
  35. agent_moss/skills/data_exfiltration_guard.md +33 -0
  36. agent_moss/skills/email_operation_guard.md +30 -0
  37. agent_moss/skills/file_access_guard.md +33 -0
  38. agent_moss/skills/general_tool_risk_guard.md +33 -0
  39. agent_moss/skills/intent_deviation_guard.md +39 -0
  40. agent_moss/skills/lateral_movement_guard.md +31 -0
  41. agent_moss/skills/persistence_backdoor_guard.md +32 -0
  42. agent_moss/skills/resource_exhaustion_guard.md +32 -0
  43. agent_moss/skills/script_execution_guard.md +37 -0
  44. agent_moss/skills/skill_installation_guard.md +30 -0
  45. agent_moss/skills/supply_chain_guard.md +31 -0
  46. agent_moss/templates/policy_mapping.md +87 -0
  47. agent_moss/templates/prompt1_template.txt +62 -0
  48. agent_moss/templates/prompt2_template.txt +45 -0
  49. agent_moss/templates/security_judge_template.txt +71 -0
  50. agent_moss-0.2.0.dist-info/METADATA +432 -0
  51. agent_moss-0.2.0.dist-info/RECORD +55 -0
  52. agent_moss-0.2.0.dist-info/WHEEL +5 -0
  53. agent_moss-0.2.0.dist-info/entry_points.txt +2 -0
  54. agent_moss-0.2.0.dist-info/licenses/LICENSE +21 -0
  55. agent_moss-0.2.0.dist-info/top_level.txt +1 -0
agent_moss/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """AgentMoss - Multi-layer security analysis engine for AI agents."""
2
+
3
+ __version__ = "0.2.0"
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
@@ -0,0 +1,7 @@
1
+ """Adapters for AgentMoss — 将 AgentOS 可观测服务数据转换为标准格式"""
2
+
3
+ from .observable import ObservableAdapter
4
+
5
+ __all__ = [
6
+ "ObservableAdapter",
7
+ ]
@@ -0,0 +1,104 @@
1
+ """Adapter for AgentOS observability service syscall data.
2
+
3
+ 将 AgentOS 可观测服务采集的 syscall 数据转换为标准 AnalyzeRequest 格式。
4
+ """
5
+
6
+ import logging
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class ObservableAdapter:
12
+ """将 AgentOS 可观测服务 syscall 数据转换为标准 AnalyzeRequest
13
+
14
+ 输入格式(来自可观测服务):
15
+ {
16
+ "agent_id": "agent-001",
17
+ "session_id": "session-abc",
18
+ "sandbox_id": "sb-001",
19
+ "os_type": "linux",
20
+ "cwd": "/home/user/project",
21
+ "prompt": "分析日志文件",
22
+ "current_action": {
23
+ "tool": "bash",
24
+ "command": "cat /var/log/app.log",
25
+ "description": "读取应用日志"
26
+ },
27
+ "action_history": [{"tool": "read", "command": "ls -la", "output": "..."}],
28
+ "trace_id": "trace-xyz"
29
+ }
30
+
31
+ 输出格式:标准 AnalyzeRequest dict
32
+ """
33
+
34
+ def adapt(self, raw_data: dict) -> dict:
35
+ """
36
+ 将可观测服务数据转换为标准 AnalyzeRequest dict。
37
+
38
+ Args:
39
+ raw_data: 可观测服务原始数据
40
+
41
+ Returns:
42
+ dict: 标准 AnalyzeRequest 格式
43
+ """
44
+ session_id = raw_data.get("session_id") or raw_data.get("agent_id", "unknown")
45
+
46
+ # 可观测服务可能使用 current_action 而非 a_next
47
+ action_raw = raw_data.get("a_next") or raw_data.get("current_action", {})
48
+ if isinstance(action_raw, dict):
49
+ a_next = {
50
+ "action_type": action_raw.get("action_type") or action_raw.get("tool", "unknown"),
51
+ "action_detail": action_raw.get("action_detail") or action_raw.get("command", ""),
52
+ }
53
+ else:
54
+ a_next = {"action_type": "unknown", "action_detail": ""}
55
+
56
+ # 可观测服务可能使用 action_history 中的 tool/command 格式
57
+ history_raw = raw_data.get("action_history", [])
58
+ action_history = []
59
+ for h in history_raw:
60
+ if isinstance(h, dict):
61
+ action_history.append({
62
+ "name": h.get("name") or h.get("action_type") or h.get("tool", ""),
63
+ "action_detail": h.get("action_detail") or h.get("command", ""),
64
+ })
65
+
66
+ result = {
67
+ "session_id": session_id,
68
+ "prompt_session": raw_data.get("prompt_session") or raw_data.get("prompt", ""),
69
+ "action_history": action_history,
70
+ "a_next": a_next,
71
+ "reason": raw_data.get("reason") or raw_data.get("current_action", {}).get("description", ""),
72
+ "os_type": raw_data.get("os_type", ""),
73
+ "cwd": raw_data.get("cwd", ""),
74
+ }
75
+
76
+ # 从 current_action.description 补充 reason
77
+ if not result["reason"] and isinstance(action_raw, dict):
78
+ result["reason"] = action_raw.get("description", "")
79
+
80
+ # 传递 metadata
81
+ metadata = raw_data.get("metadata", {})
82
+ if not metadata:
83
+ metadata = {
84
+ k: raw_data[k] for k in ("agent_id", "sandbox_id", "trace_id", "sandbox_id")
85
+ if k in raw_data and k not in result
86
+ }
87
+ if metadata:
88
+ result["metadata"] = metadata
89
+
90
+ logger.debug("Adapted request for session %s (os_type=%s)", session_id, result.get("os_type", "auto"))
91
+ return result
92
+
93
+
94
+ def is_raw_format(data: dict) -> bool:
95
+ """判断数据是否为原始可观测服务格式(需要适配器转换)"""
96
+ # 如果包含 current_action 或缺少 a_next,认为是原始格式
97
+ if "current_action" in data:
98
+ return True
99
+ if "a_next" not in data:
100
+ return True
101
+ # 如果 session_id 缺失但 agent_id 存在,也认为是原始格式
102
+ if "session_id" not in data and "agent_id" in data:
103
+ return True
104
+ return False
agent_moss/cli.py ADDED
@@ -0,0 +1,130 @@
1
+ #!/usr/bin/env python3
2
+ """Command-line interface for AgentMoss (v2: with --mode socket support)."""
3
+
4
+ import argparse
5
+ import json
6
+ import os
7
+ import sys
8
+ from pathlib import Path
9
+
10
+
11
+ def cmd_init(args):
12
+ """Generate an example input.json template (v2: includes os_type and cwd)."""
13
+ template = {
14
+ "session_id": "my-session-001",
15
+ "prompt_session": "Read /var/log/app.log and analyze errors",
16
+ "action_history": [],
17
+ "a_next": {
18
+ "action_type": "bash",
19
+ "action_detail": "cat /var/log/app.log"
20
+ },
21
+ "reason": "Need to read log file for error analysis",
22
+ "os_type": "",
23
+ "cwd": "/home/user/project",
24
+ }
25
+ output = args.output or "input.json"
26
+ with open(output, "w", encoding="utf-8") as f:
27
+ json.dump(template, f, indent=2, ensure_ascii=False)
28
+ print(f"Template written to {output}")
29
+
30
+
31
+ def cmd_analyze(args):
32
+ """Run security analysis on input JSON."""
33
+ from agent_moss.engine.analyzer import analyze
34
+ from agent_moss.profiles import get_profile
35
+
36
+ with open(args.input, "r", encoding="utf-8") as f:
37
+ data = json.load(f)
38
+
39
+ action_history = data.get("action_history", [])
40
+ a_next = data.get("a_next", {})
41
+
42
+ # OS auto-detect
43
+ os_type = data.get("os_type", "")
44
+ profile = get_profile(os_type)
45
+
46
+ result = analyze(
47
+ session_id=data.get("session_id", "unknown"),
48
+ prompt_session=data.get("prompt_session", ""),
49
+ action_history=action_history,
50
+ a_next=a_next,
51
+ reason=data.get("reason", ""),
52
+ profile=profile,
53
+ )
54
+
55
+ print(json.dumps(result, indent=2, ensure_ascii=False))
56
+
57
+ if not args.no_file:
58
+ session_id = data.get("session_id", "unknown").replace("/", "_")
59
+ out_dir = Path(args.output_dir) if args.output_dir else Path.cwd()
60
+ out_dir.mkdir(parents=True, exist_ok=True)
61
+ if result["decision"] == "Allow":
62
+ policy_path = out_dir / f"{session_id}_policy.toml"
63
+ policy_path.write_text(result.get("policy", ""), encoding="utf-8")
64
+ print(f"Policy written to {policy_path}")
65
+ else:
66
+ deny_path = out_dir / f"{session_id}_deny_reason.txt"
67
+ reason_text = result.get("violated_policy") or result.get("reason", "")
68
+ deny_path.write_text(reason_text, encoding="utf-8")
69
+ print(f"Deny reason written to {deny_path}")
70
+
71
+ return 0 if result["decision"] == "Allow" else 1
72
+
73
+
74
+ def cmd_server(args):
75
+ """Start the HTTP API server (supports http and socket modes)."""
76
+ if args.config:
77
+ os.environ["AGENT_MOSS_CONFIG_PATH"] = args.config
78
+
79
+ from agent_moss.server.app import run_server
80
+
81
+ run_server(
82
+ host=args.host,
83
+ port=args.port,
84
+ mode=args.mode,
85
+ socket_path=args.socket,
86
+ )
87
+ return 0
88
+
89
+
90
+ def main():
91
+ parser = argparse.ArgumentParser(description="AgentMoss security analysis engine")
92
+ subparsers = parser.add_subparsers(dest="command", help="Available commands")
93
+
94
+ init_parser = subparsers.add_parser("init", help="Generate input.json template")
95
+ init_parser.add_argument("--output", "-o", help="Output file path", default="input.json")
96
+
97
+ analyze_parser = subparsers.add_parser("analyze", help="Run security analysis on input JSON")
98
+ analyze_parser.add_argument("input", help="Input JSON file path")
99
+ analyze_parser.add_argument("--output-dir", "-o", help="Output directory for policy/deny files")
100
+ analyze_parser.add_argument("--no-file", action="store_true", help="Only print to stdout, don't write files")
101
+
102
+ server_parser = subparsers.add_parser("server", help="Start HTTP API server")
103
+ server_parser.add_argument("--host", default="127.0.0.1", help="Listen host (TCP mode)")
104
+ server_parser.add_argument("--port", type=int, default=9090, help="Listen port (TCP mode)")
105
+ server_parser.add_argument("--config", "-c", help="Path to config YAML file")
106
+ server_parser.add_argument(
107
+ "--mode", choices=["http", "socket"], default="http",
108
+ help="Server mode: http (TCP) or socket (Unix Domain Socket)",
109
+ )
110
+ server_parser.add_argument(
111
+ "--socket", "-s",
112
+ default="/var/run/agent_moss/agent_moss.sock",
113
+ help="Unix socket path (socket mode)",
114
+ )
115
+
116
+ args = parser.parse_args()
117
+
118
+ if args.command == "init":
119
+ cmd_init(args)
120
+ elif args.command == "analyze":
121
+ sys.exit(cmd_analyze(args))
122
+ elif args.command == "server":
123
+ sys.exit(cmd_server(args))
124
+ else:
125
+ parser.print_help()
126
+ sys.exit(1)
127
+
128
+
129
+ if __name__ == "__main__":
130
+ main()
@@ -0,0 +1,18 @@
1
+ from .coordinator import AgentMossBot, judge_security
2
+ from .heuristic import HeuristicDetector
3
+ from .logic_rules import LogicRulesChecker
4
+ from .llm_analyzer import LLMAnalyzer
5
+ from .skill_engine import SkillEngine
6
+ from .types import HeuristicResult, LogicRuleResult, SecurityJudgment, SkillMatch
7
+
8
+ __all__ = [
9
+ "AgentMossBot",
10
+ "HeuristicDetector",
11
+ "LogicRulesChecker",
12
+ "LLMAnalyzer",
13
+ "SkillEngine",
14
+ "SecurityJudgment",
15
+ "HeuristicResult",
16
+ "LogicRuleResult",
17
+ "SkillMatch",
18
+ ]
@@ -0,0 +1,288 @@
1
+ """主入口函数 — 串联 Step 1-2 的完整 AgentMoss 安全分析流程
2
+
3
+ Step 1: AgentMoss Security Agent 安全判断(三层防御)
4
+ ├── 1.1 启发式静态检测(关键命令 + 注入检测 + 用户敏感规则)
5
+ ├── 1.2 逻辑规则检测(read_before_write + 意图一致性 + 敏感路径 + 危险模式)
6
+ └── 1.3 LLM + Skill 深度分析
7
+ → 如果 Deny → 直接返回 Deny + 原因(结束)
8
+
9
+ Step 2: Allow → 查缓存 / 生成 policy
10
+ ├── 缓存命中 → 使用缓存 policy
11
+ └── 缓存未命中 → LLM 生成 policy + 写入缓存
12
+ → 返回 Allow + policy
13
+ """
14
+
15
+ import tempfile
16
+ import time
17
+ from pathlib import Path
18
+ from typing import TYPE_CHECKING
19
+
20
+ if TYPE_CHECKING:
21
+ from ..profiles.base import OSProfile
22
+
23
+ from ..infra.config import Config, get_default_config, is_policy_gen_enabled
24
+ from ..infra.llm_client import call_llm
25
+ from ..infra.parsers import parse_policy_from_llm
26
+ from ..infra.policy_cache import PolicyCache, get_default_cache
27
+ from ..infra.prompt_templates import get_prompt1
28
+ from ..infra.logging import MossLogger
29
+ from ..profiles import get_profile
30
+ from .coordinator import judge_security
31
+
32
+ # Policy 临时文件输出目录
33
+ POLICY_OUTPUT_DIR = Path(tempfile.gettempdir()) / "agent_moss"
34
+
35
+ # 白名单只读工具的默认最小权限 Policy
36
+ DEFAULT_READONLY_POLICY = """landlock_optional = false
37
+ mount_isolation_fallback = false
38
+
39
+ [path_groups]
40
+ system_binaries = true
41
+ system_libraries = true
42
+ temp_directories = true
43
+ device_files = false
44
+ proc_filesystem = false
45
+ network_config = false
46
+ wsl_paths = false
47
+
48
+ [namespaces]
49
+ mount = true
50
+ pid = false
51
+ network = true
52
+ user = false
53
+
54
+ [resources]
55
+ timeout_secs = 30
56
+ max_memory_bytes = 268435456
57
+
58
+ [environment]
59
+ whitelist = ["PATH", "LANG", "HOME", "USER", "HTTP_PROXY", "HTTPS_PROXY", "http_proxy", "https_proxy", "ALL_PROXY", "all_proxy", "NO_PROXY", "no_proxy"]
60
+ """
61
+
62
+
63
+ def analyze(
64
+ session_id: str,
65
+ prompt_session: str,
66
+ action_history: list[dict[str, object]],
67
+ a_next: dict[str, str],
68
+ reason: str,
69
+ config: Config | None = None,
70
+ cache: PolicyCache | None = None,
71
+ profile: "OSProfile | None" = None,
72
+ ) -> dict[str, str]:
73
+ """
74
+ 分析 agent 的下一步动作是否在安全范围内,并在允许时生成最小 policy。
75
+
76
+ Args:
77
+ session_id: 会话唯一标识
78
+ prompt_session: 用户输入的原始 prompt
79
+ action_history: 历史动作序列
80
+ a_next: 下一步动作,含 action_type 和 action_detail
81
+ reason: 执行该动作的理由
82
+ config: 配置项,为 None 时使用默认配置
83
+ cache: Policy 缓存实例,为 None 时使用默认缓存
84
+ profile: OS Profile(可选,默认自动检测)
85
+
86
+ Returns:
87
+ dict: {"decision": "Allow"|"Deny", "policy": "...", "reason": "...", ...}
88
+ """
89
+ cfg = config or get_default_config()
90
+ os_profile = profile or get_profile()
91
+ moss_log = MossLogger(session_id)
92
+ max_retries = cfg.retry.max_retries
93
+ retry_interval = cfg.retry.retry_interval
94
+
95
+ moss_log.log_input(prompt_session, action_history, a_next, reason)
96
+
97
+ try:
98
+ # ==================== Step 1: AgentMoss Security Agent 安全判断 ====================
99
+ moss_log.start_step("step1_security_judge")
100
+ judgment = judge_security(
101
+ prompt_session=prompt_session,
102
+ action_history=action_history,
103
+ a_next=a_next,
104
+ reason=reason,
105
+ config=cfg,
106
+ profile=os_profile,
107
+ )
108
+
109
+ if not judgment.allowed:
110
+ layers_str = ", ".join(judgment.violated_layers) if judgment.violated_layers else judgment.source
111
+ moss_log.end_step(
112
+ "step1_security_judge",
113
+ detail=f"allowed=False, risk_level={judgment.risk_level}, violated_layers=[{layers_str}]",
114
+ )
115
+ result = {
116
+ "decision": "Deny",
117
+ "policy": "",
118
+ "reason": judgment.reason,
119
+ "violated_policy": f"[{judgment.risk_type}] {judgment.reason}",
120
+ "violated_layers": judgment.violated_layers,
121
+ "risk_level": judgment.risk_level,
122
+ "risk_type": judgment.risk_type,
123
+ "confidence": judgment.confidence,
124
+ }
125
+ moss_log.log_result("Deny", result["reason"])
126
+ return result
127
+
128
+ moss_log.end_step(
129
+ "step1_security_judge",
130
+ detail=f"allowed=True, risk_level={judgment.risk_level}, source={judgment.source}",
131
+ )
132
+
133
+ # ==================== Step 2: Allow → 查缓存 / 生成 policy ====================
134
+ policy_cache = cache or get_default_cache()
135
+ enable_policy_gen = is_policy_gen_enabled()
136
+
137
+ moss_log.start_step("step2_cache_lookup")
138
+ cached_policy = policy_cache.get(session_id, prompt_session)
139
+
140
+ if cached_policy is not None:
141
+ moss_log.end_step("step2_cache_lookup", detail="缓存命中")
142
+ policy_a_next = cached_policy
143
+ _ = _save_policy_to_file(session_id, policy_a_next)
144
+ elif not enable_policy_gen:
145
+ moss_log.end_step("step2_cache_lookup", detail="缓存未命中,Policy 生成已禁用")
146
+ moss_log.start_step("step2_default_policy")
147
+ policy_a_next = DEFAULT_READONLY_POLICY
148
+ policy_cache.put(session_id, prompt_session, policy_a_next)
149
+ _ = _save_policy_to_file(session_id, policy_a_next)
150
+ moss_log.end_step("step2_default_policy", detail="使用默认最小权限 Policy")
151
+ result = {
152
+ "decision": "Allow",
153
+ "policy": policy_a_next,
154
+ "reason": judgment.reason,
155
+ "violated_policy": "",
156
+ "risk_level": judgment.risk_level,
157
+ "risk_type": judgment.risk_type,
158
+ "confidence": judgment.confidence,
159
+ }
160
+ moss_log.log_result("Allow", result["reason"], policy_a_next)
161
+ return result
162
+ else:
163
+ moss_log.end_step("step2_cache_lookup", detail="缓存未命中,启用 LLM 生成")
164
+
165
+ if judgment.source == "whitelist_bypass":
166
+ moss_log.start_step("step2_whitelist_policy")
167
+ policy_a_next = DEFAULT_READONLY_POLICY
168
+ policy_cache.put(session_id, prompt_session, policy_a_next)
169
+ _ = _save_policy_to_file(session_id, policy_a_next)
170
+ moss_log.end_step("step2_whitelist_policy", detail="白名单工具使用预定义最小权限 Policy")
171
+ result = {
172
+ "decision": "Allow",
173
+ "policy": policy_a_next,
174
+ "reason": judgment.reason,
175
+ "violated_policy": "",
176
+ "risk_level": judgment.risk_level,
177
+ "risk_type": judgment.risk_type,
178
+ "confidence": judgment.confidence,
179
+ }
180
+ moss_log.log_result("Allow", result["reason"], policy_a_next)
181
+ return result
182
+
183
+ if cfg.timeout.step_interval > 0:
184
+ time.sleep(cfg.timeout.step_interval)
185
+
186
+ moss_log.start_step("step2_generate_policy")
187
+ prompt1_text = get_prompt1(prompt_session)
188
+ step2_error: Exception | None = None
189
+ policy_a_next = None
190
+
191
+ for attempt in range(1, max_retries + 1):
192
+ try:
193
+ llm_response = call_llm(
194
+ prompt=prompt1_text,
195
+ timeout=cfg.timeout.prompt1_timeout,
196
+ config=cfg.llm,
197
+ )
198
+ policy_a_next = parse_policy_from_llm(llm_response)
199
+ moss_log.log_llm_interaction(
200
+ "step2_generate_policy",
201
+ prompt1_text[:200],
202
+ llm_response[:200],
203
+ )
204
+ policy_cache.put(session_id, prompt_session, policy_a_next)
205
+ _ = _save_policy_to_file(session_id, policy_a_next)
206
+ moss_log.end_step("step2_generate_policy", detail="Policy 生成并缓存成功")
207
+ step2_error = None
208
+ break
209
+ except (TimeoutError, ValueError, Exception) as e:
210
+ step2_error = e
211
+ if attempt < max_retries:
212
+ moss_log.log_error("step2_generate_policy", e)
213
+ moss_log.start_step(f"step2_retry_{attempt}")
214
+ time.sleep(retry_interval)
215
+ moss_log.end_step(
216
+ f"step2_retry_{attempt}",
217
+ detail=f"第 {attempt} 次重试,等待 {retry_interval}s",
218
+ )
219
+ else:
220
+ moss_log.log_error("step2_generate_policy", e)
221
+
222
+ if step2_error is not None or policy_a_next is None:
223
+ if isinstance(step2_error, TimeoutError):
224
+ moss_log.end_step(
225
+ "step2_generate_policy",
226
+ detail=f"超时(已重试 {max_retries} 次)",
227
+ )
228
+ result = {
229
+ "decision": "Deny",
230
+ "policy": "",
231
+ "reason": f"安全检测通过,但策略生成超时(已重试 {max_retries} 次),出于安全考虑拒绝执行",
232
+ "violated_policy": "无法生成策略:LLM 调用超时",
233
+ }
234
+ elif isinstance(step2_error, ValueError):
235
+ moss_log.end_step(
236
+ "step2_generate_policy",
237
+ detail=f"解析失败(已重试 {max_retries} 次): {step2_error}",
238
+ )
239
+ result = {
240
+ "decision": "Deny",
241
+ "policy": "",
242
+ "reason": f"安全检测通过,但策略生成失败(已重试 {max_retries} 次):{step2_error}",
243
+ "violated_policy": "生成的策略格式无效",
244
+ }
245
+ else:
246
+ moss_log.end_step(
247
+ "step2_generate_policy",
248
+ detail=f"异常(已重试 {max_retries} 次): {step2_error}",
249
+ )
250
+ result = {
251
+ "decision": "Deny",
252
+ "policy": "",
253
+ "reason": f"安全检测通过,但策略生成服务异常(已重试 {max_retries} 次):{step2_error}",
254
+ "violated_policy": "无法生成策略:LLM 服务异常",
255
+ }
256
+ moss_log.log_result("Deny", result["reason"])
257
+ return result
258
+
259
+ result = {
260
+ "decision": "Allow",
261
+ "policy": policy_a_next,
262
+ "reason": judgment.reason,
263
+ "violated_policy": "",
264
+ "risk_level": judgment.risk_level,
265
+ "risk_type": judgment.risk_type,
266
+ "confidence": judgment.confidence,
267
+ }
268
+ moss_log.log_result("Allow", result["reason"], policy_a_next)
269
+ return result
270
+
271
+ except Exception as e:
272
+ moss_log.log_error("unknown", e)
273
+ result = {
274
+ "decision": "Deny",
275
+ "policy": "",
276
+ "reason": f"分析过程发生未预期异常:{e}",
277
+ "violated_policy": "未知异常",
278
+ }
279
+ moss_log.log_result("Deny", result["reason"])
280
+ return result
281
+
282
+
283
+ def _save_policy_to_file(session_id: str, policy_toml: str) -> Path:
284
+ POLICY_OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
285
+ safe_name = session_id.replace("/", "_").replace("\\", "_")
286
+ filepath = POLICY_OUTPUT_DIR / f"{safe_name}.toml"
287
+ _ = filepath.write_text(policy_toml, encoding="utf-8")
288
+ return filepath