Skip to content

Commit 87b4fd6

Browse files
committed
fix: use the system config file (PLUGINS_CONFIG_FILE) for syspath update (Consistent with how the PluginManager works).
Signed-off-by: habeck <habeck@us.ibm.com>
1 parent fd99a70 commit 87b4fd6

7 files changed

Lines changed: 42 additions & 40 deletions

File tree

cpex/framework/isolated/client.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import shutil
1818
import sys
1919
import venv
20+
import yaml
2021
from pathlib import Path
2122

2223
from typing_extensions import Any, Optional
@@ -26,6 +27,7 @@
2627
from cpex.framework.errors import PluginError, convert_exception_to_error
2728
from cpex.framework.hooks.registry import get_hook_registry
2829
from cpex.framework.isolated.venv_comm import VenvProcessCommunicator
30+
from cpex.framework.loader.config import ConfigLoader
2931
from cpex.framework.models import PluginConfig, PluginContext, PluginErrorModel, PluginPayload, PluginResult
3032

3133
logger = logging.getLogger(__name__)
@@ -39,13 +41,16 @@ def __init__(self, config: PluginConfig) -> None:
3941
super().__init__(config)
4042
self.implementation = "Python"
4143
self.comm = None
42-
self.script_path: str = config.config["script_path"]
43-
path = Path(self.config.config.get("script_path")).resolve()
44+
tmp = os.environ.get("PLUGINS_CONFIG_FILE","plugins/config.yaml")
45+
plugin_loader_config = ConfigLoader.load_config(Path(tmp).resolve(), use_jinja=False)
46+
self.plugin_dirs = plugin_loader_config.plugin_dirs
47+
# use the first plugin dir specified in the plugin configuration file.
48+
path = Path(self.plugin_dirs[0]).resolve()
4449
class_root = self.config.config.get("class_name").split(".")[0]
4550
cache_root = path / class_root
4651
self.plugin_path = cache_root
4752
if not cache_root.exists():
48-
raise RuntimeError("plugin script_path does not exist")
53+
raise RuntimeError(f"plugin path does not exist: {str(cache_root)}")
4954
self.cache_dir: Path = cache_root / ".cpex" / "venv_cache"
5055
self.cache_dir.mkdir(parents=True, exist_ok=True)
5156

@@ -269,7 +274,6 @@ def _build_hook_task(self, hook_type: str, payload: PluginPayload, context: Plug
269274
Task dictionary ready for transmission
270275
"""
271276
# Cache config lookups
272-
script_path = self.config.config["script_path"]
273277
class_name = self.config.config["class_name"]
274278
safe_config = self.config.get_safe_config()
275279

@@ -279,7 +283,7 @@ def _build_hook_task(self, hook_type: str, payload: PluginPayload, context: Plug
279283

280284
return {
281285
"task_type": "load_and_run_hook",
282-
"script_path": script_path,
286+
"plugin_dirs": self.plugin_dirs,
283287
"class_name": class_name,
284288
"config": safe_config,
285289
HOOK_TYPE: hook_type,

cpex/framework/isolated/venv_comm.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ def start_worker(self, script_path: str) -> None:
8787
text=True,
8888
bufsize=1, # Line buffered
8989
cwd=os.getcwd(),
90+
env={'PLUGINS_CONFIG_FILE': os.environ.get("PLUGINS_CONFIG_FILE", "plugins/config.yaml")}
9091
)
9192

9293
self.running = True

cpex/framework/isolated/worker.py

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,12 @@
1313
import importlib.metadata
1414
import json
1515
import logging
16+
import os
1617
import platform
1718
import sys
1819
from pathlib import Path
1920
from types import ModuleType
20-
from typing import Type, cast
21+
from typing import List, Type, cast
2122

2223
from cpex.framework.base import HookRef, Plugin, PluginRef
2324
from cpex.framework.constants import HOOK_TYPE
@@ -70,11 +71,12 @@ def get_environment_info():
7071
}
7172

7273

73-
def get_proper_config(name, module_path):
74+
def get_proper_config(name):
7475
"""
7576
Load a config which has all it's proper decorations
7677
"""
77-
plugin_loader_config = ConfigLoader.load_config(Path(f"{module_path}/config.yaml").resolve(), use_jinja=False)
78+
plugin_config_file = os.environ.get("PLUGINS_CONFIG_FILE", "plugins/config.yaml")
79+
plugin_loader_config = ConfigLoader.load_config(Path(plugin_config_file).resolve(), use_jinja=False)
7880
plugins: list[dict] = []
7981
config = None
8082
if plugin_loader_config.plugins:
@@ -102,21 +104,18 @@ async def process_task(task_data, tp: TaskProcessor):
102104
# relative path from project root.
103105
json_config = task_data.get("config")
104106
config_raw = json.loads(json_config)
105-
module_path: str = task_data.get("script_path")
106-
107-
# Security: Validate module_path to prevent directory traversal
108-
if ".." in module_path or module_path.startswith("/"):
109-
raise ValueError(f"Invalid module_path: '{module_path}' - path traversal not allowed")
110-
111-
if tp.module_path_hash != tp.compute_hash(module_path) or tp.config_hash != tp.compute_hash(json_config):
112-
# pull the resolved plugin path and only add the module path if it has the same root
107+
module_paths: List[str] = task_data.get("plugin_dirs")
108+
for module_path in module_paths:
113109
path = Path(module_path).resolve()
114110
resolved_module_path = str(path)
115111
if path.exists():
116112
sys.path.append(resolved_module_path)
117113
else:
118114
raise RuntimeError(f"plugin module_path '{resolved_module_path}' does not exist.")
119-
config = get_proper_config(config_raw.get("name"), module_path)
115+
116+
if tp.config_hash != tp.compute_hash(json_config):
117+
# pull the resolved plugin path and only add the module path if it has the same root
118+
config = get_proper_config(config_raw.get("name"))
120119
hook_type = task_data.get(HOOK_TYPE)
121120
cls_name: str = task_data.get("class_name")
122121
mod_name, n_cls_name = parse_class_name(cls_name)

cpex/templates/isolated/{{cookiecutter.plugin_slug}}/config.yaml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ plugins:
2323
# Plugin config dict passed to the plugin constructor
2424
class_name: "{{ cookiecutter.plugin_slug }}.plugin.{{ class_name }}"
2525
requirements_file: "requirements.txt"
26-
# essentially the plugin folder hosting the plugin
27-
script_path: "{{ cookiecutter.plugin_slug }}"
2826

2927
# Plugin directories to scan
3028
plugin_dirs:

tests/unit/cpex/framework/isolated/test_client.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ class TestIsolatedVenvPlugin:
2929
def mock_config(self, tmp_path):
3030
"""Create a mock plugin configuration."""
3131
venv_path = tmp_path / ".venv"
32-
script_path = "tests/unit/cpex/fixtures/plugins/isolated"
3332
requirements_file = tmp_path / "test_plugin" / "requirements.txt"
3433

3534
config_dict = {
@@ -43,7 +42,6 @@ def mock_config(self, tmp_path):
4342
"class_name": "test_plugin.TestPlugin",
4443
"venv_path": venv_path,
4544
"requirements_file": requirements_file,
46-
"script_path": script_path
4745
}
4846
}
4947

@@ -63,11 +61,10 @@ def plugin_context(self):
6361
)
6462
return plugin_context
6563

66-
def test_init(self, plugin, mock_config):
64+
def test_init(self, plugin):
6765
"""Test plugin initialization."""
6866
assert plugin.name == "test_plugin"
6967
assert plugin.implementation == "Python"
70-
assert plugin.script_path == mock_config.config["script_path"]
7168
assert plugin.comm is None
7269

7370
@pytest.mark.asyncio

tests/unit/cpex/framework/isolated/test_integration.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,7 @@ async def test_plugin_manager_with_isolated_plugin(
6767
# Create manager
6868
manager = PluginManager(integration_config_path)
6969

70-
# This will fail because the config path doesn't exist in the test environment
71-
# but we can test the structure
72-
with pytest.raises(RuntimeError):
73-
await manager.initialize()
70+
await manager.initialize()
7471

7572
@pytest.mark.asyncio
7673
@patch("cpex.framework.isolated.client.VenvProcessCommunicator")

tests/unit/cpex/framework/isolated/test_worker.py

Lines changed: 19 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@
2222
class TestWorkerFunctions:
2323
"""Test suite for worker.py functions."""
2424

25+
@pytest.fixture
26+
def mock_plugin_dirs(self, tmp_path):
27+
"""ensure that the plugins directory exists"""
28+
plugin_dirs = tmp_path / "plugins"
29+
tmp = Path(plugin_dirs)
30+
tmp.mkdir(parents=True, exist_ok=True)
31+
return [str(plugin_dirs.resolve())]
32+
2533
def test_get_environment_info(self):
2634
"""Test getting environment information."""
2735
info = get_environment_info()
@@ -48,7 +56,7 @@ def test_get_proper_config_found(self, mock_load_config):
4856
mock_config.plugins = [mock_plugin]
4957
mock_load_config.return_value = mock_config
5058

51-
result = get_proper_config("test_plugin", "plugins")
59+
result = get_proper_config("test_plugin")
5260

5361
assert result is not None
5462
assert result.name == "test_plugin"
@@ -63,7 +71,7 @@ def test_get_proper_config_not_found(self, mock_load_config):
6371
mock_config.plugins = [mock_plugin]
6472
mock_load_config.return_value = mock_config
6573

66-
result = get_proper_config("test_plugin", "plugins")
74+
result = get_proper_config("test_plugin")
6775

6876
assert result is None
6977

@@ -74,7 +82,7 @@ def test_get_proper_config_no_plugins(self, mock_load_config):
7482
mock_config.plugins = None
7583
mock_load_config.return_value = mock_config
7684

77-
result = get_proper_config("test_plugin", "plugins")
85+
result = get_proper_config("test_plugin")
7886

7987
assert result is None
8088

@@ -95,7 +103,7 @@ async def test_process_task_info(self):
95103
@patch("cpex.framework.isolated.worker.get_proper_config")
96104
@patch("cpex.framework.isolated.worker.importlib.import_module")
97105
@patch("cpex.framework.isolated.worker.PluginExecutor")
98-
async def test_process_task_load_and_run_hook_success(self, mock_executor_class, mock_import, mock_get_config):
106+
async def test_process_task_load_and_run_hook_success(self, mock_executor_class, mock_import, mock_get_config, mock_plugin_dirs):
99107
"""Test processing load_and_run_hook task successfully."""
100108
# Setup mock config
101109
mock_config = MagicMock()
@@ -127,7 +135,7 @@ async def test_process_task_load_and_run_hook_success(self, mock_executor_class,
127135
task_data = {
128136
"task_type": "load_and_run_hook",
129137
"config": json.dumps(config_dict),
130-
"script_path": "plugins",
138+
"plugin_dirs": mock_plugin_dirs,
131139
"class_name": "test_plugin.TestPlugin",
132140
"hook_type": "tool_pre_invoke",
133141
"payload": {"name": "test_tool", "args": {}},
@@ -150,7 +158,6 @@ async def test_process_task_load_and_run_hook_no_config(self, mock_get_config):
150158
task_data = {
151159
"task_type": "load_and_run_hook",
152160
"config": json.dumps(config_dict),
153-
"script_path": "plugins",
154161
"class_name": "test_plugin.TestPlugin",
155162
"hook_type": "tool_pre_invoke",
156163
"payload": {},
@@ -164,7 +171,7 @@ async def test_process_task_load_and_run_hook_no_config(self, mock_get_config):
164171
@pytest.mark.asyncio
165172
@patch("cpex.framework.isolated.worker.get_proper_config")
166173
@patch("cpex.framework.isolated.worker.importlib.import_module")
167-
async def test_process_task_load_and_run_hook_import_error(self, mock_import, mock_get_config):
174+
async def test_process_task_load_and_run_hook_import_error(self, mock_import, mock_get_config, mock_plugin_dirs):
168175
"""Test processing load_and_run_hook task with import error."""
169176
mock_config = MagicMock()
170177
mock_get_config.return_value = mock_config
@@ -175,8 +182,8 @@ async def test_process_task_load_and_run_hook_import_error(self, mock_import, mo
175182
task_data = {
176183
"task_type": "load_and_run_hook",
177184
"config": json.dumps(config_dict),
178-
"script_path": "plugins",
179185
"class_name": "test_plugin.TestPlugin",
186+
"plugin_dirs": mock_plugin_dirs,
180187
"hook_type": "tool_pre_invoke",
181188
"payload": {},
182189
"context": {"state": {}, "global_context": {}, "metadata": {}},
@@ -189,7 +196,7 @@ async def test_process_task_load_and_run_hook_import_error(self, mock_import, mo
189196
@patch("cpex.framework.isolated.worker.get_proper_config")
190197
@patch("cpex.framework.isolated.worker.importlib.import_module")
191198
@patch("cpex.framework.isolated.worker.PluginExecutor")
192-
async def test_process_task_with_different_hook_types(self, mock_executor_class, mock_import, mock_get_config):
199+
async def test_process_task_with_different_hook_types(self, mock_executor_class, mock_import, mock_get_config, mock_plugin_dirs):
193200
"""Test processing tasks with different hook types."""
194201
# Setup mocks
195202
mock_config = MagicMock()
@@ -222,7 +229,7 @@ async def test_process_task_with_different_hook_types(self, mock_executor_class,
222229
task_data = {
223230
"task_type": "load_and_run_hook",
224231
"config": json.dumps(config_dict),
225-
"script_path": "plugins",
232+
"plugin_dirs": mock_plugin_dirs,
226233
"class_name": "test_plugin.TestPlugin",
227234
"hook_type": hook_type,
228235
"payload": {},
@@ -244,7 +251,7 @@ async def test_process_task_unknown_task_type(self):
244251
@patch("cpex.framework.isolated.worker.get_proper_config")
245252
@patch("cpex.framework.isolated.worker.importlib.import_module")
246253
@patch("cpex.framework.isolated.worker.PluginExecutor")
247-
async def test_process_task_with_metadata(self, mock_executor_class, mock_import, mock_get_config):
254+
async def test_process_task_with_metadata(self, mock_executor_class, mock_import, mock_get_config, mock_plugin_dirs):
248255
"""Test processing task with metadata in context."""
249256
mock_config = MagicMock()
250257
mock_get_config.return_value = mock_config
@@ -273,8 +280,8 @@ async def test_process_task_with_metadata(self, mock_executor_class, mock_import
273280
task_data = {
274281
"task_type": "load_and_run_hook",
275282
"config": json.dumps(config_dict),
276-
"script_path": "plugins",
277283
"class_name": "test_plugin.TestPlugin",
284+
"plugin_dirs": mock_plugin_dirs,
278285
"hook_type": "tool_pre_invoke",
279286
"payload": {"name": "test_tool"},
280287
"context": {
@@ -402,7 +409,6 @@ async def test_main_with_load_and_run_hook_task(self, mock_process_task, mock_pr
402409
task_data = {
403410
"task_type": "load_and_run_hook",
404411
"config": json.dumps(config_dict),
405-
"script_path": "plugins",
406412
"class_name": "test_plugin.TestPlugin",
407413
"hook_type": "tool_pre_invoke",
408414
"payload": {"name": "test_tool"},

0 commit comments

Comments
 (0)