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.
This commit is contained in:
Renn F
2026-06-04 02:12:45 +02:00
parent 16982ce887
commit 555c33ad7d
2 changed files with 62 additions and 13 deletions
+24 -13
View File
@@ -703,6 +703,17 @@ class ContentActions:
context_briefing={}, 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( async def evidence(
self, self,
*, *,
@@ -711,28 +722,28 @@ class ContentActions:
) -> Envelope: ) -> Envelope:
"""Inspect a task's PR diff, commits, files. """Inspect a task's PR diff, commits, files.
Fetches dev branch into the agent's workspace before diffing. Fetches the dev branch into the agent's workspace before diffing.
Allows inspection when caller is assignee OR task is unassigned Allows inspection when the caller is the assignee, the task is
(post-handoff transient state) — strict ownership only blocks unassigned, the caller co-reviews a shared board task, or the task is a
cross-agent inspection of an actively-owned task. 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 ``files_changed`` and ``pr_diff_summary`` are pulled from git (against
from git (against the branch's parent) — the authoritative source. the branch's parent — the authoritative source) rather than the latest
Earlier versions hard-coded ``files_changed=[]`` and used commit's delta, so reviewers see the full multi-commit change set.
``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.
""" """
t = await self.task.get(task_id) t = await self.task.get(task_id)
if t is None: if t is None:
return Envelope.not_found(message=f"task {task_id} not found") return Envelope.not_found(message=f"task {task_id} not found")
# A board co-reviewer (HoM inspecting a PO-assigned board task, or vice # Reads are allowed for the assignee, an unassigned task, a board
# versa) must be able to read the shared coordination task — the same # co-reviewer of a shared coordination task, OR a caller whose own work
# allowance the content verbs grant via co-review. # depends on this task. Strict ownership only blocks snooping an
# unrelated, actively-owned task.
if ( if (
t.assigned_to is not None t.assigned_to is not None
and t.assigned_to != agent_id and t.assigned_to != agent_id
and not await self._board_may_co_review(agent_id, t) 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) return _ownership_violation(task_id)
if t.branch_name and t.work_session_id: if t.branch_name and t.work_session_id:
@@ -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) env = await ca.evidence(agent_id=agent_id, task_id=task_id)
assert env.error is None 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()