"""ASR 진단과 별개로 exact-byte 소유자 청취 승인을 검증하는 로컬 릴리스 gate.""" import hashlib import json from pathlib import Path import zipfile ROOT = Path(__file__).resolve().parents[1] APPROVAL = ".agent/RELEASE-APPROVAL-v0.2.5.json" ASR = ".agent/evidence-v0.2.5/review-fixes/final-asr.json" PRIOR_ASR = ".agent/evidence-v0.2.5/final-asr.json" BUNDLE = ".agent/evidence-v0.2.5/release-approval/review-bundle.zip" DELIVERED_BUNDLE = "/home/hermes/voice-memos/find-word-v0.2.5-normalized-ambience-review.zip" # 사용자에게 전달된 ZIP의 고정 digest가 승인 범위의 신뢰 기준이다. # manifest/WAV/승인 JSON을 함께 바꿔도 이 ZIP과 달라지면 승인으로 통과할 수 없다. BUNDLE_SHA256 = "bb2e974b652df43db4a1f2a59716836e2c521839c724b34d4b71c1b40c3322e5" IDS = ("street", "mall", "theater", "department", "market", "subway", "cafe", "rain") REQUIRED_EMPTY = ["mall", "market", "cafe"] MANIFEST = "public/ambience/manifest.json" NOTICE = "public/ambience/AMBIENCE_NOTICES.md" SCOPE = { "version": "0.2.5", "kind": "exact-byte-ambience-bundle", "directory": "public/ambience", "files": [*(f"{name}.wav" for name in IDS), "manifest.json", "AMBIENCE_NOTICES.md"], } def require(condition, message): """검증은 최적화 실행에서도 생략되지 않으며 실패 원인을 값 노출 없이 반환한다.""" if not condition: raise ValueError(message) def sha256(raw): return hashlib.sha256(raw).hexdigest() def unique_object(pairs): """중복 JSON key의 마지막 값이 앞선 승인/clearance 값을 숨기지 못하게 한다.""" result = {} for key, value in pairs: require(key not in result, f"Duplicate JSON key: {key}") result[key] = value return result def read_json(path): return json.loads(path.read_bytes(), object_pairs_hook=unique_object) def validate_screening(root, manifest, manifest_hash): """empty 결과뿐 아니라 exact 입력·동일 모델/VAD·screening 한계를 함께 확인한다.""" screening = read_json(root / ASR) prior = read_json(root / PRIOR_ASR) require(screening["tool"] == prior["tool"] and screening["model"] == prior["model"], "ASR tool/model mismatch") for key in ("device", "computeType", "cpuThreads", "numWorkers", "randomSeed", "parameters", "vadModelSha256"): require(screening["method"][key] == prior["method"][key], f"ASR setting mismatch: {key}") require(screening["method"]["manifestSha256"] == manifest_hash, "ASR manifest mismatch") require(screening["requiredEmptyEnvironmentIds"] == REQUIRED_EMPTY, "ASR required scope mismatch") for key, value in {"screeningOnly": True, "humanListeningAcceptance": False, "legalPrivacyClearance": False, "textIsVerifiedTranscript": False}.items(): require(screening["limitations"][key] is value, f"ASR limitation overclaim: {key}") require([row["environmentId"] for row in screening["files"]] == list(IDS), "ASR file scope mismatch") empty = True for entry, row in zip(manifest["files"], screening["files"], strict=True): require(row["file"] == f"public/ambience/{entry['file']}", "ASR file path mismatch") for key in ("sha256", "bytes", "sourceSha256", "selectedSegmentStartSeconds"): require(row[key] == entry[key], f"ASR asset mismatch: {key}") require(isinstance(row["segments"], list), "ASR segments missing") if entry["environmentId"] in REQUIRED_EMPTY: empty = empty and row["segments"] == [] and row["text"] == "" and row["info"]["duration_after_vad"] == 0 require(screening["requiredEmptyPassed"] is empty, "ASR aggregate contradicts diagnostic evidence") return empty def validate_owner_approval(root, manifest, manifest_hash): """보고된 Oz 회신을 고정된 전달 ZIP 및 현재 파일 전체와 연결한다. 전자서명 인증은 아니다.""" approval = read_json(root / APPROVAL) expected = { "schemaVersion": 1, "owner": "Oz", "statement": "승인합니다. 이대로 진행하세요", "statementSource": "User-reported Oz reply after receipt of the review bundle", "timestampUtc": "2026-10-14T11:29:51Z", "timestampKst": "2026-10-14T20:29:51+09:00", "scope": SCOPE, "productOwnerReleaseApproval": True, "humanListeningAcceptance": True, "legalPrivacyClearance": False, "reviewBundle": {"file": BUNDLE, "deliveredPath": DELIVERED_BUNDLE, "sha256": BUNDLE_SHA256}, "manifest": {"file": MANIFEST, "sha256": manifest_hash}, "notice": {"file": NOTICE, "sha256": sha256((root / NOTICE).read_bytes())}, "wavs": [{"environmentId": row["environmentId"], "file": f"public/ambience/{row['file']}", "sha256": row["sha256"], "bytes": row["bytes"]} for row in manifest["files"]], } # 알 수 없는 scope/clearance 필드도 거부한다. bool은 문자열/숫자를 승인값으로 인정하지 않는다. require(approval == expected, "Exact owner approval statement/scope/timestamp/hash mismatch") for key in ("productOwnerReleaseApproval", "humanListeningAcceptance", "legalPrivacyClearance"): require(approval[key] is expected[key], f"Invalid approval flag: {key}") archive_bytes = (root / BUNDLE).read_bytes() require(sha256(archive_bytes) == BUNDLE_SHA256, "Reviewed bundle hash mismatch") with zipfile.ZipFile(root / BUNDLE) as archive: names = [*SCOPE["files"], "LISTENING-REVIEW.txt"] require(sorted(archive.namelist()) == sorted(names), "Reviewed bundle file scope mismatch") # 압축 해제 없이 전수 비교하므로 ZIP path traversal도 발생하지 않는다. for name in SCOPE["files"]: require(archive.read(name) == (root / "public/ambience" / name).read_bytes(), f"Current asset differs from reviewed bundle: {name}") def validate_release(root=ROOT): """자동 empty-ASR OR 유효한 exact 청취 승인. 존재하는 잘못된 승인은 항상 실패한다.""" root = Path(root) manifest = read_json(root / MANIFEST) manifest_hash = sha256((root / MANIFEST).read_bytes()) require(manifest["version"] == "0.2.5", "Release version mismatch") require([row["environmentId"] for row in manifest["files"]] == list(IDS), "Manifest asset scope mismatch") require(sorted(path.name for path in (root / "public/ambience").iterdir()) == sorted(SCOPE["files"]), "Current asset directory scope mismatch") for name, row in zip(IDS, manifest["files"], strict=True): require(row["file"] == f"{name}.wav", "Manifest filename mismatch") raw = (root / "public/ambience" / row["file"]).read_bytes() require(sha256(raw) == row["sha256"] and len(raw) == row["bytes"], "Current WAV hash/bytes mismatch") automated = validate_screening(root, manifest, manifest_hash) owner = (root / APPROVAL).exists() if owner: validate_owner_approval(root, manifest, manifest_hash) require(automated or owner, "ASR is nonempty and exact owner approval record is absent") return { "passed": True, "acceptancePath": "automated-empty-asr" if automated else "exact-byte-owner-listening-approval", "automatedEmptyAsrPassed": automated, "productOwnerReleaseApproval": owner, "humanListeningAcceptance": owner, "legalPrivacyClearance": False, "manifestSha256": manifest_hash, "reviewBundleSha256": BUNDLE_SHA256 if owner else None, } if __name__ == "__main__": try: print(json.dumps(validate_release(), ensure_ascii=False, indent=2)) except (ValueError, KeyError, TypeError, OSError, zipfile.BadZipFile) as error: print(json.dumps({"passed": False, "reason": str(error)}, ensure_ascii=False)) raise SystemExit(1) from error