Expand description
Runtime for the Lex extension system.
This crate hosts the registry, schema loader, namespace URI resolver,
transport adapters (subprocess, future WASM), and trust gate that turn
a set of lex_extension::Schemas plus lex_extension::LexHandler
implementations into a dispatch fabric the lexd CLI, lex-lsp
server, lex-core (for built-in lex.* resolvers), and Rust
embedders all share.
Pre-1.0 the public API surface is unstable per Cargo convention. The
crate is published so that downstream crates in the lex toolchain —
especially lex-core, which carries the lex.include resolver as
the first built-in LexHandler — can depend on it. Handler authors
should depend on lex-extension, not this crate.
§Writing a handler — the unified registration pattern (#615)
Extension authors register one lex_extension::Schema per label,
attach the lifecycle hooks that label participates in, and provide
one lex_extension::LexHandler implementation per namespace. The
Registry routes each hook to the right method by namespace + label:
use lex_extension::{LexHandler, Format, RenderOut, WireNode};
use lex_extension::handler::HandlerError;
use lex_extension::wire::LabelCtx;
use lex_extension::schema::{HookSet, RenderHook, Schema};
use lex_extension_host::Registry;
struct AcmeHandler;
impl LexHandler for AcmeHandler {
// IR-construction lifecycle: hydrate verbatim payloads
// (`:: acme.table ::`, `:: acme.image ::`) into typed wire
// nodes the host's IR builder consumes.
fn on_ir_build(&self, ctx: &LabelCtx) -> Result<Option<WireNode>, HandlerError> {
match ctx.label.as_str() {
"acme.thing" => Ok(Some(WireNode::Verbatim { /* ... */ })),
_ => Ok(None),
}
}
// Pre-serialisation lifecycle: emit the format-specific
// representation (markdown, HTML, ...). One handler can
// participate in both IR-build and render against the same
// schema — a single registration, both lifecycles.
fn on_render(&self, ctx: &LabelCtx, fmt: Format) -> Result<Option<RenderOut>, HandlerError> {
/* ... */
Ok(None)
}
}
let registry = Registry::new();
registry.register_namespace(
"acme",
vec![Schema {
schema_version: 1,
label: "acme.thing".into(),
hooks: HookSet {
ir_build: true, // declare IR-build participation
render: vec![RenderHook::new("html")], // declare render participation
..HookSet::default()
},
/* ... rest of Schema ... */
}],
Box::new(AcmeHandler),
).expect("registration ok");§Lifecycle hooks
Three hook surfaces, each on its own lifecycle phase:
| Hook | Lifecycle phase | Dispatch entry point | Built-in example |
|---|---|---|---|
on_resolve | AST substitution | Registry::dispatch_resolve | lex.include |
on_ir_build | IR construction | Registry::dispatch_ir_build | lex.tabular.table, lex.media.* |
on_render | Pre-serialisation | Registry::dispatch_render | doc.title, doc.author, … |
on_resolve and on_ir_build have the same shape
(Result<Option<WireNode>, HandlerError>); they’re separate hooks
because they fire at different lifecycle phases and have different
consumer contracts. on_resolve returns a wire node spliced into
the host AST; on_ir_build returns a wire node consumed by the IR
builder. Pre-#615 these were a single overloaded hook
(on_resolve); the unified registry surface separates them so
extension authors can declare exactly the lifecycle phase they
participate in.
§What’s in this crate
Registry— namespace registration, label lookup, and dispatch helpers wrapping every hook event withHandlerErrorfolding and panic catch.schema::SchemaLoader— YAML schema loader + post-deserialise validator.transport::native— the trivial transport: a registeredBox<dyn LexHandler>is its own transport, no adapter required.transport::subprocess(behind thesubprocessfeature) — spawn a handler binary and dispatch over LSP-framed JSON-RPC.trust::TrustGate— decides whether a handler is allowed to run, per the β/γ-correct policy in the master tracking issue (subprocess always prompts; native trusted by linkage).sandbox::Sandbox— OS-level enforcement facade. The plumbing-PR default issandbox::NullSandbox(no enforcement,available() == false). Per-OS implementations land in follow-up PRs (12a Linux, 12b macOS, 12c Windows); the trust matrix flip (PR 12d) consumes [Sandbox::available] to auto-trust declared-pure handlers under enforced sandboxing.
Coming in later PRs:
- PR 12a/b/c: per-OS sandbox enforcement.
- PR 12d: trust matrix flip (auto-trust pure handlers under enforced sandbox).
Re-exports§
pub use registry::Registry;pub use registry::RegistryError;pub use resolve::default_fetcher_registry;pub use resolve::resolve_namespace;pub use resolve::resolve_namespace_with;pub use resolve::FetchError;pub use resolve::Fetcher;pub use resolve::FetcherRegistry;pub use resolve::ParsedUri;pub use resolve::ResolveError;pub use resolve::ResolvedNamespace;pub use resolve::ResolverCache;pub use resolve::UriParseError;pub use sandbox::NullSandbox;pub use sandbox::Sandbox;pub use sandbox::SandboxError;pub use schema::SchemaError;pub use schema::SchemaLoader;pub use trust::detect_ci_environment;pub use trust::Capability;pub use trust::Source;pub use trust::Surface;pub use trust::Transport;pub use trust::TrustDecision;pub use trust::TrustGate;pub use trust::TrustKey;pub use trust::TrustPromptContext;pub use trust::TrustPromptHandler;pub use trust::TrustStore;pub use trust::TrustStoreError;