From 5146d547dd16ee9fa3eda9f9b71fa868de948458 Mon Sep 17 00:00:00 2001 From: Dennison Date: Tue, 8 Sep 2026 06:44:25 -0400 Subject: [PATCH] fix(cli): go-code starts harnessd on loopback so the bind guard admits it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `go-code` could not start on any machine without an API key store. The wrapper spawned its daemon with HARNESS_ADDR=":${port}" — an empty host, meaning every interface — and cmd/harnessd/bind_guard.go (#1328) refuses to start an unauthenticated daemon that listens beyond loopback. The daemon exited, and the wrapper reported the port was probably in use, which was never the cause. The wrapper's client base URL has always been http://127.0.0.1:${port}, so the wide bind had no consumer. It only published an unauthenticated agent-execution service to the local network, reachable by anyone on the LAN with the user's provider credentials. Binding loopback restores the entry point and closes that exposure. Also corrects the failure hint to point at the harnessd log rather than asserting port contention, and documents that HARNESS_ADDR supplies only the port to the wrapper. Closes #1411 Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01WJGxhoFhA8JjkwZFcLGdS5 --- cmd/harnesscli/go_code_script_test.go | 57 +++++++++++++++++++++++++++ docs/logs/engineering-log.md | 34 ++++++++++++++++ scripts/go-code.sh | 21 +++++++--- website/docs/cli/go-code-wrapper.md | 4 +- 4 files changed, 109 insertions(+), 7 deletions(-) diff --git a/cmd/harnesscli/go_code_script_test.go b/cmd/harnesscli/go_code_script_test.go index f2d30a632..04cc38a72 100644 --- a/cmd/harnesscli/go_code_script_test.go +++ b/cmd/harnesscli/go_code_script_test.go @@ -145,3 +145,60 @@ func TestGoCodeScriptPropagatesHarnessCLIExitCode(t *testing.T) { }) } } + +// TestGoCodeScriptStartsHarnessdOnLoopback pins the wrapper's bind contract +// (issue #1411): a harnessd the wrapper starts for its own use must listen on +// loopback only. +// +// The wrapper's client base URL is always http://127.0.0.1:${port}, so a wider +// bind has no consumer — it only exposes an unauthenticated agent-execution +// service to the local network. cmd/harnessd/bind_guard.go refuses exactly that +// address when no auth is configured, which killed `go-code` at startup on any +// machine without an API key store. +func TestGoCodeScriptStartsHarnessdOnLoopback(t *testing.T) { + scriptPath, err := filepath.Abs(filepath.Join("..", "..", "scripts", "go-code.sh")) + if err != nil { + t.Fatalf("resolve go-code script path: %v", err) + } + + tmp := t.TempDir() + binDir := t.TempDir() + addrFile := filepath.Join(tmp, "harnessd.addr") + recordFile := filepath.Join(tmp, "harnesscli.called") + countFile := filepath.Join(tmp, "curl.count") + + // curl: fail the first health check so the wrapper takes the start_server + // path, then succeed so it proceeds to harnesscli. + writeExecutable(t, filepath.Join(binDir, "curl"), "#!/usr/bin/env bash\nf=\"$CURL_COUNT_FILE\"\nn=0\nif [ -f \"$f\" ]; then n=$(cat \"$f\"); fi\nn=$((n+1))\necho \"$n\" > \"$f\"\nif [ \"$n\" -eq 1 ]; then exit 1; fi\nexit 0\n") + // harnessd: record the bind address it was handed, then exit. + writeExecutable(t, filepath.Join(binDir, "harnessd"), "#!/usr/bin/env bash\nprintf '%s' \"$HARNESS_ADDR\" > \"$ADDR_FILE\"\nexit 0\n") + writeExecutable(t, filepath.Join(binDir, "harnesscli"), "#!/usr/bin/env bash\nprintf 'called\\n' >> \"$RECORD_FILE\"\nexit 0\n") + + cmd := exec.Command("bash", scriptPath, "runs") + cmd.Env = append(os.Environ(), + "PATH="+binDir+string(os.PathListSeparator)+os.Getenv("PATH"), + "HARNESS_ADDR=:19282", + "ADDR_FILE="+addrFile, + "RECORD_FILE="+recordFile, + "CURL_COUNT_FILE="+countFile, + ) + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("go-code runs failed: %v\n%s", err, out) + } + + gotRaw, err := os.ReadFile(addrFile) + if err != nil { + t.Fatalf("read recorded harnessd address: %v\nscript output:\n%s", err, out) + } + got := strings.TrimSpace(string(gotRaw)) + + // The port from HARNESS_ADDR must still be honored, so a fix that hardcodes + // 127.0.0.1:8080 fails here too. + const want = "127.0.0.1:19282" + if got != want { + t.Fatalf("harnessd bind address = %q, want %q\n"+ + "a wildcard bind is refused by cmd/harnessd/bind_guard.go when no auth is configured\nscript output:\n%s", + got, want, out) + } +} diff --git a/docs/logs/engineering-log.md b/docs/logs/engineering-log.md index 53b92bdba..c8193b99c 100644 --- a/docs/logs/engineering-log.md +++ b/docs/logs/engineering-log.md @@ -1,5 +1,39 @@ # Engineering Log +## 2026-09-08 — Issue #1411 go-code wrapper bound harnessd beyond loopback + +- Symptom: plain `go-code` (no flags) died on a clean machine with no API key + store configured and no `HARNESS_AUTH_DISABLED` set — the daemon exited + before becoming healthy, and the wrapper's failure hint blamed port + contention. +- Cause: `scripts/go-code.sh:196` (pre-fix) hardcoded + `HARNESS_ADDR=":${port}"` — a wildcard bind — when spawning `harnessd` for + its own use, even though the wrapper's client `base_url` is always built as + `http://127.0.0.1:${port}`. `cmd/harnessd/bind_guard.go` (issue #1328) + refuses to start an unauthenticated daemon that listens beyond loopback, so + the wider bind had no legitimate purpose and just tripped the guard. +- Fix: `start_server()` now spawns with `HARNESS_ADDR="127.0.0.1:${port}"`; + the failure hint no longer asserts port contention as the only cause and + points at the harnessd log above it instead; the header comment and + `--help` text now say `HARNESS_ADDR` supplies only the port and that a + wrapper-started `harnessd` always binds `127.0.0.1`. +- Security note: this is strictly tightening, not a new restriction users + need to work around. Before the #1328 guard existed, this same line + silently ran an unauthenticated agent-execution daemon on every network + interface, reachable by anyone on the LAN with the user's provider + credentials. +- Regression: `TestGoCodeScriptStartsHarnessdOnLoopback` in + `cmd/harnesscli/go_code_script_test.go`, red before the fix with + `harnessd bind address = ":19282", want "127.0.0.1:19282"`. +- Cross-reference: the 2026-09-05 `#1380` entry below first spotted this + pattern in `internal/workspace/bootstrap.go:36` (VM cloud-init template), + filed as a follow-up rather than fixed there; that sibling instance is + tracked separately as issue #1392 and is unaffected by this fix. +- Left alone deliberately: `scripts/soak.sh:271-272`, `scripts/smoke-test.sh:87`, + and `scripts/run-bench-smoke.sh:135-136` also bind `:${PORT}` (wildcard), + but all three pass `HARNESS_AUTH_DISABLED=true`, so the bind guard admits + them. + ## 2026-09-06 — TUI scenario walk: plan mode, @ completion, question box, bubble width (#1407) - Twelve multi-step TUI scenarios driven live in tmux (fake provider with scripted streaming/tool turns, and OpenRouter DeepSeek). Fixes: `/plan` command toggles enforced plan mode with a `PLAN` status badge and `harnesscli --tui --plan-mode` now honored (`runTUI` passes the flag into `TUIConfig`); `@name` Tab completion completes bare relative file names, not only `./`, `/`, `~/` paths; the AskUserQuestion and Plan-Approval boxes size their top/bottom borders to the content instead of a fixed 40-col rule; assistant markdown bubbles render at the indented width, expand tabs, and trim padding so a bubble never exceeds the terminal width. Guards added: streamed-transcript integrity and no duplicate tool card on ctrl+o after interrupt. diff --git a/scripts/go-code.sh b/scripts/go-code.sh index 5df14274e..f4e3fb435 100755 --- a/scripts/go-code.sh +++ b/scripts/go-code.sh @@ -20,9 +20,11 @@ set -euo pipefail # Run or plan the self-improvement test loop. # # Environment: -# HARNESS_ADDR Listen address (default :8080). The port is extracted and -# used to construct the BASE_URL for health checks and CLI -# invocations. +# HARNESS_ADDR Listen address (default :8080). Only the port is used: it +# constructs the BASE_URL for health checks and CLI +# invocations, and a wrapper-started harnessd always binds +# 127.0.0.1 on that port. Run harnessd directly for a daemon +# that listens beyond this machine. SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DATA_DIR="" @@ -49,7 +51,8 @@ Usage: Run or plan the self-improvement test loop. Environment: - HARNESS_ADDR Override the server address (default: :8080). + HARNESS_ADDR Override the server port (default: :8080). A wrapper-started + harnessd always binds 127.0.0.1 on that port. Example: HARNESS_ADDR=:9090 go-code "ls *.go" Description: @@ -193,7 +196,13 @@ start_server() { # harnessd unchanged. # # Start in background, capturing the PID. - HARNESS_ADDR=":${port}" "$harnessd_bin" & + # Loopback only. The wrapper talks to this daemon exclusively over + # http://127.0.0.1:${port}, so a wildcard bind has no consumer — it would just + # publish an unauthenticated agent-execution service to the local network, and + # harnessd's bind guard (cmd/harnessd/bind_guard.go, issue #1328) refuses to + # start there without auth. HARNESS_ADDR supplies the port; the host is ours. + # Issue #1411. + HARNESS_ADDR="127.0.0.1:${port}" "$harnessd_bin" & local pid=$! PID_FILE="${TMPDIR:-/tmp}/harnessd.${$}.pid" echo "$pid" > "$PID_FILE" @@ -209,7 +218,7 @@ start_server() { die "server did not become healthy within 10 s" fi if ! kill -0 "$pid" 2>/dev/null; then - die "harnessd (pid ${pid}) exited before becoming healthy on port ${port}. If the port is already in use (see the harnessd log above), free it or run on another port with HARNESS_ADDR=:PORT (e.g. HARNESS_ADDR=:9090 go-code)." + die "harnessd (pid ${pid}) exited before becoming healthy on port ${port}. See the harnessd log above for the reason it stopped. If it reports the port is already in use, free it or run on another port with HARNESS_ADDR=:PORT (e.g. HARNESS_ADDR=:9090 go-code)." fi done info "server is ready" diff --git a/website/docs/cli/go-code-wrapper.md b/website/docs/cli/go-code-wrapper.md index 1039f2f7b..2c752afb8 100644 --- a/website/docs/cli/go-code-wrapper.md +++ b/website/docs/cli/go-code-wrapper.md @@ -171,7 +171,7 @@ The full code table (`0` completed, `1` client error, `2` failed, `3` blocked, ` ### Server address: `HARNESS_ADDR` -The `HARNESS_ADDR` environment variable controls the listen address. The default is `127.0.0.1:8080`. The wrapper extracts the port from this value and constructs the base URL as `http://127.0.0.1:`. +The `HARNESS_ADDR` environment variable controls the listen address. The default is `127.0.0.1:8080`. The wrapper only uses the **port** from this value — the host part is ignored. A `harnessd` that the wrapper starts always binds `127.0.0.1` on that port, because the wrapper only ever talks to it over loopback (`http://127.0.0.1:`). ```bash # Run on a different port @@ -180,6 +180,8 @@ HARNESS_ADDR=:9090 go-code "List the Go source files" The address can also be set in your project or user config file (`~/.harness/config.toml` or `.harness/config.toml`). `HARNESS_ADDR` takes precedence over the TOML layers. +If you need a daemon reachable from another machine, don't go through `go-code` — run `harnessd` directly with an explicit `HARNESS_ADDR` (a real host, not just a port) and either a configured API key store or `HARNESS_AUTH_DISABLED=true`. An unauthenticated daemon that listens beyond loopback refuses to start otherwise. + ### Project root detection `go-code` automatically resolves the workspace root before launching TUI or prompt mode. It walks parent directories from `$PWD`, looking for: