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
264 lines
15 KiB
Markdown
264 lines
15 KiB
Markdown
# 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 says
|
|
`ascii-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.back` and
|
|
`a=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 like `ticket=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 `CrossHostRedirect`
|
|
and an https→http change as `SchemeDowngrade`.
|
|
- `mas_version` is checked before the schema runs, so a future version is
|
|
reported as `UnsupportedVersion` and 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 = false` and 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!callback` and `ticket=PR42$4.99`.
|
|
`recipient_delimiter = +` does not change the envelope recipient that
|
|
`pipe` passes on as `${recipient}`. Upper case is preserved.
|
|
- **The quoted local-part `"ticket=PR42"` arrived unquoted** as
|
|
`ticket=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 in `mydestination`. In 3.7 the
|
|
bang-path swap fired only for a bare `site!user` with no `@domain`
|
|
(checked via local `sendmail`: it was rerouted to `site`). MAS addresses
|
|
always carry an `@domain`. Setting `swap_bangpath = no` and
|
|
`allow_percent_hack = no` is still a sensible hardening default for MAS
|
|
hosts.
|
|
- `$` passed through SMTP untouched. Rejecting it is purely a MAS parser
|
|
decision. The gateway forwarded it as `valid: 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 `+comments` facet. The gateway classified every
|
|
message as the probe predicted: P2 function with `callback` → 201
|
|
created; P1 resource → 202; the facet was recognised.
|
|
- **The reply direction also holds.** The service replied From
|
|
`ticket=GS-85QJ8WC09P@mas.coulomb.social` through 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 correct `In-Reply-To` and
|
|
`References`, 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_restrictions` must contain a
|
|
reject-class entry, or smtpd refuses to start. Also, unless
|
|
`allow_untrusted_routing = yes`, Postfix ignores access-map results for
|
|
local-parts containing `!` or `%` (it treats them as sender-specified
|
|
routing). `reject_unauth_destination` then refuses every P2 address. The
|
|
working MAS host config relays only `@DOMAIN` through an explicit
|
|
regexp, turns off `swap_bangpath` and `allow_percent_hack`, and ends the
|
|
list in `reject`. 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.
|
|
|
|
**Forwarding (T04, IONOS mail forward, 2026-10-18):** this is the first
|
|
real failure in the run.
|
|
- **The IONOS forwarding form refuses `=`** in the destination address.
|
|
`+` is accepted. A resource address cannot be configured as a forward
|
|
target at all.
|
|
- **The form also forces lower case.** It only accepts lower-case
|
|
destinations and "corrects" input. The operator entered
|
|
`ticket--GS-85QJ8WC09P+comments@` (with `--` standing in for `=`), and
|
|
`ticket--gs-85qj8wc09p+comments@` arrived. Under the spec's default
|
|
exact comparison, a lower-cased identifier no longer matches its
|
|
resource, even if the `=` had been allowed.
|
|
- **SRS does not collide with `=`.** IONOS rewrote the *envelope sender*
|
|
to `SRS0=ppc5=II=gmail.com=bernd.worsch@srs2.kundenserver.de`. SRS never
|
|
touches the recipient, so `=` in recipients is safe from it.
|
|
- The facet `+comments` survived forwarding.
|
|
- The header `To:` showed the forwarding address (`mas-fwd@coulomb.social`),
|
|
so MAS receivers must classify the **envelope recipient**, as
|
|
`mas-gateway` does.
|
|
- Result: the gateway classified the mail as an opaque target with a facet.
|
|
The ticket service correctly declined it (404, not handled), because
|
|
nothing in the statute gives `ticket--…` resource meaning. See SCP-6
|
|
(declared alternate separator) and SCP-7 (identifier case).
|
|
|
|
**Mailing list (T04, Google Groups, 2026-10-18):**
|
|
- **The member form accepted `=` and capitals:**
|
|
`ticket=GS-85QJ8WC09P+comments@` was accepted. The invitation was
|
|
delivered to it byte-identical (Auto-Submitted: auto-generated) and was
|
|
correlated to the ticket.
|
|
- **After acceptance, Google stored the member in lower case.** The list
|
|
post arrived for `ticket=gs-85qj8wc09p+comments@`. The envelope sender
|
|
was the per-message bounce address
|
|
`coulomb-social+bncBDG…@googlegroups.com`, which tags the list's own
|
|
local-part and does not touch ours, so there is no collision with `=`
|
|
or `+`.
|
|
- **With exact comparison the post did not match the ticket.** The service
|
|
quarantined it as an unknown resource and did not reply
|
|
(`Precedence: list`). That is correct under v0.1, but the post was lost
|
|
to the ticket.
|
|
|
|
**T04 conclusion:**
|
|
- SRS and list bounce addresses never collide with MAS syntax, because they
|
|
rewrite or tag the *sender*.
|
|
- Facets survived every path.
|
|
- The real risks are **provider forms**: IONOS refuses `=`, and **two of
|
|
two** forwarding/list providers (IONOS, Google Groups) lower-case the
|
|
recipient. Case-sensitive resource identifiers do not survive
|
|
forwarding or lists, which makes SCP-7 the most consequential proposal.
|
|
|
|
### 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)
|
|
|
|
**Decided 2026-10-18 (founder):** the recommendation below, plus the
|
|
address composer pulled forward. Follow-up work: MAS-WP-0004 (Postfix
|
|
profile), MAS-WP-0005 (spec errata and v0.2 drafts), MAS-WP-0006
|
|
(composer). MAS-WP-0007 retires the live test environment later.
|
|
|
|
| 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-7 (SCP-6 is an additive feature and could go to v0.2) | Do alongside the integration |
|
|
| Address composer | Partly. Mail clients accepted hand-typed addresses, but the IONOS forwarding form refused `=` and lower-cased input | Defer the tool; first settle SCP-6/SCP-7, then the generator emits the forwarder-safe form (`mas_core.generator.for_destination`, MAS-WP-0005 T09); the CLI/UI is MAS-WP-0006 |
|
|
| 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 confirmed the table: SRS and list bounce addresses never collided with
|
|
MAS syntax, and the risks it found (forms refusing `=`, lower-casing) are
|
|
covered by SCP-6/SCP-7 and the composer.
|