Plugin Development Guide¶
This guide covers how to build, test, and distribute FCC plugins. The plugin system uses Python entry points, so plugins are ordinary Python packages discovered automatically at install time.
Plugin Types¶
FCC supports 10 plugin types, each registered through its own entry-point
group. Every plugin must implement the corresponding abstract base class from
fcc.plugins.base.
| # | Type | Entry-Point Group | ABC | Purpose |
|---|---|---|---|---|
| 1 | Personas | fcc.plugins.personas |
PersonaPlugin |
Contribute additional persona YAML files |
| 2 | Engines | fcc.plugins.engines |
EnginePlugin |
Provide custom simulation engine classes |
| 3 | Templates | fcc.plugins.templates |
TemplatePlugin |
Add Jinja2 template directories for doc generation |
| 4 | Scorers | fcc.plugins.scorers |
ScorerPlugin |
Register quality scoring callables |
| 5 | Validators | fcc.plugins.validators |
ValidatorPlugin |
Add FCC validation rules |
| 6 | Providers | fcc.plugins.providers |
AIProviderPlugin |
Register AI provider client implementations |
| 7 | Governance | fcc.plugins.governance |
GovernancePlugin |
Supply tags and quality gate YAML definitions |
| 8 | Scenarios | fcc.plugins.scenarios |
ScenarioPlugin |
Contribute scenario definition JSON files |
| 9 | Workflows | fcc.plugins.workflows |
WorkflowPlugin |
Contribute workflow graph JSON definitions |
| 10 | Subscribers | fcc.plugins.subscribers |
EventSubscriberPlugin |
Wire event handlers to the EventBus |
Creating a Plugin¶
1. Project layout¶
fcc-my-plugin/
pyproject.toml
src/
fcc_my_plugin/
__init__.py
plugin.py # ABC implementation
data/
personas/ # YAML files (for persona plugins)
governance/ # tag/gate YAML (for governance plugins)
2. Implement the ABC¶
Every plugin must expose a plugin_meta() method returning a PluginMeta
dataclass:
from fcc.plugins.base import PersonaPlugin, PluginMeta, PluginType
from pathlib import Path
class MyPersonaPlugin(PersonaPlugin):
def plugin_meta(self) -> PluginMeta:
return PluginMeta(
id="fcc-my-personas",
name="My Persona Pack",
version="1.0.0",
plugin_type=PluginType.PERSONAS,
description="Extra personas for domain X",
author="Your Name",
source_package="fcc-my-plugin",
)
def get_persona_paths(self) -> list[Path]:
return [Path(__file__).parent / "data" / "personas" / "custom.yaml"]
Persona plugins¶
class PersonaPlugin(ABC):
def plugin_meta(self) -> PluginMeta: ...
def get_persona_paths(self) -> list[Path]: ...
def get_dimension_paths(self) -> list[Path]: ... # optional
def get_cross_reference_paths(self) -> list[Path]: ... # optional
Engine plugins¶
class EnginePlugin(ABC):
def plugin_meta(self) -> PluginMeta: ...
def get_engine_class(self) -> type: ...
Template plugins¶
class TemplatePlugin(ABC):
def plugin_meta(self) -> PluginMeta: ...
def get_template_dirs(self) -> list[Path]: ...
Scorer plugins¶
class ScorerPlugin(ABC):
def plugin_meta(self) -> PluginMeta: ...
def get_scorers(self) -> dict[str, Any]: ...
Validator plugins¶
class ValidatorPlugin(ABC):
def plugin_meta(self) -> PluginMeta: ...
def get_validation_rules(self) -> dict[str, Any]: ...
AI Provider plugins¶
class AIProviderPlugin(ABC):
def plugin_meta(self) -> PluginMeta: ...
def get_provider_id(self) -> str: ...
def get_client_class(self) -> type: ...
def get_default_model(self) -> str: ... # optional, defaults to "default"
Governance plugins¶
class GovernancePlugin(ABC):
def plugin_meta(self) -> PluginMeta: ...
def get_tag_paths(self) -> list[Path]: ... # optional
def get_quality_gate_paths(self) -> list[Path]: ... # optional
Scenario plugins¶
class ScenarioPlugin(ABC):
def plugin_meta(self) -> PluginMeta: ...
def get_scenario_paths(self) -> list[Path]: ...
Workflow plugins¶
class WorkflowPlugin(ABC):
def plugin_meta(self) -> PluginMeta: ...
def get_workflow_paths(self) -> list[Path]: ...
Event Subscriber plugins¶
Defined in fcc.messaging.plugin_bridge:
from fcc.messaging.plugin_bridge import EventSubscriberPlugin
from fcc.messaging.bus import EventFilter, Subscriber
class MySubscriberPlugin(EventSubscriberPlugin):
def plugin_meta(self) -> PluginMeta: ...
def get_subscribers(self) -> list[tuple[Subscriber, EventFilter | None]]: ...
3. Configure pyproject.toml entry points¶
Register the plugin class under the correct entry-point group. The key
(left side of =) is an arbitrary name; the value (right side) is the
dotted import path to the factory function or class.
[build-system]
requires = ["setuptools>=64.0"]
build-backend = "setuptools.build_meta"
[project]
name = "fcc-my-plugin"
version = "1.0.0"
dependencies = ["fcc-agent-team-ext>=0.5.0"]
[project.entry-points."fcc.plugins.personas"]
my-personas = "fcc_my_plugin.plugin:MyPersonaPlugin"
# For governance plugins:
# [project.entry-points."fcc.plugins.governance"]
# my-governance = "fcc_my_plugin.plugin:MyGovernancePlugin"
# For subscriber plugins:
# [project.entry-points."fcc.plugins.subscribers"]
# my-subscriber = "fcc_my_plugin.plugin:MySubscriberPlugin"
The table below shows all entry-point group strings:
| Plugin Type | Entry-Point Group |
|---|---|
| Personas | fcc.plugins.personas |
| Engines | fcc.plugins.engines |
| Templates | fcc.plugins.templates |
| Scorers | fcc.plugins.scorers |
| Validators | fcc.plugins.validators |
| Providers | fcc.plugins.providers |
| Governance | fcc.plugins.governance |
| Scenarios | fcc.plugins.scenarios |
| Workflows | fcc.plugins.workflows |
| Subscribers | fcc.plugins.subscribers |
4. Plugin manifest¶
The PluginMeta dataclass is the plugin's manifest. It carries:
| Field | Type | Required | Description |
|---|---|---|---|
id |
str |
Yes | Unique plugin identifier |
name |
str |
Yes | Human-readable name |
version |
str |
Yes | Semver version string |
plugin_type |
PluginType |
Yes | One of the 10 enum values |
description |
str |
No | Short description |
author |
str |
No | Author name |
source_package |
str |
No | PyPI package name |
tags |
tuple[str, ...] |
No | Capability tags |
Plugin Discovery¶
FCC uses importlib.metadata.entry_points() to discover plugins at runtime.
The PluginRegistry class handles discovery:
from fcc.plugins.registry import PluginRegistry
registry = PluginRegistry()
registry.discover()
# Get all persona plugins
persona_plugins = registry.get_plugins(PluginType.PERSONAS)
Cross-Plugin Orchestration¶
The CrossPluginOrchestrator class manages dependencies and interactions
between plugins:
from fcc.plugins.orchestration import CrossPluginOrchestrator
orchestrator = CrossPluginOrchestrator.from_yaml(
dependencies_path="plugin_dependencies.yaml",
interactions_path="plugin_interactions.yaml",
)
# Check ecosystem health
report = orchestrator.health_report(plugin_registry)
Plugin dependencies are declared in YAML files under src/fcc/data/ecosystem/:
plugin_dependencies.yaml-- declares which plugins depend on each otherplugin_interactions.yaml-- declares cross-plugin persona interactions
Testing Plugins¶
Unit tests¶
Test your plugin class directly:
from fcc_my_plugin.plugin import MyPersonaPlugin
def test_plugin_meta():
plugin = MyPersonaPlugin()
meta = plugin.plugin_meta()
assert meta.id == "fcc-my-personas"
assert meta.plugin_type == PluginType.PERSONAS
def test_persona_paths_exist():
plugin = MyPersonaPlugin()
for path in plugin.get_persona_paths():
assert path.exists(), f"Missing persona file: {path}"
Integration tests¶
Verify your plugin is discovered when installed:
from fcc.plugins.registry import PluginRegistry
def test_plugin_discovered():
registry = PluginRegistry()
registry.discover()
plugins = registry.get_plugins(PluginType.PERSONAS)
assert any(p.plugin_meta().id == "fcc-my-personas" for p in plugins)
Validating contributed data¶
If your plugin contributes YAML/JSON data files, validate them against the corresponding JSON schemas:
import json
import yaml
from jsonschema import validate
from fcc._resources import get_schema_path
def test_contributed_personas_valid():
schema_path = get_schema_path("persona")
with schema_path.open() as f:
schema = json.load(f)
with open("src/fcc_my_plugin/data/personas/custom.yaml") as f:
data = yaml.safe_load(f)
validate(instance=data, schema=schema)
Error Handling¶
The plugin system defines five error types in fcc.plugins.errors:
| Error | When raised |
|---|---|
PluginError |
Base class for all plugin errors |
PluginLoadError |
Plugin entry point cannot be imported |
PluginNotFoundError |
Requested plugin ID not in registry |
PluginValidationError |
Plugin fails validation checks |
PluginConflictError |
Two plugins have conflicting IDs or resources |
Publishing to PyPI¶
- Ensure
pyproject.tomldeclares the correct entry points. - Build the package:
- Upload to PyPI:
- Users install with:
The plugin is discovered automatically on the next
PluginRegistry.discover()call.
Best Practices¶
- Use the
fcc-prefix for plugin package names (e.g.,fcc-paom-plugin). - Include capability tags in
PluginMetafor governance traceability. - Validate contributed YAML/JSON against FCC schemas in your test suite.
- Pin your dependency on
fcc-agent-team-extto a minimum compatible version. - Document your plugin's personas, scorers, or templates in a README.
- Use
GovernancePluginto register custom quality gates that apply to your contributed personas.