diff --git a/apps/desktop/src/app/settings/memory/provider-config-modal.test.tsx b/apps/desktop/src/app/settings/memory/provider-config-modal.test.tsx index afa6a82e75d..4f109b99ac4 100644 --- a/apps/desktop/src/app/settings/memory/provider-config-modal.test.tsx +++ b/apps/desktop/src/app/settings/memory/provider-config-modal.test.tsx @@ -35,7 +35,6 @@ function field(overrides: Partial & Pick ({ getMemoryProviderConfig: (provider: string) => getMemoryProviderConfig(provider), - saveMemoryProviderConfig: (provider: string, values: unknown) => saveMemoryProviderConfig(provider, values), - runMemoryProviderAction: (provider: string, action: string, values: unknown) => - runMemoryProviderAction(provider, action, values) + saveMemoryProviderConfig: (provider: string, values: unknown) => saveMemoryProviderConfig(provider, values) })) vi.mock('@/store/profile', async () => { @@ -26,7 +23,6 @@ vi.mock('@/store/notifications', () => ({ function honchoSchema(): MemoryProviderConfig { return { - actions: [], name: 'honcho', label: 'Honcho', docs_url: 'https://docs.honcho.dev/v3/guides/integrations/hermes', @@ -104,7 +100,6 @@ function honchoSchema(): MemoryProviderConfig { beforeEach(() => { getMemoryProviderConfig.mockResolvedValue(honchoSchema()) saveMemoryProviderConfig.mockResolvedValue({ ok: true }) - runMemoryProviderAction.mockResolvedValue({ ok: true, result: { message: 'pong' } }) }) afterEach(() => { @@ -190,20 +185,6 @@ describe('ProviderConfigPanel', () => { expect(screen.getByRole('button', { name: /Full config/ })).toBeTruthy() }) - it('renders declared actions as buttons and posts the inline values', async () => { - const schema = honchoSchema() - schema.actions = [{ key: 'ping', label: 'Ping', description: 'Check the server' }] - getMemoryProviderConfig.mockResolvedValue(schema) - - await renderPanel() - - fireEvent.click(await screen.findByRole('button', { name: 'Ping' })) - - await waitFor(() => - expect(runMemoryProviderAction).toHaveBeenCalledWith('honcho', 'ping', expect.objectContaining({ workspace: 'myws' })) - ) - }) - it('shows an inline error with retry when the load fails, then recovers', async () => { getMemoryProviderConfig.mockRejectedValueOnce(new Error('Timed out connecting to Hermes backend')) diff --git a/apps/desktop/src/app/settings/memory/provider-config-panel.tsx b/apps/desktop/src/app/settings/memory/provider-config-panel.tsx index 881fc918a74..2eadc2081ac 100644 --- a/apps/desktop/src/app/settings/memory/provider-config-panel.tsx +++ b/apps/desktop/src/app/settings/memory/provider-config-panel.tsx @@ -2,9 +2,9 @@ import { useCallback, useEffect, useState } from 'react' import { Button } from '@/components/ui/button' import { DisclosureCaret } from '@/components/ui/disclosure-caret' -import { getMemoryProviderConfig, runMemoryProviderAction, saveMemoryProviderConfig } from '@/hermes' -import { Loader2, SlidersHorizontal } from '@/lib/icons' -import { notify, notifyError } from '@/store/notifications' +import { getMemoryProviderConfig, saveMemoryProviderConfig } from '@/hermes' +import { SlidersHorizontal } from '@/lib/icons' +import { notifyError } from '@/store/notifications' import type { MemoryProviderConfig, MemoryProviderField } from '@/types/hermes' import { FieldControl, FieldTitle } from './field-control' @@ -24,7 +24,6 @@ export function ProviderConfigPanel({ provider }: { provider: string }) { const [values, setValues] = useState>({}) const [saved, setSaved] = useState>({}) const [expanded, setExpanded] = useState(true) - const [runningAction, setRunningAction] = useState(null) const [showModal, setShowModal] = useState(false) const refresh = useCallback(async () => { @@ -75,26 +74,6 @@ export function ProviderConfigPanel({ provider }: { provider: string }) { [provider, saved] ) - const runAction = useCallback( - async (key: string, label: string) => { - setRunningAction(key) - try { - const { result } = await runMemoryProviderAction(provider, key, values) - notify({ - kind: 'success', - title: label, - message: typeof result.message === 'string' ? result.message : 'Action completed.' - }) - await refresh() - } catch (err) { - notifyError(err, `${label} failed`) - } finally { - setRunningAction(null) - } - }, - [provider, refresh, values] - ) - // Providers without a declared config surface (e.g. builtin) render nothing. if (config && config.fields.length === 0) { return null @@ -164,24 +143,6 @@ export function ProviderConfigPanel({ provider }: { provider: string }) { ))} - {config.actions.length > 0 && ( -
- {config.actions.map(action => ( - - ))} -
- )} )} diff --git a/apps/desktop/src/hermes.ts b/apps/desktop/src/hermes.ts index f0355faba46..573ed298619 100644 --- a/apps/desktop/src/hermes.ts +++ b/apps/desktop/src/hermes.ts @@ -429,19 +429,6 @@ export function saveMemoryProviderConfig(provider: string, values: Record -): Promise<{ ok: boolean; result: Record }> { - return window.hermesDesktop.api<{ ok: boolean; result: Record }>({ - ...profileScoped(), - path: `/api/memory/providers/${encodeURIComponent(provider)}/actions/${encodeURIComponent(action)}`, - method: 'POST', - body: { values } - }) -} - export function getEnvVars(): Promise> { return window.hermesDesktop.api>({ ...profileScoped(), diff --git a/apps/desktop/src/types/hermes.ts b/apps/desktop/src/types/hermes.ts index e5dd05b351d..b8dc01fda29 100644 --- a/apps/desktop/src/types/hermes.ts +++ b/apps/desktop/src/types/hermes.ts @@ -142,14 +142,7 @@ export interface MemoryProviderField { value: string } -export interface MemoryProviderAction { - description: string - key: string - label: string -} - export interface MemoryProviderConfig { - actions: MemoryProviderAction[] docs_url: string fields: MemoryProviderField[] label: string diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index c348fbbb7be..4ac4bb76625 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -78,7 +78,6 @@ from plugins.memory.config_schema import ( ProviderConfigSchema, ProviderField, STORAGE_HONCHO_HOST_BLOCK, - get_provider_action_handler, get_provider_config_schema, ) from gateway.status import ( @@ -858,10 +857,6 @@ class MemoryProviderConfigUpdate(BaseModel): values: Dict[str, Any] = {} -class MemoryProviderActionRequest(BaseModel): - values: Dict[str, Any] = {} - - class MemoryProviderSetupRequest(BaseModel): values: Dict[str, Any] = {} @@ -4377,16 +4372,7 @@ def _declared_provider_payload(provider: ProviderConfigSchema) -> Dict[str, Any] entry["is_set"] = native is not None if is_honcho else bool(value) fields.append(entry) - return { - "name": provider.name, - "label": provider.label, - "docs_url": provider.docs_url, - "fields": fields, - "actions": [ - {"key": action.key, "label": action.label, "description": action.description} - for action in provider.actions - ], - } + return {"name": provider.name, "label": provider.label, "docs_url": provider.docs_url, "fields": fields} def _apply_field_values(provider: ProviderConfigSchema, values: Dict[str, str], target_for) -> None: @@ -5366,43 +5352,6 @@ async def update_memory_provider_config(name: str, body: MemoryProviderConfigUpd raise HTTPException(status_code=500, detail="Internal server error") -@app.post("/api/memory/providers/{name}/actions/{action}") -async def run_memory_provider_action( - name: str, action: str, body: MemoryProviderActionRequest, profile: Optional[str] = None -): - """Run a provider-declared action (validate, start-local, …) generically. - - The schema declares actions; the plugin's config_actions.py implements - them. Undeclared actions 404, a declared action without a handler is a - plugin bug and 500s, and a handler raising ValueError maps to 400 so - plugins can surface user-facing validation messages. - """ - - _require_valid_memory_provider_name(name) - - def _run(): - with _profile_scope(profile): - provider = get_provider_config_schema(name) - if provider is None or provider.action(action) is None: - raise HTTPException(status_code=404, detail=f"Unknown action: {name}/{action}") - handler = get_provider_action_handler(name, action) - if handler is None: - raise HTTPException( - status_code=500, detail=f"No handler for declared action: {name}/{action}" - ) - return handler(dict(body.values or {})) - - try: - result = await asyncio.to_thread(_run) - return {"ok": True, "result": result if isinstance(result, dict) else {}} - except HTTPException: - raise - except ValueError as exc: - raise HTTPException(status_code=400, detail=str(exc)) from exc - except Exception: - _log.exception("POST /api/memory/providers/%s/actions/%s failed", name, action) - raise HTTPException(status_code=500, detail="Internal server error") - @app.get("/api/config") async def get_config(profile: Optional[str] = None): with _profile_scope(profile): diff --git a/plugins/memory/config_schema.py b/plugins/memory/config_schema.py index a06121a6974..2a9560b1ece 100644 --- a/plugins/memory/config_schema.py +++ b/plugins/memory/config_schema.py @@ -91,22 +91,6 @@ class ProviderField: return {opt.value for opt in self.options} -@dataclass(frozen=True) -class ProviderAction: - """A provider-specific operation exposed as a button on the config panel. - - Declared here, implemented in the plugin's ``config_actions.py`` as an - entry in ``ACTION_HANDLERS`` — ``{key: handler}`` where the handler takes - the submitted field values dict and returns a JSON-able result dict. - Like ``config_schema.py``, that file is loaded by path and must stay - import-light; heavy imports belong inside the handler bodies. - """ - - key: str - label: str - description: str = "" - - @dataclass(frozen=True) class ProviderConfigSchema: """A provider plugin's declared config surface.""" @@ -117,14 +101,10 @@ class ProviderConfigSchema: # Optional link to the provider's config docs, shown in the full-config modal. docs_url: str = "" fields: tuple[ProviderField, ...] = dataclass_field(default_factory=tuple) - actions: tuple[ProviderAction, ...] = () def inline_fields(self) -> tuple[ProviderField, ...]: return tuple(f for f in self.fields if f.inline) - def action(self, key: str) -> ProviderAction | None: - return next((a for a in self.actions if a.key == key), None) - _SCHEMA_CACHE: dict[str, ProviderConfigSchema] = {} @@ -162,30 +142,3 @@ def get_provider_config_schema(name: str) -> ProviderConfigSchema | None: if schema is not None: _SCHEMA_CACHE[key] = schema return schema - - -def get_provider_action_handler(name: str, action_key: str): - """Return the handler for a declared action, or ``None`` when missing. - - Handlers live in ``ACTION_HANDLERS`` in the plugin's ``config_actions.py``, - loaded by path like the schema. Not cached: actions are user-initiated and - rare, and a fixed handler file must not need a restart to be picked up. - """ - - from plugins.memory import find_provider_dir - - provider_dir = find_provider_dir(name) - path = provider_dir / "config_actions.py" if provider_dir else None - if path is None or not path.is_file(): - return None - - try: - spec = importlib.util.spec_from_file_location(f"_hermes_memory_config_actions.{name}", path) - module = importlib.util.module_from_spec(spec) - spec.loader.exec_module(module) - except Exception: - _log.exception("failed to load config actions for memory provider %r", name) - return None - - handlers = getattr(module, "ACTION_HANDLERS", None) - return handlers.get(action_key) if isinstance(handlers, dict) else None diff --git a/tests/hermes_cli/test_web_server.py b/tests/hermes_cli/test_web_server.py index 9f7eae3acbd..8f7282f1b0e 100644 --- a/tests/hermes_cli/test_web_server.py +++ b/tests/hermes_cli/test_web_server.py @@ -1059,82 +1059,6 @@ class TestWebServerEndpoints: ) assert fields["peerName"]["value"] == "eri" - # ── Memory provider actions ───────────────────────────────────────── - - @staticmethod - def _install_action_provider(monkeypatch, tmp_path): - plugin_dir = tmp_path / "actionprov" - plugin_dir.mkdir() - (plugin_dir / "config_schema.py").write_text( - "from plugins.memory.config_schema import ProviderAction, ProviderConfigSchema, ProviderField\n" - "CONFIG_SCHEMA = ProviderConfigSchema(\n" - ' name="actionprov",\n' - ' label="ActionProv",\n' - ' fields=(ProviderField(key="url", label="URL", inline=True),),\n' - ' actions=(ProviderAction("ping", "Ping", "Check the server"),),\n' - ")\n", - encoding="utf-8", - ) - (plugin_dir / "config_actions.py").write_text( - "def _ping(values):\n" - ' if values.get("url") == "bad":\n' - ' raise ValueError("unreachable url")\n' - ' return {"message": "pong " + values.get("url", "")}\n' - "\n" - 'ACTION_HANDLERS = {"ping": _ping}\n', - encoding="utf-8", - ) - import plugins.memory as memory - - real = memory.find_provider_dir - monkeypatch.setattr( - memory, "find_provider_dir", lambda name: plugin_dir if name == "actionprov" else real(name) - ) - - def test_provider_action_runs_declared_handler(self, monkeypatch, tmp_path): - monkeypatch.setenv("HOME", str(tmp_path)) - self._install_action_provider(monkeypatch, tmp_path) - - resp = self.client.post( - "/api/memory/providers/actionprov/actions/ping", - json={"values": {"url": "https://x.example"}}, - ) - - assert resp.status_code == 200 - assert resp.json() == {"ok": True, "result": {"message": "pong https://x.example"}} - - def test_provider_action_payload_lists_declared_actions(self, monkeypatch, tmp_path): - monkeypatch.setenv("HOME", str(tmp_path)) - self._install_action_provider(monkeypatch, tmp_path) - - data = self.client.get("/api/memory/providers/actionprov/config").json() - assert data["actions"] == [{"key": "ping", "label": "Ping", "description": "Check the server"}] - # Providers that declare no actions ship an empty list. - assert self.client.get("/api/memory/providers/honcho/config").json()["actions"] == [] - - def test_provider_action_undeclared_is_404(self, monkeypatch, tmp_path): - monkeypatch.setenv("HOME", str(tmp_path)) - self._install_action_provider(monkeypatch, tmp_path) - - assert self.client.post( - "/api/memory/providers/actionprov/actions/nope", json={"values": {}} - ).status_code == 404 - assert self.client.post( - "/api/memory/providers/honcho/actions/ping", json={"values": {}} - ).status_code == 404 - - def test_provider_action_value_error_maps_to_400(self, monkeypatch, tmp_path): - monkeypatch.setenv("HOME", str(tmp_path)) - self._install_action_provider(monkeypatch, tmp_path) - - resp = self.client.post( - "/api/memory/providers/actionprov/actions/ping", - json={"values": {"url": "bad"}}, - ) - - assert resp.status_code == 400 - assert resp.json()["detail"] == "unreachable url" - def test_put_honcho_rejects_malformed_number_and_json(self, monkeypatch, tmp_path): monkeypatch.setenv("HOME", str(tmp_path)) diff --git a/tests/plugins/memory/test_config_schema.py b/tests/plugins/memory/test_config_schema.py index db175919197..db4d999710a 100644 --- a/tests/plugins/memory/test_config_schema.py +++ b/tests/plugins/memory/test_config_schema.py @@ -65,9 +65,3 @@ def test_broken_schema_is_not_cached(monkeypatch, tmp_path): recovered = get_provider_config_schema("broken") assert recovered is not None assert recovered.label == "Broken" - - -def test_provider_without_action_module_has_no_handlers(): - from plugins.memory.config_schema import get_provider_action_handler - - assert get_provider_action_handler("honcho", "anything") is None