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={},
)
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:
@@ -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()