Files
orca/config/scripts/macos-terminal-host-bundle.cjs
T
Jinwoo Hong a5f7da4928 revert(daemon): ship official Node in the macOS terminal host again, keep stall telemetry (#33688)
Reverts #29133 except its daemon async-I/O stall telemetry, so we can measure how often stalls happen on official Node before committing to a patched runtime. The macOS terminal host goes back to the official Node 24.22.1 download and the daemon protocol back to v51; no release or hourly build published v52.
2026-10-25 20:07:30 -04:00

426 lines
15 KiB
JavaScript

// The signed `com.stablyai.orca` helper that runs the terminal daemon on the pinned Node.
// See docs/reference/macos-daemon-tcc-attribution.md.
const { spawnSync } = require('node:child_process')
const {
chmodSync,
cpSync,
existsSync,
mkdirSync,
readFileSync,
realpathSync,
rmSync,
statSync,
writeFileSync
} = require('node:fs')
const { builtinModules } = require('node:module')
const { basename, dirname, join, relative, resolve, sep } = require('node:path')
const { pathToFileURL } = require('node:url')
const MAC_TERMINAL_HOST_NAME = 'Orca Terminal Host'
const MAC_TERMINAL_HOST_EXECUTABLE = 'orca-terminal-host'
const macTerminalHostSignIgnore = [`/Contents/Helpers/${MAC_TERMINAL_HOST_NAME}\\.app(/|$)`]
const NODE_PTY_FILES = [
'package.json',
'lib',
join('build', 'Release', 'pty.node'),
join('build', 'Release', 'spawn-helper')
]
// A helper that claimed any of these could take `orca:` links or documents from Orca.app.
const FORBIDDEN_INFO_KEYS = [
'CFBundleURLTypes',
'CFBundleDocumentTypes',
'UTExportedTypeDeclarations',
'UTImportedTypeDeclarations',
'NSServices'
]
// node-pty ships as the slice subset above.
const PACKAGES_NOT_COPIED = new Set(['node-pty'])
const LOAD_CALL = /\b(?:require|import)\(/g
// The whole argument must be one plain string literal; anything else is a computed name.
const LITERAL_ARGUMENT = /^\s*([`'"])([^`'"$]+)\1\s*\)/
const LIPO_ARCH_TO_NODE_ARCH = { x86_64: 'x64', arm64: 'arm64' }
function macTerminalHostPaths(appPath) {
const bundle = join(appPath, 'Contents', 'Helpers', `${MAC_TERMINAL_HOST_NAME}.app`)
const contents = join(bundle, 'Contents')
const daemon = join(contents, 'Resources', 'daemon')
const ptyRelease = join(daemon, 'node_modules', 'node-pty', 'build', 'Release')
return {
bundle,
infoPlist: join(contents, 'Info.plist'),
executable: join(contents, 'MacOS', MAC_TERMINAL_HOST_EXECUTABLE),
icon: join(contents, 'Resources', 'icon.icns'),
daemon,
entry: join(daemon, 'out', 'main', 'daemon-entry.js'),
outPackageJson: join(daemon, 'out', 'package.json'),
nodePty: join(daemon, 'node_modules', 'node-pty'),
ptyNode: join(ptyRelease, 'pty.node'),
spawnHelper: join(ptyRelease, 'spawn-helper')
}
}
function packageNameOf(specifier) {
const parts = specifier.split('/')
return specifier.startsWith('@') ? parts.slice(0, 2).join('/') : parts[0]
}
/** The file a relative load names, as Node resolves it; null for a non-JS target. */
function resolveRelativeLoad(fromFile, specifier) {
const base = resolve(dirname(fromFile), specifier)
for (const candidate of [base, `${base}.js`, join(base, 'index.js')]) {
if (existsSync(candidate) && statSync(candidate).isFile()) {
return candidate.endsWith('.js') ? candidate : null
}
}
throw new Error(`[macos-terminal-host] ${fromFile} loads ${specifier}, which does not exist`)
}
/** Package specifiers the daemon loads, walking its files from daemon-entry.js. */
function daemonBareRequires(outMainDir) {
const builtins = new Set(builtinModules)
const specifiers = new Set()
const visited = new Set()
const pending = [join(outMainDir, 'daemon-entry.js')]
while (pending.length > 0) {
const file = pending.pop()
if (visited.has(file)) {
continue
}
visited.add(file)
const source = readFileSync(file, 'utf8')
for (const call of source.matchAll(LOAD_CALL)) {
const literal = LITERAL_ARGUMENT.exec(source.slice(call.index + call[0].length))
// A computed specifier could load a package this walk cannot see.
if (!literal) {
throw new Error(`[macos-terminal-host] ${file} loads a module by a computed name`)
}
const specifier = literal[2]
if (specifier.startsWith('.')) {
const target = resolveRelativeLoad(file, specifier)
if (target) {
pending.push(target)
}
} else if (!specifier.startsWith('node:') && !builtins.has(packageNameOf(specifier))) {
specifiers.add(specifier)
}
}
}
return [...specifiers].sort()
}
/** Where Node loads package `name` from when required inside `fromDir`, never above `nodeModules`. */
function resolvePackageDir(name, fromDir, nodeModules) {
for (let dir = fromDir; ; dir = dirname(dir)) {
const candidate =
basename(dir) === 'node_modules' ? join(dir, name) : join(dir, 'node_modules', name)
if (existsSync(join(candidate, 'package.json'))) {
return candidate
}
if (!dir.startsWith(`${nodeModules}${sep}`)) {
return null
}
}
}
/** Copies each package the daemon requires, and its dependencies, to the same relative place. */
function copyDaemonPackages(outMainDir, sourceNodeModules, targetNodeModules) {
const pending = daemonBareRequires(outMainDir)
.map(packageNameOf)
.filter((name) => !PACKAGES_NOT_COPIED.has(name))
.map((name) => ({ name, from: sourceNodeModules }))
const copied = new Set()
while (pending.length > 0) {
const { name, from } = pending.pop()
const source = resolvePackageDir(name, from, sourceNodeModules)
if (!source) {
throw new Error(`[macos-terminal-host] the daemon requires ${name}, missing from ${from}`)
}
if (copied.has(source)) {
continue
}
// A package nested inside one already copied came along with it.
if (![...copied].some((parent) => source.startsWith(`${parent}${sep}`))) {
cpSync(source, join(targetNodeModules, relative(sourceNodeModules, source)), {
recursive: true
})
}
copied.add(source)
const manifest = JSON.parse(readFileSync(join(source, 'package.json'), 'utf8'))
pending.push(
...Object.keys(manifest.dependencies ?? {}).map((dep) => ({ name: dep, from: source }))
)
}
}
/** Bare requires that do not resolve to a file inside the helper bundle. */
function findUnresolvedDaemonRequires(paths) {
const outMain = join(paths.daemon, 'out', 'main')
// Resolution returns real paths, so compare against the bundle's real path.
const bundle = realpathSync(paths.bundle)
const problems = []
for (const specifier of daemonBareRequires(outMain)) {
let resolved = null
try {
resolved = require.resolve(specifier, { paths: [outMain] })
} catch {
// Reported below.
}
if (!resolved || relative(bundle, resolved).startsWith(`..${sep}`)) {
problems.push(`${specifier} resolves to ${resolved ?? 'nothing'}, not inside the helper`)
}
}
return problems
}
function readPlist(path) {
return JSON.parse(spawnChecked('/usr/bin/plutil', ['-convert', 'json', '-o', '-', path]).stdout)
}
function writePlist(path, value) {
writeFileSync(path, JSON.stringify(value))
spawnChecked('/usr/bin/plutil', ['-convert', 'xml1', path])
}
function spawnChecked(program, args) {
const result = spawnSync(program, args, { encoding: 'utf8', maxBuffer: 16 * 1024 * 1024 })
if (result.error || result.status !== 0) {
throw new Error(
`[macos-terminal-host] ${program} ${args.join(' ')} failed: ${result.error?.message ?? result.stderr}`
)
}
return result
}
function buildMacTerminalHostInfo({ appInfo, usageDescriptions, minimumOs }) {
return {
...usageDescriptions,
CFBundleIdentifier: appInfo.CFBundleIdentifier,
CFBundleName: 'Orca',
CFBundleDisplayName: 'Orca',
CFBundleExecutable: MAC_TERMINAL_HOST_EXECUTABLE,
CFBundlePackageType: 'APPL',
CFBundleInfoDictionaryVersion: '6.0',
CFBundleIconFile: 'icon.icns',
// Lower than the app's so LaunchServices ranks the helper below Orca.app among duplicates.
CFBundleVersion: '0',
CFBundleShortVersionString: appInfo.CFBundleShortVersionString,
LSMinimumSystemVersion: minimumOs,
LSUIElement: true
}
}
function findMacTerminalHostInfoProblems(info, { appId, usageDescriptions, minimumOs }) {
const problems = []
if (info.CFBundleIdentifier !== appId) {
problems.push(`CFBundleIdentifier is ${info.CFBundleIdentifier}, expected ${appId}`)
}
for (const key of FORBIDDEN_INFO_KEYS) {
if (Object.hasOwn(info, key)) {
problems.push(`declares ${key}`)
}
}
for (const [key, value] of Object.entries(usageDescriptions)) {
if (JSON.stringify(info[key]) !== JSON.stringify(value)) {
problems.push(`${key} differs from the app's`)
}
}
if (info.LSMinimumSystemVersion !== minimumOs) {
problems.push(`LSMinimumSystemVersion is ${info.LSMinimumSystemVersion}, expected ${minimumOs}`)
}
return problems
}
/** `otool -l` prints `minos 13.5` under the LC_BUILD_VERSION load command. */
function parseBuildVersionMinos(otoolOutput) {
const lines = otoolOutput.split('\n')
const command = lines.findIndex((line) => /cmd LC_BUILD_VERSION\s*$/.test(line))
if (command === -1) {
return null
}
for (const line of lines.slice(command + 1)) {
if (/^\s*cmd /.test(line)) {
return null
}
const match = /^\s*minos (\S+)/.exec(line)
if (match) {
return match[1]
}
}
return null
}
function lipoArchsToNodeArchs(lipoOutput) {
return lipoOutput
.trim()
.split(/\s+/)
.filter(Boolean)
.map((arch) => LIPO_ARCH_TO_NODE_ARCH[arch] ?? arch)
}
/** Copies the pinned Node, the daemon JS with its packages, and this slice's node-pty. */
function assembleMacTerminalHost({ appPath, nodeExecutable, iconPath, info }) {
const resourcesDir = join(appPath, 'Contents', 'Resources')
const daemonOut = join(resourcesDir, 'app.asar.unpacked', 'out')
const nodePty = join(resourcesDir, 'node_modules', 'node-pty')
for (const required of [join(daemonOut, 'main', 'daemon-entry.js'), nodePty, nodeExecutable]) {
if (!existsSync(required)) {
throw new Error(`[macos-terminal-host] missing ${required}`)
}
}
const paths = macTerminalHostPaths(appPath)
rmSync(paths.bundle, { recursive: true, force: true })
mkdirSync(join(paths.bundle, 'Contents', 'MacOS'), { recursive: true })
cpSync(nodeExecutable, paths.executable)
chmodSync(paths.executable, 0o755)
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.
cpSync(join(daemonOut, 'package.json'), paths.outPackageJson)
copyDaemonPackages(
join(daemonOut, 'main'),
join(resourcesDir, 'node_modules'),
join(paths.daemon, 'node_modules')
)
for (const file of NODE_PTY_FILES) {
cpSync(join(nodePty, file), join(paths.nodePty, file), { recursive: true })
}
chmodSync(paths.spawnHelper, 0o755)
writePlist(paths.infoPlist, info)
return paths
}
async function loadNodeRuntimePin() {
return import(pathToFileURL(resolve(__dirname, '../../src/shared/node-runtime-pin.ts')).href)
}
async function pinnedDarwinNode(arch) {
const { ensurePinnedNodeExecutable } = await import(
pathToFileURL(resolve(__dirname, 'pinned-node-downloads.mjs')).href
)
return ensurePinnedNodeExecutable({ target: `darwin-${arch}` })
}
/**
* Assembles and signs the helper inside-out. `signCode(path, { entitlements })` signs one path
* with the build's helper identity and verifies it.
*/
async function buildMacTerminalHost({
appPath,
arch,
iconPath,
entitlementsPath,
usageDescriptions,
signCode
}) {
const { NODE_RUNTIME_DARWIN_MINIMUM_OS } = await loadNodeRuntimePin()
const paths = assembleMacTerminalHost({
appPath,
nodeExecutable: await pinnedDarwinNode(arch),
iconPath,
info: buildMacTerminalHostInfo({
appInfo: readPlist(join(appPath, 'Contents', 'Info.plist')),
usageDescriptions,
minimumOs: NODE_RUNTIME_DARWIN_MINIMUM_OS
})
})
await signCode(paths.ptyNode, {})
await signCode(paths.spawnHelper, {})
await signCode(paths.bundle, { entitlements: entitlementsPath })
return paths
}
function readCodesignDetails(bundle) {
const output = spawnChecked('/usr/bin/codesign', ['-dvv', bundle]).stderr
const field = (name) => new RegExp(`^${name}=(.*)$`, 'm').exec(output)?.[1] ?? null
return { identifier: field('Identifier'), teamId: field('TeamIdentifier') }
}
function hasAllowJitEntitlement(bundle) {
const { stdout } = spawnChecked('/usr/bin/codesign', [
'-d',
'--entitlements',
'-',
'--xml',
bundle
])
return /<key>com\.apple\.security\.cs\.allow-jit<\/key>\s*<true\/>/.test(stdout)
}
/** Fails packaging on any layout, plist, OS floor, architecture or signature drift. */
async function verifyMacTerminalHost({ appPath, arch, usageDescriptions, expectedTeamId }) {
const { NODE_RUNTIME_DARWIN_MINIMUM_OS: minimumOs } = await loadNodeRuntimePin()
const paths = macTerminalHostPaths(appPath)
const problems = []
for (const key of ['executable', 'icon', 'entry', 'outPackageJson', 'ptyNode', 'spawnHelper']) {
if (!existsSync(paths[key])) {
problems.push(`missing ${paths[key]}`)
}
}
if (problems.length === 0 && (statSync(paths.spawnHelper).mode & 0o111) === 0) {
problems.push('spawn-helper is not executable')
}
if (problems.length > 0) {
throw new Error(`[macos-terminal-host] ${problems.join('; ')}`)
}
// The build tree's node_modules sits above dist/, so the boot check alone cannot catch these.
problems.push(...findUnresolvedDaemonRequires(paths))
const appId = readPlist(join(appPath, 'Contents', 'Info.plist')).CFBundleIdentifier
problems.push(
...findMacTerminalHostInfoProblems(readPlist(paths.infoPlist), {
appId,
usageDescriptions,
minimumOs
})
)
const minos = parseBuildVersionMinos(
spawnChecked('/usr/bin/otool', ['-l', paths.executable]).stdout
)
if (minos !== minimumOs) {
problems.push(
`the Node binary's minos is ${minos}, but NODE_RUNTIME_DARWIN_MINIMUM_OS is ${minimumOs}`
)
}
for (const binary of [paths.executable, paths.ptyNode, paths.spawnHelper]) {
const archs = lipoArchsToNodeArchs(spawnChecked('/usr/bin/lipo', ['-archs', binary]).stdout)
if (archs.length !== 1 || archs[0] !== arch) {
problems.push(`${binary} has architectures ${archs.join(',')}, expected ${arch}`)
}
}
const { identifier, teamId } = readCodesignDetails(paths.bundle)
if (identifier !== appId) {
problems.push(`signed identifier is ${identifier}, expected ${appId}`)
}
if (expectedTeamId && teamId !== expectedTeamId) {
problems.push(`signed team is ${teamId}, expected ${expectedTeamId}`)
}
if (!hasAllowJitEntitlement(paths.bundle)) {
problems.push('the signature lacks the allow-jit entitlement Node needs')
}
// After the boot check, so a write into the bundle while it ran fails the build.
const verified = spawnSync('/usr/bin/codesign', ['--verify', '--strict', paths.bundle], {
encoding: 'utf8'
})
if (verified.status !== 0) {
problems.push(`codesign --verify --strict failed: ${verified.stderr.trim()}`)
}
if (problems.length > 0) {
throw new Error(`[macos-terminal-host] ${problems.join('; ')}`)
}
console.log(`[macos-terminal-host] OK — ${paths.bundle} (${arch}, macOS ${minimumOs}+)`)
}
module.exports = {
MAC_TERMINAL_HOST_NAME,
assembleMacTerminalHost,
buildMacTerminalHost,
buildMacTerminalHostInfo,
daemonBareRequires,
findMacTerminalHostInfoProblems,
findUnresolvedDaemonRequires,
lipoArchsToNodeArchs,
macTerminalHostPaths,
macTerminalHostSignIgnore,
parseBuildVersionMinos,
verifyMacTerminalHost
}