아바타 저장소 승격 계약을 완성
This commit is contained in:
parent
ac9b702688
commit
ccdcfcd2f5
36 changed files with 14734 additions and 222 deletions
369
scripts/probe-public-runtime-upload-root.py
Normal file
369
scripts/probe-public-runtime-upload-root.py
Normal file
|
|
@ -0,0 +1,369 @@
|
|||
"""Prove the exact loopback API listener inherited the pinned runtime contract.
|
||||
|
||||
The probe first resolves the unique PID that owns the exact
|
||||
``127.0.0.1:<api-port>`` listening socket. It then inspects only that PID's
|
||||
argv, cwd, upload receipt environment, and a secret-free database target
|
||||
fingerprint. Process-wide scans and look-alike uvicorn commands are deliberately
|
||||
not accepted as runtime identity proof.
|
||||
|
||||
Output is privacy-safe: no full environment or arbitrary command line is
|
||||
emitted.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import ntpath
|
||||
import re
|
||||
from typing import Any, Iterable, Mapping, Sequence
|
||||
|
||||
|
||||
LOOPBACK_ADDRESS = "127.0.0.1"
|
||||
LOWER_SHA256 = re.compile(r"^[0-9a-f]{64}$")
|
||||
RUNTIME_ENVIRONMENT_NAMES = (
|
||||
"USER_UPLOAD_DIR",
|
||||
"USER_UPLOAD_MANIFEST_REQUIRED",
|
||||
"USER_UPLOAD_MANIFEST_PATH",
|
||||
"USER_UPLOAD_MANIFEST_SHA256",
|
||||
"USER_UPLOAD_WRITE_FREEZE_PATH",
|
||||
"PUBLIC_RUNTIME_DB_TARGET_SHA256",
|
||||
)
|
||||
|
||||
|
||||
def _normalized_windows_path(value: str) -> str:
|
||||
return ntpath.normcase(ntpath.normpath(value))
|
||||
|
||||
|
||||
def _option_value(argv: Sequence[str], option: str) -> str | None:
|
||||
"""Return an option's sole split-form value, or None when ambiguous."""
|
||||
|
||||
indexes = [index for index, value in enumerate(argv) if value == option]
|
||||
if len(indexes) != 1:
|
||||
return None
|
||||
index = indexes[0]
|
||||
if index + 1 >= len(argv):
|
||||
return None
|
||||
return argv[index + 1]
|
||||
|
||||
|
||||
def _matches_public_api(argv: Sequence[str], api_port: int) -> bool:
|
||||
values = [str(value) for value in argv]
|
||||
return (
|
||||
values.count("uvicorn") == 1
|
||||
and values.count("app.main:app") == 1
|
||||
and _option_value(values, "--host") == LOOPBACK_ADDRESS
|
||||
and _option_value(values, "--port") == str(api_port)
|
||||
and _option_value(values, "--workers") == "1"
|
||||
)
|
||||
|
||||
|
||||
def _snapshot_environment(snapshot: Mapping[str, Any]) -> Mapping[str, Any]:
|
||||
environment = snapshot.get("environment")
|
||||
if isinstance(environment, Mapping):
|
||||
return environment
|
||||
# Compatibility with the original pure-function fixtures. The live probe
|
||||
# never relies on this flattened representation.
|
||||
return {"USER_UPLOAD_DIR": snapshot.get("user_upload_root")}
|
||||
|
||||
|
||||
def evaluate_listener_binding(
|
||||
listener_pids: Iterable[int],
|
||||
process_snapshots: Iterable[Mapping[str, Any]],
|
||||
*,
|
||||
expected_root: str,
|
||||
expected_api_cwd: str,
|
||||
expected_manifest_path: str,
|
||||
expected_manifest_sha256: str,
|
||||
expected_write_freeze_path: str,
|
||||
expected_database_target_sha256: str,
|
||||
api_port: int,
|
||||
) -> tuple[int, dict[str, Any]]:
|
||||
"""Evaluate snapshots only for the unique exact loopback listener PID."""
|
||||
|
||||
unique_listener_pids = sorted({int(pid) for pid in listener_pids})
|
||||
if len(unique_listener_pids) != 1:
|
||||
return 1, {
|
||||
"status": "failed",
|
||||
"reason": "api_listener_count_mismatch",
|
||||
"listening_processes": len(unique_listener_pids),
|
||||
}
|
||||
|
||||
listener_pid = unique_listener_pids[0]
|
||||
listener_snapshots = [
|
||||
snapshot
|
||||
for snapshot in process_snapshots
|
||||
if int(snapshot.get("pid", -1)) == listener_pid
|
||||
]
|
||||
if len(listener_snapshots) != 1:
|
||||
return 1, {
|
||||
"status": "failed",
|
||||
"reason": "listener_process_unavailable",
|
||||
"pid": listener_pid,
|
||||
}
|
||||
|
||||
snapshot = listener_snapshots[0]
|
||||
if not _matches_public_api(snapshot.get("argv") or [], api_port):
|
||||
return 1, {
|
||||
"status": "failed",
|
||||
"reason": "listener_command_mismatch",
|
||||
"pid": listener_pid,
|
||||
}
|
||||
|
||||
actual_cwd = snapshot.get("cwd")
|
||||
if not isinstance(actual_cwd, str) or not actual_cwd.strip():
|
||||
return 1, {
|
||||
"status": "failed",
|
||||
"reason": "listener_cwd_missing",
|
||||
"pid": listener_pid,
|
||||
}
|
||||
if _normalized_windows_path(actual_cwd) != _normalized_windows_path(
|
||||
expected_api_cwd
|
||||
):
|
||||
return 1, {
|
||||
"status": "failed",
|
||||
"reason": "listener_cwd_drift",
|
||||
"pid": listener_pid,
|
||||
"expected_api_cwd": expected_api_cwd,
|
||||
"actual_api_cwd": actual_cwd,
|
||||
}
|
||||
|
||||
environment = _snapshot_environment(snapshot)
|
||||
actual_root = environment.get("USER_UPLOAD_DIR")
|
||||
if not isinstance(actual_root, str) or not actual_root.strip():
|
||||
return 1, {
|
||||
"status": "failed",
|
||||
"reason": "user_upload_dir_missing",
|
||||
"pid": listener_pid,
|
||||
}
|
||||
if _normalized_windows_path(actual_root) != _normalized_windows_path(expected_root):
|
||||
return 1, {
|
||||
"status": "failed",
|
||||
"reason": "user_upload_dir_drift",
|
||||
"pid": listener_pid,
|
||||
"expected_root": expected_root,
|
||||
"actual_root": actual_root,
|
||||
}
|
||||
|
||||
if environment.get("USER_UPLOAD_MANIFEST_REQUIRED") != "true":
|
||||
return 1, {
|
||||
"status": "failed",
|
||||
"reason": "upload_manifest_required_mismatch",
|
||||
"pid": listener_pid,
|
||||
}
|
||||
|
||||
actual_manifest_path = environment.get("USER_UPLOAD_MANIFEST_PATH")
|
||||
if not isinstance(actual_manifest_path, str) or not actual_manifest_path.strip():
|
||||
return 1, {
|
||||
"status": "failed",
|
||||
"reason": "upload_manifest_path_missing",
|
||||
"pid": listener_pid,
|
||||
}
|
||||
if _normalized_windows_path(actual_manifest_path) != _normalized_windows_path(
|
||||
expected_manifest_path
|
||||
):
|
||||
return 1, {
|
||||
"status": "failed",
|
||||
"reason": "upload_manifest_path_drift",
|
||||
"pid": listener_pid,
|
||||
}
|
||||
|
||||
actual_manifest_sha256 = environment.get("USER_UPLOAD_MANIFEST_SHA256")
|
||||
if (
|
||||
not isinstance(actual_manifest_sha256, str)
|
||||
or LOWER_SHA256.fullmatch(actual_manifest_sha256) is None
|
||||
or LOWER_SHA256.fullmatch(expected_manifest_sha256) is None
|
||||
or actual_manifest_sha256 != expected_manifest_sha256
|
||||
):
|
||||
return 1, {
|
||||
"status": "failed",
|
||||
"reason": "upload_manifest_sha256_drift",
|
||||
"pid": listener_pid,
|
||||
}
|
||||
|
||||
actual_freeze_path = environment.get("USER_UPLOAD_WRITE_FREEZE_PATH")
|
||||
if not isinstance(actual_freeze_path, str) or not actual_freeze_path.strip():
|
||||
return 1, {
|
||||
"status": "failed",
|
||||
"reason": "upload_write_freeze_path_missing",
|
||||
"pid": listener_pid,
|
||||
}
|
||||
if _normalized_windows_path(actual_freeze_path) != _normalized_windows_path(
|
||||
expected_write_freeze_path
|
||||
):
|
||||
return 1, {
|
||||
"status": "failed",
|
||||
"reason": "upload_write_freeze_path_drift",
|
||||
"pid": listener_pid,
|
||||
}
|
||||
|
||||
actual_database_target_sha256 = environment.get("PUBLIC_RUNTIME_DB_TARGET_SHA256")
|
||||
if (
|
||||
not isinstance(actual_database_target_sha256, str)
|
||||
or LOWER_SHA256.fullmatch(actual_database_target_sha256) is None
|
||||
or LOWER_SHA256.fullmatch(expected_database_target_sha256) is None
|
||||
or actual_database_target_sha256 != expected_database_target_sha256
|
||||
):
|
||||
return 1, {
|
||||
"status": "failed",
|
||||
"reason": "database_target_sha256_drift",
|
||||
"pid": listener_pid,
|
||||
}
|
||||
|
||||
return 0, {
|
||||
"status": "passed",
|
||||
"pid": listener_pid,
|
||||
"user_upload_root": actual_root,
|
||||
"api_cwd": actual_cwd,
|
||||
"listener": f"{LOOPBACK_ADDRESS}:{api_port}",
|
||||
"manifest_sha256": actual_manifest_sha256,
|
||||
"database_target_sha256": actual_database_target_sha256,
|
||||
}
|
||||
|
||||
|
||||
def evaluate_snapshots(
|
||||
snapshots: Iterable[Mapping[str, Any]],
|
||||
*,
|
||||
expected_root: str,
|
||||
api_port: int,
|
||||
) -> tuple[int, dict[str, Any]]:
|
||||
"""Compatibility wrapper for legacy unit fixtures.
|
||||
|
||||
Live execution does not call this function. It preserves the historical
|
||||
argv/root-only contract so older focused tests remain useful without
|
||||
weakening listener-bound production proof.
|
||||
"""
|
||||
|
||||
matches = [
|
||||
snapshot
|
||||
for snapshot in snapshots
|
||||
if _matches_public_api(snapshot.get("argv") or [], api_port)
|
||||
]
|
||||
if len(matches) != 1:
|
||||
return 1, {
|
||||
"status": "failed",
|
||||
"reason": "api_process_count_mismatch",
|
||||
"matching_processes": len(matches),
|
||||
}
|
||||
|
||||
match = matches[0]
|
||||
actual_root = _snapshot_environment(match).get("USER_UPLOAD_DIR")
|
||||
if not isinstance(actual_root, str) or not actual_root.strip():
|
||||
return 1, {
|
||||
"status": "failed",
|
||||
"reason": "user_upload_dir_missing",
|
||||
"pid": int(match["pid"]),
|
||||
}
|
||||
if _normalized_windows_path(actual_root) != _normalized_windows_path(expected_root):
|
||||
return 1, {
|
||||
"status": "failed",
|
||||
"reason": "user_upload_dir_drift",
|
||||
"pid": int(match["pid"]),
|
||||
"expected_root": expected_root,
|
||||
"actual_root": actual_root,
|
||||
}
|
||||
return 0, {
|
||||
"status": "passed",
|
||||
"pid": int(match["pid"]),
|
||||
"user_upload_root": actual_root,
|
||||
}
|
||||
|
||||
|
||||
def _connection_address(connection: Any) -> tuple[str, int] | None:
|
||||
address = connection.laddr
|
||||
if not address:
|
||||
return None
|
||||
try:
|
||||
return str(address.ip), int(address.port)
|
||||
except AttributeError:
|
||||
if len(address) < 2:
|
||||
return None
|
||||
return str(address[0]), int(address[1])
|
||||
|
||||
|
||||
def _collect_listener_pids(api_port: int) -> list[int]:
|
||||
import psutil
|
||||
|
||||
listener_pids: set[int] = set()
|
||||
for connection in psutil.net_connections(kind="tcp"):
|
||||
if connection.status != psutil.CONN_LISTEN or connection.pid is None:
|
||||
continue
|
||||
address = _connection_address(connection)
|
||||
if address == (LOOPBACK_ADDRESS, api_port):
|
||||
listener_pids.add(int(connection.pid))
|
||||
return sorted(listener_pids)
|
||||
|
||||
|
||||
def _collect_process_snapshots(listener_pids: Iterable[int]) -> list[dict[str, Any]]:
|
||||
import psutil
|
||||
|
||||
snapshots: list[dict[str, Any]] = []
|
||||
for pid in sorted({int(value) for value in listener_pids}):
|
||||
try:
|
||||
process = psutil.Process(pid)
|
||||
argv = list(process.cmdline())
|
||||
cwd = process.cwd()
|
||||
environment = process.environ()
|
||||
except (psutil.AccessDenied, psutil.NoSuchProcess, psutil.ZombieProcess):
|
||||
continue
|
||||
snapshots.append(
|
||||
{
|
||||
"pid": pid,
|
||||
"argv": argv,
|
||||
"cwd": cwd,
|
||||
# Never retain unrelated process secrets (including DATABASE_URL)
|
||||
# in the snapshot or result payload.
|
||||
"environment": {
|
||||
name: environment.get(name) for name in RUNTIME_ENVIRONMENT_NAMES
|
||||
},
|
||||
}
|
||||
)
|
||||
return snapshots
|
||||
|
||||
|
||||
def _failure(reason: str) -> tuple[int, dict[str, Any]]:
|
||||
return 1, {"status": "failed", "reason": reason}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--expected-root", required=True)
|
||||
parser.add_argument("--expected-api-cwd", required=True)
|
||||
parser.add_argument("--expected-manifest-path", required=True)
|
||||
parser.add_argument("--expected-manifest-sha256", required=True)
|
||||
parser.add_argument("--expected-write-freeze-path", required=True)
|
||||
parser.add_argument("--expected-database-target-sha256", required=True)
|
||||
parser.add_argument("--api-port", type=int, required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
try:
|
||||
listener_pids = _collect_listener_pids(args.api_port)
|
||||
snapshots = _collect_process_snapshots(listener_pids)
|
||||
# Close the process-exit/PID-reuse race: the same sole PID must still
|
||||
# own the socket after its identity was inspected.
|
||||
listener_pids_after = _collect_listener_pids(args.api_port)
|
||||
if listener_pids_after != listener_pids:
|
||||
exit_code, payload = _failure("api_listener_changed_during_probe")
|
||||
else:
|
||||
exit_code, payload = evaluate_listener_binding(
|
||||
listener_pids,
|
||||
snapshots,
|
||||
expected_root=args.expected_root,
|
||||
expected_api_cwd=args.expected_api_cwd,
|
||||
expected_manifest_path=args.expected_manifest_path,
|
||||
expected_manifest_sha256=args.expected_manifest_sha256,
|
||||
expected_write_freeze_path=args.expected_write_freeze_path,
|
||||
expected_database_target_sha256=(args.expected_database_target_sha256),
|
||||
api_port=args.api_port,
|
||||
)
|
||||
except Exception:
|
||||
# psutil permission/platform failures are proof failures. Avoid
|
||||
# printing exception strings because they can contain process details.
|
||||
exit_code, payload = _failure("listener_inspection_failed")
|
||||
|
||||
print(json.dumps(payload, ensure_ascii=True, separators=(",", ":")))
|
||||
return exit_code
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue