mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-10-30 12:25:29 +08:00
321 lines
12 KiB
Python
321 lines
12 KiB
Python
"""HISTORICAL RED reproducer, excluded from normal pytest discovery.
|
|
|
|
Preserved from 35f75ee; this is not an acceptance suite. Its same-interpreter
|
|
cross-installation, inherited worker-secret and shared Codex-account assertions
|
|
were overgeneralized. See topology-reassessment.md in the retained evidence.
|
|
The current replacement suites are test_coding_relay_v1.py and
|
|
test_coding_native_lifecycle.py (no xfail). Explicitly running this historical
|
|
file reproduces the old model, not candidate certification results.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import contextlib
|
|
import hashlib
|
|
import importlib.metadata
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
import sys
|
|
import zipfile
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import websockets
|
|
from langbot_plugin.api.agent_tools import daemon
|
|
from langbot_plugin.api.definition.components.runner.runner import Runner
|
|
from langbot_plugin.api.entities.builtin.runner import (
|
|
AgentEventContext,
|
|
AgentInput,
|
|
AgentResources,
|
|
AgentRunState,
|
|
AgentRuntimeContext,
|
|
AgentTrigger,
|
|
DeliveryContext,
|
|
RunnerContext,
|
|
)
|
|
from langbot_plugin.runtime.plugin.artifact import PluginArtifactStore
|
|
from langbot_plugin.runtime.plugin.dependency_environment import (
|
|
DependencyEnvironmentPreparationError,
|
|
PluginDependencyEnvironmentStore,
|
|
)
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
PLUGINS = (
|
|
("acp-agent-runner", "ACPAgentRunner", "0.1.12", "acp"),
|
|
("claude-code-agent", "ClaudeCodeAgent", "0.1.10", "claude-code"),
|
|
("codex-agent", "CodexAgent", "0.1.16", "codex"),
|
|
)
|
|
blocker = pytest.mark.xfail(
|
|
os.environ.get("CODING_RUNNER_ENFORCE_SHARED_SAFETY") != "1",
|
|
strict=True,
|
|
raises=AssertionError,
|
|
reason="BLOCKED: b5/current coding Runner lacks this safety contract",
|
|
)
|
|
admission_blocker = pytest.mark.xfail(
|
|
os.environ.get("CODING_RUNNER_ENFORCE_SHARED_SAFETY") != "1",
|
|
strict=True,
|
|
raises=DependencyEnvironmentPreparationError,
|
|
reason="BLOCKED: exact historical archive requires b3, Runtime provides b5",
|
|
)
|
|
|
|
|
|
def load_plugin(folder):
|
|
"""Keep SDK genuine; isolate only the plugins' colliding `pkg` modules."""
|
|
old = {n: m for n, m in sys.modules.items() if n == "pkg" or n.startswith("pkg.")}
|
|
for name in old:
|
|
del sys.modules[name]
|
|
sys.path.insert(0, str(ROOT / folder))
|
|
try:
|
|
relative = "components/runner/default.py" if folder == "acp-agent-runner" else "pkg/native_cli.py"
|
|
spec = importlib.util.spec_from_file_location(folder.replace("-", "_"), ROOT / folder / relative)
|
|
assert spec is not None and spec.loader is not None
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules[spec.name] = module
|
|
spec.loader.exec_module(module)
|
|
cls = getattr(
|
|
module,
|
|
{
|
|
"acp-agent-runner": "DefaultRunner",
|
|
"claude-code-agent": "NativeClaudeCodeRunner",
|
|
"codex-agent": "NativeCodexRunner",
|
|
}[folder],
|
|
)
|
|
return module, cls()
|
|
finally:
|
|
sys.path.pop(0)
|
|
for name in list(sys.modules):
|
|
if name == "pkg" or name.startswith("pkg."):
|
|
del sys.modules[name]
|
|
sys.modules.update(old)
|
|
|
|
|
|
def context(run_id, config=None):
|
|
return RunnerContext(
|
|
run_id=run_id,
|
|
trigger=AgentTrigger(type="message.received"),
|
|
event=AgentEventContext(event_id="fixture-event", event_type="message.received", source="test"),
|
|
input=AgentInput(text="synthetic prompt"),
|
|
delivery=DeliveryContext(surface="test"),
|
|
resources=AgentResources(),
|
|
state=AgentRunState(),
|
|
runtime=AgentRuntimeContext(metadata={"steering_enabled": False}),
|
|
config=config or {},
|
|
)
|
|
|
|
|
|
class NoHostCalls:
|
|
async def call_action(self, *args, **kwargs):
|
|
raise AssertionError("This local execution bypass did not need any Host grant")
|
|
|
|
|
|
async def connect(hub, daemon_id, token="fixture-A"):
|
|
ws = await websockets.connect(hub.endpoint)
|
|
await ws.send(json.dumps({"type": "daemon.hello", "daemon_id": daemon_id, "token": token}))
|
|
assert json.loads(await asyncio.wait_for(ws.recv(), 2))["type"] == "daemon.ready"
|
|
return ws
|
|
|
|
|
|
async def collect(stream):
|
|
return [event async for event in stream]
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("folder,name,version,key", PLUGINS)
|
|
@blocker
|
|
async def test_conflicting_installation_token_cannot_route_to_existing_daemon(folder, name, version, key):
|
|
"""A run with config B must not reach the daemon authenticated with config A."""
|
|
_, runner = load_plugin(folder)
|
|
hub = daemon.get_agent_runtime_daemon_hub(key)
|
|
assert not hub.is_running, "run this suite in an isolated interpreter"
|
|
await hub.start(host="127.0.0.1", port=0, token="fixture-A")
|
|
ws = await connect(hub, "same-display-id")
|
|
runner.bind_runtime(
|
|
plugin_runtime_handler=NoHostCalls(),
|
|
plugin_config={
|
|
"daemon-host": "127.0.0.1",
|
|
"daemon-port": 1,
|
|
"daemon-token": "fixture-B",
|
|
},
|
|
plugin_identity="langbot-team/" + name,
|
|
)
|
|
ctx = context(
|
|
"run-B",
|
|
{
|
|
"location": "daemon",
|
|
"daemon-id": "same-display-id",
|
|
"workspace": "/fixture-B",
|
|
"langbot-assets-enabled": False,
|
|
},
|
|
)
|
|
config = runner._validate_config(ctx)
|
|
assert config["daemon_hub"]["token"] == "fixture-B"
|
|
args = (ctx, config, "only-B-prompt", "") + ((False,) if key == "claude-code" else ())
|
|
task = asyncio.create_task(collect(runner._run_daemon(*args)))
|
|
frame = None
|
|
try:
|
|
try:
|
|
frame = json.loads(await asyncio.wait_for(ws.recv(), 1))
|
|
except TimeoutError:
|
|
pass
|
|
finally:
|
|
task.cancel()
|
|
with contextlib.suppress(asyncio.CancelledError):
|
|
await task
|
|
await ws.close()
|
|
await hub.stop()
|
|
assert frame is None, "Foreign-config run reached A's authenticated connection: " + str(frame and frame["type"])
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_hub_start_itself_rejects_conflicting_token():
|
|
"""Positive control: the runners skip the SDK's existing conflict check."""
|
|
hub = daemon.AgentRuntimeDaemonHub()
|
|
await hub.start(host="127.0.0.1", port=0, token="fixture-A")
|
|
try:
|
|
with pytest.raises(daemon.AgentRuntimeDaemonError, match="already started"):
|
|
await hub.start(host="127.0.0.1", port=0, token="fixture-B")
|
|
finally:
|
|
await hub.stop()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@blocker
|
|
async def test_daemon_cannot_inject_event_into_another_daemons_job():
|
|
"""Knowledge of a job ID is not authority; test genuine websocket dispatch."""
|
|
hub = daemon.AgentRuntimeDaemonHub()
|
|
await hub.start(host="127.0.0.1", port=0, token="fixture-A")
|
|
a = await connect(hub, "A")
|
|
b = await connect(hub, "B")
|
|
stream = hub.run_job(daemon_id="A", payload={"run_id": "run-A"}, tools=None, timeout=3)
|
|
task = asyncio.create_task(anext(stream))
|
|
received = None
|
|
try:
|
|
frame = json.loads(await asyncio.wait_for(a.recv(), 2))
|
|
await b.send(
|
|
json.dumps(
|
|
{
|
|
"type": "run.event",
|
|
"job_id": frame["job_id"],
|
|
"event": {"type": "run.completed", "run_id": "run-A", "data": {}},
|
|
}
|
|
)
|
|
)
|
|
try:
|
|
received = await asyncio.wait_for(asyncio.shield(task), 0.3)
|
|
except TimeoutError:
|
|
pass
|
|
finally:
|
|
task.cancel()
|
|
with contextlib.suppress(asyncio.CancelledError):
|
|
await task
|
|
await stream.aclose()
|
|
await a.close()
|
|
await b.close()
|
|
await hub.stop()
|
|
assert received is None, "Hub accepted B's frame for A's job without sender ownership validation"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@blocker
|
|
async def test_cancellation_requires_remote_stop_ack_before_forgetting_job():
|
|
"""A deliberately uncooperative loopback daemon never acknowledges cleanup."""
|
|
hub = daemon.AgentRuntimeDaemonHub()
|
|
await hub.start(host="127.0.0.1", port=0, token="fixture-A")
|
|
ws = await connect(hub, "A")
|
|
task = asyncio.create_task(collect(hub.run_job(daemon_id="A", payload={}, tools=None, timeout=3)))
|
|
try:
|
|
start = json.loads(await asyncio.wait_for(ws.recv(), 2))
|
|
task.cancel()
|
|
with pytest.raises(asyncio.CancelledError):
|
|
await asyncio.wait_for(task, 2)
|
|
cleanup = json.loads(await asyncio.wait_for(ws.recv(), 2))
|
|
assert cleanup == {"type": "run.cleanup", "job_id": start["job_id"]}
|
|
retained = start["job_id"] in hub._jobs
|
|
finally:
|
|
await ws.close()
|
|
await hub.stop()
|
|
assert retained, "Cancellation forgot the job before remote stop acknowledgement or durable fencing"
|
|
|
|
|
|
@blocker
|
|
def test_codex_run_homes_do_not_share_credentials_or_session_files(tmp_path, monkeypatch):
|
|
module, _ = load_plugin("codex-agent")
|
|
shared = tmp_path / "host-home"
|
|
shared.mkdir()
|
|
(shared / "auth.json").write_text('{"fixture":"not-a-real-credential"}')
|
|
(shared / "sessions").mkdir()
|
|
(shared / "sessions" / "A.jsonl").write_text("only-A-session")
|
|
monkeypatch.setenv("CODEX_HOME", str(shared))
|
|
a, _ = module._prepare_local_codex_home(str(tmp_path / "A"), "same-session", {}, "")
|
|
b, _ = module._prepare_local_codex_home(str(tmp_path / "B"), "same-session", {}, "")
|
|
ah, bh = Path(a["CODEX_HOME"]), Path(b["CODEX_HOME"])
|
|
assert ah != bh
|
|
assert not (bh / "auth.json").exists() and not (bh / "sessions" / "A.jsonl").exists(), (
|
|
"Random run-home names still expose host auth and shared persisted sessions"
|
|
)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@blocker
|
|
async def test_acp_process_cannot_inherit_worker_environment(tmp_path, monkeypatch):
|
|
module, _ = load_plugin("acp-agent-runner")
|
|
sentinel = "synthetic-other-installation-token"
|
|
monkeypatch.setenv("CODING_FIXTURE_HOST_SECRET", sentinel)
|
|
result = tmp_path / "child.json"
|
|
script = (
|
|
"import os,json,time,pathlib; "
|
|
f"pathlib.Path({str(result)!r}).write_text(json.dumps({{'secret':os.getenv('CODING_FIXTURE_HOST_SECRET'),"
|
|
"'cwd':os.getcwd()})); time.sleep(10)"
|
|
)
|
|
client = module.AcpStdioClient(command=sys.executable, args=["-c", script], cwd=str(tmp_path), env={})
|
|
async with client:
|
|
async with asyncio.timeout(3):
|
|
while not result.exists():
|
|
await asyncio.sleep(0.01)
|
|
child = json.loads(result.read_text())
|
|
assert child["cwd"] == str(tmp_path)
|
|
assert child["secret"] is None, "Unbound subprocess inherited the worker's synthetic secret"
|
|
|
|
|
|
def test_real_b5_proxy_has_box_binding_but_no_duplex_process_or_daemon_authority():
|
|
assert importlib.metadata.version("langbot-plugin") == "0.6.0b5"
|
|
runner = Runner()
|
|
runner.bind_runtime(plugin_runtime_handler=NoHostCalls())
|
|
api = runner.get_run_api(context("scope-probe"))
|
|
assert api.run_id == "scope-probe"
|
|
assert callable(api.acquire_box) and callable(api.bind_box) and callable(api.call_tool)
|
|
public = {name for name in dir(api) if not name.startswith("_")}
|
|
assert not any("daemon" in name or "process" in name or "stdin" in name for name in public)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
@pytest.mark.parametrize("folder,name,version,key", PLUGINS)
|
|
@admission_blocker
|
|
async def test_exact_previous_archive_is_admitted_by_b5(folder, name, version, key, tmp_path):
|
|
directory = os.environ.get("CODING_RUNNER_RELEASE_DIR")
|
|
if not directory:
|
|
pytest.skip("set CODING_RUNNER_RELEASE_DIR to independently retained release archives")
|
|
archive = Path(directory) / f"langbot-team-{name}-{version}.lbpkg"
|
|
data = archive.read_bytes()
|
|
# Confirm previous archive's executable/dependency bytes match current source.
|
|
with zipfile.ZipFile(archive) as package:
|
|
for member in package.namelist():
|
|
if member.endswith(".py") or member == "requirements.txt":
|
|
assert package.read(member) == (ROOT / folder / member).read_bytes()
|
|
store = PluginArtifactStore(tmp_path / "runtime")
|
|
artifact = store.install_package(data, hashlib.sha256(data).hexdigest())
|
|
assert artifact.plugin_name == name
|
|
dependency_store = PluginDependencyEnvironmentStore(tmp_path / "runtime")
|
|
|
|
async def forbidden_installer(*args):
|
|
pytest.fail("An incompatible SDK requirement must fail before dependency installation")
|
|
|
|
try:
|
|
await dependency_store.prepare(artifact, runtime_fingerprint="coding-blocker-b5", installer=forbidden_installer)
|
|
finally:
|
|
# Only the fixture's immutable trees need writable directories for teardown.
|
|
for parent, _dirs, _files in os.walk(tmp_path):
|
|
os.chmod(parent, 0o700)
|