"""Real b5 + loopback transport regressions (no vendor or nsjail claim).""" import asyncio import contextlib import importlib.util import json import os import sys from pathlib import Path import pytest import websockets from langbot_plugin.api.agent_tools import daemon as stock ROOT = Path(os.environ.get("CODING_RUNNER_SOURCE_ROOT") or Path(__file__).resolve().parents[1]) FOLDERS = ("acp-agent-runner", "claude-code-agent", "codex-agent") TOKEN = "test-only-installation-token-8f69c142049db2bc" def load(folder): path = ROOT / folder / "pkg/daemon_relay.py" if not path.exists(): return stock # RED runs exercise the actual pre-fix implementation. spec = importlib.util.spec_from_file_location("relay_" + folder, path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module async def connect(hub, name, *, token=TOKEN, protocol="coding-relay-v1"): ws = await websockets.connect(hub.endpoint) await ws.send(json.dumps(dict(type="daemon.hello", daemon_id=name, token=token, protocol=protocol))) try: reply = json.loads(await asyncio.wait_for(ws.recv(), 2)) assert reply["type"] == "daemon.ready" return ws except BaseException: await ws.close() raise async def collect(stream): return [item async for item in stream] @pytest.mark.asyncio @pytest.mark.parametrize("folder", FOLDERS) async def test_serialized_start_and_conflicts(folder, monkeypatch): m = load(folder) hub = m.AgentRuntimeDaemonHub() servers = [] original = m.websockets.serve async def counted(*args, **kwargs): server = await original(*args, **kwargs) servers.append(server) return server monkeypatch.setattr(m.websockets, "serve", counted) try: await asyncio.gather(*(hub.start(host="127.0.0.1", port=0, token=TOKEN) for _ in range(8))) with pytest.raises(m.AgentRuntimeDaemonError, match="config|started"): await hub.start(host="127.0.0.1", port=0, token=TOKEN + "changed") assert len(servers) == 1, "concurrent startup leaked extra listeners" finally: await hub.stop() for server in servers: server.close() await server.wait_closed() @pytest.mark.asyncio @pytest.mark.parametrize("folder", FOLDERS) async def test_shared_auth_and_old_client_denied(folder, monkeypatch): m = load(folder) monkeypatch.setenv("LANGBOT_PLUGIN_RUNTIME_PROFILE", "shared") for token in ("", "short", "a" * 40): hub = m.AgentRuntimeDaemonHub() try: with pytest.raises(m.AgentRuntimeDaemonError, match="token"): await hub.start(host="127.0.0.1", port=0, token=token) finally: await hub.stop() hub = m.AgentRuntimeDaemonHub() await hub.start(host="127.0.0.1", port=0, token=TOKEN) try: with pytest.raises(websockets.exceptions.ConnectionClosed): await connect(hub, "old", protocol="") with pytest.raises(websockets.exceptions.ConnectionClosed): await connect(hub, "wrong", token="wrong") finally: await hub.stop() @pytest.mark.asyncio @pytest.mark.parametrize("folder", FOLDERS) async def test_sender_ownership_events_finish_mcp_and_replacement(folder): m = load(folder) hub = m.AgentRuntimeDaemonHub() await hub.start(host="127.0.0.1", port=0, token=TOKEN) owner = await connect(hub, "owner") attacker = await connect(hub, "attacker") calls = [] async def mcp(tools, payload): calls.append(payload) return {"ok": True} old = m.handle_agent_runtime_mcp_payload m.handle_agent_runtime_mcp_payload = mcp task = asyncio.create_task(collect(hub.run_job(daemon_id="owner", payload={}, tools=object(), timeout=10))) try: frame = json.loads(await owner.recv()) job = frame["job_id"] for msg in [ dict(type="run.event", event={"type": "injected"}), dict(type="run.finished"), dict(type="mcp.request", request_id="bad", payload={}), ]: await attacker.send(json.dumps(dict(job_id=job, **msg))) await attacker.send(json.dumps({"type": "daemon.ping"})) while json.loads(await attacker.recv())["type"] != "daemon.pong": pass assert not task.done(), "foreign finished frame ended the run" assert not calls, "foreign MCP sender acquired run tools" with pytest.raises(websockets.exceptions.ConnectionClosed): await connect(hub, "owner") await owner.send(json.dumps(dict(type="run.event", job_id=job, event={"type": "legitimate"}))) await owner.send(json.dumps(dict(type="run.finished", job_id=job))) assert await task == [{"type": "legitimate"}] finally: m.handle_agent_runtime_mcp_payload = old task.cancel() with contextlib.suppress(asyncio.CancelledError, Exception): await task await owner.close() await attacker.close() await hub.stop() @pytest.mark.asyncio @pytest.mark.parametrize("folder", FOLDERS) @pytest.mark.parametrize("action", ["cancel", "disconnect", "timeout"]) async def test_real_standard_client_cancels_and_reaps_child(folder, action, tmp_path): m = load(folder) child_started = asyncio.Event() cleaned = asyncio.Event() processes = [] class Client(m.AgentRuntimeDaemonClient): async def run_job(self, job_id, payload): p = await asyncio.create_subprocess_exec( sys.executable, "-c", "import time; time.sleep(60)", start_new_session=True ) processes.append(p) child_started.set() try: await p.wait() finally: with contextlib.suppress(ProcessLookupError): p.kill() await p.wait() cleaned.set() hub = m.AgentRuntimeDaemonHub() await hub.start(host="127.0.0.1", port=0, token=TOKEN) client = Client(url=hub.endpoint, daemon_id="worker", token=TOKEN, reconnect_delay=60) serving = asyncio.create_task(client.run_forever()) await hub.wait_for_daemon("worker", 3) run = asyncio.create_task( collect(hub.run_job(daemon_id="worker", payload={}, tools=None, timeout=0.2 if action == "timeout" else 30)) ) try: await asyncio.wait_for(child_started.wait(), 3) if action == "cancel": run.cancel() elif action == "disconnect": await client.websocket.close() with contextlib.suppress(asyncio.CancelledError, TimeoutError): await asyncio.wait_for(run, 4) if action == "disconnect": assert "worker" in hub._fenced # No socket remains to acknowledge cleanup. await asyncio.wait_for(cleaned.wait(), 2) assert cleaned.is_set(), "run returned without remote child cleanup" assert processes[0].returncode is not None if action == "cancel": assert not hub._jobs finally: serving.cancel() run.cancel() await asyncio.gather(serving, run, return_exceptions=True) for p in processes: if p.returncode is None: p.kill() await p.wait() await hub.stop() @pytest.mark.asyncio async def test_unacknowledged_cancel_fences_target_and_revokes_tools(): m = load(FOLDERS[0]) hub = m.AgentRuntimeDaemonHub() hub.cancel_timeout = 0.1 await hub.start(host="127.0.0.1", port=0, token=TOKEN) ws = await connect(hub, "uncooperative") run = asyncio.create_task(collect(hub.run_job(daemon_id="uncooperative", payload={}, tools=object(), timeout=30))) try: await ws.recv() run.cancel() with contextlib.suppress(asyncio.CancelledError): await run with pytest.raises(m.AgentRuntimeDaemonError, match="fenced"): await collect(hub.run_job(daemon_id="uncooperative", payload={}, tools=None, timeout=1)) assert not hub._jobs finally: await ws.close() await hub.stop() def test_relay_copies_identical(): paths = [ROOT / folder / "pkg/daemon_relay.py" for folder in FOLDERS] assert all(p.exists() for p in paths) assert len({p.read_bytes() for p in paths}) == 1 @pytest.mark.asyncio async def test_unresolved_fence_survives_hub_recreation(tmp_path): m = load(FOLDERS[0]) hub = m.AgentRuntimeDaemonHub(state_dir=tmp_path) hub.cancel_timeout = 0.05 await hub.start(host="127.0.0.1", port=0, token=TOKEN) ws = await connect(hub, "lost") run = asyncio.create_task(collect(hub.run_job(daemon_id="lost", payload={}, tools=None, timeout=30))) await ws.recv() run.cancel() with contextlib.suppress(asyncio.CancelledError): await run await ws.close() await hub.stop() replacement = m.AgentRuntimeDaemonHub(state_dir=tmp_path) await replacement.start(host="127.0.0.1", port=0, token=TOKEN) try: with pytest.raises(websockets.exceptions.ConnectionClosed): await connect(replacement, "lost") finally: await replacement.stop() @pytest.mark.asyncio async def test_client_cleanup_failure_refuses_reconnect(): m = load(FOLDERS[0]) class Unconfirmed(BaseException): pass class Client(m.AgentRuntimeDaemonClient): async def run_job(self, job_id, payload): raise Unconfirmed("fixture cleanup failed") hub = m.AgentRuntimeDaemonHub() hub.cancel_timeout = 0.05 await hub.start(host="127.0.0.1", port=0, token=TOKEN) c = Client(url=hub.endpoint, daemon_id="bad-cleanup", token=TOKEN, reconnect_delay=0.01) serving = asyncio.create_task(c.run_forever()) await hub.wait_for_daemon("bad-cleanup", 2) run = asyncio.create_task(collect(hub.run_job(daemon_id="bad-cleanup", payload={}, tools=None, timeout=0.2))) try: with contextlib.suppress(TimeoutError): await run await asyncio.sleep(0.1) assert serving.done(), "failed cleanup left the client available for more jobs" assert isinstance(serving.exception(), RuntimeError) assert "bad-cleanup" in hub._fenced finally: serving.cancel() await asyncio.gather(serving, run, return_exceptions=True) await hub.stop() @pytest.mark.asyncio @pytest.mark.parametrize("folder", FOLDERS) async def test_two_separate_installation_processes_shared_network(folder, tmp_path, monkeypatch): monkeypatch.setenv("FOREIGN_HOST_SECRET", "synthetic-not-a-real-host-secret") workers = [] infos = [] tokens = [TOKEN + "A", TOKEN + "B"] async def command(worker, cmd): worker.stdin.write((json.dumps(dict(command=cmd)) + "\n").encode()) await worker.stdin.drain() return json.loads(await asyncio.wait_for(worker.stdout.readline(), 4)) try: for index in range(2): state = tmp_path / str(index) for child in ("home", "tmp", "data"): (state / child).mkdir(parents=True) # Match real worker construction's empty inherited environment. This # verifies separate processes and network, not kernel mount isolation. env = dict( HOME=str(state / "home"), TMPDIR=str(state / "tmp"), PYTHONUNBUFFERED="1", PYTHONDONTWRITEBYTECODE="1", LANGBOT_PLUGIN_RUNTIME_PROFILE="shared", ) worker = await asyncio.create_subprocess_exec( sys.executable, str(Path(__file__).with_name("coding_process_worker.py")), str(ROOT / folder), str(state / "data"), tokens[index], env=env, cwd=str(state / "data"), stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, ) workers.append(worker) infos.append(json.loads(await asyncio.wait_for(worker.stdout.readline(), 5))) assert infos[0]["pid"] != infos[1]["pid"] assert infos[0]["home"] != infos[1]["home"] and infos[0]["tmp"] != infos[1]["tmp"] assert all(i["foreign_env"] is None for i in infos) # Opposite installation tokens fail on the actual other listener. for index in range(2): ws = await websockets.connect(infos[index]["endpoint"]) await ws.send( json.dumps( dict(type="daemon.hello", daemon_id="attacker", token=tokens[1 - index], protocol="coding-relay-v1") ) ) with pytest.raises(websockets.exceptions.ConnectionClosed): await ws.recv() starts = await asyncio.gather(*(command(w, "run") for w in workers)) assert starts[0]["child_pid"] != starts[1]["child_pid"] assert await command(workers[0], "cancel") == dict(cleaned=True, fenced=False) assert await command(workers[1], "status") == dict(running=True) assert await command(workers[1], "cancel") == dict(cleaned=True, fenced=False) finally: for worker in workers: if worker.returncode is None: worker.stdin.write(b'{"command":"stop"}\n') await worker.stdin.drain() await asyncio.wait_for(worker.wait(), 5) assert worker.returncode == 0, (await worker.stderr.read()).decode() @pytest.mark.asyncio async def test_bind_conflict_is_actionable(): m = load(FOLDERS[0]) first = m.AgentRuntimeDaemonHub() second = m.AgentRuntimeDaemonHub() await first.start(host="127.0.0.1", port=0, token=TOKEN) try: port = int(first.endpoint.rsplit(":", 1)[1]) with pytest.raises(m.AgentRuntimeDaemonError, match="unique daemon-port"): await second.start(host="127.0.0.1", port=port, token=TOKEN) finally: await first.stop() await second.stop() @pytest.mark.asyncio async def test_cancel_pending_refuses_target_reuse(): m = load(FOLDERS[0]) hub = m.AgentRuntimeDaemonHub() hub.cancel_timeout = 0.1 await hub.start(host="127.0.0.1", port=0, token=TOKEN) ws = await connect(hub, "busy") task = asyncio.create_task(collect(hub.run_job(daemon_id="busy", payload={}, tools=None, timeout=10))) second = None try: await ws.recv() task.cancel() cancel = json.loads(await ws.recv()) assert cancel["type"] == "run.cancel" second = asyncio.create_task(collect(hub.run_job(daemon_id="busy", payload={}, tools=None, timeout=0.01))) with pytest.raises(m.AgentRuntimeDaemonError, match="cancelling|fenced"): await second finally: task.cancel() if second is not None: second.cancel() await asyncio.gather(task, *([second] if second is not None else []), return_exceptions=True) await ws.close() await hub.stop()