diff --git a/apps/desktop/resources/entitlements.mac.plist b/apps/desktop/resources/entitlements.mac.plist new file mode 100644 index 00000000000..bd655e84ffa --- /dev/null +++ b/apps/desktop/resources/entitlements.mac.plist @@ -0,0 +1,12 @@ + + + + + com.apple.security.cs.allow-jit + + com.apple.security.cs.allow-unsigned-executable-memory + + com.apple.security.device.audio-input + + + diff --git a/apps/desktop/scripts/electron-launcher.mjs b/apps/desktop/scripts/electron-launcher.mjs index 9d7c522781a..15975d5f1c6 100644 --- a/apps/desktop/scripts/electron-launcher.mjs +++ b/apps/desktop/scripts/electron-launcher.mjs @@ -55,6 +55,16 @@ function patchMainBundleInfoPlist(appBundlePath, iconPath) { copyFileSync(iconPath, join(resourcesDir, "electron.icns")); } +function resignAppBundle(appBundlePath) { + const result = spawnSync("codesign", ["--force", "--deep", "--sign", "-", appBundlePath], { + encoding: "utf8", + }); + if (result.status !== 0) { + const detail = result.stderr?.trim() || result.stdout?.trim() || ""; + throw new Error(`Failed to re-sign app bundle at ${appBundlePath}: ${detail}`); + } +} + function patchHelperBundleInfoPlists(appBundlePath) { const frameworksDir = join(appBundlePath, "Contents", "Frameworks"); if (!existsSync(frameworksDir)) { @@ -127,6 +137,7 @@ function buildMacLauncher(electronBinaryPath) { cpSync(sourceAppBundlePath, targetAppBundlePath, { recursive: true }); patchMainBundleInfoPlist(targetAppBundlePath, iconPath); patchHelperBundleInfoPlists(targetAppBundlePath); + resignAppBundle(targetAppBundlePath); writeFileSync(metadataPath, `${JSON.stringify(expectedMetadata, null, 2)}\n`); return targetBinaryPath; diff --git a/scripts/build-desktop-artifact.ts b/scripts/build-desktop-artifact.ts index 8823b885134..55da617a848 100644 --- a/scripts/build-desktop-artifact.ts +++ b/scripts/build-desktop-artifact.ts @@ -464,6 +464,26 @@ const createBuildConfig = Effect.fn("createBuildConfig")(function* ( target: target === "dmg" ? [target, "zip"] : [target], icon: "icon.icns", category: "public.app-category.developer-tools", + extendInfo: { + NSMicrophoneUsageDescription: + "T3 Code needs microphone access so that apps launched from its integrated terminal can use audio input.", + NSCameraUsageDescription: + "T3 Code needs camera access so that apps launched from its integrated terminal can use the camera.", + NSAppleEventsUsageDescription: + "T3 Code needs to send Apple events to control other applications.", + }, + ...(signed + ? { + // Signed builds: hardened runtime + entitlements (required for notarization). + entitlements: "apps/desktop/resources/entitlements.mac.plist", + entitlementsInherit: "apps/desktop/resources/entitlements.mac.plist", + } + : { + // Unsigned builds: ad-hoc sign without hardened runtime so macOS TCC + // can identify the app and show privacy permission prompts. + identity: "-", + hardenedRuntime: false, + }), }; } @@ -654,7 +674,14 @@ const buildDesktopArtifact = Effect.fn("buildDesktopArtifact")(function* ( } } if (!options.signed) { - buildEnv.CSC_IDENTITY_AUTO_DISCOVERY = "false"; + // On macOS, we still need at least ad-hoc signing (identity: "-" in the + // mac build config) so macOS TCC can attribute privacy permissions (mic, + // camera, etc.) to the app. Disabling signing entirely would leave the + // app with an unbound Info.plist and no sealed resources, causing macOS to + // silently deny TCC prompts for child processes launched from the terminal. + if (options.platform !== "mac") { + buildEnv.CSC_IDENTITY_AUTO_DISCOVERY = "false"; + } delete buildEnv.CSC_LINK; delete buildEnv.CSC_KEY_PASSWORD; delete buildEnv.APPLE_API_KEY;