Files
Find-Word/scripts/release_approval.py
T

149 lines
7.8 KiB
Python
Raw Normal View History

2026-10-14 12:12:13 +00:00
"""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