Building a Plugin
SourceAnt's plugin system lets you extend the platform with custom integrations, authentication providers, LLM backends, and more.
Overview
Plugins are Python classes that inherit from BasePlugin. Each plugin declares its type, metadata, and lifecycle hooks. The plugin manager handles discovery, dependency resolution, and lifecycle management automatically.
Plugin Types
Every plugin must declare one of the following types via the PluginType enum:
- INTEGRATION — External service integrations (GitHub, GitLab, etc.)
- AUTHENTICATION — Auth providers (OAuth, SAML, etc.)
- LLM — Language model backends
- WEBHOOK — Webhook handlers
- NOTIFICATION — Notification channels (Slack, Email, etc.)
- UTILITY — Helper utilities
Quick Start
Create a new directory under src/plugins/ with a plugin.py file:
src/plugins/
my_plugin/
plugin.py
__init__.py Here's a minimal plugin:
from src.core.plugins import BasePlugin, PluginMetadata, PluginType
class MyPlugin(BasePlugin):
@property
def metadata(self) -> PluginMetadata:
return PluginMetadata(
name="my_plugin",
version="1.0.0",
description="A custom plugin",
author="Your Name",
plugin_type=PluginType.UTILITY,
)
async def _initialize(self) -> None:
# One-time setup: register routes, connect to services
pass
async def _start(self) -> None:
# Called after all plugins are initialized
pass
async def _stop(self) -> None:
# Graceful shutdown
pass
async def _cleanup(self) -> None:
# Final cleanup before unload
pass Plugin Metadata
The PluginMetadata dataclass defines your plugin's identity and configuration:
| Field | Type | Description |
|---|---|---|
name | str | Unique plugin identifier |
version | str | Semantic version (e.g., "1.0.0") |
description | str | Short description |
author | str | Plugin author |
plugin_type | PluginType | One of the plugin type enum values |
dependencies | List[str] | Names of plugins this depends on (default: []) |
config_schema | Dict | JSON Schema for config validation (default: None) |
enabled | bool | Whether the plugin is active (default: True) |
priority | int | Load priority, lower = earlier (default: 100) |
Lifecycle Hooks
Plugins follow a strict lifecycle managed by the PluginManager:
- Discovery — The manager scans plugin directories for
plugin.pyor__init__.py - Loading — Plugin classes are imported and instantiated
- Registration — Metadata is validated and the plugin is registered
- Initialization —
_initialize()is called in dependency order - Startup —
_start()is called after all plugins are initialized - Shutdown —
_stop()is called in reverse dependency order - Cleanup —
_cleanup()runs before the plugin is unloaded
Override _initialize, _start, _stop, and _cleanup in your plugin. The base class methods (initialize, start, etc.) handle state tracking and call your implementations.
Event System
Plugins interact with SourceAnt through hook points and event subscriptions.
Hook Points
Register callbacks for built-in lifecycle events:
from src.core.plugins import event_hooks, HookPriority
# In your _initialize method:
event_hooks.register_hook(
hook_name="before_review_generation",
callback=self._on_before_review,
plugin_name=self.metadata.name,
priority=HookPriority.NORMAL,
) Available hook points:
- App lifecycle —
app_startup,app_shutdown - Webhooks —
before_webhook_processing,after_webhook_processing - Code reviews —
before_review_generation,after_review_generation,before_review_posting,after_review_posting - Authentication —
before_user_authentication,after_user_authentication,before_user_logout,after_user_logout - Repositories —
before_repository_authorization,after_repository_authorization,before_webhook_setup,after_webhook_setup - Errors —
on_error,on_plugin_error
Event Subscriptions
Subscribe to domain-specific events for more granular control:
event_hooks.subscribe_to_events(
plugin_name=self.metadata.name,
callback=self._handle_event,
event_types=[
"pull_request.opened",
"pull_request.synchronize",
],
) Hook Priority
Control the order hooks execute with HookPriority:
| Level | Value | Use Case |
|---|---|---|
HIGHEST | 0 | Security checks, validation |
HIGH | 25 | Pre-processing, enrichment |
NORMAL | 50 | Standard processing (default) |
LOW | 75 | Post-processing, logging |
LOWEST | 100 | Cleanup, analytics |
Plugin Discovery
The PluginManager automatically discovers plugins by scanning registered directories. For a plugin to be discovered, it must:
- Be in a subdirectory of a registered plugin directory
- Contain either a
plugin.pyor__init__.pyfile - Export a class that inherits from
BasePlugin
Directories starting with an underscore are skipped.
Dependencies
Declare dependencies in your metadata to ensure correct load order:
PluginMetadata(
name="my_plugin",
version="1.0.0",
description="Depends on GitHub OAuth",
author="Your Name",
plugin_type=PluginType.INTEGRATION,
dependencies=["github_oauth"],
) The plugin registry uses topological sorting to resolve the dependency graph. Circular dependencies are detected and will raise an error at load time. A plugin's _initialize method is only called after all its dependencies have been initialized.
Configuration Validation
Define a JSON Schema in config_schema to validate plugin configuration at load time:
PluginMetadata(
name="my_notifier",
version="1.0.0",
description="Slack notifications",
author="Your Name",
plugin_type=PluginType.NOTIFICATION,
config_schema={
"type": "object",
"properties": {
"webhook_url": {"type": "string"},
"channel": {"type": "string"},
},
"required": ["webhook_url"],
},
) Access validated config values in your plugin with self.get_config("webhook_url").