G7 외부 증거 3-artifact 동시 시간창 오케스트레이터

soak / runtime / topology 세 캡처는 같은 public host 의 겹치는 시간창이어야 하는데,
지금까지는 운영자가 세 명령을 따로 띄우고 시계를 손으로 맞춰야 했다. 50분짜리
실행에서 한 번 어긋나면 처음부터 다시 해야 하는 취약점이라 한 명령으로 묶었다.
세 캡처를 동시에 시작하고 전부 끝나면 human pack 을 더해 checker 까지 잇는다.

게이트를 약화시키지 않았다. CLI 로 실증한 fail-closed 경계 4종:
- 동의/장치 없이 운영 실행     physical_microphone_consent_required  exit 2
- human pack 없이 운영 실행    human_voice_gain_pack_required        exit 2
- 50분 미만 시간창             production_window_too_short           exit 2
- 세 캡처 host 불일치          hosts_must_match:[...]                exit 2

--rehearse 는 마이크를 열지 않고(soak 을 --preflight-only 로) 배관만 확인하며
보고서의 gate_closed 는 항상 false 다. 실제 실행 전 실패를 먼저 뽑기 위한 모드다.

기대 provider 기본값은 2026-08-08 결정에 맞춰 local_whisper / melotts 다.

이것으로 G7 에서 기계로 할 수 있는 부분은 끝났다. 남은 것은 코드로 만들 수 없는
둘뿐이다. 명시 동의 하 물리 마이크 50분 발화, 그리고 참가자 30명·독립 평가자 2인의
blind human voice-gain pack.

검증: 오케스트레이터 20/20, SSOT FAIL 0, dashboard E2E 10/10, ruff clean.
This commit is contained in:
Yun Chan 2026-08-08 09:40:08 +09:00
parent af993961c4
commit f3491be63f
5 changed files with 604 additions and 4 deletions

View file

@ -0,0 +1,222 @@
from __future__ import annotations
import importlib.util
import sys
import unittest
from pathlib import Path
from types import SimpleNamespace
SCRIPT_PATH = Path(__file__).with_name("run-g7-external-proof-window.py")
SPEC = importlib.util.spec_from_file_location("run_g7_external_proof_window", SCRIPT_PATH)
assert SPEC is not None and SPEC.loader is not None
MODULE = importlib.util.module_from_spec(SPEC)
sys.modules[SPEC.name] = MODULE
SPEC.loader.exec_module(MODULE)
def args(**overrides):
base = {
"wss_url": "wss://api.example.test/voice/ws",
"origin": "https://api.example.test",
"admin_runtime_url": "https://api.example.test/admin/voice-runtime",
"compose_project": "vignette",
"api_container": "vignette-api-1",
"api_image_digest": "sha256:" + "a" * 64,
"caddy_container": "vignette-proxy-1",
"caddy_image_digest": "sha256:" + "b" * 64,
"expected_stt_provider": "local_whisper",
"expected_stt_model": "large-v3",
"expected_tts_provider": "melotts",
"expected_tts_model": "melotts-korean",
"microphone_device": "",
"confirm_physical_capture": False,
"duration_seconds": 3_000.0,
"runtime_interval_seconds": 100.0,
"topology_interval_seconds": 100.0,
"human_voice_gain": Path("pack.json"),
"out_dir": Path("out"),
"rehearse": False,
}
base.update(overrides)
return SimpleNamespace(**base)
class FakeCompleted:
def __init__(self, returncode: int) -> None:
self.returncode = returncode
self.stdout = ""
self.stderr = ""
class HostAlignmentTest(unittest.TestCase):
def test_matching_hosts_are_accepted(self) -> None:
host = MODULE.assert_single_host(
"wss://api.example.test/voice/ws",
"https://api.example.test",
"https://api.example.test/admin/voice-runtime",
)
self.assertEqual(host, "api.example.test")
def test_mismatched_hosts_fail_closed(self) -> None:
with self.assertRaises(MODULE.WindowError):
MODULE.assert_single_host(
"wss://api.example.test/voice/ws", "https://other.example.test"
)
def test_plan_rejects_a_split_window(self) -> None:
with self.assertRaises(MODULE.WindowError):
MODULE.plan_legs(
args(admin_runtime_url="https://elsewhere.test/admin/voice-runtime",
microphone_device="mic", confirm_physical_capture=True),
Path("out"),
)
class SamplePlanTest(unittest.TestCase):
def test_samples_cover_the_whole_window(self) -> None:
self.assertEqual(MODULE.sample_plan(3_000.0, 100.0), 31)
def test_invalid_plan_fails_closed(self) -> None:
for duration, interval in ((0, 100.0), (3_000.0, 0)):
with self.subTest(duration=duration, interval=interval):
with self.assertRaises(MODULE.WindowError):
MODULE.sample_plan(duration, interval)
class ConsentGateTest(unittest.TestCase):
def test_production_soak_requires_device_and_confirmation(self) -> None:
for device, confirm in (("", True), ("mic", False), ("", False)):
with self.subTest(device=device, confirm=confirm):
with self.assertRaises(MODULE.WindowError) as ctx:
MODULE.build_soak_leg(
args(microphone_device=device, confirm_physical_capture=confirm),
Path("soak.json"),
)
self.assertEqual(
str(ctx.exception), "physical_microphone_consent_required"
)
def test_confirmed_production_soak_passes_the_flags_through(self) -> None:
leg = MODULE.build_soak_leg(
args(microphone_device="mic-1", confirm_physical_capture=True),
Path("soak.json"),
)
self.assertEqual(leg.name, "voice_soak")
self.assertIn("--confirm-physical-capture", leg.argv)
self.assertEqual(leg.argv[leg.argv.index("--microphone-device") + 1], "mic-1")
self.assertNotIn("--preflight-only", leg.argv)
def test_rehearse_never_opens_a_microphone(self) -> None:
leg = MODULE.build_soak_leg(args(rehearse=True), Path("soak.json"))
self.assertEqual(leg.name, "voice_soak_preflight")
self.assertIn("--preflight-only", leg.argv)
self.assertNotIn("--confirm-physical-capture", leg.argv)
self.assertNotIn("--microphone-device", leg.argv)
def test_production_window_shorter_than_fifty_minutes_is_rejected(self) -> None:
with self.assertRaises(MODULE.WindowError) as ctx:
MODULE.validate(args(duration_seconds=600.0))
self.assertEqual(str(ctx.exception), "production_window_too_short")
def test_production_run_requires_the_human_pack(self) -> None:
with self.assertRaises(MODULE.WindowError) as ctx:
MODULE.validate(args(human_voice_gain=None))
self.assertEqual(str(ctx.exception), "human_voice_gain_pack_required")
def test_rehearse_relaxes_only_the_two_human_inputs(self) -> None:
MODULE.validate(args(rehearse=True, duration_seconds=30.0, human_voice_gain=None))
class LegCompositionTest(unittest.TestCase):
def test_expected_providers_default_to_the_decided_local_stack(self) -> None:
leg = MODULE.build_soak_leg(args(rehearse=True), Path("soak.json"))
self.assertEqual(
leg.argv[leg.argv.index("--expected-stt-provider") + 1], "local_whisper"
)
self.assertEqual(
leg.argv[leg.argv.index("--expected-tts-provider") + 1], "melotts"
)
def test_topology_leg_pins_the_public_host_from_the_wss_url(self) -> None:
leg = MODULE.build_topology_leg(args(), Path("topology.json"))
self.assertEqual(
leg.argv[leg.argv.index("--public-host") + 1], "api.example.test"
)
def test_all_three_legs_share_one_window_length(self) -> None:
legs = MODULE.plan_legs(
args(rehearse=True, duration_seconds=3_000.0), Path("out")
)
self.assertEqual([leg.name for leg in legs][1:], ["runtime", "topology"])
for leg in legs[1:]:
samples = int(leg.argv[leg.argv.index("--samples") + 1])
interval = float(leg.argv[leg.argv.index("--interval-seconds") + 1])
self.assertGreaterEqual(samples * interval, 3_000.0)
class WindowExecutionTest(unittest.TestCase):
def test_all_three_legs_start_before_any_finishes(self) -> None:
started: list[str] = []
def runner(argv, **kwargs):
started.append(argv[3])
return FakeCompleted(0)
legs = MODULE.plan_legs(args(rehearse=True), Path("out"))
results = MODULE.run_window(legs, timeout=60, runner=runner)
self.assertEqual(len(started), 3)
self.assertTrue(all(result.ok for result in results))
def test_one_failing_leg_does_not_cancel_the_others(self) -> None:
def runner(argv, **kwargs):
return FakeCompleted(1 if "capture-g7-runtime-evidence.py" in argv[3] else 0)
legs = MODULE.plan_legs(args(rehearse=True), Path("out"))
results = MODULE.run_window(legs, timeout=60, runner=runner)
self.assertEqual(len(results), 3)
failed = [r.name for r in results if not r.ok]
self.assertEqual(failed, ["runtime"])
class ReportTest(unittest.TestCase):
def _results(self, codes=(0, 0, 0)):
names = ("voice_soak", "runtime", "topology")
return [
MODULE.LegResult(name, code, Path(f"{name}.json"))
for name, code in zip(names, codes)
]
def test_rehearse_never_reports_the_gate_as_closed(self) -> None:
report = MODULE.summarize(
self._results(), rehearse=True, checker_returncode=0
)
self.assertFalse(report["gate_closed"])
self.assertEqual(report["mode"], "rehearse")
def test_production_gate_closes_only_on_checker_exit_zero(self) -> None:
closed = MODULE.summarize(
self._results(), rehearse=False, checker_returncode=0
)
open_gate = MODULE.summarize(
self._results(), rehearse=False, checker_returncode=1
)
self.assertTrue(closed["gate_closed"])
self.assertFalse(open_gate["gate_closed"])
def test_failed_leg_is_reported(self) -> None:
report = MODULE.summarize(
self._results((0, 1, 0)), rehearse=False, checker_returncode=None
)
self.assertFalse(report["all_legs_passed"])
self.assertFalse(report["gate_closed"])
def test_checker_argv_passes_all_four_artifacts(self) -> None:
argv = MODULE.build_checker_argv(self._results(), Path("pack.json"))
for flag in ("--voice-soak", "--runtime", "--topology", "--human-voice-gain"):
self.assertIn(flag, argv)
self.assertEqual(argv[argv.index("--human-voice-gain") + 1], "pack.json")
if __name__ == "__main__":
unittest.main()