feat(governance): pin selective upstream import contract

This commit is contained in:
Jesse_Chen
2026-08-15 12:13:09 +08:00
parent 0fd111d16b
commit 8abd248f49
11 changed files with 1053 additions and 51 deletions
+268 -22
View File
@@ -1,9 +1,10 @@
#!/usr/bin/env python3
"""Safely import an explicit research allowlist into the commercial repository.
The importer is deliberately one-way and offline. It never writes semantic-merge
or protected paths, and defaults to a dry run. A source without usable Git
metadata is recorded as an auditable snapshot with ``source_commit=unknown``.
The importer is deliberately one-way and offline. It never writes semantic-merge
or protected paths, and defaults to a dry run. Non-Git archives are accepted
only when an operator binds them to an explicit commit and expected source-tree
SHA-256; missing or mismatched provenance fails closed.
"""
from __future__ import annotations
@@ -20,6 +21,7 @@ import tempfile
from datetime import UTC, datetime
from pathlib import Path, PurePosixPath
from typing import Any
from urllib.parse import urlsplit
ROOT = Path(__file__).resolve().parents[1]
DEFAULT_POLICY = ROOT / "references/cross_project_contract/sync_policy.v2.json"
@@ -27,6 +29,29 @@ EXIT_OK = 0
EXIT_SEMANTIC_REVIEW = 2
EXIT_POLICY_REJECTED = 3
EXIT_VALIDATION_FAILED = 4
COMMIT_HEX_LENGTH = 40
SHA256_HEX_LENGTH = 64
IMPORT_RECORD_DIR = ROOT / "references/cross_project_contract/imports"
REQUIRED_PROTECTED_PATTERNS = frozenset({
"skills/jyotish-birth-time-rectification/**",
"skills/jyotish-vedic-astrology/**",
"SKILL.md",
"frontend/**",
"deploy/**",
".gitea/**",
".github/**",
"frontend/db/**",
"frontend/supabase/**",
"references/oracle/commercial_skill_truth_overlay.v1.json",
"**/.env",
"**/.env.*",
"**/*payment*",
"**/*billing*",
"**/*subscription*",
"**/*entitlement*",
"**/*admin*",
"**/*service_role*",
})
class ImportRejected(ValueError):
@@ -54,8 +79,14 @@ def _safe_relative(value: str) -> str:
def _resolved_file(root: Path, relative: str, *, must_exist: bool) -> Path:
path = root / _safe_relative(relative)
root_resolved = root.resolve()
safe_relative = _safe_relative(relative)
path = root_resolved / safe_relative
current = root_resolved
for part in PurePosixPath(safe_relative).parts:
current = current / part
if current.is_symlink():
raise ImportRejected(f"symlink_component_rejected:{relative}")
if must_exist and not path.exists():
return path
resolved = path.resolve(strict=must_exist)
@@ -81,12 +112,20 @@ def load_policy(path: Path) -> dict[str, Any]:
raise ImportRejected("reverse_sync_must_be_forbidden")
if "commercial_to_research" in json.dumps(policy, sort_keys=True):
raise ImportRejected("reverse_direction_is_not_expressible")
expected_identity = _repository_identity(str(policy.get("source_repository_url", "")))
if expected_identity is None:
raise ImportRejected("source_repository_url_invalid")
if expected_identity[1] != str(policy.get("source_repository", "")).strip("/").casefold():
raise ImportRejected("source_repository_url_slug_mismatch")
modes = policy.get("modes")
if not isinstance(modes, dict) or set(modes) != {"mirror", "semantic_merge", "protected"}:
raise ImportRejected("policy_modes_invalid")
if not isinstance(modes["mirror"], list) or not isinstance(modes["semantic_merge"], list):
raise ImportRejected("policy_allowlists_invalid")
protected = [_safe_relative(value) if "*" not in value else value.replace("\\", "/") for value in modes["protected"]]
missing_protected = sorted(REQUIRED_PROTECTED_PATTERNS - set(protected))
if missing_protected:
raise ImportRejected(f"required_protected_patterns_missing:{','.join(missing_protected)}")
for mapping in modes["mirror"]:
if not isinstance(mapping, dict) or not {"source", "target", "license"} <= set(mapping):
raise ImportRejected("mirror_mapping_invalid")
@@ -108,12 +147,71 @@ def _git_output(root: Path, *args: str) -> str | None:
return None
def source_identity(source: Path) -> tuple[str, str]:
def _normalized_hex(value: str | None, *, length: int, label: str) -> str | None:
if value is None:
return None
normalized = value.strip().lower()
if len(normalized) != length or any(ch not in "0123456789abcdef" for ch in normalized):
raise ImportRejected(f"{label}_invalid")
return normalized
def source_identity(
source: Path,
*,
source_mode: str = "auto",
source_commit: str | None = None,
expected_tree_sha256: str | None = None,
actual_tree_sha256: str | None = None,
) -> tuple[str, str]:
if source_mode not in {"auto", "git", "archive"}:
raise ImportRejected("source_mode_must_be_auto_git_or_archive")
if source_mode != "archive" and (source_commit is None) != (expected_tree_sha256 is None):
raise ImportRejected("source_commit_and_expected_tree_sha256_must_be_provided_together")
explicit_commit = _normalized_hex(
source_commit, length=COMMIT_HEX_LENGTH, label="source_commit"
)
expected_tree = _normalized_hex(
expected_tree_sha256, length=SHA256_HEX_LENGTH, label="expected_tree_sha256"
)
actual_tree = actual_tree_sha256 or source_tree_hash(source)
inside = _git_output(source, "rev-parse", "--show-toplevel")
commit = _git_output(source, "rev-parse", "HEAD") if inside and Path(inside).resolve() == source.resolve() else None
if commit and len(commit) == 40 and all(ch in "0123456789abcdef" for ch in commit.lower()):
return "git", commit.lower()
return "snapshot", "unknown"
detected_commit = (
_git_output(source, "rev-parse", "HEAD")
if inside and Path(inside).resolve() == source.resolve()
else None
)
detected_commit = _normalized_hex(
detected_commit, length=COMMIT_HEX_LENGTH, label="detected_source_commit"
)
if source_mode == "archive" and detected_commit:
raise ImportRejected("archive_source_must_not_be_git_checkout")
if source_mode != "archive" and detected_commit:
status = _git_output(source, "status", "--porcelain=v1", "--untracked-files=all")
ignored = _git_output(source, "ls-files", "--others", "--ignored", "--exclude-standard")
if status is None or ignored is None:
raise ImportRejected("git_source_status_unavailable")
if status or ignored:
raise ImportRejected("git_source_must_be_clean")
if explicit_commit and explicit_commit != detected_commit:
raise ImportRejected("source_commit_mismatch")
if expected_tree and expected_tree != actual_tree:
raise ImportRejected("source_tree_sha256_mismatch")
return "git", detected_commit
if source_mode == "git":
raise ImportRejected("git_source_metadata_unavailable")
if explicit_commit is None:
raise ImportRejected("archive_source_commit_required")
if expected_tree is None:
raise ImportRejected("archive_expected_tree_sha256_required")
if expected_tree != actual_tree:
raise ImportRejected("source_tree_sha256_mismatch")
return "archive", explicit_commit
def _normalized_remote(root: Path) -> str | None:
@@ -124,6 +222,54 @@ def _normalized_remote(root: Path) -> str | None:
return value.rstrip("/")
def _repository_identity(value: str) -> tuple[str, str] | None:
value = value.strip().removesuffix(".git").rstrip("/")
if not value:
return None
if "://" in value:
parsed = urlsplit(value)
host = (parsed.hostname or "").casefold()
path = parsed.path
elif ":" in value and "@" in value.split(":", 1)[0]:
host = value.split("@", 1)[1].split(":", 1)[0].casefold()
path = value.split(":", 1)[1]
else:
return None
parts = [part for part in path.split("/") if part]
if not host or len(parts) < 2:
return None
return host, "/".join(parts).casefold()
def _remote_repository_identity(root: Path) -> tuple[str, str] | None:
remote = _git_output(root, "remote", "get-url", "origin")
return _repository_identity(remote or "")
def validate_git_source_repository(source: Path, expected_repository: str, expected_url: str) -> None:
expected = expected_repository.strip().removesuffix(".git").strip("/").casefold()
expected_identity = _repository_identity(expected_url)
actual = _remote_repository_identity(source)
if actual is None:
raise ImportRejected("git_source_origin_unavailable")
if expected_identity is None or expected_identity[1] != expected:
raise ImportRejected("source_repository_url_invalid")
if actual != expected_identity:
raise ImportRejected(f"git_source_repository_mismatch:{actual[0]}/{actual[1]}")
def validate_clean_target(target: Path) -> None:
top_level = _git_output(target, "rev-parse", "--show-toplevel")
if not top_level or Path(top_level).resolve() != target.resolve():
raise ImportRejected("target_must_be_git_repository_root")
status = _git_output(target, "status", "--porcelain=v1", "--untracked-files=all")
ignored = _git_output(target, "ls-files", "--others", "--ignored", "--exclude-standard")
if status is None or ignored is None:
raise ImportRejected("target_status_unavailable")
if status or ignored:
raise ImportRejected("target_must_be_clean")
def validate_roots(source: Path, target: Path) -> None:
source = source.expanduser().resolve(strict=True)
target = target.expanduser().resolve(strict=True)
@@ -141,7 +287,11 @@ def source_tree_hash(source: Path) -> str:
digest = hashlib.sha256()
for path in sorted(source.rglob("*"), key=lambda item: item.relative_to(source).as_posix()):
relative = path.relative_to(source).as_posix()
if ".git" in PurePosixPath(relative).parts or path.is_symlink() or not path.is_file():
if ".git" in PurePosixPath(relative).parts:
continue
if path.is_symlink():
raise ImportRejected(f"source_tree_symlink_rejected:{relative}")
if not path.is_file():
continue
digest.update(relative.encode("utf-8"))
digest.update(b"\0")
@@ -150,6 +300,37 @@ def source_tree_hash(source: Path) -> str:
return digest.hexdigest()
def _normalized_commit_timestamp(value: str | None) -> str | None:
if not value:
return None
try:
parsed = datetime.fromisoformat(value.replace("Z", "+00:00"))
except ValueError:
return None
if parsed.tzinfo is None:
return None
return parsed.astimezone(UTC).isoformat(timespec="seconds").replace("+00:00", "Z")
def deterministic_generated_at(
source: Path,
*,
source_mode: str,
source_commit: str,
target: Path,
target_commit: str,
) -> str:
timestamp = None
if source_mode == "git":
timestamp = _git_output(source, "show", "-s", "--format=%cI", source_commit)
if timestamp is None:
timestamp = _git_output(target, "show", "-s", "--format=%cI", target_commit)
normalized = _normalized_commit_timestamp(timestamp)
if normalized is None:
raise ImportRejected("deterministic_generated_at_unavailable")
return normalized
def _privacy_rejection(path: Path, relative: str, privacy: dict[str, Any]) -> str | None:
lowered = relative.casefold()
basenames = {str(value).casefold() for value in privacy.get("forbidden_basenames", [])}
@@ -189,17 +370,50 @@ def build_manifest(
policy_path: Path = DEFAULT_POLICY,
target: Path = ROOT,
apply: bool = False,
source_mode: str = "auto",
source_commit: str | None = None,
expected_tree_sha256: str | None = None,
tests_run: list[str] | None = None,
) -> dict[str, Any]:
source = source.expanduser().resolve(strict=True)
target = target.expanduser().resolve(strict=True)
validate_roots(source, target)
policy = load_policy(policy_path)
validate_roots(source, target)
validate_clean_target(target)
protected = list(policy["modes"]["protected"])
privacy = policy.get("privacy") if isinstance(policy.get("privacy"), dict) else {}
source_mode, source_commit = source_identity(source)
target_commit = _git_output(target, "rev-parse", "HEAD")
if not target_commit or len(target_commit) != 40:
actual_tree_sha256 = source_tree_hash(source)
resolved_source_mode, resolved_source_commit = source_identity(
source,
source_mode=source_mode,
source_commit=source_commit,
expected_tree_sha256=expected_tree_sha256,
actual_tree_sha256=actual_tree_sha256,
)
if resolved_source_mode == "git":
validate_git_source_repository(
source,
str(policy["source_repository"]),
str(policy["source_repository_url"]),
)
source_skill_file = _resolved_file(source, "SKILL.md", must_exist=True)
if not source_skill_file.exists():
raise ImportRejected("root_skill_missing")
source_skill_sha256 = sha256_file(source_skill_file)
target_commit = _normalized_hex(
_git_output(target, "rev-parse", "HEAD"),
length=COMMIT_HEX_LENGTH,
label="target_base_commit",
)
if target_commit is None:
raise ImportRejected("target_base_commit_unavailable")
generated_at = deterministic_generated_at(
source,
source_mode=resolved_source_mode,
source_commit=resolved_source_commit,
target=target,
target_commit=target_commit,
)
mirror_files: list[dict[str, Any]] = []
privacy_rejections: list[str] = []
@@ -291,22 +505,41 @@ def build_manifest(
return {
"schema_version": 1,
"source_repository": policy["source_repository"],
"source_commit": source_commit,
"source_tree_hash": source_tree_hash(source),
"source_mode": source_mode,
"source_repository_url": policy["source_repository_url"],
"source_commit": resolved_source_commit,
"source_tree_hash": actual_tree_sha256,
"source_skill_sha256": source_skill_sha256,
"source_mode": resolved_source_mode,
"target_repository": policy["target_repository"],
"target_base_commit": target_commit.lower(),
"target_base_commit": target_commit,
"policy_version": 2,
"generated_at": datetime.now(UTC).isoformat().replace("+00:00", "Z"),
"generated_at": generated_at,
"mirror_files": mirror_files,
"semantic_merge_files": semantic_files,
"protected_rejections": [],
"tests_run": [],
"tests_run": sorted(set(tests_run or [])),
"privacy_scan": {"status": "pass", "scanned_files": len(mirror_files), "rejections": []},
"operator_review_required": any(row["status"] in {"review_required", "target_missing"} for row in semantic_files),
}
def validate_output_path(path: Path, *, target: Path = ROOT, source: Path | None = None) -> Path:
"""Allow manifests outside the target or inside the dedicated import-record directory only."""
resolved = path.expanduser().resolve(strict=False)
target_resolved = target.expanduser().resolve(strict=True)
if source is not None:
source_resolved = source.expanduser().resolve(strict=True)
if resolved == source_resolved or source_resolved in resolved.parents:
raise ImportRejected("manifest_output_must_not_overlap_source")
if resolved == target_resolved or target_resolved in resolved.parents:
import_root = (target_resolved / IMPORT_RECORD_DIR.relative_to(ROOT)).resolve(strict=False)
if import_root not in resolved.parents:
raise ImportRejected("manifest_output_must_be_external_or_import_record_json")
if resolved.suffix.casefold() != ".json":
raise ImportRejected("manifest_output_must_be_json")
return resolved
def write_json_atomic(path: Path, value: dict[str, Any]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
data = (json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True) + "\n").encode("utf-8")
@@ -326,14 +559,27 @@ def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--source", type=Path, required=True)
parser.add_argument("--policy", type=Path, default=DEFAULT_POLICY)
parser.add_argument("--source-mode", choices=("auto", "git", "archive"), default="auto")
parser.add_argument("--source-commit")
parser.add_argument("--expected-tree-sha256")
parser.add_argument("--test-run", action="append", default=[], help="Test path recorded in the manifest; repeatable.")
mode = parser.add_mutually_exclusive_group()
mode.add_argument("--dry-run", action="store_true")
mode.add_argument("--apply", action="store_true")
parser.add_argument("--output", type=Path, required=True)
args = parser.parse_args(argv)
try:
manifest = build_manifest(args.source, policy_path=args.policy, apply=args.apply)
write_json_atomic(args.output, manifest)
output = validate_output_path(args.output, source=args.source)
manifest = build_manifest(
args.source,
policy_path=args.policy,
apply=args.apply,
source_mode=args.source_mode,
source_commit=args.source_commit,
expected_tree_sha256=args.expected_tree_sha256,
tests_run=args.test_run,
)
write_json_atomic(output, manifest)
except ImportRejected as error:
print(json.dumps({"status": "rejected", "reason": str(error)}, ensure_ascii=False), file=sys.stderr)
return EXIT_POLICY_REJECTED