Register with State Hub; add MAS v0.1 spec bundle, intent and reference-experiment workplan
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Assistant: claude-code Assistant-Model: opus Assistant-Process: 623586@bnt-lap001 Assistant-Session: 10b30928-5b3b-4c11-9934-5ecca07367d8
This commit is contained in:
parent
41e2b1dfd1
commit
52140cee9b
15 changed files with 2970 additions and 0 deletions
27
.custodian-brief.md
Normal file
27
.custodian-brief.md
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<!-- custodian-brief: generated by statehub register; fix-consistency may replace this file -->
|
||||
# Custodian Brief - mail-address-semantics
|
||||
|
||||
**Project:** mail-address-semantics
|
||||
**Domain:** communication
|
||||
**State Hub:** http://127.0.0.1:8000
|
||||
**Topic ID:** `36c7421b-c537-4723-bf75-42a3ebc6a1dc`
|
||||
|
||||
## Open Workplans
|
||||
|
||||
### Bootstrap State Hub integration
|
||||
|
||||
Workplan file: `workplans/MAS-WP-0001-statehub-bootstrap.md`
|
||||
|
||||
Open tasks:
|
||||
- T01 - Review generated integration files
|
||||
- T02 - Verify local developer workflow
|
||||
- T03 - Seed first real workplan
|
||||
|
||||
## Session Start
|
||||
|
||||
1. Read `INTENT.md`, `SCOPE.md`, and `AGENTS.md`.
|
||||
2. Check inbox: `GET /messages/?to_agent=mail-address-semantics&unread_only=true`.
|
||||
3. Scan `workplans/`.
|
||||
4. Update task statuses in workplan files as work progresses.
|
||||
|
||||
Last generated: 2026-10-08
|
||||
14
.gitignore
vendored
Normal file
14
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
# state-hub: track .claude/rules
|
||||
# Claude Code local state (track shared rules; ignore machine-specific files)
|
||||
.claude/*
|
||||
!.claude/rules/
|
||||
!.claude/rules/*.md
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.venv/
|
||||
*.egg-info/
|
||||
.pytest_cache/
|
||||
build/
|
||||
dist/
|
||||
205
AGENTS.md
Normal file
205
AGENTS.md
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
# mail-address-semantics — Agent Instructions
|
||||
|
||||
## Repo Identity
|
||||
|
||||
**Purpose:** Mail Address Semantics (MAS) v0.1 reference implementation: parser, statute discovery, pure policy, CLI, mail gateway and reference ticket service.
|
||||
|
||||
**Domain:** communication
|
||||
**Repo slug:** mail-address-semantics
|
||||
**Topic ID:** `36c7421b-c537-4723-bf75-42a3ebc6a1dc`
|
||||
**Workplan prefix:** `MAS-WP-`
|
||||
|
||||
---
|
||||
|
||||
## State Hub Integration
|
||||
|
||||
The Custodian State Hub tracks work across all domains. Codex uses HTTP REST and
|
||||
the `statehub` CLI by default. MCP is opt-in because the current Codex MCP bridge
|
||||
adds severe call latency; the full administrative MCP surface remains available
|
||||
to clients that need it.
|
||||
|
||||
| Context | URL |
|
||||
|---------|-----|
|
||||
| Local workstation | `http://127.0.0.1:8000` |
|
||||
| Remote via tunnel | `http://127.0.0.1:18000` |
|
||||
| Optional local edge relay | http://127.0.0.1:18080 |
|
||||
|
||||
When an operator has enabled the edge relay, set API_BASE to the relay URL.
|
||||
Queueable writes return an explicit queued receipt if the central hub is
|
||||
unreachable. Treat that as pending local evidence, then ask the operator to run
|
||||
statehub outbox status/replay after connectivity returns.
|
||||
|
||||
Codex workspace-write sandboxes need network access enabled to reach the host's
|
||||
loopback listener. Bootstrap this once with `make -C ~/state-hub configure-codex`
|
||||
and restart Codex. The canonical REST health endpoint is `/state/health`, not
|
||||
`/health`. If a sandboxed loopback probe fails, retry it with escalated execution
|
||||
before declaring State Hub unavailable; a managed Codex permission profile may
|
||||
still enforce isolated networking. Experimental MCP can be enabled explicitly
|
||||
with `make -C ~/state-hub configure-codex WITH_MCP=1`.
|
||||
|
||||
### Orient at session start
|
||||
|
||||
```bash
|
||||
# Offline brief — works without hub connection
|
||||
cat .custodian-brief.md
|
||||
|
||||
# Active workplans for this domain
|
||||
curl -s "http://127.0.0.1:8000/workplans/?topic_id=36c7421b-c537-4723-bf75-42a3ebc6a1dc&status=active" \
|
||||
| python3 -m json.tool
|
||||
|
||||
# Check inbox
|
||||
curl -s "http://127.0.0.1:8000/messages/?to_agent=mail-address-semantics&unread_only=true" \
|
||||
| python3 -m json.tool
|
||||
```
|
||||
|
||||
Mark a message read:
|
||||
```bash
|
||||
curl -s -X PATCH "http://127.0.0.1:8000/messages/<id>/read" \
|
||||
-H "Content-Type: application/json" -d '{}'
|
||||
```
|
||||
|
||||
### Log progress (required at session close)
|
||||
|
||||
```bash
|
||||
curl -s -X POST http://127.0.0.1:8000/progress/ \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"summary": "what was done",
|
||||
"event_type": "note",
|
||||
"author": "codex",
|
||||
"workplan_id": "<uuid>",
|
||||
"task_id": "<uuid>"
|
||||
}'
|
||||
```
|
||||
|
||||
Omit `workplan_id` / `task_id` when not applicable.
|
||||
|
||||
### Update task status
|
||||
|
||||
```bash
|
||||
curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"status": "progress"}'
|
||||
# values: wait | todo | progress | done | cancel
|
||||
```
|
||||
|
||||
### Flag a task for human review
|
||||
|
||||
```bash
|
||||
curl -s -X PATCH "http://127.0.0.1:8000/tasks/<task_id>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"needs_human": true, "intervention_note": "reason"}'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Session Protocol
|
||||
|
||||
**Start:**
|
||||
1. `cat .custodian-brief.md` — domain goal and open workplans (offline-safe)
|
||||
2. Check inbox: `GET /messages/?to_agent=mail-address-semantics&unread_only=true`; mark read
|
||||
3. Scan workplans: `ls workplans/` — note `status: ready`, `active`, or `blocked` files and open tasks
|
||||
4. Check human-needed tasks: `GET /tasks/?needs_human=true`
|
||||
|
||||
**During work:**
|
||||
- Update task statuses in workplan files as tasks progress
|
||||
- Record significant decisions via `POST /decisions/`
|
||||
|
||||
**Close:**
|
||||
1. Update workplan file task statuses to reflect progress
|
||||
2. If finishing a workplan: hand off **residuals** as live work records first
|
||||
(intake with `origin: residual` + `origin_ref: <WP-id>`, or a next workplan /
|
||||
decision / engagement). Do not park leftovers only in prose or `SCOPE.md`.
|
||||
Canon: `the-custodian/canon/standards/work-record-types_v0.1.md` § Residuals.
|
||||
3. Log: `POST /progress/` with a summary of what changed (name handoff ids)
|
||||
4. After workplan file changes, run:
|
||||
```bash
|
||||
uv run --project ~/repo-manager rmgr sync --path . --push
|
||||
```
|
||||
This assigns only missing deterministic identifiers, verifies the pushed
|
||||
Forgejo commit and `primary/railliance01`, then requests one central
|
||||
reconciliation. A queued receipt is pending evidence; rerun after
|
||||
connectivity returns. Use `statehub fix-consistency` for a separate deep audit.
|
||||
|
||||
---
|
||||
|
||||
{CREDENTIAL_ROUTING}
|
||||
|
||||
<!-- REPO-AGENTS-EXTENSIONS -->
|
||||
<!-- Append repo-specific agent instructions below this marker.
|
||||
The state-hub template sync preserves content after this line. -->
|
||||
|
||||
---
|
||||
|
||||
## Workplan Convention (ADR-001)
|
||||
|
||||
Work items originate as files in this repo — not in the hub. The hub is a
|
||||
read/cache/index layer that rebuilds from files.
|
||||
|
||||
**File location:** `workplans/MAS-WP-NNNN-<slug>.md`
|
||||
|
||||
**Archived location:** finished workplans may move to
|
||||
`workplans/archived/YYMMDD-MAS-WP-NNNN-<slug>.md`. The `YYMMDD` prefix is
|
||||
the completion/archive date; the frontmatter `id` does not change.
|
||||
|
||||
**Ad Hoc Tasks:** small opportunistic fixes discovered during a session use
|
||||
`workplans/ADHOC-YYYY-MM-DD.md`, workplan id
|
||||
`MAS-WP-ADHOC-YYYY-MM-DD`, and task ids
|
||||
`MAS-WP-ADHOC-YYYY-MM-DD-T01`, etc. `MAS-WP` includes its final `-WP`
|
||||
token. Unqualified historic `ADHOC-*` ids are grandfathered and must not be
|
||||
copied into new records. Use this only for low-risk work completed directly;
|
||||
create a normal workplan for anything needing analysis, design, approval,
|
||||
dependencies, or multiple phases.
|
||||
|
||||
**Frontmatter:**
|
||||
|
||||
```yaml
|
||||
---
|
||||
id: MAS-WP-NNNN
|
||||
type: workplan
|
||||
title: "..."
|
||||
domain: communication
|
||||
repo: mail-address-semantics
|
||||
status: proposed | ready | active | blocked | backlog | finished | archived
|
||||
owner: codex
|
||||
topic_slug: ...
|
||||
created: "YYYY-MM-DD"
|
||||
updated: "YYYY-MM-DD"
|
||||
state_hub_workstream_id: "<uuid>" # deterministic UUIDv5; managed by Repo Manager
|
||||
---
|
||||
```
|
||||
|
||||
Use `proposed` for a new draft, `ready` after review against current repo
|
||||
state, and `finished` after implementation. `stalled` and `needs_review` are
|
||||
derived health labels, not frontmatter statuses.
|
||||
|
||||
**Terminology:** workplan is the fleet term; `workstream` appears only in legacy
|
||||
API/MCP/frontmatter bridges until `STATE-WP-0069` retires them — see
|
||||
`the-custodian/canon/standards/workplan-terminology-fleet_v0.1.md`.
|
||||
|
||||
**Task block format** (one per `##` section):
|
||||
|
||||
```
|
||||
## Task Title
|
||||
|
||||
` ` `task
|
||||
id: MAS-WP-NNNN-T01
|
||||
status: wait | todo | progress | done | cancel
|
||||
priority: high | medium | low
|
||||
state_hub_task_id: "<uuid>" # deterministic UUIDv5; managed by Repo Manager
|
||||
` ` `
|
||||
|
||||
Task description text.
|
||||
```
|
||||
|
||||
Status progression: `todo` → `progress` → `done`; use `wait` for waiting/blocked work and `cancel` for stopped work.
|
||||
|
||||
**Residuals when finishing:** actionable leftovers become live work records
|
||||
before `status: finished` — usually an intake (`origin: residual`,
|
||||
`origin_ref: MAS-WP-NNNN`) or a spawned workplan. Residual is a *role*,
|
||||
not a kind. Fleet list lives on State Hub, not in `SCOPE.md`.
|
||||
|
||||
To create a new workplan:
|
||||
1. Write the file following the format above
|
||||
2. Run `uv run --project ~/repo-manager rmgr sync --path . --push`.
|
||||
3. Run `statehub fix-consistency` only when a separate deep audit is needed.
|
||||
494
INTENT.md
Normal file
494
INTENT.md
Normal file
|
|
@ -0,0 +1,494 @@
|
|||
# MAS — Mail Address Semantics
|
||||
|
||||
## Intent
|
||||
|
||||
Build the smallest practical implementation of **Mail Address Semantics (MAS) v0.1** that can prove whether the specification is useful in real email workflows.
|
||||
|
||||
The project should demonstrate that ordinary email addresses can carry limited, human-readable and machine-parseable semantics for:
|
||||
|
||||
- opaque organizational targets,
|
||||
- resource addressing,
|
||||
- facets,
|
||||
- optional interaction preferences,
|
||||
|
||||
without modifying SMTP and without turning addresses into credentials, commands, contracts, or payment instructions.
|
||||
|
||||
The implementation must preserve the core architectural boundary of MAS:
|
||||
|
||||
> **Parsing is local and deterministic. Discovery is networked. Policy is pure. Delivery remains external.**
|
||||
|
||||
The first implementation should be deliberately narrow. Its purpose is not to build a full MAS ecosystem, but to test the convention against real messages, real routing, resource lifecycle, malformed input, and namespace metadata.
|
||||
|
||||
---
|
||||
|
||||
## Core hypothesis
|
||||
|
||||
MAS is valuable if the following can be made both simple and reliable:
|
||||
|
||||
```text
|
||||
ticket!callback@great-service.example
|
||||
```
|
||||
|
||||
can express:
|
||||
|
||||
> initiate a ticket interaction with callback as the preferred continuation mode
|
||||
|
||||
and result in a stable resource address:
|
||||
|
||||
```text
|
||||
ticket=GS-41872@great-service.example
|
||||
```
|
||||
|
||||
that can subsequently be used to correlate email with that ticket without embedding application identifiers in subject lines.
|
||||
|
||||
If this lifecycle works cleanly through ordinary email infrastructure, MAS has demonstrated its core value.
|
||||
|
||||
---
|
||||
|
||||
## Design principles
|
||||
|
||||
### 1. The parser does not use the network
|
||||
|
||||
Parsing operates only on the address syntax.
|
||||
|
||||
It must:
|
||||
|
||||
- validate canonical MAS syntax,
|
||||
- produce an explicit parse tree,
|
||||
- identify which MAS profile the syntax requires,
|
||||
- never perform statute discovery,
|
||||
- never infer person, actor, or function kind from `.`, `-`, or `_`.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
ticket!callback
|
||||
```
|
||||
|
||||
parses structurally as an interaction-bearing target and records:
|
||||
|
||||
```text
|
||||
required_profile = P2
|
||||
```
|
||||
|
||||
Whether the addressed namespace supports P2 is decided later.
|
||||
|
||||
---
|
||||
|
||||
### 2. Discovery returns facts or typed failure
|
||||
|
||||
MAS discovery is responsible only for retrieving and validating the exact-host statute.
|
||||
|
||||
Given:
|
||||
|
||||
```text
|
||||
anna.mueller@acme.coulomb.social
|
||||
```
|
||||
|
||||
discovery targets:
|
||||
|
||||
```text
|
||||
https://acme.coulomb.social/.well-known/mas
|
||||
```
|
||||
|
||||
It must not walk parent domains implicitly.
|
||||
|
||||
Discovery should return either:
|
||||
|
||||
```text
|
||||
ValidatedStatute
|
||||
```
|
||||
|
||||
or a typed failure such as:
|
||||
|
||||
```text
|
||||
NotFound
|
||||
HttpFailure
|
||||
CrossHostRedirect
|
||||
SchemaInvalid
|
||||
NamespaceMismatch
|
||||
InheritanceCycle
|
||||
UnsupportedVersion
|
||||
```
|
||||
|
||||
Failure to discover MAS metadata does not invalidate the email address.
|
||||
|
||||
---
|
||||
|
||||
### 3. Policy is pure
|
||||
|
||||
Policy evaluation takes:
|
||||
|
||||
```text
|
||||
parse tree
|
||||
+
|
||||
validated statute
|
||||
+
|
||||
optional local registry
|
||||
```
|
||||
|
||||
and produces semantic interpretation.
|
||||
|
||||
Policy performs no network access.
|
||||
|
||||
This allows the same policy implementation to be exercised entirely through the conformance corpus.
|
||||
|
||||
---
|
||||
|
||||
### 4. No semantic inference from naming punctuation
|
||||
|
||||
The following remain naming hints only:
|
||||
|
||||
```text
|
||||
.
|
||||
-
|
||||
_
|
||||
```
|
||||
|
||||
Therefore:
|
||||
|
||||
```text
|
||||
bernd.worsch@
|
||||
research-companion@
|
||||
invoice_ingest@
|
||||
```
|
||||
|
||||
may visually suggest person, actor, and function, but machine classification requires statute or registry metadata.
|
||||
|
||||
No statute means:
|
||||
|
||||
```text
|
||||
kind = opaque
|
||||
```
|
||||
|
||||
not:
|
||||
|
||||
```text
|
||||
kind = person-like
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Resource semantics are the primary proof
|
||||
|
||||
The initial implementation should strongly support:
|
||||
|
||||
```text
|
||||
type=id@
|
||||
```
|
||||
|
||||
Resource addresses must remain usable as correlation identifiers while remaining independent of authorization.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
ticket=GS-41872@
|
||||
```
|
||||
|
||||
identifies the ticket communication endpoint.
|
||||
|
||||
It does not grant permission to inspect or mutate ticket GS-41872.
|
||||
|
||||
---
|
||||
|
||||
### 6. Resource addressability outlives mutability
|
||||
|
||||
Closed resources remain addressable.
|
||||
|
||||
A closed ticket may:
|
||||
|
||||
- archive later email,
|
||||
- reject mutation,
|
||||
- return lifecycle information,
|
||||
- create a linked follow-up ticket.
|
||||
|
||||
It must not become mutable merely because mail can still reach its address.
|
||||
|
||||
---
|
||||
|
||||
### 7. Facets degrade safely
|
||||
|
||||
A facet:
|
||||
|
||||
```text
|
||||
ticket=GS-41872+comments@
|
||||
```
|
||||
|
||||
may be stripped by existing email infrastructure.
|
||||
|
||||
If so, the remaining address:
|
||||
|
||||
```text
|
||||
ticket=GS-41872@
|
||||
```
|
||||
|
||||
still identifies the base resource.
|
||||
|
||||
Facet preservation is desirable but never assumed.
|
||||
|
||||
---
|
||||
|
||||
### 8. Interaction remains a preference
|
||||
|
||||
The P2 syntax:
|
||||
|
||||
```text
|
||||
ticket!callback@
|
||||
```
|
||||
|
||||
requests that the addressed endpoint continue through `callback`.
|
||||
|
||||
It does not imply:
|
||||
|
||||
- who initiates the callback,
|
||||
- that callback is available,
|
||||
- that callback has been accepted,
|
||||
- an SLA,
|
||||
- a command.
|
||||
|
||||
The application decides whether and how to satisfy the request.
|
||||
|
||||
---
|
||||
|
||||
### 9. Economic syntax remains closed
|
||||
|
||||
The characters:
|
||||
|
||||
```text
|
||||
$
|
||||
^
|
||||
```
|
||||
|
||||
remain reserved only.
|
||||
|
||||
No implementation work on price, reward, conditions, offers, contracts, or settlement belongs in this phase.
|
||||
|
||||
---
|
||||
|
||||
## Initial architecture
|
||||
|
||||
The first repository should contain only the pieces needed for the experiment.
|
||||
|
||||
```text
|
||||
mas/
|
||||
├── mas_core/
|
||||
│ ├── parser
|
||||
│ ├── model
|
||||
│ ├── discovery
|
||||
│ ├── policy
|
||||
│ └── lint
|
||||
├── cli/
|
||||
│ └── mas
|
||||
├── conformance/
|
||||
│ ├── addresses
|
||||
│ ├── statutes
|
||||
│ └── properties
|
||||
├── examples/
|
||||
│ ├── statute/
|
||||
│ └── ticket_service/
|
||||
├── gateway/
|
||||
│ └── smtp_or_webhook_to_http/
|
||||
└── docs/
|
||||
```
|
||||
|
||||
The existing MAS v0.1 specification bundle is the normative basis.
|
||||
|
||||
---
|
||||
|
||||
## Initial components
|
||||
|
||||
### `mas-core`
|
||||
|
||||
Python reference implementation.
|
||||
|
||||
Responsibilities:
|
||||
|
||||
- parse canonical MAS syntax,
|
||||
- expose structured parse objects,
|
||||
- identify required profile,
|
||||
- validate statutes,
|
||||
- discover exact-host statutes,
|
||||
- apply equivalence rules,
|
||||
- evaluate semantics from parse tree plus statute,
|
||||
- lint statutes.
|
||||
|
||||
The first implementation should be clear and auditable rather than clever.
|
||||
|
||||
---
|
||||
|
||||
### `mas-cli`
|
||||
|
||||
Primary human interface.
|
||||
|
||||
Initial commands:
|
||||
|
||||
```text
|
||||
mas parse ADDRESS
|
||||
mas explain ADDRESS
|
||||
mas discover HOST
|
||||
mas lint-statute FILE
|
||||
mas test
|
||||
```
|
||||
|
||||
`mas explain` should clearly distinguish:
|
||||
|
||||
```text
|
||||
syntactic facts
|
||||
discovered namespace facts
|
||||
policy-derived semantics
|
||||
unknowns
|
||||
```
|
||||
|
||||
A discovery failure must remain visible.
|
||||
|
||||
---
|
||||
|
||||
### `mas-conformance`
|
||||
|
||||
The conformance corpus should become a first-class package rather than an incidental test directory.
|
||||
|
||||
It should contain:
|
||||
|
||||
- positive syntax cases,
|
||||
- negative syntax cases,
|
||||
- parser expectations,
|
||||
- generator expectations,
|
||||
- statute validation cases,
|
||||
- discovery policy cases,
|
||||
- property and boundary cases.
|
||||
|
||||
Every future language implementation should consume this corpus unchanged.
|
||||
|
||||
---
|
||||
|
||||
### Static statute
|
||||
|
||||
The first service does not need a statute server framework.
|
||||
|
||||
A static file served at:
|
||||
|
||||
```text
|
||||
/.well-known/mas
|
||||
```
|
||||
|
||||
is sufficient.
|
||||
|
||||
The point is to test discovery semantics, not middleware.
|
||||
|
||||
---
|
||||
|
||||
### Generic mail gateway
|
||||
|
||||
The first mail integration should translate inbound mail into a simple HTTP request.
|
||||
|
||||
It may receive mail via:
|
||||
|
||||
- a local SMTP listener, or
|
||||
- a provider-style inbound webhook.
|
||||
|
||||
Its responsibility should be minimal:
|
||||
|
||||
```text
|
||||
email recipient
|
||||
→ MAS parse
|
||||
→ statute/policy resolution
|
||||
→ normalized HTTP event
|
||||
```
|
||||
|
||||
It should not contain ticket-specific business logic.
|
||||
|
||||
---
|
||||
|
||||
### Reference ticket service
|
||||
|
||||
A deliberately small example application proving the canonical lifecycle.
|
||||
|
||||
Required behavior:
|
||||
|
||||
#### Initial interaction
|
||||
|
||||
Mail to:
|
||||
|
||||
```text
|
||||
ticket!callback@great-service.example
|
||||
```
|
||||
|
||||
creates a ticket.
|
||||
|
||||
The service generates:
|
||||
|
||||
```text
|
||||
ticket=GS-41872@great-service.example
|
||||
```
|
||||
|
||||
#### Subsequent correlation
|
||||
|
||||
Mail sent to:
|
||||
|
||||
```text
|
||||
ticket=GS-41872@great-service.example
|
||||
```
|
||||
|
||||
is associated with that ticket without depending on the subject line.
|
||||
|
||||
#### Closed lifecycle
|
||||
|
||||
After the ticket is closed, its address remains routable.
|
||||
|
||||
Later mail must not silently reopen or mutate the resource unless application policy explicitly allows it.
|
||||
|
||||
---
|
||||
|
||||
## Explicit non-goals
|
||||
|
||||
The initial implementation does not include:
|
||||
|
||||
- TypeScript reference library,
|
||||
- Go implementation,
|
||||
- Postfix policy daemon,
|
||||
- central MAS registry,
|
||||
- mail client plugin,
|
||||
- browser extension,
|
||||
- address composer,
|
||||
- payment semantics,
|
||||
- electronic signatures,
|
||||
- generalized service marketplace,
|
||||
- production identity federation,
|
||||
- automatic DNS-parent inheritance.
|
||||
|
||||
These should be considered only after the reference experiment succeeds.
|
||||
|
||||
---
|
||||
|
||||
## Success criteria
|
||||
|
||||
The first experiment succeeds if:
|
||||
|
||||
1. the parser conforms to the shared fixture corpus;
|
||||
2. parser behavior requires no network access;
|
||||
3. discovery produces validated statute or typed failure;
|
||||
4. policy evaluation is deterministic and offline;
|
||||
5. `mas explain` never invents semantics on discovery failure;
|
||||
6. the ticket lifecycle works end to end;
|
||||
7. ticket identifiers no longer need to be encoded in subject lines;
|
||||
8. closed resources remain addressable without becoming mutable;
|
||||
9. facet loss degrades safely;
|
||||
10. the experiment exposes no need to change SMTP itself.
|
||||
|
||||
---
|
||||
|
||||
## Longer-term direction
|
||||
|
||||
If the experiment validates the concept, MAS may later grow into:
|
||||
|
||||
- additional language libraries,
|
||||
- infrastructure gateways,
|
||||
- mail client affordances,
|
||||
- signed semantic metadata,
|
||||
- richer interaction discovery,
|
||||
- economic profiles,
|
||||
- service-market coordination.
|
||||
|
||||
Those extensions should remain subordinate to the core principle:
|
||||
|
||||
> **MAS should add useful semantics to email without making ordinary email depend on MAS.**
|
||||
32
SCOPE.md
Normal file
32
SCOPE.md
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# SCOPE
|
||||
|
||||
> This file was generated by `statehub register`. Refine it as the repository
|
||||
> boundaries become clearer.
|
||||
|
||||
## One-liner
|
||||
|
||||
Mail Address Semantics (MAS) v0.1 reference implementation: parser, statute discovery, pure policy, CLI, mail gateway and reference ticket service.
|
||||
|
||||
## Core Idea
|
||||
|
||||
mail-address-semantics exists to provide the capability described in INTENT.md.
|
||||
|
||||
## In Scope
|
||||
|
||||
- Maintain the repository's primary implementation.
|
||||
- Keep docs, tests, and operational metadata current.
|
||||
|
||||
## Out of Scope
|
||||
|
||||
- Own unrelated adjacent systems.
|
||||
- Make irreversible operational decisions without human approval.
|
||||
|
||||
## Current State
|
||||
|
||||
- Status: active; implementation and stability should be verified by the repo agent.
|
||||
|
||||
## Getting Oriented
|
||||
|
||||
- Start with: INTENT.md
|
||||
- Agent instructions: AGENTS.md
|
||||
- Workplans: workplans/
|
||||
83
mas-v0.1-spec/CONFORMANCE.md
Normal file
83
mas-v0.1-spec/CONFORMANCE.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# MAS v0.1 Conformance
|
||||
|
||||
## 1. Test surfaces
|
||||
|
||||
An implementation can claim any of the following independently:
|
||||
|
||||
- `MAS-P1-PARSER`
|
||||
- `MAS-P2-PARSER`
|
||||
- `MAS-V0.1-GENERATOR`
|
||||
- `MAS-V0.1-STATUTE`
|
||||
|
||||
## 2. Parser requirements
|
||||
|
||||
A parser claiming `MAS-P1-PARSER` MUST produce the expected result for every
|
||||
fixture whose `profile` is `p1`.
|
||||
|
||||
A parser claiming `MAS-P2-PARSER` MUST produce the expected result for every
|
||||
fixture whose `profile` is `p2`, and MUST also satisfy `MAS-P1-PARSER`.
|
||||
|
||||
The `parsed` object in the fixture file is normative for the test corpus.
|
||||
|
||||
A parser MUST NOT infer `person`, `actor`, or `function` from `.`, `-`, `_`.
|
||||
|
||||
## 3. Generator requirements
|
||||
|
||||
A generator:
|
||||
|
||||
- MUST emit only grammar-valid canonical ASCII local-parts;
|
||||
- MUST NOT emit empty target, resource type, resource id, facet, or interaction;
|
||||
- MUST emit `+` before `!`;
|
||||
- MUST NOT emit more than one `+` or one `!`;
|
||||
- MUST NOT emit `$` or `^`;
|
||||
- MUST NOT emit both `+` and `!` in v0.1;
|
||||
- MUST keep the encoded SMTP local-part at or below 64 octets.
|
||||
|
||||
Fixtures with `generator_allowed: false` may still be accepted by a P2 parser,
|
||||
but a conforming generator MUST NOT create them.
|
||||
|
||||
## 4. Statute requirements
|
||||
|
||||
A statute implementation MUST validate discovery documents using
|
||||
`schema/mas-statute-v0.1.schema.json`.
|
||||
|
||||
It MUST NOT apply:
|
||||
|
||||
- case folding,
|
||||
- dot folding,
|
||||
- address classification,
|
||||
- resource-type semantics,
|
||||
- interaction-mode semantics,
|
||||
|
||||
unless the exact-host statute successfully validates and declares them.
|
||||
|
||||
## 5. Discovery failure
|
||||
|
||||
Failure to retrieve or validate a statute:
|
||||
|
||||
- does not invalidate the email address;
|
||||
- does invalidate any claim that MAS namespace semantics were discovered;
|
||||
- falls back to ordinary email handling plus exact local configuration.
|
||||
|
||||
## 6. Inheritance
|
||||
|
||||
Implementations are not required to implement `inherits`.
|
||||
|
||||
Implementations that do implement it MUST:
|
||||
|
||||
- fetch the explicit URI only;
|
||||
- detect cycles;
|
||||
- treat inherited data as defaults;
|
||||
- never inherit explicit `addresses` registry entries;
|
||||
- let the child override inherited resource types and interaction modes.
|
||||
|
||||
## 7. Test command
|
||||
|
||||
The bundled reference parser can be run with:
|
||||
|
||||
```bash
|
||||
python reference/mas_parser.py --self-test
|
||||
```
|
||||
|
||||
The reference parser is non-normative; the grammar, specification, schema,
|
||||
and fixtures are normative for this draft.
|
||||
31
mas-v0.1-spec/README.md
Normal file
31
mas-v0.1-spec/README.md
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# Mail Address Semantics (MAS) v0.1 — Specification Bundle
|
||||
|
||||
This bundle turns the MAS v0.1 concept into an implementable draft.
|
||||
|
||||
## Contents
|
||||
|
||||
- `SPECIFICATION.md` — normative protocol/convention text
|
||||
- `grammar/mas-v0.1.abnf` — canonical ASCII local-part grammar
|
||||
- `schema/mas-statute-v0.1.schema.json` — statute discovery document schema
|
||||
- `examples/mas-statute.example.json` — example statute
|
||||
- `CONFORMANCE.md` — parser/generator/statute conformance rules
|
||||
- `conformance/addresses.json` — positive and negative address fixtures
|
||||
- `reference/mas_parser.py` — small non-normative reference parser
|
||||
|
||||
## Profiles
|
||||
|
||||
- **P1 / Naming + Resource:** naming hints, `=` resource binding, `+` facet
|
||||
- **P2 / Interaction:** optional `!` interaction preference
|
||||
- **P3 / Economic:** reserved only; `$` and `^` have no v0.1 semantics
|
||||
|
||||
The normative machine grammar is deliberately ASCII-only. SMTPUTF8/Unicode
|
||||
addresses may exist, but Unicode MAS semantics are not part of the canonical
|
||||
v0.1 grammar.
|
||||
|
||||
## Discovery status
|
||||
|
||||
`/.well-known/mas` is specified here as the intended discovery location.
|
||||
RFC 8615 requires new well-known URI suffixes to be registered for general
|
||||
Internet use. Until such registration exists, this path should be considered
|
||||
a draft/controlled-deployment convention rather than an IANA-registered
|
||||
Internet-wide well-known URI.
|
||||
428
mas-v0.1-spec/SPECIFICATION.md
Normal file
428
mas-v0.1-spec/SPECIFICATION.md
Normal file
|
|
@ -0,0 +1,428 @@
|
|||
# Mail Address Semantics (MAS) v0.1
|
||||
|
||||
## 1. Status and normative language
|
||||
|
||||
This document defines MAS v0.1.
|
||||
|
||||
The key words **MUST**, **MUST NOT**, **REQUIRED**, **SHOULD**, **SHOULD NOT**,
|
||||
**RECOMMENDED**, **MAY**, and **OPTIONAL** are to be interpreted as described
|
||||
in BCP 14 when, and only when, they appear in uppercase.
|
||||
|
||||
MAS is an overlay on ordinary email addressing. It does not replace SMTP,
|
||||
establish identity, grant authority, define delivery topology, or create
|
||||
contractual commitments.
|
||||
|
||||
## 2. Scope
|
||||
|
||||
MAS v0.1 standardizes:
|
||||
|
||||
1. non-structural naming hints for people, actors, and functions;
|
||||
2. structured resource targets using `=`;
|
||||
3. facets using `+`;
|
||||
4. an optional interaction profile using `!`;
|
||||
5. exact-host statute discovery;
|
||||
6. conservative equivalence behavior;
|
||||
7. parser and generator conformance.
|
||||
|
||||
`$` and `^` are reserved. They have no MAS v0.1 meaning.
|
||||
|
||||
## 3. Profiles
|
||||
|
||||
### 3.1 Profile P1 — Naming + Resource
|
||||
|
||||
P1 is the base v0.1 profile.
|
||||
|
||||
- `.` MAY be used as a person-style naming separator.
|
||||
- `-` MAY be used as an actor-style naming separator.
|
||||
- `_` MAY be used as a function-style naming separator.
|
||||
- `=` structurally binds a resource type to a resource identifier.
|
||||
- `+` structurally adds one facet.
|
||||
|
||||
`.`, `-`, and `_` never classify an address by themselves.
|
||||
|
||||
### 3.2 Profile P2 — Interaction
|
||||
|
||||
P2 extends P1 with `!`.
|
||||
|
||||
`TARGET!MODE` requests that the addressed endpoint continue the interaction
|
||||
using `MODE` as the preferred interaction mode.
|
||||
|
||||
The address does not state which party initiates a non-email leg and does not
|
||||
constitute acceptance. Acceptance is external to the address.
|
||||
|
||||
### 3.3 Profile P3 — Economic
|
||||
|
||||
P3 is not defined in v0.1.
|
||||
|
||||
`$` and `^` are reserved so that later profiles may define consideration and
|
||||
conditions without creating v0.1 parsing ambiguity.
|
||||
|
||||
## 4. Canonical ASCII local-part
|
||||
|
||||
The normative v0.1 machine grammar is the ABNF in
|
||||
`grammar/mas-v0.1.abnf`.
|
||||
|
||||
Canonical MAS v0.1 local-parts are unquoted ASCII dot-atom-like strings.
|
||||
Quoted local-parts and SMTPUTF8 local-parts are outside the canonical v0.1
|
||||
machine grammar.
|
||||
|
||||
A MAS-aware implementation MAY transport such non-canonical email addresses
|
||||
as ordinary email, but MUST NOT claim that they are canonical MAS v0.1
|
||||
addresses.
|
||||
|
||||
### 4.1 Target
|
||||
|
||||
A target is non-empty.
|
||||
|
||||
A target without `=` is opaque. Its semantic kind MUST NOT be inferred from
|
||||
`.`, `-`, or `_`.
|
||||
|
||||
If a target contains `=`, the target is a resource target:
|
||||
|
||||
- text before the first `=` is `resource-type`;
|
||||
- text after the first `=` is `resource-id`;
|
||||
- neither may be empty;
|
||||
- subsequent `=` characters are resource-id data.
|
||||
|
||||
Examples:
|
||||
|
||||
ticket=PR42
|
||||
invoice=RE=2026-4711
|
||||
|
||||
The second example has resource type `invoice` and resource id
|
||||
`RE=2026-4711`.
|
||||
|
||||
### 4.2 Facet
|
||||
|
||||
A facet is introduced by `+`.
|
||||
|
||||
`+` MAY occur at most once and its value MUST be non-empty.
|
||||
|
||||
ticket=PR42+comments
|
||||
|
||||
The base target remains meaningful if a mail system strips the facet. MAS
|
||||
therefore treats facet loss as graceful degradation:
|
||||
|
||||
ticket=PR42+comments -> ticket=PR42
|
||||
|
||||
This is a semantic design property, not a preservation guarantee.
|
||||
|
||||
### 4.3 Interaction
|
||||
|
||||
An interaction preference is introduced by `!`.
|
||||
|
||||
`!` MAY occur at most once and its value MUST be non-empty.
|
||||
|
||||
P2 examples:
|
||||
|
||||
ticket!callback
|
||||
ticket=PR42!web
|
||||
|
||||
Canonical operator order is:
|
||||
|
||||
TARGET [+FACET] [!INTERACTION]
|
||||
|
||||
Non-canonical order is invalid MAS syntax. A conforming parser MUST reject it
|
||||
and MUST NOT silently reorder it.
|
||||
|
||||
### 4.4 Composition rule
|
||||
|
||||
A parser implementing P2 MUST be able to parse the canonical combined form:
|
||||
|
||||
TARGET+FACET!INTERACTION
|
||||
|
||||
A v0.1 generator MUST NOT emit both `+` and `!` in the same generated
|
||||
local-part. Implementations requiring both semantics SHOULD move one of them
|
||||
to external metadata.
|
||||
|
||||
This deliberately makes receivers slightly more tolerant than generators
|
||||
while retaining one canonical parse.
|
||||
|
||||
### 4.5 Reserved characters
|
||||
|
||||
Within canonical MAS v0.1 structured values:
|
||||
|
||||
+ ! $ ^
|
||||
|
||||
are reserved.
|
||||
|
||||
MAS v0.1 defines no escaping mechanism.
|
||||
|
||||
A percent-looking sequence such as `%2B` is ordinary data in v0.1 and MUST NOT
|
||||
be percent-decoded by the MAS parser.
|
||||
|
||||
## 5. Naming semantics
|
||||
|
||||
The following are style conventions only:
|
||||
|
||||
bernd.worsch@example.org person-like
|
||||
research-companion@example.org actor-like
|
||||
invoice_ingest@example.org function-like
|
||||
|
||||
They are not classifier rules.
|
||||
|
||||
Examples such as these remain opaque without statute metadata:
|
||||
|
||||
anne-marie.schmidt@example.org
|
||||
support@example.org
|
||||
happy-helper.v2@example.org
|
||||
|
||||
A domain registry or statute metadata determines semantic kind.
|
||||
|
||||
## 6. Resource semantics
|
||||
|
||||
A resource address identifies a communication endpoint associated with a
|
||||
resource.
|
||||
|
||||
ticket=PR42@example.org
|
||||
|
||||
It does not grant access to, mutation of, or knowledge about the resource.
|
||||
|
||||
Resource addressability and resource mutability are independent.
|
||||
|
||||
A closed resource MAY remain addressable. A receiver MAY archive the mail,
|
||||
reject mutation, return lifecycle information, or create a linked follow-up
|
||||
resource.
|
||||
|
||||
### 6.1 Resource types
|
||||
|
||||
A statute SHOULD enumerate resource types it recognizes.
|
||||
|
||||
Unknown resource types MUST NOT acquire application semantics solely because
|
||||
they use `=` syntax.
|
||||
|
||||
### 6.2 Resource identifiers
|
||||
|
||||
Resource identifiers are correlation identifiers, not credentials.
|
||||
|
||||
Predictable identifiers are enumerable. Deployments SHOULD prefer
|
||||
non-guessable external identifiers when addresses are widely forwarded or
|
||||
exposed and enumeration is undesirable.
|
||||
|
||||
High entropy does not transform an identifier into an authorization token.
|
||||
|
||||
## 7. Interaction semantics
|
||||
|
||||
The recommended P2 initiation pattern is:
|
||||
|
||||
ticket!callback@great-service.example
|
||||
|
||||
followed by creation or disclosure of a resource:
|
||||
|
||||
ticket=GS-41872@great-service.example
|
||||
|
||||
The first address expresses interaction intent. The second names the resource
|
||||
created or selected by the service.
|
||||
|
||||
`!callback` is a preference/request, not a command, guarantee, SLA, or proof
|
||||
of acceptance.
|
||||
|
||||
## 8. Address equivalence
|
||||
|
||||
MAS does not impose case folding, dot folding, alias folding, or Unicode
|
||||
normalization in the absence of a successfully discovered statute.
|
||||
|
||||
If no statute is available, comparison is exact over the presented canonical
|
||||
ASCII local-part.
|
||||
|
||||
A statute MAY declare:
|
||||
|
||||
- `exact` local-part comparison;
|
||||
- `ascii-insensitive` local-part comparison;
|
||||
- dot folding for explicitly named address kinds or registry entries.
|
||||
|
||||
Dot folding MUST NOT be assumed by default.
|
||||
|
||||
A-label/U-label domain processing follows the underlying IDNA-aware HTTPS and
|
||||
DNS stack; MAS does not invent a second IDN mapping algorithm.
|
||||
|
||||
## 9. Unicode
|
||||
|
||||
SMTPUTF8 allows non-ASCII local-parts, but MAS v0.1 does not define a
|
||||
canonical Unicode local-part grammar.
|
||||
|
||||
A statute may declare `unicode_local_part: true` for experimental/private
|
||||
extensions. Such addresses MUST NOT be advertised as canonical MAS v0.1
|
||||
machine syntax.
|
||||
|
||||
A future MAS version may define NFC normalization and Unicode comparison
|
||||
rules. v0.1 deliberately avoids doing so.
|
||||
|
||||
## 10. Statute discovery
|
||||
|
||||
The intended discovery URI is:
|
||||
|
||||
https://HOST/.well-known/mas
|
||||
|
||||
where `HOST` is the exact domain appearing after `@`.
|
||||
|
||||
For:
|
||||
|
||||
anna.mueller@acme.coulomb.social
|
||||
|
||||
the lookup host is:
|
||||
|
||||
acme.coulomb.social
|
||||
|
||||
A client MUST NOT walk to parent domains implicitly.
|
||||
|
||||
### 10.1 Registration caveat
|
||||
|
||||
RFC 8615 requires applications minting a new well-known URI suffix to
|
||||
register it for general Internet use.
|
||||
|
||||
Therefore `/.well-known/mas` is a candidate MAS discovery location in this
|
||||
draft. General public deployment SHOULD wait for the required registration.
|
||||
Controlled deployments MAY use it by agreement.
|
||||
|
||||
### 10.2 Retrieval
|
||||
|
||||
A statute-aware client:
|
||||
|
||||
1. constructs the HTTPS URI for the exact host;
|
||||
2. performs an HTTPS GET;
|
||||
3. requires a successful 2xx response;
|
||||
4. requires a document conforming to the v0.1 statute schema;
|
||||
5. verifies that `namespace` equals the queried host after normal domain-name
|
||||
canonicalization;
|
||||
6. otherwise treats discovery as failed.
|
||||
|
||||
Discovery failure does not make the email address invalid. It means MAS
|
||||
semantics are unconfirmed.
|
||||
|
||||
### 10.3 Redirects
|
||||
|
||||
Clients SHOULD NOT follow cross-origin redirects for statute discovery.
|
||||
|
||||
Same-origin redirects MAY be followed.
|
||||
|
||||
A cross-origin policy transfer requires an explicit future delegation
|
||||
mechanism and is not defined in v0.1.
|
||||
|
||||
### 10.4 Inheritance
|
||||
|
||||
No parent-domain inheritance is implicit.
|
||||
|
||||
A child statute MAY contain an `inherits` URI. v0.1 inheritance is
|
||||
**defaults-only**:
|
||||
|
||||
- the parent document is fetched and validated independently;
|
||||
- the child remains authoritative for its exact namespace;
|
||||
- child values override inherited defaults;
|
||||
- address registry entries are never inherited;
|
||||
- resource types and interaction modes MAY be inherited;
|
||||
- an inheritance cycle is invalid.
|
||||
|
||||
Implementations MAY choose not to implement inheritance. If they do not,
|
||||
they MUST ignore `inherits` rather than walking DNS parents.
|
||||
|
||||
## 11. Statute document
|
||||
|
||||
The normative schema is:
|
||||
|
||||
schema/mas-statute-v0.1.schema.json
|
||||
|
||||
The document describes:
|
||||
|
||||
- namespace and version;
|
||||
- enabled profiles;
|
||||
- accepted resource types;
|
||||
- accepted interaction modes;
|
||||
- equivalence policy;
|
||||
- optional explicit address classification;
|
||||
- optional inheritance.
|
||||
|
||||
The statute is metadata, not an authorization database.
|
||||
|
||||
## 12. Deliverability and rewriting
|
||||
|
||||
RFC-validity is weaker than ecosystem compatibility.
|
||||
|
||||
Mailing lists, SRS, VERP, gateways, web forms, validators, and security
|
||||
products may reject or rewrite otherwise legal local-parts.
|
||||
|
||||
Deployments MUST NOT assume preservation of MAS syntax through arbitrary
|
||||
rewriting.
|
||||
|
||||
`+` is intentionally degradation-friendly: loss of a facet leaves the base
|
||||
target.
|
||||
|
||||
`!` is intended primarily for cooperating environments where all relevant
|
||||
systems are known to preserve it.
|
||||
|
||||
## 13. Length
|
||||
|
||||
An SMTP local-part is limited to 64 octets.
|
||||
|
||||
Generators MUST enforce the underlying transport limit after encoding.
|
||||
|
||||
Because the budget is small, v0.1 permits at most one facet and one
|
||||
interaction, and generators MUST NOT emit both simultaneously.
|
||||
|
||||
## 14. Security
|
||||
|
||||
1. MAS identifiers are not credentials.
|
||||
2. Resource knowledge grants no authority.
|
||||
3. Interaction selectors are not commands.
|
||||
4. Dynamic addresses MUST define handling for bounces, unknown resources,
|
||||
expired resources, verification traffic, and abuse.
|
||||
5. Receivers MUST avoid backscatter behavior.
|
||||
6. Syntax-derived classifications MUST NOT substitute for statute metadata.
|
||||
7. `$` and `^` MUST NOT be rendered as payment/contract semantics in v0.1.
|
||||
8. Authentication remains external to MAS.
|
||||
9. A statute endpoint is security-relevant configuration and SHOULD be
|
||||
protected with the same care as other origin-wide metadata.
|
||||
|
||||
## 15. Conformance classes
|
||||
|
||||
### 15.1 P1 parser
|
||||
|
||||
A P1 parser:
|
||||
|
||||
- validates canonical ASCII syntax;
|
||||
- parses resource binding and facet;
|
||||
- rejects `!`, `$`, and `^` as MAS operators;
|
||||
- does not classify opaque target kind by punctuation.
|
||||
|
||||
### 15.2 P2 parser
|
||||
|
||||
A P2 parser additionally parses `!`.
|
||||
|
||||
### 15.3 Generator
|
||||
|
||||
A generator:
|
||||
|
||||
- emits only canonical operator order;
|
||||
- emits no empty component;
|
||||
- emits no reserved operator within a value;
|
||||
- emits at most one `+`;
|
||||
- emits at most one `!`;
|
||||
- MUST NOT emit both `+` and `!` in v0.1;
|
||||
- enforces the SMTP local-part octet limit.
|
||||
|
||||
### 15.4 Statute-aware implementation
|
||||
|
||||
A statute-aware implementation additionally validates the discovery document
|
||||
against the normative schema and applies only explicitly declared equivalence
|
||||
and classification rules.
|
||||
|
||||
## 16. Reserved future work
|
||||
|
||||
Future specifications may define:
|
||||
|
||||
- Unicode canonical grammar;
|
||||
- escaping;
|
||||
- signed statutes and signed semantic assertions;
|
||||
- interaction negotiation;
|
||||
- service discovery;
|
||||
- economic request/offer/acceptance/settlement;
|
||||
- condition tokens;
|
||||
- IANA registrations.
|
||||
|
||||
## 17. One-sentence definition
|
||||
|
||||
**Mail Address Semantics is a voluntary convention for naming people, actors
|
||||
and functions, and for addressing resources, facets, and optional interaction
|
||||
preferences through ordinary email addresses while keeping classification,
|
||||
authority, authentication, equivalence, and delivery implementation explicit
|
||||
and external to the address syntax.**
|
||||
315
mas-v0.1-spec/conformance/addresses.json
Normal file
315
mas-v0.1-spec/conformance/addresses.json
Normal file
|
|
@ -0,0 +1,315 @@
|
|||
[
|
||||
{
|
||||
"id": "p1-001",
|
||||
"local_part": "bernd.worsch",
|
||||
"profile": "p1",
|
||||
"valid": true,
|
||||
"parsed": {
|
||||
"target": "bernd.worsch",
|
||||
"target_kind": "opaque",
|
||||
"facet": null,
|
||||
"interaction": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "p1-002",
|
||||
"local_part": "anne-marie.schmidt",
|
||||
"profile": "p1",
|
||||
"valid": true,
|
||||
"parsed": {
|
||||
"target": "anne-marie.schmidt",
|
||||
"target_kind": "opaque",
|
||||
"facet": null,
|
||||
"interaction": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "p1-003",
|
||||
"local_part": "support",
|
||||
"profile": "p1",
|
||||
"valid": true,
|
||||
"parsed": {
|
||||
"target": "support",
|
||||
"target_kind": "opaque",
|
||||
"facet": null,
|
||||
"interaction": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "p1-004",
|
||||
"local_part": "invoice-reviewer_backup",
|
||||
"profile": "p1",
|
||||
"valid": true,
|
||||
"parsed": {
|
||||
"target": "invoice-reviewer_backup",
|
||||
"target_kind": "opaque",
|
||||
"facet": null,
|
||||
"interaction": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "p1-005",
|
||||
"local_part": "happy-helper.v2",
|
||||
"profile": "p1",
|
||||
"valid": true,
|
||||
"parsed": {
|
||||
"target": "happy-helper.v2",
|
||||
"target_kind": "opaque",
|
||||
"facet": null,
|
||||
"interaction": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "p1-010",
|
||||
"local_part": "ticket=PR42",
|
||||
"profile": "p1",
|
||||
"valid": true,
|
||||
"parsed": {
|
||||
"target": "ticket=PR42",
|
||||
"target_kind": "resource",
|
||||
"resource_type": "ticket",
|
||||
"resource_id": "PR42",
|
||||
"facet": null,
|
||||
"interaction": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "p1-011",
|
||||
"local_part": "invoice=RE=2026-4711",
|
||||
"profile": "p1",
|
||||
"valid": true,
|
||||
"parsed": {
|
||||
"target": "invoice=RE=2026-4711",
|
||||
"target_kind": "resource",
|
||||
"resource_type": "invoice",
|
||||
"resource_id": "RE=2026-4711",
|
||||
"facet": null,
|
||||
"interaction": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "p1-012",
|
||||
"local_part": "ticket=PR42+comments",
|
||||
"profile": "p1",
|
||||
"valid": true,
|
||||
"parsed": {
|
||||
"target": "ticket=PR42",
|
||||
"target_kind": "resource",
|
||||
"resource_type": "ticket",
|
||||
"resource_id": "PR42",
|
||||
"facet": "comments",
|
||||
"interaction": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "p1-013",
|
||||
"local_part": "bernd.worsch+github",
|
||||
"profile": "p1",
|
||||
"valid": true,
|
||||
"parsed": {
|
||||
"target": "bernd.worsch",
|
||||
"target_kind": "opaque",
|
||||
"facet": "github",
|
||||
"interaction": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "p1-020",
|
||||
"local_part": "=PR42",
|
||||
"profile": "p1",
|
||||
"valid": false,
|
||||
"error": "empty resource type"
|
||||
},
|
||||
{
|
||||
"id": "p1-021",
|
||||
"local_part": "ticket=",
|
||||
"profile": "p1",
|
||||
"valid": false,
|
||||
"error": "empty resource id"
|
||||
},
|
||||
{
|
||||
"id": "p1-022",
|
||||
"local_part": "ticket=PR42+",
|
||||
"profile": "p1",
|
||||
"valid": false,
|
||||
"error": "empty facet"
|
||||
},
|
||||
{
|
||||
"id": "p1-023",
|
||||
"local_part": "ticket=PR42+comments+attachments",
|
||||
"profile": "p1",
|
||||
"valid": false,
|
||||
"error": "multiple facet operators"
|
||||
},
|
||||
{
|
||||
"id": "p1-024",
|
||||
"local_part": "ticket=PR42$4.99",
|
||||
"profile": "p1",
|
||||
"valid": false,
|
||||
"error": "reserved operator"
|
||||
},
|
||||
{
|
||||
"id": "p1-025",
|
||||
"local_part": "ticket=PR42^1h",
|
||||
"profile": "p1",
|
||||
"valid": false,
|
||||
"error": "reserved operator"
|
||||
},
|
||||
{
|
||||
"id": "p1-026",
|
||||
"local_part": ".bernd",
|
||||
"profile": "p1",
|
||||
"valid": false,
|
||||
"error": "leading dot"
|
||||
},
|
||||
{
|
||||
"id": "p1-027",
|
||||
"local_part": "bernd.",
|
||||
"profile": "p1",
|
||||
"valid": false,
|
||||
"error": "trailing dot"
|
||||
},
|
||||
{
|
||||
"id": "p1-028",
|
||||
"local_part": "bernd..worsch",
|
||||
"profile": "p1",
|
||||
"valid": false,
|
||||
"error": "repeated dot"
|
||||
},
|
||||
{
|
||||
"id": "p1-029",
|
||||
"local_part": "ticket=PR.42",
|
||||
"profile": "p1",
|
||||
"valid": true,
|
||||
"parsed": {
|
||||
"target": "ticket=PR.42",
|
||||
"target_kind": "resource",
|
||||
"resource_type": "ticket",
|
||||
"resource_id": "PR.42",
|
||||
"facet": null,
|
||||
"interaction": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "p1-030",
|
||||
"local_part": "ticket=PR%2B42",
|
||||
"profile": "p1",
|
||||
"valid": true,
|
||||
"parsed": {
|
||||
"target": "ticket=PR%2B42",
|
||||
"target_kind": "resource",
|
||||
"resource_type": "ticket",
|
||||
"resource_id": "PR%2B42",
|
||||
"facet": null,
|
||||
"interaction": null
|
||||
},
|
||||
"note": "%2B is literal data; MAS v0.1 does not percent-decode"
|
||||
},
|
||||
{
|
||||
"id": "p2-001",
|
||||
"local_part": "ticket!callback",
|
||||
"profile": "p2",
|
||||
"valid": true,
|
||||
"parsed": {
|
||||
"target": "ticket",
|
||||
"target_kind": "opaque",
|
||||
"facet": null,
|
||||
"interaction": "callback"
|
||||
},
|
||||
"generator_allowed": true
|
||||
},
|
||||
{
|
||||
"id": "p2-002",
|
||||
"local_part": "ticket=PR42!web",
|
||||
"profile": "p2",
|
||||
"valid": true,
|
||||
"parsed": {
|
||||
"target": "ticket=PR42",
|
||||
"target_kind": "resource",
|
||||
"resource_type": "ticket",
|
||||
"resource_id": "PR42",
|
||||
"facet": null,
|
||||
"interaction": "web"
|
||||
},
|
||||
"generator_allowed": true
|
||||
},
|
||||
{
|
||||
"id": "p2-003",
|
||||
"local_part": "ticket=PR42+comments!callback",
|
||||
"profile": "p2",
|
||||
"valid": true,
|
||||
"parsed": {
|
||||
"target": "ticket=PR42",
|
||||
"target_kind": "resource",
|
||||
"resource_type": "ticket",
|
||||
"resource_id": "PR42",
|
||||
"facet": "comments",
|
||||
"interaction": "callback"
|
||||
},
|
||||
"generator_allowed": false
|
||||
},
|
||||
{
|
||||
"id": "p2-010",
|
||||
"local_part": "ticket!",
|
||||
"profile": "p2",
|
||||
"valid": false,
|
||||
"error": "empty interaction"
|
||||
},
|
||||
{
|
||||
"id": "p2-011",
|
||||
"local_part": "ticket!callback!chat",
|
||||
"profile": "p2",
|
||||
"valid": false,
|
||||
"error": "multiple interaction operators"
|
||||
},
|
||||
{
|
||||
"id": "p2-012",
|
||||
"local_part": "ticket!callback+comments",
|
||||
"profile": "p2",
|
||||
"valid": false,
|
||||
"error": "non-canonical operator order"
|
||||
},
|
||||
{
|
||||
"id": "p1-031",
|
||||
"local_part": "ticket!callback",
|
||||
"profile": "p1",
|
||||
"valid": false,
|
||||
"error": "interaction not enabled in P1"
|
||||
},
|
||||
{
|
||||
"id": "p2-013",
|
||||
"local_part": "ticket=PR42+comments!callback$4.99",
|
||||
"profile": "p2",
|
||||
"valid": false,
|
||||
"error": "reserved operator"
|
||||
},
|
||||
{
|
||||
"id": "p2-014",
|
||||
"local_part": "ticket=PR42+comments!call.back",
|
||||
"profile": "p2",
|
||||
"valid": true,
|
||||
"parsed": {
|
||||
"target": "ticket=PR42",
|
||||
"target_kind": "resource",
|
||||
"resource_type": "ticket",
|
||||
"resource_id": "PR42",
|
||||
"facet": "comments",
|
||||
"interaction": "call.back"
|
||||
},
|
||||
"generator_allowed": false
|
||||
},
|
||||
{
|
||||
"id": "p1-040",
|
||||
"local_part": "\"ticket=PR42\"",
|
||||
"profile": "p1",
|
||||
"valid": false,
|
||||
"error": "quoted local-part outside canonical MAS v0.1"
|
||||
},
|
||||
{
|
||||
"id": "p1-041",
|
||||
"local_part": "tïcket=PR42",
|
||||
"profile": "p1",
|
||||
"valid": false,
|
||||
"error": "non-ASCII outside canonical MAS v0.1"
|
||||
}
|
||||
]
|
||||
54
mas-v0.1-spec/examples/mas-statute.example.json
Normal file
54
mas-v0.1-spec/examples/mas-statute.example.json
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"mas_version": "0.1",
|
||||
"namespace": "coulomb.social",
|
||||
"profiles": {
|
||||
"p1": true,
|
||||
"p2": true
|
||||
},
|
||||
"resource_types": {
|
||||
"ticket": {
|
||||
"enabled": true,
|
||||
"description": "Customer-service or work ticket",
|
||||
"identifier_policy": "non-guessable-preferred"
|
||||
},
|
||||
"invoice": {
|
||||
"enabled": true,
|
||||
"description": "Invoice communication resource",
|
||||
"identifier_policy": "opaque"
|
||||
}
|
||||
},
|
||||
"interaction_modes": {
|
||||
"callback": {
|
||||
"enabled": true,
|
||||
"description": "Continue by a voice callback"
|
||||
},
|
||||
"web": {
|
||||
"enabled": true,
|
||||
"description": "Continue in a web interface"
|
||||
},
|
||||
"chat": {
|
||||
"enabled": true,
|
||||
"description": "Continue in a chat interface"
|
||||
}
|
||||
},
|
||||
"equivalence": {
|
||||
"local_part_case": "exact",
|
||||
"dot_folding": false
|
||||
},
|
||||
"unicode_local_part": false,
|
||||
"addresses": {
|
||||
"support": {
|
||||
"kind": "function",
|
||||
"description": "General support intake"
|
||||
},
|
||||
"bernd.worsch": {
|
||||
"kind": "person"
|
||||
},
|
||||
"research-companion": {
|
||||
"kind": "actor"
|
||||
},
|
||||
"invoice_ingest": {
|
||||
"kind": "function"
|
||||
}
|
||||
}
|
||||
}
|
||||
55
mas-v0.1-spec/grammar/mas-v0.1.abnf
Normal file
55
mas-v0.1-spec/grammar/mas-v0.1.abnf
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
; Mail Address Semantics (MAS) v0.1
|
||||
; Canonical ASCII local-part grammar
|
||||
;
|
||||
; This grammar describes the local-part only. Domain syntax is provided by
|
||||
; the underlying email specifications.
|
||||
;
|
||||
; P1 accepts: target [facet]
|
||||
; P2 accepts: target [facet] [interaction]
|
||||
;
|
||||
; Generators MUST NOT emit both facet and interaction in v0.1 even though
|
||||
; P2 parsers accept the combined canonical form.
|
||||
|
||||
mas-local-p1 = target [facet]
|
||||
mas-local-p2 = target [facet] [interaction]
|
||||
|
||||
target = resource-target / opaque-target
|
||||
|
||||
resource-target = resource-type "=" resource-id
|
||||
opaque-target = dot-string
|
||||
|
||||
resource-type = resource-type-char *resource-type-char
|
||||
|
||||
; Resource identifiers may contain additional "=" characters as data.
|
||||
resource-id = resource-id-segment *("." resource-id-segment)
|
||||
resource-id-segment = resource-id-char *resource-id-char
|
||||
|
||||
facet = "+" value
|
||||
interaction = "!" value
|
||||
|
||||
value = value-segment *("." value-segment)
|
||||
value-segment = value-char *value-char
|
||||
|
||||
; dot-string prevents leading, trailing, or repeated ".".
|
||||
dot-string = atom *("." atom)
|
||||
atom = atom-char *atom-char
|
||||
|
||||
; Conservative canonical ASCII subset derived from RFC 5322 atext while
|
||||
; reserving MAS operators + ! $ ^ and "=" where structural.
|
||||
;
|
||||
; ALPHA / DIGIT are imported from RFC 5234 core rules.
|
||||
|
||||
atom-char = ALPHA / DIGIT /
|
||||
"#" / "%" / "&" / "'" / "*" / "-" / "/" / "?" /
|
||||
"_" / "`" / "{" / "|" / "}" / "~"
|
||||
|
||||
resource-type-char = ALPHA / DIGIT / "-" / "_"
|
||||
|
||||
resource-id-char = atom-char / "="
|
||||
value-char = atom-char / "="
|
||||
|
||||
; Explicitly excluded from canonical values:
|
||||
; "+" "!" "$" "^" "@"
|
||||
;
|
||||
; Quoted local-parts are outside canonical MAS v0.1.
|
||||
; SMTPUTF8 local-parts are outside canonical MAS v0.1 machine grammar.
|
||||
129
mas-v0.1-spec/reference/mas_parser.py
Executable file
129
mas-v0.1-spec/reference/mas_parser.py
Executable file
|
|
@ -0,0 +1,129 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Non-normative MAS v0.1 canonical ASCII reference parser."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
ATOM = set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789#%&'*-/ ?_`{|}~".replace(" ", ""))
|
||||
RTYPE = set("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_")
|
||||
RESERVED = set("+!$^")
|
||||
|
||||
def _valid_dot_string(s, allowed):
|
||||
if not s or s.startswith(".") or s.endswith(".") or ".." in s:
|
||||
return False
|
||||
return all((c == "." or c in allowed) for c in s)
|
||||
|
||||
def parse(local_part, profile="p1"):
|
||||
if not isinstance(local_part, str) or not local_part:
|
||||
raise ValueError("empty local-part")
|
||||
try:
|
||||
local_part.encode("ascii")
|
||||
except UnicodeEncodeError:
|
||||
raise ValueError("non-ASCII outside canonical MAS v0.1")
|
||||
|
||||
if '"' in local_part or "\\" in local_part or "@" in local_part:
|
||||
raise ValueError("quoted/escaped/full address outside canonical local-part grammar")
|
||||
if "$" in local_part or "^" in local_part:
|
||||
raise ValueError("reserved operator")
|
||||
|
||||
if local_part.count("+") > 1:
|
||||
raise ValueError("multiple facet operators")
|
||||
if local_part.count("!") > 1:
|
||||
raise ValueError("multiple interaction operators")
|
||||
|
||||
plus = local_part.find("+")
|
||||
bang = local_part.find("!")
|
||||
if plus >= 0 and bang >= 0 and bang < plus:
|
||||
raise ValueError("non-canonical operator order")
|
||||
if profile == "p1" and bang >= 0:
|
||||
raise ValueError("interaction not enabled in P1")
|
||||
if profile not in ("p1", "p2"):
|
||||
raise ValueError("unknown profile")
|
||||
|
||||
interaction = None
|
||||
s = local_part
|
||||
if bang >= 0:
|
||||
interaction = s[bang+1:]
|
||||
if not interaction:
|
||||
raise ValueError("empty interaction")
|
||||
s = s[:bang]
|
||||
if not _valid_dot_string(interaction, ATOM | set("=")):
|
||||
raise ValueError("invalid interaction")
|
||||
|
||||
facet = None
|
||||
plus = s.find("+")
|
||||
if plus >= 0:
|
||||
facet = s[plus+1:]
|
||||
if not facet:
|
||||
raise ValueError("empty facet")
|
||||
s = s[:plus]
|
||||
if not _valid_dot_string(facet, ATOM | set("=")):
|
||||
raise ValueError("invalid facet")
|
||||
|
||||
target = s
|
||||
if not target:
|
||||
raise ValueError("empty target")
|
||||
|
||||
out = {
|
||||
"target": target,
|
||||
"target_kind": "opaque",
|
||||
"facet": facet,
|
||||
"interaction": interaction,
|
||||
}
|
||||
|
||||
if "=" in target:
|
||||
rtype, rid = target.split("=", 1)
|
||||
if not rtype:
|
||||
raise ValueError("empty resource type")
|
||||
if not rid:
|
||||
raise ValueError("empty resource id")
|
||||
if not all(c in RTYPE for c in rtype):
|
||||
raise ValueError("invalid resource type")
|
||||
if not _valid_dot_string(rid, ATOM | set("=")):
|
||||
raise ValueError("invalid resource id")
|
||||
out.update({
|
||||
"target_kind": "resource",
|
||||
"resource_type": rtype,
|
||||
"resource_id": rid,
|
||||
})
|
||||
else:
|
||||
if not _valid_dot_string(target, ATOM):
|
||||
raise ValueError("invalid opaque target")
|
||||
|
||||
if len(local_part.encode("ascii")) > 64:
|
||||
raise ValueError("local-part exceeds 64 octets")
|
||||
|
||||
return out
|
||||
|
||||
def self_test():
|
||||
here = Path(__file__).resolve().parents[1]
|
||||
fixtures = json.loads((here / "conformance" / "addresses.json").read_text())
|
||||
failures = []
|
||||
for f in fixtures:
|
||||
try:
|
||||
got = parse(f["local_part"], f["profile"])
|
||||
if not f["valid"]:
|
||||
failures.append((f["id"], "expected invalid", got))
|
||||
elif got != f["parsed"]:
|
||||
failures.append((f["id"], "parse mismatch", {"got":got, "expected":f["parsed"]}))
|
||||
except ValueError as e:
|
||||
if f["valid"]:
|
||||
failures.append((f["id"], "unexpected invalid", str(e)))
|
||||
if failures:
|
||||
print(json.dumps(failures, indent=2))
|
||||
raise SystemExit(1)
|
||||
print(f"PASS: {len(fixtures)} MAS v0.1 fixtures")
|
||||
|
||||
if __name__ == "__main__":
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("local_part", nargs="?")
|
||||
ap.add_argument("--profile", choices=["p1","p2"], default="p1")
|
||||
ap.add_argument("--self-test", action="store_true")
|
||||
args = ap.parse_args()
|
||||
if args.self_test:
|
||||
self_test()
|
||||
elif args.local_part:
|
||||
print(json.dumps(parse(args.local_part, args.profile), indent=2))
|
||||
else:
|
||||
ap.print_help()
|
||||
156
mas-v0.1-spec/schema/mas-statute-v0.1.schema.json
Normal file
156
mas-v0.1-spec/schema/mas-statute-v0.1.schema.json
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://example.invalid/mas/schema/mas-statute-v0.1.schema.json",
|
||||
"title": "Mail Address Semantics v0.1 Statute",
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"mas_version",
|
||||
"namespace",
|
||||
"profiles",
|
||||
"equivalence"
|
||||
],
|
||||
"properties": {
|
||||
"mas_version": {
|
||||
"const": "0.1"
|
||||
},
|
||||
"namespace": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 253,
|
||||
"description": "Exact host governed by this statute, in its canonical DNS/IDNA presentation form."
|
||||
},
|
||||
"inherits": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"pattern": "^https://",
|
||||
"description": "Optional explicit defaults-only parent statute URI. No DNS parent walking is implied."
|
||||
},
|
||||
"profiles": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"p1",
|
||||
"p2"
|
||||
],
|
||||
"properties": {
|
||||
"p1": {
|
||||
"const": true
|
||||
},
|
||||
"p2": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"resource_types": {
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"propertyNames": {
|
||||
"pattern": "^[a-z0-9][a-z0-9_-]*$"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"enabled"
|
||||
],
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"maxLength": 500
|
||||
},
|
||||
"identifier_policy": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"opaque",
|
||||
"non-guessable-preferred",
|
||||
"sequential-allowed"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"interaction_modes": {
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"propertyNames": {
|
||||
"pattern": "^[A-Za-z0-9_-]+$"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"enabled"
|
||||
],
|
||||
"properties": {
|
||||
"enabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"maxLength": 500
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"equivalence": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"local_part_case",
|
||||
"dot_folding"
|
||||
],
|
||||
"properties": {
|
||||
"local_part_case": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"exact",
|
||||
"ascii-insensitive"
|
||||
]
|
||||
},
|
||||
"dot_folding": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"unicode_local_part": {
|
||||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Experimental/private extension only; not canonical v0.1 machine syntax."
|
||||
},
|
||||
"addresses": {
|
||||
"type": "object",
|
||||
"default": {},
|
||||
"propertyNames": {
|
||||
"pattern": "^[A-Za-z0-9#%&'*\\-/=?_`{|}~.]+$"
|
||||
},
|
||||
"additionalProperties": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"kind"
|
||||
],
|
||||
"properties": {
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"person",
|
||||
"actor",
|
||||
"function",
|
||||
"group",
|
||||
"system",
|
||||
"opaque"
|
||||
]
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"maxLength": 500
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
55
workplans/MAS-WP-0001-statehub-bootstrap.md
Normal file
55
workplans/MAS-WP-0001-statehub-bootstrap.md
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
---
|
||||
id: MAS-WP-0001
|
||||
type: workplan
|
||||
title: "Bootstrap State Hub integration"
|
||||
domain: communication
|
||||
repo: mail-address-semantics
|
||||
status: active
|
||||
owner: codex
|
||||
topic_slug: coulomb-social
|
||||
created: "2026-10-08"
|
||||
updated: "2026-10-08"
|
||||
---
|
||||
|
||||
# Bootstrap State Hub integration
|
||||
|
||||
Mail Address Semantics (MAS) v0.1 reference implementation: parser, statute discovery, pure policy, CLI, mail gateway and reference ticket service.
|
||||
|
||||
## Review Generated Integration Files
|
||||
|
||||
```task
|
||||
id: MAS-WP-0001-T01
|
||||
status: todo
|
||||
priority: high
|
||||
```
|
||||
|
||||
Review `INTENT.md`, `SCOPE.md`, `AGENTS.md`, and `.custodian-brief.md`.
|
||||
Replace generated placeholders with repo-specific facts where needed.
|
||||
|
||||
## Verify Local Developer Workflow
|
||||
|
||||
```task
|
||||
id: MAS-WP-0001-T02
|
||||
status: todo
|
||||
priority: high
|
||||
```
|
||||
|
||||
Identify the repo's install, test, lint, build, and run commands. Add or refine
|
||||
those commands in the agent instructions so future coding sessions can verify
|
||||
changes confidently.
|
||||
|
||||
## Seed First Real Workplan
|
||||
|
||||
```task
|
||||
id: MAS-WP-0001-T03
|
||||
status: done
|
||||
priority: medium
|
||||
```
|
||||
|
||||
Create the first implementation workplan for the repository's most important
|
||||
next change. After workplan file updates, run the sync locally from this repo
|
||||
checkout:
|
||||
|
||||
```bash
|
||||
statehub fix-consistency
|
||||
```
|
||||
892
workplans/MAS-WP-0002-reference-experiment.md
Normal file
892
workplans/MAS-WP-0002-reference-experiment.md
Normal file
|
|
@ -0,0 +1,892 @@
|
|||
---
|
||||
id: MAS-WP-0002
|
||||
type: workplan
|
||||
title: "MAS v0.1 reference experiment"
|
||||
domain: communication
|
||||
repo: mail-address-semantics
|
||||
status: ready
|
||||
owner: claude
|
||||
topic_slug: coulomb-social
|
||||
created: "2026-10-08"
|
||||
updated: "2026-10-08"
|
||||
---
|
||||
|
||||
# MAS v0.1 reference experiment
|
||||
|
||||
### Objective
|
||||
|
||||
Build one runnable reference experiment for MAS v0.1.
|
||||
|
||||
The experiment should validate the full path:
|
||||
|
||||
```text
|
||||
address
|
||||
→ parse
|
||||
→ discovery
|
||||
→ policy
|
||||
→ mail gateway
|
||||
→ ticket resource
|
||||
→ subsequent resource-addressed mail
|
||||
```
|
||||
|
||||
The plan deliberately avoids parallel language implementations and infrastructure-specific integrations until the model has survived one complete implementation.
|
||||
|
||||
---
|
||||
|
||||
## Milestone 0 — Establish the implementation baseline
|
||||
|
||||
```task
|
||||
id: MAS-WP-0002-T01
|
||||
status: todo
|
||||
priority: high
|
||||
```
|
||||
|
||||
### Goal
|
||||
|
||||
Turn the specification bundle into executable project inputs.
|
||||
|
||||
### Work
|
||||
|
||||
Import or vendor:
|
||||
|
||||
```text
|
||||
SPECIFICATION.md
|
||||
grammar/mas-v0.1.abnf
|
||||
schema/mas-statute-v0.1.schema.json
|
||||
conformance/addresses.json
|
||||
```
|
||||
|
||||
Establish the repository structure.
|
||||
|
||||
Define Python package boundaries:
|
||||
|
||||
```text
|
||||
mas_core.model
|
||||
mas_core.parser
|
||||
mas_core.discovery
|
||||
mas_core.policy
|
||||
mas_core.statute
|
||||
mas_core.lint
|
||||
```
|
||||
|
||||
Define an explicit version:
|
||||
|
||||
```text
|
||||
MAS 0.1
|
||||
implementation 0.1.0-dev
|
||||
```
|
||||
|
||||
### Deliverable
|
||||
|
||||
A repository where:
|
||||
|
||||
```text
|
||||
pytest
|
||||
```
|
||||
|
||||
runs the existing fixture corpus.
|
||||
|
||||
---
|
||||
|
||||
## Milestone 1 — Core parser and model
|
||||
|
||||
```task
|
||||
id: MAS-WP-0002-T02
|
||||
status: todo
|
||||
priority: high
|
||||
```
|
||||
|
||||
### Goal
|
||||
|
||||
Implement the specification without network or namespace policy.
|
||||
|
||||
### Core model
|
||||
|
||||
Define immutable objects such as:
|
||||
|
||||
```text
|
||||
MasAddress
|
||||
Target
|
||||
OpaqueTarget
|
||||
ResourceTarget
|
||||
Facet
|
||||
Interaction
|
||||
RequiredProfile
|
||||
```
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
ticket=PR42+comments!callback
|
||||
```
|
||||
|
||||
should parse to approximately:
|
||||
|
||||
```text
|
||||
MasAddress
|
||||
target:
|
||||
ResourceTarget
|
||||
type = ticket
|
||||
id = PR42
|
||||
facet = comments
|
||||
interaction = callback
|
||||
required_profile = P2
|
||||
```
|
||||
|
||||
Critically:
|
||||
|
||||
```text
|
||||
ticket!callback
|
||||
```
|
||||
|
||||
must parse successfully in the maximal parser and report:
|
||||
|
||||
```text
|
||||
required_profile = P2
|
||||
```
|
||||
|
||||
The parser must not require knowledge of whether a particular namespace enables P2.
|
||||
|
||||
### Parser APIs
|
||||
|
||||
Recommended:
|
||||
|
||||
```python
|
||||
parse_local_part(value) -> ParsedLocalPart
|
||||
parse_address(value) -> ParsedAddress
|
||||
validate_profile(parsed, profile) -> ValidationResult
|
||||
```
|
||||
|
||||
Avoid coupling syntax parsing to statute policy.
|
||||
|
||||
### Property cases to add
|
||||
|
||||
Extend beyond the existing fixtures.
|
||||
|
||||
#### Length boundaries
|
||||
|
||||
Test:
|
||||
|
||||
```text
|
||||
63 octets
|
||||
64 octets
|
||||
65 octets
|
||||
```
|
||||
|
||||
The transport limit applies after encoding.
|
||||
|
||||
#### Additional `=`
|
||||
|
||||
Validate:
|
||||
|
||||
```text
|
||||
ticket==PR42
|
||||
```
|
||||
|
||||
as:
|
||||
|
||||
```text
|
||||
resource_type = ticket
|
||||
resource_id = =PR42
|
||||
```
|
||||
|
||||
because only the first `=` is structural.
|
||||
|
||||
#### Reserved operators
|
||||
|
||||
Ensure:
|
||||
|
||||
```text
|
||||
$
|
||||
^
|
||||
```
|
||||
|
||||
always fail canonical v0.1 parsing.
|
||||
|
||||
#### Registry independence
|
||||
|
||||
Ensure a registry key such as:
|
||||
|
||||
```text
|
||||
support
|
||||
```
|
||||
|
||||
is legal while:
|
||||
|
||||
```text
|
||||
support+foo
|
||||
```
|
||||
|
||||
is not interpreted as a registry key lookup for the full faceted address.
|
||||
|
||||
Registry lookup should operate on the base target.
|
||||
|
||||
### Exit criteria
|
||||
|
||||
- all existing fixtures pass;
|
||||
- new property cases pass;
|
||||
- parser performs zero I/O;
|
||||
- parser does not infer person/actor/function kind.
|
||||
|
||||
---
|
||||
|
||||
## Milestone 2 — Statute model and linting
|
||||
|
||||
```task
|
||||
id: MAS-WP-0002-T03
|
||||
status: todo
|
||||
priority: high
|
||||
```
|
||||
|
||||
### Goal
|
||||
|
||||
Make namespace semantics explicit and verifiable.
|
||||
|
||||
### Work
|
||||
|
||||
Implement statute loading and JSON Schema validation.
|
||||
|
||||
Add semantic validation beyond the schema.
|
||||
|
||||
Checks should include:
|
||||
|
||||
#### Namespace consistency
|
||||
|
||||
A statute fetched for:
|
||||
|
||||
```text
|
||||
acme.coulomb.social
|
||||
```
|
||||
|
||||
must declare:
|
||||
|
||||
```text
|
||||
namespace = acme.coulomb.social
|
||||
```
|
||||
|
||||
or fail with:
|
||||
|
||||
```text
|
||||
NamespaceMismatch
|
||||
```
|
||||
|
||||
#### P2 consistency
|
||||
|
||||
Reject or lint:
|
||||
|
||||
```text
|
||||
profiles.p2 = false
|
||||
```
|
||||
|
||||
together with enabled interaction modes.
|
||||
|
||||
#### Resource type validity
|
||||
|
||||
Resource type names must themselves satisfy MAS resource-type syntax.
|
||||
|
||||
#### Registry key validity
|
||||
|
||||
Explicit address registry keys must refer to base opaque targets only.
|
||||
|
||||
For v0.1, registry keys should not contain:
|
||||
|
||||
```text
|
||||
+
|
||||
!
|
||||
$
|
||||
^
|
||||
```
|
||||
|
||||
#### Inheritance cycles
|
||||
|
||||
Detect:
|
||||
|
||||
```text
|
||||
A → B → A
|
||||
```
|
||||
|
||||
and longer cycles.
|
||||
|
||||
#### Child precedence
|
||||
|
||||
Where inheritance is supported:
|
||||
|
||||
```text
|
||||
child overrides inherited defaults
|
||||
```
|
||||
|
||||
Explicit address registry entries must not inherit.
|
||||
|
||||
### CLI
|
||||
|
||||
Implement:
|
||||
|
||||
```text
|
||||
mas lint-statute FILE
|
||||
```
|
||||
|
||||
Output should distinguish:
|
||||
|
||||
```text
|
||||
schema error
|
||||
semantic error
|
||||
warning
|
||||
```
|
||||
|
||||
### Exit criteria
|
||||
|
||||
A malformed but schema-valid statute can still be rejected by semantic linting.
|
||||
|
||||
---
|
||||
|
||||
## Milestone 3 — Exact-host discovery
|
||||
|
||||
```task
|
||||
id: MAS-WP-0002-T04
|
||||
status: todo
|
||||
priority: high
|
||||
```
|
||||
|
||||
### Goal
|
||||
|
||||
Resolve namespace metadata without contaminating parsing or policy.
|
||||
|
||||
### API
|
||||
|
||||
Recommended:
|
||||
|
||||
```python
|
||||
discover(host) -> DiscoveryResult
|
||||
```
|
||||
|
||||
with successful result:
|
||||
|
||||
```text
|
||||
ValidatedStatute
|
||||
```
|
||||
|
||||
and typed failures such as:
|
||||
|
||||
```text
|
||||
NotFound
|
||||
TransportFailure
|
||||
InvalidContentType
|
||||
SchemaInvalid
|
||||
NamespaceMismatch
|
||||
CrossHostRedirect
|
||||
InheritanceCycle
|
||||
UnsupportedVersion
|
||||
```
|
||||
|
||||
### Redirect rule
|
||||
|
||||
Before adding caching, define redirect semantics precisely.
|
||||
|
||||
Initial rule:
|
||||
|
||||
> **Follow same-host HTTPS redirects only. Reject cross-host redirects.**
|
||||
|
||||
A redirect must not transfer MAS authority to another hostname.
|
||||
|
||||
Scheme downgrade to HTTP must be rejected.
|
||||
|
||||
### Parent domains
|
||||
|
||||
Never perform implicit lookup such as:
|
||||
|
||||
```text
|
||||
acme.coulomb.social
|
||||
→ coulomb.social
|
||||
```
|
||||
|
||||
Inheritance occurs only through an explicit statute declaration.
|
||||
|
||||
### Caching
|
||||
|
||||
Add only minimal caching after behavior is correct.
|
||||
|
||||
Cache successful statutes separately from failures.
|
||||
|
||||
Do not let cache behavior hide typed discovery failures.
|
||||
|
||||
### Exit criteria
|
||||
|
||||
Discovery is deterministic enough to test against a local HTTP fixture server.
|
||||
|
||||
---
|
||||
|
||||
## Milestone 4 — Policy resolver
|
||||
|
||||
```task
|
||||
id: MAS-WP-0002-T05
|
||||
status: todo
|
||||
priority: high
|
||||
```
|
||||
|
||||
### Goal
|
||||
|
||||
Combine syntax and validated metadata without network access.
|
||||
|
||||
### API
|
||||
|
||||
Conceptually:
|
||||
|
||||
```python
|
||||
resolve(
|
||||
parsed_address,
|
||||
statute,
|
||||
local_registry=None,
|
||||
) -> AddressSemantics
|
||||
```
|
||||
|
||||
Policy must remain pure.
|
||||
|
||||
### Resolution examples
|
||||
|
||||
With no statute:
|
||||
|
||||
```text
|
||||
bernd.worsch@example.org
|
||||
```
|
||||
|
||||
resolves only to syntactic information:
|
||||
|
||||
```text
|
||||
target = bernd.worsch
|
||||
kind = opaque
|
||||
```
|
||||
|
||||
It must not become:
|
||||
|
||||
```text
|
||||
kind = person
|
||||
```
|
||||
|
||||
from punctuation.
|
||||
|
||||
With statute registry:
|
||||
|
||||
```text
|
||||
addresses:
|
||||
bernd.worsch:
|
||||
kind: person
|
||||
```
|
||||
|
||||
the resolver may produce:
|
||||
|
||||
```text
|
||||
kind = person
|
||||
```
|
||||
|
||||
For:
|
||||
|
||||
```text
|
||||
ticket=PR42
|
||||
```
|
||||
|
||||
and a statute declaring `ticket`:
|
||||
|
||||
```text
|
||||
kind = resource
|
||||
resource_type = ticket
|
||||
resource_id = PR42
|
||||
```
|
||||
|
||||
### Profile validation
|
||||
|
||||
For:
|
||||
|
||||
```text
|
||||
ticket!callback
|
||||
```
|
||||
|
||||
if statute says:
|
||||
|
||||
```text
|
||||
p2 = false
|
||||
```
|
||||
|
||||
policy should return a typed semantic failure such as:
|
||||
|
||||
```text
|
||||
ProfileNotEnabled
|
||||
```
|
||||
|
||||
The syntax remains valid.
|
||||
|
||||
### Exit criteria
|
||||
|
||||
The complete policy test suite runs with network disabled.
|
||||
|
||||
---
|
||||
|
||||
## Milestone 5 — `mas` CLI
|
||||
|
||||
```task
|
||||
id: MAS-WP-0002-T06
|
||||
status: todo
|
||||
priority: high
|
||||
```
|
||||
|
||||
### Goal
|
||||
|
||||
Provide the main inspection surface for developers and operators.
|
||||
|
||||
### Commands
|
||||
|
||||
#### Parse
|
||||
|
||||
```text
|
||||
mas parse ADDRESS
|
||||
```
|
||||
|
||||
Shows syntax only.
|
||||
|
||||
#### Discover
|
||||
|
||||
```text
|
||||
mas discover HOST
|
||||
```
|
||||
|
||||
Shows validated statute or typed failure.
|
||||
|
||||
#### Explain
|
||||
|
||||
```text
|
||||
mas explain ADDRESS
|
||||
```
|
||||
|
||||
Shows three distinct layers:
|
||||
|
||||
```text
|
||||
Syntax
|
||||
Namespace discovery
|
||||
Semantic resolution
|
||||
```
|
||||
|
||||
Example failure:
|
||||
|
||||
```text
|
||||
Address:
|
||||
bernd.worsch@example.org
|
||||
|
||||
Syntax:
|
||||
target: bernd.worsch
|
||||
required profile: P1
|
||||
|
||||
Discovery:
|
||||
no valid MAS statute found
|
||||
|
||||
Semantics:
|
||||
kind: opaque
|
||||
```
|
||||
|
||||
This is preferable to displaying a guessed “person”.
|
||||
|
||||
#### Lint statute
|
||||
|
||||
```text
|
||||
mas lint-statute FILE
|
||||
```
|
||||
|
||||
#### Run conformance suite
|
||||
|
||||
```text
|
||||
mas test
|
||||
```
|
||||
|
||||
### Exit criteria
|
||||
|
||||
A human can understand exactly why an address received its interpretation.
|
||||
|
||||
---
|
||||
|
||||
## Milestone 6 — Static statute deployment
|
||||
|
||||
```task
|
||||
id: MAS-WP-0002-T07
|
||||
status: todo
|
||||
priority: medium
|
||||
```
|
||||
|
||||
### Goal
|
||||
|
||||
Test real exact-host discovery.
|
||||
|
||||
Serve one validated static statute at:
|
||||
|
||||
```text
|
||||
/.well-known/mas
|
||||
```
|
||||
|
||||
No framework-specific statute server should be built yet.
|
||||
|
||||
The deployment should advertise only the features needed by the reference service.
|
||||
|
||||
Example:
|
||||
|
||||
```text
|
||||
P1 = enabled
|
||||
P2 = enabled
|
||||
|
||||
resource_types:
|
||||
ticket
|
||||
|
||||
interaction_modes:
|
||||
callback
|
||||
```
|
||||
|
||||
### Exit criteria
|
||||
|
||||
`mas discover` retrieves the live/local test statute and validates namespace consistency.
|
||||
|
||||
---
|
||||
|
||||
## Milestone 7 — Generic mail-to-HTTP gateway
|
||||
|
||||
```task
|
||||
id: MAS-WP-0002-T08
|
||||
status: todo
|
||||
priority: medium
|
||||
```
|
||||
|
||||
### Goal
|
||||
|
||||
Bridge ordinary inbound mail into an application-neutral MAS event.
|
||||
|
||||
### Input
|
||||
|
||||
Either:
|
||||
|
||||
```text
|
||||
SMTP
|
||||
```
|
||||
|
||||
or:
|
||||
|
||||
```text
|
||||
provider inbound webhook
|
||||
```
|
||||
|
||||
Choose whichever produces the smallest reliable prototype.
|
||||
|
||||
### Gateway behavior
|
||||
|
||||
For each inbound recipient:
|
||||
|
||||
1. parse address;
|
||||
2. discover statute;
|
||||
3. resolve policy;
|
||||
4. produce normalized event;
|
||||
5. POST event to configured HTTP application endpoint.
|
||||
|
||||
Example event:
|
||||
|
||||
```json
|
||||
{
|
||||
"recipient": "ticket=GS-41872@example.org",
|
||||
"mas": {
|
||||
"profile": "P1",
|
||||
"kind": "resource",
|
||||
"resource_type": "ticket",
|
||||
"resource_id": "GS-41872"
|
||||
},
|
||||
"message": {
|
||||
"from": "customer@example.net",
|
||||
"subject": "The problem happened again"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
The gateway must not contain ticket-specific logic.
|
||||
|
||||
### Exit criteria
|
||||
|
||||
The same gateway could theoretically route another MAS resource type without code changes.
|
||||
|
||||
---
|
||||
|
||||
## Milestone 8 — Reference ticket service
|
||||
|
||||
```task
|
||||
id: MAS-WP-0002-T09
|
||||
status: todo
|
||||
priority: medium
|
||||
```
|
||||
|
||||
### Goal
|
||||
|
||||
Prove the canonical lifecycle.
|
||||
|
||||
### Flow A — create
|
||||
|
||||
Customer sends:
|
||||
|
||||
```text
|
||||
ticket!callback@great-service.example
|
||||
```
|
||||
|
||||
The service:
|
||||
|
||||
1. receives a normalized MAS event;
|
||||
2. verifies P2 is enabled;
|
||||
3. creates ticket;
|
||||
4. records callback as requested interaction mode;
|
||||
5. generates resource address:
|
||||
|
||||
```text
|
||||
ticket=GS-41872@great-service.example
|
||||
```
|
||||
|
||||
6. replies using that resource address.
|
||||
|
||||
The subject does not require a ticket prefix.
|
||||
|
||||
---
|
||||
|
||||
### Flow B — correlate
|
||||
|
||||
Customer later sends or replies to:
|
||||
|
||||
```text
|
||||
ticket=GS-41872@great-service.example
|
||||
```
|
||||
|
||||
The service:
|
||||
|
||||
1. resolves resource type and id;
|
||||
2. locates GS-41872;
|
||||
3. associates the new message with that resource.
|
||||
|
||||
No subject-line parsing is required.
|
||||
|
||||
---
|
||||
|
||||
### Flow C — closed resource
|
||||
|
||||
Ticket GS-41872 is closed.
|
||||
|
||||
Mail later arrives at:
|
||||
|
||||
```text
|
||||
ticket=GS-41872@great-service.example
|
||||
```
|
||||
|
||||
The address remains valid.
|
||||
|
||||
The service must apply explicit lifecycle policy.
|
||||
|
||||
For the prototype, choose one behavior such as:
|
||||
|
||||
```text
|
||||
archive message
|
||||
+
|
||||
return "ticket closed" response
|
||||
```
|
||||
|
||||
It must not silently mutate or reopen the ticket.
|
||||
|
||||
---
|
||||
|
||||
### Flow D — unknown resource
|
||||
|
||||
Mail arrives at:
|
||||
|
||||
```text
|
||||
ticket=DOES-NOT-EXIST@great-service.example
|
||||
```
|
||||
|
||||
The service must handle this deliberately and without generating abusive backscatter.
|
||||
|
||||
---
|
||||
|
||||
## Milestone 9 — Experiment review
|
||||
|
||||
```task
|
||||
id: MAS-WP-0002-T10
|
||||
status: todo
|
||||
priority: medium
|
||||
```
|
||||
|
||||
### Goal
|
||||
|
||||
Decide whether MAS v0.1 survives contact with implementation.
|
||||
|
||||
Review:
|
||||
|
||||
#### Syntax
|
||||
|
||||
Did the parser remain simple?
|
||||
|
||||
#### Discovery
|
||||
|
||||
Was exact-host metadata sufficient?
|
||||
|
||||
#### Policy
|
||||
|
||||
Could all inference remain explicit?
|
||||
|
||||
#### Email infrastructure
|
||||
|
||||
Which characters or address forms were rewritten or rejected?
|
||||
|
||||
#### Resource addressing
|
||||
|
||||
Did resource addresses actually improve correlation?
|
||||
|
||||
#### Subject lines
|
||||
|
||||
Could application identifiers genuinely disappear from subjects?
|
||||
|
||||
#### Lifecycle
|
||||
|
||||
Did closed resources remain understandable?
|
||||
|
||||
#### Interaction
|
||||
|
||||
Was `!callback` useful or did it require more negotiation semantics?
|
||||
|
||||
#### Security
|
||||
|
||||
Did any address accidentally start behaving like a capability?
|
||||
|
||||
---
|
||||
|
||||
## Decision gate
|
||||
|
||||
Only after the reference experiment should the project decide whether to build:
|
||||
|
||||
```text
|
||||
TypeScript implementation
|
||||
Go implementation
|
||||
Postfix/Exim integration
|
||||
mail-client inspector
|
||||
address composer
|
||||
signed statutes
|
||||
P3 economic profile
|
||||
```
|
||||
|
||||
Each addition should answer a demonstrated need from the experiment.
|
||||
|
||||
---
|
||||
|
||||
## Initial execution order
|
||||
|
||||
The shortest path is:
|
||||
|
||||
```text
|
||||
1. mas-core parser/model
|
||||
2. expanded conformance corpus
|
||||
3. statute validator/linter
|
||||
4. exact-host discovery
|
||||
5. pure policy resolver
|
||||
6. mas CLI
|
||||
7. static statute
|
||||
8. generic inbound gateway
|
||||
9. reference ticket service
|
||||
10. experiment review
|
||||
```
|
||||
|
||||
The governing rule for this phase is:
|
||||
|
||||
> **One implementation, one lifecycle, one real email path.**
|
||||
Loading…
Add table
Add a link
Reference in a new issue