diff --git a/config/electron-builder.config.cjs b/config/electron-builder.config.cjs index a4af9b47d0b..56ef49f8689 100644 --- a/config/electron-builder.config.cjs +++ b/config/electron-builder.config.cjs @@ -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' diff --git a/config/reliability-gates.jsonc b/config/reliability-gates.jsonc index 11f0f5d408a..6a2bab790ee 100644 --- a/config/reliability-gates.jsonc +++ b/config/reliability-gates.jsonc @@ -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": { diff --git a/config/scripts/macos-folder-usage-descriptions.cjs b/config/scripts/macos-folder-usage-descriptions.cjs index 6dd3e2c0736..7be46e809f2 100644 --- a/config/scripts/macos-folder-usage-descriptions.cjs +++ b/config/scripts/macos-folder-usage-descriptions.cjs @@ -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 } diff --git a/config/scripts/macos-folder-usage-descriptions.test.mjs b/config/scripts/macos-folder-usage-descriptions.test.mjs index 8d5d9287222..71969fbc046 100644 --- a/config/scripts/macos-folder-usage-descriptions.test.mjs +++ b/config/scripts/macos-folder-usage-descriptions.test.mjs @@ -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 }) - } }) diff --git a/docs/reference/macos-daemon-tcc-attribution.md b/docs/reference/macos-daemon-tcc-attribution.md index 3758889a936..aa0dd50af75 100644 --- a/docs/reference/macos-daemon-tcc-attribution.md +++ b/docs/reference/macos-daemon-tcc-attribution.md @@ -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. diff --git a/src/main/daemon/daemon-adoption-telemetry-event.test.ts b/src/main/daemon/daemon-adoption-telemetry-event.test.ts index 7f25e0c1e84..fc493ea75ff 100644 --- a/src/main/daemon/daemon-adoption-telemetry-event.test.ts +++ b/src/main/daemon/daemon-adoption-telemetry-event.test.ts @@ -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', () => { diff --git a/src/main/daemon/daemon-adoption-telemetry-event.ts b/src/main/daemon/daemon-adoption-telemetry-event.ts index 512493fc372..e912abb1751 100644 --- a/src/main/daemon/daemon-adoption-telemetry-event.ts +++ b/src/main/daemon/daemon-adoption-telemetry-event.ts @@ -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, diff --git a/src/main/daemon/daemon-full-disk-access-status.test.ts b/src/main/daemon/daemon-full-disk-access-status.test.ts deleted file mode 100644 index b92c610a842..00000000000 --- a/src/main/daemon/daemon-full-disk-access-status.test.ts +++ /dev/null @@ -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>(), - current: { getFullDiskAccessStatus: vi.fn<() => Promise>() }, - legacy: { getFullDiskAccessStatus: vi.fn<() => Promise>() } -})) -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) -}) diff --git a/src/main/daemon/daemon-full-disk-access-status.ts b/src/main/daemon/daemon-full-disk-access-status.ts deleted file mode 100644 index a7fc132cb5f..00000000000 --- a/src/main/daemon/daemon-full-disk-access-status.ts +++ /dev/null @@ -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 { - 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' -} diff --git a/src/main/daemon/daemon-permission-requests.ts b/src/main/daemon/daemon-permission-requests.ts deleted file mode 100644 index 4682ef2cbad..00000000000 --- a/src/main/daemon/daemon-permission-requests.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type FullDiskAccessStatusRequest = { - id: string - type: 'fullDiskAccessStatus' -} diff --git a/src/main/daemon/daemon-pty-adapter-full-disk-access.test.ts b/src/main/daemon/daemon-pty-adapter-full-disk-access.test.ts deleted file mode 100644 index 1f45be336a5..00000000000 --- a/src/main/daemon/daemon-pty-adapter-full-disk-access.test.ts +++ /dev/null @@ -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() -}) diff --git a/src/main/daemon/daemon-pty-adapter.ts b/src/main/daemon/daemon-pty-adapter.ts index 6eb42036d1b..dfe694fbcad 100644 --- a/src/main/daemon/daemon-pty-adapter.ts +++ b/src/main/daemon/daemon-pty-adapter.ts @@ -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 { - if (!this.client.isConnected()) { - return 'unknown' - } - try { - const result = await this.client.request('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() diff --git a/src/main/daemon/daemon-request-router.ts b/src/main/daemon/daemon-request-router.ts index 28f4d073a0d..1184517c6dc 100644 --- a/src/main/daemon/daemon-request-router.ts +++ b/src/main/daemon/daemon-request-router.ts @@ -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() } diff --git a/src/main/daemon/daemon-server.test.ts b/src/main/daemon/daemon-server.test.ts index 8d30326e857..3479b4eea45 100644 --- a/src/main/daemon/daemon-server.test.ts +++ b/src/main/daemon/daemon-server.test.ts @@ -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() diff --git a/src/main/daemon/macos-daemon-bundle.ts b/src/main/daemon/macos-daemon-bundle.ts index fa785536f33..ee6c2382119 100644 --- a/src/main/daemon/macos-daemon-bundle.ts +++ b/src/main/daemon/macos-daemon-bundle.ts @@ -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') } diff --git a/src/main/daemon/types.ts b/src/main/daemon/types.ts index ab10c842847..adf1e8a1781 100644 --- a/src/main/daemon/types.ts +++ b/src/main/daemon/types.ts @@ -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 diff --git a/src/main/ipc/developer-permissions.test.ts b/src/main/ipc/developer-permissions.test.ts index bfea16e6ad8..5bece99b825 100644 --- a/src/main/ipc/developer-permissions.test.ts +++ b/src/main/ipc/developer-permissions.test.ts @@ -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() diff --git a/src/main/ipc/developer-permissions.ts b/src/main/ipc/developer-permissions.ts index 8116af8f6b7..1ce5e228324 100644 --- a/src/main/ipc/developer-permissions.ts +++ b/src/main/ipc/developer-permissions.ts @@ -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 ({ - 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({ diff --git a/src/main/macos-full-disk-access-status.ts b/src/main/macos-full-disk-access-status.ts index 44af4fe8d29..aea18b95677 100644 --- a/src/main/macos-full-disk-access-status.ts +++ b/src/main/macos-full-disk-access-status.ts @@ -5,24 +5,20 @@ import type { DeveloperPermissionStatus } from '../shared/developer-permissions- type ReadProbe = (filePath: string) => Promise -async function readProbeByte(filePath: string): Promise { +async function openForRead(filePath: string): Promise { 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 diff --git a/src/renderer/src/components/settings/DeveloperPermissionsPane.test.tsx b/src/renderer/src/components/settings/DeveloperPermissionsPane.test.tsx index f33bc4a8b95..6400e71795e 100644 --- a/src/renderer/src/components/settings/DeveloperPermissionsPane.test.tsx +++ b/src/renderer/src/components/settings/DeveloperPermissionsPane.test.tsx @@ -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()) - 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' }]) diff --git a/src/renderer/src/components/settings/DeveloperPermissionsPane.tsx b/src/renderer/src/components/settings/DeveloperPermissionsPane.tsx index 2abd9d7a9c5..18b48aa918b 100644 --- a/src/renderer/src/components/settings/DeveloperPermissionsPane.tsx +++ b/src/renderer/src/components/settings/DeveloperPermissionsPane.tsx @@ -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({
{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)}

{permission.description}

- {permission.id === 'full-disk-access' && state?.terminalHostStatus && ( -

- {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.' - )} - - )} -

- )} { 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) }) }) diff --git a/src/shared/daemon-adoption-telemetry.ts b/src/shared/daemon-adoption-telemetry.ts index 3318f4f5e68..149dc4da910 100644 --- a/src/shared/daemon-adoption-telemetry.ts +++ b/src/shared/daemon-adoption-telemetry.ts @@ -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' } diff --git a/src/shared/developer-permissions-types.ts b/src/shared/developer-permissions-types.ts index 20255575257..6b541e62fef 100644 --- a/src/shared/developer-permissions-types.ts +++ b/src/shared/developer-permissions-types.ts @@ -24,7 +24,6 @@ export type DeveloperPermissionStatus = export type DeveloperPermissionState = { id: DeveloperPermissionId status: DeveloperPermissionStatus - terminalHostStatus?: DeveloperPermissionStatus } export type DeveloperPermissionRequestResult = {