Plugin registry, lifecycle manager, and native Claude full-chain adaptation layer for fusion-cowork.
Build on fusion-cowork. Expose every plugin to Claude. Zero adapter code.
English | 简体中文
Plugin registry, lifecycle manager, and native Claude full-chain adaptation layer built on
fusion-cowork. Every plugin auto-exposes as a Claude Skill and an MCP Tool. Zero per-plugin adapter code.
简体中文版见 README_CN.md。
fusion-plugins-ecosystem is the upper-layer submodule of fusion-cowork. It is not a standalone project — process hosting, permission control, log collection, and resource throttling are all provided by fusion-cowork. This package only adds:
- Plugin registry — declarative manifests, param schemas, capability declarations
- Lifecycle manager — load/unload/enable/disable/hot-reload, timeout meltdown, auto-restart, INLINE/PROCESS sandbox modes
- Claude full-chain adaptation — plugins auto-convert to Claude Code Skills, Agents, Plugin Bundles; expose as MCP Tools (2026-07-28)
- Token metering — split Claude model consumption vs plugin local compute, JSON persistence
- MCP protocol stack — stdio/SSE/HTTP transports, JSON-RPC 2.0 handler, MCP Server CLI
- Plugin sandbox — process isolation via subprocess IPC, resource limits, heartbeat monitoring
- Cowork context bridge — reuse fusion-cowork's MCP gateway, hardware scheduler, session pool
fusion-plugin-ecosystem ← this package: registry, lifecycle, Claude adaptation, sandbox, MCP server
↓ depends on API
fusion-cowork runtime ← base runtime: MCP gateway, hardware, sessions, logging
↓
fusion-mlx core ← Mac local inference kernel (Metal/MLX)
| Access | Constant | Gateway entry |
|---|---|---|
| Claude Desktop client | CLAUDE_DESKTOP |
list_mcp_tools + gateway_info (stdio transport) |
| VS Code Claude Code plugin | CLAUDE_CODE |
dispatch_subagent + list_subagent_capable_plugins |
| Web Claude | CLAUDE_WEB |
invoke_mcp_tool (HTTP/SSE relay) |
| Volcengine Claude Coding Plan | CLAUDE_VOLCENGINE |
store_credentials("volcengine_claude", ...) |
Bidirectional:
- Forward: Claude calls all fusion local capabilities (image/video generation, MLX local inference, file ops, quantization tools)
- Reverse: fusion-cowork proactively spins up Claude Code subagents for batch refactors, PR generation, code optimization
See docs/CLAUDE_COMPATIBILITY.md for the full spec.
# Clone
git clone https://github.com/dahai80/fusion-plugins-ecosystem.git
cd fusion-plugins-ecosystem
# Create venv (avoid PEP 668)
python3 -m venv .venv
.venv/bin/pip install -e ../fusion-cowork # base runtime (not on PyPI)
.venv/bin/pip install -e ".[test]"
# Run tests
.venv/bin/python -m pytest --cov=fusion_plugins_ecosystem --cov-report=term-missing -q
# → 634 passedimport fusion_plugins_ecosystem as fpe
# Registry + built-in caveman compressor
registry = fpe.PluginRegistry()
registry.register_builtin()
# Claude full-chain gateway
lifecycle = fpe.PluginLifecycle(registry)
gw = fpe.ClaudeGateway(registry, lifecycle)
# Forward: expose to Claude
skills = gw.export_skills() # → Claude Skill dicts
tools = gw.list_mcp_tools() # → MCP tool descriptors
# Forward: Claude calls via MCP
resp = await gw.invoke_mcp_tool("caveman_compress", {"text": "# comment\ncode"})
# → {"content": [{"type":"text","text":"{\"compressed\":\"code\",...}"}], "isError": false}
# Reverse: pull Claude Code subagent
task = fpe.SubagentTask(
name="compress-session",
plugin_id="caveman_compress",
arguments={"text": "a\n\n\n\nb"},
)
result = await gw.dispatch_subagent(task)
# → {"state": "completed", "result": {"compressed": "a\n\nb", ...}}from fusion_plugins_ecosystem.registry import (
PluginCapability,
PluginCategory,
PluginManifest,
PluginParam,
)
def my_plugin(desk, params):
desk.log("my_plugin", "INFO", "called", chars=len(params["text"]))
return {"echo": params["text"].upper()}
MY_MANIFEST = PluginManifest(
id="my_echo",
name="Echo Plugin",
version="0.1.0",
category=PluginCategory.CUSTOM,
description="Echoes input text uppercased.",
capabilities=[
PluginCapability.MCP_TOOL, # auto-expose as MCP Tool
PluginCapability.CLAUDE_SKILL, # auto-convert to Claude Skill
],
params=[
PluginParam(name="text", type="string", description="Input", required=True),
],
entry_point=my_plugin,
timeout_seconds=60,
)Register once → it auto-appears in Claude's tool catalog and MCP tools/list. No adapter code. See docs/PLUGIN_DEVELOPMENT.md.
| Doc | Content |
|---|---|
| docs/ARCHITECTURE.md | Layering, module map, data flow (forward/reverse), capability mapping, pain points solved |
| docs/API.md | Full public API reference: PluginRegistry, PluginLifecycle, ClaudeGateway, TokenMeter, DeskRuntime, EcosystemConfig |
| docs/CLAUDE_COMPATIBILITY.md | Three-layer compat, forward/reverse flows, volcengine auth, MLX visual backend, token metering, config toggles |
| docs/PLUGIN_DEVELOPMENT.md | Plugin authoring guide: manifest, capabilities, params schema, async, lazy entry, testing checklist |
| examples/ | 9 runnable plugin samples (echo → file_access): each a manifest + test + README. pytest examples/ |
fusion-plugins-ecosystem/
├── pyproject.toml
├── LICENSE ← Apache 2.0
├── README.md ← this file (English)
├── README_CN.md ← 中文版
├── docs/
│ ├── ARCHITECTURE.md
│ ├── API.md
│ ├── CLAUDE_COMPATIBILITY.md
│ └
PLUGIN_DEVELOPMENT.md
├── fusion_plugins_ecosystem/
│ ├── __init__.py ← top-level exports + Lazy Import
│ ├── desk_runtime.py ← fusion-cowork runtime handle wrapper
│ ├── registry.py ← plugin registry + frozen manifest
│ ├── lifecycle.py ← load/enable/execute + meltdown + restart + INLINE/PROCESS
│ ├── sandbox.py ← plugin sandbox (process isolation, IPC)
│ ├── transport.py ← MCP transport layer (stdio/SSE/HTTP)
│ ├── jsonrpc.py ← MCP JSON-RPC 2.0 handler
│ ├── server.py ← MCP Server entry + CLI
│ ├── schema.py ← shared schema (SandboxMode, MCPAnnotations, types)
│ ├── skill_adapter.py ← plugin → Claude Code Skill bundle
│ ├── agent_adapter.py ← plugin → Claude Code Agent .md
│ ├── plugin_bundle.py ← Claude Code Plugin bundle generator
│ ├── claude_adapter.py ← legacy adapter (backward compat)
│ ├── mcp_exporter.py ← plugin → MCP Tools
│ ├── claude_gateway.py ← unified Claude full-chain gateway
│ ├── token_meter.py ← unified token accounting + WAL append-write persistence
│ ├── metrics.py ← observability: Prometheus counters/gauges + registry
│ ├── cluster_bridge.py ← multi-node distributed state bridge (cowork cluster sync + failover)
│ ├── connector.py ← external MCP connector framework (pack install, MCP client, HIGH_RISK approval gate)
│ ├── connector_packs.py ← first-party connector packs (Feishu/DingTalk/WeCom/Mail/Calendar/Notion)
│ ├── marketplace.py ← skill/connector pack marketplace (JSON index, install/uninstall)
│ ├── rest_connector.py ← declarative REST/webhook connector docs (validation, read/write channels, execution contract)
│ ├── extensions_console.py ← Extensions console backend (issues #30/#41/#42/#43): Connectors + Plugins·MCP + Skills + Sub-agents tabs, capabilities hub, bundle flows, scope revoke + audit
│ ├── audit_events.py ← authorization audit event stream → fusion-security JSONL pipeline (issue #43)
│ ├── console_app.py ← FastAPI console app + `fusion-plugin-console` CLI (port 8795)
│ ├── shell_registry.py ← fusion-gateway shell module registration + fm- key persistence (#30)
│ ├── logging_setup.py ← structured JSON logging + correlation_id (P3-6)
│ ├── config.py ← one-toggle config panel + observers
│ ├── hook_adapter.py ← Claude Code Plugin hooks adapter
│ └── builtin/
│ ├── __init__.py
│ └── caveman_compress.py ← built-in token compressor
├── web/
│ ├── extensions.html ← console GUI (Connectors + Plugins·MCP + Skills + Sub-agents tabs, hub search + market groups)
│ ├── app.css ← fs-* design tokens (verbatim)
│ └── shell.js ← sidebar injector (brand "Fusion Plugins")
├── examples/ ← 9 runnable plugin samples (ex01_echo … ex09_file_access)
│ ├── README.md ← scenario index + run instructions
│ ├── ex01_echo/ ← minimal sync plugin
│ ├── ex02_async/ ← async entry point
│ ├── ex03_skill_export/ ← Skill bundle export
│ ├── ex04_subagent/ ← reverse subagent dispatch
│ ├── ex05_mlx_chat/ ← local MLX inference
│ ├── ex06_vram/ ← VRAM consumer (INLINE)
│ ├── ex07_long_task/ ← timeout meltdown + restart
│ ├── ex08_process_sandbox/ ← PROCESS sandbox isolation
│ └── ex09_file_access/ ← file permission gating
└── tests/ ← 634 tests
├── test_caveman.py
├── test_claude_adapter.py
├── test_claude_gateway.py
├── test_config.py
├── test_desk_runtime.py
├── test_lifecycle.py
├── test_mcp_exporter.py
├── test_registry.py
├── test_registry_full.py
├── test_token_meter.py
├── test_jsonrpc.py
├── test_jsonrpc_plugins.py
├── test_transport_server.py
├── test_sandbox.py
├── test_hook_adapter.py
├── test_phase3_adapters.py
├── test_phase4_meter_config.py
├── test_schema.py
├── test_product_audit.py ← production audit P0-P3 fix regressions
├── test_metrics.py ← observability metrics + /metrics endpoint
├── test_load.py ← load/concurrency safety (lifecycle semaphore, metrics thread-safety, sandbox spawn storm, token-meter pressure)
├── test_cluster_bridge.py ← multi-node distributed state (cross-node plugin/vRAM visibility, failover evict, lifecycle sync, cluster-disabled no-op)
├── test_connector.py ← connector framework: pack validation, registry bridge, HIGH_RISK approval gate, persistence, stdio/http MCP client round-trips
├── test_connector_packs.py ← first-party packs structural validation + Feishu acceptance shape + lifecycle load
├── test_marketplace.py ← marketplace: builtin catalog, JSON index load/override, install/uninstall, HTTPS-only constraint
├── test_rest_connector.py ← declarative connector docs: validation, read/write channel split, persistence, execution contract vs local HTTP server
├── test_extensions_console.py ← console backend: connector install/enable/harness, vault (Desk + local Fernet fallback), MCP intake probe/register/tick/call, plugin tick/remove/install, resync, spine derivation
├── test_console_app.py ← FastAPI console: endpoint contract vs GUI, connector/MCP/plugin full flows, static serving
├── test_hub.py ← capabilities hub (issue #41): builtin bootstrap, four-tab classification, cross-type search, market groups, /api/hub endpoint
├── test_install_flow.py ← bundle install/OAuth/update/uninstall flows (issue #42): per-item states, scope grants, per-use policy, reverse lookup
├── test_audit_flow.py ← authorization audit events → security JSONL pipeline (issue #43): trace-id linkage, scope revoke re-bridge, used-by, HTTP endpoints
├── test_registry_contract.py ← stable registry read contract registry/1.0 (issue #44): kinds/install state, connector auth single-source (pending/granted/revoked), detail, HTTP endpoints
├── test_soak.py ← long-run stability (token-meter cap, log buffer cap, session LRU cap, counter monotonic no-drift, lifecycle sustained execute no-residual)
└── test_integration.py
External MCP connector framework: fusion acts as an MCP client consuming arbitrary external MCP servers, with first-party domestic connector packs and a skill/connector marketplace.
import fusion_plugins_ecosystem as fpe
from fusion_plugins_ecosystem.connector_packs import get_builtin_pack
desk = fpe.DeskRuntime()
registry = fpe.PluginRegistry(desk)
creg = fpe.ConnectorRegistry(
desk=desk, registry=registry, persist_path="~/.fusion-ecosystem/connectors.json"
)
lifecycle = fpe.PluginLifecycle(registry)
# 1. Install a first-party pack (Feishu / DingTalk / WeCom / Mail / Calendar / Notion)
creg.install_pack(get_builtin_pack("feishu"))
# 2. Tools are bridged into the plugin registry → exposed as MCP tools / cowork nodes
# e.g. plugin id "connector__feishu__im.message.send"
# 3. HIGH_RISK tools need explicit approval before execution
creg.approve_tool("feishu", "im.message.send")
# 4. Execute via lifecycle (routed through the external MCP server)
resp = await lifecycle.execute(
"connector__feishu__im.message.send", {"chat_id": "oc_xxx", "content": "hello"}
)Marketplace — distribute connector packs and cowork markdown skill packages:
from fusion_plugins_ecosystem.marketplace import Marketplace
market = Marketplace(
creg, marketplace_index="https://my-company/marketplace/index.json"
) # private index
entries, warnings = (
market.load_index()
) # builtin catalog + remote index (index overrides builtin on id collision)
market.install("feishu") # builtin pack: install directly
market.install("demo_skill") # skill_package: git/local source → local install dir
market.list_installed()Key properties:
- Declarative packs — a pack is pure data (transport/command/url + credential providers + tool specs with risk levels); the external MCP server command defaults are overridable
- Credentials stay local — providers resolved from
DeskRuntime.get_api_key(Fernet-encrypted) at connect time; stdio → env vars, HTTP →Authorizationheader - Approval gating —
HIGHrisk tools raisePermissionErroruntilapprove_tool(); all read-only tools areLOWrisk by convention (enforced by tests) - INLINE-only — connector tools run INLINE (the external MCP server is already an isolated process);
sandbox_default_mode=processblocks installation - First-party packs: Feishu (official
lark-mcp), DingTalk, WeCom, Mail (IMAP/SMTP), Calendar (CalDAV), Notion (official@notionhq/notion-mcp-server)
Standalone FastAPI console (fusion-plugin-console, port 8795) hosting four tabs
(the two migrated verbatim from f-ecommerce, issue #30 E3, plus the capabilities hub
tabs, issue #41): Connectors, Plugins·MCP, Skills, and Sub-agents.
Business logic generalized — no e-commerce semantics remain.
pip install -e ".[console]"
fusion-plugin-console --port 8795 # → http://127.0.0.1:8795- Connectors tab — declarative JSON connector packages (
rest_connector.py): install = full static validation + disk persistence; enable = runtime bridging of READ channels (GET) asconnector__<id>__<op>capability plugins (auto-listed, lifecycle ENABLED); WRITE channels (POST/PUT/PATCH/DELETE) enter a human review queue and are never auto-listed; encrypted credential vault (Desk Fernet store, falls back to console-localvault.jsonchmod 600 when Desk config_center is absent); harness = static validation + live check of the first read operation using vault credentials. - Plugins·MCP tab — declarative plugin install (plugin.json manifest, no arbitrary code),
tick/untick reconcile via lifecycle enable/disable, delete; remote MCP server probe & register
(initialize + tools/list, candidate tools reported); per-tool tick bridges
mcp__<server>__<tool>capability plugins dispatched over JSON-RPCtools/call. - Capability spine — derived from disk facts (connector docs + MCP ticks), rebuilt at boot
via
resync(); in-process registry holds no second source of truth. - Capabilities hub (issue #41) — the Skills and Sub-agents tabs surface the real plugin
registry (console bootstraps built-ins via
register_builtin(), idempotent when the inject side already registered them): skills = plugins withclaude_skillcapability, sub-agents = plugins withsubagentcapability; bridge ids (connector__*/mcp__*) stay on their own tabs. A cross-type search box matches id/name/description (case-insensitive) across all four tabs, and market groups (installed / updates / recommended) derive from the connector pack + skill marketplace catalog with semantic version comparison (1.0≡1.0.0). GUI ships zh/en toggle and follows the shell host dark/light theme. - Endpoints —
/api/whoami,/api/capabilities,/api/hub?q=<query>(issue #41),/api/extensions/connectors[/{id}/enable|disable|harness](DELETE removes a standalone connector — unbridge + doc + per-use policy cleanup),/api/adapters/{ref}(vault),/api/extensions/plugins[/{name}/tick],/api/extensions/mcp[/{sid}/tick],/api/hub/audit,/api/hub/connectors/{id}/revoke,/api/hub/capabilities/{cap}/used-by(issue #43),/api/registry/capabilities[/{id}](issue #44 stable read contract),/health. - Install / authorize / update / uninstall flows (issue #42) — marketplace entries
(incl. first-party packs) install as bundles from the Recommended row: skills enable,
sub-agents register, connectors land as
pending_authuntil OAuth. Per-item enable/disable after install. Connector OAuth completes in the hub (modal): per-read-scope checkboxes (ungranted read channels are never bridged), credentials go to the encrypted vault, WRITE channels bind to a per-use confirmation policy (GET /api/hub/connectors/{id}/policy— the integration point for the cowork approval system, upstream fusion-cowork#225; until it ships, execution stays blocked by the #30 write review queue). Updates are changelog-first with opt-in new items; uninstall shows the blast radius (reverse lookup of cowork nodes referencing the bundle's capabilities) before confirmation. Bundle state persists at~/.fusion-plugins-ecosystem/extensions/bundles.json. - Marketplace bundle kind (issue #42) — index entries with
kind: "bundle"carry anitemslist (skill/subagent/connector/connector_pack/skill_packagerefs); index source resolves fromFUSION_PLUGIN_MARKETPLACE_INDEX(local JSON or HTTPS URL, unset = builtin catalog only). - Authorization audit trail (issue #43) — every hub authorization action emits an
audit event into the fusion-security JSONL pipeline
(
~/.fusion_security/audit/audit_<tenant>.jsonl, same line shape asfusion_security.engine.tenant.audit.AuditEntry— the security workbench (fusion-security#61) reads it without any code coupling; file contract only, no cross-project import). Events:hub.bundle.install(medium) /hub.bundle.item.install(low) /hub.bundle.item.enable|disable/hub.connector.oauth.grant(low, one per scope) /hub.connector.scope.policy(high, per-use WRITE policy) /hub.bundle.update/hub.bundle.uninstall/hub.connector.oauth.revoke(medium) — install/OAuth/policy events under one bundle flow share atrace_id(formatfp-<hex16>) for cross-service correlation. The hub GUI shows the per-item audit history (audit button on each bundle item) with granted-scope revoke buttons. - Audit endpoints (issue #43) —
GET /api/hub/audit(query byresource_id/resource_type/action/trace_id, read-only scan),POST /api/hub/connectors/{id}/revoke(body{"scopes": [...]}— revokes granted read scopes: re-bridges the connector with the remaining scopes, or disables it when none remain; effect lands on next load per the issue #43 contract),GET /api/hub/capabilities/{capability}/used-by(reverse lookup — cowork nodes referencing the capability, same engine as the uninstall blast radius). - Stable registry read contract (issue #44, PC-4) —
GET /api/registry/capabilities(contract: "registry/1.0") is the single source of truth for external capability surfaces: the cowork capability marketplace (fusion-cowork#228) and the agent builder suggestions (fusion-agent-studio#529) consume it directly — no sync jobs, no forked capability caches. Each item:{id, kind (plugin|skill|subagent| connector|mcp_tool), name, version, description, enabled, auth}. Connector read channels expand from the connector doc (not just live bridges) so revoked/pending channels stay visible withauth.state=granted|pending|revoked(+granted,granted_scopes,all_scopes_granted,hub_url— authorization actions happen in the hub, never in a consuming surface).GET /api/registry/capabilities/{id}returns the detail (same fields +per_use_scopesfor connectors +in_use_byreverse lookup). Hub state changes (install/disable/revoke/uninstall) propagate on the consumer's next fetch —enabled: falseitems must stop being offered. Contract stability: additive field changes only, no semantic renames. - Env vars —
FUSION_PLUGIN_CONNECTOR_DIR(default~/.fusion-plugins-ecosystem/connectors),FUSION_PLUGIN_EXTENSIONS_DIR(default~/.fusion-plugins-ecosystem/extensions),FUSION_PLUGIN_KEY_SALT(key-derivation salt; unset = empty in dev with a warning, fail-fast underFUSION_PROFILE=prod— no default literal, seearchitecture/config-conventions.md§3),FUSION_PLUGINS_ECOSYSTEM_DATA_DIR(issue #38 single-point data dir viafusion_core.config.service_data_dir; default~/.fusion-plugins-ecosystem),FUSION_PLUGINS_ECOSYSTEM_PORT/FUSION_PLUGINS_ECOSYSTEM_CONSOLE_PORT(issue #38 port resolution viafusion_core.config.service_port),FUSION_PLUGIN_MARKETPLACE_INDEX(issue #42: marketplace index for bundle install — local JSON path or HTTPS URL; unset = builtin first-party catalog only),FUSION_PLUGIN_AUDIT_DIR(issue #43: audit JSONL sink directory, default~/.fusion_security/audit),FUSION_PLUGIN_AUDIT_TENANT(defaultsystem),FUSION_PLUGIN_AUDIT_SINK(offdrops events — test/offline isolation).
Long-running entry points set their process title via setproctitle (base dependency,
silent no-op fallback if unavailable): fusion-plugin-server stdio,
fusion-plugin-server sse :8765, fusion-plugin-console :8795 — ps/Activity Monitor
show the product name and role instead of a bare Python.
The console registers itself as a fusion-gateway shell module — the shell sidebar shows a
Capabilities (能力中心) entry at /shell/fusion-plugins-ecosystem/ (issue #40, DD-2 hub
form); stopping the console greys
the entry (heartbeat expiry), restarting re-registers it. Same pattern as fusion-cowork #97/#168.
FUSION_ADMIN_KEY=<web_shell.admin_key> \
fusion-plugin-console --port 8795- Registration —
shell_registry.pyPOSTsweb/manifest.json(idfusion-plugins-ecosystem, upstreamhttp://127.0.0.1:8795) then heartbeats every 15s; gateway restart (404) or key rotation (401) triggers transparent re-registration. Registration is OFF unlessFUSION_ADMIN_KEYresolves (dev: missing key = empty + warning; prod: missing key =RuntimeErrorfail-fast). The gateway address resolves viafusion_core.config.gateway_base_url()(FUSION_GATEWAY_BASE_URL, defaulthttp://127.0.0.1:11432) — issue #38 single-point resolution. - fm- module key (K1/K4) — with no static api_key in the registration body the gateway issues
an
fm-service key (returned exactly once), persisted 0600 at~/.fusion-plugins-ecosystem/web_module_key; heartbeats authenticate with it. - API auth — when a key exists (static
FUSION_PLUGINS_CONSOLE_API_KEYand/or issuedfm-key, dual-active),/api/*requiresAuthorization: Bearer/x-api-key(the gateway proxy injects the module key); with no keys configured the API stays open for loopback-only use. Static pages are session-gated at the gateway, not upstream. - Path-prefix aware GUI —
extensions.html/shell.jsresolve the API base from - Theme follow (#33) — embedded in the shell the iframe follows the host theme push (
postMessage{type:"event", event:"theme"}, same-origin only); standalone reads the initial mode fromlocalStorage["fusion.shell.theme"](legacyfs_themecookie +prefers-color-schemefallbacks) and the sidebar toggle persists to both.location.pathname, so the same build works standalone (/) and behind the gateway (/shell/fusion-plugins-ecosystem/).
EcosystemConfig — all toggles default to True for native Claude compatibility.
| Toggle | Controls | Off behavior |
|---|---|---|
enable_claude_mcp |
list_mcp_tools, invoke_mcp_tool |
returns [] / isError=True |
auto_export_claude_skill |
export_skills |
returns [] |
default_mount_compressor |
export_default_mounted_skills |
returns [] |
subagent_timeout_destroy |
dispatch_subagent |
no unload on timeout |
enable_volcengine_claude_plan |
volcengine credential store/get | refused / None |
enable_mixed_quantization |
mlx_visual_backend |
raises RuntimeError |
mcp_transport |
MCP transport type | stdio / sse / http |
sandbox_default_mode |
Default sandbox mode | inline / process |
max_token_records |
Max token meter records | pruning threshold |
token_persist_path |
Token persistence file | None = in-memory |
enable_connector_framework |
External MCP connector install/bridge | install raises RuntimeError |
connector_persist_path |
Connector registry persistence file | None = in-memory |
connector_marketplace_index |
Marketplace index (local JSON path or HTTPS URL) | None = builtin catalog only |
vram_limit_mb |
Total vRAM budget (synced to DeskRuntime.vram_total_mb) |
0 = unlimited |
max_concurrent_plugins |
Inline execution concurrency gate (asyncio.Semaphore) |
unbounded |
log_level |
Root logger level (applied on MCPServer init) |
INFO |
subagent_timeout_seconds |
Subagent execution timeout | 600 |
max_auto_restart |
Crash auto-restart cap | 3 |
heartbeat_stale_seconds |
Heartbeat staleness threshold | 120 |
from fusion_plugins_ecosystem import EcosystemConfig
# All-on by default
config = EcosystemConfig()
# Disable MCP exposure
config.enable_claude_mcp = False
# Persist via Desk config center
d = config.to_dict()
restored = EcosystemConfig.from_dict(d)| Pain point (from PRD) | Solution |
|---|---|
| Subagent runs 40 min, zero token consumption, no logs | TokenMeter warns on wall>60 ∧ total=0; all logs via DeskRuntime.log |
| vRAM contention across plugins | DeskRuntime.acquire_vram enforces vram_total_mb budget ledger |
| MCP port conflicts | fusion-cowork single MCP gateway; MCPExporter multiplexes |
| Subagent hangs, no unified restart | PluginLifecycle timeout meltdown + _maybe_restart (≤ MAX_RESTART) |
| Heartbeat stall | PluginLifecycle._watch_loop flags HEARTBEAT_STALE → TIMEOUT |
A full architecture audit (audit/fusion-plugins-ecosytem--audit-result-0830.md) surfaced
20 P0–P3 findings; all fixable in-repo are addressed:
| ID | Finding | Fix |
|---|---|---|
| A3 | Studio config keys semantically reversed | log_level/vram_limit_mb/max_concurrent_plugins map to real backend fields (no longer to mcp_transport/mcp_port/max_auto_restart) |
| R6 | SSE/HTTP zero-auth + plaintext API keys | Bearer-token auth (FUSION_PLUGIN_AUTH_TOKEN env, constant-time compare); API keys at-rest Fernet-encrypted (enc: prefix, plaintext passthrough for back-compat) |
| A4 | config/install zero-persistence | plugins/config.set, plugins/install, plugins/uninstall persist to Desk config_center; MCPServer.start() restores installed plugin set |
| R1 | PROCESS sandbox _DeskProxy no-op |
Worker → host resource-proxy RPC (acquire_vram/release_vram/mlx_chat/get_api_key/check_file_permission) over stdin/stdout, thread-safe future resolution |
| A2 | config → lifecycle break, dead fields | MCPServer/MCPHandler inject config into PluginLifecycle; timeout/restart/heartbeat thresholds config-driven with class-constant fallback |
| A5 | token_meter ↔ config break | MCPHandler/MCPServer construct TokenMeter with max_token_records/token_persist_path from config |
| R2 | inline unbounded thread pool | asyncio.Semaphore (max_concurrent_plugins) gates to_thread inline execution |
| R3 | _maybe_restart no backoff |
exponential backoff min(2**(n-1), 60) (skipped when no config, preserving test immediacy); double-check plugin existence post-backoff; enable failure → CRASHED |
| R4 | token_meter full-rewrite per record | throttled persistence (batch/interval) + explicit flush() |
| R5 | per-handler rate limit | session + rate-limit state shared on DeskRuntime (cross-handler single source, lock-guarded) |
| R7 | log lock contention | get_logs snapshots under lock, filters outside |
| E1 | spawn passes empty config | _invoke_sandbox forwards config.to_dict() to worker |
| E2 | cpu_limit decorative |
RLIMIT_CPU enforced in worker (SIGXCPU on exceed) |
| E3 | unload fire-and-forget | kill task tracked in _pending_kill_tasks to prevent GC |
| E4 | dispatch_subagent uncaught enable exception |
enable errors caught, logged, returned as failed state |
| E5 | version string compare not semver | _version_key tuple parse ("1.0" == "1.0.0") |
| E6 | in-function repeated import json |
hoisted to module top (jsonrpc.py, claude_gateway.py) |
| E7 | claude_adapter legacy double logic |
SSOT build_skill_dict in skill_adapter.py, legacy adapter delegates |
| E8 | tests mask config break | _make_handler accepts config; wiring test asserts config drives lifecycle/token_meter |
| A1 | zero cluster/multi-node awareness | out of scope — requires cross-project distributed state layer; upstream issue fusion-cowork#79 |
SSE/HTTP transports are remote-capable and therefore require a Bearer token when exposed beyond localhost. Set it via environment variable (never persisted to config):
FUSION_PLUGIN_AUTH_TOKEN="$(openssl rand -hex 32)" fusion-plugin-server --transport http --port 8765Requests without Authorization: Bearer <token> get 401 Unauthorized. stdio transport
(local subprocess) is exempt.
fusion-studio consumes this package over HTTP JSON-RPC. The SwiftUI client
(PluginBridge.swift) POSTs {"jsonrpc":"2.0",...} to http://<host>:<port>/rpc
and reads json["result"] as a dict with named keys (not an array). The
MCPHandler.handle dispatcher exposes 15 plugins/* methods whose result
envelopes are shaped to match each PluginEcosystemModels.swift fromDict
contract exactly:
| Method | Result envelope | Studio model |
|---|---|---|
plugins.ping |
{pong} |
health check |
plugins/list |
{plugins[]} |
PluginListItem (id/name/category/version/description/author/enabled/installed) |
plugins/install |
{ok} |
enable plugin |
plugins/uninstall |
{ok} |
disable + unload |
plugins/config.get |
7 Studio-named keys | EcosystemConfig (sandbox_mode/auto_update/max_concurrent_plugins/log_level/token_budget/vram_limit_mb/mcp_enabled) |
plugins/config.set |
{ok} |
single key-value pair (params IS the pair) |
plugins/states |
{states[]} |
PluginStateInfo (id/plugin_id/state/pid/start_time:str/uptime:int/error_count/last_error) |
plugins/state.get |
state dict | PluginStateInfo |
plugins/state.list |
{plugins[]} |
filtered by state |
plugins/token.records |
{records[]} |
TokenRecord (id/plugin_id/prompt_tokens/completion_tokens/total_tokens/timestamp:str/model) |
plugins/token.prune |
{ok} |
prune by max_age_seconds |
plugins/vram.usage |
{total_mb,used_mb,free_mb,by_plugin[]} |
VRAMUsage + VRAMPluginEntry (allocated_mb/peak_mb) |
plugins/logs.stream |
{entries[]} |
PluginLogEntry (id:str/plugin_id/level/message/timestamp:str) |
plugins/mcp.sessions |
{sessions[]} |
MCPSession (id/session_id/plugin_id/server/status/tool_count/connected_at:str) |
plugins/mcp.sessions.prune |
{ok} |
prune by max_age_seconds |
The backend EcosystemConfig uses different field names than Studio; the
plugins/config.* handlers project between the two namespaces. MCPServer
auto-registers built-ins (caveman_compress) on start(), so
plugins/list is discoverable without manual registration. Node discovery
(desk.list_nodes()) returns [] when fusion-cowork is not installed —
the integration path requires fusion-cowork to host the /rpc endpoint
(upstream gap, tracked separately).
MCPServer.start() blocks on an instance-level stop_event for all
transports (stdio/sse/http), so the standalone CLI (fusion-plugin-server)
stays alive until stop() or SIGTERM — previously sse/http returned
immediately and the process exited. Verified: fusion-plugin-server --transport http stays alive and /rpc returns caveman_compress v0.3.4
and {pong: true}.
.venv/bin/python -m pytest --cov=fusion_plugins_ecosystem --cov-report=term-missing -qLatest run: 634 passed.
| Test file | Tests | Covers |
|---|---|---|
test_desk_runtime.py |
30 | vRAM / logging / permissions / API keys / MLX / node bridge / scheduler bridge |
test_lifecycle.py |
40 | load/enable/disable/unload/execute/timeout/crash/restart/watcher/idempotency + _enable_with_retry (vRAM transient retry, permanent-error no-retry) |
test_mcp_exporter.py |
13 | list_tools / call_tool / gateway_info / manifest_to_mcp_tool |
test_token_meter.py |
22 | TokenRecord / record / measure / summary / stuck-subagent warning / WAL append+compaction (round-trip, crash-recovery dedup, threshold) |
test_claude_adapter.py |
13 | export_one / export_all / export_default_mounted / param types / enum / required |
test_claude_gateway.py |
45 | three-layer compat / forward-reverse / auth / config toggles / token integration |
test_config.py |
9 | defaults / to_dict / from_dict / roundtrip / partial / unknown key |
test_registry_full.py |
15 | register / unregister / list / category / default_mounted / enum values |
test_caveman.py |
22 | _compress_text / caveman_compress / CAVEMAN_MANIFEST fields |
test_registry.py |
13 | (legacy) registry + adapter + exporter + caveman integration |
test_hook_adapter.py |
8 | HookAdapter event mapping / capability filtering |
test_transport_server.py |
27 | SSE/HTTP/stdio transport + MCPServer start/stop lifecycle + CLI main() + SSE queue-bound drop-on-full |
test_jsonrpc_plugins.py |
25 | Studio plugins/* 15-method dict envelopes + exact-key matching + config-driven wiring (E8) |
test_metrics.py |
20 | observability counters/gauges / Prometheus render / label escaping / /metrics endpoint / path counters (lifecycle/MCP/sandbox/vram/session) |
test_load.py |
8 | load/concurrency safety: lifecycle concurrent execute + max_concurrent semaphore, metrics thread-safety (counter/gauge/render), token-meter high-freq + concurrent record, PROCESS sandbox spawn storm no-leak |
test_cluster_bridge.py |
9 | multi-node distributed state: cross-node plugin enable/disable + vRAM visibility, stale-node failover evict (clears state/vRAM), heartbeat keeps-alive, cluster_bridge singleton sync, lifecycle enable/disable cluster sync, DeskRuntime cluster query methods, cluster-disabled no-op degradation |
test_soak.py |
8 | long-run stability: token-meter cap holds under 50k records, log buffer deque cap, session _MAX_SESSIONS LRU cap + calls-list bound, counter 100k monotonic inc no-drift (labeled buckets), lifecycle 500 sustained execute no-residual, token-meter 8k record + query aggregate consistent |
test_logging_setup.py |
10 | structured JSON logging: JsonFormatter output + correlation_id propagation (set/reset/contextvar), configure idempotent + extras + exc_info + non-serializable fallback (P3-6) |
test_connector.py |
25 | connector framework: pack validation/roundtrip, registry bridge + MCP export, HIGH_RISK approval gate, persistence roundtrip, credential resolution, real stdio MCP client subprocess round-trip, local HTTP JSON-RPC round-trip |
test_connector_packs.py |
10 | first-party packs structural validation, unique ids/tool names, schema shape, risk-level conventions (read-only=LOW, write=MEDIUM/HIGH), Feishu acceptance shape, lifecycle load of bridged tools |
test_marketplace.py |
13 | builtin catalog, JSON index load + same-id override, HTTPS-only constraint, install builtin/local-source/skill-package, uninstall, list_installed, config wiring |
test_rest_connector.py |
15 | declarative connector docs: validation, read/write channel split, persistence roundtrip, execution contract vs local HTTP server, JSONPath-lite extraction, {var} rendering |
test_extensions_console.py |
26 | console backend: connector install/enable/disable/harness, credential vault (Desk round-trip + local Fernet fallback), bridged op execution, MCP intake probe/register/tick/call, plugin tick/remove/install, resync rebuild, spine derivation, standalone lifecycle bootstrap |
test_console_app.py |
6 | FastAPI console: whoami/capabilities, connector full flow (install→vault→enable→disable→harness), MCP flow, plugins flow, static serving |
test_hub.py |
17 | capabilities hub (issue #41): builtin bootstrap, four-tab classification, cross-type search incl. recommended facet, market groups, key isolation, /api/hub endpoint |
test_install_flow.py |
22 | bundle install/OAuth/update/uninstall flows (issue #42): per-item states, pending_auth, scope grants bridging, per-use policy, changelog-first update, uninstall blast radius |
test_registry_contract.py |
4 | |
test_audit_flow.py |
9 | authorization audit (issue #43): install/OAuth/update/uninstall events + trace-id linkage + JSONL contract, scope revoke re-bridge/disable, used-by reverse lookup, HTTP endpoints |
- Cannot run standalone — process hosting, permissions, logs, resources all depend on
fusion-cowork - Python ≥ 3.11 — aligned with fusion-cowork
- Mac M-series native — depends on fusion-mlx Metal/MLX acceleration
- No direct
fusion_coworkinternal imports — go throughDeskRuntime
Apache License 2.0 — part of the Fusion-MLX Apple Silicon local AI ecosystem.