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
12 KiB
MAS v0.1 experiment review (2026-10-08, provider pass 2026-10-18)
Status of MAS-WP-0002, Milestone 9. The answers below come from building
mas_core, the mas CLI, mas-gateway and the reference ticket service,
and from running the test suite (135 tests, all offline except the
loopback fixture servers).
Scope: the MTA half of the email path was tested against a real
Postfix (3.7.11, Debian bookworm, in Docker). The setup was SMTP → Postfix
→ pipe transport → mas-gateway → reference ticket service
(experiments/postfix-probe/).
Provider pass (MAS-WP-0003, 2026-10-18): the same stack ran as a live MX
for mas.coulomb.social on railiance01 (deploy/railiance/), with the
statute at https://mas.coulomb.social/.well-known/mas. Real mail came from
Gmail and Microsoft 365, and replies went out through Scaleway TEM. The
evidence is the gateway's JSONL event log plus the Postfix log. Forwarding
and mailing-list paths (T04) are still pending. Spec consequences are
collected in docs/spec-change-proposals.md.
Answers to the review questions
Syntax: did the parser stay simple?
Yes. The parser is about 100 lines with no I/O. It always uses the maximal
(P2) grammar and records required_profile, so ticket!callback parses
without knowing anything about the namespace. All 31 spec fixtures and 27
added property cases pass.
Gaps found in the spec:
- Resource-type case. The ABNF allows
Ticket=PR42, but the statute schema only registers lower-case type names. Such an address parses, but it can never match a declared type, even when the statute saysascii-insensitive. The spec should either make resource types lower-case in the grammar, or state that the equivalence policy also applies to the type name. - Interaction-mode names. The value grammar allows
call.backanda=b. The schema's mode-name pattern does not. Those modes parse but can never be enabled. This is harmless, but it should be documented. - Registry keys. The schema pattern already blocks
+ ! $ ^, but it still allows=and leading or trailing dots. Semantic lint is needed to reject a key liketicket=GS-1(registry-key-resource).
Discovery: was exact-host metadata sufficient?
For this experiment, yes. Implementation choices the spec does not settle:
application/json(or+json) is required as the content type (InvalidContentType). The spec only asks for 2xx plus the schema. It should say whether the content type is checked.- Redirects are followed only when scheme, host and port all match. That
is stricter than "same host". A port change counts as
CrossHostRedirectand an https→http change asSchemeDowngrade. mas_versionis checked before the schema runs, so a future version is reported asUnsupportedVersionand not as a schema error.- Operational finding: a service must not depend on discovering its
own statute. If the statute endpoint is down, the ticket service sees
namespace_confirmed = falseand correctly refuses to act. The mail is then declined. Operators should pin their own statute in local configuration (mas-gateway --statute). Discovery is for third-party namespaces.
Policy: could all inference stay explicit?
Yes. Policy is a pure function. Without a statute, kind is always
opaque, = alone gives no resource semantics, and ! stays
"unconfirmed". mas explain lists each of these as an unknown.
Folding is applied only when a statute declares it, and dot folding never
touches resource identifiers.
Facets have no statute declaration at all, so they are always reported as "application-defined". A later version could let statutes list the facets they recognise.
Email infrastructure: which forms were rewritten or rejected?
Postfix (tested): 14 address forms were sent through 5 Postfix
configurations (experiments/postfix-probe/results/):
- relay domain with defaults;
- local rewrite context, with and without
swap_bangpath/allow_percent_hack; - domain in
mydestination(local delivery class), with and without those two settings.
The results:
- 13 of 14 forms arrived byte-identical in every configuration. That
includes
ticket!callback,ticket=ID+comments,ticket==X1,ticket=PR%2B42,ticket=ID+comments!callbackandticket=PR42$4.99.recipient_delimiter = +does not change the envelope recipient thatpipepasses on as${recipient}. Upper case is preserved. - The quoted local-part
"ticket=PR42"arrived unquoted asticket=PR42. Postfix drops quotes that are not needed (RFC 5321 treats the two forms as equivalent), so a form the spec calls non-canonical became canonical in transit. Spec gap: §4 should say that a quoted local-part whose content is a valid dot-string is equivalent to the unquoted form, because an MAS receiver after an MTA cannot tell them apart. - The
!and%legacy rewrites did not trigger for any MAS address, even with Postfix defaults and the domain inmydestination. In 3.7 the bang-path swap fired only for a baresite!userwith no@domain(checked via localsendmail: it was rerouted tosite). MAS addresses always carry an@domain. Settingswap_bangpath = noandallow_percent_hack = nois still a sensible hardening default for MAS hosts. $passed through SMTP untouched. Rejecting it is purely a MAS parser decision. The gateway forwarded it asvalid: false, and the app ignored it.
Providers (tested, MAS-WP-0003): four address forms were sent from four sender setups:
- forms:
ticket!callback@,ticket=ID@,ticket=ID+comments@,ticket=RE=2026-4711@; - sender setups: Gmail web, the Gmail mobile app, and a Microsoft 365 tenant via both Outlook mobile and Outlook desktop.
| Sender | Compose form | Envelope recipient and To header | Transport |
|---|---|---|---|
| Gmail web | accepted all 4 unquoted | byte-identical | 209.85.x, STARTTLS, BDAT |
| Gmail mobile | accepted all 4 unquoted | byte-identical | same |
| M365, Outlook mobile | accepted all 4 unquoted | byte-identical | EOP 40.107.149.x |
| M365, Outlook desktop | accepted all 4 unquoted | byte-identical | EOP 52.101.171.x |
- Nothing was rewritten, quoted, rejected or stripped. That covers
!, a doubled=and the+commentsfacet. The gateway classified every message as the probe predicted: P2 function withcallback→ 201 created; P1 resource → 202; the facet was recognised. - The reply direction also holds. The service replied From
ticket=GS-85QJ8WC09P@mas.coulomb.socialthrough Scaleway TEM. Gmail showed the=From address as sent, with SPF, DKIM (d=mas.coulomb.social) and DMARC all passing. A plain Gmail "Reply" went back to that address byte-identical, with correctIn-Reply-ToandReferences, and was correlated to the ticket (200). - Not covered: consumer Outlook.com (the operator has no account; it
uses the same Exchange Online path as M365), the Outlook-on-the-web
compose form, and other providers such as Apple, Yahoo and GMX. Two
providers is evidence that
!and=work in practice; it is not a survey.
Operational findings from the live run:
- Stock Postfix kills P2.
smtpd_relay_restrictionsmust contain a reject-class entry, or smtpd refuses to start. Also, unlessallow_untrusted_routing = yes, Postfix ignores access-map results for local-parts containing!or%(it treats them as sender-specified routing).reject_unauth_destinationthen refuses every P2 address. The working MAS host config relays only@DOMAINthrough an explicit regexp, turns offswap_bangpathandallow_percent_hack, and ends the list inreject. A host that "just enables MAS" on a default Postfix will silently lose all!mail. - A TEM "blackhole" MX swallowed inbound mail. Scaleway TEM's domain
setup suggests
MX 10 blackhole.tem.scaleway.com. Added next to the real MX at the same priority, it sent about half of all inbound mail to a discard host, and nobody got a bounce. Sending providers' setup wizards are a deliverability risk for MAS subdomains that receive mail. - Outbound port 25 was blocked at the hosting provider (and on a second host). Replies need an authenticated submission relay.
Still untested (T04): overlap with SRS (SRS0=...) and VERP when
forwarding, and facet stripping by forwarders and lists.
Resource addressing: did it improve correlation?
Yes. The service replies from ticket=GS-…@great-service.example, so an
ordinary reply lands on the resource address. Correlation uses the address
only. The tests send an unrelated subject and a facet-stripped address,
and both correlate.
Subject lines: could identifiers disappear?
Yes. Replies use the customer's own subject, and no ticket id is ever put in it (asserted in the tests).
Lifecycle: did closed resources stay understandable?
Yes. A closed ticket stays addressable. Mail to it is archived separately
and the ticket is not changed. A single notice goes only to the original
requester, and never to automated senders (Auto-Submitted,
Precedence). Mail to an unknown resource is quarantined with no reply.
For an MTA pipe, the gateway exits 0 when the app declines and 75 when the
app is unreachable, so it never generates a bounce (no backscatter).
Interaction: was !callback useful?
Partly. It expresses intent cleanly, but the service still needs a phone
number and a time. The address cannot carry those, so the first reply has
to ask by email. !callback therefore starts a negotiation; it does not
replace one.
The bare ticket in ticket!callback is an opaque target. It has no
defined link to the resource type ticket. The spec's canonical example
(§7) implies such a link. The statute has to register ticket as an
intake function, and the service has to know what that means. The spec
should either say this explicitly, or define "type intake" semantics for
an opaque target that names a declared resource type.
Security: did any address start acting like a capability?
(Addendum from the live run: an intake address such as ticket!callback@
that auto-replies turns any forged From: into outbound mail to a third
party. That is a backscatter/reflection vector at the application layer.
The live deployment therefore caps all replies sent (300 in total, T06)
and discards Postfix's own bounces. See SCP-5.)
Not for reading or control. Knowing a ticket address grants no read access and no admin action. It does let anyone append correspondence to an open ticket, which is the intended function of a communication endpoint. That is also an injection and spam vector. Ticket ids are non-guessable (10 Crockford base32 characters), but sender policy belongs to the application, not to MAS.
Decision gate (MAS-WP-0002), recommendation
The gate asks which additions answer a need the experiment demonstrated. This is a recommendation; the decision is the founder's.
| Candidate | Demonstrated need? | Recommendation |
|---|---|---|
| Postfix/Exim integration | Yes. Default Postfix drops every P2 address, and the correct relay config is non-obvious | Build first: a documented, tested Postfix profile (later Exim) based on deploy/railiance/entrypoint.sh |
| Spec v0.1.1 errata | Yes, SCP-1 to SCP-5 | Do alongside the integration |
| Address composer | Weak. Providers accepted hand-typed addresses; the 64-octet budget and facet/interaction exclusivity are the only traps | Defer; mas generate in the CLI covers it |
| Mail-client inspector | Not shown; nothing was rewritten that a user would need to see | Defer |
| TypeScript / Go implementations | Not shown; no consumer outside Python yet | Defer until a second consumer exists |
| Signed statutes | Not shown; pinned local statutes covered the operator case (see Discovery) | Defer |
| P3 economic profile | Not exercised | Defer |
T04 (SRS/VERP) could change the !/= row for forwarded mail. Revisit
this table once T04 is done.