From d2eaa11680dccd11ae32e294c0e44a951cbcc5a1 Mon Sep 17 00:00:00 2001 From: Sebastien Taggart Date: Wed, 8 Apr 2026 14:35:09 -0400 Subject: [PATCH] Deny state-only plugins from invoking actions --- src/deckhand/plugins/capabilities.py | 5 +++-- tests/test_plugin_capabilities.py | 5 +++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/deckhand/plugins/capabilities.py b/src/deckhand/plugins/capabilities.py index ffd5c5f..1901d25 100644 --- a/src/deckhand/plugins/capabilities.py +++ b/src/deckhand/plugins/capabilities.py @@ -11,7 +11,8 @@ orchestrator. - ``state-only``: everything in ``read-only`` plus state mutation, signal registration, and event emission. No orchestrator access and cannot - register actions (which by definition drive orchestrator commands). + register *or invoke* actions (which by definition drive orchestrator + commands). - ``full``: unrestricted access, equivalent to the raw ``PluginRegistry``. """ @@ -65,7 +66,7 @@ def register( self._inner.register(name, handler, description, payload_schema) async def run(self, name: str, payload: dict[str, object]) -> None: - if self._capability == "read-only": + if self._capability != "full": raise _deny(self._capability, "run actions") await self._inner.run(name, payload) diff --git a/tests/test_plugin_capabilities.py b/tests/test_plugin_capabilities.py index 591f2f7..8d500fd 100644 --- a/tests/test_plugin_capabilities.py +++ b/tests/test_plugin_capabilities.py @@ -101,6 +101,11 @@ async def noop(payload: dict) -> None: with pytest.raises(PermissionError): scoped.actions.register("evil.action", noop) + # Action invocation also denied — state-only must not reach orchestrator + # commands via the action layer. + with pytest.raises(PermissionError): + await scoped.actions.run("agent.start", {"agent_id": "mock-1"}) + async def test_load_plugins_with_spec(plugin_registry: PluginRegistry) -> None: load_plugins(