mirror of
https://github.com/rennf93/roboco.git
synced 2026-08-03 07:23:24 +02:00
* feat(a2a): live view — watch fleet conversations, CEO chime-in, reply budget A2A_MESSAGE_SENT published from A2AService.send (excerpt-capped) and fanned through the existing /ws/system bridge; CEO-only admin REST for conversations/messages + a reply route on the publish-bearing send path; panel /a2a page with live transcript and a composer gated on task-linked conversations. The matrix gains its one asymmetric rule: CEO may message anyone, nobody may target the CEO — and agent replies inside a CEO-opened conversation are hard-budgeted to one per CEO message (per conversation, per agent), rejected with wait-don't-retry guidance. Built subagent-driven (Sonnet 5), reviewed; v1 seams documented in the map delta. * feat(prompter): intake remembers the task history Intake spawns now carry a per-project chronological digest of recent tasks (capped: 15 lines/project, 4000 chars total — ~300-1000 tokens) merged into the ambient layer, and the interviewer gets a bounded search_past_tasks tool (one shared implementation behind the grok MCP tool and the Claude SDK in-process tool) to check precedent mid-conversation. Informational memory only — the sequencing analyzer keeps ownership of ordering. Built subagent-driven (Sonnet 5), reviewed; pre-existing conventions-ambient MegaTask-scope gap flagged, untouched. --------- Co-authored-by: Renn F <rennf93@users.noreply.github.com>
97 lines
3.2 KiB
Python
97 lines
3.2 KiB
Python
"""enforcement.a2a_access coverage."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
from roboco.agents_config import can_a2a_direct
|
|
from roboco.enforcement.a2a_access import (
|
|
A2AAccessDeniedError,
|
|
get_a2a_allowed_targets,
|
|
validate_a2a_access,
|
|
)
|
|
|
|
|
|
def test_validate_a2a_self_a2a_denied() -> None:
|
|
with pytest.raises(A2AAccessDeniedError, match="cannot A2A yourself"):
|
|
validate_a2a_access("be-dev-1", "be-dev-1")
|
|
|
|
|
|
def test_validate_a2a_to_ceo_denied() -> None:
|
|
with pytest.raises(A2AAccessDeniedError):
|
|
validate_a2a_access("be-dev-1", "ceo")
|
|
|
|
|
|
def test_validate_a2a_within_cell() -> None:
|
|
"""Cell members can A2A within their cell."""
|
|
result = validate_a2a_access("be-dev-1", "be-qa")
|
|
assert result is True
|
|
|
|
|
|
def test_a2a_access_denied_error_has_attributes() -> None:
|
|
err = A2AAccessDeniedError(
|
|
from_agent="be-dev-1",
|
|
to_agent="ceo",
|
|
reason="CEO is human",
|
|
)
|
|
assert err.from_agent == "be-dev-1"
|
|
assert err.to_agent == "ceo"
|
|
|
|
|
|
def test_get_a2a_allowed_targets_returns_list() -> None:
|
|
targets = get_a2a_allowed_targets("be-dev-1", ["be-qa", "be-pm", "fe-dev-1", "ceo"])
|
|
assert isinstance(targets, list)
|
|
|
|
|
|
def test_get_a2a_allowed_targets_excludes_ceo() -> None:
|
|
targets = get_a2a_allowed_targets("be-dev-1", ["ceo"])
|
|
assert "ceo" not in targets
|
|
|
|
|
|
def test_get_a2a_allowed_targets_excludes_self() -> None:
|
|
targets = get_a2a_allowed_targets("be-dev-1", ["be-dev-1", "be-qa"])
|
|
# Self should be filtered.
|
|
assert "be-dev-1" not in targets or "be-qa" in targets
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# CEO-initiated A2A — the one asymmetric rule: CEO may send, nobody may
|
|
# target CEO (the block above must still hold).
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_validate_a2a_access_ceo_to_agent_allowed() -> None:
|
|
result = validate_a2a_access("ceo", "be-dev-1")
|
|
assert result is True
|
|
|
|
|
|
def test_can_a2a_direct_ceo_to_main_pm() -> None:
|
|
assert can_a2a_direct("ceo", "main-pm") == (True, None)
|
|
|
|
|
|
def test_can_a2a_direct_ceo_to_board_member() -> None:
|
|
"""Board members are normally unreachable via direct A2A for everyone
|
|
else (routed through main-pm) — CEO is exempt from that restriction."""
|
|
assert can_a2a_direct("ceo", "product-owner") == (True, None)
|
|
|
|
|
|
def test_validate_a2a_to_ceo_still_denied_with_ceo_send_rule() -> None:
|
|
"""Regression: allowing CEO-initiated A2A must not loosen the inbound
|
|
block — nobody may target the CEO."""
|
|
with pytest.raises(A2AAccessDeniedError):
|
|
validate_a2a_access("be-dev-1", "ceo")
|
|
|
|
|
|
def test_get_a2a_allowed_targets_ceo_includes_all_roles() -> None:
|
|
targets = get_a2a_allowed_targets("ceo", ["be-dev-1", "be-qa", "main-pm"])
|
|
assert set(targets) == {"be-dev-1", "be-qa", "main-pm"}
|
|
|
|
|
|
def test_can_a2a_direct_to_ceo_message_explains_reply_only() -> None:
|
|
"""An agent can never INITIATE with the CEO (only reply inside a
|
|
conversation the CEO opened) — the matrix denial message must say so,
|
|
not point at the old blanket 'use notify()' framing."""
|
|
allowed, reason = can_a2a_direct("be-dev-1", "ceo")
|
|
assert allowed is False
|
|
assert reason is not None
|
|
assert "reply" in reason.lower()
|