MAS-WP-0006: mas compose (canonical + forwarder-safe address)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 5787@bnt-lap001
Assistant-Session: 5db26b82-138b-40cc-9856-4d5eb0f752fd
This commit is contained in:
tegwick 2026-10-19 09:27:31 +02:00
parent d766d61e7f
commit 499ddce015
4 changed files with 187 additions and 6 deletions

View file

@ -9,6 +9,8 @@ from pathlib import Path
from mas_core import MAS_VERSION, __version__
from mas_core.discovery import Discovered, discover
from mas_core.generator import DESTINATION_PROFILES, CannotExpress, for_destination, generate_local_part
from mas_core.model import ResourceTarget
from mas_core.parser import MasSyntaxError, parse_address, parse_local_part, validate_profile
from mas_core.policy import resolve
from mas_core.statute import ERROR, SCHEMA, build_statute, lint
@ -120,6 +122,73 @@ def cmd_explain(args):
return 0
MAX_OCTETS = 64
TIGHT_OCTETS = 16 # fewer left than this: warn, stand-ins and forwarders add octets
def _forwarder_needs(statute, local_part):
"""The forwarder-safe form (spec v0.2 section 12.1): the stand-ins the
statute declares, then lower case."""
p = parse_local_part(local_part)
needs = []
if statute.alt_resource and isinstance(p.target, ResourceTarget):
needs.append("resource-alternate")
if statute.alt_interaction and p.interaction is not None:
needs.append("interaction-alternate")
return tuple(needs) + ("lowercase",)
def cmd_compose(args):
try:
local = generate_local_part(args.target, facet=args.facet, interaction=args.interaction)
except MasSyntaxError as e:
hint = (" a facet and an interaction are exclusive: pick one, or address the"
" resource and state the mode in the message\n"
if e.code == "generator-facet-and-interaction" else "")
_out(args, {"ok": False, "error": e.code, "message": str(e)},
f"cannot compose [{e.code}]: {e}\n{hint}".rstrip())
return 1
disc = None if args.no_discover else _discover(args, args.domain)
statute = disc.statute if disc is not None and disc.ok else None
warnings = []
if disc is not None and not disc.ok:
warnings.append(f"no valid MAS statute for {args.domain} ({disc.kind.value}); "
"the receiver may read this as ordinary email")
if statute is not None:
sem = resolve(parse_local_part(local), statute)
warnings += [f"{f.code}: {f.message}" for f in sem.failures]
octets = len(local.encode())
if MAX_OCTETS - octets < TIGHT_OCTETS:
warnings.append(f"{octets}/{MAX_OCTETS} octets: little room left for forwarders")
forms = {}
if statute is not None:
wanted = {name: DESTINATION_PROFILES[name] for name in args.destination}
if statute.alt_resource or statute.alt_interaction:
wanted = {"forwarder-safe": _forwarder_needs(statute, local), **wanted}
for name, needs in wanted.items():
try:
r = for_destination(local, statute, needs)
forms[name] = {"local_part": r.local_part, "address": f"{r.local_part}@{args.domain}",
"applied": list(r.applied), "warnings": list(r.warnings)}
except CannotExpress as e:
forms[name] = {"local_part": None, "error": e.code, "message": str(e)}
elif args.destination:
warnings.append("destination forms need the statute; none was used")
lines = [f"{local}@{args.domain}", f" octets: {octets}/{MAX_OCTETS} (local-part)"]
for name, f in forms.items():
if f["local_part"] is None:
lines.append(f" {name}: cannot express [{f['error']}]: {f['message']}")
else:
how = f" ({', '.join(f['applied'])})" if f["applied"] else " (unchanged)"
lines.append(f" {name}: {f['address']}{how}")
lines += [f" warning: {w}" for w in warnings]
_out(args, {"ok": True, "local_part": local, "address": f"{local}@{args.domain}",
"octets": octets, "forms": forms, "warnings": warnings}, "\n".join(lines))
return 0
def cmd_lint(args):
worst = 0
reports = []
@ -203,6 +272,19 @@ def build_parser():
p.add_argument("--registry", metavar="FILE", help="local registry JSON (addresses/resource_types)")
p.set_defaults(func=cmd_explain)
p = common(sub.add_parser("compose", help="write the canonical address for a target, and its "
"forwarder-safe form"))
p.add_argument("domain", help="receiving domain (its statute is discovered)")
p.add_argument("target", help="resource TYPE=ID, or a registry name such as 'ticket'")
p.add_argument("--facet", help="facet (the part after '+')")
p.add_argument("--interaction", metavar="MODE", help="requested interaction mode (P2 '!MODE')")
p.add_argument("--for", dest="destination", action="append", default=[],
choices=sorted(set(DESTINATION_PROFILES) - {"canonical"}),
help="also write the form for this destination (repeatable)")
disc_opts(p)
p.add_argument("--no-discover", action="store_true", help="skip discovery (canonical form only)")
p.set_defaults(func=cmd_compose)
p = common(sub.add_parser("lint-statute", help="schema + semantic validation of statute files"))
p.add_argument("files", nargs="+", metavar="FILE")
p.add_argument("--host", help="host the statute is meant for (checks namespace)")