Quirl0.1 RC
Extensions

Plugin platform v0.1

Canonical Quirl project documentation synced from docs/plugin-platform.md.

Quirl's first platform contract installs local plugin packages under an explicit permission lock. The Phase 3 platform slice is complete: managed trusted Lua runs under exact grants, out-of-process adapters execute a narrow bounded initialization handshake, and Wasm packages are validated but remain disabled until a production component runtime is selected.

Lifecycle

quirl plugin add ./my-plugin --allow commands.register
quirl plugin permissions my-plugin
quirl plugin doctor my-plugin
quirl plugin disable my-plugin
quirl plugin enable my-plugin
quirl plugin update --locked
quirl plugin remove my-plugin

Set QUIRL_PLUGIN_HOME to override the state directory. Otherwise Quirl uses $XDG_CONFIG_HOME/quirl/plugins or ~/.config/quirl/plugins. Every command supports --format json for stable machine output.

add accepts a local directory, plugin.toml, or file: path. Platform v0.1 does not fetch GitHub or registry sources: fetch them explicitly, inspect the content, and add the local result. Requested permissions must be sorted and each new permission must be repeated as --allow. The lock records requested and granted permissions separately, source identity, resolved API, runtime, enabled state, runtime/WIT schema hash, and SHA-256 manifest/entry/source checksums. Lock schema v3 binds executable trusted-Lua entries to manifest v2's closed command I/O contract and portable components to the checked-in WIT world. New additions remain disabled until plugin enable repeats integrity and runtime-boundary checks.

Mutations build and validate a complete candidate lock before writing and flushing a temporary file, syncing the state directory, and atomically replacing the current file. The previous lock is moved to plugins.lock.json.bak; failed replacement attempts restore it. A corrupt or incompatible lock is never silently overwritten. remove removes only the lock entry and never deletes source. update --locked verifies all local sources and rejects version, checksum, API, or permission changes.

Manifest

Plugin manifests are deny-unknown TOML with schema_version = 2. The [plugin] table declares name, version, relative entry, Quirl range, exact plugin API (0.1.0), runtime, and summary. [capabilities] lists sorted requested grants. [contributes] lists sorted commands, completions, events, panels, and indexers. Every command has a matching complete [[public_commands]] contract.

Contribution authority is explicit: commands need commands.register, completions need completion.register, events need events.observe, panels need ui.panel, and indexers need catalog.register; panels and indexers also need the registration handle extension.contribute. Scoped process permissions use forms such as process.spawn:kubectl. Unknown capability names are rejected in both manifests and locks. Only process.spawn:<relative-executable> and filesystem.read/write:<package-relative-path> accept scopes; control characters, traversal, absolute paths, and shell syntax are invalid. A scoped process handle accepts one physical command/argv line and rejects shell operators, tabs, and newlines before the shell-backed runner is reached.

Runtime boundaries

Executable command I/O failure model

Plugin command I/O declarations are executable contracts, not a general type language. Input accepts exactly Nothing for no pipeline input or one bounded value whose top-level kind is Bool, Int, UInt, Decimal, String, List, Record, Path, Duration, Size, DateTime, or Pattern. Output accepts one value of those kinds (including Nothing) or a bounded finite batch written Values<T>, where T is one of the same value kinds. Stream<T>, byte input/output, nested type expressions, aliases, unions, unknown names, and case/whitespace variants are unsupported by Lua ABI v1 and fail registration before an enabled command reaches catalog composition. A List is one typed value; Values<T> is a materialized batch, never a live or backpressured stream.

The executable boundary maintains these invariants:

  • Nothing rejects unexpected input. Every other input contract rejects a missing value and a mismatched top-level kind before Lua evaluation.
  • Dispatch-time input conversion and callback execution share the plan cancellation identity and absolute deadline. Conversion polls both while walking the value; cancellation or expiry before, during, or immediately after conversion/callback remains a ResourceLimit error and cannot publish output.
  • One command input or complete output batch is limited to 512 logical value nodes, 6 collection levels, 256 aggregate record fields, and 240 KiB of string/key bytes, inside the shared execution and raw Lua-envelope limits. Finite output batches accept at most 512 values of their declared supported kind; 512 values are accepted and value 513 is rejected. Nested batch items share the same aggregate logical limits.
  • Unknown fields, hostile/cyclic Lua tables, mixed success/error envelopes, byte or inherited output, undocumented status, output-shape mismatch, and limit overflow fail before outcome commit. A callback has no partial-output channel.
  • Discovery reads only bounded, checksummed manifest and lock metadata. It does not evaluate Lua. Disable, removal, source replacement, rejected reload, or a changed registration between snapshot and execution fails reconciliation and never falls through to native execution.
  • Scheduler cancellation waits for bounded quiescence before error/recovery observers run. Cleanup failure supplements rather than replaces the originating error; only a fully validated outcome reaches result events.
  • Plugin manifest v1 and lock v1/v2 predate executable I/O contracts. Their historical identities remain recognized for diagnostics, but they fail closed: old lock data cannot prove the newly reviewed manifest/runtime hash. Preserve the old file by moving plugins.lock.json to plugins.lock.json.legacy-v1 or .legacy-v2, then re-add each plugin under manifest v2 after explicit review to create lock v3. Quirl never fabricates a new runtime schema hash from an old lock.

Lua ABI v1 has no finite input-batch or live-stream handle. Adding either would require a later versioned ABI with backpressure, cancellation, cleanup, and ownership semantics.

Trusted Lua uses the pinned restricted Lua 5.4 runtime. Managed runtimes are constructed with exactly the granted capability strings. Host registration values are deny-unknown Rust schemas; command metadata is typed and complete; callbacks retain instruction, memory, cancellation, and deadline enforcement. Manifest command contributions must exactly match Lua registrations. Installed command metadata is projected from the validated permission lock and manifest without evaluating Lua or invoking contribution callbacks. Execution reconciles that snapshot with the active generation, validates the catalog argument schema and locked effect grants, then runs the registered callback on the shared bounded extension scheduler. The callback receives the typed Lua runner context and returns the ABI-v1 result envelope; status, structured values, ShellError, cancellation, and deadlines therefore use the same contract as typed scripts and builtins.

Command dispatch treats discovery and execution as separate states. Discovery reads bounded locked manifests and verified entry bytes, validates conflicts, and constructs catalog, completion, describe/doc, LSP, and agent projections without evaluating Lua. Execution polls the installed state again and requires the active generation to match the discovered command exactly. Removal, replacement, a rejected reload, or projection staleness fails closed instead of falling through to native process lookup. Disabled commands report that state; conflicts and invalid checksums prevent the snapshot from being composed.

Argument schemas and declared effects are checked before scheduler admission or callback effects. The fixed worker pool provides bounded queue admission, per-runtime FIFO ownership, deadlines, cancellation, saturation errors, and bounded shutdown; command dispatch never creates a thread per plugin. Lua runs only at scheduler safe points. Installed command dispatch receives the shared execution plan's absolute deadline and cancellation identity before reload, binding reconciliation, context construction, queue admission, and callback work. A timeout cancels its exact batch and waits a separate bounded cleanup interval for quiescence before recovery or result/error observers run. Malformed or oversized typed input/output, undocumented status codes, byte output, partial ABI results, callback failures, timeouts, and cancellation remain structured ShellError values. A callback cannot publish partial typed output: only a completely validated outcome is emitted, rendered, and included in the normal result/error and recovery ordering. Cleanup errors do not replace the originating diagnostic.

MCP deliberately remains builtin-only. Its catalog metadata neither reads nor executes installed plugins, so it never advertises a plugin command as remotely executable. The interactive extension host reads the same managed lock, integrity-checks every enabled trusted-Lua source, ignores disabled entries, and constructs each VM with exactly that entry's granted capabilities. It does not implicitly load unmanaged plugins/*.lua files. Explicit legacy check/test constructors remain registration-only and never receive process authority.

Wasm platform v0.1 is deliberately non-executing. It verifies an actual component with the maintained wasmparser validator, parses a nonempty component type section, requires the exact host import and guest export named by quirl-plugin.wit, binds that WIT world's hash into the lock, and enforces explicit memory, fuel, and callback budgets. The non-executing validator does not claim generated Rust bindings or full WIT structural type equivalence; selecting an execution adapter must add that proof. It does not claim to execute components, and the CLI refuses to mark one enabled until a component runtime exists. The out-of-process adapter executes the deliberately narrow quirl.plugin.v1 initialization handshake defined by ADR 0009. It launches only the locked package-relative executable under the exact scoped grant, scrubbed environment, controlled working directory, message/deadline limits, and process-tree containment. Wasm still requires a future runtime implementation that preserves the same value/catalog/capability model and isolates crashes from the shell.

doctor verifies locked checksums and the runtime/WIT schema hash, then validates the declared runtime boundary. Any mismatch prevents enablement and returns actionable text/JSON diagnostics.

On this page