From ec8008abb8fb37f1f2fee05261044dd59d396b3b Mon Sep 17 00:00:00 2001 From: berto Date: Sat, 15 Aug 2026 08:37:49 +0000 Subject: [PATCH] feat(harness): add interactive modes and GPU selection --- harness/clients/README.md | 66 ++++++++++- harness/clients/common.sh | 111 +++++++++++++++++- harness/clients/run_claude_code.sh | 47 +++++--- harness/clients/run_codex.sh | 37 +++--- harness/clients/run_hermes.sh | 52 ++++---- harness/clients/run_openclaw.sh | 53 +++++---- harness/clients/run_opencode.sh | 46 ++++---- harness/clients/run_pi.sh | 21 ++-- .../tests/test_client_launcher_timeouts.sh | 52 ++++++++ 9 files changed, 373 insertions(+), 112 deletions(-) diff --git a/harness/clients/README.md b/harness/clients/README.md index 73004b261..640e0e013 100644 --- a/harness/clients/README.md +++ b/harness/clients/README.md @@ -50,6 +50,67 @@ MAX_CTX=32768 MAX_TOKENS=512 \ harness/clients/run_codex.sh ``` +## GPU selection + +All launchers inherit `CUDA_VISIBLE_DEVICES` and `HIP_VISIBLE_DEVICES`. Native +Lucebox runs also accept `TARGET_DEVICE` and `DRAFT_DEVICE`; when the latter is +omitted it follows `TARGET_DEVICE`. Device numbers use the runtime-visible +namespace, so exposing one physical GPU makes it device zero inside the server: + +```bash +# NVIDIA GPU 0 with a CUDA build. +CUDA_VISIBLE_DEVICES=0 \ +DFLASH_SERVER_BIN=server/build-cuda/dflash_server \ +TARGET_DEVICE=cuda:0 \ +harness/clients/run_codex.sh + +# Physical HIP GPU 1, exposed as hip:0 to a HIP build. +HIP_VISIBLE_DEVICES=1 \ +DFLASH_SERVER_BIN=server/build-hip/dflash_server \ +TARGET_DEVICE=hip:0 \ +harness/clients/run_codex.sh +``` + +CUDA and HIP servers are separate build artifacts. A host with Strix Halo +(`gfx1151`) and an R9700 (`gfx1201`) can use one dual-architecture HIP build: + +```bash +cmake -S server -B server/build-hip \ + -DCMAKE_BUILD_TYPE=Release \ + -DDFLASH27B_GPU_BACKEND=hip \ + -DDFLASH27B_HIP_ARCHITECTURES='gfx1151;gfx1201' \ + -DDFLASH27B_HIP_SM80_EQUIV=ON +cmake --build server/build-hip --target dflash_server -j"$(nproc)" +``` + +Select a server binary and matching visibility variable together. The launcher +prints resolved placement before startup and uses `nvidia-smi` or `rocm-smi` +for the matching backend in its final report. + +## Interactive terminal clients + +The default launcher behavior remains a deterministic one-shot compatibility +test. Set `HARNESS_INTERACTIVE=1` to attach the real client TUI to the terminal +while the launcher manages the Lucebox server: + +```bash +HARNESS_INTERACTIVE=1 harness/clients/run_claude_code.sh +HARNESS_INTERACTIVE=1 harness/clients/run_codex.sh +HARNESS_INTERACTIVE=1 harness/clients/run_opencode.sh +HARNESS_INTERACTIVE=1 harness/clients/run_hermes.sh +HARNESS_INTERACTIVE=1 harness/clients/run_pi.sh +HARNESS_INTERACTIVE=1 harness/clients/run_openclaw.sh +``` + +`INTERACTIVE_PROMPT` supplies an optional first message where the client +supports it. Interactive client state and sessions persist under +`.harness-work/interactive/`. Exit the client or press Ctrl+C to stop +the launcher-managed server. One-shot client timeouts do not apply to a TUI. +Set `HARNESS_PROGRESS=0` to suppress launcher progress and heartbeat messages. + +Open WebUI is already interactive through its browser UI; its harness scripts +remain deterministic HTTP probes. + The C++ server is expected to handle the same client protocol shapes covered by these launchers and probes: OpenAI Chat Completions, streaming chunks, tool metadata, OpenAI Responses for Codex, Anthropic Messages for Claude Code, and @@ -128,5 +189,6 @@ Responses requests can be compared too. - `common.sh` contains the shared server startup logic. - `run_openwebui_tools.sh` supports `OPENWEBUI_FUNCTION_CALLING=default` and `OPENWEBUI_FUNCTION_CALLING=native`. -- Every launcher redirects stdin from `/dev/null`; this prevents SSH input from - being accidentally treated as a user prompt by interactive clients. +- One-shot launchers redirect stdin from `/dev/null`; this prevents SSH input + from being accidentally treated as a user prompt. `HARNESS_INTERACTIVE=1` + deliberately keeps the selected terminal client attached to the TTY. diff --git a/harness/clients/common.sh b/harness/clients/common.sh index c590cb648..36782ee5b 100755 --- a/harness/clients/common.sh +++ b/harness/clients/common.sh @@ -63,6 +63,14 @@ LLAMA_CACHE_TYPE_K="${LLAMA_CACHE_TYPE_K:-${CACHE_TYPE_K:-q8_0}}" LLAMA_CACHE_TYPE_V="${LLAMA_CACHE_TYPE_V:-${CACHE_TYPE_V:-q8_0}}" MAX_TOKENS="${MAX_TOKENS:-2048}" EXTRA_SERVER_ARGS="${EXTRA_SERVER_ARGS:-}" +TARGET_DEVICE="${TARGET_DEVICE:-}" +DRAFT_DEVICE="${DRAFT_DEVICE:-${TARGET_DEVICE:-}}" +HARNESS_INTERACTIVE="${HARNESS_INTERACTIVE:-0}" +INTERACTIVE_PROMPT="${INTERACTIVE_PROMPT:-}" +if [[ "$HARNESS_INTERACTIVE" != "0" && "$HARNESS_INTERACTIVE" != "1" ]]; then + echo "HARNESS_INTERACTIVE must be 0 or 1" >&2 + exit 2 +fi MODEL_ID="${MODEL_ID:-luce-dflash}" API_KEY="${API_KEY:-sk-lucebox}" @@ -81,6 +89,37 @@ SERVER_LOG="$LOG_DIR/server.log" mkdir -p "$LOG_DIR" +# Keep progress attached to the original terminal even while one-shot client +# stdout/stderr is redirected to its result file. +exec 3>&2 +HARNESS_PROGRESS="${HARNESS_PROGRESS:-1}" + +progress() { + if [[ "$HARNESS_PROGRESS" != "0" ]]; then + printf '[harness] %s\n' "$*" >&3 + fi +} + +client_home() { + local client="$1" + if [[ "$HARNESS_INTERACTIVE" == "1" ]]; then + printf '%s/interactive/%s\n' "$CLIENT_WORK_DIR" "$client" + else + printf '%s/%s-home\n' "$LOG_DIR" "$client" + fi +} + +run_interactive_client() { + local label="$1" + local client_out="$2" + shift 2 + progress "opening interactive $label; exit the client to stop the server" + "$@" + local rc=$? + printf 'Interactive %s terminal output was not captured.\n' "$label" > "$client_out" + return "$rc" +} + require_client_binary() { local label="$1" local path="$2" @@ -118,11 +157,45 @@ run_with_timeout() { echo "client timeout must be a non-negative integer (seconds; 0 disables it)" >&2 return 2 fi + local command_name + command_name="$(basename "$1")" + if [[ "$command_name" == "env" ]]; then + local arg + for arg in "${@:2}"; do + if [[ "$arg" != *=* ]]; then + command_name="$(basename "$arg")" + break + fi + done + fi + local timeout_label="${timeout_seconds}s" + if [[ "$timeout_seconds" == "0" ]]; then + timeout_label="disabled" + fi + progress "running $command_name (timeout: $timeout_label; logs: $LOG_DIR)" + + local started_at heartbeat_pid rc elapsed + started_at="$(date +%s)" + ( + while sleep 10; do + elapsed=$(( $(date +%s) - started_at )) + progress "$command_name still running (${elapsed}s elapsed)" + done + ) & + heartbeat_pid=$! + if [[ "$timeout_seconds" == "0" ]]; then "$@" + rc=$? else timeout "${timeout_seconds}s" "$@" + rc=$? fi + kill "$heartbeat_pid" 2>/dev/null || true + wait "$heartbeat_pid" 2>/dev/null || true + elapsed=$(( $(date +%s) - started_at )) + progress "$command_name finished (rc=$rc, ${elapsed}s elapsed)" + return "$rc" } draft_enabled() { @@ -130,6 +203,7 @@ draft_enabled() { } start_lucebox_server() { + progress "starting $MODEL_SERVER server (log: $SERVER_LOG)" if [[ "$MODEL_SERVER" == "llamacpp" ]]; then start_llamacpp_server return @@ -177,10 +251,18 @@ start_dflash_native_server() { if [[ -n "$FA_WINDOW" ]] && [[ "$FA_WINDOW" != "0" ]]; then fa_args=(--fa-window "$FA_WINDOW") fi + local device_args=() + if [[ -n "$TARGET_DEVICE" ]]; then + device_args+=(--target-device "$TARGET_DEVICE") + fi + if [[ -n "$DRAFT_DEVICE" ]]; then + device_args+=(--draft-device "$DRAFT_DEVICE") + fi # Export KV cache type env vars for the C++ server to pick up (only when # explicitly requested: the per-axis envs override family defaults). if [[ -n "$CACHE_TYPE_K" ]]; then export DFLASH27B_KV_K="$CACHE_TYPE_K"; fi if [[ -n "$CACHE_TYPE_V" ]]; then export DFLASH27B_KV_V="$CACHE_TYPE_V"; fi + progress "server placement: target=${TARGET_DEVICE:-auto:0} draft=${DRAFT_DEVICE:-auto:0} CUDA_VISIBLE_DEVICES=${CUDA_VISIBLE_DEVICES:-unset} HIP_VISIBLE_DEVICES=${HIP_VISIBLE_DEVICES:-unset}" "$DFLASH_SERVER_BIN" "$TARGET" \ "${draft_args[@]}" \ --host "$HOST" \ @@ -188,6 +270,7 @@ start_dflash_native_server() { --max-ctx "$MAX_CTX" \ --max-tokens "$MAX_TOKENS" \ --model-name "$MODEL_ID" \ + "${device_args[@]}" \ "${ddtree_args[@]}" \ "${fa_args[@]}" \ "${extra_args[@]}" \ @@ -264,10 +347,15 @@ start_llamacpp_server() { } wait_lucebox_server() { - for _ in $(seq 1 300); do + progress "waiting for server health at $BASE_URL/health" + for attempt in $(seq 1 300); do if curl -fsS "$BASE_URL/health" >/dev/null 2>&1; then + progress "server is healthy" return 0 fi + if (( attempt % 10 == 0 )); then + progress "server still starting (${attempt}s elapsed; log: $SERVER_LOG)" + fi sleep 1 if ! kill -0 "$SERVER_PID" 2>/dev/null; then echo "server exited early; log: $SERVER_LOG" >&2 @@ -304,5 +392,24 @@ finish_report() { echo "--- server tail ---" tail -n 120 "$SERVER_LOG" || true echo "--- gpu ---" - nvidia-smi --query-gpu=name,memory.used,memory.total,utilization.gpu --format=csv,noheader || true + local resolved_backend="${TARGET_DEVICE%%:*}" + if [[ -z "$TARGET_DEVICE" ]] && grep -Eq 'target_device[[:space:]]*=[[:space:]]*hip:' "$SERVER_LOG"; then + resolved_backend="hip" + elif [[ -z "$TARGET_DEVICE" ]] && grep -Eq 'target_device[[:space:]]*=[[:space:]]*cuda:' "$SERVER_LOG"; then + resolved_backend="cuda" + fi + if [[ "$resolved_backend" == "hip" ]]; then + if command -v rocm-smi >/dev/null 2>&1; then + local rocm_device_args=() + if [[ "${HIP_VISIBLE_DEVICES:-}" =~ ^[0-9]+$ ]]; then + rocm_device_args=(--device "$HIP_VISIBLE_DEVICES") + fi + rocm-smi "${rocm_device_args[@]}" --showproductname --showuse --showmeminfo vram 2>/dev/null || true + else + echo "rocm-smi not found" + fi + else + nvidia-smi --query-gpu=name,memory.used,memory.total,utilization.gpu --format=csv,noheader 2>/dev/null || \ + echo "nvidia-smi unavailable" + fi } diff --git a/harness/clients/run_claude_code.sh b/harness/clients/run_claude_code.sh index f4b383a81..b40d4f042 100755 --- a/harness/clients/run_claude_code.sh +++ b/harness/clients/run_claude_code.sh @@ -16,7 +16,7 @@ source "$SCRIPT_DIR/common.sh" CLIENT_OUT="$LOG_DIR/claude-code.out" CLAUDE_BIN="${CLAUDE_BIN:-$CLIENT_WORK_DIR/clients/claude_code/npm/bin/claude}" require_client_binary "Claude Code" "$CLAUDE_BIN" "claude_code" "CLAUDE_BIN" -HOME_DIR="$LOG_DIR/claude-home" +HOME_DIR="$(client_home claude)" mkdir -p "$HOME_DIR" start_lucebox_server @@ -57,25 +57,34 @@ if [[ -n "${PFLASH_SESSION_ID:-}" ]]; then echo "[run_claude_code] session-inject proxy up on $CLIENT_BASE_URL (session=$PFLASH_SESSION_ID)" fi +claude_env=( + "HOME=$HOME_DIR" + "ANTHROPIC_API_KEY=$API_KEY" + "ANTHROPIC_BASE_URL=$CLIENT_BASE_URL" + "CLAUDE_CODE_API_BASE_URL=$CLIENT_BASE_URL" + "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1" + "CLAUDE_CODE_DISABLE_TELEMETRY=1" + "CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK=1" +) set +e -run_with_timeout "$CLAUDE_TIMEOUT" env \ - HOME="$HOME_DIR" \ - ANTHROPIC_API_KEY="$API_KEY" \ - ANTHROPIC_BASE_URL="$CLIENT_BASE_URL" \ - CLAUDE_CODE_API_BASE_URL="$CLIENT_BASE_URL" \ - CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 \ - CLAUDE_CODE_DISABLE_TELEMETRY=1 \ - CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK=1 \ - "$CLAUDE_BIN" \ - --print \ - --output-format json \ - --model "$MODEL_ID" \ - --tools "$CLAUDE_TOOLS" \ - --permission-mode dontAsk \ - --no-session-persistence \ - "$PROMPT" \ - < /dev/null > "$CLIENT_OUT" 2>&1 -RC=$? +if [[ "$HARNESS_INTERACTIVE" == "1" ]]; then + claude_cmd=("$CLAUDE_BIN" --model "$MODEL_ID" --tools "$CLAUDE_TOOLS") + if [[ -n "$INTERACTIVE_PROMPT" ]]; then claude_cmd+=("$INTERACTIVE_PROMPT"); fi + run_interactive_client "Claude Code" "$CLIENT_OUT" env "${claude_env[@]}" "${claude_cmd[@]}" + RC=$? +else + run_with_timeout "$CLAUDE_TIMEOUT" env "${claude_env[@]}" \ + "$CLAUDE_BIN" \ + --print \ + --output-format json \ + --model "$MODEL_ID" \ + --tools "$CLAUDE_TOOLS" \ + --permission-mode dontAsk \ + --no-session-persistence \ + "$PROMPT" \ + < /dev/null > "$CLIENT_OUT" 2>&1 + RC=$? +fi set -e if [[ -n "$PROXY_PID" ]] && kill -0 "$PROXY_PID" 2>/dev/null; then diff --git a/harness/clients/run_codex.sh b/harness/clients/run_codex.sh index f9a59a7b8..578b42b51 100755 --- a/harness/clients/run_codex.sh +++ b/harness/clients/run_codex.sh @@ -16,7 +16,7 @@ CLIENT_OUT="$LOG_DIR/codex.out" LAST_MSG="$LOG_DIR/codex-last-message.txt" CODEX_BIN="${CODEX_BIN:-$CLIENT_WORK_DIR/clients/codex/npm/bin/codex}" require_client_binary "Codex" "$CODEX_BIN" "codex" "CODEX_BIN" -CODEX_HOME_DIR="$LOG_DIR/codex-home" +CODEX_HOME_DIR="$(client_home codex)" CODEX_SANDBOX="${CODEX_SANDBOX:-danger-full-access}" CODEX_WIRE_API="${CODEX_WIRE_API:-responses}" mkdir -p "$CODEX_HOME_DIR" @@ -38,22 +38,29 @@ start_lucebox_server trap stop_lucebox_server EXIT wait_lucebox_server +codex_env=("HOME=$CODEX_HOME_DIR" "CODEX_HOME=$CODEX_HOME_DIR" "OPENAI_API_KEY=$API_KEY") set +e -run_with_timeout "$CODEX_TIMEOUT" env \ - HOME="$CODEX_HOME_DIR" \ - CODEX_HOME="$CODEX_HOME_DIR" \ - OPENAI_API_KEY="$API_KEY" \ - "$CODEX_BIN" exec \ - --skip-git-repo-check \ - --sandbox "$CODEX_SANDBOX" \ - --model "$MODEL_ID" \ - --json \ - --output-last-message "$LAST_MSG" \ - "$PROMPT" \ - < /dev/null > "$CLIENT_OUT" 2>&1 -RC=$? +if [[ "$HARNESS_INTERACTIVE" == "1" ]]; then + codex_cmd=("$CODEX_BIN" --cd "$REPO_DIR" --sandbox "$CODEX_SANDBOX" --model "$MODEL_ID") + if [[ -n "$INTERACTIVE_PROMPT" ]]; then codex_cmd+=("$INTERACTIVE_PROMPT"); fi + run_interactive_client "Codex" "$CLIENT_OUT" env "${codex_env[@]}" "${codex_cmd[@]}" + RC=$? +else + run_with_timeout "$CODEX_TIMEOUT" env "${codex_env[@]}" \ + "$CODEX_BIN" exec \ + --skip-git-repo-check \ + --sandbox "$CODEX_SANDBOX" \ + --model "$MODEL_ID" \ + --json \ + --output-last-message "$LAST_MSG" \ + "$PROMPT" \ + < /dev/null > "$CLIENT_OUT" 2>&1 + RC=$? +fi set -e -cat "$LAST_MSG" >> "$CLIENT_OUT" 2>/dev/null || true +if [[ "$HARNESS_INTERACTIVE" == "0" ]]; then + cat "$LAST_MSG" >> "$CLIENT_OUT" 2>/dev/null || true +fi finish_report "$CLIENT_OUT" "$RC" exit "$RC" diff --git a/harness/clients/run_hermes.sh b/harness/clients/run_hermes.sh index a4d94419b..ffc3ec19c 100755 --- a/harness/clients/run_hermes.sh +++ b/harness/clients/run_hermes.sh @@ -13,7 +13,7 @@ source "$SCRIPT_DIR/common.sh" CLIENT_OUT="$LOG_DIR/hermes.out" HERMES_BIN="${HERMES_BIN:-$CLIENT_WORK_DIR/clients/hermes/home/.local/bin/hermes}" require_client_binary "Hermes" "$HERMES_BIN" "hermes" "HERMES_BIN" -HOME_DIR="$LOG_DIR/hermes-home" +HOME_DIR="$(client_home hermes)" mkdir -p "$HOME_DIR" cat > "$HOME_DIR/config.yaml" < "$CLIENT_OUT" 2>&1 -RC=$? +hermes_env=( + "HOME=$HOME_DIR" + "HERMES_HOME=$HOME_DIR" + "OPENAI_API_KEY=$API_KEY" + "OPENAI_BASE_URL=$BASE_URL/v1" + "HERMES_INFERENCE_PROVIDER=lucebox" + "HERMES_INFERENCE_MODEL=$MODEL_ID" + "HERMES_ACCEPT_HOOKS=1" +) +if [[ "$HARNESS_INTERACTIVE" == "1" ]]; then + hermes_cmd=( + "$HERMES_BIN" chat --tui --provider lucebox --model "$MODEL_ID" + --accept-hooks --max-turns "$HERMES_MAX_TURNS" --source lucebox-harness + ) + run_interactive_client "Hermes" "$CLIENT_OUT" env "${hermes_env[@]}" "${hermes_cmd[@]}" + RC=$? +else + run_with_timeout "$HERMES_TIMEOUT" env "${hermes_env[@]}" NO_COLOR=1 \ + "$HERMES_BIN" chat \ + --quiet \ + --provider lucebox \ + --model "$MODEL_ID" \ + --accept-hooks \ + --yolo \ + --max-turns "$HERMES_MAX_TURNS" \ + --source lucebox-harness \ + --query "$PROMPT" \ + < /dev/null > "$CLIENT_OUT" 2>&1 + RC=$? +fi set -e finish_report "$CLIENT_OUT" "$RC" diff --git a/harness/clients/run_openclaw.sh b/harness/clients/run_openclaw.sh index 6467c7999..c233c8ffd 100755 --- a/harness/clients/run_openclaw.sh +++ b/harness/clients/run_openclaw.sh @@ -13,7 +13,7 @@ source "$SCRIPT_DIR/common.sh" CLIENT_OUT="$LOG_DIR/openclaw.out" OPENCLAW_BIN="${OPENCLAW_BIN:-$CLIENT_WORK_DIR/clients/openclaw/npm/bin/openclaw}" require_client_binary "OpenClaw" "$OPENCLAW_BIN" "openclaw" "OPENCLAW_BIN" -HOME_DIR="$LOG_DIR/openclaw-home" +HOME_DIR="$(client_home openclaw)" CONFIG_PATCH="$LOG_DIR/openclaw.patch.json" PROVIDER_API="${PROVIDER_API:-openai-completions}" OPENCLAW_AGENT_ARGS="${OPENCLAW_AGENT_ARGS:-}" @@ -91,29 +91,36 @@ start_lucebox_server trap stop_lucebox_server EXIT wait_lucebox_server -openclaw_cmd=( - "$OPENCLAW_BIN" agent - --local - --json - --model "lucebox/$MODEL_ID" - --session-id "lucebox-client-harness" -) -if [[ "$OPENCLAW_TIMEOUT" != "0" ]]; then - openclaw_cmd+=(--timeout "$OPENCLAW_TIMEOUT") -fi -if [[ -n "$OPENCLAW_AGENT_ARGS" ]]; then - read -r -a agent_args <<< "$OPENCLAW_AGENT_ARGS" - openclaw_cmd+=("${agent_args[@]}") -fi -openclaw_cmd+=(--message "$PROMPT") - set +e -run_with_timeout "$OPENCLAW_TIMEOUT" env \ - HOME="$HOME_DIR" \ - OPENAI_API_KEY="$API_KEY" \ - "${openclaw_cmd[@]}" \ - < /dev/null > "$CLIENT_OUT" 2>&1 -RC=$? +if [[ "$HARNESS_INTERACTIVE" == "1" ]]; then + openclaw_cmd=("$OPENCLAW_BIN" tui --local --session lucebox-client-harness) + if [[ -n "$INTERACTIVE_PROMPT" ]]; then openclaw_cmd+=(--message "$INTERACTIVE_PROMPT"); fi + run_interactive_client "OpenClaw" "$CLIENT_OUT" env \ + HOME="$HOME_DIR" OPENAI_API_KEY="$API_KEY" "${openclaw_cmd[@]}" + RC=$? +else + openclaw_cmd=( + "$OPENCLAW_BIN" agent + --local + --json + --model "lucebox/$MODEL_ID" + --session-id "lucebox-client-harness" + ) + if [[ "$OPENCLAW_TIMEOUT" != "0" ]]; then + openclaw_cmd+=(--timeout "$OPENCLAW_TIMEOUT") + fi + if [[ -n "$OPENCLAW_AGENT_ARGS" ]]; then + read -r -a agent_args <<< "$OPENCLAW_AGENT_ARGS" + openclaw_cmd+=("${agent_args[@]}") + fi + openclaw_cmd+=(--message "$PROMPT") + run_with_timeout "$OPENCLAW_TIMEOUT" env \ + HOME="$HOME_DIR" \ + OPENAI_API_KEY="$API_KEY" \ + "${openclaw_cmd[@]}" \ + < /dev/null > "$CLIENT_OUT" 2>&1 + RC=$? +fi set -e finish_report "$CLIENT_OUT" "$RC" diff --git a/harness/clients/run_opencode.sh b/harness/clients/run_opencode.sh index 3b57ec64f..e585591de 100755 --- a/harness/clients/run_opencode.sh +++ b/harness/clients/run_opencode.sh @@ -22,7 +22,7 @@ CLIENT_OUT="$LOG_DIR/opencode.out" EXPORT_OUT="$LOG_DIR/opencode-export.json" OPENCODE_BIN="${OPENCODE_BIN:-$CLIENT_WORK_DIR/clients/opencode/npm/bin/opencode}" require_client_binary "OpenCode" "$OPENCODE_BIN" "opencode" "OPENCODE_BIN" -HOME_DIR="$LOG_DIR/opencode-home" +HOME_DIR="$(client_home opencode)" PROJECT_DIR="$LOG_DIR/opencode-project" mkdir -p "$HOME_DIR/.config" "$HOME_DIR/.local/share" "$PROJECT_DIR" @@ -73,25 +73,31 @@ wait_lucebox_server set +e cd "$PROJECT_DIR" -run_with_timeout "$OPENCODE_TIMEOUT" env \ - HOME="$HOME_DIR" \ - XDG_CONFIG_HOME="$HOME_DIR/.config" \ - XDG_DATA_HOME="$HOME_DIR/.local/share" \ - OPENAI_API_KEY="$API_KEY" \ - "$OPENCODE_BIN" run \ - --pure \ - --model "lucebox/$MODEL_ID" \ - --format json \ - "$PROMPT" \ - < /dev/null > "$CLIENT_OUT" 2>&1 -RC=$? -SESSION_ID="$(grep -m1 -o 'ses_[A-Za-z0-9]*' "$CLIENT_OUT" || true)" -if [[ -n "$SESSION_ID" ]]; then - HOME="$HOME_DIR" \ - XDG_CONFIG_HOME="$HOME_DIR/.config" \ - XDG_DATA_HOME="$HOME_DIR/.local/share" \ - "$OPENCODE_BIN" export "$SESSION_ID" > "$EXPORT_OUT" 2>&1 || true - cat "$EXPORT_OUT" >> "$CLIENT_OUT" +opencode_env=( + "HOME=$HOME_DIR" + "XDG_CONFIG_HOME=$HOME_DIR/.config" + "XDG_DATA_HOME=$HOME_DIR/.local/share" + "OPENAI_API_KEY=$API_KEY" +) +if [[ "$HARNESS_INTERACTIVE" == "1" ]]; then + opencode_cmd=("$OPENCODE_BIN" "$PROJECT_DIR" --pure --model "lucebox/$MODEL_ID") + if [[ -n "$INTERACTIVE_PROMPT" ]]; then opencode_cmd+=(--prompt "$INTERACTIVE_PROMPT"); fi + run_interactive_client "OpenCode" "$CLIENT_OUT" env "${opencode_env[@]}" "${opencode_cmd[@]}" + RC=$? +else + run_with_timeout "$OPENCODE_TIMEOUT" env "${opencode_env[@]}" \ + "$OPENCODE_BIN" run \ + --pure \ + --model "lucebox/$MODEL_ID" \ + --format json \ + "$PROMPT" \ + < /dev/null > "$CLIENT_OUT" 2>&1 + RC=$? + SESSION_ID="$(grep -m1 -o 'ses_[A-Za-z0-9]*' "$CLIENT_OUT" || true)" + if [[ -n "$SESSION_ID" ]]; then + env "${opencode_env[@]}" "$OPENCODE_BIN" export "$SESSION_ID" > "$EXPORT_OUT" 2>&1 || true + cat "$EXPORT_OUT" >> "$CLIENT_OUT" + fi fi set -e diff --git a/harness/clients/run_pi.sh b/harness/clients/run_pi.sh index 6b2f2ae42..17a409c20 100755 --- a/harness/clients/run_pi.sh +++ b/harness/clients/run_pi.sh @@ -17,7 +17,7 @@ source "$SCRIPT_DIR/common.sh" CLIENT_OUT="$LOG_DIR/pi.out" PI_BIN="${PI_BIN:-$CLIENT_WORK_DIR/clients/pi/npm/bin/pi}" require_client_binary "Pi" "$PI_BIN" "pi" "PI_BIN" -HOME_DIR="$LOG_DIR/pi-home" +HOME_DIR="$(client_home pi)" AGENT_DIR="$HOME_DIR/agent" PROVIDER_API="${PROVIDER_API:-openai-responses}" mkdir -p "$AGENT_DIR" "$HOME_DIR/sessions" @@ -75,23 +75,24 @@ pi_cmd=( "$PI_BIN" --provider lucebox --model "$MODEL_ID" - --print - --mode json --tools "$PI_TOOLS" - --no-session --offline - "$PROMPT" ) +if [[ "$HARNESS_INTERACTIVE" == "0" ]]; then + pi_cmd+=(--print --mode json --no-session "$PROMPT") +elif [[ -n "$INTERACTIVE_PROMPT" ]]; then + pi_cmd+=("$INTERACTIVE_PROMPT") +fi set +e -if [[ "$PI_TIMEOUT" == "0" ]]; then - env "${pi_env[@]}" "${pi_cmd[@]}" \ - < /dev/null > "$CLIENT_OUT" 2>&1 +if [[ "$HARNESS_INTERACTIVE" == "1" ]]; then + run_interactive_client "Pi" "$CLIENT_OUT" env "${pi_env[@]}" "${pi_cmd[@]}" + RC=$? else - env "${pi_env[@]}" timeout "${PI_TIMEOUT}s" "${pi_cmd[@]}" \ + run_with_timeout "$PI_TIMEOUT" env "${pi_env[@]}" "${pi_cmd[@]}" \ < /dev/null > "$CLIENT_OUT" 2>&1 + RC=$? fi -RC=$? set -e finish_report "$CLIENT_OUT" "$RC" diff --git a/harness/tests/test_client_launcher_timeouts.sh b/harness/tests/test_client_launcher_timeouts.sh index 712cfcf55..39a361941 100644 --- a/harness/tests/test_client_launcher_timeouts.sh +++ b/harness/tests/test_client_launcher_timeouts.sh @@ -20,11 +20,17 @@ touch "$FAKE_TARGET" "$FAKE_DRAFT" cat >"$FAKE_SERVER" <<'EOF' #!/usr/bin/env bash +if [[ -n "${SERVER_ARGS_CAPTURE:-}" ]]; then + printf '%s\n' "$*" > "$SERVER_ARGS_CAPTURE" +fi exec sleep 600 EOF cat >"$FAKE_CLIENT" <<'EOF' #!/usr/bin/env bash +if [[ -n "${CLIENT_ARGS_CAPTURE:-}" ]]; then + printf 'HOME=%s ARGS=%s\n' "$HOME" "$*" >> "$CLIENT_ARGS_CAPTURE" +fi printf '%s\n' "$*" EOF @@ -136,6 +142,40 @@ for launcher_case in "${launcher_cases[@]}"; do fi done +# Interactive mode keeps each real TUI attached to the terminal, uses a +# persistent client home, and omits the one-shot subcommand/flags. The fake +# binaries return immediately, so this remains a CPU-only contract test. +interactive_cases=( + 'run_claude_code.sh|CLAUDE_BIN|CLAUDE_TIMEOUT|claude|--model luce-dflash --tools default interactive-policy-ok|--print' + 'run_codex.sh|CODEX_BIN|CODEX_TIMEOUT|codex|--cd |ARGS=exec ' + 'run_opencode.sh|OPENCODE_BIN|OPENCODE_TIMEOUT|opencode|--prompt interactive-policy-ok|ARGS=run ' + 'run_hermes.sh|HERMES_BIN|HERMES_TIMEOUT|hermes|chat --tui|--query' + 'run_pi.sh|PI_BIN|PI_TIMEOUT|pi|--offline interactive-policy-ok|--print' + 'run_openclaw.sh|OPENCLAW_BIN|OPENCLAW_TIMEOUT|openclaw|tui --local --session lucebox-client-harness --message interactive-policy-ok|ARGS=agent ' +) +for interactive_case in "${interactive_cases[@]}"; do + IFS='|' read -r script client_var timeout_var home_name required forbidden <<<"$interactive_case" + args_capture="$TMP_DIR/${script%.sh}-interactive-args" + server_args_capture="$TMP_DIR/${script%.sh}-server-args" + rm -f "$args_capture" "$server_args_capture" "$TIMEOUT_CAPTURE" + run_launcher "$script" "$client_var" "$timeout_var" 17 \ + CLIENT_WORK_DIR="$TMP_DIR/work" \ + HARNESS_INTERACTIVE=1 \ + INTERACTIVE_PROMPT=interactive-policy-ok \ + CLIENT_ARGS_CAPTURE="$args_capture" \ + SERVER_ARGS_CAPTURE="$server_args_capture" \ + TARGET_DEVICE=cuda:2 \ + DRAFT_DEVICE=cuda:3 \ + >/dev/null + grep -Fq -- "$required" "$args_capture" + grep -Fq "HOME=$TMP_DIR/work/interactive/$home_name" "$args_capture" + if grep -Fq -- "$forbidden" "$args_capture"; then + echo "$script retained non-interactive arguments in interactive mode" >&2 + exit 1 + fi + grep -Fq -- '--target-device cuda:2 --draft-device cuda:3' "$server_args_capture" +done + set +e opencode_invalid="$({ run_launcher run_opencode.sh OPENCODE_BIN OPENCODE_TIMEOUT 17 \ @@ -149,6 +189,18 @@ if [[ "$opencode_invalid_rc" -ne 2 ]] || exit 1 fi +set +e +interactive_invalid="$({ + run_launcher run_codex.sh CODEX_BIN CODEX_TIMEOUT 17 HARNESS_INTERACTIVE=invalid +} 2>&1)" +interactive_invalid_rc=$? +set -e +if [[ "$interactive_invalid_rc" -ne 2 ]] || + ! grep -Fq 'HARNESS_INTERACTIVE must be 0 or 1' <<<"$interactive_invalid"; then + echo "invalid HARNESS_INTERACTIVE must fail before startup" >&2 + exit 1 +fi + grep -Fq ': "${OPENCODE_TIMEOUT:=3600}"' "$CLIENTS/run_opencode.sh" grep -Fq ': "${OPENCODE_REQUEST_TIMEOUT_MS:=3600000}"' "$CLIENTS/run_opencode.sh" grep -Fq ': "${OPENCODE_CHUNK_TIMEOUT_MS:=3600000}"' "$CLIENTS/run_opencode.sh"