fix(gateway): thread agent_team through all 27 lifecycle Context sites

Arms the team-match spec gate that sat in its permissive fallback since
shipping: cell-scoped roles are now rejected on cross-team verbs at the
gateway (a misrouted frontend PM blocked, escalated, and held a backend
task live 2026-07-02). Org-wide roles remain exempt via the policy-side
_ORG_WIDE_ROLES. Test mocks aligned to carry consistent teams.
This commit is contained in:
Renn F
2026-07-02 15:44:34 +02:00
parent 0f1ed3cc6a
commit 8e5f84c4eb
18 changed files with 65 additions and 9 deletions
@@ -85,6 +85,7 @@ def _dev_task_svc(agent_id: object, task_id: object) -> AsyncMock:
status="pending",
plan=None,
assigned_to=None,
team="backend",
parent_task_id=None,
sequence=0,
task_type="code",
@@ -235,7 +235,7 @@ async def test_submit_up_blocks_when_subtask_pending() -> None:
sub = MagicMock(id=sub_id, status="paused", title="Paused subtask")
task_svc = AsyncMock()
task_svc.get.return_value = t
task_svc.agent_for.return_value = MagicMock(role="cell_pm")
task_svc.agent_for.return_value = MagicMock(role="cell_pm", team="backend")
task_svc.all_subtasks_terminal.return_value = False
task_svc.get_subtasks.return_value = [sub]
journal_svc = AsyncMock()
@@ -109,6 +109,7 @@ async def test_delegate_blocks_when_parent_assigned_to_other_agent() -> None:
project_id=uuid4(),
status="in_progress",
assigned_to=other_pm_id,
team="backend",
quick_context="Decomposition planned; cells implement their slice next.",
)
task_svc = AsyncMock()
@@ -134,6 +135,7 @@ async def test_delegate_allows_when_parent_in_progress_and_owned() -> None:
project_id=uuid4(),
status="in_progress",
assigned_to=pm_id,
team="backend",
quick_context="Decomposition planned; cells implement their slice next.",
)
new_task = MagicMock(id=uuid4())
@@ -162,6 +164,7 @@ async def test_delegate_blocks_when_subtask_cap_exceeded() -> None:
project_id=uuid4(),
status="in_progress",
assigned_to=pm_id,
team="backend",
quick_context="Decomposition planned; cells implement their slice next.",
)
too_many = [MagicMock(id=uuid4()) for _ in range(13)]
@@ -189,6 +192,7 @@ async def test_delegate_allows_when_subtask_cap_within_soft_zone() -> None:
project_id=uuid4(),
status="in_progress",
assigned_to=pm_id,
team="backend",
quick_context="Decomposition planned; cells implement their slice next.",
)
many = [MagicMock(id=uuid4()) for _ in range(10)]
@@ -217,6 +221,7 @@ async def test_delegate_allows_at_zero_subtasks() -> None:
project_id=uuid4(),
status="in_progress",
assigned_to=pm_id,
team="backend",
quick_context="Decomposition planned; cells implement their slice next.",
)
new_task = MagicMock(id=uuid4())
@@ -243,6 +248,7 @@ async def test_delegate_blocks_at_exact_cap_plus_one() -> None:
project_id=uuid4(),
status="in_progress",
assigned_to=pm_id,
team="backend",
quick_context="Decomposition planned; cells implement their slice next.",
)
# Already 12 children — adding the 13th must be blocked.
@@ -272,6 +278,7 @@ async def test_delegate_blocks_when_parent_quick_context_empty() -> None:
project_id=uuid4(),
status="in_progress",
assigned_to=pm_id,
team="backend",
quick_context="",
)
task_svc = AsyncMock()
@@ -304,6 +311,7 @@ async def test_delegate_past_max_depth_returns_invalid_state_not_500() -> None:
project_id=uuid4(),
status="in_progress",
assigned_to=pm_id,
team="backend",
quick_context="Decomposition planned; cells implement their slice next.",
)
depth_msg = (
@@ -136,6 +136,7 @@ async def test_i_will_work_on_pending_with_plan() -> None:
status="pending",
plan=None,
assigned_to=None,
team="backend",
parent_task_id=None,
sequence=0,
task_type="code",
@@ -338,6 +339,7 @@ async def test_i_will_work_on_blocks_when_journal_note_at_claim_missing() -> Non
status="pending",
plan=None,
assigned_to=None,
team="backend",
parent_task_id=None,
sequence=0,
task_type="code",
@@ -632,6 +632,7 @@ async def test_delegate_parent_no_project_rejected() -> None:
parent = MagicMock(
status="in_progress",
assigned_to=pm_id,
team="backend",
project_id=None,
product_id=None,
title="p",
+1 -1
View File
@@ -757,7 +757,7 @@ async def test_complete_dispatches_cell_pm() -> None:
after = MagicMock(**{**t.__dict__, "status": "completed"})
task_svc = AsyncMock()
task_svc.get.return_value = t
task_svc.agent_for.return_value = MagicMock(role="cell_pm")
task_svc.agent_for.return_value = MagicMock(role="cell_pm", team="backend")
task_svc.all_subtasks_terminal.return_value = True
task_svc.cell_pm_complete.return_value = after
git_svc = AsyncMock()
@@ -79,6 +79,7 @@ async def test_i_will_plan_claims_starts_and_sets_plan() -> None:
status="pending",
plan=None,
assigned_to=None,
team="backend",
task_type="planning",
parent_task_id=None,
sequence=0,
@@ -164,6 +165,7 @@ async def test_i_will_plan_blocks_when_journal_decision_at_claim_missing() -> No
status="pending",
plan=None,
assigned_to=None,
team="backend",
task_type="planning",
parent_task_id=None,
sequence=0,
@@ -715,6 +717,7 @@ async def test_delegate_cell_pm_to_team_dev_creates_subtask() -> None:
project_id=project_id,
status="in_progress",
assigned_to=cell_pm_id,
team="backend",
quick_context="Decomposition planned; cells implement their slice next.",
)
new_task = MagicMock(id=uuid4())
@@ -862,6 +865,7 @@ async def test_delegate_invalid_team_enum_rejected() -> None:
project_id=uuid4(),
status="in_progress",
assigned_to=pm_id,
team="backend",
quick_context="Decomposition planned; cells implement their slice next.",
)
task_svc = AsyncMock()
@@ -1013,7 +1017,7 @@ async def test_submit_up_blocks_without_journal_decision() -> None:
async def test_submit_up_short_notes_rejected() -> None:
pm_id = uuid4()
task_id = uuid4()
t = MagicMock(id=task_id, status="in_progress", assigned_to=pm_id)
t = MagicMock(id=task_id, status="in_progress", assigned_to=pm_id, team="backend")
task_svc = AsyncMock()
task_svc.get.return_value = t
task_svc.agent_for.return_value = MagicMock(role="cell_pm", team="backend")
@@ -277,6 +277,7 @@ async def test_i_will_plan_calls_claim_and_start_with_task_id_first() -> None:
status="pending",
plan=None,
assigned_to=None,
team="backend",
task_type="planning",
parent_task_id=None,
sequence=0,
@@ -61,6 +61,7 @@ def _parent_in_progress(pm_id: Any) -> MagicMock:
project_id=uuid4(),
status="in_progress",
assigned_to=pm_id,
team="backend",
priority=2,
# delegate obligates the PM's quick_context resumption section.
quick_context="Decomposition planned; cells implement their slice next.",
@@ -60,6 +60,7 @@ def _parent(pm_id: object) -> MagicMock:
product_id=None,
status="in_progress",
assigned_to=pm_id,
team="backend",
# delegate obligates the PM's quick_context resumption section.
quick_context="Decomposition planned; cells implement their slice next.",
)
@@ -48,6 +48,7 @@ def _parent(pm_id: Any, product_id: Any = None, project_id: Any = None) -> Magic
product_id=product_id,
status="in_progress",
assigned_to=pm_id,
team="backend",
# delegate obligates the PM's quick_context resumption section.
quick_context="Decomposition planned; cells implement their slice next.",
)
+3 -3
View File
@@ -59,7 +59,7 @@ def _make_deps(**overrides: Any) -> ChoreographerDeps:
async def test_resume_transitions_paused_to_in_progress() -> None:
aid = uuid4()
tid = uuid4()
t = MagicMock(id=tid, status="paused", assigned_to=aid)
t = MagicMock(id=tid, status="paused", assigned_to=aid, team="backend")
task_svc = AsyncMock()
task_svc.get.return_value = t
task_svc.agent_for.return_value = MagicMock(
@@ -108,7 +108,7 @@ async def test_resume_rejects_when_not_claimant() -> None:
aid = uuid4()
other = uuid4()
tid = uuid4()
t = MagicMock(id=tid, status="paused", assigned_to=other)
t = MagicMock(id=tid, status="paused", assigned_to=other, team="backend")
task_svc = AsyncMock()
task_svc.get.return_value = t
task_svc.agent_for.return_value = MagicMock(
@@ -177,7 +177,7 @@ async def test_resume_success_writes_heartbeat() -> None:
"""Heartbeat fires on success — agent is back to active work."""
aid = uuid4()
tid = uuid4()
t = MagicMock(id=tid, status="paused", assigned_to=aid)
t = MagicMock(id=tid, status="paused", assigned_to=aid, team="backend")
task_svc = AsyncMock()
task_svc.get.return_value = t
task_svc.agent_for.return_value = MagicMock(
@@ -182,7 +182,7 @@ async def test_i_will_plan_calls_ensure_work_session() -> None:
commits=[],
pr_number=None,
quick_context=None,
team="main_pm",
team="backend",
task_type="planning",
parent_task_id=None,
sequence=0,
@@ -199,7 +199,7 @@ async def test_i_will_plan_calls_ensure_work_session() -> None:
commits=[],
pr_number=None,
quick_context=None,
team="main_pm",
team="backend",
task_type="planning",
parent_task_id=None,
sequence=0,