spendshield 0.6.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.
- spendshield-0.6.0/PKG-INFO +178 -0
- spendshield-0.6.0/README.md +169 -0
- spendshield-0.6.0/pyproject.toml +18 -0
- spendshield-0.6.0/setup.cfg +4 -0
- spendshield-0.6.0/spendshield/__init__.py +32 -0
- spendshield-0.6.0/spendshield/dashboard.py +119 -0
- spendshield-0.6.0/spendshield/guard.py +503 -0
- spendshield-0.6.0/spendshield/mcp_server.py +185 -0
- spendshield-0.6.0/spendshield/vault.py +75 -0
- spendshield-0.6.0/spendshield.egg-info/PKG-INFO +178 -0
- spendshield-0.6.0/spendshield.egg-info/SOURCES.txt +14 -0
- spendshield-0.6.0/spendshield.egg-info/dependency_links.txt +1 -0
- spendshield-0.6.0/spendshield.egg-info/entry_points.txt +2 -0
- spendshield-0.6.0/spendshield.egg-info/requires.txt +1 -0
- spendshield-0.6.0/spendshield.egg-info/top_level.txt +1 -0
- spendshield-0.6.0/tests/test_guard.py +529 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spendshield
|
|
3
|
+
Version: 0.6.0
|
|
4
|
+
Summary: AI Agent 付款安全层: 干跑/预算/人工确认/审计 四道闸门
|
|
5
|
+
License: MIT
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: cryptography>=41
|
|
9
|
+
|
|
10
|
+
# 💰 SpendShield — AI Agent 付款安全层
|
|
11
|
+
|
|
12
|
+
> **AI 替你花钱之前,先过 SpendShield 这关。**
|
|
13
|
+
|
|
14
|
+
让 AI Agent 下单、转账、调付费 API 之前,自动过四道闸门:
|
|
15
|
+
**干跑预览 → 预算上限 → 人工确认 → 全量审计。**
|
|
16
|
+
|
|
17
|
+
## 🩸 为什么会有这个项目(真实事故)
|
|
18
|
+
|
|
19
|
+
2026 年 8 月 9 日,我的自动化系统测试下单。
|
|
20
|
+
|
|
21
|
+
我传了 `dry: true`,以为只是试算价格。但服务器只认 `?dry=1` —— **4 单 99 元真实出码扣款,当天全部打水漂。**
|
|
22
|
+
|
|
23
|
+
这不是我一个人的坑。AI Agent 时代正在到来:Agent 替你订餐、替你充值、替你调付费 API——**当 AI 开始花真钱,谁给它上闸门?**
|
|
24
|
+
|
|
25
|
+
我把我踩过的坑,做成了一个库。
|
|
26
|
+
|
|
27
|
+
## ✨ 四道闸门
|
|
28
|
+
|
|
29
|
+
| 闸门 | 默认 | 作用 |
|
|
30
|
+
|------|------|------|
|
|
31
|
+
| 🧪 **dry_run** 干跑 | ✅ 开 | 只预览不执行——`dry` 参数失效也无所谓,库层面兜底 |
|
|
32
|
+
| 💰 **budget** 预算 | 不限 | 总预算超支直接拒绝,绝不超花 |
|
|
33
|
+
| 🚧 **max_amount** 单次上限 | 不限 | 单笔超限拦截(防"转 9999 给陌生人") |
|
|
34
|
+
| 🙋 **approval** 人工确认 | 关 | 花钱前必须人点头(console / 回调) |
|
|
35
|
+
| 📜 **audit** 审计 | ✅ 开 | 每次尝试全留痕,导出 JSON 对账 |
|
|
36
|
+
|
|
37
|
+
## 🔑 身份层(KYA 最小实现,v0.4)
|
|
38
|
+
|
|
39
|
+
AI 没有法律人格,但必须有“数字身份”。每个 agent 注册专属策略,**未注册默认拒绝**:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from spendshield import SpendShield, UnknownAgent
|
|
43
|
+
|
|
44
|
+
guard = SpendShield(dry_run=False)
|
|
45
|
+
guard.register_agent("mcd_bot", budget=50, max_amount=30,
|
|
46
|
+
blacklist=["测试收款"], whitelist=["麦当劳"],
|
|
47
|
+
rate_limit={"window_s": 60, "max_calls": 3})
|
|
48
|
+
|
|
49
|
+
@guard.protect("下单", agent="mcd_bot") # 或运行时传 agent=xx
|
|
50
|
+
|
|
51
|
+
def place_order(amount, to):
|
|
52
|
+
return call_real_api(amount, to)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- 未注册的 agent 调用 → 直接拒绝(`UnknownAgent`),审计留痕 `blocked_unknown_agent`
|
|
56
|
+
- `allow_unknown: true` 可回落全局策略(不推荐)
|
|
57
|
+
- 每条审计记录带 `agent` 字段:**谁在花、花给谁、用户知不知道**
|
|
58
|
+
- 策略即代码支持 `agents:` 段(YAML),预算/黑名单/频率/审批按 agent 隔离
|
|
59
|
+
|
|
60
|
+
## 🎯 意图一致性(防提示注入,v0.5)
|
|
61
|
+
|
|
62
|
+
AI 可能被劫持:提示注入、返利诱惑……闸门只知道“花多少、给谁”,不知道“这是用户要的吗”。
|
|
63
|
+
解法:**敏感操作强制人工确认**——即使没配全局审批,新收款方/大额也默认拦下:
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
# 新收款方(从未交易过)→ 必须确认;没配审批通道 → 直接拒绝
|
|
67
|
+
# 金额 > approve_above → 必须确认
|
|
68
|
+
guard = SpendShield(approve_new_recipient=True, approve_above=1000)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- 交易成功的收款方自动进入记忆,之后不再反复烦你
|
|
72
|
+
- 白名单收款方永远跳过
|
|
73
|
+
- 未配置审批通道时,敏感操作**直接拒绝**(宁可拦死,不放行)
|
|
74
|
+
- 拦截记录 `blocked_approval` 带原因:新收款方 / 超阈值 / 未配置通道
|
|
75
|
+
|
|
76
|
+
## 🔐 密钥保险库(v0.6)
|
|
77
|
+
|
|
78
|
+
私钥不落地是 AI 支付的命门——**一次泄露,钱包被掏空**。密钥加密落盘,主密钥放环境变量,取用必须过闸门:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
python -c "from spendshield import KeyVault; print(KeyVault.generate_key())" # 生成主密钥(仅此一次)
|
|
82
|
+
export SPENDGUARD_MASTER_KEY=<刚才的输出> # 放环境变量, 别写进代码/仓库
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
from spendshield import SpendShield, KeyVault
|
|
87
|
+
|
|
88
|
+
vault = KeyVault("vault.json") # 主密钥从环境变量读
|
|
89
|
+
vault.store("mcd_sk", "sk_live_xxxx") # 加密落盘, 文件里只有密文
|
|
90
|
+
|
|
91
|
+
guard = SpendShield(key_vault=vault)
|
|
92
|
+
guard.register_agent("mcd_bot", whitelist=["mcd_sk"])
|
|
93
|
+
sk = guard.get_secret("mcd_sk", agent="mcd_bot") # 过身份+意图闸门才能取
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
- 落盘文件无明文(AES128-CBC + HMAC);主密钥不落盘
|
|
97
|
+
- 取密钥 = 敏感操作:未注册 agent 拒绝;新密钥名无审批通道默认拒绝(防提示注入偷密钥)
|
|
98
|
+
- 每次取用留审计 `secret_access`:谁、何时、取了哪个密钥
|
|
99
|
+
|
|
100
|
+
## 🚀 快速开始
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
pip install spendshield # 或直接 clone 用
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from spendshield import SpendShield
|
|
108
|
+
|
|
109
|
+
guard = SpendShield(budget=200, dry_run=True, whitelist=["麦当劳"]) # 默认干跑 + 信任收款方
|
|
110
|
+
|
|
111
|
+
@guard.protect("下单")
|
|
112
|
+
def place_order(amount, to):
|
|
113
|
+
return call_real_api(amount, to) # 真实下单逻辑
|
|
114
|
+
|
|
115
|
+
# 干跑模式: 报错提示, 绝不真花
|
|
116
|
+
place_order(amount=99, to="麦当劳")
|
|
117
|
+
# => [SpendShield] dry_run: 下单 ¥99.0 -> 麦当劳 (未执行)
|
|
118
|
+
# => DryRunBlocked: 关掉 dry_run 才会真花
|
|
119
|
+
|
|
120
|
+
# 确认无误后放行, 预算闸门兜底
|
|
121
|
+
guard.dry_run = False
|
|
122
|
+
for i in range(4):
|
|
123
|
+
place_order(amount=99, to="麦当劳") # 第3单被 BudgetExceeded 拦住
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
> 💡 新收款方默认需人工确认(意图一致性, 防提示注入)——把常用收款方加白名单或注册 Agent 身份可免。
|
|
127
|
+
|
|
128
|
+
## 🎯 谁需要它
|
|
129
|
+
|
|
130
|
+
- **AI Agent 框架用户**:给你的 Agent 工具加装饰器,一行接入
|
|
131
|
+
- **自动化系统运维**:批量任务/定时下单,防误操作真扣款
|
|
132
|
+
- **MCP / Function Call 开发者**:LLM 生成的工具调用,过闸门再执行
|
|
133
|
+
- **所有被"测试单变真单"坑过的人** 🩸
|
|
134
|
+
|
|
135
|
+
## 🗺 Roadmap
|
|
136
|
+
|
|
137
|
+
- [x] v0.1 四道闸门 + 审计 + 装饰器接入
|
|
138
|
+
- [ ] 收款方黑名单/白名单(陌生收款方强制确认)
|
|
139
|
+
- [ ] 频率限制(同一收款方短时间 N 次)
|
|
140
|
+
- [ ] MCP server 版(Agent 工具调用直接过闸)
|
|
141
|
+
- [ ] 远程审批(企业微信/Telegram 确认)
|
|
142
|
+
- [ ] 多策略插件(风控规则引擎)
|
|
143
|
+
|
|
144
|
+
## 🧪 测试
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
python3 tests/test_guard.py # 6 个测试全过
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## 📄 License
|
|
151
|
+
|
|
152
|
+
MIT — 拿去用。愿 AI 时代,没人再被"测试单"坑第二次。
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
**⭐ 如果这个项目对你有用,点个 star,让更多被坑过的人看到。**
|
|
157
|
+
|
|
158
|
+
## 🤖 MCP Server(AI Agent 直接调用)
|
|
159
|
+
|
|
160
|
+
让 Claude Code / OpenClaw 等 MCP 兼容 agent 直接通过工具调用过闸门:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
# 启动(stdio 模式, agent 配置里指向它)
|
|
164
|
+
python -m spendshield.mcp_server --policy spendshield.yaml
|
|
165
|
+
# 或安装后: spendshield-mcp --policy spendshield.yaml
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
**工具**:
|
|
169
|
+
- `spend_protect(action, amount, to)` — 保护一次花钱操作(走全部闸门)
|
|
170
|
+
- `spend_status()` — 预算/已花/拦截统计
|
|
171
|
+
- `spend_audit(limit)` — 最近审计记录
|
|
172
|
+
- `spend_reset()` — 重置会话已花
|
|
173
|
+
|
|
174
|
+
```json
|
|
175
|
+
// agent 调用示例
|
|
176
|
+
{"name": "spend_protect", "arguments": {"action": "下单", "amount": 99, "to": "麦当劳"}}
|
|
177
|
+
// => {"ok": false, "reason": "[干跑] 下单 ¥99.0 -> 麦当劳 (未执行...)", "spent": 0.0}
|
|
178
|
+
```
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# 💰 SpendShield — AI Agent 付款安全层
|
|
2
|
+
|
|
3
|
+
> **AI 替你花钱之前,先过 SpendShield 这关。**
|
|
4
|
+
|
|
5
|
+
让 AI Agent 下单、转账、调付费 API 之前,自动过四道闸门:
|
|
6
|
+
**干跑预览 → 预算上限 → 人工确认 → 全量审计。**
|
|
7
|
+
|
|
8
|
+
## 🩸 为什么会有这个项目(真实事故)
|
|
9
|
+
|
|
10
|
+
2026 年 8 月 9 日,我的自动化系统测试下单。
|
|
11
|
+
|
|
12
|
+
我传了 `dry: true`,以为只是试算价格。但服务器只认 `?dry=1` —— **4 单 99 元真实出码扣款,当天全部打水漂。**
|
|
13
|
+
|
|
14
|
+
这不是我一个人的坑。AI Agent 时代正在到来:Agent 替你订餐、替你充值、替你调付费 API——**当 AI 开始花真钱,谁给它上闸门?**
|
|
15
|
+
|
|
16
|
+
我把我踩过的坑,做成了一个库。
|
|
17
|
+
|
|
18
|
+
## ✨ 四道闸门
|
|
19
|
+
|
|
20
|
+
| 闸门 | 默认 | 作用 |
|
|
21
|
+
|------|------|------|
|
|
22
|
+
| 🧪 **dry_run** 干跑 | ✅ 开 | 只预览不执行——`dry` 参数失效也无所谓,库层面兜底 |
|
|
23
|
+
| 💰 **budget** 预算 | 不限 | 总预算超支直接拒绝,绝不超花 |
|
|
24
|
+
| 🚧 **max_amount** 单次上限 | 不限 | 单笔超限拦截(防"转 9999 给陌生人") |
|
|
25
|
+
| 🙋 **approval** 人工确认 | 关 | 花钱前必须人点头(console / 回调) |
|
|
26
|
+
| 📜 **audit** 审计 | ✅ 开 | 每次尝试全留痕,导出 JSON 对账 |
|
|
27
|
+
|
|
28
|
+
## 🔑 身份层(KYA 最小实现,v0.4)
|
|
29
|
+
|
|
30
|
+
AI 没有法律人格,但必须有“数字身份”。每个 agent 注册专属策略,**未注册默认拒绝**:
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from spendshield import SpendShield, UnknownAgent
|
|
34
|
+
|
|
35
|
+
guard = SpendShield(dry_run=False)
|
|
36
|
+
guard.register_agent("mcd_bot", budget=50, max_amount=30,
|
|
37
|
+
blacklist=["测试收款"], whitelist=["麦当劳"],
|
|
38
|
+
rate_limit={"window_s": 60, "max_calls": 3})
|
|
39
|
+
|
|
40
|
+
@guard.protect("下单", agent="mcd_bot") # 或运行时传 agent=xx
|
|
41
|
+
|
|
42
|
+
def place_order(amount, to):
|
|
43
|
+
return call_real_api(amount, to)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- 未注册的 agent 调用 → 直接拒绝(`UnknownAgent`),审计留痕 `blocked_unknown_agent`
|
|
47
|
+
- `allow_unknown: true` 可回落全局策略(不推荐)
|
|
48
|
+
- 每条审计记录带 `agent` 字段:**谁在花、花给谁、用户知不知道**
|
|
49
|
+
- 策略即代码支持 `agents:` 段(YAML),预算/黑名单/频率/审批按 agent 隔离
|
|
50
|
+
|
|
51
|
+
## 🎯 意图一致性(防提示注入,v0.5)
|
|
52
|
+
|
|
53
|
+
AI 可能被劫持:提示注入、返利诱惑……闸门只知道“花多少、给谁”,不知道“这是用户要的吗”。
|
|
54
|
+
解法:**敏感操作强制人工确认**——即使没配全局审批,新收款方/大额也默认拦下:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
# 新收款方(从未交易过)→ 必须确认;没配审批通道 → 直接拒绝
|
|
58
|
+
# 金额 > approve_above → 必须确认
|
|
59
|
+
guard = SpendShield(approve_new_recipient=True, approve_above=1000)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
- 交易成功的收款方自动进入记忆,之后不再反复烦你
|
|
63
|
+
- 白名单收款方永远跳过
|
|
64
|
+
- 未配置审批通道时,敏感操作**直接拒绝**(宁可拦死,不放行)
|
|
65
|
+
- 拦截记录 `blocked_approval` 带原因:新收款方 / 超阈值 / 未配置通道
|
|
66
|
+
|
|
67
|
+
## 🔐 密钥保险库(v0.6)
|
|
68
|
+
|
|
69
|
+
私钥不落地是 AI 支付的命门——**一次泄露,钱包被掏空**。密钥加密落盘,主密钥放环境变量,取用必须过闸门:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
python -c "from spendshield import KeyVault; print(KeyVault.generate_key())" # 生成主密钥(仅此一次)
|
|
73
|
+
export SPENDGUARD_MASTER_KEY=<刚才的输出> # 放环境变量, 别写进代码/仓库
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from spendshield import SpendShield, KeyVault
|
|
78
|
+
|
|
79
|
+
vault = KeyVault("vault.json") # 主密钥从环境变量读
|
|
80
|
+
vault.store("mcd_sk", "sk_live_xxxx") # 加密落盘, 文件里只有密文
|
|
81
|
+
|
|
82
|
+
guard = SpendShield(key_vault=vault)
|
|
83
|
+
guard.register_agent("mcd_bot", whitelist=["mcd_sk"])
|
|
84
|
+
sk = guard.get_secret("mcd_sk", agent="mcd_bot") # 过身份+意图闸门才能取
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
- 落盘文件无明文(AES128-CBC + HMAC);主密钥不落盘
|
|
88
|
+
- 取密钥 = 敏感操作:未注册 agent 拒绝;新密钥名无审批通道默认拒绝(防提示注入偷密钥)
|
|
89
|
+
- 每次取用留审计 `secret_access`:谁、何时、取了哪个密钥
|
|
90
|
+
|
|
91
|
+
## 🚀 快速开始
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
pip install spendshield # 或直接 clone 用
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from spendshield import SpendShield
|
|
99
|
+
|
|
100
|
+
guard = SpendShield(budget=200, dry_run=True, whitelist=["麦当劳"]) # 默认干跑 + 信任收款方
|
|
101
|
+
|
|
102
|
+
@guard.protect("下单")
|
|
103
|
+
def place_order(amount, to):
|
|
104
|
+
return call_real_api(amount, to) # 真实下单逻辑
|
|
105
|
+
|
|
106
|
+
# 干跑模式: 报错提示, 绝不真花
|
|
107
|
+
place_order(amount=99, to="麦当劳")
|
|
108
|
+
# => [SpendShield] dry_run: 下单 ¥99.0 -> 麦当劳 (未执行)
|
|
109
|
+
# => DryRunBlocked: 关掉 dry_run 才会真花
|
|
110
|
+
|
|
111
|
+
# 确认无误后放行, 预算闸门兜底
|
|
112
|
+
guard.dry_run = False
|
|
113
|
+
for i in range(4):
|
|
114
|
+
place_order(amount=99, to="麦当劳") # 第3单被 BudgetExceeded 拦住
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
> 💡 新收款方默认需人工确认(意图一致性, 防提示注入)——把常用收款方加白名单或注册 Agent 身份可免。
|
|
118
|
+
|
|
119
|
+
## 🎯 谁需要它
|
|
120
|
+
|
|
121
|
+
- **AI Agent 框架用户**:给你的 Agent 工具加装饰器,一行接入
|
|
122
|
+
- **自动化系统运维**:批量任务/定时下单,防误操作真扣款
|
|
123
|
+
- **MCP / Function Call 开发者**:LLM 生成的工具调用,过闸门再执行
|
|
124
|
+
- **所有被"测试单变真单"坑过的人** 🩸
|
|
125
|
+
|
|
126
|
+
## 🗺 Roadmap
|
|
127
|
+
|
|
128
|
+
- [x] v0.1 四道闸门 + 审计 + 装饰器接入
|
|
129
|
+
- [ ] 收款方黑名单/白名单(陌生收款方强制确认)
|
|
130
|
+
- [ ] 频率限制(同一收款方短时间 N 次)
|
|
131
|
+
- [ ] MCP server 版(Agent 工具调用直接过闸)
|
|
132
|
+
- [ ] 远程审批(企业微信/Telegram 确认)
|
|
133
|
+
- [ ] 多策略插件(风控规则引擎)
|
|
134
|
+
|
|
135
|
+
## 🧪 测试
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
python3 tests/test_guard.py # 6 个测试全过
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## 📄 License
|
|
142
|
+
|
|
143
|
+
MIT — 拿去用。愿 AI 时代,没人再被"测试单"坑第二次。
|
|
144
|
+
|
|
145
|
+
---
|
|
146
|
+
|
|
147
|
+
**⭐ 如果这个项目对你有用,点个 star,让更多被坑过的人看到。**
|
|
148
|
+
|
|
149
|
+
## 🤖 MCP Server(AI Agent 直接调用)
|
|
150
|
+
|
|
151
|
+
让 Claude Code / OpenClaw 等 MCP 兼容 agent 直接通过工具调用过闸门:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
# 启动(stdio 模式, agent 配置里指向它)
|
|
155
|
+
python -m spendshield.mcp_server --policy spendshield.yaml
|
|
156
|
+
# 或安装后: spendshield-mcp --policy spendshield.yaml
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**工具**:
|
|
160
|
+
- `spend_protect(action, amount, to)` — 保护一次花钱操作(走全部闸门)
|
|
161
|
+
- `spend_status()` — 预算/已花/拦截统计
|
|
162
|
+
- `spend_audit(limit)` — 最近审计记录
|
|
163
|
+
- `spend_reset()` — 重置会话已花
|
|
164
|
+
|
|
165
|
+
```json
|
|
166
|
+
// agent 调用示例
|
|
167
|
+
{"name": "spend_protect", "arguments": {"action": "下单", "amount": 99, "to": "麦当劳"}}
|
|
168
|
+
// => {"ok": false, "reason": "[干跑] 下单 ¥99.0 -> 麦当劳 (未执行...)", "spent": 0.0}
|
|
169
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "spendshield"
|
|
7
|
+
version = "0.6.0"
|
|
8
|
+
description = "AI Agent 付款安全层: 干跑/预算/人工确认/审计 四道闸门"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
dependencies = ["cryptography>=41"]
|
|
13
|
+
|
|
14
|
+
[tool.setuptools]
|
|
15
|
+
packages = ["spendshield"]
|
|
16
|
+
|
|
17
|
+
[project.scripts]
|
|
18
|
+
spendshield-mcp = "spendshield.mcp_server:main"
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
SpendShield — AI Agent 付款安全层
|
|
4
|
+
|
|
5
|
+
给 AI Agent 的「花钱动作」加上四道闸门:
|
|
6
|
+
1. dry_run 干跑模式(默认开): 只预览, 不真花
|
|
7
|
+
2. budget 预算上限: 超了直接拒绝
|
|
8
|
+
3. approval 人工确认: 花钱前必须人点头
|
|
9
|
+
4. audit 全量审计: 每次尝试都留痕
|
|
10
|
+
|
|
11
|
+
血泪背景: 2026-08-09, 我让自动化系统测试下单, 因为 dry 参数没生效,
|
|
12
|
+
4 单 99 元真实出码扣款。这个库就是那次事故的产物——
|
|
13
|
+
AI 时代, 别让 Agent 替你花钱之前没有闸门。
|
|
14
|
+
|
|
15
|
+
用法:
|
|
16
|
+
from spendshield import SpendShield
|
|
17
|
+
|
|
18
|
+
guard = SpendShield(budget=100, dry_run=True, approval="console")
|
|
19
|
+
|
|
20
|
+
@guard.protect("下单", max_amount=50)
|
|
21
|
+
def place_order(order_id, amount, to):
|
|
22
|
+
# ... 真实下单逻辑
|
|
23
|
+
return {"ok": True}
|
|
24
|
+
"""
|
|
25
|
+
from .guard import SpendShield, GuardedError, BudgetExceeded, NeedsApproval, DryRunBlocked, UnknownAgent, AuditRecord
|
|
26
|
+
from .vault import KeyVault
|
|
27
|
+
|
|
28
|
+
__version__ = "0.6.0"
|
|
29
|
+
__all__ = [
|
|
30
|
+
"SpendShield", "GuardedError", "BudgetExceeded",
|
|
31
|
+
"NeedsApproval", "DryRunBlocked", "UnknownAgent", "AuditRecord", "KeyVault",
|
|
32
|
+
]
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""SpendShield 审计 Dashboard — 可视化审计记录
|
|
3
|
+
|
|
4
|
+
用法:
|
|
5
|
+
1. 程序里导出审计: guard.export_audit("audit.json")
|
|
6
|
+
2. 启动看板: python -m spendshield.dashboard --file audit.json --port 8775
|
|
7
|
+
3. 打开 http://localhost:8775
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
import time
|
|
16
|
+
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
17
|
+
from urllib.parse import urlparse
|
|
18
|
+
|
|
19
|
+
PAGE = """<!DOCTYPE html>
|
|
20
|
+
<html lang="zh-CN"><head><meta charset="utf-8">
|
|
21
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
22
|
+
<title>SpendShield 审计 Dashboard</title>
|
|
23
|
+
<style>
|
|
24
|
+
*{margin:0;padding:0;box-sizing:border-box}
|
|
25
|
+
body{font-family:-apple-system,'PingFang SC','Microsoft YaHei',sans-serif;background:radial-gradient(900px 500px at 20% -10%,#1b1633 0%,#0b0e14 55%);color:#e6e8ee;padding:28px;min-height:100vh}
|
|
26
|
+
h1{font-size:20px;font-weight:800;margin-bottom:4px}h1 span{color:#a78bfa}
|
|
27
|
+
.sub{color:#7d8590;font-size:13px;margin-bottom:20px}
|
|
28
|
+
.stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;margin-bottom:20px}
|
|
29
|
+
.stat{background:#141b26;border:1px solid #21262d;border-radius:10px;padding:12px 14px}
|
|
30
|
+
.stat .v{font-size:24px;font-weight:800;line-height:1.2}
|
|
31
|
+
.stat .k{font-size:12px;color:#7d8590;margin-top:2px}
|
|
32
|
+
.stat .v.green{color:#3fb950}.stat .v.red{color:#f85149}.stat .v.purple{color:#a78bfa}.stat .v.yellow{color:#d29922}
|
|
33
|
+
table{width:100%;border-collapse:collapse;font-size:13px}
|
|
34
|
+
th,td{padding:9px 12px;text-align:left;border-bottom:1px solid #21262d}
|
|
35
|
+
th{color:#7d8590;font-weight:600;font-size:12px}
|
|
36
|
+
.blocked{color:#f85149}.executed{color:#3fb950}.dry{color:#d29922}
|
|
37
|
+
</style></head><body>
|
|
38
|
+
<h1>💰 SpendShield <span>审计 Dashboard</span></h1>
|
|
39
|
+
<div class="sub" id="meta">加载中...</div>
|
|
40
|
+
<div class="stats" id="stats"></div>
|
|
41
|
+
<table id="tbl"><thead><tr>
|
|
42
|
+
<th>时间</th><th>操作</th><th>金额</th><th>收款方</th><th>决策</th><th>原因</th><th>累计已花</th>
|
|
43
|
+
</tr></thead><tbody></tbody></table>
|
|
44
|
+
<script>
|
|
45
|
+
async function load(){
|
|
46
|
+
const r=await fetch('/api/audit');const d=await r.json();
|
|
47
|
+
const recs=d.records||[];
|
|
48
|
+
document.getElementById('meta').textContent=`${recs.length} 条记录 · 更新于 ${new Date().toLocaleTimeString()} · 30s 自动刷新`;
|
|
49
|
+
const executed=recs.filter(x=>x.decision==='executed');
|
|
50
|
+
const blocked=recs.filter(x=>x.decision.startsWith('blocked')||x.decision==='dry_run');
|
|
51
|
+
const spent=executed.reduce((s,x)=>s+(x.amount||0),0);
|
|
52
|
+
const maxAmt=Math.max(...recs.map(x=>x.amount||0),0);
|
|
53
|
+
document.getElementById('stats').innerHTML=
|
|
54
|
+
`<div class="stat"><div class="v purple">${recs.length}</div><div class="k">总尝试</div></div>
|
|
55
|
+
<div class="stat"><div class="v green">${executed.length}</div><div class="k">已放行</div></div>
|
|
56
|
+
<div class="stat"><div class="v red">${blocked.length}</div><div class="k">被拦截</div></div>
|
|
57
|
+
<div class="stat"><div class="v">¥${spent.toFixed(2)}</div><div class="k">累计已花</div></div>
|
|
58
|
+
<div class="stat"><div class="v yellow">¥${maxAmt.toFixed(2)}</div><div class="k">最大单笔</div></div>`;
|
|
59
|
+
const tb=document.querySelector('#tbl tbody');tb.innerHTML='';
|
|
60
|
+
for(const r of recs){
|
|
61
|
+
const cls=r.decision==='executed'?'executed':(r.decision==='dry_run'?'dry':'blocked');
|
|
62
|
+
const t=new Date(r.ts*1000).toLocaleString('zh-CN',{month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit',second:'2-digit'});
|
|
63
|
+
tb.insertAdjacentHTML('beforeend',
|
|
64
|
+
`<tr><td>${t}</td><td>${r.action}</td><td>¥${(r.amount||0).toFixed(2)}</td><td>${r.to}</td>
|
|
65
|
+
<td class="${cls}">${r.decision}</td><td style="color:#9da7b3">${r.reason||''}</td><td>¥${(r.spent_after||0).toFixed(2)}</td></tr>`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
load();setInterval(load,30000);
|
|
69
|
+
</script></body></html>
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class Handler(BaseHTTPRequestHandler):
|
|
74
|
+
file = "audit.json"
|
|
75
|
+
|
|
76
|
+
def _json(self, obj):
|
|
77
|
+
body = json.dumps(obj, ensure_ascii=False).encode()
|
|
78
|
+
self.send_response(200)
|
|
79
|
+
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
80
|
+
self.send_header("Content-Length", str(len(body)))
|
|
81
|
+
self.end_headers()
|
|
82
|
+
self.wfile.write(body)
|
|
83
|
+
|
|
84
|
+
def do_GET(self):
|
|
85
|
+
u = urlparse(self.path)
|
|
86
|
+
if u.path in ("/", "/index.html"):
|
|
87
|
+
body = PAGE.encode()
|
|
88
|
+
self.send_response(200)
|
|
89
|
+
self.send_header("Content-Type", "text/html; charset=utf-8")
|
|
90
|
+
self.send_header("Content-Length", str(len(body)))
|
|
91
|
+
self.end_headers()
|
|
92
|
+
self.wfile.write(body)
|
|
93
|
+
elif u.path == "/api/audit":
|
|
94
|
+
try:
|
|
95
|
+
with open(self.file, encoding="utf-8") as f:
|
|
96
|
+
records = json.load(f)
|
|
97
|
+
except Exception:
|
|
98
|
+
records = []
|
|
99
|
+
self._json({"records": records})
|
|
100
|
+
else:
|
|
101
|
+
self._json({"error": "not found"})
|
|
102
|
+
|
|
103
|
+
def log_message(self, *a):
|
|
104
|
+
pass
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def main():
|
|
108
|
+
ap = argparse.ArgumentParser(prog="spendshield-dashboard")
|
|
109
|
+
ap.add_argument("--file", default="audit.json", help="审计 JSON 文件")
|
|
110
|
+
ap.add_argument("--port", type=int, default=8775)
|
|
111
|
+
args = ap.parse_args()
|
|
112
|
+
Handler.file = args.file
|
|
113
|
+
srv = ThreadingHTTPServer(("0.0.0.0", args.port), Handler)
|
|
114
|
+
print(f"SpendShield 审计看板: http://localhost:{args.port} (file={args.file})")
|
|
115
|
+
srv.serve_forever()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
if __name__ == "__main__":
|
|
119
|
+
main()
|