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
| Surface | Format | Purpose |
|---|---|---|
| On wire | Canonical CBOR (CoreDetEncOptions) | Compact, signature-stable |
| On disk | JSON (EnvelopeJSON) | Human-readable journal/logs |
| Signed bytes | Canonical CBOR with Signature cleared | ed25519 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
| Kind | Direction | Description |
|---|---|---|
intent.draft | User -> agent | Initial NL goal + slot pre-fills |
intent.compiled | Agent -> user | Typed Intent IR for review |
intent.clarify | Agent -> user | Structured questions for unknowns |
intent.answer | User -> agent | Answers to clarify questions (slot patches) |
intent.accept | User -> agent | Signed sign-off on the IR |
plan.proposed | Agent -> user | Decomposition into steps |
plan.step | Agent -> agent/tool | Execution of a single step |
plan.output | Agent -> user | Streaming intermediate output |
intent.correct | User -> agent | Patch an Intent or plan mid-flight |
intent.dispatch | Agent -> agent | Sub-intent to a delegated agent |
intent.attest | Agent -> user/chain | Signed completion receipt |
intent.fail | Agent -> user | Typed failure |
intent.cancel | User -> agent | Revoke before completion |
policy.gate | Agent -> user | Human-in-loop checkpoint |
policy.gate.resolve | User -> agent | Approve/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.
