mirror of
https://github.com/stablyai/orca.git
synced 2026-10-17 08:03:14 +00:00
* feat(onboarding): state-aware macOS notification permission step The Set up notifications step showed a one-size-fits-all 'Open Mac Settings' button that simultaneously fired the macOS permission prompt and opened System Settings — two competing system UIs, with System Settings unnecessary for the common fresh-install case. Electron exposes no API to read macOS notification authorization, but scheduling outcomes do reveal it: a silent probe notification's 'show' event means permission is granted, 'failed' means delivery is blocked. A new notifications:probeDelivery IPC runs that probe (cached via passive delivery evidence and a persisted confirmation flag), and the onboarding card now renders the real state: - fresh install: the probe itself pops the native Allow dialog the moment the step opens; the card flips to 'Notifications are enabled' automatically when the user clicks Allow (silent 2.5s re-probes) - blocked: amber card with an Open System Settings deep-link, which also self-heals once the user flips the toggle - granted: green confirmation card The test-notification button now feeds the same card instead of the ambiguous 'if no banner appeared…' toast during onboarding. Co-authored-by: Orca <help@stably.ai> * fix: don't log expected probe rejections while polling for permission Co-authored-by: Orca <help@stably.ai> * fix: amber warning styling + single stable dev bundle id for notifications - Blocked card now uses the app's shipped amber idiom (tinted surface with amber title/body) instead of white-on-amber-wash, which read muddy in dark mode; macOS permission card split into its own module to stay under the max-lines budget. - Dev instances previously minted a unique macOS bundle id per branch x Electron version, registering a new Notification Settings entry every time ('Orca: <branch>' rows piling up forever) and pointing the settings deep-link at ids System Settings can't resolve. All dev instances now share com.stablyai.orca.dev: one Notification Center entry, one permission grant covering every dev build. Co-authored-by: Orca <help@stably.ai> * fix: tighten macOS permission card copy Body copy was one long sentence; now a single short instruction with 'Updates automatically.' as a separate dimmer line. Also repairs locale catalog parity for keys introduced by commits rebased into this branch. Co-authored-by: Orca <help@stably.ai> * fix: drop 'Updates automatically.' line; ad-hoc sign dev app copies The extra line read as confusing filler — the cards now carry one short instruction each. Dev Electron copies had broken code signatures (the Info.plist identity edits invalidate the ad-hoc seal), which macOS punishes by refusing Notification Center registration outright: every dev notification failed with UNErrorDomain error 1, the app never appeared in System Settings > Notifications, and the settings deep-link had nothing to land on. The dev runner now ad-hoc re-signs the copied bundle after the plist edits (bundleLayoutVersion bumped so stale unsigned copies are recreated). Verified end-to-end: runner-built copy passes codesign --verify --deep, probe delivery returns delivered, the onboarding card flips green in dev, and the deep link opens the dev app's own notifications pane. Co-authored-by: Orca <help@stably.ai> * fix: drop confusing copy line; session-only permission evidence Removes the 'Updates automatically.' line from both permission cards. Also drops the persisted notificationDeliveryConfirmed flag: OS-level permission changes between sessions, and a stale positive rendered a false green card. Delivery evidence is now session-scoped only. Documented detection ceiling (verified empirically on macOS 26): while the permission dialog is unanswered — and when notifications are toggled off in System Settings after being authorized — macOS accepts requests and silently swallows them, with no public API (Notification Center delivered-history and legacy ncprefs both included) able to distinguish that from real delivery. 'failed' remains definitive for unsigned builds and dialog-level denials. Co-authored-by: Orca <help@stably.ai> * feat: real macOS notification permission readout via native helper Electron has no API for UNUserNotificationCenter authorization, and every observable fallback lies: scheduling succeeds (and getHistory lists the notification) even while macOS silently swallows display because the permission dialog is unanswered or notifications were toggled off in System Settings. The onboarding card therefore showed 'enabled' after the user disabled notifications. Adds native/notification-status-macos: a tiny Swift binary that prints the app's real authorization status. It runs from inside the app bundle (NSBundle resolves the bundle by walking up from the executable) and embeds the app's CFBundleIdentifier in a __TEXT,__info_plist section so every codesign --force pass — electron-builder's signing or the dev runner's ad-hoc deep sign — derives the identifier macOS keys notification records to. Spawning it from the app returns authorized / denied / not-determined exactly matching System Settings. notifications:probeDelivery now prefers this readout (authoritative, silent), firing at most one dialog-trigger probe per session while the decision is pending, and falls back to the previous delivery-probe heuristics when the helper is unavailable. The card polls the readout silently in every state, so toggling Allow notifications in System Settings flips the card within a poll — both directions, verified live. Test notifications also consult the readout so 'delivered' is no longer claimed for swallowed notifications. Packaged builds ship the helper via extraResources and sign it in afterPack like the computer-use helper; dev copies compile it on demand (swiftc, non-fatal when missing) with the shared dev bundle id. Co-authored-by: Orca <help@stably.ai> * feat: in-app fallback for swallowed notifications + permission card in Settings - Dispatch now consults the authorization readout before creating a native notification: when macOS would silently swallow it (denied or prompt unanswered) it returns reason 'blocked-by-system' instead of piling invisible notifications into Notification Center. The terminal notification path surfaces that as a once-per-session in-app toast with an Open System Settings action. Mobile fan-out is unaffected. - Settings > Notifications now shows the same live permission card as onboarding (moved to components/notifications/), polling the readout so System Settings changes reflect within seconds, and the test button updates it inline. - Test sends that are blocked at the OS level now show the settings-pointing failure toast instead of a generic error. Co-authored-by: Orca <help@stably.ai> * fix: hide macOS permission card while Orca notifications are disabled A green 'Notifications are enabled' card next to a disabled Enable Notifications toggle read as a contradiction — the card now renders (and the readout polls) only while Orca's own notifications setting is on. Also single-flights the authorization helper so simultaneous agent completions share one readout process. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai>
496 lines
19 KiB
JavaScript
496 lines
19 KiB
JavaScript
const { chmodSync, existsSync, readdirSync } = require('node:fs')
|
|
const { execFileSync } = require('node:child_process')
|
|
const { join, resolve } = require('node:path')
|
|
const electronBuilderNativeRebuild = require('./scripts/electron-builder-native-rebuild.cjs')
|
|
const { verifyPackagedDaemonEntryBoots } = require('./scripts/verify-packaged-daemon-entry.cjs')
|
|
const {
|
|
createPackagedRuntimeNodeModuleResources,
|
|
prunePackagedRuntimeNodeModules,
|
|
verifyPackagedMainRuntimeDeps
|
|
} = require('./packaged-runtime-node-modules.cjs')
|
|
|
|
const isMacRelease = process.env.ORCA_MAC_RELEASE === '1'
|
|
const isLinuxArm64Release = process.env.ORCA_LINUX_ARM64_RELEASE === '1'
|
|
const featureWallResources = {
|
|
from: 'resources/onboarding/feature-wall',
|
|
to: 'onboarding/feature-wall'
|
|
}
|
|
// Why: SSH relay deploy resolves bundles from process.resourcesPath in packaged
|
|
// apps. Keeping relay assets as extraResources makes them real directories
|
|
// instead of paths hidden inside app.asar.
|
|
const relayExtraResource = {
|
|
from: 'out/relay',
|
|
to: 'relay'
|
|
}
|
|
// Why: the main bundle, packaged CLI, SSH paths, and speech worker all execute
|
|
// from package directories where pnpm's symlink farm is absent. Copy the exact
|
|
// runtime dependency closure to Resources/node_modules so bare require() calls
|
|
// do not fall through to a developer checkout's node_modules.
|
|
const packagedRuntimeNodeModuleResources = createPackagedRuntimeNodeModuleResources()
|
|
|
|
const commonExtraResources = [relayExtraResource, ...packagedRuntimeNodeModuleResources]
|
|
const macSpeechNativeResource = {
|
|
from: 'node_modules/sherpa-onnx-darwin-${arch}',
|
|
to: 'node_modules/sherpa-onnx-darwin-${arch}'
|
|
}
|
|
const linuxSpeechNativeResource = {
|
|
from: 'node_modules/sherpa-onnx-linux-${arch}',
|
|
to: 'node_modules/sherpa-onnx-linux-${arch}'
|
|
}
|
|
const winSpeechNativeResource = {
|
|
from: 'node_modules/sherpa-onnx-win-x64',
|
|
to: 'node_modules/sherpa-onnx-win-x64'
|
|
}
|
|
|
|
/** @type {import('electron-builder').Configuration} */
|
|
module.exports = {
|
|
appId: 'com.stablyai.orca',
|
|
productName: 'Orca',
|
|
directories: {
|
|
buildResources: 'resources/build'
|
|
},
|
|
files: [
|
|
'!**/.vscode/*',
|
|
// Why: these repo-only inputs are either bundled into out/ or copied via
|
|
// extraResources. Shipping them in app.asar bloats the desktop bundle.
|
|
'!src{,/**/*}',
|
|
'!config{,/**/*}',
|
|
'!docs{,/**/*}',
|
|
'!mobile{,/**/*}',
|
|
'!native{,/**/*}',
|
|
'!skills{,/**/*}',
|
|
'!tests{,/**/*}',
|
|
'!Casks{,/**/*}',
|
|
'!{AGENTS.md,CLAUDE.md,DEVELOPING.md,bundle-size-progress.md}',
|
|
'!out/**/*.test.js',
|
|
'!electron.vite.config.{js,ts,mjs,cjs}',
|
|
'!{.eslintcache,eslint.config.mjs,.prettierignore,.prettierrc.yaml,CHANGELOG.md,README.md}',
|
|
'!{.env,.env.*,.npmrc,pnpm-lock.yaml}',
|
|
'!tsconfig.json',
|
|
// Why: feature-wall media is copied via extraResources so runtime can read
|
|
// it from process.resourcesPath; exclude the source copy from app.asar.
|
|
'!resources/onboarding/feature-wall/**'
|
|
],
|
|
// Why: the CLI entry-point lives in out/cli/ but imports shared modules
|
|
// from out/shared/ and local hook mutators from out/main/. These paths must be
|
|
// unpacked so that Node's require() can resolve the cross-directory imports
|
|
// when the CLI runs outside the asar archive.
|
|
// Why: daemon-entry.js is forked as a separate Node.js process and must be
|
|
// accessible on disk (not inside the asar archive) for child_process.fork().
|
|
// Why: the CLI is compiled by tsc (not bundled), so its runtime imports
|
|
// resolve at runtime via Node's normal module lookup. The shim launches
|
|
// the CLI with ELECTRON_RUN_AS_NODE, which bypasses Electron's asar
|
|
// integration — dependencies inside the asar archive are invisible to
|
|
// require(). Unpack CLI runtime deps so they resolve from
|
|
// app.asar.unpacked/node_modules/.
|
|
// Why: remote runtime connections use WebSocket + E2EE from the packaged CLI
|
|
// before the GUI process starts, so those deps need the same treatment.
|
|
// Why: out/package.json pins compiled output to CommonJS so parent
|
|
// package.json files with type=module cannot change the packaged CLI loader.
|
|
// Why: sherpa-onnx native bindings (platform-specific subpackages) must be
|
|
// unpacked because they ship .node addons + .dylib/.so files that cannot be
|
|
// dlopen()'d from inside the asar archive.
|
|
asarUnpack: [
|
|
'out/package.json',
|
|
'out/cli/**',
|
|
'out/shared/**',
|
|
'out/main/agent-hooks/**',
|
|
'out/main/antigravity/**',
|
|
'out/main/claude/**',
|
|
'out/main/codex/**',
|
|
'out/main/copilot/**',
|
|
'out/main/cursor/**',
|
|
'out/main/droid/**',
|
|
'out/main/gemini/**',
|
|
'out/main/grok/**',
|
|
'out/main/hermes/**',
|
|
'out/main/win32-utils.js',
|
|
'out/main/daemon-entry.js',
|
|
'out/main/computer-sidecar.js',
|
|
'out/main/parcel-watcher-process-entry.js',
|
|
'out/main/chunks/**',
|
|
'resources/**',
|
|
'node_modules/ws/**',
|
|
'node_modules/tweetnacl/**',
|
|
'node_modules/zod/**',
|
|
'node_modules/yaml/**',
|
|
'node_modules/sherpa-onnx*/**'
|
|
],
|
|
afterPack: async (context) => {
|
|
const resourcesDir =
|
|
context.electronPlatformName === 'darwin'
|
|
? join(
|
|
context.appOutDir,
|
|
`${context.packager.appInfo.productFilename}.app`,
|
|
'Contents',
|
|
'Resources'
|
|
)
|
|
: join(context.appOutDir, 'resources')
|
|
if (!existsSync(resourcesDir)) {
|
|
return
|
|
}
|
|
prunePackagedRuntimeNodeModules(resourcesDir, context.electronPlatformName, context.arch)
|
|
verifyPackagedMainRuntimeDeps(resourcesDir)
|
|
// Why: boot the packaged daemon-entry under plain Node, but only for the
|
|
// slice matching the packaging host's arch — daemon-entry.js is JS, yet it
|
|
// require()s the native (N-API) node-pty for the TARGET arch, which the host
|
|
// Node cannot load cross-arch. `Arch` enum: ia32=0, x64=1, armv7l=2,
|
|
// arm64=3, universal=4 (universal contains the host slice, so run it).
|
|
const archEnumByNodeArch = { ia32: 0, x64: 1, armv7l: 2, arm64: 3 }
|
|
const hostArchEnum = archEnumByNodeArch[process.arch]
|
|
if (context.arch === hostArchEnum || context.arch === 4) {
|
|
verifyPackagedDaemonEntryBoots(resourcesDir)
|
|
} else {
|
|
console.log(
|
|
`[verify-packaged-daemon-entry] skipped cross-arch slice (target ${context.arch}, host ${process.arch})`
|
|
)
|
|
}
|
|
chmodUnixCliLaunchers(resourcesDir, context.electronPlatformName)
|
|
chmodMacServeSimHelpers(resourcesDir, context.electronPlatformName)
|
|
for (const filename of readdirSync(resourcesDir)) {
|
|
if (!filename.startsWith('agent-browser-')) {
|
|
continue
|
|
}
|
|
// Why: the upstream package has inconsistent executable bits across
|
|
// platform binaries (notably darwin-x64). child_process.execFile needs
|
|
// the copied binary to be executable in packaged apps.
|
|
chmodSync(join(resourcesDir, filename), 0o755)
|
|
}
|
|
if (context.electronPlatformName === 'darwin') {
|
|
await signMacComputerUseHelper(join(resourcesDir, 'Orca Computer Use.app'), context.packager)
|
|
await signMacNotificationStatusHelper(
|
|
join(resourcesDir, 'orca-notification-status'),
|
|
context.packager
|
|
)
|
|
}
|
|
},
|
|
win: {
|
|
executableName: 'Orca',
|
|
// Why: Windows installers are signed after electron-builder packaging by
|
|
// SignPath, so the packager cannot infer the updater publisherName.
|
|
signtoolOptions: {
|
|
publisherName: 'SignPath Foundation'
|
|
},
|
|
extraResources: [
|
|
...commonExtraResources,
|
|
winSpeechNativeResource,
|
|
{
|
|
from: 'resources/win32/bin/orca.cmd',
|
|
to: 'bin/orca.cmd'
|
|
},
|
|
{
|
|
from: 'node_modules/agent-browser/bin/agent-browser-win32-x64.exe',
|
|
to: 'agent-browser-win32-x64.exe'
|
|
},
|
|
{
|
|
from: 'native/computer-use-windows/runtime.ps1',
|
|
to: 'computer-use-windows/runtime.ps1'
|
|
},
|
|
featureWallResources
|
|
]
|
|
},
|
|
nsis: {
|
|
artifactName: 'orca-windows-setup.${ext}',
|
|
shortcutName: '${productName}',
|
|
uninstallDisplayName: '${productName}',
|
|
createDesktopShortcut: 'always',
|
|
// Why: on a real uninstall, stop and remove the relocated terminal daemon
|
|
// (which lives outside the install dir under LOCALAPPDATA by design). Guarded
|
|
// by ${isUpdated} inside so it never runs during an update's uninstallOldVersion.
|
|
include: resolve(__dirname, 'nsis', 'daemon-host-uninstall.nsh')
|
|
},
|
|
mac: {
|
|
icon: 'resources/build/icon.icns',
|
|
entitlements: 'resources/build/entitlements.mac.plist',
|
|
entitlementsInherit: 'resources/build/entitlements.mac.plist',
|
|
extendInfo: {
|
|
NSAppleEventsUsageDescription:
|
|
'Orca allows terminal-launched developer tools to automate local apps when you request it.',
|
|
NSBluetoothAlwaysUsageDescription:
|
|
'Orca allows terminal-launched developer tools to access Bluetooth devices when you request it.',
|
|
NSBluetoothPeripheralUsageDescription:
|
|
'Orca allows terminal-launched developer tools to access Bluetooth devices when you request it.',
|
|
NSCameraUsageDescription: "Application requests access to the device's camera.",
|
|
NSLocationUsageDescription:
|
|
'Orca allows terminal-launched developer tools to access location when you request it.',
|
|
NSLocalNetworkUsageDescription:
|
|
'Orca allows terminal-launched developer tools to discover and connect to local development servers when you request it.',
|
|
NSMicrophoneUsageDescription: "Application requests access to the device's microphone.",
|
|
NSAudioCaptureUsageDescription:
|
|
'Orca allows terminal-launched developer tools to capture desktop audio when you request it.',
|
|
NSBonjourServices: ['_http._tcp', '_https._tcp'],
|
|
NSDocumentsFolderUsageDescription:
|
|
"Application requests access to the user's Documents folder.",
|
|
NSDownloadsFolderUsageDescription:
|
|
"Application requests access to the user's Downloads folder."
|
|
},
|
|
// Why: local macOS validation builds should launch without Apple release
|
|
// credentials. Hardened runtime + notarization stay enabled only on the
|
|
// explicit release path so production artifacts remain strict while dev
|
|
// artifacts do not fail with broken ad-hoc launch behavior.
|
|
hardenedRuntime: isMacRelease,
|
|
notarize: isMacRelease,
|
|
extraResources: [
|
|
...commonExtraResources,
|
|
macSpeechNativeResource,
|
|
{
|
|
from: 'resources/darwin/bin/orca',
|
|
to: 'bin/orca'
|
|
},
|
|
{
|
|
from: 'node_modules/agent-browser/bin/agent-browser-darwin-${arch}',
|
|
to: 'agent-browser-darwin-${arch}'
|
|
},
|
|
// Why: serve-sim resolves its helper binary and camera assets relative
|
|
// to dist/serve-sim.js, so the whole package must be a real resource dir.
|
|
{
|
|
from: 'node_modules/serve-sim',
|
|
to: 'serve-sim'
|
|
},
|
|
{
|
|
from: 'native/computer-use-macos/.build/release/Orca Computer Use.app',
|
|
to: 'Orca Computer Use.app'
|
|
},
|
|
{
|
|
from: 'native/notification-status-macos/.build/release/orca-notification-status',
|
|
to: 'orca-notification-status'
|
|
},
|
|
featureWallResources
|
|
],
|
|
target: [
|
|
{
|
|
target: 'dmg',
|
|
arch: ['x64', 'arm64']
|
|
},
|
|
{
|
|
target: 'zip',
|
|
arch: ['x64', 'arm64']
|
|
}
|
|
]
|
|
},
|
|
// Why: release builds should fail if signing is unavailable instead of
|
|
// silently downgrading to ad-hoc artifacts that look shippable in CI logs.
|
|
forceCodeSigning: isMacRelease,
|
|
dmg: {
|
|
artifactName: 'orca-macos-${arch}.${ext}'
|
|
},
|
|
linux: {
|
|
// Why: Ubuntu desktop ships GNOME Orca as the `orca` package and /usr/bin/orca.
|
|
// The Linux installer should not claim those system package/file names.
|
|
executableName: 'orca-ide',
|
|
// Why: the icns source lets electron-builder emit standard hicolor PNG
|
|
// sizes; a single 1024px PNG is ignored by some Linux docks/launchers.
|
|
icon: 'resources/build/icon.icns',
|
|
desktop: {
|
|
entry: {
|
|
// Why: Electron reports WM_CLASS=orca for the visible Linux window;
|
|
// GNOME docks need an exact match to group it with orca-ide.desktop.
|
|
StartupWMClass: 'orca'
|
|
}
|
|
},
|
|
extraResources: [
|
|
...commonExtraResources,
|
|
linuxSpeechNativeResource,
|
|
{
|
|
from: 'resources/linux/bin/orca-ide',
|
|
to: 'bin/orca-ide'
|
|
},
|
|
{
|
|
from: 'node_modules/agent-browser/bin/agent-browser-linux-${arch}',
|
|
to: 'agent-browser-linux-${arch}'
|
|
},
|
|
{
|
|
from: 'native/computer-use-linux/runtime.py',
|
|
to: 'computer-use-linux/runtime.py'
|
|
},
|
|
featureWallResources
|
|
],
|
|
target: ['AppImage', 'deb'],
|
|
maintainer: 'stablyai',
|
|
category: 'Utility'
|
|
},
|
|
appImage: {
|
|
artifactName: isLinuxArm64Release ? 'orca-linux-arm64.${ext}' : 'orca-linux.${ext}'
|
|
},
|
|
deb: {
|
|
packageName: 'orca-ide',
|
|
artifactName: 'orca-ide_${version}_${arch}.${ext}',
|
|
// Why: xvfb lets the bundled `orca serve` CLI run browser panes on a headless
|
|
// Linux host — Chromium needs a display server even for offscreen rendering,
|
|
// and serve starts Xvfb itself when present (see ensure-virtual-display.ts).
|
|
depends: [
|
|
'python3',
|
|
'python3-gi',
|
|
'gir1.2-atspi-2.0',
|
|
'at-spi2-core',
|
|
'xdotool',
|
|
'xclip',
|
|
'xvfb'
|
|
],
|
|
// Why: symlink the bundled CLI onto PATH at install time so `orca-ide serve`
|
|
// works on a headless host. The in-app CLI registration (CliInstaller) is
|
|
// GUI-triggered and can never run on a server, so without this the CLI is
|
|
// unreachable from the shell on exactly the hosts that need it.
|
|
afterInstall: 'resources/linux/packaging/after-install.sh',
|
|
afterRemove: 'resources/linux/packaging/after-remove.sh'
|
|
},
|
|
rpm: {
|
|
packageName: 'orca-ide',
|
|
artifactName: 'orca-ide-${version}.${arch}.${ext}',
|
|
// Why: see deb depends. RPM distros ship Xvfb as xorg-x11-server-Xvfb (there
|
|
// is no `xvfb` package), so the name differs from the deb here.
|
|
depends: [
|
|
'python3',
|
|
'python3-gobject',
|
|
'at-spi2-core',
|
|
'xdotool',
|
|
'xclip',
|
|
'xorg-x11-server-Xvfb'
|
|
],
|
|
// Why: same headless CLI-on-PATH registration as deb; rpm runs these via fpm.
|
|
afterInstall: 'resources/linux/packaging/after-install.sh',
|
|
afterRemove: 'resources/linux/packaging/after-remove.sh'
|
|
},
|
|
beforeBuild: electronBuilderNativeRebuild,
|
|
// Why: must be true so that electron-builder rebuilds native modules
|
|
// (node-pty) for each target architecture when producing dual-arch macOS
|
|
// builds (x64 + arm64). With npmRebuild disabled, CI on an arm64 runner
|
|
// packages arm64 binaries into the x64 DMG, causing "posix_spawnp failed"
|
|
// on Intel Macs. The beforeBuild hook performs Orca's targeted rebuild and
|
|
// returns false so electron-builder does not rebuild optional cpu-features.
|
|
npmRebuild: true,
|
|
publish: {
|
|
provider: 'github',
|
|
owner: 'stablyai',
|
|
repo: 'orca',
|
|
releaseType: 'release'
|
|
}
|
|
}
|
|
|
|
function chmodUnixCliLaunchers(resourcesDir, electronPlatformName) {
|
|
if (electronPlatformName === 'win32') {
|
|
return
|
|
}
|
|
for (const launcherName of ['orca', 'orca-ide']) {
|
|
const launcherPath = join(resourcesDir, 'bin', launcherName)
|
|
if (!existsSync(launcherPath)) {
|
|
continue
|
|
}
|
|
// Why: packaged Unix installs expose these extraResources as public shell
|
|
// commands, and source/packager mode drift must not ship a non-executable CLI.
|
|
chmodSync(launcherPath, 0o755)
|
|
}
|
|
}
|
|
|
|
function chmodMacServeSimHelpers(resourcesDir, electronPlatformName) {
|
|
if (electronPlatformName !== 'darwin') {
|
|
return
|
|
}
|
|
const helperPaths = [
|
|
join(resourcesDir, 'serve-sim', 'bin', 'serve-sim-bin'),
|
|
join(resourcesDir, 'serve-sim', 'dist', 'simcam', 'serve-sim-camera-helper'),
|
|
join(resourcesDir, 'node_modules', 'serve-sim', 'bin', 'serve-sim-bin'),
|
|
join(resourcesDir, 'node_modules', 'serve-sim', 'dist', 'simcam', 'serve-sim-camera-helper')
|
|
]
|
|
for (const helperPath of helperPaths) {
|
|
if (existsSync(helperPath)) {
|
|
chmodSync(helperPath, 0o755)
|
|
}
|
|
}
|
|
}
|
|
|
|
async function signMacComputerUseHelper(helperAppPath, packager) {
|
|
if (!existsSync(helperAppPath)) {
|
|
if (isMacRelease) {
|
|
throw new Error(`Missing Orca Computer Use helper app at ${helperAppPath}`)
|
|
}
|
|
return
|
|
}
|
|
const codeSigningInfo =
|
|
isMacRelease && process.env.CSC_LINK && packager?.codeSigningInfo?.value
|
|
? await packager.codeSigningInfo.value
|
|
: null
|
|
const identity =
|
|
process.env.ORCA_COMPUTER_MACOS_SIGN_IDENTITY ??
|
|
process.env.CSC_NAME ??
|
|
findInstalledMacSigningIdentity(codeSigningInfo?.keychainFile) ??
|
|
(isMacRelease ? null : '-')
|
|
if (!identity) {
|
|
throw new Error('Missing signing identity for Orca Computer Use helper app')
|
|
}
|
|
// Why: TCC grants attach to this nested app's code identity. Sign it before
|
|
// the outer Orca.app is sealed so production builds preserve that identity.
|
|
execFileSync('codesign', codesignArgs(identity, helperAppPath), { stdio: 'inherit' })
|
|
execFileSync('codesign', ['--verify', '--deep', '--strict', helperAppPath], {
|
|
stdio: 'inherit'
|
|
})
|
|
}
|
|
|
|
async function signMacNotificationStatusHelper(helperPath, packager) {
|
|
if (!existsSync(helperPath)) {
|
|
if (isMacRelease) {
|
|
throw new Error(`Missing orca-notification-status helper at ${helperPath}`)
|
|
}
|
|
return
|
|
}
|
|
const codeSigningInfo =
|
|
isMacRelease && process.env.CSC_LINK && packager?.codeSigningInfo?.value
|
|
? await packager.codeSigningInfo.value
|
|
: null
|
|
const identity =
|
|
process.env.CSC_NAME ??
|
|
findInstalledMacSigningIdentity(codeSigningInfo?.keychainFile) ??
|
|
(isMacRelease ? null : '-')
|
|
if (!identity) {
|
|
throw new Error('Missing signing identity for orca-notification-status helper')
|
|
}
|
|
// Why: macOS keys notification records to the code-signing identifier; the
|
|
// binary embeds the app's CFBundleIdentifier in __TEXT,__info_plist so this
|
|
// (and any later) `codesign --force` derives the correct identifier. Sign
|
|
// before the outer Orca.app is sealed, like the computer-use helper.
|
|
const args = ['--force', '--sign', identity]
|
|
if (isMacRelease) {
|
|
args.push('--options', 'runtime', '--timestamp')
|
|
}
|
|
args.push(helperPath)
|
|
execFileSync('codesign', args, { stdio: 'inherit' })
|
|
execFileSync('codesign', ['--verify', '--strict', helperPath], { stdio: 'inherit' })
|
|
}
|
|
|
|
function codesignArgs(identity, targetPath) {
|
|
const args = ['--force', '--deep', '--sign', identity]
|
|
if (isMacRelease) {
|
|
args.push(
|
|
'--options',
|
|
'runtime',
|
|
'--timestamp',
|
|
'--entitlements',
|
|
resolve(__dirname, '../resources/build/entitlements.computer-use.mac.plist')
|
|
)
|
|
}
|
|
args.push(targetPath)
|
|
return args
|
|
}
|
|
|
|
function findInstalledMacSigningIdentity(keychainFile) {
|
|
try {
|
|
const output = execFileSync(
|
|
'security',
|
|
['find-identity', '-v', '-p', 'codesigning', ...(keychainFile ? [keychainFile] : [])],
|
|
{
|
|
encoding: 'utf8'
|
|
}
|
|
)
|
|
const releaseMatch =
|
|
output.match(/"([^"]*Developer ID Application:[^"]+)"/) ??
|
|
output.match(/"([^"]*Apple Distribution:[^"]+)"/)
|
|
if (releaseMatch?.[1]) {
|
|
return releaseMatch[1]
|
|
}
|
|
if (!isMacRelease) {
|
|
return output.match(/"([^"]*Apple Development:[^"]+)"/)?.[1] ?? null
|
|
}
|
|
} catch {}
|
|
return null
|
|
}
|