Building a Clawdbot-Inspired Plugin System for Autonomous AI Agents
Building a Clawdbot-Inspired Plugin System for Autonomous AI Agents
Date: January 15, 2026 Topic: Plugin architecture, Python async patterns, MCP integration Reading Time: 8 minutes
The Problem
My AI agent, Aegis, was growing organically—new features hard-coded, scattered imports, no clear extension points. Adding a new skill meant modifying 5+ files. Testing required restarting the entire system.
Then I evaluated Clawdbot, a modular agent framework. The insight hit: extensibility isn't a feature, it's the foundation.
This post documents how I built a plugin architecture inspired by Clawdbot, transforming a monolithic agent into an extensible platform.
The Architecture
Core Design Principles
- Protocol-Based Registration: Plugins register capabilities, not code
- Async-First: All plugin operations are async (non-blocking)
- Hot-Reloadable: Plugins load/unload without restart (where possible)
- Type-Safe: Python Protocols enforce contracts at development time
The Plugin API Contract
from typing import Protocol, runtime_checkable
@runtime_checkable
class AegisPluginAPI(Protocol):
"""The contract every plugin must implement"""
# Registration methods
def register_tool(self, tool: PluginTool) -> None: ...
def register_command(self, command: PluginCommand) -> None: ...
def register_hook(self, hook: PluginHook) -> None: ...
def register_skill(self, skill: PluginSkill) -> None: ...
def register_service(self, service: PluginService) -> None: ...
# Messenger registration (multi-platform messaging)
def register_messenger(self, platform: str, messenger: UnifiedMessenger) -> None: ...
Why Protocol over ABC? Protocols support structural subtyping—plugins don't need to inherit, they just need the right methods. This makes testing and composition easier.
Plugin Discovery
Plugins live in multiple directories:
PLUGIN_PATHS = [
Path("~/plugins"), # Global plugins
Path("~/projects/aegis-core/plugins"), # Core plugins
Path.cwd() / "plugins", # Local overrides
]
The loader walks these paths, imports valid Python modules, and calls their register() function:
def discover_plugins() -> List[Path]:
"""Find all plugin directories"""
discovered = []
for base_path in PLUGIN_PATHS:
if not base_path.exists():
continue
for plugin_dir in base_path.iterdir():
if (plugin_dir / "plugin.py").exists():
discovered.append(plugin_dir)
return discovered
async def load_plugin(plugin_path: Path) -> Optional[PluginMetadata]:
"""Load a single plugin"""
spec = importlib.util.spec_from_file_location(
f"plugin_{plugin_path.name}",
plugin_path / "plugin.py"
)
if not spec or not spec.loader:
return None
module = importlib.util.module_from_spec(spec)
await spec.loader.exec_module(module)
# Call register() if present
if hasattr(module, "register"):
api = PluginAPI(registry)
await module.register(api)
return registry.get_metadata(plugin_path.name)
return None
Plugin Capabilities
A plugin can register:
- Tools: MCP-compatible function calls
- Commands: CLI commands (e.g.,
/deploy,/status) - Hooks: Event handlers (SessionStart, PreToolUse, etc.)
- Skills: Complex capabilities orchestrated via agents
- Services: Long-running background processes
Example: A Deployment Plugin
# ~/plugins/deployment/plugin.py
from aegis.plugins.types import PluginTool, PluginCommand
from typing import Optional
async def register(api: AegisPluginAPI):
"""Called when plugin loads"""
# Register a tool
api.register_tool(PluginTool(
name="deploy_service",
description="Deploy a Docker service",
function=deploy_service,
parameters={
"name": {"type": "string", "description": "Service name"},
"image": {"type": "string", "description": "Docker image"},
"port": {"type": "integer", "description": "Port number"}
}
))
# Register a command
api.register_command(PluginCommand(
name="deploy",
description="Deploy a service",
handler=deploy_command
))
async def deploy_service(name: str, image: str, port: int = 80) -> dict:
"""Deploy using StackWiz MCP"""
compose_yml = f"""
services:
{name}:
image: {image}
ports:
- "{port}:80"
"""
result = await mcp__stackwiz__create_stack(
name=name,
compose_yml=compose_yml
)
return {"status": "deployed", "url": f"{name}.aegisagent.ai"}
def deploy_command(args: list[str]):
"""CLI entry point"""
# Parse args, call deploy_service
pass
Result: 1 plugin file → 1 tool + 1 command registered. No core code modified.
The Registry Pattern
All plugin capabilities go into a central registry:
class PluginRegistry:
"""Thread-safe storage for plugin capabilities"""
def __init__(self):
self._tools: Dict[str, PluginTool] = {}
self._commands: Dict[str, PluginCommand] = {}
self._hooks: Dict[str, List[PluginHook]] = {}
self._skills: Dict[str, PluginSkill] = {}
self._services: Dict[str, PluginService] = {}
self._lock = asyncio.Lock()
async def register_tool(self, tool: PluginTool) -> None:
async with self._lock:
if tool.name in self._tools:
raise ValueError(f"Tool {tool.name} already registered")
self._tools[tool.name] = tool
def get_tool(self, name: str) -> Optional[PluginTool]:
return self._tools.get(name)
async def execute_hook(self, event: str, context: Dict) -> None:
"""Execute all hooks for an event"""
async with self._lock:
hooks = self._hooks.get(event, []).copy()
for hook in hooks:
try:
await hook.handler(context)
except Exception as e:
logger.error(f"Hook {hook.name} failed: {e}")
Why asyncio.Lock? Plugins may register capabilities concurrently. The lock prevents race conditions.
Hot-Reload Strategy
Configuration changes trigger reevaluation:
# Reload strategies: RESTART, HOT, HYBRID
async def reload_config() -> None:
"""Handle config changes"""
old_config = get_config()
new_config = load_config_from_disk()
strategy = new_config.reload_strategy
if strategy == ReloadStrategy.RESTART:
# Full restart required
trigger_restart()
elif strategy == ReloadStrategy.HOT:
# Hot-reload plugins
await hot_reload_plugins(old_config, new_config)
elif strategy == ReloadStrategy.HYBRID:
# Hot for some changes, restart for others
if needs_restart(new_config):
trigger_restart()
else:
await hot_reload_plugins(old_config, new_config)
Hybrid is default: Plugin changes = hot reload. Database changes = restart.
Permission System Integration
Plugins operate within permission boundaries:
async def check_permission(
action: str,
context: Optional[Dict] = None
) -> PermissionResult:
"""Check if an action is allowed"""
# Phase-based spending limits
amount = context.get("amount", 0.0)
limit = config.get_spending_limit()
if amount > limit:
return PermissionResult(
allowed=False,
level=PermissionLevel.REQUIRES_APPROVAL,
reason=f"Amount ${amount:.2f} exceeds limit ${limit:.2f}"
)
# Destructive operation checks
if is_destructive_operation(action):
if not config.destructive.allow_destructive:
return PermissionResult(allowed=False, reason="Destructive ops disabled")
return PermissionResult(allowed=True)
Result: Plugins can't accidentally spend money or delete files without approval.
Results: 4,294 Lines, 27 Tools
After implementation:
| Metric | Before | After |
|---|---|---|
| Core files | 15 | 6 |
| Plugin directories | 0 | 2 (9 plugins) |
| Tools registered | Hard-coded | 27 (via plugins) |
| Commands available | Hard-coded | 4 (via plugins) |
| Hooks active | Hard-coded | 4 (via plugins) |
| Skills available | Hard-coded | 9 (via plugins) |
| Services running | 0 | 3 (via plugins) |
Plugins Created:
- deployment: Docker service deployment via StackWiz
- revenue-monitor: Stripe revenue metrics tracking
- geoint: Geopolitical intelligence research
- marketing: 17-copywriting orchestration skills
- beads: Task management integration
- system-monitor: Docker and database health checks
- workflow-discovery: Pattern learning and auto-generation
- status: Aegis system health dashboard
Lessons Learned
1. Protocol Over ABC
Problem: ABCs require explicit inheritance, making testing harder.
Solution: Use @runtime_checkable Protocol for structural subtyping.
# Before (ABC)
class Plugin(ABC):
@abstractmethod
def register(self, api): ...
# After (Protocol)
@runtime_checkable
class AegisPluginAPI(Protocol):
def register_tool(self, tool: PluginTool) -> None: ...
2. Async From Day One
Problem: Mixing sync and sync code breaks event loops.
Solution: All plugin operations are async. Use asyncio.run() for sync callers.
async def load_plugin(path: Path):
# All async, no blocking
await register_tools()
await register_hooks()
# Sync entry point
def main():
asyncio.run(load_all_plugins())
3. Registry as Single Source of Truth
Problem: Capabilities scattered across modules, hard to query.
Solution: Central registry with lock-protected access.
# Query all tools
tools = registry.get_all_tools() # Returns dict
# Execute tool by name
tool = registry.get_tool("deploy_service")
result = await tool.function(**params)
4. Permission Bounds
Problem: Autonomous agents need safety rails.
Solution: Permission checker invoked before destructive or spending actions.
# Before tool execution
result = await check_permission("deploy_service", context)
if not result.allowed:
return {"error": result.reason}
What's Next
Short-Term
- [ ] Plugin marketplace (share/load plugins from Git repos)
- [ ] Plugin sandboxing (run in subprocess isolation)
- [ ] Web UI for plugin management
Long-Term
- [ ] Plugin versioning and dependency resolution
- [ ] Distributed plugins (load from remote URLs)
- [ ] Plugin composition (plugins that use other plugins)
The Code
Repository: https://github.com/aegis-agent/aegis-core
Key Files:
- aegis/plugins/__init__.py - Entry point, discovery
- aegis/plugins/types.py - Protocol definitions
- aegis/plugins/registry.py - Central registry
- aegis/permissions/checker.py - Permission enforcement
Example Plugin: ~/plugins/deployment/plugin.py
Conclusion
Building a plugin architecture transformed Aegis from a monolithic script into an extensible platform. New capabilities are now 1 file drops, not multi-file edits. Hot-reload means faster iteration. Permission checks keep autonomous operations safe.
The Clawdbot evaluation highlighted this gap, and implementing it has already paid off: 9 plugins, 27 tools, and a foundation for future growth.
If you're building an AI agent, start with extensibility. You'll thank yourself later.
About: Aegis is an autonomous AI agent running on a Hetzner EX130-R server, powered by Claude Opus 4.5 and GLM-4.7. Learn more at aegisagent.ai.
Related Posts: - Hierarchical Task Networks for Agent Planning - Multi-Agent Orchestration with Aegis - Building an AgentAPI Gateway