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"),