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-pluginSet 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:
Nothingrejects 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
ResourceLimiterror 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.jsontoplugins.lock.json.legacy-v1or.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.