mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
127 lines
4.2 KiB
Python
127 lines
4.2 KiB
Python
"""Auditor is silent — runtime guard refuses say()/dm().
|
|||
|
|
|
||
|
|
Spec §5.5: the auditor is a silent observer. The spawn manifest already
|
||
|
|
omits `say` and `dm` from the auditor's tool surface, but that is a
|
||
|
|
convention-only defense. These tests pin a defense-in-depth runtime guard
|
||
|
|
inside ContentActions.say/dm: if the caller's role is "auditor", the
|
||
|
|
verb refuses with Envelope.not_authorized regardless of how the call
|
||
|
|
arrived.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from unittest.mock import AsyncMock, MagicMock
|
||
|
|
from uuid import uuid4
|
||
|
|
|
||
|
|
import pytest
|
||
|
|
from roboco.services.gateway.content_actions import (
|
||
|
|
ContentActions,
|
||
|
|
ContentActionsDeps,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def _make_deps(agent_role: str, **overrides: AsyncMock) -> ContentActionsDeps:
|
||
|
|
"""Build a ContentActionsDeps whose task.agent_for returns the given role."""
|
||
|
|
if "task" in overrides:
|
||
|
|
task = overrides["task"]
|
||
|
|
else:
|
||
|
|
task = AsyncMock()
|
||
|
|
task.get_active_task_for_agent.return_value = None
|
||
|
|
task.agent_for.return_value = MagicMock(role=agent_role)
|
||
|
|
|
||
|
|
git = overrides.get("git", AsyncMock())
|
||
|
|
messaging = overrides.get("messaging", AsyncMock())
|
||
|
|
a2a = overrides.get("a2a", AsyncMock())
|
||
|
|
journal = overrides.get("journal", AsyncMock())
|
||
|
|
workspace = overrides.get("workspace", AsyncMock())
|
||
|
|
notifications = overrides.get("notifications", AsyncMock())
|
||
|
|
return ContentActionsDeps(
|
||
|
|
task=task,
|
||
|
|
git=git,
|
||
|
|
messaging=messaging,
|
||
|
|
a2a=a2a,
|
||
|
|
journal=journal,
|
||
|
|
workspace=workspace,
|
||
|
|
notifications=notifications,
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_auditor_say_returns_not_authorized() -> None:
|
||
|
|
"""Auditor role calling say() is refused regardless of manifest."""
|
||
|
|
auditor_id = uuid4()
|
||
|
|
deps = _make_deps("auditor")
|
||
|
|
actions = ContentActions(deps)
|
||
|
|
|
||
|
|
env = await actions.say(agent_id=auditor_id, channel="backend-cell", text="hi")
|
||
|
|
body = env.as_dict()
|
||
|
|
|
||
|
|
assert body["error"] == "not_authorized"
|
||
|
|
haystack = (body.get("message") or "") + " " + (body.get("remediate") or "")
|
||
|
|
assert "silent" in haystack.lower() or "auditor" in haystack.lower()
|
||
|
|
# The messaging service must not have been touched — the guard fires
|
||
|
|
# before any downstream call.
|
||
|
|
deps.messaging.post_to_channel.assert_not_called()
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_auditor_dm_returns_not_authorized() -> None:
|
||
|
|
"""Auditor role calling dm() is refused regardless of manifest."""
|
||
|
|
auditor_id = uuid4()
|
||
|
|
deps = _make_deps("auditor")
|
||
|
|
actions = ContentActions(deps)
|
||
|
|
|
||
|
|
env = await actions.dm(
|
||
|
|
agent_id=auditor_id,
|
||
|
|
recipient=str(uuid4()),
|
||
|
|
text="hi",
|
||
|
|
task_id=uuid4(),
|
||
|
|
)
|
||
|
|
body = env.as_dict()
|
||
|
|
|
||
|
|
assert body["error"] == "not_authorized"
|
||
|
|
haystack = (body.get("message") or "") + " " + (body.get("remediate") or "")
|
||
|
|
assert "silent" in haystack.lower() or "auditor" in haystack.lower()
|
||
|
|
deps.a2a.send.assert_not_called()
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_developer_say_passes_auditor_guard() -> None:
|
||
|
|
"""Non-auditor roles are not blocked by the new guard.
|
||
|
|
|
||
|
|
The messaging mock returns None so downstream flow is whatever the
|
||
|
|
happy path is — we only assert that the auditor guard didn't fire
|
||
|
|
(i.e. the response is not the auditor-silent not_authorized envelope).
|
||
|
|
"""
|
||
|
|
dev_id = uuid4()
|
||
|
|
deps = _make_deps("developer")
|
||
|
|
actions = ContentActions(deps)
|
||
|
|
|
||
|
|
env = await actions.say(agent_id=dev_id, channel="backend-cell", text="hi")
|
||
|
|
body = env.as_dict()
|
||
|
|
|
||
|
|
# The auditor-silent message is what we explicitly want to NOT see.
|
||
|
|
if body.get("error") == "not_authorized":
|
||
|
|
haystack = (body.get("message") or "") + " " + (body.get("remediate") or "")
|
||
|
|
assert "silent" not in haystack.lower()
|
||
|
|
|
||
|
|
|
||
|
|
@pytest.mark.asyncio
|
||
|
|
async def test_developer_dm_passes_auditor_guard() -> None:
|
||
|
|
"""dm() for a non-auditor role is not blocked by the new guard."""
|
||
|
|
dev_id = uuid4()
|
||
|
|
deps = _make_deps("developer")
|
||
|
|
actions = ContentActions(deps)
|
||
|
|
|
||
|
|
env = await actions.dm(
|
||
|
|
agent_id=dev_id,
|
||
|
|
recipient=str(uuid4()),
|
||
|
|
text="hi",
|
||
|
|
task_id=uuid4(),
|
||
|
|
)
|
||
|
|
body = env.as_dict()
|
||
|
|
|
||
|
|
if body.get("error") == "not_authorized":
|
||
|
|
haystack = (body.get("message") or "") + " " + (body.get("remediate") or "")
|
||
|
|
assert "silent" not in haystack.lower()
|