Plugins have undeclared runtime dependencies — eliminate service locator pattern #57
Labels
No labels
Compat/Breaking
Kind/Bug
Kind/Competitor
Kind/Documentation
Kind/Enhancement
Kind/Epic
Kind/Feature
Kind/Security
Kind/Story
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Scope/Core
Scope/Cross-Plugin
Scope/Plugin-System
Scope/Single-Plugin
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
ultanio/cobot#57
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
The plugin dependency graph is a lie. Out of 23 plugins, 10 only declare a dependency on
config— but at runtime they're coupled to each other through:registry.get_by_capability()— a service locator that hides dependencies behind string lookupsos.environinstead of declared dependenciesConcrete example:
tools→walletThe
toolsplugin hardcodeswallet_balance,wallet_pay,wallet_receiveas LLM tools. At runtime it calls:But
toolsdoesn't declarewalletas a dependency. The graph shows no connection. If wallet isn't loaded, tools silently degrades — the agent gets tool definitions that return "No wallet available."Same pattern repeats for knowledge, and likely others.
Why this matters
Root cause
The
toolsplugin is a god object — it hardcodes tool definitions for other plugins instead of letting each plugin register its own tools. Combined withget_by_capability()as a service locator, this hides all real coupling.Proposed solution
1. Plugin-owned tool registration via extension point
Each plugin registers its own tools:
The loop collects tools from all plugins:
2. Eliminate
get_by_capability()for direct couplingWhere plugins need each other, declare it:
Keep
get_by_capability()only for truly optional/swappable providers (e.g.,llmcapability where ppq and ollama are interchangeable). But make optional deps explicit too:3. Declare environment dependencies
For #50 (secrets), but also for graph completeness:
Impact on other issues
Acceptance criteria
toolsplugin no longer hardcodes tool definitions for other pluginsloop.toolsextension pointget_by_capability()removed for direct plugin-to-plugin coupling (except swappable providers)PluginMetagainsoptional_dependenciesfieldos.environdirectly for secrets (deferred to #50 if needed)Phasing suggestion
loop.toolsextension point — plugins register own tools,toolsplugin becomes a thin executoroptional_dependenciesto PluginMeta — declare what's optional vs requiredget_by_capability()where inappropriatesecretsandsystem_depsto PluginMeta — from #50Architecture Review: Plugin Dependencies
Verdict: NEEDS-REVISION ✏️
The problem is real and well-diagnosed. The solution direction is right, but there's a simpler path using existing patterns.
✅ Problem Analysis: Confirmed
I verified the issue in code:
tools/plugin.py (lines 83-117) hardcodes wallet tool definitions:
tools/plugin.py (line 245) uses service locator without declaring the dependency:
Meanwhile, wallet/plugin.py declares:
This is exactly the hidden coupling described. The dependency graph shows no edge, but runtime coupling exists.
🔍 Existing Pattern: ToolProvider Interface
However, there's already a better pattern in the codebase that the proposal overlooks:
knowledge/plugin.py implements
ToolProvidercorrectly:And loop/plugin.py already aggregates via
AggregatedToolProvider:This pattern works. Knowledge plugin tools are discovered and executed correctly.
🛠️ Suggested Fix: Simpler Than Proposed
Skip
loop.toolsextension point. TheToolProviderinterface already serves this purpose. Instead:1. Make wallet implement ToolProvider (like knowledge does)
2. Remove wallet tools from tools plugin
The tools plugin becomes a clean shell/file executor - no more coupling to wallet or knowledge.
3. Add
optional_dependenciesto PluginMeta (agree with proposal)This is valuable for cases where you want to declare "I can use this if available" without requiring it.
⚠️ Concerns With Original Proposal
loop.toolsextension point is redundant — TheToolProviderinterface +all_with_capability("tools")already does this. Adding another mechanism creates two ways to do the same thing."Eliminate
get_by_capability()" — This is too aggressive.get_by_capability()is correct for swappable providers (llm, communication). The problem isn't the pattern, it's undeclared usage. Withoptional_dependencies, the graph becomes complete.The tools plugin isn't purely a "god object" — It does aggregate from other ToolProviders correctly (line 154-168). The issue is specific: it hardcodes wallet tools instead of wallet providing them.
✅ What I Agree With
optional_dependenciesfieldToolProviderinterfacesecrets/system_deps(deferred)📋 Revised Acceptance Criteria
WalletPluginimplementsToolProvider, moves tools from tools pluginPluginMetagainsoptional_dependencies: list[str]fieldtoolsplugin removes all wallet-related code (definitions + executors)ToolProviderpattern in CONTRIBUTING.md for future plugin authors🔄 Migration Path
Phase 1: Add
optional_dependenciestoPluginMeta(backwards compatible)Phase 2: Make
WalletPluginimplementToolProvider, remove wallet tools from tools pluginPhase 3: Audit other plugins for similar patterns (I didn't find others, but worth checking)
Reviewed by examining: base.py, interfaces.py, registry.py, tools/plugin.py, wallet/plugin.py, knowledge/plugin.py, loop/plugin.py