dependabot/github_actions/github/codeql-action-4
10
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
795c3bf5c9 |
feat(app): v0.1 UX surfaces — startup panels + InputMonitoring banner + PTT (#87, #88, #89, #156)
#89: new AppDelegate.startupSurfacesMode enum (.minimal/.diagnostic). Debug defaults .diagnostic; Release defaults .minimal. openStartupSurfacesIfDiagnostic() runs at tail of bootHealthTask (skipped under XCTest or when wizard is open). Status panel top-right + Dev Overlay bottom-right of NSScreen.main.visibleFrame. DevOverlay opens to Logs tab. #88: BannerContent.inputMonitoringDenied is now nonDismissible: true. New HUDBannerCoordinator.dismiss(id:) clears a banner programmatically without writing dismissedThisLaunch. AppDelegate.reprobeInputMonitoring fires on NSApplication.didBecomeActiveNotification; rebinds the global hotkey on re-grant. Uses IOHIDCheckAccess (query-only, no prompt) per CLAUDE.md guidance. 3 new coordinator tests. #87: WizardState.pttHotkey persisted under Jarvis.pttHotkey (independent of summon hotkey). Ships unbound per CLAUDE.md hotkey hygiene. AppDelegate.rebindPTTFromState() called from installVoice, foreground TCC re-grant, Settings Save/Clear. SettingsView extended with PTT hotkey section. Audio-graph isolation: NSEvent monitors only, no audio-graph surgery — safe alongside fix/voice-crit-v0.1. 3 new WizardState tests. DevOverlay 14 + Voice 26 tests pass; all 17 grep boundary gates green (incl. single-writer-hudstate + no-modal-presentation). |
||
|
|
b92d062f8b |
fix(tcc): explicit mic + camera permission requests in installVoice/installVision
Live test 2026-05-06 surfaced that the production app never actually triggered
the macOS mic or camera permission dialogs — the wizard's TCC stage was
explainer-only ("(later)") and the "first-use prompt" code that was supposed
to land later was never written. Result: AVAudioEngine opened cleanly, mic
input was silent, no wake-word ever fired, and CameraCapture.open() failed
.cameraDenied without ever asking.
Fix: explicit AVCaptureDevice.requestAccess(for:) calls at the first-use
boundary of each subsystem.
* App/AppDelegate.swift installVoice — checks mic auth status; on
.notDetermined awaits requestAccess(for: .audio) BEFORE
graphOwner.open(); on .denied/.restricted logs and returns dormant. Voice
loop only proceeds with confirmed mic grant.
* App/AppDelegate.swift installVision — same pattern for camera. Checks
status; .notDetermined → requestAccess(for: .video); .denied/.restricted
surfaces the existing degradationStream banner via cameraDegradationTask.
* App/Wizard/WizardStageTCCView.swift — wizard rows updated from
"Microphone (later)" / "Camera (later)" to "(auto-prompts shortly)" with
body text matching reality: prompts fire automatically after the wizard,
when the matching subsystem starts. The previous "later" copy implied
indefinite deferral when actually the prompts fire ~5s after wizard
completion.
Live verification:
- tccutil reset Microphone com.koftwentytwo.jarvis
- Launch → wizard appears → click through
- tccd log: "AUTHREQ_PROMPTING ... kTCCServiceMicrophone" — system
dialog displayed
- After click-Allow: AudioGraph: probed format sampleRate=48000 channels=4,
AudioGraphOwner: opened with variant <private>, WakeWordDAG: started
ring-feed loop, VoiceController: starting
Voice 87/87 pass.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
4b5f535d86 |
fix(wizard): single-window reuse + always-closable + Re-check + System-Settings-fallback
User reported three real wizard bugs after the previous round of fixes:
1. Setup… opens a new window every click. The controller's open()
unconditionally constructed a new NSWindow without checking if one
was already on screen. After three clicks of Setup… the user has
three identical wizard windows stacked.
2. No close controls. The first-launch+apiKey branch disabled the
close button by setting .isEnabled = false ONCE at open time, with
no path to re-enable when the user advanced past Stage 1. Result:
stuck-disabled close button for the whole session, even when the
user was on Hotkey stage with no reason to be locked in.
3. Input Monitoring grant invisible to System Settings on ad-hoc
Debug builds. Even after IOHIDRequestAccess fires, the bundle may
not appear in System Settings → Privacy & Security → Input
Monitoring because the ad-hoc signature has TeamIdentifier='not
set' and Apple's TCC daemon cannot pin a stable identity. Without
a hint, the user assumes the wizard is broken when they go to
System Settings looking for Jarvis.
Fixes:
- OnboardingWizardController.open():
* If self.window already exists and is visible: makeKeyAndOrderFront +
NSApp.activate; do NOT spawn a new window.
* styleMask always [.titled, .closable, .miniaturizable] — the close
button is now governed by an enable/disable helper rather than the
style mask, so the chrome stays consistent.
* Subscribe to state.objectWillChange (Combine) and re-run
applyCloseButtonState whenever the wizard advances; the close
button auto-enables once apiKey is stored.
* applyCloseButtonState lock condition is now firstLaunch && stage ==
.apiKey && !apiKeyStored — the moment the key lands in Keychain, the
lock releases. Window level drops back to .normal too.
- WizardStageTCCView Input Monitoring row:
* Added a 'Re-check' button after first probe so user can re-query
after granting in System Settings without restarting the wizard.
* Added a System-Settings helper card explaining what to do when the
bundle isn't auto-listed (the macOS quirk for ad-hoc signed Debug
builds): 'click + and choose Jarvis.app from your build folder'.
* Granted check-mark wins over Re-check button — once granted, only
the green ✓ shows.
Verified: check-app-builds clean. dev-reset.sh + relaunch puts wizard
at apiKey with close button correctly disabled; entering API key
re-enables close; advancing through stages keeps it enabled.
|
||
|
|
435d345abf |
fix(wizard,shell): wizard re-resumes correctly after quit/reopen
The wizard had two persistence holes that made the user's quit-and-reopen
workflow surface the same stage(s) again:
1. Hotkey was in-memory only. WizardState.hotkey was @Published but
never written to disk. Quit and reopen → state defaults to nil →
firstUnresolvedStage returns .hotkey → wizard re-asks even though
the user already bound one a moment ago.
2. Input Monitoring grant detection used the in-memory
inputMonitoringProbed flag (also session-scoped), not the actual
system grant state. Quit and reopen after granting → flag resets to
false → wizard returns to TCC stage even though TCC remembers the
grant.
Fixes:
- WizardState.refresh() now reads from system sources of truth on every
invocation:
apiKeyStored ← Keychain (already correct)
inputMonitoringGranted ← IOHIDCheckAccess (new — query-only, no prompt)
hotkey ← UserDefaults 'Jarvis.hotkey' (JSON-encoded)
- firstUnresolvedStage gates TCC on !inputMonitoringGranted (the actual
grant) instead of !inputMonitoringProbed (the session UI flag).
- WizardState.hotkey has a didSet that persists to UserDefaults; the
Stage 3 'Save Hotkey' button assigning to state.hotkey now hits disk
too. Set to nil → key removed.
- HIDAccessProbe protocol gains isListenEventAccessGranted(), backed by
IOHIDCheckAccess in production. Tests can inject deterministic grant
state without TCC side effects.
Verified: check-app-builds clean. After scripts/dev-reset.sh --yes,
launching → wizard at apiKey. Entering key + granting Input Monitoring
+ binding hotkey + Quit + relaunch (fresh process) should land past
all three stages because each is recomputed from system state.
|
||
|
|
5fd6e762e0 |
fix(wizard,shell): make hotkey recorder visible + clear instructions + show selected shortcut
Three fixes for the 'pick your hotkey' screen the user reported as
unusable:
1. ShortcutRecorderHostView (Shell): the NSView had no draw(_:) override
and no mouseDown(_:) override, so it was both invisible AND uncllickable
for focus purposes. The user had nothing to click on, and even if they
guessed where the 40-px tall frame was, clicking didn't make it first
responder, so the keyDown monitor never registered.
- Add wantsLayer + cornerRadius for a visible rounded field.
- Add mouseDown(with:) → window.makeFirstResponder(self).
- Add draw(_:) that renders a placeholder ('Click to record shortcut…'),
a focused-state hint ('Press a shortcut…'), and the recorded value
once captured. Border + background change color when focused so the
focus state is obvious.
- Add setRecordedDisplay(_:) public setter so the SwiftUI parent can
push the captured shortcut text in for rendering.
2. WizardStageHotkeyView (App): tells the user, in plain language, what to
do — three numbered steps explaining click-to-record, modifier+key, and
Esc-to-cancel. Adds a 'Selected: [⌘⇧J]' confirmation row with a Clear
button so the user can see and reset the chosen shortcut before saving.
Wraps text properly via .fixedSize(horizontal: false, vertical: true).
3. KeyboardShortcut.displayString (Shell): renders shortcut as Apple's
menu-style glyph string ('⌘⇧J', '⌃⌥Space', etc.). Carbon import is
required for kVK_* constants — added directly to KeyboardShortcut.swift
per user direction. Modifier glyph order matches Apple convention
(⌃ ⌥ ⇧ ⌘); unmapped key codes fall back to 'Key{n}' for debuggability.
Verified: check-app-builds.sh PASS; relaunched (PID 68314).
|
||
|
|
8ebdf76980 |
fix(hud): wrap text in wizard + banner instead of tail-truncation
Both the first-launch wizard's TCC stage and the HUD banner panel were
showing tail-truncated body text (Grant Permissions descriptions ending
in "...", Limited hotkey mode body cut at "The hotke..."). Two layered
SwiftUI layout bugs:
1. The body Text inside row HStacks (`VStack(title+body) + Spacer +
button`) didn't have `.fixedSize(horizontal: false, vertical: true)`,
so when the trailing button was wide ("Denied — Open System Settings")
SwiftUI's HStack layout compressed Text and chose tail-truncation
over multi-line wrap.
2. The wizard's outer frame was `560×440`, leaving only ~318h for
stageBody after header(64) + dividers(2) + footer(56). The TCC stage
with 4 permission rows needed ≥520h. The vertical squeeze cascaded
into more aggressive Text compression and clipped the bottom Back row.
Fixes:
- WizardView: bump frame to 600×640.
- WizardStageTCCView: `.fixedSize(horizontal: false, vertical: true)` on
description + each row's body. `.frame(maxWidth: .infinity, alignment:
.leading)` on inner row VStacks so they receive the available width
before the trailing button claims it. Spacer minLength bumped to 16
for breathing room between body text and button.
- HUDBanner: same `.fixedSize` + `.frame(maxWidth:)` pair on the title
+body VStack. The 360pt-wide banner needed it most because
`HUDBannerPanel.positionTopTrailing` re-reads the SwiftUI fittingSize
on host — without wrapping, the panel was sized to a single-line body
and forced all subsequent body text to truncate.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
||
|
|
1fc613bcfa |
fix(01-WR-09): document refresh-before-view-build invariant in wizard open
Phase 1 REVIEW.md WR-09: `OnboardingWizardController.open` called `state.refresh()` immediately before building the hosting view. The refresh is synchronous and @MainActor-safe, so the potential race (refresh publish arriving after SwiftUI subscribes) doesn't occur in practice — but the sequencing was an implicit invariant worth codifying. App/Wizard/OnboardingWizardController.swift - Add docstring noting why refresh must complete before view construction (keychain-lock contention latency + publish ordering). - Add runtime assertion `Thread.isMainThread` after refresh — makes the invariant explicit and fails loudly if a future refactor moves refresh off-main. Notes for future work: - A truly async-off-main refresh would make the UX WORSE (user sees a flash of empty wizard for the async duration). Current posture of "run refresh synchronously, short-lived main-thread block" is correct for this scope; revisit only if the Keychain contention stalls become user-visible in practice. |
||
|
|
9850d9546d |
fix(01-WR-07): dedicated hardened URLSession for API key validation
Phase 1 REVIEW.md WR-07: `AnthropicKeyValidator` defaulted to
`URLSession.shared`, which honors the system proxy. In a corporate
environment with a MITM proxy intercepting `api.anthropic.com`, the
`x-api-key` header would be TLS-terminated at the proxy, leaking the
key on the first validation attempt before the user knows Jarvis is
making network calls.
App/Wizard/AnthropicKeyValidator.swift
- Default client is now an ephemeral URLSession built by
`makeHardenedSession()`:
* `connectionProxyDictionary = [:]` — refuse any configured proxy.
* `tlsMinimumSupportedProtocolVersion = .TLSv13`.
* 15s request / 30s resource timeouts — fail fast on flaky nets
instead of blocking the wizard for minutes.
* `httpCookieAcceptPolicy = .never`, `httpShouldSetCookies = false`.
* `urlCache = nil`, `urlCredentialStorage = nil` — nothing cached
on disk for a one-shot validation call.
- Fix optional `any` syntax for Swift 6 strict concurrency build.
No test change: `AnthropicKeyValidatorTests` already injects `MockClient`
via the initializer, so the production path switch doesn't change
behaviour under test.
|
||
|
|
929e4cee6b |
fix(01-CR-02): shorten API key plaintext lifetime in wizard @State
Phase 1 REVIEW.md CR-02 (SEC-01): `WizardStageAPIKeyView` kept the raw
Anthropic API key in an `@State String` for the lifetime of the view —
outliving the Keychain write, retained by SwiftUI attribute-graph
diffing, and visible to any memory-dump forensic vector (crash report,
Xcode memory graph, `leaks(1)`).
App/Wizard/WizardStageAPIKeyView.swift:53-101
- `verify()` now captures the key into a single `capturedKey` local and
reassigns `apiKey = ""` immediately, so the @State slot drops its
strong reference before the async validate + keychain.set chain runs.
- Error branches selectively restore `apiKey = capturedKey` only where
the user plausibly wants to retry without retyping (unauthorized /
network / generic / keychain-error); `malformed` keeps apiKey empty.
- `.onDisappear { clearAPIKey() }` hook: zeros the @State on view
teardown (including back-navigation and success-path dismissal),
preventing the plaintext from persisting in the attribute graph after
dismiss.
- `clearAPIKey()` first overwrites with null bytes of the same length
(best-effort — Swift Strings are CoW/immutable so the backing page
can't be zeroed in place), then drops the @State to empty so ARC can
release the CoW storage.
App/Tests/AppTests/WizardStageAPIKeyViewTests.swift (new)
- Source-level grep gates for the three regression points: onDisappear
hook, clearAPIKey helper shape, and verify()'s capture-then-clear
pattern. SwiftUI @State lifecycle isn't observable from XCTest
without ViewInspector, so text-level gates are the practical
regression guard.
Jarvis.xcodeproj/project.pbxproj: regenerated via xcodegen to pick up
the new test file.
|
||
|
|
ebefb91043 |
feat(01-04): build first-launch wizard + full AppDelegate launch wiring
- AnthropicKeyValidator: local sk-ant- prefix check + URLSession-injectable GET to api.anthropic.com/v1/models; returns typed enum, never logs key - WizardState: @MainActor ObservableObject; D-09 stateless re-entry via refresh(); firstUnresolvedStage advances apiKey → tcc → hotkey → complete - WizardStageAPIKeyView: SecureField, validator + Keychain round-trip; key value lives only in Keychain (SEC-01) - WizardStageTCCView: Input Monitoring row actively probes; Mic/Camera/ Automation rows are explainer-only (D-08/SEC-04); no permission APIs called from the view - WizardStageHotkeyView: ShortcutRecorderView + CollisionDetector wiring; Skip leaves hotkey unbound (SHELL-02) - WizardView: three-stage root with header dots + Back footer - OnboardingWizardController: NSWindow hosting; first-launch .modalPanel + close-button disabled until API key stored (D-06) - AppDelegate: full 8-step launch chain — logging bootstrap (S-8) → entitlement gate (Plan 03) → config load (NSAlert on malformed, D-19/S-6) → menu bar + HUD + banner install → Keychain fetch (.keychainEmpty banner on itemNotFound) → Input Monitoring probe (.inputMonitoringDenied banner on denial, SHELL-06) → HotkeyBinder init → Wizard open on first launch. copyStateDump writes boolean-only presence; API key VALUE never clipboarded - JarvisApp: Settings scene stub; real NSPanels managed by AppDelegate - Tests: 11 new (WizardState 4, AnthropicKeyValidator 3, WizardTCC 1, AppDelegateWiring 4) + updated AppDelegateEntitlementTests for the new launch chain; all 25 JarvisAppTests pass - Re-inserted PBXCopyFilesBuildPhase for Contents/Helpers per Plan 01/03 precedent (xcodegen strips empty copyFiles) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |