From 555c33ad7d9da379f406a2bb59478ae4119cc4ab Mon Sep 17 00:00:00 2001 From: Renn F Date: Thu, 4 Jun 2026 02:12:45 +0200 Subject: [PATCH] fix(gateway): let a dependent agent read its dependency via evidence evidence is read-only, but the cross-agent ownership gate blocked a caller from inspecting a task its own work depends on (a frontend cell could not read the UX task it was waiting on). Exempt reads where the target is a dependency of a task assigned to the caller. Also dropped stale internal refs from the docstring. --- roboco/services/gateway/content_actions.py | 37 +++++++++++------- .../gateway/test_content_actions_ownership.py | 38 +++++++++++++++++++ 2 files changed, 62 insertions(+), 13 deletions(-) diff --git a/roboco/services/gateway/content_actions.py b/roboco/services/gateway/content_actions.py index 30ec9064..1ac8dd67 100644 --- a/roboco/services/gateway/content_actions.py +++ b/roboco/services/gateway/content_actions.py @@ -703,6 +703,17 @@ class ContentActions: context_briefing={}, ) + async def _is_caller_dependency(self, agent_id: UUID, task: Any) -> bool: + """True when ``task`` is a dependency of a task the caller is assigned to. + + A dependent agent (e.g. a frontend cell waiting on a UX design task) + must be able to inspect what it is blocked on; read-only evidence is the + right tool, and the strict cross-agent ownership gate would otherwise + reject it. + """ + assigned = await self.task.list_assigned_for_agent(agent_id) + return any(task.id in (a.dependency_ids or []) for a in assigned) + async def evidence( self, *, @@ -711,28 +722,28 @@ class ContentActions: ) -> Envelope: """Inspect a task's PR diff, commits, files. - Fetches dev branch into the agent's workspace before diffing. - Allows inspection when caller is assignee OR task is unassigned - (post-handoff transient state) — strict ownership only blocks - cross-agent inspection of an actively-owned task. + Fetches the dev branch into the agent's workspace before diffing. + Allows inspection when the caller is the assignee, the task is + unassigned, the caller co-reviews a shared board task, or the task is a + dependency the caller is waiting on — strict ownership only blocks + snooping an unrelated, actively-owned task. - Task #154: ``files_changed`` and ``pr_diff_summary`` are pulled - from git (against the branch's parent) — the authoritative source. - Earlier versions hard-coded ``files_changed=[]`` and used - ``HEAD~1`` for the diff base, so QA / reviewers saw an empty - change list and only the latest commit's delta even when the PR - on GitHub had a multi-commit change set. + ``files_changed`` and ``pr_diff_summary`` are pulled from git (against + the branch's parent — the authoritative source) rather than the latest + commit's delta, so reviewers see the full multi-commit change set. """ t = await self.task.get(task_id) if t is None: return Envelope.not_found(message=f"task {task_id} not found") - # A board co-reviewer (HoM inspecting a PO-assigned board task, or vice - # versa) must be able to read the shared coordination task — the same - # allowance the content verbs grant via co-review. + # Reads are allowed for the assignee, an unassigned task, a board + # co-reviewer of a shared coordination task, OR a caller whose own work + # depends on this task. Strict ownership only blocks snooping an + # unrelated, actively-owned task. if ( t.assigned_to is not None and t.assigned_to != agent_id and not await self._board_may_co_review(agent_id, t) + and not await self._is_caller_dependency(agent_id, t) ): return _ownership_violation(task_id) if t.branch_name and t.work_session_id: diff --git a/tests/unit/gateway/test_content_actions_ownership.py b/tests/unit/gateway/test_content_actions_ownership.py index 52d47535..73883c5c 100644 --- a/tests/unit/gateway/test_content_actions_ownership.py +++ b/tests/unit/gateway/test_content_actions_ownership.py @@ -472,3 +472,41 @@ async def test_evidence_unassigned_task_allows_inspection() -> None: env = await ca.evidence(agent_id=agent_id, task_id=task_id) assert env.error is None + + +@pytest.mark.asyncio +async def test_evidence_allows_dependency_inspection() -> None: + """A caller whose own assigned task depends on the target may read it. + + A frontend cell waiting on a UX design task must be able to inspect the + dependency it is blocked on, even though another agent owns it. + """ + agent_id = uuid4() + other_id = uuid4() + dep_task_id = uuid4() + target = MagicMock( + id=dep_task_id, + status="in_progress", + assigned_to=other_id, + branch_name="feature/ux_ui/abc", + work_session_id=uuid4(), + commits=[], + pr_number=None, + pr_url="", + dev_notes="", + acceptance_criteria_status=[], + ) + callers_task = MagicMock(id=uuid4(), dependency_ids=[dep_task_id]) + task_svc = AsyncMock() + task_svc.get.return_value = target + task_svc.list_assigned_for_agent.return_value = [callers_task] + git_svc = AsyncMock() + git_svc.diff.return_value = "" + git_svc.list_changed_files.return_value = [] + workspace_svc = AsyncMock() + deps = _make_deps(task=task_svc, git=git_svc, workspace=workspace_svc) + ca = ContentActions(deps) + + env = await ca.evidence(agent_id=agent_id, task_id=dep_task_id) + assert env.error is None + task_svc.list_assigned_for_agent.assert_awaited()