Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/desktop/electron-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ mac:
NSScreenCaptureUsageDescription: Mothership captures approved app windows to understand and verify computer-use actions.
NSMicrophoneUsageDescription: Sim uses your microphone for voice input in Chat and for meetings you join in the built-in browser.
NSCameraUsageDescription: Sim uses your camera for meetings you join in the built-in browser, such as Google Meet.
# macOS 15 blocks connections to private addresses (self-hosted servers on a
# LAN or VPN) until the user allows Sim; this string explains that prompt.
NSLocalNetworkUsageDescription: Sim connects to your self-hosted Sim server and to local addresses you open in the built-in browser.
entitlements: build/entitlements.mac.plist
entitlementsInherit: build/entitlements.mac.plist
notarize: true
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/main/load-health.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ describe('classifyLoadError', () => {
expect(classifyLoadError(-2)).toBe('unreachable')
expect(classifyLoadError(-1)).toBe('unreachable')
})

it('reports ERR_ADDRESS_UNREACHABLE as a local-network block only under Local Network privacy', () => {
expect(classifyLoadError(-109, true)).toBe('local_network')
expect(classifyLoadError(-109, false)).toBe('unreachable')
})
})

vi.mock('electron', () => import('@/test/electron-mock'))
Expand Down
30 changes: 28 additions & 2 deletions apps/desktop/src/main/load-health.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { release } from 'node:os'
import { createLogger } from '@sim/logger'
import type { BrowserWindow } from 'electron'
import { type EventRecorder, scrubUrl } from '@/main/observability'
Expand All @@ -6,14 +7,33 @@ const logger = createLogger('DesktopLoadHealth')

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

export type LoadErrorKind = 'offline' | 'dns' | 'tls' | 'timeout' | 'unreachable' | 'ignored'
function hasLocalNetworkPrivacy(): boolean {
return (
process.platform === 'darwin' &&
Number.parseInt(release(), 10) >= FIRST_LOCAL_NETWORK_PRIVACY_DARWIN
)
}

export type LoadErrorKind =
| 'offline'
| 'dns'
| 'tls'
| 'timeout'
| 'local_network'
| 'unreachable'
| 'ignored'

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

Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/renderer/offline/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ const ERROR_COPY = {
title: 'The server isn’t responding',
message: 'The connection timed out. The server may be down or your network may be blocking it.',
},
local_network: {
title: 'Can’t reach the server',
message:
'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.',
},
unreachable: {
title: 'Can’t connect to Sim',
message: 'Sim couldn’t reach the server. Check your internet connection, then try again.',
Expand Down
Loading