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:
parent
d766d61e7f
commit
499ddce015
4 changed files with 187 additions and 6 deletions
|
|
@ -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)")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue