The ALF Protocol: Agentic Lingua Franca

Version
ALF 0.1
Status
Draft
Date
July 2026

Abstract

The Agentic Lingua Franca (ALF) is the message format agents use to talk to each other. It defines the ALF message (header, sender meta, payload, signatures and transit), agent identity and ownership attestation, the agent card, security levels and the middleware pipeline.

ALF defines data in flight only. Transport, storage and inbox handling belong to the runtime. In ADF, messages are stored in the adf_inbox and adf_outbox tables (ADF specification §11).

Summary written for this site. The source document has no abstract.

Status of this document

This is a Draft of the ALF protocol, part of ADF. It may change in incompatible ways between versions. It is not a ratified standard.

This page is a snapshot of ALF_SPEC_v0.1.md at release tag v0.7.4 of christianbalevski/adf. The source file has no date field; July 2026 is the date of its last change at that tag. Report problems or propose changes by opening an issue.

Starting Assumptions

Sovereignty requires agency. Agency requires communication. Communication between independent entities requires a shared format.

This protocol is that shared format — the default language sovereign agents use to talk to each other before they’ve agreed on anything else. It ships with conformant runtimes so that any two stock agents can communicate immediately.

We’re making several assumptions that shape the protocol:

These agents are sovereign. They carry their own identity, move between runtimes, and may operate on hostile networks. This means the protocol needs to account for portable identity, cryptographic signatures, and optional encryption — things that enterprise agent protocols reasonably skip because they assume trusted infrastructure.

These agents are primarily LLM-powered. They have context windows, token budgets, and reason better with structured input. Some fields exist purely for LLM practicality — subject helps an agent triage a full inbox without loading every message body into context. content_type lets agents route structured content to deterministic handlers without burning tokens parsing unfamiliar JSON. These aren’t universal truths about agents; they’re practical concessions to how current AI agents work.

Communication is asynchronous. Sovereign agents can’t assume the other party is online, reachable, or running on the same infrastructure. Store-and-forward is the default. Synchronous communication can be negotiated on top.

Agents understand web conventions. LLMs are trained on internet-scale data. They know REST, JSON, HTTP, email patterns. The protocol leverages these rather than inventing new conventions where existing ones work.

Some of these decisions are best guesses. The goal is to simplify common interactions while not preventing alternatives. If something doesn’t work, payload.meta is always available for agents to develop their own conventions.


What This Protocol Covers

ALF defines the shape of data in flight — the ALF message. The message is the durable artifact: stored in inboxes, signed, forwarded through relays intact. It is the equivalent of an RFC 5322 email message.

The transport layer — HTTP POST URL, WebSocket connection, local same-runtime write — is NOT part of the ALF message. It is equivalent to the SMTP envelope (RFC 5321): it exists during delivery and is discarded after. The same ALF message is delivered identically regardless of transport.

How a runtime stores messages, manages inbox state, chains hashes for audit trails, or presents messages to the agent — those are runtime concerns. Different agent formats will handle storage differently. ALF doesn’t have opinions about that.


Scope: A Participation Layer

A second scoping choice sits alongside the “borrow conventions” principle: ALF is designed as a compatible surface for participating in conversations across messaging platforms, not for managing those platforms.

Each external platform an ALF-conformant runtime might bridge — Discord, Telegram, Microsoft Teams, Slack, email — has its own extensive data model for messages, reactions, threads, mentions, embeds, ephemeral interactions, channels, members, roles, permissions, and admin operations. Trying to subsume all of that into one protocol would produce either a leaky abstraction or a permanent moving target chasing the largest platform’s surface area.

Instead, ALF deliberately boils the shared substrate down to the primitives that recur across every conversational platform:

  • a sender and recipient
  • a thread and a parent
  • a subject and a content body
  • a timestamp the author can attest to
  • optional attachments
  • routing and identity metadata

These are sufficient for an agent to participate in a conversation on any platform — read what arrived, reply in context, send fresh messages, thread coherently. They are not sufficient for an agent to operate the platform — edit roles, enumerate members, restructure channels, manage permissions. Those concerns are intentionally out of scope.

That delineation does double duty: it keeps the protocol minimal enough to actually implement uniformly across heterogeneous runtimes, and it gives a clear rule for future evolution. A proposed addition belongs in ALF if it expresses a primitive of conversation that recurs across platforms. It does not belong in ALF if it expresses a per-platform management capability — that surface is better exposed through platform-specific tooling at the runtime layer (in ADF’s case, MCP servers; in other agent formats, whatever equivalent they offer).

This is also why ALF stops where it does on the rich-formatting axis. Platform-native concepts like Discord embeds, Slack blocks, or Teams adaptive cards are valuable but not common. ALF carries content and content_type; runtimes are free to negotiate richer formats on top via the open payload.meta and content_type dictionary, without ALF having to canonize anyone’s component model.


Conventions Borrowed vs. Invented

Most of ALF is borrowed:

DecisionSourceWhy borrow it
from/to/reply_toSMTP (RFC 5322)Email message headers. Universally understood.
subject + contentSMTPTriage without reading the full body. Useful for LLM context management.
content_typeMIME (RFC 2045)MIME type for content. Standard across email and HTTP.
thread_id/parent_idEmail/forumsAsync threading. Well-established pattern.
JSON wire formatWebUniversal data exchange. LLMs parse it natively.
DIDsW3CDecentralized identity. did:key currently — any standard crypto library can parse it without custom resolvers.
Ed25519Widely deployedFast, small signatures. Well-understood security properties.
X25519Widely deployedStatic key agreement for payload encryption. Stateless, async-friendly.
Hashcash v1Anti-spamBaseline spam prevention. No external dependencies.
Endpoint-based routingREST/HTTPLLMs understand REST natively. Endpoints are transparent and flexible.
Open dictionariesHTTP headersRequest headers vs hop-by-hop headers. Clear ownership model.
E2E encryptionSignalProven pattern for encrypted payloads over untrusted networks.
Ownership attestationDKIM/DMARCSender’s owner vouches for the agent. Receiver can verify.
Agent card / policyDKIM selector + DMARCDiscoverable identity and signing policy.
SLIP-0010HD walletsDeterministic key derivation for agent recovery.

A few things are novel because sovereign agents have requirements that existing conventions don’t cover:

DecisionWhy it’s new
Dual signaturesAuthorship proof that survives forwarding and re-encryption. No existing convention does this.
Aliases inside encrypted payloadHuman-readable names that intermediaries can’t see. Privacy requirement unique to sovereign communication.
sent_at inside payload signatureUnforgeable author timestamp. Distinct from the header timestamp which is transport-level.
Owner attestation on wireFast ownership verification without agent card fetch. Unique to sovereign agent identity.

Message Schema

The ALF message has five sections. The whole object is the durable artifact — stored, signed, forwarded. Equivalent to an RFC 5322 email message.

{
  // ── 1. HEADER ────────────────────────────────────────────
  // Addressing and routing. Equivalent to RFC 5322 headers.
  "version": "1.0",
  "network": "mainnet",
  "id": "msg_01HQ9ZxKp4mN7qR2wT",
  "timestamp": "2026-02-28T20:00:00Z",
  "from": "did:key:z6MkAlice...",
  "to": "did:key:z6MkBob...",
  "reply_to": "https://alice-server.com/alice/inbox",

  // ── 2. SENDER META ───────────────────────────────────────
  // Signed by sender. Immutable. Open dictionary for sender-asserted
  // claims about identity and context.
  "meta": {
    "owner": "did:key:z6MkAliceOwner...",
    "owner_sig": "ed25519:...",
    "card": "https://alice-server.com/alice/card",
    "pow": "1:20:2026-02-28:did:key:z6MkBob...::abc123:0000f"
  },

  // ── 3. PAYLOAD ───────────────────────────────────────────
  // E2E encrypted on public networks. Decrypted by destination runtime.
  "payload": {
    "meta": {},
    "sender_alias": "Alice",
    "recipient_alias": "Bob",
    "thread_id": "thr_abc123",
    "parent_id": null,
    "subject": "Project Schema V1",
    "content_type": "text/plain",
    "content": "Hey Bob, schema is done!",
    "attachments": [
      {
        "filename": "schema.json",
        "content_type": "application/json",
        "transfer": "inline",
        "data": "eyB2ZXJzaW9uOiAi..."
      }
    ],
    "sent_at": "2026-02-28T20:00:00Z",
    "signature": "ed25519:sender_signs_payload..."
  },

  // ── 4. MESSAGE SIGNATURE ─────────────────────────────────
  // Covers header + meta + payload as a unit.
  "signature": "ed25519:sender_signs_message...",

  // ── 5. TRANSIT ───────────────────────────────────────────
  // Append-only. Each intermediary adds its entry. Not sender-signed.
  "transit": {
    "route": [
      {
        "did": "did:key:z6MkRelay...",
        "name": "us-east-relay",
        "timestamp": "2026-02-28T20:00:01Z",
        "signature": "ed25519:relay_signs_its_hop..."
      }
    ]
  }
}
SectionOwnerMutabilitySigned by sender
HeaderSenderImmutableYes
Sender MetaSenderImmutableYes
PayloadSenderImmutable (encrypted)Yes
SignatureSenderImmutable—
TransitNetworkAppend-onlyNo

Addressing and routing. Unencrypted so intermediaries can route without reading content. Equivalent to RFC 5322 message headers.

FieldTypeRequiredWhy it’s here
versionstringYesProtocol compatibility.
networkstringYesPrevents cross-network leakage. "mainnet", "testnet", "devnet", or custom.
idstringYesGlobally unique. Minimum 20 characters (~120 bits entropy). Used for deduplication, threading references, and provenance tracking across runtimes.
timestampstringYesISO 8601. Ordering and freshness.
fromstringYesSender’s DID. Portable identity.
tostringYesRecipient’s DID. Signed, so it’s proof of intended destination.
reply_tostringYesURL where replies should be sent. In the message body, not a transport header — survives relay hops without special forwarding logic. Consistent with email’s Reply-To: which is an RFC 5322 message header, not an SMTP envelope field.

from and to are identity (who). reply_to is routing (where). from and to use DIDs because they identify agents portably. reply_to uses a URL because it’s a delivery address — the sender’s preferred inbox endpoint. If omitted, the receiver constructs a default from the sender’s DID and connection metadata.


Sender Meta

Open dictionary. Signed by the sender — immutable after creation. This is the sender’s space for identity context and transport-level data.

"meta": {
  // Identity context
  "owner": "did:key:z6MkAliceOwner...",
  "owner_sig": "ed25519:...",
  "card": "https://alice-server.com/alice/card",

  // Anti-spam
  "pow": "1:20:2026-02-28:did:key:z6MkBob...::abc123:0000f",

  // Transport hints
  "sender_address": "https://relay.example.com/a7f2k9/inbox",
  "ttl": 300,
  "priority": "high",

  // Economics
  "fee_tx": "sol_tx_98765...",

  // Source routing
  "route": ["did:key:z6MkRelayA...", "did:key:z6MkRelayB..."],

  // Runtime attestation
  "runtime": "sha256:abc123..."
}

All keys optional. Middleware processes what it recognizes, ignores the rest. New capabilities are meta keys plus middleware, not protocol changes.

meta.owner and meta.owner_sig — Ownership

owner is the DID of the entity that owns or operates this agent. Singular on the wire — identifies the current authorizing party.

owner_sig is the owner’s signature proving they claim this agent. The signed payload is minimal — just the agent DID and owner DID — so that verifiers can reconstruct and check it entirely from information already in the message:

owner_sig = sign(owner_private_key, canonical_json({
  agent: "<agent DID from header.from>",
  owner: "<owner DID from meta.owner>"
}))

This is deliberately different from the full attestation signatures on the agent card, which cover role, issued_at, expires_at, scope. The wire signature enables fast verification without a card fetch. The card signatures provide full attestation details when needed.

If the agent has no owner attestation, both fields are omitted.

meta.card — Agent Card URL

The URL where the sender’s agent card can be fetched. Equivalent to DKIM’s selector + domain pointing to a DNS record.

Receivers fetch this once on first contact, cache the result, and use it for full attestation verification, signing policy lookup, endpoint discovery, and display metadata. Self-hosted by default. Can point to a registry or directory service.

Baseline Spam Prevention

A lingua franca needs a default dialect. If Alice uses Hashcash, Bob uses Solana micro-transactions, and Charlie uses a staking mechanism, they can’t reach each other without bilateral negotiation — which defeats the purpose of a shared language.

ALF specifies Hashcash v1 as the Tier 2 baseline for spam prevention. No external dependencies, no blockchain, no payment infrastructure, just CPU. The pow meta field uses the standard Hashcash format:

pow: "1:20:2026-02-28:did:key:z6MkBob...::abc123:0000f"

The difficulty (number of leading zero bits) is set by the recipient and advertised in their agent card. Default: 20 bits.

Agents can upgrade to crypto-economics, staking, reputation systems, or anything else via custom middleware. But if two stock agents meet for the first time with no prior arrangement, Hashcash is the fallback that always works.


Payload

The actual message content. Encrypted on public networks. Intermediaries can’t read it.

FieldTypeRequiredWhy it’s here
metaobjectNoOpen dictionary inside the encrypted envelope. Application-level data, adapter context, anything.
sender_aliasstringNoHuman-readable sender name. Inside encryption so intermediaries can’t see it.
recipient_aliasstringNoHuman-readable recipient name. Encrypted.
thread_idstringNoGroups messages into conversations. Practical for LLMs managing multiple threads.
parent_idstringNoWhich message this replies to. Enables tree-structured conversations.
subjectstringNoShort summary for inbox triage. LLM practicality — avoids loading full message bodies into context.
content_typestringNoMIME type of the content field. Defaults to text/plain if omitted. Lets agents route structured content to deterministic handlers without burning tokens parsing unfamiliar JSON.
contentstring/objectYesThe message body.
attachmentsarrayNoFiles attached to the message. Each entry is inline (base64) or reference (URL + digest). See Attachments.
sent_atstringYesISO 8601. Author’s timestamp inside the signature — unforgeable. Different from the header timestamp which is transport-level.
signaturestringNoPayload signature. See Dual Signatures.

payload.content_type

Content TypeUse Case
text/plainHuman-readable messages (default)
text/markdownFormatted messages
application/jsonStructured data exchange between agents

Receivers that don’t understand the content type fall back to treating content as plain text. Simple agents ignore the field, sophisticated agents parse structured content.

payload.meta is an open dictionary. A few conventions are worth standardizing to prevent fragmentation:

"meta": {
  // Wrapper hint — helps middleware select unwrap behavior
  "wrapper": "fanout",

  // Adapter context — round-trip data for channel adapters
  "source": "telegram-adapter",
  "source_context": { "chat_id": "12345" }
}

Message Signature

Sender signs sections 1–3 (header + sender meta + payload as encrypted bytes).

"signature": "ed25519:base64encoded..."

Verifiable by anyone using the sender’s public key (resolved from DID). On trusted networks the signature may be absent — the agent accepts the risk.


Transit

Append-only. NOT signed by the sender. This is the network’s space — each intermediary adds its entry. Equivalent to email’s Received: headers.

"transit": {
  "route": [
    {
      "did": "did:key:z6MkRelayA...",
      "name": "us-east-relay",
      "timestamp": "2026-02-28T20:00:01Z",
      "signature": "ed25519:relay_signs_its_hop..."
    },
    {
      "did": "did:key:z6MkRelayB...",
      "name": "eu-west-relay",
      "timestamp": "2026-02-28T20:00:03Z",
      "signature": "ed25519:relay_signs_its_hop..."
    }
  ]
}

Each route entry identifies the relay, timestamps its hop, and signs its contribution. The route array is a standard convention — transit remains an open dictionary and intermediaries can write other keys they need.

If meta.route exists (source routing), the transit.route array records progress against the sender’s declared path.

Two open dictionaries, two owners:

DictionaryOwnerSigned by senderPurpose
metaSenderYesIdentity context, transport hints, economics
transitNetworkNoRelay chain, intermediary annotations

Identity Architecture

Identity Provider Abstraction

The spec defines what identity looks like (Ed25519 keypairs, DIDs derived from public keys). How keys are generated, stored, and accessed is a runtime concern, abstracted behind an identity provider interface.

The runtime asks the provider: “give me the keypair for this agent” and “sign this payload.” The agent stores its DID (public identity) but private key material lives wherever the identity provider puts it.

Three provider models, all producing identical on-the-wire identity:

ModelKey StorageRecoveryTradeoff
SovereignUser holds seed phrase, derives keys locallyRe-derive from seed + pathMaximum control, maximum responsibility
Platform-managedPlatform keychain (Secure Enclave, Titan, etc.)Recover platform accountTransparent to user, platform-dependent
CustodialCloud-hosted (HSM)Account recoveryLowest friction, least sovereignty

From the network’s perspective, all three are indistinguishable. A message signed by a DID verifies the same way regardless of where the private key lives. The spec does not define identity providers — it defines that agents have Ed25519 keypairs and DIDs.

Hierarchical Deterministic Key Derivation

Within any identity provider, agent keypairs SHOULD be derived deterministically from an owner seed using SLIP-0010 (Ed25519 hardened derivation):

owner_seed + derivation_path → Ed25519 keypair → DID

Same seed + same path = same key = same DID. This enables recovery: wipe the agent, re-derive from the same path, the DID comes back.

Derivation is a private concern. It does not appear on the wire, in the agent card, or in any protocol message. The derived key IS the agent’s key — derivation is just how it was produced.

The owner who holds the seed can reconstruct any child agent’s private key. The ability to recover and the ability to impersonate are the same capability. This is by design — the agent is sovereign from other agents and other owners, not from its own creator.

DID Resolution and Transferability

The current did:key identifier is derived directly from the public key. This means:

  • The DID is the key. Verification is self-contained.
  • If the key changes, the DID changes. All existing attestations are invalidated.
  • Transfer requires either raw key handoff or re-attestation under a new DID.

Under this model, an agent’s identity-linked reputation (attestations, certifications, trust ratings) is non-transferable. The agent has configuration value (its document, tools, knowledge) but its reputation is bound to the original keypair.

Future path: To enable clean transfer, the DID method could evolve into a resolvable method where the identifier is a stable random ID and the current public key is resolved through a decentralized mechanism (on-chain registry, gossip, or similar). This would decouple identity from key material, enabling key rotation and ownership transfer without attestation loss. The attestation schema defined below is forward-compatible with resolvable DIDs.


Ownership Attestation

Concept

Ownership attestation is a signed statement by an owner certifying an agent as theirs. It serves the same role as DKIM in email: the owner vouches for the agent.

Two layers:

  • On the wire (every message): Owner DID and attestation signature in meta. Minimal overhead, fast verification without fetching the agent card.
  • On the agent card (fetched once, cached): Full attestation objects with metadata, expiry, scope, and multiple attestors.

Attestation Schema

The agent card carries an attestations array. Each attestation is an independent signed claim by an external party about the agent.

"attestations": [
  {
    "issuer": "did:key:z6MkWalmart...",
    "role": "owner",
    "issued_at": "2026-03-01T00:00:00Z",
    "expires_at": null,
    "scope": "full",
    "signature": "ed25519:..."
  },
  {
    "issuer": "did:key:z6MkRetailCert...",
    "role": "verified_merchant",
    "issued_at": "2026-02-15T00:00:00Z",
    "expires_at": "2027-02-15T00:00:00Z",
    "scope": "commerce",
    "signature": "ed25519:..."
  }
]
FieldTypeRequiredDescription
issuerstring (DID)YesDID of the attesting party.
rolestringYesRelationship being attested. "owner" is reserved for ownership claims. Other roles are application-defined.
issued_atstring (ISO 8601)YesWhen the attestation was created.
expires_atstring or nullNoWhen it expires. Null = no expiry.
scopestring or nullNoWhat the attestation covers. Application-defined.
signaturestringYesEd25519 signature by the issuer over the canonical attestation payload.

What the issuer signs (card attestation):

The card attestation signature covers the full attestation payload — more than the minimal wire signature:

sign(issuer_private_key, canonical_json({
  agent: "<agent DID>",
  issuer: "<issuer DID>",
  role: "<role>",
  issued_at: "<ISO timestamp>",
  scope: "<scope or null>"
}))

This enables detailed verification: what role was attested, when, with what scope and expiry. The wire signature (meta.owner_sig) is a separate, minimal signature for fast verification without these details.

Verification Flow

Fast path (wire only, no card fetch):

  1. Extract owner’s public key from meta.owner DID
  2. Reconstruct canonical payload: { agent: header.from, owner: meta.owner }
  3. Verify meta.owner_sig against the reconstructed payload
  4. Result: this owner claims this agent. No expiry, scope, or role information.

Full path (first contact, then cached):

  1. Fast-path verification first
  2. Fetch agent card from meta.card URL
  3. Find attestation(s) with matching issuer DID
  4. Verify each attestation signature against its full canonical payload (agent, issuer, role, issued_at, scope)
  5. Check expires_at, scope, role as needed
  6. Cache the card — subsequent messages use fast path only

Multiple Attestations

An agent can carry attestations from multiple independent parties:

  • Co-ownership: Multiple role: "owner" attestations from different parties
  • Certification: An industry body attests the agent meets standards
  • Marketplace verification: A platform attests the agent is a verified participant
  • Reputation layering: Different authorities vouch for different capabilities

The meta.owner field on the wire is singular — it identifies the current operator. The full attestation picture is on the card.

Revocation

Attestation revocation is a distribution problem independent of the format. Three approaches, any of which work:

  • Short-lived credentials: Set expires_at and require periodic renewal
  • Revocation announcement: Issuer publishes a signed revocation message through the network
  • Registry: External revocation list (centralized or on-chain)

The spec does not mandate a revocation mechanism. Implementations SHOULD check expires_at when present.


Routing

Messages are delivered to endpoints. The endpoint is the URL path the message is POSTed to — the first piece of information available about an incoming message, known before decryption, before parsing, before anything.

Agents advertise their endpoints in agent cards. Senders choose which endpoint to target. Recipients define handlers for their endpoints. The ALF message format is the same regardless of endpoint — only the destination path changes.

The protocol doesn’t define which endpoints agents should have. /inbox and /health will probably become common conventions. Relays will likely expose /api/register and /api/members. These are conventions that emerge from usage, not protocol mandates.

If agents need message categorization within a single endpoint, payload.meta is available for that.


Attachments

Files are attached inside the encrypted payload. Each attachment is self-describing with its own content_type and transfer mode.

Inline

Base64-encoded, included directly in the message. Practical for small to medium files.

{
  "filename": "config.json",
  "content_type": "application/json",
  "transfer": "inline",
  "data": "eyB2ZXJzaW9uOiAi..."
}

Reference

Hosted elsewhere. The message includes a URL, content digest, and file size. Practical for large files.

{
  "filename": "model_weights.safetensors",
  "content_type": "application/octet-stream",
  "transfer": "reference",
  "url": "https://host:port/alice/shared/model_weights.safetensors",
  "digest": "sha256:d2a84f4b8b650937ec8f73cd8be2c74add5a911ba64df27458ed8229da804a26",
  "size_bytes": 4294967296
}

Imported (Runtime-Only)

After delivery, the receiving runtime extracts inline attachments to the recipient’s local filesystem and changes the transfer field from "inline" to "imported". This is a storage-side marker — it never appears on the wire. It indicates that the file data has been extracted and the data field is no longer present.

{
  "filename": "config.json",
  "content_type": "application/json",
  "transfer": "imported",
  "path": "imported/sender-name/config.json",
  "size_bytes": 1024
}

The path field is a StoredAttachment extension (not part of the wire format) pointing to the local file in the recipient’s workspace.

Attachment Fields

FieldTypeRequiredDescription
filenamestringYesHuman-readable filename.
content_typestringYesMIME type.
transferstringYes"inline", "reference", or "imported" (storage-only, post-delivery).
datastringIf inlineBase64-encoded file content.
urlstringIf referenceURL where the file can be fetched.
digeststringIf referenceContent hash for integrity verification. Format: algorithm:hex.
size_bytesintegerNoFile size. Useful so the recipient can decide before fetching.
pathstringIf importedLocal filesystem path (runtime extension, not on wire).

Both modes can coexist in the same message. Attachments are inside the payload, so they’re covered by E2E encryption. Reference URLs are also encrypted — intermediaries can’t see what files are being shared or where they’re hosted.


Dual Signatures

Two signatures for two different purposes.

SignatureLocationCoversPurpose
Message signatureTop-level signatureEntire message (encrypted payload bytes)Transport integrity — “this DID sent this to that DID”
Payload signaturepayload.signaturePayload content (plaintext)Authorship proof — “this person wrote these words”

Why Both?

The message signature breaks when the payload is re-encrypted (group fan-out, forwarding). The payload signature survives because it covers the plaintext, not the encrypted bytes.

This matters for sovereign agents because accountability requires unforgeable authorship. If Alice sends a message to a group and the group forwards it to Bob, Bob needs to verify Alice wrote it — not just that the group relayed it.

Flow

Alice → Group:
  1. Alice constructs payload, signs it → payload.signature
  2. Alice encrypts payload, signs entire message → top-level signature
  3. POST to group's /inbox

Group → Bob:
  1. Verify Alice's message signature
  2. Decrypt, verify Alice's payload signature
  3. Wrap Alice's message inside a new message
  4. POST to Bob's /inbox

Bob receives:
  1. Verify group's wrapper signature
  2. Unwrap, find Alice's inner message
  3. Verify Alice's payload signature → Alice wrote this

Tradeoffs

Accountability over deniability. Deniable encryption is a well-understood and often desirable feature in communication systems — but those systems have primarily been designed for human users, where privacy from third parties is a reasonable default. For AI agents, the calculus is different. Autonomous agents acting on behalf of users, spending resources, making commitments, and interacting with other agents’ resources need to be accountable for their actions. We think accountability is the more important default for agentic systems.

Deniability is always available — agents can use middleware to strip payload signatures or implement deniable signature schemes — but it requires both parties to agree and accept the implications. The protocol doesn’t prevent it; it just doesn’t start there.

Both signatures are optional. Recipients that require them reject messages without them.


The Wrapper Pattern

When an intermediary re-routes a message while preserving the original, it wraps it: the original becomes the content of a new message. The inner message is never modified.

Group Fan-Out Example

Alice sends to the group’s /inbox:

{
  "version": "1.0",
  "network": "mainnet",
  "id": "msg_01HQ9ZxKp4mN7qR2wT",
  "timestamp": "2026-02-28T20:00:00Z",
  "from": "did:key:z6MkAlice...",
  "to": "did:key:z6MkProjectChat...",
  "reply_to": "https://alice-server.com/alice/inbox",
  "meta": {
    "owner": "did:key:z6MkAliceOwner...",
    "owner_sig": "ed25519:...",
    "card": "https://alice-server.com/alice/card"
  },
  "payload": {
    "sender_alias": "Alice",
    "thread_id": "thr_dev_updates",
    "content": "Hey team, schema is done.",
    "sent_at": "2026-02-28T20:00:00Z",
    "signature": "ed25519:alice_payload_sig..."
  },
  "signature": "ed25519:alice_message_sig...",
  "transit": {}
}

The group wraps for each member and POSTs to their /inbox:

{
  "version": "1.0",
  "network": "mainnet",
  "id": "msg_fanout_bob_01HQ9a...",
  "timestamp": "2026-02-28T20:00:01Z",
  "from": "did:key:z6MkProjectChat...",
  "to": "did:key:z6MkBob...",
  "reply_to": "https://group-server.com/project-chat/inbox",
  "meta": {
    "card": "https://group-server.com/project-chat/card"
  },
  "payload": {
    "meta": { "wrapper": "fanout" },
    "content": {
      "version": "1.0",
      "network": "mainnet",
      "id": "msg_01HQ9ZxKp4mN7qR2wT",
      "timestamp": "2026-02-28T20:00:00Z",
      "from": "did:key:z6MkAlice...",
      "to": "did:key:z6MkProjectChat...",
      "reply_to": "https://alice-server.com/alice/inbox",
      "meta": {
        "owner": "did:key:z6MkAliceOwner...",
        "owner_sig": "ed25519:...",
        "card": "https://alice-server.com/alice/card"
      },
      "payload": {
        "sender_alias": "Alice",
        "thread_id": "thr_dev_updates",
        "content": "Hey team, schema is done.",
        "sent_at": "2026-02-28T20:00:00Z",
        "signature": "ed25519:alice_payload_sig..."
      },
      "signature": "ed25519:alice_message_sig..."
    },
    "sent_at": "2026-02-28T20:00:01Z",
    "signature": "ed25519:group_payload_sig..."
  },
  "signature": "ed25519:group_message_sig...",
  "transit": {}
}

Recognizing Wrapped Messages

When content is an object containing ALF message fields (version, from, to, signature), it’s a nested message. The optional payload.meta.wrapper hint ("fanout", "forward", "error") helps select unwrap behavior, but structural recognition is the primary mechanism.

Common Patterns

PatternOuter fromInner frompayload.meta.wrapper
Group fan-outgroup DIDoriginal author"fanout"
Forwardingforwarder DIDoriginal author"forward"
Error bouncerelay DIDrelay DID"error"

Threading

Follows email and forum conventions. Practical for LLMs managing multiple concurrent conversations.

if thread_id provided → use it
else if parent_id provided → inherit from parent
else → thread_id = own message id

Inner messages in wrappers carry their own threading through wrapping.


Security Levels

Four levels. Agents choose based on their threat model. The protocol doesn’t mandate any level.

Level 0: Open

No signature. No encryption. Localhost, private VPC, development. Fine for twelve agents on a laptop.

Level 1: Signed

Signature present. No encryption. Content readable by anyone on the path. For public broadcasts, announcements, open coordination — when you want to prove you said something but don’t need privacy.

Level 2: Signed + Encrypted

Signature present. Payload encrypted using X25519 static key agreement — the recipient’s public key is derived from their did:key. Simple, stateless, works for async store-and-forward without requiring session state.

The practical minimum for sovereign agents on public infrastructure. Without it: content is interceptable, aliases leak to intermediaries, authorship isn’t provable.

Tradeoff: no forward secrecy. Static key encryption means that if an agent’s private key is ever compromised, past captured traffic can be retroactively decrypted. This is a deliberate choice — forward secrecy requires session state, which conflicts with the async store-and-forward model. Agents that need forward secrecy upgrade via Level 3.

Level 3: Advanced

Level 2 plus additional guarantees via middleware and meta conventions. What “advanced” means is up to the agent:

  • Forward secrecy — Double Ratchet or similar, ephemeral key material exchanged via meta fields
  • Path tracing — signed relay chain in transit
  • Onion routing — layered encryption, each relay sees only the next hop
  • Zero-knowledge proofs — prove runtime integrity without revealing code
  • Post-quantum cryptography — lattice-based signatures
  • Deniable signatures — provable to recipient but not to third parties

Level 3 is a space, not a feature. The protocol provides extensibility mechanisms; agents choose their guarantees.


Middleware Pipeline

Three tiers on both ingress and egress.

Tier 1: Runtime

Ships with the runtime. Core crypto — signing, encryption, decryption, signature verification, wrapper recognition.

Tier 2: Standard

Ships with the default stack. Common operations — DID resolution, Hashcash v1 PoW generation/verification, thread resolution, peer auto-discovery, rate limiting, owner attestation verification. Swappable and lockable by owner.

Tier 3: Custom

Installed by agent owner. PQC, custom DID resolution, content filtering, payment verification, protocol bridges (A2A, ACP), onion routing, ZK attestation, whatever the agent needs.

Middleware Locking

Owners can lock middleware to prevent modification. A locked encryption requirement can’t be downgraded. A locked PoW requirement can’t be bypassed.

DID Resolution

Tier 2. Default resolves from local peer table. Alternatives: relay query, registry lookup, DHT, blockchain. Swap the resolver middleware to change the strategy.


Transport Layer

The transport layer is NOT part of the ALF message. It is the mechanism used to deliver the message — equivalent to the SMTP envelope (RFC 5321). The same ALF message is delivered identically over HTTP, WebSocket, or local same-runtime transfer.

TransportDeliveryConfirmationUse Case
HTTP POSTPOST to recipient’s endpointHTTP 202 AcceptedDefault. Direct delivery, relay forwarding.
WebSocketWrite ALF message as text frameSocket acceptancePersistent connections, NAT traversal, relay push.
LocalDirect inbox write (skip network)Immediate (same process)Agents on the same runtime.

The runtime selects transport automatically on egress: same-runtime → active WebSocket → HTTP POST.

What Lives in Transport

ConcernWhereEmail equivalent
Delivery addressPOST URL / connection targetSMTP RCPT TO
Bounce addressTransport context, hoisted to inbox on receiptSMTP MAIL FROM → Return-Path:
Connection metadataTCP/HTTP/WS contextSMTP connection info

These never enter the ALF message. The receiver hoists what it needs into inbox columns or transit entries on receipt. The return_path (where delivery failure notifications go) is constructed from transport context and stored on the inbox row — separate from reply_to because failure notifications are a transport concern, not an application concern.

WebSocket Authentication Handshake

WebSocket connections use mutual DID authentication before any ALF messages are exchanged:

  1. Client sends auth: { type: "auth", did, nonce: <random-32-bytes-hex>, signature: "ed25519:<base64>", timestamp }
    • signature signs: did + nonce + timestamp (concatenated as UTF-8)
  2. Server verifies: extract pubkey from DID, verify signature, check timestamp within 30s.
    • Invalid → close with code 4001.
  3. Server responds: { type: "auth_result", success: true, server_did, nonce: <client-nonce>, signature: "ed25519:<base64>" }
    • signature signs: server_did + client_nonce + timestamp
  4. Client verifies: server signature. If expected DID was configured and server_did doesn’t match → close.
  5. Connection authenticated. All subsequent text frames are ALF messages (one per frame).

Auth timeout: 30s from connection open.

Inbound: If security.allow_unsigned: true on the receiving agent, the handshake is skipped. Clients may still send an optional auth frame to claim a DID — it is accepted without verification and stamped identity_verified: false.

Outbound: Auth behavior is controlled per-connection via the auth field on WsConnectionConfig (auto | required | none), independent of the agent’s allow_unsigned setting. Default auto authenticates whenever a private key is available.

WebSocket Close Codes

CodeMeaning
1000Normal closure
1001Going away (agent unregistered, shutdown)
4001Authentication failed
4003Invalid frame (not valid JSON or not a valid ALF message)
4004No WebSocket route configured
4503WebSocket manager not available

Agent Card

The portable identity and policy document for agent discovery. ALF defines the wire format — how cards are constructed and stored is a runtime concern.

handle is the single required identity label — the runtime-unique, URL-safe identifier that appears in every endpoint URL. description provides human-readable context. Agent capabilities are expressed through description and shared files rather than a machine-readable capabilities list.

{
  // Identity
  "did": "did:key:z6MkAgent...",
  "handle": "walmart-support",
  "description": "Customer service agent",
  "public_key": "z6MkAgent...",

  // Resolution — how to verify the current public key for this DID
  "resolution": {
    "method": "self",
    "endpoint": "https://relay.example.com/walmart-support/card"
  },

  // Endpoints
  "endpoints": {
    "inbox": "https://relay.example.com/walmart-support/inbox",
    "card": "https://relay.example.com/walmart-support/card",
    "health": "https://relay.example.com/walmart-support/health",
    "ws": "wss://relay.example.com/walmart-support/ws"
  },

  // Attestations
  "attestations": [
    {
      "issuer": "did:key:z6MkWalmart...",
      "role": "owner",
      "issued_at": "2026-03-01T00:00:00Z",
      "expires_at": null,
      "scope": "full",
      "signature": "ed25519:..."
    }
  ],

  // Policies — typed policy objects declaring send/receive behavior
  "policies": [
    {
      "type": "signing",
      "standard": "ed25519",
      "send": "required",
      "receive": "required"
    },
    {
      "type": "owner_attestation",
      "send": "required",
      "receive": "optional"
    }
  ],

  "public": true,
  "shared": ["public/capabilities.md"],

  // Card signature — runtime signs the card on every build
  "signed_at": "2026-03-07T12:00:00Z",
  "signature": "ed25519:..."
}

Policies

Array of typed policy objects declaring send/receive behavior. Each policy has a type, optional standard, and send/receive levels:

LevelOn sendOn receive
"required"I always do thisI demand this from you
"optional"I can do thisI can handle this
"none"I don’t do thisI don’t care about this

Pre-send: Fetch peer’s card, check their receive requirements before sending. Peer says signing.receive: "required" — you must sign or your message will be rejected.

Post-receive (spoofing detection): Check peer’s send declarations against received message. Peer says signing.send: "required" but message has no signature — likely spoofed. Policies are advisory. The receiver decides enforcement.

Card Signature

The runtime signs the card whenever it builds one, using the same Ed25519 signing used for ALF messages. signed_at is an ISO timestamp of when the card was signed. Registries or other agents receiving a card verify the signature against the public_key in the card before trusting it.

Signature scope. The signature covers identity and policy fields only — specifically, the canonical JSON of all card fields except:

  • signature itself (a signature can’t cover itself)
  • endpoints (inbox, card, health, ws)
  • resolution.endpoint (the URL within the resolution block)

These excluded fields are reachability metadata that the directory endpoint rewrites per-requester: a LAN peer fetching /agents receives cards with LAN-reachable URLs, while a loopback caller receives 127.0.0.1 URLs — same card, different endpoints, same signature. The signature protects identity (did, public_key, handle, description, policies, resolution method, …) and the receiver treats endpoints as transport hints, not identity claims.

Verifiers must apply the same canonicalization on verify: strip signature, endpoints, and resolution.endpoint before hashing. The reference implementation exposes canonicalizeCardForSignature(card) for this.

Resolution

The resolution block tells verifiers how to look up and confirm the current public key for this DID. For "self" resolution (the default), the card itself is authoritative. Other methods ("chain", "registry", "dns") enable key rotation and resolvable DIDs.

Card Overrides

Agents can override auto-derived card fields via config (card.endpoints, card.resolution). This enables agents behind public relays to advertise their relay URLs instead of local mesh addresses. Update via sys_update_config:

sys_update_config({ path: "card.endpoints", value: { "inbox": "https://relay.example.com/me/inbox" } })

Alias Registration

On public networks, aliases should be cryptographically bound to identity.

alias = hash(public_key + relay_salt)

Relay publishes its salt. Anyone can verify the binding.

Registration Handshake

Agent POSTs to relay’s /api/register:

{
  "version": "1.0",
  "network": "mainnet",
  "id": "msg_reg_01HQa2bCdEfGhIjK",
  "timestamp": "2026-02-28T20:00:00Z",
  "from": "did:key:z6MkBob...",
  "to": "did:key:z6MkRelayUSEast...",
  "reply_to": "https://bob-server.com/bob/inbox",
  "meta": {},
  "payload": {
    "content": { "public_key": "ed25519:..." },
    "sent_at": "2026-02-28T20:00:00Z",
    "signature": "ed25519:bob_payload_sig..."
  },
  "signature": "ed25519:bob_message_sig...",
  "transit": {}
}

Relay responds to Bob’s /inbox:

{
  "from": "did:key:z6MkRelayUSEast...",
  "to": "did:key:z6MkBob...",
  "payload": {
    "content": {
      "alias": "x7f9k2",
      "address": "https://relay-us-east.example.com/x7f9k2/inbox",
      "capabilities": ["trace"],
      "terms": { "cost_per_message": 0.001, "cost_unit": "usd", "free_tier": 1000 }
    },
    "sent_at": "2026-02-28T20:00:01Z"
  }
}

Alias lifetime (session, TTL, permanent) is relay policy.


Relay Economics

Relays set their own terms. Payment requests are standard ALF messages:

{
  "payload": {
    "content": {
      "outstanding": 12.50,
      "currency": "usd",
      "held_messages": 47,
      "payment_address": "0x..."
    },
    "sent_at": "2026-02-28T20:00:00Z"
  }
}

The agent reasons about it. Payment mechanisms are middleware concerns. Costs compound through relay chains, creating natural pressure toward flatter topologies.


What the Protocol Doesn’t Cover

These are runtime, application, or agent-level concerns:

ConcernWhere it lives
Endpoint definitionsAgent cards
Message categoriespayload.meta or endpoint selection
Group chat mechanicsGroup agent implementation
Message storageRuntime (inbox/outbox schema)
Audit trailsRuntime (hash-chaining, attestation)
Contact managementAgent (peer tables)
ModerationCustom middleware
Protocol bridgingCustom middleware
Cost trackingAgent
Identity providersRuntime (sovereign, platform-managed, custodial)
Key derivationRuntime (SLIP-0010, private concern)
Bounce handlingTransport layer (return_path)

Group E2EE

Two approaches, both valid.

Trusted Group Relay

The common case. The group relay is trusted to see plaintext. Alice signs her payload, encrypts the message to the group, sends it. The group decrypts, verifies Alice’s payload signature, then re-encrypts and forwards to each member individually. Members verify Alice’s payload signature to confirm authorship.

This is what the wrapper pattern already supports. The dual signature model exists precisely for this — the payload signature survives the group’s decrypt-and-re-encrypt cycle.

Symmetric Key (Untrusted Relay)

For groups where the relay shouldn’t see content. Alice requests the member list from the group relay, then messages each member directly with a shared symmetric key. Group messages are encrypted with the symmetric key. The relay forwards encrypted blobs it can’t read.

Key distribution is out-of-band from the relay’s perspective — just normal ALF messages between Alice and each member. Key rotation is the group’s responsibility.

Both approaches use the same ALF message format. The difference is whether the group relay decrypts or just forwards.


Message Size

Protocol default: 25MB per message including inline attachments.

This matches the email convention. The payload without attachments is typically a few KB. Inline attachments are where size matters — 25MB accommodates documents, images, code files, and medium datasets. Anything larger should use reference transfers.

Relays can advertise lower limits in their terms. Runtimes can enforce their own limits. 25MB is the baseline that conformant implementations should support.


Non-Normative Conventions

Stream Control Messages — application/alf-stream-*+json

Agents negotiating continuous byte streams (shell sessions, log tailing, CDC replication, TCP forwarding, WebRTC signaling, LLM token streaming) SHOULD use the application/alf-stream-*+json content_type family so tooling — UIs, audit systems, middleware — can recognize stream control messages without understanding every underlying stream protocol.

Canonical verbs:

content_typePurpose
application/alf-stream-open+jsonInitiator requests a stream
application/alf-stream-accept+jsonResponder accepts, provides endpoint
application/alf-stream-reject+jsonResponder refuses, provides reason
application/alf-stream-close+jsonEither side signals end of session

Payload shapes are agent-defined by convention. These are ordinary ALF messages — existing middleware, signature verification, owner attestation, and trust gates apply unchanged.

This is a non-normative reservation. No changes to the message schema, no new fields, no new sections in the normative protocol. Adherence enables interop; ignorance does not break conformance.


Future Work

ItemDescription
ARC-style relay chain authauth_results on transit.route entries — each relay records verification results (message_sig, payload_sig, owner_sig pass/fail). Enables end-to-end trust through relay chains.
Resolvable DID methodStable DID decoupled from key material via blockchain/consensus. Enables key rotation and ownership transfer without attestation loss.
Revocation standardStandard mechanism for invalidating attestations (short-lived credentials, broadcast, or registry).
References-style threadingFull message ID chain for resilient thread reconstruction across runtimes (RFC 5322 References: equivalent).
Structured bounce messagesTyped failure notifications to return_path.
Per-message owner co-signingHigher security level where owner co-signs each message (requires owner key to be hot).