mirror of
https://github.com/stablyai/orca.git
synced 2026-11-15 10:27:33 +00:00
fix(mac): give the terminal host the app's executable UUID (#42270)
macOS Local Network caches one executable UUID per bundle id, and the terminal host shares the app's bundle id. With a UUID of its own the cache flips between the two, which blocked some connections and prompted again after an update. Give the host exactly the main executable's UUID per CPU type, and check that in packaging and after notarization.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { createRequire } from 'node:module'
|
||||
import { basename, join, resolve } from 'node:path'
|
||||
import { macTerminalHostPaths } from '../../macos/terminal-host-bundle/macos-terminal-host-bundle.cjs'
|
||||
import { assertMacTerminalHostUuid } from '../../macos/terminal-host-bundle/macos-terminal-host-bundle.cjs'
|
||||
import { assertMacExecutableUuid } from './macos-executable-uuid.cjs'
|
||||
|
||||
const ARCHITECTURES = [1, 3] // electron-builder: x64, arm64.
|
||||
@@ -144,7 +144,7 @@ export async function main(args = process.argv.slice(2)) {
|
||||
join(appPath, 'Contents', 'MacOS', basename(appPath, '.app')),
|
||||
config.appId
|
||||
)
|
||||
assertMacExecutableUuid(macTerminalHostPaths(appPath).executable, config.appId)
|
||||
assertMacTerminalHostUuid(appPath, config.appId)
|
||||
},
|
||||
targets: new Map([
|
||||
[
|
||||
|
||||
@@ -74,11 +74,14 @@ function rewriteMachOUuids(buffer, appId, executableName) {
|
||||
})
|
||||
}
|
||||
|
||||
/** Gives the executable its own UUID, then re-signs it ad hoc; real signing replaces that later. */
|
||||
function giveMacExecutableUniqueUuid(executable, appId) {
|
||||
/**
|
||||
* Gives the executable the UUID derived for executable `uuidName` (its own name by default), then
|
||||
* re-signs it ad hoc; real signing replaces that later.
|
||||
*/
|
||||
function giveMacExecutableUniqueUuid(executable, appId, uuidName = basename(executable)) {
|
||||
const name = basename(executable)
|
||||
const buffer = readFileSync(executable)
|
||||
const changes = rewriteMachOUuids(buffer, appId, name)
|
||||
const changes = rewriteMachOUuids(buffer, appId, uuidName)
|
||||
// The edit breaks Electron's linker ad hoc signature and arm64 kills unsigned code, but identity-less builds skip app signing.
|
||||
// Signed outside the bundle: in place, codesign would treat it as the bundle and require its unsigned siblings be signed.
|
||||
const staging = mkdtempSync(join(tmpdir(), 'orca-executable-uuid-'))
|
||||
@@ -98,12 +101,11 @@ function giveMacExecutableUniqueUuid(executable, appId) {
|
||||
}
|
||||
|
||||
/** Fails when a shipped executable lost its own UUID, e.g. the hook was skipped or signing rewrote it. */
|
||||
function assertMacExecutableUuid(executable, appId) {
|
||||
const name = basename(executable)
|
||||
function assertMacExecutableUuid(executable, appId, uuidName = basename(executable)) {
|
||||
const buffer = readFileSync(executable)
|
||||
for (const { cpuType, uuidOffset } of machOUuidSlots(buffer)) {
|
||||
const actual = formatUuid(buffer.subarray(uuidOffset, uuidOffset + 16))
|
||||
const expected = formatUuid(deriveExecutableUuid(appId, name, cpuType))
|
||||
const expected = formatUuid(deriveExecutableUuid(appId, uuidName, cpuType))
|
||||
if (actual !== expected) {
|
||||
throw new Error(
|
||||
`[macos-executable-uuid] ${executable} cpu ${cpuType} has UUID ${actual}, expected ${expected}`
|
||||
|
||||
@@ -43,6 +43,20 @@ const LOAD_CALL = /\b(?:require|import)\(/g
|
||||
const LITERAL_ARGUMENT = /^\s*([`'"])([^`'"$]+)\1\s*\)/
|
||||
const LIPO_ARCH_TO_NODE_ARCH = { x86_64: 'x64', arm64: 'arm64' }
|
||||
|
||||
// electron-builder names the app bundle and its main executable after productFilename.
|
||||
function macAppExecutableName(appPath) {
|
||||
return basename(appPath, '.app')
|
||||
}
|
||||
|
||||
/** Throws unless the helper executable carries the app's main-executable UUID for every CPU type. */
|
||||
function assertMacTerminalHostUuid(appPath, appId) {
|
||||
assertMacExecutableUuid(
|
||||
macTerminalHostPaths(appPath).executable,
|
||||
appId,
|
||||
macAppExecutableName(appPath)
|
||||
)
|
||||
}
|
||||
|
||||
function macTerminalHostPaths(appPath) {
|
||||
const bundle = join(appPath, 'Contents', 'Helpers', `${MAC_TERMINAL_HOST_NAME}.app`)
|
||||
const contents = join(bundle, 'Contents')
|
||||
@@ -284,9 +298,13 @@ function assembleMacTerminalHost({ appPath, nodeExecutable, iconPath, info }) {
|
||||
mkdirSync(join(paths.bundle, 'Contents', 'MacOS'), { recursive: true })
|
||||
cpSync(nodeExecutable, paths.executable)
|
||||
chmodSync(paths.executable, 0o755)
|
||||
// Terminals' responsible process is this helper. Stock Node's UUID is shared by every install of that build
|
||||
// (nvm, Homebrew, other apps), so Local Network privacy could attribute terminals to another program (#21988).
|
||||
giveMacExecutableUniqueUuid(paths.executable, info.CFBundleIdentifier)
|
||||
// Terminals' responsible process is this helper, and stock Node's UUID is shared by every install of that build (#21988).
|
||||
// Local Network caches one UUID per bundle id, so the helper carries the app's: its own would flip that cache.
|
||||
giveMacExecutableUniqueUuid(
|
||||
paths.executable,
|
||||
info.CFBundleIdentifier,
|
||||
macAppExecutableName(appPath)
|
||||
)
|
||||
cpSync(iconPath, paths.icon)
|
||||
cpSync(join(daemonOut, 'main'), join(paths.daemon, 'out', 'main'), { recursive: true })
|
||||
// Pins out/ to CommonJS so a stray parent package.json cannot change the loader.
|
||||
@@ -392,7 +410,7 @@ async function verifyMacTerminalHost({ appPath, arch, usageDescriptions, expecte
|
||||
})
|
||||
)
|
||||
try {
|
||||
assertMacExecutableUuid(paths.executable, appId)
|
||||
assertMacTerminalHostUuid(appPath, appId)
|
||||
} catch (error) {
|
||||
problems.push(error.message)
|
||||
}
|
||||
@@ -436,6 +454,7 @@ async function verifyMacTerminalHost({ appPath, arch, usageDescriptions, expecte
|
||||
module.exports = {
|
||||
MAC_TERMINAL_HOST_NAME,
|
||||
assembleMacTerminalHost,
|
||||
assertMacTerminalHostUuid,
|
||||
buildMacTerminalHost,
|
||||
buildMacTerminalHostInfo,
|
||||
daemonBareRequires,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { copyFileSync, existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { createRequire } from 'node:module'
|
||||
import { tmpdir } from 'node:os'
|
||||
@@ -7,13 +8,11 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
const require = createRequire(new URL('../../macos-terminal-host-bundle.test.mjs', import.meta.url))
|
||||
const {
|
||||
assembleMacTerminalHost,
|
||||
assertMacTerminalHostUuid,
|
||||
daemonBareRequires,
|
||||
findUnresolvedDaemonRequires,
|
||||
parseBuildVersionMinos
|
||||
} = require('./macos/terminal-host-bundle/macos-terminal-host-bundle.cjs')
|
||||
const {
|
||||
assertMacExecutableUuid
|
||||
} = require('./desktop-distribution/macos/macos-executable-uuid.cjs')
|
||||
const INFO = { CFBundleIdentifier: 'com.stablyai.orca' }
|
||||
describe('terminal host binary checks', () => {
|
||||
it('reads minos from the LC_BUILD_VERSION load command only', () => {
|
||||
@@ -67,15 +66,19 @@ describe.skipIf(process.platform !== 'darwin')('terminal host assembly', () => {
|
||||
rmSync(root, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
it("gives the helper executable Orca's own UUID instead of stock Node's", () => {
|
||||
it("gives the helper executable the main executable's UUID instead of stock Node's", () => {
|
||||
const paths = assembleMacTerminalHost({
|
||||
appPath,
|
||||
nodeExecutable: join(root, 'node'),
|
||||
iconPath: join(root, 'icon.icns'),
|
||||
info: INFO
|
||||
})
|
||||
expect(() => assertMacExecutableUuid(join(root, 'node'), INFO.CFBundleIdentifier)).toThrow()
|
||||
expect(() => assertMacExecutableUuid(paths.executable, INFO.CFBundleIdentifier)).not.toThrow()
|
||||
execFileSync(paths.executable)
|
||||
const uuids = execFileSync('dwarfdump', ['--uuid', paths.executable], { encoding: 'utf8' })
|
||||
// Orca.app's main executable carries these (macos-executable-uuid.test.mjs).
|
||||
expect(uuids).toContain('016EE681-3DC3-81C7-A6DA-12DC23B89DD9 (x86_64)')
|
||||
expect(uuids).toContain('E6430410-3B22-8BD6-9677-B18884034A30 (arm64e)')
|
||||
assertMacTerminalHostUuid(appPath, INFO.CFBundleIdentifier)
|
||||
})
|
||||
|
||||
it('copies every package the daemon requires, with its dependencies', () => {
|
||||
|
||||
@@ -124,12 +124,17 @@ Do not register copies explicitly (`lsregister -f` or `LSRegisterURL`).
|
||||
|
||||
Terminal processes' responsible process is the helper, and macOS Local Network privacy maps a
|
||||
responsible process to an app by its main executable's `LC_UUID` (TN3179). Stock Node's UUID is
|
||||
shared by every install of that Node build (nvm, Homebrew, other apps), so with it Local Network
|
||||
attributed Orca terminals to other programs and blocked LAN access while System Settings showed
|
||||
Orca allowed (#21988). Packaging rewrites the helper executable's UUID before signing, the same
|
||||
way as the main executable's (#41676): derived from the app id, `Orca Agents` and the CPU type, so
|
||||
it is unique to Orca and stable across releases. `verifyMacTerminalHost` and the release check
|
||||
fail if it drifts.
|
||||
shared by every install of that Node build, so with it Orca's terminals and any other program on
|
||||
that Node shared Local Network decisions in both directions, and LAN access was blocked while System
|
||||
Settings showed Orca allowed (#21988).
|
||||
|
||||
Local Network also caches one UUID per bundle id, and the helper shares `com.stablyai.orca` with
|
||||
the app. A helper UUID of its own keeps replacing the app's cached UUID and back: in testing it
|
||||
flip-flopped, blocked some terminal and app connections, and prompted again after an update. So
|
||||
packaging gives the helper exactly the main executable's UUID per CPU type (#41676: derived from
|
||||
the app id, the app's executable name and the CPU type), stable across releases.
|
||||
`verifyMacTerminalHost` and the release check fail if it drifts. The designated requirement is
|
||||
unchanged, so file-access grants are unaffected.
|
||||
|
||||
## Rollback
|
||||
|
||||
|
||||
Reference in New Issue
Block a user