fix(macos): isolate all local package identities

This commit is contained in:
Brennan Benson
2026-07-22 12:50:10 -07:00
parent 2a1c2b0af1
commit 4bbfa35563
6 changed files with 140 additions and 16 deletions
+19 -5
View File
@@ -2,6 +2,9 @@
import { spawnSync } from 'node:child_process'
const isLocalMacPackage = process.argv.includes('--local-macos-package')
const localMacAppBundleId = 'com.stablyai.orca.local'
if (process.platform === 'win32') {
runNodeScript('config/scripts/build-windows-cli-launcher.mjs')
process.exit(0)
@@ -12,19 +15,30 @@ if (process.platform !== 'darwin') {
process.exit(0)
}
runPnpmScript('build:computer-macos')
runPnpmScript('build:notification-status-macos')
runPnpmScript(
'build:computer-macos',
[],
isLocalMacPackage
? { ...process.env, ORCA_COMPUTER_MACOS_BUNDLE_ID: `${localMacAppBundleId}.computer-use` }
: process.env
)
runPnpmScript(
'build:notification-status-macos',
isLocalMacPackage ? ['--bundle-id', localMacAppBundleId] : []
)
process.exit(0)
function runPnpmScript(scriptName) {
function runPnpmScript(scriptName, scriptArgs = [], env = process.env) {
const npmExecPath = process.env.npm_execpath
const command = npmExecPath
? process.execPath
: process.platform === 'win32'
? 'pnpm.cmd'
: 'pnpm'
const args = npmExecPath ? [npmExecPath, 'run', scriptName] : ['run', scriptName]
const result = spawnSync(command, args, { stdio: 'inherit' })
const args = npmExecPath
? [npmExecPath, 'run', scriptName, ...scriptArgs]
: ['run', scriptName, ...scriptArgs]
const result = spawnSync(command, args, { env, stdio: 'inherit' })
if (result.signal) {
process.kill(process.pid, result.signal)
@@ -19,6 +19,32 @@ const {
} = require('../packaged-runtime-node-modules.cjs')
describe('electron-builder config', () => {
it('changes only the non-release macOS app id', () => {
const configPath = require.resolve('../electron-builder.config.cjs')
const original = process.env.ORCA_MAC_RELEASE
try {
delete require.cache[configPath]
delete process.env.ORCA_MAC_RELEASE
const localConfig = require('../electron-builder.config.cjs')
expect(localConfig.appId).toBe('com.stablyai.orca')
expect(localConfig.mac.appId).toBe('com.stablyai.orca.local')
delete require.cache[configPath]
process.env.ORCA_MAC_RELEASE = '1'
const releaseConfig = require('../electron-builder.config.cjs')
expect(releaseConfig.appId).toBe('com.stablyai.orca')
expect(releaseConfig.mac.appId).toBeUndefined()
} finally {
if (original === undefined) {
delete process.env.ORCA_MAC_RELEASE
} else {
process.env.ORCA_MAC_RELEASE = original
}
delete require.cache[configPath]
require('../electron-builder.config.cjs')
}
})
it('excludes repo-only source trees from app.asar', () => {
expect(electronBuilderConfig.files).toEqual(
expect.arrayContaining([
@@ -99,7 +99,9 @@ describe('Electron runtime package contract', () => {
expect(scripts['build:linux']).toContain('pnpm run build:desktop')
expect(scripts['build:linux']).not.toContain('pnpm run build ')
expect(scripts['build:linux']).not.toContain('build:computer-macos')
expect(scripts['build:mac']).toContain('pnpm run build:computer-macos')
expect(scripts['build:unpack']).toContain('pnpm run build:native -- --local-macos-package')
expect(scripts['build:mac']).toContain('pnpm run build:native -- --local-macos-package')
expect(scripts['build:mac:release']).not.toContain('com.stablyai.orca.local')
expect(scripts['build:release']).toContain('pnpm run build:native')
expect(scripts['build:release']).not.toContain('build:computer-macos')
})
+2 -2
View File
@@ -71,10 +71,10 @@
"postinstall": "node config/scripts/rebuild-native-deps.mjs",
"rebuild:electron": "node config/scripts/rebuild-native-deps.mjs",
"rebuild:node": "pnpm rebuild node-pty",
"build:unpack": "pnpm run build && pnpm run ensure:electron-runtime && electron-builder --config config/electron-builder.config.cjs --dir",
"build:unpack": "pnpm run build:desktop && pnpm run build:native -- --local-macos-package && pnpm run ensure:electron-runtime && electron-builder --config config/electron-builder.config.cjs --dir",
"build:win": "pnpm run build:desktop && pnpm run ensure:electron-runtime && electron-builder --config config/electron-builder.config.cjs --win",
"build:icons": "bash resources/icon-source/generate.sh",
"build:mac": "pnpm run build:desktop && pnpm run build:computer-macos && pnpm run build:notification-status-macos --bundle-id com.stablyai.orca.local && pnpm run ensure:electron-runtime && electron-builder --config config/electron-builder.config.cjs --mac",
"build:mac": "pnpm run build:desktop && pnpm run build:native -- --local-macos-package && pnpm run ensure:electron-runtime && electron-builder --config config/electron-builder.config.cjs --mac",
"build:mac:release": "node config/scripts/verify-macos-release-env.mjs && ORCA_MAC_RELEASE=1 pnpm run build:desktop && ORCA_MAC_RELEASE=1 pnpm run build:computer-macos && ORCA_MAC_RELEASE=1 pnpm run build:notification-status-macos && pnpm run ensure:electron-runtime && ORCA_MAC_RELEASE=1 electron-builder --config config/electron-builder.config.cjs --mac",
"build:linux": "pnpm run build:desktop && pnpm run ensure:electron-runtime && electron-builder --config config/electron-builder.config.cjs --linux AppImage deb",
"test:e2e": "pnpm run ensure:electron-runtime && npx playwright test --config tests/playwright.config.ts --project=electron-headless",
+68 -5
View File
@@ -15,7 +15,9 @@ const {
notificationCtorMock,
notificationIsSupportedMock,
getAllWindowsMock,
shellOpenExternalMock
shellOpenExternalMock,
electronAppMock,
execFileSyncMock
} = vi.hoisted(() => {
const removeHandlerMock = vi.fn()
const handleMock = vi.fn()
@@ -36,6 +38,12 @@ const {
const notificationIsSupportedMock = vi.fn(() => true)
const getAllWindowsMock = vi.fn(() => [])
const shellOpenExternalMock = vi.fn()
const electronAppMock = {
focus: vi.fn(),
getPath: vi.fn(),
isPackaged: false
}
const execFileSyncMock = vi.fn()
return {
removeHandlerMock,
handleMock,
@@ -47,10 +55,14 @@ const {
notificationCtorMock,
notificationIsSupportedMock,
getAllWindowsMock,
shellOpenExternalMock
shellOpenExternalMock,
electronAppMock,
execFileSyncMock
}
})
vi.mock('node:child_process', () => ({ execFileSync: execFileSyncMock }))
vi.mock('electron', () => ({
ipcMain: {
removeHandler: removeHandlerMock,
@@ -62,9 +74,7 @@ vi.mock('electron', () => ({
BrowserWindow: {
getAllWindows: getAllWindowsMock
},
app: {
focus: vi.fn()
},
app: electronAppMock,
shell: {
openExternal: shellOpenExternalMock
}
@@ -122,6 +132,10 @@ describe('registerNotificationHandlers', () => {
getAllWindowsMock.mockReset()
getAllWindowsMock.mockReturnValue([])
shellOpenExternalMock.mockClear()
electronAppMock.focus.mockClear()
electronAppMock.getPath.mockReset()
electronAppMock.isPackaged = false
execFileSyncMock.mockReset()
setTrayAttentionMock.mockClear()
})
@@ -278,6 +292,55 @@ describe('registerNotificationHandlers', () => {
}
})
it('reads the packaged bundle id when launchd does not provide one', async () => {
const originalPlatform = process.platform
const originalDevBundleId = process.env.ORCA_DEV_MACOS_BUNDLE_ID
const originalLaunchdBundleId = process.env.__CFBundleIdentifier
Object.defineProperty(process, 'platform', { value: 'darwin', configurable: true })
delete process.env.ORCA_DEV_MACOS_BUNDLE_ID
delete process.env.__CFBundleIdentifier
electronAppMock.isPackaged = true
electronAppMock.getPath.mockReturnValue('/Applications/Orca.app/Contents/MacOS/Orca')
execFileSyncMock.mockReturnValue('com.stablyai.orca.local\n')
try {
registerNotificationHandlers({
getSettings: () => ({
notifications: {
enabled: true,
agentTaskComplete: true,
terminalBell: true,
suppressWhenFocused: true
}
})
} as never)
const handler = getOpenSystemSettingsHandler()
handler({})
expect(execFileSyncMock).toHaveBeenCalledWith(
'/usr/libexec/PlistBuddy',
['-c', 'Print :CFBundleIdentifier', '/Applications/Orca.app/Contents/Info.plist'],
{ encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }
)
expect(shellOpenExternalMock).toHaveBeenCalledWith(
'x-apple.systempreferences:com.apple.Notifications-Settings.extension?id=com.stablyai.orca.local'
)
} finally {
Object.defineProperty(process, 'platform', { value: originalPlatform, configurable: true })
electronAppMock.isPackaged = false
if (originalDevBundleId === undefined) {
delete process.env.ORCA_DEV_MACOS_BUNDLE_ID
} else {
process.env.ORCA_DEV_MACOS_BUNDLE_ID = originalDevBundleId
}
if (originalLaunchdBundleId === undefined) {
delete process.env.__CFBundleIdentifier
} else {
process.env.__CFBundleIdentifier = originalLaunchdBundleId
}
}
})
it('opens Windows notification settings', async () => {
const originalPlatform = process.platform
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true })
+22 -3
View File
@@ -1,7 +1,8 @@
/* eslint-disable max-lines -- Why: notification IPC keeps permission, dispatch, custom sound asset, and sound-loading handlers colocated so renderer/main contracts stay auditable. */
import { app, BrowserWindow, Notification, ipcMain, shell } from 'electron'
import { execFileSync } from 'node:child_process'
import { readFile, stat } from 'node:fs/promises'
import { extname, isAbsolute, normalize } from 'node:path'
import { dirname, extname, isAbsolute, join, normalize } from 'node:path'
import beepSoundPath from '../../../resources/notification-sounds/beep.mp3?asset'
import blipSoundPath from '../../../resources/notification-sounds/blip.mp3?asset'
import blopSoundPath from '../../../resources/notification-sounds/blop.mp3?asset'
@@ -186,11 +187,29 @@ function probeNotificationDelivery(): Promise<NotificationDeliveryProbeResult> {
return deliveryProbeInFlight
}
function readPackagedMacBundleId(): string | null {
if (!app.isPackaged) {
return null
}
try {
const infoPlistPath = join(dirname(app.getPath('exe')), '..', 'Info.plist')
return (
execFileSync('/usr/libexec/PlistBuddy', ['-c', 'Print :CFBundleIdentifier', infoPlistPath], {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore']
}).trim() || null
)
} catch {
return null
}
}
function getMacNotificationSettingsUrl(): string {
// Why: local packaging uses com.stablyai.orca.local (#9756), so trust the
// launchd-provided id before falling back to the release id.
// Why: direct executable launches do not reliably receive launchd's private
// bundle-id environment variable, so packaged apps read their own plist.
const bundleId =
process.env.ORCA_DEV_MACOS_BUNDLE_ID ??
readPackagedMacBundleId() ??
process.env.__CFBundleIdentifier ??
MACOS_PACKAGED_BUNDLE_ID
return `${MACOS_NOTIFICATION_SETTINGS_URL}?id=${encodeURIComponent(bundleId)}`