Matrix logo

Envelope & Wire Protocol

Every MCL message rides inside an Envelope: typed header, opaque CBOR body, ed25519 signature over canonical CBOR with Signature cleared. There are exactly 15 message kinds.

Every MCL message rides inside an Envelope: typed header, opaque CBOR body, ed25519 signature over the canonical CBOR encoding with Signature cleared. There are exactly 15 message kinds. No chat.message kind exists by design.

Source files: envelope/envelope.go, envelope/kinds.go, envelope/body.go, envelope/json.go, envelope/keyresolver.go.


Envelope struct

type Envelope struct {
    SchemaVersion   uint8           `cbor:"0,keyasint"`  // replay protection
    ProtocolVersion string          `cbor:"1,keyasint"`  // "mcl/0.1"
    Kind            string          `cbor:"2,keyasint"`  // message kind
    ID              string          `cbor:"3,keyasint"`  // ULID
    At              string          `cbor:"4,keyasint"`  // ISO-8601 timestamp
    From            string          `cbor:"5,keyasint"`  // matrix://agent/<did> or matrix://user/<did>
    To              string          `cbor:"6,keyasint,omitempty"`
    Intent          string          `cbor:"7,keyasint"`  // matrix://intent/<id>
    CorrelationID   string          `cbor:"8,keyasint,omitempty"`
    CausationID     string          `cbor:"9,keyasint,omitempty"`
    Body            cbor.RawMessage `cbor:"10,keyasint"` // canonical CBOR of typed body
    Signature       []byte          `cbor:"11,keyasint,omitempty"` // ed25519 sig
}

CBOR field tags are integer keyasint. New fields land at the next unused integer; deletions require a SchemaVersion bump.


Encoding posture

SurfaceFormatPurpose
On wireCanonical CBOR (CoreDetEncOptions)Compact, signature-stable
On diskJSON (EnvelopeJSON)Human-readable journal/logs
Signed bytesCanonical CBOR with Signature cleareded25519 sign/verify input

The Body is held as cbor.RawMessage so a single round-trip preserves byte equality (required for replay determinism).


The 15 message kinds

KindDirectionDescription
intent.draftUser -> agentInitial NL goal + slot pre-fills
intent.compiledAgent -> userTyped Intent IR for review
intent.clarifyAgent -> userStructured questions for unknowns
intent.answerUser -> agentAnswers to clarify questions (slot patches)
intent.acceptUser -> agentSigned sign-off on the IR
plan.proposedAgent -> userDecomposition into steps
plan.stepAgent -> agent/toolExecution of a single step
plan.outputAgent -> userStreaming intermediate output
intent.correctUser -> agentPatch an Intent or plan mid-flight
intent.dispatchAgent -> agentSub-intent to a delegated agent
intent.attestAgent -> user/chainSigned completion receipt
intent.failAgent -> userTyped failure
intent.cancelUser -> agentRevoke before completion
policy.gateAgent -> userHuman-in-loop checkpoint
policy.gate.resolveUser -> agentApprove/deny gate

There is no chat.message kind by design. All user input is intent.draft (new) or intent.answer / intent.correct (continuing).


Typed body structs

Each kind has a dedicated Go struct. Key examples:

IntentDraftBody

type IntentDraftBody struct {
    Prose          string            `cbor:"0,keyasint"`
    SlotValues     map[string]string `cbor:"1,keyasint,omitempty"`
    PreferredSkill string            `cbor:"2,keyasint,omitempty"`
}

IntentAcceptBody

type IntentAcceptBody struct {
    IntentHash      string `cbor:"0,keyasint"`  // sha256 of canonical Intent
    AcceptedAt      string `cbor:"1,keyasint"`  // ISO-8601
    AnchorRequested bool   `cbor:"2,keyasint,omitempty"`
}

IntentAttestBody

type IntentAttestBody struct {
    Outcome      string   `cbor:"0,keyasint"` // "success", "failure", "partial"
    CitedURIs    []string `cbor:"1,keyasint,omitempty"`
    EvidenceJSON []byte   `cbor:"2,keyasint,omitempty"`
    CompletedAt  string   `cbor:"3,keyasint"`
    AnchorTx     string   `cbor:"4,keyasint,omitempty"`
}

IntentFailBody

type IntentFailBody struct {
    Reason       string   `cbor:"0,keyasint"` // structured failure reason
    Message      string   `cbor:"1,keyasint,omitempty"`
    EvidenceJSON []byte   `cbor:"2,keyasint,omitempty"`
    FailedAt     string   `cbor:"3,keyasint"`
    PartialURIs  []string `cbor:"4,keyasint,omitempty"`
}

Structured failure reasons: blocked_by_constraint, tool_error, policy_denied, deadline_exceeded, budget_exceeded, subagent_failed, ambiguous_after_clarify, correction_invalid, x:custom.


Signing and verification

// Sign sets env.Signature to ed25519.Sign(priv, UnsignedBytes(env))
func Sign(env *Envelope, priv ed25519.PrivateKey) error

// VerifySignature checks env.Signature against pub over UnsignedBytes(env)
func VerifySignature(env *Envelope, pub ed25519.PublicKey) error

// Verify runs the full chain: SchemaVersion, required fields, kind validation,
// KeyResolver lookup, signature check
func Verify(env *Envelope, resolver KeyResolver) error

UnsignedBytes returns the canonical CBOR encoding with Signature explicitly cleared. SchemaVersion is retained so signatures from one schema cannot be replayed under another.


KeyResolver

type KeyResolver interface {
    ResolveKey(principal string) (ed25519.PublicKey, error)
}

The principal format is matrix://agent/<did> or matrix://user/<did>. Implementations live in the agent runtime / executor / tools/registry layer. The envelope package provides StaticKeyResolver for tests.


Self-hash and content addressing

func SelfHash(env *Envelope) (string, error) // sha256(UnsignedBytes)

The self-hash is a content-address for journal storage and Merkle anchoring input. It does not require the envelope to be signed.


On-disk JSON representation

For journal/logs readability, EnvelopeJSON renders an envelope as JSON with typed body fields. The signed bytes are always the canonical CBOR; JSON is purely for human/debug convenience.

type JSONEnvelope struct {
    SchemaVersion   uint8           `json:"schema_version"`
    ProtocolVersion string          `json:"v"`
    Kind            string          `json:"kind"`
    ID              string          `json:"id"`
    At              string          `json:"at"`
    From            string          `json:"from"`
    Intent          string          `json:"intent"`
    Body            json.RawMessage `json:"body"`
    Signature       string          `json:"signature,omitempty"` // base64
    SelfHash        string          `json:"self_hash,omitempty"` // sha256 hex
    // ...
}

EnvelopeFromJSON round-trips the JSON back to a canonical CBOR Envelope, cross-checking the self-hash against the on-disk value.