Skip to content

Commit 002cc14

Browse files
committed
fix(collaboration): derive worker workspace from its pinned binding
Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com>
1 parent 2cd77d6 commit 002cc14

5 files changed

Lines changed: 30 additions & 11 deletions

File tree

‎loopx/collaboration_mcp.py‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,6 @@ def _spawn(self, operation_id: str) -> None:
177177
sys.executable, "-m", "loopx.collaboration_mcp", "--delegation-action", "worker", "--runtime-root", str(self.root),
178178
"--registry", str(self.registry), "--goal-id", self.goal_id,
179179
"--agent-id", self.agent_id, "--execution-config", str(self.config), "--operation-id=" + operation_id,
180-
"--workspace", _read(self.path(operation_id))["identity"]["binding"]["workspace"],
181180
], stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
182181
start_new_session=True, close_fds=True)
183182

@@ -305,6 +304,7 @@ def _execute(self, path: Path, row: dict, binding: dict) -> None:
305304
request_id = row["identity"]["request_id"]
306305
common = ["--goal-id", self.goal_id, "--agent-id", binding["agent_id"]]
307306
host = binding["host_args"]
307+
# Preserve the journaled validator argv so existing Turns retain their resume identity.
308308
validator = [sys.executable, "-m", "loopx.collaboration_mcp", "--delegation-action", "validate", "--runtime-root", str(self.root),
309309
"--registry", str(self.registry), "--goal-id", self.goal_id,
310310
"--agent-id", self.agent_id, "--execution-config", str(self.config),
@@ -407,7 +407,7 @@ def main():
407407
parser.add_argument("--registry", type=Path, required=True)
408408
parser.add_argument("--goal-id", required=True)
409409
parser.add_argument("--agent-id", required=True)
410-
parser.add_argument("--workspace", type=Path, required=True)
410+
parser.add_argument("--workspace", type=Path, help="Required when serving MCP; workers use their pinned binding")
411411
parser.add_argument("--execution-config", type=Path, help="Explicit operator-owned local execution bindings")
412412
parser.add_argument("--delegation-action", choices=["worker", "validate"],
413413
help="Run a host-owned delegation action instead of serving MCP")
@@ -426,6 +426,8 @@ def main():
426426
except LockAcquireTimeoutError:
427427
pass # Another worker still owns the operation after the bounded wait.
428428
return
429+
if args.workspace is None:
430+
parser.error("--workspace is required when serving MCP")
429431
create_server(
430432
args.runtime_root.resolve(),
431433
args.registry.resolve(),

‎loopx/control_plane/turn_driver/host_candidate.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ def build_result(
253253
if field == "vision_unchanged_reason":
254254
continue
255255
value = candidate.get(field)
256-
text = _bounded(value, limit=limit) if value else ""
256+
text = _bounded(value, limit=limit)
257257
if text:
258258
result[field] = text
259259

‎tests/test_collaboration_mcp.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,23 @@
22

33
import asyncio
44
import json
5+
import subprocess
56
import sys
67

78
from mcp import ClientSession, StdioServerParameters
89
from mcp.client.stdio import stdio_client
910

1011

12+
def test_serving_mcp_still_requires_an_explicit_workspace(tmp_path):
13+
result = subprocess.run([
14+
sys.executable, "-m", "loopx.collaboration_mcp",
15+
"--runtime-root", str(tmp_path), "--registry", str(tmp_path / "registry.json"),
16+
"--goal-id", "delivery", "--agent-id", "builder",
17+
], capture_output=True, text=True, timeout=10)
18+
assert result.returncode == 2
19+
assert "--workspace is required when serving MCP" in result.stderr
20+
21+
1122
def test_scoped_stdio_tools_do_not_offer_shell_or_sender_override(tmp_path):
1223
registry = tmp_path / "registry.json"
1324
config = {

‎tests/test_local_delegation.py‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -158,18 +158,22 @@ async def disconnect_requester():
158158
assert (root / "analyst" / "initial" / "host-invocations").read_text() == "1"
159159
assert demo.canonical_tasks(root)["todo_analyst-initial"]["done"]
160160
returned = returns(original.root, original.goal_id, "lead")["items"]
161-
assert len(returned) == 1 and returned[0]["decision"] == "adopt"
161+
assert len(returned) == 1
162+
assert returned[0]["decision"] == "adopt"
162163
assert wait(reconnected)["artifacts"] == result["artifacts"]
164+
changed_brief = {**brief(), "purpose": "Changed instruction"}
163165
with pytest.raises(ValueError, match="identity conflict"):
164-
reconnected.start("analysis", "analysis-1", {**brief(), "purpose": "Changed instruction"})
166+
reconnected.start("analysis", "analysis-1", changed_brief)
167+
ungranted = Delegations(original.root, original.registry, original.goal_id, "reviewer", original.config)
168+
original_brief = brief()
165169
with pytest.raises(Exception, match="no delegation grant"):
166-
Delegations(original.root, original.registry, original.goal_id, "reviewer", original.config).start("analysis", "other", brief())
170+
ungranted.start("analysis", "other", original_brief)
167171
registry = json.loads(original.registry.read_text())
168172
registry["goals"][0]["status"] = "stopped"
169173
original.registry.write_text(json.dumps(registry))
170174
assert reconnected.read("analysis-1")["status"] == "accepted"
171175
with pytest.raises(ValueError, match="stopped"):
172-
reconnected.start("analysis", "new-operation", brief())
176+
reconnected.start("analysis", "new-operation", original_brief)
173177
registry["goals"][0]["status"] = "active"
174178
original.registry.write_text(json.dumps(registry))
175179
output = root / "analyst" / "initial" / "output.json"
@@ -183,7 +187,8 @@ def test_model_success_without_receiver_adoption_cannot_complete(service):
183187
(root / "skip-adoption").touch()
184188
runner.start("analysis", "analysis-1", brief())
185189
result = wait(runner)
186-
assert result["status"] == "rejected" and "did not adopt" in result["error"]
190+
assert result["status"] == "rejected"
191+
assert "did not adopt" in result["error"]
187192
assert not demo.canonical_tasks(root)["todo_analyst-initial"]["done"]
188193

189194

‎tests/test_managed_research_team.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,11 @@ def test_canonical_delivery_requires_completed_current_dependencies(team, monkey
8282
"--todo-id", "todo_reviewer-initial", "--text", "Independently analyze the initial filing and sources")
8383
with pytest.raises(RuntimeError, match="goal_acceptance_stale"):
8484
demo.complete(root, "reviewer", "initial")
85+
document = json.loads((root / "bootstrap.json").read_text())["document"]
86+
provider_revision = inspect_goal_acceptance(**route)["provider_revision"]
8587
with pytest.raises(ValueError):
86-
configure_goal_acceptance(**route, document=json.loads((root / "bootstrap.json").read_text())["document"],
87-
agent_id="lead", expected_provider_revision=inspect_goal_acceptance(**route)["provider_revision"],
88-
execute=True)
88+
configure_goal_acceptance(**route, document=document, agent_id="lead",
89+
expected_provider_revision=provider_revision, execute=True)
8990
held = plan(root, "reviewer", "initial")
9091
assert not held.get("turn_envelope", {}).get("action", {}).get("delivery_allowed", False), held
9192
# Explicit owner amendment for this negative fixture, never done by delegate().

0 commit comments

Comments
 (0)