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
20 KiB
| id | type | title | domain | repo | status | owner | topic_slug | created | updated | origin | origin_ref | state_hub_workstream_id |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| MAS-WP-0005 | workplan | MAS spec v0.1.1 errata and v0.2 proposals | communication | mail-address-semantics | finished | claude | coulomb-social | 2026-10-18 | 2026-10-18 | residual | MAS-WP-0003 | e9ae68c7-b960-51c0-b697-f05278384a82 |
MAS spec v0.1.1 errata and v0.2 proposals
Decision-gate outcome of MAS-WP-0002 (founder, 2026-10-18). Turn
docs/spec-change-proposals.md into spec text. The mas-v0.1-spec/ bundle
is normative, and mas_conformance/ plus mas_core/schema/ are vendored
copies checked for drift, so each change updates the bundle, the vendored
copies, the fixtures and the implementation together.
Review the proposals with the founder
id: MAS-WP-0005-T01
status: done
priority: high
state_hub_task_id: "10c0433a-9b25-5b62-987f-2be7e9d49df2"
Walk through SCP-1 to SCP-7, settle the open options (SCP-2 (a) or (b), SCP-6 split rules), and record which proposals are accepted.
- 2026-10-18: SCP-2 decided as option (a): resource types are lower-case
in the grammar. SCP-6 is under discussion. Founder direction:
=stays canonical and--is accepted as a replacement only for endpoints that refuse=. A transform profile per recipient/destination may be needed when something we send out must match that destination's requirements. - 2026-10-18: SCP-6 decided. The statute opts in (option A) to
alternate_separator. The split is at the first--after a declared type, and type names may not contain--. The sending side gets a normative "destination transform" mechanism (separator,lowercase) plus an informative integration section with IONOS as the example. Concrete destination profiles live in implementation config, not the spec. Still open: transforms beyond separator and case, to be added only on evidence. - 2026-10-18: SCP-6 extended at the founder's request to cover similar
cases.
alternate_syntaxholdsresource: "--"andinteraction: "__"(split at the last__, only when the suffix is an enabled mode). There is no stand-in for+. The closed transform set is resource-alternate, interaction-alternate, lowercase, drop-facet and drop-interaction. Quoting, dots and length need no transform. - 2026-10-18: revised by the founder. Stand-ins use dashes only:
---replaces__for!, and no other stand-in characters are added.+is functional by default with no stand-in. The stand-ins are now fixed by the spec; the statute only enables them (alternate_syntax: {resource: true, interaction: true}). Matching order: interaction (---MODEat the end, enabled mode) first, then resource (TYPE--ID). IDs must not contain---when the interaction stand-in is enabled. - 2026-10-18: SCP-1, SCP-3, SCP-5 and SCP-7 confirmed as written. All seven proposals are accepted, so T01 is done. T02 implements the v0.1.1 errata (SCP-1, SCP-2 (a), SCP-3, SCP-5, and the SCP-7 recommendation); T04 implements SCP-6 and the SCP-7 statute field for v0.2.
v0.1.1 errata
id: MAS-WP-0005-T02
status: done
priority: high
state_hub_task_id: "eb4c83d0-5b2c-5b7c-a6e3-57b85fe78334"
Apply the accepted errata: SCP-1 (quoted equivalence), SCP-2 (lower-case
types), SCP-3 (type intake, intake_for), SCP-5 (intake auto-reply
bounds), and the SCP-7 recommendation (case-robust identifiers). Add
fixtures and update the parser, policy and schema.
Decisions (founder, 2026-10-18): keep the folder mas-v0.1-spec/ and mark
the revision v0.1.1 inside it (SPECIFICATION §1, README, new CHANGELOG).
For SCP-5, write the spec text now and track the DMARC-gated reply code as
its own task.
-
2026-10-18 SCP-2 done:
- grammar
resource-type = type-word *(("-" / "_") type-word); - statute schema key pattern
^[a-z0-9]+([_-][a-z0-9]+)*$(bundle and vendored copy); mas_core.parser.is_resource_type, also used by statute lint;- the bundle's reference parser;
- §6.1 text and the CHANGELOG;
- fixtures p1-042 to p1-045, and prop-case-001 flipped to invalid.
141 tests pass, and the reference parser agrees with all 35 bundle fixtures. SCP-2 text corrected:
Ticket=PR42becomes non-canonical, not opaque. - grammar
-
2026-10-18 SCP-1 done: §4 text;
mas_core.parser.unquote_local_partandparse_received_address(receiver side; the canonical parser stays strict); the gateway now parses with them; fixture p1-040 note and CHANGELOG. The end-to-end test now uses a real non-MAS recipient, because"quoted"@is equivalent toquoted@. 148 tests pass. -
2026-10-18 SCP-3 done:
- §7 text and the CHANGELOG;
- statute schema
addresses.*.intake_for, with the resource-type pattern (bundle and vendored copy); - lint
intake-not-function(error) andintake-unknown-type(error, or warning when the statute inherits); - the policy reports
intake_for, and it reaches gateway events; - the ticket service creates tickets only for
intake_for == "ticket", not for a hard-coded target name; - example statutes (great-service, mas.coulomb.social) and the conformance statute declare the intake.
151 tests pass. The live statute needs a redeploy to carry
intake_for; until then the new ticket service would not recognise the intake. -
2026-10-18 SCP-5 (§14 item 10) and SCP-7 (§6.2 recommendation) spec text done. The reference service already meets the SCP-5 MUSTs (cap, no replies to automated mail); the SHOULD (authenticated sender) is T05. CHANGELOG marked v0.1.1. T02 done.
Reference service: case-robust ticket ids
id: MAS-WP-0005-T03
status: done
priority: high
state_hub_task_id: "7c19e951-7ab4-580c-9e4c-9fead9ee946e"
SCP-7 evidence: IONOS and Google Groups both lower-cased the recipient, and a list post was lost to its ticket. Make the reference ticket service issue and match ids case-insensitively. Crockford base32 is case-insensitive by design. Verify on the live MX by repeating the Google Groups post.
- 2026-10-18: implemented. Ticket lookup uses
COLLATE NOCASE(also in theticket()/close()admin calls). Once found, the service stores, replies and reports under the id as issued, so replies keep coming Fromticket=GS-…@. Testtest_lower_cased_id_correlatespasses, as do all 137 tests. Deployed asmas-live:wp0005-20261018. A dry check on a copy of the live DB resolvesgs-85qj8wc09ptoGS-85QJ8WC09P. - 2026-10-18 15:38Z: verified live. A Google Groups post arrived for
ticket=gs-85qj8wc09p+comments@and was correlated (200) to GS-85QJ8WC09P with facetcomments. The identical post at 12:05Z had been quarantined. No reply was sent (Precedence: list); still 1/300 sent.
v0.2 drafts: SCP-6 and the SCP-7 statute field
id: MAS-WP-0005-T04
status: done
priority: medium
state_hub_task_id: "4ff67908-52c4-5106-b851-e1dec8274226"
Draft the statute object alternate_syntax (enables the fixed stand-ins
-- for = and --- for !),
the destination transforms (see SCP-6), and
equivalence.resource_id_case, with policy semantics and fixtures.
Split into T07 to T10 on 2026-10-18 (token budget). Stand-in recognition
lives in the policy layer, not the parser: the parser stays statute-blind,
and only a statute that enables the stand-ins gives --/--- meaning.
v0.2 is a separate draft bundle mas-v0.2-spec/, because it adds features
rather than errata.
SCP-5: reply only to authenticated senders
id: MAS-WP-0005-T05
status: done
priority: medium
state_hub_task_id: "8d3bc09e-6f8d-5125-b10c-45b9aa5643a2"
Implement the SHOULD part of spec §14 item 10 in the reference stack:
- Postfix adds
Authentication-Results(e.g. via OpenDMARC or rspamd milter) on the live MX; mas-gatewaypasses the DMARC verdict into the event (event schema addition);- the ticket service sends intake auto-replies only on DMARC pass and records unauthenticated requests without replying.
Decided 2026-10-18 to keep this out of the v0.1.1 errata code.
Split 2026-10-18 into T11 to T13. Authentication runs in the MTA
(OpenDKIM + OpenDMARC milters), because SPF, DKIM and DMARC need DNS and
only mas_core/discovery.py may touch the network. The gateway only
reads the Authentication-Results header carrying our own authserv-id,
and the MTA strips incoming headers that claim it.
Redeploy the live MX with v0.1.1
id: MAS-WP-0005-T06
status: done
priority: medium
state_hub_task_id: "f9bbfc03-e611-573f-8017-91cd18472ba9"
Build and deploy an image with the v0.1.1 parser, the intake_for
statute and the case-insensitive ticket service. Then re-run a smoke test
(intake create, resource correlate, lower-case correlate).
-
2026-10-18: before the deploy, the ticket service was changed to issue lower-case ids (
gs-+ lower-case Crockford base32), at the founder's request after a reply showedticket=GS-6KDSB1BA11@. Existing upper-case tickets still match. Deployedmas-live:v0.1.1-f78939a. The live statute servesintake_for: "ticket",mas discoveris valid,--max-sent 300and the TEM relay are intact, and EHLO answers 250.mas explainnow showsintake_for. -
2026-10-18 smoke test (founder, Gmail):
ticket!callback@(16:34Z) created ticketgs-8b72wz9tyeviaintake_for, and the reply was sent through TEM;- the answer to
ticket=gs-8b72wz9tye@was correlated (In-Reply-To set); ticket=GS-8B72WZ9TYE@(20:14Z) arrived upper-case, because Gmail does not fold, and was correlated to the same ticket;- two typo sends to
ticket=GW-8B72WZ9TYE@(Gmail autocomplete) were quarantined with no reply.
Sends: 3/300. Done.
v0.2 draft bundle and version support
id: MAS-WP-0005-T07
status: done
priority: medium
state_hub_task_id: "908688e6-559b-5d44-a8bc-284e3bbc9366"
Create mas-v0.2-spec/ (draft): the specification with SCP-6
(alternate_syntax, matching order, destination transforms, informative
integration section) and the SCP-7 statute field
equivalence.resource_id_case, the v0.2 statute schema, and a CHANGELOG.
mas_core accepts statutes with mas_version 0.1 and 0.2, each validated
against its own schema. Discovery accepts both.
- 2026-10-18 done:
mas-v0.2-spec/draft: §6.3 stand-ins with the matching order, §8resource_id_case, §11 fields, §12.1 destination transforms, §12.2 informative integrations (IONOS, Google Groups), and §13 length after transforms;- schema v0.2 (
mas_version0.2,alternate_syntax,equivalence.resource_id_case), an example statute and the CHANGELOG. The grammar is unchanged; stand-ins are not canonical syntax. mas_core: one schema validator per version (SUPPORTED_VERSIONS);Statutecarriesmas_version,alt_resource,alt_interactionandresource_id_case; inheritance usesdataclasses.replace; discovery accepts 0.1 and 0.2.- Tests that used 0.2 as the "unsupported" example now use 0.3. 155 tests pass.
- The draft keeps 25 "v0.1" mentions for rules that are unchanged; align the wording when v0.2 is finalized.
v0.2 receiving side: stand-ins and resource_id_case in policy
id: MAS-WP-0005-T08
status: done
priority: medium
state_hub_task_id: "a345e073-04ca-54e1-bac7-837fe7955e4e"
The policy interprets ---MODE (interaction first, enabled mode only) and
TYPE--ID (declared type only) when the statute enables them, and applies
resource_id_case: insensitive in the equivalence key. Statute lint covers
the stand-in constraints. Statute-aware fixtures.
- 2026-10-18 done:
mas_core.policy.apply_stand_insruns first inresolve(). It matches the interaction (---MODE, enabled mode, last---; in the facet or the target) and then the resource (TYPE--ID, declared type, first--). Everything later (classification, registry,intake_for, equivalence) sees the canonical parse, and the parser stays statute-blind.AddressSemanticsgainsstand_insandcanonical_local_part; the equivalence key lower-cases resource ids underresource_id_case: insensitive.- The gateway event reports the canonical target and base, plus
target_syntax: resource-alternatewhen--was used, whilelocal_partkeeps what arrived. - Lint:
stand-in-collision(registry keys that would change meaning),stand-in-mode-name, andstand-in-without-p2(warning). - Tests: the spec §6.3 example table, correlation of
ticket--GS-…+commentswithticket=gs-…, intake via---, the switch requirement (v0.1, off, no statute), exact id case, and lint. 168 tests pass. - A v0.2 statute for mas.coulomb.social lints clean, and the gateway
turns
ticket--gs-85qj8wc09p+comments@into resourceticket=gs-85qj8wc09pwith facetcomments.
v0.2 sending side: destination transforms in the generator
id: MAS-WP-0005-T09
status: done
priority: medium
state_hub_task_id: "853d1b76-8b7f-5234-aacc-dc1fb5d155c2"
Add a generator destination parameter with the five transforms. Emit the
canonical form when allowed; refuse when the namespace would not accept the
result; re-check the length after transforms. Include an example destination
profile ionos-forward. This is the base for the composer (MAS-WP-0006).
- 2026-10-18 done:
mas_core.generator.for_destination(local_part, statute, needs, allow_lossy=False)returnsDestinationAddress(local_part, applied, warnings).- It applies only the transforms that change something.
- Lossy transforms need
allow_lossy;drop-interactionadds a warning. - Refusals raise
CannotExpress:transform-not-permitted(statute switch off, or a v0.1 statute),cannot-express(id contains=),too-long(after transforms) andwould-be-misread. - Safety is a round trip: the result is resolved with the receiving
policy and must give the same equivalence key, facet and interaction.
This catches lower-casing under
resource_id_case: exactand case changes in facets. - Example profiles:
canonical,ionos-forward(resource-alternate + lowercase),google-groups(lowercase). - 11 new tests; 179 pass.
- Correction: there is no
mas generateCLI command. The review's gate table and MAS-WP-0006 now point tofor_destination; the CLI/UI is MAS-WP-0006.
v0.2 live check: IONOS forward with stand-ins
id: MAS-WP-0005-T10
status: done
priority: medium
state_hub_task_id: "6431ae1e-c449-5732-a40f-99b0644da44a"
Switch mas.coulomb.social to a v0.2 statute that enables both stand-ins
and case-insensitive ids. Repeat the IONOS forward test with
ticket--gs-…+comments@. It must now be correlated to the ticket.
- 2026-10-18 done. The live statute is v0.2 (
alternate_syntaxresource+interaction,resource_id_case: insensitive), deployed as imagemas-live:v0.2-51bf87c. The operator sent tomas-fwd@coulomb.social(IONOS forward, unchanged since MAS-WP-0003). It arrived at 20:29:57Z forticket--gs-85qj8wc09p+comments@(envelope sender SRS0 from IONOS) and was read asticket=gs-85qj8wc09p+comments(target_syntax: resource-alternate). It was correlated (200) to GS-85QJ8WC09P with facetcomments; in MAS-WP-0003 the same forward was 404 not-handled. The ticket now holds mail via all four paths: direct, reply, Google Groups (lower-cased) and IONOS forward (stand-in). T04 is complete.mas explainalso shows "read as … (v0.2 stand-in)".
MTA authentication: OpenDKIM + OpenDMARC milters
id: MAS-WP-0005-T11
status: done
priority: medium
state_hub_task_id: "5c4eddfd-a371-59d2-a4f1-3915a675f639"
In the live image, OpenDKIM (verify only) and OpenDMARC (SPF
self-validation) run as Postfix smtpd milters. Both add
Authentication-Results: mas.coulomb.social; …. Incoming A-R headers
claiming that authserv-id are removed (RemoveARFrom). Annotate only;
never reject on DMARC failure. Milter failure means accept
(milter_default_action = accept).
- 2026-10-18 done (verified in a local container; live in T13):
- Image packages: opendkim, opendmarc, publicsuffix. OpenDKIM runs in
verify mode; OpenDMARC with
SPFSelfValidate,RejectFailures falseandTrustedAuthservIDsset to the domain. - Forged A-R stripping is done by Postfix, not OpenDKIM.
RemoveARFromandRemoveARAlldid not remove forgedAuthentication-Results: mas.coulomb.social; dmarc=passheaders. Aheader_checksregexp IGNORE rule does, and it leaves the milters' own headers alone, because header_checks do not apply to milter-added headers. Verified for a forged pass, an upper-case variant and a foreign id (foreign ids are kept; the gateway ignores them). - Daemonized, OpenDKIM died on its first connection ("can't read
SMFIC_OPTNEG reply"), so both milters now run with
-fin the background. - Result for a spoofed
From: test@coulomb.socialsent directly:spf=fail,dmarc=fail (p=none). Our A-R headers sit above ourReceived:line; anything forged sits below it. - Note for MAS-WP-0004: use header_checks for A-R stripping and foreground milters.
- Image packages: opendkim, opendmarc, publicsuffix. OpenDKIM runs in
verify mode; OpenDMARC with
Gateway verdict and reply gating
id: MAS-WP-0005-T12
status: done
priority: medium
state_hub_task_id: "5a3b7356-37c0-5292-8936-492946c6b211"
mas-gateway takes --trusted-authserv-id. It parses only A-R headers
from that id and puts message.authentication = {dmarc, spf, dkim, header_from} into the event. The ticket service gets
--require-dmarc-pass: it still creates tickets and attaches mail, but
sends auto-replies (intake confirmation, closed notice) only when
dmarc == pass. Unit tests cover forged A-R headers from other ids.
- 2026-10-18 done:
mas_gateway.core.authentication_from_headersreads only A-R headers with the trusted authserv-id above the first Received header (RFC 8601 §5). Forged copies below our hop are ignored even if the MTA did not strip them. Several DKIM results count as pass if any one passes.- The event gets
message.authentication = {dmarc, spf, dkim, header_from}or null.--trusted-authserv-idworks on both pipe and webhook; a JSON webhook passes the provider's verdict through. - Ticket service
--require-dmarc-pass: tickets are still created and mail attached, but replies go out only ondmarc == pass. - Entrypoint: gateway
--trusted-authserv-id $DOMAIN, ticket service--require-dmarc-pass. - Tests: own header read; forged (below Received) and foreign ids ignored; no trusted id gives no verdict; reply gated off without a verdict and sent on pass. 183 tests pass.
Live check: authenticated vs forged sender
id: MAS-WP-0005-T13
status: done
priority: medium
state_hub_task_id: "8a1f1fc4-f300-5e60-8714-5a47c39d88a1"
Deploy. A Gmail intake (DMARC pass) gets a reply. A mail with a
From: in the operator's own domain sent directly from the workstation
(SPF/DKIM fail, so DMARC fail) creates a ticket but gets no reply. Check
the send counter.
- 2026-10-18 done (image
mas-live:v0.2-a45f1a1; both milters listening, gateway--trusted-authserv-id, ticket service--require-dmarc-pass):- Forged: sent from the workstation with
From: test@coulomb.socialand a forgedAuthentication-Results: mas.coulomb.social; dmarc=pass. The event verdict wasdmarc: fail, spf: fail: the forgery was stripped and the real verdict used. Ticketgs-mt1hcg3taewas created and no reply sent (no outbox row; sends stayed at 3/300). - Gmail (founder): the verdict was
dkim: pass, spf: pass, dmarc: pass(header.from gmail.com), the first live DKIM verification by OpenDKIM. Ticketgs-rcce2nne4kwas created and the reply sent through TEM (sends 4/300). - T05 is complete. The reference stack now meets all of spec §14 item 10.
- Forged: sent from the workstation with
Residuals
Handed off as live work records (origin: residual, origin_ref MAS-WP-0005):
- MAS-WP-0008: finalize v0.2 (wording pass, bundle conformance cases for v0.2, release decision).
- MAS-WP-0004 (existing) carries the milter/header_checks lessons for the
Postfix profile; MAS-WP-0006 (existing) builds on
generator.for_destination.