Enforce fixture interaction policy

This commit is contained in:
jwinterm
2026-07-06 03:28:14 -04:00
parent f930f606fd
commit d3c287074b
6 changed files with 245 additions and 4 deletions
+5
View File
@@ -190,6 +190,11 @@ def _cmd_fixture(args) -> int:
f"actual_transport={check.actual_transport} "
f"expected_profile={check.expected_profile} "
f"actual_profile={check.actual_profile} "
f"tier={check.tier} "
f"identity={check.identity} "
f"payments={check.payments} "
f"realtime={str(check.realtime).lower()} "
f"public_allowed={str(check.public_allowed).lower()} "
f"status={check.status} "
f"message=\"{_safe(check.message)}\""
)
+49
View File
@@ -5,6 +5,7 @@ import json
from pathlib import Path
from typing import Any
from .interaction_policy import interaction_failures
from .routing import route_url
@@ -20,6 +21,11 @@ class FixtureCheck:
actual_transport: str
expected_profile: str
actual_profile: str
tier: str
identity: str
payments: str
realtime: bool
public_allowed: bool
status: str
message: str
@@ -70,6 +76,7 @@ def _check_fixture(site_id: str, fixture: Any, path: Path) -> FixtureCheck:
raise ValueError(f"{path}: {protocol}: missing checks table")
expected_transport = _string(checks, "transport")
expected_profile = _string(checks, "profile")
interaction = _interaction_policy(fixture)
route = route_url(url)
failures: list[str] = []
@@ -77,6 +84,16 @@ def _check_fixture(site_id: str, fixture: Any, path: Path) -> FixtureCheck:
failures.append(f"transport expected {expected_transport} got {route.transport}")
if route.profile != expected_profile:
failures.append(f"profile expected {expected_profile} got {route.profile}")
failures.extend(
interaction_failures(
expected_transport,
tier=interaction["tier"],
identity=interaction["identity"],
payments=interaction["payments"],
realtime=interaction["realtime"],
public_allowed=interaction["public_allowed"],
)
)
return FixtureCheck(
site_id=site_id,
@@ -86,6 +103,11 @@ def _check_fixture(site_id: str, fixture: Any, path: Path) -> FixtureCheck:
actual_transport=route.transport,
expected_profile=expected_profile,
actual_profile=route.profile,
tier=interaction["tier"],
identity=interaction["identity"],
payments=interaction["payments"],
realtime=interaction["realtime"],
public_allowed=interaction["public_allowed"],
status="fail" if failures else "ok",
message="; ".join(failures) if failures else "route matches",
)
@@ -103,3 +125,30 @@ def _string(data: dict[str, Any], key: str) -> str:
if not isinstance(value, str) or not value:
raise ValueError(f"missing required string field {key!r}")
return value
def _interaction_policy(fixture: dict[str, Any]) -> dict[str, Any]:
raw = fixture.get("interaction", {})
if not isinstance(raw, dict):
raise ValueError(f"{fixture.get('protocol', 'fixture')}: interaction must be a table")
return {
"tier": _optional_string(raw, "tier", "static"),
"identity": _optional_string(raw, "identity", "none"),
"payments": _optional_string(raw, "payments", "none"),
"realtime": _optional_bool(raw, "realtime", False),
"public_allowed": _optional_bool(raw, "public_allowed", True),
}
def _optional_string(data: dict[str, Any], key: str, default: str) -> str:
value = data.get(key, default)
if not isinstance(value, str) or not value:
raise ValueError(f"interaction.{key} must be a non-empty string")
return value
def _optional_bool(data: dict[str, Any], key: str, default: bool) -> bool:
value = data.get(key, default)
if not isinstance(value, bool):
raise ValueError(f"interaction.{key} must be true or false")
return value
+96
View File
@@ -0,0 +1,96 @@
from __future__ import annotations
from dataclasses import dataclass
TIER_ORDER = {
"static": 0,
"interactive-lite": 1,
"identity": 2,
"transactional": 3,
"realtime": 4,
"internal": 5,
}
@dataclass(frozen=True)
class TransportInteractionPolicy:
transport: str
max_tier: str
identities: tuple[str, ...]
payments: tuple[str, ...]
realtime: str
TRANSPORT_POLICIES = {
"clearnet": TransportInteractionPolicy(
"clearnet",
"realtime",
("none", "http-session", "siwe"),
("none", "server-invoice", "wallet-signature"),
"yes",
),
"tor": TransportInteractionPolicy(
"tor",
"transactional",
("none", "http-session", "siwe"),
("none", "server-invoice", "wallet-signature"),
"private-only",
),
"i2p": TransportInteractionPolicy(
"i2p",
"transactional",
("none", "http-session", "siwe"),
("none", "server-invoice", "wallet-signature"),
"private-only",
),
"gemini": TransportInteractionPolicy(
"gemini",
"interactive-lite",
("none", "signed-link"),
("none", "static-instructions"),
"no",
),
"ipfs": TransportInteractionPolicy(
"ipfs",
"static",
("none",),
("none", "static-instructions"),
"no",
),
"reticulum": TransportInteractionPolicy(
"reticulum",
"interactive-lite",
("none", "signed-link"),
("none", "static-instructions"),
"research",
),
}
def interaction_failures(
transport: str,
*,
tier: str,
identity: str,
payments: str,
realtime: bool,
public_allowed: bool,
) -> list[str]:
failures: list[str] = []
policy = TRANSPORT_POLICIES.get(transport)
if not policy:
return [f"no interaction policy for transport {transport}"]
if not public_allowed:
failures.append("fixture is not public_allowed")
if tier not in TIER_ORDER:
failures.append(f"unknown tier {tier}")
elif TIER_ORDER[tier] > TIER_ORDER[policy.max_tier]:
failures.append(f"tier {tier} exceeds {transport} max {policy.max_tier}")
if identity not in policy.identities:
failures.append(f"identity {identity} not allowed on {transport}")
if payments not in policy.payments:
failures.append(f"payments {payments} not allowed on {transport}")
if realtime and policy.realtime not in {"yes", "private-only", "research"}:
failures.append(f"realtime not allowed on {transport}")
return failures