From 8b62f7151ab6a9e7d162d6cc817fe723d2a329d3 Mon Sep 17 00:00:00 2001 From: frdel <38891707+frdel@users.noreply.github.com> Date: Wed, 25 Feb 2026 12:26:08 +0100 Subject: [PATCH] Make API handler caching optional; plugin fixes Introduce CACHE_ENABLED (default False) and update CACHE_AREA in api.py so cached handlers are only read/added when caching is enabled; also cast request.remote_addr to str in requires_loopback. In plugins.py add cache import and an invalidate_plugin_cache() helper that clears plugin caches, tidy imports/formatting, simplify override detection using any(), and apply minor refactors/whitespace fixes (including toggle_plugin and get_plugin_config). Note: meta.always_enabled early-return was removed. --- python/helpers/api.py | 15 +++++--- python/helpers/plugins.py | 78 ++++++++++++++++++++++++--------------- 2 files changed, 58 insertions(+), 35 deletions(-) diff --git a/python/helpers/api.py b/python/helpers/api.py index 046c97ef6..eab60baa7 100644 --- a/python/helpers/api.py +++ b/python/helpers/api.py @@ -16,7 +16,8 @@ from python.helpers import files, cache ThreadLockType = Union[threading.Lock, threading.RLock] -CACHE_AREA = "api_handlers" +CACHE_AREA = "api_handlers(api)(plugins)" +CACHE_ENABLED = False Input = dict @@ -167,7 +168,7 @@ def requires_api_key(f): def requires_loopback(f): @wraps(f) async def decorated(*args, **kwargs): - if not is_loopback_address(request.remote_addr): + if not is_loopback_address(str(request.remote_addr)): return Response("Access denied.", 403, {}) return await f(*args, **kwargs) @@ -209,9 +210,10 @@ def register_api_route(app: Flask, lock: ThreadLockType) -> None: async def _dispatch(path: str) -> BaseResponse: # Return cached wrapped handler if available - cached = cache.get(CACHE_AREA, path) - if cached is not None: - return await cached() + if CACHE_ENABLED: + cached = cache.get(CACHE_AREA, path) + if cached is not None: + return await cached() # Resolve file path for the handler # Try built-in api folder first, then plugin api folders @@ -259,7 +261,8 @@ def register_api_route(app: Flask, lock: ThreadLockType) -> None: if handler_cls.requires_loopback(): handler_fn = requires_loopback(handler_fn) - cache.add(CACHE_AREA, path, handler_fn) + if CACHE_ENABLED: + cache.add(CACHE_AREA, path, handler_fn) return await handler_fn() app.add_url_rule( diff --git a/python/helpers/plugins.py b/python/helpers/plugins.py index 9d240a7e5..999245601 100644 --- a/python/helpers/plugins.py +++ b/python/helpers/plugins.py @@ -2,9 +2,18 @@ from __future__ import annotations import re, json, glob from pathlib import Path -from typing import Any, Dict, Iterator, List, Literal, Optional, TYPE_CHECKING, TypedDict +from typing import ( + Any, + Dict, + Iterator, + List, + Literal, + Optional, + TYPE_CHECKING, + TypedDict, +) -from python.helpers import files, print_style, yaml as yaml_helper +from python.helpers import files, print_style, yaml as yaml_helper, cache from pydantic import BaseModel, Field if TYPE_CHECKING: @@ -17,11 +26,14 @@ _META_TARGET_RE = re.compile( ) type ToggleState = Literal["enabled", "disabled", "advanced"] + + class PluginAssetFile(TypedDict): path: str project_name: str agent_profile: str + META_FILE_NAME = "plugin.yaml" CONFIG_FILE_NAME = "config.json" CONFIG_DEFAULT_FILE_NAME = "default_config.yaml" @@ -59,7 +71,11 @@ class PluginListItem(BaseModel): toggle_state: ToggleState = "disabled" -def get_plugin_roots(plugin_name:str="") -> List[str]: +def invalidate_plugin_cache(): + cache.clear("*(plugins)*") + + +def get_plugin_roots(plugin_name: str = "") -> List[str]: """Plugin root directories, ordered by priority (user first).""" return [ files.get_abs_path(files.USER_DIR, files.PLUGINS_DIR, plugin_name), @@ -97,9 +113,7 @@ def get_enhanced_plugins_list( meta_file = str(d / META_FILE_NAME) if not files.exists(meta_file): continue - meta = PluginMetadata.model_validate( - files.read_file_yaml(meta_file) - ) + meta = PluginMetadata.model_validate(files.read_file_yaml(meta_file)) has_main_screen = files.exists(str(d / "webui" / "main.html")) has_config_screen = files.exists(str(d / "webui" / "config.html")) has_readme = files.exists(str(d / "README.md")) @@ -233,7 +247,8 @@ def get_enabled_plugins(agent: Agent | None): return active -def determined_toggle_from_paths(default:bool, paths:Iterator[str]): + +def determined_toggle_from_paths(default: bool, paths: Iterator[str]): enabled = default for plugin_path in paths: if enabled: @@ -241,11 +256,10 @@ def determined_toggle_from_paths(default:bool, paths:Iterator[str]): files.get_abs_path(plugin_path, DISABLED_FILE_NAME) ) else: - enabled = files.exists( - files.get_abs_path(plugin_path, ENABLED_FILE_NAME) - ) + enabled = files.exists(files.get_abs_path(plugin_path, ENABLED_FILE_NAME)) return enabled + def get_toggle_state(plugin_name: str) -> ToggleState: meta = get_plugin_meta(plugin_name) if not meta: @@ -255,12 +269,22 @@ def get_toggle_state(plugin_name: str) -> ToggleState: # root plugin paths plugin_paths = get_plugin_roots(plugin_name) - state = "enabled" if determined_toggle_from_paths(True, reversed(plugin_paths)) else "disabled" + state = ( + "enabled" + if determined_toggle_from_paths(True, reversed(plugin_paths)) + else "disabled" + ) # global toggles usr_toggles = [ - files.find_existing_paths_by_pattern(files.get_abs_path(files.PLUGINS_DIR, plugin_name, TOGGLE_FILE_PATTERN)), - files.find_existing_paths_by_pattern(files.get_abs_path(files.USER_DIR, files.PLUGINS_DIR, plugin_name, TOGGLE_FILE_PATTERN)) + files.find_existing_paths_by_pattern( + files.get_abs_path(files.PLUGINS_DIR, plugin_name, TOGGLE_FILE_PATTERN) + ), + files.find_existing_paths_by_pattern( + files.get_abs_path( + files.USER_DIR, files.PLUGINS_DIR, plugin_name, TOGGLE_FILE_PATTERN + ) + ), ] # additional toggles in project/agent directories, return advanced @@ -272,18 +296,10 @@ def get_toggle_state(plugin_name: str) -> ToggleState: agent_profile="*" if meta.per_agent_config else "", only_first=False, ) - - # Advanced if there are specific overrides (project or agent specific) - specific_overrides = [ - c for c in configs - if c.get("project_name") or c.get("agent_profile") - ] - - if len(specific_overrides) > 0: - state = "advanced" - if state != "advanced" and meta.always_enabled: - return "enabled" + # Advanced if there are specific overrides (project or agent specific) + if any(c.get("project_name") or c.get("agent_profile") for c in configs): + state = "advanced" return state @@ -291,8 +307,12 @@ def get_toggle_state(plugin_name: str) -> ToggleState: def toggle_plugin( plugin_name: str, enabled: bool, project_name: str = "", agent_profile: str = "" ): - enabled_file = determine_plugin_asset_path(plugin_name, project_name, agent_profile, ENABLED_FILE_NAME) - disabled_file = determine_plugin_asset_path(plugin_name, project_name, agent_profile, DISABLED_FILE_NAME) + enabled_file = determine_plugin_asset_path( + plugin_name, project_name, agent_profile, ENABLED_FILE_NAME + ) + disabled_file = determine_plugin_asset_path( + plugin_name, project_name, agent_profile, DISABLED_FILE_NAME + ) # ensure clean state by deleting both potential files first files.delete_file(enabled_file) @@ -346,9 +366,9 @@ def get_plugin_config( find_plugin_dir(plugin_name), CONFIG_DEFAULT_FILE_NAME ) if file_path and files.exists(file_path): - return (json.loads if file_path.lower().endswith(".json") else yaml_helper.loads)( - files.read_file(file_path) - ) + return ( + json.loads if file_path.lower().endswith(".json") else yaml_helper.loads + )(files.read_file(file_path)) return None