feat(a2a): CEO can DM the Auditor and PR reviewers (#623)

* feat(a2a): CEO can DM the Auditor and PR reviewers

A mid-flight PR reviewer or Auditor that's stuck was unreachable — the CEO
had no way to DM them. Both roles now carry dm/read_a2a, so the CEO can open
a 1:1 and they can reply in-thread through the existing CEO-reply path.

Scoped deliberately: the Auditor stays a silent observer to its peers — it
gains no peer-initiation surface (can_a2a_direct routes it through
_check_auditor_a2a, which refuses every initiation target; it can only reply
inside a CEO-opened DM). PR reviewers keep their owning-PM scope. Intake and
Secretary stay excluded — they have their own dedicated chat pages.

NO_COMMS_ROLES drops to {prompter, secretary}; the panel's EXCLUDE_NON_DM_ROLES
matches. KB/docs updated so the 'auditor/pr_reviewer have no dm' claim isn't
left stale.

* test(a2a): smoke guard checks _NO_COMMS_ROLES, not a hardcoded 'auditor'

The dm() runtime guard no longer names the auditor (it now carries dm to
reply to the CEO); it refuses the canonical _NO_COMMS_ROLES set. Assert on
that set so the smoke test tracks the guard, not a stale role name.

* chore(foundation): regenerate verb tables for auditor/pr_reviewer dm+read_a2a

---------

Co-authored-by: Renn F <rennf93@users.noreply.github.com>
This commit is contained in:
Renzo F
2026-07-21 05:54:29 +02:00
committed by GitHub
co-authored by Renn F
parent 775872cac0
commit 73c05cfa5e
24 changed files with 295 additions and 146 deletions
+15 -6
View File
@@ -99,23 +99,32 @@ def test_can_a2a_direct_to_ceo_message_explains_reply_only() -> None:
@pytest.mark.parametrize(
"target_slug",
["auditor", "pr-reviewer-1", "intake-1", "secretary-1"],
["intake-1", "secretary-1"],
)
def test_can_a2a_direct_ceo_to_no_comms_role_denied(target_slug: str) -> None:
"""The CEO's asymmetric reach still can't target a role with no dm/
read_a2a on its manifest (auditor, pr_reviewer, prompter, secretary)
nothing on the other end could ever read or answer the DM. The panel's
New-DM dialog already filters these client-side (EXCLUDE_NON_DM_ROLES);
this is the server-side backstop for a direct API/A2A-service call."""
read_a2a on its manifest (prompter, secretary — human-only, own chat
pages) — nothing on the other end could ever read or answer the DM."""
allowed, reason = can_a2a_direct("ceo", target_slug)
assert allowed is False
assert reason is not None
assert "comms" in reason.lower()
@pytest.mark.parametrize("target_slug", ["auditor", "pr-reviewer-1"])
def test_can_a2a_direct_ceo_to_auditor_or_pr_reviewer_allowed(target_slug: str) -> None:
"""The CEO can now DM a mid-flight auditor or PR reviewer — both carry
dm/read_a2a so they can read and reply in-thread, even though neither
gains a peer-initiation surface (auditor stays silent via
can_a2a_direct; the PR reviewer stays scoped to its owning PM)."""
allowed, reason = can_a2a_direct("ceo", target_slug)
assert allowed is True
assert reason is None
def test_can_a2a_direct_ceo_to_no_comms_role_reuses_canonical_set() -> None:
"""The refusal set must be exactly foundation.policy.communications'
NO_COMMS_ROLES — the same set services.gateway.content_actions uses to
gate the dm() sender side — so the two never drift apart."""
expected = {"auditor", "pr_reviewer", "prompter", "secretary"}
expected = {"prompter", "secretary"}
assert {role.value for role in NO_COMMS_ROLES} == expected