Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions .agents/plugins/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,20 @@
"source": {
"source": "url",
"url": "https://github.com/partme-ai/partme-codeguard-plugin.git",
"ref": "v0.14.1"
"ref": "v0.14.2"
},
"policy": {
"installation": "AVAILABLE",
"authentication": "ON_USE"
},
"category": "Developer Tools",
"version": "0.14.1",
"version": "0.14.2",
"description": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.",
"icon": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.14.1/assets/official-logo.png",
"icon": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.14.2/assets/official-logo.png",
"interface": {
"displayName": "代码规范守卫",
"shortDescription": "Trustworthy code checks and Java impact analysis",
"logo": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.14.1/assets/official-logo.png"
"logo": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.14.2/assets/official-logo.png"
}
}
]
Expand Down
2 changes: 1 addition & 1 deletion .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codeguard",
"version": "0.14.1+codex.20260923",
"version": "0.14.2+codex.20260923",
"description": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.",
"author": {
"name": "Full Stack Skills / PartMe.AI",
Expand Down
2 changes: 1 addition & 1 deletion .zcode-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"en": "CodeGuard",
"zh-CN": "代码规范检查"
},
"version": "0.14.1",
"version": "0.14.2",
"description": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.",
"description_i18n": {
"en": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.",
Expand Down
4 changes: 2 additions & 2 deletions docs/current-architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ flowchart TD
| `bin/codeguard`、`scripts/run_check.py`、`scripts/fix.py` | CLI 参数、输出与 MCP stdio 注册 | 不复制扫描器或判定逻辑 |
| `hooks/` | 宿主 JSON、会话事件、通知与退出协议 | 五类 Hook 将检查编排委托应用服务;入口保留旧导入兼容面及 fail-open 协议 |
| `scripts/codeguard/check_application.py`、`language_check.py` | 语言选择、检查与修复请求、MCP 工具分发 | 不导入 MCP SDK;旧 `run_per_language.py` 仅再导出 |
| `startup_application.py`、`session_application.py`、`prompt_application.py`、`git_guard_application.py`、`save_application.py` | 五类 Hook 的盘点、状态消费、软提示、Git 守卫及保存反馈编排 | 返回结构化结果,不打印或调用宿主通知;Hook 控制输出、通知与 fail-open。Stop 在 stdout 刷新后才确认消费;若同时有新统计,为避免丢记录,后续 Stop 可能重复展示旧统计 |
| `startup_application.py`、`session_application.py`、`prompt_application.py`、`git_guard_application.py`、`save_application.py` | 五类 Hook 的盘点、状态消费、软提示、Git 守卫及保存反馈编排 | 返回结构化结果,不打印或调用宿主通知;Hook 控制输出、通知与 fail-open。Stop、软提示与保存反馈在 stdout 刷新后确认状态;Git 已知拦截在输出故障时仍拦截但不缓存。并发或交付重试可能重复反馈,不得丢掉未交付记录 |
| `gate.py`、`gate_checks.py`、`repository_policy.py`、`baseline.py` | Git 门禁、单语言结果、安全规则及有证据的存量比较 | 只有准确基线失败且逐条诊断的文件归属、内容及次数覆盖当前结果才可豁免;基线临时路径映射回被检查文件 |
| `java_build.py`、`java_impact.py`、`java_planning.py` 等 | 构建读取、纯影响闭包、命令选择与环境观察 | 默认保留跳过测试的行为;复杂构建保守扩大检查范围 |
| `cve_reports.py`、`cve_policy.py`、`cve_scanners.py`、`cve.py` | 漏洞报告、阈值、进程适配与复扫编排 | 无有效结构化报告就没有安全通过结论 |
Expand Down Expand Up @@ -66,4 +66,4 @@ python3 scripts/vendor/skill_vendor.py check
openspec validate refactor-codeguard-architecture --strict
```

真实 Maven/Gradle 大型项目、联网漏洞数据库、57 种工具链、Windows 与三宿主已安装运行均需要独立证据。临时 Git 快照不是执行沙箱;构建和扫描命令以当前用户权限运行。当前重构的任务进度和本地证据见 `openspec/changes/refactor-codeguard-architecture/verification.md`。v0.13.0 的源码、CI、tag、Release 和市场已核对;其后工作树变更仍需独立发布,宿主安装运行尚未验收。
真实 Maven/Gradle 大型项目、联网漏洞数据库、57 种工具链、Windows 与三宿主已安装运行均需要独立证据。临时 Git 快照不是执行沙箱;构建和扫描命令以当前用户权限运行。当前重构的任务进度和本地证据见 `openspec/changes/refactor-codeguard-architecture/verification.md`。v0.14.1 的源码、CI、tag、Release 和市场已核对;其后工作树变更仍需独立发布,宿主安装运行尚未验收。
1 change: 1 addition & 0 deletions hooks/post_tool_lint.py
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ def _main(payload: dict) -> int:
if result.system_message is not None:
message["systemMessage"] = result.system_message
print(json.dumps(message, ensure_ascii=False))
sys.stdout.flush()
result.acknowledge()
if result.notification:
lang, title, message = result.notification
Expand Down
29 changes: 20 additions & 9 deletions hooks/pre_tool_git_guard.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,26 +79,37 @@ def extract_command(payload: dict) -> str:
def main() -> int:
payload = read_payload()
with session_scope(payload):
if not payload.get("tool_use_id"):
return _main(payload)
key = "pre:" + str(payload["tool_use_id"]) + ":" + extract_command(payload)
cached = completed_event(key)
key = ("pre:" + str(payload["tool_use_id"]) + ":" + extract_command(payload)
if payload.get("tool_use_id") else None)
cached = completed_event(key) if key else None
if cached is not None and cached["code"] == 2:
sys.stdout.write(cached["stdout"])
sys.stderr.write(cached["stderr"])
_deliver(cached["code"], cached["stdout"], cached["stderr"])
return cached["code"]
stdout, stderr = io.StringIO(), io.StringIO()
with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
code = _main(payload)
out, err = stdout.getvalue(), stderr.getvalue()
# 只复用阻断结果,硬门放行和未验证永远重跑准确快照。
sys.stdout.write(out)
sys.stderr.write(err)
if code == 2:
delivered = _deliver(code, out, err)
if key and code == 2 and delivered:
record_completed_event(key, code, out, err)
return code


def _deliver(code: int, stdout: str, stderr: str) -> bool:
"""刷新宿主输出后才允许缓存;已知拦截不能因输出故障降为放行。"""
try:
sys.stdout.write(stdout)
sys.stderr.write(stderr)
sys.stdout.flush()
sys.stderr.flush()
except (OSError, UnicodeError, ValueError):
if code == 2:
return False
raise
return True


def _main(payload: dict) -> int:
ensure_user_path()
result = evaluate_git_command(
Expand Down
1 change: 1 addition & 0 deletions hooks/user_prompt_validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,7 @@ def _main(payload: dict) -> int:
"hookEventName": "UserPromptSubmit",
"additionalContext": result.additional_context,
}}, ensure_ascii=False))
sys.stdout.flush()
result.acknowledge()
return 0

Expand Down
2 changes: 1 addition & 1 deletion kimi.plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codeguard",
"version": "0.14.1",
"version": "0.14.2",
"description": "Evidence-backed code checks and Git content gates for AI assistants, with Maven/Gradle module impact analysis. Save hooks provide feedback; unverified checks are explicit.",
"author": {
"name": "Full Stack Skills / PartMe.AI"
Expand Down
5 changes: 5 additions & 0 deletions openspec/changes/refactor-codeguard-architecture/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,11 @@ Stop 的宿主入口先准备原子可读的状态快照,成功写入并刷新
后续 Stop 可能重复展示旧统计,但不会丢掉未展示的新统计。旧直接消费接口继续兼容,
不用于宿主 Stop 交付路径。

同一交付规则也适用于 UserPromptSubmit 和 PostToolUse:入口完成 stdout 刷新后才确认
已完成事件、保存去重和会话统计。PreToolUse 的已知拦截先保留退出码;写入、编码或刷新
故障时不缓存未交付结果,不能通过通用 fail-open 异常处理把明确的拦截改成放行。
这只证明本进程写入管道成功,不能证明宿主最终消费;因此重试允许重复反馈。

事件去重不等于权限缓存。观察 Hook 成功返回后才登记完成,异常允许重试;UPS 有 skipped 时
保守地不登记。硬门禁只重放已完成拒绝,放行、未验证和执行中一律不复用;并发副本允许
重复检查而不能预占后直接放行。保存检查只有已获得结论才记去重,未知可立即重试。
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@

统计、绕过明细、冷却与审计的读改写 MUST 在进程锁内完成并原子替换文件。宿主提供 session_id 时,统计 MUST 绑定会话与当前 worktree;Stop MUST 只消费该作用域的记录,并与并发写入互斥。无 session_id 时保留旧共享状态兼容,MUST 明示不能证明跨会话隔离。去重 MUST NOT 把未完成或 UNVERIFIED 的检查当成已完成检查。
Stop MUST 在宿主输出成功刷新后才确认消费;输出失败时记录必须保留供重试。若确认前有并发新写入,允许后续 Stop 重复展示旧统计,但不得删除未展示的新记录。
UserPromptSubmit 与 PostToolUse 的已完成事件、文件去重和会话统计 MUST 在相应 stdout 成功刷新后才登记;写入或刷新失败时不得把未交付反馈标记为已交付。PreToolUse 已得到拦截结论时,即使宿主输出失败也 MUST 保留拦截退出码,并且不得缓存该次未交付结果。

#### Scenario: Concurrent writers preserve all increments
- **WHEN** 多个 Hook 进程同时更新同一统计、绕过记录或审计日志
Expand All @@ -75,6 +76,14 @@ Stop MUST 在宿主输出成功刷新后才确认消费;输出失败时记录
- **WHEN** Stop 已准备总结但宿主 stdout 写入或刷新失败,或在输出期间有新统计写入
- **THEN** 输出失败时旧统计可在重试中再次展示;并发新统计不得被确认消费误删,允许为了不丢记录而在后续 Stop 重复展示旧统计

#### Scenario: Buffered host feedback fails before delivery
- **WHEN** UserPromptSubmit 或 PostToolUse 已写入反馈,但 stdout 刷新失败
- **THEN** 不登记已完成事件、文件去重或会话统计;下次触发仍可重新检查并交付反馈

#### Scenario: Hard Git block survives host output failure
- **WHEN** PreToolUse 已判定需要拦截,但 stdout 或 stderr 写入或刷新失败
- **THEN** 保持拦截退出码且不缓存该次结果,不能因宿主输出故障改为放行

#### Scenario: Retry follows an unverified save check
- **WHEN** 相同文件未变化,上次保存检查未能获得结论,紧接着再次触发
- **THEN** 再次执行检查,不因此前写入的去重标记而静默跳过
Expand Down
2 changes: 1 addition & 1 deletion openspec/changes/refactor-codeguard-architecture/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,5 @@
- [x] 5.2 四个历史 skipped 已全部恢复;Dockerfile CLI 接入统一报告解析与扫描应用,14 项真实进程边界测试通过;基线比较按诊断内容和次数核验并要求基线实际 FAIL,三项误豁免用例先红后绿。完整 433 项单测零跳过、142 项回归零失败。
- [x] 5.3 新增当前架构 Mermaid 与扩展指南;报告版本读取插件 manifest,双语 README 指向当前证据,旧设计标明历史范围;双语标题/链接/版本及本地链接存在性测试通过。发布版本仍需在 5.5 检查。
- [ ] 5.4 全量单测/真实 hook 回归/ruff/语言 schema/vendor 离线与在线/本 change strict 和逐条规格追溯通过。
- [ ] 5.5 按仓规升级版本、生成市场元数据、授权的 PR/CI/不可变 tag/Release 闭环;v0.14.0 阶段版本已完成插件/市场双仓合并、PR/main CI、tag/Release,但后续工作树改动仍未发布。宿主安装运行独立记录,未授权不得改宿主。
- [ ] 5.5 按仓规升级版本、生成市场元数据、授权的 PR/CI/不可变 tag/Release 闭环;v0.14.1 阶段版本已完成插件/市场双仓合并、PR/main CI、tag/Release,但本 change 后续改动仍需独立发布。宿主安装运行独立记录,未授权不得改宿主。
- [ ] 5.6 完成全目标审计,代码/规格/证据一致后同步并归档 change,才可将整体优化标记完成。
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,11 @@
- Stop 原逻辑在输出前 `take_json`,故故障注入先 RED:stdout 写入失败时会话记录已被删除。现新增准备结果和输出后确认;写入或刷新失败时记录保留可重试。若准备后有并发新写入,锁内快照不一致就不消费,允许下一次重复展示旧统计而不删除新记录。旧 `consume_summary` 兼容接口保留,Hook 改用 `prepare_summary`。两种输出故障和并发写入均有目标测试。
- 本机现有 `/opt/anaconda3/bin/python3` 3.13.5 完整执行 **460 unittest,全部通过、0 skipped**;`tests/run_all.py` **142/0/0**。ruff、架构门禁、语言 schema 57 项/11 规则、vendor 离线与在线、本 change strict、diff whitespace 均通过;受管技能及 lock 未改。上述为本地证据,远端 CI/版本/市场需在本批发布后另验。
- 这两处消除了已复现的假豁免与输出前丢总结;仍不声称完整 Shell 解释、三宿主现场、Windows、在线漏洞库、全部工具链或 OpenSpec 5.4/5.6 的全目标审计完成。stdout 刷新成功不等于宿主最终消费确认,因此异常重试可出现重复总结;该取舍优先保证不丢统计。

## 第十六批实际验证:跨 Hook 输出交付边界

- 已发布的 v0.14.1 源码经插件 PR #47、市场 PR #4 合并到双仓 `main`;插件 `skills-check` 通过,注释 tag 与 GitHub Release 指向合并提交 `b8fa3850c877eae454845326fc167103d406b7d3`,三宿主市场清单均为 0.14.1。该证据只覆盖此前源码,不覆盖本批工作树。
- 临时 CodeGraph 快照同步最新源码后,追踪 `PromptResult`/`SaveResult`/PreToolUse 的交付调用链:Stop 已刷新 stdout 再确认,而其余两个软 Hook 及 Git 硬门在刷新前就登记完成或缓存。当前项目的真实测试文件覆盖范围比 CodeGraph 的受影响测试提示更广,故以源码链和实际测试为准。
- 新增 RED:UserPromptSubmit 与 PostToolUse 在 stdout 刷新失败时,旧入口仍登记完成;Git 已知拦截的输出写入失败向外抛异常,若由入口顶层 fail-open 捕获则可能放行;刷新失败还会缓存未交付拦截。现在所有确认都在刷新后;已知拦截输出故障保留 exit 2、不缓存,含无 tool_use_id 路径和缓存重放。对普通软反馈仍保持原 fail-open 协议。
- 定向测试 **26/26**,完整单测 **465/465、0 skipped**,真实 Hook 回归 **142/0/0**;ruff、架构依赖/循环、本 change strict、语言 schema **57 项/11 规则**、vendor 离线和在线以及 diff whitespace 通过。vendor lock 仍为 v0.1.2 → `2c0c8071f96de48dc53e11de2499c083c100e44c`,受管技能与 lock 未修改。
- 本批只证明本地输出时序与故障注入;stdout 刷新不等于宿主最终消费,可能在重试时重复反馈。三宿主已安装运行、Windows、联网漏洞库和全部语言工具链仍需独立验收;本批在此记录时尚未 bump/提交/发布。
53 changes: 50 additions & 3 deletions tests/test_git_guard_application.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,58 @@ def block(_payload):
patch.object(pre_tool_git_guard, "_main", side_effect=block), \
patch.object(pre_tool_git_guard, "completed_event", return_value=None), \
patch.object(pre_tool_git_guard, "record_completed_event") as record, \
patch.object(sys, "stdout", BrokenOutput()), \
self.assertRaises(OSError):
pre_tool_git_guard.main()
patch.object(sys, "stdout", BrokenOutput()):
self.assertEqual(2, pre_tool_git_guard.main())
record.assert_not_called()

def test_blocked_event_is_not_cached_if_host_flush_fails(self):
class BrokenFlush(io.StringIO):
def flush(self):
raise OSError("host flush unavailable")

payload = {"tool_use_id": "event-2", "tool_input": {"command": "git commit -m x"}}

def block(_payload):
print("blocked")
return 2

with patch.object(pre_tool_git_guard, "read_payload", return_value=payload), \
patch.object(pre_tool_git_guard, "_main", side_effect=block), \
patch.object(pre_tool_git_guard, "completed_event", return_value=None), \
patch.object(pre_tool_git_guard, "record_completed_event") as record, \
patch.object(sys, "stdout", BrokenFlush()):
self.assertEqual(2, pre_tool_git_guard.main())
record.assert_not_called()

def test_block_without_event_id_still_blocks_if_stderr_flush_fails(self):
class BrokenFlush(io.StringIO):
def flush(self):
raise OSError("host stderr flush unavailable")

def block(_payload):
print("blocked", file=sys.stderr)
return 2

payload = {"tool_input": {"command": "git push"}}
with patch.object(pre_tool_git_guard, "read_payload", return_value=payload), \
patch.object(pre_tool_git_guard, "_main", side_effect=block), \
patch.object(sys, "stderr", BrokenFlush()):
self.assertEqual(2, pre_tool_git_guard.main())

def test_cached_block_remains_blocked_if_replay_cannot_write(self):
class BrokenOutput:
def write(self, _text):
raise OSError("host output unavailable")

payload = {"tool_use_id": "event-3", "tool_input": {"command": "git push"}}
cached = {"code": 2, "stdout": "blocked\n", "stderr": ""}
with patch.object(pre_tool_git_guard, "read_payload", return_value=payload), \
patch.object(pre_tool_git_guard, "completed_event", return_value=cached), \
patch.object(pre_tool_git_guard, "_main") as run, \
patch.object(sys, "stdout", BrokenOutput()):
self.assertEqual(2, pre_tool_git_guard.main())
run.assert_not_called()


if __name__ == "__main__":
unittest.main()
Loading
Loading