Skip to content

Commit 4f39b26

Browse files
committed
Stop Harbor agents after interception
1 parent bf6d1ff commit 4f39b26

4 files changed

Lines changed: 115 additions & 0 deletions

File tree

src/clawbench/eval/harbor_adapter.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,10 @@ def setup_script(browser_runtime: str = "local") -> str:
327327
--extra-info-dir /extra_info \
328328
--output-dir /my-info
329329
330+
# Harbor installs its stock agent before step setup. Wrap that executable so
331+
# ClawBench's existing /data/.stop-requested signal ends the agent cleanly.
332+
/app/src/harbor/wrap-harbor-agent.sh
333+
330334
{kernel_setup}/app/src/harbor/start-runtime.sh
331335
332336
for _ in $(seq 1 60); do
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
wrap_agent() {
5+
local executable=$1
6+
local path real
7+
8+
path=$(command -v "$executable" 2>/dev/null || true)
9+
if [[ -z "$path" && -x "$HOME/.local/bin/$executable" ]]; then
10+
path="$HOME/.local/bin/$executable"
11+
fi
12+
[[ -n "$path" && -x "$path" ]] || return 0
13+
14+
real="${path}.clawbench-real"
15+
if [[ ! -e "$real" ]]; then
16+
mv "$path" "$real"
17+
fi
18+
19+
cat >"$path" <<'WRAPPER'
20+
#!/bin/bash
21+
set +e
22+
23+
real="${BASH_SOURCE[0]}.clawbench-real"
24+
stop_file=${CLAWBENCH_STOP_FILE:-/data/.stop-requested}
25+
stop_result=${CLAWBENCH_STOP_RESULT:-/data/agent-stop.json}
26+
rm -f "$stop_file" "$stop_result"
27+
28+
"$real" "$@" <&0 &
29+
agent_pid=$!
30+
(
31+
while kill -0 "$agent_pid" 2>/dev/null; do
32+
if [[ -f "$stop_file" ]]; then
33+
detected_at=$(date +%s.%N)
34+
kill -INT "$agent_pid" 2>/dev/null || true
35+
for _ in $(seq 1 20); do
36+
kill -0 "$agent_pid" 2>/dev/null || break
37+
sleep 0.1
38+
done
39+
if kill -0 "$agent_pid" 2>/dev/null; then
40+
kill -TERM "$agent_pid" 2>/dev/null || true
41+
fi
42+
printf '{"stop_detected_at":%s,"signal":"INT"}\n' "$detected_at" >"$stop_result"
43+
exit 0
44+
fi
45+
sleep 0.1
46+
done
47+
) &
48+
watcher_pid=$!
49+
50+
wait "$agent_pid"
51+
status=$?
52+
kill "$watcher_pid" 2>/dev/null || true
53+
wait "$watcher_pid" 2>/dev/null || true
54+
55+
if [[ -f "$stop_file" ]]; then
56+
exit 0
57+
fi
58+
exit "$status"
59+
WRAPPER
60+
chmod 0755 "$path"
61+
}
62+
63+
if (( $# > 0 )); then
64+
for executable in "$@"; do
65+
wrap_agent "$executable"
66+
done
67+
else
68+
wrap_agent claude
69+
wrap_agent codex
70+
fi

src/clawbench/runtime/runtime-server/server.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,7 @@ def activate_session_target(session_id, reason):
493493
if not INTERCEPTION_FILE.exists():
494494
result = {
495495
"intercepted": True,
496+
"intercepted_at": time.time(),
496497
"request": request_obj,
497498
"schema": eval_schema,
498499
}

tests/test_harbor_kernel_control.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
import importlib.util
66
import json
77
import re
8+
import subprocess
89
import sys
10+
import time
911
import tomllib
1012
from pathlib import Path
1113

@@ -22,6 +24,9 @@
2224
KERNEL_BROWSER_SCRIPT = (
2325
REPO_ROOT / "src" / "clawbench" / "runtime" / "harbor" / "kernel-browser.py"
2426
)
27+
HARBOR_AGENT_WRAPPER = (
28+
REPO_ROOT / "src" / "clawbench" / "runtime" / "harbor" / "wrap-harbor-agent.sh"
29+
)
2530

2631

2732
def _task() -> dict:
@@ -114,6 +119,7 @@ def test_kernel_setup_and_test_scripts_wire_lifecycle(
114119
assert "export CLAWBENCH_BROWSER_CDP_URL_FILE=" in setup
115120
assert "trap cleanup_browser EXIT" in setup
116121
assert "kernel-browser.py cleanup" in setup
122+
assert "wrap-harbor-agent.sh" in setup
117123
assert "127.0.0.1:7878/json/version" in setup
118124

119125
test = (tests / "test.sh").read_text()
@@ -124,6 +130,40 @@ def test_kernel_setup_and_test_scripts_wire_lifecycle(
124130
assert (env_dir / "harbor" / "browser_runtime_providers.py").is_file()
125131

126132

133+
def test_stop_wrapper_interrupts_agent_and_exits_cleanly(tmp_path: Path) -> None:
134+
bin_dir = tmp_path / "bin"
135+
bin_dir.mkdir()
136+
fake_agent = bin_dir / "claude"
137+
started = tmp_path / "started"
138+
stop_file = tmp_path / "stop-requested"
139+
stop_result = tmp_path / "agent-stop.json"
140+
fake_agent.write_text(
141+
"#!/bin/bash\n"
142+
"trap 'exit 130' INT\n"
143+
'touch "$FAKE_AGENT_STARTED"\n'
144+
"while true; do sleep 0.1; done\n"
145+
)
146+
fake_agent.chmod(0o755)
147+
env = {
148+
"PATH": f"{bin_dir}:/usr/bin:/bin",
149+
"HOME": str(tmp_path),
150+
"FAKE_AGENT_STARTED": str(started),
151+
"CLAWBENCH_STOP_FILE": str(stop_file),
152+
"CLAWBENCH_STOP_RESULT": str(stop_result),
153+
}
154+
155+
subprocess.run([HARBOR_AGENT_WRAPPER, "claude"], env=env, check=True)
156+
process = subprocess.Popen([fake_agent], env=env)
157+
deadline = time.monotonic() + 2
158+
while not started.exists() and time.monotonic() < deadline:
159+
time.sleep(0.01)
160+
assert started.exists()
161+
162+
stop_file.touch()
163+
assert process.wait(timeout=3) == 0
164+
assert json.loads(stop_result.read_text())["signal"] == "INT"
165+
166+
127167
def test_local_runtime_default_has_no_kernel_hooks(tmp_path: Path) -> None:
128168
case = tmp_path / "case"
129169
case.mkdir()

0 commit comments

Comments
 (0)