diff --git a/tools/fleet/README.md b/tools/fleet/README.md new file mode 100644 index 0000000..ec833c6 --- /dev/null +++ b/tools/fleet/README.md @@ -0,0 +1,33 @@ +# Fleet hardening — lid-close / internet-loss (#324) + +Operational fix applied 2026-08-09, captured here as versioned template. + +## Problem + +Closing the MacBook lid killed the `ssh -L 127.0.0.1:4096` tunnel, so chat appeared to stop even though `Aarons-Mac-mini` (canonical server, `co.harmoniqs.amicode-server`, `opencode-dev.db` 707 sessions) kept running. Tunnel used `ServerAliveInterval 30 / CountMax 3` (90s detection) and the fleet guard was missing on the mini with a stale `~/harmoniqs` path, so a port-race could fork a laptop-local `opencode.db` during reconnect gaps. + +## Fix (applied on hosts) + +* **Guard** — `amico-opencode-fleet-guard` installed to `~/.local/bin` on both hosts. Client exits 1 (panel rides tunnel), server execs frozen `~/.amico/server/bin/opencode` falling back to VSIX. Prevents silent fork (ADR 0005, #279, #324). +* **Tunnel** — `co.harmoniqs.amico-tunnel.plist` tuned to `ServerAliveInterval 15 / CountMax 2 / TCPKeepAlive yes` (30s detection, matches `amico-mini` Host `ServerAliveInterval 15`). LAN-first `Match host amico-mini exec "ping -c1 -W1000 Aarons-Mac-mini.local"` keeps tunnel up on same Wi-Fi with no internet. +* **Hygiene** — forked `opencode.db` archived on macbook to `~/.local/share/opencode/archive/`, `~/.config/opencode/opencode.json` symlink repointed to `vault-aaron`, duplicate `Host the-feynmachine` removed. + +## Verification + +``` +lsof -nP -iTCP:4096 -sTCP:LISTEN # ssh LISTEN, no opencode +curl http://127.0.0.1:4096/session # 200 via tunnel +sqlite3 ~/.local/share/opencode/opencode-dev.db "SELECT COUNT(*) FROM session;" # 707 on mini +cat ~/.amico/ops/fleet-status.json # guard ok, served 639 +``` + +Lid-close/wake or LAN-only internet loss: tunnel respawns via `KeepAlive` within ~30s. + +## Install + +```bash +cp tools/fleet/amico-opencode-fleet-guard ~/.local/bin/amico-opencode-fleet-guard +chmod +x ~/.local/bin/amico-opencode-fleet-guard +# scope: machine — never synced +# settings.json: "amicode.opencodeBinary": "/Users/aaron/.local/bin/amico-opencode-fleet-guard" +``` diff --git a/tools/fleet/amico-opencode-fleet-guard b/tools/fleet/amico-opencode-fleet-guard new file mode 100755 index 0000000..e5a7db0 --- /dev/null +++ b/tools/fleet/amico-opencode-fleet-guard @@ -0,0 +1,28 @@ +#!/bin/bash +# amico-opencode-fleet-guard — fleet client guard for the Amicode extension. +# Only Aarons-Mac-mini may run the canonical opencode server. On any other +# machine, exit 1 immediately: the extension's health probe then rides the +# SSH tunnel on 127.0.0.1:4096 (canonical server), or fails closed if the +# tunnel is down — a client must NEVER spawn a local fork. (harmoniqs/amicode#279, #324) +# +# Install to ~/.local/bin on every host and point amicode.opencodeBinary at it: +# cp tools/fleet/amico-opencode-fleet-guard ~/.local/bin/amico-opencode-fleet-guard +# # then in settings.json (scope: machine): "amicode.opencodeBinary": "/Users/aaron/.local/bin/amico-opencode-fleet-guard" +if [ "$(scutil --get LocalHostName 2>/dev/null)" != "Aarons-Mac-mini" ]; then + echo "[amico-fleet-guard] refusing to spawn a local opencode server on a fleet client — panel will ride the tunnel" >&2 + exit 1 +fi +# SERVER: resolve opencode binary — frozen first (same order as amicode-server.sh) +FROZEN="$HOME/.amico/server/bin/opencode" +VSIX_BIN="$(ls -dt "$HOME"/.vscode/extensions/harmoniqs.amicode-*/vendor/opencode/darwin-arm64/opencode 2>/dev/null | head -1)" +DEV_BIN="$HOME/armonia/repos/amicode/packages/extension/vendor/opencode/darwin-arm64/opencode" +if [ -x "$FROZEN" ]; then + exec "$FROZEN" "$@" +elif [ -n "$VSIX_BIN" ] && [ -x "$VSIX_BIN" ]; then + exec "$VSIX_BIN" "$@" +elif [ -x "$DEV_BIN" ]; then + exec "$DEV_BIN" "$@" +else + echo "[amico-fleet-guard] no opencode binary found (frozen, VSIX, or armonia repos)" >&2 + exit 1 +fi