feat: add caller context to plugin config hooks

This commit is contained in:
Alessandro 2026-08-12 03:17:28 +02:00
parent 6257f2cb3b
commit 1121758a6f
3 changed files with 77 additions and 3 deletions

View file

@ -45,6 +45,7 @@ _META_TARGET_RE = re.compile(
type ToggleState = Literal["enabled", "disabled"]
type CallerContext = Literal["ui", "agent", "api"]
class PluginAssetFile(TypedDict):
@ -590,6 +591,7 @@ def get_plugin_config(
agent: Agent | None = None,
project_name: str | None = None,
agent_profile: str | None = None,
caller: CallerContext = "api",
):
default_used = False
@ -635,6 +637,7 @@ def get_plugin_config(
agent=agent,
project_name=project_name,
agent_profile=agent_profile,
hook_context={"caller": caller},
)
return result
@ -663,7 +666,11 @@ def get_default_plugin_config(plugin_name: str):
@extension.extensible
def save_plugin_config(
plugin_name: str, project_name: str, agent_profile: str, settings: dict
plugin_name: str,
project_name: str,
agent_profile: str,
settings: dict,
caller: CallerContext = "api",
):
file_path = determine_plugin_asset_path(
plugin_name, project_name, agent_profile, CONFIG_FILE_NAME
@ -677,6 +684,7 @@ def save_plugin_config(
project_name=project_name,
agent_profile=agent_profile,
settings=settings,
hook_context={"caller": caller},
)
# or do standard load

View file

@ -35,9 +35,9 @@
- `determined_toggle_from_paths(default: bool, paths: Iterator[str])`
- `get_toggle_state(plugin_name: str) -> ToggleState`
- `toggle_plugin(plugin_name: str, enabled: bool, project_name: str=..., agent_profile: str=..., clear_overrides: bool=...)`
- `get_plugin_config(plugin_name: str, agent: Agent | None=..., project_name: str | None=..., agent_profile: str | None=...)`
- `get_plugin_config(plugin_name: str, agent: Agent | None=..., project_name: str | None=..., agent_profile: str | None=..., caller: CallerContext=...)`
- `get_default_plugin_config(plugin_name: str)`
- `save_plugin_config(plugin_name: str, project_name: str, agent_profile: str, settings: dict)`
- `save_plugin_config(plugin_name: str, project_name: str, agent_profile: str, settings: dict, caller: CallerContext=...)`
- `find_plugin_asset(plugin_name: str, *subpaths, project_name=..., agent_profile=...)`
- `find_plugin_assets(*subpaths, plugin_name: str=..., project_name: str=..., agent_profile: str=..., only_first: bool=...) -> list[PluginAssetFile]`
- `determine_plugin_asset_path(plugin_name: str, project_name: str, agent_profile: str, *subpaths)`
@ -51,6 +51,8 @@
- Helper modules own reusable framework APIs and must preserve public callers unless all callers, tests, and docs are updated together.
- Plugins marked `always_enabled` remain in runtime discovery regardless of
stale global or scoped disable files, and disable attempts are rejected.
- Config hooks receive `hook_context={"caller": caller}` with one of `ui`,
`agent`, or `api`; this is behavioral context, not an authorization boundary.
- Update this file whenever public functions, classes, persistence behavior, path/security assumptions, side effects, or cross-module contracts change.
- Observed side-effect areas: filesystem reads, filesystem writes, filesystem deletion, WebSocket state, plugin state, settings/state persistence, secret handling.
- Imported dependency areas include: `__future__`, `asyncio`, `glob`, `helpers`, `helpers.defer`, `helpers.watchdog`, `json`, `pathlib`, `pydantic`, `re`, `regex`, `time`, `typing`.

View file

@ -0,0 +1,64 @@
import sys
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from helpers import plugins
def _capture_hook_context(monkeypatch):
captured = {}
def call_plugin_hook(plugin_name, hook_name, default=None, **kwargs):
captured.update(
plugin_name=plugin_name,
hook_name=hook_name,
hook_context=kwargs["hook_context"],
)
return default
monkeypatch.setattr(plugins, "call_plugin_hook", call_plugin_hook)
return captured
def test_get_plugin_config_forwards_caller_to_hook(monkeypatch):
captured = _capture_hook_context(monkeypatch)
monkeypatch.setattr(
plugins, "find_plugin_asset", lambda *_args, **_kwargs: {"path": "config.json"}
)
monkeypatch.setattr(plugins.files, "exists", lambda _path: True)
monkeypatch.setattr(plugins.files, "read_file", lambda _path: '{"enabled": true}')
assert plugins.get_plugin_config.__wrapped__("example", caller="ui") == {
"enabled": True
}
assert captured == {
"plugin_name": "example",
"hook_name": "get_plugin_config",
"hook_context": {"caller": "ui"},
}
def test_save_plugin_config_forwards_caller_to_hook(monkeypatch):
captured = _capture_hook_context(monkeypatch)
saved = []
monkeypatch.setattr(
plugins, "determine_plugin_asset_path", lambda *_args, **_kwargs: "config.json"
)
monkeypatch.setattr(
plugins.files, "write_file", lambda path, content: saved.append((path, content))
)
plugins.save_plugin_config.__wrapped__(
"example", "", "", {"enabled": True}, caller="agent"
)
assert captured == {
"plugin_name": "example",
"hook_name": "save_plugin_config",
"hook_context": {"caller": "agent"},
}
assert saved == [("config.json", '{"enabled": true}')]