From fd9b8b3104cee4d9b0c9fae663d07acc409b3025 Mon Sep 17 00:00:00 2001 From: loong10k <20489781+loong10k@users.noreply.github.com> Date: Wed, 23 Sep 2026 19:03:50 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix:=20=E6=A0=A1=E9=AA=8C=20Git=20=E5=AF=B9?= =?UTF-8?q?=E8=B1=A1=E5=88=97=E8=A1=A8=E5=AE=8C=E6=95=B4=E6=80=A7=E4=B8=8E?= =?UTF-8?q?=E8=B7=AF=E5=BE=84=E4=B8=80=E8=87=B4=E6=80=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/current-architecture.md | 2 +- .../refactor-codeguard-architecture/design.md | 5 + .../specs/execution-kernel/spec.md | 8 ++ .../refactor-codeguard-architecture/tasks.md | 4 +- .../verification.md | 8 ++ scripts/git_snapshot.py | 72 ++++++++--- tests/test_git_snapshot_protocol.py | 113 ++++++++++++++++++ 7 files changed, 195 insertions(+), 17 deletions(-) diff --git a/docs/current-architecture.md b/docs/current-architecture.md index 308024f..a327728 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -27,7 +27,7 @@ flowchart TD ``` `execution` 只记录进程证据,不把非零退出自动认定为代码违规。`verdict`、CVE 与 Dockerfile 报告解析决定 PASS、FAIL 或 UNVERIFIED;入口仅按各自协议呈现和聚合退出码。Git 提交检查使用 index 或预测暂存快照,推送检查使用 HEAD;保存钩子只给反馈。计划、未执行和未验证不能宣传为通过。 -Git 准确快照的二进制读取独立于文本检查执行器:`cat-file` 两阶段响应逐项核对 ID、blob 类型、大小与边界,并按 SHA-1/SHA-256 blob 对象格式复算内容哈希;缺项、截断、尾部脏数据或同长度内容替换均不交付临时树,而是明确标记 Git UNVERIFIED。原始 index 不在观察过程中改写。该校验只证明读取到的对象与列出的对象一致,不是执行沙箱或完整 Git/Shell 模拟。 +Git 准确快照的二进制读取独立于文本检查执行器:先严格解析 index/HEAD 对象列表的 NUL 帧、模式、ID、类型/阶段和唯一文件名,并与独立路径列举核对;`cat-file` 两阶段响应再逐项核对 ID、blob 类型、大小与边界,按 SHA-1/SHA-256 blob 对象格式复算内容哈希。缺项、截断、尾部脏数据或同长度内容替换均不交付临时树,而是明确标记 Git UNVERIFIED。原始 index 不在观察过程中改写。该校验不是恶意 Git 进程沙箱或完整 Git/Shell 模拟。 `language_check` 将 `PlanExecution` 的实际命令依序映射为应用层有界 `execution_trace`(检查、修复、复检阶段);`check_application` 将其进一步压成 MCP 安全元数据,不回显原始 argv、环境覆盖或捕获输出。`auto_fix` 的 `fix_results` 也只公开状态与安全元数据;formatter stderr 尾部写入私有 `.codeguard-fix.log` 后只公开路径,日志不可用时不退回公开原文。失败日志保留各已执行检查的完整输出,可能含敏感文本,应排除出版本控制。项目日志和门禁截断日志共用 `storage.write_private_text` 的私有原子落盘;预置日志文件链接不被跟随,默认 `out` 目录为链接或不可写时不返回虚假日志路径,也不覆盖检查结论。终止命令仍决定既有状态、退出码和旧字段;未运行的计划命令不进入证据。 Git 命令的仓库定位与拟暂存解析共用入口传入的 cwd;显式 `cd`/`git -C` 无法绑定 Git 工作树时,硬门禁给出目标未验证并阻断,不退回到调用者仓或无关子仓。未给出显式目标且调用目录非仓时仍保留 workspace 子仓兜底。静态命令解析不等于完整 Shell 执行模拟。 workspace 兜底产生的每个子仓是合成目标,暂存观察也绑定该子仓根目录;不能继续以非 Git 的 workspace cwd 解析,从而把脏子仓的已知违规变成未知放行。 diff --git a/openspec/changes/refactor-codeguard-architecture/design.md b/openspec/changes/refactor-codeguard-architecture/design.md index b8e18cd..f1d264f 100644 --- a/openspec/changes/refactor-codeguard-architecture/design.md +++ b/openspec/changes/refactor-codeguard-architecture/design.md @@ -82,6 +82,11 @@ MCP 的 `auto_fix` 还必须在应用层单独投影公开结果:`run_fix` 保 payload 哈希。任一不一致转为 `SnapshotError`,准确门禁沿既有策略显示 UNVERIFIED;不把 错误对象或同长度但被替换的内容交给语言检查器。256 MiB 预算按已核对的对象大小累计, 读取与写入保留原始二进制字节且不改真实 index。这仍非恶意文件系统或 Git 进程沙箱。 +对象列举自身也必须视为协议输入:`ls-files --stage` / `ls-tree -r` 的 NUL 帧、模式、 +对象格式对应 ID、阶段/类型、非空且唯一的路径统一解析。再用独立的 `ls-files --cached` +或 `ls-tree --name-only` 路径面交叉核对,防止单个响应在完整记录边界被截断后仍交付 +部分树。通用 `-z` 路径列举同样拒绝未终止或空记录;并发 index/HEAD 变化宁可转 +UNVERIFIED,不把两个时点拼成一次确定快照。这不是对恶意 Git 进程的完整双源证明。 直接命令链的拟暂存分析分成静态事件观察与按提交时间投影:先把同仓 `add`/`commit` 及其调用目录记录为不可变事件,再只将最后一个 commit 及以前的 add 送往 Git pathspec 查询和内容快照。这样后续 add 不会回溯污染已完成提交,也不跳过两个 commit 之间的 add。旧的独立 `staging_intent` 调用默认仍返回整条链的暂存意图;硬门禁只在没有一层间接脚本或 Shell 命令替换时启用按提交投影。`$(...)`/反引号的内层实际先执行,而旧静态展开把它附在文本尾部;间接脚本与外层 add 的跨层顺序也尚无统一事件时钟。这两类命令继续采用保守并集,不能以局部排序缩小检查面。 diff --git a/openspec/changes/refactor-codeguard-architecture/specs/execution-kernel/spec.md b/openspec/changes/refactor-codeguard-architecture/specs/execution-kernel/spec.md index 26a61a4..51bba08 100644 --- a/openspec/changes/refactor-codeguard-architecture/specs/execution-kernel/spec.md +++ b/openspec/changes/refactor-codeguard-architecture/specs/execution-kernel/spec.md @@ -233,6 +233,10 @@ CLI 呈现 MUST 各有单一所有者。只有有效报告且执行状态可解 ### Requirement: Accurate Git snapshots SHALL validate object transport +index/HEAD 对象列表 MUST 是完整的 NUL 分隔记录:每项有合法模式、对象格式对应的 ID、 +阶段/类型与非空路径,重复路径或不完整末项不得静默跳过。准确快照 MUST 用独立的 +Git 路径列举核对对象列表的路径集合;单次列表在记录边界被截断时也不能交付部分树。 +这些协议故障 MUST 归一化为快照 UNVERIFIED,而不是入口异常或空树 PASS。 Git 准确快照 MUST 对 index/HEAD 列出的每个对象,逐项核对 `cat-file --batch-check` 与 `--batch` 响应的对象 ID、blob 类型、非负大小、顺序和完整字节边界。响应缺项、重复项、 内容截断或多余尾部字节 MUST 报告快照 UNVERIFIED,不得把错误对象或部分内容交给语言检查器。 @@ -245,6 +249,10 @@ Git 准确快照 MUST 对 index/HEAD 列出的每个对象,逐项核对 `cat-f - **WHEN** 批量读取的对象 ID、类型、大小、数量或顺序与已列出的 Git 对象不一致 - **THEN** 准确快照不可交付,门禁报告 Git UNVERIFIED,而不是检查错位或部分内容 +#### Scenario: Git object listing is malformed or incomplete +- **WHEN** `ls-files --stage` 或 `ls-tree -r` 缺少终止 NUL、含畸形/重复记录,或虽在完整记录边界结束却漏掉另一列举可见的路径 +- **THEN** 准确快照不可交付,门禁报告 Git UNVERIFIED;真实 index 不变 + #### Scenario: Batch payload has an invalid boundary or identity - **WHEN** 对象内容截断、缺少协议分隔符、尾随额外数据或同长度内容与对象哈希不符 - **THEN** 不交付临时树,不改变真实 index;有效二进制 blob 仍可逐字节重建 diff --git a/openspec/changes/refactor-codeguard-architecture/tasks.md b/openspec/changes/refactor-codeguard-architecture/tasks.md index 8aee7d3..75c4c4b 100644 --- a/openspec/changes/refactor-codeguard-architecture/tasks.md +++ b/openspec/changes/refactor-codeguard-architecture/tasks.md @@ -33,6 +33,6 @@ - [x] 5.1 增加自动架构依赖/循环检查并接入 CI,临时源码树注入反向依赖/函数内导入/相对导入环后确实拒绝;本地已验证,远端 CI 待发布阶段。不以文件行数当合格标准。 - [x] 5.2 四个历史 skipped 已全部恢复;Dockerfile CLI 接入统一报告解析与扫描应用,14 项真实进程边界测试通过;基线比较按诊断内容和次数核验并要求基线实际 FAIL,三项误豁免用例先红后绿。完整 433 项单测零跳过、142 项回归零失败。 - [x] 5.3 新增当前架构 Mermaid 与扩展指南;报告版本读取插件 manifest,双语 README 指向当前证据,旧设计标明历史范围;双语标题/链接/版本及本地链接存在性测试通过。发布版本仍需在 5.5 检查。 -- [x] 5.4 全量单测/真实 hook 回归/ruff/语言 schema/vendor 离线与在线/本 change strict 和逐条规格追溯通过;第二十五批本地 526 项单测、143 项真实 Hook 回归、57 项/11 规则,证据见 verification.md。仅证明本地契约,不等同三宿主现场验收。 -- [ ] 5.5 按仓规升级版本、生成市场元数据、授权的 PR/CI/不可变 tag/Release 闭环;第二十四批已发布 v0.14.10(插件 PR #56、市场 PR #14、源码 PR/main CI、注释 tag/正式 Release 均核对;市场 PR 无 CI 检查)。第二十五批 Git blob 传输校验仍在隔离 worktree,本地实现不等于新版本发布。宿主安装运行独立记录,未授权不得改宿主。 +- [x] 5.4 全量单测/真实 hook 回归/ruff/语言 schema/vendor 离线与在线/本 change strict 和逐条规格追溯通过;第二十六批本地 533 项单测、143 项真实 Hook 回归、57 项/11 规则,证据见 verification.md。仅证明本地契约,不等同三宿主现场验收。 +- [ ] 5.5 按仓规升级版本、生成市场元数据、授权的 PR/CI/不可变 tag/Release 闭环;第二十五批已发布 v0.14.11(插件 PR #57、市场 PR #15、源码 PR/main CI、注释 tag/正式 Release 均核对;市场 PR 无 CI 检查)。第二十六批对象列表完整性改动尚在隔离 worktree,未 bump/推送/发布。宿主安装运行独立记录,未授权不得改宿主。 - [ ] 5.6 完成全目标审计,代码/规格/证据一致后同步并归档 change,才可将整体优化标记完成。 diff --git a/openspec/changes/refactor-codeguard-architecture/verification.md b/openspec/changes/refactor-codeguard-architecture/verification.md index 99021d1..7854ce3 100644 --- a/openspec/changes/refactor-codeguard-architecture/verification.md +++ b/openspec/changes/refactor-codeguard-architecture/verification.md @@ -262,3 +262,11 @@ - 最新 main 的隔离临时 CodeGraph 索引沿 `validation_tree → cat-file --batch-check/--batch → gate.run_gate` 追到准确快照的信任边界:旧实现只汇总大小,再以 `--batch` 头部长度切片,未逐项核对 ID、类型、数量、大小、尾部边界或 payload 哈希。独立故障注入证明错误对象 ID 的内容仍被接受;新增测试中 9 类协议错位/截断及门禁 UNVERIFIED 场景先 RED,真实二进制 blob 对照保持通过。 - `git_snapshot` 的字节型 Git 入口统一执行错误归一化;批量大小响应和内容响应按 index/HEAD 的同一对象顺序验证。每段 blob 按 SHA-1 或 SHA-256 Git 对象格式重新计算哈希,因此同长度替换也不能通过伪造响应头被接受;有效包含换行/NUL 的内容仍原样重建。错误响应转 `SnapshotError`,准确门禁返回 `git UNVERIFIED`,真实 index 字节不变。进一步用应用层集成用例确认:同批已经暂存的 `.env` 仍被安全规则 exit 2 拦截,未知快照不会抹掉已知违规;既有未知项 fail-open/可见告警政策未改。 - 隔离 worktree 本地完整单测 **526/526、0 skipped**,真实 Hook 回归 **143/0/0**;Ruff、架构依赖/循环、语言 schema **57 项/11 规则**、vendor 离线与在线、本 change strict、diff whitespace 均通过。当前只证明本地静态/故障注入契约与真实 SHA-1/SHA-256 仓库,不等于三宿主已安装现场、Windows、在线 CVE、大型 Maven/Gradle 或恶意 Git 进程沙箱验收。第二十五批尚未 bump、提交、推送或发布。 + +- 第二十五批随后发布为 v0.14.11:源码 PR #57 合并到 `e3b4262f1b5b5ef8d1c8ad2952a3012f47e42fbd`,市场 PR #15 合并到 `799c0369121c794bbb99fbdcd29fedf6e048aec0`;源码 PR 与合并后 main 的 `vendor-check` 成功。远端注释 tag `v0.14.11` 解引用到源码合并提交,正式 Release 非 draft、非 prerelease,市场 Codex/ZCode/Kimi 清单全量与远端 tag/Release 校验通过。市场 PR 无 CI 检查,不称其 CI 已通过。宿主安装/运行仍未验收。 + +## 第二十六批本地验证:Git 对象列表完整性 + +- 最新 v0.14.11 main 的隔离 CodeGraph 索引追踪 `validation_tree → git` 及准确门禁调用者:blob 哈希已验证,但 `ls-files --stage` / `ls-tree -r` 的对象列举仍以宽松 `split(NUL)` 读取,缺少终止符、重复/空记录或在完整记录边界漏项时可交付不完整树;畸形头会抛裸 `ValueError`。故障注入先 RED:10 个部分树错误接受,1 个异常类型不符。 +- 当前实现把 `-z` 帧解析集中为 `_nul_records`,对象列表另验证模式、类型/阶段、对象格式长度和十六进制 ID、非空唯一安全路径,并与独立 Git 路径列举交叉核对。两种列表不一致或 index 并发变化均转 `SnapshotError`;准确门禁可见 `git UNVERIFIED`,不改真实 index。包含制表符/换行的合法文件名仍逐字节物化。CodeGraph `affected` 对本次文件返回无测试,但真实新增故障注入与既有集成测试覆盖较广,故不把图谱空结果当作无影响证明。 +- 隔离 worktree 本地完整单测 **533/533、0 skipped**,真实 Hook 回归 **143/0/0**;Ruff、架构依赖/循环、语言 schema **57 项/11 规则**、vendor 离线与在线、本 change strict、diff whitespace 均通过。还正向覆盖空 index、有效 SHA-1/SHA-256、push HEAD 与脏工作树分离。临时 CodeGraph 索引同步后为 139 文件、2,181 节点、4,931 边;源仓仍未初始化索引。本批尚未 bump、推送或发布;宿主现场、Windows、在线 CVE、大型仓库与恶意 Git 进程仍不在已验证范围。 diff --git a/scripts/git_snapshot.py b/scripts/git_snapshot.py index 77f0232..149ac2e 100644 --- a/scripts/git_snapshot.py +++ b/scripts/git_snapshot.py @@ -4,6 +4,7 @@ import contextlib import hashlib import os +import re import subprocess import tempfile from pathlib import Path, PurePosixPath @@ -24,8 +25,20 @@ def git(root: Path, *args: str, input_data: bytes | None = None) -> bytes: return proc.stdout +def _nul_records(raw: bytes) -> list[bytes]: + """Git -z 响应必须按完整记录结束,不能丢弃半条或空记录。""" + if not raw: + return [] + if not raw.endswith(b"\0"): + raise SnapshotError("Git 路径列表缺少终止 NUL") + records = raw[:-1].split(b"\0") + if any(not record for record in records): + raise SnapshotError("Git 路径列表包含空记录") + return records + + def names(root: Path, *args: str) -> set[str]: - return {os.fsdecode(p) for p in git(root, *args).split(b"\0") if p} + return {os.fsdecode(record) for record in _nul_records(git(root, *args))} def safe_path(root: Path, name: str) -> Path: @@ -136,23 +149,54 @@ def _batch_blob(raw: bytes, offset: int, oid: str, expected_size: int) -> tuple[ return content, end + 1 +def _snapshot_entries(root: Path, head: bool) -> list[tuple[str, str, str]]: + """严格解析对象列表,并以独立的路径列举发现完整记录边界上的漏项。""" + listing = ("ls-tree", "-r", "-z", "HEAD") if head else ("ls-files", "--stage", "-z") + records = _nul_records(git(root, *listing)) + if len(records) > 20000: + raise SnapshotError("快照超过 20000 文件上限,需要独立 CI 检查") + object_format = git(root, "rev-parse", "--show-object-format").strip() + oid_size = {b"sha1": 40, b"sha256": 64}.get(object_format) + if oid_size is None: + raise SnapshotError("Git 对象格式未验证") + entries = [] + seen = set() + for record in records: + metadata, separator, filename = record.partition(b"\t") + if not separator or not filename or filename in seen: + raise SnapshotError("Git 对象列表路径缺失或重复") + seen.add(filename) + fields = metadata.split(b" ") + if len(fields) != 3: + raise SnapshotError("Git 对象列表元数据无效") + mode_bits, second, third = fields + if mode_bits not in (b"100644", b"100755"): + raise SnapshotError("符号链接、子模块或未解决冲突需要独立检查") + if head: + if second != b"blob": + raise SnapshotError("Git 树对象不是 blob") + oid = third + else: + if third != b"0": + raise SnapshotError("Git index 存在未解决冲突") + oid = second + if not re.fullmatch(rb"[0-9a-f]{%d}" % oid_size, oid): + raise SnapshotError("Git 对象列表 ID 与仓库格式不一致") + name = os.fsdecode(filename) + safe_path(root, name) + entries.append((mode_bits.decode("ascii"), oid.decode("ascii"), name)) + expected = (names(root, "ls-tree", "-r", "--name-only", "-z", "HEAD") if head + else names(root, "ls-files", "--cached", "-z")) + if {name for _, _, name in entries} != expected: + raise SnapshotError("Git 对象列表与独立路径列举不一致") + return entries + + @contextlib.contextmanager def validation_tree(root: Path, mode="commit", *, lanes=None, extra=None, pending_commit=False): """原始 Git blobs 构成快照;拒绝外链、submodule、冲突和超限内容。""" head = mode == "push" and not pending_commit - raw = git(root, "ls-tree", "-r", "-z", "HEAD") if head else git(root, "ls-files", "--stage", "-z") - entries = [] - for line in raw.split(b"\0"): - if not line: - continue - meta, filename = line.split(b"\t", 1) - mode_bits, middle, last = meta.decode().split() - oid = last if head else middle - if mode_bits not in ("100644", "100755") or (not head and last != "0"): - raise SnapshotError("符号链接、子模块或未解决冲突需要独立检查") - entries.append((mode_bits, oid, os.fsdecode(filename))) - if len(entries) > 20000: - raise SnapshotError("快照超过 20000 文件上限,需要独立 CI 检查") + entries = _snapshot_entries(root, head) object_ids = "".join(oid + "\n" for _, oid, _ in entries).encode() sizes = _batch_sizes(git(root, "cat-file", "--batch-check", input_data=object_ids), entries) blob = git(root, "cat-file", "--batch", input_data=object_ids) diff --git a/tests/test_git_snapshot_protocol.py b/tests/test_git_snapshot_protocol.py index 3fbafe3..154f14b 100644 --- a/tests/test_git_snapshot_protocol.py +++ b/tests/test_git_snapshot_protocol.py @@ -67,6 +67,41 @@ def run(argv, *args, **kwargs): return run + @staticmethod + def corrupt_listing(variant: str): + real_run = subprocess.run + + def run(argv, *args, **kwargs): + result = real_run(argv, *args, **kwargs) + if argv not in (["git", "ls-files", "--stage", "-z"], + ["git", "ls-tree", "-r", "-z", "HEAD"]): + return result + raw = result.stdout + first = raw.split(b"\0", 1)[0] + if variant == "missing_terminator": + raw = raw[:-1] + elif variant == "duplicate": + raw += first + b"\0" + elif variant == "missing_complete_record": + raw = first + b"\0" + elif variant == "empty": + raw = b"" + elif variant == "empty_record": + raw = raw.replace(b"\0", b"\0\0", 1) + elif variant == "empty_name": + raw = first.split(b"\t", 1)[0] + b"\t\0" + elif variant == "invalid_oid": + metadata, name = first.split(b"\t", 1) + fields = metadata.split() + oid_index = 2 if argv[1] == "ls-tree" else 1 + fields[oid_index] = b"z" * len(fields[oid_index]) + raw = b" ".join(fields) + b"\t" + name + raw[len(first):] + elif variant == "invalid_header": + raw = b"bogus\t" + first.split(b"\t", 1)[1] + raw[len(first):] + return subprocess.CompletedProcess(result.args, result.returncode, raw, result.stderr) + + return run + def test_real_binary_blob_is_preserved_without_index_mutation(self): before = (self.root / ".git/index").read_bytes() with git_snapshot.validation_tree(self.root) as (snapshot, changed): @@ -74,6 +109,23 @@ def test_real_binary_blob_is_preserved_without_index_mutation(self): self.assertEqual(["sample.bin"], changed) self.assertEqual(before, (self.root / ".git/index").read_bytes()) + def test_empty_index_remains_a_valid_empty_snapshot(self): + with tempfile.TemporaryDirectory(prefix="cg-snapshot-empty-") as directory: + root = Path(directory).resolve() + subprocess.run(["git", "init", "-q"], cwd=root, check=True, capture_output=True) + with git_snapshot.validation_tree(root) as (snapshot, changed): + self.assertEqual([], list(snapshot.iterdir())) + self.assertEqual([], changed) + + def test_push_snapshot_reads_head_not_dirty_worktree(self): + subprocess.run(["git", "-c", "user.name=Test", "-c", "user.email=test@example.invalid", + "commit", "-qm", "fixture"], cwd=self.root, check=True, capture_output=True) + (self.root / "sample.bin").write_bytes(b"dirty worktree\n") + before = (self.root / ".git/index").read_bytes() + with git_snapshot.validation_tree(self.root, mode="push") as (snapshot, _changed): + self.assertEqual(self.content, (snapshot / "sample.bin").read_bytes()) + self.assertEqual(before, (self.root / ".git/index").read_bytes()) + def test_sha256_repository_uses_its_object_format(self): with tempfile.TemporaryDirectory(prefix="cg-snapshot-sha256-") as directory: root = Path(directory).resolve() @@ -102,6 +154,67 @@ def test_inconsistent_batch_protocol_never_yields_a_snapshot(self): pass self.assertEqual(before, (self.root / ".git/index").read_bytes()) + def test_incomplete_index_listing_never_yields_a_snapshot(self): + (self.root / "second.txt").write_text("another file\n", encoding="utf-8") + subprocess.run(["git", "add", "second.txt"], cwd=self.root, + check=True, capture_output=True) + before = (self.root / ".git/index").read_bytes() + variants = ("missing_terminator", "duplicate", "missing_complete_record", + "empty", "empty_record", "empty_name", "invalid_oid", "invalid_header") + for variant in variants: + with (self.subTest(variant=variant), + patch.object(git_snapshot.subprocess, "run", + side_effect=self.corrupt_listing(variant)), + self.assertRaises(git_snapshot.SnapshotError), + git_snapshot.validation_tree(self.root)): + pass + self.assertEqual(before, (self.root / ".git/index").read_bytes()) + + def test_incomplete_head_listing_never_yields_a_push_snapshot(self): + (self.root / "second.txt").write_text("another file\n", encoding="utf-8") + subprocess.run(["git", "add", "second.txt"], cwd=self.root, + check=True, capture_output=True) + subprocess.run(["git", "-c", "user.name=Test", "-c", "user.email=test@example.invalid", + "commit", "-qm", "fixture"], cwd=self.root, check=True, capture_output=True) + for variant in ("missing_terminator", "missing_complete_record", "empty", + "duplicate", "invalid_oid"): + with (self.subTest(variant=variant), + patch.object(git_snapshot.subprocess, "run", + side_effect=self.corrupt_listing(variant)), + self.assertRaises(git_snapshot.SnapshotError), + git_snapshot.validation_tree(self.root, mode="push")): + pass + + def test_incomplete_path_name_response_is_not_treated_as_empty(self): + real_run = subprocess.run + + def run(argv, *args, **kwargs): + result = real_run(argv, *args, **kwargs) + if argv[:4] == ["git", "diff", "--cached", "--name-only"]: + return subprocess.CompletedProcess(result.args, result.returncode, + result.stdout[:-1], result.stderr) + return result + + with (patch.object(git_snapshot.subprocess, "run", side_effect=run), + self.assertRaises(git_snapshot.SnapshotError)): + git_snapshot.proposed_paths(self.root) + + def test_malformed_listing_is_visible_to_exact_gate(self): + with patch.object(git_snapshot.subprocess, "run", + side_effect=self.corrupt_listing("missing_terminator")): + failures, skipped = run_gate(self.root, {}, ["python"], exact=True) + self.assertEqual([], failures) + self.assertTrue(any("git UNVERIFIED" in item for item in skipped), skipped) + + def test_special_filename_is_preserved_by_nul_listing(self): + name = "tab\tline\n.txt" + (self.root / name).write_text("content\n", encoding="utf-8") + subprocess.run(["git", "add", name], cwd=self.root, + check=True, capture_output=True) + with git_snapshot.validation_tree(self.root) as (snapshot, changed): + self.assertEqual("content\n", (snapshot / name).read_text(encoding="utf-8")) + self.assertIn(name, changed) + def test_exact_gate_reports_corrupt_blob_as_unverified(self): with patch.object(git_snapshot.subprocess, "run", side_effect=self.corrupt_run("wrong_oid")): From c795c4298f6cd0480f1c148e9d12f49e4cf43807 Mon Sep 17 00:00:00 2001 From: loong10k <20489781+loong10k@users.noreply.github.com> Date: Wed, 23 Sep 2026 19:15:40 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix:=20=E6=94=B6=E7=B4=A7=20MCP=20=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E4=BF=AE=E5=A4=8D=E6=96=87=E4=BB=B6=E8=BA=AB=E4=BB=BD?= =?UTF-8?q?=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/current-architecture.md | 2 +- .../refactor-codeguard-architecture/design.md | 1 + .../specs/execution-kernel/spec.md | 12 +++ .../refactor-codeguard-architecture/tasks.md | 4 +- .../verification.md | 3 + scripts/check_architecture.py | 2 +- scripts/codeguard/check_application.py | 47 +++++++++- tests/test_mcp_autofix_identity.py | 87 +++++++++++++++++++ 8 files changed, 150 insertions(+), 8 deletions(-) create mode 100644 tests/test_mcp_autofix_identity.py diff --git a/docs/current-architecture.md b/docs/current-architecture.md index a327728..8fdc26e 100644 --- a/docs/current-architecture.md +++ b/docs/current-architecture.md @@ -28,7 +28,7 @@ flowchart TD `execution` 只记录进程证据,不把非零退出自动认定为代码违规。`verdict`、CVE 与 Dockerfile 报告解析决定 PASS、FAIL 或 UNVERIFIED;入口仅按各自协议呈现和聚合退出码。Git 提交检查使用 index 或预测暂存快照,推送检查使用 HEAD;保存钩子只给反馈。计划、未执行和未验证不能宣传为通过。 Git 准确快照的二进制读取独立于文本检查执行器:先严格解析 index/HEAD 对象列表的 NUL 帧、模式、ID、类型/阶段和唯一文件名,并与独立路径列举核对;`cat-file` 两阶段响应再逐项核对 ID、blob 类型、大小与边界,按 SHA-1/SHA-256 blob 对象格式复算内容哈希。缺项、截断、尾部脏数据或同长度内容替换均不交付临时树,而是明确标记 Git UNVERIFIED。原始 index 不在观察过程中改写。该校验不是恶意 Git 进程沙箱或完整 Git/Shell 模拟。 -`language_check` 将 `PlanExecution` 的实际命令依序映射为应用层有界 `execution_trace`(检查、修复、复检阶段);`check_application` 将其进一步压成 MCP 安全元数据,不回显原始 argv、环境覆盖或捕获输出。`auto_fix` 的 `fix_results` 也只公开状态与安全元数据;formatter stderr 尾部写入私有 `.codeguard-fix.log` 后只公开路径,日志不可用时不退回公开原文。失败日志保留各已执行检查的完整输出,可能含敏感文本,应排除出版本控制。项目日志和门禁截断日志共用 `storage.write_private_text` 的私有原子落盘;预置日志文件链接不被跟随,默认 `out` 目录为链接或不可写时不返回虚假日志路径,也不覆盖检查结论。终止命令仍决定既有状态、退出码和旧字段;未运行的计划命令不进入证据。 +`language_check` 将 `PlanExecution` 的实际命令依序映射为应用层有界 `execution_trace`(检查、修复、复检阶段);`check_application` 将其进一步压成 MCP 安全元数据,不回显原始 argv、环境覆盖或捕获输出。MCP `auto_fix` 先以 `fingerprint.file_content` 对仓内改动文件采集有总量预算的流式身份,仓外路径、符号链接或读取故障不启动 formatter;复检后无法再次采集身份时保留实际执行证据但不宣称已确认 `fixed`。`fix_results` 只公开状态与安全元数据;formatter stderr 尾部写入私有 `.codeguard-fix.log` 后只公开路径,日志不可用时不退回公开原文。失败日志保留各已执行检查的完整输出,可能含敏感文本,应排除出版本控制。项目日志和门禁截断日志共用 `storage.write_private_text` 的私有原子落盘;预置日志文件链接不被跟随,默认 `out` 目录为链接或不可写时不返回虚假日志路径,也不覆盖检查结论。终止命令仍决定既有状态、退出码和旧字段;未运行的计划命令不进入证据。 Git 命令的仓库定位与拟暂存解析共用入口传入的 cwd;显式 `cd`/`git -C` 无法绑定 Git 工作树时,硬门禁给出目标未验证并阻断,不退回到调用者仓或无关子仓。未给出显式目标且调用目录非仓时仍保留 workspace 子仓兜底。静态命令解析不等于完整 Shell 执行模拟。 workspace 兜底产生的每个子仓是合成目标,暂存观察也绑定该子仓根目录;不能继续以非 Git 的 workspace cwd 解析,从而把脏子仓的已知违规变成未知放行。 一条命令链中的 Git 副作用先绑定为仓库与操作的有序对,再按仓库聚合检查面:纯 push 不读取该仓未提交的 index,同仓 commit→push 则保留已有 HEAD 与拟提交内容的并集。仓库级、内联及链式 `skipGate` 均按目标仓与操作顺序生效;已存在的仓库配置也可被同链先行 `git config --unset` 取消。链式设置跨 `||`、`;` 或换行时无法证明生效,门禁要求拆分命令;写入其它文件的 config 设置也不构成本仓豁免。语法、仓库归属、间接脚本与拟暂存分析共用引号/转义感知切分;参数文本中的控制符不能合成豁免。解析能力仍限于受支持的静态 Shell 形态。 diff --git a/openspec/changes/refactor-codeguard-architecture/design.md b/openspec/changes/refactor-codeguard-architecture/design.md index f1d264f..40c100d 100644 --- a/openspec/changes/refactor-codeguard-architecture/design.md +++ b/openspec/changes/refactor-codeguard-architecture/design.md @@ -69,6 +69,7 @@ flowchart TD 本轮审计发现 CLI/MCP 与 Git 门禁均直接写原始诊断日志:前者位于项目 `out`,后者使用可预测的临时文件名。保留各自的日志路径和呈现职责,但由 `storage` 的统一私有原子文本写入负责落盘;应用不得再用普通 `write_text` 跟随预置链接或留下默认可读权限。日志故障只影响日志证据的可用性,不得覆盖检查结论;这不是完整的对抗性文件系统沙箱。 MCP 的 `auto_fix` 还必须在应用层单独投影公开结果:`run_fix` 保留执行所需的原始命令与 stderr,但 JSON 只允许修复状态、退出码及收敛后的执行元数据。formatter 的 stderr 尾部写入项目 `out/.codeguard-fix.log`,成功落盘才公开私有日志路径;日志不可写时不回退输出原文。此边界不修改 CLI 的本地诊断契约,也不把私有日志路径当成宿主已读取的证明。 +修复范围的前后观察复用 `fingerprint.file_content` 的流式散列和 64 MiB/10,000 文件预算;应用层先确认路径在根内、拒绝直接符号链接、设备和循环路径,再允许 `run_fix`。读取故障或身份超限不会靠整份 `read_bytes` 兜底;修复后观察失败仍返回已执行的修复与复检结果,但顶层 `fixed` 不可据此宣称成功。架构依赖门禁显式允许应用服务依赖 fingerprint 基础设施,不增加反向依赖。 ### 5. 命令语法、仓库观察与路径政策分离 diff --git a/openspec/changes/refactor-codeguard-architecture/specs/execution-kernel/spec.md b/openspec/changes/refactor-codeguard-architecture/specs/execution-kernel/spec.md index 51bba08..9117169 100644 --- a/openspec/changes/refactor-codeguard-architecture/specs/execution-kernel/spec.md +++ b/openspec/changes/refactor-codeguard-architecture/specs/execution-kernel/spec.md @@ -92,6 +92,18 @@ CLI/MCP 失败日志与 Git 门禁截断诊断日志可能包含检查器的原 - **WHEN** 计划没有任何可执行命令 - **THEN** 规划调用方返回 SKIPPED、PLANNED 或 UNVERIFIED;执行器拒绝将空计划解释为成功 +### Requirement: MCP auto-fix SHALL observe changed files within a bounded safe scope + +MCP `auto_fix` 在运行 formatter 之前 MUST 验证所有拟修复路径仍位于项目根内、不是符号链接或特殊文件,并以有总量预算的流式内容身份记录修复前状态,不得整份载入任意大小文件。路径逃逸、读取故障、并发替换或超预算 MUST 明确 UNVERIFIED 且不启动修复。修复后身份无法可靠采集时 MUST 保留实际修复与复检证据,但不得声称已确认 `fixed`。已确认的内容或权限变化仍按既有 `fixed` 字段呈现。 + +#### Scenario: Changed path escapes through a parent symlink +- **WHEN** Git 改动路径经父目录符号链接指向仓外普通文件 +- **THEN** `auto_fix` 不读取仓外内容、不运行 formatter,返回明确 UNVERIFIED + +#### Scenario: Changed file exceeds the observation budget or changes during reading +- **WHEN** 改动文件超出身份预算、读取失败或被并发替换 +- **THEN** 修复前故障不启动 formatter;修复后故障保留已执行结果但不宣称 `fixed` + ### Requirement: Hook state SHALL preserve concurrent and session ownership 统计、绕过明细、冷却与审计的读改写 MUST 在进程锁内完成并原子替换文件。宿主提供 session_id 时,统计 MUST 绑定会话与当前 worktree;Stop MUST 只消费该作用域的记录,并与并发写入互斥。无 session_id 时保留旧共享状态兼容,MUST 明示不能证明跨会话隔离。去重 MUST NOT 把未完成或 UNVERIFIED 的检查当成已完成检查。 diff --git a/openspec/changes/refactor-codeguard-architecture/tasks.md b/openspec/changes/refactor-codeguard-architecture/tasks.md index 75c4c4b..92cebf7 100644 --- a/openspec/changes/refactor-codeguard-architecture/tasks.md +++ b/openspec/changes/refactor-codeguard-architecture/tasks.md @@ -33,6 +33,6 @@ - [x] 5.1 增加自动架构依赖/循环检查并接入 CI,临时源码树注入反向依赖/函数内导入/相对导入环后确实拒绝;本地已验证,远端 CI 待发布阶段。不以文件行数当合格标准。 - [x] 5.2 四个历史 skipped 已全部恢复;Dockerfile CLI 接入统一报告解析与扫描应用,14 项真实进程边界测试通过;基线比较按诊断内容和次数核验并要求基线实际 FAIL,三项误豁免用例先红后绿。完整 433 项单测零跳过、142 项回归零失败。 - [x] 5.3 新增当前架构 Mermaid 与扩展指南;报告版本读取插件 manifest,双语 README 指向当前证据,旧设计标明历史范围;双语标题/链接/版本及本地链接存在性测试通过。发布版本仍需在 5.5 检查。 -- [x] 5.4 全量单测/真实 hook 回归/ruff/语言 schema/vendor 离线与在线/本 change strict 和逐条规格追溯通过;第二十六批本地 533 项单测、143 项真实 Hook 回归、57 项/11 规则,证据见 verification.md。仅证明本地契约,不等同三宿主现场验收。 -- [ ] 5.5 按仓规升级版本、生成市场元数据、授权的 PR/CI/不可变 tag/Release 闭环;第二十五批已发布 v0.14.11(插件 PR #57、市场 PR #15、源码 PR/main CI、注释 tag/正式 Release 均核对;市场 PR 无 CI 检查)。第二十六批对象列表完整性改动尚在隔离 worktree,未 bump/推送/发布。宿主安装运行独立记录,未授权不得改宿主。 +- [x] 5.4 全量单测/真实 hook 回归/ruff/语言 schema/vendor 离线与在线/本 change strict 和逐条规格追溯通过;第二十六批本地 537 项单测、143 项真实 Hook 回归、57 项/11 规则,证据见 verification.md。仅证明本地契约,不等同三宿主现场验收。 +- [ ] 5.5 按仓规升级版本、生成市场元数据、授权的 PR/CI/不可变 tag/Release 闭环;第二十五批已发布 v0.14.11(插件 PR #57、市场 PR #15、源码 PR/main CI、注释 tag/正式 Release 均核对;市场 PR 无 CI 检查)。第二十六批对象列表与 MCP 修复边界改动尚在隔离 worktree,未 bump/推送/发布。宿主安装运行独立记录,未授权不得改宿主。 - [ ] 5.6 完成全目标审计,代码/规格/证据一致后同步并归档 change,才可将整体优化标记完成。 diff --git a/openspec/changes/refactor-codeguard-architecture/verification.md b/openspec/changes/refactor-codeguard-architecture/verification.md index 7854ce3..eba63e9 100644 --- a/openspec/changes/refactor-codeguard-architecture/verification.md +++ b/openspec/changes/refactor-codeguard-architecture/verification.md @@ -270,3 +270,6 @@ - 最新 v0.14.11 main 的隔离 CodeGraph 索引追踪 `validation_tree → git` 及准确门禁调用者:blob 哈希已验证,但 `ls-files --stage` / `ls-tree -r` 的对象列举仍以宽松 `split(NUL)` 读取,缺少终止符、重复/空记录或在完整记录边界漏项时可交付不完整树;畸形头会抛裸 `ValueError`。故障注入先 RED:10 个部分树错误接受,1 个异常类型不符。 - 当前实现把 `-z` 帧解析集中为 `_nul_records`,对象列表另验证模式、类型/阶段、对象格式长度和十六进制 ID、非空唯一安全路径,并与独立 Git 路径列举交叉核对。两种列表不一致或 index 并发变化均转 `SnapshotError`;准确门禁可见 `git UNVERIFIED`,不改真实 index。包含制表符/换行的合法文件名仍逐字节物化。CodeGraph `affected` 对本次文件返回无测试,但真实新增故障注入与既有集成测试覆盖较广,故不把图谱空结果当作无影响证明。 - 隔离 worktree 本地完整单测 **533/533、0 skipped**,真实 Hook 回归 **143/0/0**;Ruff、架构依赖/循环、语言 schema **57 项/11 规则**、vendor 离线与在线、本 change strict、diff whitespace 均通过。还正向覆盖空 index、有效 SHA-1/SHA-256、push HEAD 与脏工作树分离。临时 CodeGraph 索引同步后为 139 文件、2,181 节点、4,931 边;源仓仍未初始化索引。本批尚未 bump、推送或发布;宿主现场、Windows、在线 CVE、大型仓库与恶意 Git 进程仍不在已验证范围。 + +- 同批跨入口 CodeGraph 审查沿 `mcp_tool_payload → auto_fix → run_fix/run_check` 发现,旧 `auto_fix` 在作用域校验前对 Git 改动路径 `read_bytes`:父目录符号链接可先读仓外文件,大文件无内存预算,读取故障直接越过 MCP 结果边界。新增三种故障注入先 RED(仓外读取、超预算缺 UNVERIFIED、修复后身份故障缺 UNVERIFIED),另覆盖循环链接。应用服务现复用 `fingerprint.file_content` 的流式身份与 64 MiB/10,000 文件预算,修复前不可靠则不运行 formatter;修复后不可靠仍保留修复/复检证据,但顶层 `fixed=false` 且明确 UNVERIFIED。架构白名单只增加应用到 fingerprint 基础设施的有向依赖,反向导入与环仍受检。 +- 补充后全量单测 **537/537、0 skipped**,真实 Hook 回归 **143/0/0**;Ruff、架构检查、语言 schema **57 项/11 规则**、vendor 离线与在线、本 change strict、diff whitespace 均通过。MCP `auto_fix` 的私有日志/公开投影及真实 formatter 回归仍通过;这不等于三宿主实际安装或恶意并发文件系统的完整沙箱证明。 diff --git a/scripts/check_architecture.py b/scripts/check_architecture.py index a810d4b..12406f5 100644 --- a/scripts/check_architecture.py +++ b/scripts/check_architecture.py @@ -58,7 +58,7 @@ "codeguard.registry", "codeguard.storage", "codeguard.verdict", "codeguard.java_analysis"}, "codeguard.check_application": {"__future__", "pathlib", "scope", "codeguard.config", - "codeguard.discovery", "codeguard.java_analysis", + "codeguard.discovery", "codeguard.fingerprint", "codeguard.java_analysis", "codeguard.language_check", "codeguard.registry", "codeguard.storage"}, "codeguard.session_application": {"__future__", "collections.abc", "dataclasses", "pathlib", "codeguard.execution", diff --git a/scripts/codeguard/check_application.py b/scripts/codeguard/check_application.py index 2d6b6f4..4c1d088 100644 --- a/scripts/codeguard/check_application.py +++ b/scripts/codeguard/check_application.py @@ -7,6 +7,7 @@ from .config import ConfigurationError, get_overrides, load_user_config from .discovery import detect_languages, find_project_root +from .fingerprint import MAX_BYTES, MAX_FILES, file_content from .java_analysis import analyze from .language_check import run_check, run_fix from .registry import REGISTRY @@ -93,19 +94,57 @@ def _public_fix_results(root: Path, results: list[dict]) -> list[dict]: return public +def _repair_identity(root: Path, files: list[str]) -> dict[Path, str]: + """对拟修复文件采集有预算的内容身份,不读取仓外路径或整份大文件。""" + root = root.resolve() + if len(files) > MAX_FILES: + raise ValueError("改动文件数超过身份预算") + identities = {} + remaining = MAX_BYTES + for name in files: + if not isinstance(name, str): + raise TypeError("改动路径不是字符串") + path = root / name + try: + resolved = path.resolve() + except (OSError, RuntimeError) as exc: + raise ValueError("改动路径无法解析") from exc + if not resolved.is_relative_to(root) or path.is_symlink(): + raise ValueError("改动路径越界或包含符号链接") + try: + path.lstat() + except FileNotFoundError: + # Git 已删除的文件不需要修复;读取期间消失的文件仍由 file_content 拒绝。 + continue + identity, used = file_content(path, remaining) + identities[path] = identity + remaining -= used + return identities + + def auto_fix(root: Path, languages: list[str]) -> dict: """仅修复当前 Git 改动;无法确定范围时不扩大写入面。""" files = changed_files(root) if files is None: return {"fixed": False, "status": "UNVERIFIED", "reason": "无法确定 Git 改动范围;请显式使用 CLI fix --all", "check": []} - targets = [root / file for file in files if (root / file).is_file() and not (root / file).is_symlink()] - before = {path: path.read_bytes() for path in targets} + try: + before = _repair_identity(root, files) + except (OSError, TypeError, ValueError): + return {"fixed": False, "status": "UNVERIFIED", + "reason": "改动文件路径或内容身份不可验证(链接、读取故障或超预算);未执行自动修复", + "check": []} fix_results = run_fix(languages, root, files=files) results = run_check(languages, root, files=files, log_dir=root / "out") - changed = any(not path.is_file() or path.read_bytes() != body for path, body in before.items()) public_fixes = _public_fix_results(root, fix_results) - return {"fixed": changed, "fix_results": public_fixes, "check": result_envelope(results)} + checks = result_envelope(results) + try: + after = _repair_identity(root, files) + except (OSError, TypeError, ValueError): + return {"fixed": False, "status": "UNVERIFIED", + "reason": "修复后的文件身份不可验证,不能确认内容变化", + "fix_results": public_fixes, "check": checks} + return {"fixed": before != after, "fix_results": public_fixes, "check": checks} def mcp_tool_payload(name: str, arguments: dict | None, project_root: Path): diff --git a/tests/test_mcp_autofix_identity.py b/tests/test_mcp_autofix_identity.py new file mode 100644 index 0000000..41e4086 --- /dev/null +++ b/tests/test_mcp_autofix_identity.py @@ -0,0 +1,87 @@ +"""MCP 自动修复只能在有界、仓内、可复核的文件面执行。""" +from __future__ import annotations + +import sys +import tempfile +import unittest +from pathlib import Path +from unittest.mock import patch + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT / "scripts")) + +from codeguard import check_application + + +class McpAutoFixIdentityTests(unittest.TestCase): + def setUp(self): + temporary = tempfile.TemporaryDirectory(prefix="cg-autofix-identity-") + self.addCleanup(temporary.cleanup) + base = Path(temporary.name).resolve() + self.root = base / "repo" + self.root.mkdir() + self.external = base / "external" + self.external.mkdir() + + def test_parent_symlink_escape_is_rejected_before_read_or_fix(self): + (self.external / "secret.py").write_text("SECRET_FIXTURE\n", encoding="utf-8") + (self.root / "linked").symlink_to(self.external, target_is_directory=True) + with (patch.object(check_application, "changed_files", return_value=["linked/secret.py"]), + patch.object(check_application, "run_fix") as fix, + patch.object(check_application, "run_check") as check, + patch.object(Path, "read_bytes", side_effect=AssertionError("read outside repository"))): + payload = check_application.mcp_tool_payload( + "auto_fix", {"path": str(self.root), "languages": ["python"]}, self.root) + self.assertEqual("UNVERIFIED", payload["status"]) + self.assertFalse(payload["fixed"]) + self.assertNotIn("SECRET_FIXTURE", str(payload)) + fix.assert_not_called() + check.assert_not_called() + + def test_oversize_input_does_not_start_formatter(self): + (self.root / "large.py").write_bytes(b"0123456789abcdef") + with (patch.object(check_application, "changed_files", return_value=["large.py"]), + patch.object(check_application, "MAX_BYTES", 8, create=True), + patch.object(check_application, "run_fix") as fix, + patch.object(check_application, "run_check") as check): + payload = check_application.auto_fix(self.root, ["python"]) + self.assertEqual("UNVERIFIED", payload["status"]) + self.assertFalse(payload["fixed"]) + fix.assert_not_called() + check.assert_not_called() + + def test_symlink_loop_is_unverified_without_fix(self): + (self.root / "loop").symlink_to("loop", target_is_directory=True) + with (patch.object(check_application, "changed_files", return_value=["loop/file.py"]), + patch.object(check_application, "run_fix") as fix): + payload = check_application.auto_fix(self.root, ["python"]) + self.assertEqual("UNVERIFIED", payload["status"]) + fix.assert_not_called() + + def test_post_fix_identity_failure_preserves_execution_but_not_fixed_claim(self): + (self.root / "changed.py").write_text("value = 1\n", encoding="utf-8") + observed = iter([("before", 10), ValueError("file changed during read")]) + + def observe(_path, _budget): + value = next(observed) + if isinstance(value, Exception): + raise value + return value + + checks = [{"language": "python", "passed": True, "status": "PASS", "exit_code": 0}] + repairs = [{"language": "python", "fixed": True, "exit_code": 0}] + with (patch.object(check_application, "changed_files", return_value=["changed.py"]), + patch.object(check_application, "file_content", side_effect=observe, create=True), + patch.object(check_application, "run_fix", return_value=repairs) as fix, + patch.object(check_application, "run_check", return_value=checks) as check): + payload = check_application.auto_fix(self.root, ["python"]) + self.assertEqual("UNVERIFIED", payload["status"]) + self.assertFalse(payload["fixed"]) + self.assertEqual(True, payload["fix_results"][0]["fixed"]) + self.assertEqual("PASS", payload["check"][0]["status"]) + fix.assert_called_once() + check.assert_called_once() + + +if __name__ == "__main__": + unittest.main() From 8b2a281c5a2e418da39b7c6ab01da49d0f452526 Mon Sep 17 00:00:00 2001 From: loong10k <20489781+loong10k@users.noreply.github.com> Date: Wed, 23 Sep 2026 19:16:38 +0800 Subject: [PATCH 3/3] release: codeguard v0.14.12 --- .agents/plugins/marketplace.json | 8 ++++---- .codex-plugin/plugin.json | 2 +- .zcode-plugin/plugin.json | 2 +- kimi.plugin.json | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index ef57251..707c6b0 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -9,20 +9,20 @@ "source": { "source": "url", "url": "https://github.com/partme-ai/partme-codeguard-plugin.git", - "ref": "v0.14.11" + "ref": "v0.14.12" }, "policy": { "installation": "AVAILABLE", "authentication": "ON_USE" }, "category": "Developer Tools", - "version": "0.14.11", + "version": "0.14.12", "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.11/assets/official-logo.png", + "icon": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.14.12/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.11/assets/official-logo.png" + "logo": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.14.12/assets/official-logo.png" } } ] diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index b76c5fa..f1c2c6d 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "codeguard", - "version": "0.14.11+codex.20260923", + "version": "0.14.12+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", diff --git a/.zcode-plugin/plugin.json b/.zcode-plugin/plugin.json index 9e7970a..4b42fd6 100644 --- a/.zcode-plugin/plugin.json +++ b/.zcode-plugin/plugin.json @@ -5,7 +5,7 @@ "en": "CodeGuard", "zh-CN": "代码规范检查" }, - "version": "0.14.11", + "version": "0.14.12", "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.", diff --git a/kimi.plugin.json b/kimi.plugin.json index 4e9b13b..f498b07 100644 --- a/kimi.plugin.json +++ b/kimi.plugin.json @@ -1,6 +1,6 @@ { "name": "codeguard", - "version": "0.14.11", + "version": "0.14.12", "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"