mail-address-semantics/workplans/MAS-WP-0005-spec-errata-v0-1-1.md
tegwick 2123e2e4c9 MAS-WP-0005 finished; residual MAS-WP-0008 (finalize v0.2)
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
2026-10-18 22:47:04 +02:00

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_syntax holds resource: "--" and interaction: "__" (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 (---MODE at 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=PR42 becomes non-canonical, not opaque.

  • 2026-10-18 SCP-1 done: §4 text; mas_core.parser.unquote_local_part and parse_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 to quoted@. 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) and intake-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 the ticket()/close() admin calls). Once found, the service stores, replies and reports under the id as issued, so replies keep coming From ticket=GS-…@. Test test_lower_cased_id_correlates passes, as do all 137 tests. Deployed as mas-live:wp0005-20261018. A dry check on a copy of the live DB resolves gs-85qj8wc09p to GS-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 facet comments. 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-gateway passes 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 showed ticket=GS-6KDSB1BA11@. Existing upper-case tickets still match. Deployed mas-live:v0.1.1-f78939a. The live statute serves intake_for: "ticket", mas discover is valid, --max-sent 300 and the TEM relay are intact, and EHLO answers 250. mas explain now shows intake_for.

  • 2026-10-18 smoke test (founder, Gmail):

    • ticket!callback@ (16:34Z) created ticket gs-8b72wz9tye via intake_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, §8 resource_id_case, §11 fields, §12.1 destination transforms, §12.2 informative integrations (IONOS, Google Groups), and §13 length after transforms;
    • schema v0.2 (mas_version 0.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); Statute carries mas_version, alt_resource, alt_interaction and resource_id_case; inheritance uses dataclasses.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_ins runs first in resolve(). 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.
    • AddressSemantics gains stand_ins and canonical_local_part; the equivalence key lower-cases resource ids under resource_id_case: insensitive.
    • The gateway event reports the canonical target and base, plus target_syntax: resource-alternate when -- was used, while local_part keeps what arrived.
    • Lint: stand-in-collision (registry keys that would change meaning), stand-in-mode-name, and stand-in-without-p2 (warning).
    • Tests: the spec §6.3 example table, correlation of ticket--GS-…+comments with ticket=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 resource ticket=gs-85qj8wc09p with facet comments.

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) returns DestinationAddress(local_part, applied, warnings).
    • It applies only the transforms that change something.
    • Lossy transforms need allow_lossy; drop-interaction adds a warning.
    • Refusals raise CannotExpress: transform-not-permitted (statute switch off, or a v0.1 statute), cannot-express (id contains =), too-long (after transforms) and would-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: exact and 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 generate CLI command. The review's gate table and MAS-WP-0006 now point to for_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_syntax resource+interaction, resource_id_case: insensitive), deployed as image mas-live:v0.2-51bf87c. The operator sent to mas-fwd@coulomb.social (IONOS forward, unchanged since MAS-WP-0003). It arrived at 20:29:57Z for ticket--gs-85qj8wc09p+comments@ (envelope sender SRS0 from IONOS) and was read as ticket=gs-85qj8wc09p+comments (target_syntax: resource-alternate). It was correlated (200) to GS-85QJ8WC09P with facet comments; 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 explain also 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 false and TrustedAuthservIDs set to the domain.
    • Forged A-R stripping is done by Postfix, not OpenDKIM. RemoveARFrom and RemoveARAll did not remove forged Authentication-Results: mas.coulomb.social; dmarc=pass headers. A header_checks regexp 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 -f in the background.
    • Result for a spoofed From: test@coulomb.social sent directly: spf=fail, dmarc=fail (p=none). Our A-R headers sit above our Received: line; anything forged sits below it.
    • Note for MAS-WP-0004: use header_checks for A-R stripping and foreground milters.

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_headers reads 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-id works 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 on dmarc == 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.social and a forged Authentication-Results: mas.coulomb.social; dmarc=pass. The event verdict was dmarc: fail, spf: fail: the forgery was stripped and the real verdict used. Ticket gs-mt1hcg3tae was 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. Ticket gs-rcce2nne4k was created and the reply sent through TEM (sends 4/300).
    • T05 is complete. The reference stack now meets all of spec §14 item 10.

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.