Add harness coding docs for SoftBox (Go + Gio dual-build)
Harness governance / validate (push) Has been cancelled

Initialize the full harness coding document set from the
harness_coding_docs template, customized for the SoftBox project:

- Vision, requirements, tech stack (modern Go 1.25 + Gio v0.10.1;
  Win7 legacy Go 1.20.14 + Gio v0.6.0), architecture, coding rules
- Protocol contracts (signed catalog, package protocol v1, Ed25519
  license, events, CLI) and Gio view structure
- Roadmap Phase 0-6 with 20 suggested tasks; T-001 (monorepo
  skeleton) filed and ready to claim
- Agent entry points (AGENTS.md, docs/00-ai-start-here.md),
  context manifest, governance scripts and tests
- Merge Go gitignore with harness rules; keep go.work tracked

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
ila
2026-07-16 14:33:54 +08:00
co-authored by Claude Fable 5
parent 03bd4ab62a
commit 0184595ccc
43 changed files with 5010 additions and 10 deletions
+688
View File
@@ -0,0 +1,688 @@
#!/usr/bin/env python3
"""Read-only audit of Harness Coding task coordination in Gitea."""
from __future__ import annotations
import argparse
import base64
import os
import re
import sys
import urllib.parse
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from pathlib import Path
from typing import Any
from setup_gitea_labels import ApiError, GiteaClient, LABELS, validate_config
from validate_harness_governance import (
TASK_ID,
is_safe_repo_path,
parse_frontmatter,
parse_frontmatter_text,
scopes_overlap,
)
STATUS_LABELS = {
"status/todo",
"status/doing",
"status/blocked",
"status/review",
"status/done",
}
ACTIVE_LABELS = {"status/doing", "status/blocked", "status/review"}
READY_OR_ACTIVE_LABELS = ACTIVE_LABELS | {"status/todo"}
TASK_IN_TITLE = re.compile(r"^\[(T-\d{3}[a-z]?)\]")
BODY_TASK_ID = re.compile(r"(?m)^\s*-\s*task_id:\s*`?(T-\d{3}[a-z]?)`?\s*$")
BODY_TASK_FILE = re.compile(
r"(?m)^\s*-\s*task_file:\s*`?(docs/tasks/T-\d{3}[a-z]?\.md)`?\s*$"
)
FIELD = re.compile(r"(?m)^\s*(?:-\s*)?([a-z_]+):\s*`?([^`\r\n]+?)`?\s*$")
MAX_LEASE = timedelta(hours=24)
CLOCK_SKEW = timedelta(minutes=5)
CLAIM_IDENTITY_FIELDS = (
"task",
"claimed_by",
"allocated_by",
"context_ref",
"claim_branch",
"work_branch",
)
@dataclass(frozen=True, order=True)
class AuditFinding:
issue: int
rule: str
message: str
def render(self) -> str:
subject = "repository" if self.issue <= 0 else f"issue #{self.issue}"
return f"ERROR [{self.rule}] {subject}: {self.message}"
@dataclass
class RemoteTask:
number: int
task_id: str
state: str
status: str | None
labels: set[str]
body: str
work_branch: str | None = None
context_ref: str | None = None
claimed_by: str | None = None
claimed_at: datetime | None = None
lease_until: datetime | None = None
write_paths: list[str] | None = None
def label_names(item: dict[str, Any]) -> set[str]:
labels = item.get("labels")
if not isinstance(labels, list):
return set()
return {
label["name"]
for label in labels
if isinstance(label, dict) and isinstance(label.get("name"), str)
}
def paged(client: GiteaClient, path: str) -> list[dict[str, Any]]:
result: list[dict[str, Any]] = []
seen_pages: set[tuple[str, ...]] = set()
page = 1
separator = "&" if "?" in path else "?"
while True:
values = client.request("GET", f"{path}{separator}limit=50&page={page}")
if not isinstance(values, list):
raise RuntimeError("Gitea 分页响应格式异常。")
if not values:
return result
if not all(isinstance(value, dict) for value in values):
raise RuntimeError("Gitea 分页响应包含非对象条目。")
signature = tuple(
str(value.get("id") or value.get("number") or value.get("name"))
for value in values
)
if signature in seen_pages or page > 1000:
raise RuntimeError("Gitea 分页重复,已停止以避免无限读取。")
seen_pages.add(signature)
result.extend(value for value in values if isinstance(value, dict))
page += 1
def parse_fields(text: str) -> dict[str, str]:
return {match.group(1): match.group(2).strip() for match in FIELD.finditer(text)}
def parse_write_paths(text: str) -> list[str]:
lines = text.splitlines()
values: list[str] = []
collecting = False
for line in lines:
if re.match(r"^\s*(?:-\s*)?write_paths:\s*$", line):
collecting = True
continue
if collecting:
item = re.match(r"^\s+-\s+`?([^`\r\n]+?)`?\s*$", line)
if item:
value = item.group(1).strip()
if "【" not in value:
values.append(value)
continue
if line.strip():
break
return values
def parse_datetime(value: str | None) -> datetime | None:
if not value or "【" in value:
return None
if not re.fullmatch(
r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})",
value,
):
return None
try:
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError:
return None
if parsed.tzinfo is None:
parsed = parsed.replace(tzinfo=timezone.utc)
return parsed.astimezone(timezone.utc)
def select_latest_claim(
comments: list[dict[str, Any]], task_id: str, dispatcher_login: str | None
) -> tuple[str | None, list[str]]:
required = {
"task",
"claimed_by",
"allocated_by",
"context_ref",
"claim_branch",
"work_branch",
"claimed_at",
"lease_until",
}
if not dispatcher_login:
return None, ["未配置可信 dispatcher Gitea 登录名,无法验证 CLAIM 作者。"]
claims: list[tuple[int, str, dict[str, str], str, str]] = []
for index, comment in enumerate(comments):
body = comment.get("body")
if not isinstance(body, str):
continue
lines = body.strip().splitlines()
if not lines or lines[0].strip() not in {"CLAIM", "CLAIM RENEWAL"}:
continue
marker = lines[0].strip()
fields = parse_fields(body)
if fields.get("task") != task_id or not required.issubset(fields):
continue
if not parse_write_paths(body):
continue
user = comment.get("user")
author = str(user.get("login") or "") if isinstance(user, dict) else ""
comment_id = comment.get("id")
order = comment_id if isinstance(comment_id, int) else index
claims.append((order, marker, fields, author, body))
selected: str | None = None
identity: tuple[str, ...] | None = None
errors: list[str] = []
for _, marker, fields, author, body in sorted(claims, key=lambda item: item[0]):
if author != dispatcher_login or fields.get("allocated_by") != dispatcher_login:
errors.append("CLAIM 必须由配置的 dispatcher 账号发布,且 allocated_by 与作者一致。")
continue
candidate_identity = tuple(fields.get(key, "") for key in CLAIM_IDENTITY_FIELDS)
if marker == "CLAIM":
if identity is not None:
errors.append("同一任务存在重复的初始 CLAIM。")
continue
identity = candidate_identity
selected = body
continue
if identity is None:
errors.append("CLAIM RENEWAL 之前缺少有效初始 CLAIM。")
continue
if candidate_identity != identity:
errors.append("CLAIM RENEWAL 改变了任务、领取者、dispatcher 或分支身份字段。")
continue
selected = body
return selected, sorted(set(errors))
def latest_claim(
comments: list[dict[str, Any]], task_id: str, dispatcher_login: str | None
) -> str | None:
return select_latest_claim(comments, task_id, dispatcher_login)[0]
def pull_request_head(pr: dict[str, Any]) -> str:
head = pr.get("head")
return str(head.get("ref") or "") if isinstance(head, dict) else ""
def branch_commits(branches: list[dict[str, Any]]) -> dict[str, str]:
result: dict[str, str] = {}
for branch in branches:
name = branch.get("name")
commit = branch.get("commit")
if isinstance(name, str) and isinstance(commit, dict) and isinstance(
commit.get("id"), str
):
result[name] = commit["id"]
return result
def validate_pull_request(
pr: dict[str, Any], task: RemoteTask
) -> list[AuditFinding]:
findings: list[AuditFinding] = []
if not task.work_branch or pull_request_head(pr) != task.work_branch:
findings.append(
AuditFinding(task.number, "pull-request", "PR head 与 claim 工作分支不一致。")
)
body = pr.get("body")
body = body if isinstance(body, str) else ""
required_values = [f"docs/tasks/{task.task_id}.md"]
if task.context_ref:
required_values.append(task.context_ref)
required_values.extend(task.write_paths or [])
if not re.search(rf"(?i)\bCloses\s+#{task.number}\b", body):
findings.append(AuditFinding(task.number, "pull-request", "PR body 未链接对应 Issue。"))
if any(value not in body for value in required_values):
findings.append(
AuditFinding(task.number, "pull-request", "PR body 缺少任务、context_ref 或写路径。")
)
return findings
def remote_task_metadata(
client: GiteaClient, task_id: str, branch: str
) -> tuple[dict[str, Any] | None, list[str]]:
file_path = urllib.parse.quote(f"docs/tasks/{task_id}.md", safe="/")
ref = urllib.parse.quote(branch, safe="")
try:
response = client.request("GET", f"/contents/{file_path}?ref={ref}")
except ApiError as exc:
if exc.status == 404:
return None, ["工作分支缺少任务文件。"]
raise
if not isinstance(response, dict) or not isinstance(response.get("content"), str):
return None, ["工作分支任务文件响应格式异常。"]
try:
text = base64.b64decode(response["content"]).decode("utf-8")
except (ValueError, UnicodeDecodeError):
return None, ["工作分支任务文件不是有效 UTF-8 / base64。"]
metadata, _, errors = parse_frontmatter_text(text)
return metadata, errors
def local_tasks(root: Path) -> dict[str, dict[str, Any]]:
result: dict[str, dict[str, Any]] = {}
task_dir = root / "docs" / "tasks"
if not task_dir.is_dir():
return result
for path in sorted(task_dir.glob("T-*.md")):
metadata, _, _ = parse_frontmatter(path)
task_id = metadata.get("id")
if isinstance(task_id, str) and TASK_ID.fullmatch(task_id):
result[task_id] = metadata
return result
def audit_labels(client: GiteaClient) -> list[AuditFinding]:
findings: list[AuditFinding] = []
existing = client.list_labels()
for desired in LABELS:
current = existing.get(desired["name"])
if current is None:
findings.append(AuditFinding(0, "labels", f"缺少 {desired['name']}。"))
elif bool(current.get("exclusive")) != desired["exclusive"]:
findings.append(AuditFinding(0, "labels", f"{desired['name']} exclusive 属性不一致。"))
return findings
def audit_repository(
root: Path,
client: GiteaClient,
now: datetime,
dispatcher_login: str | None = None,
) -> tuple[list[AuditFinding], int]:
findings = audit_labels(client)
issues = [
issue
for issue in paged(client, "/issues?state=all&type=issues")
if "kind/task" in label_names(issue) and not issue.get("pull_request")
]
branches = branch_commits(paged(client, "/branches"))
pull_requests = paged(client, "/pulls?state=all")
local = local_tasks(root)
remote: dict[str, RemoteTask] = {}
for issue in issues:
number = issue.get("number")
title = issue.get("title")
body = issue.get("body")
state = issue.get("state")
if not isinstance(number, int) or not isinstance(title, str):
continue
body = body if isinstance(body, str) else ""
title_match = TASK_IN_TITLE.search(title)
body_match = BODY_TASK_ID.search(body)
task_id = title_match.group(1) if title_match else ""
if not task_id:
findings.append(AuditFinding(number, "mapping", "标题缺少 [T-编号]。"))
if body_match is None or body_match.group(1) != task_id:
findings.append(AuditFinding(number, "mapping", "正文 task_id 与标题不一致。"))
task_file_match = BODY_TASK_FILE.search(body)
if task_id and (
task_file_match is None
or task_file_match.group(1) != f"docs/tasks/{task_id}.md"
):
findings.append(AuditFinding(number, "mapping", "task_file 与任务 ID 不一致。"))
if task_id in remote:
findings.append(AuditFinding(number, "mapping", "任务 ID 映射到多个 Issue。"))
labels = label_names(issue)
statuses = sorted(labels & STATUS_LABELS)
status = statuses[0] if len(statuses) == 1 else None
local_issue = local.get(task_id, {}).get("issue") if task_id else None
if len(statuses) > 1:
findings.append(AuditFinding(number, "status", "存在多个 status/* 标签。"))
elif not statuses and local_issue == number:
findings.append(AuditFinding(number, "status", "已映射任务缺少 status/* 标签。"))
if status:
if task_id not in local or local_issue != number:
findings.append(AuditFinding(number, "mapping", "可领取 Issue 未映射默认分支任务文件。"))
else:
local_status = local[task_id].get("status")
expected_local = "DONE" if status == "status/done" else "TODO"
if local_status != expected_local:
findings.append(
AuditFinding(
number,
"status",
f"远端 {status} 要求默认分支任务为 {expected_local}。",
)
)
if status in READY_OR_ACTIVE_LABELS:
deps = local[task_id].get("deps")
if not isinstance(deps, list):
findings.append(
AuditFinding(number, "dependency", "默认分支任务 deps 不是列表。")
)
else:
unready = sorted(
str(dep)
for dep in deps
if not isinstance(dep, str)
or dep not in local
or local[dep].get("status") != "DONE"
)
if unready:
findings.append(
AuditFinding(
number,
"dependency",
"任务依赖尚未全部 DONE:" + ", ".join(unready) + "。",
)
)
for prefix in ("type/", "priority/"):
scoped = [name for name in labels if name.startswith(prefix)]
if len(scoped) != 1:
findings.append(
AuditFinding(number, "labels", f"可领取任务必须恰有一个 {prefix} 标签。")
)
if status == "status/done" and state != "closed":
findings.append(AuditFinding(number, "status", "status/done 的 Issue 必须关闭。"))
if status != "status/done" and state == "closed":
findings.append(AuditFinding(number, "status", "未完成 Issue 不应关闭。"))
task = RemoteTask(
number=number,
task_id=task_id,
state=state if isinstance(state, str) else "",
status=status,
labels=labels,
body=body,
write_paths=parse_write_paths(body),
)
if task_id:
remote[task_id] = task
local_metadata = local.get(task_id, {})
if status == "status/done":
work_branch = local_metadata.get("work_branch")
context_ref = local_metadata.get("context_ref")
write_paths = local_metadata.get("write_paths")
task.work_branch = work_branch if isinstance(work_branch, str) else None
task.context_ref = context_ref if isinstance(context_ref, str) else None
task.write_paths = (
[value for value in write_paths if isinstance(value, str)]
if isinstance(write_paths, list)
else []
)
if not task.work_branch or not task.context_ref or not task.write_paths:
findings.append(
AuditFinding(number, "task-file", "DONE 任务缺少长期分支、context_ref 或写路径。")
)
claim_branch = f"claims/{task_id}" if task_id else ""
if status in ACTIVE_LABELS:
if claim_branch not in branches:
findings.append(AuditFinding(number, "claim", "活跃任务缺少 claim 分支。"))
comments = paged(client, f"/issues/{number}/comments")
claim, claim_errors = select_latest_claim(
comments, task_id, dispatcher_login
)
for message in claim_errors:
findings.append(AuditFinding(number, "claim-author", message))
if claim is None:
findings.append(
AuditFinding(number, "claim", "活跃任务缺少由可信 dispatcher 发布的结构化 CLAIM。")
)
else:
fields = parse_fields(claim)
task.work_branch = fields.get("work_branch")
task.context_ref = fields.get("context_ref")
task.claimed_by = fields.get("claimed_by")
task.claimed_at = parse_datetime(fields.get("claimed_at"))
task.lease_until = parse_datetime(fields.get("lease_until"))
claim_paths = parse_write_paths(claim)
if claim_paths:
task.write_paths = claim_paths
required_claim = {
"task",
"claimed_by",
"allocated_by",
"context_ref",
"claim_branch",
"work_branch",
"claimed_at",
"lease_until",
}
missing_claim = sorted(required_claim - set(fields))
if missing_claim:
findings.append(
AuditFinding(
number,
"claim",
"CLAIM 缺少字段:" + ", ".join(missing_claim) + "。",
)
)
if fields.get("task") != task_id:
findings.append(AuditFinding(number, "claim", "CLAIM task 不一致。"))
if fields.get("claim_branch") != claim_branch:
findings.append(AuditFinding(number, "claim", "CLAIM claim_branch 不一致。"))
context_ref = task.context_ref
if not context_ref or not re.fullmatch(r"[0-9a-fA-F]{40}", context_ref):
findings.append(AuditFinding(number, "claim", "CLAIM context_ref 无效。"))
elif branches.get(claim_branch) != context_ref:
findings.append(AuditFinding(number, "claim", "claim 分支 SHA 与 context_ref 不一致。"))
if (
not task.claimed_by
or not re.fullmatch(r"[A-Za-z0-9._-]+", task.claimed_by)
or task.work_branch != f"agent/{task.claimed_by}/{task_id}"
):
findings.append(AuditFinding(number, "claim", "claimed_by 与工作分支命名不一致。"))
if (
not claim_paths
or len(claim_paths) != len(set(claim_paths))
or any(not is_safe_repo_path(path) for path in claim_paths)
or f"docs/tasks/{task_id}.md" not in claim_paths
):
findings.append(
AuditFinding(
number,
"claim",
"CLAIM write_paths 必须安全、唯一并包含任务文件。",
)
)
if not task.work_branch or task.work_branch not in branches:
findings.append(AuditFinding(number, "claim", "工作分支不存在。"))
elif task.work_branch:
metadata, metadata_errors = remote_task_metadata(
client, task_id, task.work_branch
)
for _ in metadata_errors:
findings.append(AuditFinding(number, "task-file", "工作分支任务文件无效。"))
if metadata is not None:
expected_status = {
"status/doing": "DOING",
"status/blocked": "BLOCKED",
"status/review": "DONE",
}.get(status)
comparisons = {
"id": task_id,
"issue": number,
"context_ref": context_ref,
"claim_branch": claim_branch,
"work_branch": task.work_branch,
"status": expected_status,
}
for key, expected in comparisons.items():
if metadata.get(key) != expected:
findings.append(
AuditFinding(
number,
"task-file",
f"工作分支任务字段 {key} 与协调状态不一致。",
)
)
metadata_paths = metadata.get("write_paths")
if not isinstance(metadata_paths, list) or set(metadata_paths) != set(
claim_paths
):
findings.append(
AuditFinding(
number,
"task-file",
"工作分支 write_paths 与 CLAIM 不一致。",
)
)
if task.claimed_at is None:
findings.append(AuditFinding(number, "stale", "CLAIM 缺少有效 claimed_at。"))
elif task.claimed_at > now + CLOCK_SKEW:
findings.append(AuditFinding(number, "stale", "claimed_at 超出允许时钟偏差。"))
if task.lease_until is None:
findings.append(AuditFinding(number, "stale", "CLAIM 缺少有效 lease_until。"))
elif task.claimed_at is not None:
if task.lease_until <= task.claimed_at:
findings.append(AuditFinding(number, "stale", "lease_until 必须晚于 claimed_at。"))
elif task.lease_until - task.claimed_at > MAX_LEASE:
findings.append(AuditFinding(number, "stale", "claim 租期不得超过 24 小时。"))
if task.lease_until <= now:
findings.append(AuditFinding(number, "stale", "claim 已过期,需人工审查回收。"))
elif status == "status/todo" and claim_branch in branches:
findings.append(AuditFinding(number, "claim", "TODO 仍存在 claim 分支。"))
matching_prs = [
pr
for pr in pull_requests
if task_id and TASK_IN_TITLE.search(str(pr.get("title") or ""))
and TASK_IN_TITLE.search(str(pr.get("title") or "")).group(1) == task_id
]
if status == "status/review":
open_prs = [pr for pr in matching_prs if pr.get("state") == "open"]
if len(open_prs) != 1:
findings.append(
AuditFinding(number, "pull-request", "status/review 必须恰有一个 open PR。")
)
else:
findings.extend(validate_pull_request(open_prs[0], task))
if status == "status/done":
merged_prs = [pr for pr in matching_prs if bool(pr.get("merged"))]
if len(merged_prs) != 1:
findings.append(
AuditFinding(number, "pull-request", "status/done 必须恰有一个 merged PR。")
)
else:
findings.extend(validate_pull_request(merged_prs[0], task))
for task_id, metadata in sorted(local.items()):
issue_number = metadata.get("issue")
if isinstance(issue_number, int):
mapped = remote.get(task_id)
if mapped is None or mapped.number != issue_number:
findings.append(AuditFinding(issue_number, "mapping", f"本地 {task_id} 没有唯一远端映射。"))
elif metadata.get("status") == "DONE" and mapped.status != "status/done":
findings.append(AuditFinding(issue_number, "status", "本地 DONE 与远端状态不一致。"))
active = sorted(
(task for task in remote.values() if task.status in ACTIVE_LABELS),
key=lambda task: task.task_id,
)
workers: dict[str, str] = {}
for task in active:
if not task.claimed_by:
continue
previous = workers.get(task.claimed_by)
if previous:
findings.append(
AuditFinding(
task.number,
"worker-overlap",
f"claimed_by 同时活跃于 {previous} 和 {task.task_id}。",
)
)
else:
workers[task.claimed_by] = task.task_id
for index, left in enumerate(active):
for right in active[index + 1 :]:
if any(
scopes_overlap(a, b)
for a in left.write_paths or []
for b in right.write_paths or []
):
findings.append(
AuditFinding(
right.number,
"scope-overlap",
f"活跃 write_paths 与 {left.task_id} 重叠。",
)
)
return sorted(set(findings)), len(issues)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="只读审计 Gitea 任务协调状态。")
parser.add_argument("--repo", help="目标 owner/repo;也可设置 GITEA_REPOSITORY。")
parser.add_argument(
"--dispatcher",
default=os.environ.get("GITEA_DISPATCHER_LOGIN"),
help="可信 dispatcher 的 Gitea 登录名;也可设置 GITEA_DISPATCHER_LOGIN。",
)
parser.add_argument(
"--root",
type=Path,
default=Path(__file__).resolve().parents[1],
help="本地仓库根目录。",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
root = args.root.resolve()
if not root.is_dir() or not (root / "docs" / "tasks").is_dir():
print("ERROR: --root 必须是包含 docs/tasks 的仓库目录。", file=sys.stderr)
return 2
repo = args.repo or os.environ.get("GITEA_REPOSITORY")
if not repo:
print("ERROR: 需要 --repo owner/repo 或 GITEA_REPOSITORY。", file=sys.stderr)
return 2
try:
root_url, owner, name = validate_config(
os.environ.get("GITEA_URL", ""),
os.environ.get("GITEA_TOKEN", ""),
repo,
)
client = GiteaClient(root_url, owner, name, os.environ["GITEA_TOKEN"])
findings, count = audit_repository(
root, client, datetime.now(timezone.utc), args.dispatcher
)
except ValueError as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 2
except ApiError as exc:
print(f"ERROR: Gitea 只读审计失败(HTTP {exc.status})。", file=sys.stderr)
return 2
except RuntimeError:
print("ERROR: Gitea 只读审计失败(网络、代理或响应格式异常)。", file=sys.stderr)
return 2
if findings:
for finding in findings:
print(finding.render(), file=sys.stderr)
print(f"Gitea 协调审计失败:{len(findings)} 项不一致。", file=sys.stderr)
return 1
print(f"Gitea 协调审计通过:检查 {count} 个 kind/task Issue,未执行远端写入。")
return 0
if __name__ == "__main__":
raise SystemExit(main())
+94
View File
@@ -0,0 +1,94 @@
#!/usr/bin/env pwsh
[CmdletBinding()]
param(
[string]$EnvFile = $(
if ($env:GITEA_ENV_FILE) {
$env:GITEA_ENV_FILE
} else {
Join-Path $HOME ".codex/gitea.env"
}
),
[string]$Version = "0.5.1",
[switch]$CheckConfig,
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$ServerArgs
)
$ErrorActionPreference = "Stop"
$utf8 = [System.Text.UTF8Encoding]::new($false)
[Console]::OutputEncoding = $utf8
$OutputEncoding = $utf8
if (-not (Test-Path -LiteralPath $EnvFile -PathType Leaf)) {
throw "Gitea MCP 配置文件不存在:$EnvFile"
}
$values = @{}
foreach ($rawLine in Get-Content -LiteralPath $EnvFile) {
$line = $rawLine.Trim()
if (-not $line -or $line.StartsWith("#")) {
continue
}
$pair = $line -split "=", 2
if ($pair.Count -ne 2) {
throw "Gitea MCP 配置行必须使用 KEY=VALUE 格式。"
}
$values[$pair[0].Trim()] = $pair[1].Trim()
}
foreach ($name in @("GITEA_URL", "GITEA_TOKEN")) {
if (-not $values.ContainsKey($name) -or [string]::IsNullOrWhiteSpace($values[$name])) {
throw "$name 未配置或为空。"
}
}
try {
$giteaUri = [Uri]$values["GITEA_URL"]
} catch {
throw "GITEA_URL 不是有效 URL。"
}
if ($giteaUri.Scheme -notin @("http", "https")) {
throw "GITEA_URL 只支持 http 或 https。"
}
if ($giteaUri.AbsolutePath.Trim("/") -ne "") {
throw "GITEA_URL 必须填写实例根地址,不要包含 /api/v1;gitea-mcp 会自动追加 API 路径。"
}
if ($giteaUri.Scheme -eq "http" -and $values["GITEA_ALLOW_INSECURE_HTTP"] -ne "1") {
throw "当前使用 HTTP。确认接受 Token 明文传输风险后,在私有配置中设置 GITEA_ALLOW_INSECURE_HTTP=1。"
}
foreach ($entry in $values.GetEnumerator()) {
if ($entry.Key -like "GITEA_*") {
Set-Item -Path "Env:$($entry.Key)" -Value $entry.Value
}
}
$noProxyEntries = @($env:NO_PROXY -split "," | ForEach-Object { $_.Trim() } | Where-Object { $_ })
if ($noProxyEntries -notcontains $giteaUri.Host) {
$noProxyEntries += $giteaUri.Host
}
$env:NO_PROXY = $noProxyEntries -join ","
$env:no_proxy = $env:NO_PROXY
if ($values["GITEA_DIRECT"] -eq "1") {
foreach ($proxyVariable in @("ALL_PROXY", "all_proxy", "HTTP_PROXY", "http_proxy", "HTTPS_PROXY", "https_proxy")) {
Remove-Item -Path "Env:$proxyVariable" -ErrorAction SilentlyContinue
}
}
if ($CheckConfig) {
Write-Output "Gitea MCP 配置有效:URL=$($giteaUri.GetLeftPart([UriPartial]::Authority)),Token 已设置,版本=$Version。"
exit 0
}
$uvx = Get-Command uvx -ErrorAction Stop
$stderrLog = Join-Path ([IO.Path]::GetTempPath()) "gitea-mcp-$PID.stderr.log"
& $uvx.Source --from "gitea-mcp==$Version" gitea-mcp @ServerArgs 2>> $stderrLog
exit $LASTEXITCODE
+297
View File
@@ -0,0 +1,297 @@
#!/usr/bin/env python3
"""Preview or idempotently apply Harness Coding labels to one Gitea repo."""
from __future__ import annotations
import argparse
import json
import os
import sys
import urllib.error
import urllib.parse
import urllib.request
from typing import Any
LABELS: tuple[dict[str, Any], ...] = (
{
"name": "kind/task",
"color": "0052CC",
"description": "Harness Coding task",
"exclusive": False,
},
{
"name": "type/docs",
"color": "5319E7",
"description": "Documentation change",
"exclusive": True,
},
{
"name": "type/code",
"color": "1D76DB",
"description": "Code or automation change",
"exclusive": True,
},
{
"name": "status/todo",
"color": "C5DEF5",
"description": "Ready to claim",
"exclusive": True,
},
{
"name": "status/doing",
"color": "FBCA04",
"description": "Claimed and in progress",
"exclusive": True,
},
{
"name": "status/blocked",
"color": "D93F0B",
"description": "Blocked; claim retained",
"exclusive": True,
},
{
"name": "status/review",
"color": "BFD4F2",
"description": "Pull request under review",
"exclusive": True,
},
{
"name": "status/done",
"color": "0E8A16",
"description": "Merged and completed",
"exclusive": True,
},
{
"name": "priority/p0",
"color": "B60205",
"description": "Highest priority",
"exclusive": True,
},
{
"name": "priority/p1",
"color": "D93F0B",
"description": "High priority",
"exclusive": True,
},
{
"name": "priority/p2",
"color": "FBCA04",
"description": "Normal priority",
"exclusive": True,
},
)
class ApiError(RuntimeError):
def __init__(self, status: int, reason: str) -> None:
super().__init__(f"Gitea API 返回 HTTP {status}:{reason}")
self.status = status
class NoRedirect(urllib.request.HTTPRedirectHandler):
"""Never forward the Authorization header to a redirected origin."""
def redirect_request(self, *args: Any, **kwargs: Any) -> None:
return None
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="读取远端差异,并可幂等创建或校正 Harness Coding Gitea 标签。"
)
parser.add_argument(
"--repo",
default=os.environ.get("GITEA_REPOSITORY"),
help="目标 owner/repo;也可设置 GITEA_REPOSITORY。",
)
parser.add_argument(
"--apply",
action="store_true",
help="应用预览中的 create/update;省略时只读远端并打印差异。",
)
return parser.parse_args()
def validate_config(url: str, token: str, repo: str) -> tuple[str, str, str]:
parsed = urllib.parse.urlsplit(url.strip())
if parsed.scheme not in {"http", "https"} or not parsed.hostname:
raise ValueError("GITEA_URL 必须是 http(s) 实例根地址。")
if parsed.username or parsed.password or parsed.query or parsed.fragment:
raise ValueError("GITEA_URL 不得包含凭据、query 或 fragment。")
path = parsed.path.rstrip("/")
if path.lower().endswith("/api/v1"):
raise ValueError("GITEA_URL 不得包含 /api/v1。")
if parsed.scheme == "http" and os.environ.get("GITEA_ALLOW_INSECURE_HTTP") != "1":
raise ValueError("HTTP 需要显式设置 GITEA_ALLOW_INSECURE_HTTP=1。")
if not token:
raise ValueError("缺少 GITEA_TOKEN。")
parts = repo.split("/")
if len(parts) != 2 or not all(parts):
raise ValueError("--repo 必须使用 owner/repo 格式。")
root = urllib.parse.urlunsplit((parsed.scheme, parsed.netloc, path, "", ""))
return root.rstrip("/"), parts[0], parts[1]
class GiteaClient:
def __init__(self, root: str, owner: str, repo: str, token: str) -> None:
owner_q = urllib.parse.quote(owner, safe="")
repo_q = urllib.parse.quote(repo, safe="")
self.base = f"{root}/api/v1/repos/{owner_q}/{repo_q}"
self.token = token
proxy_handler = (
urllib.request.ProxyHandler({})
if os.environ.get("GITEA_DIRECT") == "1"
else urllib.request.ProxyHandler()
)
self.opener = urllib.request.build_opener(proxy_handler, NoRedirect())
def request(
self, method: str, path: str, payload: dict[str, Any] | None = None
) -> Any:
data = None if payload is None else json.dumps(payload).encode("utf-8")
request = urllib.request.Request(
self.base + path,
data=data,
method=method,
headers={
"Accept": "application/json",
"Authorization": f"token {self.token}",
"Content-Type": "application/json",
},
)
try:
with self.opener.open(request, timeout=30) as response:
body = response.read()
return json.loads(body.decode("utf-8")) if body else None
except urllib.error.HTTPError as exc:
raise ApiError(exc.code, exc.reason) from None
except urllib.error.URLError as exc:
raise RuntimeError(f"连接 Gitea 失败:{exc.reason}") from None
def list_labels(self) -> dict[str, dict[str, Any]]:
result: dict[str, dict[str, Any]] = {}
seen_pages: set[tuple[str, ...]] = set()
page = 1
while True:
labels = self.request("GET", f"/labels?limit=50&page={page}")
if not isinstance(labels, list):
raise RuntimeError("Gitea labels 响应格式异常。")
if not labels:
return result
if not all(isinstance(label, dict) for label in labels):
raise RuntimeError("Gitea labels 响应包含非对象条目。")
signature = tuple(str(label.get("id") or label.get("name")) for label in labels)
if signature in seen_pages or page > 1000:
raise RuntimeError("Gitea labels 分页重复,已停止以避免无限读取。")
seen_pages.add(signature)
for label in labels:
if isinstance(label, dict) and isinstance(label.get("name"), str):
result[label["name"]] = label
page += 1
def normalize_color(value: Any) -> str:
return str(value or "").lstrip("#").upper()
def needs_update(current: dict[str, Any], desired: dict[str, Any]) -> bool:
return (
normalize_color(current.get("color")) != desired["color"]
or str(current.get("description") or "") != desired["description"]
or bool(current.get("exclusive")) != desired["exclusive"]
)
def build_plan(
existing: dict[str, dict[str, Any]],
) -> list[tuple[str, dict[str, Any], dict[str, Any] | None]]:
plan = []
for desired in LABELS:
current = existing.get(desired["name"])
if current is None:
action = "create"
elif needs_update(current, desired):
action = "update"
else:
action = "unchanged"
plan.append((action, desired, current))
return plan
def show_plan(repo: str, plan: list[tuple[str, dict[str, Any], Any]]) -> None:
print(f"目标仓库:{repo}")
for action, desired, _ in plan:
scope = "exclusive" if desired["exclusive"] else "normal"
print(f"- {action:9} {desired['name']} #{desired['color']} {scope}")
counts = {name: sum(action == name for action, _, _ in plan) for name in (
"create",
"update",
"unchanged",
)}
print(
"计划汇总:"
f"创建 {counts['create']},更新 {counts['update']},未变化 {counts['unchanged']}。"
)
def apply_plan(
client: GiteaClient,
plan: list[tuple[str, dict[str, Any], dict[str, Any] | None]],
) -> None:
created = updated = unchanged = 0
for action, desired, current in plan:
if action == "unchanged":
unchanged += 1
continue
if action == "update":
label_id = None if current is None else current.get("id")
if not isinstance(label_id, int):
raise RuntimeError(f"标签 {desired['name']} 缺少数字 id。")
client.request("PATCH", f"/labels/{label_id}", dict(desired))
updated += 1
continue
try:
client.request("POST", "/labels", dict(desired))
created += 1
except ApiError as exc:
if exc.status != 422:
raise
latest = client.list_labels().get(desired["name"])
if latest is None:
raise
if needs_update(latest, desired):
label_id = latest.get("id")
if not isinstance(label_id, int):
raise RuntimeError(f"标签 {desired['name']} 缺少数字 id。")
client.request("PATCH", f"/labels/{label_id}", dict(desired))
updated += 1
else:
unchanged += 1
print(f"标签同步完成:创建 {created},更新 {updated},未变化 {unchanged}。")
def main() -> int:
args = parse_args()
if not args.repo:
print("ERROR: 需要 --repo owner/repo 或 GITEA_REPOSITORY。", file=sys.stderr)
return 2
url = os.environ.get("GITEA_URL", "")
token = os.environ.get("GITEA_TOKEN", "")
try:
root, owner, name = validate_config(url, token, args.repo)
client = GiteaClient(root, owner, name, token)
plan = build_plan(client.list_labels())
show_plan(args.repo, plan)
if not args.apply:
print("dry-run:未写入;追加 --apply 才会应用上述 create/update。")
return 0
apply_plan(client, plan)
return 0
except (ValueError, ApiError, RuntimeError) as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main())
+166
View File
@@ -0,0 +1,166 @@
#!/usr/bin/env python3
"""Compatibility smoke for Gitea's same-name claim branch race behavior."""
from __future__ import annotations
import argparse
import os
import sys
import threading
import urllib.parse
import uuid
from concurrent.futures import ThreadPoolExecutor
from datetime import datetime, timezone
from typing import Any
from setup_gitea_labels import ApiError, GiteaClient, validate_config
PROBE_PREFIX = "claims/__probe__/race-"
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(
description="并发创建唯一临时 claim 分支,smoke 期望一个 201、一个 409。"
)
parser.add_argument("--repo", help="目标 owner/repo;也可设置 GITEA_REPOSITORY。")
parser.add_argument(
"--apply",
action="store_true",
help="执行两次写入并清理临时分支;省略时只读并打印计划。",
)
return parser.parse_args()
def new_probe_branch() -> str:
stamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ")
return f"{PROBE_PREFIX}{stamp}-{uuid.uuid4().hex[:12]}"
def branch_commit(client: GiteaClient, branch: str) -> str | None:
encoded = urllib.parse.quote(branch, safe="")
try:
response = client.request("GET", f"/branches/{encoded}")
except ApiError as exc:
if exc.status == 404:
return None
raise
if not isinstance(response, dict):
raise RuntimeError("Gitea branch 响应格式异常。")
commit = response.get("commit")
if not isinstance(commit, dict) or not isinstance(commit.get("id"), str):
raise RuntimeError("Gitea branch 响应缺少 commit.id。")
return commit["id"]
def repository_base(client: GiteaClient) -> tuple[str, str]:
repository = client.request("GET", "")
if not isinstance(repository, dict) or not isinstance(
repository.get("default_branch"), str
):
raise RuntimeError("Gitea repository 响应缺少 default_branch。")
default_branch = repository["default_branch"]
commit = branch_commit(client, default_branch)
if commit is None:
raise RuntimeError("默认分支不存在。")
return default_branch, commit
def create_once(client: GiteaClient, barrier: threading.Barrier, branch: str, ref: str) -> int:
barrier.wait(timeout=10)
try:
client.request(
"POST",
"/branches",
{"new_branch_name": branch, "old_ref_name": ref},
)
return 201
except ApiError as exc:
return exc.status
def cleanup_probe(client: GiteaClient, branch: str, expected_sha: str) -> None:
if not branch.startswith(PROBE_PREFIX):
raise RuntimeError("拒绝清理非探针分支。")
actual_sha = branch_commit(client, branch)
if actual_sha is None:
return
if actual_sha != expected_sha:
raise RuntimeError("探针分支 SHA 与预期不一致,已保留供人工检查。")
encoded = urllib.parse.quote(branch, safe="")
client.request("DELETE", f"/branches/{encoded}")
if branch_commit(client, branch) is not None:
raise RuntimeError("探针分支清理后仍然存在。")
def client_for(root: str, owner: str, repo: str, token: str) -> GiteaClient:
return GiteaClient(root, owner, repo, token)
def run_probe(root: str, owner: str, repo: str, token: str, branch: str, sha: str) -> list[int]:
barrier = threading.Barrier(2)
clients = [client_for(root, owner, repo, token) for _ in range(2)]
with ThreadPoolExecutor(max_workers=2) as executor:
futures = [
executor.submit(create_once, client, barrier, branch, sha) for client in clients
]
return sorted(future.result(timeout=40) for future in futures)
def main() -> int:
args = parse_args()
repo_value = args.repo or os.environ.get("GITEA_REPOSITORY")
if not repo_value:
print("ERROR: 需要 --repo owner/repo 或 GITEA_REPOSITORY。", file=sys.stderr)
return 2
token = os.environ.get("GITEA_TOKEN", "")
try:
root, owner, repo = validate_config(
os.environ.get("GITEA_URL", ""), token, repo_value
)
control = client_for(root, owner, repo, token)
default_branch, sha = repository_base(control)
branch = new_probe_branch()
print(f"目标仓库:{repo_value}")
print(f"基准分支:{default_branch} @ {sha}")
print(f"临时分支:{branch}")
if not args.apply:
print("dry-run:未写入;追加 --apply 才会执行竞态探针和受控清理。")
return 0
results: list[int] = []
probe_error: Exception | None = None
try:
results = run_probe(root, owner, repo, token, branch, sha)
except Exception as exc: # cleanup still has to run after partial writes
probe_error = exc
try:
cleanup_probe(control, branch, sha)
except (ApiError, RuntimeError) as cleanup_error:
print(f"ERROR: 清理失败:{cleanup_error}", file=sys.stderr)
return 2
if probe_error is not None:
print("ERROR: 竞态请求未完整返回;临时分支已安全清理。", file=sys.stderr)
return 2
print("竞态结果:" + ", ".join(str(status) for status in results))
if results != [201, 409]:
print(
"ERROR: 未得到恰好一个 201 和一个 409;目标实例不符合预期 smoke,临时分支已安全清理。",
file=sys.stderr,
)
return 1
print("claim 并发兼容性 smoke 通过;这不证明线性化,临时分支已删除并确认 404。")
return 0
except ValueError as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 2
except ApiError as exc:
print(f"ERROR: Gitea 探针失败(HTTP {exc.status})。", file=sys.stderr)
return 2
except RuntimeError as exc:
print(f"ERROR: {exc}", file=sys.stderr)
return 2
if __name__ == "__main__":
raise SystemExit(main())
+226
View File
@@ -0,0 +1,226 @@
#!/usr/bin/env python3
"""Validate the agent context manifest with the Python standard library."""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path, PurePosixPath
from typing import Any
EXPECTED_SCHEMA = "docs/agent-context.schema.json"
REQUIRED_TOP_LEVEL = {
"schema",
"schema_version",
"authority",
"bootstrap",
"routes",
"tasks",
"refresh",
"degraded_mode",
}
REQUIRED_BOOTSTRAP = {
"AGENTS.md",
"docs/00-ai-start-here.md",
"docs/05-coding-rules.md",
"docs/current-state.md",
}
TASK_PATH_KEYS = {"roadmap", "directory", "template"}
SENSITIVE_KEY = re.compile(r"(?:token|password|secret|credential)", re.IGNORECASE)
URI_SCHEME = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*:")
def load_json(path: Path, root: Path, errors: list[str]) -> Any:
try:
return json.loads(path.read_text(encoding="utf-8"))
except FileNotFoundError:
errors.append(f"文件不存在:{display_path(path, root)}")
except json.JSONDecodeError as exc:
errors.append(
f"JSON 语法错误:{display_path(path, root)}:{exc.lineno}:{exc.colno}"
)
return None
def display_path(path: Path, root: Path) -> str:
try:
return path.relative_to(root).as_posix()
except ValueError:
return path.as_posix()
def require_mapping(value: Any, name: str, errors: list[str]) -> dict[str, Any]:
if not isinstance(value, dict):
errors.append(f"{name} 必须是对象。")
return {}
return value
def require_string_list(value: Any, name: str, errors: list[str]) -> list[str]:
if not isinstance(value, list) or not value or not all(
isinstance(item, str) and item for item in value
):
errors.append(f"{name} 必须是非空字符串数组。")
return []
if len(value) != len(set(value)):
errors.append(f"{name} 不得包含重复路径。")
return value
def validate_repo_path(root: Path, value: str, name: str, errors: list[str]) -> None:
path = PurePosixPath(value)
if (
path.is_absolute()
or ".." in path.parts
or "\\" in value
or URI_SCHEME.match(value)
):
errors.append(f"{name} 必须是安全的仓库相对路径:{value}")
return
target = root.joinpath(*path.parts)
if not target.exists():
errors.append(f"{name} 引用路径不存在:{value}")
def find_sensitive_keys(value: Any, location: str, errors: list[str]) -> None:
if isinstance(value, dict):
for key, child in value.items():
child_location = f"{location}.{key}"
if SENSITIVE_KEY.search(key):
errors.append(f"清单不得保存敏感配置字段:{child_location}")
find_sensitive_keys(child, child_location, errors)
elif isinstance(value, list):
for index, child in enumerate(value):
find_sensitive_keys(child, f"{location}[{index}]", errors)
def validate_manifest(root: Path) -> list[str]:
root = root.resolve()
manifest_path = root / "docs" / "agent-context.json"
errors: list[str] = []
manifest = load_json(manifest_path, root, errors)
schema = load_json(root / EXPECTED_SCHEMA, root, errors)
if manifest is None or schema is None:
return errors
if not isinstance(schema, dict) or schema.get("type") != "object":
errors.append("agent-context.schema.json 不是有效的对象 Schema。")
root_object = require_mapping(manifest, "manifest", errors)
actual_keys = set(root_object)
missing = sorted(REQUIRED_TOP_LEVEL - actual_keys)
unexpected = sorted(actual_keys - REQUIRED_TOP_LEVEL)
if missing:
errors.append("缺少顶层字段:" + ", ".join(missing))
if unexpected:
errors.append("存在未知顶层字段:" + ", ".join(unexpected))
if root_object.get("schema") != EXPECTED_SCHEMA:
errors.append(f"schema 必须是 {EXPECTED_SCHEMA}。")
if root_object.get("schema_version") != 1:
errors.append("schema_version 必须为 1。")
authority = require_mapping(root_object.get("authority"), "authority", errors)
for key in ("bootstrap", "framework_templates", "project_facts", "coordination"):
if not isinstance(authority.get(key), str) or not authority[key]:
errors.append(f"authority.{key} 必须是非空字符串。")
bootstrap = require_mapping(root_object.get("bootstrap"), "bootstrap", errors)
always_read = require_string_list(
bootstrap.get("always_read"), "bootstrap.always_read", errors
)
missing_bootstrap = sorted(REQUIRED_BOOTSTRAP - set(always_read))
if missing_bootstrap:
errors.append("bootstrap.always_read 缺少:" + ", ".join(missing_bootstrap))
routes = require_mapping(root_object.get("routes"), "routes", errors)
if not routes:
errors.append("routes 至少需要一个任务类型。")
path_values: list[tuple[str, str]] = [(EXPECTED_SCHEMA, "schema")]
path_values.extend((path, "bootstrap.always_read") for path in always_read)
for route, value in routes.items():
paths = require_string_list(value, f"routes.{route}", errors)
path_values.extend((path, f"routes.{route}") for path in paths)
tasks = require_mapping(root_object.get("tasks"), "tasks", errors)
if set(tasks) != TASK_PATH_KEYS:
errors.append("tasks 必须且只能包含 roadmap、directory、template。")
for key in sorted(TASK_PATH_KEYS):
value = tasks.get(key)
if isinstance(value, str) and value:
path_values.append((value, f"tasks.{key}"))
else:
errors.append(f"tasks.{key} 必须是非空字符串。")
refresh = require_mapping(root_object.get("refresh"), "refresh", errors)
expected_refresh = {
"context_ref": "default_branch_head_sha",
"cache_key": "file_sha",
"unchanged_file": "reuse_within_current_session",
"changed_ref": "reread_manifest_and_routed_documents",
}
if refresh != expected_refresh:
errors.append("refresh 必须使用约定的提交 SHA 与文件 SHA 刷新策略。")
degraded = require_mapping(root_object.get("degraded_mode"), "degraded_mode", errors)
expected_degraded = {
"continue_claimed_task": True,
"claim_new_task": False,
"write_remote_state": False,
}
if degraded != expected_degraded:
errors.append("degraded_mode 必须禁止领取新任务和写入远端状态。")
for value, name in path_values:
validate_repo_path(root, value, name, errors)
find_sensitive_keys(root_object, "manifest", errors)
return errors
def manifest_summary(root: Path) -> tuple[int, int]:
manifest = json.loads(
(root / "docs" / "agent-context.json").read_text(encoding="utf-8")
)
paths = {manifest["schema"]}
paths.update(manifest["bootstrap"]["always_read"])
for values in manifest["routes"].values():
paths.update(values)
paths.update(manifest["tasks"].values())
return len(manifest["routes"]), len(paths)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="校验 Agent 上下文清单。")
parser.add_argument(
"--root",
type=Path,
default=Path(__file__).resolve().parents[1],
help="仓库根目录;默认取脚本上一级。",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
root = args.root.resolve()
if not root.is_dir():
print("ERROR: 仓库根目录不存在。", file=sys.stderr)
return 2
errors = validate_manifest(root)
if errors:
for error in errors:
print(f"ERROR: {error}", file=sys.stderr)
return 1
route_count, path_count = manifest_summary(root)
print(
"agent-context 校验通过:"
f"{route_count} 个任务路由,{path_count} 个有效仓库路径。"
)
return 0
if __name__ == "__main__":
raise SystemExit(main())
+685
View File
@@ -0,0 +1,685 @@
#!/usr/bin/env python3
"""Offline governance checks for a Harness Coding repository."""
from __future__ import annotations
import argparse
import re
import subprocess
import sys
import unicodedata
import urllib.parse
from dataclasses import dataclass
from datetime import date
from pathlib import Path, PurePosixPath
from typing import Any, Iterable
from validate_agent_context import validate_manifest
TASK_ID = re.compile(r"^T-\d{3}[a-z]?$")
SHA40 = re.compile(r"^[0-9a-fA-F]{40}$")
MARKDOWN_LINK = re.compile(r"!?\[[^\]]*\]\(([^)]+)\)")
URI_SCHEME = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*:")
AUTH_VALUE = re.compile(
r"(?i)authorization\s*[:=]\s*['\"]?(?:basic|bearer|token)\s+[A-Za-z0-9._~+/=-]{8,}"
)
TOKEN_ASSIGNMENT = re.compile(
r"(?i)^\s*\{?\s*(?:(?:export\s+)?(?:\$env:)?GITEA_TOKEN|['\"]GITEA_TOKEN['\"])"
r"\s*[:=]\s*(.*?)\s*[,}]?\s*$"
)
URL_CREDENTIAL = re.compile(r"(?i)https?://[^/\s:@]+:[^/\s@]+@")
GITEA_TOKEN_LITERAL = re.compile(r"\bgta_[A-Za-z0-9_-]{16,}\b")
CMD_TOKEN_ASSIGNMENT = re.compile(
r"(?ix)^\s*(?:"
r"setx\s+(?:\"GITEA_TOKEN\"|GITEA_TOKEN)\s+(?:\"([^\"]*)\"|(.*?))"
r"|set\s+(?:\"GITEA_TOKEN\s*=\s*([^\"]*)\"|GITEA_TOKEN\s*=\s*(.*?))"
r")\s*$"
)
DOTNET_TOKEN_SETTER = re.compile(
r"(?is)\[Environment\]::SetEnvironmentVariable\s*\(\s*['\"]GITEA_TOKEN['\"]"
r"\s*,\s*(['\"])(.*?)\1"
)
SAFE_VARIABLE_REFERENCE = re.compile(
r"(?i)(?:\$\{[A-Za-z_][A-Za-z0-9_]*\}|\$env:[A-Za-z_][A-Za-z0-9_]*|"
r"\$[A-Za-z_][A-Za-z0-9_]*|%[A-Za-z_][A-Za-z0-9_]*%)"
)
TASK_REQUIRED_FIELDS = {
"id",
"title",
"phase",
"deps",
"status",
"created",
"issue",
"context_ref",
"claim_branch",
"work_branch",
"write_paths",
}
TASK_REQUIRED_SECTIONS = {
"问题 / 背景",
"方案",
"验收要点",
"边界(不改什么)",
"协作约束",
"执行记录",
}
VALID_STATUS = {"TODO", "DOING", "DONE", "BLOCKED"}
ACTIVE_STATUS = {"DOING", "BLOCKED"}
KNOWN_TEXT_SUFFIXES = {
".md",
".py",
".ps1",
".sh",
".json",
".yaml",
".yml",
".toml",
".txt",
".env",
".example",
}
@dataclass(frozen=True, order=True)
class Finding:
rule: str
path: str
line: int
message: str
def render(self) -> str:
location = self.path if self.line <= 0 else f"{self.path}:{self.line}"
return f"ERROR [{self.rule}] {location}: {self.message}"
@dataclass
class Task:
path: Path
metadata: dict[str, Any]
body: str
@property
def task_id(self) -> str:
value = self.metadata.get("id")
return value if isinstance(value, str) else ""
@property
def status(self) -> str:
value = self.metadata.get("status")
return value if isinstance(value, str) else ""
def relative(path: Path, root: Path) -> str:
return path.relative_to(root).as_posix()
def read_text(path: Path) -> str | None:
try:
data = path.read_bytes()
if data.startswith((b"\xff\xfe", b"\xfe\xff")):
return data.decode("utf-16")
return data.decode("utf-8-sig")
except (OSError, UnicodeDecodeError):
return None
def candidate_files(root: Path) -> list[Path]:
command = [
"git",
"-C",
str(root),
"ls-files",
"--cached",
"--others",
"--exclude-standard",
"-z",
]
try:
result = subprocess.run(
command,
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL,
)
names = [name for name in result.stdout.decode("utf-8").split("\0") if name]
return sorted(root / PurePosixPath(name) for name in names if (root / name).is_file())
except (OSError, subprocess.CalledProcessError, UnicodeDecodeError):
return sorted(
path for path in root.rglob("*") if path.is_file() and ".git" not in path.parts
)
def parse_scalar(value: str) -> Any:
value = value.split(" #", 1)[0].strip()
if not value or value.lower() in {"null", "~"}:
return None
if value == "[]":
return []
if value.startswith("[") and value.endswith("]"):
inner = value[1:-1].strip()
return [] if not inner else [parse_scalar(item) for item in inner.split(",")]
if len(value) >= 2 and value[0] == value[-1] and value[0] in {"'", '"'}:
value = value[1:-1]
if value.isdigit():
return int(value)
return value
def parse_frontmatter(path: Path) -> tuple[dict[str, Any], str, list[str]]:
text = read_text(path)
if text is None:
return {}, "", ["文件不是 UTF-8 文本。"]
return parse_frontmatter_text(text)
def parse_frontmatter_text(text: str) -> tuple[dict[str, Any], str, list[str]]:
lines = text.splitlines()
if not lines or lines[0].strip() != "---":
return {}, text, ["缺少起始 frontmatter 分隔符。"]
try:
end = next(index for index in range(1, len(lines)) if lines[index].strip() == "---")
except StopIteration:
return {}, text, ["缺少结束 frontmatter 分隔符。"]
metadata: dict[str, Any] = {}
current_list: str | None = None
errors: list[str] = []
for number, raw in enumerate(lines[1:end], start=2):
if not raw.strip() or raw.lstrip().startswith("#"):
continue
item = re.match(r"^\s+-\s+(.+)$", raw)
if item and current_list:
metadata[current_list].append(parse_scalar(item.group(1)))
continue
field = re.match(r"^([A-Za-z_][A-Za-z0-9_-]*):(?:\s*(.*))?$", raw)
if not field:
errors.append(f"frontmatter 第 {number} 行语法不受支持。")
current_list = None
continue
key, raw_value = field.groups()
if key in metadata:
errors.append(f"frontmatter 字段重复:{key}。")
value = parse_scalar(raw_value or "")
if value is None and not (raw_value or "").strip():
value = []
current_list = key
else:
current_list = None
metadata[key] = value
return metadata, "\n".join(lines[end + 1 :]), errors
def is_safe_repo_path(value: str) -> bool:
path = PurePosixPath(value)
return bool(value) and value == value.strip() and not (
path.is_absolute()
or ".." in path.parts
or "\\" in value
or URI_SCHEME.match(value)
or "【" in value
or any(character in value for character in "*?[]{}")
or any(ord(character) < 32 for character in value)
)
def normalize_scope(value: str) -> tuple[str, ...]:
return tuple(
unicodedata.normalize("NFC", part).casefold()
for part in PurePosixPath(value.rstrip("/")).parts
if part not in {"."}
)
def scopes_overlap(left: str, right: str) -> bool:
left_parts = normalize_scope(left)
right_parts = normalize_scope(right)
if not left_parts or not right_parts:
return True
width = min(len(left_parts), len(right_parts))
return left_parts[:width] == right_parts[:width]
def section_content(body: str, heading: str) -> str:
pattern = re.compile(
rf"(?ms)^##\s+{re.escape(heading)}\s*$\n(.*?)(?=^##\s+|\Z)"
)
match = pattern.search(body)
return "" if match is None else match.group(1).strip()
def validate_tasks(root: Path) -> list[Finding]:
findings: list[Finding] = []
task_dir = root / "docs" / "tasks"
template = task_dir / "_template.md"
if template.is_file():
metadata, body, errors = parse_frontmatter(template)
for message in errors:
findings.append(Finding("task-template", relative(template, root), 0, message))
missing = sorted(TASK_REQUIRED_FIELDS - set(metadata))
if missing:
findings.append(
Finding(
"task-template",
relative(template, root),
0,
"缺少字段:" + ", ".join(missing),
)
)
headings = set(re.findall(r"(?m)^##\s+(.+?)\s*$", body))
missing_sections = sorted(TASK_REQUIRED_SECTIONS - headings)
if missing_sections:
findings.append(
Finding(
"task-template",
relative(template, root),
0,
"缺少章节:" + ", ".join(missing_sections),
)
)
else:
findings.append(Finding("task-template", "docs/tasks/_template.md", 0, "文件不存在。"))
tasks: dict[str, Task] = {}
issue_numbers: dict[int, str] = {}
for path in sorted(task_dir.glob("T-*.md")) if task_dir.is_dir() else []:
rel = relative(path, root)
metadata, body, errors = parse_frontmatter(path)
for message in errors:
findings.append(Finding("task-frontmatter", rel, 0, message))
filename_id = path.stem
if not TASK_ID.fullmatch(filename_id):
findings.append(Finding("task-id", rel, 0, "文件名必须是 T-<三位编号>[可选小写后缀]。"))
missing = sorted(TASK_REQUIRED_FIELDS - set(metadata))
if missing:
findings.append(
Finding("task-frontmatter", rel, 0, "缺少字段:" + ", ".join(missing))
)
task_id = metadata.get("id")
if task_id != filename_id:
findings.append(Finding("task-id", rel, 0, "frontmatter id 必须与文件名一致。"))
if isinstance(task_id, str) and task_id in tasks:
findings.append(Finding("task-id", rel, 0, "任务 ID 重复。"))
status = metadata.get("status")
if status not in VALID_STATUS:
findings.append(Finding("task-status", rel, 0, "status 不在允许枚举中。"))
title = metadata.get("title")
if not isinstance(title, str) or not title.strip() or "【" in title:
findings.append(Finding("task-metadata", rel, 0, "title 必须是已填写的非空字符串。"))
phase = metadata.get("phase")
if type(phase) is not int or phase < 0:
findings.append(Finding("task-metadata", rel, 0, "phase 必须是非负整数。"))
created = metadata.get("created")
try:
if not isinstance(created, str):
raise ValueError
date.fromisoformat(created)
except ValueError:
findings.append(Finding("task-metadata", rel, 0, "created 必须是 YYYY-MM-DD。"))
deps = metadata.get("deps")
if not isinstance(deps, list) or not all(isinstance(dep, str) for dep in deps):
findings.append(Finding("task-deps", rel, 0, "deps 必须是任务 ID 数组。"))
elif task_id in deps:
findings.append(Finding("task-deps", rel, 0, "任务不得依赖自身。"))
elif any(not TASK_ID.fullmatch(dep) for dep in deps):
findings.append(Finding("task-deps", rel, 0, "deps 含无效任务 ID。"))
write_paths = metadata.get("write_paths")
if not isinstance(write_paths, list) or not write_paths:
findings.append(Finding("task-scope", rel, 0, "write_paths 必须是非空数组。"))
else:
values = [value for value in write_paths if isinstance(value, str)]
if len(values) != len(write_paths) or any(not is_safe_repo_path(value) for value in values):
findings.append(Finding("task-scope", rel, 0, "write_paths 含不安全或非字符串路径。"))
if len(values) != len(set(values)):
findings.append(Finding("task-scope", rel, 0, "write_paths 含重复路径。"))
if rel not in values:
findings.append(Finding("task-scope", rel, 0, "write_paths 必须包含任务文件自身。"))
issue = metadata.get("issue")
if issue is not None and (type(issue) is not int or issue <= 0):
findings.append(Finding("task-issue", rel, 0, "issue 必须是正整数或 null。"))
elif type(issue) is int:
if issue in issue_numbers:
findings.append(Finding("task-issue", rel, 0, "Issue 编号与其他任务重复。"))
issue_numbers[issue] = filename_id
context_ref = metadata.get("context_ref")
if context_ref is not None and (
not isinstance(context_ref, str) or not SHA40.fullmatch(context_ref)
):
findings.append(Finding("task-claim", rel, 0, "context_ref 必须是 40 位 SHA 或 null。"))
claim_branch = metadata.get("claim_branch")
if claim_branch is not None and claim_branch != f"claims/{filename_id}":
findings.append(Finding("task-claim", rel, 0, "claim_branch 与任务 ID 不一致。"))
work_branch = metadata.get("work_branch")
if work_branch is not None and (
not isinstance(work_branch, str)
or not re.fullmatch(rf"agent/[^/]+/{re.escape(filename_id)}", work_branch)
):
findings.append(Finding("task-claim", rel, 0, "work_branch 格式或任务 ID 不一致。"))
if status == "TODO":
for key in ("context_ref", "claim_branch", "work_branch"):
if metadata.get(key) is not None:
findings.append(Finding("task-claim", rel, 0, f"TODO 的 {key} 必须为 null。"))
if issue is not None and status in ACTIVE_STATUS:
if not isinstance(context_ref, str) or not SHA40.fullmatch(context_ref):
findings.append(Finding("task-claim", rel, 0, "Gitea 活跃任务缺少 40 位 context_ref。"))
if metadata.get("claim_branch") != f"claims/{filename_id}":
findings.append(Finding("task-claim", rel, 0, "claim_branch 与任务 ID 不一致。"))
if not isinstance(work_branch, str) or not work_branch.endswith(f"/{filename_id}"):
findings.append(Finding("task-claim", rel, 0, "work_branch 与任务 ID 不一致。"))
headings = set(re.findall(r"(?m)^##\s+(.+?)\s*$", body))
missing_sections = sorted(TASK_REQUIRED_SECTIONS - headings)
if missing_sections:
findings.append(
Finding("task-sections", rel, 0, "缺少章节:" + ", ".join(missing_sections))
)
if status == "DONE":
evidence = section_content(body, "执行记录")
if not evidence or "(做完在此记录" in evidence or "【" in evidence:
findings.append(Finding("task-evidence", rel, 0, "DONE 缺少真实执行证据。"))
if isinstance(task_id, str):
tasks[task_id] = Task(path, metadata, body)
for task_id, task in sorted(tasks.items()):
rel = relative(task.path, root)
deps = task.metadata.get("deps")
if not isinstance(deps, list):
continue
for dep in deps:
if dep not in tasks:
findings.append(Finding("task-deps", rel, 0, f"依赖任务不存在:{dep}。"))
elif task.status != "TODO" and tasks[dep].status != "DONE":
findings.append(Finding("task-deps", rel, 0, f"非 TODO 任务依赖尚未 DONE:{dep}。"))
visiting: set[str] = set()
visited: set[str] = set()
def visit(task_id: str) -> None:
if task_id in visiting:
findings.append(
Finding("task-deps", relative(tasks[task_id].path, root), 0, "依赖图存在环。")
)
return
if task_id in visited:
return
visiting.add(task_id)
deps = tasks[task_id].metadata.get("deps")
if isinstance(deps, list):
for dep in deps:
if dep in tasks:
visit(dep)
visiting.remove(task_id)
visited.add(task_id)
for task_id in sorted(tasks):
visit(task_id)
active = [task for task in tasks.values() if task.status in ACTIVE_STATUS]
for index, left in enumerate(sorted(active, key=lambda task: task.task_id)):
left_paths = left.metadata.get("write_paths", [])
if not isinstance(left_paths, list):
continue
for right in sorted(active, key=lambda task: task.task_id)[index + 1 :]:
right_paths = right.metadata.get("write_paths", [])
if not isinstance(right_paths, list):
continue
if any(
isinstance(a, str) and isinstance(b, str) and scopes_overlap(a, b)
for a in left_paths
for b in right_paths
):
findings.append(
Finding(
"task-scope-overlap",
relative(right.path, root),
0,
f"活跃任务与 {left.task_id} 的 write_paths 重叠。",
)
)
return findings
def markdown_targets(text: str) -> Iterable[tuple[int, str]]:
in_fence = False
for line_number, line in enumerate(text.splitlines(), start=1):
stripped = line.lstrip()
if stripped.startswith("```") or stripped.startswith("~~~"):
in_fence = not in_fence
continue
if in_fence:
continue
for match in MARKDOWN_LINK.finditer(line):
yield line_number, match.group(1).strip()
def clean_link_target(raw: str) -> str | None:
if raw.startswith("<") and ">" in raw:
target = raw[1 : raw.index(">")]
else:
target = raw.split(maxsplit=1)[0]
target = urllib.parse.unquote(target).split("#", 1)[0].split("?", 1)[0]
if (
not target
or target.startswith("#")
or target.startswith("//")
or URI_SCHEME.match(target)
or "【" in target
):
return None
return target
def validate_markdown_links(root: Path, files: list[Path]) -> list[Finding]:
findings: list[Finding] = []
for path in files:
if path.suffix.lower() != ".md":
continue
text = read_text(path)
if text is None:
continue
for line, raw in markdown_targets(text):
target = clean_link_target(raw)
if target is None:
continue
resolved = root / target.lstrip("/") if target.startswith("/") else path.parent / target
try:
resolved.resolve().relative_to(root.resolve())
except ValueError:
findings.append(
Finding("markdown-link", relative(path, root), line, "链接逃出仓库根目录。")
)
continue
if not resolved.exists():
findings.append(
Finding("markdown-link", relative(path, root), line, "本地链接目标不存在。")
)
return findings
def validate_navigation(root: Path) -> list[Finding]:
findings: list[Finding] = []
root_readme = read_text(root / "README.md") or ""
docs_readme = read_text(root / "docs" / "README.md") or ""
for doc in sorted((root / "docs").glob("*.md")):
root_target = f"docs/{doc.name}"
if root_target not in root_readme:
findings.append(Finding("navigation", "README.md", 0, f"未登记 {root_target}。"))
if doc.name != "README.md" and f"({doc.name})" not in docs_readme:
findings.append(
Finding("navigation", "docs/README.md", 0, f"未登记 {doc.name}。")
)
required_root_entries = (
"scripts/validate_agent_context.py",
"scripts/setup_gitea_labels.py",
"scripts/validate_harness_governance.py",
"scripts/audit_gitea_coordination.py",
"scripts/test_gitea_claim_race.py",
"tests/test_governance.py",
".gitea/ISSUE_TEMPLATE/task.md",
".gitea/PULL_REQUEST_TEMPLATE.md",
".gitea/workflows/harness-governance.yml",
)
for entry in required_root_entries:
if entry not in root_readme:
findings.append(Finding("navigation", "README.md", 0, f"未登记 {entry}。"))
return findings
def safe_token_assignment(value: str) -> bool:
value = value.strip().rstrip(",}").strip().strip("'\"")
upper = value.upper()
return (
not value
or value.startswith(("【", "<"))
or SAFE_VARIABLE_REFERENCE.fullmatch(value) is not None
or upper in {"REPLACE", "CHANGEME", "EXAMPLE"}
or upper.startswith(("REPLACE_", "CHANGEME_", "EXAMPLE_"))
)
def validate_secrets(root: Path, files: list[Path]) -> list[Finding]:
findings: list[Finding] = []
for path in files:
rel = relative(path, root)
lower_name = path.name.lower()
if lower_name == "gitea.env" or (
lower_name.startswith("gitea.env.") and lower_name != "gitea.env.example"
):
findings.append(Finding("secret-file", rel, 0, "私有 Gitea 环境文件不得被跟踪。"))
text = read_text(path)
if text is None:
if path.suffix.lower() in KNOWN_TEXT_SUFFIXES or lower_name in {
".env",
"dockerfile",
"makefile",
}:
findings.append(
Finding("secret-scan", rel, 0, "已跟踪文本无法安全解码并扫描。")
)
continue
for line_number, line in enumerate(text.splitlines(), start=1):
token_assignment = TOKEN_ASSIGNMENT.match(line)
cmd_assignment = CMD_TOKEN_ASSIGNMENT.match(line)
rules = []
if token_assignment and not safe_token_assignment(token_assignment.group(1)):
rules.append("GITEA_TOKEN 实值")
if cmd_assignment:
cmd_value = next(
(value for value in cmd_assignment.groups() if value is not None),
"",
)
if not safe_token_assignment(cmd_value):
rules.append("Windows 命令 Token 实值")
if AUTH_VALUE.search(line):
rules.append("Authorization 实值")
if URL_CREDENTIAL.search(line):
rules.append("URL 内嵌凭据")
if GITEA_TOKEN_LITERAL.search(line):
rules.append("Gitea Token 字面值")
for rule in rules:
findings.append(Finding("secret-value", rel, line_number, f"检测到{rule}。"))
for match in DOTNET_TOKEN_SETTER.finditer(text):
if not safe_token_assignment(match.group(2)):
line_number = text.count("\n", 0, match.start()) + 1
findings.append(
Finding(
"secret-value",
rel,
line_number,
"检测到 .NET 环境变量 Token 实值。",
)
)
return findings
def require_markers(root: Path, path_string: str, markers: Iterable[str]) -> list[Finding]:
path = root / PurePosixPath(path_string)
if not path.is_file():
return [Finding("required-artifact", path_string, 0, "文件不存在。")]
text = read_text(path) or ""
return [
Finding("required-artifact", path_string, 0, f"缺少标记:{marker}。")
for marker in markers
if marker not in text
]
def validate_gitea_artifacts(root: Path) -> list[Finding]:
findings = []
findings.extend(
require_markers(
root,
".gitea/ISSUE_TEMPLATE/task.md",
("task_id:", "task_file:", "context_ref:", "write_paths:", "lease_until:"),
)
)
findings.extend(
require_markers(
root,
".gitea/PULL_REQUEST_TEMPLATE.md",
("Closes #", "task_file:", "context_ref:", "write_paths:", "验证证据"),
)
)
workflow = ".gitea/workflows/harness-governance.yml"
findings.extend(
require_markers(
root,
workflow,
(
"push:",
"pull_request:",
"actions/checkout@v4",
"permissions: read-all",
"persist-credentials: false",
"python -m unittest discover",
"python scripts/validate_harness_governance.py",
),
)
)
return findings
def validate_repository(root: Path) -> list[Finding]:
files = candidate_files(root)
findings = [
Finding("agent-context", "docs/agent-context.json", 0, message)
for message in validate_manifest(root)
]
findings.extend(validate_navigation(root))
findings.extend(validate_markdown_links(root, files))
findings.extend(validate_tasks(root))
findings.extend(validate_secrets(root, files))
findings.extend(validate_gitea_artifacts(root))
return sorted(set(findings))
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="离线校验 Harness Coding 仓库治理工件。")
parser.add_argument(
"--root",
type=Path,
default=Path(__file__).resolve().parents[1],
help="仓库根目录;默认取脚本上一级。",
)
return parser.parse_args()
def main() -> int:
args = parse_args()
root = args.root.resolve()
if not root.is_dir():
print("ERROR: 仓库根目录不存在。", file=sys.stderr)
return 2
findings = validate_repository(root)
if findings:
for finding in findings:
print(finding.render(), file=sys.stderr)
print(f"治理校验失败:{len(findings)} 项不一致。", file=sys.stderr)
return 1
print("治理校验通过:上下文、导航、链接、任务、模板、工作流与敏感信息均一致。")
return 0
if __name__ == "__main__":
raise SystemExit(main())