mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 08:03:20 +00:00
* Recommend Tailscale when the remote Orca runtime is unreachable When a remote-runtime connection fails (RemoteRuntimeClientError "Could not connect to the remote Orca runtime."), append an actionable Tailscale hint to the user-facing error, branched on whether the endpoint is already on a tailnet: - Non-Tailscale endpoint: recommend connecting both devices over Tailscale and pairing with its Tailscale address, with a download link. - Tailscale endpoint (*.ts.net or 100.64.0.0/10): point at the real causes — server offline on the tailnet, or Funnel reverted to tailnet-only — and note that already-paired devices reconnect without re-pairing. Applied at the desktop transport chokepoint (status probe, in-use calls, and subscriptions — connection failures reject, so the hint is applied to the thrown error, not just ok:false responses) and at the web client's connect/timeout sites. New pure shared helper mirrors withMacTailscaleDnsHint. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Address review: scope CGNAT hint to IPv4 literals, track re-paired endpoint - isTailscaleEndpoint: gate the 100.64.0.0/10 check on a full IPv4 literal so DNS names like 100.64.0.1.example.com no longer get tailnet-specific advice. - callRuntimeEnvironment: capture the endpoint the queued closure actually used, so a re-pair between enqueue and dispatch can't append the wrong hint. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Recognize Tailscale IPv6 endpoints and trailing-dot FQDNs in hint The remote-runtime Tailscale hint classified IPv6 Tailscale nodes (fd7a:115c:a1e0::/48) and trailing-dot FQDNs as non-Tailscale, so a user already reaching their server over Tailscale by IPv6 literal was wrongly told to 'connect both devices to Tailscale'. Pairing endpoints can carry bracketed IPv6 literals (resolvePairingEndpoint), so this is a reachable path. Normalize the extracted host (strip brackets and the trailing FQDN dot) and add an IPv6 ULA-range check. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: s546126 <268420947+s546126@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com> Co-authored-by: Orca <help@stably.ai>
81 lines
3.7 KiB
TypeScript
81 lines
3.7 KiB
TypeScript
/**
|
||
* Appends an actionable Tailscale recommendation to remote-runtime connection
|
||
* failures, mirroring `withMacTailscaleDnsHint`. Lives in `shared` as a pure,
|
||
* dependency-free function so both the main process (desktop transport) and the
|
||
* renderer (web client) can route their user-facing errors through it without
|
||
* leaking presentation copy into the shared error constructors (which the CLI,
|
||
* logs, and mobile typecheck also consume).
|
||
*/
|
||
|
||
const TAILSCALE_DOWNLOAD_URL = 'https://tailscale.com/download'
|
||
|
||
// Why: only the "runtime is unreachable" family of failures has a Tailscale
|
||
// remedy; auth/protocol errors pass through untouched.
|
||
const REMOTE_RUNTIME_UNREACHABLE_RE =
|
||
/could not connect to the remote orca runtime|remote orca runtime closed the connection|timed out (?:waiting for|while connecting to) the remote orca runtime/i
|
||
|
||
const TAILSCALE_MAGIC_DNS_SUFFIX_RE = /(?:^|\.)ts\.net$/i
|
||
// Why: gate the CGNAT check on a full IPv4 literal — the range regex alone also
|
||
// matches DNS names like `100.64.0.1.example.com`, which aren't Tailscale IPs.
|
||
const IPV4_LITERAL_RE =
|
||
/^(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/
|
||
// Tailscale assigns node IPs from the 100.64.0.0/10 CGNAT range (second octet 64–127).
|
||
const TAILSCALE_CGNAT_RE = /^100\.(?:6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./
|
||
// Tailscale also assigns each node an IPv6 address from the fd7a:115c:a1e0::/48 ULA
|
||
// block, and pairing endpoints can carry an IPv6 literal (see resolvePairingEndpoint).
|
||
const TAILSCALE_IPV6_RE = /^fd7a:115c:a1e0:/i
|
||
|
||
function extractHost(endpoint: string): string | null {
|
||
let host: string | null
|
||
try {
|
||
host = new URL(endpoint).hostname || null
|
||
} catch {
|
||
// Why: a bare host (no scheme) isn't a valid URL; strip any scheme and take
|
||
// the authority up to the first port/path/query delimiter.
|
||
host = endpoint.replace(/^[a-z]+:\/\//i, '').split(/[/:?#]/, 1)[0] || null
|
||
}
|
||
if (!host) {
|
||
return null
|
||
}
|
||
// Why: WHATWG URL keeps IPv6 literals bracketed (`[fd7a:…]`) and FQDNs can carry
|
||
// a trailing dot; normalize both so the host checks below see a bare address/name.
|
||
return host.replace(/^\[|\]$/g, '').replace(/\.$/, '') || null
|
||
}
|
||
|
||
export function isTailscaleEndpoint(endpoint: string | null | undefined): boolean {
|
||
if (!endpoint) {
|
||
return false
|
||
}
|
||
const host = extractHost(endpoint)
|
||
if (!host) {
|
||
return false
|
||
}
|
||
return (
|
||
TAILSCALE_MAGIC_DNS_SUFFIX_RE.test(host) ||
|
||
(IPV4_LITERAL_RE.test(host) && TAILSCALE_CGNAT_RE.test(host)) ||
|
||
TAILSCALE_IPV6_RE.test(host)
|
||
)
|
||
}
|
||
|
||
export function withRemoteRuntimeTailscaleHint(
|
||
message: string,
|
||
endpoint: string | null | undefined
|
||
): string {
|
||
if (!REMOTE_RUNTIME_UNREACHABLE_RE.test(message)) {
|
||
return message
|
||
}
|
||
// Why: keep the hint idempotent so a message routed through this helper twice
|
||
// (e.g. re-wrapped error response) isn't suffixed with duplicate guidance.
|
||
if (/tailscale/i.test(message)) {
|
||
return message
|
||
}
|
||
if (isTailscaleEndpoint(endpoint)) {
|
||
// Why: a server already reached over Tailscale fails for tailnet-specific
|
||
// reasons, so "use Tailscale" would be useless — point at the real causes.
|
||
// Already-paired devices keep their saved token across server restarts, so
|
||
// re-pairing only matters when adding a new device.
|
||
return `${message} The server may be offline on your tailnet, or its Tailscale Funnel reverted to tailnet-only. Confirm it's reachable; re-pair only when adding a new device, since already-paired devices reconnect with their saved token.`
|
||
}
|
||
return `${message} If the server is on another network, connect both devices to Tailscale and pair using its Tailscale address (100.x or a *.ts.net name). See ${TAILSCALE_DOWNLOAD_URL}.`
|
||
}
|