Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions src/deckhand/plugins/capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -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``.
"""

Expand Down Expand Up @@ -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)

Expand Down
5 changes: 5 additions & 0 deletions tests/test_plugin_capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down