mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-03 02:11:48 +00:00
feat(dashboard): add Plugins page with enable/disable, auth status, install/remove
- New PluginsPage.tsx: full plugin management UI (list, enable/disable, install from git, remove, git pull updates, provider picker) - Backend: dashboard_set_agent_plugin_enabled now also toggles the plugin's toolset in platform_toolsets so enabling actually makes tools visible in agent sessions - Backend: /api/dashboard/plugins/hub returns auth_required + auth_command per plugin (checks tool registry check_fn) - Frontend: auth_required shown as Badge + CommandBlock with copy-able auth command - Fix: Select overflow in providers card (min-w-0 grid cells, removed truncate/overflow-hidden that clipped dropdown) - Refactor: _install_plugin_core extracted for non-interactive reuse, PluginOperationError for structured error handling - i18n: en/zh/types updated with all new plugin page strings
This commit is contained in:
parent
e5dad4ac57
commit
e2a4905606
10 changed files with 1521 additions and 189 deletions
|
|
@ -3633,6 +3633,223 @@ async def rescan_dashboard_plugins():
|
|||
return {"ok": True, "count": len(plugins)}
|
||||
|
||||
|
||||
class _AgentPluginInstallBody(BaseModel):
|
||||
identifier: str
|
||||
force: bool = False
|
||||
enable: bool = True
|
||||
|
||||
|
||||
def _strip_dashboard_manifest(p: Dict[str, Any]) -> Dict[str, Any]:
|
||||
return {k: v for k, v in p.items() if not k.startswith("_")}
|
||||
|
||||
|
||||
def _merged_plugins_hub() -> Dict[str, Any]:
|
||||
"""Agent discovery + dashboard manifests + optional provider picker metadata."""
|
||||
from hermes_cli.plugins_cmd import (
|
||||
_discover_all_plugins,
|
||||
_get_current_context_engine,
|
||||
_get_current_memory_provider,
|
||||
_discover_context_engines,
|
||||
_discover_memory_providers,
|
||||
_get_disabled_set,
|
||||
_get_enabled_set,
|
||||
_read_manifest as _read_plugin_manifest_at,
|
||||
)
|
||||
|
||||
dashboard_list = _get_dashboard_plugins()
|
||||
dash_by_name = {str(p["name"]): p for p in dashboard_list}
|
||||
|
||||
disabled_set = _get_disabled_set()
|
||||
enabled_set = _get_enabled_set()
|
||||
|
||||
plugins_root_resolved = (get_hermes_home() / "plugins").resolve()
|
||||
rows: List[Dict[str, Any]] = []
|
||||
|
||||
for name, version, description, source, dir_str in _discover_all_plugins():
|
||||
if name in disabled_set:
|
||||
runtime_status = "disabled"
|
||||
elif name in enabled_set:
|
||||
runtime_status = "enabled"
|
||||
else:
|
||||
runtime_status = "inactive"
|
||||
|
||||
dir_path = Path(dir_str)
|
||||
dm = dash_by_name.get(name)
|
||||
has_dash_manifest = dm is not None or (dir_path / "dashboard" / "manifest.json").exists()
|
||||
|
||||
under_user_tree = False
|
||||
try:
|
||||
dir_path.resolve().relative_to(plugins_root_resolved)
|
||||
under_user_tree = True
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
can_remove_update = (
|
||||
source in ("user", "git") and under_user_tree and Path(dir_str).is_dir()
|
||||
)
|
||||
|
||||
# Check if this plugin provides tools that require auth
|
||||
auth_required = False
|
||||
auth_command = ""
|
||||
manifest_data = _read_plugin_manifest_at(dir_path)
|
||||
provides_tools = manifest_data.get("provides_tools") or []
|
||||
if provides_tools:
|
||||
try:
|
||||
from tools.registry import registry
|
||||
for tname in provides_tools:
|
||||
entry = registry.get_entry(tname)
|
||||
if entry and entry.check_fn and not entry.check_fn():
|
||||
auth_required = True
|
||||
auth_command = f"hermes auth {name}"
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
rows.append({
|
||||
"name": name,
|
||||
"version": version or "",
|
||||
"description": description or "",
|
||||
"source": source,
|
||||
"runtime_status": runtime_status,
|
||||
"has_dashboard_manifest": has_dash_manifest,
|
||||
"dashboard_manifest": _strip_dashboard_manifest(dm) if dm else None,
|
||||
"path": dir_str,
|
||||
"can_remove": can_remove_update,
|
||||
"can_update_git": can_remove_update and (Path(dir_str) / ".git").exists(),
|
||||
"auth_required": auth_required,
|
||||
"auth_command": auth_command,
|
||||
})
|
||||
|
||||
agent_names = {r["name"] for r in rows}
|
||||
orphan_dashboard = [
|
||||
_strip_dashboard_manifest(p)
|
||||
for p in dashboard_list
|
||||
if str(p["name"]) not in agent_names
|
||||
]
|
||||
|
||||
memory_providers: List[Dict[str, str]] = []
|
||||
try:
|
||||
for n, desc in _discover_memory_providers():
|
||||
memory_providers.append({"name": n, "description": desc})
|
||||
except Exception:
|
||||
memory_providers = []
|
||||
|
||||
context_engines: List[Dict[str, str]] = []
|
||||
try:
|
||||
for n, desc in _discover_context_engines():
|
||||
context_engines.append({"name": n, "description": desc})
|
||||
except Exception:
|
||||
context_engines = []
|
||||
|
||||
return {
|
||||
"plugins": rows,
|
||||
"orphan_dashboard_plugins": orphan_dashboard,
|
||||
"providers": {
|
||||
"memory_provider": _get_current_memory_provider() or "",
|
||||
"memory_options": memory_providers,
|
||||
"context_engine": _get_current_context_engine(),
|
||||
"context_options": context_engines,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/dashboard/plugins/hub")
|
||||
async def get_plugins_hub(request: Request):
|
||||
"""Unified agent plugins + dashboard extension metadata (session protected)."""
|
||||
_require_token(request)
|
||||
try:
|
||||
return _merged_plugins_hub()
|
||||
except Exception as exc:
|
||||
_log.warning("plugins/hub failed: %s", exc)
|
||||
raise HTTPException(status_code=500, detail="Failed to build plugins hub.") from exc
|
||||
|
||||
|
||||
@app.post("/api/dashboard/agent-plugins/install")
|
||||
async def post_agent_plugin_install(request: Request, body: _AgentPluginInstallBody):
|
||||
_require_token(request)
|
||||
from hermes_cli.plugins_cmd import dashboard_install_plugin
|
||||
|
||||
result = dashboard_install_plugin(
|
||||
body.identifier.strip(),
|
||||
force=body.force,
|
||||
enable=body.enable,
|
||||
)
|
||||
if not result.get("ok"):
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=result.get("error") or "Install failed.",
|
||||
)
|
||||
_get_dashboard_plugins(force_rescan=True)
|
||||
return result
|
||||
|
||||
|
||||
@app.post("/api/dashboard/agent-plugins/{name}/enable")
|
||||
async def post_agent_plugin_enable(request: Request, name: str):
|
||||
_require_token(request)
|
||||
from hermes_cli.plugins_cmd import dashboard_set_agent_plugin_enabled
|
||||
|
||||
result = dashboard_set_agent_plugin_enabled(name, enabled=True)
|
||||
if not result.get("ok"):
|
||||
raise HTTPException(status_code=400, detail=result.get("error") or "Enable failed.")
|
||||
return result
|
||||
|
||||
|
||||
@app.post("/api/dashboard/agent-plugins/{name}/disable")
|
||||
async def post_agent_plugin_disable(request: Request, name: str):
|
||||
_require_token(request)
|
||||
from hermes_cli.plugins_cmd import dashboard_set_agent_plugin_enabled
|
||||
|
||||
result = dashboard_set_agent_plugin_enabled(name, enabled=False)
|
||||
if not result.get("ok"):
|
||||
raise HTTPException(status_code=400, detail=result.get("error") or "Disable failed.")
|
||||
return result
|
||||
|
||||
|
||||
@app.post("/api/dashboard/agent-plugins/{name}/update")
|
||||
async def post_agent_plugin_update(request: Request, name: str):
|
||||
_require_token(request)
|
||||
from hermes_cli.plugins_cmd import dashboard_update_user_plugin
|
||||
|
||||
result = dashboard_update_user_plugin(name)
|
||||
if not result.get("ok"):
|
||||
raise HTTPException(status_code=400, detail=result.get("error") or "Update failed.")
|
||||
_get_dashboard_plugins(force_rescan=True)
|
||||
return result
|
||||
|
||||
|
||||
@app.delete("/api/dashboard/agent-plugins/{name}")
|
||||
async def delete_agent_plugin(request: Request, name: str):
|
||||
_require_token(request)
|
||||
from hermes_cli.plugins_cmd import dashboard_remove_user_plugin
|
||||
|
||||
result = dashboard_remove_user_plugin(name)
|
||||
if not result.get("ok"):
|
||||
raise HTTPException(status_code=400, detail=result.get("error") or "Remove failed.")
|
||||
_get_dashboard_plugins(force_rescan=True)
|
||||
return result
|
||||
|
||||
|
||||
class _PluginProvidersPutBody(BaseModel):
|
||||
memory_provider: Optional[str] = None
|
||||
context_engine: Optional[str] = None
|
||||
|
||||
|
||||
@app.put("/api/dashboard/plugin-providers")
|
||||
async def put_plugin_providers(request: Request, body: _PluginProvidersPutBody):
|
||||
"""Persist memory provider / context engine selection (writes config.yaml)."""
|
||||
_require_token(request)
|
||||
from hermes_cli.plugins_cmd import (
|
||||
_save_context_engine,
|
||||
_save_memory_provider,
|
||||
)
|
||||
|
||||
if body.memory_provider is not None:
|
||||
_save_memory_provider(body.memory_provider)
|
||||
if body.context_engine is not None:
|
||||
_save_context_engine(body.context_engine)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@app.get("/dashboard-plugins/{plugin_name}/{file_path:path}")
|
||||
async def serve_plugin_asset(plugin_name: str, file_path: str):
|
||||
"""Serve static assets from a dashboard plugin directory.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue