Skip to content

v1.11: Mac Catalyst, security hardening, source-centric workflow - #91

Merged
bisonbet merged 27 commits into
mainfrom
v1.11
Jun 2, 2026
Merged

v1.11: Mac Catalyst, security hardening, source-centric workflow#91
bisonbet merged 27 commits into
mainfrom
v1.11

Conversation

@bisonbet

@bisonbet bisonbet commented Jun 1, 2026

Copy link
Copy Markdown
Owner

Summary

v1.11 is a substantial release. The biggest single change is that the app now ships on Mac via Mac Catalyst, but the cycle also includes security/credential hardening, a source-centric transcript/summary workflow, the experimental MLX Swift engine, and a number of pre-v1.11 features (archive to iCloud, summary attachments, recording title editing, watch complications, Google Calendar integration).

Highlights

  • Mac Catalyst. The app builds and runs as a Catalyst Mac app. Recording on Mac uses a dedicated AVAudioEngine pipeline, settings sheets are Form-based for reliable scrolling, navigation switched to NavigationStack everywhere, llama.cpp ships with a manually built Catalyst slice, and the llama Metal crash on quit is fixed.
  • Pause / Resume recording. Universal pause and resume on iOS, iPadOS, and Mac.
  • Security hardening. API keys, AWS credentials, and Bedrock session tokens moved to the iOS Keychain with automatic legacy migration. Process-environment AWS credentials are cleared at launch. File protection applied to recordings, transcripts, notes, attachments, and Core Data. User-configurable AI endpoints now validated to block insecure cleartext destinations (Development Mode override per service). Share Extension imports require one-time tokens. iCloud settings backups exclude sensitive data; APNs uses per-config APS_ENVIRONMENT.
  • Source-centric workflow. "Generate Transcript" lives on the recording; "Generate Summary" lives on the transcript. Buttons disappear once the artifact exists; regeneration happens from the existing detail view.
  • MLX Swift engine (experimental). Apple Silicon-native on-device summarization behind the experimental engines toggle.
  • llama.cpp updated to b9134 with the Catalyst slice baked in.

Also included in this cycle

  • Archive recordings to iCloud Drive with tracked restore pointers and post-restore cleanup
  • Summary attachments (text/PDF/Quick Look fallback) and note exports
  • Recording title editing from the audio player and transcript editor
  • Explicit recording start timestamps; cleaner export filenames
  • Watch complications target and Control Center recording widget
  • Google Calendar destination for tasks and reminders
  • Comedy Mode tone option for AI summaries
  • Swap iPad sidebar order (Transcripts before Summaries)
  • Misc bug fixes and Codex review fixes

Docs

  • README.md rewritten to cover all of the above and corrected the On-Device AI requirement (6 GB+ RAM, not iPhone 15 Pro+).
  • docs/bisonnotes-ai-guide.html (WordPress user guide) bumped to v1.11, new "What's new", "Mac Catalyst", "Pause & Resume", "Privacy & Security", and "MLX Swift" sections; first-transcript/first-summary instructions updated for the new workflow; "all 7 / all 8 engines" inconsistency fixed.

Dependencies

  • aws-sdk-swift 1.7.8 → 1.7.9
  • FluidAudio 0.14.7 → 0.14.8
  • smithy-swift 0.213.0 → 0.214.0
  • bisonbet/textual: tracked-branch revision bump (Mac Catalyst guards)

Test plan

  • Build and run on iPhone (real device, not simulator) — record, pause, resume, stop; generate transcript from the recording row; generate summary from the transcript
  • Build and run on iPad — same flow, confirm sidebar order (Transcripts before Summaries)
  • Build and run on Mac Catalyst — record (built-in mic, USB/Bluetooth), pause/resume, transcribe, summarize; confirm settings sheets scroll cleanly; confirm clean quit (no Metal crash)
  • Confirm Keychain migration: install over a prior v1.10 build with stored API keys → verify keys still work and are no longer in UserDefaults
  • Confirm endpoint policy: try configuring a public HTTP endpoint for OpenAI Compatible → should be blocked unless Development Mode is on; private/loopback endpoints should work
  • Confirm Share Extension import still works end to end
  • Confirm MLX Swift engine appears only when the experimental toggle is on, and disabling the toggle gracefully reverts active engines
  • Confirm Apple Watch complications appear and update during recording
  • Confirm Control Center recording widget starts/stops a recording on iOS 18+
  • Confirm Google Calendar destination opens the app (if installed) or web fallback
  • Smoke-test Archive to iCloud Drive: archive, offload local audio, restore, confirm archived cloud copy is deleted post-restore

🤖 Generated with Claude Code

bisonbet and others added 27 commits June 1, 2026 19:31
…FORM_SPECIALIZATION

- Remove LSRequiresIPhoneOS from Info.plist so macOS accepts the app bundle
- Add ALLOW_TARGET_PLATFORM_SPECIALIZATION = YES to Debug and Release build
  configs so Xcode uses the existing macos-arm64_x86_64 slice in llama.xcframework
  when linking for Mac Catalyst (no maccatalyst slice exists in the vendored build)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add ios-arm64-maccatalyst slice to llama.xcframework (arm64 extracted from
  the macOS fat binary) so the Catalyst linker can find the framework
- Make INFOPLIST_FILE unconditional in Debug and Release configs — the
  previous [sdk=iphone*] condition was skipped for Catalyst's macosx SDK,
  causing the code signing failure

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Patch llama Catalyst binary with vtool to set platform=MACCATALYST,
  removing the 'built for macOS' linker mismatch warning
- Add platformFilters=(ios) to the Controls extension embed build file
  so Xcode skips embedding it on Catalyst (extension has SUPPORTS_MACCATALYST=NO
  so it never builds, causing 'no such file' when the main app tries to embed it)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a 'Mac Catalyst Build Notes' section to CLAUDE.md with step-by-step
commands to recreate the ios-arm64-maccatalyst slice (lipo + vtool) whenever
llama.xcframework is rebuilt. Also notes the textual fork Catalyst fix.
Updates README.md llama.cpp entry with a concise Mac Catalyst warning.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mac Catalyst does not use shallow bundles — Xcode expects the framework
binary at Versions/A/llama with top-level symlinks, not a flat iOS layout.
Restructured ios-arm64-maccatalyst/llama.framework to match the macOS
versioned convention and updated BinaryPath in xcframework Info.plist.
Updated CLAUDE.md build instructions accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BisonNotes Share.appex is iOS-only and cannot be embedded in a macOS app.
Add platformFilters=(ios) to its embed build file entry so Xcode skips it
when building for Mac Catalyst.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
BisonNotes AI Watch App.app is watchOS-only. Add platformFilters=(ios) so
it is skipped when building for Mac Catalyst.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eenCovers

NavigationView defaults to a two-column split layout on Mac Catalyst,
which breaks scrolling inside modal sheets. Replacing it with
NavigationStack restores single-column presentation everywhere.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On Mac Catalyst, SFSafariViewController is unavailable. Add the same
onChange handler pattern used in SimpleSettingsView and RecordingsView
so tapping the console buttons opens Safari instead of an empty sheet.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three changes:
1. On Mac Catalyst, use configureMixedAudioSession() instead of
   configureBackgroundRecording() — background audio modes are iOS-specific
   and the UIBackgroundModes check was blocking the audio session setup.
2. Add logging for the permission granted/denied result so failures
   are visible in the console.
3. Observe recorderVM.errorMessage in RecordingsView and surface it as
   an alert — previously errors like mic permission denied were silent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
AVAudioApplication.requestRecordPermission does not trigger the macOS
TCC privacy dialog. On Mac Catalyst, switch to AVCaptureDevice.requestAccess
which properly integrates with System Settings → Privacy & Security →
Microphone. Also handles the denied/restricted case with a clear error
message, and uses configureMixedAudioSession instead of
configureBackgroundRecording (background audio modes are iOS-only).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
NavigationView defaults to a two-column split layout on Mac Catalyst,
which breaks scrolling in any view presented modally. This completes the
full codebase migration — SummaryDetailView, all settings views,
TranscriptViews, RecordingsListView, BackgroundProcessingView,
and all utility dialogs now use NavigationStack.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
On Mac, quitting via the menu calls NSApplication.terminate: → exit().
ggml_metal_device's C++ static destructor then runs before Swift deinits,
triggering GGML_ASSERT([rsets->data count] == 0) if Metal command buffers
are still live.

Add a UIApplication.willTerminateNotification observer in OnDeviceLLMEngine
that calls service.unloadModel() before exit(), forcing llama_free and
llama_model_free to run while the Metal runtime is still valid.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Mark the function @mainactor so direct calls to didReceiveMicrophonePermission
are valid, and replace DispatchQueue.main.async with Task { @mainactor in }
in the AVCaptureDevice callback so the Swift concurrency actor hop is explicit.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
startRecording() and startBackgroundRecording() are nonisolated, so they
cannot call a @mainactor method directly. Wrap the call in
Task { @mainactor in } to hop to the main actor explicitly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
AVAudioSession.interruptionNotification and routeChangeNotification rely
on Mach ports that don't exist on Mac, producing hundreds of
"cannot add handler to 4/3 from 1 - dropping" log lines per session.
Phone-call interruptions and Bluetooth routing are iOS-only concerns
so these observers are simply skipped on Mac Catalyst.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
AVAudioSession.requestRecordPermission is deprecated in Mac Catalyst 17.
Switch to AVAudioApplication.shared.recordPermission / requestRecordPermission
as the compiler recommends.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SwiftUI ScrollView is broken inside Mac Catalyst sheets — diagnostics
show HostingScrollView gets deallocated mid-interaction and presenting
controllers report as detached. Form (UITableView-backed) is the only
pattern that scrolls reliably, matching what AISettingsView already
does.

- Convert SettingsView, SummaryDetailView, TranscriptDetailView,
  EditableTranscriptView, and AcknowledgementsView from ScrollView
  bodies to NavigationStack { Form }
- Add Done toolbar button to BackgroundProcessingView (was unreachable
  on Mac)
- Add .contentShape(Rectangle()) to Form button rows for full-row tap
  targets
- Fix nested text/PDF attachment sheets in SummaryDetailView
- Add MacScrollDiagnosticView harness for verifying the fix from a
  top-level (non-detached) presentation context
- Silence AVAudioSession Mach port warnings on Mac Catalyst by guarding
  setCategory/setActive calls
- Add com.apple.security.device.audio-input entitlement for mic access

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- SummaryDetailView.rebuildSummaryData no longer falls through to the
  legacy initializer when recordingId is nil. The legacy init generated
  a fresh UUID for `id`, which orphaned attachments/notes keyed off the
  original summary id. Make the id-preserving init's recordingId param
  optional and use it for both branches.
- RecordingArchiveService now persists a security-scoped bookmark for
  archive destinations on Mac Catalyst (and stops using .minimalBookmark
  on iOS, which discarded the picker-granted scope). Resolution side
  matches with .withSecurityScope on Catalyst so a relaunch can still
  startAccessingSecurityScopedResource() against the saved location.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mac Catalyst recording was broken because AVAudioRecorder cannot
negotiate the input format with CoreAudio without an AVAudioSession,
producing AudioConverter -50 errors and partial/empty files. Replace
the AVAudioRecorder code path on Catalyst with AVAudioEngine +
AVAudioFile (new AudioRecorderViewModel+CatalystEngine.swift): the
input node tap delivers PCM buffers that AVAudioFile encodes directly
to AAC/M4A, matching the iOS on-disk format exactly. setCategory is
re-enabled on Catalyst because the engine's input audio unit needs it
to initialize (AUIOBase Initialize -50 otherwise).

Add Pause/Resume to recording for all platforms:
- iOS: AVAudioRecorder.pause()/.record() — single file
- Catalyst: AVAudioEngine input tap remove/install — single file
- ViewModel: pauseRecording(), resumeRecording(), isPaused
- UI: side-by-side Pause+Stop / Resume+Stop buttons; "Paused" badge
- Live transcription mode rejects pause (AVAudioEngine path can't
  cleanly pause without dropping the recognizer state)

Gate iOS-only paths that produced log spam or cosmetic errors on
Catalyst:
- AVAudioSession property accesses (input selection, route info,
  restoreAudioSession, requestBackgroundAudioCapability)
- UIDevice battery monitoring (PerformanceOptimizer, recording
  warnings, BackgroundProcessingManager diagnostics)
- UIApplication.beginBackgroundTask + keep-alive silent audio in
  BackgroundProcessingManager and AudioRecorderViewModel
- Recording-timer "unexpected stop" recovery that surfaced misleading
  "Microphone became unavailable" errors
- Notification permission failures demoted from error to debug

Also reset recordingState explicitly on start/stop so a paused state
can't leak across recordings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Bump MARKETING_VERSION and CFBundleShortVersionString to 1.11
- Remove MacScrollDiagnosticView harness and its Scroll Tests entry now
  that Mac Catalyst sheet scrolling is fixed
- In simple setup's Save & Configure, honor any already-selected local
  AI engine (On-Device LLM, MLX, or Apple Intelligence) along with its
  selected model; only fall back to On-Device LLM + Granite Micro when
  no local engine is configured

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Download and replace llama.xcframework with latest release b9134
- Create ios-arm64-maccatalyst slice from macOS arm64 binary
- Apply vtool patch to set platform to MACCATALYST
- Add maccatalyst entry to Info.plist
- Update README-LLAMA-SETUP.md with version history and Mac Catalyst instructions

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
- Bump textual to 0c2c3b5 to pick up the bisonbet fork's Catalyst font
  fix. The previous pin (5b06b81) was upstream tip without the
  !targetEnvironment(macCatalyst) guards on the AppKit path, which
  caused FontDescriptor to resolve to NSFontDescriptor and broke
  TextStyleFontProvider (pointSize, withDesign, addingAttributes,
  SystemDesign, TraitKey.weight all unavailable on Catalyst).
- MLXSwiftEngine: replace direct os_proc_available_memory() with
  Self.availableMemoryForModelLoad(). That API returns 0 on Mac
  Catalyst (no jetsam limits), which made the pre-load guard always
  fail. On Catalyst, read host_statistics64 / HOST_VM_INFO64 instead
  to compute free + inactive + speculative + purgeable page memory.
- SummaryDetailView: add .buttonStyle(.borderless) to the Regenerate
  Summary, Edit/Add Location, and Delete buttons. They share one
  Form Section row, so without an explicit buttonStyle a tap on any
  one fires all three actions. That made tapping Regenerate also
  trigger the Delete Summary alert, which briefly flashed before the
  regen flow's dismiss() tore the view down.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Generate Transcript now lives on the recording (row + AudioPlayerView)
and Generate Summary lives on the transcript (editor + second row
button in the Transcripts tab). Buttons disappear once the artifact
exists; regeneration happens from the existing detail views.

A shared TranscriptionStarter service owns the audio-cleanup queue so
all three entry points share one serial queue. Fixes a Mac Catalyst
freeze on the Transcripts list by replacing per-row disk-touching
calls (getAbsoluteURL, recording.summary relationship fault) with
cheap attribute reads, and falls back to an all-inline list on
Catalyst since the preview-+-"More" NavigationLink push wedged the
responder chain on that platform.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
FluidAudioManager migrates to the new AsrManager(config:models:)
single-step initializer and the decoder-state-based transcribe API
(TdtDecoderState built from decoderLayerCount, passed inout). Pulls
in swift-argument-parser and coordinated point upgrades across the
AWS stack and Apple/community NIO/crypto packages.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move provider API keys, AWS credentials, and Bedrock session tokens into Keychain-backed storage, migrate legacy UserDefaults values, clear process-wide AWS credential environment variables, and use explicit AWS SDK credential resolvers for Bedrock, Transcribe, and background jobs.

Add endpoint validation for user-configurable AI services so public HTTP and WS endpoints are blocked by default while local/private endpoints remain allowed, with Development Mode warnings/toggles in OpenAI, OpenAI-compatible, Ollama, and Whisper settings.

Require one-time share-extension import tokens before the main app scans the shared container, avoid logging full import URLs, protect shared import files, and disable iTunes file sharing.

Enforce AWS Bedrock model response size limits before JSON parsing and strip raw control characters from Bedrock response data before decode.

Apply explicit file protection to recordings, imported/restored audio, watch backups, transcript placeholder audio, attachment metadata/files, persistent error logs, and Core Data SQLite files.

Set APNs entitlement through per-configuration APS_ENVIRONMENT values so Debug uses development and Release uses production.

Default iCloud sensitive settings backups off, keep API keys and AWS credentials out of iCloud settings backups, and restore legacy sensitive backup values into Keychain when encountered.

Add focused tests for endpoint policy, Keychain migration, and share import authorization, and update test helpers for current model/status APIs.

Validation: git diff --cached --check; xcodebuild -project 'BisonNotes AI/BisonNotes AI.xcodeproj' -scheme 'BisonNotes AI' -configuration Debug -destination 'generic/platform=iOS Simulator' -derivedDataPath /private/tmp/BisonNotesAIDerivedData CODE_SIGNING_ALLOWED=NO build.
Update the README and the WordPress user guide (docs/bisonnotes-ai-guide.html)
to reflect everything shipped in v1.11, and pick up minor dependency updates.

Docs:
- Mac Catalyst support, the AVAudioEngine recording pipeline, and the
  llama.xcframework Catalyst slice requirement
- Pause and Resume recording across iOS, iPadOS, and Mac
- Keychain-backed credential storage, file protection, endpoint validation,
  and Share Extension token authorization
- The source-centric workflow (Generate Transcript on the recording,
  Generate Summary on the transcript)
- MLX Swift experimental summarization engine and the experimental toggle
- Watch complications, Control Center recording widget, summary attachments,
  recording title editing, Comedy Mode, and Google Calendar integration
- Corrected the On-Device AI requirement (6 GB+ RAM, not iPhone 15 Pro+)
- Fixed the "all 7 / all 8 AI engines" inconsistency in the user guide
- Hero version badge bumped v1.8 -> v1.11; new Privacy & Security section
  added before Troubleshooting

Dependencies (Package.resolved):
- aws-sdk-swift 1.7.8 -> 1.7.9
- FluidAudio 0.14.7 -> 0.14.8
- smithy-swift 0.213.0 -> 0.214.0
- bisonbet/textual: tracked-branch revision bump

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 710bf006f0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

return isPrivateIPv4(ipv4)
}

return normalized.hasPrefix("fc") || normalized.hasPrefix("fd") || normalized.hasPrefix("fe80:")

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate IPv6 private literals before allowing HTTP

This prefix check treats any hostname beginning with fc or fd as a private IPv6 address, so a public DNS name such as http://fd-example.com bypasses the new HTTP block and can receive recordings/transcripts in cleartext without Development Mode. Please parse IPv6 literals explicitly (after removing brackets) and only allow actual ULA/link-local addresses.

Useful? React with 👍 / 👎.

import Foundation

enum AppFileProtection {
static let sensitiveFileProtection: FileProtectionType = .complete

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve access for locked-device background work

Using .complete as the global protection class is applied to newly recorded/imported audio and the Core Data store, but .complete makes files unavailable whenever the device is locked. In this app's background recording/processing flows, any transcription/cleanup or metadata save that opens these files after the user locks the phone can fail even though background audio/tasks are intended to keep running; use a class such as complete-until-first-authentication unless the flow is strictly foreground-only.

Useful? React with 👍 / 👎.

@bisonbet
bisonbet merged commit e55c9d6 into main Jun 2, 2026
@bisonbet
bisonbet deleted the v1.11 branch June 2, 2026 21:34
@bisonbet
bisonbet restored the v1.11 branch June 2, 2026 21:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant