refactor(macos): move terminal-host telemetry, Settings status and Helper folder strings to a follow-up

Keeps this change to the update-safe terminal host itself. The follow-up restores:
- the `stable-copy` spawner path class in daemon adoption telemetry;
- the per-terminal-host Full Disk Access row in Settings and its daemon RPC;
- writing folder usage descriptions into the app's Electron Helpers for the fork fallback.

The helper still takes its Desktop, Documents and Downloads descriptions from
`macos-folder-usage-descriptions.cjs`, so that constant stays.
This commit is contained in:
Jinwoo-H
2026-10-10 22:00:03 -04:00
parent 7be6448a88
commit a32018836b
26 changed files with 67 additions and 465 deletions
+1 -8
View File
@@ -1,10 +1,7 @@
const { chmodSync, existsSync, readdirSync, readFileSync, writeFileSync } = require('node:fs')
const { execFileSync } = require('node:child_process')
const { join, resolve } = require('node:path')
const {
MACOS_FOLDER_USAGE_DESCRIPTIONS,
applyMacHelperFolderUsageDescriptions
} = require('./scripts/macos-folder-usage-descriptions.cjs')
const { MACOS_FOLDER_USAGE_DESCRIPTIONS } = require('./scripts/macos-folder-usage-descriptions.cjs')
const electronBuilderNativeRebuild = require('./scripts/electron-builder-native-rebuild.cjs')
const {
assertPackagedDaemonEntryExists,
@@ -398,10 +395,6 @@ module.exports = {
writeFileSync(join(resourcesDir, 'package-type'), 'AppImage')
}
if (context.electronPlatformName === 'darwin') {
applyMacHelperFolderUsageDescriptions(
join(context.appOutDir, `${context.packager.appInfo.productFilename}.app`),
context.packager.appInfo.productFilename
)
const architecture = macSliceArch(context.arch, 'local-build compatibility')
const version = context.packager.appInfo.version
let commit = process.env.ORCA_BUILD_COMMIT || process.env.GITHUB_SHA || 'unknown'
+8 -15
View File
@@ -22108,11 +22108,7 @@
"protection": "partial",
"owner": "terminal-runtime",
"layer": "daemon-native-signing-and-provider-contract",
"surfaces": [
"packaged macOS daemon launch",
"signed bundle retirement",
"Full Disk Access status"
],
"surfaces": ["packaged macOS daemon launch", "signed bundle retirement"],
"platforms": ["macos", "linux", "windows"],
"providers": ["local", "daemon", "ssh", "wsl", "remote-runtime", "mobile"],
"coveredPlatforms": ["macos"],
@@ -22120,9 +22116,9 @@
"coverageNotes": "Signed arm64 macOS evidence; other platforms and hosts keep the existing launcher. See docs/reference/macos-daemon-tcc-attribution.md.",
"motivatingLinks": ["https://github.com/stablyai/orca/issues/13921"],
"invariant": "A packaged macOS daemon runs from a verified signed bundle copy the updater never touches, and the fork fallback still fits the startup gate. Unverified observations never delete code or replace live sessions.",
"oracle": "Deterministic launch, fallback-deadline, retirement and status tests, plus the signed launcher validation recorded in #25848.",
"oracle": "Deterministic launch, fallback-deadline and retirement tests, plus the signed launcher validation recorded in #25848.",
"commands": [
"ORCA_BACKGROUND_LAUNCH=1 pnpm test src/main/daemon/macos-daemon-bundle.test.ts src/main/daemon/macos-daemon-launchd.test.ts src/main/daemon/daemon-init-macos-stable-launch-fallback.test.ts src/main/daemon/macos-daemon-job-state.test.ts src/main/daemon/macos-daemon-bundle-retirement.test.ts src/main/daemon/daemon-full-disk-access-status.test.ts src/main/daemon/daemon-pty-adapter-full-disk-access.test.ts config/scripts/macos-folder-usage-descriptions.test.mjs src/renderer/src/components/settings/DeveloperPermissionsPane.test.tsx"
"ORCA_BACKGROUND_LAUNCH=1 pnpm test src/main/daemon/macos-daemon-bundle.test.ts src/main/daemon/macos-daemon-launchd.test.ts src/main/daemon/daemon-init-macos-stable-launch-fallback.test.ts src/main/daemon/macos-daemon-job-state.test.ts src/main/daemon/macos-daemon-bundle-retirement.test.ts config/scripts/macos-folder-usage-descriptions.test.mjs"
],
"testFiles": [
"src/main/daemon/macos-daemon-bundle.test.ts",
@@ -22130,10 +22126,7 @@
"src/main/daemon/daemon-init-macos-stable-launch-fallback.test.ts",
"src/main/daemon/macos-daemon-job-state.test.ts",
"src/main/daemon/macos-daemon-bundle-retirement.test.ts",
"src/main/daemon/daemon-full-disk-access-status.test.ts",
"src/main/daemon/daemon-pty-adapter-full-disk-access.test.ts",
"config/scripts/macos-folder-usage-descriptions.test.mjs",
"src/renderer/src/components/settings/DeveloperPermissionsPane.test.tsx"
"config/scripts/macos-folder-usage-descriptions.test.mjs"
],
"assertionRefs": [
{
@@ -22158,13 +22151,13 @@
],
"evidenceRuns": [
{
"date": "2026-10-06",
"date": "2026-10-10",
"runner": "local",
"platform": "macos",
"command": "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/main/daemon/macos-daemon-bundle.test.ts src/main/daemon/macos-daemon-launchd.test.ts src/main/daemon/daemon-init-macos-stable-launch-fallback.test.ts src/main/daemon/macos-daemon-job-state.test.ts src/main/daemon/macos-daemon-bundle-retirement.test.ts src/main/daemon/daemon-full-disk-access-status.test.ts src/main/daemon/daemon-pty-adapter-full-disk-access.test.ts config/scripts/macos-folder-usage-descriptions.test.mjs src/renderer/src/components/settings/DeveloperPermissionsPane.test.tsx",
"command": "ORCA_BACKGROUND_LAUNCH=1 pnpm test src/main/daemon/macos-daemon-bundle.test.ts src/main/daemon/macos-daemon-launchd.test.ts src/main/daemon/daemon-init-macos-stable-launch-fallback.test.ts src/main/daemon/macos-daemon-job-state.test.ts src/main/daemon/macos-daemon-bundle-retirement.test.ts config/scripts/macos-folder-usage-descriptions.test.mjs",
"result": "passed",
"durationSeconds": 6.06,
"summary": "All 71 focused launch, fallback-deadline, retirement, status and packaging tests passed."
"durationSeconds": 2.92,
"summary": "All 66 focused launch, fallback-deadline, retirement and packaging tests passed."
}
],
"runtimeBudget": {
@@ -1,7 +1,3 @@
const { readdirSync, writeFileSync } = require('node:fs')
const { join } = require('node:path')
const { execFileSync } = require('node:child_process')
const MACOS_FOLDER_USAGE_DESCRIPTIONS = {
NSDesktopFolderUsageDescription:
"Orca allows terminal tools to access the user's Desktop folder when requested.",
@@ -11,26 +7,4 @@ const MACOS_FOLDER_USAGE_DESCRIPTIONS = {
"Orca allows terminal tools to access the user's Downloads folder when requested."
}
function applyMacHelperFolderUsageDescriptions(appPath, productFilename, run = execFileSync) {
const frameworks = join(appPath, 'Contents', 'Frameworks')
for (const entry of readdirSync(frameworks, { withFileTypes: true })) {
if (
!entry.isDirectory() ||
!entry.name.startsWith(`${productFilename} Helper`) ||
!entry.name.endsWith('.app')
) {
continue
}
const plist = join(frameworks, entry.name, 'Contents', 'Info.plist')
const info = JSON.parse(
run('/usr/bin/plutil', ['-convert', 'json', '-o', '-', plist], { encoding: 'utf8' })
)
if (!info || typeof info !== 'object' || Array.isArray(info)) {
throw new Error('The packaged macOS Helper has an invalid Info.plist')
}
writeFileSync(plist, JSON.stringify({ ...info, ...MACOS_FOLDER_USAGE_DESCRIPTIONS }))
run('/usr/bin/plutil', ['-convert', 'xml1', plist], { encoding: 'utf8' })
}
}
module.exports = { MACOS_FOLDER_USAGE_DESCRIPTIONS, applyMacHelperFolderUsageDescriptions }
module.exports = { MACOS_FOLDER_USAGE_DESCRIPTIONS }
@@ -1,57 +1,11 @@
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { createRequire } from 'node:module'
import { expect, it, vi } from 'vitest'
import { expect, it } from 'vitest'
const require = createRequire(import.meta.url)
const {
MACOS_FOLDER_USAGE_DESCRIPTIONS,
applyMacHelperFolderUsageDescriptions
} = require('./macos-folder-usage-descriptions.cjs')
const { MACOS_FOLDER_USAGE_DESCRIPTIONS } = require('./macos-folder-usage-descriptions.cjs')
const config = require('../electron-builder.config.cjs')
it('uses the same three folder descriptions for the app and every packaged Electron Helper', () => {
it('gives the app the same three folder descriptions as the terminal host helper', () => {
expect(config.mac.extendInfo).toMatchObject(MACOS_FOLDER_USAGE_DESCRIPTIONS)
expect(Object.keys(MACOS_FOLDER_USAGE_DESCRIPTIONS)).toHaveLength(3)
const root = mkdtempSync(join(tmpdir(), 'orca-helper-privacy-'))
try {
const names = [
'Orca Dev Helper.app',
'Orca Dev Helper (GPU).app',
'Orca Dev Helper (Renderer).app',
'Orca Dev Helper (Plugin).app',
'Orca Computer.app'
]
for (const name of names) {
const contents = join(root, 'Contents', 'Frameworks', name, 'Contents')
mkdirSync(contents, { recursive: true })
writeFileSync(
join(contents, 'Info.plist'),
JSON.stringify({ CFBundleIdentifier: name, preserved: true })
)
}
const run = vi.fn((_program, args) =>
args.includes('json') ? readFileSync(args.at(-1), 'utf8') : ''
)
applyMacHelperFolderUsageDescriptions(root, 'Orca Dev', run)
for (const name of names.slice(0, 4)) {
expect(
JSON.parse(
readFileSync(join(root, 'Contents', 'Frameworks', name, 'Contents', 'Info.plist'), 'utf8')
)
).toEqual({ CFBundleIdentifier: name, preserved: true, ...MACOS_FOLDER_USAGE_DESCRIPTIONS })
}
expect(run).toHaveBeenCalledTimes(8)
expect(
JSON.parse(
readFileSync(
join(root, 'Contents', 'Frameworks', names[4], 'Contents', 'Info.plist'),
'utf8'
)
)
).not.toHaveProperty('NSDesktopFolderUsageDescription')
} finally {
rmSync(root, { recursive: true, force: true })
}
})
@@ -33,8 +33,7 @@ So a packaged macOS GUI app starts its daemon from a private copy that the updat
Readiness is the normal authenticated handshake, fenced by the launch nonce.
Node servers, SSH hosts, unpackaged builds, Linux and Windows keep the fork launcher. Adopting an
existing daemon never copies. Telemetry reports a daemon started this way as
`spawner_path_class=stable-copy`.
existing daemon never copies.
Only Developer ID builds exercise the helper path in practice. A local ad hoc build's designated
requirement is a cdhash, which the helper (a different binary) cannot match, so it falls back to the
@@ -93,7 +92,7 @@ one background collection (one at a time per process) that retires a copy only w
Any timeout, permission error, warning or truncated output keeps the copy. Explicit shutdown
unregisters the job, then applies rule 3. Cleanup never reads sockets, tokens or PID records.
Collection matches the directory, not the bundle inside it, so copies made before the
`app.noindex` folder (`runtime-*/Orca.app`) retire, and classify as `stable-copy`, the same way.
`app.noindex` folder (`runtime-*/Orca.app`) retire the same way.
## LaunchServices and Spotlight
@@ -122,9 +121,7 @@ Never revert the cleanup: the old code never deletes copies, and each one keeps
full-app copy from an earlier build, about 628 MiB) after an update removes the bundle it was cloned
from.
## Existing sessions and status
## Existing sessions
Running old daemons keep their terminals; settings reports app and terminal-host Full Disk Access
separately, and a denied terminal is recovered by closing it or restarting the daemon. The packaging
hook also gives the main bundle and every Helper the Desktop, Documents and Downloads descriptions,
because the fork fallback still hosts terminals in `Orca Helper`.
Running old daemons keep their terminals; a denied terminal is recovered by closing it or restarting
the daemon.
@@ -8,7 +8,6 @@ const {
existsSyncMock,
readFileSyncMock,
getVersionMock,
getPathMock,
codeIdentityMock
} = vi.hoisted(() => ({
trackMock: vi.fn(),
@@ -16,7 +15,6 @@ const {
existsSyncMock: vi.fn(() => true),
readFileSyncMock: vi.fn(),
getVersionMock: vi.fn(() => '1.4.191'),
getPathMock: vi.fn((_name: string) => '/Users/alice/Library/Application Support/Orca'),
codeIdentityMock: vi.fn(async () => 'parked')
}))
vi.mock('../telemetry/client', () => ({ track: trackMock }))
@@ -37,7 +35,7 @@ vi.mock('node:os', async (importOriginal) => ({
homedir: () => '/Users/alice'
}))
vi.mock('../../shared/app-environment', () => ({
getAppEnvironment: () => ({ getVersion: getVersionMock, getPath: getPathMock })
getAppEnvironment: () => ({ getVersion: getVersionMock })
}))
import {
@@ -112,31 +110,6 @@ describe('classifyDaemonAdoptionOrigin', () => {
})
expect(codeIdentityMock).toHaveBeenLastCalledWith(undefined)
})
it('reports a daemon started from the private stable copy', async () => {
const copies = '/Users/alice/Library/Application Support/Orca/daemon-host/macos'
const stableCopy = {
...stalePidRecord,
spawnerExecPath: `${copies}/runtime-x1/app.noindex/Orca.app/Contents/MacOS/Orca`
}
expect(await classifyDaemonAdoptionOrigin(stableCopy)).toMatchObject({
spawner_path_class: 'stable-copy'
})
// An adopted daemon from a copy made before the `.noindex` folder.
expect(
await classifyDaemonAdoptionOrigin({
...stalePidRecord,
spawnerExecPath: `${copies}/runtime-Ztpw1G/Orca.app/Contents/MacOS/Orca`
})
).toMatchObject({ spawner_path_class: 'stable-copy' })
expect(getPathMock).toHaveBeenCalledWith('userData')
getPathMock.mockImplementationOnce(() => {
throw new Error('AppEnvironment not initialized')
})
expect(await classifyDaemonAdoptionOrigin(stableCopy)).toMatchObject({
spawner_path_class: 'other'
})
})
})
describe('trackDaemonAdopted', () => {
@@ -4,7 +4,6 @@
import { existsSync } from 'node:fs'
import { homedir } from 'node:os'
import { join } from 'node:path'
import { getAppEnvironment } from '../../shared/app-environment'
import {
classifyDaemonPtyCwd,
@@ -18,7 +17,6 @@ import type { EventProps } from '../../shared/telemetry-events'
import { track } from '../telemetry/client'
import { readDaemonPidRecord } from './daemon-endpoint-incarnation'
import { getDaemonMacCodeIdentity } from './daemon-mac-code-identity'
import { getMacDaemonBundleRoot } from './macos-daemon-bundle'
import { enumerateDirectoryOnce } from './directory-enumeration-probe'
import type { ParsedDaemonPid } from './daemon-pid-file-parse'
import type { MacDaemonTccAttributionHealth } from './daemon-tcc-attribution'
@@ -33,14 +31,6 @@ export type DaemonAdoptionOrigin = Pick<
'app_version_match' | 'code_identity' | 'spawner_path_class'
>
function stableCopyPrefix(): string | null {
try {
return join(getMacDaemonBundleRoot(getAppEnvironment().getPath('userData')), 'runtime-')
} catch {
return null
}
}
/** Classifies the adopted daemon's pid record against the running app; enum-only by construction. */
export async function classifyDaemonAdoptionOrigin(
pidRecord: ParsedDaemonPid | null
@@ -52,8 +42,7 @@ export async function classifyDaemonAdoptionOrigin(
: 'different'
const spawnerPathClass: DaemonSpawnerPathClass = classifyDaemonSpawnerPath(
pidRecord?.spawnerExecPath ?? null,
existsSync,
stableCopyPrefix()
existsSync
)
return {
app_version_match: appVersionMatch,
@@ -1,57 +0,0 @@
import { beforeEach, expect, it, vi } from 'vitest'
import type { DeveloperPermissionStatus } from '../../shared/developer-permissions-types'
const { state, current, legacy, DegradedProvider, localStatus } = vi.hoisted(() => ({
state: { installed: true, degraded: false },
DegradedProvider: class {},
localStatus: vi.fn<() => Promise<DeveloperPermissionStatus>>(),
current: { getFullDiskAccessStatus: vi.fn<() => Promise<DeveloperPermissionStatus>>() },
legacy: { getFullDiskAccessStatus: vi.fn<() => Promise<DeveloperPermissionStatus>>() }
}))
vi.mock('./daemon-provider-state', () => ({
getDaemonProvider: () => (state.installed ? (state.degraded ? new DegradedProvider() : {}) : null)
}))
vi.mock('./degraded-daemon-pty-provider', () => ({ DegradedDaemonPtyProvider: DegradedProvider }))
vi.mock('../macos-full-disk-access-status', () => ({ getMacosFullDiskAccessStatus: localStatus }))
vi.mock('./daemon-provider-routing', () => ({
getCurrentDaemonAdapter: () => current,
getLegacyDaemonAdapters: () => [legacy]
}))
import { getTerminalHostsFullDiskAccessStatus } from './daemon-full-disk-access-status'
beforeEach(() => {
vi.spyOn(process, 'platform', 'get').mockReturnValue('darwin')
state.installed = true
state.degraded = false
localStatus.mockReset().mockResolvedValue('denied')
current.getFullDiskAccessStatus.mockReset().mockResolvedValue('granted')
legacy.getFullDiskAccessStatus.mockReset().mockResolvedValue('granted')
})
it.each(['granted', 'denied', 'unknown'] as const)(
'includes the older generation when it reports %s',
async (status) => {
legacy.getFullDiskAccessStatus.mockResolvedValue(status)
expect(await getTerminalHostsFullDiskAccessStatus()).toBe(status)
expect(current.getFullDiskAccessStatus).toHaveBeenCalledTimes(1)
expect(legacy.getFullDiskAccessStatus).toHaveBeenCalledTimes(1)
}
)
it('keeps an observed denial even if another generation cannot answer', async () => {
current.getFullDiskAccessStatus.mockResolvedValue('unknown')
legacy.getFullDiskAccessStatus.mockResolvedValue('denied')
expect(await getTerminalHostsFullDiskAccessStatus()).toBe('denied')
})
it('does not launch a daemon merely to check permissions', async () => {
state.installed = false
expect(await getTerminalHostsFullDiskAccessStatus()).toBe('unknown')
expect(current.getFullDiskAccessStatus).not.toHaveBeenCalled()
})
it('includes the in-process host used for fresh terminals in degraded mode', async () => {
state.degraded = true
expect(await getTerminalHostsFullDiskAccessStatus()).toBe('denied')
expect(localStatus).toHaveBeenCalledTimes(1)
})
@@ -1,25 +0,0 @@
import type { DeveloperPermissionStatus } from '../../shared/developer-permissions-types'
import { getCurrentDaemonAdapter, getLegacyDaemonAdapters } from './daemon-provider-routing'
import { getDaemonProvider } from './daemon-provider-state'
import { DegradedDaemonPtyProvider } from './degraded-daemon-pty-provider'
import { getMacosFullDiskAccessStatus } from '../macos-full-disk-access-status'
/** macOS only; the caller gates it. A denied generation wins; granted needs every generation. */
export async function getTerminalHostsFullDiskAccessStatus(): Promise<DeveloperPermissionStatus> {
const provider = getDaemonProvider()
if (!provider) {
return 'unknown'
}
const probes = [getCurrentDaemonAdapter(provider), ...getLegacyDaemonAdapters(provider)].map(
(adapter) => adapter.getFullDiskAccessStatus()
)
if (provider instanceof DegradedDaemonPtyProvider) {
probes.push(getMacosFullDiskAccessStatus())
}
const statuses = await Promise.all(probes)
return statuses.includes('denied')
? 'denied'
: statuses.every((status) => status === 'granted')
? 'granted'
: 'unknown'
}
@@ -1,4 +0,0 @@
export type FullDiskAccessStatusRequest = {
id: string
type: 'fullDiskAccessStatus'
}
@@ -1,37 +0,0 @@
import { afterEach, expect, it, vi } from 'vitest'
import { DaemonClient } from './client'
import { DaemonPtyAdapter } from './daemon-pty-adapter'
afterEach(() => vi.restoreAllMocks())
it.each([
[{ status: 'granted' }, 'granted'],
[{ status: 'denied' }, 'denied'],
[{ status: 'future-status' }, 'unknown'],
[null, 'unknown']
])('checks the owning connection and validates %j', async (response, expected) => {
vi.spyOn(DaemonClient.prototype, 'isConnected').mockReturnValue(true)
const request = vi.spyOn(DaemonClient.prototype, 'request').mockResolvedValue(response)
const adapter = new DaemonPtyAdapter({ socketPath: '/unused', tokenPath: '/unused' })
expect(await adapter.getFullDiskAccessStatus()).toBe(expected)
expect(request).toHaveBeenCalledWith('fullDiskAccessStatus', undefined, 3000)
})
it.each(['Unknown request type', 'Request timed out', 'Connection lost'])(
'keeps old or unreachable hosts unknown: %s',
async (message) => {
vi.spyOn(DaemonClient.prototype, 'isConnected').mockReturnValue(true)
vi.spyOn(DaemonClient.prototype, 'request').mockRejectedValue(new Error(message))
const adapter = new DaemonPtyAdapter({ socketPath: '/unused', tokenPath: '/unused' })
expect(await adapter.getFullDiskAccessStatus()).toBe('unknown')
}
)
it('does not reconnect or respawn a disconnected host to obtain a verdict', async () => {
const request = vi.spyOn(DaemonClient.prototype, 'request')
const connect = vi.spyOn(DaemonClient.prototype, 'ensureConnected')
const adapter = new DaemonPtyAdapter({ socketPath: '/unused', tokenPath: '/unused' })
expect(await adapter.getFullDiskAccessStatus()).toBe('unknown')
expect(request).not.toHaveBeenCalled()
expect(connect).not.toHaveBeenCalled()
})
-19
View File
@@ -3,27 +3,8 @@ import { DaemonPtyDaemonRecovery } from './daemon-pty-daemon-recovery'
import { supportsMode2031UnsubscribeFact, type DaemonEvent } from './types'
import type { DaemonEndpointIdentity } from './daemon-hello-protocol'
import type { IPtyProvider } from '../providers/types'
import type { DeveloperPermissionStatus } from '../../shared/developer-permissions-types'
export class DaemonPtyAdapter extends DaemonPtyDaemonRecovery implements IPtyProvider {
async getFullDiskAccessStatus(): Promise<DeveloperPermissionStatus> {
if (!this.client.isConnected()) {
return 'unknown'
}
try {
const result = await this.client.request<unknown>('fullDiskAccessStatus', undefined, 3000)
if (result && typeof result === 'object' && 'status' in result) {
const { status } = result
if (status === 'granted' || status === 'denied' || status === 'unsupported') {
return status
}
}
} catch {
// Older daemons and lost host contact cannot establish a permission verdict.
}
return 'unknown'
}
/** Identity of the daemon behind this adapter; null until hello completes or after a disconnect. */
getDaemonIdentity(): DaemonEndpointIdentity | null {
return this.client.getDaemonIdentity()
-3
View File
@@ -1,7 +1,6 @@
import { performance } from 'node:perf_hooks'
import { setPtyOwnerHostColors } from '../../shared/pty-owner-color-query-colors'
import { readCurrentProcessMacSystemResolverHealth } from '../network/macos-system-resolver-health'
import { getMacosFullDiskAccessStatus } from '../macos-full-disk-access-status'
import type { ConnectedDaemonClient, DaemonClientConnections } from './daemon-client-connections'
import type { DaemonFileLog } from './daemon-file-log'
import type { DaemonPtySpawnPreparations } from './daemon-pty-spawn-preparations'
@@ -156,8 +155,6 @@ export class DaemonRequestRouter {
return { pong: true }
case 'systemResolverHealth':
return { health: await readCurrentProcessMacSystemResolverHealth() }
case 'fullDiskAccessStatus':
return { status: await getMacosFullDiskAccessStatus() }
case 'ptySpawnHealth':
await this.options.ptySpawnHealthCheck()
return { healthy: true, coverage: ptySpawnHealthPlatformCoverage() }
-7
View File
@@ -437,13 +437,6 @@ describe('DaemonServer', () => {
expect(['healthy', 'unhealthy', 'unknown']).toContain(result.health)
})
it('probes Full Disk Access in the owning daemon', async () => {
await startServer()
const c = await connectClient()
const result = await c.request<{ status: unknown }>('fullDiskAccessStatus', undefined)
expect(['granted', 'denied', 'unknown', 'unsupported']).toContain(result.status)
})
it('handles resize', async () => {
await startServer()
const c = await connectClient()
+1 -1
View File
@@ -46,7 +46,7 @@ async function codesignRequirement(bundlePath: string, signal: AbortSignal): Pro
return requirement
}
/** Private signed runtime copies; telemetry classifies a daemon spawned from here as `stable-copy`. */
/** Private signed runtime copies the updater never touches. */
export function getMacDaemonBundleRoot(userDataPath: string): string {
return join(userDataPath, 'daemon-host', 'macos')
}
-2
View File
@@ -1,4 +1,3 @@
import type { FullDiskAccessStatusRequest } from './daemon-permission-requests'
import type {
ConfirmForegroundProcessRequest,
ConfirmShellForegroundRequest,
@@ -323,7 +322,6 @@ export type DaemonRequest =
| PingRequest
| SystemResolverHealthRequest
| PtySpawnHealthRequest
| FullDiskAccessStatusRequest
| GetSnapshotRequest
| GetSizeRequest
| TakePendingOutputRequest
+1 -20
View File
@@ -7,7 +7,6 @@ const {
getMediaAccessStatusMock,
isTrustedAccessibilityClientMock,
getMacosFullDiskAccessStatusMock,
terminalHostsStatusMock,
execFileMock,
createSocketMock,
socketMock,
@@ -32,7 +31,6 @@ const {
getMediaAccessStatusMock: vi.fn(),
isTrustedAccessibilityClientMock: vi.fn(),
getMacosFullDiskAccessStatusMock: vi.fn(),
terminalHostsStatusMock: vi.fn(async () => 'denied'),
execFileMock: vi.fn(),
createSocketMock: vi.fn(() => socketMock),
socketMock,
@@ -67,9 +65,6 @@ vi.mock('node:child_process', () => ({
vi.mock('../macos-full-disk-access-status', () => ({
getMacosFullDiskAccessStatus: getMacosFullDiskAccessStatusMock
}))
vi.mock('../daemon/daemon-full-disk-access-status', () => ({
getTerminalHostsFullDiskAccessStatus: terminalHostsStatusMock
}))
import type { DeveloperPermissionState } from '../../shared/developer-permissions-types'
import { registerDeveloperPermissionHandlers } from './developer-permissions'
@@ -171,24 +166,10 @@ describe('registerDeveloperPermissionHandlers', () => {
await expect(handler?.()).resolves.toContainEqual({
id: 'full-disk-access',
status: 'granted',
terminalHostStatus: 'denied'
status: 'granted'
})
})
it.each(['win32', 'linux'] as const)('does not probe terminal hosts on %s', async (platform) => {
Object.defineProperty(process, 'platform', { value: platform, configurable: true })
getMacosFullDiskAccessStatusMock.mockResolvedValue('unsupported')
terminalHostsStatusMock.mockClear()
registerDeveloperPermissionHandlers()
await expect(getStatusHandler()()).resolves.toContainEqual({
id: 'full-disk-access',
status: 'unsupported'
})
expect(terminalHostsStatusMock).not.toHaveBeenCalled()
})
it('keeps the local-network status unknown when UDP send settles without an error', async () => {
registerDeveloperPermissionHandlers()
+1 -8
View File
@@ -2,7 +2,6 @@ import { execFile } from 'node:child_process'
import dgram from 'node:dgram'
import { ipcMain, shell, systemPreferences } from 'electron'
import { getMacosFullDiskAccessStatus } from '../macos-full-disk-access-status'
import { getTerminalHostsFullDiskAccessStatus } from '../daemon/daemon-full-disk-access-status'
import { testLocalNetworkConnection } from './local-network-connection-test'
import type {
DeveloperPermissionId,
@@ -154,13 +153,7 @@ async function getPermissionState(id: DeveloperPermissionId): Promise<DeveloperP
case 'accessibility':
return { id, status: getAccessibilityStatus() }
case 'full-disk-access':
return {
id,
status: await getMacosFullDiskAccessStatus(),
...(process.platform === 'darwin'
? { terminalHostStatus: await getTerminalHostsFullDiskAccessStatus() }
: {})
}
return { id, status: await getMacosFullDiskAccessStatus() }
// Why 'unknown' and not a probe: macOS reports no per-app Files-and-Folders grant, and the
// only caller opens the pane rather than reading a status.
case 'files-and-folders':
+2 -27
View File
@@ -1,11 +1,5 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { afterEach, describe, expect, it, vi } from 'vitest'
import { join } from 'node:path'
const { readMock, closeMock, openMock } = vi.hoisted(() => ({
readMock: vi.fn(),
closeMock: vi.fn(),
openMock: vi.fn()
}))
vi.mock('node:fs/promises', () => ({ open: openMock }))
import {
getMacosFullDiskAccessStatus,
probeMacosFullDiskAccess
@@ -28,34 +22,15 @@ function fileSystemError(code: string): NodeJS.ErrnoException {
afterEach(() => {
Object.defineProperty(process, 'platform', { configurable: true, value: originalPlatform })
})
beforeEach(() => {
readMock.mockReset().mockResolvedValue({ bytesRead: 1 })
closeMock.mockReset().mockResolvedValue(undefined)
openMock.mockReset().mockResolvedValue({ read: readMock, close: closeMock })
})
describe('probeMacosFullDiskAccess', () => {
it('reports granted only when the TCC database can be read', async () => {
it('reports granted only when the TCC database opens for reading', async () => {
const readProbe = vi.fn().mockResolvedValue(undefined)
await expect(probeMacosFullDiskAccess({ homeDirectory, readProbe })).resolves.toBe('granted')
expect(readProbe).toHaveBeenCalledWith(databasePath)
})
it('reads only one discarded byte and closes the handle', async () => {
expect(await probeMacosFullDiskAccess({ homeDirectory })).toBe('granted')
expect(openMock).toHaveBeenCalledWith(databasePath, 'r')
expect(readMock).toHaveBeenCalledWith(expect.any(Buffer), 0, 1, 0)
expect(readMock.mock.calls[0]?.[0].length).toBe(1)
expect(closeMock).toHaveBeenCalledTimes(1)
})
it('does not mistake an allowed open for allowed contents, and closes after denial', async () => {
readMock.mockRejectedValueOnce(fileSystemError('EPERM'))
expect(await probeMacosFullDiskAccess({ homeDirectory })).toBe('denied')
expect(closeMock).toHaveBeenCalledTimes(1)
})
it.each(['EACCES', 'EPERM'])('reports denied for %s', async (code) => {
await expect(
probeMacosFullDiskAccess({
+5 -9
View File
@@ -5,24 +5,20 @@ import type { DeveloperPermissionStatus } from '../shared/developer-permissions-
type ReadProbe = (filePath: string) => Promise<void>
async function readProbeByte(filePath: string): Promise<void> {
async function openForRead(filePath: string): Promise<void> {
const handle = await open(filePath, 'r')
try {
await handle.read(Buffer.alloc(1), 0, 1, 0)
} finally {
await handle.close()
}
await handle.close()
}
function errorCode(error: unknown): string | undefined {
return error && typeof error === 'object' && 'code' in error && typeof error.code === 'string'
? error.code
return error instanceof Error && 'code' in error
? (error as NodeJS.ErrnoException).code
: undefined
}
export async function probeMacosFullDiskAccess({
homeDirectory = homedir(),
readProbe = readProbeByte
readProbe = openForRead
}: {
homeDirectory?: string
readProbe?: ReadProbe
@@ -53,23 +53,6 @@ function setPermissionStates(states: DeveloperPermissionState[]): void {
api.developerPermissions.getStatus = vi.fn(async () => states)
}
it.each(['denied', 'unknown'] as const)(
'distinguishes app access from a %s terminal host',
async (terminalHostStatus) => {
setPermissionStates([{ id: 'full-disk-access', status: 'granted', terminalHostStatus }])
await act(async () => root.render(<DeveloperPermissionsPane />))
const row = container.querySelector(
'[data-settings-section="developer-permissions-full-disk-access"]'
)
expect(row?.textContent).toContain('App: Granted')
expect(row?.textContent).toContain(
terminalHostStatus === 'denied' ? 'Terminal hosts: Denied' : 'Terminal hosts: Check manually'
)
expect(row?.textContent).toContain('restarting closes its current terminals')
expect(window.api.developerPermissions.request).not.toHaveBeenCalled()
}
)
it('requests Local Network access without claiming a permission verdict', async () => {
setPermissionStates([{ id: 'local-network', status: 'unknown' }])
@@ -231,7 +231,7 @@ export function DeveloperPermissionsPane({
const refreshSequenceRef = useRef(0)
const stateById = useMemo(
() => new Map(states.map((state) => [state.id, state] as const)),
() => new Map(states.map((state) => [state.id, state.status] as const)),
[states]
)
@@ -341,8 +341,7 @@ export function DeveloperPermissionsPane({
<div className="divide-y divide-border/60 rounded-lg border border-border/60">
{PERMISSIONS.map((permission) => {
const state = stateById.get(permission.id)
const status = state?.status
const status = stateById.get(permission.id)
const pending = pendingId === permission.id
const settingId = `developer-permissions-${permission.id}`
@@ -363,33 +362,10 @@ export function DeveloperPermissionsPane({
status
)}`}
>
{permission.id === 'full-disk-access' &&
translate(
'auto.components.settings.DeveloperPermissionsPane.appStatus',
'App: '
)}
{developerPermissionStatusLabel(permission.id, status)}
</span>
</div>
<p className="text-xs text-muted-foreground">{permission.description}</p>
{permission.id === 'full-disk-access' && state?.terminalHostStatus && (
<p className="text-xs text-muted-foreground">
{translate(
'auto.components.settings.DeveloperPermissionsPane.terminalHostStatus',
'Terminal hosts: '
)}
{developerPermissionStatusLabel(permission.id, state.terminalHostStatus)}
{state.terminalHostStatus !== 'granted' && (
<>
{' '}
{translate(
'auto.components.settings.DeveloperPermissionsPane.terminalHostRecovery',
'If a terminal cannot read protected files, save its work and close it in Terminal → Manage Sessions. Restart the daemon if new terminals are also affected; restarting closes its current terminals.'
)}
</>
)}
</p>
)}
</div>
</div>
<DeveloperPermissionActions
+24 -5
View File
@@ -5388,6 +5388,9 @@
"df6fbcf880": "No SSH targets configured.",
"44637f43bd": "SSH target",
"80557be85a": "Choose a connected SSH target and enter the path to a Git repository.",
"serverDescription": "Choose an SSH host that {{server}} connects to, and enter the path to a Git repository on it.",
"serverEmpty": "{{server}} has no SSH hosts yet.",
"serverAddHost": "Add SSH host",
"91b93a90a4": "Open project on SSH host",
"007651bdf9": "Navigate to a directory and click Select to choose it.",
"dd3ff65486": "Browse remote filesystem",
@@ -6124,6 +6127,9 @@
"5f9ffac036": "Clone a remote Git repository",
"7edb8ebe24": "Clone from URL",
"a6c20dca96": "Open a project from an SSH target",
"serverSshTitle": "Project on this server's SSH host",
"serverSshDescription": "Open a project folder from an SSH host this server connects to",
"serverSshUpdate": "Update this server to add projects on its SSH hosts",
"sshCreateUnavailable": "Not available for SSH hosts yet",
"3d162cc76f": "Project on SSH host",
"fb4fc5380e": "Local project, Git repo, or folder with many repos",
@@ -6454,6 +6460,8 @@
"serverDescription": "Pair with Orca running on another computer.",
"serverRunsAsYou": "This device will be able to run commands as you on this server.",
"sshDescription": "Add a persistent machine you can log into over SSH.",
"serverSshTitle": "Add SSH host to {{server}}",
"serverSshDescription": "{{server}} saves this host and connects to it. Passwords and passphrases are asked for when it connects.",
"cancel": "Cancel",
"saving": "Saving...",
"save": "Save",
@@ -7414,10 +7422,7 @@
"connectionTestHost": "Host",
"connectionTestPort": "Port",
"connectionTestRunning": "Testing...",
"connectionTestAction": "Test Connection",
"appStatus": "App: ",
"terminalHostStatus": "Terminal hosts: ",
"terminalHostRecovery": "If a terminal cannot read protected files, save its work and close it in Terminal → Manage Sessions. Restart the daemon if new terminals are also affected; restarting closes its current terminals."
"connectionTestAction": "Test Connection"
},
"ExperimentalPane": {
"9762364929": "Uses APFS clone-copy on macOS when possible, otherwise symlinks configured folders or files into created worktrees.",
@@ -12637,6 +12642,20 @@
"terminalsElsewhere_other": "Runs the relay while {{count}} terminals another Orca desktop or session opened on this host are running.",
"terminalsElsewhereUncounted": "Runs the relay while terminals another Orca desktop or session opened on this host are running."
},
"ZcodeCliSetupSection": {
"title": "Use GLM with ZCode",
"description": "Sign in to use GLM as a coding agent. The quota API key above only monitors usage; ZCode owns its own sign-in and model selection.",
"help": "Choose browser sign-in for your plan site, or use ZCode’s masked API-key setup. After signing in, start ZCode and use /model to choose a GLM model.",
"zai": "Z.AI browser sign-in",
"bigmodel": "BigModel browser sign-in",
"apiKey": "ZCode API-key setup",
"close": "Close setup terminal",
"loading": "Loading sign-in terminal…",
"terminalTitle": "ZCode sign-in",
"terminalLabel": "ZCode sign-in terminal",
"apiKeyInstructions": "Press Enter to start ZCode, then enter /login and choose the API-key option for your plan site. Enter the key in ZCode’s masked prompt.",
"oauthInstructions": "Press Enter to run the sign-in command, then open the authorization URL printed by ZCode. Sign-in is saved on this terminal’s execution host."
},
"ZcodePlanAccountsSection": {
"site": {
"bigmodel": "Zhipu · BigModel (open.bigmodel.cn)",
@@ -12657,7 +12676,7 @@
"keyStored": "API key saved · {{value0}}",
"keyStoredHelp": "Stored locally and sent only to the selected site for usage refreshes. It takes priority over the ZCode CLI sign-in.",
"usingCli": "Using the ZCode CLI sign-in",
"usingCliHelp": "Orca reads the Coding Plan key from ~/.zcode/cli/config.json. Save an API key below to link the plan here instead.",
"usingCliHelp": "Orca reads the selected Coding Plan from the ZCode CLI credential store on this computer. Save an API key below to link the plan here instead.",
"notConfigured": "No GLM Coding Plan linked",
"notConfiguredHelp": "Save the plan API key below, or sign in with the ZCode CLI on this computer.",
"refreshUsage": "Refresh usage",
+9 -42
View File
@@ -13,53 +13,30 @@ describe('classifyDaemonSpawnerPath', () => {
it('classifies the installed app, the ShipIt staging area, and everything else', () => {
expect(
classifyDaemonSpawnerPath('/Applications/Orca.app/Contents/MacOS/Orca', alwaysExists, null)
classifyDaemonSpawnerPath('/Applications/Orca.app/Contents/MacOS/Orca', alwaysExists)
).toBe('applications')
expect(
classifyDaemonSpawnerPath(
'/private/Applications/Orca.app/Contents/MacOS/Orca',
alwaysExists,
null
)
classifyDaemonSpawnerPath('/private/Applications/Orca.app/Contents/MacOS/Orca', alwaysExists)
).toBe('applications')
expect(
classifyDaemonSpawnerPath(
'/Users/a/Library/Caches/com.stablyai.orca.ShipIt/update.abc/Orca.app/Contents/MacOS/Orca',
alwaysExists,
null
alwaysExists
)
).toBe('updater-cache')
expect(
classifyDaemonSpawnerPath(
'/Users/a/Applications/Orca.app/Contents/MacOS/Orca',
alwaysExists,
null
)
).toBe('other')
expect(
classifyDaemonSpawnerPath('/tmp/OrcaA.app/Contents/MacOS/Orca', alwaysExists, null)
classifyDaemonSpawnerPath('/Users/a/Applications/Orca.app/Contents/MacOS/Orca', alwaysExists)
).toBe('other')
expect(classifyDaemonSpawnerPath('/tmp/OrcaA.app/Contents/MacOS/Orca', alwaysExists)).toBe(
'other'
)
})
it('reports a deleted spawner as missing and an unrecorded one as unknown', () => {
expect(
classifyDaemonSpawnerPath('/Applications/Orca.app/Contents/MacOS/Orca', () => false, null)
classifyDaemonSpawnerPath('/Applications/Orca.app/Contents/MacOS/Orca', () => false)
).toBe('missing')
expect(classifyDaemonSpawnerPath(null, alwaysExists, null)).toBe('unknown')
})
it("classifies Orca's private stable copy from the recorded spawner path", () => {
const prefix = '/Users/alice/Library/Application Support/Orca/daemon-host/macos/runtime-'
const copy = `${prefix}Ab12/app.noindex/Orca.app/Contents/MacOS/Orca`
expect(classifyDaemonSpawnerPath(copy, alwaysExists, prefix)).toBe('stable-copy')
// Copies made before the `.noindex` folder still classify as the stable copy.
const oldLayout = `${prefix}Ztpw1G/Orca.app/Contents/MacOS/Orca`
expect(classifyDaemonSpawnerPath(oldLayout, alwaysExists, prefix)).toBe('stable-copy')
expect(classifyDaemonSpawnerPath(copy, () => false, prefix)).toBe('missing')
expect(classifyDaemonSpawnerPath(copy, alwaysExists, null)).toBe('other')
expect(
classifyDaemonSpawnerPath('/Applications/Orca.app/Contents/MacOS/Orca', alwaysExists, prefix)
).toBe('applications')
expect(classifyDaemonSpawnerPath(null, alwaysExists)).toBe('unknown')
})
})
@@ -129,16 +106,6 @@ describe('daemon_adopted / daemon_pty_cwd_denied schemas', () => {
expect(
eventSchemas.daemon_adopted.safeParse({ ...adopted, code_identity: 'severed' }).success
).toBe(false)
expect(
eventSchemas.daemon_pty_cwd_readable.safeParse({ ...denied, launch_method: 'stable-copy' })
.success
).toBe(false)
expect(
eventSchemas.daemon_pty_cwd_readable.safeParse({
...denied,
spawner_path_class: 'stable-copy'
}).success
).toBe(true)
})
})
+2 -8
View File
@@ -9,12 +9,10 @@ export type DaemonAdoptedAppVersionMatch = (typeof DAEMON_ADOPTED_APP_VERSION_MA
/**
* Where the binary that forked the adopted daemon lives now. `updater-cache` is the Squirrel
* ShipIt staging area — a daemon attributed there is the reported #17696 shape. `stable-copy` is
* Orca's private signed bundle copy, run by launchd (#25848).
* ShipIt staging area — a daemon attributed there is the reported #17696 shape.
*/
export const DAEMON_SPAWNER_PATH_CLASSES = [
'applications',
'stable-copy',
'updater-cache',
'other',
'missing',
@@ -62,8 +60,7 @@ export function isMacTccFolderClass(cwdClass: DaemonPtyCwdClass): cwdClass is Ma
export function classifyDaemonSpawnerPath(
spawnerExecPath: string | null,
exists: (path: string) => boolean,
stableCopyPrefix: string | null
exists: (path: string) => boolean
): DaemonSpawnerPathClass {
if (!spawnerExecPath) {
return 'unknown'
@@ -71,9 +68,6 @@ export function classifyDaemonSpawnerPath(
if (!exists(spawnerExecPath)) {
return 'missing'
}
if (stableCopyPrefix && spawnerExecPath.startsWith(stableCopyPrefix)) {
return 'stable-copy'
}
if (/\/Library\/Caches\/[^/]*ShipIt\//.test(spawnerExecPath)) {
return 'updater-cache'
}
@@ -24,7 +24,6 @@ export type DeveloperPermissionStatus =
export type DeveloperPermissionState = {
id: DeveloperPermissionId
status: DeveloperPermissionStatus
terminalHostStatus?: DeveloperPermissionStatus
}
export type DeveloperPermissionRequestResult = {