Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions flocks/hub/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,7 @@ def _entry_from_manifest(manifest: HubPluginManifest) -> HubCatalogEntry:
riskLevel=manifest.risk.level,
state=state,
installedVersion=installed_version,
enabled=record.enabled if record else True,
source=manifest.source.kind,
manifestPath=str(manifest_path(manifest.type, manifest.id).relative_to(get_bundled_hub_root())),
installPath=str(install_path) if install_path else None,
Expand Down Expand Up @@ -858,6 +859,7 @@ def _entry_from_index(
riskLevel=item.riskLevel,
state=state,
installedVersion=installed_version,
enabled=record.enabled if record else True,
source="bundled",
manifestPath=item.manifestPath,
installPath=str(install_path) if install_path else None,
Expand All @@ -883,6 +885,7 @@ def _entry_from_system_manifest(manifest: HubPluginManifest, root: Path) -> HubC
riskLevel=manifest.risk.level,
state="installed",
installedVersion=manifest.version,
enabled=True,
source="system",
manifestPath=_system_manifest_path(manifest.type, manifest.id),
installPath=str(root),
Expand Down Expand Up @@ -950,6 +953,7 @@ def _entry_from_bundled_tool(
riskLevel=manifest.risk.level,
state=state,
installedVersion=installed_version,
enabled=record.enabled if record else True,
source="bundled",
manifestPath=manifest_rel,
installPath=str(install_path) if install_path else None,
Expand Down
27 changes: 27 additions & 0 deletions flocks/hub/installer.py
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,33 @@ async def update_plugin(plugin_type: PluginType, plugin_id: str, *, scope: str =
return await install_plugin(plugin_type, plugin_id, scope=scope)


def _set_api_services_enabled(storage_keys: list[str], enabled: bool) -> None:
if not storage_keys:
return
from flocks.config.config_writer import ConfigWriter

for storage_key in storage_keys:
current = ConfigWriter.get_api_service_raw(storage_key)
service_config = dict(current) if isinstance(current, dict) else {}
service_config["enabled"] = enabled
ConfigWriter.set_api_service(storage_key, service_config)


async def set_plugin_enabled(plugin_type: PluginType, plugin_id: str, enabled: bool) -> InstalledPluginRecord:
record = local.set_installed_record_enabled(plugin_type, plugin_id, enabled)
install_path = Path(record.installPath) if record.installPath else local.infer_local_install(plugin_type, plugin_id)

if plugin_type == "skill":
from flocks.skill.skill import Skill

Skill.set_disabled(plugin_id, not enabled)
elif plugin_type in {"tool", "device"} and install_path is not None:
_set_api_services_enabled(_collect_storage_keys(install_path), enabled)

await _refresh_runtime(plugin_type)
return record


def _collect_storage_keys(install_path: Path) -> list[str]:
"""Return ``api_services`` storage keys declared inside *install_path*.

Expand Down
21 changes: 21 additions & 0 deletions flocks/hub/local.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,27 @@ def save_installed_record(record: InstalledPluginRecord) -> None:
path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + "\n", encoding="utf-8")


def set_installed_record_enabled(plugin_type: PluginType, plugin_id: str, enabled: bool) -> InstalledPluginRecord:
record = get_record(plugin_type, plugin_id)
install_path = Path(record.installPath) if record and record.installPath else infer_local_install(plugin_type, plugin_id)
if install_path is None:
raise FileNotFoundError(f"Plugin is not installed: {plugin_type}:{plugin_id}")
if record is None:
record = make_record(
plugin_type=plugin_type,
plugin_id=plugin_id,
version="0.0.0",
source="local",
install_path=install_path,
enabled=enabled,
scope="project" if _project_plugins_root().resolve() in install_path.resolve().parents else "global",
)
else:
record.enabled = enabled
save_installed_record(record)
return record


def remove_installed_record(plugin_type: PluginType, plugin_id: str) -> None:
import json

Expand Down
1 change: 1 addition & 0 deletions flocks/hub/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ class HubCatalogEntry(BaseModel):
riskLevel: str = "low"
state: PluginState = "available"
installedVersion: Optional[str] = None
enabled: bool = True
source: str = "bundled"
manifestPath: str
installPath: Optional[str] = None
Expand Down
18 changes: 17 additions & 1 deletion flocks/server/routes/hub.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
load_taxonomy,
)
from flocks.hub.files import file_tree, read_file_content
from flocks.hub.installer import install_plugin, uninstall_plugin, update_plugin
from flocks.hub.installer import install_plugin, set_plugin_enabled, uninstall_plugin, update_plugin
from flocks.hub.models import (
HubCatalogEntry,
HubFileContent,
Expand All @@ -41,6 +41,10 @@ class HubInstallRequest(BaseModel):
scope: str = Field(default="global", description="'global' only")


class HubEnableRequest(BaseModel):
enabled: bool


class HubCatalogFacets(BaseModel):
type: dict[str, int] = Field(default_factory=dict)
category: dict[str, int] = Field(default_factory=dict)
Expand Down Expand Up @@ -292,6 +296,18 @@ async def hub_update_plugin(
raise HTTPException(status_code=422, detail=str(exc)) from exc


@router.patch("/hub/plugins/{plugin_type}/{plugin_id}/enabled", response_model=InstalledPluginRecord)
async def hub_set_plugin_enabled(plugin_type: PluginType, plugin_id: str, req: HubEnableRequest):
_guard_legacy_removed_plugin(plugin_type, plugin_id)
try:
return await set_plugin_enabled(plugin_type, plugin_id, req.enabled)
except FileNotFoundError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
except Exception as exc:
log.error("hub.enable.failed", {"type": plugin_type, "id": plugin_id, "enabled": req.enabled, "error": str(exc)})
raise HTTPException(status_code=422, detail=str(exc)) from exc


@router.delete("/hub/plugins/{plugin_type}/{plugin_id}")
async def hub_uninstall_plugin(
plugin_type: PluginType,
Expand Down
11 changes: 11 additions & 0 deletions tests/hub/test_hub_catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -823,6 +823,17 @@ def test_hub_routes_cover_catalog_files_install_and_uninstall(isolated_hub_env):
assert installed.status_code == 200
assert installed.json()["id"] == "ndr-alert-analysis"

disabled = client.patch("/api/hub/plugins/skill/ndr-alert-analysis/enabled", json={"enabled": False})
assert disabled.status_code == 200
assert disabled.json()["enabled"] is False
disabled_catalog = client.get("/api/hub/catalog", params={"state": "installed"}).json()
disabled_entry = next(item for item in disabled_catalog if item["id"] == "ndr-alert-analysis")
assert disabled_entry["enabled"] is False

enabled = client.patch("/api/hub/plugins/skill/ndr-alert-analysis/enabled", json={"enabled": True})
assert enabled.status_code == 200
assert enabled.json()["enabled"] is True

installed_catalog = client.get("/api/hub/catalog", params={"state": "installed"}).json()
assert any(item["id"] == "ndr-alert-analysis" for item in installed_catalog)

Expand Down
4 changes: 4 additions & 0 deletions webui/src/api/hub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface HubCatalogEntry {
riskLevel: string;
state: HubPluginState;
installedVersion?: string;
enabled: boolean;
source: string;
manifestPath: string;
installPath?: string;
Expand Down Expand Up @@ -222,6 +223,9 @@ export const hubAPI = {
uninstall: (type: HubPluginType, id: string) =>
client.delete(`/api/hub/plugins/${type}/${id}`),

setEnabled: (type: HubPluginType, id: string, enabled: boolean) =>
client.patch(`/api/hub/plugins/${type}/${id}/enabled`, { enabled }),

refresh: () =>
client.post('/api/hub/refresh'),
};
Loading