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: BaseModel

What 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.

toolsets: list[str]
tools: list[str]
hooks: list[str]
slash_commands: list[str]
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: BaseModel

Declarative 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
on_features: list[str]
model_support: list[str]
provides: PluginProvides
source_dir: Path | None
model_config: ClassVar[ConfigDict] = {'arbitrary_types_allowed': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

matches_model(model_id)[source]

True if model_support contains "*" or a prefix of the model id.

Return type:

bool

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.json in your plugin directory.

  • Ship a Python module matching entrypoint (default plugin.py).

  • Export an activate(ctx: PluginContext) -> None function. Calling ctx.register_tool, ctx.register_toolset, or ctx.register_handler is 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: object

Minimal 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: object

A 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
handler: Callable[[str, SlashContext], Awaitable[None]]
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.

Return type:

dict[str, SlashCommand]

oats.plugins.loader.plugins_enabled()[source]

Master gate for the plugin system.

Return type:

bool

class oats.plugins.loader.PluginContext(manifest)[source]

Bases: object

Stable extension API handed to each plugin’s activate function.

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 Tool with 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:

None

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:

None

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.

name must start with / (case-insensitive). Later registrations for the same name win, with a warning — keeps plugin reloads sane in dev.

Return type:

None

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 explicit name argument).

Return type:

None

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.

manifests defaults to discover_manifests(). Errors while activating one plugin never take down another — they’re logged and the plugin is skipped.

Return type:

list[PluginManifest]

oats.plugins.loader.install(*, model_id=None)[source]

Entry point called from the interactive CLI. No-op unless the flag is on.

Return type:

list[PluginManifest]

oats.plugins.loader.reset_for_tests()[source]

Testing hook — forget which plugins have been loaded.

Return type:

None