mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 00:57:45 +00:00
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:
@@ -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'
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
@@ -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()
|
||||
|
||||
@@ -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() }
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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')
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
Reference in New Issue
Block a user