From 7bfb07aeba44cb1d4e140b2375742dadbe665d47 Mon Sep 17 00:00:00 2001 From: loong10k <20489781+loong10k@users.noreply.github.com> Date: Sat, 3 Oct 2026 15:54:09 +0800 Subject: [PATCH 1/4] feat: expose pinned 32-grammar Rust runtime candidate --- .agents/plugins/marketplace.json | 8 +-- .codex-plugin/plugin.json | 2 +- .zcode-plugin/plugin.json | 2 +- README.md | 8 ++- README.zh-CN.md | 8 ++- kimi.plugin.json | 2 +- .../.openspec.yaml | 2 + .../design.md | 27 +++++++++ .../proposal.md | 20 +++++++ .../specs/hook-protocol/spec.md | 18 ++++++ .../tasks.md | 15 +++++ plugin.json | 2 +- runtime/codeguard.lock.json | 46 ++++++++++++++-- runtime/codeguard_runtime.cjs | 55 +++++++++++++++---- tests/rust-wasm-runtime-candidate.md | 24 ++++++++ tests/test_rust_runtime.cjs | 39 ++++++++++++- 16 files changed, 245 insertions(+), 33 deletions(-) create mode 100644 openspec/changes/2026-10-03-rust-wasm-runtime-candidate/.openspec.yaml create mode 100644 openspec/changes/2026-10-03-rust-wasm-runtime-candidate/design.md create mode 100644 openspec/changes/2026-10-03-rust-wasm-runtime-candidate/proposal.md create mode 100644 openspec/changes/2026-10-03-rust-wasm-runtime-candidate/specs/hook-protocol/spec.md create mode 100644 openspec/changes/2026-10-03-rust-wasm-runtime-candidate/tasks.md create mode 100644 tests/rust-wasm-runtime-candidate.md diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index 58e0cdd..b5100a4 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -9,20 +9,20 @@ "source": { "source": "url", "url": "https://github.com/full-stack-plugins/codeguard-plugin.git", - "ref": "v0.18.2" + "ref": "v0.19.0" }, "policy": { "installation": "AVAILABLE", "authentication": "ON_USE" }, "category": "Developer Tools", - "version": "0.18.2", + "version": "0.19.0", "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.18.2/assets/official-logo.png", + "icon": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.19.0/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.18.2/assets/official-logo.png" + "logo": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.19.0/assets/official-logo.png" } } ] diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 9ec0568..889016b 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "codeguard", - "version": "0.18.2+codex.20260929", + "version": "0.19.0+codex.20261003", "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 6d26778..16af659 100644 --- a/.zcode-plugin/plugin.json +++ b/.zcode-plugin/plugin.json @@ -5,7 +5,7 @@ "en": "代码规范守卫", "zh-CN": "代码规范检查" }, - "version": "0.18.2", + "version": "0.19.0", "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/README.md b/README.md index 464e188..e1d5369 100644 --- a/README.md +++ b/README.md @@ -12,16 +12,18 @@ CodeGuard provides native check evidence and guards supported Git commit/push ca ### Rust runtime candidate (macOS arm64) -The plugin now contains a **candidate**, explicit Rust runtime binding. It pins `@partme.ai/codeguard@0.1.2`, its registry tarball and native binary hashes, candidate source commit, platform, and check protocol major in [`runtime/codeguard.lock.json`](runtime/codeguard.lock.json). Install only on Apple Silicon macOS: +The plugin now contains a **candidate**, explicit Rust runtime binding. It pins `@partme.ai/codeguard@0.1.3`, its registry tarball and native binary hashes, 32 grammar-license hashes, candidate source commit, platform, and check protocol major in [`runtime/codeguard.lock.json`](runtime/codeguard.lock.json). Install only on Apple Silicon macOS: ```bash node runtime/codeguard_runtime.cjs install --download node runtime/codeguard_runtime.cjs verify +node runtime/codeguard_runtime.cjs exec grammar status --format=json +node runtime/codeguard_runtime.cjs exec check all /absolute/project --format=json ``` -For offline installation, use `install --tarball /absolute/path/to/partme.ai-codeguard-0.1.2.tgz`; the same lock hashes are enforced. Installation stages and verifies the package before switching the active receipt. [`hooks/rust_runtime_dispatch.cjs`](hooks/rust_runtime_dispatch.cjs) is a candidate Claude Code adapter for SessionStart, UserPromptSubmit, PostToolUse, PostToolUseFailure, and Stop. It rechecks the active binary before each call, never selects a random `codeguard` on `PATH`, and reports an incomplete runtime instead of falling back to Python. UserPromptSubmit returns fixed, non-blocking timing guidance; it does not run lint or treat prompt words as a Git gate. The current [`hooks/hooks.json`](hooks/hooks.json) still uses the legacy Python hooks; the Rust candidate has not been made the default, connected to a strict Git gate, or accepted on Codex/ZCode/Kimi. See the [candidate acceptance record](tests/rust-runtime-candidate.md) and [hook protocol](hooks/__protocol__.md). +For offline installation, use `install --tarball /absolute/path/to/partme.ai-codeguard-0.1.3.tgz`; the same lock hashes are enforced. Installation stages and verifies the package before switching the active receipt. The package contains 32 callable WASM grammar candidates; `grammar status` reports them as unqualified, and `grammar probe --format=json` provides explicit, non-gating observations. A candidate pass cannot replace native lint or a complete project check. [`hooks/rust_runtime_dispatch.cjs`](hooks/rust_runtime_dispatch.cjs) is a candidate Claude Code adapter for SessionStart, UserPromptSubmit, PostToolUse, PostToolUseFailure, and Stop. It rechecks the active binary before each call, never selects a random `codeguard` on `PATH`, and reports an incomplete runtime instead of falling back to Python. UserPromptSubmit returns fixed, non-blocking timing guidance; it does not run lint or treat prompt words as a Git gate. The current [`hooks/hooks.json`](hooks/hooks.json) still uses the legacy Python hooks; the Rust candidate has not been made the default, connected to automatic WASM feedback or a strict Git gate, or accepted on Codex/ZCode/Kimi. See the [candidate acceptance record](tests/rust-runtime-candidate.md), [32-grammar runtime acceptance](tests/rust-wasm-runtime-candidate.md), and [hook protocol](hooks/__protocol__.md). -The installed plugin version is recorded in its manifests. The architecture refactor keeps the existing 68 managed skills and focuses on reliable evidence, clear module ownership and Java project awareness. +Explicit `exec check all` runs applicable native checks first, then bounded WASM candidates for uncovered files; its report stays incomplete. The installed plugin version is recorded in its manifests. The architecture refactor keeps the existing 68 managed skills and focuses on reliable evidence, clear module ownership and Java project awareness. ### Runtime boundaries diff --git a/README.zh-CN.md b/README.zh-CN.md index e38f5c4..2d2da27 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -12,16 +12,18 @@ CodeGuard 为 AI 编程助手提供原生检查证据,守护已支持的 Git ### Rust 运行时候选(macOS arm64) -插件新增**显式候选** Rust 运行时绑定。[`runtime/codeguard.lock.json`](runtime/codeguard.lock.json) 固定 `@partme.ai/codeguard@0.1.2`、注册表 tarball 和原生二进制摘要、候选源码提交、平台及检查协议主版本。仅在 Apple Silicon macOS 上安装: +插件新增**显式候选** Rust 运行时绑定。[`runtime/codeguard.lock.json`](runtime/codeguard.lock.json) 固定 `@partme.ai/codeguard@0.1.3`、注册表 tarball 和原生二进制摘要、32 份 grammar 许可证摘要、候选源码提交、平台及检查协议主版本。仅在 Apple Silicon macOS 上安装: ```bash node runtime/codeguard_runtime.cjs install --download node runtime/codeguard_runtime.cjs verify +node runtime/codeguard_runtime.cjs exec grammar status --format=json +node runtime/codeguard_runtime.cjs exec check all /absolute/project --format=json ``` -离线安装可执行 `install --tarball /absolute/path/to/partme.ai-codeguard-0.1.2.tgz`,仍须满足同一锁定摘要。安装器先在暂存目录核验包,再切换活动收据。[`hooks/rust_runtime_dispatch.cjs`](hooks/rust_runtime_dispatch.cjs) 是 Claude Code 的 SessionStart、UserPromptSubmit、PostToolUse、PostToolUseFailure、Stop 候选适配器;每次调用前复核活动二进制,不从 PATH 随机选择 `codeguard`,缺运行时时报告未完成而不回退 Python。UserPromptSubmit 仅返回固定、非阻断的检查时机建议,不运行 lint,也不把提示词当作 Git 门禁。当前 [`hooks/hooks.json`](hooks/hooks.json) 仍使用旧 Python 钩子;Rust 候选尚未成为默认入口,未接严格 Git 门禁,也未在 Codex/ZCode/Kimi 完成宿主验收。见[候选验收记录](tests/rust-runtime-candidate.md)和[钩子协议](hooks/__protocol__.md)。 +离线安装可执行 `install --tarball /absolute/path/to/partme.ai-codeguard-0.1.3.tgz`,仍须满足同一锁定摘要。安装器先在暂存目录核验包,再切换活动收据。包内有 32 份可调用的 WASM grammar 候选;`grammar status` 明确报告尚未验收,`grammar probe --format=json` 提供显式、非门禁的观察。候选通过不能代替原生 lint 或完整项目检查。[`hooks/rust_runtime_dispatch.cjs`](hooks/rust_runtime_dispatch.cjs) 是 Claude Code 的 SessionStart、UserPromptSubmit、PostToolUse、PostToolUseFailure、Stop 候选适配器;每次调用前复核活动二进制,不从 PATH 随机选择 `codeguard`,缺运行时时报告未完成而不回退 Python。UserPromptSubmit 仅返回固定、非阻断的检查时机建议,不运行 lint,也不把提示词当作 Git 门禁。当前 [`hooks/hooks.json`](hooks/hooks.json) 仍使用旧 Python 钩子;Rust 候选尚未成为默认入口,未接 WASM 自动反馈或严格 Git 门禁,也未在 Codex/ZCode/Kimi 完成宿主验收。见[候选验收记录](tests/rust-runtime-candidate.md)、[32 语法运行时验收](tests/rust-wasm-runtime-candidate.md)和[钩子协议](hooks/__protocol__.md)。 -已安装插件的版本以各清单为准。本次架构重构保留现有 68 个受管技能,重点收敛判定证据、模块职责与 Java 项目感知。 +显式 `exec check all` 先运行适用的原生检查,再对未覆盖文件执行有界 WASM 候选检查;报告仍是不完整状态。已安装插件的版本以各清单为准。本次架构重构保留现有 68 个受管技能,重点收敛判定证据、模块职责与 Java 项目感知。 ### 运行边界 diff --git a/kimi.plugin.json b/kimi.plugin.json index 6b5f369..2ada4b9 100644 --- a/kimi.plugin.json +++ b/kimi.plugin.json @@ -1,6 +1,6 @@ { "name": "codeguard", - "version": "0.18.2", + "version": "0.19.0", "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/openspec/changes/2026-10-03-rust-wasm-runtime-candidate/.openspec.yaml b/openspec/changes/2026-10-03-rust-wasm-runtime-candidate/.openspec.yaml new file mode 100644 index 0000000..6a87d2b --- /dev/null +++ b/openspec/changes/2026-10-03-rust-wasm-runtime-candidate/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-03 diff --git a/openspec/changes/2026-10-03-rust-wasm-runtime-candidate/design.md b/openspec/changes/2026-10-03-rust-wasm-runtime-candidate/design.md new file mode 100644 index 0000000..c526e38 --- /dev/null +++ b/openspec/changes/2026-10-03-rust-wasm-runtime-candidate/design.md @@ -0,0 +1,27 @@ +## Context + +The published `@partme.ai/codeguard@0.1.3` archive and binary match the local release bytes from CodeGuard source commit `7900a1a8ccd5b4f5b54de8b5db3ccba16a356d3b`. The npm archive has 38 regular members: the previous six plus 32 license files. The existing installer pins archive and binary digests, but its old member and size limits reject the new package. + +## Decisions + +1. Keep a single exact package lock. Add a sorted map of 32 license paths and SHA-256 digests from the pinned grammar manifest. Validate names as relative `LICENSE.codegraph` or `/LICENSE` only; derive the exact archive allowlist from those validated names plus the six fixed package members. +2. Set explicit 16 MiB compressed and 128 MiB binary ceilings, above the measured 12.1 MB and 100.1 MB artifact. Validate the full archive digest before listing/extracting; require regular archive members; verify binary and license digests after extraction and whenever the active candidate is used. +3. Keep installation explicit and content-addressed. Do not start parsing during install, switch default Hooks, or treat `candidate_count=32` as qualified lint. After installation, a separate test invokes `grammar status` and a real Zig probe through the installed binary; both must retain incomplete semantics. +4. Release the plugin through its version generator and update only CodeGuard metadata in the separate marketplace repository after tests. If a host installation cannot be verified, report that gap without claiming installed-host acceptance. + +```mermaid +flowchart LR + A[Pinned 0.1.3 archive] --> B[SHA-256 + SHA-512] + B --> C[Exact 38 regular members] + C --> D[Bounded extract] + D --> E[Binary + 32 license digests] + E --> F[Content-addressed candidate] + F --> G[Explicit grammar status / probe] + G --> H[32 candidates; incomplete] +``` + +## Risks + +- Larger binary increases per-Hook re-verification cost. Measure the local candidate and keep the default Hook unchanged until host latency is accepted. +- Expanded size limits could admit a larger malicious archive if identity checks are weakened. Preserve exact archive digest and member checks before extraction; test mutation failures. +- npm availability is platform-specific. Retain `macos_arm64` pin and explicit incomplete feedback elsewhere. diff --git a/openspec/changes/2026-10-03-rust-wasm-runtime-candidate/proposal.md b/openspec/changes/2026-10-03-rust-wasm-runtime-candidate/proposal.md new file mode 100644 index 0000000..2f15334 --- /dev/null +++ b/openspec/changes/2026-10-03-rust-wasm-runtime-candidate/proposal.md @@ -0,0 +1,20 @@ +## Why + +The explicit Rust candidate runtime still pins npm 0.1.2, which has no bundled WASM. The published 0.1.3 package contains all 32 pinned grammar candidates, but the plugin rejects its 38 archive members, 12.1 MB tarball and 100.1 MB binary. Merely changing the lock would leave the candidate unusable. + +## What Changes + +- Pin the registry-verified 0.1.3 macOS arm64 package, source commit, integrity and binary digest. +- Accept exactly the pinned package members and verify every bundled upstream license digest before activation and on candidate use. Raise bounded package and binary limits to accommodate this measured artifact without accepting arbitrary archive members. +- Exercise offline and registry installation, candidate `grammar status` and a real WASM probe from the installed binary, plus archive/license mutation failures. +- Update bilingual candidate documentation, then bump and publish the plugin and marketplace versions under this repository's release rules. + +## Capabilities + +### Modified Capabilities + +- `hook-protocol`: the opt-in Rust candidate can install the published 32-grammar package while preserving exact artifact verification and incomplete semantics. + +## Impact + +`runtime/codeguard_runtime.cjs`, its lock and tests, bilingual README, candidate acceptance evidence, OpenSpec, plugin manifests and marketplace metadata. The default Python Hook, strict delivery gate and automatic WASM conversation feedback are separate work. diff --git a/openspec/changes/2026-10-03-rust-wasm-runtime-candidate/specs/hook-protocol/spec.md b/openspec/changes/2026-10-03-rust-wasm-runtime-candidate/specs/hook-protocol/spec.md new file mode 100644 index 0000000..a9a7a09 --- /dev/null +++ b/openspec/changes/2026-10-03-rust-wasm-runtime-candidate/specs/hook-protocol/spec.md @@ -0,0 +1,18 @@ +## ADDED Requirements + +### Requirement: Opt-in Rust runtime SHALL verify the complete pinned WASM package + +The explicit candidate runtime MUST accept only the published platform-specific archive whose version, source commit, SHA-256, SHA-512 integrity and native binary digest match the lock. It MUST validate a bounded archive of exactly the declared regular members, including every pinned grammar license, before activation. It MUST verify the binary and license contents after extraction and when selecting an active runtime. Package or file changes MUST produce incomplete feedback without selecting a binary from `PATH` or silently falling back to Python. Expanded byte ceilings MUST remain finite and justified by the measured artifact. + +#### Scenario: The complete 32-grammar package is installed +- **WHEN** the exact 0.1.3 macOS arm64 tarball is installed offline or downloaded from the pinned registry URL +- **THEN** the content-addressed candidate activates, `grammar status` reports 32 candidates and zero qualified grammars, and a real WASM probe remains incomplete rather than approving lint or delivery +- **AND** explicit `exec check all ABS_PROJECT` preserves native-first ordering and returns bounded candidate observations without converting them into a pass + +#### Scenario: A package or active file is altered +- **WHEN** an archive member, binary or license differs from the locked identity, or an extra/non-regular member is present +- **THEN** installation or candidate use reports incomplete and does not activate or execute the changed package + +#### Scenario: The host has no candidate package or uses another platform +- **WHEN** the pinned runtime is absent or the host is not macOS arm64 +- **THEN** the opt-in Hook returns bounded incomplete feedback without running another CodeGuard implementation diff --git a/openspec/changes/2026-10-03-rust-wasm-runtime-candidate/tasks.md b/openspec/changes/2026-10-03-rust-wasm-runtime-candidate/tasks.md new file mode 100644 index 0000000..eb040b2 --- /dev/null +++ b/openspec/changes/2026-10-03-rust-wasm-runtime-candidate/tasks.md @@ -0,0 +1,15 @@ +## 1. Runtime identity and safety + +- [x] 1.1 Record the published 0.1.3 archive, source, binary, manifest license identities and platform in the candidate lock; confirm registry and local hashes match. +- [x] 1.2 Write failing tests for the 0.1.3 archive rejection and license mutation, then extend exact members, measured size limits and active-file verification without weakening digest checks. + +## 2. Candidate execution + +- [x] 2.1 From the installed candidate run `grammar status` and a real Zig WASM probe; verify 32 candidates, zero qualified grammars, exit 3 and non-blocking feedback. +- [ ] 2.2 Verify offline and registry installation, wrong archive/binary/license rejection, platform fallback and the existing Claude candidate event behavior. + +## 3. Documentation and release + +- [x] 3.1 Update bilingual README and acceptance evidence; distinguish callable candidate assets from default Hook and automatic conversation integration. +- [ ] 3.2 Run OpenSpec strict validation, Node/Python regressions, vendor checks, manifest/parity checks and remote CI. +- [ ] 3.3 Bump/publish the plugin version through the generator, synchronize marketplace metadata, and verify GitHub release/tag plus clean local/remote heads. diff --git a/plugin.json b/plugin.json index 751db65..38e96f8 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "codeguard", - "version": "0.18.2", + "version": "0.19.0", "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/runtime/codeguard.lock.json b/runtime/codeguard.lock.json index 7ef3eb4..f0ff074 100644 --- a/runtime/codeguard.lock.json +++ b/runtime/codeguard.lock.json @@ -1,12 +1,46 @@ { "schema_version": "1.0.0", "package": "@partme.ai/codeguard", - "version": "0.1.2", + "version": "0.1.3", "platform": "macos_arm64", - "source_commit": "d2ae54355ea0e8b67b7fa42fc66829656db9de01", + "source_commit": "7900a1a8ccd5b4f5b54de8b5db3ccba16a356d3b", "check_protocol_major": 1, - "tarball_url": "https://registry.npmjs.org/@partme.ai/codeguard/-/codeguard-0.1.2.tgz", - "tarball_sha256": "4411136f2cdf678615335a6a9fe4f3435fe87f82e789b98a1e07be4518587401", - "tarball_integrity": "sha512-3ae3OIZDzqZaoNv2/W3a/Uq4rMMthjpjts/RQvKI5envR+NbyKOD4NTS/w645QFSw8ZDzHUWsnD7DObWFQAEpw==", - "binary_sha256": "785d8b9abb685ce5a3f4c1de0697d67a669394160bd7fb9a3554019d1d223bca" + "tarball_url": "https://registry.npmjs.org/@partme.ai/codeguard/-/codeguard-0.1.3.tgz", + "tarball_sha256": "37509cdd108fee631eebb9ebcd794a6e8d63652801b3522625d0909c3da2ae12", + "tarball_integrity": "sha512-RdhW/Tvf6a+7ZhRQFzSCJL87olzKmCjzQfyiqPZ3djTOodbdP7YfchT9VOFq/d7i+g95AME1mQC4HilHdb6WXQ==", + "binary_sha256": "04bdea8c489693a8e97b451df8d0c2b7a89ba82372db5a4d1fed277ba19b0f4d", + "grammar_licenses": { + "arkts/LICENSE": "048e4dcb7a71fb725495ebb3e7052d0e76dcb6705b828af8b9b461df10bcd8ca", + "c/LICENSE": "2e0110e07abef7c2548b26ec9d6969775617ca539a0dc8dbeeb14d6452c711d1", + "cfml/LICENSE": "7e17ba5e26b0438ca04ebf2d9e7931da3080509da6625e117f6feb943ecf767a", + "cfquery/LICENSE": "7e17ba5e26b0438ca04ebf2d9e7931da3080509da6625e117f6feb943ecf767a", + "cfscript/LICENSE": "7e17ba5e26b0438ca04ebf2d9e7931da3080509da6625e117f6feb943ecf767a", + "cobol/LICENSE": "d724405ce238a22c0d35769c5a36b386ad5958192efe8bbb304fb2896254575f", + "cpp/LICENSE": "2e0110e07abef7c2548b26ec9d6969775617ca539a0dc8dbeeb14d6452c711d1", + "csharp/LICENSE": "778fb7d63b8c1844da315648c02f325c4713a6f4a5d19644fd413421422776d3", + "dart/LICENSE": "d270cb3a4985d75033bd77d875ccebff1d66e32788a3f727891e28d76132dd46", + "erlang/LICENSE": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4", + "go/LICENSE": "2e0110e07abef7c2548b26ec9d6969775617ca539a0dc8dbeeb14d6452c711d1", + "java/LICENSE": "52ed137b039cd9c46409bc22e89938af911c95b157feae2d040b51e6084369a7", + "javascript/LICENSE": "2e0110e07abef7c2548b26ec9d6969775617ca539a0dc8dbeeb14d6452c711d1", + "kotlin/LICENSE": "948495f61768f7de26bcc61113d8cd95f50bbc15adb678c28c941c6c8fcd5903", + "LICENSE.codegraph": "e6d98f98c666bebe065ac2492a0a19232cc318d4d67bac3ca42ffb77bacc8809", + "lua/LICENSE": "9a32b02e4c917b1ce6b5e79d8ea81e25cefd7f27d89c7235f2afb262c06cf32e", + "luau/LICENSE": "099c44248f8cf353123211318680e93465587c005a4b3730ce8cb5334de043d6", + "nix/LICENSE": "c1b72e50266464ec5118d26200e17bf1f472d142e62ab637e1ffc23986657a78", + "objc/LICENSE": "099c44248f8cf353123211318680e93465587c005a4b3730ce8cb5334de043d6", + "pascal/LICENSE": "2e2fcc500eacb15b76c11fd2229aebf0a5b4c8ed476f068447c6dce778a17ac9", + "php/LICENSE": "664764fabf40c0b318e06324a5a12379092a2df25b8cbf2415f71bc0bb352346", + "python/LICENSE": "d724405ce238a22c0d35769c5a36b386ad5958192efe8bbb304fb2896254575f", + "r/LICENSE": "de2e49529f03d573bc3fa229dc83acfe22c63a5ad1766b289563edfd45b72dea", + "ruby/LICENSE": "ee006f02a3d856df282e409be2a86e24a65bb573a98b9c28343771141351bb6b", + "rust/LICENSE": "31d5b6f4243d5c7c6e1c4ebbbb9f6407bd1457a08bcc4f706521710341acba36", + "scala/LICENSE": "1f95ed26e1f4074074c9c7083e61c0a9e4c3b9f435745044995f3beb4ed28575", + "solidity/LICENSE": "8844f0cc9b76b9c8a9d0251904eb7536b6bb9976e0ec577e8f27ab96d42523ef", + "swift/LICENSE": "3533cec129bb4bba015c0d61d86dd7c3b7e82110e4d2ff7837a01eff5bad5ccc", + "terraform/LICENSE": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4", + "typescript/LICENSE": "49bf33cf78ef5897e4e161ce1517df7de1ae5042a65b6bcfd44401e0fc606559", + "vbnet/LICENSE": "fa7ab39bcf92c216d429e8aa8be8a5a40db85bfbe4ff14a7bb0ecbbcca937454", + "zig/LICENSE": "0eea8dc45e89deeb03c7799bbbc7b4688f365fb274562f4540ecfebdea82e727" + } } diff --git a/runtime/codeguard_runtime.cjs b/runtime/codeguard_runtime.cjs index ce80adb..40fb834 100644 --- a/runtime/codeguard_runtime.cjs +++ b/runtime/codeguard_runtime.cjs @@ -11,14 +11,20 @@ const { spawnSync } = require('node:child_process'); const LOCK_PATH = path.join(__dirname, 'codeguard.lock.json'); const SYSTEM_TAR = '/usr/bin/tar'; -const EXPECTED_MEMBERS = [ +const FIXED_MEMBERS = [ 'package/LICENSE', 'package/NOTICE', 'package/README.md', 'package/codeguard.cjs', 'package/native/codeguard', 'package/package.json', -].sort(); +]; +const MAX_TARBALL_BYTES = 16 * 1024 * 1024; +const MAX_BINARY_BYTES = 128 * 1024 * 1024; + +function expectedMembers(lock) { + return [...FIXED_MEMBERS, ...Object.keys(lock.grammar_licenses).map(name => `package/grammar-licenses/${name}`)].sort(); +} function fail(reason) { throw new Error(`codeguard runtime incomplete: ${reason}`); @@ -32,14 +38,20 @@ function readLock() { const lock = JSON.parse(fs.readFileSync(LOCK_PATH, 'utf8')); const hex = (value, length) => typeof value === 'string' && new RegExp(`^[0-9a-f]{${length}}$`).test(value); if (lock.schema_version !== '1.0.0' || lock.package !== '@partme.ai/codeguard' - || lock.version !== '0.1.2' || lock.platform !== 'macos_arm64' + || lock.version !== '0.1.3' || lock.platform !== 'macos_arm64' || lock.check_protocol_major !== 1 || !hex(lock.source_commit, 40) || !hex(lock.tarball_sha256, 64) || !hex(lock.binary_sha256, 64) - || lock.tarball_url !== 'https://registry.npmjs.org/@partme.ai/codeguard/-/codeguard-0.1.2.tgz' + || lock.tarball_url !== 'https://registry.npmjs.org/@partme.ai/codeguard/-/codeguard-0.1.3.tgz' || typeof lock.tarball_integrity !== 'string' - || !/^sha512-[A-Za-z0-9+/]{86}==$/.test(lock.tarball_integrity)) { + || !/^sha512-[A-Za-z0-9+/]{86}==$/.test(lock.tarball_integrity) + || !lock.grammar_licenses || typeof lock.grammar_licenses !== 'object' + || Array.isArray(lock.grammar_licenses)) { fail('runtime_lock_invalid'); } + const licenseNames = Object.keys(lock.grammar_licenses); + if (licenseNames.length !== 32 || !licenseNames.includes('LICENSE.codegraph') + || licenseNames.some(name => !/^(?:LICENSE\.codegraph|[a-z0-9_-]+\/LICENSE)$/.test(name) + || !hex(lock.grammar_licenses[name], 64))) fail('runtime_lock_invalid'); return lock; } @@ -83,8 +95,13 @@ function verifyPackage(root, lock = readLock()) { || JSON.stringify(manifest.os) !== '["darwin"]' || JSON.stringify(manifest.cpu) !== '["arm64"]') fail('package_identity_mismatch'); const binary = path.join(root, 'native', 'codeguard'); - if (regularFile(binary).size > 32 * 1024 * 1024) fail('binary_too_large'); + if (regularFile(binary).size > MAX_BINARY_BYTES) fail('binary_too_large'); if (sha256(fs.readFileSync(binary)) !== lock.binary_sha256) fail('binary_digest_mismatch'); + for (const [name, digest] of Object.entries(lock.grammar_licenses)) { + const license = path.join(root, 'grammar-licenses', name); + if (regularFile(license).size > 16 * 1024) fail('grammar_license_too_large'); + if (sha256(fs.readFileSync(license)) !== digest) fail('grammar_license_digest_mismatch'); + } const result = spawnSync(binary, ['--version', '--format=json'], { encoding: 'utf8', shell: false, timeout: 3000, maxBuffer: 8192, env: { ...process.env, CODEGUARD_SKIP_GATE: '' }, @@ -100,7 +117,7 @@ function verifyPackage(root, lock = readLock()) { function verifyTarball(file, lock) { const info = regularFile(file); - if (info.size > 8 * 1024 * 1024) fail('tarball_too_large'); + if (info.size > MAX_TARBALL_BYTES) fail('tarball_too_large'); const bytes = fs.readFileSync(file); if (sha256(bytes) !== lock.tarball_sha256 || `sha512-${crypto.createHash('sha512').update(bytes).digest('base64')}` !== lock.tarball_integrity) { @@ -110,6 +127,7 @@ function verifyTarball(file, lock) { function installTarball(file, root = cacheRoot()) { const lock = readLock(); + const members = expectedMembers(lock); checkPlatform(lock); ensureCache(root); verifyTarball(file, lock); @@ -124,12 +142,12 @@ function installTarball(file, root = cacheRoot()) { fs.chmodSync(staging, 0o700); const list = spawnSync(SYSTEM_TAR, ['-tzf', file], { encoding: 'utf8', shell: false, timeout: 10000, maxBuffer: 8192 }); if (list.error || list.status !== 0 || list.signal - || JSON.stringify(list.stdout.trim().split('\n').sort()) !== JSON.stringify(EXPECTED_MEMBERS)) { + || JSON.stringify(list.stdout.trim().split('\n').sort()) !== JSON.stringify(members)) { fail('tarball_members_invalid'); } const kinds = spawnSync(SYSTEM_TAR, ['-tvzf', file], { encoding: 'utf8', shell: false, timeout: 10000, maxBuffer: 8192 }); if (kinds.error || kinds.status !== 0 || kinds.signal - || kinds.stdout.trim().split('\n').length !== EXPECTED_MEMBERS.length + || kinds.stdout.trim().split('\n').length !== members.length || kinds.stdout.trim().split('\n').some((entry) => !entry.startsWith('-'))) { fail('tarball_member_type_invalid'); } @@ -181,7 +199,7 @@ function downloadTarball(lock) { let size = 0; response.on('data', (chunk) => { size += chunk.length; - if (size > 8 * 1024 * 1024) { request.destroy(new Error('tarball_too_large')); return; } + if (size > MAX_TARBALL_BYTES) { request.destroy(new Error('tarball_too_large')); return; } chunks.push(chunk); }); response.on('end', () => resolve(Buffer.concat(chunks))); @@ -215,8 +233,23 @@ if (require.main === module) { process.stdout.write(JSON.stringify({ status: 'installed', root: await installFromRegistry() }) + '\n'); } else if (action === 'install' && process.argv[3] === '--tarball' && path.isAbsolute(process.argv[4] || '') && process.argv.length === 5) { process.stdout.write(JSON.stringify({ status: 'installed', root: installTarball(process.argv[4]) }) + '\n'); + } else if (action === 'exec' && ((process.argv[3] === 'grammar' + && ['status', 'probe'].includes(process.argv[4])) + || (process.argv[3] === 'check' && process.argv[4] === 'all' + && path.isAbsolute(process.argv[5] || '')))) { + const args = process.argv.slice(3); + const result = spawnSync(activeBinary(), args, { + encoding: 'utf8', shell: false, + timeout: args[0] === 'check' ? 120000 : 30000, + maxBuffer: args[0] === 'check' ? 4 * 1024 * 1024 : 1024 * 1024, + env: { ...process.env, CODEGUARD_SKIP_GATE: '' }, + }); + if (result.error || result.signal) fail('grammar_command_failed'); + process.stdout.write(result.stdout); + process.stderr.write(result.stderr); + process.exitCode = result.status; } else { - fail('usage: verify | install --download | install --tarball ABS_PATH'); + fail('usage: verify | install --download | install --tarball ABS_PATH | exec grammar status|probe ... | exec check all ABS_PROJECT ...'); } })().catch((error) => { process.stderr.write(`${error.message}\n`); diff --git a/tests/rust-wasm-runtime-candidate.md b/tests/rust-wasm-runtime-candidate.md new file mode 100644 index 0000000..2d70054 --- /dev/null +++ b/tests/rust-wasm-runtime-candidate.md @@ -0,0 +1,24 @@ +# 32 语法 WASM 候选运行时验收 + +对应 `openspec/changes/2026-10-03-rust-wasm-runtime-candidate/`。本记录验证插件显式候选安装器对 CodeGuard CLI `0.1.3` 的调用,不代表默认 Hook 已自动执行 WASM 检查。 + +## 制品与边界 + +- 来源提交:`7900a1a8ccd5b4f5b54de8b5db3ccba16a356d3b`;npm 包:`@partme.ai/codeguard@0.1.3`,macOS arm64。 +- 本地与注册表 tarball 的 SHA-256 均为 `37509cdd108fee631eebb9ebcd794a6e8d63652801b3522625d0909c3da2ae12`;包内程序 SHA-256 为 `04bdea8c489693a8e97b451df8d0c2b7a89ba82372db5a4d1fed277ba19b0f4d`。 +- 锁定 38 个普通 tar 成员:原有 6 个文件、32 个 grammar 许可证文件。压缩包上限 16 MiB、程序上限 128 MiB、单份许可证上限 16 KiB。先核对完整 tarball 摘要及精确成员,再提取到私有暂存目录;激活及后续调用均复核程序和 32 份许可证摘要。 +- 使用 `node runtime/codeguard_runtime.cjs install --download` 与 `verify` 从注册表完成独立缓存安装和核验,均退出 0。普通 Hook 不自动下载。 + +## 候选能力与测试 + +- `CODEGUARD_TEST_TARBALL=/tmp/partme.ai-codeguard-0.1.3.tgz node --test tests/test_rust_runtime.cjs`:5/5 通过、零跳过。覆盖错误 tarball、缺运行时、现有 Claude 候选事件、活动许可证及程序篡改拒绝。 +- 安装后的真实程序 `grammar status --format=json` 返回 32 个候选、0 个已发布、`delivery_decision=not_evaluated`,退出 3;通过插件的 `exec grammar status --format=json` 得到同样的库存和退出码。 +- 插件 `exec check all <项目物理绝对路径> --format=json` 对 Zig 样例返回 `delivery_decision=incomplete`,`syntax_candidates.observations` 含 `candidate_observed`;不会把候选观察升级为通过。 +- 真实 Zig 文件的 `grammar probe zig <物理绝对路径> --format=json` 返回 `grammar_candidate_probe`、`grammar_qualified=false`、`delivery_decision=not_evaluated`,退出 3。macOS `/var` 为 `/private/var` 的别名,测试以 `realpath` 传入源码,避免路径安全校验与 grammar 能力混淆。 +- CodeGuard Rust 仓以 `CODEGUARD_WASM_BIN=$PWD/target/release/codeguard node --test --test-name-pattern='统一 check all 调用全部 32' tests/npm_pack_wasm.test.mjs` 运行真实离线 npm 包,1/1 通过。测试按每组 8 个文件调用统一 `check all`,最终观察语言集合精确等于 32 份 manifest 资产;该能力属于 CLI 0.1.3,插件默认 Hook 尚未接通。 +- 插件 `PYTHONDONTWRITEBYTECODE=1 python3 tests/run_all.py` 为 144/144 通过;Python unittest 为 654/654 通过;README 中英文对齐 7/7 通过;技能 vendor 离线与在线检查、OpenSpec strict、修改的 Node 语法检查及 `git diff --check` 均通过。 +- 本机五次新进程 `verify` 耗时 86.1、78.8、78.5、79.9、80.3 ms,中位数 79.9 ms;这是安装器复核耗时,不是宿主 Hook 端到端延迟。 + +## 未验收能力 + +32 份 WASM 均为**可调用候选资产**,不是 32 种已验收语法检查。当前默认 `hooks/hooks.json` 仍调用 Python;现有 Rust `PostToolUse` 尚未把 32 种 WASM 的检查结果自动注入智能体对话,候选也未接严格 Git 门禁。原生 lint 优先级、各语言误报率、真实宿主事件、跨平台制品与 CI 门禁仍须分别验证。候选返回 `not_evaluated` 不能被解释为代码通过或替代原生 lint。 diff --git a/tests/test_rust_runtime.cjs b/tests/test_rust_runtime.cjs index de9a52d..6dcaed5 100644 --- a/tests/test_rust_runtime.cjs +++ b/tests/test_rust_runtime.cjs @@ -19,15 +19,16 @@ function temporaryRoot() { function invoke(script, args, env, input = '') { return spawnSync(process.execPath, [script, ...args], { cwd: plugin, env: { ...process.env, ...env }, input, - encoding: 'utf8', timeout: 15000, maxBuffer: 16384, + encoding: 'utf8', timeout: 15000, maxBuffer: 1024 * 1024, }); } test('the plugin lock pins a single published runtime', () => { const lock = runtime.readLock(); - assert.equal(lock.version, '0.1.2'); + assert.equal(lock.version, '0.1.3'); assert.equal(lock.platform, 'macos_arm64'); assert.equal(lock.check_protocol_major, 1); + assert.equal(Object.keys(lock.grammar_licenses).length, 32); }); test('missing runtime returns incomplete without executing PATH codeguard or Python', (context) => { @@ -99,8 +100,36 @@ test('locked candidate installs, dispatches, and rejects a later binary mutation assert.equal(installed.status, 0, installed.stderr); const binary = runtime.activeBinary(cache); assert.equal(path.basename(binary), 'codeguard'); + const inventory = spawnSync(binary, ['grammar', 'status', '--format=json'], { encoding: 'utf8', timeout: 30000 }); + assert.equal(inventory.status, 3, inventory.stderr); + const grammarStatus = JSON.parse(inventory.stdout); + assert.equal(grammarStatus.candidate_count, 32); + assert.equal(grammarStatus.released_count, 0); + assert.equal(grammarStatus.delivery_decision, 'not_evaluated'); + const inventoryViaPlugin = invoke(path.join(__dirname, '..', 'runtime', 'codeguard_runtime.cjs'), + ['exec', 'grammar', 'status', '--format=json'], { CODEGUARD_RUNTIME_CACHE: cache }); + assert.equal(inventoryViaPlugin.status, 3, inventoryViaPlugin.stderr); + assert.equal(JSON.parse(inventoryViaPlugin.stdout).candidate_count, 32); const project = path.join(root, 'project'); fs.mkdirSync(project); + const zigFile = path.join(project, 'sample.zig'); + fs.writeFileSync(zigFile, 'const Empty = struct {};\n'); + const checked = invoke(manager, ['exec', 'check', 'all', fs.realpathSync(project), '--format=json', '--timeout', '30s'], { + CODEGUARD_RUNTIME_CACHE: cache, + }); + assert.equal(checked.status, 3, checked.stderr); + const checkReport = JSON.parse(checked.stdout); + assert.equal(checkReport.delivery_decision, 'incomplete'); + assert.equal(checkReport.syntax_candidates.observations.some(item => item.language === 'zig' + && item.status === 'candidate_observed'), true); + const probe = spawnSync(binary, ['grammar', 'probe', 'zig', fs.realpathSync(zigFile), '--format=json'], { + encoding: 'utf8', timeout: 30000, + }); + assert.equal(probe.status, 3, probe.stderr); + const candidate = JSON.parse(probe.stdout); + assert.equal(candidate.report_type, 'grammar_candidate_probe'); + assert.equal(candidate.grammar_qualified, false); + assert.equal(candidate.delivery_decision, 'not_evaluated'); const event = JSON.stringify({ hook_event_name: 'SessionStart', cwd: project, source: 'startup' }); const result = invoke(dispatcher, ['session-start'], { CODEGUARD_RUNTIME_CACHE: cache, CLAUDE_PROJECT_DIR: project, @@ -126,6 +155,12 @@ test('locked candidate installs, dispatches, and rejects a later binary mutation }, JSON.stringify({ ...prompt, prompt: '解释这个项目' })); assert.equal(ordinaryResult.status, 0, ordinaryResult.stderr); assert.deepEqual(JSON.parse(ordinaryResult.stdout), promptOutput); + const license = path.join(path.dirname(binary), '..', 'grammar-licenses', 'zig', 'LICENSE'); + const originalLicense = fs.readFileSync(license); + fs.appendFileSync(license, '\nmodified\n'); + assert.throws(() => runtime.activeBinary(cache), /grammar_license_digest_mismatch/); + fs.writeFileSync(license, originalLicense); + assert.equal(runtime.activeBinary(cache), binary); fs.appendFileSync(binary, '\nmodified\n'); assert.throws(() => runtime.activeBinary(cache), /binary_digest_mismatch/); const rejected = invoke(dispatcher, ['session-start'], { From dec5f9d1361eefff493e120b4278939a243814a9 Mon Sep 17 00:00:00 2001 From: loong10k <20489781+loong10k@users.noreply.github.com> Date: Sun, 4 Oct 2026 04:42:09 +0800 Subject: [PATCH 2/4] =?UTF-8?q?=E6=8E=A5=E9=80=9A=20Rust=20=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E7=94=9F=E5=91=BD=E5=91=A8=E6=9C=9F=E5=92=8C=E5=9B=BA?= =?UTF-8?q?=E5=AE=9A=E5=8E=9F=E7=94=9F=E5=A4=8D=E6=A3=80=E5=85=A5=E5=8F=A3?= =?UTF-8?q?=EF=BC=8C=E5=87=86=E5=A4=87=200.20.0=20=E5=8F=91=E5=B8=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .agents/plugins/marketplace.json | 8 +- .claude-plugin/marketplace.json | 2 +- .codex-plugin/plugin.json | 2 +- .github/workflows/skills-check.yml | 2 +- .zcode-plugin/plugin.json | 2 +- CHANGELOG.md | 7 + README.md | 21 ++- README.zh-CN.md | 21 ++- com.github.copilot/hooks/__protocol__.md | 27 +++- com.github.copilot/hooks/hooks.json | 28 ++-- .../hooks/rust_runtime_dispatch.cjs | 4 +- dev.openhands/hooks/__protocol__.md | 27 +++- dev.openhands/hooks/hooks.json | 28 ++-- dev.openhands/hooks/rust_runtime_dispatch.cjs | 4 +- hooks/__protocol__.md | 27 +++- hooks/hooks.json | 28 ++-- hooks/rust_runtime_dispatch.cjs | 4 +- kimi.plugin.json | 2 +- .../design.md | 25 ++++ .../proposal.md | 19 +++ .../specs/hook-protocol/spec.md | 27 ++++ .../tasks.md | 19 +++ plugin.json | 2 +- runtime/codeguard.lock.json | 12 +- runtime/codeguard_runtime.cjs | 15 +- tests/rust-lifecycle-default.md | 26 ++++ tests/test_rust_lifecycle.cjs | 136 ++++++++++++++++++ tests/test_rust_runtime.cjs | 2 +- 28 files changed, 451 insertions(+), 76 deletions(-) create mode 100644 openspec/changes/2026-10-04-rust-lifecycle-default/design.md create mode 100644 openspec/changes/2026-10-04-rust-lifecycle-default/proposal.md create mode 100644 openspec/changes/2026-10-04-rust-lifecycle-default/specs/hook-protocol/spec.md create mode 100644 openspec/changes/2026-10-04-rust-lifecycle-default/tasks.md create mode 100644 tests/rust-lifecycle-default.md create mode 100644 tests/test_rust_lifecycle.cjs diff --git a/.agents/plugins/marketplace.json b/.agents/plugins/marketplace.json index 1576d6d..58fa387 100644 --- a/.agents/plugins/marketplace.json +++ b/.agents/plugins/marketplace.json @@ -9,20 +9,20 @@ "source": { "source": "url", "url": "https://github.com/full-stack-plugins/codeguard-plugin.git", - "ref": "v0.19.0" + "ref": "v0.20.0" }, "policy": { "installation": "AVAILABLE", "authentication": "ON_USE" }, "category": "Developer Tools", - "version": "0.19.0", + "version": "0.20.0", "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.19.0/assets/official-logo.png", + "icon": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.20.0/assets/official-logo.png", "interface": { "displayName": "CodeGuard", "shortDescription": "Trustworthy code checks and Java impact analysis", - "logo": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.19.0/assets/official-logo.png" + "logo": "https://cdn.jsdelivr.net/gh/full-stack-plugins/codeguard-plugin@v0.20.0/assets/official-logo.png" } } ] diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index feb38e7..bc07f3b 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -7,7 +7,7 @@ { "name": "codeguard", "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.", - "version": "0.19.0", + "version": "0.20.0", "source": "./" } ] diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 18eb2da..e7d6ea0 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "codeguard", - "version": "0.19.0+codex.20261004", + "version": "0.20.0+codex.20261004", "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/.github/workflows/skills-check.yml b/.github/workflows/skills-check.yml index aacbe70..25089ed 100644 --- a/.github/workflows/skills-check.yml +++ b/.github/workflows/skills-check.yml @@ -18,7 +18,7 @@ jobs: run: | node --check runtime/codeguard_runtime.cjs node --check hooks/rust_runtime_dispatch.cjs - node --test tests/test_rust_runtime.cjs + node --test --test-concurrency=1 tests/test_rust_runtime.cjs tests/test_rust_lifecycle.cjs vendor-check: runs-on: ubuntu-latest strategy: diff --git a/.zcode-plugin/plugin.json b/.zcode-plugin/plugin.json index cb03884..81de658 100644 --- a/.zcode-plugin/plugin.json +++ b/.zcode-plugin/plugin.json @@ -5,7 +5,7 @@ "en": "CodeGuard", "zh-CN": "代码规范检查" }, - "version": "0.19.0", + "version": "0.20.0", "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/CHANGELOG.md b/CHANGELOG.md index 558645f..4cec458 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 0.20.0 — 2026-10-04 + +- 固定 Rust CLI 0.1.4 与全部 32 份可执行、未验收 grammar 的公开制品身份。 +- canonical 与分发镜像的五类非阻断生命周期改走 Rust:指定保存范围、稳定确认任务、有界修复指引;缺运行时明确未完成,不下载、不回退 Python。 +- 同一固定运行时支持显式 init/next/task show/task verify,保留原生复检及可信关闭边界。 +- Git PreToolUse 与旧 CLI/MCP 暂留兼容入口;真实宿主、完整原生优先与语法精度未验收。 + ## 0.18.3 — 2026-10-03 - 各宿主和市场清单的显示名称统一为 `CodeGuard`,保留中文本地化翻译。 diff --git a/README.md b/README.md index aa25b1f..e162d47 100644 --- a/README.md +++ b/README.md @@ -10,34 +10,43 @@ CodeGuard provides native check evidence and guards supported Git commit/push calls from AI coding assistants. PostToolUse gives feedback, not blocking. Verified violations block the Git call; unavailable checks remain explicitly UNVERIFIED. Passing a configured check is not proof of complete code correctness. -### Rust runtime candidate (macOS arm64) +### Rust lifecycle runtime (macOS arm64) -The plugin now contains a **candidate**, explicit Rust runtime binding. It pins `@partme.ai/codeguard@0.1.3`, its registry tarball and native binary hashes, 32 grammar-license hashes, candidate source commit, platform, and check protocol major in [`runtime/codeguard.lock.json`](runtime/codeguard.lock.json). Install only on Apple Silicon macOS: +Canonical [`hooks/hooks.json`](hooks/hooks.json) now routes SessionStart, UserPromptSubmit, PostToolUse, PostToolUseFailure and Stop through [`hooks/rust_runtime_dispatch.cjs`](hooks/rust_runtime_dispatch.cjs) to fixed `@partme.ai/codeguard@0.1.4`. [`runtime/codeguard.lock.json`](runtime/codeguard.lock.json) pins the public tarball, binary, source commit and 32 grammar-license hashes. Node 18+ is the thin host binding; Rust performs discovery, native checks, WASM parsing and task synchronization. + +Install explicitly on Apple Silicon macOS, then initialize the selected project: ```bash node runtime/codeguard_runtime.cjs install --download node runtime/codeguard_runtime.cjs verify +node runtime/codeguard_runtime.cjs exec init /absolute/project --apply --format=json +node runtime/codeguard_runtime.cjs exec next /absolute/project --format=json node runtime/codeguard_runtime.cjs exec grammar status --format=json node runtime/codeguard_runtime.cjs exec check all /absolute/project --format=json +# Use the real ID returned by next: +node runtime/codeguard_runtime.cjs exec task show TASK_ID /absolute/project --format=json +node runtime/codeguard_runtime.cjs exec task verify TASK_ID /absolute/project --zig-tool /absolute/zig --format=json ``` -For offline installation, use `install --tarball /absolute/path/to/partme.ai-codeguard-0.1.3.tgz`; the same lock hashes are enforced. Installation stages and verifies the package before switching the active receipt. The package contains 32 callable WASM grammar candidates; `grammar status` reports them as unqualified, and `grammar probe --format=json` provides explicit, non-gating observations. A candidate pass cannot replace native lint or a complete project check. [`hooks/rust_runtime_dispatch.cjs`](hooks/rust_runtime_dispatch.cjs) is a candidate Claude Code adapter for SessionStart, UserPromptSubmit, PostToolUse, PostToolUseFailure, and Stop. It rechecks the active binary before each call, never selects a random `codeguard` on `PATH`, and reports an incomplete runtime instead of falling back to Python. UserPromptSubmit returns fixed, non-blocking timing guidance; it does not run lint or treat prompt words as a Git gate. The current [`hooks/hooks.json`](hooks/hooks.json) still uses the legacy Python hooks; the Rust candidate has not been made the default, connected to automatic WASM feedback or a strict Git gate, or accepted on Codex/ZCode/Kimi. See the [candidate acceptance record](tests/rust-runtime-candidate.md), [32-grammar runtime acceptance](tests/rust-wasm-runtime-candidate.md), and [hook protocol](hooks/__protocol__.md). +Offline installation uses `install --tarball /absolute/path/to/codeguard-public-0.1.4-darwin-arm64.tgz` with the same digest checks. Hooks do not download packages. An unavailable or unsupported runtime returns visible incomplete/setup guidance, without PATH codeguard or Python fallback. Successful edits select only the changed file: supported native Ruff/ESLint checks precede bounded WASM; suspected syntax creates a stable native-confirmation task in an initialized workspace. Zero recovery nodes only recommend native checking and never close an old task. The program contains all 32 runnable but **unqualified** grammar candidates, including Dart and Zig. + +Prompt events only give fixed timing guidance, failed edits do not scan source, and Stop gives bounded next-task guidance with reentry handling. Internal check budget is 5 seconds, child-process timeout 8 seconds, host timeout 10 seconds. This does not certify all filesystem I/O deadlines or installed-host latency. `auto_fix_on_save` and legacy timeout settings remain compatibility fields; Rust does not silently apply fixes. An uninitialized workspace needs explicit init before durable tasks exist. -Explicit `exec check all` runs applicable native checks first, then bounded WASM candidates for uncovered files; its report stays incomplete. The installed plugin version is recorded in its manifests. The architecture refactor keeps the existing 68 managed skills and focuses on reliable evidence, clear module ownership and Java project awareness. +The PreToolUse Git gate and legacy CLI/MCP remain Python compatibility surfaces; Copilot/OpenHands carry identical mirrors of the lifecycle binding for package consistency. Those mirrors are not installed-host acceptance. Installed Claude/Codex/ZCode/Kimi automation, trusted closure policy, full native-first coverage, other platforms and precision remain open. See [default lifecycle acceptance](tests/rust-lifecycle-default.md), [historical candidate evidence](tests/rust-runtime-candidate.md), [historical 32-grammar acceptance](tests/rust-wasm-runtime-candidate.md), and [hook protocol](hooks/__protocol__.md). ### Runtime boundaries | Surface | What it checks | Result | |---|---|---| | PostToolUse | Edited file, for file-scoped tools | Feedback, exit 0; project-level checks deferred | -| UserPromptSubmit | Working-tree changes relevant to commit intent | Advisory, never blocks the user message | +| UserPromptSubmit | Fixed checking-time guidance; no source scan | Advisory, never blocks the user message | | PreToolUse Git gate | Proposed index snapshot or HEAD snapshot for push | Verified violations exit 2; uncertain checks report UNVERIFIED and fail open | | CLI check / MCP check_code_style | Project checks, including Java build verification | Explicit status, reason, raw exit code, ordered execution trace and output log | | pre-commit / CI | Independently configured checks | Separate acceptance; not replaced by hook success | Hooks do not run in every host command surface automatically. Historical V0.5.4 installation evidence is not acceptance of this version in Codex, ZCode or Kimi. -Claude Code does not support a matcher on `UserPromptSubmit`: it starts the legacy Python Hook for every submitted prompt. The Hook returns before environment or repository work when its own intent filter does not match. A commit-intent prompt may still run the legacy soft check; only the actual Git command reaches the PreToolUse gate. The manifest cannot avoid per-prompt process startup, and installed-host latency has not been measured. +Claude Code invokes the canonical Rust prompt binding on every UserPromptSubmit; no keyword matcher or prompt-triggered lint is used. Copilot/OpenHands carry the same mirror; their actual host protocol compatibility still needs separate acceptance. Actual Git commands still reach PreToolUse. Installed-host latency has not been measured. ### Verdict contract diff --git a/README.zh-CN.md b/README.zh-CN.md index 92f2d91..d9dafa4 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -10,34 +10,43 @@ CodeGuard 为 AI 编程助手提供原生检查证据,守护已支持的 Git 提交/推送调用。PostToolUse 只反馈、不阻断;确定违规会拦截 Git 调用;检查无法完成时明确 UNVERIFIED。某项检查通过,不等于代码完全正确。 -### Rust 运行时候选(macOS arm64) +### Rust 生命周期运行时(macOS arm64) -插件新增**显式候选** Rust 运行时绑定。[`runtime/codeguard.lock.json`](runtime/codeguard.lock.json) 固定 `@partme.ai/codeguard@0.1.3`、注册表 tarball 和原生二进制摘要、32 份 grammar 许可证摘要、候选源码提交、平台及检查协议主版本。仅在 Apple Silicon macOS 上安装: +Canonical [`hooks/hooks.json`](hooks/hooks.json) 现将 SessionStart、UserPromptSubmit、PostToolUse、PostToolUseFailure、Stop 经 [`hooks/rust_runtime_dispatch.cjs`](hooks/rust_runtime_dispatch.cjs) 接到固定 `@partme.ai/codeguard@0.1.4`。[`runtime/codeguard.lock.json`](runtime/codeguard.lock.json) 锁定公开 tarball、程序、源码提交和 32 份 grammar 许可证摘要。Node 18+ 仅绑定宿主;发现、原生检查、WASM 解析和任务同步由 Rust 执行。 + +在 Apple Silicon macOS 上显式安装,再初始化指定项目: ```bash node runtime/codeguard_runtime.cjs install --download node runtime/codeguard_runtime.cjs verify +node runtime/codeguard_runtime.cjs exec init /absolute/project --apply --format=json +node runtime/codeguard_runtime.cjs exec next /absolute/project --format=json node runtime/codeguard_runtime.cjs exec grammar status --format=json node runtime/codeguard_runtime.cjs exec check all /absolute/project --format=json +# TASK_ID 使用 next 返回的真实 ID: +node runtime/codeguard_runtime.cjs exec task show TASK_ID /absolute/project --format=json +node runtime/codeguard_runtime.cjs exec task verify TASK_ID /absolute/project --zig-tool /absolute/zig --format=json ``` -离线安装可执行 `install --tarball /absolute/path/to/partme.ai-codeguard-0.1.3.tgz`,仍须满足同一锁定摘要。安装器先在暂存目录核验包,再切换活动收据。包内有 32 份可调用的 WASM grammar 候选;`grammar status` 明确报告尚未验收,`grammar probe --format=json` 提供显式、非门禁的观察。候选通过不能代替原生 lint 或完整项目检查。[`hooks/rust_runtime_dispatch.cjs`](hooks/rust_runtime_dispatch.cjs) 是 Claude Code 的 SessionStart、UserPromptSubmit、PostToolUse、PostToolUseFailure、Stop 候选适配器;每次调用前复核活动二进制,不从 PATH 随机选择 `codeguard`,缺运行时时报告未完成而不回退 Python。UserPromptSubmit 仅返回固定、非阻断的检查时机建议,不运行 lint,也不把提示词当作 Git 门禁。当前 [`hooks/hooks.json`](hooks/hooks.json) 仍使用旧 Python 钩子;Rust 候选尚未成为默认入口,未接 WASM 自动反馈或严格 Git 门禁,也未在 Codex/ZCode/Kimi 完成宿主验收。见[候选验收记录](tests/rust-runtime-candidate.md)、[32 语法运行时验收](tests/rust-wasm-runtime-candidate.md)和[钩子协议](hooks/__protocol__.md)。 +离线安装使用 `install --tarball /absolute/path/to/codeguard-public-0.1.4-darwin-arm64.tgz`,仍核对相同摘要。普通 Hook 不下载包;运行时缺失或平台不支持时给出明确未完成与准备指引,不执行 PATH codeguard,不回退 Python。成功保存仅检查本次文件:已接入的 Ruff/ESLint 原生检查优先,再对未覆盖文件执行有界 WASM;疑似语法问题在已初始化工作区形成稳定原生确认任务。零恢复仅推荐原生检查,不能关闭旧任务。包内全部 32 份 grammar 可执行但**未完成精度验收**,包含 Dart、Zig。 + +提示事件只给固定时机指引,失败保存不扫描,Stop 提供有界 next 指引并处理重入。内部检查预算 5 秒、子进程超时 8 秒、宿主超时 10 秒;不表示所有文件系统 I/O 硬期限和真实宿主延迟已验收。auto_fix_on_save 与旧超时配置仅保留兼容含义,Rust 不静默修复源码;未初始化工作区须先显式 init 才能持久任务。 -显式 `exec check all` 先运行适用的原生检查,再对未覆盖文件执行有界 WASM 候选检查;报告仍是不完整状态。已安装插件的版本以各清单为准。本次架构重构保留现有 68 个受管技能,重点收敛判定证据、模块职责与 Java 项目感知。 +PreToolUse Git 门禁与旧 CLI/MCP 暂留 Python 兼容入口,Copilot/OpenHands 镜像与 canonical 绑定保持字节一致,不等于对应宿主验收完成。真实 Claude/Codex/ZCode/Kimi 自动触发、可信关闭策略、完整原生优先、多平台与精度仍缺。见[默认生命周期验收](tests/rust-lifecycle-default.md)、[历史候选证据](tests/rust-runtime-candidate.md)、[历史 32 语法验收](tests/rust-wasm-runtime-candidate.md)与[钩子协议](hooks/__protocol__.md)。 ### 运行边界 | 入口 | 检查对象 | 结果 | |---|---|---| | PostToolUse | 文件型工具检查本次编辑文件 | 反馈、exit 0;项目级检查延后 | -| UserPromptSubmit | 与提交意图相关的工作树变更 | 建议,不阻断用户消息 | +| UserPromptSubmit | 固定检查时机指引,不扫描源码 | 建议,不阻断用户消息 | | PreToolUse Git 门禁 | 拟提交 index 快照或推送 HEAD 快照 | 确定违规 exit 2;不确定项明确 UNVERIFIED 并 fail-open | | CLI check / MCP check_code_style | 项目检查,含 Java 构建验证 | 明确状态、原因、原始退出码、顺序执行证据和日志 | | pre-commit / CI | 独立配置的检查 | 单独验收,不能由钩子成功代替 | 钩子不会自动覆盖宿主的每个命令入口。历史 V0.5.4 安装证据不能代表当前版本已在 Codex、ZCode、Kimi 验收。 -Claude Code 的 `UserPromptSubmit` 不支持 matcher:每次提交提示都会启动旧 Python Hook。内部意图过滤未命中时,Hook 会在环境或仓库探测前退出;提交意图仍可能执行旧软检查,实际 Git 命令才进入 PreToolUse 门禁。清单无法消除每条提示的进程启动,已安装宿主的延迟尚未实测。 +Claude Code 每次 UserPromptSubmit 都启动 canonical Rust 提示绑定,不依赖关键词 matcher,也不因提示词启动 lint。Copilot/OpenHands 同步相同绑定镜像,其实际宿主协议仍待独立验收;实际 Git 命令仍由 PreToolUse 检查。真实宿主延迟尚未测量。 ### 判定契约 diff --git a/com.github.copilot/hooks/__protocol__.md b/com.github.copilot/hooks/__protocol__.md index 7572fac..f54e30a 100644 --- a/com.github.copilot/hooks/__protocol__.md +++ b/com.github.copilot/hooks/__protocol__.md @@ -1,9 +1,9 @@ # hooks/ 与宿主 CLI 之间的契约(canonical cheat-sheet) -> Rust 候选入口:`runtime/codeguard_runtime.cjs` 固定 npm 0.1.2 制品和本机二进制身份;`hooks/rust_runtime_dispatch.cjs` 显式处理 Claude Code 的 SessionStart/UserPromptSubmit/PostToolUse/PostToolUseFailure/Stop。UserPromptSubmit 只输出固定的非阻断检查时机建议,源码检查未运行、交付未评估;提示词不能触发 lint 或替代真实 Git 门禁。候选入口在运行时缺失、摘要或版本失配时返回对话中的“未完成”,不执行 PATH 上的 `codeguard`,也不回退到 Python。它尚未写入 `hooks/hooks.json`,以下旧协议表仍描述默认 Python 运行时。Rust 严格交付门禁与其它宿主接线尚未完成,不能把候选保存反馈当作通过。 +> 当前 canonical 默认:Node 18+ 宿主绑定固定 npm 0.1.4 Rust 程序,非阻断生命周期不回退 Python;安装显式、失败明确未完成。真实 Git PreToolUse 仍为 Python 兼容门禁,Copilot/OpenHands 镜像同步同一绑定,实际宿主另行验收。CLI/脚本重放不等于实际宿主验收,保存反馈退出 0 不代表质量通过。 > 这份文档是 codeguard-plugin 与 Codex CLI / ZCode / Kimi Code 三端宿主 -> 交互契约的**单源事实**。所有 hook 脚本(`hooks/*.py`)的实现都必须与本文件 +> 交互契约的**单源事实**。所有 Hook 脚本(Rust 绑定与 `hooks/*.py` 兼容入口)的实现都必须与本文件 > 一致;若未来调整任何退出码或 JSON schema,**同一 commit 内**必须更新本文件。 > > 对应的 OpenSpec 规范是 `hook-protocol`(见 `openspec/specs/hook-protocol/spec.md`)。 @@ -12,6 +12,25 @@ ## 1. 协议总表 +当前 `hooks/hooks.json` 的默认入口: + +| 事件 | 默认指令 | 输出与检查范围 | 宿主超时/退出 | +|---|---|---|---| +| SessionStart | `node hooks/rust_runtime_dispatch.cjs session-start` | 只读发现;未安装明确准备动作 | 10 秒 / 0 | +| UserPromptSubmit | `node hooks/rust_runtime_dispatch.cjs user-prompt-submit` | 固定时机指引;不运行 lint | 10 秒 / 0 | +| PostToolUse | `node hooks/rust_runtime_dispatch.cjs post-tool-use` | 成功编辑范围;原生优先、有界 WASM、稳定任务和复检指引 | 10 秒 / 0 | +| PostToolUseFailure | `node hooks/rust_runtime_dispatch.cjs post-tool-use-failure` | 明示未执行源码检查 | 10 秒 / 0 | +| Stop | `node hooks/rust_runtime_dispatch.cjs stop` | next 指引;重入不阻断 | 10 秒 / 0 | +| PreToolUse Bash | `python3 hooks/pre_tool_git_guard.py` | 既有真实 Git 内容及原生兼容门禁 | 120 秒 / 0 或 2 | + +上表指令在清单中使用绝对插件根。Rust 内部预算 5 秒、宿主子进程上限 8 秒、摘要限 1200 字符;事件失败、输入超限、范围异常、运行时缺失均给脱敏未完成摘要。stdout 使用 `hookSpecificOutput` 对应事件的 additionalContext;Stop 使用 systemMessage 或 Stop 事件输出。适配能力不足、未初始化和持久化失败保留未完成,不自批任务或签发交付许可。 + +修复指引通过同一固定程序执行:`node runtime/codeguard_runtime.cjs exec init ABS_PROJECT --apply`、`exec next ABS_PROJECT --format=json`、`exec task show TASK_ID ABS_PROJECT --format=json`、`exec task verify TASK_ID ABS_PROJECT --zig-tool ABS_ZIG --format=json`。可查询命令退出 0 仍不代表质量 allow;原生复检结果保持现有未完成语义。普通 Hook 不联网安装。 + +### 1.1 旧兼容入口协议 + +以下表格、Python 符号和异常约定描述保留的旧 CLI/MCP、显式 Python Hook 与显式旧 Python Hook;不描述上表当前 canonical Rust 生命周期。 + | 事件 | Hook 脚本 | stdout | stderr | exit | 阻断? | |---|---|---|---|---|---| | SessionStart | `env_check.py` | 人类可读一行摘要 `codeguard 插件环境:...` | 仅内部错误时 | 0 | 否 | @@ -20,7 +39,7 @@ | PostToolUse `Write\|Edit\|MultiEdit` | `post_tool_lint.py` | JSON `{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"..."}, "systemMessage":"..."}` | 仅内部错误时 | 0 | 否 | | Stop | `stop_summary.py` | 人类可读会话摘要 | 仅内部错误时 | 0 | 否 | -[Claude Code Hooks reference](https://code.claude.com/docs/en/hooks) 说明 `UserPromptSubmit` 不支持 matcher,宿主每次收到用户提示都会启动该 Hook。三份 `hooks.json` 因此不声明关键词 matcher;`user_prompt_validator.py::main` 先执行 `is_trigger`,未命中时在会话/worktree 归属、PATH 探测、仓库发现和原生门禁前退出且无对话输出。命中时旧软门禁仍可能运行;真实提交/推送只能由 PreToolUse 的命令时门禁判断。此变更不证明宿主进程启动开销已经消失或已实测。 +[Claude Code Hooks reference](https://code.claude.com/docs/en/hooks) 说明 `UserPromptSubmit` 不支持 matcher,宿主每次收到用户提示都会启动该 Hook。三份 `hooks.json` 均不声明关键词 matcher;当前默认不运行旧过滤逻辑,以下 Python 符号仅描述显式兼容入口;`user_prompt_validator.py::main` 先执行 `is_trigger`,未命中时在会话/worktree 归属、PATH 探测、仓库发现和原生门禁前退出且无对话输出。命中时旧软门禁仍可能运行;真实提交/推送只能由 PreToolUse 的命令时门禁判断。此变更不证明宿主进程启动开销已经消失或已实测。 实现定位(**符号而非行号**——行号随每次重构腐烂,符号不腐;v0.8.0 起弃用行号指针): - SessionStart 摘要:`hooks/env_check.py::main`(含双副本告警段)。 @@ -54,7 +73,7 @@ CLI/MCP 明确返回 UNVERIFIED,自动修复不执行。门禁以未验证说 | ZCode | 同 Codex | 同 Codex,additionalContext 进 systemMessage | 同 Codex | 同 Codex | 同 Codex | | Kimi Code CLI | stdout 摘要注入会话上下文 | stdout JSON 注入上下文(观察型,**不阻断用户消息**) | exit 2 拦截 + stderr 指令;exit 0 静默 | stdout JSON 注入上下文 | stdout 摘要 | -所有三端在所有事件上,**内部错误都走 fail-open**(见 §3),hook bug 永远不阻断工作流。 +旧兼容三端在所有事件上,**内部错误都走 fail-open**(见 §3),hook bug 永远不阻断工作流。 --- diff --git a/com.github.copilot/hooks/hooks.json b/com.github.copilot/hooks/hooks.json index 62dda55..2fd3d4a 100644 --- a/com.github.copilot/hooks/hooks.json +++ b/com.github.copilot/hooks/hooks.json @@ -6,8 +6,8 @@ "hooks": [ { "type": "command", - "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/env_check.py\"", - "timeout": 30, + "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/rust_runtime_dispatch.cjs\" session-start", + "timeout": 10, "statusMessage": "codeguard: 检测项目语言与现有 linter 配置…" } ] @@ -18,8 +18,8 @@ "hooks": [ { "type": "command", - "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/user_prompt_validator.py\"", - "timeout": 120 + "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/rust_runtime_dispatch.cjs\" user-prompt-submit", + "timeout": 10 } ] } @@ -42,8 +42,8 @@ "hooks": [ { "type": "command", - "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/post_tool_lint.py\"", - "timeout": 180 + "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/rust_runtime_dispatch.cjs\" post-tool-use", + "timeout": 10 } ] } @@ -53,8 +53,20 @@ "hooks": [ { "type": "command", - "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/stop_summary.py\"", - "timeout": 15 + "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/rust_runtime_dispatch.cjs\" stop", + "timeout": 10 + } + ] + } + ], + "PostToolUseFailure": [ + { + "matcher": "Write|Edit|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/rust_runtime_dispatch.cjs\" post-tool-use-failure", + "timeout": 10 } ] } diff --git a/com.github.copilot/hooks/rust_runtime_dispatch.cjs b/com.github.copilot/hooks/rust_runtime_dispatch.cjs index 992e7a2..33de11d 100644 --- a/com.github.copilot/hooks/rust_runtime_dispatch.cjs +++ b/com.github.copilot/hooks/rust_runtime_dispatch.cjs @@ -1,7 +1,7 @@ #!/usr/bin/env node 'use strict'; -// Candidate Claude Code binding. It never resolves `codeguard` from PATH or falls back to Python. +// Canonical non-blocking Claude-shape lifecycle binding. It never resolves `codeguard` from PATH or falls back to Python. const fs = require('node:fs'); const path = require('node:path'); const { spawnSync } = require('node:child_process'); @@ -19,7 +19,7 @@ const EVENTS = new Map([ function incomplete(event, reason) { const safe = /^[a-z_]+$/.test(reason) ? reason : 'runtime_unavailable'; - const message = `CodeGuard Rust 运行时未完成(${safe});本次源码检查未运行,交付未评估。请执行插件 runtime/codeguard_runtime.cjs verify 或修复安装。`; + const message = `CodeGuard Rust 运行时未完成(${safe});本次源码检查未运行,交付未评估。请用 Node 18+ 执行 node runtime/codeguard_runtime.cjs verify;支持的 macOS arm64 上显式运行 node runtime/codeguard_runtime.cjs install --download。运行时可用后,使用同一入口 exec init ABS_PROJECT --apply 初始化修复任务,或 exec next ABS_PROJECT 获取指引。`; return event === 'stop' ? { systemMessage: message } : { hookSpecificOutput: { hookEventName: EVENTS.get(event), additionalContext: message } }; diff --git a/dev.openhands/hooks/__protocol__.md b/dev.openhands/hooks/__protocol__.md index 7572fac..f54e30a 100644 --- a/dev.openhands/hooks/__protocol__.md +++ b/dev.openhands/hooks/__protocol__.md @@ -1,9 +1,9 @@ # hooks/ 与宿主 CLI 之间的契约(canonical cheat-sheet) -> Rust 候选入口:`runtime/codeguard_runtime.cjs` 固定 npm 0.1.2 制品和本机二进制身份;`hooks/rust_runtime_dispatch.cjs` 显式处理 Claude Code 的 SessionStart/UserPromptSubmit/PostToolUse/PostToolUseFailure/Stop。UserPromptSubmit 只输出固定的非阻断检查时机建议,源码检查未运行、交付未评估;提示词不能触发 lint 或替代真实 Git 门禁。候选入口在运行时缺失、摘要或版本失配时返回对话中的“未完成”,不执行 PATH 上的 `codeguard`,也不回退到 Python。它尚未写入 `hooks/hooks.json`,以下旧协议表仍描述默认 Python 运行时。Rust 严格交付门禁与其它宿主接线尚未完成,不能把候选保存反馈当作通过。 +> 当前 canonical 默认:Node 18+ 宿主绑定固定 npm 0.1.4 Rust 程序,非阻断生命周期不回退 Python;安装显式、失败明确未完成。真实 Git PreToolUse 仍为 Python 兼容门禁,Copilot/OpenHands 镜像同步同一绑定,实际宿主另行验收。CLI/脚本重放不等于实际宿主验收,保存反馈退出 0 不代表质量通过。 > 这份文档是 codeguard-plugin 与 Codex CLI / ZCode / Kimi Code 三端宿主 -> 交互契约的**单源事实**。所有 hook 脚本(`hooks/*.py`)的实现都必须与本文件 +> 交互契约的**单源事实**。所有 Hook 脚本(Rust 绑定与 `hooks/*.py` 兼容入口)的实现都必须与本文件 > 一致;若未来调整任何退出码或 JSON schema,**同一 commit 内**必须更新本文件。 > > 对应的 OpenSpec 规范是 `hook-protocol`(见 `openspec/specs/hook-protocol/spec.md`)。 @@ -12,6 +12,25 @@ ## 1. 协议总表 +当前 `hooks/hooks.json` 的默认入口: + +| 事件 | 默认指令 | 输出与检查范围 | 宿主超时/退出 | +|---|---|---|---| +| SessionStart | `node hooks/rust_runtime_dispatch.cjs session-start` | 只读发现;未安装明确准备动作 | 10 秒 / 0 | +| UserPromptSubmit | `node hooks/rust_runtime_dispatch.cjs user-prompt-submit` | 固定时机指引;不运行 lint | 10 秒 / 0 | +| PostToolUse | `node hooks/rust_runtime_dispatch.cjs post-tool-use` | 成功编辑范围;原生优先、有界 WASM、稳定任务和复检指引 | 10 秒 / 0 | +| PostToolUseFailure | `node hooks/rust_runtime_dispatch.cjs post-tool-use-failure` | 明示未执行源码检查 | 10 秒 / 0 | +| Stop | `node hooks/rust_runtime_dispatch.cjs stop` | next 指引;重入不阻断 | 10 秒 / 0 | +| PreToolUse Bash | `python3 hooks/pre_tool_git_guard.py` | 既有真实 Git 内容及原生兼容门禁 | 120 秒 / 0 或 2 | + +上表指令在清单中使用绝对插件根。Rust 内部预算 5 秒、宿主子进程上限 8 秒、摘要限 1200 字符;事件失败、输入超限、范围异常、运行时缺失均给脱敏未完成摘要。stdout 使用 `hookSpecificOutput` 对应事件的 additionalContext;Stop 使用 systemMessage 或 Stop 事件输出。适配能力不足、未初始化和持久化失败保留未完成,不自批任务或签发交付许可。 + +修复指引通过同一固定程序执行:`node runtime/codeguard_runtime.cjs exec init ABS_PROJECT --apply`、`exec next ABS_PROJECT --format=json`、`exec task show TASK_ID ABS_PROJECT --format=json`、`exec task verify TASK_ID ABS_PROJECT --zig-tool ABS_ZIG --format=json`。可查询命令退出 0 仍不代表质量 allow;原生复检结果保持现有未完成语义。普通 Hook 不联网安装。 + +### 1.1 旧兼容入口协议 + +以下表格、Python 符号和异常约定描述保留的旧 CLI/MCP、显式 Python Hook 与显式旧 Python Hook;不描述上表当前 canonical Rust 生命周期。 + | 事件 | Hook 脚本 | stdout | stderr | exit | 阻断? | |---|---|---|---|---|---| | SessionStart | `env_check.py` | 人类可读一行摘要 `codeguard 插件环境:...` | 仅内部错误时 | 0 | 否 | @@ -20,7 +39,7 @@ | PostToolUse `Write\|Edit\|MultiEdit` | `post_tool_lint.py` | JSON `{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"..."}, "systemMessage":"..."}` | 仅内部错误时 | 0 | 否 | | Stop | `stop_summary.py` | 人类可读会话摘要 | 仅内部错误时 | 0 | 否 | -[Claude Code Hooks reference](https://code.claude.com/docs/en/hooks) 说明 `UserPromptSubmit` 不支持 matcher,宿主每次收到用户提示都会启动该 Hook。三份 `hooks.json` 因此不声明关键词 matcher;`user_prompt_validator.py::main` 先执行 `is_trigger`,未命中时在会话/worktree 归属、PATH 探测、仓库发现和原生门禁前退出且无对话输出。命中时旧软门禁仍可能运行;真实提交/推送只能由 PreToolUse 的命令时门禁判断。此变更不证明宿主进程启动开销已经消失或已实测。 +[Claude Code Hooks reference](https://code.claude.com/docs/en/hooks) 说明 `UserPromptSubmit` 不支持 matcher,宿主每次收到用户提示都会启动该 Hook。三份 `hooks.json` 均不声明关键词 matcher;当前默认不运行旧过滤逻辑,以下 Python 符号仅描述显式兼容入口;`user_prompt_validator.py::main` 先执行 `is_trigger`,未命中时在会话/worktree 归属、PATH 探测、仓库发现和原生门禁前退出且无对话输出。命中时旧软门禁仍可能运行;真实提交/推送只能由 PreToolUse 的命令时门禁判断。此变更不证明宿主进程启动开销已经消失或已实测。 实现定位(**符号而非行号**——行号随每次重构腐烂,符号不腐;v0.8.0 起弃用行号指针): - SessionStart 摘要:`hooks/env_check.py::main`(含双副本告警段)。 @@ -54,7 +73,7 @@ CLI/MCP 明确返回 UNVERIFIED,自动修复不执行。门禁以未验证说 | ZCode | 同 Codex | 同 Codex,additionalContext 进 systemMessage | 同 Codex | 同 Codex | 同 Codex | | Kimi Code CLI | stdout 摘要注入会话上下文 | stdout JSON 注入上下文(观察型,**不阻断用户消息**) | exit 2 拦截 + stderr 指令;exit 0 静默 | stdout JSON 注入上下文 | stdout 摘要 | -所有三端在所有事件上,**内部错误都走 fail-open**(见 §3),hook bug 永远不阻断工作流。 +旧兼容三端在所有事件上,**内部错误都走 fail-open**(见 §3),hook bug 永远不阻断工作流。 --- diff --git a/dev.openhands/hooks/hooks.json b/dev.openhands/hooks/hooks.json index 62dda55..2fd3d4a 100644 --- a/dev.openhands/hooks/hooks.json +++ b/dev.openhands/hooks/hooks.json @@ -6,8 +6,8 @@ "hooks": [ { "type": "command", - "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/env_check.py\"", - "timeout": 30, + "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/rust_runtime_dispatch.cjs\" session-start", + "timeout": 10, "statusMessage": "codeguard: 检测项目语言与现有 linter 配置…" } ] @@ -18,8 +18,8 @@ "hooks": [ { "type": "command", - "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/user_prompt_validator.py\"", - "timeout": 120 + "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/rust_runtime_dispatch.cjs\" user-prompt-submit", + "timeout": 10 } ] } @@ -42,8 +42,8 @@ "hooks": [ { "type": "command", - "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/post_tool_lint.py\"", - "timeout": 180 + "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/rust_runtime_dispatch.cjs\" post-tool-use", + "timeout": 10 } ] } @@ -53,8 +53,20 @@ "hooks": [ { "type": "command", - "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/stop_summary.py\"", - "timeout": 15 + "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/rust_runtime_dispatch.cjs\" stop", + "timeout": 10 + } + ] + } + ], + "PostToolUseFailure": [ + { + "matcher": "Write|Edit|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/rust_runtime_dispatch.cjs\" post-tool-use-failure", + "timeout": 10 } ] } diff --git a/dev.openhands/hooks/rust_runtime_dispatch.cjs b/dev.openhands/hooks/rust_runtime_dispatch.cjs index 992e7a2..33de11d 100644 --- a/dev.openhands/hooks/rust_runtime_dispatch.cjs +++ b/dev.openhands/hooks/rust_runtime_dispatch.cjs @@ -1,7 +1,7 @@ #!/usr/bin/env node 'use strict'; -// Candidate Claude Code binding. It never resolves `codeguard` from PATH or falls back to Python. +// Canonical non-blocking Claude-shape lifecycle binding. It never resolves `codeguard` from PATH or falls back to Python. const fs = require('node:fs'); const path = require('node:path'); const { spawnSync } = require('node:child_process'); @@ -19,7 +19,7 @@ const EVENTS = new Map([ function incomplete(event, reason) { const safe = /^[a-z_]+$/.test(reason) ? reason : 'runtime_unavailable'; - const message = `CodeGuard Rust 运行时未完成(${safe});本次源码检查未运行,交付未评估。请执行插件 runtime/codeguard_runtime.cjs verify 或修复安装。`; + const message = `CodeGuard Rust 运行时未完成(${safe});本次源码检查未运行,交付未评估。请用 Node 18+ 执行 node runtime/codeguard_runtime.cjs verify;支持的 macOS arm64 上显式运行 node runtime/codeguard_runtime.cjs install --download。运行时可用后,使用同一入口 exec init ABS_PROJECT --apply 初始化修复任务,或 exec next ABS_PROJECT 获取指引。`; return event === 'stop' ? { systemMessage: message } : { hookSpecificOutput: { hookEventName: EVENTS.get(event), additionalContext: message } }; diff --git a/hooks/__protocol__.md b/hooks/__protocol__.md index 7572fac..f54e30a 100644 --- a/hooks/__protocol__.md +++ b/hooks/__protocol__.md @@ -1,9 +1,9 @@ # hooks/ 与宿主 CLI 之间的契约(canonical cheat-sheet) -> Rust 候选入口:`runtime/codeguard_runtime.cjs` 固定 npm 0.1.2 制品和本机二进制身份;`hooks/rust_runtime_dispatch.cjs` 显式处理 Claude Code 的 SessionStart/UserPromptSubmit/PostToolUse/PostToolUseFailure/Stop。UserPromptSubmit 只输出固定的非阻断检查时机建议,源码检查未运行、交付未评估;提示词不能触发 lint 或替代真实 Git 门禁。候选入口在运行时缺失、摘要或版本失配时返回对话中的“未完成”,不执行 PATH 上的 `codeguard`,也不回退到 Python。它尚未写入 `hooks/hooks.json`,以下旧协议表仍描述默认 Python 运行时。Rust 严格交付门禁与其它宿主接线尚未完成,不能把候选保存反馈当作通过。 +> 当前 canonical 默认:Node 18+ 宿主绑定固定 npm 0.1.4 Rust 程序,非阻断生命周期不回退 Python;安装显式、失败明确未完成。真实 Git PreToolUse 仍为 Python 兼容门禁,Copilot/OpenHands 镜像同步同一绑定,实际宿主另行验收。CLI/脚本重放不等于实际宿主验收,保存反馈退出 0 不代表质量通过。 > 这份文档是 codeguard-plugin 与 Codex CLI / ZCode / Kimi Code 三端宿主 -> 交互契约的**单源事实**。所有 hook 脚本(`hooks/*.py`)的实现都必须与本文件 +> 交互契约的**单源事实**。所有 Hook 脚本(Rust 绑定与 `hooks/*.py` 兼容入口)的实现都必须与本文件 > 一致;若未来调整任何退出码或 JSON schema,**同一 commit 内**必须更新本文件。 > > 对应的 OpenSpec 规范是 `hook-protocol`(见 `openspec/specs/hook-protocol/spec.md`)。 @@ -12,6 +12,25 @@ ## 1. 协议总表 +当前 `hooks/hooks.json` 的默认入口: + +| 事件 | 默认指令 | 输出与检查范围 | 宿主超时/退出 | +|---|---|---|---| +| SessionStart | `node hooks/rust_runtime_dispatch.cjs session-start` | 只读发现;未安装明确准备动作 | 10 秒 / 0 | +| UserPromptSubmit | `node hooks/rust_runtime_dispatch.cjs user-prompt-submit` | 固定时机指引;不运行 lint | 10 秒 / 0 | +| PostToolUse | `node hooks/rust_runtime_dispatch.cjs post-tool-use` | 成功编辑范围;原生优先、有界 WASM、稳定任务和复检指引 | 10 秒 / 0 | +| PostToolUseFailure | `node hooks/rust_runtime_dispatch.cjs post-tool-use-failure` | 明示未执行源码检查 | 10 秒 / 0 | +| Stop | `node hooks/rust_runtime_dispatch.cjs stop` | next 指引;重入不阻断 | 10 秒 / 0 | +| PreToolUse Bash | `python3 hooks/pre_tool_git_guard.py` | 既有真实 Git 内容及原生兼容门禁 | 120 秒 / 0 或 2 | + +上表指令在清单中使用绝对插件根。Rust 内部预算 5 秒、宿主子进程上限 8 秒、摘要限 1200 字符;事件失败、输入超限、范围异常、运行时缺失均给脱敏未完成摘要。stdout 使用 `hookSpecificOutput` 对应事件的 additionalContext;Stop 使用 systemMessage 或 Stop 事件输出。适配能力不足、未初始化和持久化失败保留未完成,不自批任务或签发交付许可。 + +修复指引通过同一固定程序执行:`node runtime/codeguard_runtime.cjs exec init ABS_PROJECT --apply`、`exec next ABS_PROJECT --format=json`、`exec task show TASK_ID ABS_PROJECT --format=json`、`exec task verify TASK_ID ABS_PROJECT --zig-tool ABS_ZIG --format=json`。可查询命令退出 0 仍不代表质量 allow;原生复检结果保持现有未完成语义。普通 Hook 不联网安装。 + +### 1.1 旧兼容入口协议 + +以下表格、Python 符号和异常约定描述保留的旧 CLI/MCP、显式 Python Hook 与显式旧 Python Hook;不描述上表当前 canonical Rust 生命周期。 + | 事件 | Hook 脚本 | stdout | stderr | exit | 阻断? | |---|---|---|---|---|---| | SessionStart | `env_check.py` | 人类可读一行摘要 `codeguard 插件环境:...` | 仅内部错误时 | 0 | 否 | @@ -20,7 +39,7 @@ | PostToolUse `Write\|Edit\|MultiEdit` | `post_tool_lint.py` | JSON `{"hookSpecificOutput":{"hookEventName":"PostToolUse","additionalContext":"..."}, "systemMessage":"..."}` | 仅内部错误时 | 0 | 否 | | Stop | `stop_summary.py` | 人类可读会话摘要 | 仅内部错误时 | 0 | 否 | -[Claude Code Hooks reference](https://code.claude.com/docs/en/hooks) 说明 `UserPromptSubmit` 不支持 matcher,宿主每次收到用户提示都会启动该 Hook。三份 `hooks.json` 因此不声明关键词 matcher;`user_prompt_validator.py::main` 先执行 `is_trigger`,未命中时在会话/worktree 归属、PATH 探测、仓库发现和原生门禁前退出且无对话输出。命中时旧软门禁仍可能运行;真实提交/推送只能由 PreToolUse 的命令时门禁判断。此变更不证明宿主进程启动开销已经消失或已实测。 +[Claude Code Hooks reference](https://code.claude.com/docs/en/hooks) 说明 `UserPromptSubmit` 不支持 matcher,宿主每次收到用户提示都会启动该 Hook。三份 `hooks.json` 均不声明关键词 matcher;当前默认不运行旧过滤逻辑,以下 Python 符号仅描述显式兼容入口;`user_prompt_validator.py::main` 先执行 `is_trigger`,未命中时在会话/worktree 归属、PATH 探测、仓库发现和原生门禁前退出且无对话输出。命中时旧软门禁仍可能运行;真实提交/推送只能由 PreToolUse 的命令时门禁判断。此变更不证明宿主进程启动开销已经消失或已实测。 实现定位(**符号而非行号**——行号随每次重构腐烂,符号不腐;v0.8.0 起弃用行号指针): - SessionStart 摘要:`hooks/env_check.py::main`(含双副本告警段)。 @@ -54,7 +73,7 @@ CLI/MCP 明确返回 UNVERIFIED,自动修复不执行。门禁以未验证说 | ZCode | 同 Codex | 同 Codex,additionalContext 进 systemMessage | 同 Codex | 同 Codex | 同 Codex | | Kimi Code CLI | stdout 摘要注入会话上下文 | stdout JSON 注入上下文(观察型,**不阻断用户消息**) | exit 2 拦截 + stderr 指令;exit 0 静默 | stdout JSON 注入上下文 | stdout 摘要 | -所有三端在所有事件上,**内部错误都走 fail-open**(见 §3),hook bug 永远不阻断工作流。 +旧兼容三端在所有事件上,**内部错误都走 fail-open**(见 §3),hook bug 永远不阻断工作流。 --- diff --git a/hooks/hooks.json b/hooks/hooks.json index 62dda55..2fd3d4a 100644 --- a/hooks/hooks.json +++ b/hooks/hooks.json @@ -6,8 +6,8 @@ "hooks": [ { "type": "command", - "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/env_check.py\"", - "timeout": 30, + "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/rust_runtime_dispatch.cjs\" session-start", + "timeout": 10, "statusMessage": "codeguard: 检测项目语言与现有 linter 配置…" } ] @@ -18,8 +18,8 @@ "hooks": [ { "type": "command", - "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/user_prompt_validator.py\"", - "timeout": 120 + "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/rust_runtime_dispatch.cjs\" user-prompt-submit", + "timeout": 10 } ] } @@ -42,8 +42,8 @@ "hooks": [ { "type": "command", - "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/post_tool_lint.py\"", - "timeout": 180 + "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/rust_runtime_dispatch.cjs\" post-tool-use", + "timeout": 10 } ] } @@ -53,8 +53,20 @@ "hooks": [ { "type": "command", - "command": "python3 \"${CLAUDE_PLUGIN_ROOT}/hooks/stop_summary.py\"", - "timeout": 15 + "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/rust_runtime_dispatch.cjs\" stop", + "timeout": 10 + } + ] + } + ], + "PostToolUseFailure": [ + { + "matcher": "Write|Edit|MultiEdit", + "hooks": [ + { + "type": "command", + "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/rust_runtime_dispatch.cjs\" post-tool-use-failure", + "timeout": 10 } ] } diff --git a/hooks/rust_runtime_dispatch.cjs b/hooks/rust_runtime_dispatch.cjs index 992e7a2..33de11d 100644 --- a/hooks/rust_runtime_dispatch.cjs +++ b/hooks/rust_runtime_dispatch.cjs @@ -1,7 +1,7 @@ #!/usr/bin/env node 'use strict'; -// Candidate Claude Code binding. It never resolves `codeguard` from PATH or falls back to Python. +// Canonical non-blocking Claude-shape lifecycle binding. It never resolves `codeguard` from PATH or falls back to Python. const fs = require('node:fs'); const path = require('node:path'); const { spawnSync } = require('node:child_process'); @@ -19,7 +19,7 @@ const EVENTS = new Map([ function incomplete(event, reason) { const safe = /^[a-z_]+$/.test(reason) ? reason : 'runtime_unavailable'; - const message = `CodeGuard Rust 运行时未完成(${safe});本次源码检查未运行,交付未评估。请执行插件 runtime/codeguard_runtime.cjs verify 或修复安装。`; + const message = `CodeGuard Rust 运行时未完成(${safe});本次源码检查未运行,交付未评估。请用 Node 18+ 执行 node runtime/codeguard_runtime.cjs verify;支持的 macOS arm64 上显式运行 node runtime/codeguard_runtime.cjs install --download。运行时可用后,使用同一入口 exec init ABS_PROJECT --apply 初始化修复任务,或 exec next ABS_PROJECT 获取指引。`; return event === 'stop' ? { systemMessage: message } : { hookSpecificOutput: { hookEventName: EVENTS.get(event), additionalContext: message } }; diff --git a/kimi.plugin.json b/kimi.plugin.json index f52ee40..7ffac32 100644 --- a/kimi.plugin.json +++ b/kimi.plugin.json @@ -1,6 +1,6 @@ { "name": "codeguard", - "version": "0.19.0", + "version": "0.20.0", "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/openspec/changes/2026-10-04-rust-lifecycle-default/design.md b/openspec/changes/2026-10-04-rust-lifecycle-default/design.md new file mode 100644 index 0000000..e6b35c9 --- /dev/null +++ b/openspec/changes/2026-10-04-rust-lifecycle-default/design.md @@ -0,0 +1,25 @@ +# Default lifecycle binding + +该变更延续 CLI `introduce-rust-codeguard-cli` 的 S11.17/S14 规格,不建立第二份 CLI 规格。`2026-10-03-rust-wasm-runtime-candidate` 仍记录 0.1.3 显式候选历史。 + +```mermaid +flowchart LR + A[默认生命周期事件] --> B[Node 宿主绑定] + B --> C{固定 Rust 0.1.4 可核验?} + C -->|否| D[未完成与显式安装指引] + C -->|是| E[Rust 校验事件与范围] + E --> F{事件} + F -->|保存成功| G[最多 8 文件 原生优先 有界 WASM] + F -->|失败保存| H[不扫描源码] + F -->|会话| I[只读发现] + F -->|提示或结束| J[检查时机或 next 指引] + G --> K[稳定任务与有界对话摘要] + H --> K + I --> K + J --> K + L[实际 Git 命令] --> M[现有兼容 PreToolUse 门禁] +``` + +普通生命周期退出 0 是宿主反馈,不是质量通过。运行时须显式安装到内容寻址缓存;每次调用复核程序与许可证摘要。5 秒内部原生/WASM 预算、8 秒子进程、10 秒宿主预算;文件系统 I/O 全硬截止尚未验收,超时不签发 clean。任务必须通过 CLI 原生确认与可信政策关闭;插件不读取项目自选密钥或自批准名单。真实宿主安装、逐语言精度、所有原生工具自动选择、跨平台、Git/MCP Rust 迁移保持开放。 + +Canonical `hooks/hooks.json` 与分发镜像保持一致,实际宿主验收分开登记,不能因 Claude 形状事件重放就勾选 Codex/ZCode/Kimi/Claude 自动触发验收。原先 auto_fix_on_save 仅保留兼容配置,Rust 事件不静默应用修复。 diff --git a/openspec/changes/2026-10-04-rust-lifecycle-default/proposal.md b/openspec/changes/2026-10-04-rust-lifecycle-default/proposal.md new file mode 100644 index 0000000..291c94f --- /dev/null +++ b/openspec/changes/2026-10-04-rust-lifecycle-default/proposal.md @@ -0,0 +1,19 @@ +# Why + +已发布 CLI 0.1.4 含编辑检查、稳定任务和原生复检,但插件默认生命周期仍调用 Python,用户无法自动得到该修复指引。 + +# What Changes + +- 将 canonical Claude 形状 SessionStart、UserPromptSubmit、PostToolUse、PostToolUseFailure、Stop 接到受固定摘要保护的 Rust 0.1.4;保留真实 Git PreToolUse 兼容门禁。 +- 显式安装,普通 Hook 不下载、不安装、不自动改源码或关闭任务;缺运行时给可执行准备指引。 +- 安装器公开 init、next、task show/verify 的受限命令转发,使对话指引可通过同一固定程序执行。 +- 同步协议、双语文档、制品证据和插件/市场版本;已安装宿主与全部语言精度独立验收。 + +# Capabilities + +## Modified Capabilities +- `hook-protocol`: Rust 非阻断生命周期默认入口和修复指引。 + +# Impact + +仅插件宿主绑定与固定制品锁改变;检查逻辑仍由 Rust 与生态原生工具承担。Copilot/OpenHands 分发镜像保持字节一致,Python Git 门禁、MCP 兼容服务保留独立边界,不冒充 Rust 完整迁移。原生未接线、未初始化与运行时未安装均明确未完成。 diff --git a/openspec/changes/2026-10-04-rust-lifecycle-default/specs/hook-protocol/spec.md b/openspec/changes/2026-10-04-rust-lifecycle-default/specs/hook-protocol/spec.md new file mode 100644 index 0000000..8b9a5f0 --- /dev/null +++ b/openspec/changes/2026-10-04-rust-lifecycle-default/specs/hook-protocol/spec.md @@ -0,0 +1,27 @@ +## ADDED Requirements + +### Requirement: Canonical non-blocking lifecycle hooks SHALL use the pinned Rust runtime + +Canonical `hooks/hooks.json` SHALL route SessionStart, UserPromptSubmit, successful and failed file edits, and Stop to the verified `@partme.ai/codeguard@0.1.4` binary through a thin Node host binding. Native lint SHALL precede bounded WASM where the existing adapter supports it. Ordinary events MUST NOT download tools, apply source fixes, self-approve tasks, or claim complete delivery. The existing Git PreToolUse gate SHALL remain active until its Rust replacement is accepted. + +#### Scenario: Edited file produces a suspected syntax observation +- **WHEN** the locked runtime is installed, the workspace initialized and a confirmed edited file has recovery nodes +- **THEN** the default entry returns a bounded summary with a real stable native-confirmation task and recheck guidance +- **AND** repeated events update the same task and later WASM clean does not close it + +#### Scenario: Runtime or platform unavailable +- **WHEN** the pinned runtime cannot be verified or the platform is unsupported +- **THEN** the ordinary Hook exits 0 with explicit incomplete and setup guidance, without executing PATH codeguard, falling back to Python or downloading a package + +#### Scenario: Prompt, failed edit or Stop reentry +- **WHEN** a prompt mentions commit, an edit fails or Stop is reentered +- **THEN** it does not run project-wide lint, infer approval from prompt text, scan failed edit source or repeatedly block Stop + +### Requirement: Plugin repair instructions SHALL be executable through the same pinned runtime + +The runtime manager SHALL permit explicit init, next, task show and task verify requests with an absolute project root, retaining Rust argument validation and ordinary incomplete semantics. It MUST NOT add local approval flags or allow arbitrary command execution. + +#### Scenario: Agent follows a native-confirmation task +- **WHEN** the agent calls task show or task verify via the manager with the reported ID and project root +- **THEN** the same locked program returns existing task evidence and native recheck feedback +- **AND** native zero diagnostics without trusted policy does not turn the task into resolved or grant delivery allow diff --git a/openspec/changes/2026-10-04-rust-lifecycle-default/tasks.md b/openspec/changes/2026-10-04-rust-lifecycle-default/tasks.md new file mode 100644 index 0000000..0f5a57a --- /dev/null +++ b/openspec/changes/2026-10-04-rust-lifecycle-default/tasks.md @@ -0,0 +1,19 @@ +## 1. 固定制品与可执行指引 + +- [x] 1.1 先验证旧锁拒绝 0.1.4,再固定注册表 tarball、程序、源码与许可证身份;原生版本不可从 PATH 替换。 +- [x] 1.2 增加受限 init/next/task show/task verify 转发;未支持命令不运行,参数由 Rust 校验。 + +## 2. 默认生命周期接线 + +- [x] 2.1 默认 canonical Hook 接通五类非阻断事件、保留原 Git 兼容门禁;缺运行时明确未完成且不下载/回退。 +- [x] 2.2 真实固定公开程序经 manifest 入口完成 init、编辑、重复任务、失败编辑、next、原生复检、Stop 重入;摘要脱敏有界,问题仍需可信复检关闭。 +- [x] 2.3 测量默认入口冷/重复调用时间,核对子进程及宿主预算;记录无法核验的硬 I/O 与真实宿主范围。 +- [ ] 2.4 用实际安装 Claude/Codex/ZCode/Kimi 等宿主验证自动触发、对话可见、权限及失败恢复;直接脚本重放不能代替。 + +## 3. 交付 + +- [x] 3.1 更新双语 README、canonical 协议和验收记录,区分当前默认入口与兼容宿主镜像。 +- [ ] 3.2 执行 Node/Python 回归、vendor 离线/在线、OpenSpec strict、manifest/README/架构检查与远端 CI。 +- [ ] 3.3 经生成器版本升级,发布 tag/release、同步市场并核对本地/远端身份;受保护分支限制真实反馈。 + +本机证据:`tests/rust-lifecycle-default.md`。2.4 真实宿主、3.2 远端 CI、3.3 发布/市场保留开放。 diff --git a/plugin.json b/plugin.json index b54183b..ef73baa 100644 --- a/plugin.json +++ b/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json", "name": "codeguard", - "version": "0.19.0", + "version": "0.20.0", "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/runtime/codeguard.lock.json b/runtime/codeguard.lock.json index f0ff074..3a0d65f 100644 --- a/runtime/codeguard.lock.json +++ b/runtime/codeguard.lock.json @@ -1,14 +1,14 @@ { "schema_version": "1.0.0", "package": "@partme.ai/codeguard", - "version": "0.1.3", + "version": "0.1.4", "platform": "macos_arm64", - "source_commit": "7900a1a8ccd5b4f5b54de8b5db3ccba16a356d3b", + "source_commit": "1cd458f6e01a44a74388243e964e3f45290ac18e", "check_protocol_major": 1, - "tarball_url": "https://registry.npmjs.org/@partme.ai/codeguard/-/codeguard-0.1.3.tgz", - "tarball_sha256": "37509cdd108fee631eebb9ebcd794a6e8d63652801b3522625d0909c3da2ae12", - "tarball_integrity": "sha512-RdhW/Tvf6a+7ZhRQFzSCJL87olzKmCjzQfyiqPZ3djTOodbdP7YfchT9VOFq/d7i+g95AME1mQC4HilHdb6WXQ==", - "binary_sha256": "04bdea8c489693a8e97b451df8d0c2b7a89ba82372db5a4d1fed277ba19b0f4d", + "tarball_url": "https://registry.npmjs.org/@partme.ai/codeguard/-/codeguard-0.1.4.tgz", + "tarball_sha256": "a0d605f4a102e78d42766afaef61c6bd002556ccc5d16881c7553a91d6e20c80", + "tarball_integrity": "sha512-99VQcM0JtasdqNW3gYoyMqbBUOQ1MXQJrncAIfnwz1UZOmyUbwlKKEcGsfi5ytlSl6JlgtehpX1+LAGPazpzAw==", + "binary_sha256": "34c2490ff4ccaa04bce9e91efd6ccabff6d679e97b08966b1edba9b6aec37266", "grammar_licenses": { "arkts/LICENSE": "048e4dcb7a71fb725495ebb3e7052d0e76dcb6705b828af8b9b461df10bcd8ca", "c/LICENSE": "2e0110e07abef7c2548b26ec9d6969775617ca539a0dc8dbeeb14d6452c711d1", diff --git a/runtime/codeguard_runtime.cjs b/runtime/codeguard_runtime.cjs index 40fb834..7ab42d5 100644 --- a/runtime/codeguard_runtime.cjs +++ b/runtime/codeguard_runtime.cjs @@ -38,10 +38,10 @@ function readLock() { const lock = JSON.parse(fs.readFileSync(LOCK_PATH, 'utf8')); const hex = (value, length) => typeof value === 'string' && new RegExp(`^[0-9a-f]{${length}}$`).test(value); if (lock.schema_version !== '1.0.0' || lock.package !== '@partme.ai/codeguard' - || lock.version !== '0.1.3' || lock.platform !== 'macos_arm64' + || lock.version !== '0.1.4' || lock.platform !== 'macos_arm64' || lock.check_protocol_major !== 1 || !hex(lock.source_commit, 40) || !hex(lock.tarball_sha256, 64) || !hex(lock.binary_sha256, 64) - || lock.tarball_url !== 'https://registry.npmjs.org/@partme.ai/codeguard/-/codeguard-0.1.3.tgz' + || lock.tarball_url !== 'https://registry.npmjs.org/@partme.ai/codeguard/-/codeguard-0.1.4.tgz' || typeof lock.tarball_integrity !== 'string' || !/^sha512-[A-Za-z0-9+/]{86}==$/.test(lock.tarball_integrity) || !lock.grammar_licenses || typeof lock.grammar_licenses !== 'object' @@ -236,11 +236,16 @@ if (require.main === module) { } else if (action === 'exec' && ((process.argv[3] === 'grammar' && ['status', 'probe'].includes(process.argv[4])) || (process.argv[3] === 'check' && process.argv[4] === 'all' - && path.isAbsolute(process.argv[5] || '')))) { + && path.isAbsolute(process.argv[5] || '')) + || (['init', 'next'].includes(process.argv[3]) + && path.isAbsolute(process.argv[4] || '')) + || (process.argv[3] === 'task' && ['show', 'verify'].includes(process.argv[4]) + && /^CG-[A-Z]-[0-9a-f]{32}$/.test(process.argv[5] || '') + && path.isAbsolute(process.argv[6] || '')))) { const args = process.argv.slice(3); const result = spawnSync(activeBinary(), args, { encoding: 'utf8', shell: false, - timeout: args[0] === 'check' ? 120000 : 30000, + timeout: ['check', 'task'].includes(args[0]) ? 120000 : 30000, maxBuffer: args[0] === 'check' ? 4 * 1024 * 1024 : 1024 * 1024, env: { ...process.env, CODEGUARD_SKIP_GATE: '' }, }); @@ -249,7 +254,7 @@ if (require.main === module) { process.stderr.write(result.stderr); process.exitCode = result.status; } else { - fail('usage: verify | install --download | install --tarball ABS_PATH | exec grammar status|probe ... | exec check all ABS_PROJECT ...'); + fail('usage: verify | install --download | install --tarball ABS_PATH | exec grammar status|probe ... | exec check all ABS_PROJECT ... | exec init|next ABS_PROJECT ... | exec task show|verify TASK_ID ABS_PROJECT ...'); } })().catch((error) => { process.stderr.write(`${error.message}\n`); diff --git a/tests/rust-lifecycle-default.md b/tests/rust-lifecycle-default.md new file mode 100644 index 0000000..8c237eb --- /dev/null +++ b/tests/rust-lifecycle-default.md @@ -0,0 +1,26 @@ +# Rust 0.1.4 默认生命周期接线验收 + +对应 `2026-10-04-rust-lifecycle-default`;源码与清单接线不等于已安装宿主自动触发。 + +锁定 CLI 来源 `1cd458f6e01a44a74388243e964e3f45290ac18e`,tarball SHA-256 `a0d605f4a102e78d42766afaef61c6bd002556ccc5d16881c7553a91d6e20c80`,程序 SHA-256 `34c2490ff4ccaa04bce9e91efd6ccabff6d679e97b08966b1edba9b6aec37266`。38 普通成员与 32 许可证身份保持精确核对;程序与许可证每次调用复核。 + +先 RED:旧锁拒绝真实 0.1.4 注册表 tarball,默认 canonical Hook 未接 Rust;回归记录保留。GREEN 与注册表安装、延迟、全回归、远端 CI 和发行证据完成后在下方追加,未运行项不勾选。 + +当前默认 canonical SessionStart/UserPromptSubmit/PostToolUse/PostToolUseFailure/Stop 调用 Rust。Git PreToolUse、旧 CLI/MCP 与显式旧 Python Hook保留兼容入口。5 秒内部预算、8 秒子进程、10 秒 Hook 预算;全硬 I/O、缓存/性能和真实宿主仍待验收。未安装运行时显式反馈未完成,不自动下载或回退 Python。原生 Ruff/ESLint 的已实现范围优先,其它语种候选任务保留原生 adapter 缺口;32 grammar 仍未验收。 + +## 本机实际结果(2026-10-04) + +- 固定注册表 0.1.4 tarball 的 8 项 Node 回归全部通过、0 failed、0 skipped:默认 manifest 选择、五类缺运行时反馈、真实公开包安装、真实保存/重复稳定任务、可执行 init/next/show/verify、真实 Zig 0.16.0 原生复检、Stop 重入、错误包及活动许可证/程序篡改拒绝。 +- 实际默认入口读取 canonical manifest 的命令运行,不用单独候选入口替代默认接线;每次输出限 1200 字符,不回显宿主源码。init 返回 3/partial、next/show 返回操作成功 0,均不解释为质量通过。无可信政策时原生零诊断保留 open。 +- 注册表 HTTPS 另在独立缓存安装并 activeBinary 核验成功,约 4068 ms;未写入用户的全局运行时缓存。普通生命周期没有下载。 +- 三次独立 PostToolUse 本机重放耗时 406.7/337.8/337.0 ms;最终日志的更新测量见下列日志。此为小 Zig 文件、当前机器、直接默认入口重放,不是冷启动分布或实际宿主 p95。 +- Python 兼容 unittest:655 passed、0 failed、0 skipped(使用已有 Framework Python 3.13,具备 Ruff/MCP);插件集成:144 passed、0 failed、0 skipped;README 对齐 7/7。默认 Homebrew Python 3.14 缺 Ruff/MCP 的首次运行 1 failed/4 skipped,已保留该环境失败,不计成功。 +- 技能 vendor 离线/在线检查、架构、Ruff、57 语言/11 schema 规则、便携插件与两份 Hook 镜像、两项 OpenSpec strict、diff 检查通过。未修改外部受管技能。 +- 版本经生成器从已合并的未发布候选升级到插件 0.20.0,各平台 manifest 与市场 CodeGuard 元数据同步。远端 CI、GitHub release/tag、市场推送与已安装宿主证据须按后续真实结果补充。 + +## 日志身份 + +- `/tmp/codeguard-plugin-014-lock-red.log`:SHA-256 `116108f52fa9f47329e73941d9507869241764be9211c4d6058aa68088052f3c`。 +- `/tmp/codeguard-plugin-default-green-final.log`:SHA-256 `8520d1e6a301bfc63d2ebd7682618ed77221711f3708c41d74633c7868f1a05e`。 +- `/tmp/codeguard-plugin-lifecycle-unittest-final.log`:SHA-256 `5bd4a7de4e2075b1d92c14b472a55e3cb33db4176ee5eea02d0d6dd684472e55`。 +- `/tmp/codeguard-plugin-lifecycle-e2e.log`:SHA-256 `920e7950d58d9f2aa4963429e4e0d56c188e15a657fb16d0d7d36505c12f53fc`。 diff --git a/tests/test_rust_lifecycle.cjs b/tests/test_rust_lifecycle.cjs new file mode 100644 index 0000000..21b9bc6 --- /dev/null +++ b/tests/test_rust_lifecycle.cjs @@ -0,0 +1,136 @@ +'use strict'; + +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const os = require('node:os'); +const path = require('node:path'); +const { spawnSync } = require('node:child_process'); +const { performance } = require('node:perf_hooks'); +const test = require('node:test'); +const runtime = require('../runtime/codeguard_runtime.cjs'); +const plugin = path.resolve(__dirname, '..'); +const manager = path.join(plugin, 'runtime/codeguard_runtime.cjs'); +const hooks = JSON.parse(fs.readFileSync(path.join(plugin, 'hooks/hooks.json'), 'utf8')).hooks; +const routes = { SessionStart: 'session-start', UserPromptSubmit: 'user-prompt-submit', + PostToolUse: 'post-tool-use', PostToolUseFailure: 'post-tool-use-failure', Stop: 'stop' }; + +function invoke(script, args, env, input = '') { + return spawnSync(process.execPath, [script, ...args], { cwd: plugin, + env: { ...process.env, ...env }, input, encoding: 'utf8', + timeout: 15000, maxBuffer: 1024 * 1024 }); +} +function defaultCall(event, env, payload) { + const entry = hooks[event][0].hooks[0]; + const match = /^node "\$\{CLAUDE_PLUGIN_ROOT\}\/hooks\/rust_runtime_dispatch.cjs" ([a-z-]+)$/.exec(entry.command); + assert.ok(match, `default ${event} does not route to the Rust dispatcher`); + assert.equal(match[1], routes[event]); + const start = performance.now(); + const result = invoke(path.join(plugin, 'hooks/rust_runtime_dispatch.cjs'), [match[1]], env, + JSON.stringify(payload)); + assert.equal(result.status, 0, result.stderr); + const value = JSON.parse(result.stdout); + const context = value.hookSpecificOutput?.additionalContext ?? value.systemMessage; + assert.equal(typeof context, 'string'); + assert.ok([...context].length <= 1200); + assert.doesNotMatch(context, /RAW_HOST_INPUT_PRIVATE/); + return { value, context, elapsed_ms: performance.now() - start }; +} + +test('canonical lifecycle selects pinned 0.1.4 and bounded Rust defaults while retaining Git gate', () => { + assert.equal(runtime.readLock().version, '0.1.4'); + for (const [event, route] of Object.entries(routes)) { + assert.equal(hooks[event][0].hooks[0].command, + `node "\${CLAUDE_PLUGIN_ROOT}/hooks/rust_runtime_dispatch.cjs" ${route}`); + assert.ok(hooks[event][0].hooks[0].timeout >= 10); + assert.ok(hooks[event][0].hooks[0].timeout <= 15); + } + assert.equal(hooks.UserPromptSubmit[0].matcher, undefined); + assert.match(hooks.PreToolUse[0].hooks[0].command, /pre_tool_git_guard\.py/); +}); + +test('missing runtime is visible on every default lifecycle and does not install or use PATH checkers', (t) => { + const root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-default-missing-')); + t.after(() => fs.rmSync(root, { recursive: true, force: true })); + const marker = path.join(root, 'checker-executed'); + for (const name of ['codeguard', 'python3']) { + const file = path.join(root, name); + fs.writeFileSync(file, `#!/bin/sh\ntouch '${marker}'\n`); fs.chmodSync(file, 0o755); + } + const env = { CODEGUARD_RUNTIME_CACHE: path.join(root, 'cache'), CLAUDE_PROJECT_DIR: root, + PATH: `${root}${path.delimiter}${process.env.PATH}` }; + for (const event of Object.keys(routes)) { + const result = defaultCall(event, env, { hook_event_name: event, cwd: root, + source: 'startup', prompt: 'RAW_HOST_INPUT_PRIVATE', stop_hook_active: false }); + assert.match(result.context, /未完成/); + assert.match(result.context, /未运行/); + assert.match(result.context, /install/); + } + assert.equal(fs.existsSync(marker), false); + assert.equal(fs.existsSync(path.join(root, 'cache/active.json')), false); +}); + +const tarball = process.env.CODEGUARD_TEST_TARBALL; +test('actual locked public package drives default editing, stable tasks and executable recheck guidance', { + skip: !tarball || process.platform !== 'darwin' || process.arch !== 'arm64', +}, (t) => { + const temporary = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-default-installed-')); + t.after(() => fs.rmSync(temporary, { recursive: true, force: true })); + const project = path.join(temporary, 'project'); fs.mkdirSync(project); + const root = fs.realpathSync(project); + const env = { CODEGUARD_RUNTIME_CACHE: path.join(temporary, 'cache'), CLAUDE_PROJECT_DIR: root }; + const installed = invoke(manager, ['install', '--tarball', tarball], env); + assert.equal(installed.status, 0, installed.stderr); + const init = invoke(manager, ['exec', 'init', root, '--apply', '--format=json'], env); + assert.equal(init.status, 3, init.stderr); + assert.equal(JSON.parse(init.stdout).report_type, "init_plan"); + const started = defaultCall('SessionStart', env, { hook_event_name: 'SessionStart', cwd: root, source: 'startup' }); + assert.match(started.context, /只读发现/); + const file = path.join(root, 'bad.zig'); fs.writeFileSync(file, 'const broken = ;\n'); + const payload = { hook_event_name: 'PostToolUse', cwd: root, tool_name: 'Write', + tool_input: { file_path: file, content: 'RAW_HOST_INPUT_PRIVATE' }, + tool_response: { filePath: file, type: 'create' } }; + const timings = []; + for (let i = 0; i < 3; i++) { + const result = defaultCall('PostToolUse', env, payload); timings.push(result.elapsed_ms); + assert.match(result.context, /必须/); + assert.match(result.context, /CG-B-[0-9a-f]{32}/); + assert.match(result.context, /task show/); + } + const next = invoke(manager, ['exec', 'next', root, '--format=json'], env); + assert.equal(next.status, 0, next.stderr); + const brief = JSON.parse(next.stdout); + const id = brief.repair_brief.task_id; + assert.match(id, /^CG-B-[0-9a-f]{32}$/); + const shown = invoke(manager, ['exec', 'task', 'show', id, root, '--format=json'], env); + assert.equal(shown.status, 0, shown.stderr); + assert.equal(JSON.parse(shown.stdout).task_id, id); + const taskCount = fs.readdirSync(path.join(root, '.codeguard/tasks')).filter(n => n.endsWith('.md')).length; + assert.equal(taskCount, 1); + const failed = defaultCall('PostToolUseFailure', env, { ...payload, + hook_event_name: 'PostToolUseFailure', error: 'RAW_HOST_INPUT_PRIVATE' }); + assert.match(failed.context, /未运行/); + for (const [event, extra] of [['UserPromptSubmit', { prompt: 'commit RAW_HOST_INPUT_PRIVATE' }], + ['Stop', { stop_hook_active: false }], ['Stop', { stop_hook_active: true }]]) { + defaultCall(event, env, { hook_event_name: event, cwd: root, ...extra }); + } + assert.equal(fs.readdirSync(path.join(root, '.codeguard/tasks')).filter(n => n.endsWith('.md')).length, 1); + const unsupported = invoke(manager, ['exec', 'fix', root], env); + assert.equal(unsupported.status, 3); + assert.match(unsupported.stderr, /usage/); + const zig = '/opt/homebrew/bin/zig'; + if (fs.existsSync(zig)) { + const result = invoke(manager, ['exec', 'task', 'verify', id, root, '--zig-tool', zig, + '--timeout', '5s', '--format=json'], env); + assert.equal(result.status, 3, result.stderr); + assert.equal(JSON.parse(result.stdout).observation, 'still_blocked'); + fs.writeFileSync(file, 'const Fixed = struct {};\n'); + const fixed = invoke(manager, ['exec', 'task', 'verify', id, root, '--zig-tool', zig, + '--timeout', '5s', '--format=json'], env); + assert.equal(fixed.status, 3, fixed.stderr); + const observed = JSON.parse(fixed.stdout); + assert.equal(observed.observation, 'candidate_absent_unverified_policy'); + assert.equal(JSON.parse(fs.readFileSync(path.join(root, '.codeguard/findings', id, 'finding.json'))).state, 'open'); + } + t.diagnostic(JSON.stringify({ default_post_tool_ms: timings, task_id_stable: true, + source: runtime.readLock().source_commit, native_zig_rechecked: fs.existsSync(zig), installed_host_accepted: false })); +}); diff --git a/tests/test_rust_runtime.cjs b/tests/test_rust_runtime.cjs index 6dcaed5..5b1669f 100644 --- a/tests/test_rust_runtime.cjs +++ b/tests/test_rust_runtime.cjs @@ -25,7 +25,7 @@ function invoke(script, args, env, input = '') { test('the plugin lock pins a single published runtime', () => { const lock = runtime.readLock(); - assert.equal(lock.version, '0.1.3'); + assert.equal(lock.version, '0.1.4'); assert.equal(lock.platform, 'macos_arm64'); assert.equal(lock.check_protocol_major, 1); assert.equal(Object.keys(lock.grammar_licenses).length, 32); From 79af622b339c7957a06c49862bb1aab2663c72a0 Mon Sep 17 00:00:00 2001 From: loong10k <20489781+loong10k@users.noreply.github.com> Date: Wed, 7 Oct 2026 15:02:25 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E8=BF=90=E8=A1=8C?= =?UTF-8?q?=E6=97=B6=E6=81=A2=E5=A4=8D=E6=8C=87=E5=BC=95=E4=B8=8D=E5=8F=AF?= =?UTF-8?q?=E6=89=A7=E8=A1=8C=E4=B8=8E=E6=8D=9F=E5=9D=8F=E7=BC=93=E5=AD=98?= =?UTF-8?q?=E6=97=A0=E6=B3=95=E8=87=AA=E6=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit review 指出两个真实缺陷: 1) hooks 在用户 workspace 运行,而管理器位于已安装插件根目录,指引却给相对 路径 `node runtime/codeguard_runtime.cjs`,照抄必然 MODULE_NOT_FOUND, 新装用户会被永久卡在未完成。改为用已计算的绝对 pluginRoot 拼接。 2) 内容寻址的缓存目录存在但核验失败时(grammar 许可被改动或删除), 重装对同一个损坏目录反复验证,用户无从判断该清理哪里。 binary-distribution 规范要求「已存在但内容不同的目录 MUST 保留且拒绝, 不得替换」,因此不静默重建覆盖;而是在拒绝时把确切的缓存目录写进原因, 让用户据此移除后重试。新增 packageIntact 作非失败式复用判断, 最终仍由 verifyPackage 复核。 本仓库测试 655 passed;node --check 通过。 --- hooks/rust_runtime_dispatch.cjs | 5 ++++- runtime/codeguard_runtime.cjs | 12 ++++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/hooks/rust_runtime_dispatch.cjs b/hooks/rust_runtime_dispatch.cjs index 33de11d..b8621b5 100644 --- a/hooks/rust_runtime_dispatch.cjs +++ b/hooks/rust_runtime_dispatch.cjs @@ -19,7 +19,10 @@ const EVENTS = new Map([ function incomplete(event, reason) { const safe = /^[a-z_]+$/.test(reason) ? reason : 'runtime_unavailable'; - const message = `CodeGuard Rust 运行时未完成(${safe});本次源码检查未运行,交付未评估。请用 Node 18+ 执行 node runtime/codeguard_runtime.cjs verify;支持的 macOS arm64 上显式运行 node runtime/codeguard_runtime.cjs install --download。运行时可用后,使用同一入口 exec init ABS_PROJECT --apply 初始化修复任务,或 exec next ABS_PROJECT 获取指引。`; + // hooks 在用户 workspace 里运行,而管理器位于已安装插件根目录;相对路径会让照抄的用户 + // 得到 MODULE_NOT_FOUND,生命周期就永久卡在未完成。必须给出可直接执行的绝对路径。 + const manager = path.join(pluginRoot, 'runtime', 'codeguard_runtime.cjs'); + const message = `CodeGuard Rust 运行时未完成(${safe});本次源码检查未运行,交付未评估。请用 Node 18+ 执行 node ${manager} verify;支持的 macOS arm64 上显式运行 node ${manager} install --download。运行时可用后,使用同一入口 exec init ABS_PROJECT --apply 初始化修复任务,或 exec next ABS_PROJECT 获取指引。`; return event === 'stop' ? { systemMessage: message } : { hookSpecificOutput: { hookEventName: EVENTS.get(event), additionalContext: message } }; diff --git a/runtime/codeguard_runtime.cjs b/runtime/codeguard_runtime.cjs index 7ab42d5..c208caf 100644 --- a/runtime/codeguard_runtime.cjs +++ b/runtime/codeguard_runtime.cjs @@ -125,6 +125,12 @@ function verifyTarball(file, lock) { } } +// 非失败式核验:只回答「这个已安装目录是否仍然完好」,供重装路径判断能否复用。 +// 不吞掉真实结论——调用方仍会用 verifyPackage 做最终复核。 +function packageIntact(dir, lock) { + try { verifyPackage(dir, lock); return true; } catch { return false; } +} + function installTarball(file, root = cacheRoot()) { const lock = readLock(); const members = expectedMembers(lock); @@ -137,6 +143,12 @@ function installTarball(file, root = cacheRoot()) { try { leaseFd = fs.openSync(lease, 'wx', 0o600); } catch { fail('installation_busy'); } let staging; try { + // 内容寻址目录存在但核验失败时(例如 grammar 许可被改动或删除),binary-distribution + // 规范要求「已存在但内容不同的目录 MUST 保留且拒绝,不得替换」——不能静默重建覆盖。 + // 但也不能只留一个无法自查的失败:把确切的缓存目录写进原因,用户据此移除后重试。 + if (fs.existsSync(final) && !packageIntact(final, lock)) { + fail(`cached_package_corrupt_remove_then_reinstall (remove: ${final})`); + } if (!fs.existsSync(final)) { staging = fs.mkdtempSync(path.join(root, '.stage-')); fs.chmodSync(staging, 0o700); From 0130198b73343f7cc418d91bd4d6c5dcec8ab73c Mon Sep 17 00:00:00 2001 From: loong10k <20489781+loong10k@users.noreply.github.com> Date: Wed, 7 Oct 2026 15:13:25 +0800 Subject: [PATCH 4/4] =?UTF-8?q?=E5=90=8C=E6=AD=A5=E9=95=9C=E5=83=8F?= =?UTF-8?q?=E5=89=AF=E6=9C=AC=E5=B9=B6=E8=BD=AC=E4=B9=89=E5=90=AB=E7=A9=BA?= =?UTF-8?q?=E6=A0=BC=E7=9A=84=E6=81=A2=E5=A4=8D=E5=91=BD=E4=BB=A4=E8=B7=AF?= =?UTF-8?q?=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一提交只改了 canonical hook,两个分发镜像仍是旧的相对路径,导致 validate_portable_plugin.py 的 mirror-drift 校验失败;现三副本一致。 同时转义恢复命令中的绝对路径:插件路径可能含空格(macOS 的 Application Support)或单引号,未转义会被 shell 拆成多个参数,照抄即失败。 按 POSIX 规则单引号包裹,内部单引号转义;已实测普通路径、含空格路径、 含单引号路径三种情况经 sh 还原均与原值一致。 mirror-drift 校验 PASS;655 项测试通过;三副本 node --check 通过。 --- com.github.copilot/hooks/rust_runtime_dispatch.cjs | 11 ++++++++++- dev.openhands/hooks/rust_runtime_dispatch.cjs | 11 ++++++++++- hooks/rust_runtime_dispatch.cjs | 8 +++++++- 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/com.github.copilot/hooks/rust_runtime_dispatch.cjs b/com.github.copilot/hooks/rust_runtime_dispatch.cjs index 33de11d..10f78a5 100644 --- a/com.github.copilot/hooks/rust_runtime_dispatch.cjs +++ b/com.github.copilot/hooks/rust_runtime_dispatch.cjs @@ -17,9 +17,18 @@ const EVENTS = new Map([ ['stop', 'Stop'], ]); +// 恢复命令是给用户直接复制执行的;插件路径可能含空格(macOS 的 Application Support) +// 或单引号,未转义会被 shell 拆成多个参数。按 POSIX 规则用单引号包裹,内部单引号转义。 +function shellQuote(value) { + return `'${String(value).replace(/'/g, "'\\''")}'`; +} + function incomplete(event, reason) { const safe = /^[a-z_]+$/.test(reason) ? reason : 'runtime_unavailable'; - const message = `CodeGuard Rust 运行时未完成(${safe});本次源码检查未运行,交付未评估。请用 Node 18+ 执行 node runtime/codeguard_runtime.cjs verify;支持的 macOS arm64 上显式运行 node runtime/codeguard_runtime.cjs install --download。运行时可用后,使用同一入口 exec init ABS_PROJECT --apply 初始化修复任务,或 exec next ABS_PROJECT 获取指引。`; + // hooks 在用户 workspace 里运行,而管理器位于已安装插件根目录;相对路径会让照抄的用户 + // 得到 MODULE_NOT_FOUND,生命周期就永久卡在未完成。必须给出可直接执行的绝对路径。 + const manager = shellQuote(path.join(pluginRoot, 'runtime', 'codeguard_runtime.cjs')); + const message = `CodeGuard Rust 运行时未完成(${safe});本次源码检查未运行,交付未评估。请用 Node 18+ 执行 node ${manager} verify;支持的 macOS arm64 上显式运行 node ${manager} install --download。运行时可用后,使用同一入口 exec init ABS_PROJECT --apply 初始化修复任务,或 exec next ABS_PROJECT 获取指引。`; return event === 'stop' ? { systemMessage: message } : { hookSpecificOutput: { hookEventName: EVENTS.get(event), additionalContext: message } }; diff --git a/dev.openhands/hooks/rust_runtime_dispatch.cjs b/dev.openhands/hooks/rust_runtime_dispatch.cjs index 33de11d..10f78a5 100644 --- a/dev.openhands/hooks/rust_runtime_dispatch.cjs +++ b/dev.openhands/hooks/rust_runtime_dispatch.cjs @@ -17,9 +17,18 @@ const EVENTS = new Map([ ['stop', 'Stop'], ]); +// 恢复命令是给用户直接复制执行的;插件路径可能含空格(macOS 的 Application Support) +// 或单引号,未转义会被 shell 拆成多个参数。按 POSIX 规则用单引号包裹,内部单引号转义。 +function shellQuote(value) { + return `'${String(value).replace(/'/g, "'\\''")}'`; +} + function incomplete(event, reason) { const safe = /^[a-z_]+$/.test(reason) ? reason : 'runtime_unavailable'; - const message = `CodeGuard Rust 运行时未完成(${safe});本次源码检查未运行,交付未评估。请用 Node 18+ 执行 node runtime/codeguard_runtime.cjs verify;支持的 macOS arm64 上显式运行 node runtime/codeguard_runtime.cjs install --download。运行时可用后,使用同一入口 exec init ABS_PROJECT --apply 初始化修复任务,或 exec next ABS_PROJECT 获取指引。`; + // hooks 在用户 workspace 里运行,而管理器位于已安装插件根目录;相对路径会让照抄的用户 + // 得到 MODULE_NOT_FOUND,生命周期就永久卡在未完成。必须给出可直接执行的绝对路径。 + const manager = shellQuote(path.join(pluginRoot, 'runtime', 'codeguard_runtime.cjs')); + const message = `CodeGuard Rust 运行时未完成(${safe});本次源码检查未运行,交付未评估。请用 Node 18+ 执行 node ${manager} verify;支持的 macOS arm64 上显式运行 node ${manager} install --download。运行时可用后,使用同一入口 exec init ABS_PROJECT --apply 初始化修复任务,或 exec next ABS_PROJECT 获取指引。`; return event === 'stop' ? { systemMessage: message } : { hookSpecificOutput: { hookEventName: EVENTS.get(event), additionalContext: message } }; diff --git a/hooks/rust_runtime_dispatch.cjs b/hooks/rust_runtime_dispatch.cjs index b8621b5..10f78a5 100644 --- a/hooks/rust_runtime_dispatch.cjs +++ b/hooks/rust_runtime_dispatch.cjs @@ -17,11 +17,17 @@ const EVENTS = new Map([ ['stop', 'Stop'], ]); +// 恢复命令是给用户直接复制执行的;插件路径可能含空格(macOS 的 Application Support) +// 或单引号,未转义会被 shell 拆成多个参数。按 POSIX 规则用单引号包裹,内部单引号转义。 +function shellQuote(value) { + return `'${String(value).replace(/'/g, "'\\''")}'`; +} + function incomplete(event, reason) { const safe = /^[a-z_]+$/.test(reason) ? reason : 'runtime_unavailable'; // hooks 在用户 workspace 里运行,而管理器位于已安装插件根目录;相对路径会让照抄的用户 // 得到 MODULE_NOT_FOUND,生命周期就永久卡在未完成。必须给出可直接执行的绝对路径。 - const manager = path.join(pluginRoot, 'runtime', 'codeguard_runtime.cjs'); + const manager = shellQuote(path.join(pluginRoot, 'runtime', 'codeguard_runtime.cjs')); const message = `CodeGuard Rust 运行时未完成(${safe});本次源码检查未运行,交付未评估。请用 Node 18+ 执行 node ${manager} verify;支持的 macOS arm64 上显式运行 node ${manager} install --download。运行时可用后,使用同一入口 exec init ABS_PROJECT --apply 初始化修复任务,或 exec next ABS_PROJECT 获取指引。`; return event === 'stop' ? { systemMessage: message }