2 Commits
Author SHA1 Message Date
james.maes 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.
2026-04-23 16:09:59 -05:00
james.maesandClaude Opus 4.7 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>
2026-04-22 14:07:04 -05:00