G7 선행 조건 발견 — 공개 런타임에 admin/voice-runtime 이 없다

실측으로 찾았다. 공개 런타임은 119 paths 구버전이고 /admin/voice-runtime 이
배포돼 있지 않다. capture-g7-runtime-evidence.py 는 정확히 그 경로만 부르므로
artifact 2 를 만들 수 없다. 즉 마이크와 사람을 다 준비해도 오늘 실행하면
50분을 버리고 실패한다. 현재 소스에는 있다(admin.py:1699, 프리뷰 126 paths).

오케스트레이터가 시작 전에 이걸 검사하고 admin_voice_runtime_not_deployed 로
즉시 멈춘다. 공개 런타임 대상 --rehearse 가 exit 2 로 몇 초 만에 차단됐다.

판정 방식을 한 번 고쳤다. 처음엔 미인증 404/401 상태로 짰는데, Cloudflare 앞단이
자동화 클라이언트에게 존재하지 않는 경로까지 포함해 모든 경로를 403(error code
1010)으로 돌려주는 것을 확인했다. 그 신호로는 "배포 누락"과 "edge 차단"을 구분할
수 없어 작동하는 것처럼 보이지만 무의미한 검사였다. OpenAPI spec 의 paths 만
authoritative 하게 쓰도록 바꾸고 그 이유를 테스트로 남겼다.

G7 실행 순서가 이렇게 확정된다.
1. current source 를 공개 런타임에 승격(비-secure origin crypto.randomUUID 수정 포함)
2. --rehearse 로 배관 확인
3. 동의 하 물리 마이크 50분 + human pack 으로 실제 실행

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

View file

@ -175,8 +175,70 @@ def build_topology_leg(args: argparse.Namespace, output: Path) -> Leg:
return Leg("topology", argv, output)
def plan_legs(args: argparse.Namespace, out_dir: Path) -> list[Leg]:
ADMIN_RUNTIME_PATH = "/admin/voice-runtime"
# Cloudflare 앞단은 자동화 클라이언트에 error code 1010 으로 **모든 경로**에 403 을 준다.
# 그래서 미인증 상태 코드로는 "endpoint 없음"과 "edge 차단"을 구분할 수 없다.
# 배포 여부는 OpenAPI spec 의 paths 로만 판정한다.
_BROWSER_UA = (
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/140.0 Safari/537.36"
)
def assert_admin_endpoint_deployed(
url: str, *, paths_probe: Callable[[str], Sequence[str]] | None = None
) -> str:
"""관리자 runtime endpoint 가 그 배포에 실제로 존재하는지 먼저 본다.
2026-08-08 확인: 공개 런타임은 119 paths 구버전이라 `/admin/voice-runtime`
아예 없었다. 상태로 50 창을 돌리면 마이크와 사람을 준비하고도 artifact 2
에서 실패한다. 그래서 시작 전에 막는다.
미인증 HTTP 상태로 판정하지 않는다. Cloudflare 자동화 클라이언트에 모든 경로를
403(error code 1010)으로 돌려주므로 신호로는 배포 누락을 없다. OpenAPI
spec `paths` authoritative 하게 쓴다.
"""
from urllib.parse import urlsplit
parsed = urlsplit(url)
if parsed.path.rstrip("/") != ADMIN_RUNTIME_PATH:
raise WindowError("admin_runtime_url_unexpected_path")
origin = f"{parsed.scheme}://{parsed.netloc}"
paths = (paths_probe or _openapi_paths)(origin)
if ADMIN_RUNTIME_PATH not in set(paths):
raise WindowError("admin_voice_runtime_not_deployed")
return origin
def _openapi_paths(origin: str) -> Sequence[str]: # pragma: no cover - 네트워크 I/O
import urllib.error
import urllib.request
request = urllib.request.Request(
f"{origin}/openapi.json",
headers={"User-Agent": _BROWSER_UA, "Accept": "application/json"},
method="GET",
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
document = json.loads(response.read(8 * 1024 * 1024))
except (urllib.error.URLError, OSError, ValueError) as exc:
raise WindowError("openapi_unreachable") from exc
paths = document.get("paths") if isinstance(document, dict) else None
if not isinstance(paths, dict):
raise WindowError("openapi_shape_invalid")
return list(paths)
def plan_legs(
args: argparse.Namespace,
out_dir: Path,
*,
admin_probe: Callable[[str], Sequence[str]] | None = None,
) -> list[Leg]:
assert_single_host(args.wss_url, args.origin, args.admin_runtime_url)
assert_admin_endpoint_deployed(args.admin_runtime_url, paths_probe=admin_probe)
return [
build_soak_leg(args, out_dir / "voice-soak.json"),
build_runtime_leg(args, out_dir / "runtime.json"),

View file

@ -70,6 +70,7 @@ class HostAlignmentTest(unittest.TestCase):
args(admin_runtime_url="https://elsewhere.test/admin/voice-runtime",
microphone_device="mic", confirm_physical_capture=True),
Path("out"),
admin_probe=lambda origin: ["/admin/voice-runtime"],
)
@ -146,7 +147,8 @@ class LegCompositionTest(unittest.TestCase):
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")
args(rehearse=True, duration_seconds=3_000.0), Path("out"),
admin_probe=lambda origin: ["/admin/voice-runtime"],
)
self.assertEqual([leg.name for leg in legs][1:], ["runtime", "topology"])
for leg in legs[1:]:
@ -163,7 +165,7 @@ class WindowExecutionTest(unittest.TestCase):
started.append(argv[3])
return FakeCompleted(0)
legs = MODULE.plan_legs(args(rehearse=True), Path("out"))
legs = MODULE.plan_legs(args(rehearse=True), Path("out"), admin_probe=lambda origin: ["/admin/voice-runtime"])
results = MODULE.run_window(legs, timeout=60, runner=runner)
self.assertEqual(len(started), 3)
self.assertTrue(all(result.ok for result in results))
@ -172,13 +174,63 @@ class WindowExecutionTest(unittest.TestCase):
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"))
legs = MODULE.plan_legs(args(rehearse=True), Path("out"), admin_probe=lambda origin: ["/admin/voice-runtime"])
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 AdminEndpointPreflightTest(unittest.TestCase):
"""공개 런타임이 구버전이라 endpoint 가 없으면 50분을 버리기 전에 막는다."""
ADMIN_URL = "https://api.example.test/admin/voice-runtime"
def test_missing_endpoint_is_rejected_before_the_window_starts(self) -> None:
with self.assertRaises(MODULE.WindowError) as ctx:
MODULE.assert_admin_endpoint_deployed(
self.ADMIN_URL, paths_probe=lambda origin: ["/health", "/voice/speech"]
)
self.assertEqual(str(ctx.exception), "admin_voice_runtime_not_deployed")
def test_deployed_endpoint_is_accepted(self) -> None:
origin = MODULE.assert_admin_endpoint_deployed(
self.ADMIN_URL,
paths_probe=lambda origin: ["/health", "/admin/voice-runtime"],
)
self.assertEqual(origin, "https://api.example.test")
def test_unauthenticated_status_is_never_used_as_the_signal(self) -> None:
"""Cloudflare 는 자동화 클라이언트에 모든 경로를 403 으로 준다.
상태 코드로 판정하면 배포 누락을 통과시킨다. spec 본다.
"""
probed: list[str] = []
def probe(origin: str):
probed.append(origin)
return ["/admin/voice-runtime"]
MODULE.assert_admin_endpoint_deployed(self.ADMIN_URL, paths_probe=probe)
self.assertEqual(probed, ["https://api.example.test"])
def test_wrong_admin_url_path_fails_closed(self) -> None:
with self.assertRaises(MODULE.WindowError) as ctx:
MODULE.assert_admin_endpoint_deployed(
"https://api.example.test/admin/something-else",
paths_probe=lambda origin: ["/admin/voice-runtime"],
)
self.assertEqual(str(ctx.exception), "admin_runtime_url_unexpected_path")
def test_plan_refuses_a_deployment_without_the_endpoint(self) -> None:
with self.assertRaises(MODULE.WindowError) as ctx:
MODULE.plan_legs(
args(rehearse=True), Path("out"), admin_probe=lambda origin: ["/health"]
)
self.assertEqual(str(ctx.exception), "admin_voice_runtime_not_deployed")
class ReportTest(unittest.TestCase):
def _results(self, codes=(0, 0, 0)):
names = ("voice_soak", "runtime", "topology")