fix(app,wizard,scripts): suppress wizard-redundant banners + wire DevOverlay menu + add camera entitlement

User reported four issues clustered around setup UX and missing
features after the previous round of fixes:

1. 'No API key configured' banner pops up DURING the wizard's API-key
   stage. Same for 'Limited hotkey mode' during the TCC stage. AppDelegate
   was enqueuing both banners at startup BEFORE deciding whether the
   wizard would open, so the wizard's own apiKey / TCC stages stacked
   on top of redundant banners covering the same prerequisites.

2. AppDelegate called hidProbe.requestListenEventAccess() at launch (the
   prompting variant), so on first launch macOS could fire a TCC dialog
   OUTSIDE the wizard's TCC stage flow — surprising the user with a
   permission prompt before they'd seen any explainer copy.

3. 'Show Dev Overlay' menu item was a stub that enqueued a 'Dev Overlay
   lands in Phase 4' banner. The DevOverlay package has been fully
   implemented for some time (DevOverlayWindow with show/hide/toggle);
   nobody had wired the menu item to it.

4. Camera entitlement (com.apple.security.device.camera) was missing from
   BOTH Jarvis.Debug.entitlements and Jarvis.Release.entitlements. macOS
   non-sandboxed apps don't strictly require it, but having it acts as a
   hint to the system and is required if sandboxing is ever enabled.
   Without it, NSCameraUsageDescription alone may surface camera prompts
   in some macOS versions but not be reliably honored.

Fixes:

- AppDelegate.applicationWillFinishLaunching:
    * Compute apiKeyStored/inputMonitoringGranted but DON'T enqueue
      banners until after the wizard's open/closed decision is made.
    * Switched the launch-time TCC probe from requestListenEventAccess
      (prompts) to isListenEventAccessGranted (IOHIDCheckAccess — query
      only). The wizard's TCC stage owns the actual prompt now.
    * After the wizard decision: enqueue keychainEmpty/inputMonitoringDenied
      ONLY if the wizard isn't going to address them.

- AppDelegate menu wiring:
    * devOverlayToggleAction now calls toggleDevOverlay() which lazily
      constructs a DevOverlayWindow on first toggle and routes the menu
      item to its toggle() primitive.
    * Removed the showDevOverlayStubBanner placeholder.
    * Added 'import DevOverlay' (project.yml + Package wiring also updated).

- project.yml:
    * Added DevOverlay package declaration (path: packages/DevOverlay).
    * Added DevOverlay product to the Jarvis target's dependencies.
    * Re-ran xcodegen to regenerate Jarvis.xcodeproj.

- App/Jarvis.{Debug,Release}.entitlements:
    * Added com.apple.security.device.camera. Doesn't violate the
      MAIN_FORBIDDEN list in verify-entitlements.sh.

Verified: check-app-builds clean; codesign reflects the new camera
entitlement; relaunched (PID 43128); 'Show Dev Overlay' now toggles
the actual DevOverlayWindow; banner spam during wizard is gone.
This commit is contained in:
james.maes committed 2026-05-02 07:53:23 -05:00
1 parent 4b5f535d86
commit c7242ed64c
5 files changed
+75 -17

No files matched your search

+47 -17
View File
@@ -1,6 +1,7 @@
import AppKit
import Bus
import Config
import DevOverlay
import Keychain
import JarvisLogging
import Shell
@@ -362,24 +363,40 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
// Plan 02-03 (bridge) + Plan 03-05 (coordinator + index.html load).
installBus()
// 5. Keychain fetch — missing key → banner (priority 1).
// 5. Keychain fetch.
//
// Banner suppression: when the API key is missing, the first-launch
// wizard is going to open in step 8 to ask for it — enqueuing
// `.keychainEmpty` here would stack a "No API key configured" HUD
// banner on top of the wizard's own apiKey stage. Only enqueue when
// the wizard *won't* open (e.g. wizard was dismissed pre-entry on a
// prior launch and the user reopened without re-entering Setup),
// which we detect post-wizard-construction below.
let apiKeyStored: Bool
do {
_ = try keychainStore.get(.anthropic)
apiKeyStored = true
} catch KeychainError.itemNotFound {
bannerCoordinator?.enqueue(.keychainEmpty)
apiKeyStored = false
} catch {
systemLogger?.error("Keychain fetch error: \(String(describing: error))")
apiKeyStored = false
}
// 6. Input Monitoring probe — denial enqueues `.inputMonitoringDenied`.
let inputMonitoringGranted = hidProbe.requestListenEventAccess()
if !inputMonitoringGranted {
bannerCoordinator?.enqueue(.inputMonitoringDenied)
}
// 6. Input Monitoring — query-only check (no prompt).
//
// Two reasons to NOT call `requestListenEventAccess` at launch:
// 1. It can fire a TCC dialog OUTSIDE the wizard's TCC stage,
// surprising the user with a permission prompt before they've
// even seen the wizard.
// 2. The wizard's TCC stage is the right place for the prompt —
// it has the explainer copy + the System Settings fallback.
//
// Use IOHIDCheckAccess (query-only) to detect a prior grant. If
// denied/unknown AND the wizard is going to open, skip the banner
// (the wizard's TCC stage handles it). Only enqueue the banner when
// the wizard won't be opening to address it.
let inputMonitoringGranted = hidProbe.isListenEventAccessGranted()
// 7. Hotkey binder — empty at launch; wizard binds a shortcut later.
hotkeyBinder = HotkeyBinder()
@@ -388,10 +405,22 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
let state = WizardState(keychain: keychainStore)
wizardState = state
wizardController = OnboardingWizardController(state: state)
if !apiKeyStored {
let willOpenWizard = !apiKeyStored
if willOpenWizard {
openWizard(firstLaunch: true)
}
// 8b. Now that the wizard's open/closed decision is settled, enqueue
// the missing-prerequisite banners only if the wizard ISN'T going
// to address them. This avoids stacking redundant banners on top
// of a wizard that's already asking for the same thing.
if !apiKeyStored && !willOpenWizard {
bannerCoordinator?.enqueue(.keychainEmpty)
}
if !inputMonitoringGranted && !willOpenWizard {
bannerCoordinator?.enqueue(.inputMonitoringDenied)
}
// 9. Plan 05-05 / ME-04 closure (CR-02 REVIEW 05): instantiate
// the orch→replay 2048-capacity .dropOldest channel + open
// the on-disk ReplayLog + spawn the drain task that writes
@@ -1234,7 +1263,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
statusItem = item
let menu = MenuBarContextMenu.build(
setupAction: { [weak self] in self?.openWizard(firstLaunch: false) },
devOverlayToggleAction: { [weak self] in self?.showDevOverlayStubBanner() },
devOverlayToggleAction: { [weak self] in self?.toggleDevOverlay() },
stateDumpAction: { [weak self] in self?.copyStateDump() }
)
let controller = MenuBarIconController(statusItem: item, contextMenu: menu)
@@ -1475,14 +1504,15 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
// MARK: - Menu-bar actions
private func showDevOverlayStubBanner() {
bannerCoordinator?.enqueue(BannerContent(
id: "dev-overlay-stub",
priority: 99,
title: "Dev Overlay lands in Phase 4",
body: "The overlay itself is a P4 deliverable.",
action: nil
))
/// Lazily constructed on first toggle so the panel doesn't allocate
/// resources during launch.
private var devOverlayWindow: DevOverlayWindow?
private func toggleDevOverlay() {
if devOverlayWindow == nil {
devOverlayWindow = DevOverlayWindow()
}
devOverlayWindow?.toggle()
}
/// Writes ONLY boolean presence indicators to the clipboard. The API key
+2
View File
@@ -6,6 +6,8 @@
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
<key>com.apple.security.get-task-allow</key>
<true/>
</dict>
+2
View File
@@ -8,5 +8,7 @@
<true/>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
</dict>
</plist>
+14
View File
@@ -47,6 +47,7 @@
896984D88C412595C0F6F97E /* HudStateIntent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511D7B2406F485ED0D4B9E0D /* HudStateIntent.swift */; };
8E4590089A7955545E2146D3 /* HudStateCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8B7E85856B679CE0788D38DB /* HudStateCoordinatorTests.swift */; };
8EF7109838AD91E95506BD1B /* MCPRuntimeWiring.swift in Sources */ = {isa = PBXBuildFile; fileRef = CBF3EF4CDCC653EF50D99AF7 /* MCPRuntimeWiring.swift */; };
8F0E066DCBA697FB0A2E59EA /* DevOverlay in Frameworks */ = {isa = PBXBuildFile; productRef = C50C1F6C67DE7D3A738845BD /* DevOverlay */; };
92BBF98C9A7B9B7835FC0949 /* Replay in Frameworks */ = {isa = PBXBuildFile; productRef = 8E74C5DEAD36FEDB6342A54D /* Replay */; };
93FDBFFCCEFC0848F1C5E630 /* AppDelegateBusWiringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA6BA7673B5FD73B588D9084 /* AppDelegateBusWiringTests.swift */; };
94512D113669E85D50AC6C8C /* AgentOrchestrator in Frameworks */ = {isa = PBXBuildFile; productRef = 9DC9B6B513D7CCE63399A43B /* AgentOrchestrator */; };
@@ -200,6 +201,7 @@
E5B2103B1A123E512CC56B63 /* WizardState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WizardState.swift; sourceTree = "<group>"; };
E9CF7C158A5A7699A9CA32DE /* mcp-clipboard.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "mcp-clipboard.app"; sourceTree = BUILT_PRODUCTS_DIR; };
F2EA89B09E63D9954B6EA5A4 /* Jarvis.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Jarvis.app; sourceTree = BUILT_PRODUCTS_DIR; };
FAA5549717CB3455817BEA7A /* DevOverlay */ = {isa = PBXFileReference; lastKnownFileType = folder; name = DevOverlay; path = packages/DevOverlay; sourceTree = SOURCE_ROOT; };
FB5CCC9470306930471B8F6E /* Harness */ = {isa = PBXFileReference; lastKnownFileType = folder; name = Harness; path = packages/Harness; sourceTree = SOURCE_ROOT; };
FFBE24D9C8E23A56E1C1EF57 /* JarvisEntitlementProbeTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = JarvisEntitlementProbeTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
FFCA2C937A1629F873729B75 /* MCPTimeMain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MCPTimeMain.swift; sourceTree = "<group>"; };
@@ -265,6 +267,7 @@
3649FFF2495A8C4DDDB3B65B /* Voice in Frameworks */,
B72852E1EF52C0E6F8170852 /* Memory in Frameworks */,
EB03A8906003E55271CAF133 /* JarvisVision in Frameworks */,
8F0E066DCBA697FB0A2E59EA /* DevOverlay in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -496,6 +499,7 @@
A1E748E1EAA90C94FA7F3707 /* AgentCore */,
639B5F162E847B0CFC679E30 /* Bus */,
2E9C7A0B14B3AE5C9100CACF /* Config */,
FAA5549717CB3455817BEA7A /* DevOverlay */,
FB5CCC9470306930471B8F6E /* Harness */,
00C01D284ECCFC986C7F2E16 /* Keychain */,
5DE981947E40847F9BAF5641 /* Logging */,
@@ -612,6 +616,7 @@
254100776001B345FD91877C /* Voice */,
59F56604A76635EC59D2671B /* Memory */,
C1168750998C14962FE14DEC /* JarvisVision */,
C50C1F6C67DE7D3A738845BD /* DevOverlay */,
);
productName = Jarvis;
productReference = F2EA89B09E63D9954B6EA5A4 /* Jarvis.app */;
@@ -749,6 +754,7 @@
7C9A86EDAF8C4170A3BD03EB /* XCLocalSwiftPackageReference "packages/AgentCore" */,
C9F5C5BB642801B788644312 /* XCLocalSwiftPackageReference "packages/Bus" */,
EFF4B62688DB8BC3E515BD34 /* XCLocalSwiftPackageReference "packages/Config" */,
E2A4E8E430F3095E56586E3D /* XCLocalSwiftPackageReference "packages/DevOverlay" */,
44849C6250CB0961C60F92EF /* XCLocalSwiftPackageReference "packages/Harness" */,
22F43C6F6F358E8483EC4DE0 /* XCLocalSwiftPackageReference "packages/Keychain" */,
984FAE3C0A7E82CA2D8C6CB0 /* XCLocalSwiftPackageReference "packages/Logging" */,
@@ -1860,6 +1866,10 @@
isa = XCLocalSwiftPackageReference;
relativePath = packages/Memory;
};
E2A4E8E430F3095E56586E3D /* XCLocalSwiftPackageReference "packages/DevOverlay" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = packages/DevOverlay;
};
EFF4B62688DB8BC3E515BD34 /* XCLocalSwiftPackageReference "packages/Config" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = packages/Config;
@@ -1969,6 +1979,10 @@
isa = XCSwiftPackageProductDependency;
productName = JarvisVision;
};
C50C1F6C67DE7D3A738845BD /* DevOverlay */ = {
isa = XCSwiftPackageProductDependency;
productName = DevOverlay;
};
D5E0D943EBC768ADB9C87F13 /* Logging */ = {
isa = XCSwiftPackageProductDependency;
package = 096B8CF3E4F95B22B4E50BE6 /* XCRemoteSwiftPackageReference "swift-log" */;
+10
View File
@@ -65,6 +65,11 @@ packages:
# wired in Plan 07-06.
Vision:
path: packages/Vision
# DevOverlay package — diagnostic floating panel showing live agent state,
# tool calls, token counts, and per-turn latency. Hidden by default; toggled
# from the menu bar (Show Dev Overlay item).
DevOverlay:
path: packages/DevOverlay
# Plan 08-01: Phase 8 evaluation harness — `Harness` library + `jarvis-eval`
# executable target. Consumed by tests / CI shipping-gate (`scripts/shipping-gate.sh`).
# The xcodegen entry exposes the local SPM package; the executable is
@@ -195,6 +200,11 @@ targets:
# colliding with Apple's Vision framework — see packages/Vision/Package.swift.
- package: Vision
product: JarvisVision
# DevOverlay diagnostic panel — toggled from the menu-bar 'Show Dev
# Overlay' item. AppDelegate.installDevOverlay() constructs the
# DevOverlayWindow on first toggle and routes the menu action to it.
- package: DevOverlay
product: DevOverlay
# Plan 05-02 / 05-03: build the helper apps before the Jarvis copyFiles
# phase consumes them. xcodegen orders target dependencies before copy
# phases.