Public Access
fix(macos): bundle audio capture for launchd privacy
This commit is contained in:
@@ -16,7 +16,7 @@ namespace MeetingAssistant.Tests;
|
|||||||
public sealed class MacOsMeetingAudioSourceTests
|
public sealed class MacOsMeetingAudioSourceTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public void NativeMicrophoneHelperDeclaresMacOsPrivacyMetadata()
|
public void NativeAudioCaptureIsPackagedAsSignedMacOsAppForPersistentPrivacyGrant()
|
||||||
{
|
{
|
||||||
if (!OperatingSystem.IsMacOS())
|
if (!OperatingSystem.IsMacOS())
|
||||||
{
|
{
|
||||||
@@ -25,7 +25,7 @@ public sealed class MacOsMeetingAudioSourceTests
|
|||||||
|
|
||||||
var configuration = new DirectoryInfo(AppContext.BaseDirectory)
|
var configuration = new DirectoryInfo(AppContext.BaseDirectory)
|
||||||
.Parent?.Name ?? "Debug";
|
.Parent?.Name ?? "Debug";
|
||||||
var helperPath = Path.GetFullPath(Path.Combine(
|
var appPath = Path.GetFullPath(Path.Combine(
|
||||||
AppContext.BaseDirectory,
|
AppContext.BaseDirectory,
|
||||||
"..",
|
"..",
|
||||||
"..",
|
"..",
|
||||||
@@ -36,11 +36,30 @@ public sealed class MacOsMeetingAudioSourceTests
|
|||||||
configuration,
|
configuration,
|
||||||
"net10.0",
|
"net10.0",
|
||||||
"Native",
|
"Native",
|
||||||
"macos-meeting-audio-capture"));
|
"MeetingAssistantAudioCapture.app"));
|
||||||
var helperImage = System.Text.Encoding.UTF8.GetString(File.ReadAllBytes(helperPath));
|
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.True(Directory.Exists(appPath), $"Expected macOS audio capture app at '{appPath}'.");
|
||||||
Assert.Contains("NSMicrophoneUsageDescription", helperImage, StringComparison.Ordinal);
|
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]
|
[Fact]
|
||||||
|
|||||||
@@ -19,7 +19,8 @@
|
|||||||
<MacOsNativeHelpersEnabled>true</MacOsNativeHelpersEnabled>
|
<MacOsNativeHelpersEnabled>true</MacOsNativeHelpersEnabled>
|
||||||
<MacOsAudioCaptureSource>$(MSBuildProjectDirectory)/Native/MacOsMeetingAudioCapture/main.swift</MacOsAudioCaptureSource>
|
<MacOsAudioCaptureSource>$(MSBuildProjectDirectory)/Native/MacOsMeetingAudioCapture/main.swift</MacOsAudioCaptureSource>
|
||||||
<MacOsAudioCaptureInfoPlist>$(MSBuildProjectDirectory)/Native/MacOsMeetingAudioCapture/Info.plist</MacOsAudioCaptureInfoPlist>
|
<MacOsAudioCaptureInfoPlist>$(MSBuildProjectDirectory)/Native/MacOsMeetingAudioCapture/Info.plist</MacOsAudioCaptureInfoPlist>
|
||||||
<MacOsAudioCaptureOutputPath>Native/macos-meeting-audio-capture</MacOsAudioCaptureOutputPath>
|
<MacOsAudioCaptureBundlePath>Native/MeetingAssistantAudioCapture.app</MacOsAudioCaptureBundlePath>
|
||||||
|
<MacOsAudioCaptureOutputPath>$(MacOsAudioCaptureBundlePath)/Contents/MacOS/macos-meeting-audio-capture</MacOsAudioCaptureOutputPath>
|
||||||
<MacOsAudioCaptureRid Condition="'$(RuntimeIdentifier)' != ''">$(RuntimeIdentifier)</MacOsAudioCaptureRid>
|
<MacOsAudioCaptureRid Condition="'$(RuntimeIdentifier)' != ''">$(RuntimeIdentifier)</MacOsAudioCaptureRid>
|
||||||
<MacOsAudioCaptureRid Condition="'$(MacOsAudioCaptureRid)' == ''">$(NETCoreSdkRuntimeIdentifier)</MacOsAudioCaptureRid>
|
<MacOsAudioCaptureRid Condition="'$(MacOsAudioCaptureRid)' == ''">$(NETCoreSdkRuntimeIdentifier)</MacOsAudioCaptureRid>
|
||||||
<MacOsAudioCaptureArchitecture Condition="'$(MacOsAudioCaptureRid)' == 'osx-x64'">x86_64</MacOsAudioCaptureArchitecture>
|
<MacOsAudioCaptureArchitecture Condition="'$(MacOsAudioCaptureRid)' == 'osx-x64'">x86_64</MacOsAudioCaptureArchitecture>
|
||||||
@@ -83,7 +84,10 @@
|
|||||||
AfterTargets="Build"
|
AfterTargets="Build"
|
||||||
Condition="'$(MacOsNativeHelpersEnabled)' == 'true'">
|
Condition="'$(MacOsNativeHelpersEnabled)' == 'true'">
|
||||||
<MakeDir Directories="$(TargetDir)Native" />
|
<MakeDir Directories="$(TargetDir)Native" />
|
||||||
|
<MakeDir Directories="$(TargetDir)$(MacOsAudioCaptureBundlePath)/Contents/MacOS" />
|
||||||
|
<Copy SourceFiles="$(MacOsAudioCaptureInfoPlist)" DestinationFiles="$(TargetDir)$(MacOsAudioCaptureBundlePath)/Contents/Info.plist" />
|
||||||
<Exec Command="/usr/bin/xcrun swiftc -parse-as-library -O -target $(MacOsAudioCaptureArchitecture)-apple-macos13.0 -framework AVFoundation -framework CoreMedia -framework ScreenCaptureKit -Xlinker -sectcreate -Xlinker __TEXT -Xlinker __info_plist -Xlinker "$(MacOsAudioCaptureInfoPlist)" "$(MacOsAudioCaptureSource)" -o "$(TargetDir)$(MacOsAudioCaptureOutputPath)"" />
|
<Exec Command="/usr/bin/xcrun swiftc -parse-as-library -O -target $(MacOsAudioCaptureArchitecture)-apple-macos13.0 -framework AVFoundation -framework CoreMedia -framework ScreenCaptureKit -Xlinker -sectcreate -Xlinker __TEXT -Xlinker __info_plist -Xlinker "$(MacOsAudioCaptureInfoPlist)" "$(MacOsAudioCaptureSource)" -o "$(TargetDir)$(MacOsAudioCaptureOutputPath)"" />
|
||||||
|
<Exec Command="/usr/bin/codesign --force --deep --sign - "$(TargetDir)$(MacOsAudioCaptureBundlePath)"" />
|
||||||
<Exec Command="/usr/bin/xcrun swiftc -parse-as-library -O -target $(MacOsAudioCaptureArchitecture)-apple-macos13.0 -framework AppKit -framework Carbon -framework WebKit "$(MacOsDesktopControlsSource)" -o "$(TargetDir)$(MacOsDesktopControlsOutputPath)"" />
|
<Exec Command="/usr/bin/xcrun swiftc -parse-as-library -O -target $(MacOsAudioCaptureArchitecture)-apple-macos13.0 -framework AppKit -framework Carbon -framework WebKit "$(MacOsDesktopControlsSource)" -o "$(TargetDir)$(MacOsDesktopControlsOutputPath)"" />
|
||||||
<Exec Command="/usr/bin/xcrun swiftc -parse-as-library -O -target $(MacOsAudioCaptureArchitecture)-apple-macos13.0 -framework AppKit -framework CoreGraphics -framework EventKit -framework ImageIO -framework UniformTypeIdentifiers -Xlinker -sectcreate -Xlinker __TEXT -Xlinker __info_plist -Xlinker "$(MacOsMeetingIntegrationsInfoPlist)" "$(MacOsMeetingIntegrationsSource)" -o "$(TargetDir)$(MacOsMeetingIntegrationsOutputPath)"" />
|
<Exec Command="/usr/bin/xcrun swiftc -parse-as-library -O -target $(MacOsAudioCaptureArchitecture)-apple-macos13.0 -framework AppKit -framework CoreGraphics -framework EventKit -framework ImageIO -framework UniformTypeIdentifiers -Xlinker -sectcreate -Xlinker __TEXT -Xlinker __info_plist -Xlinker "$(MacOsMeetingIntegrationsInfoPlist)" "$(MacOsMeetingIntegrationsSource)" -o "$(TargetDir)$(MacOsMeetingIntegrationsOutputPath)"" />
|
||||||
</Target>
|
</Target>
|
||||||
@@ -93,7 +97,10 @@
|
|||||||
AfterTargets="Publish"
|
AfterTargets="Publish"
|
||||||
Condition="'$(MacOsNativeHelpersEnabled)' == 'true'">
|
Condition="'$(MacOsNativeHelpersEnabled)' == 'true'">
|
||||||
<MakeDir Directories="$(PublishDir)Native" />
|
<MakeDir Directories="$(PublishDir)Native" />
|
||||||
<Copy SourceFiles="$(TargetDir)$(MacOsAudioCaptureOutputPath)" DestinationFolder="$(PublishDir)Native" />
|
<MakeDir Directories="$(PublishDir)$(MacOsAudioCaptureBundlePath)/Contents/MacOS" />
|
||||||
|
<Copy SourceFiles="$(TargetDir)$(MacOsAudioCaptureOutputPath)" DestinationFiles="$(PublishDir)$(MacOsAudioCaptureOutputPath)" />
|
||||||
|
<Copy SourceFiles="$(MacOsAudioCaptureInfoPlist)" DestinationFiles="$(PublishDir)$(MacOsAudioCaptureBundlePath)/Contents/Info.plist" />
|
||||||
|
<Exec Command="/usr/bin/codesign --force --deep --sign - "$(PublishDir)$(MacOsAudioCaptureBundlePath)"" />
|
||||||
<Copy SourceFiles="$(TargetDir)$(MacOsDesktopControlsOutputPath)" DestinationFolder="$(PublishDir)Native" />
|
<Copy SourceFiles="$(TargetDir)$(MacOsDesktopControlsOutputPath)" DestinationFolder="$(PublishDir)Native" />
|
||||||
<Copy SourceFiles="$(TargetDir)$(MacOsMeetingIntegrationsOutputPath)" DestinationFolder="$(PublishDir)Native" />
|
<Copy SourceFiles="$(TargetDir)$(MacOsMeetingIntegrationsOutputPath)" DestinationFolder="$(PublishDir)Native" />
|
||||||
</Target>
|
</Target>
|
||||||
|
|||||||
@@ -2,11 +2,21 @@
|
|||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>cloud.schweigert.meeting-assistant.audio-capture</string>
|
<string>macos-meeting-audio-capture</string>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>Meeting Assistant Audio Capture</string>
|
<string>cloud.schweigert.meeting-assistant.audio-capture</string>
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>CFBundleName</key>
|
||||||
<string>Meeting Assistant records microphone audio for live meeting transcription.</string>
|
<string>Meeting Assistant Audio Capture</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1</string>
|
||||||
|
<key>LSUIElement</key>
|
||||||
|
<true/>
|
||||||
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
|
<string>Meeting Assistant records microphone audio for live meeting transcription.</string>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -199,7 +199,13 @@ internal sealed class MacOsAudioCaptureProcessFactory : IMacOsAudioCaptureProces
|
|||||||
throw new PlatformNotSupportedException("The macOS audio capture helper can only run on macOS.");
|
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))
|
if (!File.Exists(helperPath))
|
||||||
{
|
{
|
||||||
throw new FileNotFoundException(
|
throw new FileNotFoundException(
|
||||||
|
|||||||
@@ -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.
|
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 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.
|
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** captures computer output through ScreenCaptureKit
|
||||||
- **AND** passes both signed 16-bit PCM streams through the existing mixer
|
- **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
|
#### Scenario: macOS capture uses run audio format
|
||||||
- **GIVEN** an active macOS recording configures a sample rate and channel count
|
- **GIVEN** an active macOS recording configures a sample rate and channel count
|
||||||
- **WHEN** the native capture helpers start
|
- **WHEN** the native capture helpers start
|
||||||
|
|||||||
Reference in New Issue
Block a user