fix: 岗位会商 handoff 将上游产物镜像到当前席位沙箱

席位线程隔离导致下游 read_file 找不到上游路径;镜像到 workspace/upstream 并改写 handoff 引用,附带安全路径校验与 SQL 回退测试。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
董宏凯andCursor committed 2026-08-27 18:35:17 +08:00
1 parent 440b490b0d
commit 32e509b2b3
3 files changed
+238 -7

No files matched your search

@@ -6,6 +6,7 @@ import asyncio
import importlib.util
import json
import zipfile
from pathlib import Path
from types import SimpleNamespace
from typing import Any
@@ -1244,8 +1245,11 @@ def test_activation_requires_a_structured_intent_snapshot() -> None:
@pytest.mark.asyncio
async def test_handoff_context_inlines_only_upstream_thread_text_artifacts(tmp_path, monkeypatch):
artifact = tmp_path / "handoff.md"
upstream_dir = tmp_path / "up" / "outputs"
upstream_dir.mkdir(parents=True)
artifact = upstream_dir / "handoff.md"
artifact.write_text("上游产物正文:可供下一岗位直接参考。", encoding="utf-8")
mirror_root = tmp_path / "current" / "workspace" / "upstream"
calls: list[tuple[str, str]] = []
class StubStore:
@@ -1307,9 +1311,12 @@ async def test_handoff_context_inlines_only_upstream_thread_text_artifacts(tmp_p
async def resolve(thread_id: str, path: str):
calls.append((thread_id, path))
assert thread_id == "upstream-thread"
assert path == "mnt/user-data/outputs/handoff.md"
return artifact
if thread_id == "upstream-thread":
assert path == "mnt/user-data/outputs/handoff.md"
return artifact
assert thread_id == "current-thread"
assert path == "mnt/user-data/workspace/upstream/handoff.md"
return mirror_root / "handoff.md"
monkeypatch.setattr(position_roundtable_router, "aresolve_thread_virtual_path", resolve)
request = SimpleNamespace(
@@ -1325,8 +1332,102 @@ async def test_handoff_context_inlines_only_upstream_thread_text_artifacts(tmp_p
assert len(context["upstream_nodes"]) == 1
assert upstream["latest_answer"] == "上游问答结果"
assert upstream["artifacts"][0]["content"] == "上游产物正文:可供下一岗位直接参考。"
assert upstream["artifacts"][0]["upstream_path"] == "mnt/user-data/outputs/handoff.md"
assert upstream["artifacts"][0]["path"] == "mnt/user-data/workspace/upstream/handoff.md"
assert upstream["artifacts"][0]["mirrored_to_sandbox"] is True
assert "content" not in upstream["artifacts"][1]
assert calls == [("upstream-thread", "mnt/user-data/outputs/handoff.md")]
assert "mirrored_to_sandbox" not in upstream["artifacts"][1]
assert upstream["artifacts"][1]["path"] == "mnt/user-data/uploads/outside.md"
assert (mirror_root / "handoff.md").read_text(encoding="utf-8") == "上游产物正文:可供下一岗位直接参考。"
assert "/mnt/user-data/workspace/upstream/handoff.md" in context["instructions"]
assert set(calls) == {
("upstream-thread", "mnt/user-data/outputs/handoff.md"),
("current-thread", "mnt/user-data/workspace/upstream/handoff.md"),
}
@pytest.mark.asyncio
async def test_handoff_mirror_falls_back_to_persisted_content_and_skips_unsafe_paths(
tmp_path, monkeypatch
):
mirror_root = tmp_path / "mirror"
resolved: list[tuple[str, str]] = []
class StubStore:
async def get_session(self, session_id, user_id):
assert (session_id, user_id) == ("session-a", "user-a")
return {"task_snapshot": {"title": "测试任务"}, "intent_snapshot": {}}
async def list_nodes(self, session_id, user_id):
return [
{
"node_key": "stage-1-seat-1-agent-a",
"stage_index": 0,
"seat_index": 0,
"agent_id": "agent-a",
"position_id": "planning",
"thread_id": "upstream-thread",
"status": "done",
"revision": 1,
"latest_answer": "上游问答结果",
"artifact_manifest": [
{
"name": "gone.md",
"path": "mnt/user-data/outputs/gone.md",
"mime_type": "text/markdown",
"content": "数据库恢复正文",
},
{
"name": "escape.md",
"path": "mnt/user-data/outputs/../escape.md",
"mime_type": "text/markdown",
"content": "不应被镜像",
},
],
},
{
"node_key": "stage-2-seat-1-agent-b",
"stage_index": 1,
"seat_index": 0,
"agent_id": "agent-b",
"position_id": "solution",
"thread_id": "current-thread",
"status": "ready",
"revision": 0,
"latest_answer": None,
"artifact_manifest": [],
},
]
missing_source = tmp_path / "missing-source"
async def resolve(thread_id: str, path: str):
resolved.append((thread_id, path))
if thread_id == "upstream-thread":
return missing_source / "not-materialized.md"
assert thread_id == "current-thread"
return mirror_root / Path(path).name
monkeypatch.setattr(position_roundtable_router, "aresolve_thread_virtual_path", resolve)
request = SimpleNamespace(
app=SimpleNamespace(state=SimpleNamespace(position_roundtable_store=StubStore())),
state=SimpleNamespace(user=SimpleNamespace(id="user-a")),
)
context = await position_roundtable_router._handoff_context(
request, "session-a", "stage-2-seat-1-agent-b"
)
artifacts = context["upstream_nodes"][0]["artifacts"]
assert artifacts[0]["path"] == "mnt/user-data/workspace/upstream/gone.md"
assert artifacts[0]["mirrored_to_sandbox"] is True
assert (mirror_root / "gone.md").read_text(encoding="utf-8") == "数据库恢复正文"
# 持久化正文同时内嵌,沙箱磁盘丢失也不影响下一岗位读到内容。
assert artifacts[0]["content"] == "数据库恢复正文"
assert artifacts[1]["path"] == "mnt/user-data/outputs/../escape.md"
assert "mirrored_to_sandbox" not in artifacts[1]
assert not (mirror_root / "escape.md").exists()
assert all(".." not in path for _, path in resolved)
@pytest.mark.asyncio