This document defines the extension manager we want to build. It is the stable architecture contract for installing, locking, loading, updating, and diagnosing librecode extensions.
The core decision:
Extensions are explicit dependencies, not arbitrary files that happen to exist on disk.
The stock librecode UI remains Go-owned and fast. Extensions are trusted optional code loaded only when the user/project explicitly declares them.
- Make extension loading predictable and reproducible.
- Support first-party, GitHub-hosted, and local development extensions.
- Keep Lua as one runtime adapter behind a runtime-neutral extension host.
- Avoid embedding official extensions in the binary.
- Avoid auto-executing random directories under
.librecode/extensions. - Provide commands that feel like a package manager, not a manual copy workflow.
- Sandboxing extension code.
- Moving the stock chat UI into Lua.
- Auto-loading every extension present on disk.
- Designing a Lua-only architecture that blocks future shell/toolbox/MCP/mvm-style runtimes.
Extensions are configured through extensions.use.
extensions:
enabled: true
use:
- official:vim-mode
- github:example/librecode-extension
- github:example/monorepo//extensions/fancy
- path:.librecode/extensions/local-dev
- source: official:vim-mode
version: v0.1.0
- source: github:example/librecode-extension
version: v1.2.3Supported forms:
- string source:
official:vim-mode - object source:
{ source: github:user/ext, version: v1.2.3 }
Supported schemes:
official:<name>— first-party extension alias.github:<owner>/<repo>— extension at repository root.github:<owner>/<repo>//<subdir>— extension inside a repository subdirectory.path:<path>— local extension directory or Lua file.
No local: scheme is supported. Use path: for local filesystem extensions.
At app startup, librecode loads only entries declared in extensions.use.
It must not recursively auto-load all directories under:
./.librecode/extensions~/.librecode/extensions- any installed extension cache directory
This rule prevents stale clones, experimental worktrees, or removed extensions from executing unexpectedly.
Global extension manager state lives under librecode home:
~/.librecode/
config.yaml
extensions-lock.yaml
extensions/
store/
github.com/<owner>/<repo>/...
official/<name>/...
Project-local extension config and lock can live under:
./.librecode/
config.yaml
extensions-lock.yaml
extensions/
local-dev-extension/
Project config wins over global config when it is present, matching the broader librecode config model.
The lockfile pins resolved extension versions/tags. It is human-readable and version-first, not commit-first.
Recommended shape:
extensions:
official:vim-mode:
resolved: github:omarluq/librecode//extensions/vim-mode
version: v0.1.0
github:example/librecode-extension:
version: v1.2.3Field meanings:
- key: original configured source.
resolved: canonical source when the configured source is an alias likeofficial:*.version: resolved tag/version to install.
The initial implementation should not require commit hashes. A future implementation may add a checksum or commit for verification, but the primary user-facing lock should stay tag/version based.
The core command set:
librecode extension list
librecode extension add <source> [--version vX.Y.Z]
librecode extension remove <source-or-name>
librecode extension install
librecode extension update
librecode extension tidy
librecode extension doctorShows configured, installed, loaded, errored, and disabled extensions.
Expected columns:
- name
- source
- version
- status
- runtime
- entry
- diagnostics/errors
Adds an entry to extensions.use, installs it immediately, and updates the lockfile.
Example:
librecode extension add official:vim-mode
librecode extension add github:example/librecode-extension --version v1.2.3Removes an entry from extensions.use, runs tidy immediately, and updates the lockfile.
Example:
librecode extension remove vim-mode
librecode extension remove official:vim-modeReconciles config + lock into installed extension files.
Rules:
- honor existing lockfile versions when present;
- install missing locked extensions;
- resolve and lock missing versions for configured entries;
- never update already locked versions unless
updateis requested.
Resolves newer versions/tags for configured non-path: entries, installs them, and rewrites the lockfile.
Initial implementation can update all extensions. Later, support:
librecode extension update vim-modeRemoves installed extension directories that are no longer referenced by config or lock.
Validates extension config, lockfile, installed directories, manifests, runtime compatibility, and load errors.
Official extensions are aliases resolved by librecode.
Initial registry:
official:
vim-mode:
source: github:omarluq/librecode//extensions/vim-mode
version: v0.1.0The registry can start as a small Go map. Later it may be fetched from librecode.sh or from release metadata.
Official extensions are not embedded into the binary. They are installed like any other extension.
A directory extension has an init.lua manifest.
my-extension/
init.lua
main.lua
helpers.lua
Manifest:
return {
name = "my-extension",
version = "0.1.0",
api_version = "v1alpha1",
description = "Example extension.",
entry = "main.lua",
}The manifest should stay small and declarative. Behavior belongs in the entry file and sibling modules.
The extension manager installs and resolves extensions. The extension host loads them through runtime adapters.
Current runtime:
- Lua adapter
Future runtimes:
- shell hooks
- toolbox executables
- MCP-backed tool collections
- experimental Go-like runtime adapter
The manager should not know about Lua internals beyond manifest/runtime selection. Runtime-specific loading belongs in the adapter.
- Load config.
- If extensions are disabled or
--no-extensionsis set, skip extension loading. - Read
extensions.use. - Resolve each source:
path:directly to filesystem;official:through the official registry and lock;github:through install store and lock.
- Load only resolved entries.
- Report load diagnostics through
extension listand logs.
Extensions are trusted, but failures should not silently corrupt the app.
- Invalid config should fail validation with a clear message.
- Missing installed remote extensions should tell the user to run
librecode extension install. - Invalid manifests should show in
extension doctorandextension list. - Runtime errors should disable that extension for the current load and keep librecode usable.
The extension manager is ready when:
extensions.useis the only source of loaded extensions.path:,official:, andgithub:parse consistently in string and object forms.librecode extension add/remove/install/update/tidy/doctor/listexist.addinstalls immediately and updates config + lock.removetidies immediately and updates config + lock.installis deterministic from config + lock.updateupdates versions/tags intentionally.- official
vim-modecan be installed without embedding it in the binary. - extension loading remains off the hot render path unless an extension is explicitly active.