Introduction
Lonis is an AI-native tool harness for the Anima ecosystem — a local-first, machine-readable alternative to ambient server protocols for exposing sharply bounded tool surfaces to agents.
"MCP exposes servers to models; Lonis exposes tools to agents."
Lonis is built on one conviction, inherited from its namesake's first life as a vision analyzer: never let the model guess. Where the original Lonis measured pixels so a vision model couldn't hallucinate colors, this Lonis binds tools to contracts so an agent can't misread what it can do. In both incarnations, the LLM is a pure reasoner suspended between an unreliable sensorium and an unreliable effector system — and Lonis is the well-formed I/O membrane around it.
What it is
- A contract (
lonis-schema): theBlock— a structured domain object every tool emits through, uniformly versioned, attributed, bounded, replayable, and render-parity (human and machine render from the same typed value). - A runtime (
lonis-core): theTooltrait, registries, per-mode rendering, and bounded adapters that host any composable CLI as a tool — with hard timeouts, byte caps, cleared environments, and kill-and-reap discipline. - A CLI (
lonis): the harness binary — discover tools, invoke them, emit their schemas.lonisis itself a conforming provider: it can host itself. - Macros (
lonis-derive): derives that make the contract's sharp edges compile-time guarantees instead of runtime failures. - A facade (
lonis): one crate re-exporting all of it, serde-style.
Where it sits
In the Anima doctrine's planes, Lonis is the Tools plane — the piece
that turns the other planes' domain objects into invocable verbs with
bounded surfaces. amari-discovery is the reference vertical whose
machinery Lonis generalizes (and which will eventually delete its own
protocol layer into lonis-schema). karpal-discovery is the first
external consumer.
Why not MCP?
MCP is the diplomacy layer — how Anima talks to external harnesses. Lonis is domestic tooling: Unix, not RPC. Fixed argv, JSON on stdin, blocks on stdout, structured errors on stderr, exit codes, cleared environments, byte caps, kill-and-reap on timeout. For internal tools, a bounded subprocess with a sharp contract beats a long-lived server: less context pollution, no connection lifecycle, crash isolation by construction.
Getting Started
Install
cargo install lonis-cli # the `lonis` binary
Or from source:
git clone https://github.com/Industrial-Algebra/Lonis
cd Lonis && cargo install --path crates/lonis-cli
Five minutes with the harness
# What tools are registered?
lonis tools list
# Describe one (its ToolContract)
lonis tools describe lonis:builtin:echo
# Invoke it — inline JSON, a @file, or stdin
lonis call lonis:builtin:echo '{"hello": "world"}' --mode json
Every invocation follows the same split: blocks on stdout, structured errors on stderr with a stable exit code.
[
{
"schema_version": "lonis.block/v1",
"provenance": { "tool_version": "0.1.0", "input_hash": "…" },
"attribution": {
"identity": "lonis:builtin:echo",
"provenance": { "when": "2026-08-10T12:00:00Z", "producer": "lonis:builtin:echo" }
},
"payload": { "kind": "result", "data": { "output": { "hello": "world" } } }
}
]
The normative contract
lonis schema # list all 16 schema families
lonis schema block # the envelope (all kinds + the extension seam)
lonis schema message # one kind's payload schema
Validate anything you emit against these — they're the same documents the golden fixtures validate against in CI.
As a library
[dependencies]
lonis = "0.1" # the facade: schema + derive + core
#![allow(unused)] fn main() { use lonis::{Block, Capabilities, Tool, ToolRegistry}; }
See The Block Contract next.
The Block Contract
A block is the canonical structured domain object — what every Lonis tool emits through. Defined by the Anima doctrine §2.7; decided in ADR-0001.
Six cross-cutting properties
Every block carries all six:
| Property | Where | Meaning |
|---|---|---|
| Envelope | schema_version, provenance, warnings | Versioned, self-describing frame |
| Attribution | attribution | Who (identity), under what lens (viewpoint), and when/where/producer |
| Bounded | bounds | Resource limits (max_items/max_bytes/max_length/timeout_millis) are first-class |
| Versioned | schema_version + per-kind $id | lonis.block/v1 + lonis.block/<kind>/v1 |
| Replayable | provenance hashes + seed | Canonical content hashes → deterministic replay |
| Render-parity | render_human() | Human and machine render from the same typed value — drift is structurally impossible |
The wire shape
Flat — the envelope's data is the payload, not a wrapper:
{
"schema_version": "lonis.block/v1",
"provenance": { "replay": { "replayable": true }, "input_hash": "…", "seed": 7 },
"warnings": [],
"attribution": {
"identity": "dominic",
"viewpoint": "reviewer",
"provenance": { "when": "…", "where": "session:abc", "producer": "lonis:test" }
},
"bounds": { "max_items": 64 },
"payload": { "kind": "message", "data": { "content": "hello" } }
}
Unknown top-level fields are rejected (deny_unknown_fields everywhere).
The 14 seed kinds
Three categories, plus the open seam:
- Participant-stream (7):
message,question,answer,decision,action,assumption,summary— what a transcript decomposes into. - Knowledge/definition (4):
evidence,definition,capability,intent— the "what is" set. - Process (3):
plan,result,outcome— the "how / what-happened" set.outcomecovers both structured domain results and structured errors (kind/message/details/exit_code). - Extension — any other kind tag, carried losslessly
(
Extension { kind, data }). This is how verticals and future kinds cross the wire without breaking older consumers.
The normative form
The contract's machine-checkable form is 16 curated draft-2020-12 JSON
Schemas (lonis schema), validated both directions against golden fixtures
in CI — see Schema Reference and Validation.
Typed Payloads and the Erased Seam
Decided in ADR-0002, from the karpal-discovery session's feedback.
The principle
Typing matters in-process; across a process/JSON boundary it's always JSON anyway. So maximize in-process typing, and erase only where erasure is unavoidable — a subprocess.
What it means concretely
Block is generic over its payload:
#![allow(unused)] fn main() { pub trait BlockPayload: Serialize + DeserializeOwned + Send + Sync + 'static { fn kind_name(&self) -> &str; fn schema_id(&self) -> String; fn render_human(&self) -> String; } }
A vertical defines its own payload enum — karpal-discovery's
KarpalPayload — and gets a fully-typed Block<KarpalPayload>,
Tool<KarpalPayload>, and ToolRegistry<KarpalPayload>. Zero erasure
anywhere the vertical reaches.
Erasure reappears exactly once: at the umbrella host, where blocks arrive
across a subprocess JSON channel and are parsed as SeedBlock
(Block<BlockKind>). Unknown kinds land in BlockKind::Extension
losslessly — the erased seam. Erasure is topological: on the boundary
of the system, nowhere in the interior.
The derive
#[derive(BlockPayload)] makes the seam's two easy-to-get-wrong rules
compile-time guarantees (ADR-0004):
#![allow(unused)] fn main() { #[derive(Debug, Clone, PartialEq, lonis_schema::BlockPayload)] #[lonis_payload(namespace = "karpal", render_fn = "render_search")] enum KarpalPayload { Search { query: String, results: Vec<ItemSummary> }, Ready, } // serde tag == kind_name() == "karpal.search" — from one declaration. }
- Payloads serialize adjacently tagged (
{"kind", "data"}) — an internally-tagged enum hard-fails at the seam, and the derive makes that shape automatic. - Kind tags are namespaced (
<vertical>.<kind>) so kinds never collide across verticals. render_fnkeeps a custom human render without giving up the derived wire safety.
Subprocess Tools: Bounded and Isolated
SubprocessTool hosts an arbitrary external CLI as a Lonis Tool —
the thesis made concrete. Decided in
ADR-0003.
The wire protocol
| Direction | Channel | Shape |
|---|---|---|
| In | stdin | One JSON value, then EOF |
| Success | stdout | Blocks: JSON array, ndjson lines, or a single block object |
| Failure | stderr + nonzero exit | Structured ToolError JSON, propagated verbatim |
Anything else on stderr maps to kind tool_failed with the child's exit
code. Non-block stdout maps to invalid_output (exit 9).
Bounds are first-class
Every invocation runs under:
- a hard wall-clock timeout (default 5 s),
- byte caps on stdout (default 1 MiB) and stderr (default 256 KiB).
Exceeding either kills and reaps the child and reports
LIMIT_EXCEEDED (exit 7).
Isolation
Following the amari-discovery probe blueprint: direct exec (no shell), a
cleared environment (only PATH inherited, plus explicit additions),
and a neutral working directory (the system temp dir, configurable).
Consequence for tool authors: targets arrive via stdin input or argv — never env or cwd.
Availability
A tri-state probe (Ready / Missing / NotExecutable + reason) — a tool
the harness knows about may still be absent on this host. invoke on an
unavailable tool fails without spawning.
Legacy CLIs
StdoutMapping::Text wraps raw stdout in an attributed result block with
a pinned input_hash — grep/jq-style tools work today; prefer real blocks
for anything you control.
Legacy text wrap
See Authoring Tools for the Subprocess Seam for the full protocol guide, including streaming (ndjson + flush) and the provider surface.
The Provider Model
One executable hosting many tools. Decided in ADR-0006.
The problem it solves
SubprocessTool binds one argv prefix to one tool. A vertical with a full
operation set (search / detail / inspect / recommend / …) shouldn't
register each one individually — the executable itself should describe its
surface.
The four subcommands
mytool --mode json manifest # {"name", "version", "tools": [...], ...}
mytool --mode json tools list # {"provider", "tools": [{"name", "description"}]}
mytool --mode json tools describe <name> # a ToolContract JSON
mytool --mode json call <name> # ADR-0003 invocation (stdin JSON → blocks)
SubprocessProvider discovers that surface and constructs a
SubprocessTool per operation — with provider-wide bounds (timeout, byte
caps, env, cwd) inherited by every tool.
Details that matter
- Forward-compatible: manifests tolerate unknown fields — a host never rejects a newer provider for new metadata.
- Name mangling: dotted v0 names (
figma.get_document) become namespacedToolIds (figma:get_document). - The same bounded, isolated execution core as
SubprocessToolruns discovery and invocation.
lonis is a conforming provider
lonis manifest
lonis --mode json tools list
The harness hosting itself through the seam is the dogfood — and the integration test.
Stream Mode
Decided in ADR-0009.
The principle
Async is a property of the session orchestrator, not of the tool boundary. A subprocess is bytes on a pipe — synchronous by physics. Wallace-class hosts are async by nature, but that's the host layer. So Lonis streams synchronously and bridges at the seam.
The shape
BlockStream<P>— a pull iterator ofResult<Block<P>, ToolError>.Tool::invoke_streamdefaults to collect-then-stream (object-safe: noasync fnin the trait). A terminal failure is the stream's final item; blocks delivered before it are kept.SubprocessToolstreams for real: ndjson stdout lines become blocks as they arrive; the supervisor enforces timeout and byte caps concurrently with delivery; stderr drains bounded for the terminal error mapping.- Backpressure is real: a bounded channel means a slow consumer backs up the pipe, which throttles the child.
- Dropping the stream kills the child — an abandoned stream never leaves a running process.
Async hosts
BlockStream::into_async() (feature futures) yields a runtime-agnostic
futures_core::Stream. The library never names a runtime; tokio is the
host's choice.
For tool authors
Emit one block per line (ndjson) and flush after each line —
language runtimes block-buffer stdout on pipes (Rust's println!
included), so without an explicit flush your blocks arrive in one burst at
exit.
CLI
lonis call <tool> '<input>' --stream --mode ndjson
ndjson/human render incrementally; --mode json buffers to one array (a
valid JSON document can't be emitted incrementally).
Replay and Content Hashing
Decided in ADR-0001 §5 (content hashing), ADR-0007 (canonicalization policy), and ADR-0008 (verification).
Pins
Blocks carry replay provenance: typed hash slots (project_hash,
input_hash, plan_hash, result_hash), an optional seed, and a
replay { replayable, required_hashes, reasons } declaration.
Block::content_hash() is SHA-256 over the canonical payload JSON:
object keys recursively sorted, and numbers normalized — integral floats
collapse to integers (100.0, 1e2 → 100), negative zero to zero.
Semantically equal payloads hash identically across producers and
languages.
Residual limitation: exotic float spellings (
0.30000000000000004vs0.3) still hash differently. Hash-critical values should be integers or strings when cross-producer equality matters.
Verification
verify_replay(block, observed) -> ReplayStatus adjudicates a block's pins
against the hashes observed now:
NotReplayable { reasons }— the producer declared it (e.g. environment-specific state);Replayable— every required hash is present and equal;Failed { missing, mismatches }— one combined report; unknown required field names fail closed (a v2 hash field can't silently pass an older consumer).
Recomputing the observed hashes (re-inspecting a project, re-canonicalizing
an input with json_content_hash) is the vertical's business; the
horizontal contract adjudicates equality.
Golden pins
The wire shape and the canonicalization are pinned by checked-in golden
instances and hashes (tests/golden/blocks/) — a drift breaks CI, not a
consumer. See Schema Reference and Validation.
The lonis CLI
lonis [--mode human|json|ndjson] <command>
Commands
| Command | Purpose |
|---|---|
lonis tools list | Registered tool ids and versions (--mode json → provider-list shape) |
lonis tools describe <id> | A tool's ToolContract (JSON) |
lonis call <id> [input] [--stream] | Invoke a tool |
lonis schema [kind] | Emit the curated JSON Schemas (lonis schema lists all 16) |
lonis manifest | The provider manifest — lonis is a conforming provider |
Input
call accepts three input forms, all explicit:
- inline JSON:
lonis call lonis:builtin:echo '{"a": 1}' - a file:
lonis call lonis:builtin:echo @input.json - stdin (when the argument is omitted)
The amari split
- stdout: blocks (a JSON array, one block per ndjson line, or human render) — always parseable.
- stderr: a structured
ToolError({"kind", "message", "details?", "exit_code"}) — with the process exit code set from it.
Exit-code baseline: 0 ok, 1 generic, 2 invalid input, 3 not found,
4 confirmation required, 5 rate limited, 6 tool failed, 7 limit
exceeded, 8 io, 9 serialization, 69 not implemented, 70 internal.
Tools extend this map via Capabilities::exit_code_map.
Streaming
lonis call <id> <input> --stream --mode ndjson renders blocks as the tool
produces them (see Stream Mode).
Authoring Tools for the Subprocess Seam
The full guide lives in the repository at
docs/guides/subprocess-tool-authoring.md
— this page is the summary.
The wire protocol
- in: one JSON value on stdin, then EOF
- out (success): blocks on stdout (array / ndjson / single object)
- out (failure): structured
ToolErroron stderr + nonzero exit
The rules
- Payloads serialize adjacently tagged (
{"kind", "data"}). Internal tagging hard-fails at the seam. In Rust, use#[derive(BlockPayload)]; in other languages, emit the two-key object. - Kind tags are namespaced (
<vertical>.<kind>) and equal tokind_name()— one declaration, no divergence. - The minimal block requires
schema_version,attribution(withprovenance.whenRFC 3339 +provenance.producer), andpayload. Unknown top-level fields are rejected. Validate againstlonis schema block. - Targets arrive via stdin or argv — never env or cwd. The environment is cleared and the cwd neutral by design.
- You are bounded — hard timeout, byte caps; exceeding either kills your process. Keep payloads small; stream incrementally.
- Streaming: one block per line (ndjson), and flush after each line — runtimes block-buffer stdout on pipes.
Hosting many tools
Ship one executable with the provider surface (manifest / tools list /
tools describe / call) — the host discovers your whole operation set.
See The Provider Model.
Depending on Lonis pre-1.0-era
Since v0.1.0 the crates are on crates.io: lonis-schema = "0.1". Before
publication, the discipline was git deps pinned to a rev, optional behind a
feature — the pattern is preserved in the full guide for pre-release
branches.
Feature Flags
Features are additive only — enabling a feature never removes API.
lonis-schema
| Feature | Effect |
|---|---|
derive | Re-exports the LonisCapabilities and BlockPayload derives from lonis-derive (serde-style) |
lonis-core
| Feature | Effect |
|---|---|
futures | BlockStream::into_async() — a runtime-agnostic futures_core::Stream bridge (adds futures-core + futures-channel only; never a runtime) |
lonis (facade)
| Feature | Default | Effect |
|---|---|---|
core | ✓ | Re-export the harness runtime (Tool, ToolRegistry, SubprocessTool, …) |
derive | Re-export the derives | |
futures | Propagates lonis-core/futures |
Schema Reference and Validation
The block contract's normative, machine-checkable form is 16 curated
draft-2020-12 JSON Schemas, checked into
crates/lonis-schema/schemas/
and emitted by the CLI.
lonis schema # catalog: 16 families with their $ids
lonis schema block # the envelope (payload = oneOf 14 kinds + extension)
lonis schema message # one kind's payload schema
lonis schema extension # the erased seam itself
Conventions
$ids mirrorBlockKind::schema_id():https://industrialalgebra.com/schemas/lonis.block/<kind>/v1additionalProperties: falsethroughout — mirrors serde'sdeny_unknown_fieldsmaxItems/maxLengthbounds on every collection and string — the doctrine's "bounded" property expressed in the wire contract itself- The
extensionbranch admits any kind tag not in the seed enum, so vertical payloads validate against the envelope (structure here; data shape against the vertical's own schema)
The two-way pin
Schemas and golden fixtures validate each other in CI
(crates/lonis-schema/tests/schemas.rs):
- golden block instances (one per kind, produced from real Rust values)
parse as
SeedBlockand re-serialize identically — the wire-shape pin; - the same instances validate against the envelope and their kind schema — serde↔schema consistency;
- each block's
content_hashis pinned inhashes.json— the canonicalization pin.
Regenerating
# After an intentional per-kind schema change:
cargo run -p lonis-schema --example compose_block_schema
# After an intentional wire-shape change:
cargo run -p lonis-schema --example dump_golden_blocks
Both leave an auditable diff that a reviewer can judge.
Blocks and Payloads
From lonis-schema (re-exported by the lonis facade).
Block<P: BlockPayload>
The canonical structured domain object. Flat wire shape:
schema_version, provenance, warnings, attribution, bounds,
payload. Key methods:
Block::new(attribution, payload)— at the v1 contractwith_provenance/with_warnings/with_bounds— buildersschema_id()— the payload kind's stable$idcontent_hash()— SHA-256 over canonical payload JSONrender_human()— render-parity human form
BlockPayload
The trait verticals implement (usually via the derive):
#![allow(unused)] fn main() { fn kind_name(&self) -> &str; fn schema_id(&self) -> String; fn render_human(&self) -> String; }
BlockKind / SeedBlock
The 14-kind seed payload enum (implementing BlockPayload) plus the
Extension { kind, data } catch-all. SeedBlock = Block<BlockKind> is the
umbrella host's type.
Supporting types
Attribution { identity, viewpoint, provenance: AttributionSource { when, where, producer } }—Attribution::new(identity, producer)stamps RFC 3339 UTC.BlockBounds { max_items, max_bytes, max_length, timeout_millis }— all optional; a default (unbounded) set is omitted from the wire.ReplayProvenance— tool version, compatibility, replay metadata, the four typed hash slots, seed.verify_replay(block, observed) -> ReplayStatus— replay verification (see Replay and Content Hashing).json_content_hash(&Value)— canonical hashing for arbitrary JSON (e.g. pinninginput_hash).
Errors and identifiers
ToolError { kind, message, details?, exit_code } ·
ToolId(<tool>:<namespace>:<item>) · SchemaVersion(<name>/v<N>) ·
exit_code baseline constants.
Tool and ToolRegistry
From lonis-core.
Capabilities
Self-description — every tool implements it:
#![allow(unused)] fn main() { fn schema_version(&self) -> SchemaVersion; fn tool_version(&self) -> &str; fn output_formats(&self) -> &'static [OutputMode]; fn exit_code_map(&self) -> &'static [(&'static str, u8)]; fn tool_id(&self) -> ToolId; }
Usually derived: #[derive(LonisCapabilities)] with
#[lonis(tool_id = "...")].
Tool<P: BlockPayload>
#![allow(unused)] fn main() { fn invoke(&self, input: Value) -> Result<Vec<Block<P>>, ToolError>; fn invoke_stream(&self, input: Value) -> Result<BlockStream<P>, ToolError> { /* default */ } fn contract(&self) -> Option<ToolContract> { None } }
invoke_stream defaults to collect-then-stream; tools with genuine
incremental output override it.
ToolRegistry<P>
In-process registry, homogeneous in the payload type (a vertical's registry is fully typed). Deterministic order (sorted by id).
register(Box<dyn Tool<P>>)—already_registeredon duplicatesget(id)/iter()/len()/is_empty()invoke(id, input)/invoke_stream(id, input)—not_found(exit 3) on unknown ids
Rendering
render(&[Block<P>], mode, writer)— json array / ndjson lines / humanrender_error(&ToolError, mode, writer)run_tool(registry, id, input, mode) -> u8— the amari split: blocks to stdout, structured error to stderr with its exit coderun_stream(registry, id, input, mode) -> u8— the streaming variant
SubprocessTool and SubprocessProvider
From lonis-core.
SubprocessTool
Hosts one external CLI as one Tool<BlockKind>.
#![allow(unused)] fn main() { SubprocessTool::new(tool_id, command) .with_args(args) .with_timeout_millis(5_000) .with_max_stdout_bytes(1_048_576) .with_max_stderr_bytes(262_144) .with_cwd(path) .with_env(vars) .with_mapping(StdoutMapping::Blocks /* or Text */) .with_description(..).with_version(..) }
availability() -> Availability—Ready/Missing/NotExecutableinvoke— stdin JSON in; blocks orToolErrorout; bounded and isolated (see Subprocess Tools)invoke_stream— ndjson lines become blocks as they arrive (see Stream Mode)contract()— aToolContract(determinismNondeterministic, side effectsMutatesExternalby default)
SubprocessProvider
Discovers and hosts a whole surface from one executable.
#![allow(unused)] fn main() { let provider = SubprocessProvider::new("mytool"); let manifest = provider.manifest()?; // ProviderManifest let tools = provider.tools()?; // Vec<ProviderToolSummary> let contract = provider.describe("op")?; // ToolContract let tool = provider.tool("op"); // SubprocessTool (argv: call op) }
Discovery args default to the v0 surface with --mode json and are
overridable (with_manifest_args, with_tools_list_args); bounds set on
the provider are inherited by constructed tools. Dotted names mangle to
namespaced ToolIds.
Derive Macros
From lonis-derive, re-exported by lonis-schema behind its derive
feature (and by the facade behind derive).
#[derive(LonisCapabilities)]
Generates the five-method Capabilities impl from one attribute:
#![allow(unused)] fn main() { #[derive(LonisCapabilities)] #[lonis(tool_id = "amari:discovery:search")] struct SearchTool; }
Defaults: current protocol SchemaVersion, all three output modes, the
baseline exit-code map, CARGO_PKG_VERSION.
#[derive(BlockPayload)]
Generates, from one enum declaration (ADR-0004):
- adjacently-tagged
{"kind", "data"}serde impls, kind_name()— snake_case variant names, dot-namespaced via#[lonis_payload(namespace = "karpal")]→karpal.search,schema_id()—lonis.block/<kind>/v1,render_human()— a<kind>: <Debug>default, or a hook:render_fn = "path::to::render"(fn(&Self) -> String).
Supports struct variants (named fields) and unit variants (data: null).
Tuple variants and generics are compile errors. Unknown kinds are rejected
on the vertical's own enum (closed universe); cross-vertical tolerance
lives host-side in BlockKind::Extension.
Consumers need serde and serde_json as dependencies; the enum must
implement Debug when using the default render.
Architecture Decision Records
The ADRs are the case law of this workspace — every structural decision is
recorded with its context and consequences. They live in
docs/adr/:
| ADR | Decision |
|---|---|
| 0001 | The block contract; invoke → Vec<Block>; extraction direction |
| 0002 | Block<P: BlockPayload>; erasure is topological |
| 0003 | The subprocess wire protocol (bounded, isolated) |
| 0004 | #[derive(BlockPayload)] + the authoring guide |
| 0005 | Curated JSON Schemas + golden fixtures (with the issue #10 erratum) |
| 0006 | The provider surface; lonis-as-provider |
| 0007 | Content-hash canonicalization (number normalization) |
| 0008 | verify_replay — replay verification helper |
| 0009 | Stream mode: sync pull core, async at the host |
The upstream constitution is the Anima Ecosystem Doctrine §2.7 (block taxonomy) and §3 (Lonis as the Tools plane).
Changelog
All notable changes to the Lonis workspace are documented here.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
[0.1.0] — Unreleased
First public release of the Lonis workspace: an AI-native tool harness for the Anima ecosystem. "MCP exposes servers to models; Lonis exposes tools to agents."
The contract (lonis-schema)
Block<P: BlockPayload>— the doctrine §2.7 structured domain object: flat wire shape (schema_version,provenance,warnings,attribution,bounds,payload),deny_unknown_fieldsthroughout (ADR-0001/0002)- The 14-kind seed corpus (
BlockKind) in three categories with a losslessExtensionseam; verticals implementBlockPayloadfor fully-typed in-process contracts - Full
Attribution(identity / viewpoint / when / where / producer), first-classBlockBounds,ReplayProvenance(a superset of amari-discovery's provenance, for the future extraction), andverify_replay(ADR-0008) - Canonical content hashing (SHA-256, key-sorted + number-normalized per
ADR-0007) and render-parity (
render_humanfrom the same typed value) - 16 curated draft-2020-12 JSON Schemas (14 kinds + envelope + extension seam) with golden wire fixtures and pinned hashes (ADR-0005)
- Namespaced-string
SchemaVersion(<name>/v<N>) and the amari-aligned exit-code vocabulary
The runtime (lonis-core)
Tool<P>/ToolRegistry<P>— generic over the typed payload; a vertical's registry is homogeneous and fully typed (ADR-0002)SubprocessTool— bounded, isolated external CLI adapter: stdin JSON in, blocks out, structuredToolErroron stderr; hard timeout + byte caps kill and reap; availability tri-state (ADR-0003)SubprocessProvider— one executable hosting many tools viamanifest/tools list/tools describe/call;lonisitself is a conforming provider (ADR-0006)- Stream mode:
BlockStream<P>pull iterator with real backpressure (bounded channel), drop-kills-child, and a runtime-agnosticfutures_core::Streambridge behind thefuturesfeature (ADR-0009)
The macros (lonis-derive)
#[derive(LonisCapabilities)]from#[lonis(tool_id = "...")]#[derive(BlockPayload)]— adjacently-tagged wire serde, namespaced kind tags,schema_id, and arender_fnhook, from one declaration (ADR-0004)
The CLI (lonis-cli)
lonis tools list|describe,lonis call <id> [input | @file] [--stream],lonis schema [kind],lonis manifest;--mode human|json|ndjson
The facade (lonis)
- One crate re-exporting schema + derive + core, serde-style (
coredefault;derive,futuresopt-in)