merge: integrate Computer Use into local main

This commit is contained in:
程序员阿江(Relakkes)
2026-09-01 20:14:24 +08:00
208 changed files with 56350 additions and 5519 deletions
+57 -5
View File
@@ -257,7 +257,7 @@ describe('release desktop workflow', () => {
expect(workflow.indexOf('Build unsigned Electron release artifacts')).toBeLessThan(workflow.indexOf('Verify packaged app structure'))
})
test('release workflow records macOS and SignPath signing state and blocks unsigned releases', () => {
test('release workflow requires signed macOS Computer Use and preserves SignPath draft policy', () => {
const workflow = readReleaseWorkflow()
const signingJob = workflow.match(
/signing-preflight:[\s\S]*?(?:\n {2}[a-zA-Z0-9_-]+:|$)/,
@@ -289,10 +289,8 @@ describe('release desktop workflow', () => {
expect(signingJob).toContain(setting)
}
expect(signingJob).toContain('Missing macOS signing/notarization secrets')
expect(signingJob).toContain('macOS artifacts will be unsigned')
expect(signingJob).toContain('install-macos-unsigned.sh')
expect(signingJob).toContain('refusing to build a macOS release whose Computer Use runtime cannot pass client attestation')
expect(signingJob).toContain("RELEASE_DRAFT: ${{ github.event_name == 'workflow_dispatch' && inputs.draft == true }}")
expect(signingJob).toContain('Refusing to publish a non-draft desktop release without macOS signing/notarization secrets.')
expect(signingJob).toContain('macos_signed=false')
expect(signingJob).toContain('macos_signed=true')
expect(signingJob).toContain('SignPath configuration missing')
@@ -304,7 +302,7 @@ describe('release desktop workflow', () => {
const macRequiredBlock = signingJob?.match(
/missing=\(\)[\s\S]*?# Drafts may remain unsigned/,
)?.[0]
expect(macRequiredBlock).toContain('if [ "$RELEASE_DRAFT" != "true" ]; then')
expect(macRequiredBlock).not.toContain('if [ "$RELEASE_DRAFT" != "true" ]; then')
expect(macRequiredBlock).toContain('exit 1')
expect(signingJob).toContain('if [ "$RELEASE_DRAFT" != "true" ]; then')
expect(signingJob).toContain('exit 1')
@@ -609,12 +607,66 @@ describe('release desktop workflow', () => {
expect(desktopPackage.build.mac?.notarize).toBe(true)
expect(desktopPackage.build.mac?.entitlements).toBe('build/entitlements.mac.plist')
expect(desktopPackage.build.mac?.entitlementsInherit).toBe('build/entitlements.mac.inherit.plist')
// The Computer Use helper is excluded on purpose. `native/cu-helper/build.sh`
// already signed it under the stable identity `dev.cchaha.cu-helper`, and
// macOS ties the user's Accessibility and Screen Recording grants to that
// signing identity — re-signing it here would rotate the identity and
// silently drop both permissions on every update.
//
// The sidecar is excluded for a different reason — see the dedicated test
// below, which states the causal chain this literal list cannot express.
expect(desktopPackage.build.mac?.signIgnore).toEqual([
'/Contents/Frameworks/.+\\.(?:pak|bin|dat|nib)$',
'/Contents/Resources/.+\\.(?:asar|pak|bin|dat|icns|png|jpg|jpeg|gif|svg|ttf|woff|woff2)$',
'cc-haha-computer-use\\.app',
'claude-sidecar-[^/]+$',
])
})
// Regression: this entry was once dropped from signIgnore while the literal
// assertion above was edited to match, so the suite stayed green and every
// Computer Use call in the shipped build failed closed with
// `unauthorized_client` — the settings page just said "checking…" forever.
//
// The causal chain: `build-sidecars.ts` signs the sidecar with an explicit
// `--identifier com.claude-code-haha.desktop.sidecar`, because
// `ClientAttestation.swift` compares that identifier EXACTLY when it walks the
// helper -> sidecar -> desktop process chain. If electron-builder re-signs the
// sidecar it drops that flag, and codesign falls back to deriving the
// identifier from the file name (`claude-sidecar-aarch64-apple-darwin`), which
// never matches. So this test asserts the behaviour (real sidecar file names
// are excluded) rather than the spelling of one array element.
test('macOS signIgnore keeps electron-builder off the attested sidecar', () => {
const desktopPackage = JSON.parse(readFileSync('desktop/package.json', 'utf8')) as {
build: { mac?: { signIgnore?: string[] } }
}
const patterns = (desktopPackage.build.mac?.signIgnore ?? []).map(
p => new RegExp(p),
)
// Both architectures ship under these names; ClientAttestation.swift accepts
// exactly these two, so both must survive electron-builder's signing pass.
for (const sidecar of [
'/Contents/Resources/app.asar.unpacked/src-tauri/binaries/claude-sidecar-aarch64-apple-darwin',
'/Contents/Resources/app.asar.unpacked/src-tauri/binaries/claude-sidecar-x86_64-apple-darwin',
]) {
expect(
patterns.some(p => p.test(sidecar)),
`${sidecar} must be in signIgnore, or electron-builder re-signs it and ` +
'the attestation chain breaks',
).toBe(true)
}
// The identifier the exclusion exists to protect. If this constant moves,
// ClientAttestation.swift's `sidecarIdentifier` has to move with it.
expect(
readFileSync('desktop/scripts/sign-identity.ts', 'utf8'),
).toContain("SIDECAR_SIGNING_IDENTIFIER = 'com.claude-code-haha.desktop.sidecar'")
expect(
readFileSync('native/cu-helper/Sources/cu-helper/ClientAttestation.swift', 'utf8'),
).toContain('sidecarIdentifier = "com.claude-code-haha.desktop.sidecar"')
})
test('Windows NSIS installer lets users choose the install directory', () => {
const desktopPackage = JSON.parse(readFileSync('desktop/package.json', 'utf8')) as {
build: {
@@ -0,0 +1,439 @@
import { describe, expect, test } from 'bun:test'
import {
assertChangedState,
assertCleanupEvidence,
assertMonitorContinuity,
assertPointerTrace,
assertNoChangeState,
assertSafeRunDirectory,
assertScreenshotChanged,
assertStaleHandleFailure,
assertSystemStatePreserved,
acquireLiveSmokeLock,
deriveLiveSmokePaths,
findEditableHandle,
hasExactNoChangeState,
hasFreshScreenshot,
errorMessage,
parseInputMonitorSnapshot,
parseLiveSmokeArgs,
parseSystemSnapshot,
type LiveAppState,
type SystemSnapshot,
} from './computer-use-live-smoke.js'
const initialSystemSnapshot: SystemSnapshot = {
frontmost: {
pid: 101,
bundleId: 'com.openai.codex',
executablePath: '/Applications/Codex.app/Contents/MacOS/Codex',
launchTime: 1234.5,
},
pointer: { x: 100, y: 200 },
input: {
flags: '0',
buttons: [false, false, false, false, false],
},
}
const PNG_ONE =
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII='
const PNG_TWO =
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAADElEQVR42mP4z8AAAAMBAQDJ/pLvAAAAAElFTkSuQmCC'
function state(overrides: Partial<LiveAppState> = {}): LiveAppState {
return {
pid: 220,
bundleId: 'com.apple.TextEdit',
appName: 'TextEdit',
windowTitle: 'smoke-fixture.txt',
elementCount: 3,
truncated: false,
durationMs: 10,
axText: [
'g8:0 standard window smoke-fixture.txt',
'\tg8:7 text area CC_HAHA_SMOKE_STABLE_TOKEN',
'\tg8:9 button close',
].join('\n'),
elements: [
{
index: 7,
role: 'AXTextArea',
settable: true,
value: 'CC_HAHA_SMOKE_STABLE_TOKEN',
},
],
screenshot: {
base64: PNG_ONE,
width: 1,
height: 1,
},
...overrides,
}
}
describe('computer-use live smoke CLI safety', () => {
test('surfaces every primary and cleanup error from an AggregateError', () => {
expect(errorMessage(new AggregateError([
new Error('primary failed'),
new Error('cleanup failed'),
], 'smoke failed'))).toContain('primary failed')
expect(errorMessage(new AggregateError([
new Error('primary failed'),
new Error('cleanup failed'),
], 'smoke failed'))).toContain('cleanup failed')
})
test('defaults to the dedicated TextEdit target and accepts only its exact bundle id', () => {
expect(parseLiveSmokeArgs([])).toEqual({
targetBundleId: 'com.apple.TextEdit',
})
expect(parseLiveSmokeArgs(['--target', 'com.apple.TextEdit'])).toEqual({
targetBundleId: 'com.apple.TextEdit',
})
})
test('acquires/releases the production lock and performs no release when blocked', async () => {
let releases = 0
const release = await acquireLiveSmokeLock(
async () => ({ kind: 'acquired', fresh: true }),
async () => {
releases += 1
return true
},
)
expect(releases).toBe(0)
await release()
expect(releases).toBe(1)
let blockedRelease = 0
await expect(
acquireLiveSmokeLock(
async () => ({ kind: 'blocked', by: 'other-session' }),
async () => {
blockedRelease += 1
return true
},
),
).rejects.toThrow(/did not start/i)
expect(blockedRelease).toBe(0)
})
test('refuses Finder, terminal, system, and arbitrary targets', () => {
for (const target of [
'Finder',
'com.apple.finder',
'Terminal',
'com.apple.Terminal',
'System Settings',
'com.apple.systempreferences',
'com.googlecode.iterm2',
'com.example.OtherApp',
]) {
expect(() => parseLiveSmokeArgs(['--target', target])).toThrow(
/dedicated TextEdit/i,
)
}
})
test('rejects missing values and unknown CLI flags', () => {
expect(() => parseLiveSmokeArgs(['--target'])).toThrow(/value/i)
expect(() => parseLiveSmokeArgs(['--fixture', '/tmp/user-file.txt'])).toThrow(
/unknown argument/i,
)
})
})
describe('computer-use live smoke path confinement', () => {
test('derives the fixture and this process daemon artifacts deterministically', () => {
expect(
deriveLiveSmokePaths(
'/tmp/cc-haha-cu-live-smoke-ABC123',
'/Users/test/.claude/.runtime',
4321,
),
).toEqual({
runDirectory: '/tmp/cc-haha-cu-live-smoke-ABC123',
fixturePath:
'/tmp/cc-haha-cu-live-smoke-ABC123/computer-use-smoke-fixture.txt',
targetIdentityPath:
'/tmp/cc-haha-cu-live-smoke-ABC123/.textedit-identity.json',
daemonSocket:
'/Users/test/.claude/.runtime/cu-helper.daemon.4321.1.sock',
daemonPidfile:
'/Users/test/.claude/.runtime/cu-helper.daemon.4321.1.sock.pid',
})
})
test('accepts only one generated child directory directly beneath /tmp', () => {
expect(() =>
assertSafeRunDirectory('/tmp/cc-haha-cu-live-smoke-ABC123'),
).not.toThrow()
for (const unsafe of [
'/',
'/tmp',
'/tmp/cc-haha-cu-live-smoke-',
'/tmp/cc-haha-cu-live-smoke-ABC123/..',
'/var/tmp/cc-haha-cu-live-smoke-ABC123',
'/tmp/other-ABC123',
]) {
expect(() => assertSafeRunDirectory(unsafe)).toThrow(/unsafe/i)
}
})
})
describe('computer-use live smoke state evidence', () => {
test('parses a complete system snapshot and rejects unproven foreground identity', () => {
expect(parseSystemSnapshot(JSON.stringify(initialSystemSnapshot))).toEqual(
initialSystemSnapshot,
)
expect(() =>
parseSystemSnapshot(
JSON.stringify({
...initialSystemSnapshot,
frontmost: { ...initialSystemSnapshot.frontmost, launchTime: null },
}),
),
).toThrow(/frontmost/i)
})
test('accepts at most one pixel of pointer drift with exact foreground and held-input state', () => {
expect(() =>
assertSystemStatePreserved(initialSystemSnapshot, {
...initialSystemSnapshot,
pointer: { x: 100.6, y: 200.6 },
}),
).not.toThrow()
expect(() =>
assertSystemStatePreserved(initialSystemSnapshot, {
...initialSystemSnapshot,
pointer: { x: 101.01, y: 200 },
}),
).toThrow(/pointer drift/i)
})
test('rejects PID reuse, foreground replacement, and stuck input state', () => {
expect(() =>
assertSystemStatePreserved(initialSystemSnapshot, {
...initialSystemSnapshot,
frontmost: { ...initialSystemSnapshot.frontmost, launchTime: 9999 },
}),
).toThrow(/frontmost identity/i)
expect(() =>
assertSystemStatePreserved(initialSystemSnapshot, {
...initialSystemSnapshot,
input: {
...initialSystemSnapshot.input,
buttons: [true, false, false, false, false],
},
}),
).toThrow(/held input/i)
})
test('rejects transient pointer movement even if the endpoint was restored', () => {
expect(() => assertPointerTrace({ samples: 50, maxDriftPx: 0.8 })).not.toThrow()
expect(() => assertPointerTrace({ samples: 50, maxDriftPx: 12 })).toThrow(
/transiently/i,
)
expect(() => assertPointerTrace({ samples: 1, maxDriftPx: 0 })).toThrow(
/too few samples/i,
)
})
test('requires an available, continuous physical-input monitor', () => {
const before = parseInputMonitorSnapshot({
epoch: '42',
available: true,
continuityGeneration: '3',
})
const after = parseInputMonitorSnapshot({
epoch: '42',
available: true,
continuityGeneration: '3',
})
expect(() => assertMonitorContinuity(before, after)).not.toThrow()
expect(() =>
parseInputMonitorSnapshot({
epoch: '42',
available: false,
continuityGeneration: '3',
}),
).toThrow(/physical-input monitor/i)
expect(() =>
assertMonitorContinuity(before, { ...after, epoch: 43n }),
).toThrow(/physical input/i)
expect(() =>
assertMonitorContinuity(before, {
...after,
continuityGeneration: 4n,
}),
).toThrow(/continuity/i)
})
})
describe('computer-use live smoke AX proof', () => {
test('wait predicates reject transiently missing captures', () => {
expect(hasFreshScreenshot(state())).toBe(true)
expect(hasFreshScreenshot(state({ screenshot: undefined }))).toBe(false)
expect(hasFreshScreenshot(state({
screenshot: { base64: 'not-a-png', width: 1, height: 1 },
}))).toBe(false)
})
test('derives an opaque editable handle from raw element metadata plus rendered generation', () => {
expect(findEditableHandle(state(), 'CC_HAHA_SMOKE_STABLE_TOKEN')).toBe(
'g8:7',
)
})
test('rejects ambiguous or non-settable editable elements', () => {
expect(() =>
findEditableHandle(
state({
elements: [
{
index: 7,
role: 'AXTextArea',
settable: false,
value: 'CC_HAHA_SMOKE_STABLE_TOKEN',
},
],
}),
'CC_HAHA_SMOKE_STABLE_TOKEN',
),
).toThrow(/exactly one/i)
expect(() =>
findEditableHandle(
state({
elements: [
{
index: 7,
role: 'AXTextArea',
settable: true,
value: 'CC_HAHA_SMOKE_STABLE_TOKEN',
},
{
index: 8,
role: 'AXTextField',
settable: true,
value: 'CC_HAHA_SMOKE_STABLE_TOKEN',
},
],
axText:
'g8:7 text area CC_HAHA_SMOKE_STABLE_TOKEN\ng8:8 text field CC_HAHA_SMOKE_STABLE_TOKEN',
}),
'CC_HAHA_SMOKE_STABLE_TOKEN',
),
).toThrow(/exactly one/i)
})
test('requires the exact no-change header and a changed diff with a real screenshot', () => {
expect(hasExactNoChangeState(state({
axText:
'There has been no change in the accessibility tree for Window: "smoke-fixture.txt".',
}))).toBe(true)
expect(hasExactNoChangeState(state({
axText:
'The following is a diff from the previous accessibility tree for Window: "smoke-fixture.txt".',
}))).toBe(false)
expect(() =>
assertNoChangeState(
state({
axText:
'There has been no change in the accessibility tree for Window: "smoke-fixture.txt".',
}),
),
).not.toThrow()
expect(() =>
assertNoChangeState(state({ axText: 'g8:0 standard window' })),
).toThrow(/no-change/i)
expect(() =>
assertChangedState(
state({
axText:
'The following is a diff from the previous accessibility tree for Window: "smoke-fixture.txt" with ~ and + representing changed and added elements, respectively. Removed elements are summarized by ID range.\n~\tg8:7 text area MUTATED',
}),
'MUTATED',
),
).not.toThrow()
expect(() =>
assertChangedState(
state({
axText:
'The following is a diff from the previous accessibility tree for Window: "smoke-fixture.txt" with ~ and + representing changed and added elements, respectively. Removed elements are summarized by ID range.\n~\tg8:7 text area MUTATED',
screenshot: { base64: '', width: 0, height: 0 },
}),
'MUTATED',
),
).toThrow(/screenshot/i)
})
test('rejects fake PNG text and reused mutation screenshots', () => {
expect(() =>
assertNoChangeState(
state({
axText:
'There has been no change in the accessibility tree for Window: "smoke-fixture.txt".',
screenshot: { base64: 'a'.repeat(128), width: 1, height: 1 },
}),
),
).toThrow(/PNG|screenshot/i)
expect(() => assertScreenshotChanged(state(), state())).toThrow(/reused/i)
expect(() =>
assertScreenshotChanged(
state(),
state({ screenshot: { base64: PNG_TWO, width: 1, height: 1 } }),
),
).not.toThrow()
})
test('accepts only an authoritative stale-handle failure', () => {
expect(() =>
assertStaleHandleFailure(
new Error(
'Snapshot handle g9:4 is stale. Re-query the latest state with get_app_state before sending more actions.',
),
),
).not.toThrow()
expect(() =>
assertStaleHandleFailure(new Error('Accessibility permission is required')),
).toThrow(/not a stale-handle/i)
})
})
describe('computer-use live smoke cleanup proof', () => {
test('requires socket, pidfile, owned daemon, and held input to be gone', () => {
expect(() =>
assertCleanupEvidence({
daemonSocketExists: false,
daemonPidfileExists: false,
daemonProcessStillMatches: false,
inputBefore: initialSystemSnapshot.input,
inputAfter: initialSystemSnapshot.input,
}),
).not.toThrow()
expect(() =>
assertCleanupEvidence({
daemonSocketExists: true,
daemonPidfileExists: false,
daemonProcessStillMatches: false,
inputBefore: initialSystemSnapshot.input,
inputAfter: initialSystemSnapshot.input,
}),
).toThrow(/socket/i)
})
})
File diff suppressed because it is too large Load Diff
@@ -8,6 +8,8 @@ import {
} from './current'
import {
inspectPackagedArtifacts,
parseCodesignMetadata,
parseMachOMinimumMacosVersions,
parsePackageSmokeArgs,
} from './index'
@@ -27,7 +29,7 @@ function createRepoRoot() {
return rootDir
}
function writeFile(rootDir: string, relativePath: string, content = 'ok') {
function writeFile(rootDir: string, relativePath: string, content: string | Uint8Array = 'ok') {
const fullPath = join(rootDir, relativePath)
mkdirSync(dirname(fullPath), { recursive: true })
writeFileSync(fullPath, content)
@@ -45,9 +47,35 @@ function writeFile(rootDir: string, relativePath: string, content = 'ok') {
for (const licenseName of ['COPYING', 'LICENSE-MIT', 'UNLICENSE']) {
writeFileSync(join(licensesDir, licenseName), content)
}
if (fileName.includes('apple-darwin')) {
const helperRoot = join(dirname(fullPath), 'cc-haha-computer-use.app', 'Contents')
mkdirSync(join(helperRoot, 'MacOS'), { recursive: true })
writeFileSync(
join(helperRoot, 'Info.plist'),
'<plist><dict><key>LSMinimumSystemVersion</key><string>14.4</string></dict></plist>',
)
writeFileSync(join(helperRoot, 'MacOS', 'cc-haha-computer-use'), content)
}
}
}
function thinMachO(arch: 'arm64' | 'x64', minimum = '14.4') {
const [major, minor, patch = 0] = minimum.split('.').map(Number)
const encodedMinimum = (major << 16) | (minor << 8) | patch
const bytes = Buffer.alloc(56)
bytes.writeUInt32LE(0xfeedfacf, 0)
bytes.writeUInt32LE(arch === 'arm64' ? 0x0100000c : 0x01000007, 4)
bytes.writeUInt32LE(2, 12)
bytes.writeUInt32LE(1, 16)
bytes.writeUInt32LE(24, 20)
bytes.writeUInt32LE(0x32, 32)
bytes.writeUInt32LE(24, 36)
bytes.writeUInt32LE(1, 40)
bytes.writeUInt32LE(encodedMinimum, 44)
bytes.writeUInt32LE(15 << 16, 48)
return bytes
}
const tempDirs: string[] = []
afterEach(() => {
@@ -78,6 +106,27 @@ describe('package smoke args', () => {
expect(currentPackageSmokeArch('x64')).toBe('x64')
expect(currentPackageSmokeArch('ia32')).toBeNull()
})
test('reads the helper deployment target from the Mach-O load commands', () => {
expect(parseMachOMinimumMacosVersions(thinMachO('arm64', '14.4'))).toEqual(['14.4'])
expect(parseMachOMinimumMacosVersions(thinMachO('x64', '14.0'))).toEqual(['14.0'])
expect(parseMachOMinimumMacosVersions(Buffer.from('not Mach-O'))).toEqual([])
})
test('reads the identity fields required by Computer Use client attestation', () => {
expect(parseCodesignMetadata([
'Identifier=dev.cchaha.cu-helper',
'Authority=Developer ID Application: Example (TEAM123456)',
'Authority=Developer ID Certification Authority',
'Timestamp=Sep 1, 2026 at 18:43:53',
'TeamIdentifier=TEAM123456',
].join('\n'))).toEqual({
identifier: 'dev.cchaha.cu-helper',
authority: 'Developer ID Application: Example (TEAM123456)',
team: 'TEAM123456',
timestamp: 'Sep 1, 2026 at 18:43:53',
})
})
})
describe('packaged artifact inspection', () => {
@@ -147,6 +196,84 @@ describe('packaged artifact inspection', () => {
)).toBe(true)
})
test('fails closed when an arm64 package contains an x64 cu-helper', async () => {
const rootDir = createRepoRoot()
tempDirs.push(rootDir)
const appRoot = 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app'
const resources = `${appRoot}/Contents/Resources`
const sidecarRoot = `${resources}/app.asar.unpacked/src-tauri/binaries`
const nodePtyRoot = `${resources}/app.asar.unpacked/node_modules/node-pty`
writeFile(rootDir, `${appRoot}/Contents/Info.plist`)
writeFile(rootDir, `${appRoot}/Contents/MacOS/Claude Code Haha`, thinMachO('arm64'))
writeFile(rootDir, `${resources}/app.asar`)
writeFile(rootDir, `${resources}/app.asar.unpacked/dist/index.html`)
writeFile(rootDir, `${sidecarRoot}/claude-sidecar-aarch64-apple-darwin`, thinMachO('arm64'))
writeFile(rootDir, `${nodePtyRoot}/package.json`)
writeFile(rootDir, `${nodePtyRoot}/prebuilds/darwin-arm64/pty.node`, thinMachO('arm64'))
writeFile(rootDir, `${nodePtyRoot}/prebuilds/darwin-arm64/spawn-helper`, thinMachO('arm64'))
const validReport = await inspectPackagedArtifacts(rootDir, {
platform: 'macos',
arch: 'arm64',
packageKind: 'dir',
})
expect(validReport.passed).toBe(true)
writeFile(
rootDir,
`${sidecarRoot}/cc-haha-computer-use.app/Contents/MacOS/cc-haha-computer-use`,
thinMachO('x64'),
)
const report = await inspectPackagedArtifacts(rootDir, {
platform: 'macos',
arch: 'arm64',
packageKind: 'dir',
})
expect(report.passed).toBe(false)
expect(report.missingChecks.some(
check => check.label === 'macOS arm64 cu-helper Mach-O architecture',
)).toBe(true)
expect(report.notes.join('\n')).toContain('expected arm64, found x86_64')
})
test('fails closed when the helper Mach-O deployment target drifts below 14.4', async () => {
const rootDir = createRepoRoot()
tempDirs.push(rootDir)
const appRoot = 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app'
const resources = `${appRoot}/Contents/Resources`
const sidecarRoot = `${resources}/app.asar.unpacked/src-tauri/binaries`
const nodePtyRoot = `${resources}/app.asar.unpacked/node_modules/node-pty`
writeFile(rootDir, `${appRoot}/Contents/Info.plist`)
writeFile(rootDir, `${appRoot}/Contents/MacOS/Claude Code Haha`, thinMachO('arm64'))
writeFile(rootDir, `${resources}/app.asar`)
writeFile(rootDir, `${resources}/app.asar.unpacked/dist/index.html`)
writeFile(rootDir, `${sidecarRoot}/claude-sidecar-aarch64-apple-darwin`, thinMachO('arm64'))
writeFile(rootDir, `${nodePtyRoot}/package.json`)
writeFile(rootDir, `${nodePtyRoot}/prebuilds/darwin-arm64/pty.node`, thinMachO('arm64'))
writeFile(rootDir, `${nodePtyRoot}/prebuilds/darwin-arm64/spawn-helper`, thinMachO('arm64'))
writeFile(
rootDir,
`${sidecarRoot}/cc-haha-computer-use.app/Contents/MacOS/cc-haha-computer-use`,
thinMachO('arm64', '14.0'),
)
const report = await inspectPackagedArtifacts(rootDir, {
platform: 'macos',
arch: 'arm64',
packageKind: 'dir',
})
expect(report.passed).toBe(false)
expect(report.missingChecks.some(
check => check.label === 'macOS cu-helper Mach-O deployment target (14.4)',
)).toBe(true)
expect(report.notes.join('\n')).toContain('expected 14.4, found 14.0')
})
test('fails macOS inspection when the H5 shell is not unpacked for the sidecar', async () => {
const rootDir = createRepoRoot()
tempDirs.push(rootDir)
@@ -272,6 +399,69 @@ describe('packaged artifact inspection', () => {
expect(report.notes.join('\n')).toContain('notarization ticket validation exited with status 65')
})
test('requires one Developer ID signer across host, sidecar, and helper', async () => {
const rootDir = createRepoRoot()
tempDirs.push(rootDir)
const appRoot = 'desktop/build-artifacts/electron/mac-arm64/Claude Code Haha.app'
const resources = `${appRoot}/Contents/Resources`
const sidecarRoot = `${resources}/app.asar.unpacked/src-tauri/binaries`
const nodePtyRoot = `${resources}/app.asar.unpacked/node_modules/node-pty`
writeFile(rootDir, `${appRoot}/Contents/Info.plist`)
writeFile(rootDir, `${appRoot}/Contents/MacOS/Claude Code Haha`, thinMachO('arm64'))
writeFile(rootDir, `${resources}/app.asar`)
writeFile(rootDir, `${resources}/app.asar.unpacked/dist/index.html`)
writeFile(rootDir, `${sidecarRoot}/claude-sidecar-aarch64-apple-darwin`, thinMachO('arm64'))
writeFile(rootDir, `${nodePtyRoot}/package.json`)
writeFile(rootDir, `${nodePtyRoot}/prebuilds/darwin-arm64/pty.node`, thinMachO('arm64'))
writeFile(rootDir, `${nodePtyRoot}/prebuilds/darwin-arm64/spawn-helper`, thinMachO('arm64'))
const inspect = (sidecarAuthority: string) => inspectPackagedArtifacts(rootDir, {
platform: 'macos',
arch: 'arm64',
packageKind: 'dir',
requireMacosGatekeeper: true,
hostPlatform: 'macos',
commandRunner: (command, args) => {
if (command.endsWith('/spctl')) return { status: 0, stdout: 'accepted', stderr: '' }
if (command.endsWith('/codesign') && args[0] === '--verify') {
return { status: 0, stdout: '', stderr: '' }
}
if (command.endsWith('/codesign') && args[0] === '-dv') {
const target = args.at(-1) ?? ''
const isSidecar = target.includes('claude-sidecar-')
const identifier = target.endsWith('cc-haha-computer-use.app')
? 'dev.cchaha.cu-helper'
: isSidecar
? 'com.claude-code-haha.desktop.sidecar'
: 'com.claude-code-haha.desktop'
const authority = isSidecar
? sidecarAuthority
: 'Developer ID Application: Example (TEAM123456)'
return {
status: 0,
stdout: '',
stderr: [
`Identifier=${identifier}`,
`Authority=${authority}`,
'Timestamp=Sep 1, 2026 at 18:43:53',
'TeamIdentifier=TEAM123456',
].join('\n'),
}
}
return { status: 0, stdout: '', stderr: '' }
},
})
const valid = await inspect('Developer ID Application: Example (TEAM123456)')
expect(valid.passedChecks.some(
check => check.label === 'macOS Computer Use signing attestation chain',
)).toBe(true)
const mismatched = await inspect('Developer ID Application: Other (TEAM123456)')
expect(mismatched.passed).toBe(false)
expect(mismatched.notes.join('\n')).toContain('mismatched Developer ID authority/team')
})
test('retries macOS Gatekeeper assessment with a raised file limit when spctl hits open-file limits', async () => {
const rootDir = createRepoRoot()
tempDirs.push(rootDir)
+359 -22
View File
@@ -274,6 +274,190 @@ function addMatchCheck(
})
}
type MachOArch = 'arm64' | 'x86_64'
const MACHO_CPU_TYPES: Record<number, MachOArch> = {
[0x0100000c]: 'arm64',
[0x01000007]: 'x86_64',
}
export function parseMachOArchitectures(bytes: Uint8Array): MachOArch[] {
const buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength)
if (buffer.length < 8) return []
const architectures = new Set<MachOArch>()
const addCpu = (value: number) => {
const arch = MACHO_CPU_TYPES[value >>> 0]
if (arch) architectures.add(arch)
}
const littleMagic = buffer.readUInt32LE(0)
const bigMagic = buffer.readUInt32BE(0)
if (littleMagic === 0xfeedface || littleMagic === 0xfeedfacf) {
addCpu(buffer.readUInt32LE(4))
return [...architectures]
}
if (bigMagic === 0xfeedface || bigMagic === 0xfeedfacf) {
addCpu(buffer.readUInt32BE(4))
return [...architectures]
}
const fat64 = bigMagic === 0xcafebabf || littleMagic === 0xcafebabf
const fat32 = bigMagic === 0xcafebabe || littleMagic === 0xcafebabe
if (!fat32 && !fat64) return []
const bigEndian = bigMagic === 0xcafebabe || bigMagic === 0xcafebabf
const readU32 = (offset: number) => bigEndian
? buffer.readUInt32BE(offset)
: buffer.readUInt32LE(offset)
const count = readU32(4)
const stride = fat64 ? 32 : 20
for (let index = 0; index < count; index += 1) {
const offset = 8 + index * stride
if (offset + 4 > buffer.length) return []
addCpu(readU32(offset))
}
return [...architectures].sort()
}
function decodeMachOVersion(encoded: number): string {
const major = (encoded >>> 16) & 0xffff
const minor = (encoded >>> 8) & 0xff
const patch = encoded & 0xff
return patch > 0 ? `${major}.${minor}.${patch}` : `${major}.${minor}`
}
function parseThinMachOMinimumVersion(
buffer: Buffer,
start: number,
length: number,
): string | null {
if (length < 28 || start < 0 || start + length > buffer.length) return null
const littleMagic = buffer.readUInt32LE(start)
const bigMagic = buffer.readUInt32BE(start)
const littleEndian = littleMagic === 0xfeedface || littleMagic === 0xfeedfacf
const bigEndian = bigMagic === 0xfeedface || bigMagic === 0xfeedfacf
if (!littleEndian && !bigEndian) return null
const readU32 = (offset: number) => littleEndian
? buffer.readUInt32LE(offset)
: buffer.readUInt32BE(offset)
const is64Bit = (littleEndian ? littleMagic : bigMagic) === 0xfeedfacf
const commandCount = readU32(start + 16)
let cursor = start + (is64Bit ? 32 : 28)
const end = start + length
for (let index = 0; index < commandCount; index += 1) {
if (cursor + 8 > end) return null
const command = readU32(cursor)
const commandSize = readU32(cursor + 4)
if (commandSize < 8 || cursor + commandSize > end) return null
if (command === 0x32 && commandSize >= 24) {
// LC_BUILD_VERSION.minos
return decodeMachOVersion(readU32(cursor + 12))
}
if (command === 0x24 && commandSize >= 16) {
// Legacy LC_VERSION_MIN_MACOSX.version
return decodeMachOVersion(readU32(cursor + 8))
}
cursor += commandSize
}
return null
}
export function parseMachOMinimumMacosVersions(bytes: Uint8Array): string[] {
const buffer = Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength)
if (buffer.length < 8) return []
const bigMagic = buffer.readUInt32BE(0)
const fat64 = bigMagic === 0xcafebabf || bigMagic === 0xbfbafeca
const fat32 = bigMagic === 0xcafebabe || bigMagic === 0xbebafeca
if (!fat32 && !fat64) {
const version = parseThinMachOMinimumVersion(buffer, 0, buffer.length)
return version ? [version] : []
}
const bigEndian = bigMagic === 0xcafebabe || bigMagic === 0xcafebabf
const readU32 = (offset: number) => bigEndian
? buffer.readUInt32BE(offset)
: buffer.readUInt32LE(offset)
const readU64 = (offset: number) => Number(bigEndian
? buffer.readBigUInt64BE(offset)
: buffer.readBigUInt64LE(offset))
const count = readU32(4)
const stride = fat64 ? 32 : 20
const versions = new Set<string>()
for (let index = 0; index < count; index += 1) {
const entry = 8 + index * stride
if (entry + stride > buffer.length) return []
const offset = fat64 ? readU64(entry + 8) : readU32(entry + 8)
const size = fat64 ? readU64(entry + 16) : readU32(entry + 12)
const version = parseThinMachOMinimumVersion(buffer, offset, size)
if (!version) return []
versions.add(version)
}
return [...versions].sort()
}
function addExactMachOArchitectureCheck(
report: PackageSmokeReport,
rootDir: string,
label: string,
targetPath: string,
expected: MachOArch,
) {
const record = { label, path: toRelative(rootDir, targetPath) }
try {
const actual = parseMachOArchitectures(readFileSync(targetPath))
if (actual.length === 1 && actual[0] === expected) {
report.passedChecks.push(record)
return
}
report.notes.push(`${label} expected ${expected}, found ${actual.join(', ') || 'not Mach-O'}.`)
} catch (error) {
report.notes.push(`${label} could not be inspected: ${String(error)}`)
}
report.missingChecks.push(record)
}
function addHelperMinimumSystemCheck(
report: PackageSmokeReport,
rootDir: string,
infoPlistPath: string,
) {
const label = 'macOS cu-helper minimum system version (14.4)'
const record = { label, path: toRelative(rootDir, infoPlistPath) }
try {
const plist = readFileSync(infoPlistPath, 'utf8')
const version = plist.match(
/<key>LSMinimumSystemVersion<\/key>\s*<string>([^<]+)<\/string>/,
)?.[1]?.trim()
if (version === '14.4') {
report.passedChecks.push(record)
return
}
report.notes.push(`${label} expected 14.4, found ${version ?? 'missing'}.`)
} catch (error) {
report.notes.push(`${label} could not be inspected: ${String(error)}`)
}
report.missingChecks.push(record)
}
function addHelperMachOMinimumSystemCheck(
report: PackageSmokeReport,
rootDir: string,
helperExecutable: string,
) {
const label = 'macOS cu-helper Mach-O deployment target (14.4)'
const record = { label, path: toRelative(rootDir, helperExecutable) }
try {
const versions = parseMachOMinimumMacosVersions(readFileSync(helperExecutable))
if (versions.length > 0 && versions.every(version => version === '14.4')) {
report.passedChecks.push(record)
return
}
report.notes.push(`${label} expected 14.4, found ${versions.join(', ') || 'missing'}.`)
} catch (error) {
report.notes.push(`${label} could not be inspected: ${String(error)}`)
}
report.missingChecks.push(record)
}
function parseUpdateMetadataReferences(content: string) {
const references = [] as string[]
const pattern = /^\s*(?:url|path):\s*['"]?([^'"\n]+?)['"]?\s*$/gm
@@ -411,6 +595,104 @@ function addCommandDiagnostics(
report.notes.push(`${label} exited with status ${status}: ${lines.join(' | ')}`)
}
type CodesignMetadata = {
identifier: string | null
authority: string | null
team: string | null
timestamp: string | null
}
export function parseCodesignMetadata(output: string): CodesignMetadata {
const first = (prefix: string) => output
.split(/\r?\n/)
.find(line => line.startsWith(prefix))
?.slice(prefix.length)
.trim() ?? null
const team = first('TeamIdentifier=')
return {
identifier: first('Identifier='),
authority: first('Authority='),
team: team === 'not set' ? null : team,
timestamp: first('Timestamp='),
}
}
function addMacosComputerUseAttestationCheck(
report: PackageSmokeReport,
rootDir: string,
appBundle: string,
sidecar: string,
helperApp: string,
commandRunner: PackageSmokeCommandRunner,
) {
const label = 'macOS Computer Use signing attestation chain'
const record = { label, path: toRelative(rootDir, helperApp) }
if (report.hostPlatform !== 'macos') {
report.notes.push(`${label} was requested but skipped because host platform is ${report.hostPlatform}.`)
return
}
const targets = [
{ name: 'host', path: appBundle, identifier: 'com.claude-code-haha.desktop', deep: true },
{ name: 'sidecar', path: sidecar, identifier: 'com.claude-code-haha.desktop.sidecar', deep: false },
{ name: 'helper', path: helperApp, identifier: 'dev.cchaha.cu-helper', deep: true },
] as const
const metadata: CodesignMetadata[] = []
for (const target of targets) {
const verifyArgs = ['--verify', ...(target.deep ? ['--deep'] : []), '--strict', '--verbose=2', target.path]
const verify = commandRunner('/usr/bin/codesign', verifyArgs)
if (verify.status !== 0) {
report.missingChecks.push(record)
addCommandDiagnostics(report, `${target.name} codesign verification`, verify)
return
}
const details = commandRunner('/usr/bin/codesign', ['-dv', '--verbose=4', target.path])
if (details.status !== 0) {
report.missingChecks.push(record)
addCommandDiagnostics(report, `${target.name} codesign details`, details)
return
}
const parsed = parseCodesignMetadata(`${details.stdout ?? ''}${details.stderr ?? ''}`)
if (
parsed.identifier !== target.identifier
|| !parsed.authority?.startsWith('Developer ID Application:')
|| !parsed.team
|| !parsed.timestamp
) {
report.missingChecks.push(record)
report.notes.push(
`${label} rejected ${target.name}: identifier=${parsed.identifier ?? 'missing'}, `
+ `authority=${parsed.authority ?? 'missing'}, team=${parsed.team ?? 'missing'}, `
+ `timestamp=${parsed.timestamp ? 'present' : 'missing'}.`,
)
return
}
metadata.push(parsed)
}
if (metadata.length !== targets.length) {
report.missingChecks.push(record)
report.notes.push(`${label} could not collect metadata for every required executable.`)
return
}
const [host, sidecarMetadata, helper] = metadata as [
CodesignMetadata,
CodesignMetadata,
CodesignMetadata,
]
if (
host.authority !== sidecarMetadata.authority
|| host.authority !== helper.authority
|| host.team !== sidecarMetadata.team
|| host.team !== helper.team
) {
report.missingChecks.push(record)
report.notes.push(`${label} rejected mismatched Developer ID authority/team values.`)
return
}
report.passedChecks.push(record)
}
function addMacosGatekeeperCheck(
report: PackageSmokeReport,
rootDir: string,
@@ -539,9 +821,13 @@ function inspectMacosArtifacts(rootDir: string, report: PackageSmokeReport, opti
const nodePtyDir = join(unpackedDir, 'node_modules', 'node-pty')
const prebuildsDir = join(nodePtyDir, 'prebuilds')
const sidecarDir = join(unpackedDir, 'src-tauri', 'binaries')
const helperApp = join(sidecarDir, 'cc-haha-computer-use.app')
const helperInfoPlist = join(helperApp, 'Contents', 'Info.plist')
const helperExecutable = join(helperApp, 'Contents', 'MacOS', 'cc-haha-computer-use')
const hostExecutable = join(contentsDir, 'MacOS', report.productName)
addPresenceCheck(report, rootDir, 'macOS Info.plist', join(contentsDir, 'Info.plist'))
addPresenceCheck(report, rootDir, 'macOS app executable', join(contentsDir, 'MacOS', report.productName))
addPresenceCheck(report, rootDir, 'macOS app executable', hostExecutable)
addPresenceCheck(report, rootDir, 'macOS app.asar', join(resourcesDir, 'app.asar'))
addPresenceCheck(report, rootDir, 'macOS unpacked H5 shell', join(unpackedDir, 'dist', 'index.html'))
addInstalledUpdateMetadataCheck(
@@ -552,13 +838,10 @@ function inspectMacosArtifacts(rootDir: string, report: PackageSmokeReport, opti
releaseMode,
)
addPresenceCheck(report, rootDir, 'macOS node-pty package.json', join(nodePtyDir, 'package.json'))
addMatchCheck(
report,
rootDir,
'macOS unpacked sidecar binary',
findMatches(sidecarDir, (candidate) => normalizePath(candidate).includes('/claude-sidecar-')),
sidecarDir,
)
addPresenceCheck(report, rootDir, 'macOS cu-helper app bundle', helperApp)
addPresenceCheck(report, rootDir, 'macOS cu-helper Info.plist', helperInfoPlist)
addPresenceCheck(report, rootDir, 'macOS cu-helper executable', helperExecutable)
if (existsSync(helperInfoPlist)) addHelperMinimumSystemCheck(report, rootDir, helperInfoPlist)
addBundledRipgrepLicenseChecks(report, rootDir, sidecarDir, 'macOS')
addMatchCheck(
report,
@@ -568,23 +851,77 @@ function inspectMacosArtifacts(rootDir: string, report: PackageSmokeReport, opti
normalizePath(candidate).endsWith(bundledRipgrepNeedle('macos'))),
sidecarDir,
)
addMatchCheck(
report,
rootDir,
'macOS node-pty native module',
findMatches(prebuildsDir, (candidate) => normalizePath(candidate).includes('/darwin-') && normalizePath(candidate).endsWith('/pty.node')),
prebuildsDir,
)
addMatchCheck(
report,
rootDir,
'macOS node-pty spawn-helper',
findMatches(prebuildsDir, (candidate) => normalizePath(candidate).includes('/darwin-') && normalizePath(candidate).endsWith('/spawn-helper')),
prebuildsDir,
)
if (report.arch) {
const expectedMachOArch: MachOArch = report.arch === 'arm64' ? 'arm64' : 'x86_64'
const targetTriple = report.arch === 'arm64'
? 'aarch64-apple-darwin'
: 'x86_64-apple-darwin'
const nodePtyArch = report.arch === 'arm64' ? 'darwin-arm64' : 'darwin-x64'
const sidecar = join(sidecarDir, `claude-sidecar-${targetTriple}`)
const pty = join(prebuildsDir, nodePtyArch, 'pty.node')
const spawnHelper = join(prebuildsDir, nodePtyArch, 'spawn-helper')
addPresenceCheck(report, rootDir, `macOS ${report.arch} unpacked sidecar binary`, sidecar)
addPresenceCheck(report, rootDir, `macOS ${report.arch} node-pty native module`, pty)
addPresenceCheck(report, rootDir, `macOS ${report.arch} node-pty spawn-helper`, spawnHelper)
if (existsSync(helperExecutable)) {
addHelperMachOMinimumSystemCheck(report, rootDir, helperExecutable)
}
for (const [label, target] of [
['app executable', hostExecutable],
['sidecar', sidecar],
['cu-helper', helperExecutable],
['node-pty native module', pty],
['node-pty spawn-helper', spawnHelper],
] as const) {
if (existsSync(target)) {
addExactMachOArchitectureCheck(
report,
rootDir,
`macOS ${report.arch} ${label} Mach-O architecture`,
target,
expectedMachOArch,
)
}
}
} else {
addMatchCheck(
report,
rootDir,
'macOS unpacked sidecar binary',
findMatches(sidecarDir, (candidate) => normalizePath(candidate).includes('/claude-sidecar-')),
sidecarDir,
)
addMatchCheck(
report,
rootDir,
'macOS node-pty native module',
findMatches(prebuildsDir, (candidate) => normalizePath(candidate).includes('/darwin-') && normalizePath(candidate).endsWith('/pty.node')),
prebuildsDir,
)
addMatchCheck(
report,
rootDir,
'macOS node-pty spawn-helper',
findMatches(prebuildsDir, (candidate) => normalizePath(candidate).includes('/darwin-') && normalizePath(candidate).endsWith('/spawn-helper')),
prebuildsDir,
)
}
report.notes.push('No GUI launch was attempted. This command only inspects packaged bundle structure and key unpacked resources.')
if (options.requireMacosGatekeeper) {
if (report.arch) {
const targetTriple = report.arch === 'arm64'
? 'aarch64-apple-darwin'
: 'x86_64-apple-darwin'
addMacosComputerUseAttestationCheck(
report,
rootDir,
appBundle,
join(sidecarDir, `claude-sidecar-${targetTriple}`),
helperApp,
options.commandRunner ?? defaultCommandRunner,
)
}
addMacosGatekeeperCheck(report, rootDir, appBundle, options.commandRunner)
} else if (report.hostPlatform === 'macos') {
report.notes.push('macOS Gatekeeper launch approval was not assessed. Add --require-macos-gatekeeper for release-readiness launch policy checks.')