feat(ack): add project knowledge guardrails

This commit is contained in:
2026-07-31 21:09:36 +08:00
parent 7d1994cf93
commit ee66dbe9ce
32 changed files with 8353 additions and 139 deletions
+455 -12
View File
@@ -3,9 +3,15 @@
from __future__ import annotations
import argparse
import ctypes
import errno
import os
import re
import secrets
import stat
import subprocess
import sys
import tempfile
from datetime import datetime
from pathlib import Path
@@ -63,6 +69,20 @@ from skiff.sources import (
from skiff.yaml_io import safe_dump
from skiff.symlinks import check_link, copy_template, create_link, find_repo_root, remove_link
_RENAME_NOREPLACE = 1
def _encode_single_path_component(value: str) -> bytes:
encoded = os.fsencode(value)
if (
not encoded
or encoded in {b".", b".."}
or b"/" in encoded
or b"\0" in encoded
):
raise ValueError(f"必须是单一路径组件: {value!r}")
return encoded
def _print(msg: str = "") -> None:
print(msg, file=sys.stdout)
@@ -78,11 +98,146 @@ def _project_root(explicit: str | None = None) -> Path:
return find_repo_root() or Path.cwd()
def _render_template(source: Path, destination: Path, values: dict[str, str]) -> None:
def _render_template(
source: Path,
destination: Path,
values: dict[str, str],
) -> str:
content = source.read_text(encoding="utf-8")
for placeholder, value in values.items():
content = content.replace(placeholder, value)
destination.write_text(content, encoding="utf-8")
return content
def _open_or_create_directory_at(
parent_fd: int,
name: str,
) -> tuple[int, bool]:
if (
not name
or name in {".", ".."}
or Path(name).name != name
or "/" in name
or "\\" in name
or (os.altsep is not None and os.altsep in name)
):
raise SystemExit(f"初始化目录名必须是单个安全路径段: {name!r}")
created = False
try:
os.mkdir(name, dir_fd=parent_fd)
created = True
except FileExistsError:
pass
try:
directory_fd = os.open(
name,
os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
dir_fd=parent_fd,
)
except OSError as exc:
raise SystemExit(
f"初始化路径必须是普通目录且不能是软链接: {name}: {exc}"
) from exc
return directory_fd, created
def _rename_directory_noreplace(
source_parent_fd: int,
source_name: str,
destination_parent_fd: int,
destination_name: str,
) -> None:
"""Atomically publish a directory without replacing an existing path."""
source = _encode_single_path_component(source_name)
destination = _encode_single_path_component(destination_name)
if source == destination:
raise ValueError("暂存目录名与目标目录名不能相同")
try:
renameat2 = ctypes.CDLL(None, use_errno=True).renameat2
except (AttributeError, OSError) as exc:
raise SystemExit(
"当前平台缺少原子 no-replace 目录发布能力,拒绝执行初始化"
) from exc
renameat2.argtypes = [
ctypes.c_int,
ctypes.c_char_p,
ctypes.c_int,
ctypes.c_char_p,
ctypes.c_uint,
]
renameat2.restype = ctypes.c_int
ctypes.set_errno(0)
result = renameat2(
source_parent_fd,
source,
destination_parent_fd,
destination,
_RENAME_NOREPLACE,
)
if result == 0:
return
error_number = ctypes.get_errno()
if error_number in {errno.EEXIST, errno.ENOTEMPTY}:
raise FileExistsError(
error_number,
os.strerror(error_number),
destination_name,
)
if error_number in {
errno.ENOSYS,
errno.EINVAL,
getattr(errno, "ENOTSUP", errno.EOPNOTSUPP),
errno.EOPNOTSUPP,
}:
raise SystemExit(
"当前文件系统不支持原子 no-replace 目录发布,拒绝执行初始化"
)
if error_number == 0:
raise RuntimeError("renameat2 失败但未设置 errno")
raise OSError(
error_number,
os.strerror(error_number),
f"{source_name} -> {destination_name}",
)
def _assert_open_directory_path(
directory_fd: int,
path: Path,
*,
phase: str,
label: str = "项目目录",
) -> None:
"""Fail if a named directory no longer resolves to the opened inode."""
opened = os.fstat(directory_fd)
try:
current = os.stat(path, follow_symlinks=False)
except OSError as exc:
raise SystemExit(f"{phase}{label}已移动或不可访问: {path}") from exc
if (
not stat.S_ISDIR(current.st_mode)
or (opened.st_dev, opened.st_ino) != (current.st_dev, current.st_ino)
):
raise SystemExit(f"{phase}{label}已被替换: {path}")
def _directory_entry_matches_open_fd(
parent_fd: int,
name: str,
opened_fd: int,
) -> bool:
try:
current = os.stat(name, dir_fd=parent_fd, follow_symlinks=False)
except OSError:
return False
opened = os.fstat(opened_fd)
return (
stat.S_ISDIR(current.st_mode)
and (current.st_dev, current.st_ino) == (opened.st_dev, opened.st_ino)
)
def _collect_skill_names(positional: list[str] | None, flagged: list[str] | None) -> list[str]:
@@ -1130,17 +1285,35 @@ def cmd_doctor(args: argparse.Namespace) -> None:
def cmd_init(args: argparse.Namespace) -> None:
"""使用 builtin skill 自带的模板初始化目标项目状态。"""
ensure_skills_home()
skill_source = SKILLS_DIR / args.name
validate_skill_name(args.name)
skills_root = SKILLS_DIR.resolve()
skill_source = (SKILLS_DIR / args.name).resolve()
try:
skill_source.relative_to(skills_root)
except ValueError as exc:
raise SystemExit(f"builtin skill 路径逃逸仓库边界: {args.name}") from exc
if not (skill_source / "SKILL.md").is_file():
raise SystemExit(f"builtin skill 不存在: {args.name}")
project = _project_root(args.project)
if not project.is_dir():
try:
initial_project_stat = os.stat(project, follow_symlinks=False)
except OSError as exc:
raise SystemExit(f"项目目录不存在: {project}") from exc
if not stat.S_ISDIR(initial_project_stat.st_mode):
raise SystemExit(f"项目目录不存在: {project}")
initial_project_identity = (
initial_project_stat.st_dev,
initial_project_stat.st_ino,
stat.S_IFMT(initial_project_stat.st_mode),
)
destination = project / "docs" / args.name
project_file = destination / "project.md"
tasks_file = destination / "tasks.yaml"
managed_targets = (project_file, tasks_file)
knowledge_file = destination / "knowledge.yaml"
managed_targets = [project_file, tasks_file]
if args.name == "ack":
managed_targets.append(knowledge_file)
existing = [path for path in managed_targets if path.exists() or path.is_symlink()]
if existing:
paths = ", ".join(str(path.relative_to(project)) for path in existing)
@@ -1148,12 +1321,29 @@ def cmd_init(args: argparse.Namespace) -> None:
project_template = skill_source / "templates" / "project.template.md"
tasks_template = skill_source / "templates" / "tasks.template.yaml"
missing = [path for path in (project_template, tasks_template) if not path.is_file()]
template_targets = [
(project_template, project_file),
(tasks_template, tasks_file),
]
if args.name == "ack":
template_targets.append(
(skill_source / "templates" / "knowledge.template.yaml", knowledge_file)
)
missing = [path for path, _ in template_targets if not path.is_file()]
if missing:
paths = ", ".join(str(path.relative_to(SKILLS_HOME)) for path in missing)
raise SystemExit(f"skill 缺少初始化模板: {paths}")
destination.mkdir(parents=True, exist_ok=True)
validator = skill_source / "scripts" / "validate_tasks.py"
knowledge_validator = skill_source / "scripts" / "validate_knowledge.py"
if args.name == "ack":
missing_validators = [
path
for path in (validator, knowledge_validator)
if not path.is_file()
]
if missing_validators:
paths = ", ".join(path.name for path in missing_validators)
raise SystemExit(f"ACK skill 缺少初始化校验器: {paths}")
version_file = skill_source / "VERSION"
ack_version = version_file.read_text(encoding="utf-8").strip() if version_file.is_file() else "unknown"
@@ -1167,17 +1357,270 @@ def cmd_init(args: argparse.Namespace) -> None:
"<接入时的 ack skill 版本>": ack_version,
"<YYYY-MM-DDTHH:mm:ss+TZ>": now,
}
_render_template(project_template, project_file, values)
_render_template(tasks_template, tasks_file, values)
validator = skill_source / "scripts" / "validate_tasks.py"
if validator.is_file():
subprocess.run([sys.executable, str(validator), str(tasks_file)], check=True)
with tempfile.TemporaryDirectory(prefix=f"skiff-{args.name}-init-") as temp_dir:
staging = Path(temp_dir)
staged_files: dict[Path, Path] = {}
rendered_files: dict[Path, str] = {}
for template, target in template_targets:
staged = staging / target.relative_to(project)
staged.parent.mkdir(parents=True, exist_ok=True)
rendered_files[target] = _render_template(template, staged, values)
staged_files[target] = staged
if validator.is_file():
completed = subprocess.run(
[sys.executable, str(validator), str(staged_files[tasks_file])],
check=False,
)
if completed.returncode != 0:
raise SystemExit(
f"初始化任务板校验失败(exit {completed.returncode}"
)
if args.name == "ack" and knowledge_validator.is_file():
completed = subprocess.run(
[
sys.executable,
str(knowledge_validator),
str(staged_files[knowledge_file]),
"--tasks",
str(staged_files[tasks_file]),
"--project-root",
str(staging),
],
check=False,
)
if completed.returncode != 0:
raise SystemExit(
f"初始化知识库校验失败(exit {completed.returncode}"
)
for target, staged in staged_files.items():
if staged.read_text(encoding="utf-8") != rendered_files[target]:
raise SystemExit(
f"初始化临时文件在校验期间发生变化: {target.name}"
)
# Validation may take time, so guard against a concurrent initializer before writing.
existing = [path for path in managed_targets if path.exists() or path.is_symlink()]
if existing:
paths = ", ".join(str(path.relative_to(project)) for path in existing)
raise SystemExit(f"拒绝覆盖已有路径: {paths}")
project_fd: int | None = None
docs_fd: int | None = None
transaction_fd: int | None = None
staging_fd: int | None = None
docs_created = False
transaction_name: str | None = None
staged_names: list[str] = []
published = False
committed = False
try:
try:
project_fd = os.open(
project,
os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
)
except OSError as exc:
raise SystemExit(
f"校验期间项目目录已移动或不可访问: {project}"
) from exc
opened_project_stat = os.fstat(project_fd)
opened_project_identity = (
opened_project_stat.st_dev,
opened_project_stat.st_ino,
stat.S_IFMT(opened_project_stat.st_mode),
)
if opened_project_identity != initial_project_identity:
raise SystemExit(f"校验期间项目目录已被替换: {project}")
_assert_open_directory_path(
project_fd,
project,
phase="初始化",
)
docs_fd, docs_created = _open_or_create_directory_at(project_fd, "docs")
if docs_created:
os.fsync(project_fd)
_assert_open_directory_path(
project_fd,
project,
phase="初始化",
)
_assert_open_directory_path(
docs_fd,
project / "docs",
phase="初始化",
label="docs 目录",
)
try:
destination_stat = os.stat(
args.name,
dir_fd=docs_fd,
follow_symlinks=False,
)
except FileNotFoundError:
pass
else:
if stat.S_ISLNK(destination_stat.st_mode):
raise SystemExit(
"初始化路径必须是普通目录且不能是软链接: "
f"docs/{args.name}"
)
raise SystemExit(f"拒绝覆盖已有路径: docs/{args.name}")
for _ in range(32):
candidate = f".{args.name}-init-{secrets.token_hex(8)}"
try:
os.mkdir(candidate, mode=0o700, dir_fd=docs_fd)
except FileExistsError:
continue
transaction_name = candidate
break
if transaction_name is None:
raise SystemExit("无法创建唯一的初始化暂存目录")
transaction_fd = os.open(
transaction_name,
os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
dir_fd=docs_fd,
)
os.mkdir("payload", mode=0o755, dir_fd=transaction_fd)
staging_fd = os.open(
"payload",
os.O_RDONLY | os.O_DIRECTORY | os.O_NOFOLLOW,
dir_fd=transaction_fd,
)
for target in staged_files:
file_fd = os.open(
target.name,
os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW,
0o644,
dir_fd=staging_fd,
)
staged_names.append(target.name)
with os.fdopen(file_fd, "w", encoding="utf-8") as destination_file:
destination_file.write(rendered_files[target])
destination_file.flush()
os.fsync(destination_file.fileno())
os.fsync(staging_fd)
os.fsync(transaction_fd)
_assert_open_directory_path(
project_fd,
project,
phase="发布",
)
_assert_open_directory_path(
docs_fd,
project / "docs",
phase="发布",
label="docs 目录",
)
try:
_rename_directory_noreplace(
transaction_fd,
"payload",
docs_fd,
args.name,
)
except FileExistsError as exc:
raise SystemExit(
f"拒绝覆盖已有路径: docs/{args.name}"
) from exc
published = True
_assert_open_directory_path(
staging_fd,
destination,
phase="发布",
label="ACK 目录",
)
if (
transaction_name is not None
and _directory_entry_matches_open_fd(
docs_fd,
transaction_name,
transaction_fd,
)
):
try:
os.rmdir(transaction_name, dir_fd=docs_fd)
except OSError:
pass
else:
transaction_name = None
try:
os.fsync(docs_fd)
except OSError as exc:
raise SystemExit(
"初始化目录已完整发布,但无法确认目录项持久化;"
f"请检查 docs/{args.name} 后再重试"
) from exc
_assert_open_directory_path(
project_fd,
project,
phase="完成初始化",
)
_assert_open_directory_path(
docs_fd,
project / "docs",
phase="完成初始化",
label="docs 目录",
)
_assert_open_directory_path(
staging_fd,
destination,
phase="完成初始化",
label="ACK 目录",
)
committed = True
except BaseException:
if not published:
for name in reversed(staged_names):
try:
if staging_fd is not None:
os.unlink(name, dir_fd=staging_fd)
except FileNotFoundError:
pass
if transaction_fd is not None:
try:
os.rmdir("payload", dir_fd=transaction_fd)
except OSError:
pass
if (
transaction_name is not None
and transaction_fd is not None
and docs_fd is not None
and _directory_entry_matches_open_fd(
docs_fd,
transaction_name,
transaction_fd,
)
):
try:
os.rmdir(transaction_name, dir_fd=docs_fd)
except OSError:
pass
raise
finally:
for directory_fd in (
staging_fd,
transaction_fd,
docs_fd,
project_fd,
):
if directory_fd is not None:
os.close(directory_fd)
if not committed:
raise SystemExit("初始化事务未提交")
_print(f"✓ skill 项目状态初始化完成: {args.name}")
_print(f" 项目: {project}")
_print(f" 覆盖层: {project_file}")
_print(f" 任务板: {tasks_file}")
if args.name == "ack":
_print(f" 知识库: {knowledge_file}")
_print("下一步: 填写 project.md 中的项目命令、路径权限和 Base URL")