mail-address-semantics/mas_core/policy.py
tegwick eb10f54fd2 MAS-WP-0005-T08: v0.2 stand-ins and resource_id_case in policy; lint; gateway reports canonical form
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 252470@bnt-lap001
Assistant-Session: ad628c8f-4be2-4979-94c9-76c7ed6f8241
2026-10-18 22:24:28 +02:00

218 lines
9.1 KiB
Python

"""Pure MAS policy: parse tree + validated statute (+ local registry) -> semantics.
No network access, no clock, no randomness. Without a statute, nothing
beyond syntax is claimed: opaque targets stay `kind = opaque`, resource
syntax stays unconfirmed, interaction preferences stay unconfirmed.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Mapping, Optional
from .model import Facet, Interaction, OpaqueTarget, ParsedAddress, ParsedLocalPart, ResourceTarget, _render
from .statute import Statute
# Kind sources, in the order they are consulted.
SOURCE_STATUTE = "statute"
SOURCE_LOCAL = "local-registry"
SOURCE_NONE = "none"
@dataclass(frozen=True)
class SemanticFailure:
code: str # ProfileNotEnabled | InteractionModeNotEnabled | ResourceTypeNotEnabled | UnknownResourceType
message: str
@dataclass(frozen=True)
class AddressSemantics:
address: str
parsed: ParsedLocalPart
namespace_confirmed: bool
kind: str
kind_source: str
registry_key: Optional[str] = None
intake_for: Optional[str] = None
resource_type: Optional[str] = None
resource_id: Optional[str] = None
facet: Optional[str] = None
interaction: Optional[str] = None
equivalence_key: str = ""
failures: tuple[SemanticFailure, ...] = ()
unknowns: tuple[str, ...] = ()
stand_ins: tuple[str, ...] = () # v0.2: "resource" and/or "interaction"
@property
def ok(self) -> bool:
return not self.failures
def to_dict(self) -> dict:
return {
"address": self.address,
"namespace_confirmed": self.namespace_confirmed,
"kind": self.kind,
"kind_source": self.kind_source,
"registry_key": self.registry_key,
"intake_for": self.intake_for,
"resource_type": self.resource_type,
"resource_id": self.resource_id,
"facet": self.facet,
"interaction": self.interaction,
"required_profile": self.parsed.required_profile.value,
"equivalence_key": self.equivalence_key,
"canonical_local_part": str(self.parsed.raw),
"stand_ins": list(self.stand_ins),
"failures": [{"code": f.code, "message": f.message} for f in self.failures],
"unknowns": list(self.unknowns),
}
def _fold(s: str, case: str, dots: bool) -> str:
if case == "ascii-insensitive":
s = s.lower()
if dots:
s = s.replace(".", "")
return s
def equivalence_key(parsed: ParsedLocalPart, statute: Optional[Statute]) -> str:
"""Comparison key for the base target. Exact unless a statute declares folding.
Dot folding applies to opaque targets only, never to resource identifiers.
"""
base = parsed.base
if statute is None:
return base
if isinstance(parsed.target, ResourceTarget):
key = _fold(base, statute.local_part_case, False)
if statute.resource_id_case == "insensitive":
key = key.lower() # resource types are lower-case already (v0.1.1)
return key
return _fold(base, statute.local_part_case, statute.dot_folding)
def _split_interaction(value: str, statute: Statute) -> tuple[str, Optional[str]]:
"""`x---MODE` -> (`x`, MODE) when MODE is enabled; split at the last `---`."""
i = value.rfind("---")
if i > 0:
mode = value[i + 3:]
if mode and not mode.startswith("-") and statute.interaction_mode_enabled(mode):
return value[:i], mode
return value, None
def apply_stand_ins(parsed: ParsedLocalPart, statute: Optional[Statute]) -> tuple[ParsedLocalPart, tuple[str, ...]]:
"""Read v0.2 stand-ins as canonical syntax (spec v0.2 section 6.3).
Only a statute that enables a stand-in gives it meaning. Interaction
(`---MODE`, enabled mode) is matched first, then resource (`TYPE--ID`,
declared type, first `--`). Returns the canonical parse and the stand-ins used.
"""
if statute is None or not (statute.alt_resource or statute.alt_interaction):
return parsed, ()
target, facet, interaction = parsed.target, parsed.facet, parsed.interaction
used: list[str] = []
if statute.alt_interaction and statute.p2 and interaction is None:
if facet is not None:
rest, mode = _split_interaction(facet.value, statute)
if mode and rest:
facet, interaction = Facet(rest), Interaction(mode)
elif isinstance(target, OpaqueTarget):
rest, mode = _split_interaction(target.value, statute)
if mode and rest:
target, interaction = OpaqueTarget(rest), Interaction(mode)
if interaction is not None:
used.append("interaction")
if statute.alt_resource and isinstance(target, OpaqueTarget) and "--" in target.value:
rtype, rid = target.value.split("--", 1)
if rtype in statute.resource_types and rid and not rid.startswith("-"):
target = ResourceTarget(rtype, rid)
used.append("resource")
if not used:
return parsed, ()
return ParsedLocalPart(raw=_render(target, facet, interaction), target=target,
facet=facet, interaction=interaction), tuple(used)
def _lookup(registry: Mapping[str, Mapping], key: str, case: str, dots: bool):
if key in registry:
return key, registry[key]
want = _fold(key, case, dots)
for k, v in registry.items():
if _fold(k, case, dots) == want:
return k, v
return None, None
def resolve(parsed, statute: Optional[Statute] = None,
local_registry: Optional[Mapping] = None, *, domain: Optional[str] = None) -> AddressSemantics:
"""Resolve semantics. `local_registry` has the statute's `addresses` /
`resource_types` shape and applies only where the statute is silent."""
if isinstance(parsed, ParsedAddress):
domain, parsed = parsed.domain, parsed.local
address = f"{parsed.raw}@{domain}" if domain else parsed.raw
parsed, stand_ins = apply_stand_ins(parsed, statute)
local = local_registry or {}
failures: list[SemanticFailure] = []
unknowns: list[str] = []
case = statute.local_part_case if statute else "exact"
dots = statute.dot_folding if statute else False
kind, source, reg_key, intake_for = "opaque", SOURCE_NONE, None, None
rtype = rid = None
target = parsed.target
if statute is None:
unknowns.append("namespace: no validated MAS statute; semantics unconfirmed")
if isinstance(target, ResourceTarget):
rtype, rid = target.resource_type, target.resource_id
if statute is not None and rtype in statute.resource_types:
if statute.resource_type_enabled(rtype):
kind, source = "resource", SOURCE_STATUTE
else:
failures.append(SemanticFailure("ResourceTypeNotEnabled",
f"resource type {rtype!r} is disabled by the statute"))
elif local.get("resource_types", {}).get(rtype, {}).get("enabled"):
kind, source = "resource", SOURCE_LOCAL
elif statute is not None:
failures.append(SemanticFailure("UnknownResourceType",
f"statute does not declare resource type {rtype!r}"))
else:
unknowns.append(f"resource type {rtype!r}: '=' syntax alone confers no application semantics")
else:
if statute is not None:
reg_key, entry = _lookup(statute.addresses, target.value, case, dots)
if entry:
kind, source, intake_for = entry["kind"], SOURCE_STATUTE, entry.get("intake_for")
if source == SOURCE_NONE:
k, entry = _lookup(local.get("addresses", {}), target.value, case, dots)
if entry:
reg_key, kind, source = k, entry["kind"], SOURCE_LOCAL
intake_for = entry.get("intake_for")
if source == SOURCE_NONE:
unknowns.append("kind: no registry entry; punctuation is not a classifier")
facet = parsed.facet.value if parsed.facet else None
if facet is not None:
unknowns.append(f"facet {facet!r}: application-defined; may be stripped in transit")
mode = parsed.interaction.mode if parsed.interaction else None
if mode is not None:
if statute is None:
unknowns.append(f"interaction {mode!r}: P2 support unconfirmed")
elif not statute.p2:
failures.append(SemanticFailure("ProfileNotEnabled",
"address uses '!' (P2) but the statute does not enable P2"))
elif not statute.interaction_mode_enabled(mode):
failures.append(SemanticFailure("InteractionModeNotEnabled",
f"interaction mode {mode!r} is not enabled by the statute"))
return AddressSemantics(
address=address, parsed=parsed, namespace_confirmed=statute is not None,
kind=kind, kind_source=source, registry_key=reg_key, intake_for=intake_for,
resource_type=rtype, resource_id=rid, facet=facet, interaction=mode,
equivalence_key=equivalence_key(parsed, statute),
failures=tuple(failures), unknowns=tuple(unknowns), stand_ins=stand_ins,
)