diff --git a/cc-ci-plan/launch-upgrader.sh b/cc-ci-plan/launch-upgrader.sh index 4f629cc..67c1f5f 100755 --- a/cc-ci-plan/launch-upgrader.sh +++ b/cc-ci-plan/launch-upgrader.sh @@ -36,8 +36,7 @@ UPGRADER_MODEL="${LOOP_MODEL:-${UPGRADER_MODEL:-sonnet}}" CLAUDE_BIN="${CLAUDE_BIN:-claude}" CLAUDE_FLAGS="${CLAUDE_FLAGS:---dangerously-skip-permissions}" OPENCODE_BIN="${OPENCODE_BIN:-/home/loops/.local/bin/opencode}" -OPENCODE_HOST="${OPENCODE_HOST:-$(tailscale ip -4 2>/dev/null | head -1)}" -OPENCODE_PORT="${OPENCODE_PORT:-4098}" # upgrader gets its own port offset +OPENCODE_SERVER="${OPENCODE_SERVER:-http://127.0.0.1:4096}" REMOTE_CONTROL="${REMOTE_CONTROL:-1}" # 1 => --remote-control / opencode web LOG_DIR="${LOG_DIR:-/srv/cc-ci/.cc-ci-logs}" UPGRADER_ARGS="${UPGRADER_ARGS:-}" @@ -112,10 +111,9 @@ start() { "$CLAUDE_BIN $rc --model '$UPGRADER_MODEL' $CLAUDE_FLAGS \"\$(cat '$kf')\"" ;; opencode) - local web_url="http://${OPENCODE_HOST}:${OPENCODE_PORT}" tmux new-session -d -s "$SESSION" -c "$WORKDIR" \ - "set -a; . /srv/cc-ci/.testenv; set +a; $OPENCODE_BIN --model '$UPGRADER_MODEL' --hostname '$OPENCODE_HOST' --port '$OPENCODE_PORT' run \"\$(cat '$kf')\"" - log "$SESSION web UI: $web_url (tailnet only)" + "set -a; . /srv/cc-ci/.testenv; set +a; $OPENCODE_BIN --model '$UPGRADER_MODEL' run --attach '$OPENCODE_SERVER' --title '$SESSION' \"\$(cat '$kf')\"" + log "$SESSION visible in web UI at http://oc.commoninternet.net (tailnet only)" ;; esac tmux pipe-pane -o -t "$SESSION" "cat >> '$LOG_DIR/$SESSION.log'" @@ -144,7 +142,7 @@ cc-ci upgrader launcher — one-shot weekly recipe-upgrade job agent (remote-con Env: UPGRADER_BACKEND=$UPGRADER_BACKEND UPGRADER_MODEL=$UPGRADER_MODEL UPGRADER_ARGS='${UPGRADER_ARGS:-}' claude: CLAUDE_BIN=$CLAUDE_BIN REMOTE_CONTROL=$REMOTE_CONTROL - opencode: OPENCODE_BIN=$OPENCODE_BIN web=http://${OPENCODE_HOST}:${OPENCODE_PORT} (tailnet only) + opencode: OPENCODE_BIN=$OPENCODE_BIN OPENCODE_SERVER=$OPENCODE_SERVER web=http://oc.commoninternet.net (LOOP_BACKEND / LOOP_MODEL override UPGRADER_BACKEND / UPGRADER_MODEL for unified control) The agent runs /upgrade-all (DEFAULT mode) to completion, then STOPS and stays idle (viewable in the web UI). It does NOT self-terminate; the next weekly `start` clears the idle session and runs fresh. diff --git a/cc-ci-plan/launch.sh b/cc-ci-plan/launch.sh index 70a747c..6285e02 100755 --- a/cc-ci-plan/launch.sh +++ b/cc-ci-plan/launch.sh @@ -41,10 +41,10 @@ LOOP_MODEL="${LOOP_MODEL:-}" CLAUDE_BIN="${CLAUDE_BIN:-claude}" OPENCODE_BIN="${OPENCODE_BIN:-/home/loops/.local/bin/opencode}" -# Tailscale IP of this host — opencode web is bound here so only tailnet peers can reach it. -# Auto-detected from tailscale; override with OPENCODE_HOST if needed. -OPENCODE_HOST="${OPENCODE_HOST:-$(tailscale ip -4 2>/dev/null | head -1)}" -OPENCODE_PORT="${OPENCODE_PORT:-4096}" # fixed port so the URL is predictable +# opencode web server listens on localhost (nginx proxies it at oc.commoninternet.net). +# One shared server hosts all sessions; agents attach with --attach. +OPENCODE_SERVER="${OPENCODE_SERVER:-http://127.0.0.1:4096}" +OPENCODE_PORT="${OPENCODE_PORT:-4096}" # --dangerously-skip-permissions cannot be passed as a FLAG when running as root (claude blocks it). # Use the env var form instead; detect root and switch automatically. @@ -110,8 +110,7 @@ preflight() { need tmux case "$LOOP_BACKEND" in claude) command -v "$CLAUDE_BIN" >/dev/null 2>&1 || die "claude CLI not found (set CLAUDE_BIN)" ;; - opencode) command -v "$OPENCODE_BIN" >/dev/null 2>&1 || die "opencode not found (set OPENCODE_BIN); install from https://opencode.ai" - [[ -n "$OPENCODE_HOST" ]] || die "could not detect tailscale IP for OPENCODE_HOST" ;; + opencode) command -v "$OPENCODE_BIN" >/dev/null 2>&1 || die "opencode not found at $OPENCODE_BIN; install from https://opencode.ai" ;; *) die "unknown LOOP_BACKEND '$LOOP_BACKEND' — use 'claude' or 'opencode'" ;; esac local p plan @@ -167,17 +166,12 @@ PREAMBLE "$CLAUDE_BIN $rc $model_flag $CLAUDE_FLAGS \"\$(cat '$kf')\"" ;; opencode) - # Assign a stable port per session so each has a unique web URL on the tailnet. - local port_offset=0 - [[ "$session" == "$ADV_SESSION" ]] && port_offset=1 - local port=$(( OPENCODE_PORT + port_offset )) - local web_url="http://${OPENCODE_HOST}:${port}" - log "starting $session (backend=opencode, phase=$pid, model=${LOOP_MODEL:-default}, web=$web_url)" - # opencode serve binds the web UI; opencode run sends the initial prompt to it. - # We start the server then send the kickoff message so the session is observable immediately. + # One shared opencode web server (opencode-web.service or manually started) hosts all sessions. + # Each agent attaches to it as a named session visible in the web UI at oc.commoninternet.net. + log "starting $session (backend=opencode, phase=$pid, model=${LOOP_MODEL:-default}, server=$OPENCODE_SERVER)" tmux new-session -d -s "$session" -c "$workdir" \ - "set -a; . /srv/cc-ci/.testenv; set +a; $OPENCODE_BIN $model_flag --hostname '$OPENCODE_HOST' --port '$port' run \"\$(cat '$kf')\"" - log "$session web UI: $web_url (tailnet only)" + "set -a; . /srv/cc-ci/.testenv; set +a; $OPENCODE_BIN $model_flag run --attach '$OPENCODE_SERVER' --title '$session' \"\$(cat '$kf')\"" + log "$session visible in web UI at http://oc.commoninternet.net (tailnet only)" ;; esac tmux pipe-pane -o -t "$session" "cat >> '$LOG_DIR/$session.log'" @@ -501,8 +495,8 @@ Phase sequence (auto-transition on per-phase ## DONE; STOP after the last = manu $(all_ids) Env: LOOP_BACKEND=$LOOP_BACKEND LOOP_MODEL=${LOOP_MODEL:-} claude: CLAUDE_BIN=$CLAUDE_BIN REMOTE_CONTROL=$REMOTE_CONTROL - opencode: OPENCODE_BIN=$OPENCODE_BIN OPENCODE_HOST=${OPENCODE_HOST:-} OPENCODE_PORT=$OPENCODE_PORT - (web UI: http://HOST:PORT per session, tailnet only; TINFOIL_API_KEY from .testenv) + opencode: OPENCODE_BIN=$OPENCODE_BIN OPENCODE_SERVER=$OPENCODE_SERVER + (one shared server; each session attaches with --title; web UI: http://oc.commoninternet.net) WATCH_INTERVAL=${WATCH_INTERVAL}s SIGNAL_INTERVAL=${SIGNAL_INTERVAL}s PHASES_SPEC='$PHASES_SPEC' RESUME_PHASE=1 to keep the current phase index instead of resetting to 0. diff --git a/nix/hosts/cc-ci-orchestrator-hetzner/configuration.nix b/nix/hosts/cc-ci-orchestrator-hetzner/configuration.nix index 9d60559..2354dda 100644 --- a/nix/hosts/cc-ci-orchestrator-hetzner/configuration.nix +++ b/nix/hosts/cc-ci-orchestrator-hetzner/configuration.nix @@ -25,6 +25,7 @@ networking.firewall = { enable = true; trustedInterfaces = [ "tailscale0" ]; + # Port 80 open only on the tailscale interface (trusted) — nginx binds there for oc.commoninternet.net. allowedTCPPorts = [ 22 ]; }; nix.settings.experimental-features = [ "nix-command" "flakes" ]; @@ -117,6 +118,44 @@ SSHCFG ''; }; + # opencode web server — one shared instance; all agent sessions attach to it. + # Serves the web UI at http://oc.commoninternet.net (via nginx below, tailscale-only). + # TINFOIL_API_KEY and other creds are read from /srv/cc-ci/.testenv at startup. + systemd.services.opencode-web = { + description = "opencode web server for cc-ci agents (tinfoil/deepseek backend)"; + wantedBy = [ "multi-user.target" ]; + after = [ "network-online.target" "tailscaled.service" ]; + wants = [ "network-online.target" ]; + serviceConfig = { + Type = "simple"; + User = "loops"; Group = "users"; + WorkingDirectory = "/srv/cc-ci"; + EnvironmentFile = "/srv/cc-ci/.testenv"; + ExecStartPre = "${pkgs.coreutils}/bin/rm -rf /tmp/opencode"; + ExecStart = "/home/loops/.local/bin/opencode serve --hostname 127.0.0.1 --port 4096"; + Restart = "on-failure"; + RestartSec = "5s"; + }; + environment = { HOME = "/home/loops"; }; + path = [ pkgs.bash pkgs.coreutils ]; + }; + + # nginx — reverse-proxy oc.commoninternet.net → opencode web server. + # Bound to the tailscale IP so it is only reachable on the tailnet. + # DNS: add A record oc.commoninternet.net → 100.84.190.30 (operator step). + services.nginx = { + enable = true; + recommendedProxySettings = true; + virtualHosts."oc.commoninternet.net" = { + # Listen on the tailscale interface only — not the public IP. + listen = [{ addr = "100.84.190.30"; port = 80; ssl = false; }]; + locations."/" = { + proxyPass = "http://127.0.0.1:4096"; + proxyWebsockets = true; + }; + }; + }; + # cc-ci-loops supervisor — workspace staged 2026-05-31, so ENABLED for reboot-resilience. systemd.services.cc-ci-loops = { description = "cc-ci Builder/Adversary loops + watchdog (launch.sh start)";