Skip to content

Commit d5451f6

Browse files
authored
fix(desktop): explain macOS Local Network blocks for private self-hosted servers (#9777)
* fix(desktop): explain macOS Local Network blocks for private self-hosted servers * fix(desktop): only show Local Network guidance on macOS 15+
1 parent 44ce671 commit d5451f6

4 files changed

Lines changed: 41 additions & 2 deletions

File tree

‎apps/desktop/electron-builder.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ mac:
7272
NSScreenCaptureUsageDescription: Mothership captures approved app windows to understand and verify computer-use actions.
7373
NSMicrophoneUsageDescription: Sim uses your microphone for voice input in Chat and for meetings you join in the built-in browser.
7474
NSCameraUsageDescription: Sim uses your camera for meetings you join in the built-in browser, such as Google Meet.
75+
# macOS 15 blocks connections to private addresses (self-hosted servers on a
76+
# LAN or VPN) until the user allows Sim; this string explains that prompt.
77+
NSLocalNetworkUsageDescription: Sim connects to your self-hosted Sim server and to local addresses you open in the built-in browser.
7578
entitlements: build/entitlements.mac.plist
7679
entitlementsInherit: build/entitlements.mac.plist
7780
notarize: true

‎apps/desktop/src/main/load-health.test.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ describe('classifyLoadError', () => {
77
expect(classifyLoadError(-2)).toBe('unreachable')
88
expect(classifyLoadError(-1)).toBe('unreachable')
99
})
10+
11+
it('reports ERR_ADDRESS_UNREACHABLE as a local-network block only under Local Network privacy', () => {
12+
expect(classifyLoadError(-109, true)).toBe('local_network')
13+
expect(classifyLoadError(-109, false)).toBe('unreachable')
14+
})
1015
})
1116

1217
vi.mock('electron', () => import('@/test/electron-mock'))

‎apps/desktop/src/main/load-health.ts‎

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { release } from 'node:os'
12
import { createLogger } from '@sim/logger'
23
import type { BrowserWindow } from 'electron'
34
import { type EventRecorder, scrubUrl } from '@/main/observability'
@@ -6,14 +7,33 @@ const logger = createLogger('DesktopLoadHealth')
67

78
const AUTO_RETRY_INTERVAL_MS = 5000
89
const LOAD_WATCHDOG_MS = 30_000
10+
/** Darwin 24 is macOS 15, the first release that gates apps behind Local Network privacy. */
11+
const FIRST_LOCAL_NETWORK_PRIVACY_DARWIN = 24
912

10-
export type LoadErrorKind = 'offline' | 'dns' | 'tls' | 'timeout' | 'unreachable' | 'ignored'
13+
function hasLocalNetworkPrivacy(): boolean {
14+
return (
15+
process.platform === 'darwin' &&
16+
Number.parseInt(release(), 10) >= FIRST_LOCAL_NETWORK_PRIVACY_DARWIN
17+
)
18+
}
19+
20+
export type LoadErrorKind =
21+
| 'offline'
22+
| 'dns'
23+
| 'tls'
24+
| 'timeout'
25+
| 'local_network'
26+
| 'unreachable'
27+
| 'ignored'
1128

1229
/**
1330
* Maps Chromium net error codes to recovery copy. -3 (ERR_ABORTED) is emitted
1431
* constantly by OAuth redirect chains and in-app aborts and must be ignored.
1532
*/
16-
export function classifyLoadError(errorCode: number): LoadErrorKind {
33+
export function classifyLoadError(
34+
errorCode: number,
35+
localNetworkPrivacy: boolean = hasLocalNetworkPrivacy()
36+
): LoadErrorKind {
1737
// Only ERR_ABORTED (-3) and success (0) are ignored. ERR_FAILED (-2) and
1838
// ERR_IO_PENDING (-1) are real failures that must surface the offline page.
1939
if (errorCode === 0 || errorCode === -3) {
@@ -31,6 +51,12 @@ export function classifyLoadError(errorCode: number): LoadErrorKind {
3151
if (errorCode <= -200 && errorCode >= -213) {
3252
return 'tls'
3353
}
54+
// macOS Local Network privacy fails the socket with EHOSTUNREACH, which
55+
// Chromium reports as ERR_ADDRESS_UNREACHABLE (-109), so a self-hosted server
56+
// on a private address looks like a routing failure until Sim is allowed.
57+
if (errorCode === -109 && localNetworkPrivacy) {
58+
return 'local_network'
59+
}
3460
return 'unreachable'
3561
}
3662

‎apps/desktop/src/renderer/offline/index.tsx‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,11 @@ const ERROR_COPY = {
3030
title: 'The server isn’t responding',
3131
message: 'The connection timed out. The server may be down or your network may be blocking it.',
3232
},
33+
local_network: {
34+
title: 'Can’t reach the server',
35+
message:
36+
'If your Sim server is on a local or private network, macOS may be blocking Sim. Turn on Sim in System Settings > Privacy & Security > Local Network, then try again.',
37+
},
3338
unreachable: {
3439
title: 'Can’t connect to Sim',
3540
message: 'Sim couldn’t reach the server. Check your internet connection, then try again.',

0 commit comments

Comments
 (0)