langchain-starring 0.1.0__tar.gz
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.
- langchain_starring-0.1.0/PKG-INFO +95 -0
- langchain_starring-0.1.0/README.md +78 -0
- langchain_starring-0.1.0/langchain_starring.egg-info/PKG-INFO +95 -0
- langchain_starring-0.1.0/langchain_starring.egg-info/SOURCES.txt +10 -0
- langchain_starring-0.1.0/langchain_starring.egg-info/dependency_links.txt +1 -0
- langchain_starring-0.1.0/langchain_starring.egg-info/requires.txt +1 -0
- langchain_starring-0.1.0/langchain_starring.egg-info/top_level.txt +2 -0
- langchain_starring-0.1.0/pyproject.toml +30 -0
- langchain_starring-0.1.0/setup.cfg +4 -0
- langchain_starring-0.1.0/starring/__init__.py +7 -0
- langchain_starring-0.1.0/starring/callback.py +131 -0
- langchain_starring-0.1.0/xh_engine.py +217 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: langchain-starring
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LangChain callback that blocks malicious agent tool-call chains (behavior-chain security)
|
|
5
|
+
Author: Star-Ring AI Behavior Security Lab
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/starring-ai-sec/star-ring-agent-sentinel
|
|
8
|
+
Keywords: langchain,agent,security,behavior,llm,callback
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Topic :: Security
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Requires-Dist: langchain-core>=0.2.0
|
|
17
|
+
|
|
18
|
+
# langchain-starring
|
|
19
|
+
|
|
20
|
+
> **A LangChain callback that blocks malicious agent tool-call chains.**
|
|
21
|
+
> Content moderation cares what the Agent *says* — this cares what the Agent *does*.
|
|
22
|
+
|
|
23
|
+
一个 LangChain 回调处理器:在工具执行前累积**行为链**,判定「每一步都合法、串起来就是攻击」的链式攻击(如 `read_sms → send_credentials`),命中即熔断。
|
|
24
|
+
|
|
25
|
+
## 为什么需要它
|
|
26
|
+
|
|
27
|
+
单步安全工具拦不住这条链:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
read_sms ← 读用户短信(客服的常规权限,白名单内)
|
|
31
|
+
send_credentials ← 把验证码发出去
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
每一步都合法,串起来就是验证码外泄。传统基于单步规则的防护是盲区,`langchain-starring` 专注**行为链关联分析**。
|
|
35
|
+
|
|
36
|
+
## 安装
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install langchain-starring
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 用法(零代码修改,一行挂载)
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from langchain.agents import AgentExecutor, create_tool_calling_agent
|
|
46
|
+
from langchain_openai import ChatOpenAI
|
|
47
|
+
from starring import StarRingCallbackHandler
|
|
48
|
+
|
|
49
|
+
agent = create_tool_calling_agent(ChatOpenAI(), tools, prompt)
|
|
50
|
+
|
|
51
|
+
executor = AgentExecutor(
|
|
52
|
+
agent=agent,
|
|
53
|
+
tools=tools,
|
|
54
|
+
# 一行挂载,行为链危险时自动抛 StarRingBlockedError
|
|
55
|
+
callbacks=[StarRingCallbackHandler()],
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
executor.invoke({"input": "帮我查一下退款"})
|
|
59
|
+
# 当 Agent 尝试 read_sms -> send_credentials 时,第二个工具调用抛出异常,验证码不外发
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## 能力
|
|
63
|
+
|
|
64
|
+
| 能力 | 说明 |
|
|
65
|
+
|---|---|
|
|
66
|
+
| 行为链判定 | 累积最近 N 步工具调用,判「组合起来是否危险」 |
|
|
67
|
+
| 参数闸门 | 路径穿越、超额转账、域名黑名单、批量次数 |
|
|
68
|
+
| 未知动作熔断 | 幻觉出的工具名直接拒绝(fail-closed) |
|
|
69
|
+
| 自定义映射 | `action_map` 把自定义工具名映射到标准动作 |
|
|
70
|
+
|
|
71
|
+
## 配置
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
handler = StarRingCallbackHandler(
|
|
75
|
+
max_history=6, # 行为链最多保留 6 步
|
|
76
|
+
action_map={"search_web": "external_http"}, # 工具名映射
|
|
77
|
+
config={"amount_max": 5000, "path_deny": [".."]}, # 参数闸门阈值
|
|
78
|
+
raise_on="block_fuse", # block 和 fuse 都熔断
|
|
79
|
+
)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 与竞品的差异
|
|
83
|
+
|
|
84
|
+
`interven-langchain`、`iris-security`、`ActShield` 等方案主要做**单步**检测(这一句话有没有毒、这个工具危不危险)。`langchain-starring` 的差异化在于:
|
|
85
|
+
|
|
86
|
+
> 抓住「每一步都合法,但串联起来就是攻击」的复杂场景——这是单步规则覆盖不了的盲区。
|
|
87
|
+
|
|
88
|
+
## 更多
|
|
89
|
+
|
|
90
|
+
- 端到端示例:`examples/demo.py`
|
|
91
|
+
- 完整 CLI(离线自检 + 深度体检):[star-ring-agent-sentinel](https://github.com/starring-ai-sec/star-ring-agent-sentinel)
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
MIT © 2026 Star-Ring AI Behavior Security Lab
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# langchain-starring
|
|
2
|
+
|
|
3
|
+
> **A LangChain callback that blocks malicious agent tool-call chains.**
|
|
4
|
+
> Content moderation cares what the Agent *says* — this cares what the Agent *does*.
|
|
5
|
+
|
|
6
|
+
一个 LangChain 回调处理器:在工具执行前累积**行为链**,判定「每一步都合法、串起来就是攻击」的链式攻击(如 `read_sms → send_credentials`),命中即熔断。
|
|
7
|
+
|
|
8
|
+
## 为什么需要它
|
|
9
|
+
|
|
10
|
+
单步安全工具拦不住这条链:
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
read_sms ← 读用户短信(客服的常规权限,白名单内)
|
|
14
|
+
send_credentials ← 把验证码发出去
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
每一步都合法,串起来就是验证码外泄。传统基于单步规则的防护是盲区,`langchain-starring` 专注**行为链关联分析**。
|
|
18
|
+
|
|
19
|
+
## 安装
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install langchain-starring
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 用法(零代码修改,一行挂载)
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from langchain.agents import AgentExecutor, create_tool_calling_agent
|
|
29
|
+
from langchain_openai import ChatOpenAI
|
|
30
|
+
from starring import StarRingCallbackHandler
|
|
31
|
+
|
|
32
|
+
agent = create_tool_calling_agent(ChatOpenAI(), tools, prompt)
|
|
33
|
+
|
|
34
|
+
executor = AgentExecutor(
|
|
35
|
+
agent=agent,
|
|
36
|
+
tools=tools,
|
|
37
|
+
# 一行挂载,行为链危险时自动抛 StarRingBlockedError
|
|
38
|
+
callbacks=[StarRingCallbackHandler()],
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
executor.invoke({"input": "帮我查一下退款"})
|
|
42
|
+
# 当 Agent 尝试 read_sms -> send_credentials 时,第二个工具调用抛出异常,验证码不外发
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## 能力
|
|
46
|
+
|
|
47
|
+
| 能力 | 说明 |
|
|
48
|
+
|---|---|
|
|
49
|
+
| 行为链判定 | 累积最近 N 步工具调用,判「组合起来是否危险」 |
|
|
50
|
+
| 参数闸门 | 路径穿越、超额转账、域名黑名单、批量次数 |
|
|
51
|
+
| 未知动作熔断 | 幻觉出的工具名直接拒绝(fail-closed) |
|
|
52
|
+
| 自定义映射 | `action_map` 把自定义工具名映射到标准动作 |
|
|
53
|
+
|
|
54
|
+
## 配置
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
handler = StarRingCallbackHandler(
|
|
58
|
+
max_history=6, # 行为链最多保留 6 步
|
|
59
|
+
action_map={"search_web": "external_http"}, # 工具名映射
|
|
60
|
+
config={"amount_max": 5000, "path_deny": [".."]}, # 参数闸门阈值
|
|
61
|
+
raise_on="block_fuse", # block 和 fuse 都熔断
|
|
62
|
+
)
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## 与竞品的差异
|
|
66
|
+
|
|
67
|
+
`interven-langchain`、`iris-security`、`ActShield` 等方案主要做**单步**检测(这一句话有没有毒、这个工具危不危险)。`langchain-starring` 的差异化在于:
|
|
68
|
+
|
|
69
|
+
> 抓住「每一步都合法,但串联起来就是攻击」的复杂场景——这是单步规则覆盖不了的盲区。
|
|
70
|
+
|
|
71
|
+
## 更多
|
|
72
|
+
|
|
73
|
+
- 端到端示例:`examples/demo.py`
|
|
74
|
+
- 完整 CLI(离线自检 + 深度体检):[star-ring-agent-sentinel](https://github.com/starring-ai-sec/star-ring-agent-sentinel)
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
MIT © 2026 Star-Ring AI Behavior Security Lab
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: langchain-starring
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LangChain callback that blocks malicious agent tool-call chains (behavior-chain security)
|
|
5
|
+
Author: Star-Ring AI Behavior Security Lab
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/starring-ai-sec/star-ring-agent-sentinel
|
|
8
|
+
Keywords: langchain,agent,security,behavior,llm,callback
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Topic :: Security
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Requires-Dist: langchain-core>=0.2.0
|
|
17
|
+
|
|
18
|
+
# langchain-starring
|
|
19
|
+
|
|
20
|
+
> **A LangChain callback that blocks malicious agent tool-call chains.**
|
|
21
|
+
> Content moderation cares what the Agent *says* — this cares what the Agent *does*.
|
|
22
|
+
|
|
23
|
+
一个 LangChain 回调处理器:在工具执行前累积**行为链**,判定「每一步都合法、串起来就是攻击」的链式攻击(如 `read_sms → send_credentials`),命中即熔断。
|
|
24
|
+
|
|
25
|
+
## 为什么需要它
|
|
26
|
+
|
|
27
|
+
单步安全工具拦不住这条链:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
read_sms ← 读用户短信(客服的常规权限,白名单内)
|
|
31
|
+
send_credentials ← 把验证码发出去
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
每一步都合法,串起来就是验证码外泄。传统基于单步规则的防护是盲区,`langchain-starring` 专注**行为链关联分析**。
|
|
35
|
+
|
|
36
|
+
## 安装
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
pip install langchain-starring
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 用法(零代码修改,一行挂载)
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
from langchain.agents import AgentExecutor, create_tool_calling_agent
|
|
46
|
+
from langchain_openai import ChatOpenAI
|
|
47
|
+
from starring import StarRingCallbackHandler
|
|
48
|
+
|
|
49
|
+
agent = create_tool_calling_agent(ChatOpenAI(), tools, prompt)
|
|
50
|
+
|
|
51
|
+
executor = AgentExecutor(
|
|
52
|
+
agent=agent,
|
|
53
|
+
tools=tools,
|
|
54
|
+
# 一行挂载,行为链危险时自动抛 StarRingBlockedError
|
|
55
|
+
callbacks=[StarRingCallbackHandler()],
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
executor.invoke({"input": "帮我查一下退款"})
|
|
59
|
+
# 当 Agent 尝试 read_sms -> send_credentials 时,第二个工具调用抛出异常,验证码不外发
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## 能力
|
|
63
|
+
|
|
64
|
+
| 能力 | 说明 |
|
|
65
|
+
|---|---|
|
|
66
|
+
| 行为链判定 | 累积最近 N 步工具调用,判「组合起来是否危险」 |
|
|
67
|
+
| 参数闸门 | 路径穿越、超额转账、域名黑名单、批量次数 |
|
|
68
|
+
| 未知动作熔断 | 幻觉出的工具名直接拒绝(fail-closed) |
|
|
69
|
+
| 自定义映射 | `action_map` 把自定义工具名映射到标准动作 |
|
|
70
|
+
|
|
71
|
+
## 配置
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
handler = StarRingCallbackHandler(
|
|
75
|
+
max_history=6, # 行为链最多保留 6 步
|
|
76
|
+
action_map={"search_web": "external_http"}, # 工具名映射
|
|
77
|
+
config={"amount_max": 5000, "path_deny": [".."]}, # 参数闸门阈值
|
|
78
|
+
raise_on="block_fuse", # block 和 fuse 都熔断
|
|
79
|
+
)
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 与竞品的差异
|
|
83
|
+
|
|
84
|
+
`interven-langchain`、`iris-security`、`ActShield` 等方案主要做**单步**检测(这一句话有没有毒、这个工具危不危险)。`langchain-starring` 的差异化在于:
|
|
85
|
+
|
|
86
|
+
> 抓住「每一步都合法,但串联起来就是攻击」的复杂场景——这是单步规则覆盖不了的盲区。
|
|
87
|
+
|
|
88
|
+
## 更多
|
|
89
|
+
|
|
90
|
+
- 端到端示例:`examples/demo.py`
|
|
91
|
+
- 完整 CLI(离线自检 + 深度体检):[star-ring-agent-sentinel](https://github.com/starring-ai-sec/star-ring-agent-sentinel)
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
MIT © 2026 Star-Ring AI Behavior Security Lab
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
xh_engine.py
|
|
4
|
+
langchain_starring.egg-info/PKG-INFO
|
|
5
|
+
langchain_starring.egg-info/SOURCES.txt
|
|
6
|
+
langchain_starring.egg-info/dependency_links.txt
|
|
7
|
+
langchain_starring.egg-info/requires.txt
|
|
8
|
+
langchain_starring.egg-info/top_level.txt
|
|
9
|
+
starring/__init__.py
|
|
10
|
+
starring/callback.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
langchain-core>=0.2.0
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "langchain-starring"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "LangChain callback that blocks malicious agent tool-call chains (behavior-chain security)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.8"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Star-Ring AI Behavior Security Lab" }]
|
|
13
|
+
keywords = ["langchain", "agent", "security", "behavior", "llm", "callback"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Topic :: Security",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
]
|
|
21
|
+
dependencies = [
|
|
22
|
+
"langchain-core>=0.2.0",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[tool.setuptools]
|
|
26
|
+
py-modules = ["xh_engine"]
|
|
27
|
+
packages = ["starring"]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/starring-ai-sec/star-ring-agent-sentinel"
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""StarRingCallbackHandler:LangChain 行为链熔断回调。
|
|
3
|
+
|
|
4
|
+
在工具执行前(`on_tool_start`)累积行为链,交给星环规则引擎判定;
|
|
5
|
+
命中「凭据外泄 / 提权 / 文件外发」等组合链时抛 `StarRingBlockedError`,中断执行。
|
|
6
|
+
|
|
7
|
+
用法(零代码修改,只挂一个回调)::
|
|
8
|
+
|
|
9
|
+
from langchain.agents import AgentExecutor
|
|
10
|
+
from starring import StarRingCallbackHandler
|
|
11
|
+
|
|
12
|
+
executor = AgentExecutor(
|
|
13
|
+
agent=agent, tools=tools,
|
|
14
|
+
callbacks=[StarRingCallbackHandler()],
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
只依赖 `langchain-core`(不依赖完整 langchain),可与 LangChain / LangGraph 任意版本共存。
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
from typing import Any, Dict, List, Optional
|
|
24
|
+
|
|
25
|
+
from langchain_core.callbacks import BaseCallbackHandler
|
|
26
|
+
|
|
27
|
+
#: 从自测包 src/ 引用规则引擎(作为独立 PyPI 包时,由依赖提供 xh_engine)
|
|
28
|
+
try:
|
|
29
|
+
import xh_engine
|
|
30
|
+
except ImportError: # pragma: no cover - 仓库内引用
|
|
31
|
+
import os
|
|
32
|
+
import sys
|
|
33
|
+
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "src"))
|
|
34
|
+
import xh_engine
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class StarRingBlockedError(RuntimeError):
|
|
38
|
+
"""行为链被判定为危险时抛出,用于熔断工具执行。"""
|
|
39
|
+
|
|
40
|
+
def __init__(self, decision: str, reasons: List[str], actions: List[str]) -> None:
|
|
41
|
+
self.decision = decision
|
|
42
|
+
self.reasons = reasons
|
|
43
|
+
self.actions = actions
|
|
44
|
+
chain = " → ".join(actions)
|
|
45
|
+
super().__init__("[星环熔断] 行为链被拦截(%s):%s | 链:%s"
|
|
46
|
+
% (decision, ";".join(reasons), chain))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class StarRingCallbackHandler(BaseCallbackHandler):
|
|
50
|
+
"""LangChain 回调:累积工具调用序列,危险即熔断。
|
|
51
|
+
|
|
52
|
+
参数
|
|
53
|
+
----
|
|
54
|
+
max_history : int
|
|
55
|
+
行为链最多保留多少步(超过则只保留最近 N 步),默认 6。
|
|
56
|
+
action_map : dict
|
|
57
|
+
自定义工具名 → 标准动作名映射,例如 ``{"search_web": "external_http"}``。
|
|
58
|
+
config : dict
|
|
59
|
+
透传给规则引擎的参数闸门配置(路径/金额/域名/次数阈值)。
|
|
60
|
+
raise_on : {"block_fuse", "fuse"}
|
|
61
|
+
何时熔断:默认 ``block_fuse``(拦截与熔断都抛异常);``fuse`` 只在熔断时抛。
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
max_history: int = 6,
|
|
67
|
+
action_map: Optional[Dict[str, str]] = None,
|
|
68
|
+
config: Optional[Dict[str, Any]] = None,
|
|
69
|
+
raise_on: str = "block_fuse",
|
|
70
|
+
) -> None:
|
|
71
|
+
super().__init__()
|
|
72
|
+
self.max_history = max(1, int(max_history))
|
|
73
|
+
self.action_map = dict(action_map or {})
|
|
74
|
+
self.config = config
|
|
75
|
+
self.raise_on = raise_on
|
|
76
|
+
self.actions: List[Dict[str, Any]] = []
|
|
77
|
+
|
|
78
|
+
# ── 工具名映射 ────────────────────────────────────────
|
|
79
|
+
def _map_action(self, tool_name: str) -> str:
|
|
80
|
+
return self.action_map.get(tool_name, tool_name)
|
|
81
|
+
|
|
82
|
+
# ── 参数提取(尽力而为)────────────────────────────────
|
|
83
|
+
@staticmethod
|
|
84
|
+
def _extract_params(input_str: str) -> Dict[str, Any]:
|
|
85
|
+
"""从工具输入字符串里提取结构化参数(JSON 优先,键值对兜底)。"""
|
|
86
|
+
if not input_str:
|
|
87
|
+
return {}
|
|
88
|
+
s = input_str.strip()
|
|
89
|
+
try:
|
|
90
|
+
obj = json.loads(s)
|
|
91
|
+
return obj if isinstance(obj, dict) else {}
|
|
92
|
+
except (json.JSONDecodeError, ValueError):
|
|
93
|
+
pass
|
|
94
|
+
params: Dict[str, Any] = {}
|
|
95
|
+
for token in s.replace(",", " ").split():
|
|
96
|
+
if "=" in token:
|
|
97
|
+
k, v = token.split("=", 1)
|
|
98
|
+
params[k.strip()] = v.strip()
|
|
99
|
+
return params
|
|
100
|
+
|
|
101
|
+
# ── 核心:工具执行前判定 ───────────────────────────────
|
|
102
|
+
def on_tool_start(
|
|
103
|
+
self,
|
|
104
|
+
serialized: Dict[str, Any],
|
|
105
|
+
input_str: str,
|
|
106
|
+
**kwargs: Any,
|
|
107
|
+
) -> None:
|
|
108
|
+
tool_name = str(serialized.get("name") or "unknown")
|
|
109
|
+
action_type = self._map_action(tool_name)
|
|
110
|
+
self.actions.append({
|
|
111
|
+
"action_type": action_type,
|
|
112
|
+
"params": self._extract_params(input_str or ""),
|
|
113
|
+
})
|
|
114
|
+
if len(self.actions) > self.max_history:
|
|
115
|
+
self.actions = self.actions[-self.max_history:]
|
|
116
|
+
|
|
117
|
+
verdict = xh_engine.evaluate(self.actions, self.config)
|
|
118
|
+
decision = verdict["decision"]
|
|
119
|
+
should_raise = (
|
|
120
|
+
decision in ("block", "fuse") if self.raise_on == "block_fuse"
|
|
121
|
+
else decision == "fuse"
|
|
122
|
+
)
|
|
123
|
+
if should_raise:
|
|
124
|
+
raise StarRingBlockedError(
|
|
125
|
+
decision, verdict["reasons"],
|
|
126
|
+
[a["action_type"] for a in self.actions])
|
|
127
|
+
|
|
128
|
+
# ── 会话重置 ──────────────────────────────────────────
|
|
129
|
+
def reset(self) -> None:
|
|
130
|
+
"""清空累积的行为链(每次独立会话开始时调用)。"""
|
|
131
|
+
self.actions = []
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""星环 · Agent 行为自检引擎(分发包用的轻量规则版)。
|
|
3
|
+
|
|
4
|
+
为什么是「轻量规则版」而不是完整基座
|
|
5
|
+
----------------------------------
|
|
6
|
+
分发给客户的东西必须**不含核心资产**:
|
|
7
|
+
* 不含 177 条攻击样本库(这是可复现、可验证的护城河)
|
|
8
|
+
* 不含三轨模型权重(48 维 / 16 维)
|
|
9
|
+
* 不含真神领域台与红队剧本
|
|
10
|
+
|
|
11
|
+
所以这里只保留**规则层**:动作风险 + 参数闸门 + 组合链 + 未知动作 fail-closed。
|
|
12
|
+
这足以做「初筛」,且逆向出来也只拿到规则,拿不到样本与模型。
|
|
13
|
+
|
|
14
|
+
判定顺序(任一命中即停)
|
|
15
|
+
-----------------------
|
|
16
|
+
1. 未知动作 → 拒绝(模型幻觉出的工具名)
|
|
17
|
+
2. 参数越界 → 拒绝(路径穿越 / 超额 / 非白名单域名 / 批量次数)
|
|
18
|
+
3. 组合链 → 提权后敏感、外发链
|
|
19
|
+
4. 单动作风险 → LOW 放行 / MEDIUM 确认 / HIGH 拦截 / CRITICAL 熔断
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import fnmatch
|
|
25
|
+
import posixpath
|
|
26
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
27
|
+
|
|
28
|
+
#: 动作风险表(与基座 agent_guard.ACTION_RISK + 扩展表口径一致)
|
|
29
|
+
ACTION_RISK: Dict[str, Tuple[float, str]] = {
|
|
30
|
+
"read_screen": (0.05, "LOW"), "open_app": (0.10, "LOW"), "organize": (0.10, "LOW"),
|
|
31
|
+
"read_docs": (0.05, "LOW"), "summarize": (0.05, "LOW"), "list_docs": (0.05, "LOW"),
|
|
32
|
+
"send_report": (0.30, "MEDIUM"),
|
|
33
|
+
"send_message": (0.30, "MEDIUM"), "read_location": (0.40, "MEDIUM"),
|
|
34
|
+
"invoke_agent_tool": (0.50, "MEDIUM"),
|
|
35
|
+
"read_contacts": (0.55, "HIGH"), "share_file": (0.60, "HIGH"),
|
|
36
|
+
"read_sms": (0.60, "HIGH"), "change_permission": (0.70, "HIGH"),
|
|
37
|
+
"external_http": (0.70, "HIGH"), "read_otp": (0.80, "HIGH"),
|
|
38
|
+
"delete_bulk": (0.85, "CRITICAL"), "payment": (0.90, "CRITICAL"),
|
|
39
|
+
"transfer": (0.95, "CRITICAL"), "send_credentials": (0.98, "CRITICAL"),
|
|
40
|
+
"sandbox_read_outside": (0.85, "CRITICAL"), "access_other_process": (0.85, "CRITICAL"),
|
|
41
|
+
"restricted_syscall": (0.85, "CRITICAL"), "port_scan": (0.80, "CRITICAL"),
|
|
42
|
+
"accessibility_bypass": (0.85, "CRITICAL"), "automation_batch_click": (0.85, "CRITICAL"),
|
|
43
|
+
"recursive_loop": (0.75, "HIGH"), "sensor_battery_drain": (0.70, "HIGH"),
|
|
44
|
+
"bulk_message": (0.60, "HIGH"), "root": (0.90, "CRITICAL"),
|
|
45
|
+
"escalate": (0.88, "CRITICAL"), "sudo": (0.85, "CRITICAL"), "chmod": (0.75, "HIGH"),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
#: 动作族(用于「换个同族动作名」的链判定)
|
|
49
|
+
FAMILY = {
|
|
50
|
+
"transfer": "finance", "payment": "finance", "read_otp": "finance",
|
|
51
|
+
"external_http": "egress", "share_file": "egress", "send_credentials": "egress",
|
|
52
|
+
"change_permission": "privilege", "escalate": "privilege", "root": "privilege",
|
|
53
|
+
"sudo": "privilege", "chmod": "privilege", "accessibility_bypass": "privilege",
|
|
54
|
+
"read_contacts": "collect", "read_sms": "collect", "read_screen": "collect",
|
|
55
|
+
"read_location": "collect", "read_docs": "collect",
|
|
56
|
+
"sandbox_read_outside": "escape", "access_other_process": "escape",
|
|
57
|
+
"restricted_syscall": "escape", "port_scan": "escape",
|
|
58
|
+
"recursive_loop": "exhaust", "sensor_battery_drain": "exhaust",
|
|
59
|
+
"automation_batch_click": "automation", "bulk_message": "automation",
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
_FAMILY_PREFIX = (
|
|
63
|
+
("read_", "collect"), ("get_", "collect"), ("list_", "collect"),
|
|
64
|
+
("send_", "egress"), ("share_", "egress"), ("external_", "egress"),
|
|
65
|
+
("upload_", "egress"), ("export_", "egress"),
|
|
66
|
+
("change_", "privilege"), ("grant_", "privilege"), ("set_perm", "privilege"),
|
|
67
|
+
("sandbox_", "escape"), ("transfer", "finance"), ("pay", "finance"),
|
|
68
|
+
("automation_", "automation"), ("bulk_", "automation"),
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
PRIVILEGE_OPS = {"change_permission", "escalate", "root", "sudo", "chmod"}
|
|
72
|
+
PRIVILEGE_SINKS = {"read_contacts", "read_sms", "read_location", "external_http",
|
|
73
|
+
"share_file", "send_credentials", "read_otp"}
|
|
74
|
+
|
|
75
|
+
DECISION_ORDER = ["allow", "confirm", "block", "fuse"]
|
|
76
|
+
|
|
77
|
+
#: 默认参数闸门(客户可用 config.json 覆盖)
|
|
78
|
+
DEFAULT_CONFIG = {
|
|
79
|
+
"path_deny": ["..", "/etc/", "~/", "C:\\Windows"],
|
|
80
|
+
"amount_max": 5000,
|
|
81
|
+
"domain_deny": ["attacker", "evil.", "example.com", "pastebin", "tmpfiles"],
|
|
82
|
+
"count_max": 50,
|
|
83
|
+
"exempt_ops": [],
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#: 输出标注(三色)
|
|
87
|
+
MARK_CONFIRM = "【确认拦截】"
|
|
88
|
+
MARK_REVIEW = "【需复核】"
|
|
89
|
+
MARK_FALSE = "【疑似误报】"
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def family_of(op: str) -> str:
|
|
93
|
+
if op in FAMILY:
|
|
94
|
+
return FAMILY[op]
|
|
95
|
+
for prefix, fam in _FAMILY_PREFIX:
|
|
96
|
+
if op.startswith(prefix):
|
|
97
|
+
return fam
|
|
98
|
+
if op.endswith("_loop"):
|
|
99
|
+
return "exhaust"
|
|
100
|
+
return op
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _num(value: Any) -> Optional[float]:
|
|
104
|
+
if isinstance(value, bool):
|
|
105
|
+
return None
|
|
106
|
+
if isinstance(value, (int, float)):
|
|
107
|
+
return float(value)
|
|
108
|
+
try:
|
|
109
|
+
return float(str(value).strip())
|
|
110
|
+
except (TypeError, ValueError):
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _check_params(action_type: str, params: Dict[str, Any], cfg: Dict[str, Any]) -> List[str]:
|
|
115
|
+
"""参数闸门:返回违规描述列表。"""
|
|
116
|
+
out: List[str] = []
|
|
117
|
+
raw_path = str(params.get("path") or params.get("target") or "")
|
|
118
|
+
if raw_path:
|
|
119
|
+
norm = posixpath.normpath(raw_path.replace("\\", "/"))
|
|
120
|
+
for bad in cfg.get("path_deny") or []:
|
|
121
|
+
if str(bad) in raw_path.replace("\\", "/") or str(bad) in norm:
|
|
122
|
+
out.append("路径命中禁用片段 %r(%s)" % (bad, raw_path))
|
|
123
|
+
break
|
|
124
|
+
|
|
125
|
+
if action_type in ("transfer", "payment"):
|
|
126
|
+
amount = _num(params.get("amount"))
|
|
127
|
+
limit = float(cfg.get("amount_max") or 0)
|
|
128
|
+
if amount is None:
|
|
129
|
+
out.append("金额缺失或非数字,无法确认在额度内")
|
|
130
|
+
elif limit and amount > limit:
|
|
131
|
+
out.append("金额 %.2f 超过上限 %.2f" % (amount, limit))
|
|
132
|
+
|
|
133
|
+
target = str(params.get("to") or params.get("url") or params.get("domain") or "")
|
|
134
|
+
if target:
|
|
135
|
+
low = target.lower()
|
|
136
|
+
for bad in cfg.get("domain_deny") or []:
|
|
137
|
+
if str(bad).lower() in low:
|
|
138
|
+
out.append("目标域名命中禁用片段 %r(%s)" % (bad, target))
|
|
139
|
+
break
|
|
140
|
+
|
|
141
|
+
if action_type in ("automation_batch_click", "bulk_message"):
|
|
142
|
+
count = _num(params.get("count"))
|
|
143
|
+
limit = float(cfg.get("count_max") or 0)
|
|
144
|
+
if count is not None and limit and count > limit:
|
|
145
|
+
out.append("次数 %.0f 超过上限 %.0f" % (count, limit))
|
|
146
|
+
return out
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def evaluate(actions: List[Dict[str, Any]], cfg: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
150
|
+
"""评估一条行为链,返回裁决 + 标注 + 依据。"""
|
|
151
|
+
conf = dict(DEFAULT_CONFIG)
|
|
152
|
+
conf.update(cfg or {})
|
|
153
|
+
exempt = set(str(x) for x in (conf.get("exempt_ops") or []))
|
|
154
|
+
|
|
155
|
+
types = [str(a.get("action_type") or a.get("tool") or "") for a in actions]
|
|
156
|
+
params_list = [dict(a.get("params") or {}) for a in actions]
|
|
157
|
+
reasons: List[str] = []
|
|
158
|
+
|
|
159
|
+
# 空行为链:无可判定对象,放行(否则下方 max 会因空序列崩溃)
|
|
160
|
+
if not types:
|
|
161
|
+
return {"decision": "allow", "mark": "", "reasons": ["空行为链"], "types": []}
|
|
162
|
+
|
|
163
|
+
# ① 未知动作 fail-closed
|
|
164
|
+
unknowns = [t for t in types if t and t not in ACTION_RISK and family_of(t) == t]
|
|
165
|
+
if unknowns:
|
|
166
|
+
return {"decision": "block", "mark": MARK_CONFIRM,
|
|
167
|
+
"reasons": ["未知动作(fail-closed):%s" % ",".join(sorted(set(unknowns)))],
|
|
168
|
+
"types": types}
|
|
169
|
+
|
|
170
|
+
# ② 参数闸门
|
|
171
|
+
for action_type, params in zip(types, params_list):
|
|
172
|
+
if action_type in exempt:
|
|
173
|
+
continue
|
|
174
|
+
for problem in _check_params(action_type, params, conf):
|
|
175
|
+
reasons.append("%s:%s" % (action_type, problem))
|
|
176
|
+
if reasons:
|
|
177
|
+
return {"decision": "block", "mark": MARK_CONFIRM,
|
|
178
|
+
"reasons": reasons, "types": types}
|
|
179
|
+
|
|
180
|
+
# ③ 组合链
|
|
181
|
+
type_set = set(types)
|
|
182
|
+
if PRIVILEGE_OPS & type_set and PRIVILEGE_SINKS & type_set:
|
|
183
|
+
return {"decision": "fuse", "mark": MARK_CONFIRM,
|
|
184
|
+
"reasons": ["提权链:%s + %s"
|
|
185
|
+
% (sorted(PRIVILEGE_OPS & type_set),
|
|
186
|
+
sorted(PRIVILEGE_SINKS & type_set))],
|
|
187
|
+
"types": types}
|
|
188
|
+
if {"share_file", "external_http"} <= type_set:
|
|
189
|
+
return {"decision": "fuse", "mark": MARK_CONFIRM,
|
|
190
|
+
"reasons": ["文件外发链:share_file + external_http"], "types": types}
|
|
191
|
+
# 凭据外泄链:读取短信/验证码/通讯录 → 外发
|
|
192
|
+
cred_src = {"read_sms", "read_otp", "read_contacts"} & type_set
|
|
193
|
+
cred_sink = {"send_credentials", "external_http", "share_file"} & type_set
|
|
194
|
+
if cred_src and cred_sink:
|
|
195
|
+
return {"decision": "fuse", "mark": MARK_CONFIRM,
|
|
196
|
+
"reasons": ["凭据外泄链:%s → %s"
|
|
197
|
+
% (sorted(cred_src), sorted(cred_sink))], "types": types}
|
|
198
|
+
|
|
199
|
+
# ④ 单动作风险(取最高)
|
|
200
|
+
levels = []
|
|
201
|
+
for action_type in types:
|
|
202
|
+
if action_type in exempt:
|
|
203
|
+
levels.append(("LOW", 0.10))
|
|
204
|
+
continue
|
|
205
|
+
levels.append(ACTION_RISK.get(action_type, (0.5, "MEDIUM")))
|
|
206
|
+
worst_level = max((lv for _score, lv in levels),
|
|
207
|
+
key=lambda x: DECISION_ORDER.index(
|
|
208
|
+
{"LOW": "allow", "MEDIUM": "confirm",
|
|
209
|
+
"HIGH": "block", "CRITICAL": "fuse"}[x]))
|
|
210
|
+
decision = {"LOW": "allow", "MEDIUM": "confirm",
|
|
211
|
+
"HIGH": "block", "CRITICAL": "fuse"}[worst_level]
|
|
212
|
+
reasons.append("最高动作等级 %s" % worst_level)
|
|
213
|
+
|
|
214
|
+
# 标注:HIGH 级单动作若无组合证据 → 需复核;MEDIUM → 需复核;LOW → 放行
|
|
215
|
+
mark = MARK_CONFIRM if decision in ("block", "fuse") else (
|
|
216
|
+
MARK_REVIEW if decision == "confirm" else "")
|
|
217
|
+
return {"decision": decision, "mark": mark, "reasons": reasons, "types": types}
|