Legion Architecture — the 4-layer API model
Status: DRAFT (Foundation 09, 2026-06-18). This documents the L1–L4 layering and, in particular, makes the L3 (provider) and L4 (consumer) surfaces visible for the first time. The L3/L4 public APIs described here are drafts, not release candidates — expect discussion and changes before any surface is frozen. Feedback welcome. The export boundary is CI-locked; the specific symbol set is pre-release (see
abi.md).
Read this first
Legion is a distributed capability mesh for individual data sovereignty: a cohort of nodes (your phone, your machines) that discover and invoke each other's capabilities — speech-to-text, an LLM, storage, audio — without routing your data through a big-cloud dependency.
Its API is a 4-layer stack. As a developer you almost always work at exactly one of the top two layers:
- Building an app? You work at L4 (consume capabilities).
- Providing a capability? You write a module at L3.
L1 and L2 are the machinery underneath — they are not application-facing and you should never need to call into them directly.
flowchart TB
L4["<b>L4 — Consumer / application</b><br/>legion.h · l4_node.h · legion_ffi_*.h<br/><i>apps + bindings — the dynamic ABI (~131 legion_* exports)</i>"]
L3["<b>L3 — Capability provider</b><br/>sdk.h · legion_identity.h<br/><i>module authors — in-process header contract (not exported)</i>"]
L2["<b>L2 — Consensus · membership · identity</b><br/>consensus VM + BFT · cert-gated membership · signed beacon<br/><i>internal</i>"]
L1["<b>L1 — Transport / mesh</b><br/>LNMP · discovery · bridge tunnels · E2E encryption<br/><i>internal</i>"]
L4 --- L3 --- L2 --- L1
Each layer is built on the one below; an app at the top never reaches past L3.
| Layer | Name | Responsibility | Who uses it | Exposed as |
|---|---|---|---|---|
| L4 | Consumer / app | Discover + consume capabilities; node lifecycle; language bindings | App developers (native desktop · embedded/MCU); language bindings | Dynamic ABI — exported legion_* symbols |
| L3 | Capability provider | Declare + serve capabilities (declaring is advertising); identity/cert primitives | Module authors | In-process header contract — sdk.h, legion_identity.h (hidden from the .so) |
| L2 | Consensus / membership / identity | Cohort agreement (consensus VM + BFT), cert-gated membership, capability advertisement | Internal | — (not app-facing) |
| L1 | Transport / mesh | Peer discovery, LNMP framing, bridge tunnels (WAN), encryption | Internal | — (not app-facing) |
The layers in detail
L1 — Transport / mesh (foundation req 01)
The mesh that moves bytes between nodes: LAN multicast discovery, the LNMP
framing/wire format, WAN bridge tunnels (for class-B devices like phones behind
carrier NAT), and end-to-end encryption. Everything above L1 is oblivious to
how a peer is reached. Internal headers: legion_wire.h, legion_lnmp.h.
On the LAN, peers discover and reach each other directly (LNMP multicast). A class-B device — e.g. a phone on cellular, behind carrier NAT — can't be reached that way, so it opens an end-to-end-encrypted bridge tunnel to a WAN relay that forwards opaque frames to the cohort. The relay is untrusted: it routes bytes it cannot read, and it takes no part in consensus.
flowchart LR
subgraph lan["Home LAN — direct peers"]
D["Desktop"]
S["Server"]
K["Speaker"]
D <-->|"LNMP multicast"| S
D <--> K
S <--> K
end
P["Phone · 4G<br/><i>class-B — behind carrier NAT,<br/>no multicast</i>"]
R{{"WAN relay<br/><i>untrusted · consensus-blind<br/>forwards opaque frames</i>"}}
P <-->|"bridge tunnel · E2E encrypted"| R
R <-->|"delivers to the cohort"| S
L2 — Consensus, membership & identity (foundation req 02, 03, 07)
The agreement-and-trust layer. A cert-gated membership model (master →
auth-node → service-node certificate chains, req 07) decides who is in a cohort;
a consensus VM + BFT engine (req 02/03) give the cohort a convergent, ordered
view of shared state — including which capabilities each member advertises.
Internal header: legion_vm.h. Apps and modules never call this directly; they
see only its effects (a peer's capabilities appear; a write converges).
Membership is cert-gated: a master signs an auth-node, which admits members by signing service-node certificates. A node is in the cohort only if it presents a certificate that chains to the cohort master.
flowchart TB
M["Master<br/><i>cohort root of trust</i>"]
AN["Auth-node<br/><i>admits members</i>"]
SN["Service-node<br/><i>a device · capability provider</i>"]
M -->|signs cert| AN
AN -->|signs cert| SN
M -.->|may sign directly| SN
Identity is single-seed at the root: a member's master identity derives
from one BIP-39 seed through a SLIP-0010 HD tree (did:legion:user:…) — the
whole fleet's root of trust. Each node carries its own Ed25519 keypair and
did:legion:node:… DID (not seed-derived); the master signs it a service
certificate offline to admit it, so a node proves cohort membership without
contacting any online authority. One mnemonic backs up the root that authorises
the entire fleet.
flowchart TB
SEED["<b>One BIP-39 seed</b><br/><i>a single mnemonic</i>"]
SEED --> HD["SLIP-0010 HD derivation"]
HD --> MK["<b>Master</b> identity<br/><i>did:legion:user:… — cohort root of trust</i>"]
P["Phone<br/><i>own Ed25519 key · did:legion:node:…</i>"]
D["Desktop<br/><i>own Ed25519 key · did:legion:node:…</i>"]
K["Speaker<br/><i>own Ed25519 key · did:legion:node:…</i>"]
MK -->|signs service cert| P
MK -->|signs service cert| D
MK -->|signs service cert| K
L3 — Capability provider surface (foundation req 04, 05, 08)
What a module author writes against. A module declares the capabilities it
provides (provided_caps), implements their method handlers, and — once its
node is certified — those capabilities are automatically advertised on
consensus + the signed beacon. Declaring a capability is advertising it;
there is no separate "publish" step, and apps never advertise — advertising is
purely a provider (L3) concern, driven by a certified module's provided_caps.
A capability is a (name, semantic) pair (req 08), where the semantic is one of
a closed set (replicate, any_one, specific, follow_focus) — e.g.
ai.stt.transcribe declared any_one.
L3 lives in sdk.h (module register/dispatch, the L3 KV wrapper, caps &
membership queries) and legion_identity.h (cert/identity primitives). These
are an in-process header contract, not part of the dynamic ABI — the host
runtime/tool links the static liblegion.a with --whole-archive --export-dynamic, so the L3 symbols live in the host executable's dynamic
symbol table. A built-in module resolves them directly; a dynamically-loaded
(dlopen) module resolves them against that host executable at load time (its
init receives a legion_plugin_manager_t * handle to register against). They
stay hidden in liblegion.so, which is the L4 surface. (See abi.md.)
L4 — Consumer / application surface (foundation req 06)
What an app developer (or a language binding) works against. An app:
- manages a node's lifecycle (
legion_node_create_from_file,_start,_stop), - discovers available capabilities and consumes them via callbacks,
- and never touches consensus, BFT, or LNMP.
L4 is the dynamic ABI: the ~131 exported legion_* symbols across legion.h
(version/logging/config), l4_node.h (node lifecycle), and the legion_ffi_*.h
family (audio, bridge, connection, display, focus, identity, js, node, objectstore — plus the
legion_ffi.h binding helpers). This is the native C ABI any consumer links
against — native desktop applications, embedded / microcontroller targets, and
language bindings alike — and it is the sovereignty boundary. (The Android NDK
voice-loop app was the first proof-of-concept exercising L1–L4 end to end;
Android is one consumer, not a constraint.) Full inventory + stability status
in abi.md.
Provider & consumer — not client & server
L3 and L4 are provider and consumer roles, not a client/server split. A single process commonly wears both hats: the on-device voice-loop app provides microphone/playback capabilities (L3) and consumes remote speech-to-text, LLM, and text-to-speech capabilities (L4) — all in one process, no separate service. The mesh makes "local capability" and "a cohort peer's capability" look the same to the consumer.
How a capability flows (the layers in motion)
Provider side (a module on node B):
- The module declares
provided_capsand implements handlers — L3. - Node B's certificate authorises those caps — L2 (identity/membership).
- The caps are advertised on consensus + the signed beacon — L2.
Consumer side (an app on node A):
- The app discovers B's capability (via an L4 callback) — L4.
- It invokes the capability; the request is routed over the mesh to B — L1.
- B's L3 handler runs; the result returns to A's callback — L4.
The app at A sees only steps 1 and 3 (L4). Discovery, ordering, trust, and transport (L1/L2) are handled beneath it.
sequenceDiagram
autonumber
participant A as App · node A (L4)
participant N as Mesh · L1 / consensus L2
participant B as Module · node B (L3)
Note over N,B: B declares provided_caps — its cert authorises them,<br/>caps advertised on consensus + the signed beacon
A->>N: discover B's capability
N-->>A: available — L4 callback
A->>N: invoke capability
N->>B: route request over the mesh (L1)
B->>B: L3 handler runs
B-->>A: result → A's callback (L4)
Note over A: A sees only discover + result (both L4)
Layers vs Tiers — do not confuse them
Legion has two distinct "4-part" models, and they are orthogonal:
| The 4 layers (this doc) | The 4 tiers (specs/05_tiers.md) | |
|---|---|---|
| What it is | API abstraction stack | Data-placement / privacy policy |
| Values | L1 transport · L2 consensus · L3 provider · L4 consumer | T0 Sovereign · T1 Pragmatic · T2 Group · T3 Interface |
| Applies to | Code (which API you call) | Data & capabilities (where they may live / who may reach them) |
The tiers classify how sensitive a piece of data or a capability is and where
it may travel — Sovereign (private, air-gapped) → Pragmatic (controlled) → Group
(shared on the local cohort) → Interface (public/external). The tier travels with
data and capabilities through the API as a tier level (e.g. a method's
min_tier, a dispatch's caller_tier, a tiered L3 KV write) and is a per-node,
cert-gated policy — enforced within the layers, not as a layer of its own. The
detailed cross-tier movement policy is specified in specs/05_tiers.md.
The public API surface (what you link against)
| Surface | Headers | Linkage | Audience |
|---|---|---|---|
| L4 (dynamic ABI) | legion.h, l4_node.h, legion_ffi*.h | Exported from liblegion.so (~131 legion_* symbols) | App developers, language bindings |
| L3 (in-process) | sdk.h, legion_identity.h | Header contract; static-link or vtable-at-init (not exported) | Module authors |
| Internal (L1/L2) | legion_wire.h, legion_vm.h, legion_lnmp.h | Not installed | — |
See abi.md for the exact exported-symbol set, the enforcement stack
(LEGION_API + C_VISIBILITY_PRESET hidden + -Wl,--exclude-libs,ALL + the
header-install split), the CI gate, and stability status.
Status & how this evolves
This is a draft. The L3 and L4 surfaces are now visible precisely so they
can be reviewed and revised — names, signatures, and groupings may change before
anything is declared a release candidate. What is already locked is the export
boundary (liblegion.so exports only legion_*, CI-enforced); the specific
set of symbols is pre-release. Companion docs in this series:
abi.md (the exported ABI), l3_modules.md (writing
a capability-providing module), and l4_apps.md (building an app
that discovers + consumes capabilities).
