Plugin Distribution
Distribute Codumentor plugins as .tgz archives that users install with a single CLI command.
Quick Start
Installing a Plugin
codumentor plugin install my_plugin-1.0.0.tgz
The plugin auto-loads on the next Codumentor startup. No YAML editing required.
Listing Installed Plugins
codumentor plugin list
Removing a Plugin
codumentor plugin remove my_plugin
Package Format
Legacy dependency-free packages retain the format below. Version-2 dependency
packages additionally contain dependencies.lock and wheels/; they are validated
and installed offline with host-generated receipts and transactional activation.
Dependency-bearing frozen installs are not certified yet and fail closed.
The initial verified dependency target is the current Linux source host only.
See implementation decisions and release gaps.
A plugin .tgz archive contains:
plugin.yaml # Required: package manifest
backend/ # Optional: Python source code
<package_name>/ # Uniquely-named Python package
__init__.py
plugin.py
tui_widgets.py # Optional: TUI widget classes
...
ui/ # Optional: pre-built UI bundle
manifest.json # UI extension manifest
index.js # Pre-built ES module
plugin.yaml
The manifest declares the plugin's identity, entry points, and dependencies:
name: my_plugin
version: 1.0.0
description: A brief description of what the plugin does
author: Your Name
has_backend: true
has_ui: true
# Backend entry point (dotted Python module path inside backend/)
entry_module: my_plugin.plugin
entry_class: MyPlugin
# Plugin registration priority (lower = registers earlier, default 50)
priority: 50
# Optional: declare load-order dependencies on other plugins by name
# (matched against each plugin's meta["name"]). The loader registers
# every plugin in this list before yours. See "Plugin Load Order" below.
dependencies:
- workspace_isolation
# Legacy dependency-free example; version 2 is required for bundled wheels
python_dependencies: []
# Default constructor arguments (users can override in codumentor.yaml)
default_args:
some_option: true
threshold: 100
Field reference:
| Field | Required | Description |
|---|---|---|
name | Yes | Unique identifier ([a-z][a-z0-9_]{1,63}) |
version | Yes | Semver string (e.g., 1.0.0) |
description | No | Human-readable description |
author | No | Plugin author |
has_backend | Yes* | Whether the plugin has Python backend code |
has_ui | Yes* | Whether the plugin has a UI component |
entry_module | If backend | Dotted module path inside backend/ |
entry_class | If backend | Python class name to instantiate |
priority | No | Plugin registration priority (default: 50) |
dependencies | No | Names of other plugins that must register first |
python_dependencies | No | List of pip requirements |
default_args | No | Default constructor keyword arguments |
*At least one of has_backend or has_ui must be true.
Python dependencies: current limitation and proposed direction
Recipients never run pip. Legacy packages with nonempty python_dependencies
must be repackaged as manifest version 2. Dependencies must be disjoint from host
and other installed plugins (including disabled plugins), even at equal versions.
Native wheels, namespaces, extras, URLs, scripts and .pth are unsupported.
Author-only workflow on the exact supported source host:
codumentor plugin dependency-target
# Set manifest_version: 2 and python_dependencies in plugin.yaml.
codumentor plugin pack my_plugin/ --resolve-dependencies --output my_plugin.tgz
# Alternatively provide a wheels directory, then use --lock-wheels.
# Plain pack validates an already locked version-2 source package.
Locks declare exact host Python patch/platform/architecture/edition and marker
values. The recipient independently checks pins, hashes, closure, ownership,
wheel metadata, compatibility and preflight before activation. This does not
provide isolation or a sandbox: install only trusted plugins, because candidate
preflight imports supplied code. Resolution requires author Python/pip only.
Stop every application using the instance plugin root before install/force/remove.
Running applications hold a shared process-lifetime lease; mutations require an
exclusive lease and fail promptly if the root is in use. A forced upgrade preserves
the old tree on failure. Success includes a committed transaction ID. Activation
requires restart. Interrupted precommit transactions roll back on next startup or
codumentor plugin recover; committed transactions retain the new tree. Corrupt
journals fail closed and retain evidence for manual operator recovery. list
coordinates with recovery and requires applications stopped in this initial tier.
The initial lease/transaction implementation supports Linux local filesystems
only. Dependency-free package format compatibility is preserved, but transactional
mutation on Windows is not yet supported. Frozen dependency acceptance, portable
leases and the broader fault matrix remain release blockers, not implied support.
Plugin Load Order
Built-in and external plugins are registered in a single ordered pass at
startup. Two manifest fields control where your plugin lands:
dependencies— names of plugins that must register before yours. Matched against each loaded plugin'smeta["name"]. Topological order always wins: a plugin in this list registers earlier than yours, no matter whatpriorityeither side declares.priority— integer used as a tie-breaker within a topological layer (lower number = registers earlier). Plugins that don't declaredependenciesbehave exactly as they did before this field existed.
Use dependencies when your plugin reads state another plugin publishes
(e.g. branch_switching reads the execution strategy that
workspace_isolation injects, so branch_switching declares
dependencies: ["workspace_isolation"]). Use priority when you only
need rough ordering relative to plugins you don't know about by name.
Failure modes
- A
dependenciesentry naming a plugin that isn't loaded is not fatal: the edge is dropped, a warning is logged (Plugin '<name>' declares dependency '<missing>', which is not among the loaded plugins; ignoring …), and your plugin loads at default ordering for its layer. An edge therefore buys ordering, not presence — check for anything you actually read at runtime. See Plugin Dependencies. - A cycle in the dependency graph aborts startup with
Plugin dependency cycle detected: A -> B -> A. Break the cycle by removing one of the edges (often one direction was unintended).
Overriding from codumentor.yaml
Both fields can be overridden per-deployment by adding a matching entry
to the user's plugins: list. This is useful when an operator needs to
patch ordering without editing plugin code:
plugins:
- module: my_plugin.plugin
class: MyPlugin
dependencies: ["workspace_isolation", "branch_switching"]
The config-level value fully replaces the manifest's dependencies
list, mirroring how priority overrides work today.
Creating a Plugin Package
1. Set Up the Directory
my_plugin/
plugin.yaml
backend/
my_plugin/
__init__.py
plugin.py
tui_widgets.py # Optional: TUI widget classes
ui/
manifest.json
src/
index.ts
package.json
Important: The Python package inside backend/ must have a unique name (conventionally the same as the plugin name). This prevents import collisions with other plugins.
2. Build the UI (if applicable)
cd my_plugin/ui
npm install && npm run build
The build must produce index.js in the ui/ directory. The .tgz ships the pre-built bundle — users do not need Node.js to install plugins.
Two flags are worth copying from how in-tree plugins are built
(ui/scripts/plugin-esbuild.js): --minify, and marking the SDK external
so your bundle imports the host's shared copy instead of inlining ~100 KB of
its own —
esbuild src/index.ts --bundle --format=esm --minify \
--alias:@codumentor/plugin-sdk=/ui/extensions/sdk/index.js \
--external:/ui/extensions/sdk/index.js \
--outfile=index.js
The alias must come out as that URL in the emitted import (a bare
@codumentor/plugin-sdk cannot resolve in the browser — the loader does a
plain dynamic import() with no import map). The trade-off is that your
bundle then depends on the host's SDK being compatible, whereas an inlined
copy is frozen at build time.
3. Pack
codumentor plugin pack my_plugin/
This creates my_plugin-1.0.0.tgz in the current directory.
Options:
--output path/to/output.tgz— custom output path
4. Distribute
Share the .tgz file. Users install with:
codumentor plugin install my_plugin-1.0.0.tgz
Installation Directory
Installed plugins live in ~/.codumentor/plugins/:
~/.codumentor/plugins/
my_plugin/
plugin.yaml
backend/
my_plugin/
__init__.py
plugin.py
ui/
manifest.json
index.js
Auto-Discovery and Activation
External plugins are automatically loaded on Codumentor startup. The loader:
- Scans
~/.codumentor/plugins/*/plugin.yaml - Adds each plugin's
backend/directory to the Python import path - Loads and registers the plugin alongside built-in plugins
No
plugins:config entry is required. Discovery runs regardless of
whethercodumentor.yamlhas aplugins:section — an empty or absent list
does not disable external plugins. (Before this was fixed, the loader
gated on a truthyplugins:list, so installing a plugin with no built-in
entries configured silently loaded nothing.) Add a config entry only to
override settings (below) or disable a plugin.
Disabling a Plugin
Add the plugin name to disabled_plugins in codumentor.yaml:
disabled_plugins:
- my_plugin
Overriding Plugin Settings
To customize an external plugin's args or priority, add a matching entry to your plugins: list in codumentor.yaml. The config entry takes precedence over the plugin's default_args:
plugins:
# ... built-in plugins ...
- module: my_plugin.plugin
class: MyPlugin
priority: 10
args:
some_option: false
threshold: 200
Backend Plugin Requirements
External backend plugins follow the same interface as built-in plugins:
class MyPlugin:
def __init__(self, config=None, **kwargs):
self.meta = {
"name": "my_plugin",
"version": "1.0.0",
"description": "My plugin description",
"priority": 50,
}
async def register(self, bus):
bus.on("onInputReceived", self.on_input, priority=10)
async def on_input(self, agent, ctx, payload):
return None # or {"type": "continue", "patch": {...}}
def capabilities(self):
return {}
def ui_manifest(self):
return None # or dict with UI extension config
def api_router(self):
return None # or FastAPI APIRouter
See Backend Hooks for the full hook reference.
UI Plugin Requirements
External UI plugins follow the same conventions as built-in ones:
ui/manifest.json— declares custom elements and placementsui/index.js— pre-built ES module that registers web components
The UI bundle is served at /ui/extensions/plugins/<plugin_id>/index.js and loaded by the frontend automatically.
See UI Components for the SDK and component reference.
TUI Widget Support
External plugins can provide custom terminal UI widgets that render in the TUI (Textual-based interface). This is optional — without it, plugin events render using a generic fallback card.
Adding TUI Widgets
- Create a widget module in your plugin's
backend/directory:
backend/
my_plugin/
__init__.py
plugin.py
tui_widgets.py # TUI widget classes
- Implement a widget class that subclasses
PluginCard:
# my_plugin/tui_widgets.py
from codumentor.plugins.tui_sdk import PluginCard
from rich.text import Text
class MyPluginCard(PluginCard):
"""Custom TUI card for my plugin events."""
def _build_header_text(self) -> Text:
text = Text()
text.append(" \u25b8 ", style="dim")
text.append("My Plugin", style="bold cyan")
summary = self.plugin_data.get("summary", "")
if summary:
text.append(f" {summary}", style="dim")
return text
def _build_details_text(self) -> Text:
text = Text()
for key, val in self.plugin_data.items():
if key.startswith("_"):
continue
text.append(f" {key}: ", style="bold")
text.append(f"{val}\n")
return text
- Declare the
tuisection in yourui_manifest():
def ui_manifest(self):
return {
"plugin_id": "my_plugin",
# ... other fields ...
"tui": {
"sdk_version": "1.0",
"widget_module": "my_plugin.tui_widgets",
"widget_classes": ["MyPluginCard"],
"placements": ["transcript-card"],
},
}
The widget_module uses standard Python dotted import paths. Since backend/ is automatically added to sys.path during plugin loading, modules are importable directly by package name.
Base Classes
| Class | Import | Use Case |
|---|---|---|
PluginCard | codumentor.plugins.tui_sdk | Collapsible transcript cards with header/details (recommended) |
BaseTUIPluginWidget | codumentor.plugins.tui_sdk | Fully custom Textual widgets with reactive event data |
Import both from codumentor.plugins.tui_sdk — that is the published SDK
surface, and the only codumentor.* path a widget module should name. Its
__all__ also carries TUIPluginEvent, TUIPlacement, TUIWidgetManifest,
the format_ms / format_tps / format_tokens / truncate_to_width /
cell_width / render_markdown_text helpers, and the brand palette
(TEAL, GOLD_LIGHT, GOLD, NAVY_LIGHT, GREEN, PURPLE, YELLOW) the
core TUI widgets draw with — so a plugin card matches the transcript around it
without copying hex values. Everything the SDK re-exports is internal TUI code
that may move; the SDK name for it will not.
The built-in plugins follow exactly this shape: each keeps its card in its own
package as tui_card.py and points widget_module at it, so the card travels
with the plugin the way its web bundle under ui/plugins/<plugin_id>/ does.
src/codumentor/plugins/thinking_modes/tui_card.py is the smallest worked
example; context_visualizer/tui_card.py the most involved.
How It Works
On startup, the TUIWidgetRegistry reads each plugin's ui_manifest(), imports the widget_module, and maps event types to widget classes. When a plugin event arrives, the TUI looks up the registered widget class instead of using the generic fallback.
See Backend Hooks — TUI Widget Section for the full manifest field reference.
Troubleshooting
Plugin not loading?
- Check logs:
codumentor --log-level DEBUG - Verify plugin.yaml is valid:
codumentor plugin listshould show it - Check for name collisions with built-in plugins
Python import errors?
- Ensure the package name inside
backend/is unique - In source deployments only, provision requirements into the actual host Python environment; installing into an unrelated Python does not help a frozen binary.
- Frozen dependency-bearing packages are not reliably provisioned by the current installer; see the dependency limitation and proposal. Source-host bundled-wheel support has a deliberately narrow verified envelope.
UI not showing?
- Verify
ui/index.jsexists (must be pre-built) - Check the browser console for custom element registration errors
- Ensure
ui/manifest.jsondeclares the correct custom elements