| name | plugin-architecture |
| description | Design extensible plugin systems that let third parties extend your application without modifying core code. Outputs plugin interface design, registry patterns, sandboxing strategy, and versioning contracts. |
| argument-hint | ["extension points needed","language","security requirements","plugin author audience"] |
| allowed-tools | Read, Write |
Plugin Architecture
A plugin architecture makes your application extensible without modifying its core. Users, customers, or third-party developers add functionality by writing plugins that conform to a defined interface. Done well, it creates a platform. Done poorly, it creates a security hole and a maintenance nightmare.
Process
- Define extension points. Where should plugins hook in? Data transformation, UI rendering, event handling, command registration. Be conservative — every extension point is a contract you must maintain.
- Design the plugin interface. What methods must every plugin implement? What does the host provide to plugins (the API surface)?
- Choose the execution model. In-process (fast, less isolated) or out-of-process (slower, sandboxed). Higher trust plugins → in-process. Untrusted third parties → out-of-process.
- Build the registry. How are plugins discovered, loaded, and managed?
- Version the interface. Plugins break when the interface changes. Version it from day one.
- Sandbox untrusted plugins. Limit what plugins can access — file system, network, other plugins.
Plugin Interface Design
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any, Optional
@dataclass
class PluginManifest:
"""Metadata every plugin must provide."""
name: str
version: str
description: str
author: str
interface_version: str
permissions: list[str]
class DataProcessorPlugin(ABC):
"""Extension point: data transformation pipeline."""
@property
@abstractmethod
def manifest(self) -> PluginManifest:
...
@abstractmethod
def can_handle(self, data_type: str) -> bool:
"""Returns True if this plugin handles this data type."""
...
@abstractmethod
def process(self, data: Any, context: "PluginContext") -> Any:
"""Process data and return transformed result."""
...
def on_load() -> :
() -> :
:
():
._plugin_name = plugin_name
._permissions = (permissions)
._storage = storage_path
() -> :
logging
logging.getLogger().log(
(logging, level.upper()), message
)
() -> []:
os
os.environ.get()
() -> :
._permissions:
PermissionError()
pathlib
safe_path = pathlib.Path(._storage) / path
safe_path.resolve().is_relative_to(pathlib.Path(._storage).resolve()):
PermissionError()
safe_path.read_bytes()
() -> :
...
Plugin Registry
import importlib
import importlib.util
import sys
from pathlib import Path
class PluginRegistry:
def __init__(self, interface_version: str = "1.0"):
self._plugins: dict[str, DataProcessorPlugin] = {}
self._interface_version = interface_version
def discover(self, plugins_dir: str) -> list[str]:
"""Scan directory for plugin modules."""
found = []
for path in Path(plugins_dir).glob("*/plugin.py"):
try:
plugin = self._load_from_path(path)
self.register(plugin)
found.append(plugin.manifest.name)
except Exception as e:
print(f"Failed to load plugin at {path}: {e}")
return found
def _load_from_path(self, path: Path) -> DataProcessorPlugin:
spec = importlib.util.spec_from_file_location(path.parent.name, path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
plugin_class = None
for attr_name (module):
attr = (module, attr_name)
((attr, ) (attr, DataProcessorPlugin)
attr DataProcessorPlugin):
plugin_class = attr
plugin_class:
ValueError()
plugin = plugin_class()
manifest = plugin.manifest
manifest.interface_version != ._interface_version:
ValueError(
)
plugin
() -> :
name = plugin.manifest.name
name ._plugins:
ValueError()
plugin.on_load()
._plugins[name] = plugin
() -> :
name ._plugins:
._plugins[name].on_unload()
._plugins[name]
() -> [DataProcessorPlugin]:
[p p ._plugins.values() p.can_handle(data_type)]
() -> [PluginManifest]:
[p.manifest p ._plugins.values()]
Example Plugin Implementation
from my_app.plugins import DataProcessorPlugin, PluginManifest, PluginContext
import csv, io
class CSVEnricherPlugin(DataProcessorPlugin):
@property
def manifest(self) -> PluginManifest:
return PluginManifest(
name="csv-enricher",
version="1.2.0",
description="Adds computed columns to CSV data",
author="Acme Corp",
interface_version="1.0",
permissions=["storage"],
)
def can_handle(self, data_type: str) -> bool:
return data_type == "text/csv"
def process(self, data: Any, context: PluginContext) -> Any:
context.log("info", "Starting CSV enrichment")
reader = csv.DictReader(io.StringIO(data))
rows = list(reader)
for row in rows:
if "price" in row and "quantity" in row:
row["total"] = str(float(row["price"]) * (row[]))
output = io.StringIO()
rows:
writer = csv.DictWriter(output, fieldnames=rows[].keys())
writer.writeheader()
writer.writerows(rows)
output.getvalue()
Out-of-Process Sandboxing (for Untrusted Plugins)
import subprocess, json, resource
def run_sandboxed_plugin(plugin_path: str, data: Any, timeout: int = 5) -> Any:
"""Execute plugin in isolated subprocess."""
proc = subprocess.run(
["python3", "-c", f"""
import sys, json, resource
# Restrict: 64MB memory, no network (handled at OS level with seccomp/namespaces)
resource.setrlimit(resource.RLIMIT_AS, (64 * 1024 * 1024, 64 * 1024 * 1024))
sys.path.insert(0, '{plugin_path}')
import plugin
p = plugin.Plugin()
data = json.loads(sys.stdin.read())
result = p.process(data, None)
print(json.dumps(result))
"""],
input=json.dumps(data).encode(),
capture_output=True,
timeout=timeout,
)
if proc.returncode != 0:
raise RuntimeError(f"Plugin failed: {proc.stderr.decode()}")
return json.loads(proc.stdout.decode())
Plugin Versioning
## Interface Versioning Contract
Interface version: MAJOR.MINOR
MINOR bump: Additive changes (new optional methods, new context APIs)
Plugins on v1.0 still work with v1.1 host
Host checks: plugin_version.major == host_version.major
MAJOR bump: Breaking changes (removed methods, changed signatures)
Must increment, plugins must update
Host rejects plugins on wrong major version
## Plugin manifest declares supported interface version
## Host validates on load, refuses incompatible plugins
## Keep changelog of interface changes in docs/PLUGIN_API.md
Anti-Patterns to Avoid
| Anti-Pattern | Problem | Fix |
|---|
| No interface versioning | Updating host breaks all plugins | Version interface from day one |
| Plugins access global state | Tight coupling; plugins interfere with each other | All host access through PluginContext only |
| No sandboxing for untrusted code | Malicious plugin reads secrets or crashes host | Out-of-process execution for untrusted plugins |
| Too many extension points | Every extension point is a contract to maintain | Start with one; add only when needed |
| Loading plugins synchronously at startup | Slow startup; one bad plugin blocks all | Lazy load; catch and log individual plugin failures |
10 Rules
- Every extension point is a contract — add only what you will maintain forever.
- Version the plugin interface from day one — changes without versions break plugins silently.
- Plugins access the host through a defined context API — never global state.
- In-process for trusted plugins; out-of-process with resource limits for untrusted.
- Plugin discovery and loading failures are isolated — one bad plugin never blocks others.
- Declare permissions in the manifest — plugins state what they need, hosts enforce it.
- Plugin on_load and on_unload lifecycle hooks enable clean resource management.
- Keep the plugin API surface small — every method you expose is complexity you maintain.
- Test plugins against a contract test suite — the host provides a test harness.
- Document the full plugin API in one place — plugin authors need complete, stable reference docs.