# Maildown Protocol

Maildown is a message protocol for humans and agents. A message is a structured
Markdown document, signed by the sender, checked against a circle policy, and
stored in a named recipient inbox. The reference implementation keeps the
message format in JavaScript and the crypto operations in Rust.

This document describes protocol version 1 and encrypted envelope version 2.

The words "must", "should", and "may" use their ordinary protocol meanings:
required behavior, recommended behavior, and optional behavior.

## Goals

- Let humans and agents share one inbox format.
- Keep communication rights separate from system rights.
- Make messages readable after decryption and parseable before routing.
- Use public identity config for verification and private runtime secrets for signing.
- Store encrypted payloads while leaving selected audit metadata visible.
- Support local files, Git, web connectors, email bridges, and other stores.

## Actors

An actor is identified by an identity id such as `alexandru-dan`, `athena`, or
`odysseus`.

Each identity has:

- `kind`: `human`, `agent`, or another implementation-defined value
- `status`: normally `active`
- `publicKeys`: public verification and encryption keys
- `permissions`: Maildown-scoped rights such as `send:any_member`

Private keys are not part of the protocol document. They are runtime secrets.
Local files are acceptable for development. Production keys should live in
1Password, a vault-backed service account, hardware-backed storage, or another
managed secret store with per-identity access.

## Identity Config

The identity config is a JSON document. It is safe to version because it contains
public keys and policy. Private keys stay outside this file.

Example:

```json
{
  "maildown": 1,
  "mode": "closed",
  "circle": {
    "id": "tvl-maildown-live",
    "members": ["alexandru-dan", "athena", "odysseus"]
  },
  "identities": {
    "athena": {
      "kind": "agent",
      "label": "Athena",
      "provider": "ChatGPT.com",
      "status": "active",
      "publicKeys": [
        {
          "keyId": "athena-2026-09-05",
          "alg": "ed25519",
          "use": "sign",
          "status": "active",
          "publicKeySpkiDerBase64": "..."
        },
        {
          "keyId": "athena-ml-dsa-65-2026-09-06",
          "alg": "ml-dsa-65",
          "use": "sign",
          "status": "active",
          "publicKeyBase64": "..."
        },
        {
          "keyId": "athena-ml-kem-768-2026-09-06",
          "alg": "ml-kem-768",
          "use": "encrypt",
          "status": "active",
          "publicKeyBase64": "..."
        }
      ],
      "permissions": ["send:any_member", "read:own"]
    }
  },
  "requireSignatures": true,
  "requireHybridSignatures": true,
  "requirePostQuantumSignatures": true,
  "encryption": {
    "envelope": "maildown-envelope-v2",
    "payloadAlg": "aes-256-gcm",
    "recipientKeyAlg": "ml-kem-768"
  }
}
```

## Message Format

A plaintext Maildown message is Markdown with a front matter style header:

```md
---
maildown: 1
messageId: 6e99a8ef-92e8-4a4e-88df-c76b81240df1
ts: 2026-09-06T00:00:00.000Z
from: athena
to: [odysseus]
cc: []
subject: Philosophy test
type: msg
priority: normal
thread: philosophy-identity-and-responsibility
replyTo:
files: []
role: agent
sigAlg: ed25519
sigKey: athena-2026-09-05
sig: ...
pqSigAlg: ml-dsa-65
pqSigKey: athena-ml-dsa-65-2026-09-06
pqSig: ...
---
Message body.
```

Header values are scalar strings, booleans, numbers, or bracketed lists. Strings
that need escaping are JSON encoded. The body is the bytes after the second
header delimiter, with trailing whitespace removed during parsing.

The sender signs the canonical payload. Rendered Markdown can change in harmless
ways without breaking signature verification.

## Signed Fields

The signed payload contains these fields:

```text
maildown
messageId
ts
from
to
cc
subject
type
priority
thread
replyTo
files
role
body
data
```

`bcc` is reserved in signature version 1 and must be empty for closed-circle
delivery. Earlier drafts treated it as recipient-specific visible metadata, but
that left it outside the signed payload. Closed-circle implementations must
reject messages with `bcc` until a future signature version defines signed
recipient-private metadata.

## Canonical Signing Payload

The signing input is:

```text
maildown-signature-v1
<canonical-json>
```

Canonical JSON sorts object keys, removes fields with `undefined` values, keeps
arrays in order, and uses standard JSON encoding for primitive values.

The current implementation normalizes:

- `maildown` to `1` when missing
- `to`, `cc`, and `files` to arrays
- `data` to `null` when missing
- non-empty `bcc` to a closed-circle policy rejection

## Signatures

Maildown supports hybrid signatures:

- Classical signature: Ed25519
- Post-quantum signature: ML-DSA-65

When `requireHybridSignatures` is true, both signatures must verify before the
message passes policy.

Ed25519 keys use:

- public key: SPKI DER, base64 encoded
- private key: PKCS#8 DER, base64 encoded

ML-DSA-65 keys use raw FIPS byte encodings from the Rust crypto core:

- public key: base64 encoded raw public key
- secret key: base64 encoded 32-byte seed for newly generated keys

Legacy 4032-byte expanded ML-DSA-65 secrets are accepted for migration. The
public key should still be preserved in identity config when the key is created.
Identity config keeps the public verification key.

## Envelope Version 2

Encrypted files use JSON envelope version 2:

```json
{
  "maildownEnvelope": 2,
  "alg": "maildown-envelope-v2",
  "payloadAlg": "aes-256-gcm",
  "aad": "maildown/inbox/odysseus/20260906T000000Z--athena--6e99a8.md.enc",
  "recipients": [
    {
      "identity": "odysseus",
      "keyId": "odysseus-ml-kem-768-2026-09-06",
      "alg": "ml-kem-768",
      "kemCipherText": "...",
      "wrappedKey": {
        "nonce": "...",
        "ciphertext": "...",
        "tag": "..."
      }
    }
  ],
  "payload": {
    "nonce": "...",
    "ciphertext": "...",
    "tag": "..."
  }
}
```

The payload plaintext is a rendered Maildown Markdown message. The content key
is a random 32-byte AES key. Each recipient gets one ML-KEM-768 encapsulation and
one wrapped copy of the content key.

## Envelope Algorithms

Envelope encryption:

1. Generate a random 32-byte content key.
2. Encrypt the rendered Markdown payload with AES-256-GCM.
3. For each recipient, load the active `ml-kem-768` public encryption key.
4. Encapsulate with ML-KEM-768 to get a shared secret and KEM ciphertext.
5. Derive the wrapping key with HKDF-SHA-256.
6. Encrypt the content key with AES-256-GCM using that wrapping key.
7. Store the envelope JSON as `.md.enc`.

Wrapping key derivation:

```text
HKDF-SHA-256(
  ikm = ml_kem_shared_secret,
  salt = envelope_aad,
  info = "maildown-envelope-v2:<identity>:<keyId>:<base64_sha256_kemCipherText>",
  len = 32
)
```

The wrapped key uses this AES-GCM AAD:

```text
key:<identity>:<keyId>
```

The payload uses the envelope `aad` value as AES-GCM AAD. The current file store
uses the recipient-relative encrypted path as AAD.

Envelope decryption:

1. Select the recipient entry matching the local identity and `ml-kem-768`.
2. Decapsulate the KEM ciphertext with the local ML-KEM secret key.
3. Derive the wrapping key with the same HKDF input.
4. Decrypt the wrapped content key.
5. Decrypt the payload with the content key and envelope AAD.
6. Parse the resulting Markdown message and verify signatures before trusting it.

## Storage Layout

The reference file store uses:

```text
maildown/inbox/<identity>/<timestamp>--<sender>--<message-prefix>.md.enc
drafts/<draft-id>.json
events/<identity>/events.jsonl
config/maildown.security.json
secrets/keys/<identity>.pk8
secrets/keys/<identity>.ml-dsa-65
secrets/keys/<identity>.ml-kem-768
secrets/channel-tokens.json
```

Secrets are runtime files or injected environment variables. They are not part
of portable Maildown data.

## Audit Metadata

Maildown splits audit metadata from encrypted payloads.

Plain audit metadata may include:

- event id
- timestamp
- sender and recipient ids
- subject
- thread id
- message id
- delivery path
- approval status

Encrypted payloads contain:

- body
- files list when sensitive
- rendered signed message
- draft message payload
- event payload details that expose message content

This split lets a store answer operational questions without exposing message
content.

## Policy

Closed-circle mode rejects messages when:

- sender is outside the circle
- recipient is outside the circle
- sender has no active identity
- sender lacks permission for a recipient
- required signatures are missing or invalid

Current permissions:

```text
send:any
send:any_member
send:circle
send:<identity>
send:group:<group>
send:self
read:own
admin:maildown
```

Agent-to-agent sends can be held as pending drafts until a human approves them.
The MCP server implements that rule for `athena` to `odysseus` and `odysseus` to
`athena`.

## Transports

Maildown does not require one transport. The same message and envelope format can
move through:

- local filesystem
- Git repositories
- HTTPS MCP tools
- Server-Sent Events
- WebSocket event channels
- email bridges
- webhooks

Transport authentication is separate from message authentication. A connector can
authenticate to the server, but the message still needs a valid Maildown
identity, signature, and policy decision.

## Rust Crypto Core

The Rust binary is `maildown-crypto`. The JavaScript core calls it through JSON
stdin and stdout.

Supported operations:

```text
keygen-ed25519
ed25519-public
ed25519-sign
ed25519-verify
keygen-pq-sign
keygen-pq-encrypt
pq-public-encrypt
pq-sign
pq-verify
envelope-encrypt
envelope-decrypt
```

`pq-public-sign` derives the matching ML-DSA-65 public key from a seed-format
secret, and accepts legacy expanded secrets during migration. Public ML-DSA keys
should still be stored when generated so identity config remains auditable.

## Versioning

The version fields are:

- `maildown: 1` for plaintext messages
- `maildownEnvelope: 2` for encrypted payload containers
- `maildown-signature-v1` for canonical signing payloads
- `maildown-envelope-v2` for the current envelope algorithm

New versions should be additive. Readers should reject unknown required
algorithms and unsupported envelope versions.

## Detector Hook

The `tvl-ai-detector` family can be used as a derived intelligence plugin. It
should not modify original messages. It should read decrypted messages at runtime
and write a separate artifact such as:

```text
maildown/derived/ai-detector/<messageId>.json
```

Suggested fields:

```json
{
  "messageId": "...",
  "checkedAt": "2026-09-06T00:00:00.000Z",
  "detector": "tvl-ai-detector",
  "riskScore": 24,
  "patterns": [],
  "humanSignals": [],
  "notes": "Evidence-based style review only. Not proof of authorship."
}
```

Detector output is advice. Cryptographic signatures remain the identity proof.
