git-ai-control 0.4.0 → 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 CHANGED
@@ -2,6 +2,34 @@
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
+
5
33
  ## [0.4.0] - 2026-07-30
6
34
 
7
35
  ### 新增
package/COMPATIBILITY.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## 操作系统
4
4
 
5
- `npx git-ai-control start` 支持 macOS、Linux 和 Windows,三个系统共用相同的配置格式、过滤运行时和 Web 页面,仅用户级服务管理方式不同:
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
- - 仓库插件与 Skill 过滤插件使用各自独立的配置表单;
18
+ - 仓库插件可按不同主机添加多个,并支持全局兜底、仓库、目录、分支与优先级;
19
+ - 敏感内容脱敏插件可识别 API Key、私钥、凭据,并按规则脱敏或拦截;
20
+ - Agent / 模型治理插件支持允许名单、阻止正则及审计/拦截模式;
21
+ - Skill、敏感内容和 Agent / 模型治理均使用独立配置表单;
19
22
  - 支持配置验证、运行状态检查和本机过滤服务重启。
20
23
 
21
24
  ## 界面截图
@@ -28,7 +31,9 @@
28
31
 
29
32
  ![仓库插件与 Skill 过滤插件管理页面](docs/images/plugin-management.jpg)
30
33
 
31
- 仓库插件可以同时配置匹配主机、允许上传的数据类型和允许保留的字段;Skill 过滤插件使用独立表单管理名称规则。每种插件类型只能配置一次,新增其他类型的插件会增加新的配置模块,不会替换已有模块。
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 start
52
+ npx git-ai-control
48
53
  ```
49
54
 
50
- 该命令会自动下载最新版本、安装或更新本机服务,并打开 Git AI 配置中心。安装过程会:
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
- 同一个仓库插件也可以填写多个主机,每行一个。GitLab、Gitee 和自建 Git 服务不需要单独开发插件。
92
+ 同一个仓库插件也可以填写多个主机,每行一个;也可以为不同主机分别添加仓库插件。GitLab、Gitee 和自建 Git 服务不需要单独开发插件。
93
+
94
+ 仓库策略会先匹配特定规则,再匹配“全部仓库”全局规则;特定规则之间优先级数字越大越靠前,同优先级按插件列表顺序处理。仓库、目录、分支同时填写时需要全部命中。
95
+
96
+ ### 3. 配置敏感内容与 Agent
86
97
 
87
- ### 3. 配置 Skill 过滤插件
98
+ 在“插件管理”中可分别添加以下独立插件:
99
+
100
+ 1. “敏感内容脱敏”:选择作用事件、内置规则和自定义正则;命中后可脱敏或阻止上报;
101
+ 2. “Agent / 模型治理”:配置允许的 Agent、模型与阻止正则;先使用“仅审计”确认命中情况,再按需改为阻止。
102
+
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
- ### 4. 验证配置
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 原生设置;
@@ -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 start 安装或更新本机服务,并打开配置页面
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" || !command) {
23
+ if (command === "--help" || command === "-h") {
23
24
  printHelp()
24
25
  process.exit(0)
25
26
  }
26
27
 
27
- if (!["start", "uninstall"].includes(command)) {
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 {install} = await import("../scripts/install.mjs")
40
- await install()
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 === "start" ? "安装" : "卸载"}失败:${error.message}`)
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.4.0",
3
+ "version": "0.4.3",
4
4
  "description": "Git AI 本地配置与上传隐私控制面板",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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": 1,
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
- "match": {"hosts": ["github.com"]},
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") != 1:
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
- if not needles:
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
- if any(needle in raw_text for needle in needles):
282
- return True
283
- if any(needle in text.lower() for text in iter_all_text(payload) for needle in needles):
284
- return True
285
- for path in iter_path_text(payload):
286
- remote_text = read_remote_text(path).lower()
287
- if any(needle in remote_text for needle in needles):
288
- return True
289
- return False
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
- for plugin in config.get("plugins", []):
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
- upstreams, upstream_error = load_upstreams()
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,
@@ -1,5 +1,6 @@
1
1
  {
2
- "version": 1,
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
- "hosts": [
33
- "github.com"
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
  }
package/scripts/check.mjs CHANGED
@@ -36,6 +36,21 @@ if (
36
36
  process.exit(1)
37
37
  }
38
38
 
39
+ if (
40
+ !run(python, [
41
+ ...prefix,
42
+ "-m",
43
+ "unittest",
44
+ "discover",
45
+ "-s",
46
+ "tests",
47
+ "-p",
48
+ "test_*.py",
49
+ ])
50
+ ) {
51
+ process.exit(1)
52
+ }
53
+
39
54
  for (const file of [
40
55
  "bin/git-ai-control.js",
41
56
  "scripts/check.mjs",