SourceAnt

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
namestrUnique plugin identifier
versionstrSemantic version (e.g., "1.0.0")
descriptionstrShort description
authorstrPlugin author
plugin_typePluginTypeOne of the plugin type enum values
dependenciesList[str]Names of plugins this depends on (default: [])
config_schemaDictJSON Schema for config validation (default: None)
enabledboolWhether the plugin is active (default: True)
priorityintLoad priority, lower = earlier (default: 100)

Lifecycle Hooks

Plugins follow a strict lifecycle managed by the PluginManager:

  1. Discovery — The manager scans plugin directories for plugin.py or __init__.py
  2. Loading — Plugin classes are imported and instantiated
  3. Registration — Metadata is validated and the plugin is registered
  4. Initialization_initialize() is called in dependency order
  5. Startup_start() is called after all plugins are initialized
  6. Shutdown_stop() is called in reverse dependency order
  7. 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 lifecycleapp_startup, app_shutdown
  • Webhooksbefore_webhook_processing, after_webhook_processing
  • Code reviewsbefore_review_generation, after_review_generation, before_review_posting, after_review_posting
  • Authenticationbefore_user_authentication, after_user_authentication, before_user_logout, after_user_logout
  • Repositoriesbefore_repository_authorization, after_repository_authorization, before_webhook_setup, after_webhook_setup
  • Errorson_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
HIGHEST0Security checks, validation
HIGH25Pre-processing, enrichment
NORMAL50Standard processing (default)
LOW75Post-processing, logging
LOWEST100Cleanup, analytics

Plugin Discovery

The PluginManager automatically discovers plugins by scanning registered directories. For a plugin to be discovered, it must:

  1. Be in a subdirectory of a registered plugin directory
  2. Contain either a plugin.py or __init__.py file
  3. 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").