Skip to content

Add local desktop automation bridge and managed Sparkle updates#5635

Closed
kodjima33 wants to merge 2 commits intomainfrom
codex/desktop-local-automation-auto-update
Closed

Add local desktop automation bridge and managed Sparkle updates#5635
kodjima33 wants to merge 2 commits intomainfrom
codex/desktop-local-automation-auto-update

Conversation

@kodjima33
Copy link
Collaborator

Summary

  • add a local-only desktop automation bridge for semantic in-app navigation and state checks
  • wire the dev desktop app launcher to enable the bridge when requested
  • enforce managed Sparkle auto-update behavior for release builds while keeping dev builds opt-out

Verification

  • xcrun swift build -c debug --package-path desktop/Desktop
  • launched a local app bundle with the automation bridge and verified GET /state + POST /navigate
  • launched a release-style test bundle and verified Sparkle performed an automatic launch-time update check
  • confirmed Sparkle docs in the vendored dependency for launch-time background checks and immediate-install hook behavior

Notes

  • left local-only agent instruction edits out of this PR
  • did not touch unrelated local worktree changes

@kodjima33
Copy link
Collaborator Author

Superseded by #5636, which is branched directly from main and contains only this change set.

@kodjima33 kodjima33 closed this Mar 14, 2026
@github-actions
Copy link
Contributor

Hey @kodjima33 👋

Thank you so much for taking the time to contribute to Omi! We truly appreciate you putting in the effort to submit this pull request.

After careful review, we've decided not to merge this particular PR. Please don't take this personally — we genuinely try to merge as many contributions as possible, but sometimes we have to make tough calls based on:

  • Project standards — Ensuring consistency across the codebase
  • User needs — Making sure changes align with what our users need
  • Code best practices — Maintaining code quality and maintainability
  • Project direction — Keeping aligned with our roadmap and vision

Your contribution is still valuable to us, and we'd love to see you contribute again in the future! If you'd like feedback on how to improve this PR or want to discuss alternative approaches, please don't hesitate to reach out.

Thank you for being part of the Omi community! 💜

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 14, 2026

Greptile Summary

This PR adds two features to the macOS desktop app: (1) a local HTTP automation bridge for programmatic app state inspection and in-app navigation, and (2) managed Sparkle auto-update behavior that enforces automatic checks and installs on release builds while keeping dev builds opt-out.

  • Automation bridge (DesktopAutomationBridge.swift): New opt-in TCP server (enabled via --automation-bridge flag or OMI_ENABLE_LOCAL_AUTOMATION=1) exposing GET /state, GET /health, and POST /navigate endpoints. State is reported from DesktopHomeView via .onChange observers and stored in a shared actor. The listener binds to all network interfaces rather than loopback only — this should be restricted before merge.
  • Managed Sparkle updates (UpdaterViewModel.swift): Release builds now always auto-check and auto-install updates; the willInstallUpdateOnQuit delegate triggers immediate installation instead of waiting for user quit. Settings UI disables the toggles for managed builds.
  • Floating bar as NSPanel (FloatingControlBarWindow.swift): Changed from NSWindow to NSPanel with .nonactivatingPanel style, preventing Ask Omi from surfacing the main window.
  • Shell integration (run.sh): Passes automation flags when OMI_ENABLE_LOCAL_AUTOMATION=1.
  • Large portions of the diff are code reformatting (4-space → 2-space indentation) across AppState.swift, DesktopHomeView.swift, SettingsPage.swift, and OmiApp.swift.

Confidence Score: 3/5

  • The automation bridge has a network binding issue that should be fixed before merge; the rest of the changes are safe.
  • Score of 3 reflects one security-relevant issue (TCP listener binding to all interfaces instead of loopback) in the new automation bridge. The Sparkle update changes and NSPanel conversion are well-implemented. Most of the large diff is cosmetic reformatting.
  • desktop/Desktop/Sources/DesktopAutomationBridge.swift — TCP listener must be restricted to loopback to prevent LAN exposure of app state and navigation control.

Important Files Changed

Filename Overview
desktop/Desktop/Sources/DesktopAutomationBridge.swift New local HTTP automation bridge for app state inspection and navigation. TCP listener binds to all interfaces instead of loopback only, exposing app state to the local network.
desktop/Desktop/Sources/UpdaterViewModel.swift Adds managed update policy (always auto-check/install for release builds, disabled for dev), immediate post-launch background check, and willInstallUpdateOnQuit delegate for instant install on release builds.
desktop/Desktop/Sources/OmiApp.swift Starts automation bridge on launch and triggers immediate Sparkle update check. Minor indentation inconsistency on the updater init block.
desktop/Desktop/Sources/FloatingControlBar/FloatingControlBarWindow.swift Changes FloatingControlBarWindow from NSWindow to NSPanel with .nonactivatingPanel style mask, preventing Ask Omi from surfacing the main window. Clean and well-motivated change.
desktop/Desktop/Sources/MainWindow/DesktopHomeView.swift Adds automation state reporting via .onChange observers and notification handling for navigation requests. Mostly reformatting with new automation integration.
desktop/Desktop/Sources/MainWindow/Pages/SettingsPage.swift Disables auto-update toggles for managed (release) and dev builds, adds explanatory text for each mode. Mostly reformatting with minor UI additions.
desktop/Desktop/Sources/AppState.swift Adds desktopAutomationNavigateRequested notification name. Bulk of diff is reformatting from 4-space to 2-space indentation.
desktop/run.sh Passes --automation-bridge and --automation-port flags to the app when OMI_ENABLE_LOCAL_AUTOMATION=1. Clean shell integration.

Sequence Diagram

sequenceDiagram
    participant Agent as External Agent
    participant Bridge as DesktopAutomationBridge<br/>(TCP :47777)
    participant Store as DesktopAutomationStateStore<br/>(Actor)
    participant Home as DesktopHomeView
    participant App as macOS App

    Note over Home,Store: State Reporting Flow
    Home->>Home: .onChange / NSNotification
    Home->>Store: update(snapshot)

    Note over Agent,App: GET /state Flow
    Agent->>Bridge: GET /state
    Bridge->>Store: current()
    Store-->>Bridge: DesktopAutomationSnapshot
    Bridge-->>Agent: JSON response

    Note over Agent,App: POST /navigate Flow
    Agent->>Bridge: POST /navigate {target, settingsSection}
    Bridge->>App: NotificationCenter.post(.desktopAutomationNavigateRequested)
    App->>Home: handleAutomationNavigation()
    Home->>Home: Update selectedIndex / settingsSection
    Home->>Store: reportAutomationState()
    Bridge->>Store: current()
    Store-->>Bridge: Updated snapshot
    Bridge-->>Agent: JSON response
Loading

Last reviewed commit: f18813a

Comment on lines +105 to +112
do {
let parameters = NWParameters.tcp
parameters.allowLocalEndpointReuse = true
guard let port = NWEndpoint.Port(rawValue: DesktopAutomationLaunchOptions.port) else {
log("DesktopAutomationBridge: invalid port \(DesktopAutomationLaunchOptions.port)")
return
}
let listener = try NWListener(using: parameters, on: port)
Copy link
Contributor

Choose a reason for hiding this comment

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

TCP listener binds to all interfaces, not just loopback

NWListener with NWParameters.tcp listens on all network interfaces by default, not just 127.0.0.1. The log message on line 122 says "listening on http://127.0.0.1:..." but any device on the local network can connect to this port and call /state (leaking app state) or /navigate (triggering in-app navigation).

To restrict the listener to loopback only, set requiredLocalEndpoint on the parameters before creating the listener:

Suggested change
do {
let parameters = NWParameters.tcp
parameters.allowLocalEndpointReuse = true
guard let port = NWEndpoint.Port(rawValue: DesktopAutomationLaunchOptions.port) else {
log("DesktopAutomationBridge: invalid port \(DesktopAutomationLaunchOptions.port)")
return
}
let listener = try NWListener(using: parameters, on: port)
let parameters = NWParameters.tcp
parameters.allowLocalEndpointReuse = true
guard let port = NWEndpoint.Port(rawValue: DesktopAutomationLaunchOptions.port) else {
log("DesktopAutomationBridge: invalid port \(DesktopAutomationLaunchOptions.port)")
return
}
parameters.requiredLocalEndpoint = NWEndpoint.hostPort(host: .ipv4(.loopback), port: port)
let listener = try NWListener(using: parameters, on: port)

Comment on lines 209 to +212
// Initialize Sparkle auto-updater early so the 10-minute check timer starts at launch
// Without this, the updater only starts when the user opens Settings or clicks "Check for Updates"
_ = UpdaterViewModel.shared

// Initialize Sentry for crash reporting and error tracking (including dev builds)
let isDev = AnalyticsManager.isDevBuild
SentrySDK.start { options in
options.dsn = "https://8f700584deda57b26041ff015539c8c1@o4507617161314304.ingest.us.sentry.io/4510790686277632"
options.debug = false
options.enableAutoSessionTracking = true
options.environment = isDev ? "development" : "production"
// Disable automatic HTTP client error capture — the SDK creates noisy events
// for every 4xx/5xx response (e.g. Cloud Run 503 cold starts on /v1/crisp/unread).
// App code already handles HTTP errors and reports meaningful ones explicitly.
options.enableCaptureFailedRequests = false
options.maxBreadcrumbs = 100
options.beforeSend = { event in
// Allow user feedback through from all builds (dev + prod)
if event.message?.formatted.hasPrefix("User Report") == true { return event }
// Never send other events from dev builds — they pollute production Sentry data
if isDev { return nil }
// Filter out HTTP errors targeting the dev tunnel — noise when the tunnel is down
if let urlTag = event.tags?["url"], urlTag.contains("m13v.com") {
return nil
}
// Filter out NSURLErrorCancelled (-999) — these are intentional cancellations
// (e.g. proactive assistants cancelling in-flight Gemini requests on context switch)
if let exceptions = event.exceptions, exceptions.contains(where: { exc in
exc.type == "NSURLErrorDomain" && exc.value.contains("Code=-999") ||
exc.type == "NSURLErrorDomain" && exc.value.contains("Code: -999")
}) {
return nil
}
// Filter out AuthError.notSignedIn — this is thrown when token refresh transiently
// fails (network blip, expired token mid-refresh). The user is still signed in per
// UserDefaults; the 30s refresh timer will retry. Not actionable as a Sentry error.
if let exceptions = event.exceptions, exceptions.contains(where: { exc in
exc.type == "Omi_Computer.AuthError" && exc.value.contains("notSignedIn")
}) {
return nil
}
return event
}
UpdaterViewModel.shared.checkForUpdatesImmediatelyAfterLaunchIfNeeded()
Copy link
Contributor

Choose a reason for hiding this comment

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

Inconsistent indentation (8 spaces vs 4)

These four lines use 8-space indentation while the rest of applicationDidFinishLaunching uses 4 spaces. This looks like a merge artifact.

Suggested change
// Initialize Sparkle auto-updater early so the 10-minute check timer starts at launch
// Without this, the updater only starts when the user opens Settings or clicks "Check for Updates"
_ = UpdaterViewModel.shared
// Initialize Sentry for crash reporting and error tracking (including dev builds)
let isDev = AnalyticsManager.isDevBuild
SentrySDK.start { options in
options.dsn = "https://8f700584deda57b26041ff015539c8c1@o4507617161314304.ingest.us.sentry.io/4510790686277632"
options.debug = false
options.enableAutoSessionTracking = true
options.environment = isDev ? "development" : "production"
// Disable automatic HTTP client error capture — the SDK creates noisy events
// for every 4xx/5xx response (e.g. Cloud Run 503 cold starts on /v1/crisp/unread).
// App code already handles HTTP errors and reports meaningful ones explicitly.
options.enableCaptureFailedRequests = false
options.maxBreadcrumbs = 100
options.beforeSend = { event in
// Allow user feedback through from all builds (dev + prod)
if event.message?.formatted.hasPrefix("User Report") == true { return event }
// Never send other events from dev builds — they pollute production Sentry data
if isDev { return nil }
// Filter out HTTP errors targeting the dev tunnel — noise when the tunnel is down
if let urlTag = event.tags?["url"], urlTag.contains("m13v.com") {
return nil
}
// Filter out NSURLErrorCancelled (-999) — these are intentional cancellations
// (e.g. proactive assistants cancelling in-flight Gemini requests on context switch)
if let exceptions = event.exceptions, exceptions.contains(where: { exc in
exc.type == "NSURLErrorDomain" && exc.value.contains("Code=-999") ||
exc.type == "NSURLErrorDomain" && exc.value.contains("Code: -999")
}) {
return nil
}
// Filter out AuthError.notSignedIn — this is thrown when token refresh transiently
// fails (network blip, expired token mid-refresh). The user is still signed in per
// UserDefaults; the 30s refresh timer will retry. Not actionable as a Sentry error.
if let exceptions = event.exceptions, exceptions.contains(where: { exc in
exc.type == "Omi_Computer.AuthError" && exc.value.contains("notSignedIn")
}) {
return nil
}
return event
}
UpdaterViewModel.shared.checkForUpdatesImmediatelyAfterLaunchIfNeeded()
// Initialize Sparkle auto-updater early so the 10-minute check timer starts at launch
// Without this, the updater only starts when the user opens Settings or clicks "Check for Updates"
_ = UpdaterViewModel.shared
UpdaterViewModel.shared.checkForUpdatesImmediatelyAfterLaunchIfNeeded()

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

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