git-ai-control 0.3.1 → 0.4.3
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.
- package/CHANGELOG.md +41 -0
- package/COMPATIBILITY.md +2 -1
- package/README.md +36 -7
- package/bin/git-ai-control.js +12 -6
- package/package.json +5 -1
- package/plugins/upload-filter/plugin_filter_runtime.py +266 -23
- package/policy.example.json +43 -5
- package/scripts/check.mjs +16 -0
- package/scripts/install.mjs +40 -1
- package/scripts/verify-release.mjs +19 -0
- package/server.py +182 -11
- package/static/assets/index-DGRb1OwU.js +50 -0
- package/static/assets/index-DSQfR-ve.css +2 -0
- package/static/index.html +2 -2
- package/static/assets/index-DWeFCLPQ.js +0 -50
- package/static/assets/index-WWn7U1pB.css +0 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
本项目的所有重要变更都会记录在此文件中。
|
|
4
4
|
|
|
5
|
+
## [0.4.3] - 2026-07-30
|
|
6
|
+
|
|
7
|
+
### 修复
|
|
8
|
+
|
|
9
|
+
- 使用 Linux Node.js 24 环境重新生成前端锁文件,补齐 Linux 发布环境所需的 `@emnapi/core` 和 `@emnapi/runtime` 可选依赖,修复 `npm ci` 跨平台解析失败。
|
|
10
|
+
|
|
11
|
+
## [0.4.2] - 2026-07-30
|
|
12
|
+
|
|
13
|
+
### 修复
|
|
14
|
+
|
|
15
|
+
- 重建前端依赖锁文件,补齐 WASM 可选依赖的嵌套锁定版本,修复发布工作流中 `npm ci` 失败的问题。
|
|
16
|
+
|
|
17
|
+
## [0.4.1] - 2026-07-30
|
|
18
|
+
|
|
19
|
+
### 新增
|
|
20
|
+
|
|
21
|
+
- 新增敏感内容脱敏和 Agent / 模型治理独立插件;
|
|
22
|
+
- 仓库插件支持多个规则、全局兜底、仓库/目录/分支匹配和优先级;
|
|
23
|
+
- Git AI 设置与插件管理均可查看已写入磁盘的 JSON 配置;
|
|
24
|
+
- 插件管理使用统一添加顺序,新插件始终追加到列表最后;
|
|
25
|
+
- 新增 Git AI 启用与暂停状态展示及本机服务复用检测。
|
|
26
|
+
|
|
27
|
+
### 变更
|
|
28
|
+
|
|
29
|
+
- `npx git-ai-control` 改为唯一启动命令;已运行时复用现有本机服务,移除 `start` 子命令;
|
|
30
|
+
- 移除本地用量与成本看板、聚合数据文件和相关接口;
|
|
31
|
+
- 配置查看弹窗改为默认全展开的 JSON 树,并加宽显示区域。
|
|
32
|
+
|
|
33
|
+
## [0.4.0] - 2026-07-30
|
|
34
|
+
|
|
35
|
+
### 新增
|
|
36
|
+
|
|
37
|
+
- 新增 Git 标签触发的 npm Trusted Publishing 工作流;
|
|
38
|
+
- 发布前在 macOS、Linux 和 Windows 上验证同一份源码,并校验标签与 `package.json` 版本一致;
|
|
39
|
+
- npm 发布使用 GitHub Actions OIDC 短期凭据,不再需要长期 npm Access Token。
|
|
40
|
+
|
|
41
|
+
### 变更
|
|
42
|
+
|
|
43
|
+
- 前端静态资源在每个工作流中只构建一次,三平台矩阵只验证平台相关运行时;
|
|
44
|
+
- GitHub Actions 升级到 Node.js 24 运行时版本。
|
|
45
|
+
|
|
5
46
|
## [0.3.1] - 2026-07-30
|
|
6
47
|
|
|
7
48
|
### 修复
|
package/COMPATIBILITY.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## 操作系统
|
|
4
4
|
|
|
5
|
-
`npx git-ai-control
|
|
5
|
+
`npx git-ai-control` 支持 macOS、Linux 和 Windows,三个系统共用相同的配置格式、过滤运行时和 Web 页面,仅用户级服务管理方式不同:
|
|
6
6
|
|
|
7
7
|
- macOS:LaunchAgent;
|
|
8
8
|
- Linux:systemd user;
|
|
@@ -22,6 +22,7 @@ Linux 需要可用的 systemd 用户会话。Windows 需要当前用户拥有创
|
|
|
22
22
|
| 仓库级数据类型开关 | 支持 | 不支持 |
|
|
23
23
|
| 仓库、路径、分支字段脱敏 | 支持 | 不支持 |
|
|
24
24
|
| Skill 名称过滤 | 支持 | 不支持 |
|
|
25
|
+
| 敏感内容脱敏、Agent/模型治理 | 支持 | 不支持 |
|
|
25
26
|
| GitHub 仅上传脱敏 Token | 支持 | 官方版当前无法通过外部配置实现 |
|
|
26
27
|
|
|
27
28
|
安装脚本会检查 `git-ai` 二进制是否包含 `custom_metrics.json` 扩展点:
|
package/README.md
CHANGED
|
@@ -15,7 +15,10 @@
|
|
|
15
15
|
- 可删除仓库信息、项目路径和分支名称等敏感字段;
|
|
16
16
|
- 可将允许上传的 Skill 项目目录替换为固定目录;
|
|
17
17
|
- 可按关键词或正则拦截指定 Skill;
|
|
18
|
-
-
|
|
18
|
+
- 仓库插件可按不同主机添加多个,并支持全局兜底、仓库、目录、分支与优先级;
|
|
19
|
+
- 敏感内容脱敏插件可识别 API Key、私钥、凭据,并按规则脱敏或拦截;
|
|
20
|
+
- Agent / 模型治理插件支持允许名单、阻止正则及审计/拦截模式;
|
|
21
|
+
- Skill、敏感内容和 Agent / 模型治理均使用独立配置表单;
|
|
19
22
|
- 支持配置验证、运行状态检查和本机过滤服务重启。
|
|
20
23
|
|
|
21
24
|
## 界面截图
|
|
@@ -28,7 +31,9 @@
|
|
|
28
31
|
|
|
29
32
|

|
|
30
33
|
|
|
31
|
-
|
|
34
|
+
仓库插件可以同时配置匹配主机、允许上传的数据类型和允许保留的字段,也可以按不同主机添加多个。主机规则重叠时按列表顺序使用第一个命中插件;Skill 过滤插件使用独立表单管理名称规则,且只能添加一次。
|
|
35
|
+
|
|
36
|
+
插件管理页面按添加顺序展示全部插件。无论添加仓库插件还是独立插件,新卡片都会追加到现有列表最后;这只影响页面展示顺序,不改变仓库规则的匹配优先级。
|
|
32
37
|
|
|
33
38
|
## 快速开始
|
|
34
39
|
|
|
@@ -44,10 +49,12 @@ Windows: %USERPROFILE%\.git-ai\bin\git-ai.exe
|
|
|
44
49
|
确认文件存在后运行:
|
|
45
50
|
|
|
46
51
|
```bash
|
|
47
|
-
npx git-ai-control
|
|
52
|
+
npx git-ai-control
|
|
48
53
|
```
|
|
49
54
|
|
|
50
|
-
|
|
55
|
+
首次运行时,该命令会自动下载最新版本、安装本机服务,并打开 Git AI 配置中心;之后检测到已运行的配置中心时,会直接复用现有进程。
|
|
56
|
+
|
|
57
|
+
首次安装过程会:
|
|
51
58
|
|
|
52
59
|
1. 识别当前 Git AI 是否支持 `custom_metrics` 扩展;
|
|
53
60
|
2. 将原始上报端点备份到仅当前用户可读的本机配置;
|
|
@@ -82,9 +89,18 @@ http://127.0.0.1:38742
|
|
|
82
89
|
4. 关闭“仓库信息”“项目路径”和“分支名称”;
|
|
83
90
|
5. 保存配置。
|
|
84
91
|
|
|
85
|
-
|
|
92
|
+
同一个仓库插件也可以填写多个主机,每行一个;也可以为不同主机分别添加仓库插件。GitLab、Gitee 和自建 Git 服务不需要单独开发插件。
|
|
93
|
+
|
|
94
|
+
仓库策略会先匹配特定规则,再匹配“全部仓库”全局规则;特定规则之间优先级数字越大越靠前,同优先级按插件列表顺序处理。仓库、目录、分支同时填写时需要全部命中。
|
|
95
|
+
|
|
96
|
+
### 3. 配置敏感内容与 Agent
|
|
97
|
+
|
|
98
|
+
在“插件管理”中可分别添加以下独立插件:
|
|
99
|
+
|
|
100
|
+
1. “敏感内容脱敏”:选择作用事件、内置规则和自定义正则;命中后可脱敏或阻止上报;
|
|
101
|
+
2. “Agent / 模型治理”:配置允许的 Agent、模型与阻止正则;先使用“仅审计”确认命中情况,再按需改为阻止。
|
|
86
102
|
|
|
87
|
-
###
|
|
103
|
+
### 4. 配置 Skill 过滤插件
|
|
88
104
|
|
|
89
105
|
需要按名称拦截 Skill 时:
|
|
90
106
|
|
|
@@ -95,7 +111,7 @@ http://127.0.0.1:38742
|
|
|
95
111
|
|
|
96
112
|
Skill 过滤插件只负责名称规则。仓库匹配、上传类型和目录替换仍由仓库插件负责。
|
|
97
113
|
|
|
98
|
-
###
|
|
114
|
+
### 5. 验证配置
|
|
99
115
|
|
|
100
116
|
点击页面中的“验证配置”。结果会通过右上角 Toast 显示,不会占用配置表单空间。
|
|
101
117
|
|
|
@@ -119,6 +135,8 @@ Git AI
|
|
|
119
135
|
-> 拦截,或脱敏后转发至原上报服务
|
|
120
136
|
```
|
|
121
137
|
|
|
138
|
+
两个页签中的“查看配置”会重新读取当前磁盘内容,不会显示未保存的表单修改。Git AI 原生配置只展示本页面安全管理的字段,插件策略展示完整已写入策略。
|
|
139
|
+
|
|
122
140
|
控制页面保存时只更新:
|
|
123
141
|
|
|
124
142
|
- `~/.git-ai/config.json`:Git AI 原生设置;
|
|
@@ -179,6 +197,17 @@ npm run check
|
|
|
179
197
|
|
|
180
198
|
新增插件类型时,请为它设计独立的数据结构和配置表单;不要复用仓库插件表单来表达无关能力。涉及上报兼容性时,应基于实际 Git AI 版本和请求路径验证,避免把“页面可配置”误写成“运行时已接管”。
|
|
181
199
|
|
|
200
|
+
## 发布
|
|
201
|
+
|
|
202
|
+
项目只发布一个跨平台 npm 包,不分别生成 macOS、Linux 或 Windows 安装包。推送与 `package.json` 版本一致的 Git 标签后,GitHub Actions 会:
|
|
203
|
+
|
|
204
|
+
1. 在 macOS、Ubuntu 和 Windows 上检查同一份安装器与运行时源码;
|
|
205
|
+
2. 在 Ubuntu 上只构建一次前端静态资源;
|
|
206
|
+
3. 确认构建产物与仓库内容一致;
|
|
207
|
+
4. 通过 npm Trusted Publishing(OIDC)发布到 npmjs.com。
|
|
208
|
+
|
|
209
|
+
发布工作流不使用长期 npm Access Token。npm 包需要将 `.github/workflows/publish.yml` 配置为可信发布者。
|
|
210
|
+
|
|
182
211
|
## 安全边界
|
|
183
212
|
|
|
184
213
|
- Web 服务只绑定本机回环地址;
|
package/bin/git-ai-control.js
CHANGED
|
@@ -5,6 +5,7 @@ import path from "node:path"
|
|
|
5
5
|
import {fileURLToPath} from "node:url"
|
|
6
6
|
|
|
7
7
|
import {browserCommand} from "../scripts/platform-services.mjs"
|
|
8
|
+
import {controlPanelIsRunning} from "../scripts/install.mjs"
|
|
8
9
|
|
|
9
10
|
const command = process.argv[2]
|
|
10
11
|
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")
|
|
@@ -14,17 +15,17 @@ function printHelp() {
|
|
|
14
15
|
console.log(`Git AI Control Panel
|
|
15
16
|
|
|
16
17
|
用法:
|
|
17
|
-
git-ai-control
|
|
18
|
+
git-ai-control 首次安装服务;已运行时复用现有进程并打开配置页面
|
|
18
19
|
git-ai-control uninstall 卸载本机服务并恢复上报端点
|
|
19
20
|
git-ai-control --help 显示帮助`)
|
|
20
21
|
}
|
|
21
22
|
|
|
22
|
-
if (command === "--help" || command === "-h"
|
|
23
|
+
if (command === "--help" || command === "-h") {
|
|
23
24
|
printHelp()
|
|
24
25
|
process.exit(0)
|
|
25
26
|
}
|
|
26
27
|
|
|
27
|
-
if (
|
|
28
|
+
if (command && command !== "uninstall") {
|
|
28
29
|
console.error(`未知命令:${command}`)
|
|
29
30
|
printHelp()
|
|
30
31
|
process.exit(1)
|
|
@@ -36,10 +37,15 @@ try {
|
|
|
36
37
|
await uninstall()
|
|
37
38
|
process.exit(0)
|
|
38
39
|
}
|
|
39
|
-
const
|
|
40
|
-
|
|
40
|
+
const reused = await controlPanelIsRunning()
|
|
41
|
+
if (!reused) {
|
|
42
|
+
const {install} = await import("../scripts/install.mjs")
|
|
43
|
+
await install()
|
|
44
|
+
} else {
|
|
45
|
+
console.log("检测到已运行的 Git AI 配置中心,复用现有进程")
|
|
46
|
+
}
|
|
41
47
|
} catch (error) {
|
|
42
|
-
console.error(`${command === "
|
|
48
|
+
console.error(`${command === "uninstall" ? "卸载" : "启动"}失败:${error.message}`)
|
|
43
49
|
process.exit(1)
|
|
44
50
|
}
|
|
45
51
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "git-ai-control",
|
|
3
|
-
"version": "0.3
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Git AI 本地配置与上传隐私控制面板",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -40,5 +40,9 @@
|
|
|
40
40
|
"homepage": "https://github.com/yingyanzhitong/git-ai-control#readme",
|
|
41
41
|
"bugs": {
|
|
42
42
|
"url": "https://github.com/yingyanzhitong/git-ai-control/issues"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public",
|
|
46
|
+
"registry": "https://registry.npmjs.org/"
|
|
43
47
|
}
|
|
44
48
|
}
|
|
@@ -10,6 +10,7 @@ import subprocess
|
|
|
10
10
|
import sys
|
|
11
11
|
import urllib.error
|
|
12
12
|
import urllib.request
|
|
13
|
+
from fnmatch import fnmatchcase
|
|
13
14
|
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
|
|
14
15
|
from pathlib import Path
|
|
15
16
|
from urllib.parse import urlsplit
|
|
@@ -86,10 +87,26 @@ BRANCH_KEYS = {
|
|
|
86
87
|
"baseref",
|
|
87
88
|
}
|
|
88
89
|
NORMALIZED_PATH_KEYS = {re.sub(r"[^a-z0-9]", "", key.lower()) for key in PATH_KEYS}
|
|
90
|
+
AGENT_KEYS = {"agent", "agentname", "agentid", "agenttype", "tool", "toolname"}
|
|
91
|
+
MODEL_KEYS = {"model", "modelname", "modelid", "llmmodel"}
|
|
92
|
+
SENSITIVE_PATTERNS = {
|
|
93
|
+
"api_key": re.compile(
|
|
94
|
+
r"(?i)\b(?:sk|rk|ghp|github_pat|glpat|xox[baprs]|akia)[a-z0-9_=-]{12,}\b"
|
|
95
|
+
),
|
|
96
|
+
"private_key": re.compile(
|
|
97
|
+
r"-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----"
|
|
98
|
+
),
|
|
99
|
+
"credential": re.compile(
|
|
100
|
+
r"(?i)\b(?:password|passwd|secret|api[_-]?key)\s*[:=]\s*([^\s,;\]\}]{4,})"
|
|
101
|
+
),
|
|
102
|
+
"email": re.compile(r"(?i)\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b"),
|
|
103
|
+
"phone": re.compile(r"(?<!\d)(?:\+?\d[\s-]?){7,14}\d(?!\d)"),
|
|
104
|
+
}
|
|
89
105
|
DROP = object()
|
|
90
106
|
|
|
91
107
|
DEFAULT_POLICY_CONFIG = {
|
|
92
|
-
"version":
|
|
108
|
+
"version": 2,
|
|
109
|
+
"git_ai_enabled": True,
|
|
93
110
|
"default_allow_unmatched": True,
|
|
94
111
|
"skill_policy": {
|
|
95
112
|
"installed": True,
|
|
@@ -120,7 +137,14 @@ DEFAULT_POLICY_CONFIG = {
|
|
|
120
137
|
"id": "github",
|
|
121
138
|
"name": "GitHub 仓库",
|
|
122
139
|
"enabled": True,
|
|
123
|
-
"
|
|
140
|
+
"priority": 100,
|
|
141
|
+
"match": {
|
|
142
|
+
"all_repositories": False,
|
|
143
|
+
"hosts": ["github.com"],
|
|
144
|
+
"repositories": [],
|
|
145
|
+
"directories": [],
|
|
146
|
+
"branches": [],
|
|
147
|
+
},
|
|
124
148
|
"fixed_project_directory": "",
|
|
125
149
|
"allow": {
|
|
126
150
|
"token": False,
|
|
@@ -138,6 +162,37 @@ DEFAULT_POLICY_CONFIG = {
|
|
|
138
162
|
},
|
|
139
163
|
}
|
|
140
164
|
],
|
|
165
|
+
"plugin_order": ["repository:github", "skill_filter"],
|
|
166
|
+
"sensitive_data_policy": {
|
|
167
|
+
"installed": False,
|
|
168
|
+
"enabled": False,
|
|
169
|
+
"action": "redact",
|
|
170
|
+
"event_types": {
|
|
171
|
+
"token": True,
|
|
172
|
+
"skill": True,
|
|
173
|
+
"commit": True,
|
|
174
|
+
"checkpoint": True,
|
|
175
|
+
"agent": True,
|
|
176
|
+
"prompt_duration": True,
|
|
177
|
+
"prompt_report": True,
|
|
178
|
+
},
|
|
179
|
+
"built_in_rules": {
|
|
180
|
+
"api_key": True,
|
|
181
|
+
"private_key": True,
|
|
182
|
+
"credential": True,
|
|
183
|
+
"email": False,
|
|
184
|
+
"phone": False,
|
|
185
|
+
},
|
|
186
|
+
"custom_patterns": [],
|
|
187
|
+
},
|
|
188
|
+
"agent_model_policy": {
|
|
189
|
+
"installed": False,
|
|
190
|
+
"enabled": False,
|
|
191
|
+
"mode": "audit",
|
|
192
|
+
"allowed_agents": [],
|
|
193
|
+
"allowed_models": [],
|
|
194
|
+
"blocked_patterns": [],
|
|
195
|
+
},
|
|
141
196
|
}
|
|
142
197
|
|
|
143
198
|
|
|
@@ -226,6 +281,42 @@ def iter_path_text(value):
|
|
|
226
281
|
yield from iter_path_text(child)
|
|
227
282
|
|
|
228
283
|
|
|
284
|
+
def iter_repository_text(value):
|
|
285
|
+
if isinstance(value, dict):
|
|
286
|
+
for key, child in value.items():
|
|
287
|
+
if is_repository_key(key):
|
|
288
|
+
yield from iter_all_text(child)
|
|
289
|
+
else:
|
|
290
|
+
yield from iter_repository_text(child)
|
|
291
|
+
elif isinstance(value, list):
|
|
292
|
+
for child in value:
|
|
293
|
+
yield from iter_repository_text(child)
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def iter_branch_text(value):
|
|
297
|
+
if isinstance(value, dict):
|
|
298
|
+
for key, child in value.items():
|
|
299
|
+
if is_branch_key(key):
|
|
300
|
+
yield from iter_all_text(child)
|
|
301
|
+
else:
|
|
302
|
+
yield from iter_branch_text(child)
|
|
303
|
+
elif isinstance(value, list):
|
|
304
|
+
for child in value:
|
|
305
|
+
yield from iter_branch_text(child)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def iter_named_text(value, keys: set[str]):
|
|
309
|
+
if isinstance(value, dict):
|
|
310
|
+
for key, child in value.items():
|
|
311
|
+
if normalize_key(key) in keys:
|
|
312
|
+
yield from iter_all_text(child)
|
|
313
|
+
else:
|
|
314
|
+
yield from iter_named_text(child, keys)
|
|
315
|
+
elif isinstance(value, list):
|
|
316
|
+
for child in value:
|
|
317
|
+
yield from iter_named_text(child, keys)
|
|
318
|
+
|
|
319
|
+
|
|
229
320
|
def read_remote_text(path: str) -> str:
|
|
230
321
|
if not isinstance(path, str) or not path:
|
|
231
322
|
return ""
|
|
@@ -255,7 +346,7 @@ def load_policy_config() -> tuple[dict, str]:
|
|
|
255
346
|
try:
|
|
256
347
|
with POLICY_CONFIG_PATH.open("r", encoding="utf-8") as handle:
|
|
257
348
|
config = json.load(handle)
|
|
258
|
-
if not isinstance(config, dict) or config.get("version")
|
|
349
|
+
if not isinstance(config, dict) or config.get("version") not in {1, 2}:
|
|
259
350
|
raise ValueError("unsupported policy version")
|
|
260
351
|
if not isinstance(config.get("plugins"), list):
|
|
261
352
|
raise ValueError("plugins must be a list")
|
|
@@ -273,24 +364,59 @@ def host_needles(plugin: dict) -> list[str]:
|
|
|
273
364
|
]
|
|
274
365
|
|
|
275
366
|
|
|
367
|
+
def selector_matches(pattern: str, candidates) -> bool:
|
|
368
|
+
normalized_pattern = str(pattern).strip().lower()
|
|
369
|
+
if not normalized_pattern:
|
|
370
|
+
return False
|
|
371
|
+
for candidate in candidates:
|
|
372
|
+
normalized_candidate = str(candidate).lower()
|
|
373
|
+
if fnmatchcase(normalized_candidate, normalized_pattern):
|
|
374
|
+
return True
|
|
375
|
+
if "*" not in normalized_pattern and "?" not in normalized_pattern and normalized_pattern in normalized_candidate:
|
|
376
|
+
return True
|
|
377
|
+
return False
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def selectors_match(patterns, candidates) -> bool:
|
|
381
|
+
if not patterns:
|
|
382
|
+
return True
|
|
383
|
+
return any(selector_matches(pattern, candidates) for pattern in patterns)
|
|
384
|
+
|
|
385
|
+
|
|
276
386
|
def plugin_matches(plugin: dict, payload, raw_body: bytes) -> bool:
|
|
387
|
+
match = plugin.get("match", {})
|
|
388
|
+
if match.get("all_repositories", False):
|
|
389
|
+
return True
|
|
277
390
|
needles = host_needles(plugin)
|
|
278
|
-
|
|
391
|
+
repositories = match.get("repositories", [])
|
|
392
|
+
directories = match.get("directories", [])
|
|
393
|
+
branches = match.get("branches", [])
|
|
394
|
+
if not any((needles, repositories, directories, branches)):
|
|
279
395
|
return False
|
|
280
396
|
raw_text = raw_body.decode("utf-8", errors="ignore").lower()
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
397
|
+
path_values = list(iter_path_text(payload))
|
|
398
|
+
remote_values = [read_remote_text(path) for path in path_values]
|
|
399
|
+
host_values = [raw_text, *iter_all_text(payload), *remote_values]
|
|
400
|
+
repository_values = [*iter_repository_text(payload), *remote_values]
|
|
401
|
+
return (
|
|
402
|
+
selectors_match(needles, host_values)
|
|
403
|
+
and selectors_match(repositories, repository_values)
|
|
404
|
+
and selectors_match(directories, path_values)
|
|
405
|
+
and selectors_match(branches, iter_branch_text(payload))
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def plugin_sort_key(item) -> tuple[int, int, int]:
|
|
410
|
+
index, plugin = item
|
|
411
|
+
match = plugin.get("match", {})
|
|
412
|
+
global_rule = bool(match.get("all_repositories", False))
|
|
413
|
+
priority = plugin.get("priority", 100)
|
|
414
|
+
return (1 if global_rule else 0, -priority if isinstance(priority, int) else -100, index)
|
|
290
415
|
|
|
291
416
|
|
|
292
417
|
def find_matching_plugin(config: dict, payload, raw_body: bytes) -> dict | None:
|
|
293
|
-
|
|
418
|
+
plugins = sorted(enumerate(config.get("plugins", [])), key=plugin_sort_key)
|
|
419
|
+
for _, plugin in plugins:
|
|
294
420
|
if plugin.get("enabled", True) and plugin_matches(plugin, payload, raw_body):
|
|
295
421
|
return plugin
|
|
296
422
|
return None
|
|
@@ -353,6 +479,73 @@ def sanitize_fields(value, fields: dict, hosts: list[str]):
|
|
|
353
479
|
return value
|
|
354
480
|
|
|
355
481
|
|
|
482
|
+
def sensitive_policy_matches(policy: dict, event_type: str | None) -> bool:
|
|
483
|
+
return bool(
|
|
484
|
+
policy.get("installed", False)
|
|
485
|
+
and policy.get("enabled", False)
|
|
486
|
+
and policy.get("event_types", {}).get(event_type, False)
|
|
487
|
+
)
|
|
488
|
+
|
|
489
|
+
|
|
490
|
+
def redact_sensitive_content(value, policy: dict) -> tuple[object, list[str]]:
|
|
491
|
+
patterns = [
|
|
492
|
+
(name, pattern)
|
|
493
|
+
for name, pattern in SENSITIVE_PATTERNS.items()
|
|
494
|
+
if policy.get("built_in_rules", {}).get(name, False)
|
|
495
|
+
]
|
|
496
|
+
for index, source in enumerate(policy.get("custom_patterns", []), start=1):
|
|
497
|
+
try:
|
|
498
|
+
patterns.append((f"custom_{index}", re.compile(str(source), re.IGNORECASE)))
|
|
499
|
+
except re.error:
|
|
500
|
+
continue
|
|
501
|
+
|
|
502
|
+
findings: list[str] = []
|
|
503
|
+
|
|
504
|
+
def clean(current):
|
|
505
|
+
if isinstance(current, dict):
|
|
506
|
+
return {key: clean(child) for key, child in current.items()}
|
|
507
|
+
if isinstance(current, list):
|
|
508
|
+
return [clean(child) for child in current]
|
|
509
|
+
if not isinstance(current, str):
|
|
510
|
+
return current
|
|
511
|
+
sanitized = current
|
|
512
|
+
for name, pattern in patterns:
|
|
513
|
+
sanitized, count = pattern.subn(f"[已脱敏:{name}]", sanitized)
|
|
514
|
+
if count:
|
|
515
|
+
findings.append(name)
|
|
516
|
+
return sanitized
|
|
517
|
+
|
|
518
|
+
return clean(value), sorted(set(findings))
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def matches_allowlist(value: str, patterns) -> bool:
|
|
522
|
+
return any(selector_matches(pattern, [value]) for pattern in patterns)
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def governance_findings(payload, policy: dict) -> list[str]:
|
|
526
|
+
if not policy.get("installed", False) or not policy.get("enabled", False):
|
|
527
|
+
return []
|
|
528
|
+
agents = sorted(set(iter_named_text(payload, AGENT_KEYS)))
|
|
529
|
+
models = sorted(set(iter_named_text(payload, MODEL_KEYS)))
|
|
530
|
+
compiled_patterns = []
|
|
531
|
+
for source in policy.get("blocked_patterns", []):
|
|
532
|
+
try:
|
|
533
|
+
compiled_patterns.append(re.compile(str(source), re.IGNORECASE))
|
|
534
|
+
except re.error:
|
|
535
|
+
continue
|
|
536
|
+
findings = []
|
|
537
|
+
for label, values, allowlist in (
|
|
538
|
+
("agent", agents, policy.get("allowed_agents", [])),
|
|
539
|
+
("model", models, policy.get("allowed_models", [])),
|
|
540
|
+
):
|
|
541
|
+
for value in values:
|
|
542
|
+
if any(pattern.search(value) for pattern in compiled_patterns):
|
|
543
|
+
findings.append(f"blocked_{label}")
|
|
544
|
+
elif allowlist and not matches_allowlist(value, allowlist):
|
|
545
|
+
findings.append(f"unapproved_{label}")
|
|
546
|
+
return sorted(set(findings))
|
|
547
|
+
|
|
548
|
+
|
|
356
549
|
def evaluate_request(
|
|
357
550
|
endpoint_path: str,
|
|
358
551
|
payload,
|
|
@@ -366,6 +559,14 @@ def evaluate_request(
|
|
|
366
559
|
if raw_body is None:
|
|
367
560
|
raw_body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
|
368
561
|
event_type = EVENT_TYPES.get(endpoint_path)
|
|
562
|
+
if not config.get("git_ai_enabled", True):
|
|
563
|
+
return {
|
|
564
|
+
"blocked": True,
|
|
565
|
+
"reason": "git_ai_paused",
|
|
566
|
+
"payload": payload,
|
|
567
|
+
"plugin": None,
|
|
568
|
+
"config_error": config_error,
|
|
569
|
+
}
|
|
369
570
|
plugin = find_matching_plugin(config, payload, raw_body)
|
|
370
571
|
plugin_id = plugin.get("id") if plugin else None
|
|
371
572
|
|
|
@@ -419,12 +620,46 @@ def evaluate_request(
|
|
|
419
620
|
"config_error": config_error,
|
|
420
621
|
}
|
|
421
622
|
|
|
623
|
+
sensitive_findings: list[str] = []
|
|
624
|
+
sensitive_policy = config.get("sensitive_data_policy", {})
|
|
625
|
+
if sensitive_policy_matches(sensitive_policy, event_type):
|
|
626
|
+
transformed, sensitive_findings = redact_sensitive_content(
|
|
627
|
+
transformed, sensitive_policy
|
|
628
|
+
)
|
|
629
|
+
if sensitive_findings and sensitive_policy.get("action") == "block":
|
|
630
|
+
return {
|
|
631
|
+
"blocked": True,
|
|
632
|
+
"reason": "sensitive_content",
|
|
633
|
+
"payload": payload,
|
|
634
|
+
"plugin": plugin_id,
|
|
635
|
+
"config_error": config_error,
|
|
636
|
+
"findings": sensitive_findings,
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
governance_findings_list = governance_findings(
|
|
640
|
+
transformed, config.get("agent_model_policy", {})
|
|
641
|
+
)
|
|
642
|
+
agent_model_policy = config.get("agent_model_policy", {})
|
|
643
|
+
if governance_findings_list and agent_model_policy.get("mode") == "block":
|
|
644
|
+
return {
|
|
645
|
+
"blocked": True,
|
|
646
|
+
"reason": "agent_model_policy",
|
|
647
|
+
"payload": payload,
|
|
648
|
+
"plugin": plugin_id,
|
|
649
|
+
"config_error": config_error,
|
|
650
|
+
"findings": governance_findings_list,
|
|
651
|
+
}
|
|
652
|
+
|
|
422
653
|
return {
|
|
423
654
|
"blocked": False,
|
|
424
655
|
"reason": "",
|
|
425
656
|
"payload": transformed,
|
|
426
657
|
"plugin": plugin_id,
|
|
427
658
|
"config_error": config_error,
|
|
659
|
+
"findings": {
|
|
660
|
+
"sensitive": sensitive_findings,
|
|
661
|
+
"agent_model": governance_findings_list,
|
|
662
|
+
},
|
|
428
663
|
}
|
|
429
664
|
|
|
430
665
|
|
|
@@ -450,6 +685,10 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
450
685
|
{
|
|
451
686
|
"ok": not bool(config_error or upstream_error),
|
|
452
687
|
"mode": "plugins",
|
|
688
|
+
"gitAiEnabled": config.get("git_ai_enabled", True),
|
|
689
|
+
"gitAiStatus": "enabled"
|
|
690
|
+
if config.get("git_ai_enabled", True)
|
|
691
|
+
else "paused",
|
|
453
692
|
"policyVersion": config.get("version"),
|
|
454
693
|
"plugins": [
|
|
455
694
|
plugin.get("id")
|
|
@@ -466,15 +705,7 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
466
705
|
|
|
467
706
|
def do_POST(self):
|
|
468
707
|
endpoint_path = urlsplit(self.path).path
|
|
469
|
-
|
|
470
|
-
if upstream_error:
|
|
471
|
-
self.send_json(
|
|
472
|
-
503,
|
|
473
|
-
{"code": -1, "data": None, "message": upstream_error},
|
|
474
|
-
)
|
|
475
|
-
return
|
|
476
|
-
upstream_url = upstreams.get(endpoint_path)
|
|
477
|
-
if not upstream_url:
|
|
708
|
+
if endpoint_path not in EVENT_TYPES:
|
|
478
709
|
self.send_json(404, {"code": -1, "data": None, "message": "unknown endpoint"})
|
|
479
710
|
return
|
|
480
711
|
|
|
@@ -491,6 +722,18 @@ class Handler(BaseHTTPRequestHandler):
|
|
|
491
722
|
self.send_filtered(endpoint_path, decision["reason"], decision.get("plugin"))
|
|
492
723
|
return
|
|
493
724
|
|
|
725
|
+
upstreams, upstream_error = load_upstreams()
|
|
726
|
+
if upstream_error:
|
|
727
|
+
self.send_json(
|
|
728
|
+
503,
|
|
729
|
+
{"code": -1, "data": None, "message": upstream_error},
|
|
730
|
+
)
|
|
731
|
+
return
|
|
732
|
+
upstream_url = upstreams.get(endpoint_path)
|
|
733
|
+
if not upstream_url:
|
|
734
|
+
self.send_json(404, {"code": -1, "data": None, "message": "unknown endpoint"})
|
|
735
|
+
return
|
|
736
|
+
|
|
494
737
|
body = json.dumps(
|
|
495
738
|
decision["payload"],
|
|
496
739
|
ensure_ascii=False,
|
package/policy.example.json
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version":
|
|
2
|
+
"version": 2,
|
|
3
|
+
"git_ai_enabled": true,
|
|
3
4
|
"default_allow_unmatched": true,
|
|
4
5
|
"skill_policy": {
|
|
5
6
|
"installed": true,
|
|
@@ -28,10 +29,13 @@
|
|
|
28
29
|
"id": "github",
|
|
29
30
|
"name": "GitHub 仓库",
|
|
30
31
|
"enabled": true,
|
|
32
|
+
"priority": 100,
|
|
31
33
|
"match": {
|
|
32
|
-
"
|
|
33
|
-
|
|
34
|
-
]
|
|
34
|
+
"all_repositories": false,
|
|
35
|
+
"hosts": ["github.com"],
|
|
36
|
+
"repositories": [],
|
|
37
|
+
"directories": [],
|
|
38
|
+
"branches": []
|
|
35
39
|
},
|
|
36
40
|
"fixed_project_directory": "",
|
|
37
41
|
"allow": {
|
|
@@ -49,5 +53,39 @@
|
|
|
49
53
|
"repository": false
|
|
50
54
|
}
|
|
51
55
|
}
|
|
52
|
-
]
|
|
56
|
+
],
|
|
57
|
+
"plugin_order": [
|
|
58
|
+
"repository:github",
|
|
59
|
+
"skill_filter"
|
|
60
|
+
],
|
|
61
|
+
"sensitive_data_policy": {
|
|
62
|
+
"installed": false,
|
|
63
|
+
"enabled": false,
|
|
64
|
+
"action": "redact",
|
|
65
|
+
"event_types": {
|
|
66
|
+
"agent": true,
|
|
67
|
+
"checkpoint": true,
|
|
68
|
+
"commit": true,
|
|
69
|
+
"prompt_duration": true,
|
|
70
|
+
"prompt_report": true,
|
|
71
|
+
"skill": true,
|
|
72
|
+
"token": true
|
|
73
|
+
},
|
|
74
|
+
"built_in_rules": {
|
|
75
|
+
"api_key": true,
|
|
76
|
+
"credential": true,
|
|
77
|
+
"email": false,
|
|
78
|
+
"phone": false,
|
|
79
|
+
"private_key": true
|
|
80
|
+
},
|
|
81
|
+
"custom_patterns": []
|
|
82
|
+
},
|
|
83
|
+
"agent_model_policy": {
|
|
84
|
+
"installed": false,
|
|
85
|
+
"enabled": false,
|
|
86
|
+
"mode": "audit",
|
|
87
|
+
"allowed_agents": [],
|
|
88
|
+
"allowed_models": [],
|
|
89
|
+
"blocked_patterns": []
|
|
90
|
+
}
|
|
53
91
|
}
|