Files
llm-wiki/harness/runtime/semantic_certificate.py
T

415 lines
20 KiB
Python

#!/usr/bin/env python3
"""Issue and validate byte-bound semantic certificates for design-bearing documents."""
from __future__ import annotations
import argparse
import hashlib
import json
import os
from pathlib import Path
import re
import sys
from typing import Any, Iterable, Mapping
import semantic_audit
import semantic_candidate_builder
import semantic_surface_extractor
import typed_contract_check
DEFAULT_ROOT = Path(__file__).resolve().parents[2]
DEFAULT_STATE = Path("harness/state/semantic-certificates")
DEFAULT_AGENT_METADATA = Path("harness/source/agents/wiki-semantic-coherence-auditor.json")
DEFAULT_AGENT_BODY = Path("harness/source/agents/bodies/wiki-semantic-coherence-auditor.md")
SCHEMA_VERSION = "semantic-certificate/v1"
RESULT_SCHEMA = "semantic-certificate-result/v1"
HEX_SHA256 = re.compile(r"^[0-9a-f]{64}$")
class SemanticCertificateError(ValueError):
def __init__(self, code: str, message: str, path: str = "") -> None:
self.code = code
self.path = path
super().__init__(message)
def _source_path(root: Path, value: Path) -> Path:
return value if value.is_absolute() else root / value
def _file_sha(path: Path) -> str:
return hashlib.sha256(path.read_bytes()).hexdigest()
def _canonical_sha(value: Any) -> str:
return hashlib.sha256(semantic_audit.canonical_json_bytes(value)).hexdigest()
def document_id(relative: str) -> str:
candidate = Path(relative)
if not relative or candidate.is_absolute() or ".." in candidate.parts or "\\" in relative:
raise SemanticCertificateError("INVALID_CERTIFICATE_SUBJECT", "subject must be a canonical repo-relative POSIX path", relative)
return hashlib.sha256(relative.encode("utf-8")).hexdigest()
def certificate_path(root: Path, subject: str, document_sha256: str, state_path: Path = DEFAULT_STATE) -> Path:
if not HEX_SHA256.fullmatch(document_sha256):
raise SemanticCertificateError("INVALID_DOCUMENT_SHA256", "document sha256 is invalid", subject)
state = _source_path(root, state_path)
return state / document_id(subject) / f"{document_sha256}.json"
def logical_subject(root: Path, document: Path) -> str:
"""Return the stable pre-cutover identity for an active document path."""
relative = document.resolve().relative_to(root.resolve()).as_posix()
layout_path = root / "harness/source/vault-layout.json"
if not layout_path.is_file():
return relative
try:
layout = json.loads(layout_path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError):
return relative
if layout.get("mode") != "canonical":
return relative
migration = layout.get("migration_manifest")
entries = migration.get("entries") if isinstance(migration, Mapping) else None
if not isinstance(entries, list):
return relative
matches = [
str(item.get("legacy_path"))
for item in entries
if isinstance(item, Mapping) and item.get("canonical_path") == relative
]
if len(matches) != 1:
return relative
return matches[0]
def _policy_hashes(
root: Path,
*,
policy_path: Path = semantic_surface_extractor.DEFAULT_POLICY,
ontology_path: Path = semantic_candidate_builder.DEFAULT_ONTOLOGY,
agent_metadata_path: Path = DEFAULT_AGENT_METADATA,
agent_body_path: Path = DEFAULT_AGENT_BODY,
) -> dict[str, str]:
policy = _source_path(root, policy_path)
ontology = _source_path(root, ontology_path)
metadata = _source_path(root, agent_metadata_path)
body = _source_path(root, agent_body_path)
for source in (policy, ontology, metadata, body):
if not source.is_file():
raise SemanticCertificateError("SEMANTIC_POLICY_SOURCE_MISSING", "certificate binding source is missing", source.as_posix())
contract_payload = metadata.read_bytes() + b"\0" + body.read_bytes()
return {
"policy_sha256": _file_sha(policy),
"ontology_sha256": _file_sha(ontology),
"auditor_contract_sha256": hashlib.sha256(contract_payload).hexdigest(),
}
def _typed_hash(root: Path) -> str:
result = typed_contract_check.check(root)
if result.get("status") != "PASS":
codes = sorted({str(item.get("code", "UNKNOWN")) for item in result.get("findings", [])})
raise SemanticCertificateError("TYPED_CONTRACT_FAILED", ",".join(codes))
return str(result["typed_contract_graph_sha256"])
def build_certificate(
root: Path,
validated_audit: Mapping[str, Any],
*,
audit_request: Mapping[str, Any],
audit_result: Mapping[str, Any],
run_root: Path | None = None,
policy_path: Path = semantic_surface_extractor.DEFAULT_POLICY,
ontology_path: Path = semantic_candidate_builder.DEFAULT_ONTOLOGY,
agent_metadata_path: Path = DEFAULT_AGENT_METADATA,
agent_body_path: Path = DEFAULT_AGENT_BODY,
) -> dict[str, Any]:
root = root.resolve(strict=True)
try:
revalidated = semantic_audit.validate_result(root, audit_request, audit_result, run_root=run_root)
except semantic_audit.SemanticAuditError as exc:
raise SemanticCertificateError("INVALID_SEMANTIC_AUDIT", str(exc)) from exc
if semantic_audit.canonical_json_bytes(revalidated) != semantic_audit.canonical_json_bytes(validated_audit):
raise SemanticCertificateError("INVALID_SEMANTIC_AUDIT", "validated audit differs from request/result replay")
if validated_audit.get("schema_version") != semantic_audit.VALIDATED_RESULT_SCHEMA:
raise SemanticCertificateError("INVALID_SEMANTIC_AUDIT", "validated semantic audit schema mismatch")
subject = str(validated_audit.get("subject", ""))
doc_id = document_id(subject)
document = Path(os.path.abspath(root / subject))
try:
document.relative_to(root)
document.resolve(strict=True).relative_to(root)
except ValueError as exc:
raise SemanticCertificateError("INVALID_CERTIFICATE_SUBJECT", "subject escapes repository", subject) from exc
if not document.is_file():
raise SemanticCertificateError("CERTIFICATE_SUBJECT_MISSING", "subject document does not exist", subject)
document_sha = _file_sha(document)
if document_sha != validated_audit.get("document_sha256"):
raise SemanticCertificateError("SEMANTIC_CERTIFICATE_STALE", "audit is not bound to current document bytes", subject)
coverage = validated_audit.get("coverage")
counts = validated_audit.get("counts")
if not isinstance(coverage, Mapping) or not isinstance(counts, Mapping):
raise SemanticCertificateError("INVALID_SEMANTIC_AUDIT", "audit coverage/counts are missing", subject)
if coverage.get("eligible_surfaces") != coverage.get("processed_surfaces"):
raise SemanticCertificateError("SEMANTIC_SURFACE_UNCOVERED", "audit did not process every eligible surface", subject)
if coverage.get("candidate_pairs") != coverage.get("processed_pairs"):
raise SemanticCertificateError("SEMANTIC_PAIR_UNCOVERED", "audit did not process every candidate pair", subject)
mode = validated_audit.get("mode")
if mode not in {"local", "hub"}:
raise SemanticCertificateError("INVALID_SEMANTIC_AUDIT", "audit mode is invalid", subject)
proof_hashes = sorted({str(item["proof_manifest_sha256"]) for item in validated_audit.get("findings", [])})
hashes = _policy_hashes(
root,
policy_path=policy_path,
ontology_path=ontology_path,
agent_metadata_path=agent_metadata_path,
agent_body_path=agent_body_path,
)
verdict = "PASS" if validated_audit.get("status") == "PASS" else "FAIL"
return {
"schema_version": SCHEMA_VERSION,
"subject": subject,
"document_id": doc_id,
"document_sha256": document_sha,
**hashes,
"typed_contract_graph_sha256": _typed_hash(root),
"mode": mode,
"verdict": verdict,
"coverage": {
"eligible_surfaces": int(coverage["eligible_surfaces"]),
"processed_surfaces": int(coverage["processed_surfaces"]),
"candidate_pairs": int(coverage["candidate_pairs"]),
"processed_pairs": int(coverage["processed_pairs"]),
"dropped_pairs": int(coverage["dropped_pairs"]),
},
"findings": {
"blocking": int(counts["blocking"]),
"readiness_blocking": int(counts["readiness_blocking"]),
"verified": int(counts["verified_findings"]),
},
"proof_manifest_sha256": _canonical_sha(proof_hashes),
"audit_request_sha256": str(validated_audit["request_sha256"]),
"semantic_audit_sha256": _canonical_sha(validated_audit),
"audit_request": dict(audit_request),
"audit_result": dict(audit_result),
"auditor": dict(validated_audit["auditor"]),
}
def prepare_certificate(
root: Path,
validated_audit: Mapping[str, Any],
*,
state_path: Path = DEFAULT_STATE,
**kwargs: Any,
) -> tuple[Path, bytes, dict[str, Any]]:
certificate = build_certificate(root, validated_audit, **kwargs)
path = certificate_path(root, certificate["subject"], certificate["document_sha256"], state_path)
return path, semantic_audit.canonical_json_bytes(certificate), certificate
def _load_certificate(path: Path) -> Mapping[str, Any]:
try:
value = json.loads(path.read_text(encoding="utf-8"))
except (OSError, UnicodeError, json.JSONDecodeError) as exc:
raise SemanticCertificateError("INVALID_SEMANTIC_CERTIFICATE", str(exc), path.as_posix()) from exc
required = {
"schema_version", "subject", "document_id", "document_sha256", "policy_sha256", "ontology_sha256",
"auditor_contract_sha256", "typed_contract_graph_sha256", "mode", "verdict", "coverage", "findings",
"proof_manifest_sha256", "audit_request_sha256", "semantic_audit_sha256", "audit_request", "audit_result",
"auditor",
}
if not isinstance(value, dict) or set(value) != required or value.get("schema_version") != SCHEMA_VERSION:
raise SemanticCertificateError("INVALID_SEMANTIC_CERTIFICATE", "certificate has missing or unknown fields", path.as_posix())
return value
def validate_certificate(
root: Path,
path: Path,
*,
state_path: Path = DEFAULT_STATE,
policy_path: Path = semantic_surface_extractor.DEFAULT_POLICY,
ontology_path: Path = semantic_candidate_builder.DEFAULT_ONTOLOGY,
agent_metadata_path: Path = DEFAULT_AGENT_METADATA,
agent_body_path: Path = DEFAULT_AGENT_BODY,
) -> dict[str, Any]:
root = root.resolve(strict=True)
path = path.resolve(strict=True)
certificate = _load_certificate(path)
subject = str(certificate["subject"])
expected_path = certificate_path(root, subject, str(certificate["document_sha256"]), state_path).resolve()
if path != expected_path:
raise SemanticCertificateError("INVALID_CERTIFICATE_PATH", "certificate path does not match document-id/document sha", path.as_posix())
if certificate["document_id"] != document_id(subject):
raise SemanticCertificateError("SEMANTIC_CERTIFICATE_STALE", "document id mismatch", subject)
audit_request = certificate.get("audit_request")
audit_result = certificate.get("audit_result")
if not isinstance(audit_request, Mapping) or not isinstance(audit_result, Mapping):
raise SemanticCertificateError("INVALID_SEMANTIC_CERTIFICATE", "embedded audit artifacts must be objects", subject)
try:
replayed = semantic_audit.validate_result(root, audit_request, audit_result)
except semantic_audit.SemanticAuditError as exc:
raise SemanticCertificateError("SEMANTIC_CERTIFICATE_STALE", f"embedded audit replay failed: {exc}", subject) from exc
if certificate.get("audit_request_sha256") != _canonical_sha(audit_request):
raise SemanticCertificateError("SEMANTIC_CERTIFICATE_STALE", "audit request hash mismatch", subject)
if certificate.get("semantic_audit_sha256") != _canonical_sha(replayed):
raise SemanticCertificateError("SEMANTIC_CERTIFICATE_STALE", "semantic audit hash mismatch", subject)
replayed_hashes = sorted({str(item["proof_manifest_sha256"]) for item in replayed.get("findings", [])})
if certificate.get("proof_manifest_sha256") != _canonical_sha(replayed_hashes):
raise SemanticCertificateError("SEMANTIC_CERTIFICATE_STALE", "proof manifest binding changed", subject)
document = Path(os.path.abspath(root / subject))
try:
document.relative_to(root)
document.resolve(strict=True).relative_to(root)
except (ValueError, FileNotFoundError) as exc:
raise SemanticCertificateError("SEMANTIC_CERTIFICATE_STALE", "document subject is missing or escapes repository", subject) from exc
if not document.is_file() or _file_sha(document) != certificate["document_sha256"]:
raise SemanticCertificateError("SEMANTIC_CERTIFICATE_STALE", "document bytes changed", subject)
hashes = _policy_hashes(
root,
policy_path=policy_path,
ontology_path=ontology_path,
agent_metadata_path=agent_metadata_path,
agent_body_path=agent_body_path,
)
for key, expected in hashes.items():
if certificate.get(key) != expected:
raise SemanticCertificateError("SEMANTIC_CERTIFICATE_STALE", f"{key} changed", subject)
if certificate.get("typed_contract_graph_sha256") != _typed_hash(root):
raise SemanticCertificateError("SEMANTIC_CERTIFICATE_STALE", "typed contract graph changed", subject)
expected_verdict = "PASS" if replayed.get("status") == "PASS" else "FAIL"
if certificate.get("verdict") != expected_verdict:
raise SemanticCertificateError("SEMANTIC_CERTIFICATE_STALE", "audit verdict changed", subject)
if certificate.get("auditor") != replayed.get("auditor"):
raise SemanticCertificateError("SEMANTIC_CERTIFICATE_STALE", "auditor identity changed", subject)
if certificate.get("mode") != replayed.get("mode") or certificate.get("document_sha256") != replayed.get("document_sha256"):
raise SemanticCertificateError("SEMANTIC_CERTIFICATE_STALE", "audit subject binding changed", subject)
policy = semantic_surface_extractor.load_policy(root, policy_path)
extraction = semantic_surface_extractor.extract_document(root, document, policy)
if extraction["mode"] != certificate.get("mode"):
raise SemanticCertificateError("SEMANTIC_CERTIFICATE_STALE", "semantic mode changed", subject)
coverage = certificate.get("coverage")
findings = certificate.get("findings")
if not isinstance(coverage, Mapping) or not isinstance(findings, Mapping):
raise SemanticCertificateError("INVALID_SEMANTIC_CERTIFICATE", "coverage/findings must be objects", subject)
if coverage.get("eligible_surfaces") != extraction["coverage"]["eligible_surface_blocks"]:
raise SemanticCertificateError("SEMANTIC_CERTIFICATE_STALE", "eligible surface count changed", subject)
replayed_coverage = replayed.get("coverage", {})
replayed_counts = replayed.get("counts", {})
expected_coverage = {
"eligible_surfaces": replayed_coverage.get("eligible_surfaces"),
"processed_surfaces": replayed_coverage.get("processed_surfaces"),
"candidate_pairs": replayed_coverage.get("candidate_pairs"),
"processed_pairs": replayed_coverage.get("processed_pairs"),
"dropped_pairs": replayed_coverage.get("dropped_pairs"),
}
expected_findings = {
"blocking": replayed_counts.get("blocking"),
"readiness_blocking": replayed_counts.get("readiness_blocking"),
"verified": replayed_counts.get("verified_findings"),
}
if dict(coverage) != expected_coverage or dict(findings) != expected_findings:
raise SemanticCertificateError("SEMANTIC_CERTIFICATE_STALE", "audit coverage or finding counts changed", subject)
if coverage.get("eligible_surfaces") != coverage.get("processed_surfaces"):
raise SemanticCertificateError("SEMANTIC_SURFACE_UNCOVERED", "certificate surface coverage is incomplete", subject)
if coverage.get("candidate_pairs") != coverage.get("processed_pairs"):
raise SemanticCertificateError("SEMANTIC_PAIR_UNCOVERED", "certificate pair coverage is incomplete", subject)
if certificate.get("verdict") != "PASS" or findings.get("blocking") != 0 or findings.get("readiness_blocking") != 0:
raise SemanticCertificateError("SEMANTIC_BLOCKING_VERDICT", "certificate contains a blocking semantic result", subject)
if certificate["mode"] == "hub" and coverage.get("dropped_pairs") != 0:
raise SemanticCertificateError("SEMANTIC_PAIR_DROPPED", "hub certificate contains dropped candidates", subject)
return dict(certificate)
def check(
root: Path,
*,
mode: str | None = None,
paths: Iterable[Path] | None = None,
state_path: Path = DEFAULT_STATE,
) -> dict[str, Any]:
root = root.resolve(strict=True)
policy = semantic_surface_extractor.load_policy(root)
selected = tuple(paths) if paths is not None else semantic_surface_extractor.eligible_documents(root, policy, required_only=True, mode=mode)
findings: list[dict[str, Any]] = []
current: list[dict[str, Any]] = []
# 자동 탐색이 0건이면 "요구 문서가 없어 전부 최신" 처럼 PASS 로 보이지만, 실제로는
# 탐색이 깨져 인증 검사를 *조용히 건너뛴* 상태일 수 있다. 명시적 paths 는 호출자 책임이나,
# 자동 탐색의 0건은 loud FAIL. 단 mode 필터(local/hub)의 0건은 "그 모드 문서가 없을 뿐"
# 이라 정상일 수 있으므로(예: local 만 있고 hub 는 없음), 필터 없는 전수 탐색에만 적용한다.
if paths is None and mode is None and not selected:
findings.append({
"code": "NO_REQUIRED_DOCUMENTS",
"path": "",
"line": 0,
"message": "자동 탐색이 인증 대상(설계-보유) 문서를 0건 발견 — 탐색이 깨졌을 수 있음.",
})
for raw in selected:
document = raw if raw.is_absolute() else root / raw
extraction = semantic_surface_extractor.extract_document(root, document, policy)
if mode is not None and extraction["mode"] != mode:
continue
subject = logical_subject(root, document)
path = certificate_path(root, subject, extraction["document_sha256"], state_path)
if not path.is_file():
findings.append({
"code": "SEMANTIC_CERTIFICATE_MISSING",
"path": extraction["path"],
"line": 0,
"message": f"current {extraction['mode']} certificate is missing",
})
continue
try:
certificate = validate_certificate(root, path, state_path=state_path)
current.append({"subject": certificate["subject"], "mode": certificate["mode"], "path": path.relative_to(root).as_posix()})
except SemanticCertificateError as exc:
findings.append({"code": exc.code, "path": exc.path or extraction["path"], "line": 0, "message": str(exc)})
return {
"schema_version": RESULT_SCHEMA,
"status": "PASS" if not findings else "FAIL",
"mode": mode or "all",
"required_documents": len(selected),
"current_certificates": len(current),
"coverage": {
"required": len(selected),
"current": len(current),
"missing_or_stale": len(findings),
},
"certificates": current,
"findings": sorted(findings, key=lambda item: (item["path"], item["code"])),
}
def quality_extension(root: Path, paths: Iterable[Path]) -> dict[str, Any]:
"""QualityExtension-compatible current-certificate validation."""
return check(root, paths=paths)
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--root", type=Path, default=DEFAULT_ROOT)
parser.add_argument("--mode", choices=("local", "hub"))
parser.add_argument("--path", type=Path, action="append")
parser.add_argument("--check", action="store_true")
args = parser.parse_args(argv)
try:
result = check(args.root, mode=args.mode, paths=args.path)
exit_code = 0 if result["status"] == "PASS" else 1
except (SemanticCertificateError, semantic_surface_extractor.SemanticSurfaceError, OSError, UnicodeError, json.JSONDecodeError) as exc:
result = {"schema_version": RESULT_SCHEMA, "status": "ERROR", "errors": [{"code": getattr(exc, "code", "SEMANTIC_CERTIFICATE_ERROR"), "message": str(exc)}]}
exit_code = 2
json.dump(result, sys.stdout, ensure_ascii=False, indent=2, sort_keys=True)
sys.stdout.write("\n")
return exit_code
if __name__ == "__main__":
raise SystemExit(main())