All notable changes to Anvil are recorded here. The format follows
Keep a Changelog. Pre-1.0 version
lines are release-series labels, not a compatibility promise; 0.4.x is the
post-0.4.0 feature line.
C/Alt-Cmulti-cursor creation now seeds the active caret from the moved cursor instead of a stale file-start selection.- Alt-modified ASCII letter bindings normalize case before dispatch, so
terminals reporting either
Alt+CorAlt+cresolve to the same action. - Repeated key events are dispatched like presses, so keyboard protocols that report hold/repeat do not lose autorepeat.
Escin normal mode merges multiple cursors back into the primary cursor. Insert-modeEsconly returns to normal mode and preserves the cursors.- Text-entry and pending states reject Ctrl/Alt/Super-modified characters instead of treating them as plain input.
- Command matching uses a shared key wrapper for modifier flags and Alt character normalization.
- Ignored key-release events no longer count as syntax activity.
0now moves to the line start when no count is pending instead of being consumed as a count.- Pending change, split, and space-prefixed commands consume the count, so
2cwdoes not leak the count into the next motion. - Syntax highlights use a 32-entry LRU path cache and are restored before the first frame; uncached files keep the normal text color until parsing completes.
- Inlay hint viewport ranges clamp to the last line, avoiding the language server's invalid EOF offset error.
- Workspace tree entries include inode identity, so selection follows an external rename when the new path remains visible.
plugin.jsonnow reads the documented camelCaseactivationEventskey; the previous snake_case spelling remains accepted as an alias.
- Plugin activation matching for
onStartup,onLanguage, andonWorkspaceContains; matching hosts start with an empty grant set. onStartupactivation requiresANVIL_PLUGIN_ALLOW_STARTUP=1; workspace activation can be disabled withANVIL_PLUGIN_DISABLE_WORKSPACE_ACTIVATION=1.- Multiple plugin hosts keyed by plugin id, with per-plugin status and revoke on stop or failure.
- Language activation is deduplicated per plugin; workspace-marker activation reads workspace root entries through the gateway.
-
Keyboard enhancement flags are enabled only when the terminal reports support, and popped on exit. Unsupported terminals keep the legacy key path.
-
Normal-mode and operator-pending bindings resolve through a dedicated keymap module; terminal key normalization lives in an input adapter.
-
Normal and operator-pending commands have stable namespaced IDs and a read-only registry. An optional TOML keymap validates overrides against those IDs and applies them atomically.
-
Debug plugin hosts now register a per-session grant before spawn and revoke on stop or failure through a dedicated plugin session coordinator.
-
Local plugin manifests are discovered from
ANVIL_PLUGIN_DIRwith duplicate id rejection; activation is not wired yet.
This release bundles the 0.4.4 gateway capability control plane and the
0.4.5 manifest/host-loading slice.
plugin.register/plugin.revokeon an editor-authenticated gateway session.- Per-plugin token hashes and grants; plugin handshakes receive only their own permission set, and revoke immediately removes access.
GatewayClient::register_plugin/revoke_plugin.- Bounded
plugin.jsonmanifest validation and a sandboxed--plugin <dir>host path that loads one Luau entry after token read and sandbox setup. - Plugin-root read access in the host Landlock rules; manifest validation rejects escaping entries, duplicate commands, and unknown activation events.
- Markdown highlighting with
tree-sitter-mdblock and inline grammars. - Heading, link, code, emphasis, list, quote, table, and frontmatter classes.
- Fenced code blocks use the nested language grammar when available.
- TOML keys no longer color the entire key/value pair; strings and values keep their own classes.
- Tree watcher smoke instructions now create a new file instead of touching an existing file.
- Tree-sitter coverage for TOML, Go, Bash, Python, YAML, JSONC, Makefile, and
Dockerfile, detected by extension or by the
Makefile/Dockerfile/go.modfile name. - A bounded
go.moddirective/comment/string highlighter.
- Syntax queries are compiled once in a static registry instead of on every parse request; Rust keyword walking is limited to Rust.
- JSONC comments and booleans now use the JSON grammar's comment support.
- Unsupported formats continue to fall back to plain text, and the existing 256 KiB highlight limit still applies.
- Workspace tree watcher: the gateway exposes
sys.read_dir_signature, and the TUI polls the root plus expanded directories throughFileWorker. External create, delete, and rename now refresh the tree while preserving selection and expansion; watchers are capped and marked stale when the gateway cannot observe the tree. See ADR-0018.
- In-flight tree snapshots no longer collapse a directory expanded after the snapshot was requested; missing listings queue a follow-up refresh.
- Added INSTALL.md with source build, sibling-binary, runtime, verification, and uninstall instructions.
0.4.0closes the Polish & Specification train: fixes, tests, documentation, and contract closure only; no new capability families. See the release policy.- The plugin host stops at the sandboxed empty-host boundary. Plugin runtime,
loading, capability bindings, gateway control-socket registration, and
end-to-end plugin
sys.*dispatch move to the post-0.4.0feature line; the runtime slice is tracked as0.4.4.
- Buffer lifecycle commands:
:newcreates an unnamed buffer and:bd [n]closes the current or indexed buffer. Dirty close prompts for save/discard/cancel;:bd!discards; closing the last buffer leaves one valid empty buffer. - Multi-cursor caret insertion now applies text edits without per-edit index
updates, rebuilds
LineIndexonce, and maps selections in a sorted batch; a Criterion benchmark compares it with the legacy transaction path. - Start dashboard for empty launches: workspace/version context and shortcuts for insert, fuzzy file finding, the file tree, help, and quit.
- Dark truecolor TUI theme with rounded panels, accent focus borders, mode badges, panel surfaces, and WCAG-AA text/syntax contrast.
- Fixed line-number gutter clipping and indentation guides overwriting the first code character.
- Workspace tree create (
a/A), rename (r), and confirmed delete (d) through confinedsys.create_dir/sys.rename/sys.removegateway operations; tree and fuzzy lists now scroll to keep the selection visible. - LSP starts automatically for supported files and keeps one worker per
(tool, language_id), so switching Rust ↔ Go no longer stops the other language server. - Plugin-host now initializes a sandboxed Luau runtime for a smoke check after Landlock/seccomp; plugin loading and capability bindings remain pending.
- CLI
-h/--help,-V/--version, and--workspace=<DIR>parsing, with parser tests and documented usage. - Opt-in PGO and BOLT workflow:
scripts/pgo-bolt.shbuilds instrumented and profile-use binaries, records a PTY workload, and supports post-link BOLT profiles viaperf2bolt; no default release or CI behavior changes. - Vertical two-pane splits with
Ctrl-w v,Ctrl-w h/lfocus switching, andCtrl-w qclose; each pane keeps its own cursor, scroll, and buffer. - Multi-selection core: atomic multi-range transactions,
sword-occurrence selection,Alt-sline-cursor splitting,C/Alt-Ccursor extension, and secondary-caret rendering. - Space which-key overlay and indentation guides with current-block highlighting.
- Signature Help requests after
(/,with a reusable popup, cancellation, and stale response checks. - Workspace Symbols (
Space S) with client-side filtering and cross-file navigation. - Built-in LSP settings for TypeScript/JavaScript, C/C++, Java, Lua, JSON/JSONC, YAML, and shell files, with per-language root markers, arguments, and environment overrides.
- LSP initialize errors are surfaced in the status bar instead of being treated
as a generic crash;
jdtlsshows an indexing-workspace startup hint.
- Search mode now preserves Tree-sitter foreground colors; matches patch a background layer instead of replacing syntax spans.
- Open buffers now watch external metadata changes through the gateway in the
background
FileWorker: clean buffers reload, dirty buffers prompt before reloading, and unchanged metadata is debounced. - File tree move/copy now run through gateway
sys.rename/sys.copyin the background worker, anddrecursively deletes entries throughsys.remove_all; the status bar shows a spinner while the operation runs. - Upgraded the Go module and CI toolchain to Go 1.26.10 and
golang.org/x/systo v0.44.0, resolving the 45 standard-library vulnerabilities reported bygovulncheckagainst Go 1.22.3 and the remaining module advisory; CI now runs a pinnedgovulncheckv1.9.0 vulnerability scan. - Rename, Code Action, and Formatting responses are ignored when the active path or editor revision changed after the request, preventing stale edits from being applied to a different or newer buffer.
- Document-symbol traversal is bounded to 64 levels and 4096 items so malformed or adversarial symbol trees cannot exhaust the editor stack or memory.
- Gateway file reads, directory listings, and stats now open with
O_NONBLOCK, so a workspace FIFO cannot block the gateway or editor. - File permissions now require both the requested path and its resolved symlink target to match the granted subtree, preventing a link inside an allowed path from exposing a workspace file outside that grant.
- The
e2e_editperformance harness now compiles again through a feature-gated scroll helper, so 100 MB end-to-end measurements can run. - Added pinned
golangci-lintv2.15.0 to CI with the standard linters and intentional deferred-close exclusions; fixed the remaining errcheck and staticcheck findings. - Bounded the seccomp program instruction count before converting it to
uint16, with a regression test for oversized programs.
- LSP Code Actions/Quick Fixes (
Space a) with a selectable popup and a sharedWorkspaceEditapplication path. Code Action requests forward the complete diagnostic payload (code,codeDescription,relatedInformation,tags, anddata), and the client advertises Code Action literal support. - LSP Rename (
Space r) with an inline rename prompt; edits apply to every file returned by the language server. - LSP Formatting (
Space F,:format) appliesTextEdit[]through the sharedWorkspaceEditpath without writing to disk. - Document Symbols (
Space s,:symbols) with hierarchicalDocumentSymbol/SymbolInformationparsing, selection, and cursor jump. :wasaves every modified buffer.WorkspaceEditchanges to unopened files create background buffers and never write files during an edit.
- LSP Code Actions/Rename now share UTF-16 workspace-edit application, unopened-file loading, overlap rejection, and active-buffer snapshot updates.
- Stale Code Action responses are cancelled when the popup closes or another overlay/request replaces it.
- WorkspaceEdit now updates the matching active-buffer snapshot as well as
model.editor; switching away/back no longer resurrects stale text after an edit. Added regression coverage for editing the active buffer after opening another file.
WorkspaceEditengine for LSPchangesanddocumentChanges: URI decoding, UTF-16 range conversion, cross-file application, unopened-file loading, descending edits, overlap rejection, and atomic validation before buffers change. Code Actions/Rename UI wiring remains the next step.
- Extracted editing/motions/registers into
editing.rsand unit/integration tests intotests.rs.lib.rsis now 981 lines, down from 4355 before the modularization pass.
- Corrected the no-file save status from
E32: no file nametoE: no file nameand tightened the regression assertion.
- Further split
anvil-tui/src/lib.rs: file I/O lives infile_io.rs, buffer lifecycle/navigation inbuffer.rs, and search/substitution/Ex dispatch incommands.rs.lib.rsis now 2552 lines, down from 3113 at the start of this extraction pass.
- Completion cancellation now removes every pending completion request, even when no popup is visible. Insert-mode Esc and buffer switches therefore cannot let a late response reopen a popup after the request was cancelled.
- Completion now auto-triggers after
.and:in Insert mode, cancels pending completion requests on close/buffer switch/fuzzy open, and ignores stale responses. Completion, hover, diagnostics, and fuzzy overlays are mutually exclusive.
- Diagnostic navigation prefers severity-bearing primary diagnostics over
related/secondary entries, so
]d/[dand the popup show the actionable error instead of a related-location note.
]d/[ddiagnostic navigation with wraparound, a Diagnostic popup, and severity labels. Selecting a diagnostic moves the cursor to its UTF-16 start position.
- File tree
Htoggles hidden directories;.githuband.cargoremain visible by default. Fuzzy file discovery passes the hidden state torg --filesand excludes.git,target,.cache,.cargo, and.rustup.
- Fuzzy file results show workspace-relative paths, clip long paths from the left with an ellipsis, and highlight case-insensitive query matches with bold/underline styling.
- Multi-document LSP sessions: buffer navigation sends
didOpen/didChangefor the active document, and:wsendsdidChange+didSave. - Automatic language-server switching when the active buffer changes language (Rust -> Go -> Python), with stale worker state cleared.
- Navigation through definition, file tree, and fuzzy finder no longer replaces
an edited active buffer without snapshotting it.
open_pathnow preserves modified buffers, reuses already-open buffers, and routes navigation through the multi-buffer model.
- Split the TUI monolith into
keys,render,lsp_ui, andfile_tree_uimodules.lib.rsdropped from 4355 to 2863 lines without behavior changes. - LSP settings, completion/hover/definition requests, and file-tree/fuzzy behavior now live at their domain boundaries.
- Synchronized the Phase 0.7 plan with the implemented Rust/Go/Python LSP and module layout.
- LSP worker failures are classified as crashes consistently, stale completion/hover/pending state is cleared on stop/restart/reconnect, and an unsupported file no longer consumes the LSP gateway connection.
- Removed unused
ClientError::kindand duplicated LSP timeout plumbing; poll readability is the single framed-connection wait path.
wait_readabletreatsHUP/ERRas readiness, so a closed gateway or LSP socket reachesrecv_notificationand emitsCrashedinstead of spinning until timeout.
- LSP command multiplexing now polls socket readability before blocking on a complete Content-Length frame, avoiding partial-frame timeout desync during large completion/hover responses or busy gateway traffic.
- LSP completion popup with fuzzy label filtering and snippet insertion,
gddefinition jumps, andKhover documentation.
- Tab outside Insert mode no longer modifies the buffer; Tab indentation is insert-mode only.
- Trusted
-no-sandbox-lspprocesses inherit the gateway PATH, fixing child toolchains and Python shebangs for gopls/pyright.
- Language-server selection is standardized by file type: rust-analyzer for Rust, gopls for Go, and pyright/basedpyright/pylsp for Python, each with its own language id and project-root markers.
:reconnectdrops the old file-tree worker and LSP worker before replacing the gateway session, so later tree/fuzzy operations use the new socket.
- File tree and fuzzy-file enumeration now run in a dedicated
FileWorkerthread over a separate gateway connection; the UI renders a loading state instead of blocking. File discovery usesrg --filesthroughsys.execwith a boundedsys.read_dirfallback.
-
:reconnectrestarts the gateway child and performs a fresh handshake, replacing a poisoned client without discarding buffers or unsaved text. -
LSP wire foundation: Rust
Content-Lengthframed JSON-RPC transport, request/notification helpers,textDocument/publishDiagnosticsparsing, and a Gointernal/lspframed manager with persistent stdin/stdout pipes and crash/lifecycle primitives. -
GatewayClientnow queues interleaved server-initiated JSON-RPC notifications while waiting for a response; binary read/write paths skip notifications instead of desynchronizing. -
Gateway
lsp.start/lsp.send/lsp.stoproutes with persistentinternal/lspprocess pipes, sandboxed exec-runner launch,lsp.messagenotifications,lsp.stderr, andlsp.crashedon reader failure. LSP tool launches can grant workspace write plus rustup/cargo read/execute paths.GatewayClientexposes matching LSP helpers plus blocking notification receive. -
Buffer paths are canonicalized before matching LSP diagnostics, including symlinks and
..segments. -
LSP document and root URIs are canonical absolute
file://URIs with percent-encoding; diagnostics URIs are decoded before path matching. -
TUI LSP worker on a second authenticated gateway connection; initialize response ordering, initialized, didOpen, didSave, publishDiagnostics parsing, diagnostics map, and red underlined diagnostic ranges. Trusted local language servers run through
-no-sandbox-lsp; plugin-host remains sandboxed.ANVIL_GATEWAY_LOGcaptures gateway/LSP traffic. -
Four-space insert-mode Tab, display-width tab expansion, a gateway-backed workspace tree (
Space e/:tree), and a fuzzy file finder (Space f). The tree filters.gitandtarget. -
LSP restarts up to three times after crashes; the LSP handshake token is reusable only within the owning gateway session so the editor can reconnect. Diagnostics use UTF-16 to byte/display-column mapping for exact underlined ranges.
- LSP is experimental; trusted local language servers use
-no-sandbox-lsp, while plugin-host remains sandboxed. - Splits and advanced Ex commands are not implemented yet.
-
Vi word motions
w/b/eand operatorsdw/db/de/yw/yb/yewith byte-borrowing word classification inanvil-core; multibyte word ends stay on UTF-8 boundaries. Numeric prefixes are supported for motions,d/yword operators,dd,yy,x, and line movement. -
ciw,ci", andci'inner text-object changes; named yank/put and delete/change registers via"a,"_blackhole support, system clipboard register"+, macro recordingq{reg}/ replay@{reg}/@@, and numeric prefixes for motions and operators. -
Literal
:s/old/new/[g]and:%s/old/new/gsubstitution with dynamic offset updates after length-changing replacements. -
Tree-sitter syntax highlighting for Rust and JSON with a revision-keyed cache, zero-copy span slicing, keyword/string/comment/number rendering, a background parser thread, 50 ms debounce, and stale-span retention while a new parse is in flight. Files over 256 KiB remain unhighlighted to keep key latency bounded.
-
TUI relative line-number gutter, current-line background highlight, scrolloff margin, dirty
[+]title marker, colored mode badge in the status line, block/bar cursor shape switching, and current/other search-match highlighting. -
ADR-0012 plugin-host isolation contract: one sandboxed host per plugin, FD-delivered one-shot tokens, no inherited editor/gateway FDs, process-group lifecycle, and accepted escape/orphan tests.
-
Empty plugin host: token-FD read,
close_rangeto stdio only,no_new_privs, Landlock filesystem read rules,session.hello, and tests for/etc/passwddenial, exec/write denial, fd closure, native-ABI seccomp with AF_UNIX-only sockets (TCP/UDPEPERM), editor UDS EOF exit, and editorSIGKILLreaping. -
Plugin-host cgroup v2 helper:
pids.max=64,memory.max=256MiB, post-spawn PID attach, cgroup2statfsvalidation with existing-file writes only, andHostExit::OomKilledclassification for SIGKILL when a memory limit was active. -
Plugin-host framed UDS dispatch:
anvil-ipclength-prefixed frames,sys.ping -> pong, andsys.exec -> Unsupporteduntil the sandboxed executor is wired. -
TUI debug
HostSession::plugin-start/:plugin-exec/:plugin-stop/:plugin-oom, background UDS thread with mpsc events, 2-second RPC timeout, OOM status mapping, host SIGKILL event-loop test, and host/cgroup cleanup on drop. No VM or plugin API yet.
- 0.1.1 / Phase 0.7: plugin-host isolation, Tree-sitter syntax highlighting,
LSP client, and optional
:reconnectpoison recovery. See docs/phase-0-7-plan.md.
-
Phase 0.6 exec sandbox foundation:
anvil-exec-runner, Landlock read-only workspace/runtime rules with per-tool and ELF-interpreter execute grants, a positive seccomp allowlist withclone/clone3and executable-mapping filtering, OS-thread locking and identity checks for per-thread confinement,rg/fdargv policy parsers, optional cgroup v2pids.maxwith pre-exec membership, soft-RLIMIT relaxation, stale-cgroup cleanup, andRLIMIT_NPROCfallback, plus a supervised process-group runner for timeout and output caps. -
Phase 0.6 gateway per-session cgroup provisioning and cleanup, plus a deployment model document for systemd delegation and root-owned roots.
-
Phase 0.6 Go gateway skeleton: Unix-socket framed JSON-RPC, one-shot
session.hellotoken-hash authentication,SO_PEERCREDUID checks, connection-cancel propagation, andsys.execrouting throughanvil-exec-runner; Rust handshake example included. -
Phase 0.6 SSRF-constrained
sys.fetch: exact host allowlist, DNS pinning, blocked-address validation, disabled redirects, bounded bodies, allowlisted request headers with name-only sensitive auditing, and host-bound TLS SNI. -
Phase 0.6 TUI-0/TUI-1: minimal
anvil-tuieditor with insert variants (i/a/A/o/O),:w/:wq/:w file/:q!, an unsaved-change quit guard, undo/redo,x/dd/yy/pwith an unnamed register, substring/search withn/N, an embedded read-only:helppage, multi-buffer switching with:e/:bn/:bp, grapheme-aware cursor movement and deletion, horizontal scrolling, cursor/viewport separation, dirty-only redraws, terminal restore, and borrowed text chunks. -
Phase 0.6 TUI edit-loop benchmark: layered Rust-side and PTY latency for key-to-screen, 100 MB open, 100 MB scroll, and 100 MB search.
-
TUI 100 MB open memory: build
LineIndexdirectly from the source&strinstead of a fullTextBuffer::slicecopy; peak VmHWM drops from 493 MB to 300 MB, steady RSS remains 198 MB. -
Two-pass parallel line scanning for large text: disjoint worker ranges count and fill the final line-start array, cutting 100 MB Rust open from 174 ms to 89–96 ms and the JSON gateway path from 640 ms to 563 ms without increasing the 198/300 MB TUI RSS/peak.
-
Open phase instrumentation (
ANVIL_OPEN_TIMING) and a decomposition showing ~80 ms stat/pipeline read, ~62 ms line index, and ~0.4 ms first frame.
+- Binary sys.read_file frames: "binary": true returns [eof flag][raw bytes], 7 MiB chunks, no base64/JSON on the read path. 100 MB gateway PTY
open drops from 563 ms serial to 171.1 ms with a 4-request window; gateway
RSS is 66/91 MB. Writes use binary data frames plus JSON control; 100 MB
save is 22.2 ms on ext4 (page-cache warm).
-
Workspace-scoped chunked
sys.read_file/sys.write_file: fail-closed gateway I/O, base64 5.5 MiB JSON writes, binary 7 MiB reads, atomic rename, streaming writes, path canonicalization before permission checks, and nostd::fsfallback. -
anvil-ipc::GatewayClient: synchronous handshake, chunked read/write, response-id checks, policy-error detection, and complete-file helpers. -
TUI gateway integration:
--workspace, per-sessionanvil-gatewayspawn, token handshake, gateway-backed open/save/:e/:w, 256 MiB open limit, and fail-closed behavior when the gateway cannot start. Nostd::fsdocument fallback remains in the runtime path. -
Phase 0.5 complete: PieceTable production selection, cursor/line-index primitives, editor snapshots, selection-aware undo, and the locked-core A/C benchmark and 100k stress gate.
-
Project charter and Phase 0 security/performance baseline.
-
Threat model covering path traversal, SSRF, plugin escape, IPC spoofing, resource exhaustion, cancellation, and manifest escalation.
-
Architecture decision records for plugin isolation, the per-session Go gateway, lightweight IDL, text-model selection, exec policy, upstream dependencies, the JSON-RPC/token contract, and the selection-first interaction model, including the text-model snapshot decision gate and a accepted cursor/snapshot ownership ADR.
-
Repository governance files: security policy, contribution guide, code of conduct, editor configuration, license files, and CI skeleton.
-
anvil-ipc: length-prefixed JSON-RPC framing with an 8 MiB limit, clean-EOF handling, truncation detection, I/O error preservation, Unix socket connections with0600socket permissions,SO_PEERCREDlookup, and validatedRequest/Response/Meta/ErrorObjectenvelopes. -
anvil-api: interaction, view, and error contracts:InteractionMode,EditorView,Viewport,Selection,TextPosition,SetModeRequest,SetModeResponse,ApiError, and stable method-name constants. -
anvil-api/bindings: generated TypeScript declarations, JSON Schema, LuaLS annotations, and the API version constant. -
anvil-core:TextBufferwithPieceTableselected as the productionTextDocumentbackend; Rope and Gap Buffer remain benchmark-only. UTF-8 boundary checks, cached line counts, proptest model comparisons,EditCommand/Historyundo-redo with revisions and typing groups, COW PieceTable text chunks, chunkedLineIndex, cursor/grapheme/display conversion, a criterion decision harness, a p99/snapshot timing probe, a Linuxtext-rssprobe, and a PR-only 10 MB same-runner p99 regression gate. -
anvil-editor: sharedEditorSnapshot,EditorState, selection mapping, andEditorViewprojection. -
go/internal/fsroot:openat2-confined workspace reader, atomic writer, size query, and traversal/symlink/regular-file/limit tests.
-
anvil-exec-runnerapplies Landlock/seccomp, the symlink-swap race test passes, and non-allowlisted exec (including throughld.so) is denied; the gateway routessys.execthrough this runner. -
Untrusted plugins run outside the editor process; the Go gateway is a policy gateway, not the plugin sandbox.
-
Four-space insert-mode Tab, display-width tab expansion, a gateway-backed workspace tree (
Space e/:tree), and a fuzzy file finder (Space f). The tree filters.gitandtarget. -
LSP restarts up to three times after crashes; the LSP handshake token is reusable only within the owning gateway session so the editor can reconnect. Diagnostics use UTF-16 to byte/display-column mapping for exact underlined ranges.
- 0.1.0 is pre-alpha; the TUI is runnable but the plugin API is not stable.
- Untrusted plugin-host isolation remains the Phase 0.6/0.7 open security item.
- Landlock-backed untrusted plugins require Linux 5.13+.
- 100 MB open/save meet 200 ms on this NVMe Linux host; 50 ms open remains a long-term target.