Legion L4 — Application / Consumer SDK (building apps)
Status: DRAFT (Foundation 09). The first visible cut of the L4 application API. A draft, not a release candidate — names and signatures may change. Start with
architecture.md(the layer model) andabi.md(the exported ABI). The L3 side — providing a capability — is inl3_modules.md.
What L4 is
L4 is the consumer surface: you build an app that runs a Legion node,
discovers capabilities the cohort advertises, and consumes them — calling
a remote ai.stt.transcribe, a peer's display.gui, a storage cap, etc. Apps
never advertise capabilities (that's L3); they consume.
L4 is the dynamic ABI — the ~131 exported legion_* symbols. It is the
native-C surface any consumer links against: native desktop apps, embedded /
microcontroller targets, and language bindings alike (the Android NDK voice-loop
app was the first proof-of-concept, not a constraint). See abi.md.
#include <legion/legion.h> /* umbrella — pulls the whole L4 surface */
legion.h transitively includes the library base, l4_node.h, the
legion_ffi_*.h family, and (for convenience) sdk.h.
Two flavors of the L4 surface
L4 exposes the same node two ways; pick by how you're building:
Native C API (l4_node.h) | Binding / FFI API (legion_ffi_*.h) | |
|---|---|---|
| Handle | typed legion_node_t * | opaque intptr_t |
| Create | legion_node_create_from_file(<JSON config>) | legion_ffi_node_start(node_id, cohort, master_pk, seed, cert, cert_len, multicast) |
| Consume a cap | via the plugin manager + L3 dispatch (legion_node_get_plugins → legion_module_dispatch_to_cap_async, see l3_modules.md) | self-contained legion_ffi_call_cap(...) |
| Best for | native apps that want typed pointers + config files | language bindings, embedding, simple consumers |
Both are L4 (both exported). Don't mix handles between the two. The worked example below uses the FFI flavor because it's the most self-contained and is exactly what the voice-loop app does.
Library base (legion.h)
| Function | Purpose |
|---|---|
legion_version / _version_major / _minor / _patch | Version string + components. |
legion_build_id | Git hash + build metadata. |
legion_free(ptr) | Free a buffer the library allocated and handed back. |
legion_log_set_callback(fn, ud) / legion_log_set_level(lvl) | Logging sink (level, component, msg, ud) + minimum level. |
legion_config_find(cli_path, …) | Resolve a config file: CLI path → $LEGION_CONFIG → ./ → user dir → system dir. |
Node lifecycle
Native (l4_node.h): legion_node_create_from_file (read a JSON config →
legion_node_t *) → legion_node_start → … → legion_node_stop →
legion_node_destroy. Plus legion_node_reload and read-only getters:
get_node_id, get_pk_hex, get_epoch, get_cohort_name, get_peers,
get_plugins, format_status (a human-readable status report).
FFI (legion_ffi_node.h): legion_ffi_node_start(node_id, cohort_name, master_pk_hex, seed_hex, service_cert, service_cert_len, multicast_enabled)
returns an intptr_t handle (0 on failure) with discovery / heartbeat /
transport threads already running; legion_ffi_node_stop(handle);
legion_ffi_node_is_running(handle). All FFI calls are thread-safe. The
member-enrolment params anchor the cohort for a member device: master_pk_hex +
seed_hex identify the cohort, and service_cert / _len is an optional
master-issued member certificate (pass NULL / 0 for a self-master node).
Worked example — discover + consume a remote capability
This is the pattern the voice-loop app uses to drive a remote ai.stt.transcribe
provider, using the real, verified legion_ffi_node.h surface.
#include <legion/legion.h>
#include <string.h>
#include <stdio.h>
/* 1. Start an in-process node (discovery + transport threads run in the bg).
* master_pk_hex + seed_hex anchor the cohort for a member device; the cert
* is an optional master-issued member cert (NULL/0 for a self-master node). */
intptr_t node = legion_ffi_node_start("my-app", "personal",
/*master_pk_hex=*/ NULL, /*seed_hex=*/ NULL,
/*service_cert=*/ NULL, /*service_cert_len=*/ 0,
/*multicast=*/ 1);
if (!node) return 1;
/* 2. DISCOVER — what peers are visible, and what caps do they advertise?
* JSON: [{"node_id":"...","did":"...","caps":["ai.stt.transcribe"],"trust":0.9}, ...] */
char peers[8192];
if (legion_ffi_node_get_peers_json(node, peers, sizeof(peers)) > 0)
printf("peers: %s\n", peers);
/* 3. (LEGACY FALLBACK — usually NOT needed.) A full cohort member catches up
* via consensus sync (cert-validated discovery) and resolves any_one caps on
* its own, so skip this. Only a device that can't converge on consensus needs
* to pin a module's delegation to a provider discovered in step 2. */
/* legion_ffi_node_delegate_pin(node, "stt", "balrog-ai-node"); */
/* 4. CONSUME — call `transcribe` on ANY live provider of `ai.stt.transcribe`
* (fungible any_one delegation). The single binary param carries the audio. */
uint8_t out[64 * 1024];
size_t out_len = 0;
int rc = legion_ffi_call_cap(node,
/*cap=*/ "ai.stt.transcribe",
/*module=*/ "stt",
/*method=*/ "transcribe",
/*param_key=*/ "audio",
/*param_data=*/ pcm_bytes, pcm_len,
/*timeout_ms=*/ 15000,
out, sizeof(out), &out_len);
if (rc == LEGION_OK) {
fwrite(out, 1, out_len, stdout); /* the transcript */
} else if (rc == LEGION_ERR_NOMEM && out_len > sizeof(out)) {
/* out_len is the FULL result size — grow the buffer and retry. */
} else if (rc == LEGION_ERR_NO_DELEGATE) {
/* No live provider of the cap (loud, never silently swallowed). */
}
/* 5. Done. */
legion_ffi_node_stop(node);
Notes:
legion_ffi_call_captargets any provider of the cap (any_onesemantic);*out_lenis always set to the full result size, soLEGION_ERR_NOMEMwith*out_len > out_capmeans "grow and retry," andLEGION_ERR_NO_DELEGATEmeans no provider is live.legion_ffi_node_delegate_pinis a legacy fallback, not the current class-B path: a class-B device now catches up via consensus sync (cert-validated discovery), so a full member resolvesany_onecaps without pinning. It still exists for a device that can't converge on consensus — pin the call to a peer discovered via the beacon — but a full cohort member should skip it.
Reference
Node + consume (legion_ffi_node.h, 24 fns)
| Group | Functions |
|---|---|
| Lifecycle | legion_ffi_node_start, _stop, _is_running |
| Discovery | legion_ffi_node_peer_count, _get_peers_json |
| Caps introspection | legion_ffi_node_provided_caps (enumerate the caps THIS build would provide, without starting a node — so a joining device can declare its own caps) |
| Consume a cap | legion_ffi_call_cap (any provider), legion_ffi_node_delegate / _delegate_async (a specific peer), legion_ffi_node_delegate_pin (legacy fallback — pin to a discovered peer; superseded by consensus-sync catch-up) |
| Liveness | legion_ffi_node_ping_peer_start / _poll / _cancel (non-blocking) |
| Byte streams | legion_ffi_node_stream_open / _status / _write / _close |
| Events | legion_ffi_node_poll_events (drain focus/content events — poll, not callback) |
| OS process | legion_ffi_osp_activate (ask a peer to launch a signed app) |
| Transport | legion_ffi_node_attach_mobile_bridge (class-B WAN tunnel), legion_ffi_node_network_changed (signal an OS network change to drive reconnect — Spec 34), legion_ffi_node_transport_status (poll mesh link-state — Spec 34) |
| Routing | legion_ffi_node_route_query (resolve a multi-hop route to a target node before delegating — Spec 33.04) |
| Storage | legion_ffi_node_enable_persistent_store (turn the objectstore file-backed) |
FFI subsystem families
Each FFI header bridges one subsystem to apps/bindings (opaque handles, explicit memory ownership). Consume what your app needs:
| Header | Fns | What it's for |
|---|---|---|
legion_ffi_audio.h | 12 | Mic capture + playback: audio_in_start/_stop/_get_utterance/_poll_events, audio_out_play/_poll_events. |
legion_ffi_focus.h | 4 | Follow-focus targeting: focus_get/_set/_set_for/_clear (which node a follow_focus cap routes to). |
legion_ffi_display.h | 11 | GUI surface: display_create/_destroy/_get_html/_open_shell/_pop_event/_pop_handoff. |
legion_ffi_identity.h | 17 | Keys + certs: generate_seed, derive_did, derive_master_pk, did_to_pk_hex, issue_auth_cert, issue_service_cert; plus device enrolment — enroll_request_build / _open (a joining device self-signs an enrolment request with proof-of-possession; the master verifies it before issuing a cert — channel-agnostic, QR or file). |
legion_ffi_js.h | 11 | JS app runtime: js_create/_destroy/_eval/_eval_string/_deliver_event. |
legion_ffi_objectstore.h | 13 | Content store: objectstore_create/_get/_get_bytes/_fetch_bytes/_list/_delete/_gc_cid. |
legion_ffi_bridge.h | 8 | Class-B WAN transport: mobile_bridge_connect/_connect_with_seed/_send/_recv/_add_route/_close/_is_connected. |
legion_ffi_connection.h | 1 | Connection hints: connection_manager_resume_hint. |
FFI helpers (legion_ffi.h)
legion_result_t + legion_result_free; the params builder
legion_params_create/_destroy/_set_string/_set_int/_set_bytes (for languages
without native maps); legion_string_len.
Key rules & gotchas
- Apps consume, never advertise. Advertising is purely a provider (L3)
concern — a certified module declaring
provided_caps. The L4 surface has no advertise call. - Three ways to invoke:
legion_ffi_call_cap→ any live provider (any_one);legion_ffi_node_delegate→ a named peer;legion_ffi_node_delegate_pin→ pin a module to a discovered peer (legacy fallback — superseded by consensus-sync catch-up; a full member needn't pin).LEGION_ERR_NO_DELEGATEis the loud "no provider" signal. - Events are polled, not pushed.
legion_ffi_node_poll_events,legion_ffi_audio_*_poll_events, etc. return pending events on your cadence — there is no C callback fired into your runtime (a deliberate choice; a blocking callback once stalled a cellular isolate). - Result buffers:
*out_lenis the full size;LEGION_ERR_NOMEMmeans grow and retry. Free library-allocated buffers withlegion_free/legion_pal->free, never libcfree. - Don't mix flavors: a
legion_node_t *(native) and anintptr_t(FFI) handle are not interchangeable. - Native apps invoke caps via the plugin manager (
legion_node_get_plugins- L3 dispatch) — see
l3_modules.md.
- L3 dispatch) — see
Status
DRAFT — surfaced for review; expect changes. With this, the API-doc series is
architecture.md → abi.md →
l3_modules.md → this. Next phase: build real Legion apps on
this surface.
Note: the
examples/*.ctree predates this L4 surface and needs a refresh to match it; treat the docs above (not those examples) as canonical for now.
