lex_extension_host/lib.rs
1//! Runtime for the Lex extension system.
2//!
3//! This crate hosts the registry, schema loader, namespace URI resolver,
4//! transport adapters (subprocess, future WASM), and trust gate that turn
5//! a set of [`lex_extension::Schema`]s plus [`lex_extension::LexHandler`]
6//! implementations into a dispatch fabric the `lexd` CLI, `lex-lsp`
7//! server, `lex-core` (for built-in `lex.*` resolvers), and Rust
8//! embedders all share.
9//!
10//! Pre-1.0 the public API surface is unstable per Cargo convention. The
11//! crate is published so that downstream crates in the lex toolchain —
12//! especially `lex-core`, which carries the `lex.include` resolver as
13//! the first built-in `LexHandler` — can depend on it. Handler authors
14//! should depend on `lex-extension`, not this crate.
15//!
16//! # Writing a handler — the unified registration pattern (#615)
17//!
18//! Extension authors register one [`lex_extension::Schema`] per label,
19//! attach the lifecycle hooks that label participates in, and provide
20//! one [`lex_extension::LexHandler`] implementation per namespace. The
21//! `Registry` routes each hook to the right method by namespace + label:
22//!
23//! ```ignore
24//! use lex_extension::{LexHandler, Format, RenderOut, WireNode};
25//! use lex_extension::handler::HandlerError;
26//! use lex_extension::wire::LabelCtx;
27//! use lex_extension::schema::{HookSet, RenderHook, Schema};
28//! use lex_extension_host::Registry;
29//!
30//! struct AcmeHandler;
31//! impl LexHandler for AcmeHandler {
32//! // IR-construction lifecycle: hydrate verbatim payloads
33//! // (`:: acme.table ::`, `:: acme.image ::`) into typed wire
34//! // nodes the host's IR builder consumes.
35//! fn on_ir_build(&self, ctx: &LabelCtx) -> Result<Option<WireNode>, HandlerError> {
36//! match ctx.label.as_str() {
37//! "acme.thing" => Ok(Some(WireNode::Verbatim { /* ... */ })),
38//! _ => Ok(None),
39//! }
40//! }
41//! // Pre-serialisation lifecycle: emit the format-specific
42//! // representation (markdown, HTML, ...). One handler can
43//! // participate in both IR-build and render against the same
44//! // schema — a single registration, both lifecycles.
45//! fn on_render(&self, ctx: &LabelCtx, fmt: Format) -> Result<Option<RenderOut>, HandlerError> {
46//! /* ... */
47//! Ok(None)
48//! }
49//! }
50//!
51//! let registry = Registry::new();
52//! registry.register_namespace(
53//! "acme",
54//! vec![Schema {
55//! schema_version: 1,
56//! label: "acme.thing".into(),
57//! hooks: HookSet {
58//! ir_build: true, // declare IR-build participation
59//! render: vec![RenderHook::new("html")], // declare render participation
60//! ..HookSet::default()
61//! },
62//! /* ... rest of Schema ... */
63//! # description: None, params: Default::default(), attaches_to: vec![],
64//! # body: Default::default(), verbatim_label: false,
65//! # capabilities: Default::default(), handler: None, diagnostics: Vec::new(),
66//! }],
67//! Box::new(AcmeHandler),
68//! ).expect("registration ok");
69//! ```
70//!
71//! ## Lifecycle hooks
72//!
73//! Three hook surfaces, each on its own lifecycle phase:
74//!
75//! | Hook | Lifecycle phase | Dispatch entry point | Built-in example |
76//! |-----------------|-----------------------------|-------------------------------|-----------------------|
77//! | `on_resolve` | AST substitution | [`Registry::dispatch_resolve`]| `lex.include` |
78//! | `on_ir_build` | IR construction | [`Registry::dispatch_ir_build`]| `lex.tabular.table`, `lex.media.*` |
79//! | `on_render` | Pre-serialisation | [`Registry::dispatch_render`] | `doc.title`, `doc.author`, ... |
80//!
81//! `on_resolve` and `on_ir_build` have the same shape
82//! (`Result<Option<WireNode>, HandlerError>`); they're separate hooks
83//! because they fire at different lifecycle phases and have different
84//! consumer contracts. `on_resolve` returns a wire node spliced into
85//! the host AST; `on_ir_build` returns a wire node consumed by the IR
86//! builder. Pre-#615 these were a single overloaded hook
87//! (`on_resolve`); the unified registry surface separates them so
88//! extension authors can declare exactly the lifecycle phase they
89//! participate in.
90//!
91//! # What's in this crate
92//!
93//! - [`Registry`] — namespace registration, label lookup, and dispatch
94//! helpers wrapping every hook event with `HandlerError` folding and
95//! panic catch.
96//! - [`schema::SchemaLoader`] — YAML schema loader + post-deserialise
97//! validator.
98//! - [`transport::native`] — the trivial transport: a registered
99//! `Box<dyn LexHandler>` is its own transport, no adapter required.
100//! - [`transport::subprocess`] (behind the `subprocess` feature) —
101//! spawn a handler binary and dispatch over LSP-framed JSON-RPC.
102//! - [`trust::TrustGate`] — decides whether a handler is allowed to
103//! run, per the β/γ-correct policy in the master tracking issue
104//! (subprocess always prompts; native trusted by linkage).
105//! - [`sandbox::Sandbox`] — OS-level enforcement facade. The
106//! plumbing-PR default is [`sandbox::NullSandbox`] (no
107//! enforcement, `available() == false`). Per-OS implementations
108//! land in follow-up PRs (12a Linux, 12b macOS, 12c Windows); the
109//! trust matrix flip (PR 12d) consumes [`Sandbox::available`] to
110//! auto-trust declared-pure handlers under enforced sandboxing.
111//!
112//! Coming in later PRs:
113//!
114//! - PR 12a/b/c: per-OS sandbox enforcement.
115//! - PR 12d: trust matrix flip (auto-trust pure handlers under
116//! enforced sandbox).
117
118pub mod registry;
119pub mod resolve;
120pub mod sandbox;
121pub mod schema;
122pub mod transport;
123pub mod trust;
124
125pub use registry::{Registry, RegistryError};
126pub use resolve::{
127 default_fetcher_registry, resolve_namespace, resolve_namespace_with, FetchError, Fetcher,
128 FetcherRegistry, ParsedUri, ResolveError, ResolvedNamespace, ResolverCache, UriParseError,
129};
130pub use sandbox::{NullSandbox, Sandbox, SandboxError};
131pub use schema::{SchemaError, SchemaLoader};
132pub use trust::{
133 detect_ci_environment, Capability, Source, Surface, Transport, TrustDecision, TrustGate,
134 TrustKey, TrustPromptContext, TrustPromptHandler, TrustStore, TrustStoreError,
135};