fix(maestro): surface cross-host attach failures instead of silent close
ci / rust (push) Failing after 2m11s

Cross-host (T2-brokered) attach fails when no shared perimeter is
configured: the daemon rejects the upgrade with a structured 502
(no_perimeter_candidate / no_local_perimeter / perimeter_mismatch /
peer_unreachable), but maestro-web's WS proxy had already upgraded the
browser socket and merely dropped it on the upstream failure — the
attach pane flashed and vanished with no reason shown.

- maestro-web proxy: on a failed upstream handshake, relay the daemon's
  error message to the browser as an application close frame (4502 +
  reason) instead of a bare drop; parse the structured JSON error body.
- web attach.ts: on a 4000+ close with a reason, show it in the pane
  header and terminal body and keep the pane up, rather than auto-closing.
- daemon: WARN at startup when no perimeter_id/private_addr is configured,
  since cross-host attach is then unavailable.
- docs/INSTALL.md: document the required matching perimeter_id + per-host
  private_addr for cross-host attach.

Closes #39

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-24 15:08:03 +00:00
co-authored by Claude Opus 4.8
parent b67591695e
commit 70c592e884
5 changed files with 151 additions and 3 deletions
+12
View File
@@ -245,6 +245,18 @@ export function createAttachConnection(
};
ws.onclose = (ev) => {
// The web proxy relays a broker/daemon rejection (no shared perimeter, peer
// down, unknown session) as an application close code (4000+) carrying a
// human reason. Show it in the pane — both the header and the terminal body
// — and KEEP the pane up so the user can read why, instead of letting it
// flash and vanish (#39). Clean session-ends and ordinary drops keep the
// auto-close behavior below.
const reason = ev.reason?.trim();
if (ev.code >= 4000 && reason && !disposed) {
setState(reason, "error");
term.write(`\r\n\x1b[31m⚠ cannot attach: ${reason}\x1b[0m\r\n`);
return;
}
setState(`closed (${ev.code})`, "closed");
// Only bubble up server/network closes — an intentional dispose has already
// set `disposed`, and eviction/user-close handle their own pane teardown.