oats.plugins
Plugin manifest and loader infrastructure.
oats.plugins.manifest
Plugin manifest schema and discovery.
A plugin lives in a directory containing coder.plugin.json:
my-plugin/
coder.plugin.json
__init__.py # optional, if entrypoint is a package
plugin.py # conventional entrypoint — must export activate(ctx)
Manifest example:
{
"id": "my-plugin",
"name": "My Plugin",
"version": "0.1.0",
"description": "Adds a `lint` tool and a pre_tool_use logger",
"entrypoint": "plugin",
"enabled_by_default": true,
"on_features": ["planning"],
"model_support": ["*"],
"provides": {
"toolsets": ["lint"]
}
}
Discovery walks two roots by default: <data_dir>/plugins (user-installed)
and <project_dir>/.coder/plugins (project-local). Additional roots can
be passed in for testing.
- class oats.plugins.manifest.PluginProvides(**data)[source]
Bases:
BaseModelWhat the plugin is declared to register.
Purely descriptive — used for UI and activation filtering, not enforced against what
activate()actually does. Keeps the manifest honest without coupling the loader to runtime side effects.- model_config: ClassVar[ConfigDict] = {}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- class oats.plugins.manifest.PluginManifest(**data)[source]
Bases:
BaseModelDeclarative plugin descriptor.
Kept intentionally small. The loader reads the manifest first (cheap JSON parse) so it can filter by feature flags or model before paying the cost of importing the plugin’s Python module.
- id: str
- name: str
- version: str
- description: str
- entrypoint: str
- enabled_by_default: bool
- provides: PluginProvides
- model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True}
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
- oats.plugins.manifest.discover_manifests(roots=None)[source]
Walk the plugin roots and return one validated manifest per plugin dir.
- Return type:
list[PluginManifest]
- Default roots (skipped if they don’t exist):
<data_dir>/plugins<cwd>/.coder/plugins
Does not import plugin Python. Safe to call multiple times; idempotent. Duplicate ids are resolved by first-wins (stable sort on root order).
oats.plugins.loader
Plugin loader.
Given a list of PluginManifest records, filters by activation gates,
imports each plugin’s entrypoint module lazily, and calls its activate
function with a PluginContext that exposes the stable extension API.
Contract for plugin authors:
Put a
oats.plugins.jsonin your plugin directory.Ship a Python module matching
entrypoint(defaultplugin.py).Export an
activate(ctx: PluginContext) -> Nonefunction. Callingctx.register_tool,ctx.register_toolset, orctx.register_handleris how you add capabilities. Do not touch global state directly — the context is the API, keeping plugins portable if internals change.
Idempotency: install() tracks which plugin ids have already been
activated so subsequent calls are no-ops, even across process reloads in a
single session.
- class oats.plugins.loader.SlashContext(cwd, console, session=None)[source]
Bases:
objectMinimal context for plugin-registered slash handlers.
Fields are keyword-only extensible — new fields can be added without breaking existing handlers that only read what they need.
- cwd: Path
- console: Any
- session: Any = None
- __init__(cwd, console, session=None)
- class oats.plugins.loader.SlashCommand(name, handler, help_usage, help_desc, plugin_id)[source]
Bases:
objectA user-facing /command registered by a plugin.
- name
The command name (e.g.
/vi).
- handler
Async callable
(args_str, SlashContext) -> None.
- help_usage
Short usage string shown in
/help(e.g./vi [path]).
- help_desc
Human-readable description of the command.
- plugin_id
ID of the plugin that registered this command.
- name: str
- help_usage: str
- help_desc: str
- plugin_id: str
- __init__(name, handler, help_usage, help_desc, plugin_id)
- oats.plugins.loader.get_slash_commands()[source]
Snapshot of registered slash commands. Read-only for callers.
- class oats.plugins.loader.PluginContext(manifest)[source]
Bases:
objectStable extension API handed to each plugin’s
activatefunction.Plugins should treat this as their only coupling to oats internals. If the loader changes how registries or hooks are wired, the context’s method surface stays the same.
- manifest: PluginManifest
- register_tool(tool, toolset=None)[source]
Register a
Toolwith the global tool registry.Optionally groups the tool under one or more toolset names. Falls back gracefully if the registry doesn’t support toolsets.
- Return type:
- Parameters:
tool – The tool instance to register.
toolset – Optional toolset name(s) to associate with the tool.
- register_toolset(name, *, members=None, includes=None)[source]
Register a named toolset with the global tool registry.
A toolset groups related tools so they can be enabled or disabled together. No-op if the registry doesn’t support toolsets.
- Return type:
- Parameters:
name – The toolset name.
members – Optional list of tool names belonging to this toolset.
includes – Optional list of other toolset names to include.
- register_slash_command(name, handler, *, usage=None, description='')[source]
Register a user-facing /command handled inside the interactive REPL.
namemust start with/(case-insensitive). Later registrations for the same name win, with a warning — keeps plugin reloads sane in dev.- Return type:
- register_handler(event, fn, matcher=None, name=None)[source]
Register a hook handler on the global
HookEngine.The handler is registered under a name derived from the plugin id and the function’s
__name__(or the explicitnameargument).- Return type:
- Parameters:
event – The hook event to listen for.
fn – The async handler callable.
matcher – Optional matcher string to scope the handler.
name – Optional explicit name for the handler registration.
- __init__(manifest)
- oats.plugins.loader.load_all(manifests=None, *, model_id=None)[source]
Activate every manifest that passes the filter; return the activated list.
manifestsdefaults todiscover_manifests(). Errors while activating one plugin never take down another — they’re logged and the plugin is skipped.- Return type:
list[PluginManifest]