feat: opencode web at oc.commoninternet.net (one server, named sessions)

configuration.nix:
- systemd.services.opencode-web: one shared opencode server on 127.0.0.1:4096,
  EnvironmentFile=/srv/cc-ci/.testenv (TINFOIL_API_KEY), ExecStartPre clears
  stale /tmp/opencode so restarts never fail on the EEXIST race.
- services.nginx: reverse-proxy oc.commoninternet.net → localhost:4096,
  bound to tailscale IP 100.84.190.30 (tailnet-only, plain HTTP).
  DNS: A record oc.commoninternet.net → 100.84.190.30 (operator step).

launch.sh + launch-upgrader.sh:
- Drop per-session ports / OPENCODE_HOST; add OPENCODE_SERVER=http://127.0.0.1:4096.
- opencode backend: agents use `opencode run --attach $OPENCODE_SERVER --title $session`
  so each shows up as a named session in the web UI.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
autonomic-bot
2026-05-31 17:37:03 +00:00
parent a87d42f491
commit e0e5bf6e64
3 changed files with 55 additions and 24 deletions

View File

@ -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:-<none>}'
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.

View File

@ -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:-<default>}
claude: CLAUDE_BIN=$CLAUDE_BIN REMOTE_CONTROL=$REMOTE_CONTROL
opencode: OPENCODE_BIN=$OPENCODE_BIN OPENCODE_HOST=${OPENCODE_HOST:-<tailscale-ip>} 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.

View File

@ -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)";