@yottameta/yotta-dev-mcp 0.2.4 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## v0.2.5 (2026-10-31)
4
+
5
+ 安全热修批次(ClawHub 0.2.4 扫描复核):
6
+
7
+ - `review_diff`:`base` 先解析为 commit SHA(`git rev-parse --verify --end-of-options`),
8
+ 拒绝以 `-` 开头、含空白、超长或无法解析的 revision;防止 `--output=` 等 Git 选项注入
9
+ 在仓库外写文件。
10
+ - `workflow_state`:原子写入改为同目录随机独占临时文件 + `os.replace`;备份同样先写
11
+ 随机临时文件;拒绝目标 / 祖先路径中的 symlink、junction 等 reparse point;预置
12
+ `.tmp` / `.bak` 链接不会再重定向写入。
13
+ - 回归:新增 option injection、valid base、temp / backup / hardlink / junction 用例;
14
+ 全量测试 **199(3 skip)**。
15
+
3
16
  ## v0.2.4 (2026-10-31)
4
17
 
5
18
  外部缺陷报告修复批次:
package/README.md CHANGED
@@ -18,14 +18,14 @@ Deterministic local development tools exposed over a stdio MCP server.
18
18
  | `find_code` | Locate symbols and text with bounded results. |
19
19
  | `compress_output` | Compress long logs while keeping errors and head/tail context. |
20
20
  | `review_code` | Apply deterministic review rules with file, line and evidence. |
21
- | `review_diff` | Review only added lines in a unified diff. |
21
+ | `review_diff` | Review only added lines in a unified diff; `base` resolves to a commit SHA. |
22
22
  | `mcp_doctor` | Inspect installed skills and MCP JSON configuration files. |
23
23
  | `scan_secrets` | Scan for credentials and high-entropy tokens with redacted evidence. |
24
24
  | `scan_dependencies` | Check manifests, lockfiles, insecure sources and typosquat suspicion. |
25
25
  | `check_publish_readiness` | Check version alignment, release files and package metadata. |
26
26
  | `run_checks` | Run whitelisted tests, lint or compile checks; execution is explicit. |
27
27
  | `scaffold_skill` | Plan or create a minimal skill scaffold; dry-run by default. |
28
- | `workflow_state` | Read `.workflow` and optionally append an explicit log entry. |
28
+ | `workflow_state` | Read `.workflow` and optionally append an explicit log entry; writes are atomic and reject link paths. |
29
29
 
30
30
  Tools are read-only by default; `run_checks`, the L2-L4 policy checks in
31
31
  `verify_change`, the test subset in `self_test`, and `run_adapter` with
package/README.zh-CN.md CHANGED
@@ -18,14 +18,14 @@
18
18
  | `find_code` | 符号与文本定位,结果有数量上限。 |
19
19
  | `compress_output` | 压缩长日志,保留错误、错误栈和首尾上下文。 |
20
20
  | `review_code` | 规则化代码评审,输出文件、行号、证据与建议。 |
21
- | `review_diff` | 只评审 unified diff 的新增行。 |
21
+ | `review_diff` | 只评审 unified diff 的新增行;`base` 先解析为 commit SHA。 |
22
22
  | `mcp_doctor` | 只读检查技能目录与 MCP JSON 配置。 |
23
23
  | `scan_secrets` | 密钥 / 凭据 / 高熵令牌扫描,证据强制脱敏。 |
24
24
  | `scan_dependencies` | 依赖清单、lockfile、来源与 typosquat 启发式检查。 |
25
25
  | `check_publish_readiness` | 版本对齐、发布文件与包元数据检查。 |
26
26
  | `run_checks` | 运行白名单测试 / lint / compile,执行必须显式开启。 |
27
27
  | `scaffold_skill` | 规划或生成最小技能脚手架,默认 dry-run。 |
28
- | `workflow_state` | 读取 `.workflow`,可选显式追加快照日志。 |
28
+ | `workflow_state` | 读取 `.workflow`,可选显式追加快照日志;写入原子化并拒绝链接路径。 |
29
29
 
30
30
  默认只读;`run_checks`、`verify_change` 的 L2-L4 策略检查、`self_test` 的测试子集,
31
31
  以及 `run_adapter` 的 `action=run` 必须显式 `allow_execute=true`,
package/SKILL.md CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: yotta-dev-mcp
3
3
  description: 元开(yotta-dev-mcp)—— 本地、确定性的开发工具 MCP,把只读默认的开发能力暴露为 stdio MCP server:repo_map / system_model / architecture_review / impact_analysis / verify_change / self_test / run_adapter / find_code / compress_output / review_code / review_diff / mcp_doctor / scan_secrets / scan_dependencies / check_publish_readiness / run_checks / scaffold_skill / workflow_state。触发:让 AI 在陌生项目里先做结构盘点、构建系统模型或架构契约(.yotta/architecture.json)、按契约评审架构、分析改动影响锥与回归面、按验证阶梯产出证据账本、对元开自身做完整性 / 反证自测、探测或显式运行 import-linter / dependency-cruiser / Repomix 可选适配器、定位代码、评审改动、扫描密钥/依赖、检查发布就绪、运行白名单检查、生成脚手架或读取 .workflow 状态时;或用户说 元开 / 开发能力 MCP / yotta-dev-mcp / 代码库地图 / 系统模型 / 架构契约 / 架构评审 / 影响分析 / 验证账本 / 反证自测 / 代码评审 MCP / 适配器 等。边界:Python 3.8+ 标准库、离线默认;除 run_checks(显式 allow_execute)、verify_change 的 L2-L4 策略检查(显式 allow_execute)与 self_test 的测试子集(显式 allow_execute)、run_adapter 的 action=run(显式 allow_execute)、scaffold_skill / workflow_state 的显式 apply 外均为只读;不上传源码、不自动修改、不提交、不联网查询包是否存在、不自动安装或下载适配器。
4
- version: 0.2.4
4
+ version: 0.2.5
5
5
  license: MIT
6
6
  ---
7
7
 
@@ -72,14 +72,14 @@ python scripts/yotta_dev_mcp.py --tools full # 18 工具(默认)
72
72
  | `find_code` | 符号 / 文本定位,结果有上限 | 否 |
73
73
  | `compress_output` | 保留错误与首尾的长输出压缩 | 否 |
74
74
  | `review_code` | 规则化代码评审,带行号与建议;忽略 `.workflow` / `scratch` / `_probe` 等临时目录 | 否 |
75
- | `review_diff` | 只评审 diff 的新增行 | 否 |
75
+ | `review_diff` | 只评审 diff 的新增行;`base` 先解析为 commit SHA,拒绝选项注入 | 否 |
76
76
  | `mcp_doctor` | 技能版本与多宿主 MCP 配置体检;返回 coverage,只列 server 名,不含 command / env | 否 |
77
77
  | `scan_secrets` | 密钥 / 凭据 / 高熵令牌扫描(强制脱敏;路径 / 哈希 / 文件名噪声过滤) | 否 |
78
78
  | `scan_dependencies` | 依赖清单、lockfile、来源与 typosquat 启发式检查 | 否 |
79
79
  | `check_publish_readiness` | 版本四件、发布文件、仓库与 publishConfig 检查 | 否 |
80
80
  | `run_checks` | 白名单测试 / lint / compile 并返回结构化摘要 | 仅显式 allow_execute |
81
81
  | `scaffold_skill` | 生成最小技能脚手架,默认 dry-run | 仅显式 apply |
82
- | `workflow_state` | 读取 `.workflow`,可选显式追加日志 | 仅显式 apply |
82
+ | `workflow_state` | 读取 `.workflow`,可选显式追加日志;写入拒绝链接路径并使用原子替换 | 仅显式 apply |
83
83
 
84
84
  详细契约见 `references/tools.md`;架构契约、评审语义与影响锥字段见 `references/architecture-contract.md`。
85
85
 
@@ -88,13 +88,18 @@ python scripts/yotta_dev_mcp.py --tools full # 18 工具(默认)
88
88
  - 会执行项目代码的工具默认关闭:`run_checks`、`verify_change` 的 L2-L4 策略检查、`self_test` 的测试子集都必须显式 `allow_execute=true`,且只运行白名单检查或已声明的策略检查。
89
89
  - 外部适配器只在用户已安装时接入:`run_adapter` 不安装、不下载、不访问远端、不接收任意命令参数;工具缺失或配置缺失一律返回 `UNKNOWN` 并给出 next_step。
90
90
  - `scaffold_skill` / `workflow_state` 默认只预览,显式 `apply=true` 才写入;脚手架拒绝覆盖已有非空目录,状态写入使用原子替换并为被覆盖文件保留 `.bak`。
91
+ - `workflow_state` 写入会拒绝目标或祖先路径中的 symlink / junction 等链接重定向;
92
+ 临时文件使用随机独占名,避免可预测 `.tmp` / `.bak` 路径被预置链接利用。
91
93
  - `review_code` / `review_diff` 支持 `.review-allowlist.json`(或 `.yotta/review-allowlist.json`)精确豁免(path + rule + line + reason,无通配符);非法配置 fail-closed,不抑制 findings。
94
+ - `review_diff` 的 `base` 必须是可解析 Git revision;以 `-` 开头、含空白或无法解析的值会被拒绝,
95
+ 防止 `--output=` 等选项注入。
92
96
  - 不联网,不查询包是否存在于公共仓库。
93
97
  - 不读取或修改 YottaCode 仓库。
94
98
  - 结论是确定性静态判断,不替代人工评审与最终决策。
95
99
 
96
100
  ## 当前版本
97
101
 
102
+ - v0.2.5(2026-10-31):安全热修——`review_diff` 的 `base` 先解析为 commit SHA,拒绝 Git 选项注入;`workflow_state` 改为随机独占临时文件 + 原子替换,备份同样原子化,并拒绝 symlink / junction 等链接路径;新增 option injection、temp / backup / hardlink / junction 回归。
98
103
  - v0.2.4(2026-10-31):外部缺陷报告修复——`architecture_review` 修 BOM 假 PASS 与零证据 PASS;legacy `initialize` 报告当前工具档位;`review_code` / `review_diff` 修 `.exec(` 与字符串内待办标记误报并新增精确 allowlist;补齐临时 / 备份目录忽略;`scan_secrets` 补弱口令并去重;`find_code` 支持 `def ` 前缀;`self_test` 增加当前版本正文检查。
99
104
  - v0.2.3(2026-10-01):安装器卫生批次——`bin/install.js` / `install.sh` 统一,未知参数 exit 2、`--help` / `--version`、残留清理白名单与嵌套载荷保留。
100
105
  - v0.2.2(2026-09-26):新增 `--tools core|full` 工具分组;core 5 工具 / full 18 工具,core 拒绝 full 专属工具。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yottameta/yotta-dev-mcp",
3
- "version": "0.2.4",
3
+ "version": "0.2.5",
4
4
  "description": "YuanKai (yotta-dev-mcp) - deterministic local development tools exposed over stdio MCP: repo_map, system_model, architecture_review, impact_analysis, verify_change, self_test, run_adapter, find_code, compress_output, review_code, review_diff, mcp_doctor, scan_secrets, scan_dependencies, check_publish_readiness, run_checks, scaffold_skill and workflow_state. Reads an optional .yotta/architecture.json contract plus .yotta/verification.json policy for architecture reviews, change impact cones and the L0-L5 evidence ledger. Optional explicit adapters integrate import-linter, dependency-cruiser and Repomix without auto-install or network access. Python 3.8+ standard library, offline by default; writes and command execution require explicit flags.",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -203,7 +203,9 @@ Each finding contains `path`, `line`, `rule`, `severity`, `evidence`, `suggestio
203
203
  Input:
204
204
 
205
205
  - `diff_text` or `path` (one required).
206
- - `base` (optional git revision when `path` is used).
206
+ - `base` (optional git revision when `path` is used). It is resolved to a commit SHA
207
+ before `git diff` runs; option-like, whitespace-containing, oversized or
208
+ unresolvable revisions are rejected.
207
209
  - `max_findings` (optional).
208
210
 
209
211
  Only added lines are reviewed. Output: `files`, `findings`, `truncated`.
@@ -305,4 +307,6 @@ Input: `root`, `action` (`read` / `append-log` / `append-file`), optional
305
307
  `date`, `text`, `file`, explicit `apply=true`.
306
308
 
307
309
  Output: `ok`, `files`, `missing`, `excerpts` for read; `target`, `applied`,
308
- `preview` for writes. Writes are atomic and keep a `.bak` of an existing file.
310
+ `preview` for writes. Writes use a random exclusive temporary file plus
311
+ `os.replace`, keep a `.bak` of an existing file, and reject target or ancestor
312
+ paths that are symlinks, junctions or other reparse points.
@@ -13,8 +13,10 @@ import math
13
13
  import os
14
14
  import re
15
15
  import shutil
16
+ import stat
16
17
  import subprocess
17
18
  import sys
19
+ import tempfile
18
20
  from pathlib import Path
19
21
 
20
22
  import dev_contract
@@ -41,7 +43,7 @@ from dev_model import (
41
43
  )
42
44
  from dev_rules import REVIEW_RULES
43
45
 
44
- VERSION = "0.2.4"
46
+ VERSION = "0.2.5"
45
47
 
46
48
 
47
49
 
@@ -398,6 +400,28 @@ def _parse_added_lines(diff_text):
398
400
  line_no += 1
399
401
 
400
402
 
403
+ def _resolve_git_commit(path, revision):
404
+ value = str(revision or "").strip()
405
+ if (
406
+ not value
407
+ or len(value) > 200
408
+ or value.startswith("-")
409
+ or any(char.isspace() for char in value)
410
+ ):
411
+ raise ValueError("base 不是有效 revision")
412
+ command = [
413
+ "git", "-C", str(path), "rev-parse", "--verify", "--quiet",
414
+ "--end-of-options", value + "^{commit}",
415
+ ]
416
+ proc = subprocess.run(command, capture_output=True, text=True)
417
+ if proc.returncode != 0:
418
+ raise ValueError("base revision 无法解析: %s" % value)
419
+ commit = proc.stdout.strip()
420
+ if not re.match(r"^[0-9a-fA-F]{40,64}$", commit):
421
+ raise ValueError("base revision 解析结果非法")
422
+ return commit
423
+
424
+
401
425
  def review_diff(diff_text=None, path=None, base=None, max_findings=200):
402
426
  allowlist = {}
403
427
  allowlist_errors = []
@@ -407,7 +431,7 @@ def review_diff(diff_text=None, path=None, base=None, max_findings=200):
407
431
  allowlist, allowlist_errors = _load_review_allowlist(path)
408
432
  command = ["git", "-C", str(path), "diff", "--no-ext-diff", "--unified=0"]
409
433
  if base:
410
- command.append(str(base))
434
+ command.append(_resolve_git_commit(path, base))
411
435
  proc = subprocess.run(command, capture_output=True, text=True)
412
436
  if proc.returncode != 0:
413
437
  raise ValueError("git diff 失败: %s" % (proc.stderr.strip() or proc.stdout.strip()))
@@ -917,15 +941,80 @@ def scaffold_skill(name, output_dir, apply=False, description=None):
917
941
  }
918
942
 
919
943
 
944
+ def _write_temp_text(path, content):
945
+ descriptor, name = tempfile.mkstemp(
946
+ prefix="." + path.name + ".", suffix=".tmp", dir=str(path.parent),
947
+ )
948
+ try:
949
+ with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
950
+ handle.write(content)
951
+ except BaseException:
952
+ try:
953
+ os.unlink(name)
954
+ except OSError:
955
+ pass
956
+ raise
957
+ return Path(name)
958
+
959
+
960
+ def _write_temp_bytes(path, data):
961
+ descriptor, name = tempfile.mkstemp(
962
+ prefix="." + path.name + ".", suffix=".tmp", dir=str(path.parent),
963
+ )
964
+ try:
965
+ with os.fdopen(descriptor, "wb") as handle:
966
+ handle.write(data)
967
+ except BaseException:
968
+ try:
969
+ os.unlink(name)
970
+ except OSError:
971
+ pass
972
+ raise
973
+ return Path(name)
974
+
975
+
976
+ def _preserve_mode(source, target):
977
+ try:
978
+ os.chmod(str(target), os.stat(str(source)).st_mode & 0o777)
979
+ except OSError:
980
+ pass
981
+
982
+
983
+ def _is_link_like(path):
984
+ try:
985
+ info = os.lstat(str(path))
986
+ except OSError:
987
+ return False
988
+ if stat.S_ISLNK(info.st_mode):
989
+ return True
990
+ attributes = getattr(info, "st_file_attributes", 0)
991
+ reparse_point = getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0)
992
+ return bool(attributes & reparse_point)
993
+
994
+
995
+ def _assert_no_symlink_path(path, root):
996
+ root = Path(root)
997
+ current = Path(path)
998
+ while True:
999
+ if _is_link_like(current):
1000
+ raise ValueError("拒绝通过符号链接写入: %s" % current)
1001
+ if current == root or current.parent == current:
1002
+ break
1003
+ current = current.parent
1004
+
1005
+
920
1006
  def _atomic_write(path, content):
921
1007
  path = Path(path)
922
1008
  path.parent.mkdir(parents=True, exist_ok=True)
923
1009
  backup = None
924
1010
  if path.exists():
925
1011
  backup = path.with_suffix(path.suffix + ".bak")
926
- shutil.copy2(str(path), str(backup))
927
- temporary = path.with_suffix(path.suffix + ".tmp")
928
- temporary.write_text(content, encoding="utf-8")
1012
+ temporary_backup = _write_temp_bytes(backup, path.read_bytes())
1013
+ _preserve_mode(path, temporary_backup)
1014
+ os.replace(str(temporary_backup), str(backup))
1015
+ temporary = _write_temp_text(path, content)
1016
+ if path.exists():
1017
+ _preserve_mode(path, temporary)
929
1018
  os.replace(str(temporary), str(path))
930
1019
  return str(backup) if backup else None
931
1020
 
@@ -964,6 +1053,7 @@ def workflow_state(root, action="read", date=None, text=None, file=None, apply=F
964
1053
  body = (text or "").rstrip() + "\n"
965
1054
  preview = body
966
1055
  if apply:
1056
+ _assert_no_symlink_path(target, Path(root))
967
1057
  target.parent.mkdir(parents=True, exist_ok=True)
968
1058
  if target.exists():
969
1059
  existing = _read_text(target)
package/server.json CHANGED
@@ -6,12 +6,12 @@
6
6
  "url": "https://github.com/YottaMeta/yotta-dev-mcp",
7
7
  "source": "github"
8
8
  },
9
- "version": "0.2.4",
9
+ "version": "0.2.5",
10
10
  "packages": [
11
11
  {
12
12
  "registryType": "npm",
13
13
  "identifier": "@yottameta/yotta-dev-mcp",
14
- "version": "0.2.4",
14
+ "version": "0.2.5",
15
15
  "transport": {
16
16
  "type": "stdio"
17
17
  }