From d77187e9ecb10f941c2bf48f452bb62bbb65d80e Mon Sep 17 00:00:00 2001 From: dh Date: Fri, 11 Sep 2026 19:27:21 +0200 Subject: [PATCH] fix(macos): bundle audio capture for launchd privacy --- .../MacOsMeetingAudioSourceTests.cs | 31 +++++++++++++++---- MeetingAssistant/MeetingAssistant.csproj | 11 +++++-- .../MacOsMeetingAudioCapture/Info.plist | 22 +++++++++---- .../Recording/MacOsMeetingAudioSource.cs | 8 ++++- openspec/specs/meeting-recording/spec.md | 8 +++++ 5 files changed, 65 insertions(+), 15 deletions(-) diff --git a/MeetingAssistant.Tests/MacOsMeetingAudioSourceTests.cs b/MeetingAssistant.Tests/MacOsMeetingAudioSourceTests.cs index c2f5801..2d6cbb7 100644 --- a/MeetingAssistant.Tests/MacOsMeetingAudioSourceTests.cs +++ b/MeetingAssistant.Tests/MacOsMeetingAudioSourceTests.cs @@ -16,7 +16,7 @@ namespace MeetingAssistant.Tests; public sealed class MacOsMeetingAudioSourceTests { [Fact] - public void NativeMicrophoneHelperDeclaresMacOsPrivacyMetadata() + public void NativeAudioCaptureIsPackagedAsSignedMacOsAppForPersistentPrivacyGrant() { if (!OperatingSystem.IsMacOS()) { @@ -25,7 +25,7 @@ public sealed class MacOsMeetingAudioSourceTests var configuration = new DirectoryInfo(AppContext.BaseDirectory) .Parent?.Name ?? "Debug"; - var helperPath = Path.GetFullPath(Path.Combine( + var appPath = Path.GetFullPath(Path.Combine( AppContext.BaseDirectory, "..", "..", @@ -36,11 +36,30 @@ public sealed class MacOsMeetingAudioSourceTests configuration, "net10.0", "Native", - "macos-meeting-audio-capture")); - var helperImage = System.Text.Encoding.UTF8.GetString(File.ReadAllBytes(helperPath)); + "MeetingAssistantAudioCapture.app")); + var infoPlistPath = Path.Combine(appPath, "Contents", "Info.plist"); + var helperPath = Path.Combine(appPath, "Contents", "MacOS", "macos-meeting-audio-capture"); - Assert.Contains("cloud.schweigert.meeting-assistant.audio-capture", helperImage, StringComparison.Ordinal); - Assert.Contains("NSMicrophoneUsageDescription", helperImage, StringComparison.Ordinal); + Assert.True(Directory.Exists(appPath), $"Expected macOS audio capture app at '{appPath}'."); + Assert.True(File.Exists(infoPlistPath), $"Expected Info.plist at '{infoPlistPath}'."); + Assert.True(File.Exists(helperPath), $"Expected native helper at '{helperPath}'."); + + var infoPlist = File.ReadAllText(infoPlistPath); + Assert.Contains("cloud.schweigert.meeting-assistant.audio-capture", infoPlist, StringComparison.Ordinal); + Assert.Contains("NSMicrophoneUsageDescription", infoPlist, StringComparison.Ordinal); + + using var verification = System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo + { + FileName = "/usr/bin/codesign", + ArgumentList = { "--verify", "--deep", "--strict", appPath }, + RedirectStandardError = true, + UseShellExecute = false + }); + Assert.NotNull(verification); + verification.WaitForExit(); + Assert.True( + verification.ExitCode == 0, + $"Expected a valid app-bundle signature: {verification.StandardError.ReadToEnd()}"); } [Fact] diff --git a/MeetingAssistant/MeetingAssistant.csproj b/MeetingAssistant/MeetingAssistant.csproj index de79c42..2eb4c0c 100644 --- a/MeetingAssistant/MeetingAssistant.csproj +++ b/MeetingAssistant/MeetingAssistant.csproj @@ -19,7 +19,8 @@ true $(MSBuildProjectDirectory)/Native/MacOsMeetingAudioCapture/main.swift $(MSBuildProjectDirectory)/Native/MacOsMeetingAudioCapture/Info.plist - Native/macos-meeting-audio-capture + Native/MeetingAssistantAudioCapture.app + $(MacOsAudioCaptureBundlePath)/Contents/MacOS/macos-meeting-audio-capture $(RuntimeIdentifier) $(NETCoreSdkRuntimeIdentifier) x86_64 @@ -83,7 +84,10 @@ AfterTargets="Build" Condition="'$(MacOsNativeHelpersEnabled)' == 'true'"> + + + @@ -93,7 +97,10 @@ AfterTargets="Publish" Condition="'$(MacOsNativeHelpersEnabled)' == 'true'"> - + + + + diff --git a/MeetingAssistant/Native/MacOsMeetingAudioCapture/Info.plist b/MeetingAssistant/Native/MacOsMeetingAudioCapture/Info.plist index f7086a2..bfaccee 100644 --- a/MeetingAssistant/Native/MacOsMeetingAudioCapture/Info.plist +++ b/MeetingAssistant/Native/MacOsMeetingAudioCapture/Info.plist @@ -2,11 +2,21 @@ - CFBundleIdentifier - cloud.schweigert.meeting-assistant.audio-capture - CFBundleName - Meeting Assistant Audio Capture - NSMicrophoneUsageDescription - Meeting Assistant records microphone audio for live meeting transcription. + CFBundleExecutable + macos-meeting-audio-capture + CFBundleIdentifier + cloud.schweigert.meeting-assistant.audio-capture + CFBundleName + Meeting Assistant Audio Capture + CFBundlePackageType + APPL + CFBundleShortVersionString + 1.0 + CFBundleVersion + 1 + LSUIElement + + NSMicrophoneUsageDescription + Meeting Assistant records microphone audio for live meeting transcription. diff --git a/MeetingAssistant/Recording/MacOsMeetingAudioSource.cs b/MeetingAssistant/Recording/MacOsMeetingAudioSource.cs index c766fdb..a1aac17 100644 --- a/MeetingAssistant/Recording/MacOsMeetingAudioSource.cs +++ b/MeetingAssistant/Recording/MacOsMeetingAudioSource.cs @@ -199,7 +199,13 @@ internal sealed class MacOsAudioCaptureProcessFactory : IMacOsAudioCaptureProces throw new PlatformNotSupportedException("The macOS audio capture helper can only run on macOS."); } - var helperPath = Path.Combine(AppContext.BaseDirectory, "Native", "macos-meeting-audio-capture"); + var helperPath = Path.Combine( + AppContext.BaseDirectory, + "Native", + "MeetingAssistantAudioCapture.app", + "Contents", + "MacOS", + "macos-meeting-audio-capture"); if (!File.Exists(helperPath)) { throw new FileNotFoundException( diff --git a/openspec/specs/meeting-recording/spec.md b/openspec/specs/meeting-recording/spec.md index c77c43b..c7b222e 100644 --- a/openspec/specs/meeting-recording/spec.md +++ b/openspec/specs/meeting-recording/spec.md @@ -43,6 +43,8 @@ On Windows, Meeting Assistant SHALL retain the existing NAudio microphone and WA On macOS, Meeting Assistant SHALL capture the default microphone through AVFoundation and computer output through ScreenCaptureKit without requiring a virtual audio device. +The macOS native audio helper SHALL be packaged and launched from a signed application bundle with the stable bundle identifier `cloud.schweigert.meeting-assistant.audio-capture` and a microphone usage description so macOS privacy grants apply to background LaunchAgent capture and persist across deployments. + The macOS capture adapter SHALL convert both native sources to signed 16-bit PCM using the active run's configured sample rate and channel count before passing chunks to the existing managed mixing pipeline. The macOS capture adapter SHALL stop its native capture process when the recording capture token is cancelled. @@ -154,6 +156,12 @@ When no runtime microphone override is selected, the checked microphone SHALL be - **AND** captures computer output through ScreenCaptureKit - **AND** passes both signed 16-bit PCM streams through the existing mixer +#### Scenario: macOS background capture uses a stable privacy identity +- **GIVEN** Meeting Assistant runs as a macOS LaunchAgent +- **WHEN** it starts the native audio helper +- **THEN** it launches the executable from the signed Meeting Assistant audio-capture application bundle +- **AND** macOS evaluates Microphone and Screen Recording/System Audio access against the bundle identifier `cloud.schweigert.meeting-assistant.audio-capture` + #### Scenario: macOS capture uses run audio format - **GIVEN** an active macOS recording configures a sample rate and channel count - **WHEN** the native capture helpers start